term

package module
v0.0.0-...-10cece6 Latest Latest
Warning

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

Go to latest
Published: Feb 8, 2025 License: MIT, Unlicense Imports: 16 Imported by: 0

README

term -- for simple terminal apps

This package augment go's term package to provide a context oriented, REPL-like, self explaining and easy to consume terminal ui api. E.g.:

    package main

    import (
        "fmt"
        "log"

        "git.sr.ht/~slukits/term"
    )

    func init() { log.Default().SetFlags(0) }

    var root = term.Node{
        Label: "terminal ui", // the initial prompt
        Callback: func(_ term.Terminal) (term.Nodes, term.Inputs) {
            return term.Nodes{{
                Label: "hallo",
                Help: "prints 'world'",
                Callback: helloEndpoint,
            }, {
                Label: "echo",
                Help: "repeats a user's input",
                Callback: echoEndpoint
            }}, nil
        },
    }

    func helloEndpoint(trm term.Terminal) (term.Nodes, term.Inputs) {
        fmt.Fprintln(trm, "world")
        return nil, nil
    }

    func echoEndpoint(trm term.Terminal) (term.Nodes, term.Inputs) {
        return nil, echoInput
    }

    func echoInput(ii term.Inputs) Nodes {
        ii.SetPromptSuffix("your input please")
        ii.OnInput(func(ii term.Inputs) term.Node {
            ii.SetPromptSuffix("press any key")
            fmt.Fprintf(ii.Terminal(), 
                "your input was: '%s'\n", ii.String())
            ii.OnKeyPress(func(ii term.Inputs) term.Node {
                return root
            })
        })
    }

    func main() {
        // StartCallbackLoop blocks until user inserts ctrl-c/ctrl-d
        if err:= term.Start(root); err != nil {
            log.Fatal(err)
        }
    }

Above is the term-version of a hello world program including basic input processing. The basic idea is that a consumer of this package only needs to implement endpoints where one can choose to provide further nodes or not or an input collector. Is no input collector and no nodes returned the node is called a command node, it is called a context node if nodes are returned but no input collector and it is called an input node if an input collector is returned. I.e. helloEndpoint is a command node and echoEndpoint is an input node while root is a context node. Since an endpoint gets an Terminal implementation to write to, one can easily test endpoints against one owns implementations. Of course in this case it is advisable to define ones own interface to not have the test implementations brake if the Terminal interface gets extended.

Note in the term-ui the question mark user input shows the ui-help display while the pressing enter without any other input shows the context-help display. The ui supports auto-completion and cycling through autocompletion along with the feature of golang.org/x/term.

Specifics

  • a consumer of the term package ideally doesn't need to write functional ui-code, i.e. the ui is data driven.

  • the terminal ui should be able to make hundreds of features available with a few keystrokes.

  • the terminal ui should be self explaining.

The data structure and its interpretation
uiDef := term.Node {
    Label: "prompt",
    Short: "prmpt",
    Help: "the first node of an ui-definition is its root node",
    Callback: func(_ term.Terminal) term.Nodes {
        return term.Nodes{
            {Label: "node_1"}, /* ... */, {Label: "node_n"}}
    },
}

The Node type is the central (data) type that drives a term-ui. Let n be a term.Node.

n is called invalid if its label is zero or if its callback is zero or if two nodes n_i, n_j in n.Callback's return value exist with n_i =/= n_j and n_i.Label == n_j.Label or n_i.Short == n_j.Short.

For the following definitions it is assumed that a node is valid.

Let n be a term.Node then n is called a command-node iff n.Callback's return value is zero.

Let n be a term.Node then n is called a context-switch iff n.Callback's return value is not zero. A node n' which is in the return value of n.Callback is called context node of n. Note n.Callback denotes the set of n's callback nodes.

Let n and m be term.Nodes then it is said that m branches of from n iff m is in n.Callback.

Let n and m be term.Nodes then m is reachable from n iff a sequence of term.Nodes exists n(1), ..., n(p) with n(1) == n, n(p) == m and for n(i), n(i+1) branches n(i+1) of from n(i) while i in {1, ..., p-1}. Then the sequence n(1), ..., n(p) is called the path from n to m and denoted with P(n, m).

Is P(n, m) == {n(1), ..., n(p)} with p > 1 a path from n to m then the joining of the strings s(1), ..., s(p) with the separator : is called the string representation of P(n, m) denoted by S(P(n, m)) iff an s(i) with i in {1, ..., p-1} is n(i).Short if n(i).Short is not zero and n(i).Label otherwise while s(p) == n(p).Label.

Note S(P(n, n)) == n.Label.

Navigating the ui
////////////////////////////////////////////////////////////////////////
//////////////////////// display-content ///////////////////////////////
////////////////////////////////////////////////////////////////////////
prompt > user-input

Above sketches the terminal ui where user-input is mapped to its callback that sets typically the display-content or switches the context. Next to mapping input to callbacks also maintaining the prompt is done by the term-package.

Let r be a term.Node then r is called an term-ui's root node or its root context iff it is passed into term.StartCallbackLoop. term.StartCallbackLoop will fail if r is invalid or r.Callback is zero. The string representation of P(r, r) is called the initial prompt and is set as ui prompt for the user's first input.

Note the prompt suffix -- > in above example -- is a setting of the term-package and not part of a path's string representation.

Let r be the root node of a term-ui and ui(1) is the first user input in the root context. Exists a context node n of r with n.Label == ui(1) it is said the user has n selected.

Note if the user inputs the first letter the first found node label of the current context which starts with that letter is auto completed. Pressing the tab-key provides the next label with the same first letter. Is the tab key pressed without any other input the ui starts cycling through all context nodes labels.

Note a node's context nodes are always processed alphabetically ordered by the ui.

Let r be the root node of a term-ui and n a selected node of r's context. Then n.Callback is executed. Is its return value not zero a context switch happens:

  • the prompt r.Label > is transformed to r.Short: n(i).Label > (is r.Short unset r.Label is used instead).

  • n(i) context nodes become selectable while nodes from the root context can't be selected anymore.

  • the backspace key becomes available to go back to the root context.

Then we call P(r, n) the current context and its string representation the name, label or prompt of the current context.

Is P(r, m) the current context with the prompt S(P(r, m)) > , ui a user input that selects the node n of m's context. Then n's callback is executed. Is its return value not zero S(P(r, m)): n.Label > becomes the prompt and n's context-nodes become selectable while m's context nodes are not selectable anymore. Is the next user input a backspace a context switch back from P(r, n) to P(r, m) happens.

Printing to the ui

A callback is provided with a term.Terminal implementation which is also an alias for an io.Writer, i.e. the callback can write to the terminal. When the callback returns no more writes to the terminal are accepted. A terminal also provides width and height for layout calculations. term also provides control sequences like term.NL or term.Cell to create (tabular) formatting.

Has the write to the terminal more lines than its height the print to the physical terminal stops at its full height and further writes are cached. Cached terminal content is available through the PgDown key.

Help display

Let n be a node and n' one of its context nodes. Then

        n'.Label - n'.Help

is called a context help item of n. Note a context help item's print ends in a new-line. The alphabetic sequence of n's context help items prefixed by a context help header is called n's context help (section).

A keyboard key k is called special if it is either the return key, the tab key, the backspace key, the PgDown key, the PgUp key or the question mark key.

Note special keys control the ui under specific circumstances. E.g.: tab cycles through auto completions, backspace switches to the parent context or PgDown that scrolls the terminal content.

Let k be a special key then

        k.String() - ui help-text for k

is called an ui help item. Note a ui help item print ends in a new-line. The alphabetic sequence of calculated ui help items prefixed by a ui help header is called n's ui help (section).

Displaying the help display

The context help display is always automatically displayed if a context is entered the first time. It can be requested by the user pressing the enter key while the ui help display is printed if the user inserts the question mark.

Documentation

Overview

Package term offers a (data) type term.Node that allows to define a user interface using callbacks:

package main

import (
	"fmt"
	"log"

	"git.sr.ht/~slukits/term"
)

func init() { log.Default().SetFlags(0) }

var uiDef = term.Node{
	Label: "terminal ui", // the initial prompt
	Callback: func(_ term.Terminal) (term.Nodes, term.Inputs) {
		return term.Nodes{{
			Label:    "hello",
			Help:     "responds with 'world'"
			Callback: hello,
		}, {
			Label: "echo",
			Help: "repeats a user's input",
			Callback: echoEndpoint
		}}, nil
	},
}

func hello(trm *term.Terminal) (term.Nodes, term.Inputs) {
	fmt.Fprintln(trm, "world\r")
	return nil, nil
}

func echoEndpoint(trm term.Terminal) (term.Nodes, term.Inputs) {
	return nil, echoInput
}

func echoInput(ii term.Inputs) Nodes {
	ii.SetPromptSuffix("your input please")
	ii.OnInput(func(ii term.Inputs) term.Node {
		ii.SetPromptSuffix("press any key")
		fmt.Fprintf(ii.Terminal(),
			"your input was: '%s'\n", ii.String())
		ii.OnKeyPress(func(ii term.Inputs) term.Node {
			return root
		})
	})
}

func main() {
	if err:= trm.StartCallbackLoop(uiDef); err != nil {
		log.Fatal(err)
	}
}

Above is the `term`-version of a hello world program including basic input processing. The basic idea is that a consumer of this package only needs to implement endpoints where one can choose to provide further nodes or not or an input collector. Is no input collector term.Inputs and no term.Nodes returned the node is called a "command node", it is called a "context node" if nodes are returned but no input collector and it is called an "input node" if an input collector is returned. I.e. `helloEndpoint` is a command node and `echoEndpoint` is an input node while `root` is a context node. Since an endpoint gets a Terminal implementation to write to, one can easily test endpoints against one owns implementations. Of course in this case it is advisable to define ones own interface to not have the test implementations brake if the Terminal interface gets extended.

Note in the term-ui the question mark user input shows the ui-help display while the pressing enter without any other input shows the context-help display. The ui supports auto-completion and cycling through autocompletion along with the feature of golang.org/x/term.

Index

Constants

View Source
const (
	CtrlC = internal.CtrlC
	Cr    = internal.Cr
	BS    = internal.BS
	TAB   = internal.TAB
	QM    = internal.QM
	CtrlN = internal.CtrlN
	CtrlP = internal.CtrlP
	CtrlL = internal.CtrlL
)
View Source
const DefaultAppName = "term"
View Source
const (
	InitialBSHelp = internal.InitialBSHelp
)

Variables

View Source
var ErrInputProcessor = errors.New("context: report input: no input processor")
View Source
var ErrInvalidNode = errors.New("invalid node")
View Source
var ErrInvalidRoot = errors.New("invalid root node")
View Source
var ErrNoParent = errors.New("root has no parent")
View Source
var ErrRW = errors.New("setup default stdin read/writer")

ErrRW may be returned from term.Start when the default read/writer fails set up (os.Stdin), i.e. can not be made raw.

View Source
var ErrReadLine = errors.New("terminal: read-line error")

ErrReadLine is returned by Terminal.StartCallbackLoop in case something went wrong during reading the user input.

Functions

func AppName

func AppName(n string) settings.Setting

func Exiter

func Exiter(cb func(FnExiter) FnExiter) settings.Setting

TODO: refac replace exiting by returning form term.REPL.

func MakeRaw

func MakeRaw(cb func(FnMakeRaw) FnMakeRaw) settings.Setting

MakeRaw allows to mock term.MakeRaw for the default read/writer generation which fails under test since we don't have a command line available. With this mock this error can be suppressed but it is only suitable for tests NOT calling Terminal.StartCallbackLoop. For tests calling StartCallbackLoop use the term.ReadWriter setting to mock up the whole read/writer instead of only an aspect of its construction.

func PrintAlwaysContextHelp

func PrintAlwaysContextHelp(b bool) settings.Setting

PrintAlwaysContextHelp indicates if the context help is printed with every context switch.

func REPL

func REPL(root Node, ss ...settings.Setting) error

REPL sets the root context applying all given settings and blocks until the user presses ctrl-c or ctrl-d. REPL calls back to any of root's context nodes n if it is selected by the user and keeps track of the current context. REPL fails if given root node is not valid or the application of a given setting fails.

func ReadWriter

func ReadWriter(rw io.ReadWriter, restore func()) settings.Setting

ReadWriter allows to mock up a terminals read/writer. Since the default read/writer manipulates os.Stdin, creating the default read/writer also sets up the restoring of os.Stdin. Hence a mockup of a terminal's read/writer also must mock the restore feature.

func RegisterAt

func RegisterAt(scc *settings.Contexts) settings.Setting

RegisterAt registers a term.Start at given settings contexts. NOTE this must be the first setting passed into term.Start!

func Sizer

func Sizer(s FnSizer) settings.Setting

Sizer allows to replace the default golang.org/x/term.GetSize which is particularly handy for testing. The sizer is used by a display instanced to calculate the positioning of its content.

func TerminalWriter

func TerminalWriter(w func(FnWriter) FnWriter) settings.Setting

TerminalWriter gets the content of the current terminal flushed.

Types

type Anim

type Anim string

func (Anim) Next

func (a Anim) Next(r rune) rune

func (Anim) Regexp

func (a Anim) Regexp() *regexp.Regexp

type Buffer

type Buffer struct {
	// contains filtered or unexported fields
}

Buffer is a concurrency save bytes Buffer which can be send over channels to avoid permanent reallocation.

func (*Buffer) Bytes

func (bb *Buffer) Bytes() []byte

func (*Buffer) Reset

func (bb *Buffer) Reset()

func (*Buffer) Write

func (bb *Buffer) Write(b []byte) (int, error)

type BusyPrinter

type BusyPrinter func(previous *Buffer, last bool) (next *Buffer)

BusyPrinter implementation may be set on a Node.SetBusyPrinter where you also specify the frequency. In each interval during the node's callback execution the busy printer implementation gets the previously printed bytes and is notified if it is called the last time. Then the busy printer will returns the bytes to be printer next to the terminal. To remove pressure from the garbage collector its reasonable for the previous and next bytes buffer to be the same (create a local bytes copy of previous as needed and use Buffer.Reset before adding next content).

type Callback

type Callback func(Focus) (Nodes, Inputs)

Callback implementations control a nodes relationship to the term-ui in case it has the focus and are set to a Node's Callback field; both return values may be nil. If both return-values are nil then the Callback's Node is called a command-node and the node looses focus instantly after its callback returns. If the callback's returned Nodes are not nil while returned Inputs are the Callback's Node is called a context-node. Is the returned Inputs not nil while returned Nodes may be nil or not the Callback's Node is called an input Node.

type Flag

type Flag uint64
const (
	// GoToParentsParent is evaluated on command nodes and typically
	// used if the context should be left from which the flagged node
	// was called.  E.g. a 'save' command in an 'add user context'.
	GoToParentsParent Flag = 1 << iota
	GoToParent
	PromptAlwaysShort
)

type FnExiter

type FnExiter func(int)

type FnMakeRaw

type FnMakeRaw func(int) (*term.State, error)

FnMakeRaw is the function signature for the setting to mock the "make raw" functionality which defaults to term.MakeRaw (of "golang.org/x/term").

type FnSizer

type FnSizer func() (width, height int, _ error)

FnSizer is a function that takes a file-handle (representing a terminal) an returns in case of a terminal its width and height (i.e. number of columns and rows). FnSizer fails if width and height can't be determined.

type FnWriter

type FnWriter func([]byte) (int, error)

FnWriter is the function to which a Terminal.Write call is passed on. By default its golang.org/x/term.Terminal.Write.

type Focus

type Focus struct {
	// contains filtered or unexported fields
}

func (Focus) Apply

func (env Focus) Apply(ss ...settings.Setting) error

func (Focus) Prompt

func (fcs Focus) Prompt() string

func (Focus) String

func (fcs Focus) String() string

func (Focus) Write

func (env Focus) Write(bb []byte) (int, error)

type Input

type Input struct {
	// contains filtered or unexported fields
}

An Input instance provided to a Nodes term.Inputs return value of its term.Callback function lets the node control how the input is collected and finally retrieve the collected input.

func (*Input) OnInput

func (i *Input) OnInput(cb InputProcessor)

OnInput sets an InputProcessor callback for the next user input terminated by an end of line.

func (*Input) OnKeyPress

func (i *Input) OnKeyPress(cb InputProcessor)

OnKeyPress sets an InputProcessor callback for the next key-press of the user.

func (*Input) SetPromptSuffix

func (i *Input) SetPromptSuffix(s string)

func (*Input) String

func (i *Input) String() string

String returns the string representation of a user input or the zero string if no user input happened.

func (*Input) UintOr

func (i *Input) UintOr(dflt uint) uint

type InputProcessor

type InputProcessor func(*Input) Node

InputProcessor is informed about an input and either wants to collect further input or switch to a new context replacing the input node's parent node. In the former case an Input.On* callback must be set and returned node must be nil. In the later case returned node must not be nil. If an InputProcessor returns while no Node is returned and no Input.On* callback is set term panics.

type Inputs

type Inputs func(*Input)

Inputs is one of the two return values of a term.Callback and if it is not nil it indicates that the Node whose callback returned an not nil input wants to collect a more arbitrary user input then it can be collected by a node selection.

type LayoutDirective

type LayoutDirective uint32
const (
	VUnaligned LayoutDirective = iota + 1
	VCenter
	VBottom
	ContextBody
)

func (LayoutDirective) Byte

func (ld LayoutDirective) Byte() []byte

func (LayoutDirective) String

func (ld LayoutDirective) String() string

type LayoutDirectives

type LayoutDirectives []LayoutDirective

type Node

type Node struct {
	// Label is a mandatory unique string amongst a Nodes neighbors and
	// the expected user input selecting this node which is triggering
	// its callback and in case of children also a context change.
	Label string

	// Short is a shortcut of the label which is used in a terminal's
	// prompt if a Node is a parent context of the terminal's current
	// context.
	Short string

	// Note describes (optionally) the semantics of a selectable node.
	Note string

	// Selected describes (optionally) the semantics of a selected node.
	Selected string

	// Callback is executed when a Node is selected by the user.  A node
	// is invalid if it has no Callback set or there are two different
	// nodes in the callbacks return value with the same label.
	Callback Callback
	// contains filtered or unexported fields
}

A Node defines a context or command of a term user interface.

func (*Node) Flag

func (n *Node) Flag(f Flag, ff ...Flag) *Node

Flag given node n with exceptional properties for example to control context changes.

func (Node) IsZero

func (n Node) IsZero() bool

IsZero returns true if no field of given node n has been set.

func (*Node) SetBusyPrinter

func (n *Node) SetBusyPrinter(bp BusyPrinter, interval time.Duration)

SetBusyPrinter sets given node n's busy-printer which is activated while n's callback is executed.

func (*Node) SetProgressPrinter

func (n *Node) SetProgressPrinter(pp ProgressPrinter)

SetProgressPrinter sets given node n's progress-printer which is activated while n is the active context.

type Nodes

type Nodes []Node

type ProgressPrinter

type ProgressPrinter func() ([]byte, bool)

ProgressPrinter may be set at node where progress should be shown while the node is the active context. I.e. it is only evaluated for context nodes. A context printer may return false to indicate that there is no more progress to be made.

type Spinner

type Spinner struct {
	// Anim the sequence of runes which we animate over; defaults to `-\|/`
	Anim Anim
	// Last for the last print; defaults to -
	Last rune
	// LineFilter indicates the line in which we should find the rune to animate
	LineFilter []byte
	// contains filtered or unexported fields
}

Spinner animates a '-' to '/' => '|' => '\' => '-'. Typically you will want to set Spinner.LineFilter to indicate in which line to find the animation rune. (The first find in that line is animated).

func (Spinner) BusyPrinter

func (s Spinner) BusyPrinter() BusyPrinter

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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