JLine3のTerminalのメモ。
|
Terminalは、JLine3で端末(コンソール)(文字列の入力・表示)を扱うクラス。
コンソールからの入力に関してはLineReaderの中で使われる。
コンソールへの出力はterminal.writer()(PrintWriter)に対して行うのが良いと思う。
端末の種類(Windows(コマンドプロンプト)かどうか等)はOSUtilsで判別できる。
import java.io.IOException; import org.jline.reader.LineReader; import org.jline.reader.LineReaderBuilder; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder;
public class JLineExample { public static void main(String[] args) throws IOException { try (Terminal terminal = TerminalBuilder.terminal()) { LineReader lineReader = LineReaderBuilder.builder() // .terminal(terminal) // .build(); for (;;) { String line = lineReader.readLine("jline> "); terminal.writer().println(line); } } } }
Terminalインスタンスは、ひとつのアプリケーションの中でひとつだけしか生成できないようだ。[2022-09-13]
(2つ目はDumbTerminalになってしまう)
LineReaderインスタンスは何回作り直しても大丈夫そうだが、その場合はTerminalインスタンスは同じものを使った方が良さそう。
TerminalはCloseableを継承しているので、終了時にはクローズを呼ぼう。[2022-09-15]
コンソールの種類によっては、表示する文字列に色を付けることが出来る。
(DumbTerminalでは色は付かない)
import org.jline.utils.AttributedStringBuilder; import org.jline.utils.AttributedStyle;
String styledLine = new AttributedStringBuilder() // .append("before-") // .style(AttributedStyle.DEFAULT.foreground(AttributedStyle.GREEN)) // 緑色 .append(line) // .style(AttributedStyle.DEFAULT) // スタイルを元に戻す .append("-after") // .toAnsi(terminal); terminal.writer().println(styledLine);
RGBで色を指定するforeground(r,g,b)
やforegroundRgb(rgb)
といったメソッドもある。[2022-09-08]
色に対応しているコンソールの場合 | DumbTerminalの場合 |
---|---|
jline> abc
before-abc-after
|
jline> abc before-abc-after |
LineReaderを使っている場合、Windowsのコマンドプロンプトでは、Ctrl+Dを押すとEndOfFileExceptionが発生してアプリケーションが終了し、Ctrl+Cを押すとUserInterruptExceptionで終了する。
(これらの例外をキャッチして処理することも出来る)
ただ、MinGWだと、Ctrl+Cを押してからEnterを押さないと終了しない(InterruptedExceptionからのIOErrorが発生する)。
また、CygwinだとCtrl+Cの挙動がおかしい(例外が発生しない、アプリが終了した感じになるがコンソールが固まる)ので、Ctrl+Cを無効にした方がいいかもしれない。
DumbTerminal以外では、Ctrl+Cを無効化できる。
import org.jline.terminal.Attributes; import org.jline.terminal.Attributes.ControlChar;
Terminal terminal = TerminalBuilder.terminal(); Attributes originalAttributes = terminal.getAttributes(); Attributes attributes = new Attributes(originalAttributes); attributes.setControlChar(ControlChar.VINTR, 0); // DumbTerminal以外でCtrl+Cを無効化する terminal.setAttributes(attributes); // terminal.enterRawMode();
参考: JShellのConsoleIOContext.java
→Ctrl+C(SIGINT)のハンドラーを登録する例 [2022-10-03]
Eclipseのコンソールを使う場合や、Windowsのコマンドプロンプトを使うのに依存ライブラリーにJNAを入れていない場合、Terminalの具象クラスとしてDumbTerminalクラス(ダム端末)が使用される。
System.out.println(terminal); ↓ org.jline.terminal.impl.DumbTerminal@523884b2
DumbTerminalは色々機能が制限されるので、不便。
TerminalBuilderでTerminalインスタンスを生成する際、TerminalBuilderSupportというクラスが使われる。
Windowsで依存ライブラリーにJNAを入れていない場合、TerminalBuilderSupportコンストラクターの中で「java.lang.NoClassDefFoundError:
com/sun/jna/LastErrorException
」が発生して(debugログ出力だけして握りつぶされる)、JnaSupportインスタンスの生成に失敗する。
(この場合、最終的にDumbTerminalが使われることになる)
依存ライブラリーにJNAが入っていれば、JnaSupportImplが生成されて、そこからJnaWinSysTerminalが生成される。
DumbTerminalでJLine3のアプリケーションを実行すると、Terminal作成時に警告が出る。
警告: Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)
TerminalBuilder.doBuild()がこの警告を出しているので、その条件を回避してやれば警告は出なくなる。
『ダム端末』が何を意味しているのか正直よく分からないので、これでいいのかどうか何とも言えないけれど…。
Terminal terminal = TerminalBuilder.builder() // .dumb(true) // .build();
Terminal terminal = TerminalBuilder.builder() // .color(false) // ダム端末で色をサポートしているかどうか。ダム端末以外(そもそも色をサポートしている端末)では無関係 .build();
(LineReaderを経由せずに)TerminalからReaderを取得して文字入力を行うことが出来る。[2022-10-03]
import org.jline.terminal.TerminalBuilder; import org.jline.utils.NonBlockingReader;
try (var terminal = TerminalBuilder.terminal()) { NonBlockingReader reader = terminal.reader(); for (;;) { int c = reader.read(100L); if (c < 0) { System.out.printf("c=%d%n", c); } else { System.out.printf("c=%02x[%c]%n", c, (c >= 0x20) ? c : 0); } if (c == -1) { System.out.println("eof"); break; } if (c == -2) { System.out.println("timeout"); continue; } } }
terminal.reader()でNonBlockingReaderを取得し、そのreadメソッドでキー入力を受け取る。
read()の引数はタイムアウト時間(単位はたぶんミリ秒)。
terminal.reader()で取得したNonBlockingReaderはTerminalのクローズ時にクローズされるので、ユーザー側でクローズする必要は無い。
read()の戻り値はキー入力されたUnicodeの1文字(char相当)。
(ちゃんとするならサロゲートペアの処理が必要)
ただし、-1は(他のReaderと同じく)EOFで、タイムアウトした場合は-2が返る。
Ctrlキーを押しながらだと、例えば(Windowsのコマンドプロンプトの場合)Ctrl+Bは2、Ctrl+Dは4、Ctrl+Eは5が返る。
Ctrl+AやCtrl+Cなど、一部のキー入力は受け付けない。(デフォルトではCtrl+Cを押すとアプリケーションが終了する)
Altキーを押しながらだと、ESC(0x1b)と一緒に押した文字コードの2回の入力(2個のint)が返る。(Alt+aだと0x1b, 0x61)
また、カーソルキーを押すとEscOA(0x1b, 0x4f, 0x41)のような3個のintが返る。
NonBlockingReaderではCtrl+Cを検知できない(Ctrl+Cを入力できない)。[2022-10-03]
(デフォルトでは、Ctrl+Cを押すとアプリケーションが終了する)
しかしTerminalにはシグナルを受け取る機能があるので、INT(Ctrl+Cを押したときに発生するinterrupt)の処理を書けばCtrl+Cを検知できる。(アプリケーションも終了しなくなる)
シグナルを受け取ったときに呼ばれるSignalHandlerをTerminalに登録する。
ただ、シグナルハンドラーは(Readerとは別の)スレッドで実行されるので、このハンドラーから例外を投げてもNonBlockingReader.read()からその例外が発生するわけではない。
なので、共通に参照できるフラグを用意して、シグナルハンドラー内でフラグを更新し、NonBlockingReader.read()から制御が返ったときにフラグをチェックする。
import org.jline.terminal.Terminal.Signal; import org.jline.terminal.TerminalBuilder;
private volatile boolean interrupt = false;
try (var terminal = TerminalBuilder.terminal()) { // SIGINT(Ctrl+C)のハンドラーを登録 terminal.handle(Signal.INT, signal -> { interrupt = true; }); var reader = terminal.reader(); for (;;) { int c = reader.read(100L); if (c == -2 && interrupt) { System.out.println("Ctrl+C"); interrupt = false; continue; } 〜 } }
なお、Ctrl+Cを受け取っても終了しないようにしたら、アプリケーションが終了できなくなるので注意。
(何らかのキーを入力したら終了するように作っておく必要がある)
ちなみに、terminal.handle()は直前に登録されていたハンドラーを返す。
NonBlockingReaderを使い終わった後にアプリケーションが終了する場合はハンドラーを戻す必要は無いと思うが、
NonBlockingReaderを使い終わった後もTerminalを使い続けるなら、元に戻す方がいいかもしれない。
var prevHandler = terminal.handle(Signal.INT, signal -> { 〜 }); try { 〜 } finally { terminal.handle(Signal.INT, prevHandler); }