illwill

Search:
Group by:

Authors:John Novak

This is a curses inspired simple terminal library that aims to make writing cross-platform text mode applications easier. The main features are:

  • Non-blocking keyboard input
  • Support for key combinations and special keys available in the standard Windows Console (cmd.exe) and most common POSIX terminals
  • Virtual terminal buffers with double-buffering support (only display changes from the previous frame and minimise the number of attribute changes to reduce CPU usage)
  • Simple graphics using UTF-8 box drawing symbols
  • Full-screen support with restoring the contents of the terminal after exit (restoring works only on POSIX)
  • Basic suspend/continue (SIGTSTP, SIGCONT) support on POSIX
  • Basic mouse support

The module depends only on the standard terminal module. However, you should not use any terminal functions directly, neither should you use echo, write or other similar functions for output. You should only use the interface provided by the module to interact with the terminal.

The following symbols are exported from the terminal module (these are safe to use):

Types

BackgroundColor = enum
  bgNone = 0,               ## default (transparent)
  bgBlack = 40,             ## black
  bgRed,                    ## red
  bgGreen,                  ## green
  bgYellow,                 ## yellow
  bgBlue,                   ## blue
  bgMagenta,                ## magenta
  bgCyan,                   ## cyan
  bgWhite                    ## white
Background colors
BoxBuffer = ref object
  
Box buffers are used to store the results of multiple consecutive box drawing calls. The idea is that when you draw a series of lines and rectangles into the buffer, the overlapping lines will get automatically connected by placing the appropriate UTF-8 symbols at the corner and junction points. The results can then be written to a terminal buffer.
ForegroundColor = enum
  fgNone = 0,               ## default
  fgBlack = 30,             ## black
  fgRed,                    ## red
  fgGreen,                  ## green
  fgYellow,                 ## yellow
  fgBlue,                   ## blue
  fgMagenta,                ## magenta
  fgCyan,                   ## cyan
  fgWhite                    ## white
Foreground colors
IllwillError = object of CatchableError
Key {.pure.} = enum
  None = (-1, "None"), CtrlA = (1, "CtrlA"), CtrlB = (2, "CtrlB"),
  CtrlC = (3, "CtrlC"), CtrlD = (4, "CtrlD"), CtrlE = (5, "CtrlE"),
  CtrlF = (6, "CtrlF"), CtrlG = (7, "CtrlG"), CtrlH = (8, "CtrlH"),
  Tab = (9, "Tab"), CtrlJ = (10, "CtrlJ"), CtrlK = (11, "CtrlK"),
  CtrlL = (12, "CtrlL"), Enter = (13, "Enter"), CtrlN = (14, "CtrlN"),
  CtrlO = (15, "CtrlO"), CtrlP = (16, "CtrlP"), CtrlQ = (17, "CtrlQ"),
  CtrlR = (18, "CtrlR"), CtrlS = (19, "CtrlS"), CtrlT = (20, "CtrlT"),
  CtrlU = (21, "CtrlU"), CtrlV = (22, "CtrlV"), CtrlW = (23, "CtrlW"),
  CtrlX = (24, "CtrlX"), CtrlY = (25, "CtrlY"), CtrlZ = (26, "CtrlZ"),
  Escape = (27, "Escape"), CtrlBackslash = (28, "CtrlBackslash"),
  CtrlRightBracket = (29, "CtrlRightBracket"), Space = (32, "Space"),
  ExclamationMark = (33, "ExclamationMark"), DoubleQuote = (34, "DoubleQuote"),
  Hash = (35, "Hash"), Dollar = (36, "Dollar"), Percent = (37, "Percent"),
  Ampersand = (38, "Ampersand"), SingleQuote = (39, "SingleQuote"),
  LeftParen = (40, "LeftParen"), RightParen = (41, "RightParen"),
  Asterisk = (42, "Asterisk"), Plus = (43, "Plus"), Comma = (44, "Comma"),
  Minus = (45, "Minus"), Dot = (46, "Dot"), Slash = (47, "Slash"),
  Zero = (48, "Zero"), One = (49, "One"), Two = (50, "Two"),
  Three = (51, "Three"), Four = (52, "Four"), Five = (53, "Five"),
  Six = (54, "Six"), Seven = (55, "Seven"), Eight = (56, "Eight"),
  Nine = (57, "Nine"), Colon = (58, "Colon"), Semicolon = (59, "Semicolon"),
  LessThan = (60, "LessThan"), Equals = (61, "Equals"),
  GreaterThan = (62, "GreaterThan"), QuestionMark = (63, "QuestionMark"),
  At = (64, "At"), ShiftA = (65, "ShiftA"), ShiftB = (66, "ShiftB"),
  ShiftC = (67, "ShiftC"), ShiftD = (68, "ShiftD"), ShiftE = (69, "ShiftE"),
  ShiftF = (70, "ShiftF"), ShiftG = (71, "ShiftG"), ShiftH = (72, "ShiftH"),
  ShiftI = (73, "ShiftI"), ShiftJ = (74, "ShiftJ"), ShiftK = (75, "ShiftK"),
  ShiftL = (76, "ShiftL"), ShiftM = (77, "ShiftM"), ShiftN = (78, "ShiftN"),
  ShiftO = (79, "ShiftO"), ShiftP = (80, "ShiftP"), ShiftQ = (81, "ShiftQ"),
  ShiftR = (82, "ShiftR"), ShiftS = (83, "ShiftS"), ShiftT = (84, "ShiftT"),
  ShiftU = (85, "ShiftU"), ShiftV = (86, "ShiftV"), ShiftW = (87, "ShiftW"),
  ShiftX = (88, "ShiftX"), ShiftY = (89, "ShiftY"), ShiftZ = (90, "ShiftZ"),
  LeftBracket = (91, "LeftBracket"), Backslash = (92, "Backslash"),
  RightBracket = (93, "RightBracket"), Caret = (94, "Caret"),
  Underscore = (95, "Underscore"), GraveAccent = (96, "GraveAccent"),
  A = (97, "A"), B = (98, "B"), C = (99, "C"), D = (100, "D"), E = (101, "E"),
  F = (102, "F"), G = (103, "G"), H = (104, "H"), I = (105, "I"),
  J = (106, "J"), K = (107, "K"), L = (108, "L"), M = (109, "M"),
  N = (110, "N"), O = (111, "O"), P = (112, "P"), Q = (113, "Q"),
  R = (114, "R"), S = (115, "S"), T = (116, "T"), U = (117, "U"),
  V = (118, "V"), W = (119, "W"), X = (120, "X"), Y = (121, "Y"),
  Z = (122, "Z"), LeftBrace = (123, "LeftBrace"), Pipe = (124, "Pipe"),
  RightBrace = (125, "RightBrace"), Tilde = (126, "Tilde"),
  Backspace = (127, "Backspace"), Up = (1001, "Up"), Down = (1002, "Down"),
  Right = (1003, "Right"), Left = (1004, "Left"), Home = (1005, "Home"),
  Insert = (1006, "Insert"), Delete = (1007, "Delete"), End = (1008, "End"),
  PageUp = (1009, "PageUp"), PageDown = (1010, "PageDown"), F1 = (1011, "F1"),
  F2 = (1012, "F2"), F3 = (1013, "F3"), F4 = (1014, "F4"), F5 = (1015, "F5"),
  F6 = (1016, "F6"), F7 = (1017, "F7"), F8 = (1018, "F8"), F9 = (1019, "F9"),
  F10 = (1020, "F10"), F11 = (1021, "F11"), F12 = (1022, "F12"),
  Mouse = (5000, "Mouse")
Supported single key presses and key combinations
MouseButton {.pure.} = enum
  mbNone, mbLeft, mbMiddle, mbRight
MouseButtonAction {.pure.} = enum
  mbaNone, mbaPressed, mbaReleased
MouseInfo = object
  x*: int                    ## X mouse position
  y*: int                    ## Y mouse position
  button*: MouseButton       ## which button was pressed
  action*: MouseButtonAction ## if button was released or pressed
  ctrl*: bool                ## was Ctrl down
  shift*: bool               ## was Shift down
  scroll*: bool              ## if this is a mouse scroll event
  scrollDir*: ScrollDirection ## scroll direction
  move*: bool                ## if this is a mouse move event
  
ScrollDirection {.pure.} = enum
  sdNone, sdUp, sdDown
TerminalBuffer = ref object
  

A virtual terminal buffer of a fixed width and height. It remembers the current color and style settings and the current cursor position.

Write to the terminal buffer with TerminalBuffer.write() or access the character buffer directly with the index operators.

Example:

import illwill, unicode

# Initialise the console in non-fullscreen mode
illwillInit(fullscreen=false)

# Create a new terminal buffer
var tb = newTerminalBuffer(terminalWidth(), terminalHeight())

# Write the character "X" at position (5,5) then read it back
tb[5,5] = TerminalChar(ch: "X".runeAt(0), fg: fgYellow, bg: bgNone, style: {})
let ch = tb[5,5]

# Write "foo" at position (10,10) in bright red
tb.setForegroundColor(fgRed, bright=true)
tb.setCursorPos(10, 10)
tb.write("foo")

# Write "bar" at position (15,12) in bright red, without changing
# the current cursor position
tb.write(15, 12, "bar")

tb.write(0, 20, "Normal ", fgYellow, "ESC", fgWhite,
                " or ", fgYellow, "Q", fgWhite, " to quit")

# Output the contents of the buffer to the terminal
tb.display()

# Clean up
illwillDeinit()
TerminalChar = object
  ch*: Rune
  fg*: ForegroundColor
  bg*: BackgroundColor
  style*: set[Style]
  forceWrite*: bool

Represents a character in the terminal buffer, including color and style information.

If forceWrite is set to true, the character is always output even when double buffering is enabled (this is a hack to achieve better continuity of horizontal lines when using UTF-8 box drawing symbols in the Windows Console).

TerminalCmd = enum
  resetStyle                 ## reset attributes
commands that can be expressed as arguments

Procs

proc `[]`(tb: TerminalBuffer; x, y: Natural): TerminalChar {....raises: [],
    tags: [], forbids: [].}
Index operator to read a character from the terminal buffer at the specified location. Returns nil if the location is outside of the extents of the terminal buffer.
proc `[]=`(tb: var TerminalBuffer; x, y: Natural; ch: TerminalChar) {.
    ...raises: [], tags: [], forbids: [].}
Index operator to write a character into the terminal buffer at the specified location. Does nothing if the location is outside of the extents of the terminal buffer.
proc clear(tb: var TerminalBuffer; ch: string = " ") {....raises: [], tags: [],
    forbids: [].}
Clears the contents of the terminal buffer with the ch character using the fgNone and bgNone attributes.
proc copyFrom(bb: var BoxBuffer; src: BoxBuffer) {....raises: [], tags: [],
    forbids: [].}

Copies the full contents of the src box buffer into this one.

If the extents of the source buffer is greater than the extents of the destination buffer, the copied area is clipped to the destination area.

proc copyFrom(bb: var BoxBuffer; src: BoxBuffer;
              srcX, srcY, width, height: Natural; destX, destY: Natural) {.
    ...raises: [], tags: [], forbids: [].}

Copies the contents of the src box buffer into this one. A rectangular area of dimension width and height is copied from the position srcX and srcY in the source buffer to the position destX and destY in this buffer.

If the extents of the area to be copied lie outside the extents of the buffers, the copied area will be clipped to the available area (in other words, the call can never fail; in the worst case it just copies nothing).

proc copyFrom(tb: var TerminalBuffer; src: TerminalBuffer;
              srcX, srcY, width, height: Natural; destX, destY: Natural;
              transparency = false) {....raises: [], tags: [], forbids: [].}

Copies the contents of the src terminal buffer into this one. A rectangular area of dimension width and height is copied from the position srcX and srcY in the source buffer to the position destX and destY in this buffer.

If the extents of the area to be copied lie outside the extents of the buffers, the copied area will be clipped to the available area (in other words, the call can never fail; in the worst case it just copies nothing).

If transparency is true, white-space characters in the source buffer will not overwrite the contents of the target buffer (they're treated as transparent).

proc copyFrom(tb: var TerminalBuffer; src: TerminalBuffer; transparency = false) {.
    ...raises: [], tags: [], forbids: [].}

Copies the full contents of the src terminal buffer into this one.

If the extents of the source buffer is greater than the extents of the destination buffer, the copied area is clipped to the destination area.

If transparency is true, white-space characters in the source buffer will not overwrite the contents of the target buffer (they're treated as transparent).

proc display(tb: TerminalBuffer) {....raises: [IllwillError, IOError, ValueError],
                                   tags: [WriteIOEffect], forbids: [].}

Outputs the contents of the terminal buffer to the actual terminal.

If the module is not intialised, IllwillError is raised.

proc drawHorizLine(bb: var BoxBuffer; x1, x2, y: Natural;
                   doubleStyle: bool = false; connect: bool = true) {.
    ...raises: [], tags: [], forbids: [].}
Draws a horizontal line into the box buffer. Set doubleStyle to true to draw double lines. Set connect to true to connect overlapping lines.
proc drawHorizLine(tb: var TerminalBuffer; x1, x2, y: Natural;
                   doubleStyle: bool = false) {....raises: [], tags: [],
    forbids: [].}
Convenience method to draw a single horizontal line into a terminal buffer directly.
proc drawRect(bb: var BoxBuffer; x1, y1, x2, y2: Natural;
              doubleStyle: bool = false; connect: bool = true) {....raises: [],
    tags: [], forbids: [].}
Draws a rectangle into the box buffer. Set doubleStyle to true to draw double lines. Set connect to true to connect overlapping lines.
proc drawRect(tb: var TerminalBuffer; x1, y1, x2, y2: Natural;
              doubleStyle: bool = false) {....raises: [], tags: [], forbids: [].}
Convenience method to draw a rectangle into a terminal buffer directly.
proc drawVertLine(bb: var BoxBuffer; x, y1, y2: Natural;
                  doubleStyle: bool = false; connect: bool = true) {....raises: [],
    tags: [], forbids: [].}
Draws a vertical line into the box buffer. Set doubleStyle to true to draw double lines. Set connect to true to connect overlapping lines.
proc drawVertLine(tb: var TerminalBuffer; x, y1, y2: Natural;
                  doubleStyle: bool = false) {....raises: [], tags: [], forbids: [].}
Convenience method to draw a single vertical line into a terminal buffer directly.
proc fill(tb: var TerminalBuffer; x1, y1, x2, y2: Natural; ch: string = " ") {.
    ...raises: [], tags: [], forbids: [].}
Fills a rectangular area with the ch character using the current text attributes. The rectangle is clipped to the extends of the terminal buffer and the call can never fail.
func getBackgroundColor(tb: var TerminalBuffer): BackgroundColor {....raises: [],
    tags: [], forbids: [].}
Returns the current background color.
func getCursorPos(tb: TerminalBuffer): tuple[x: Natural, y: Natural] {.
    ...raises: [], tags: [], forbids: [].}
Returns the current cursor position.
func getCursorXPos(tb: TerminalBuffer): Natural {....raises: [], tags: [],
    forbids: [].}
Returns the current X cursor position.
func getCursorYPos(tb: TerminalBuffer): Natural {....raises: [], tags: [],
    forbids: [].}
Returns the current Y cursor position.
func getForegroundColor(tb: var TerminalBuffer): ForegroundColor {....raises: [],
    tags: [], forbids: [].}
Returns the current foreground color.
proc getKey(): Key {....raises: [IllwillError], tags: [], forbids: [].}

Reads the next keystroke in a non-blocking manner. If there are no keypress events in the buffer, Key.None is returned.

If a mouse event was captured, Key.Mouse is returned. Call getMouse() to get the details about the event.

If the module is not intialised, IllwillError is raised.

proc getKeyWithTimeout(ms = 1000): Key {....raises: [IllwillError], tags: [],
    forbids: [].}

Reads the next keystroke with a timeout. If there were no keypress events in the specified ms period, Key.None is returned.

If a mouse event was captured, Key.Mouse is returned. Call getMouse() to get the details about the event.

If the module is not intialised, IllwillError is raised.

proc getMouse(): MouseInfo {....raises: [], tags: [], forbids: [].}

When the library is initialised with illwillInit(mouse=true), mouse events are captured and can be retrieved by calling this function.

See MouseInfo for further details.

Example:

import illwill, os

proc exitProc() {.noconv.} =
  illwillDeinit()
  showCursor()
  quit(0)

setControlCHook(exitProc)
illwillInit(mouse=true)

var tb = newTerminalBuffer(terminalWidth(), terminalHeight())

while true:
  var key = getKey()
  if key == Key.Mouse:
    echo getMouse()
  tb.display()
  sleep(10)
func getStyle(tb: var TerminalBuffer): set[Style] {....raises: [], tags: [],
    forbids: [].}
Returns the current style flags.
proc hasDoubleBuffering(): bool {....raises: [IllwillError], tags: [], forbids: [].}

Returns true if double buffering is enabled.

If the module is not intialised, IllwillError is raised.

func height(bb: BoxBuffer): Natural {....raises: [], tags: [], forbids: [].}
Returns the height of the box buffer.
func height(tb: TerminalBuffer): Natural {....raises: [], tags: [], forbids: [].}
Returns the height of the terminal buffer.
proc illwillDeinit() {....raises: [IllwillError, IOError],
                       tags: [ReadEnvEffect, WriteIOEffect], forbids: [].}

Resets the terminal to its previous state. Needs to be called before exiting the application.

If the module is not intialised, IllwillError is raised.

proc illwillInit(fullScreen: bool = true; mouse: bool = false) {.
    ...raises: [IllwillError, IOError], tags: [ReadEnvEffect, WriteIOEffect],
    forbids: [].}

Initializes the terminal and enables non-blocking keyboard input. Needs to be called before doing anything with the library.

If mouse is set to true, mouse events are captured and can be retrieved with getMouse().

If the module is already intialised, IllwillError is raised.

proc newBoxBuffer(width, height: Natural): BoxBuffer {....raises: [], tags: [],
    forbids: [].}
Creates a new box buffer of a fixed width and height.
proc newBoxBufferFrom(src: BoxBuffer): BoxBuffer {....raises: [], tags: [],
    forbids: [].}
Creates a new box buffer with the dimensions of the src buffer and copies its contents into the new buffer.
proc newTerminalBuffer(width, height: Natural): TerminalBuffer {....raises: [],
    tags: [], forbids: [].}
Creates a new terminal buffer of a fixed width and height.
proc newTerminalBufferFrom(src: TerminalBuffer): TerminalBuffer {....raises: [],
    tags: [], forbids: [].}
Creates a new terminal buffer with the dimensions of the src buffer and copies its contents into the new buffer.
proc resetAttributes(tb: var TerminalBuffer) {....raises: [], tags: [], forbids: [].}
Resets the current text attributes to bgNone, fgWhite and clears all style flags.
proc setBackgroundColor(tb: var TerminalBuffer; bg: BackgroundColor) {.
    ...raises: [], tags: [], forbids: [].}
Sets the current background color.
proc setCursorPos(tb: var TerminalBuffer; x, y: Natural) {....raises: [], tags: [],
    forbids: [].}
Sets the current cursor position.
proc setCursorXPos(tb: var TerminalBuffer; x: Natural) {....raises: [], tags: [],
    forbids: [].}
Sets the current X cursor position.
proc setCursorYPos(tb: var TerminalBuffer; y: Natural) {....raises: [], tags: [],
    forbids: [].}
Sets the current Y cursor position.
proc setDoubleBuffering(enabled: bool) {....raises: [], tags: [], forbids: [].}
Enables or disables double buffering (enabled by default).
proc setForegroundColor(tb: var TerminalBuffer; fg: ForegroundColor;
                        bright: bool = false) {....raises: [], tags: [],
    forbids: [].}
Sets the current foreground color and the bright style flag.
proc setStyle(tb: var TerminalBuffer; style: set[Style]) {....raises: [], tags: [],
    forbids: [].}
Sets the current style flags.
func width(bb: BoxBuffer): Natural {....raises: [], tags: [], forbids: [].}
Returns the width of the box buffer.
func width(tb: TerminalBuffer): Natural {....raises: [], tags: [], forbids: [].}
Returns the width of the terminal buffer.
proc write(tb: var TerminalBuffer; bb: var BoxBuffer) {....raises: [], tags: [],
    forbids: [].}
Writes the contents of the box buffer into this terminal buffer with the current text attributes.
proc write(tb: var TerminalBuffer; s: string) {....raises: [], tags: [],
    forbids: [].}
Writes s into the terminal buffer at the current cursor position using the current text attributes.
proc write(tb: var TerminalBuffer; x, y: Natural; s: string) {....raises: [],
    tags: [], forbids: [].}
Writes s into the terminal buffer at the specified position using the current text attributes. Lines do not wrap and attempting to write outside the extents of the buffer will not raise an error; the output will be just cropped to the extents of the buffer.

Macros

macro write(tb: var TerminalBuffer; args: varargs[typed]): untyped

Special version of write that allows to intersperse text literals with set attribute commands.

Example:

import illwill

illwillInit(fullscreen=false)

var tb = newTerminalBuffer(terminalWidth(), terminalHeight())

tb.setForegroundColor(fgGreen)
tb.setBackgroundColor(bgBlue)
tb.write(0, 10, "before")

tb.write(0, 11, "unchanged", resetStyle, fgYellow, "yellow", bgRed, "red bg",
                styleBlink, "blink", resetStyle, "reset")

tb.write(0, 12, "after")

tb.display()

illwillDeinit()

This will output the following:

  • 1st row:
    • before with blue background, green foreground and default style
  • 2nd row:
    • unchanged with blue background, green foreground and default style
    • yellow with default background, yellow foreground and default style
    • red bg with red background, yellow foreground and default style
    • blink with red background, yellow foreground and blink style (if supported by the terminal)
    • reset with the default background and foreground and default style
  • 3rd row:
    • after with the default background and foreground and default style