jline とは
Java で高機能な CUI コンソールを作るためのライブラリ。
標準 API の入出力だけでは実現できない機能を実現できる。
100% Java ではなく、実行環境に依存したライブラリが jar に同梱されている(Windows なら dll が使用される)。
Java 9 から導入される jshell も jline を使用している(jdk.internal.jshell.tool.ConsoleIOContext
とか)。
環境
OS
- Windows 7
Java
- 1.8
インストール
compile 'jline:jline:2.14.2'
Hello World
package sample.jline;
import jline.console.ConsoleReader;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
ConsoleReader console = new ConsoleReader();
String line = console.readLine("jline > ");
System.out.println(line);
}
}
jline > 【hello】
hello
※【】
で括っている部分は、キーボードから入力した文字列を表現している。
-
ConsoleReader
クラスが、コンソールの入出力を操作する基本のクラスとなる。 -
readLine()
で、ユーザー入力を待機する。 - 引数に文字列を渡せば、それが画面に表示されたうえでユーザー入力が待機される。
プロンプトを設定する
package sample.jline;
import jline.console.ConsoleReader;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
ConsoleReader console = new ConsoleReader();
console.setPrompt("jline > ");
System.out.println(console.readLine());
System.out.println(console.readLine());
}
}
jline > 【foo】
foo
jline > 【bar】
bar
-
setPrompt()
で文字列を渡すと、以後入力を待機するたびにその文字列が表示されるようになる。
ユーザー入力をマスクする
package sample.jline;
import jline.console.ConsoleReader;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
ConsoleReader console = new ConsoleReader();
System.out.println(console.readLine('*'));
}
}
【*****】
jline
-
readLine()
の引数にchar
を渡すと、ユーザー入力がその文字でマスキングされる。 - 文字を表示させたくない場合は、
(char)0
を引数に渡せばいい。
文字を出力する
package sample.jline;
import jline.console.ConsoleReader;
public class Main {
public static void main(String[] args) throws Exception {
ConsoleReader console = new ConsoleReader();
console.putString("123");
console.flush();
console.putString("abc");
}
}
123
-
putString()
で文字列を出力する。 -
flush()
を呼ばないと画面には出力されない。
print()
, println()
との違い
文字列を出力するメソッドとして、他に print()
や println()
というメソッドが用意されている。
print()
や println()
は、指定した文字列をそのまま OutputStream
(デフォルトは標準出力)に出力するだけだが、 putString()
はバッファーと呼ばれるものにも文字列を出力している。
このバッファーが具体的にどういうものなのかはよく分かっていないが、これにより print()
などとは異なり putString()
で出力した場合は以下のようなことが可能になる。
- 一度文字を出力してから、カーソル位置を戻すことができる。
- バックスペースキーなどを送信して、出力した文字を消すことができる。
-
readLine()
したときにputString()
で出力した文字は手動で編集できる。
例えば、以下のような実装を動かしてみる。
package sample.jline;
import jline.console.ConsoleReader;
public class Main {
public static void main(String[] args) throws Exception {
ConsoleReader console = new ConsoleReader();
console.print("ABC");
console.putString("123");
console.flush();
console.readLine();
}
}
ちょっと分かりづらいが、 putString()
で出力した文字(123
)の上ではカーソルを動かす事ができているが、 print()
で出力した文字(ABC
)の上にはカーソルを移動させることができない。
バックスペースを入力した場合も、 putString()
で出力した文字列なら削除できるが、 print()
で出力した分は削除できない。
正しいかどうかは別として、感覚としては print()
で出力した分は完全にコンソール上に出力が確定してしまい後で表示を変更することはできず、 putString()
で出力した分については一応コンソール上も表示はさせているがまだ確定はしていないので、任意に表示を変更できる、というイメージで良いのかなぁと思う。
ただし、あくまで色々試したりした結果個人的に辿り着いた推測なので、厳密に正しいかどうかは分からない(全然ドキュメントが無いのでつらい)。
Enter を送信する
package sample.jline;
import jline.console.ConsoleReader;
public class Main {
public static void main(String[] args) throws Exception {
ConsoleReader console = new ConsoleReader();
System.out.println("1");
console.accept();
System.out.println("2");
}
}
1
2
バックスペースを送信する
package sample.jline;
import jline.console.ConsoleReader;
public class Main {
public static void main(String[] args) throws Exception {
ConsoleReader console = new ConsoleReader();
console.putString("123");
console.flush();
console.backspace();
console.flush();
}
}
12
-
backspace()
実行後にflush()
しないと反映されないので注意。
ビープ音を鳴らす
package sample.jline;
import jline.console.ConsoleReader;
public class Main {
public static void main(String[] args) throws Exception {
ConsoleReader console = new ConsoleReader();
console.setBellEnabled(true);
console.beep();
console.flush();
}
}
-
beep()
してからflush()
するとビープ音を鳴らすことができる。 - ただし
setBellEnabled()
にtrue
を設定しておく必要がある。
スクリーンをクリアする
package sample.jline;
import jline.console.ConsoleReader;
public class Main {
public static void main(String[] args) throws Exception {
ConsoleReader console = new ConsoleReader();
console.clearScreen();
console.flush();
}
}
-
clearScreen()
してからflush()
すると、コンソールがクリアされる。 - ただし、文字が消されるわけではなく、見えないところまでスクロールされるだけ。
- ※Windows 7 で試した限りでは。
カーソル位置を変更する
package sample.jline;
import jline.console.ConsoleReader;
public class Main {
public static void main(String[] args) throws Exception {
ConsoleReader console = new ConsoleReader();
console.putString("12345");
console.flush();
console.moveCursor(-3);
console.readLine();
}
}
-
moveCursor()
でカーソル位置を変更できる。 - 正数を指定した場合は右方向に、負数を指定した場合は左方向に移動する。
現在のカーソル行から末尾までを削除する
package sample.jline;
import jline.console.ConsoleReader;
public class Main {
public static void main(String[] args) throws Exception {
ConsoleReader console = new ConsoleReader();
console.putString("12345");
console.flush();
console.moveCursor(-3);
console.killLine();
console.flush();
}
}
12
-
killLine()
で、現在カーソルが存在する場所から末尾までを削除できる。 -
flush()
で反映される。
クリップボードの情報を貼り付ける
package sample.jline;
import jline.console.ConsoleReader;
public class Main {
public static void main(String[] args) throws Exception {
ConsoleReader console = new ConsoleReader();
console.putString("paste >>> ");
console.paste();
console.flush();
}
}
-
paste()
メソッドで、現在クリップボードに保存されている情報を出力できる。
複数の文字列を整列して表示させる
package sample.jline;
import jline.console.ConsoleReader;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws Exception {
ConsoleReader console = new ConsoleReader();
console.printColumns(Arrays.asList(
"aaa", "bbbbb", "ccccc", "dd", "eeeeeeee",
"ffffff", "gggg", "hhhhhhhh", "iiiiiii",
"jjjj", "kkkkkk", "lll", "mmmmmmmmm"
));
console.flush();
}
}
aaa bbbbb ccccc dd eeeeeeee ffffff
gggg hhhhhhhh iiiiiii jjjj kkkkkk lll
mmmmmmmmm
-
printColumns()
のCharSequence
のCollection
を渡すと、各要素を整列させて表示することができる。
行を再描画する
package sample.jline;
import jline.console.ConsoleReader;
public class Main {
public static void main(String[] args) throws Exception {
ConsoleReader console = new ConsoleReader();
console.setPrompt("jline > ");
console.putString("redrawLine しない場合");
console.flush();
console.accept();
console.putString("redrawLine した場合");
console.flush();
console.redrawLine();
console.flush();
}
}
redrawLine しない場合
jline > redrawLine した場合
-
redrawLine()
を実行すると、現在の行のバッファを一旦クリアして、プロンプトの文字を先頭につけて再描画する。
入力待機中に Ctrl + C を検知する
package sample.jline;
import jline.console.ConsoleReader;
import jline.console.UserInterruptException;
public class Main {
public static void main(String[] args) throws Exception {
ConsoleReader console = new ConsoleReader();
console.setHandleUserInterrupt(true);
try {
console.readLine();
} catch (UserInterruptException e) {
System.out.println("中断!");
}
}
}
-
setHandleUserInterrupt()
にtrue
を設定すると、readLine()
中にCtrl
+C
を入力された場合にUserInterruptException
がスローされるようになる。 - デフォルトは
false
なので、Ctrl
+C
を入力しても何も起きない。
ヒストリー
package sample.jline;
import jline.console.ConsoleReader;
import jline.console.UserInterruptException;
import jline.console.history.History;
public class Main {
public static void main(String[] args) throws Exception {
ConsoleReader console = new ConsoleReader();
console.setPrompt("jline>");
console.setHandleUserInterrupt(true);
try {
while (true) {
console.readLine();
History history = console.getHistory();
history.forEach(entry -> {
System.out.println(" " + entry.index() + " : " + entry.value());
});
}
} catch (UserInterruptException e) {
// ignore
}
}
}
-
getHistory()
でHistory
オブジェクトが取得できる。 -
History
オブジェクトには、それまでコンソールから入力された文字列が格納されており、任意に情報を取り出すことができる。 - 同じ文字が連続で入力された場合は、履歴は増えない。
Tab 補完
FileNameCompleter
package sample.jline;
import jline.console.ConsoleReader;
import jline.console.UserInterruptException;
import jline.console.completer.FileNameCompleter;
public class Main {
public static void main(String[] args) throws Exception {
ConsoleReader console = new ConsoleReader();
console.setPrompt("jline>");
console.addCompleter(new FileNameCompleter());
console.setHandleUserInterrupt(true);
try {
while (true) {
console.readLine();
}
} catch (UserInterruptException e) {
// ignore
}
}
}
-
ConsoleReader.addCompleter()
にCompleter
インターフェースを実装したオブジェクトを渡すことで、タブ入力時の補完機能を実装できる。 - 標準でいくつか
Completer
の実装が用意されている。 - 上記例では、
FileNameCompleter
を使用している。 -
FileNameCompleter
は実行時のカレントディレクトリ以下に存在するファイルを補完対象として表示してくれる。 - パス区切り文字を入れることで、さらに下の階層まで補完候補として検索してくれる。
StringCompleter
package sample.jline;
import jline.console.ConsoleReader;
import jline.console.UserInterruptException;
import jline.console.completer.Completer;
import jline.console.completer.StringsCompleter;
public class Main {
public static void main(String[] args) throws Exception {
ConsoleReader console = new ConsoleReader();
console.setPrompt("jline>");
Completer completer = new StringsCompleter("one", "two", "three", "four", "five");
console.addCompleter(completer);
console.setHandleUserInterrupt(true);
try {
while (true) {
console.readLine();
}
} catch (UserInterruptException e) {
// ignore
}
}
}
-
StringCompleter
は、あらかじめ候補となる任意の文字のリストをセットしておくことができる。
Completer を自作する
package sample.jline;
import jline.console.ConsoleReader;
import jline.console.UserInterruptException;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) throws Exception {
ConsoleReader console = new ConsoleReader();
console.setPrompt("jline>");
console.addCompleter((buffer, cursor, candidates) -> {
Stream.of("one", "two", "three", "four", "five")
.filter(s -> s.startsWith(buffer))
.forEach(candidates::add);
return candidates.isEmpty() ? -1 : 0;
});
console.setHandleUserInterrupt(true);
try {
while (true) {
console.readLine();
}
} catch (UserInterruptException e) {
// ignore
}
}
}
- 動きは StringCompleter を使った例と同じ。
-
Completer
は関数型インターフェースなので、ラムダ式で実装可能。 -
complete()
というメソッドを実装する。 - 引数には以下の3つが渡される。
-
buffer
:現在コンソールに入力されている文字列。この文字列を元に候補を抽出する。 -
cursor
:現在のカーソル位置。 -
candidate
:候補となる文字列をセットするList
。このList
に画面に表示したい候補をセットすると、あとは jline が良しなに候補一覧の表示や補完をしてくれる。
毎回空のList
が渡される。
-
- 戻り値は、候補を表示した後の相対的なカーソル位置を指定する。
- 普通は
0
を返せばよさそうだが、StringCompleter
が候補が無い場合に-1
を返しているので、とりあえず同じ形にしている。
- 普通は
キーバインド
正直、ドキュメントが全く無くて、以下はソースや jshell のコードなどから推測した結果です。
デフォルトのキーバインド
readLine()
した場合、デフォルトのキーバインドは emacs と同じになっている。
emacs 以外には vi のキーバインドがサポートされており、以下の方法で変更できる。
package sample.jline;
import jline.console.ConsoleReader;
import jline.console.KeyMap;
import jline.console.UserInterruptException;
public class Main {
public static void main(String[] args) throws Exception {
ConsoleReader console = new ConsoleReader();
console.setPrompt("jline>");
console.setHandleUserInterrupt(true);
console.setKeyMap(KeyMap.VI_MOVE);
try {
while (true) {
console.readLine();
}
} catch (UserInterruptException e) {
// ignore
}
}
}
-
ConsoleReader
のsetKeyMap()
メソッドに、設定したいキーバインドの名前を指定する。 - vi のコマンドモードに指定しているので、
i
やa
を入力すれば編集モードに切り替わる。esc
を入力すれば再びコマンドモードに戻すこともできる。 - 使用できる名前は
KeyMap
に定数で定義されている。
キーバインドを変更する(基本)
package sample.jline;
import jline.console.ConsoleReader;
import jline.console.KeyMap;
import jline.console.UserInterruptException;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.io.UncheckedIOException;
public class Main {
public static void main(String[] args) throws Exception {
ConsoleReader console = new ConsoleReader();
console.setPrompt("jline>");
console.setHandleUserInterrupt(true);
KeyMap keyMap = console.getKeys();
keyMap.bind("a", (ActionListener)e -> {
try {
console.putString("<a>");
console.flush();
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
});
try {
while (true) {
console.readLine();
}
} catch (UserInterruptException e) {
// ignore
}
}
}
- 普通に
a
と入力したら<a>
と出力するようにしている。 -
Console
からgetKeys()
でKeyMap
のインスタンスが取得できる。 - この
KeyMap
に、キーが入力されたときに実行する処理のマッピングが登録されているっぽい。 -
bind()
メソッドを使うことで、任意のキー入力に対して処理をマッピングできる。 - 第一引数には、マッピング対象のキーを
CharSequence
で指定する。 - 第二引数に
java.awt.event.ActionListener
を実装したインスタンスを渡すと、キー入力時の処理を任意の処理に差し替えることができる。
Ctrl と組み合わせたキーにマッピングする
package sample.jline;
import jline.console.ConsoleReader;
import jline.console.KeyMap;
import jline.console.UserInterruptException;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.io.UncheckedIOException;
public class Main {
public static void main(String[] args) throws Exception {
ConsoleReader console = new ConsoleReader();
console.setPrompt("jline>");
console.setHandleUserInterrupt(true);
KeyMap keyMap = console.getKeys();
keyMap.bind(String.valueOf(KeyMap.CTRL_D), (ActionListener)e -> {
try {
console.putString("Ctrl + D");
console.flush();
} catch (IOException e1) {
throw new UncheckedIOException(e1);
}
});
keyMap.bind(String.valueOf((char)26), (ActionListener)e -> {
try {
console.putString("Ctrl + Z");
console.flush();
} catch (IOException e1) {
throw new UncheckedIOException(e1);
}
});
try {
while (true) {
console.readLine();
}
} catch (UserInterruptException e) {
// ignore
}
}
}
- Ascii コード表の制御文字の一部は
Ctrl
+ アルファベットに対応しているので、それを利用することでCtrl
と組み合わせたキー入力とマッピングできる。- なんかハックぽいが、これ以外の方法は分からなかった。。。
- 一部は
KeyMap
クラスに定数として定義されているので、それを利用できる。 - 定数が無いものについては、
String.valueOf((char)4)
などして自作できる。 - 自分が試した中では、以下のキー入力はうまく認識された(Windows 7 のコマンドプロンプト上で確認)。
数値 | キーボード入力 |
---|---|
1 |
Ctrl + A
|
2 |
Ctrl + B
|
3 |
Ctrl + C
|
4 |
Ctrl + D
|
5 |
Ctrl + E
|
6 |
Ctrl + F
|
7 |
Ctrl + G
|
8 |
Ctrl + H or Backspace
|
9 |
Ctrl + I or Tab
|
10 |
Ctrl + J or Ctrl + Enter
|
11 |
Ctrl + K
|
12 |
Ctrl + L
|
13 |
Ctrl + M or Enter
|
14 |
Ctrl + N
|
15 |
Ctrl + O
|
16 |
Ctrl + P
|
17 |
Ctrl + Q
|
18 |
Ctrl + R
|
19 |
Ctrl + S
|
20 |
Ctrl + T
|
21 |
Ctrl + U
|
22 |
Ctrl + V
|
23 |
Ctrl + W
|
24 |
Ctrl + X
|
25 |
Ctrl + Y
|
26 |
Ctrl + Z
|
29 |
Ctrl + ]
|
Terminal
ターミナルの幅・高さを取得する
package sample.jline;
import jline.Terminal;
import jline.TerminalFactory;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
Terminal terminal = TerminalFactory.get();
System.out.println(
"height = " + terminal.getHeight() + "\r\n" +
"width = " + terminal.getWidth() + "\r\n"
);
}
}
height = 40
width = 140