This tutorial walks through building a simple Ultraviolet application that displays centered "Hello, World!" text. You'll learn the core concepts: creating a terminal, managing a screen, handling input events, and rendering.
A Terminal manages the console, input event loop, and screen state.
t := uv.DefaultTerminal()You can also create a terminal with a custom console and options:
con := uv.NewConsole(os.Stdin, os.Stdout, os.Environ())
t := uv.NewTerminal(con, &uv.Options{
Logger: myLogger, // optional, for debugging I/O
})The terminal's screen is where you draw content, manage the alternate screen buffer, and set cells.
scr := t.Screen()The alternate screen buffer lets your application display content without affecting the user's scrollback. Most fullscreen TUIs use it.
scr.EnterAltScreen()Starting puts the console into raw mode (disabling echoing and line buffering so we receive individual keypresses like ctrl+c), initializes the input event loop, and prepares the screen for rendering.
if err := t.Start(); err != nil {
log.Fatalf("failed to start terminal: %v", err)
}
defer t.Stop()Stop() restores the console, exits the alternate screen, and cleans up. It
is safe to call multiple times and supports suspend/resume cycles.
You can set individual cells directly:
for i, r := range "Hello, World!" {
scr.SetCell(i, 0, &uv.Cell{Content: string(r), Width: 1})
}Or use the screen helper package for convenience:
ctx := screen.NewContext(scr)
ctx.DrawString("Hello, World!", 0, 0)The Context supports styled text, links, and wrapping. It implements
io.Writer, so you can use fmt.Fprint and friends.
Drawing to the screen is a two-step process:
- Render — computes the minimal diff between the current and new screen state, writing ANSI escape sequences to an internal buffer.
- Flush — writes the buffer to the terminal. This is the only step that performs real I/O and can return an error.
scr.Render()
if err := scr.Flush(); err != nil {
log.Fatalf("flush failed: %v", err)
}The terminal provides a channel of input events. Range over it to process keyboard, mouse, and resize events:
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
}
}
}MatchString accepts key names and modifier combinations like "ctrl+a",
"shift+enter", or "alt+tab".
Here's the complete program—centered "Hello, World!" that redraws on resize
and exits on q or ctrl+c:
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.Fatalf("failed to start terminal: %v", err)
}
defer t.Stop()
ctx := screen.NewContext(scr)
text := "Hello, World!"
textWidth := scr.StringWidth(text)
display := func() {
screen.Clear(scr)
bounds := scr.Bounds()
x := (bounds.Dx() - textWidth) / 2
y := bounds.Dy() / 2
ctx.DrawString(text, x, y)
scr.Render()
scr.Flush()
}
for ev := range t.Events() {
switch ev := ev.(type) {
case uv.WindowSizeEvent:
scr.Resize(ev.Width, ev.Height)
display()
case uv.KeyPressEvent:
if ev.MatchString("q", "ctrl+c") {
return
}
}
}
}Part of Charm.
Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة
