No description
Find a file
Sky Johnson a1a7a911be Improve TUI input handling and selection reliability
- Remove duplicate focus event handling (bare ESC I/O)
- Add robust mouse event parsing with bounds validation
- Remove input buffer blocking for mouse selection
- Replace flaky ESC deselection with navigation-based clearing
- Preserve colors in selection using reverse video with ANSI reset handling
- Ignore mouse press when unfocused to prevent accidental selection on focus
- Clear selection on focus loss for visual consistency
- Add focus reporting infrastructure (enable/disable methods)
- Centralize log formatting with tag registry system
- Update mouse tracking to button-event mode (ESC[?1002h)
2025-12-27 16:58:26 -06:00
examples/mockserver Improve TUI input handling and selection reliability 2025-12-27 16:58:26 -06:00
buffer.go first commit 2025-12-27 11:34:19 -06:00
colors.go Improve TUI input handling and selection reliability 2025-12-27 16:58:26 -06:00
go.mod first commit 2025-12-27 11:34:19 -06:00
go.sum first commit 2025-12-27 11:34:19 -06:00
input.go Improve TUI input handling and selection reliability 2025-12-27 16:58:26 -06:00
README.md first commit 2025-12-27 11:34:19 -06:00
renderer.go Improve TUI input handling and selection reliability 2025-12-27 16:58:26 -06:00
terminal.go Improve TUI input handling and selection reliability 2025-12-27 16:58:26 -06:00
tui.go Improve TUI input handling and selection reliability 2025-12-27 16:58:26 -06:00

TUI

Super-lightweight TUI library for use in the server binaries. Lets us send commands to the server easily even while its running, scroll through output cleanly, et cetera. Just looks good! Built to be non-blocking and has a huge default buffer.

Installation

go get git.sharkk.net/eq2go/tui

Quick Start

package main

import (
    "log"
    "git.sharkk.net/eq2go/tui"
)

func main() {
    // Create TUI
    ui, err := tui.New(tui.Config{
        ServerName:    "MyServer",
        ServerVersion: "1.0.0",
    })
    if err != nil {
        log.Fatal(err)
    }

    // Register commands
    ui.RegisterCommand("status", "Show server status", func(t *tui.TUI) {
        t.WriteLine("Status: Running")
    })

    // Start TUI
    if err := ui.Start(); err != nil {
        log.Fatal(err)
    }
    defer ui.Stop()

    // Process commands (blocks until quit)
    ui.ProcessCommands()
}

API Reference

Creating a TUI

ui, err := tui.New(tui.Config{
    ServerName:    "MyServer",    // Required
    ServerVersion: "1.0.0",       // Optional (default: "0.0.0")
    PromptSymbol:  "➜",           // Optional (default: "➜")
})

Starting and Stopping

// Start the TUI (takes over terminal)
err := ui.Start()

// Stop the TUI (restore terminal)
err := ui.Stop()

Writing Output

// Write a line (thread-safe, non-blocking)
ui.WriteLine("Hello, world!")

// Write formatted line
ui.WriteLinef("Processed %d requests", count)

Command Registration

// Register a command
ui.RegisterCommand("name", "Description", func(t *tui.TUI) {
    t.WriteLine("Command executed!")
})

// Process commands (blocking, runs until quit)
ui.ProcessCommands()

Built-in commands (automatically registered):

  • help - Show all commands (auto-generated)
  • quit / exit - Exit the application
  • clear - Clear the screen

Colors and Formatting

// Available colors
tui.Black, tui.Red, tui.Green, tui.Yellow
tui.Blue, tui.Magenta, tui.Cyan, tui.White

// Background colors
tui.BgBlack, tui.BgRed, tui.BgGreen, tui.BgYellow
tui.BgBlue, tui.BgMagenta, tui.BgCyan, tui.BgWhite

// Styles
tui.Bold, tui.Reset

// Colorize text
colored := tui.Colorize("Error!", tui.Red, tui.Bold)
ui.WriteLine(colored)

Utility Functions

// Clear the output buffer
ui.ClearBuffer()

// Update header at runtime
ui.SetHeader("NewName", "2.0.0")

Complete Example

See examples/mockserver/main.go for a full working example.

package main

import (
    "fmt"
    "log"
    "time"
    "git.sharkk.net/eq2go/tui"
)

func main() {
    ui, _ := tui.New(tui.Config{
        ServerName:    "ExampleServer",
        ServerVersion: "1.0.0",
    })

    // Register custom commands
    ui.RegisterCommand("stats", "Show statistics", func(t *tui.TUI) {
        t.WriteLine(tui.Colorize("Statistics:", tui.Cyan, tui.Bold))
        t.WriteLinef("Uptime: %s", time.Since(startTime))
        t.WriteLinef("Requests: %d", requestCount)
    })

    ui.RegisterCommand("debug", "Toggle debug mode", func(t *tui.TUI) {
        debugMode = !debugMode
        t.WriteLinef("Debug mode: %v", debugMode)
    })

    ui.Start()
    defer ui.Stop()

    // Background logging
    go func() {
        ticker := time.NewTicker(5 * time.Second)
        for range ticker.C {
            ui.WriteLine(formatLog("INFO", tui.Green, "Heartbeat"))
        }
    }()

    ui.ProcessCommands()
}

func formatLog(level string, color tui.Color, msg string) string {
    ts := time.Now().Format("15:04:05")
    tag := tui.Colorize(fmt.Sprintf("[%-7s]", level), color)
    return fmt.Sprintf("%s %s %s", ts, tag, msg)
}

User Interface

ServerName vVersion ================================
14:32:45 [INFO   ] Server started successfully
14:32:46 [INFO   ] Listening on 0.0.0.0:8080
14:32:47 [WARNING] Cache miss detected
14:32:48 [INFO   ] Request completed in 42ms

====================================================
➜ status
====================================================

Features Detail

Mouse Scrolling

  • Scroll up: Mouse wheel up
  • Scroll down: Mouse wheel down
  • Auto-scroll: Automatically scrolls to bottom on new output
  • Manual scroll: Disables auto-scroll until you scroll to bottom

Command History

  • Up arrow: Previous command
  • Down arrow: Next command (or empty if at end)
  • History size: Last 100 commands

Buffer Management

  • Size: 10,000 lines (≈1-2 MB)
  • Type: Circular buffer (auto-evicts old lines)
  • Thread-safe: Safe concurrent access
  • Scrollable: Full history accessible via mouse wheel

Performance

  • Non-blocking writes: WriteLine() never blocks (O(1))
  • Double-buffering: Flicker-free rendering
  • Rate limiting: Max 20 FPS rendering
  • Hot-path safe: 10,000 message buffer for burst handling

Architecture

┌─────────────────────────────────────┐
│          User Application           │
│  (Registers commands, writes logs)  │
└────────────┬────────────────────────┘
             │
             v
┌─────────────────────────────────────┐
│         TUI Library (tui.go)        │
│  - Command routing                  │
│  - Output buffering (10K channel)   │
│  - Event loop orchestration         │
└────┬────────────────┬───────────────┘
     │                │
     v                v
┌─────────┐    ┌──────────────┐
│ Renderer│    │ InputHandler │
│ (render)│    │  (keyboard)  │
└─────────┘    └──────────────┘
     │                │
     v                v
┌─────────────────────────────────────┐
│          Terminal (terminal.go)     │
│  - Raw mode, ANSI codes, mouse     │
└─────────────────────────────────────┘

Technical Details

Terminal Control

  • Raw mode: Using golang.org/x/term
  • Alternate screen: Preserves shell history
  • ANSI escape codes: Direct control for speed
  • Mouse tracking: SGR extended mode

Rendering

  • Strategy: Double-buffered in-memory rendering
  • Line clearing: \033[2K before each line write
  • Cursor positioning: Manual control for prompt placement
  • No flicker: Overwrite instead of clear+redraw

Thread Safety

  • All public methods are thread-safe
  • Uses channels for async communication
  • Read/write mutexes for shared state
  • Safe to call from goroutines

License

Copyright © 2025 EQ2Go, Sharkk Sharkk Minimal License

Examples

Run the mock server:

cd examples/mockserver
go build
./mockserver

Try these commands:

  • help - See all available commands
  • status - Show server status
  • info - Show an info message
  • warn - Show a warning message
  • error - Show an error message
  • clear - Clear the screen
  • quit - Exit

Use mouse wheel to scroll through output history!