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",
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() {
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.Node
s then it is said that m branches of from
n iff m is in n.Callback.
Let n and m be term.Node
s then m is reachable from n iff a
sequence of term.Node
s 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.