S-JIS[2022-09-04/2022-10-03] 変更履歴

JLine3 Terminal

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

Ctrl+Cを無効化する例

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]


DumbTerminal

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が生成される。


dumb terminalの警告を消す方法

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();

NonBlockingReader

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が返る。


TerminalでCtrl+Cを検知する方法

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);  
        }

JLine3へ戻る / JLineへ戻る / Javaへ戻る / 技術メモへ戻る
メールの送信先:ひしだま