Skip to content

tim5wang/min-tui

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

50 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

min-tui

Go Reference

A lightweight terminal UI library in pure Go, purpose-built for coding agents.
Streaming output, multi-line input, markdown with syntax highlighting, slash commands, popups — ~2,400 lines, zero heavy deps.

min-tui demo screenshot

go get github.com/tim5wang/min-tui

Features

  • Streaming outputWrite() renders incrementally; overflow enters terminal scrollback via a temporary scroll region
  • Multi-line inputShift+Enter / Ctrl+J newlines; input box expands to MaxInputRows (default 8)
  • Slash commands/name filtered dropdown; arrow keys navigate, Enter selects, Esc cancels
  • Multi-turn interactionPrompt() for text, Select() for menus, written like sync code
  • Popup windows — overlay dialogs with focus switching (Tab), interactive OnKey, custom colors
  • Markdown — headings, bold, italic, inline code, fenced code blocks (with syntax highlighting), aligned tables, blockquotes
  • Status bar — 5 styles (default / info / warning / error / success)
  • Configurable — border color, custom markdown renderer, event channel, heading marks, spacious mode, app-owned input history
  • Concurrency-safeWrite() from any goroutine while ReadLine() blocks

Quick Start

package main

import "github.com/tim5wang/min-tui"

func main() {
    tui, _ := minitui.New()
    defer tui.Close()

    tui.SetStatus("Enter to submit | / for commands", minitui.StatusInfo)

    for {
        input, err := tui.ReadLine()
        if err != nil { return }
        tui.WriteString("You said: " + input + "\n")
    }
}

Config

tui, _ := minitui.NewWithConfig(minitui.Config{
    EventCh:          myEventCh,         // optional event sink
    BorderColor:      "\x1b[36m",       // input box borders (default: dim)
    RenderLine:       myRenderer,        // custom markdown→ANSI
    MaxInputRows:     12,                // default 8
    ShowHeadingMarks: true,              // keep `## Title` visible (default: false)
    Spacious:         true,              // blank lines between blocks (default: false)
    HistoryFn:        myHistory,         // ↑/↓ on empty input (see below)
})

tui.WriteString("output\n")              // streaming output
input, err := tui.ReadLine()             // blocking (Ctrl+C → err)
tui.SetStatus("...", minitui.StatusWarning)
defer tui.Close()                        // restore terminal

Config fields

Field Default Description
EventCh nil Optional chan<- Event for EventSubmit / EventResize / EventInterrupt
BorderColor \x1b[2m (dim) ANSI escape for input box border
RenderLine built-in Custom markdown → ANSI line renderer. Receives raw line, returns styled
MaxInputRows 8 Maximum visible rows in the input box
ShowHeadingMarks false Keep # / ## marks visible on headings
Spacious false Insert blank lines before/after headings, code blocks, tables
HistoryFn nil Called on / when input is empty — see Input History

Input History

Config.HistoryFn lets the application implement command history without min-tui owning any state. The callback fires on every or press in the input box. The application owns the history list, the cursor, and the boundary semantics; min-tui only fires the callback and places the returned text into the input.

history := []string{"help", "echo", "/login"}
idx := len(history) // past the end

tui, _ := minitui.NewWithConfig(minitui.Config{
    HistoryFn: func(direction int, current string) string {
        // direction: -1 = ↑ (older), +1 = ↓ (newer)
        // current:   the input-box text right now
        // Return the next entry, or `current` unchanged to signal
        // "no more in this direction" — min-tui restores the user's
        // original draft automatically.
        if direction < 0 {
            if idx > 0 { idx--; return history[idx] }
            return current // no older — stay
        }
        if idx+1 < len(history) { idx++; return history[idx] }
        idx = len(history)
        return current // one past end — min-tui restores draft
    },
})

// In your ReadLine loop, append on submit:
input, _ := tui.ReadLine()
if input != "" { history = append(history, input); idx = len(history) }

Contract:

  • direction is -1 for (older) and +1 for (newer).
  • current is the input-box text at the moment of the keypress. Return current unchanged to signal "no more entries in this direction".
  • The callback runs on the TUI input goroutine. It must not call back into the TUI (no Write, SetStatus, ReadLine from inside it).
  • min-tui saves the user's draft on the first ↑/↓ of a session. The draft is restored automatically when the callback returns current unchanged while recall is active.
  • Editing the recalled text (typing, backspace, paste, etc.) exits recall mode and drops the draft.
  • Submitting (Enter) clears recall state.

The application owns the storage, the cursor index, de-duplication, and persistence across sessions — min-tui only manages the draft.

Slash Commands

tui.RegisterCommand(minitui.SlashCommand{
    Name: "login",
    Handler: func(ctx *minitui.CommandContext) {
        // secondary menu
        method := ctx.Select("选择方式", []minitui.SelectOption{
            {Label: "password", Description: "用户名+密码"},
            {Label: "token",    Description: "Token 认证"},
        })
        if method < 0 { return }           // Esc cancelled

        // text input
        username := ctx.Prompt("用户名")
        password := ctx.Prompt("密码")

        ctx.Write("登录成功: " + username + "\n")
        ctx.SetStatus("就绪", minitui.StatusSuccess)
    },
})

SlashCommand Fields

Field Description
Name Command identifier (e.g. "help", "login")
Description Shown in the dropdown when filtering
Handler func(ctx *CommandContext) called when the user selects the command

CommandContext Methods

Method Description
Prompt(prompt) string Block until user presses Enter, return text. If prompt is non-empty, it is shown in the status bar
Select(prompt, opts) int Show dropdown, return index (-1 = Esc). If prompt is non-empty, it is shown in the status bar
Write(s) Append output to the history area
SetStatus(text, style) Update status bar

Popup Windows

// Open a popup by registering a global key handler.
tui.SetGlobalKeyHandler(func(k minitui.KeyEvent) bool {
    if k.Ctrl && k.Rune == 'p' {
        tui.PushPopup(minitui.Popup{
            Title:  "Help",
            Width:  40, Height: 12,
            Render: func(w, h int) []string {
                return []string{"", "  Esc to close", "", "  Tab to toggle focus"}
            },
            OnKey: func(k minitui.KeyEvent) minitui.PopupAction {
                if k.Special == minitui.KeyDown { return minitui.PopupUpdate }
                return minitui.PopupPassthrough
            },
        })
        return true
    }
    return false
})

Popup Fields

Field Description
Title Window title (auto-truncated if too wide)
Width, Height Size in cells (0 = auto: 80% width, 60% height)
BorderColor ANSI color when focused, default cyan
BgColor Background when focused, default white
BorderColorUnfocus Border when unfocused, default dim cyan
BgColorUnfocus Background when unfocused, default gray
Render(w,h) Return content lines (must be ≤ h-2 lines)
OnKey Handle keys → PopupPassthrough / PopupUpdate / PopupClose
OnClose Called after popup removed

Popup Actions

Action Effect
PopupPassthrough Key not handled — input editor receives it
PopupUpdate Key handled — re-render popup and input box
PopupClose Key handled — dismiss the popup

Focus & Interaction

Key Action
Tab Toggle focus between input ↔ popup
Esc Close popup
Focused popup Bright border, keys go to OnKey
Unfocused popup Dim border, keys pass through to input editor

Key Bindings

Key Action
Enter Submit / confirm
Shift+Enter / Ctrl+J Insert newline
Ctrl+C Interrupt (returns error from ReadLine)
Esc Cancel slash / close popup
Tab Insert spaces (normal) / toggle popup focus (popup open)
Navigate dropdowns; on empty input, fires Config.HistoryFn (see Input History)
Move cursor
Home End Jump start/end of line
Ctrl+A E K U W Emacs shortcuts
Backspace / Delete Delete char

KeyEvent.Special exposes the constants KeyUp / KeyDown / KeyLeft / KeyRight / KeyHome / KeyEnd for popup handlers.

Markdown

Syntax Rendering
# Heading###### Bold (marks hidden by default; set ShowHeadingMarks: true to keep them)
**text** Bold
*text* Italic
`code` Dim inline code
``` fenced block Full-width dark background (256-color 235); with language info, syntax-highlighted
| col | col | Aligned table with header separator
> quote Gray background blockquote (no > prefix visible)

Fenced Code Highlighting

Add the language to the fence info string:

```go
func add(a, b int) int { return a + b }
```

Supported languages (zero-dep tokenizer): go, python (py), javascript (js), typescript (ts), rust (rs), bash (sh), sql.

Each language tokenizes: keywords (yellow), types (cyan), strings (green), comments (gray), numbers (purple).

Diff View

WriteDiff(diffStr, showLineNum) renders a unified diff with:

  • Line-number gutter: distinct background (238)
  • Context lines: code-area background (236) full-width
  • Add lines: green background (22) full-width
  • Delete lines: red background (52) full-width
  • Headers / hunks / meta: no background fill

Both context and +/- backgrounds extend the full terminal width for a clean block appearance.

tui.WriteDiff(`--- a/main.go
+++ b/main.go
@@ -1,4 +1,5 @@
 package main
+import "os"
-func greet() {
+func greet(name string) {
`, true)

Events

eventCh := make(chan minitui.Event, 8)
go func() {
    for ev := range eventCh {
        switch ev.Type {
        case minitui.EventSubmit:    // ev.Input holds the submitted text
        case minitui.EventResize:    // ev.Width, ev.Height
        case minitui.EventInterrupt: // Ctrl+C pressed
        }
    }
}()

Event is delivered on the goroutine that calls ReadLine. Reading is non-blocking — events are sent best-effort.

Status Bar

tui.SetStatus("Ready",     minitui.StatusDefault) // dim white
tui.SetStatus("Hint",      minitui.StatusInfo)    // cyan
tui.SetStatus("Working…",  minitui.StatusWarning) // yellow
tui.SetStatus("Failed",    minitui.StatusError)   // red
tui.SetStatus("Done",      minitui.StatusSuccess) // green

Dependencies

Only golang.org/x/term.

The built-in syntax highlighter is hand-written, ~200 lines — no chroma, no prism, no WASM.

Terminal Support

iTerm2, Terminal.app, WezTerm, Kitty, VS Code terminal.

  • Shift+Enter needs kitty/XTerm modifyOtherKeys. Use Ctrl+J as universal fallback.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages