SKILL.md

  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