Skip to content

Line Editing ​

LineReader reads user input with line editing for REPLs and interactive command-line programs. The same code runs on the JVM, as a Scala Native binary, and on Node.js.

It provides:

  • Emacs-style editing (Ctrl-A/E, Ctrl-K/U/W/Y, Alt-B/F, arrows, Home/End)
  • History with Up/Down, Ctrl-P/N, and reverse search (Ctrl-R), saved to a file
  • Tab completion
  • Syntax highlighting with Tint
  • Multi-line input, e.g., until a statement ends with ;
  • Custom key bindings, including chords such as Ctrl-J Ctrl-R
  • Bracketed paste, so pasted multi-line text stays in one input
  • Wide characters (CJK, emoji) and long lines that wrap

Basic Usage ​

scala
import wvlet.uni.cli.*

val reader = LineReader(
  LineReaderConfig()
    .withHistoryFile(".my_history")
    .withCompleter(Completer.words(Seq("select", "from", "where")))
    .withHighlighter(_.replace("select", Tint.cyan("select")))
    .withInputComplete(_.trim.endsWith(";"))
    .withContinuationPrompt("  | ")
)

// JVM and Scala Native: block until the user presses Enter
reader.readLine("> ").await match
  case LineResult.Line(text)        => println(s"input: ${text}")
  case LineResult.Interrupted(text) => println("Ctrl-C")
  case LineResult.EndOfInput        => println("Ctrl-D or end of input")

readLine returns an Rx[LineResult]. On Scala.js there is no blocking, so chain the next read with flatMap. This loop works on every platform:

scala
def loop(reader: LineReader): Rx[Unit] = reader
  .readLine("> ")
  .flatMap {
    case LineResult.Line("exit") | LineResult.EndOfInput =>
      Rx.single(())
    case LineResult.Line(text) =>
      println(text)
      loop(reader)
    case LineResult.Interrupted(_) =>
      loop(reader)
  }

loop(reader).run()

Completion ​

A Completer returns the candidates that replace part of the input before the cursor. Completer.word completes the word before the cursor; a one-element result is inserted with a trailing space, and several are listed above the prompt after their common prefix is inserted.

scala
val files = Completer.word(delimiters = "'\"") { prefix =>
  listFiles(prefix).map { f =>
    // Keep typing into directories: no trailing space
    Candidate(f.path, complete = !f.isDirectory)
  }
}

Key Bindings ​

KeyMap.emacs is the default. Bind a key sequence, written in ^X notation, to a built-in LineAction or to custom code with LineAction.Custom. The custom code receives the LineEditor, which can read and change the input, print above it, or accept it.

scala
val runStatement = LineAction.Custom(
  "run-statement",
  editor =>
    editor.moveCursor(editor.buffer.length)
    editor.insert(";")
    editor.accept()
)

val config = LineReaderConfig()
  .withKeyMap(KeyMap.emacs.unbind("^J")) // Ctrl-J accepts by default; free it for chords
  .bind("^J^R", runStatement)

While the keys typed so far start a longer binding, the editor waits for the next key.

Output While Reading ​

reader.printAbove(message) prints a message above the prompt and draws the input again below it, for example, from a background task. To stop a running task on Ctrl-C without exiting, register a handler on the terminal:

scala
val cancel = reader.terminal.onInterrupt(() => task.cancel())
try task.await
finally cancel.cancel

Terminal Access ​

TerminalIO is the layer LineReader is built on, and can be used directly for key-driven UIs: enterRawMode()/exitRawMode(), readInput, size, write, and onInterrupt. KeyDecoder turns input bytes into Key events.

PlatformRaw mode and window size
JVMlibc termios through the Foreign Function & Memory API; needs Java 22 or later
Scala Nativelibc termios through a small C shim bundled with uni
Scala.jsNode.js process.stdin.setRawMode; not available in browsers

When stdin or stdout is not a terminal (a pipe, a file, CI), on Windows, or on a JVM older than 22, input is read line by line without editing, and nothing is drawn.

On Java 24 and later, run the JVM with --enable-native-access=ALL-UNNAMED to turn off the warning the JVM prints on the first native call.

TerminalIO.inMemory(...) replays input and records output, for testing code built on LineReader without a terminal.

Example ​

examples/repl is a small SQL-like REPL with completion, highlighting, multi-line statements, and chord bindings.

Released under the Apache 2.0 License.