Documentation
¶
Overview ¶
Package program runs a terminal interface.
It owns the terminal, the frame schedule, and the one goroutine that is allowed to touch the interface's state. It knows nothing about what the interface is for: what it drives is a Component, which draws itself and answers input, and everything a component needs from the program it asks for through a Runtime.
The concurrency model, in full ¶
One goroutine draws and handles input. Anything that happens elsewhere — a request finishing, a file changing, a timer firing — reaches the interface through a Dispatcher obtained from Runtime.Dispatcher, and runs there. That is the whole of it, and it is why state reached only from that goroutine needs no internal lock.
The program parks when there is nothing to do. It wakes for input, for posted work, and for the frame writer settling output — never on a clock that runs regardless. A component that wants a clock starts one with Runtime.After or Runtime.Every, and an interface with nothing scheduled costs nothing.
The two places an interface can be ¶
A program either takes a screen of its own, which it gives back on the way out, or draws in the terminal's own screen as a block with the session's output above it. The second is what Config.Inline asks for, and it is the difference between a program the user enters and leaves and one that is part of their session: what an inline interface has finished with is printed with InlineRuntime.Print and belongs to the terminal from then on — scrollable, selectable, and still there afterwards.
Index ¶
- Constants
- Variables
- func Run(ctx context.Context, cfg Config) (err error)
- func ValidateSize(width, height int) error
- type BellHost
- type ByteBatch
- type ByteIngress
- type ByteIngressConfig
- type Clipboard
- type ColorHost
- type Component
- type Config
- type CopyHost
- type DirectoryHost
- type Dispatcher
- type Environment
- type EventSource
- type FrameWriter
- type GroundHost
- type HandoverHost
- type Host
- type ImageHost
- type Images
- type InlineRuntime
- type KeyboardHost
- type LocaleHost
- type NotifyHost
- type PasteHost
- type ProgressHost
- type Runtime
- func (r *Runtime) After(d time.Duration, fn func()) (stop func())
- func (r *Runtime) Clipboard() Clipboard
- func (r *Runtime) Dispatcher() Dispatcher
- func (r *Runtime) Environment() Environment
- func (r *Runtime) Every(d time.Duration, fn func()) (stop func())
- func (r *Runtime) Images() Images
- func (r *Runtime) Quit()
- func (r *Runtime) Refresh()
- func (r *Runtime) Session() Session
- type Session
- type TitleHost
- type WheelHost
Examples ¶
Constants ¶
const DefaultFrameInterval = 16 * time.Millisecond
DefaultFrameInterval is the shortest time between program redraws. A terminal cannot usefully show more, and a stream of updates would otherwise ask for a frame each.
const MaxCells = 1 << 18
MaxCells is the largest host-controlled program surface. A screen owns both a front and back cell store, so a bound belongs at the host edge before either is allocated. The limit admits terminals far larger than ordinary displays while keeping one resize from becoming an open-ended memory request.
Variables ¶
var ErrFrameTimeout = errors.New("program: frame writer did not drain")
ErrFrameTimeout means a frame writer did not account for its pending frames before display ownership had to change. The program refuses the transition: a late frame would otherwise be written into the next owner's output.
var ErrInvalidFrameSequence = errors.New("program: invalid frame writer sequence")
ErrInvalidFrameSequence means a host accepted a non-empty frame without assigning it a usable position in its progress watermark. Continuing would allow a later frame to overtake output the presenter still owns, so publication stops instead.
var ErrInvalidSize = errors.New("program: invalid host size")
ErrInvalidSize means a host reported geometry that cannot safely back a program surface. Hosts are transport boundaries and their dimensions may come from an untrusted peer, so invalid input is an error rather than a grid allocation or panic.
var ErrStopped = errors.New("program: stopped")
ErrStopped means an ingress lost its interface owner before all accepted data could be applied. Pending data is deliberately released: cancellation ends the live interface and does not turn unconsumed input into published output.
Functions ¶
func Run ¶
Run draws the interface until it is asked to stop, its input ends, or the terminal fails.
A cancelled context stops the program without being reported as a failure: being asked to stop is not one.
func ValidateSize ¶ added in v0.3.0
ValidateSize reports whether width and height describe a safe non-empty program surface. Host adapters can call it before acquiring transport resources; Run applies it to both the opening size and every later resize.
Types ¶
type BellHost ¶ added in v0.0.5
type BellHost interface{ Bell() }
BellHost rings the user-facing host's audible or visible bell.
type ByteBatch ¶ added in v0.1.0
ByteBatch is one owner-side delivery from a ByteIngress.
Data is the ordered concatenation of bytes accepted since the previous delivery. The consumer owns it and may retain or change it after the callback returns. Final is true exactly once, after every accepted byte; Err is meaningful only then. A nil Err is successful completion.
type ByteIngress ¶ added in v0.1.0
type ByteIngress struct {
// contains filtered or unexported fields
}
ByteIngress carries a lossless ordered byte stream to an interface owner.
Write may block when limit bytes are waiting for the owner, applying backpressure at the producer rather than growing Dispatcher's general task queue. Adjacent writes are combined and at most one delivery task is pending at a time. The consumer is always called on the interface goroutine.
Close or CloseWithError completes the stream after all accepted bytes. When the program stops first, blocked writes return ErrStopped, pending bytes are released, and the consumer is not called from a background goroutine. A ByteIngress must be closed by its producer; its internal cancellation waiter then exits. The zero value is stopped. A ByteIngress must not be copied after construction: its pending bytes, backpressure and final delivery belong to one stream.
func NewByteIngress ¶ added in v0.1.0
func NewByteIngress(config ByteIngressConfig) (*ByteIngress, error)
NewByteIngress makes a bounded byte ingress.
A stopped or zero dispatcher is refused because it has no owner on which to invoke config.Consume.
func (*ByteIngress) Close ¶ added in v0.1.0
func (i *ByteIngress) Close() error
Close completes the stream successfully after all accepted bytes are delivered.
func (*ByteIngress) CloseWithError ¶ added in v0.1.0
func (i *ByteIngress) CloseWithError(err error) error
CloseWithError completes the stream with err after all accepted bytes are delivered. The first close wins. io.EOF is normalized to successful completion.
func (*ByteIngress) Done ¶ added in v0.1.0
func (i *ByteIngress) Done() <-chan struct{}
Done closes after the final batch is consumed or the interface owner stops.
type ByteIngressConfig ¶ added in v0.13.0
type ByteIngressConfig struct {
Dispatcher Dispatcher
Limit int
Consume func(ByteBatch)
}
ByteIngressConfig owns the complete construction contract of a ByteIngress. Dispatcher and Consume are required. Limit is the maximum number of bytes accepted but not yet taken by the interface owner and must be positive.
type Clipboard ¶ added in v0.0.5
type Clipboard struct {
// contains filtered or unexported fields
}
Clipboard is the clipboard associated with the user-facing host. Its zero value refuses writes and reads.
func (Clipboard) Paste ¶ added in v0.0.5
Paste requests clipboard contents and reports whether the host accepted the request. False means the host cannot read the clipboard or an earlier unidentified request is still in flight. An accepted answer arrives asynchronously as input.Paste; true does not mean the terminal will answer.
type Component ¶
Component is an interface a program can run: it draws itself into the space it is given, and says whether it wants an event.
It is handed a view that is already positioned and clipped, so its coordinates are its own. An event it does not consume is dropped by the program — a component is the root of its own tree and there is nobody above it to pass one on to.
type Config ¶
type Config struct {
// Root builds the component to run on a screen of its own. It is given the runtime
// first, so the component can hold it from the moment it exists. Returning nil is
// an error.
Root func(*Runtime) Component
// Inline builds the component to run as a block in the terminal's own screen,
// with output that is finished printed above it. Its component is given an
// [InlineRuntime], which is a [Runtime] that can also print.
// Returning nil is an error.
Inline func(*InlineRuntime) Component
// Terminal says which optional terminal features to request. Run applies them
// when opening the local terminal; a transport adapter may interpret them before
// supplying Host. A Host passed directly to Run already owns its setup and
// receives none of these settings.
//
// Screen ownership is deliberately absent. Root owns the alternate screen and
// Inline owns the terminal's ordinary screen, so the rendering model has exactly
// one entry point and cannot be contradicted by terminal configuration.
Terminal term.Features
// Color says how much colour the terminal can show. The zero value, [grid.Auto],
// asks the host's optional [ColorHost]. A local terminal derives that answer with
// [term.DetectDepth]; a host without the capability safely uses no colour.
//
// Setting it is how a program that already knows — from its own configuration,
// or because it is writing to something that is not a terminal at all — takes
// that decision back.
Color grid.Depth
// Host overrides where input comes from and frames go. Nil opens the real terminal
// and gives it back on the way out.
Host Host
// FrameInterval is the shortest time between redraws. Zero uses
// [DefaultFrameInterval]; a negative duration is invalid.
FrameInterval time.Duration
}
Config is what a program needs to run.
Exactly one of Root and Inline says what to run, and which one it is decides where the interface is drawn: Root takes a screen of its own, Inline draws in the terminal's own screen and prints finished output into its scrollback.
Example ¶
Config is what a program needs to run, and passing it to program.Run takes the terminal and gives it back on the way out — including when the component panics or the context is cancelled.
Exactly one of Root and Inline says what to run. Nothing here starts a frame loop that runs regardless: a component that wants a clock asks for one, and an interface with nothing scheduled costs nothing.
The examples in this package validate a configuration rather than running it, because running one would take the terminal the test is printing to.
package main
import (
"fmt"
"time"
"github.com/Tangerg/oolong/core/grid"
"github.com/Tangerg/oolong/core/input"
"github.com/Tangerg/oolong/core/program"
"github.com/Tangerg/oolong/core/term"
)
// clock is a component: it draws itself, and says whether it wants an event. There is
// no base type to embed and no lifecycle to implement.
type clock struct {
runtime *program.Runtime
now time.Time
}
func (c *clock) Draw(view grid.View) {
view.Text(0, 0, c.now.Format(time.TimeOnly), grid.Style{})
view.Text(0, 2, "q to quit", grid.Style{Attr: grid.Dim})
}
func (c *clock) Handle(event input.Event) bool {
key, ok := event.(input.Key)
if !ok || !key.Down() || key.Rune != 'q' {
return false
}
c.runtime.Quit()
return true
}
func main() {
config := program.Config{
Root: func(runtime *program.Runtime) program.Component {
c := &clock{runtime: runtime, now: time.Now()}
runtime.Every(time.Second, func() { c.now = time.Now() })
return c
},
// Optional terminal behaviours are requested, never assumed.
Terminal: term.Features{Mouse: true, Focus: true},
}
fmt.Println(config.Validate())
}
Output: <nil>
Example (Inline) ¶
Config.Inline draws in the terminal's own screen instead of taking one, with the session's output above it. What such an interface has finished with is printed with program.InlineRuntime.Print and belongs to the terminal from then on — scrollable, selectable, and still there after the program exits.
Which of the two is set decides where the interface lives, so setting both is a configuration with no answer and Validate says so.
package main
import (
"fmt"
"time"
"github.com/Tangerg/oolong/core/grid"
"github.com/Tangerg/oolong/core/input"
"github.com/Tangerg/oolong/core/program"
)
// clock is a component: it draws itself, and says whether it wants an event. There is
// no base type to embed and no lifecycle to implement.
type clock struct {
runtime *program.Runtime
now time.Time
}
func (c *clock) Draw(view grid.View) {
view.Text(0, 0, c.now.Format(time.TimeOnly), grid.Style{})
view.Text(0, 2, "q to quit", grid.Style{Attr: grid.Dim})
}
func (c *clock) Handle(event input.Event) bool {
key, ok := event.(input.Key)
if !ok || !key.Down() || key.Rune != 'q' {
return false
}
c.runtime.Quit()
return true
}
func main() {
inline := program.Config{
Inline: func(runtime *program.InlineRuntime) program.Component {
return &clock{runtime: runtime.Runtime, now: time.Now()}
},
}
fmt.Println("inline:", inline.Validate())
both := inline
both.Root = func(*program.Runtime) program.Component { return nil }
fmt.Println("both:", both.Validate() == nil)
}
Output: inline: <nil> both: false
func (Config) TerminalConfig ¶ added in v0.13.0
TerminalConfig derives the complete terminal-session configuration selected by c.
It is the adapter boundary between a program's rendering model and a concrete terminal transport. Root owns the alternate screen, Inline owns the ordinary screen, and Terminal contributes only optional features. Keeping that projection here gives local, SSH, and future transports one answer instead of making each reconstruct the ownership rule.
Call Config.Validate before acquiring transport resources; this projection does not make an otherwise invalid root selection runnable.
type CopyHost ¶ added in v0.0.5
CopyHost writes to the clipboard associated with the user-facing host.
type DirectoryHost ¶ added in v0.0.5
DirectoryHost tells a terminal how to resolve relative paths in program output.
type Dispatcher ¶ added in v0.0.5
type Dispatcher struct {
// contains filtered or unexported fields
}
Dispatcher is a copyable, concurrency-safe handle into a running program. Its zero value drops work. It deliberately exposes no owner-only operation.
func (Dispatcher) Done ¶ added in v0.1.0
func (d Dispatcher) Done() <-chan struct{}
Done is closed when the program can no longer accept or apply work. The zero Dispatcher's channel is already closed. A background producer selects on Done to stop work that has no remaining owner.
func (Dispatcher) Post ¶ added in v0.0.5
func (d Dispatcher) Post(fn func())
Post runs fn on the interface goroutine and requests a frame afterwards. A nil function requests only the frame. Calls never wait, preserve FIFO acceptance order, and are dropped after the program stops or on a zero Dispatcher.
Never waiting is the whole point and it is also the whole cost: the queue has no upper bound, so a producer posting faster than the interface goroutine can drain grows it without limit. Nothing here can fix that, because the only two things a queue can do when it is full are block the caller and lose work, and this edge exists to do neither. Coalescing therefore belongs to the caller, at the source: post the state a burst arrived at rather than one call per item, or keep the state somewhere the interface goroutine reads and post nothing but a request for a frame — which is what a nil function is for, and why several of them collapse into one. Runtime.Every is the same discipline applied to a clock.
type Environment ¶ added in v0.0.5
type Environment struct {
// contains filtered or unexported fields
}
Environment is the stable set of terminal facts learned before a program runs. Its zero value reports that nothing was learned.
func (Environment) Color ¶ added in v0.11.0
func (e Environment) Color() grid.Depth
Color reports the colour depth of the user-facing terminal.
func (Environment) Ground ¶ added in v0.0.5
func (e Environment) Ground() grid.Ground
Ground reports the host's foreground and background colours when known.
func (Environment) Keyboard ¶ added in v0.0.5
func (e Environment) Keyboard() (input.KeyboardFeatures, bool)
Keyboard reports negotiated keyboard protocol features.
func (Environment) Locale ¶ added in v0.8.0
func (e Environment) Locale() string
Locale reports the character locale of the user-facing terminal. An empty string means the host made no claim; it does not mean the process locale should be used.
func (Environment) Wheel ¶ added in v0.0.5
func (e Environment) Wheel() input.Wheel
Wheel reports how host wheel events should be scaled.
type EventSource ¶ added in v0.1.0
EventSource is one ordered input stream and its terminal result.
Events closes after the last event. Once it has closed, Err reports why the stream ended: nil means a clean end of input, while a non-nil error is the transport failure that ended it. Err must not report end-of-file as a failure.
The two methods form one lifecycle. Keeping the result on the source avoids a race between an event channel closing and a separate error channel becoming readable, and follows the same iteration-then-error shape as a scanner.
type FrameWriter ¶ added in v0.0.5
type FrameWriter interface {
// Queue takes ownership of frame. The caller does not read or change the slice
// after the call; an asynchronous implementation may retain it without copying.
// Every accepted call returns a non-zero sequence strictly greater than earlier
// sequences from the same writer. Written reports a watermark in that sequence
// space.
Queue(frame []byte) uint64
// Changes coalesces state changes; the receiver must re-read Written and Err.
Changes() <-chan struct{}
Written() uint64
Err() error
Drain(timeout time.Duration) error
}
FrameWriter is the part of a frame queue the program needs.
It is defined by the consumer rather than exposing term.Writer through Host. Implementations must preserve queue order, report successful completion as a watermark and be safe for concurrent use. Changes returns one stable, single-consumer channel for the writer's lifetime; a receive means Written or Err may have changed. Closing it means the writer has permanently stopped and Err must report the cause. Drain returns nil only after every frame accepted before the call has either been written or accounted for. term.Writer is the standard implementation.
type GroundHost ¶ added in v0.0.5
GroundHost supplies the colours discovered before a program starts.
type HandoverHost ¶ added in v0.0.5
HandoverHost temporarily gives exclusive ownership of its display to run.
type Host ¶
type Host interface {
// Input is the ordered input stream and the reason it eventually ends.
Input() EventSource
// Writer is where frames go. The interface is defined here, where it is used;
// a host is not coupled to the terminal package's concrete writer.
Writer() FrameWriter
// Size is the terminal's size in cells. The result must satisfy [ValidateSize].
Size() (w, h int, err error)
}
Host is where a program's input comes from and its frames go.
A program opens the real terminal unless it is given one of these. Being able to supply it is what lets an interface be driven and inspected in a test, with no terminal in sight.
Everything beyond transport is optional. Each independently useful operation is represented by a small consumer interface such as GroundHost, CopyHost or NotifyHost, so implementing one never silently depends on implementing its neighbours. ImageHost is the exception because its transport and geometry form one protocol. Absent capabilities receive harmless defaults.
type ImageHost ¶ added in v0.0.5
type ImageHost interface {
Graphics() graphics.Protocol
CellSize() (image.Point, bool)
Transmit(png []byte) (graphics.Image, error)
}
ImageHost transmits images and reports the protocol geometry needed to place them. These methods form one capability: a handle from Transmit cannot be placed without the protocol and cell geometry that interpret it.
type Images ¶ added in v0.0.5
type Images struct {
// contains filtered or unexported fields
}
Images is the host's image transport. Its zero value reports no protocol and refuses transmission.
func (Images) CellSize ¶ added in v0.0.5
CellSize reports one terminal cell's pixel size when known.
type InlineRuntime ¶ added in v0.0.5
type InlineRuntime struct{ *Runtime }
InlineRuntime is a Runtime that can publish completed output into terminal scrollback. It is only constructed for Config.Inline, and its zero value is inert.
func (*InlineRuntime) Append ¶ added in v0.0.5
func (r *InlineRuntime) Append(draw func(grid.View) bool)
Append continues the last published row until draw reports completion.
func (*InlineRuntime) Print ¶ added in v0.0.5
func (r *InlineRuntime) Print(p grid.Drawable)
Print publishes a measured drawable above an inline interface.
type KeyboardHost ¶ added in v0.0.5
type KeyboardHost interface {
Keyboard() (input.KeyboardFeatures, bool)
}
KeyboardHost supplies keyboard protocol features negotiated with the host.
type LocaleHost ¶ added in v0.8.0
type LocaleHost interface{ Locale() string }
LocaleHost supplies the character locale of the user-facing terminal.
type NotifyHost ¶ added in v0.0.5
type NotifyHost interface{ Notify(text string) }
NotifyHost sends a notification through the user-facing host.
type PasteHost ¶ added in v0.0.5
type PasteHost interface{ Paste() bool }
PasteHost requests text from the clipboard associated with the user-facing host. Paste reports whether the request was accepted; answers arrive asynchronously through EventSource.Events as an input.Paste.
type ProgressHost ¶ added in v0.5.0
ProgressHost presents task progress outside the interface's cell grid.
type Runtime ¶ added in v0.0.5
type Runtime struct {
// contains filtered or unexported fields
}
Runtime is the program resource owned by the interface goroutine.
It is concrete rather than a provider-defined interface: consumers that need only a subset declare that interface where they use it. Background work receives only Runtime.Dispatcher, preserving ownership in the type system. Host features are grouped into the concrete Environment, Clipboard, Session and Images values rather than flattened into one capability catalogue. The zero value is inert; it is safe to embed in an object that has not been attached to a program.
func (*Runtime) After ¶ added in v0.5.0
After schedules fn once on the interface goroutine after d. A non-positive delay makes fn ready for the next owner turn; it does not call fn inline. The returned stop function is concurrency-safe and idempotent. Stop prevents work that has not begun; when it races with the callback starting, either may win.
func (*Runtime) Dispatcher ¶ added in v0.0.5
func (r *Runtime) Dispatcher() Dispatcher
Dispatcher returns the concurrency-safe handle for background work.
Example ¶
One goroutine draws and handles input, and state reached only from it needs no lock. Work that happens anywhere else — a request finishing, a file changing, a timer firing — comes back through a Dispatcher and runs there.
This is the whole concurrency model. There is no second rule.
package main
import (
"fmt"
"io"
"net/http"
"github.com/Tangerg/oolong/core/program"
)
func main() {
type page struct {
runtime *program.Runtime
status string
}
const endpoint = "https://example.test/status"
load := func(p *page) {
// Taken here because this is the goroutine that owns the runtime; asking the
// runtime for one from inside the goroutine below would be the race this
// whole arrangement exists to remove.
post := p.runtime.Dispatcher()
go func() {
response, err := http.Get(endpoint) //nolint:noctx // an example, not a client
// Back on the program goroutine: assigning p.status here is safe, and
// assigning it in the goroutine above would not have been.
post.Post(func() {
if err != nil {
p.status = "unreachable"
return
}
defer func() {
_, _ = io.Copy(io.Discard, response.Body)
_ = response.Body.Close()
}()
p.status = response.Status
})
}()
}
_ = load
fmt.Println("work returns to the goroutine that owns the state")
}
Output: work returns to the goroutine that owns the state
func (*Runtime) Environment ¶ added in v0.0.5
func (r *Runtime) Environment() Environment
Environment returns the host facts available to this runtime.
func (*Runtime) Every ¶ added in v0.0.5
Every schedules coalesced ticks on the interface goroutine. A non-positive interval or nil fn schedules nothing.
type Session ¶ added in v0.0.5
type Session struct {
// contains filtered or unexported fields
}
Session controls the live terminal session around rendered frames. It groups operations that must remain ordered with the interface owner's state. Its zero value performs harmless notification no-ops and can hand control to a callback, but cannot suspend a process.
It holds the runtime rather than the resolved services the other capabilities hold, because two of its methods need the owner itself and not what the host can answer: see Session.Hand.
func (Session) Bell ¶ added in v0.0.5
func (s Session) Bell()
Bell asks the host for the user's attention.
func (Session) Hand ¶ added in v0.0.5
Hand gives exclusive display ownership to run and repaints after it returns. If pending frames cannot drain, it returns ErrFrameTimeout without calling run.
func (Session) ReportDirectory ¶ added in v0.0.5
ReportDirectory tells the host which directory relative links belong to.
func (Session) SetProgress ¶ added in v0.5.0
SetProgress presents task progress outside the cell grid when supported. The zero value clears an earlier value. It is independent of progress drawn inside the interface, which remains ordinary component state.