Sign in

    tui

    Terminal UI library for MoonBit with reactive signals

    tui
    terminal
    ui
    cli
    Download zip
    Author
    Version
    0.1.1
    License
    MIT
    Last updated
    9 months ago
    Downloads
    70K

    #mizchi/tui

    Terminal UI library for MoonBit with reactive signals integration.

    #Features

    • ANSI rendering with diff-based updates
    • Reactive signals integration (mizchi/signals)
    • Component-based architecture (box, text, input, textarea, button)
    • Keyboard and mouse input handling
    • Layout powered by mizchi/crater

    #Installation

    { "deps": { "mizchi/tui": "0.1.0" } }

    #Quick Start

    fn main {
    let count = @signals.signal(0)

    let app = @tui.App::new(80, 24)
    app.create_render_effect(
    fn() {
    @tui.box([
    @tui.text("Count: " + count.get().to_string())
    ])
    },
    fn(output) { println(output) }
    )
    }

    #Examples

    • examples/counter - Simple counter with keyboard input
    • examples/chat - Chat-like interface
    • examples/form - Form with input fields
    • examples/button - Button states and interactions
    • examples/layout - Layout examples

    Run examples:
    moon run examples/counter

    #License

    MIT

    App

    pub struct App {
    width : Int
    height : Int
    prev_buffer : CharBuffer?
    last_layout :
    Layout
    ?
    handlers : EventHandlers
    }

    App manages the render loop and diff-based updates

    App::clear_prev_buffer

    fn App::clear_prev_buffer(self : App) -> Unit

    Clear the previous buffer to force a full re-render on next render call

    App::create_render_effect

    fn App::create_render_effect(self : App, render_fn : () -> Component, output_fn : (String) -> Unit) -> (() -> Unit)

    Create a reactive effect that re-renders when signals change Returns a dispose function

    App::find_by_id

    fn App::find_by_id(self : App, id : String) -> HitTestResult?

    Find component by ID

    App::handle_mouse

    fn App::handle_mouse(self : App, event : MouseEvent) -> Bool

    Handle a mouse event Returns true if an event handler was invoked

    App::hit_test

    fn App::hit_test(self : App, x : Int, y : Int) -> HitTestResult?

    Perform hit test at coordinates

    App::init_terminal

    fn App::init_terminal() -> String

    Initialize terminal (enter alt screen, hide cursor)

    App::new

    fn App::new(width : Int, height : Int) -> App

    App::on_click

    fn App::on_click(self : App, id : String, handler : () -> Unit) -> Unit

    Register a click handler for a component ID

    App::render

    fn App::render(self : App, component : Component) -> String

    Render a component tree to ANSI string

    App::render_frame

    fn App::render_frame(self : App, component : Component) -> String

    Render with cursor hidden and positioned at origin

    App::restore_terminal

    fn App::restore_terminal() -> String

    Restore terminal (show cursor, leave alt screen)

    BorderChars

    pub(all) struct BorderChars {
    top_left : Char
    top_right : Char
    bottom_left : Char
    bottom_right : Char
    horizontal : Char
    vertical : Char
    }

    Border characters for box drawing

    BorderChars::ascii

    fn BorderChars::ascii() -> BorderChars

    BorderChars::double

    fn BorderChars::double() -> BorderChars

    BorderChars::rounded

    fn BorderChars::rounded() -> BorderChars

    BorderChars::single

    fn BorderChars::single() -> BorderChars

    CharBuffer

    pub struct CharBuffer {
    width : Int
    height : Int
    cells : Array[CharCell]
    }

    Character-based framebuffer

    CharBuffer::clear

    fn CharBuffer::clear(self : CharBuffer) -> Unit

    CharBuffer::clone

    fn CharBuffer::clone(self : CharBuffer) -> CharBuffer

    Clone the buffer

    CharBuffer::fill_bg

    fn CharBuffer::fill_bg(self : CharBuffer, x : Int, y : Int, w : Int, h : Int, bg : Color) -> Unit

    CharBuffer::fill_rect

    fn CharBuffer::fill_rect(self : CharBuffer, x : Int, y : Int, w : Int, h : Int, cell : CharCell) -> Unit

    CharBuffer::get_cell

    fn CharBuffer::get_cell(self : CharBuffer, x : Int, y : Int) -> CharCell

    CharBuffer::new

    fn CharBuffer::new(width : Int, height : Int) -> CharBuffer

    CharBuffer::set_cell

    fn CharBuffer::set_cell(self : CharBuffer, x : Int, y : Int, cell : CharCell) -> Unit

    CharBuffer::write_text

    fn CharBuffer::write_text(self : CharBuffer, x : Int, y : Int, text : String, style : TextStyle, max_width : Int) -> Int

    CharBuffer::write_text_wrapped

    fn CharBuffer::write_text_wrapped(self : CharBuffer, x : Int, y : Int, text : String, style : TextStyle, max_width : Int, max_height : Int) -> Int

    CharCell

    pub(all) struct CharCell {
    char : Char
    fg : Color
    bg : Color
    bold : Bool
    underline : Bool
    reverse : Bool
    }

    A single character cell in the terminal
    impl Eq for CharCell

    CharCell::default

    fn CharCell::default() -> CharCell

    CharCell::from_char

    fn CharCell::from_char(c : Char) -> CharCell

    CharCell::styled

    fn CharCell::styled(c : Char, fg : Color, bg : Color) -> CharCell

    ClickHandler

    pub type ClickHandler () -> Unit

    Click handler type

    ClickHandler::inner

    #deprecated("Use `struct T(A)` to declare a newtype and use `.0` access the underlying type instead.")
    fn ClickHandler::inner(self : ClickHandler) -> (() -> Unit)
    Convert newtype to its underlying type, automatically derived.

    Color

    pub(all) struct Color {
    r : Int
    g : Int
    b : Int
    a : Double
    }

    TUI Color type (RGBA)
    impl Eq for Color

    Color::black

    fn Color::black() -> Color

    Color::blue

    fn Color::blue() -> Color

    Color::cyan

    fn Color::cyan() -> Color

    Color::from_crater

    Convert from crater Color

    Color::green

    fn Color::green() -> Color

    Color::is_transparent

    fn Color::is_transparent(self : Color) -> Bool

    Color::magenta

    fn Color::magenta() -> Color

    Color::red

    fn Color::red() -> Color

    Color::rgb

    fn Color::rgb(r : Int, g : Int, b : Int) -> Color

    Color::rgba

    fn Color::rgba(r : Int, g : Int, b : Int, a : Double) -> Color

    Color::transparent

    fn Color::transparent() -> Color

    Color::white

    fn Color::white() -> Color

    Color::yellow

    fn Color::yellow() -> Color

    Component

    pub struct Component {
    node :
    Node

    styles : RenderStyleMap
    texts : TextContentMap
    }

    Component represents a renderable UI element with its layout, style, and content

    EventHandlers

    pub struct EventHandlers {
    click_handlers : Map[String, ClickHandler]
    }

    Event handlers map (component ID -> handler)

    EventHandlers::get_click

    fn EventHandlers::get_click(self : EventHandlers, id : String) -> ClickHandler?

    Get click handler for a component

    EventHandlers::new

    EventHandlers::on_click

    fn EventHandlers::on_click(self : EventHandlers, id : String, handler : ClickHandler) -> Unit

    Register a click handler for a component

    FocusManager

    pub struct FocusManager {
    focus : FocusState
    mode : InputMode
    focusable_ids : Array[String]
    }

    Focus manager for handling focus state and navigation

    FocusManager::blur

    fn FocusManager::blur(self : FocusManager) -> Unit

    Remove focus (blur)

    FocusManager::focus

    fn FocusManager::focus(self : FocusManager, id : String) -> Unit

    Set focus to a specific component

    FocusManager::focus_next

    fn FocusManager::focus_next(self : FocusManager) -> Unit

    Focus the next component in the focusable list (Tab navigation)

    FocusManager::focus_prev

    fn FocusManager::focus_prev(self : FocusManager) -> Unit

    Focus the previous component in the focusable list (Shift+Tab navigation)

    FocusManager::get_focused

    fn FocusManager::get_focused(self : FocusManager) -> String?

    Get the currently focused component ID

    FocusManager::get_mode

    fn FocusManager::get_mode(self : FocusManager) -> InputMode

    Get current input mode

    FocusManager::is_editing

    fn FocusManager::is_editing(self : FocusManager) -> Bool

    Check if currently in editing mode

    FocusManager::is_focused

    fn FocusManager::is_focused(self : FocusManager, id : String) -> Bool

    Check if a component is currently focused

    FocusManager::new

    Create a new focus manager

    FocusManager::register

    fn FocusManager::register(self : FocusManager, id : String) -> Unit

    Register a focusable component ID

    FocusManager::start_editing

    fn FocusManager::start_editing(self : FocusManager) -> Unit

    Enter editing mode

    FocusManager::stop_editing

    fn FocusManager::stop_editing(self : FocusManager) -> Unit

    Exit editing mode

    FocusManager::unregister

    fn FocusManager::unregister(self : FocusManager, id : String) -> Unit

    Unregister a focusable component ID

    FocusState

    pub(all) enum FocusState {
    None
    Focused(String)
    }

    Focus state for the application

    HitTestResult

    pub(all) struct HitTestResult {
    id : String
    x : Int
    y : Int
    width : Int
    height : Int
    }

    Hit test result

    InputEvent

    pub(all) enum InputEvent {
    Key(KeyEvent)
    Mouse(MouseEvent)
    Resize(Int, Int)
    Unknown(String)
    }

    Input event
    impl Show for InputEvent

    InputMode

    pub(all) enum InputMode {
    Normal
    Editing
    }

    Input mode for the application

    InputResult

    pub(all) enum InputResult {
    Confirmed(String)
    Cancelled
    TabNext(String)
    TabPrev(String)
    }

    Result of an input session

    KeyEvent

    pub(all) enum KeyEvent {
    Char(Char, KeyModifier)
    Special(SpecialKey, KeyModifier)
    }

    Keyboard event
    impl Show for KeyEvent

    KeyModifier

    pub(all) enum KeyModifier {
    None
    Shift
    Ctrl
    Alt
    CtrlShift
    CtrlAlt
    ShiftAlt
    CtrlShiftAlt
    }

    Key modifiers
    impl Show for KeyModifier

    MouseButton

    pub(all) enum MouseButton {
    Left
    Middle
    Right
    ScrollUp
    ScrollDown
    None
    }

    Mouse button
    impl Show for MouseButton

    MouseEvent

    pub(all) struct MouseEvent {
    button : MouseButton
    event_type : MouseEventType
    x : Int
    y : Int
    modifier : KeyModifier
    }

    Mouse event
    impl Show for MouseEvent

    MouseEventType

    pub(all) enum MouseEventType {
    Press
    Release
    Move
    Drag
    }

    Mouse event type

    RenderStyle

    pub(all) struct RenderStyle {
    fg : Color
    bg : Color
    bold : Bool
    underline : Bool
    border : BorderChars?
    border_fg : Color
    }

    Rendering options for a node

    RenderStyle::default

    fn RenderStyle::default() -> RenderStyle

    RenderStyleMap

    pub type RenderStyleMap Map[String, RenderStyle]

    Map from node ID to render style

    RenderStyleMap::get

    fn RenderStyleMap::get(self : RenderStyleMap, id : String) -> RenderStyle

    RenderStyleMap::inner

    #deprecated("Use `struct T(A)` to declare a newtype and use `.0` access the underlying type instead.")
    fn RenderStyleMap::inner(self : RenderStyleMap) -> Map[String, RenderStyle]
    Convert newtype to its underlying type, automatically derived.

    RenderStyleMap::new

    RenderStyleMap::set

    fn RenderStyleMap::set(self : RenderStyleMap, id : String, style : RenderStyle) -> Unit

    SpecialKey

    pub(all) enum SpecialKey {
    Up
    Down
    Left
    Right
    Home
    End
    PageUp
    PageDown
    Insert
    Delete
    Tab
    BackTab
    Backspace
    Enter
    Escape
    F(Int)
    }

    Special keys
    impl Show for SpecialKey

    TextContentMap

    pub type TextContentMap Map[String, String]

    Map from node ID to text content

    TextContentMap::get

    fn TextContentMap::get(self : TextContentMap, id : String) -> String?

    TextContentMap::inner

    #deprecated("Use `struct T(A)` to declare a newtype and use `.0` access the underlying type instead.")
    fn TextContentMap::inner(self : TextContentMap) -> Map[String, String]
    Convert newtype to its underlying type, automatically derived.

    TextContentMap::new

    TextContentMap::set

    fn TextContentMap::set(self : TextContentMap, id : String, text : String) -> Unit

    TextStyle

    pub(all) struct TextStyle {
    fg : Color
    bg : Color
    bold : Bool
    underline : Bool
    }

    Text style for writing text

    TextStyle::default

    fn TextStyle::default() -> TextStyle

    abort_input_session

    fn abort_input_session() -> Unit

    Abort the current input session

    ansi_bg_256

    fn ansi_bg_256(color_idx : Int) -> String

    ansi_bg_color

    fn ansi_bg_color(color : Color) -> String

    ansi_bg_reset

    fn ansi_bg_reset() -> String

    Reset background color to default

    ansi_bold

    fn ansi_bold() -> String

    ansi_clear_line

    fn ansi_clear_line() -> String

    ansi_clear_screen

    fn ansi_clear_screen() -> String

    ansi_enter_alt_screen

    fn ansi_enter_alt_screen() -> String

    ansi_fg_256

    fn ansi_fg_256(color_idx : Int) -> String

    ansi_fg_color

    fn ansi_fg_color(color : Color) -> String

    ansi_full_reset

    fn ansi_full_reset() -> String

    Full terminal reset sequence for recovering from corrupted state

    ansi_hide_cursor

    fn ansi_hide_cursor() -> String

    ansi_leave_alt_screen

    fn ansi_leave_alt_screen() -> String

    ansi_move_to

    fn ansi_move_to(row : Int, col : Int) -> String

    ansi_reset

    fn ansi_reset() -> String

    ansi_reset_attrs

    fn ansi_reset_attrs() -> String

    Reset all terminal attributes (colors, styles)

    ansi_reverse

    fn ansi_reverse() -> String

    ansi_show_cursor

    fn ansi_show_cursor() -> String

    ansi_underline

    fn ansi_underline() -> String

    box

    fn box(children : Array[Component], id? : String, width? :
    Dimension
    , height? :
    Dimension
    , min_width? :
    Dimension
    , min_height? :
    Dimension
    , max_width? :
    Dimension
    , max_height? :
    Dimension
    , flex_grow? : Double, flex_shrink? : Double, flex_basis? :
    Dimension
    , flex_direction? :
    FlexDirection
    , flex_wrap? :
    FlexWrap
    , justify_content? :
    Alignment
    , align_items? :
    Alignment
    , align_content? :
    Alignment
    , gap? : Double, padding? : Double, padding_x? : Double, padding_y? : Double, margin? : Double, margin_x? : Double, margin_y? : Double, border? : BorderChars?, border_color? : Color, bg? : Color, fg? : Color) -> Component

    Create a box component

    buffer_diff_to_ansi

    fn buffer_diff_to_ansi(current : CharBuffer, prev : CharBuffer) -> String

    Optimized ANSI output with diff (only output changed cells)

    buffer_to_ansi

    fn buffer_to_ansi(buf : CharBuffer) -> String

    Convert CharBuffer to ANSI-encoded string

    char_display_width

    fn char_display_width(c : Char) -> Int

    cleanup_stdin

    fn cleanup_stdin() -> Unit

    Cleanup stdin state

    color_to_256

    fn color_to_256(color : Color) -> Int

    compute_layout

    fn compute_layout(node :
    Node
    , width : Int, height : Int) ->
    Layout

    Compute layout using crater engine with character-based viewport

    debug_stderr

    fn debug_stderr(s : String) -> Unit

    Debug print to stderr (public)

    debug_stdin_state

    fn debug_stdin_state(label : String) -> Unit

    Debug stdin state (public)

    disable_mouse

    fn disable_mouse() -> String

    dump_layout

    fn dump_layout(layout :
    Layout
    , offset_x : Int, offset_y : Int, depth : Int) -> String

    Debug: dump layout tree structure

    enable_mouse

    fn enable_mouse() -> String

    enable_raw_mode

    fn enable_raw_mode() -> Unit

    Enable raw mode

    find_layout_by_id

    fn find_layout_by_id(layout :
    Layout
    , target_id : String, offset_x : Int, offset_y : Int) -> HitTestResult?

    Find layout by ID Returns the position and size of the component with the given ID

    find_layout_by_id_root

    fn find_layout_by_id_root(layout :
    Layout
    , target_id : String) -> HitTestResult?

    Find layout by ID from root

    get_terminal_size

    fn get_terminal_size() -> (Int, Int)

    Get terminal size (columns, rows)

    handle_click

    fn handle_click(layout :
    Layout
    , handlers : EventHandlers, mouse_x : Int, mouse_y : Int) -> Bool

    Handle mouse click event Returns true if a handler was invoked

    hit_test

    fn hit_test(layout :
    Layout
    , mouse_x : Int, mouse_y : Int, offset_x : Int, offset_y : Int) -> HitTestResult?

    Perform hit test on layout tree Returns the deepest (most specific) node containing the point

    hit_test_root

    fn hit_test_root(layout :
    Layout
    , mouse_x : Int, mouse_y : Int) -> HitTestResult?

    Hit test from root (convenience function)

    is_raw_mode

    fn is_raw_mode() -> Bool

    is_tty

    fn is_tty() -> Bool

    Check if stdin is a TTY (always true in JS terminal context)

    keep_alive_nonblocking

    fn keep_alive_nonblocking(ms : Int) -> Unit

    Keep process alive without blocking the event loop

    key_is_char

    fn key_is_char(event : KeyEvent, c : Char) -> Bool

    Check if key matches a specific character with optional modifier

    key_is_ctrl_c

    fn key_is_ctrl_c(event : KeyEvent) -> Bool

    Check if key is Ctrl+C

    key_is_ctrl_q

    fn key_is_ctrl_q(event : KeyEvent) -> Bool

    Check if key is Ctrl+Q (common quit)

    key_is_special

    fn key_is_special(event : KeyEvent, special : SpecialKey) -> Bool

    Check if key matches a special key

    parse_input

    fn parse_input(input : String) -> InputEvent

    Parse escape sequence

    poll_keypress

    fn poll_keypress() -> Bool

    Poll for keypress (not applicable in JS - use start_keypress_listener instead) Returns false as JS uses event-driven model
    fn print_raw(s : String) -> Unit

    Print string to stdout (no newline)

    read_key

    async fn read_key() -> String

    Read a key asynchronously

    render_layout

    fn render_layout(buf : CharBuffer, layout :
    Layout
    , styles : RenderStyleMap, texts : TextContentMap, offset_x : Int, offset_y : Int) -> Unit

    Render Layout to CharBuffer

    render_once

    fn render_once(width : Int, height : Int, component : Component) -> String

    Simple render function for one-shot rendering (no reactivity)

    render_to_ansi

    fn render_to_ansi(layout :
    Layout
    , styles : RenderStyleMap, texts : TextContentMap, width : Int, height : Int) -> String

    Quick render function: Layout -> ANSI string

    rgb_to_256

    fn rgb_to_256(r : Int, g : Int, b : Int) -> Int

    Convert RGB color to 256-color palette index

    run_app

    fn run_app(width : Int, height : Int, render_fn : () -> Component, output_fn : (String) -> Unit) -> (() -> Unit)

    Full-screen app helper: sets up alt screen and handles cleanup

    sleep

    async fn sleep(ms : Int) -> Unit

    Sleep for milliseconds

    spacer

    fn spacer(flex_grow? : Double) -> Component

    Spacer component - flexible empty space

    start_inline_input

    fn start_inline_input(label : String, initial : String, row : Int, col : Int, max_width : Int, max_height : Int, on_result : (InputResult) -> Unit) -> Unit

    Start an inline input session (for editing in place) row/col are 1-indexed ANSI coordinates max_width/max_height define the input area for text wrapping

    start_inline_input_cooked

    fn start_inline_input_cooked(field_name : String, initial : String, on_result : (InputResult) -> Unit) -> Unit

    Start an inline input session with cooked mode (for IME support) Temporarily exits alternate screen for proper IME input field_name: Display name for the field being edited initial: Current value to pre-fill

    start_inplace_input

    fn start_inplace_input(row : Int, col : Int, width : Int, height : Int, multiline : Bool, initial : String, on_result : (InputResult) -> Unit, on_lines_change? : (Int) -> Int?) -> Unit

    Start an inplace input session with cooked mode (IME enabled) Stays in alternate screen, edits at specified position row/col are 1-indexed ANSI coordinates width is the max display width for the input

    start_input_session

    async fn start_input_session(x : Int, y : Int, initial : String) -> InputResult

    Start an input session (async version - may not work with raw mode) WARNING: This uses Promise::wait which can block the Node.js event loop Use start_input_session_cb instead for reliable operation

    start_input_session_cb

    fn start_input_session_cb(initial : String, on_result : (InputResult) -> Unit) -> Unit

    Start an input session (callback-based, non-blocking) This is the recommended way to read input - it doesn't block the event loop

    start_input_session_promise

    fn start_input_session_promise(initial : String) ->
    Promise
    [InputResult]

    Start an input session that returns a Promise (for async/await) Use with @js_async.Promise::wait() in async functions

    start_keypress_listener

    fn start_keypress_listener(callback : (String) -> Unit) -> Unit

    Start listening for keypresses (event-driven)

    start_render_ticker

    fn start_render_ticker(ms : Int, callback : () -> Unit) -> Int

    Start a render ticker that calls the callback every ms milliseconds

    stop_keypress_listener

    fn stop_keypress_listener() -> Unit

    Stop listening for keypresses

    stop_render_ticker

    fn stop_render_ticker(ticker_id : Int) -> Unit

    Stop a render ticker

    string_display_width

    fn string_display_width(s : String) -> Int

    Calculate display width of a string

    styled_text

    fn styled_text(content : String, fg? : Color, bg? : Color, bold? : Bool, underline? : Bool) -> Component

    Create a styled text (alias for text with labeled arguments)

    text

    fn text(content : String, fg? : Color, bg? : Color, bold? : Bool, underline? : Bool) -> Component

    Create a text component

    text_box

    fn text_box(content : String, id? : String, width? :
    Dimension
    , height? :
    Dimension
    , min_width? :
    Dimension
    , min_height? :
    Dimension
    , max_width? :
    Dimension
    , max_height? :
    Dimension
    , flex_grow? : Double, flex_shrink? : Double, flex_basis? :
    Dimension
    , padding? : Double, padding_x? : Double, padding_y? : Double, margin? : Double, margin_x? : Double, margin_y? : Double, border? : BorderChars?, border_color? : Color, bg? : Color, fg? : Color, bold? : Bool, underline? : Bool) -> Component

    Create a box with text content that wraps within the box's bounds Unlike text(), this renders the text directly within the box's layout dimensions

    text_measure_func

    fn text_measure_func(text : String) ->
    MeasureFunc

    Create a MeasureFunc for text that uses character width