Documentation
¶
Overview ¶
Package ward wraps a child process in a pseudo-terminal and reserves the last terminal row for a notification bar.
Two data paths (input and output) plus a status channel feed into a single event loop that owns the bar. The stdin path includes a two-state machine (normal/command) that intercepts the hotkey (Ctrl+], 0x1D) when OnKey is configured and stdin is a TTY.
Input: user keystrokes → child stdin (+ window-size report rewriting)
┌──────┐ ┌──────────┐ stdin.Read() ┌─────────────────┐ ┌────────┐ ┌───────┐
│ user │ os.Stdin │ │ ──────────────► │ winsizeRewriter │ ──► │ PTY │ ──► │ child │
│ term │ ─────────► │ Wrapper │ │ .Write() │ │ master │ │ stdin │
└──────┘ │ │ └─────────────────┘ └────────┘ └───────┘
└──────────┘ rewrites CSI 4/8/9/48 … t
to the N-1 row PTY height
Output: child stdout → user terminal (+ escape sequence scanning)
┌───────┐ ┌────────┐ ┌──────────────┐ ┌───────────┐ ┌──────┐
│ child │ ─► │ PTY │ ──► │ output │ ──► │ escScanner│ ──► │ user │
│stdout │ │ master │ │ goroutine │ │ .Scan() │ │ term │
└───────┘ └────────┘ │ ptmx.Read() │ └─────┬─────┘ └──────┘
└──────────────┘ │
│ detects:
│ • ESC c (RIS)
│ • CSI r (no params)
│ • CSI J / 2J / 3J
▼
┌─────────────────┐
│ re-emit scroll │
│ region + repaint│
│ bar if needed │
└─────────────────┘
Notifications: status channel feeds the event loop
┌───────────┐
│ Status │ barEventStatus / barEventAlert
│ channel │ ─────────────────┐
│ (Go chan) │ │
└───────────┘ ▼
┌────────────────────────────────────────────────┐
│ event loop │
│ │
│ • barEventStatus → update lastStatus, │
│ show if no active alert │
│ • barEventAlert → show immediately or queue │
│ (max 64), start dismiss timer │
│ • barEventDismiss → pop next alert or fall │
│ back to lastStatus or clear │
│ │
│ sole owner of bar content; writes to stdout │
│ under outputMu │
└──────────────────────┬─────────────────────────┘
│
▼
┌────────────────────────────────────────────────┐
│ terminal last row │
│ ╱╱╱ VIBEPIT message ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ │
└────────────────────────────────────────────────┘
Command mode lifecycle (stdin goroutine ↔ event loop):
stdin goroutine event loop
─────────────── ──────────
detect 0x1D
→ barEventEnterCommand ────────► commandGen++
← commandResponse ◄──────────── stop dismiss timer
start idle timer
render command bar
(waiting for key)
matched key pressed
→ barEventBeginAction ─────────► stop idle timer
← ack (true) ◄────────────────── enter pendingAction
call OnKey(ctx, key, target)
→ barEventAction ──────────────► flash result
dismiss alert
process queue
Terminal protection via DECSTBM scroll region:
row 1 ┌────────────────────────┐
│ │ ◄── scroll region: rows 1..(height-1)
│ child output area │ child's PTY size = height-1 rows
│ │ normal scrolling confined here
row N-1 │ │
├────────────────────────┤
row N │ ward notification bar │ ◄── protected: outside scroll region
└────────────────────────┘
Five things worth calling out:
The scroll region is the primary protection mechanism. Ward sets DECSTBM to rows 1..N-1 so normal output and scrolling cannot reach row N. The child's PTY is sized to N-1 rows, so it believes the terminal is one row shorter than reality. Ward re-applies the scroll region after any detected reset (ESC c, parameterless CSI r).
The escScanner is intentionally minimal — not a terminal emulator. It recognizes just enough ECMA-48/DEC grammar to detect the three sequence classes that can destroy the bar (reset, margin clear, screen erase). It also tracks whether the byte stream is mid-sequence so ward never injects its own escapes inside the child's incomplete sequences.
The bar is not repainted on every PTY read. Doing so would leak bar escape sequences into terminal scrollback. Repainting occurs only on three occasions: when the escScanner detects a scroll/erase reset, when the event loop changes bar content, and when SIGWINCH fires.
The PTY size alone does not hide row N. Applications such as tmux query the terminal directly (CSI 18 t / CSI 14 t) and trust the reply over TIOCGWINSZ, and terminals with in-band resize (mode 2048) push CSI 48 … t unsolicited on every resize. The winsizeRewriter on the stdin path rewrites those XTWINOPS reports to the N-1 row height so the child's two views of the terminal agree. Replies are rewritten on input rather than queries suppressed on output because the in-band notifications have no query to suppress. A report prefix split across reads is held for at most 50 ms; a lone ESC is never held, so the Esc key stays responsive.
In command mode a complete escape sequence (arrow or function key, mouse or focus event, size report, OSC/DCS reply) is forwarded intact and does not cancel command mode: command keys are single bytes, so a sequence is never meant for the bar. A bare Esc, Alt+key, a sequence cut at the read boundary and a bracketed paste still cancel; all but the Esc are forwarded with their ESC intact.
Index ¶
Constants ¶
const DefaultTimeout = 3 * time.Second
DefaultTimeout is the default alert display duration.
Variables ¶
This section is empty.
Functions ¶
func RenderCommandBar ¶
RenderCommandBar renders the command mode bar with key hints and optional target. hasAlert controls whether RequireAlert hints are shown.
Types ¶
type OSC133Parser ¶
type OSC133Parser struct {
// contains filtered or unexported fields
}
OSC133Parser is a streaming, zero-allocation parser for OSC 133 escape sequences.
func NewOSC133Parser ¶
func NewOSC133Parser() *OSC133Parser
NewOSC133Parser creates a parser in the initial ground/unknown state.
func (*OSC133Parser) Push ¶
func (p *OSC133Parser) Push(data []byte, onEvent func(Event, *int))
Push processes a chunk of bytes, calling onEvent for every OSC 133 marker found. The caller is responsible for forwarding bytes to their destination.
func (*OSC133Parser) Zone ¶
func (p *OSC133Parser) Zone() Zone
Zone returns the current semantic zone.
type Options ¶
type Options struct {
Command []string
Hotkey byte // default 0x1D = Ctrl+]
Env []string // extra KEY=VALUE pairs for the child process
Status <-chan StatusUpdate // nil-safe; bar stays hidden until first event
OnKey func(ctx context.Context, key byte, target string) (string, error)
}
Options configures the PTY wrapper.
type StatusUpdate ¶
type StatusUpdate struct {
Message string
Alert bool
Timeout time.Duration
Target string
KeyHints []KeyHint
}
StatusUpdate carries a message, alert flag, and display timeout for the status bar.
type Wrapper ¶
type Wrapper struct {
// contains filtered or unexported fields
}
Wrapper is the core PTY wrapper that runs a child process in a pseudo-terminal, manages toast notifications, and handles terminal resizing.
func NewWrapper ¶
NewWrapper creates a new Wrapper with the given options.