shellterm

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 9, 2026 License: BSD-3-Clause Imports: 10 Imported by: 0

README

shellterm

A line-oriented terminal for go-widgets. It puts a line editor (go-widgets/lineedit: history, Tab completion, Ctrl-R, Ctrl-C, Ctrl-L) in front of a Shell you supply, and draws into a toolkit.Terminal. Each application keeps only its shell.

go get github.com/go-widgets/shellterm

Use

type Shell interface {
    Run(ctx context.Context, line string, out io.Writer) (code int, clear bool)
}

s := shellterm.New(shellterm.Options{
    Post:     post,                // runs a func on the UI goroutine (required)
    Greeting: "my shell\r\n",      // written once, before the first prompt
    OnDone:   refresh,             // optional: after each command, on the UI goroutine
})
s.SetShell(myShell, myCompleter) // a lineedit.Completer, or nil
term := toolkit.NewTerminal(80, 24, 5000)
shellterm.Bind(term, s, invalidate) // typing → s, output → term, clear → screen and scrollback

example_test.go is the same code, compiled by go vet on every change.

How a session behaves:

  • The prompt: a submitted line goes to Shell.Run on a goroutine of its own. Its output becomes terminal output (\n → \r\n). A non-zero status shows [exit status N], then the prompt returns.
  • While a command runs: only Ctrl-C reaches it (it cancels ctx). Running (an mvvm.Observable[bool]) says so.
  • Keys: they are handled on the session's goroutine, never the UI's, so a Completer may ask a slow back end. Each key's work is bounded by CompleteTimeout.
  • Note(text): shows a line above the prompt and keeps the line being edited below it. Nothing is shown while a command runs.
  • Bounds: at most InputQueue keystrokes wait (more are dropped, never blocking the UI); at most PendingWrites outputs are kept before the first Attach.
  • Hosts: a host without toolkit.Terminal calls Attach(out, clear), Input and Resize itself.

Font and cell size: set them on the terminal: term.SetFont(face), term.SetCellSize(16, 16) (toolkit v0.362.0).

Origin

Extracted from go-wasmcontainers/dashboard v0.8.0 (vm/terminal.go) for go-widgets/toolkit#548, so that two consumers share one implementation:

  • the go-wasmcontainers dashboard and playground;
  • wasmdesk/wasmbox (wasmdesk/wasmbox#173).

What changed in the move:

  • The dashboard's app queue became Options.Post, and its refresh became Options.OnDone.
  • The wctr greeting became Options.Greeting.
  • The "# dashboard:" note became the generic Note; the caller formats and cleans its text.
  • wctr's shell and completer stay in go-wasmcontainers.

Tests

  • 100 % statement coverage, gated in CI.
  • The session tests run under -race against a real vt.Screen and a serialised UI goroutine, and cover: the greeting, history kept across SetShell, Ctrl-C at the prompt and during a command, exit status, clear and Ctrl-L, completion listings, notes, and the queue bounds.
  • CI runs on Linux (amd64 and arm64), macOS, Windows and 32-bit Linux, vets js/wasm, and runs govulncheck.
  • Untrusted input (keys, terminal bytes) is parsed by lineedit, which fuzzes it in its own CI. Untrusted output goes to toolkit.Terminal, which is safe for it by construction.

Licence

BSD 3-Clause, see LICENSE.

Documentation

Overview

Package shellterm is a line-oriented terminal: a line editor (github.com/go-widgets/lineedit: history, Tab completion, Ctrl-R, Ctrl-C, Ctrl-L) in front of a Shell that the caller supplies, drawing into a terminal (Bind connects a toolkit.Terminal).

A Session is the model. Input arrives as the bytes a terminal sends; output goes to the function given to Attach. Keys are handled on the session's own goroutine, never the UI's: a Completer may ask a slow back end, and must not block the event loop that delivers its answer. Everything the session hands the view goes through Options.Post, which runs a function on the UI goroutine (an mvvm queue's Post, or a plain host's equivalent), so the view is only ever touched from there.

It was extracted from go-wasmcontainers/dashboard v0.8.0 (vm/terminal.go) so that the dashboard and wasmdesk share one implementation.

Example

A session in front of a toolkit.Terminal. post runs a function on the UI goroutine; here it runs it at once, as a single-threaded host would.

package main

import (
	"context"
	"fmt"
	"io"
	"strings"

	"github.com/go-widgets/lineedit"
	"github.com/go-widgets/shellterm"
	"github.com/go-widgets/toolkit"
)

// echo is a Shell: it prints its line back, and "clear" clears the screen.
type echo struct{}

func (echo) Run(_ context.Context, line string, out io.Writer) (int, bool) {
	words, err := lineedit.Split(line)
	if err != nil {
		fmt.Fprintln(out, "syntax error:", err)
		return 2, false
	}
	if len(words) > 0 && words[0] == "clear" {
		return 0, true
	}
	fmt.Fprintln(out, strings.Join(words, " "))
	return 0, false
}

// A session in front of a toolkit.Terminal. post runs a function on the UI
// goroutine; here it runs it at once, as a single-threaded host would.
func main() {
	post := func(f func()) { f() }
	term := toolkit.NewTerminal(80, 24, 1000)
	s := shellterm.New(shellterm.Options{Post: post, Greeting: "echo shell\r\n"})
	s.SetShell(echo{}, func(_ context.Context, words []string, _ string) ([]lineedit.Candidate, bool) {
		if len(words) == 0 {
			return []lineedit.Candidate{{Value: "clear"}}, false
		}
		return nil, false
	})
	shellterm.Bind(term, s, func() { /* ask the host to redraw */ })
	// The host now draws term and routes its events to it: typed keys
	// reach s, and s's output reaches term.
}

Index

Examples

Constants

View Source
const (
	// InputQueue bounds the keystrokes waiting for the key goroutine; keys
	// past it are dropped rather than blocking the UI.
	InputQueue = 256
	// PendingWrites bounds the output kept before the first Attach.
	PendingWrites = 256
	// CompleteTimeout bounds one key's work, a Tab completion included.
	CompleteTimeout = 5 * time.Second
)

Limits on what waits for the view or for the key goroutine.

Variables

This section is empty.

Functions

func Bind

func Bind(term *toolkit.Terminal, s *Session, invalidate func())

Bind connects a session to a toolkit.Terminal: what the user types goes to the session, a resize tells it the width, and its output and clears go to the terminal, after which invalidate (nil: nothing) asks the host to redraw. A clear empties the screen and the scrollback. Call it on the UI goroutine; it attaches the session.

Types

type Options

type Options struct {
	// Post runs f on the UI goroutine, in order. Required: New panics
	// without it, since a session that wrote from its own goroutine would
	// race the view.
	Post func(f func())
	// Greeting is written once, at the first Attach, before the prompt.
	Greeting string
	// OnDone, if set, runs on the UI goroutine after each command, once its
	// output and the prompt are written (to refresh what it changed).
	OnDone func()
	// HistorySize bounds the history (0: 500).
	HistorySize int
}

Options configure a Session.

type Session

type Session struct {

	// Out receives everything to draw and Clear clears the screen; both
	// are set by Attach and called on the UI goroutine.
	Out   func(string)
	Clear func()
	// Running says a command runs (only Ctrl-C reaches it). It is set on
	// the UI goroutine.
	Running *mvvm.Observable[bool]
	// contains filtered or unexported fields
}

Session is the terminal's model.

func New

func New(o Options) *Session

New returns a session with no shell (a submitted line says so) until SetShell.

func (*Session) Attach

func (t *Session) Attach(out func(string), clear func())

Attach connects the view and starts the session: greeting, prompt, and the goroutine that handles keys. A second Attach (a new view) replays nothing and greets no more. Called on the UI goroutine. clear may be nil: the escape sequences that clear the screen are written instead.

func (*Session) History

func (t *Session) History() []string

History returns the lines entered, oldest first (for persistence).

func (*Session) Input

func (t *Session) Input(data string)

Input queues what the user typed. It never blocks (keys past a full queue are dropped).

func (*Session) Note

func (t *Session) Note(text string)

Note shows one line of text above the prompt, the line being edited kept below it; nothing while a command runs or before Attach. The text is written as given (escape sequences included): clean anything untrusted first. Called on the UI goroutine, which it never blocks (see Resize).

func (*Session) Resize

func (t *Session) Resize(cols int)

Resize tells the editor the terminal's width. It never blocks the caller (the UI goroutine): the key goroutine may hold the editor while a completion waits.

func (*Session) SetShell

func (t *Session) SetShell(s Shell, complete lineedit.Completer)

SetShell sets what runs the lines and what completes them (nil: no completion). The history is kept.

type Shell

type Shell interface {
	Run(ctx context.Context, line string, out io.Writer) (code int, clear bool)
}

Shell runs one command line, writing its output to out, and returns the exit status and whether the screen should be cleared. It runs on a goroutine of its own; ctx is cancelled by Ctrl-C.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL