illwill

repository·master·Indexed 17 days ago

https://github.com/johnnovak/illwill

A lightweight, pure-Nim terminal library inspired by ncurses for building cross-platform, full-screen text-mode applications. It features non-blocking input via getKey(), double-buffered virtual buffers to minimize flicker and CPU usage, and support for UTF-8 box-drawing symbols. Designed for immediate-mode UI patterns without external dependencies or a terminfo database.

Tokens
1.5K
Snippets
5
Records
6
Agent score
17%

What's inside illwill

  1. Overview of illwill features and use cases

    master

    What is illwill?

    illwill is a lightweight, (n)curses-inspired terminal library written in pure Nim. It is designed for creating cross-platform text-mode applications without external dependencies or a terminfo database.

    Key Features

    • Non-blocking input: Use getKey() to poll for keyboard events without halting execution.
    • Virtual Buffers: Supports double-buffering to minimize terminal updates and CPU usage.
    • Graphics: Simple UTF-8 box-drawing symbols for UI elements.
    • Input Support: Handles key combinations, special keys, and mouse input with modifier reporting.
    • Fullscreen: Full-screen mode with terminal state restoration (Note: restoration may not work on Windows).

    When to use it

    • You want a simple, dependency-free way to write full-screen terminal apps.
    • You are building a custom UI from scratch using an immediate mode approach.
    • You only need to support UTF-8 encodings.

    When NOT to use it

    • You need highly robust support for obscure terminals or character encodings.
    • You require a library with predefined widgets (e.g., buttons, sliders).
    • You need perfect terminal state restoration on Windows.
  2. How to structure a fullscreen terminal application with illwill

    master

    A typical illwill application follows an immediate-mode UI pattern. The lifecycle involves:

    1. Initialization: Call illwillInit(fullscreen=true) to enter fullscreen mode. Use hideCursor() to clean up the UI.
    2. Cleanup Setup: Register a cleanup procedure (e.g., using setControlCHook) that calls illwillDeinit() and showCursor() to restore the terminal state upon exit.
    3. Buffer Management: Create a TerminalBuffer using newTerminalBuffer(terminalWidth(), terminalHeight()). All drawing operations should be performed on this virtual buffer.
    4. Event Loop:
      • Poll for input using getKey().
      • Update the buffer contents based on input or application state.
      • Call tb.display() to render the buffer to the actual terminal. illwill uses double-buffering by default to minimize CPU usage and terminal flicker by only printing changes.
    import os, strutils
    import illwill
    
    # 1. Setup cleanup
    proc exitProc() {.noconv.} =
      illwillDeinit()
      showCursor()
      quit(0)
    
    illwillInit(fullscreen=true)
    setControlCHook(exitProc)
    hideCursor()
    
    # 2. Create buffer
    var tb = newTerminalBuffer(terminalWidth(), terminalHeight())
    
    # 3. Initial draw
    tb.write(2, 1, fgWhite, "Hello World")
    
    # 4. Main loop
    while true:
      var key = getKey()
      case key
      of Key.None: discard
      of Key.Escape, Key.Q: exitProc()
      else:
        tb.write(8, 4, "Key pressed: ", fgGreen, $key)
    
      tb.display()
      sleep(20)
  3. Initialize and deinitialize the terminal

    master

    To use illwill, you must initialize the terminal state. For fullscreen applications, use illwillInit(fullscreen=true). It is critical to restore the terminal state (e.g., showing the cursor and deinitializing the library) when the application exits, typically by using a cleanup procedure or a control-C hook via setControlCHook.

    import illwill
    
    proc exitProc() {.noconv.} =
      illwillDeinit()
      showCursor()
      quit(0)
    
    illwillInit(fullscreen=true)
    setControlCHook(exitProc)
    hideCursor()
  4. Implement a main event loop with key polling

    master

    A typical illwill application uses a while true loop to poll for user input using getKey(). The loop handles input events (like Key.Escape or Key.Q to quit), modifies the terminal buffer based on that input, and then calls tb.display() to update the screen.

    while true:
      var key = getKey()
      case key
      of Key.None: 
        discard
      of Key.Escape, Key.Q: 
        exitProc()
      else:
        # Modify buffer based on input
        tb.write(2, 4, resetStyle, "Key pressed: ", fgGreen, $key)
        # Render changes
        tb.display()
    
      sleep(20)
  5. Create and manipulate a terminal buffer

    master

    The TerminalBuffer is used to construct the next frame to be displayed. You can create a new buffer using newTerminalBuffer(width, height), where width and height are typically obtained via terminalWidth() and terminalHeight().

    Common buffer operations include:

    • setForegroundColor(color, style): Sets the foreground color.
    • drawRect(x, y, width, height): Draws a rectangle.
    • drawHorizLine(x, endX, y, doubleStyle=false): Draws a horizontal line.
    • write(x, y, ...): Writes text with various color/style arguments.
    • display(): Instructs the library to render the buffer contents to the actual terminal. Note that double buffering is enabled by default, so only differences from the previous frame are printed.
    var tb = newTerminalBuffer(terminalWidth(), terminalHeight())
    
    # Drawing examples
    tb.setForegroundColor(fgBlack, true)
    tb.drawRect(0, 0, 40, 5)
    tb.drawHorizLine(2, 38, 3, doubleStyle=true)
    tb.write(2, 1, fgWhite, "Text content")
    
    # Render to terminal
    tb.display()