Skip to content

The Sen Terminal (BETA)

Screenshot

The Sen Terminal (term) lets you open buses, find the objects on them, call their methods, watch their events and inspect their types, from one prompt. It is the successor to the shell and the two share the same workflow.

Term is in beta.

Getting it started

Load it into any kernel configuration:

load:
  - name: term
    group: 2
    open:
      - local.demo

Or run it on its own, which starts a kernel with nothing in it but the term and its own bus:

sen term
sen run my_config.yaml

The examples on this page come from the showcase configuration in Sen's own source tree. It puts one object with every kind of method argument on local.demo, ticks its properties so there is live data, and creates two queries. From a source checkout built with tests enabled:

sen run components/term/test/integration/config/term_showcase.yaml

The file's own header lists every editor, property and event it offers. The showcase package is built with the tests, so it is not in a release install; against your own objects, the commands are the same and only the names change.

On startup you get a banner with the Sen version, the compiler, whether this is a debug or a release build, and a quote, then a prompt. The version command has the rest, including the git branch and revision this binary was built from.

exit, shutdown, Ctrl+D on an empty line, or Escape twice all ask the kernel to stop every component gracefully.

Term needs a real terminal on both standard input and standard output. If either one is a pipe or a file, term says so and stops; the rest of the kernel carries on without it. So a configuration that carries term still starts in a build job or under a pipe, with an error in the log and no terminal.

The screen

Output scrolls above a horizontal rule; the prompt sits below it. Command results, log lines and event emissions all arrive in that one stream, in the order they happened.

The prompt shows your current scope, for instance sen:/local.demo❯.

The scrollback holds the last 5000 entries. Past that the oldest go, so a long session does not keep everything you have seen.

Typing and editing

Key Does
Left / Right Move one character
Ctrl+Left / Ctrl+Right Move one word. A dotted path is several words
Home / End Start or end of the line
Backspace / Delete Erase one character either side of the cursor
Ctrl+W Erase the word before the cursor
Up / Down Walk the command history
Ctrl+R Recall the most recent command containing what is on the line
Tab / Shift+Tab Completion, forwards and backwards
PageUp / PageDown Scroll the output ten rows
F1 Run help
Escape Clear the line; on an empty line, arm exit; Escape again exits
Ctrl+D Exit, from an empty line only
Ctrl+Y Copy the current selection to the clipboard

Ctrl+Backspace is not in that table because the term cannot tell it from an ordinary Backspace: FTXUI delivers both as the same event. Use Ctrl+W.

Movement and erasing work in characters, not bytes, so accented letters, CJK text and emoji behave the way you expect. History is kept across sessions in ~/.sen_history.txt (%USERPROFILE% on Windows), capped at 2000 lines and shared with the shell component. With no home directory in the environment, history lasts only as long as the session.

What the line accepts

The command line holds 8192 bytes. Each field of the guided-input form, the form that opens when a method needs arguments, holds the same. Past that, characters are refused and the term says so once; Escape clears the line. A line that was cut is not submitted: pressing Enter says so instead, because sending it would report a parse failure about text you never finished.

A paste arrives as text, not as keystrokes. The term asks the terminal to wrap pasted content in markers, which is the only way to tell a paste from someone typing very fast. Inside those markers a newline becomes a space rather than running the line, and so does a Tab, rather than opening completion. The term says how many line breaks it joined, so a multi-line paste does not look like something you typed. Every other control character in a paste is dropped.

Escape during a paste cancels it and keeps the text received so far, which is the way out if the terminal's end marker never arrives; failing that the term releases itself after two seconds of silence and says so. A marker split across a slow link is handled without either: its pieces are recognised and its tail is not left on your line.

A terminal that does not send the markers gives the term nothing to recognise, and then a paste really is typed: a newline in it submits the line. If your paste runs three commands instead of becoming one line, that is what happened.

A line long enough to wrap shows the prompt's row, the rows around the cursor, and a note saying how many rows are not shown.

Tab completion

Press Tab. One match is inserted; several open a grid above the prompt. Tab again cycles forward, Shift+Tab backward, Enter accepts the highlighted candidate — it does not run the line, so press Enter again to do that — . accepts and lets you keep typing a deeper path, and Escape dismisses the grid and restores what you had typed.

When only one match exists at an intermediate level, Tab inserts it and moves on — so l, Tab can take you to local.demo. in two keystrokes when there is only one session and one bus.

Context What Tab completes
Empty input Every command, and the objects in scope
Partial command (he) Matching commands (help)
Object path (local.) Available buses, then objects
Object + dot (showcase.) Methods, property getters and setters, and print
open argument Sources discovery has seen, whether or not they are open
close argument Currently open sources
cd and ls argument Child scopes, .., -, /, @query names
query argument SELECT, type names, FROM, bus names, WHERE keywords
listen argument Object paths and object.event paths
unlisten argument The same, plus all
log argument level, log level names, logger names
theme argument Available theme names
help argument Command names
inspect and types argument Object paths and type names
units argument Unit category names

Re-opening a source that is already open does nothing and says nothing, so an offer from the open list is not a promise that there is work to do.

Scoping narrows the view, which shortens what you have to type and focuses ls.

sen:/❯ cd local.demo
sen:/local.demo❯ ls
└── showcase [term_showcase.ShowcaseImpl]
sen:/local.demo❯ cd ..
sen:/local❯ cd ..
sen:/❯

A bus is two levels below the root: cd local.demo goes straight to the bus, and cd .. from there leaves you at the session, which lists the buses that session has. cd .. again reaches the root.

cd - returns to the previous scope, cd / and cd with no argument go to the root, and cd @name enters a query.

ls draws the objects in scope as a tree, with each object's class in brackets after its name.

Nested object groups are separated with /, the same way the rest of the path is, so whatever the prompt shows is a target you can type back — leading separator included: cd /local.demo/sensors/indoor and cd sensors then cd indoor reach the same place. A target containing a dot is always read as session.bus, wherever you are, so cd a.b at a bus scope goes to bus b of session a rather than into a group.

ls takes an optional path or @query, and pwd prints the current scope.

Opening and closing buses

sen:/❯ open local.demo
[term] [info]  + source local.demo opened
[term] [info]  + showcase  [ShowcaseImpl, Native]
sen:/❯ close local.demo

A batch of five objects or fewer is listed one object to a line, as above. A larger batch is counted instead: + 12 objects detected, and above fifty the number of distinct classes comes with it.

Those notices go through the term's own logger, which is why they arrive tagged [term] [info]. That also means the log level governs them: after log level warn, open and query do their work and print nothing at all. If a command looks as though it did nothing, check the level with log.

Sources listed under open in the configuration are opened at startup. A query whose FROM clause names an unopened bus opens it for you.

Calling methods

Type the object path, a dot, and the method name.

sen:/local.demo❯ showcase.print

print is the one call every object answers without arguments: it prints the value of every property. A method that needs no arguments runs immediately. The line shows a spinner with an elapsed-time counter while the call is out, then a green check and the return value, or a red cross and the error. Calls are asynchronous and several can be in flight at once. A call that gets no answer within callTimeout is reported as timed out rather than spinning forever.

Arguments on the command line

sen:/local.demo❯ showcase.setFraction 0.75
sen:/local.demo❯ showcase.moveTo {"x": 10, "y": 20}

Simple values go straight after the method name; structures take JSON. Too many arguments, or a value of the wrong type, is refused with the format that was expected:

sen:/local.demo❯ showcase.moveTo 1 2 3
  Error: Invalid arguments
    Too many arguments: expected 1, got 3. Expected format: showcase.moveTo <Point>

The guided-input form

Press Enter on a method that needs arguments and a form opens under the prompt, one field per argument, with an editor chosen for the type. Tab and Shift+Tab move between fields.

Type Editor Keys
bool Toggle checkbox Space to toggle
integer (i32, u64, etc.) Text field with spin Left/Right to increment/decrement, clamped to type range
float (f32, f64) Text field Type decimal numbers
string Text field Free-form text input
enum Cycle selector Left/Right arrows to cycle through enumerators; description shown in hint
Duration Text field Type with unit suffix (e.g., 100ms, 1.5s, 2m)
Timestamp Text field Type as ISO 8601 string
Type Editor Keys
struct Named sub-fields in a tree Tab into each field
sequence Indexed elements, dynamically sized Ctrl+N to add element, Ctrl+X to remove focused element
variant Type selector + value editor Left/Right to change the active type; value editor adapts
optional Presence toggle + value editor Ctrl+O to toggle between empty and filled
quantity Value field + unit selector Edit the numeric value; Left/Right on the unit row to change units

A bounded sequence shows a count and refuses Ctrl+N at capacity. A fixed-size array starts full and refuses both add and remove.

Listening to events

sen:/local.demo❯ listen showcase.thresholdCrossed
  Listening to 'showcase.thresholdCrossed'.

Naming an object without an event listens to all of its events. An emission arrives inline as a bullet, session.bus.object → eventName, with the time on the right, and its arguments indented on the line beneath. An event that carries no arguments is that one line on its own — tick and heartbeat in the showcase are both like this.

You can listen to an object that has not appeared yet: the listener is kept and attached the moment one with that name arrives. Listeners also survive their object disappearing and reconnect when it returns, matched on the object's full name rather than on whatever scope you were in.

unlisten <object> drops the listeners on one object, unlisten all drops every one, and listeners lists them.

Listeners named under listen in the configuration are registered at startup.

Creating queries

A query is a named filter over objects, in Sen's selection language.

sen:/❯ query shown SELECT term_showcase.Showcase FROM local.demo
[term] [info]  + query 'shown' created
sen:/❯ cd @shown
sen:@shown❯ ls
└─┬ local
  └─┬ demo
    └── showcase [term_showcase.ShowcaseImpl]

A query scope lists its objects by their full name, so the session and the bus appear as the two levels above them.

SELECT <Type> FROM <session>.<bus> matches a type, SELECT * matches everything, and WHERE filters on properties. Two queries cannot select the same thing, whatever they are called, and all is reserved. The showcase configuration already defines workers and everything, so those two names and their selections are taken.

queries lists them and query rm <name> removes one. close <session>.<bus>.<name> also removes a query and closes the bus with it if that query was the only reason it was open, but only a query that holds a source of its own: one whose selection matched a bus that was already open shares that bus's source, and close then says it cannot find it. query rm removes either.

Inspecting types and objects

sen:/❯ inspect local.demo.showcase
sen:/❯ inspect term_showcase.Point
sen:/❯ types
sen:/❯ types Worker

Inspecting an object shows its class and inheritance chain, its description, then its properties, methods and events, with types and descriptions, inherited members included. Inspecting a type shows a struct's fields, an enum's values and keys, a sequence's element type and bounds, or a variant's alternatives.

inspect also lists an object's events. Tab knows them too, but only where an event is what you could mean: after listen showcase. or unlisten showcase.. After showcase. on its own it offers methods, property getters and setters, and print.

Pressing Enter on a method with its arguments left off opens the guided form, described above. The signature is printed instead of a form only when an argument is of a kind the form cannot represent. It names each argument as name : Type followed by the argument's description, or the type's own description when the argument has none, and it leaves out the returns line for a method that returns nothing.

units lists the unit categories and the units in each, and takes a filter.

Logging

sen:/❯ log                           # every logger and its level
sen:/❯ log level info                # the global level
sen:/❯ log level myComponent debug   # one logger

Levels are trace, debug, info, warn, error, critical and off. A log line arrives as its sink wrote it, colours and all, so the logger name, the level and whether there is a timestamp are whatever that sink's pattern says rather than something the term adds. The term's own rows do carry a time on the right: command echoes, call results and event emissions.

Two notices say when the term has dropped something rather than shown it. N lines of captured output were dropped: they arrived faster than the screen can draw them means more was written than a screen can take. N further discovery notices were not shown means a burst of objects arrived. Neither loses anything but the notice: the log file sinks and the objects themselves are unaffected.

Logging from other components

Term captures the process's standard output and standard error and shows what arrives on them. A printf, a std::cout, a write(STDOUT_FILENO, …) or a logger's console sink all reach the output pane rather than landing on top of the display.

Use spdlog, and take the logger from the kernel:

// in your component
auto logger = sen::kernel::KernelApi::getOrCreateLogger("my_component");
logger->info("my message: {}", value);

Most components wrap that call in a getLogger() helper of their own, as Logging shows. A logger made with an spdlog factory instead lands in the same registry, so log lists it and log level reaches it, but it misses what the kernel attaches when it makes one: the crash reporter's ring. A logger constructed directly and never registered is in no registry at all. To walk every logger, use KernelApi::applyToAllLoggers.

Themes

sen:/❯ theme nord
  Theme changed to 'nord'.
Theme Style Description
oneDark Dark Atom One Dark
oneLight Light Atom One Light
catppuccinMocha Dark Catppuccin Mocha (warm, pastel, default)
catppuccinLatte Light Catppuccin Latte (warm, pastel)
dracula Dark Dracula (purple accents)
nord Dark Nord (arctic blue)
gruvboxDark Dark Gruvbox Dark (warm retro)
gruvboxLight Light Gruvbox Light (warm retro)
tokyoNight Dark Tokyo Night (blue-purple)
solarizedLight Light Solarized Light (classic)

Set one in the configuration with theme: nord, or in the environment with SEN_TERM_THEME=nord. Precedence runs default (catppuccinMocha), then configuration, then the environment variable.

Kernel status

sen:/❯ status
  Run mode  realTime

  Components
  Name          Group  Cycle time  Objects
  term              2    30.0 Hz        0  realtime
  my_package        1   100.0 Hz       12  realtime
  showcase          1       N/A         5

With a transport active, the output also carries cumulative UDP and TCP bytes sent and received.

Mouse

Scroll the output with the wheel. Drag across text to select it, and the selection is copied to the clipboard when you release, with a note saying how much was copied; Ctrl+Y copies the current selection on demand. Copying uses the OSC 52 terminal escape, so it works over ssh, and also hands the text to a local helper when one is installed: pbcopy on macOS, wl-copy on Wayland, xclip or xsel on X11, and clip on Windows.

Commands

help lists the commands by category, each with a one-line description, and ? is a shorter spelling of it. help <command> prints that command's usage line and its longer description instead of the list.

The list comes from the command table the term dispatches on, so it cannot name a command the build does not have. It is not the whole surface either: shutdown is left out deliberately, and ? and print are dispatched without table entries. The keyboard shortcuts printed under the list are a hand-written list and can drift.

Configuration reference

The configuration options are defined in the component's STL, with the types they refer to:

Term configuration
// How timestamps should be displayed
enum TimeStyle: u8
{
  utc,    // use UTC timestamps
  local,  // use local time timestamps
}

// Color theme for the terminal UI
enum ThemeStyle: u8
{
  oneDark,          // Atom One Dark
  oneLight,         // Atom One Light
  catppuccinMocha,  // Catppuccin Mocha (dark, warm, default)
  catppuccinLatte,  // Catppuccin Latte (light, warm)
  dracula,          // Dracula (dark, purple)
  nord,             // Nord (dark, arctic)
  gruvboxDark,      // Gruvbox Dark (warm retro)
  gruvboxLight,     // Gruvbox Light (warm retro)
  tokyoNight,       // Tokyo Night (dark, blue-purple)
  solarizedLight,   // Solarized Light (classic)
}

// Holds information about a named query
struct QueryDef
{
  name      : string,  // the name of the query (cannot contain spaces or dots)
  selection : string   // the query expression
}

sequence<string> SourcesList;
sequence<QueryDef> QueryList;
sequence<string> NameList;

// Configuration params for the term component
struct Configuration
{
  initialScope : string,       // initial scope, e.g. "local.demo"; its bus must be in open too. Empty means root.
  timeStyle   : TimeStyle,    // how to display time (default: utc)
  theme       : ThemeStyle,   // color theme (default: catppuccinMocha)
  noLogo      : bool,         // true to suppress the welcome banner
  open        : SourcesList,  // sources to open automatically at startup
  query       : QueryList,    // named queries to create automatically at startup
  listen      : NameList,     // event listeners to register at startup (object or object.event)
  callTimeout : Duration,     // async call timeout (default: 30s)
}