Skip to content

Nim implementation of linenoise command line editor

License

Notifications You must be signed in to change notification settings

jangko/nim-noise

Repository files navigation

noise

nimble license Github action

Nim implementation of linenoise command line editor, inspired by replxx and linenoise-ng

Features

  • Line editing with emacs keybindings
  • History handling
  • Completion
  • Unicode aware
  • Intuitive ESC key sub menu escaping
  • Incremental history search
  • Support multiline editing out of the box
  • Support multiline prompt with color and unicode characters
  • A bunch of compile time switches to select which features you want to turn on/off
  • Support Windows, Linux and Mac OS
  • Disable all basic features, then you'll get a cross platform key stroke library

Planned Features

  • Hints(work in progress)
  • Syntax coloring(work in progress)

API

Primitive API(available when prompt_no_basic is defined):

  • proc readChar(): char32

Basic API:

  • proc init(x: typedesc[Noise]): Noise
  • proc getKeyType(self: Noise): KeyType
  • proc getLine(self: var Noise): string
  • proc readLine(self: var Noise): bool
  • proc setPrompt(self: var Noise, text: Styler)
  • proc setPrompt(self: var Noise, text: string)
  • proc getPrompt(self: var Noise): Styler

History API:

  • proc historyAdd(self: var Noise, line: string)
  • proc historySetMaxLen(self: var Noise, len: int)
  • iterator histories(self: var Noise): string
  • iterator historyPairs(self: var Noise): (int, string)
  • proc historySave(self: var Noise, fileName: string): bool
  • proc historyLoad(self: var Noise, fileName: string): bool
  • proc historyClear(self: var Noise)

Completion API:

  • proc setCompletionHook(self: var Noise, prc: CompletionHook)
  • proc addCompletion(self: var Noise, text: string)

PreloadBuffer API:

  • proc preloadBuffer(self: var Noise, preloadText: string)

KillRing API:

  • proc killSetMaxLen(self: var Noise, len: int)

Examples

import noise, strutils

proc main() =
  var noise = Noise.init()

  let prompt = Styler.init(fgRed, "Red ", fgGreen, "苹果> ")
  noise.setPrompt(prompt)

  when promptPreloadBuffer:
    noise.preloadBuffer("Superman")

  when promptHistory:
    var file = "history"
    discard noise.historyLoad(file)

  when promptCompletion:
    proc completionHook(noise: var Noise, text: string): int =
      const words = ["apple", "diamond", "diadem", "diablo", "horse", "home", "quartz", "quit"]
      for w in words:
        if w.find(text) != -1:
          noise.addCompletion w

    noise.setCompletionHook(completionHook)

  while true:
    let ok = noise.readLine()
    if not ok: break

    let line = noise.getLine
    case line
    of ".help": printHelp()
    of ".quit": break
    else: discard

    when promptHistory:
      if line.len > 0:
        noise.historyAdd(line)

  when promptHistory:
    discard noise.historySave(file)

main()

Key Binding

  # Completion
    CTRL-I/TAB                   activates completion
       TAB again                 rotate between completion alternatives
       ESC, CTRL-C               undo changes and exit to normal editing
       Other keys                accept completion and resume to normal editing

  # History
    CTRL-P, UP_ARROW_KEY         recall previous line in history
    CTRL-N, DOWN_ARROW_KEY       recall next line in history
    ALT-<, PAGE_UP_KEY           beginning of history
    ALT->, PAGE_DOWN_KEY         end of history

  # Incremental history search
    CTRL-R, CTRL-S               forward/reverse interactive history search
       TAB, DOWN_ARROW_KEY       rotate between history alternatives(+)
       UP_ARROW_KEY              rotate between history alternatives(-)
       ESC, CTRL-C               cancel selection and exit to normal editing
       Other keys                accept selected history

  # Kill and yank
    ALT-D                        kill word to right of cursor
    ALT + Backspace              kill word to left of cursor
    CTRL-K                       kill from cursor to end of line
    CTRL-U                       kill all characters to the left of the cursor
    CTRL-W                       kill to whitespace (not word) to left of cursor
    CTRL-Y                       yank killed text
       ALT-Y                     'yank-pop', rotate popped text

  # Word editing
    ALT-C                        give word initial cap
    ALT-L                        lowercase word
    CTRL-T                       transpose characters
    ALT-U                        uppercase word

  # Cursor navigation
    CTRL-A, HOME_KEY             move cursor to start of line
    CTRL-E, END_KEY              move cursor to end of line
    CTRL-B, LEFT_ARROW_KEY       move cursor left by one character
    CTRL-F, RIGHT_ARROW_KEY      move cursor right by one character
    ALT-F,
    CTRL + RIGHT_ARROW_KEY,
    ALT + RIGHT_ARROW_KEY        move cursor right by one word
    ALT-B,
    CTRL + LEFT_ARROW_KEY,
    ALT + LEFT_ARROW_KEY         move cursor left by one word

  # Basic Editing
    CTRL-C                       abort this line
    CTRL-H/backspace             delete char to left of cursor
    DELETE_KEY                   delete the character under the cursor
    CTRL-D                       delete the character under the cursor
                                 on an empty line, exit the shell
    CTRL-J, CTRL-M/Enter         accept line
    CTRL-L                       clear screen and redisplay line

Compile time switches:

Please use -d: or --define: during build time.

  • prompt_no_history
  • prompt_no_kill
  • prompt_no_completion
  • prompt_no_word_editing
  • prompt_no_preload_buffer
  • prompt_no_incremental_history_search(if you disabled history, this one also disabled)
  • prompt_no_basic(disable all features, you'll only have access to primitives API.)

Altough you can use killSetMaxLen and historySetMaxLen at runtime, there are compile time options to set them too. e.g. -d:DefaultHistoryMaxLen=150

  • DefaultHistoryMaxLen

  • DefaultKillRingMaxLen

  • esc_exit_editing

    While Ctrl-C perform hard abort, esc_exit_editing allow soft abort when ESC pressed. You can get the key type using getKeyType.

    while true:
      let ok = noise.readLine()
      if not ok: break
    
      if noise.getKeyType == ktEsc:
        echo "do something"
  • prompt_emacs_word_editing

    By default case editing keys will change the case from the beginning of a word, this switch will enable Emacs behavior, where the case is changed from the cursor position until the end of the word.

Unicode awareness

On Posix OSes, everything is encoded in UTF-8. On Windows, the API dictates UTF-16 usage. Internally, nim-noise use UTF-32 to encode the text and some homebrew encoding to encode keyboard keys. Altough this is sound complicated, you as a user will only deal with UTF-8 when interacting with nim-noise. If your application only use ASCII subset, then you will not to worry about anything.

When you write your completion callback, add and retrieve history, preloaded buffer, you will receive UTF-8 encoded string and give UTF-8/ASCII encoded string too.

Primitives API

Sometimes, when building a console UI, all you need is only a cross platform key stroke library. You can use nim-noise like that by specifying prompt_no_basic.

Installation via nimble

nimble install noise