minline

This module provides a simple, limited but fully-functional line editing library written in pure Nim.

To use this library, you must first initialize a LineEditor object using the initEditor method, and then use the readLine method to capture standard input instead of stdout.readLine:

var ed = initEditor(historyFile = "history.txt")
while true:
  let str = ed.readLine("-> ")
  echo "You typed: ", str

Optionally, you can also configure custom key bindings for keys and key sequences:

KEYMAP["ctrl+k"] = proc(ed: var LineEditor) =
  ed.clearLine()

Additionally, you can also configure a completionCallback proc to trigger auto-completion by pressing TAB:

ed.completionCallback = proc(ed: LineEditor): seq[string] =
  return @["copy", "list", "delete", "move", "remove"]

Note When compared to the readline or linenoise libraries, this module has the following limitations:

  • It is only possible to edit one line of text at a time. When using the readLine method, it will not be possible to physically go to the next line (this simplifies things a bit...).
  • No UTF8 support, only ASCII characters are supported.
  • No support for colorized output.
  • Only limited support for Emacs keybindings, no support for Vi mode and Vi keybindings.

Types

Key = int
The ASCII code of a keyboard key.
KeyCallback = proc (ed: var LineEditor) {.closure, ...gcsafe.}
A proc that can be bound to a key or a key sequence to access line editing functionalities.
KeySeq = seq[Key]
A sequence of one or more Keys.
Line = object
  text: string
  position: int
An object representing a line of text.
LineEditor = object
  completionCallback*: proc (ed: LineEditor): seq[string] {.closure, ...gcsafe.}
  history: LineHistory
  line: Line
  mode: LineEditorMode
An object representing a line editor, used to process text typed in the terminal.
LineEditorError = ref Exception
An error occured in the LineEditor.
LineEditorMode = enum
  mdInsert, mdReplace
The mode a LineEditor operates in (insert or replace).
LineError = ref Exception
A generic minline error.
LineHistory = object
  file: string
  tainted: bool
  position: int
  queue: Deque[string]
  max: int
An object representing the history of all commands typed in a LineEditor.

Vars

KEYMAP: CritBitTree[KeyCallback]
The following key mappings are configured by default:
  • backspace: deletePrevious
  • delete: deleteNext
  • insert: toggle editor mode
  • down: historyNext
  • up: historyPrevious
  • ctrl+n: historyNext
  • ctrl+p: historyPrevious
  • left: back
  • right: forward
  • ctrl+b: back
  • ctrl+f: forward
  • ctrl+c: quits the program
  • ctrl+d: quits the program
  • ctrl+u: clearLine
  • ctrl+a: goToStart
  • ctrl+e: goToEnd
  • home: goToStart
  • end: goToEnd
KEYNAMES: array[0 .. 31, string]
The following strings can be used in keymaps instead of the correspinding ASCII codes:
KEYNAMES[1]    =    "ctrl+a"
KEYNAMES[2]    =    "ctrl+b"
KEYNAMES[3]    =    "ctrl+c"
KEYNAMES[4]    =    "ctrl+d"
KEYNAMES[5]    =    "ctrl+e"
KEYNAMES[6]    =    "ctrl+f"
KEYNAMES[7]    =    "ctrl+g"
KEYNAMES[8]    =    "ctrl+h"
KEYNAMES[9]    =    "ctrl+i"
KEYNAMES[9]    =    "tab"
KEYNAMES[10]   =    "ctrl+j"
KEYNAMES[11]   =    "ctrl+k"
KEYNAMES[12]   =    "ctrl+l"
KEYNAMES[13]   =    "ctrl+m"
KEYNAMES[14]   =    "ctrl+n"
KEYNAMES[15]   =    "ctrl+o"
KEYNAMES[16]   =    "ctrl+p"
KEYNAMES[17]   =    "ctrl+q"
KEYNAMES[18]   =    "ctrl+r"
KEYNAMES[19]   =    "ctrl+s"
KEYNAMES[20]   =    "ctrl+t"
KEYNAMES[21]   =    "ctrl+u"
KEYNAMES[22]   =    "ctrl+v"
KEYNAMES[23]   =    "ctrl+w"
KEYNAMES[24]   =    "ctrl+x"
KEYNAMES[25]   =    "ctrl+y"
KEYNAMES[26]   =    "ctrl+z"
KEYSEQS: CritBitTree[KeySeq]
The following key sequences are defined and are used internally by LineEditor:
KEYSEQS["up"]         = @[27, 91, 65]      # Windows: @[224, 72]
KEYSEQS["down"]       = @[27, 91, 66]      # Windows: @[224, 80]
KEYSEQS["right"]      = @[27, 91, 67]      # Windows: @[224, 77]
KEYSEQS["left"]       = @[27, 91, 68]      # Windows: @[224, 75]
KEYSEQS["home"]       = @[27, 91, 72]      # Windows: @[224, 71]
KEYSEQS["end"]        = @[27, 91, 70]      # Windows: @[224, 79]
KEYSEQS["insert"]     = @[27, 91, 50, 126] # Windows: @[224, 82]
KEYSEQS["delete"]     = @[27, 91, 51, 126] # Windows: @[224, 83]

Consts

CTRL = {0..31}
Control characters.
DIGIT = {48..57}
Digits.
ESCAPES = {27}
Escape characters.
LETTER = {65..122}
Letters.
LOWERLETTER = {97..122}
Lowercase letters.
PRINTABLE = {32..126}
Printable characters.
UPPERLETTER = {65..90}
Uppercase letters.

Procs

proc back(ed: var LineEditor; n = 1) {....raises: [IOError, ValueError],
                                       tags: [WriteIOEffect].}
Move the cursor back by n characters on the current line (unless the beginning of the line is reached).
proc changeLine(ed: var LineEditor; s: string) {....raises: [IOError, ValueError],
    tags: [WriteIOEffect].}
Replaces the contents of the current line with the string s.
proc clearLine(ed: var LineEditor) {....raises: [IOError, ValueError],
                                     tags: [WriteIOEffect].}
Clears the contents of the current line and reset the cursor position to the beginning of the line.
proc completeLine(ed: var LineEditor): int {.
    ...raises: [Exception, IOError, ValueError, EOFError],
    tags: [RootEffect, WriteIOEffect, ReadIOEffect].}

If a completionCallback proc has been specified for the current editor, attempts to auto-complete the current line by running completionProc to return a list of possible values. It is possible to cycle through the matches by pressing the same key that triggered this proc.

The matches provided will be filtered based on the contents of the line when this proc was first triggered. If a match starts with the contents of the line, it will be displayed.

The following is a real-world example of a completionCallback used to complete the last word on the line with valid file paths.

import sequtils, strutils, ospath

ed.completionCallback = proc(ed: LineEditor): seq[string] =
  var words = ed.lineText.split(" ")
  var word: string
  if words.len == 0:
    word = ed.lineText
  else:
    word = words[words.len-1]
  var f = word[1..^1]
  if f == "":
    f = getCurrentDir().replace("\\", "/")
    return toSeq(walkDir(f, true))
      .mapIt("\"$1" % it.path.replace("\\", "/"))
  elif f.dirExists:
    f = f.replace("\\", "/")
    if f[f.len-1] != '/':
      f = f & "/"
    return toSeq(walkDir(f, true))
      .mapIt("\"$1$2" % [f, it.path.replace("\\", "/")])
  else:
    var dir: string
    if f.contains("/") or dir.contains("\\"):
      dir = f.parentDir
      let file = f.extractFileName
      return toSeq(walkDir(dir, true))
        .filterIt(it.path.toLowerAscii.startsWith(file.toLowerAscii))
        .mapIt("\"$1/$2" % [dir, it.path.replace("\\", "/")])
    else:
      dir = getCurrentDir()
      return toSeq(walkDir(dir, true))
        .filterIt(it.path.toLowerAscii.startsWith(f.toLowerAscii))
        .mapIt("\"$1" % [it.path.replace("\\", "/")])
proc deleteNext(ed: var LineEditor) {....raises: [LineError, IOError, ValueError],
                                      tags: [WriteIOEffect].}
Move the cursor to the right by one character (unless at the end of the line) and delete the existing character, if any.
proc deletePrevious(ed: var LineEditor) {.
    ...raises: [IOError, ValueError, LineError], tags: [WriteIOEffect].}
Move the cursor to the left by one character (unless at the beginning of the line) and delete the existing character, if any.
proc forward(ed: var LineEditor; n = 1) {....raises: [IOError, ValueError],
    tags: [WriteIOEffect].}
Move the cursor forward by n characters on the current line (unless the beginning of the line is reached).
proc getchr(): cint {....raises: [IOError, EOFError],
                      tags: [WriteIOEffect, ReadIOEffect].}
Retrieves an ASCII character from stdin.
proc goToEnd(ed: var LineEditor) {....raises: [IOError, ValueError],
                                   tags: [WriteIOEffect].}
Move the cursor to the end of the line.
proc goToStart(ed: var LineEditor) {....raises: [], tags: [WriteIOEffect].}
Move the cursor to the beginning of the line.
proc historyAdd(ed: var LineEditor; force = false) {....raises: [IOError],
    tags: [WriteIOEffect].}
Adds the current editor line to the history. If force is set to true, the line will be added even if it's blank.
proc historyFlush(ed: var LineEditor) {....raises: [], tags: [].}
If there is at least one entry in the history, it sets the position of the cursor to the last element and sets the tainted flag to false.
proc historyInit(size = 256; file: string = ""): LineHistory {.
    ...raises: [IOError], tags: [ReadDirEffect, ReadIOEffect, WriteIOEffect].}
Creates a new LineHistory object with the specified size and file.
proc historyNext(ed: var LineEditor) {....raises: [IOError, ValueError],
                                       tags: [WriteIOEffect].}
Replaces the contents of the current line with the following line stored in the history (if any).
proc historyPrevious(ed: var LineEditor) {....raises: [IOError, ValueError],
    tags: [WriteIOEffect].}
Replaces the contents of the current line with the previous line stored in the history (if any). The current line will be added to the history and the hisory will be marked as tainted.
proc initEditor(mode = mdInsert; historySize = 256; historyFile: string = ""): LineEditor {.
    ...raises: [IOError], tags: [ReadDirEffect, ReadIOEffect, WriteIOEffect].}
Creates a LineEditor object.
proc lineText(ed: LineEditor): string {....raises: [], tags: [].}
Returns the contents of the current line.
proc password(ed: var LineEditor; prompt = ""): string {.
    ...raises: [IOError, EOFError, Exception, KeyError, ValueError],
    tags: [WriteIOEffect, ReadIOEffect, RootEffect].}
Convenience method to use instead of readLine to hide the characters inputed by the user.
proc printChar(ed: var LineEditor; c: int) {.
    ...raises: [LineError, IOError, ValueError], tags: [WriteIOEffect].}
Prints the character c to the current line. If in the middle of the line, the following characters are shifted right or replaced depending on the editor mode.
proc putchr(c: cint) {.header: "stdio.h", importc: "putchar", ...raises: [],
                       tags: [].}
Prints an ASCII character to stdout.
proc readLine(ed: var LineEditor; prompt = ""; hidechars = false): string {.
    ...gcsafe, raises: [IOError, EOFError, Exception, KeyError, ValueError],
    tags: [WriteIOEffect, ReadIOEffect, RootEffect].}

High-level proc to be used instead of stdin.readLine to read a line from standard input using the specified LineEditor object.

Note that:

  • prompt is a string (that cannot contain escape codes, so it cannot be colored) that will be prepended at the start of the line and not included in the contents of the line itself.
  • If hidechars is set to true, asterisks will be printed to stdout instead of the characters entered by the user.