1---
2name: charm-ultraviolet
3description: "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."
4---
5
6# Ultraviolet (charmbracelet/ultraviolet)
7
8## What Is Ultraviolet (and When NOT to Use It)
9
10Ultraviolet 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.
11
12**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.
13
14### When NOT to use it
15
16- Building a standard TUI app - use Bubble Tea v2 instead
17- Styling terminal output - use Lip Gloss v2 instead
18- You want an architecture/framework with state management - use Bubble Tea v2
19- Prototyping - too low-level, too much boilerplate
20
21### When to use it directly
22
23- Building your own TUI framework on top of these primitives
24- Writing a custom renderer that needs cell-level control
25- Performance-critical rendering where you need direct buffer manipulation
26- Embedding terminal rendering into a non-Bubble Tea application
27- Working on Bubble Tea / Lip Gloss internals
28
29## API Stability Warning
30
31The 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.
32
33## Core Abstractions
34
35The library lives in a single flat Go package `uv` (import path: `github.com/charmbracelet/ultraviolet`), with helper sub-packages `screen/` and `layout/`.
36
37### Cell
38
39The fundamental unit. One terminal cell = one grapheme cluster.
40
41```go
42type Cell struct {
43 Content string // single grapheme cluster
44 Style Style // fg, bg, attrs (bold, italic, etc.)
45 Link Link // OSC 8 hyperlink
46 Width int // columns occupied (1 for normal, 2 for wide chars like CJK)
47}
48```
49
50Key constants/values:
51- `EmptyCell` - a cell with `" "`, width 1, no style
52- Zero-width cells (`Width == 0`) are placeholders for wide characters
53
54### Buffer
55
56A 2D grid of cells, organized as `Lines []Line` where `Line []Cell`.
57
58```go
59buf := uv.NewBuffer(80, 24) // width, height
60buf.SetCell(x, y, &cell) // write a cell
61cell := buf.CellAt(x, y) // read a cell (nil if out of bounds)
62buf.Resize(newW, newH) // resize, preserving content
63buf.Clear() // fill with EmptyCell
64buf.Fill(&cell) // fill with custom cell
65buf.FillArea(&cell, area) // fill rectangular region
66clone := buf.Clone() // deep copy
67```
68
69Buffer implements `Drawable`, so you can `buf.Draw(screen, area)` to composite buffers onto screens.
70
71### RenderBuffer
72
73Wraps Buffer with change tracking. Only touched lines/cells get re-rendered.
74
75```go
76rbuf := uv.NewRenderBuffer(80, 24)
77rbuf.SetCell(x, y, &cell) // auto-marks line as touched
78rbuf.TouchLine(x, y, n) // manually mark region dirty
79rbuf.TouchedLines() // count of dirty lines
80```
81
82### Screen (Interface)
83
84The core abstraction that anything drawable targets.
85
86```go
87type Screen interface {
88 Bounds() Rectangle
89 CellAt(x, y int) *Cell
90 SetCell(x, y int, c *Cell)
91 WidthMethod() WidthMethod
92}
93```
94
95Implemented by: `Buffer`, `ScreenBuffer`, `Window`, `TerminalScreen`.
96
97### Drawable (Interface)
98
99Anything that can render itself onto a Screen.
100
101```go
102type Drawable interface {
103 Draw(scr Screen, area Rectangle)
104}
105```
106
107Implemented by: `Buffer`, `Window`, `StyledString`, and your own components.
108
109### Window
110
111A rectangular area that can own its own buffer or share a parent's buffer (view).
112
113```go
114// Root window (owns its buffer)
115root := uv.NewScreen(80, 24)
116
117// Child window with own buffer
118child := root.NewWindow(x, y, width, height)
119
120// View into parent buffer (shared memory)
121view := root.NewView(x, y, width, height)
122```
123
124Windows support `MoveTo`, `MoveBy`, `Resize`, `Clone`.
125
126### Terminal
127
128The main entry point for standalone UV apps. Manages console I/O, raw mode, event loop.
129
130```go
131t := uv.DefaultTerminal()
132// or: t := uv.NewTerminal(console, opts)
133
134t.Start() // enter raw mode, start event loop
135defer t.Stop() // restore terminal, clean up
136
137scr := t.Screen() // returns *TerminalScreen
138
139for ev := range t.Events() {
140 switch ev := ev.(type) {
141 case uv.WindowSizeEvent:
142 scr.Resize(ev.Width, ev.Height)
143 case uv.KeyPressEvent:
144 if ev.MatchString("ctrl+c") { return }
145 }
146}
147```
148
149### TerminalScreen
150
151The concrete screen for terminal output. Manages alt screen, cursor, colors, mouse mode, keyboard enhancements, synchronized updates.
152
153```go
154scr := t.Screen()
155
156// Screen modes
157scr.EnterAltScreen() // alternate screen buffer
158scr.ExitAltScreen()
159
160// Rendering cycle
161scr.SetCell(x, y, &cell)
162scr.Render() // diff current vs previous state
163scr.Flush() // write changes to terminal
164
165// Or use Display for Drawable components
166scr.Display(myDrawable) // clear + draw + render + flush
167
168// Terminal features
169scr.ShowCursor()
170scr.SetCursorPosition(x, y)
171scr.SetMouseMode(uv.MouseModeClick)
172scr.SetBackgroundColor(color)
173scr.SetWindowTitle("My App")
174scr.SetSynchronizedUpdates(true) // mode 2026
175scr.SetKeyboardEnhancements(enh) // kitty protocol
176
177// Inline mode helper
178scr.InsertAbove(content) // insert text above without disrupting screen
179```
180
181### StyledString
182
183Converts ANSI-styled strings into cell-based representation. Implements Drawable.
184
185```go
186ss := uv.NewStyledString("Hello \x1b[1mWorld\x1b[0m")
187ss.Draw(screen, area)
188```
189
190## Sub-Packages
191
192### screen/ - Screen Helpers
193
194Utility functions that work with any `Screen` implementation.
195
196```go
197import "github.com/charmbracelet/ultraviolet/screen"
198
199screen.Clear(scr) // clear entire screen
200screen.ClearArea(scr, area) // clear region
201screen.Fill(scr, &cell) // fill screen
202screen.FillArea(scr, &cell, area) // fill region
203screen.Clone(scr) // deep copy to Buffer
204screen.CloneArea(scr, area) // deep copy region
205
206// Drawing context with stateful style
207ctx := screen.NewContext(scr)
208ctx.SetForeground(ansi.Red)
209ctx.SetBold(true)
210ctx.DrawString("hello", x, y)
211ctx.Printf("count: %d", n) // implements io.Writer
212```
213
214### layout/ - Constraint-Based Layout
215
216Cassowary-based layout solver (ported from Ratatui). Splits areas into non-overlapping rectangles.
217
218```go
219import "github.com/charmbracelet/ultraviolet/layout"
220
221// Split area vertically into 3 parts
222chunks := layout.New().
223 Direction(layout.Vertical).
224 Constraints(
225 layout.Len(3), // fixed 3 rows
226 layout.Fill(1), // fill remaining
227 layout.Len(1), // fixed 1 row
228 ).
229 Split(area)
230```
231
232Constraint types: `Len`, `Ratio`, `Percent`, `Fill`, `Min`, `Max`.
233
234## Events
235
236Events come from `t.Events()` channel. Key types:
237
238| Event | Description |
239|---|---|
240| `WindowSizeEvent` | Terminal resized (width, height in cells) |
241| `PixelSizeEvent` | Terminal resized (width, height in pixels) |
242| `KeyPressEvent` | Key pressed. Use `ev.MatchString("ctrl+c", "q")` |
243| `KeyReleaseEvent` | Key released (requires kitty keyboard protocol) |
244| `MouseClickEvent` | Mouse click with position and button |
245| `MouseMotionEvent` | Mouse moved (requires mouse mode enabled) |
246| `PasteEvent` | Bracketed paste content |
247
248Key matching uses human-readable strings: `"ctrl+a"`, `"shift+enter"`, `"alt+tab"`, `"f1"`, `"space"`.
249
250## Geometry
251
252Uses `image.Point` and `image.Rectangle` from stdlib:
253
254```go
255pos := uv.Pos(x, y) // == image.Point{X: x, Y: y}
256rect := uv.Rect(x, y, width, height) // origin + size (NOT min/max)
257```
258
259Note: `uv.Rect(x, y, w, h)` takes width/height, not max coordinates. This differs from `image.Rect(x0, y0, x1, y1)`.
260
261## Style System
262
263Styles are value types with bitfield attributes:
264
265```go
266style := uv.Style{
267 Fg: ansi.Red,
268 Bg: ansi.Black,
269 UnderlineColor: ansi.Blue,
270 Underline: uv.UnderlineCurly,
271 Attrs: uv.AttrBold | uv.AttrItalic,
272}
273```
274
275Attributes: `AttrBold`, `AttrFaint`, `AttrItalic`, `AttrBlink`, `AttrReverse`, `AttrConceal`, `AttrStrikethrough`.
276
277Underline styles: `UnderlineNone`, `UnderlineSingle`, `UnderlineDouble`, `UnderlineCurly`, `UnderlineDotted`, `UnderlineDashed`.
278
279Style diffing is built in - the renderer computes minimal ANSI sequences to transition between styles.
280
281## Rendering Pipeline
282
283The "Cursed Renderer" is a cell-based diffing engine inspired by ncurses:
284
2851. You write cells to the screen buffer via `SetCell`
2862. `Render()` diffs current buffer against previous state
2873. Renderer emits minimal ANSI escape sequences (cursor movement, style changes, text)
2884. `Flush()` writes the accumulated output to the terminal
289
290Optimizations include:
291- Only touched lines are re-rendered
292- Style diffs minimize SGR sequence length
293- Cursor movement uses shortest path (absolute, relative, tabs, backspace)
294- Supports synchronized updates (mode 2026) to prevent flicker
295- Hash-based scroll detection for efficient content shifts
296
297## Minimal Hello World
298
299```go
300package main
301
302import (
303 "log"
304 uv "github.com/charmbracelet/ultraviolet"
305 "github.com/charmbracelet/ultraviolet/screen"
306)
307
308func main() {
309 t := uv.DefaultTerminal()
310 scr := t.Screen()
311 scr.EnterAltScreen()
312
313 if err := t.Start(); err != nil {
314 log.Fatal(err)
315 }
316 defer t.Stop()
317
318 ctx := screen.NewContext(scr)
319
320 for ev := range t.Events() {
321 switch ev := ev.(type) {
322 case uv.WindowSizeEvent:
323 scr.Resize(ev.Width, ev.Height)
324 case uv.KeyPressEvent:
325 if ev.MatchString("q", "ctrl+c") {
326 return
327 }
328 }
329
330 screen.Clear(scr)
331 ctx.DrawString("Hello, World!", 0, 0)
332 scr.Render()
333 scr.Flush()
334 }
335}
336```
337
338## Relationship to Bubble Tea v2
339
340```
341ultraviolet (primitives)
342 |
343 +-- Lip Gloss v2 (styling, composition)
344 |
345 +-- Bubble Tea v2 (framework: Elm architecture, state management, commands)
346 |
347 +-- Bubbles (components: text input, list, table, etc.)
348```
349
350- Ultraviolet provides: cells, buffers, screen management, input decoding, rendering
351- Bubble Tea v2 provides: `Program`, `Model`, `Update`, `View`, commands, subscriptions
352- Lip Gloss v2 provides: `Style`, layout, borders, padding, composition
353
354If you are building a TUI application, start with Bubble Tea v2. Only reach for ultraviolet when Bubble Tea's abstractions get in your way.
355
356## Checklist
357
358Before using ultraviolet directly, confirm:
359
360- [ ] Bubble Tea v2 genuinely cannot solve your problem
361- [ ] You need cell-level rendering control
362- [ ] You accept API instability risk
363- [ ] You understand the rendering pipeline (SetCell -> Render -> Flush)
364- [ ] You handle WindowSizeEvent and call Resize yourself
365- [ ] You manage terminal raw mode and cleanup (Start/Stop)
366- [ ] You have read the examples in the `examples/` directory