A pure-Go, zero-dependency, hyper-minimalist toolkit for building Terminal User Interfaces (TUIs).
Notice: This is a heavily optimized, thread-safe fork of the original nyangkosense/tinybox.
Adhering to the "suckless" philosophy, ttbox does exactly what it needs to do and nothing more. It skips bloated terminfo databases and Cgo bindings in favor of hardcoded, universally supported ANSI escape sequences.
While the original tinybox was a brilliant exercise in minimalism, real-world TUI applications often require concurrent rendering and robust handling of massive I/O streams. This fork maintains the minimalist footprint while introducing enterprise-grade optimizations:
- True Non-Blocking Event Loop (Self-Pipe Trick): Unlike typical minimalist libs that block entirely on
stdinreads, this fork implements a POSIX self-pipe architecture.PollEvent()listens to both user input and internal OS signals concurrently, guaranteeing zero-latency reactions to events like background window resizing. - Thread Safety (
sync.Mutex): The entire terminal state is now concurrency-safe. You can safely calltb.SetCellortb.Presentfrom multiple goroutines (e.g., background workers updating a progress bar). - Blazing Fast Diffing (64-bit Bit-packing): Instead of comparing multi-field structs to determine screen changes, this fork packs each cell's state (Rune, Foreground, Background, and Attributes) into a single
uint64signature (c.pack()). Diffing the screen is now a single scalar integer check, drastically reducing CPU overhead duringPresent(). - 1D Memory Architecture: The 2D slice buffer (
[][]Cell) was flattened into a 1D slice ([]Cell). This maximizes CPU cache locality and allows functions likeScroll()andapplyResize()to use Go's ultra-fast nativecopy()built-in, moving memory blocks directly rather than iterating through loops. - True Bracketed Paste Handling: The original library flooded the event queue with individual character events when pasting. This fork introduces a stateful parser that captures pasted text into an
EventPastetype (withText stringandIsLast bool), safely quarantining massive clipboard dumps. - Intelligent Event Debouncing: Rapidly dragging a terminal window generates a flood of
SIGWINCHsignals. This fork natively debounces resize events, preventing UI thrashing. - Global Background Support: Added
SetGlobalBg()for seamless, full-terminal background coloring without manually filling empty cells.
- Double-buffered, immediate-mode rendering (only diffs are written to stdout).
- 256-color palette + Default terminal colors + Global Background coloring.
- Mouse input tracking (X11 and SGR modes > 223 coords, scroll wheels).
- Clean Suspend/Resume handling (
SIGTSTP/SIGCONT). - Window resize tracking via
SIGWINCH(withioctland\033[6nfallbacks). - Modal overlays via fast
SaveBuffer()andRestoreBuffer().
To maintain its zero-dependency, <1,500 LOC footprint, ttbox intentionally does not support:
- Windows: No
cmd.exe, PowerShell, or ConPTY fallback wrappers. POSIX only (Linux, macOS, BSD). - TrueColor (24-bit RGB): Strictly utilizes the 256-color ANSI space.
- Complex Unicode Widths: The grid assumes 1
rune= 1 visual cell width. Zero-width joiners, emojis, and double-width CJK characters will misalign the rendering grid. - Dynamic Termcap Parsing: Relies entirely on standard ANSI escape sequences.
go get github.com/ttasc/ttboxUsing this fork is exactly the same as using the original library!
You can replace the original tinybox import with this one without fearing any conflicts or needing to rewrite your existing code. All original functions behave identically, just much faster and safer.
The only new API addition you need to know about is tb.SetGlobalBg(color int). In the original library, you had to manually fill the screen with spaces to set a background color. Now, you just call this function once to set a default background color for your entire application.
ttbox acts as a global singleton. You initialize it, run your event loop, and close it when done.
package main
import (
"log"
tb "github.com/ttasc/ttbox"
)
func main() {
// 1. Initialize the terminal into raw mode
if err := tb.Init(); err != nil {
log.Fatalf("failed to initialize ttbox: %v", err)
}
defer tb.Close() // Always defer Close to restore the user's terminal
// Optional: Enable mouse tracking
tb.EnableMouse()
// 2. Draw something to the buffer
tb.Clear()
tb.PrintAt(10, 5, "Hello, minimalist world!")
// Set an individual cell (X, Y, Rune, Foreground, Background)
tb.SetCell(10, 6, '🚀', tb.ColorDefault, tb.ColorDefault)
// 3. Render the buffer to the screen
tb.Present()
// 4. Run the Event Loop
for {
ev, err := tb.PollEvent()
if err != nil {
break
}
switch ev.Type {
case tb.EventKey:
if ev.Key == tb.KeyCtrlC || ev.Key == tb.KeyEscape {
return // Exit application
}
case tb.EventResize:
// Buffer resizes automatically in this fork, just trigger a redraw
tb.Clear()
tb.PrintAt(10, 5, "Terminal resized!")
tb.Present()
}
}
}This fork makes handling large pastes and global styling trivial.
package main
import (
"fmt"
tb "github.com/ttasc/ttbox"
)
func main() {
tb.Init()
defer tb.Close()
tb.EnableBracketedPaste()
// Set the global background color (e.g., dark blue)
tb.SetGlobalBg(17)
tb.Clear()
tb.PrintAt(2, 1, "Try pasting some text (Ctrl+V or Cmd+V)...")
tb.PrintAt(2, 2, "Press ESC to quit.")
tb.Present()
var pastedText string
for {
ev, _ := tb.PollEvent()
switch ev.Type {
case tb.EventKey:
if ev.Key == tb.KeyEscape {
return
}
case tb.EventPaste:
// Append chunk
pastedText += ev.Text
// IsLast is true when the \033[201~ terminator is received
if ev.IsLast {
tb.Clear()
tb.PrintAt(2, 4, fmt.Sprintf("Pasted %d bytes!", len(pastedText)))
// Draw a box around some content
tb.SetColor(46, tb.ColorDefault) // Green text
tb.Box(2, 6, 40, 5)
tb.PrintAt(4, 8, "Paste successful")
tb.ResetAttr()
tb.Present()
pastedText = "" // reset
}
}
}
}You can easily save the background state before popping up a menu.
// ... main loop ...
// Save current screen
tb.SaveBuffer()
// Draw a modal menu
tb.ClearRect(10, 5, 20, 10)
tb.Box(10, 5, 20, 10)
tb.PrintAt(12, 7, "1. Option A")
tb.PrintAt(12, 8, "2. Option B")
tb.Present()
// Wait for input...
ev, _ := tb.PollEvent()
// Instantly restore previous screen
tb.RestoreBuffer()
tb.Present()