Documentation
¶
Overview ¶
Package loop implements the engine's frame scheduler and event demultiplexer.
The Loop owns the synchronous frame pipeline:
tty (raw bytes) ──► Parser ──► typed event channels │ └──► ticker @ FPS ──► Flush() ──► Diff ──► Render ──► tty.Write
Loop is the single owner of terminal I/O while it is running. It coordinates three goroutines:
- the I/O reader, which parses raw bytes from the terminal into typed event channels and never blocks the render path (drops events on full channels via non-blocking selects);
- the resize watcher (Unix only), which forwards SIGWINCH into the resize channel;
- the frame scheduler, which fires Flush on a fixed-cadence ticker.
Hot-path invariants (steady state, no input, no changes):
- Flush performs zero heap allocations;
- the scheduler ticker never allocates;
- event-channel dispatches are non-blocking and allocation-free.
Index ¶
Constants ¶
const ( // EventKey identifies a keyboard event. EventKey = term.EventKey // EventMouse identifies a mouse event. EventMouse = term.EventMouse // EventResize identifies a terminal-resize notification. Resize events // carry no payload — callers query the terminal dimensions via Term. EventResize = 3 // EventError identifies an asynchronous error forwarded from I/O or // render. Inspect Err for the underlying cause. EventError = 4 )
Event type discriminator constants for the public Event struct.
The internal/term package already defines EventKey/EventMouse for the parser's tagged union. Here we expose the same discriminator values plus EventResize and EventError for the loop-level event surface. The values are kept in sync with internal/term so callers can reuse a single discriminator if they only need input events.
const DefaultFPS = 60
DefaultFPS is the steady-state target frame rate for the scheduler. 60 FPS keeps the frame budget at ~16.6ms which is sufficient for smooth animations while leaving headroom for input parsing.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Event ¶
type Event struct {
// Type discriminates the payload. See Event* constants.
Type int
// Key holds the keyboard payload when Type == EventKey.
Key term.KeyEvent
// Mouse holds the mouse payload when Type == EventMouse.
Mouse term.MouseEvent
// Err holds the underlying error when Type == EventError.
Err error
}
Event is a tagged-union value emitted on typed channels. It mirrors the discriminator of internal/term.InputEvent and adds Resize and Error variants for loop-level signals.
type Loop ¶
type Loop struct {
// Term is the platform terminal handle. It must outlive the Loop.
Term *platform.Terminal
// FrontBuf is the cell grid previously presented to the user.
FrontBuf *buffer.Buffer
// BackBuf is the cell grid currently being mutated by the application.
BackBuf *buffer.Buffer
// Differ computes the minimal changeset between front and back buffers.
Differ *diff.Differ
// Renderer serialises a changeset into an ANSI byte payload.
Renderer *ansi.Renderer
// KeyEvents receives keyboard events. Buffered; full channels drop.
KeyEvents chan term.KeyEvent
// MouseEvents receives mouse events. Buffered; full channels drop.
MouseEvents chan term.MouseEvent
// ResizeEvents receives a struct{} on every terminal resize.
ResizeEvents chan struct{}
// Errors receives asynchronous errors from I/O and render.
Errors chan error
// Ticker is the frame scheduler. Fires every 1/FPS.
Ticker *time.Ticker
// Quit is closed by Stop() to signal every internal goroutine.
Quit chan struct{}
// contains filtered or unexported fields
}
Loop is the engine's main scheduler. All fields are zero-value usable after NewLoop returns; Start() drives the goroutines.
Concurrency:
- Field reads after Start() are safe without locks for the typed channels and the ticker.
- Flush() is the only method that may be called concurrently with Start(); it acquires a mutex to serialise against the scheduler.
- Stop() is safe to call from any goroutine; it is idempotent.
func NewLoop ¶
NewLoop constructs a Loop with pre-allocated channels, buffers, differ and renderer. The terminal is not owned by the Loop and must be Closed by the caller after Stop() returns.
width and height define the initial front/back buffer dimensions. The caller is expected to call Term.EnterRawMode before Start().
func (*Loop) Flush ¶
Flush executes a single frame: compute the diff, render changes, write them to the terminal and update the front buffer. It is safe to call concurrently with the scheduler; concurrent calls are serialised.
Flush returns the first error from the underlying terminal Write, or nil on success. Errors from Flush are also forwarded to the Errors channel.
Hot-path: when BackBuf equals FrontBuf (no application mutations), Flush performs zero heap allocations.
func (*Loop) Start ¶
func (l *Loop) Start()
Start launches the I/O and scheduler goroutines. It is safe to call exactly once; subsequent calls are no-ops.
func (*Loop) Stop ¶
func (l *Loop) Stop()
Stop signals all goroutines to exit, closes the ticker and waits for them to finish. Stop is idempotent and safe to call from any goroutine, including before Start has been called.
Stop does NOT close the typed event channels: callers may still drain pending events after Stop returns.