| name | charm-ultraviolet |
| description | Low-level Go terminal primitives - cell-based rendering, input handling, screen management. Use when building custom Go terminal renderers, ultraviolet, cell buffers, or performance-critical TUI work below Bubble Tea's abstraction level. |
Ultraviolet (charmbracelet/ultraviolet)
What Is Ultraviolet (and When NOT to Use It)
Ultraviolet is a set of low-level primitives for terminal manipulation in Go. It provides cell-based rendering, unified input handling, and screen management. It powers Bubble Tea v2 and Lip Gloss v2 internally.
This is NOT a framework. It is infrastructure. Think of it like syscalls vs stdlib - you can use it directly, but most of the time you should not.
When NOT to use it
- Building a standard TUI app - use Bubble Tea v2 instead
- Styling terminal output - use Lip Gloss v2 instead
- You want an architecture/framework with state management - use Bubble Tea v2
- Prototyping - too low-level, too much boilerplate
When to use it directly
- Building your own TUI framework on top of these primitives
- Writing a custom renderer that needs cell-level control
- Performance-critical rendering where you need direct buffer manipulation
- Embedding terminal rendering into a non-Bubble Tea application
- Working on Bubble Tea / Lip Gloss internals
API Stability Warning
The project README explicitly states: "This project currently exists to serve internal use cases. API stability is a goal, but expect no stability guarantees as of now." Plan accordingly.
Core Abstractions
The library lives in a single flat Go package uv (import path: github.com/charmbracelet/ultraviolet), with helper sub-packages screen/ and layout/.
Cell
The fundamental unit. One terminal cell = one grapheme cluster.
type Cell struct {
Content string
Style Style
Link Link
Width int
}
Key constants/values:
EmptyCell - a cell with " ", width 1, no style
- Zero-width cells (
Width == 0) are placeholders for wide characters
Buffer
A 2D grid of cells, organized as Lines []Line where Line []Cell.
buf := uv.NewBuffer(80, 24)
buf.SetCell(x, y, &cell)
cell := buf.CellAt(x, y)
buf.Resize(newW, newH)
buf.Clear()
buf.Fill(&cell)
buf.FillArea(&cell, area)
clone := buf.Clone()
Buffer implements Drawable, so you can buf.Draw(screen, area) to composite buffers onto screens.
RenderBuffer
Wraps Buffer with change tracking. Only touched lines/cells get re-rendered.
rbuf := uv.NewRenderBuffer(80, 24)
rbuf.SetCell(x, y, &cell)
rbuf.TouchLine(x, y, n)
rbuf.TouchedLines()
Screen (Interface)
The core abstraction that anything drawable targets.
type Screen interface {
Bounds() Rectangle
CellAt(x, y int) *Cell
SetCell(x, y int, c *Cell)
WidthMethod() WidthMethod
}
Implemented by: Buffer, ScreenBuffer, Window, TerminalScreen.
Drawable (Interface)
Anything that can render itself onto a Screen.
type Drawable interface {
Draw(scr Screen, area Rectangle)
}
Implemented by: Buffer, Window, StyledString, and your own components.
Window
A rectangular area that can own its own buffer or share a parent's buffer (view).
root := uv.NewScreen(80, 24)
child := root.NewWindow(x, y, width, height)
view := root.NewView(x, y, width, height)
Windows support MoveTo, MoveBy, Resize, Clone.
Terminal
The main entry point for standalone UV apps. Manages console I/O, raw mode, event loop.
t := uv.DefaultTerminal()
t.Start()
defer t.Stop()
scr := t.Screen()
for ev := range t.Events() {
switch ev := ev.(type) {
case uv.WindowSizeEvent:
scr.Resize(ev.Width, ev.Height)
case uv.KeyPressEvent:
if ev.MatchString("ctrl+c") { return }
}
}
TerminalScreen
The concrete screen for terminal output. Manages alt screen, cursor, colors, mouse mode, keyboard enhancements, synchronized updates.
scr := t.Screen()
scr.EnterAltScreen()
scr.ExitAltScreen()
scr.SetCell(x, y, &cell)
scr.Render()
scr.Flush()
scr.Display(myDrawable)
scr.ShowCursor()
scr.SetCursorPosition(x, y)
scr.SetMouseMode(uv.MouseModeClick)
scr.SetBackgroundColor(color)
scr.SetWindowTitle("My App")
scr.SetSynchronizedUpdates(true)
scr.SetKeyboardEnhancements(enh)
scr.InsertAbove(content)
StyledString
Converts ANSI-styled strings into cell-based representation. Implements Drawable.
ss := uv.NewStyledString("Hello \x1b[1mWorld\x1b[0m")
ss.Draw(screen, area)
Sub-Packages
screen/ - Screen Helpers
Utility functions that work with any Screen implementation.
import "github.com/charmbracelet/ultraviolet/screen"
screen.Clear(scr)
screen.ClearArea(scr, area)
screen.Fill(scr, &cell)
screen.FillArea(scr, &cell, area)
screen.Clone(scr)
screen.CloneArea(scr, area)
ctx := screen.NewContext(scr)
ctx.SetForeground(ansi.Red)
ctx.SetBold(true)
ctx.DrawString("hello", x, y)
ctx.Printf("count: %d", n)
layout/ - Constraint-Based Layout
Cassowary-based layout solver (ported from Ratatui). Splits areas into non-overlapping rectangles.
import "github.com/charmbracelet/ultraviolet/layout"
chunks := layout.New().
Direction(layout.Vertical).
Constraints(
layout.Len(3),
layout.Fill(1),
layout.Len(1),
).
Split(area)
Constraint types: Len, Ratio, Percent, Fill, Min, Max.
Events
Events come from t.Events() channel. Key types:
| Event | Description |
|---|
WindowSizeEvent | Terminal resized (width, height in cells) |
PixelSizeEvent | Terminal resized (width, height in pixels) |
KeyPressEvent | Key pressed. Use ev.MatchString("ctrl+c", "q") |
KeyReleaseEvent | Key released (requires kitty keyboard protocol) |
MouseClickEvent | Mouse click with position and button |
MouseMotionEvent | Mouse moved (requires mouse mode enabled) |
PasteEvent | Bracketed paste content |
Key matching uses human-readable strings: "ctrl+a", "shift+enter", "alt+tab", "f1", "space".
Geometry
Uses image.Point and image.Rectangle from stdlib:
pos := uv.Pos(x, y)
rect := uv.Rect(x, y, width, height)
Note: uv.Rect(x, y, w, h) takes width/height, not max coordinates. This differs from image.Rect(x0, y0, x1, y1).
Style System
Styles are value types with bitfield attributes:
style := uv.Style{
Fg: ansi.Red,
Bg: ansi.Black,
UnderlineColor: ansi.Blue,
Underline: uv.UnderlineCurly,
Attrs: uv.AttrBold | uv.AttrItalic,
}
Attributes: AttrBold, AttrFaint, AttrItalic, AttrBlink, AttrReverse, AttrConceal, AttrStrikethrough.
Underline styles: UnderlineNone, UnderlineSingle, UnderlineDouble, UnderlineCurly, UnderlineDotted, UnderlineDashed.
Style diffing is built in - the renderer computes minimal ANSI sequences to transition between styles.
Rendering Pipeline
The "Cursed Renderer" is a cell-based diffing engine inspired by ncurses:
- You write cells to the screen buffer via
SetCell
Render() diffs current buffer against previous state
- Renderer emits minimal ANSI escape sequences (cursor movement, style changes, text)
Flush() writes the accumulated output to the terminal
Optimizations include:
- Only touched lines are re-rendered
- Style diffs minimize SGR sequence length
- Cursor movement uses shortest path (absolute, relative, tabs, backspace)
- Supports synchronized updates (mode 2026) to prevent flicker
- Hash-based scroll detection for efficient content shifts
Minimal Hello World
package main
import (
"log"
uv "github.com/charmbracelet/ultraviolet"
"github.com/charmbracelet/ultraviolet/screen"
)
func main() {
t := uv.DefaultTerminal()
scr := t.Screen()
scr.EnterAltScreen()
if err := t.Start(); err != nil {
log.Fatal(err)
}
defer t.Stop()
ctx := screen.NewContext(scr)
for ev := range t.Events() {
switch ev := ev.(type) {
case uv.WindowSizeEvent:
scr.Resize(ev.Width, ev.Height)
case uv.KeyPressEvent:
if ev.MatchString("q", "ctrl+c") {
return
}
}
screen.Clear(scr)
ctx.DrawString("Hello, World!", 0, 0)
scr.Render()
scr.Flush()
}
}
Relationship to Bubble Tea v2
ultraviolet (primitives)
|
+-- Lip Gloss v2 (styling, composition)
|
+-- Bubble Tea v2 (framework: Elm architecture, state management, commands)
|
+-- Bubbles (components: text input, list, table, etc.)
- Ultraviolet provides: cells, buffers, screen management, input decoding, rendering
- Bubble Tea v2 provides:
Program, Model, Update, View, commands, subscriptions
- Lip Gloss v2 provides:
Style, layout, borders, padding, composition
If you are building a TUI application, start with Bubble Tea v2. Only reach for ultraviolet when Bubble Tea's abstractions get in your way.
Checklist
Before using ultraviolet directly, confirm: