jline 使い方メモ


# jline とは
Java で高機能な CUI コンソールを作るためのライブラリ。
標準 API の入出力だけでは実現できない機能を実現できる。

100% Java ではなく、実行環境に依存したライブラリが jar に同梱されている（Windows なら dll が使用される）。

Java 9 から導入される jshell も jline を使用している（`jdk.internal.jshell.tool.ConsoleIOContext` とか）。

# 環境
## OS
- Windows 7

## Java
- 1.8

# インストール
```groovy:build.gradle
compile 'jline:jline:2.14.2'
```

# Hello World
```java
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);
    }
}
```

```text:実行結果
jline > 【hello】
hello
```

※`【】` で括っている部分は、キーボードから入力した文字列を表現している。

- `ConsoleReader` クラスが、コンソールの入出力を操作する基本のクラスとなる。
- `readLine()` で、ユーザー入力を待機する。
- 引数に文字列を渡せば、それが画面に表示されたうえでユーザー入力が待機される。

# プロンプトを設定する
```java
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());
    }
}
```

```text:実行結果
jline > 【foo】
foo
jline > 【bar】
bar
```

- `setPrompt()` で文字列を渡すと、以後入力を待機するたびにその文字列が表示されるようになる。

# ユーザー入力をマスクする
```java
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('*'));
    }
}
```

```text:実行結果
【*****】
jline
```

- `readLine()` の引数に `char` を渡すと、ユーザー入力がその文字でマスキングされる。
- 文字を表示させたくない場合は、 `(char)0` を引数に渡せばいい。

# 文字を出力する
```java
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");
    }
}
```

```text:実行結果
123
```

- `putString()` で文字列を出力する。
- `flush()` を呼ばないと画面には出力されない。

## `print()`, `println()` との違い
文字列を出力するメソッドとして、他に `print()` や `println()` というメソッドが用意されている。
`print()` や `println()` は、指定した文字列をそのまま `OutputStream` （デフォルトは標準出力）に出力するだけだが、 `putString()` はバッファーと呼ばれるものにも文字列を出力している。

このバッファーが具体的にどういうものなのかはよく分かっていないが、これにより `print()` などとは異なり `putString()` で出力した場合は以下のようなことが可能になる。

- 一度文字を出力してから、カーソル位置を戻すことができる。
- バックスペースキーなどを送信して、出力した文字を消すことができる。
- `readLine()` したときに `putString()` で出力した文字は手動で編集できる。

例えば、以下のような実装を動かしてみる。

```java
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();
    }
}
```

![jline.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/35c4ce01-5f47-a54a-38f8-6af994f0ce72.gif)

ちょっと分かりづらいが、 `putString()` で出力した文字（`123`）の上ではカーソルを動かす事ができているが、 `print()` で出力した文字（`ABC`）の上にはカーソルを移動させることができない。
バックスペースを入力した場合も、 `putString()` で出力した文字列なら削除できるが、 `print()` で出力した分は削除できない。

正しいかどうかは別として、感覚としては `print()` で出力した分は完全にコンソール上に出力が確定してしまい後で表示を変更することはできず、 `putString()` で出力した分については一応コンソール上も表示はさせているがまだ確定はしていないので、任意に表示を変更できる、というイメージで良いのかなぁと思う。

ただし、あくまで色々試したりした結果個人的に辿り着いた推測なので、厳密に正しいかどうかは分からない（全然ドキュメントが無いのでつらい）。

# Enter を送信する
```java
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");
    }
}
```

```text:実行結果
1

2
```

# バックスペースを送信する
```java
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();
    }
}
```

```text:実行結果
12
```

- `backspace()` 実行後に `flush()` しないと反映されないので注意。

# ビープ音を鳴らす
```java
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` を設定しておく必要がある。

# スクリーンをクリアする
```java
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 で試した限りでは。

# カーソル位置を変更する
```java
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();
    }
}
```

![jline.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/3a2af644-c6b4-4fec-42c2-9d3f2dc07c12.gif)

- `moveCursor()` でカーソル位置を変更できる。
- 正数を指定した場合は右方向に、負数を指定した場合は左方向に移動する。

# 現在のカーソル行から末尾までを削除する
```java
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();
    }
}
```

```text:実行結果
12
```

- `killLine()` で、現在カーソルが存在する場所から末尾までを削除できる。
- `flush()` で反映される。

# クリップボードの情報を貼り付ける
```java
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();
    }
}
```

![jline.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/c0a36b20-e81e-daa1-c419-11da8dde0258.gif)

- `paste()` メソッドで、現在クリップボードに保存されている情報を出力できる。

# 複数の文字列を整列して表示させる
```java
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();
    }
}
```

```text:実行結果
aaa         bbbbb       ccccc       dd          eeeeeeee    ffffff
gggg        hhhhhhhh    iiiiiii     jjjj        kkkkkk      lll
mmmmmmmmm
```

- `printColumns()` の `CharSequence` の `Collection` を渡すと、各要素を整列させて表示することができる。

# 行を再描画する
```java
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();
    }
}
```

```text:実行結果
redrawLine しない場合
jline > redrawLine した場合
```

- `redrawLine()` を実行すると、現在の行のバッファを一旦クリアして、プロンプトの文字を先頭につけて再描画する。

# 入力待機中に Ctrl + C を検知する
```java
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("中断！");
        }
    }
}
```

![jline.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/1130db0f-6b6d-a1a7-f280-a8b3d3c0dee8.gif)

- `setHandleUserInterrupt()` に `true` を設定すると、 `readLine()` 中に `Ctrl` + `C` を入力された場合に `UserInterruptException` がスローされるようになる。
- デフォルトは `false` なので、 `Ctrl` + `C` を入力しても何も起きない。

# ヒストリー
```java
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
        }
    }
}
```

![jline.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/c98062ae-8ddb-e84b-9ba0-41f27e9b7ad6.gif)

- `getHistory()` で `History` オブジェクトが取得できる。
- `History` オブジェクトには、それまでコンソールから入力された文字列が格納されており、任意に情報を取り出すことができる。
- 同じ文字が連続で入力された場合は、履歴は増えない。


# Tab 補完
## FileNameCompleter
```java
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
        }
    }
}
```

![jline.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/eb7619cf-21f8-d26d-6508-a01c75b5aed2.gif)

- `ConsoleReader.addCompleter()` に `Completer` インターフェースを実装したオブジェクトを渡すことで、タブ入力時の補完機能を実装できる。
- 標準でいくつか `Completer` の実装が用意されている。
- 上記例では、 `FileNameCompleter` を使用している。
- `FileNameCompleter` は実行時のカレントディレクトリ以下に存在するファイルを補完対象として表示してくれる。
- パス区切り文字を入れることで、さらに下の階層まで補完候補として検索してくれる。

## <span id="stringcompleter">StringCompleter</span>
```java
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
        }
    }
}
```

![jline.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/97f19eab-7bc3-5abf-c579-1fa31914890f.gif)

- `StringCompleter` は、あらかじめ候補となる任意の文字のリストをセットしておくことができる。

## Completer を自作する
```java
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 を使った例と同じ](#stringcompleter)。
- `Completer` は関数型インターフェースなので、ラムダ式で実装可能。
- `complete()` というメソッドを実装する。
- 引数には以下の３つが渡される。
    - `buffer`：現在コンソールに入力されている文字列。この文字列を元に候補を抽出する。
    - `cursor`：現在のカーソル位置。
    - `candidate`：候補となる文字列をセットする `List`。この `List` に画面に表示したい候補をセットすると、あとは jline が良しなに候補一覧の表示や補完をしてくれる。  
毎回空の `List` が渡される。
- 戻り値は、候補を表示した後の相対的なカーソル位置を指定する。
    - 普通は `0` を返せばよさそうだが、 `StringCompleter` が候補が無い場合に `-1` を返しているので、とりあえず同じ形にしている。

# キーバインド
正直、ドキュメントが全く無くて、以下はソースや jshell のコードなどから推測した結果です。

## デフォルトのキーバインド
`readLine()` した場合、デフォルトのキーバインドは emacs と同じになっている。
emacs 以外には vi のキーバインドがサポートされており、以下の方法で変更できる。

```java
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` に定数で定義されている。


## キーバインドを変更する（基本）
```java
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
        }
    }
}
```

![jline.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/cbcaae7a-50ca-a1a1-c057-8d9b5765235c.gif)

- 普通に `a` と入力したら `<a>` と出力するようにしている。
- `Console` から `getKeys()` で `KeyMap` のインスタンスが取得できる。
- この `KeyMap` に、キーが入力されたときに実行する処理のマッピングが登録されているっぽい。
- `bind()` メソッドを使うことで、任意のキー入力に対して処理をマッピングできる。
- 第一引数には、マッピング対象のキーを `CharSequence` で指定する。
- 第二引数に `java.awt.event.ActionListener` を実装したインスタンスを渡すと、キー入力時の処理を任意の処理に差し替えることができる。

## Ctrl と組み合わせたキーにマッピングする
```java
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
        }
    }
}
```

![jline.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/535002a6-4913-3037-c185-48cc3af5cc6f.gif)

- 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
## ターミナルの幅・高さを取得する
```java
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"
        );
    }
}
```

```text:実行結果
height = 40
width = 140
```

# 参考
- [Package Index | JLine](https://jline.github.io/jline2/apidocs/reference/packages.html)
- [JLine - JLine](http://jline.sourceforge.net/)
- [Routrek Networks ASCII制御文字の入力方法](http://www.routrek.co.jp/product/varaterm/controlcode.html)
- [Examples of jline.console.KeyMap.bind() | massapi.com](http://massapi.com/method/jline/console/KeyMap.bind.html)
- [Introduction to JShell: the Java REPL Tool #jjug_ccc #ccc_ab4](http://www.slideshare.net/bitter_fox/introduction-to-jshell-the-java-repl-tool)