The Sola Manual
How Sola is put together and how to run it, as it works today: setting it up, what is open and how to close it, the processes of a session, the bus and the call plane, the compositor, the shell, the theme, and the command line. Built from docs/manual in the repository; a change in behaviour changes these pages in the same commit.
Contents
- 1Introduction
- 2Security
- 3Architecture
- 4Process manager
- 5The bus
- 6The call plane
- 7Compositor
- 8Session manager
- 9Shell
- 10Theme and toolkit
- 11solactl
- 11.1Synopsis
- 11.2Output and exit status
- 11.3Environment
- 11.4Live-owner commands
- 11.5Compositor (
solactl compositor) - 11.6Desk (
solactl desk) - 11.7Bots (
solactl bots) - 11.8Projects (
solactl projects) - 11.9Browser (
solactl browser) - 11.10Design (
solactl design) - 11.11Vault (
solactl vault) - 11.12MCP (
solactl mcp) - 11.13Other commands
- 12Applications
1Introduction§
Sola is a desktop environment for Linux on Wayland, written in Rust. A session is a set of separate processes: an event bus, a request/reply host, a window manager for the compositor, the shell (the top row, the bar of windows on the left, the launcher), a session manager, and applications that share one toolkit, sola-kit, and one theme.
Sola runs on Oath, a Linux distribution administered through its catalog
with the oath command. Oath supplies the compositor, rivulet (River 0.4.5
ported to C, serving the same protocols), and its PID 1 runs Sola's
processes as services. This manual describes Sola on Oath as it works
today. Sola is pre-release and each part is incomplete; what each can do,
and what it lacks, is listed in docs/capabilities.md.
1.1Requirements§
- An x86-64 machine running Oath with its
deskprofile. The profile adds the compositor, sound, fonts, the tools that build Sola, and Sola's own packages to Oath's base system. - An AMD GPU. Oath's Mesa drives AMD GPUs only (radeonsi and RADV); on an Intel GPU, or in QEMU, Sola's shell gets no GL context and the desktop does not come up.
- To build Sola on the machine: a checkout of Sola's repository. The desk
profile carries the compilers and libraries the build needs and the
browser engine (
pkg:cef), and the desk user,home, hassudowithout a password, whichcargo make oath applyuses.
1.2Installing§
Sola is a set of Oath packages: pkg:sola is the desk core (the bus, the
call host, the window manager, the shell, the session manager, the lock
screen, the KVM bridge, solactl, and the icons and cursors), and each
application has its own, pkg:sola-<app> (Terminal, Projects, Browser,
Design, Oath, …). Architecture lists them.
Oath's desk profile pins each package by hash, together with the hashes
of the packages it needs, so a machine installed from a desk image has
Sola with nothing more to do. Oath's own manual covers building a desk
image (cargo make build --profile desk in Oath's repository) and
installing it. Each package's programs are linked into /bin;
oath get pkg:sola shows the pin and what is live.
oath update moves these three packages to the pins of the output it
takes while the machine still has the pins its current output set (an
output lists them; older ones did not, and an update from one of those
leaves them). Once you move a pin yourself, with cargo make oath apply
or oath set, the machine keeps it, and an update moves only its needs.
Take an update that moves Sola with --reboot: an update without it
leaves the desk's services running their old programs (see
Updates). To take a published Sola release
on a machine that keeps its own pins, set each pin to the entry the
release's publish printed, which is the JSON oath set --from-json
takes, and apply the three together:
sudo oath set pkg:sola --from-json '{"present": true, "hash": "sha256-…", "needs": […]}'
sudo oath set pkg:sola-browser --from-json '{"present": true, "hash": "sha256-…", "needs": […]}'
sudo oath set pkg:sola-design --from-json '{"present": true, "hash": "sha256-…", "needs": […]}'
sudo oath apply pkg:sola pkg:sola-browser pkg:sola-design
Apply fetches a pinned package the store lacks from the machine's origins.
A release is built against particular packages (Mesa, Wayland, CEF, and
others), and its needs name their hashes; apply refuses it on a machine
that has any of those at another hash. Build Sola on that machine instead.
1.3Building from source§
On the Oath machine, as home:
git clone --branch next https://github.com/moonlight-pm/Sola
cd Sola
cargo make oath stage
cargo make oath apply # only with the machine owner's go-ahead
| Command | What it does | What it changes |
|---|---|---|
cargo make oath stage | Builds every program in the three packages, optimized (--debug for an unoptimized build), against this machine's packages and pkg:cef. Lays out each package under target/oath/<name>/: bin/, share/ for pkg:sola, a cef link to pkg:cef for Browser and Design, and components.json with the version | The checkout. If ~/.cache/sola/cef-<version> holds no engine, it becomes a link to pkg:cef's. |
cargo make oath apply | Stages, then puts each tree in the store (sudo oath store), pins it (sudo oath set pkg:<name> hash=… needs=…, each need at the hash this machine has live, or the one just stored), and applies the ones that changed in one sudo oath apply. Name packages (cargo make oath apply sola) to build and move only those | The machine: one apply and one generation. The /bin links move, and PID 1 restarts every service whose program moved (Updating and rolling back). |
cargo make oath publish | From a committed tree only. Stores what a build reads of the source (pkg:sola-src) and of every crate the lock file names (pkg:sola-crates); files plan:sola-build, which compiles every program once in oath build's sandbox against this machine's live packages, and a plan per package that copies its programs out; builds them; uploads the plans and sources to Oath's default origin (not pkg:sola-build itself: a rebuild runs its plan); pins and applies the products here as apply does, refusing as it does; then runs oath publish --confirm pkg:<name> --committed <commit time> for each, which uploads the package if the origin lacks it and its head, pkg/<name>.json, signed with ~/.config/oath/sign_ed25519. A package whose head is that hash already is left alone. Only from a clean master that is origin/master. --local builds the plans and uploads, applies and publishes nothing | This machine: one apply, as apply makes, so it runs what it publishes. The origin, which anyone can read. Every machine whose host:local.publishers trusts this machine's key for sola,sola-* takes the new heads at its next oath update. |
apply and publish need the machine owner's go-ahead: apply changes
what the machine runs, and publish changes it too and puts out heads
that other machines install. publish reads the origin's write key from
OATH_ORIGIN_S3, OATH_ORIGIN_KEY_ID, and OATH_ORIGIN_SECRET, in the
environment or in ~/.config/oath/origin.env, and signs with
~/.config/oath/sign_ed25519 (or OATH_SIGN_KEY). That key signs heads
of Sola's packages only where a person added its public half to a
machine's host:local.publishers as names="sola,sola-*" ssh-ed25519 ….
It needs an oath with publish (Oath R72, desk release bda181db1fc6).
If it stops after the apply, running it again finishes: the builds are
cached, nothing moves again, and heads already out are left alone. A
package's version is the crate's version, never the commit: where a
package came from is its source (oath get pkg:<name>).
A new package, or a new or changed service, still needs Oath's desk profile: a machine reads heads only for the packages it has.
On Oath, cargo make install and cargo make install-cef are not used:
install copies binaries into /opt/sola/bin, which nothing on Oath runs,
and the engine comes from pkg:cef.
1.4Starting a session§
There is nothing to start by hand. At boot, PID 1 starts seatd, rivulet,
and Sola's services as home, and the screen shows Sola once rivulet and
the shell are up. The desktop comes up signed in, with no password, until
you set one in Settings → Login and turn Sign in automatically off;
Super+L locks it. Reach the machine otherwise over SSH as home, with a
key. Security lists every way into a running desk
and how to close each one, and Process manager
lists the services and how PID 1 runs them.
Applications open from the launcher (Super+Space) or with
solactl desk launch <app_id>. The session manager reopens the applications
that were open when the session last ended.
To end the session, choose Restart Computer or Shut Down from the
flower menu. Exit Sola closes the applications and hands the screen to
Oath's console, signed in or asking for the password as Settings → Login
says; oath desk brings the desk back
(Ending the session).
1.5Updating and rolling back§
A change to Sola is an apply of its packages: cargo make oath apply
from a checkout, or oath set and oath apply of a published release
(Installing). PID 1 restarts each service whose program
comes from a package that moved, and no other: the Sola services restart,
while rivulet and the tmux servers that hold Terminal's and Projects'
shells keep running, and so do the open applications (the session
manager keeps its children). No session needs ending. An application
whose own package moved is stopped and started again on its new program by
the session manager, alone, once the apply has said so; one that isn't Sola's (Steam,
a game) is never restarted for it.
Every apply takes a generation. sudo oath undo puts back the pins the
last apply changed and relinks /bin, and PID 1 restarts the same
services on the previous programs; run it again to go back one more
apply. oath log lists the applies. Undo puts back only the pins: your
files and settings stay as they are.
1.6Fonts§
Sola ships no fonts. pkg:sola needs Oath's font packages, Noto (with
CJK and colour emoji) and Iosevka Term Slab, so they are present wherever
Sola is. Interface text is Noto Sans and mono text Iosevka Term Slab;
when SF Pro Text is installed, interface text uses it.
Fonts covers how a family is chosen and how to add one,
and Theme and toolkit covers font roles.
1.7Where things are§
On Oath, ~ is /home, the desk user's home directory.
| Path | What |
|---|---|
/bin/sola-*, /bin/solactl | Links to Sola's programs in the store (/oath/store/pkg/sola/<hash>/bin/, and the Browser and Design packages) |
/oath/store/pkg/sola/share | Icons and the McMojave cursors |
~/.config/sola/ | Applications' settings, and the bus's saved state, state.yaml |
~/.local/state/sola/ | Sola's logs: sola.log for every Sola process, and app-<app id>.log for each application |
/oath/log/svc/<service>.log | Each service's output, as PID 1 records it (oath logs svc:<service>) |
/run/user/1/ | The bus and call sockets (sola-bus, sola-call) and rivulet's wayland-N |
solactl logs <name> prints ~/.local/state/sola/<name>.log; add -f to
keep following it (solactl logs app-sola-mail -f).
oath logs svc:rivulet shows the compositor's output. Logs
covers the log filter.
1.8First-run problems§
| Symptom | Cause |
|---|---|
| No top row or bar after boot, on an Intel GPU or in QEMU | Oath's Mesa drives AMD GPUs only, so the shell gets no GL context. |
sola: not on Oath; PID 1 runs Sola's services (oath ls --kind svc) | sola was run on Oath, where PID 1 runs the session. Nothing needs starting. |
cargo make oath: not an Oath machine (no /oath/INDEX.md) | cargo make oath runs only on an Oath machine. |
commit first: a published source is the committed tree | cargo make oath publish was run with uncommitted changes. |
this machine's oath has no publish | cargo make oath publish needs Oath R72 (desk release bda181db1fc6) on the machine. |
no signing key at … | cargo make oath publish signs heads with ~/.config/oath/sign_ed25519. Make one with the ssh-keygen line in the message. |
oath apply refuses a published release, naming a need | The release was built against a package the machine has at another hash. Build Sola on the machine. |
| A new build does not run | cargo make install copied it into /opt/sola/bin, which nothing on Oath runs. Use cargo make oath apply. |
Bots and solactl bots cannot reach the bots daemon | The desk profile runs no service for sola-botsd. |
| No desktop and no console after Exit Sola | Oath has no login service (svc:login, R42), so nothing takes the screen, and the shell and the session manager stay stopped after a clean exit. Restart the machine. |
~/.local/state/sola/sola.log keeps growing | Only sola rotates it, and sola does not run on Oath. |
2Security§
By default a Sola desk on Oath trusts whoever sits at its keyboard: the desk starts signed in and nothing locks it. Each way in can be closed. This chapter lists each way into a running desk, what it lets through by default, and what closes it. Oath keeps the desk user's password and decides who has the screen and keyboard; Sola draws the desk, locks it, and runs the KVM. The design is in the secure access spec.
2.1Ways in§
| Way in | By default | To close it |
|---|---|---|
| The network, through the KVM | sola-kvm does not start unpaired (Oath's desk profile has svc:sola-kvm off); paired, it takes input and clipboard only from the paired machine's key | Nothing more to close; to open it, pair (Pairing the KVM) |
| The network, over SSH | Oath's sshd takes keys only (no passwords, no root login) | Nothing more to close |
| The keyboard, at boot | The desk starts signed in | A password, with Sign in automatically off (The password) |
| The keyboard, while you are away | Nothing locks the desk | Super+L, or Lock when the windows have faded (Locking) |
| The keyboard, at the console | The console that Exit Sola or oath desk stop puts on the screen opens a shell as home without asking | A password, with Sign in automatically off |
The password, the lock and the console login need Oath's login service
(svc:login, Oath R42 and later). Without it there is no password: the
desk starts signed in, Super+L says Locking needs Oath's login
service, and Settings → Login says the service is not running.
2.2Pairing the KVM§
sola-kvm (the server, and listen on a peer) and the Mac agent do not
start unpaired, and say so in their log. Pairing gives each machine its own
key pair and the other's public key, as SSH swaps host keys; from then on
each drops whatever does not come from the other's key.
On the server, once, with ssh to the peer working:
sola-kvm pair home@<peer-host> # an Oath peer
sola-kvm pair <mac-host> --remote-bin sola-kvm-mac # a Mac peer
The peer is an ssh destination; without one, pair uses peer.host from
the server's config. It sends the server's public key to the peer
(sola-kvm pair --accept there), which keeps it and answers with its own,
which the server keeps. The private key, ~/.config/sola-kvm/key, never
leaves its machine and is readable by its owner only; the other's public
key is in peer.pub. If the peer cannot take the key, the server does not
change. Then restart sola-kvm on both machines; on Oath, turn
svc:sola-kvm on, or pkill -x sola-kvm when it already runs, and PID 1
starts it. Each side then logs paired with the other's key (oath logs svc:sola-kvm on Oath). Without ssh, swap the keys by hand with sola-kvm pair --public and sola-kvm pair --accept (KVM notes).
Paired, every input packet and every clipboard message is encrypted and
authenticated with Noise IK between the two keys. The peer drops a packet
from any other key, one it has already taken, and a handshake no later
than the last one; it refuses a clipboard connection from any other key.
Dropped input is logged at most every 10 seconds, with the sender's
address. Keys that cannot be read stop sola-kvm from starting.
sola-kvm pair --forget (sola-kvm-mac pair --forget on a Mac) forgets
the other machine's key: that side does not start until paired again. A
machine paired by an older sola-kvm (a shared pair.key) does not start
until paired again. Setup of the server and of each kind of peer is in
the KVM notes.
The server captures input through the compositor while the pointer is on
the peer: it holds the keyboard, a pointer lock and a shortcuts inhibitor,
and needs no access to /dev/input. Ctrl+Alt+Esc takes the keyboard and
pointer back.
2.3The password§
The desk user, home, has no password until one is set. Settings →
Login sets, changes or removes it; changing or removing it needs the
current one. Oath keeps it, not Sola: svc:login stores it as an
Argon2id hash that only root can read, and answers every wrong password
after a delay that doubles each time, from 1 second up to 30. From a
console or over SSH:
oath password # asks the current password (if one is set), then the new one twice
oath password --status # password: set or none; autologin: on or off; seat: desk or console
An empty new password removes it.
Sign in automatically (Settings → Login; Oath's host:local.autologin,
on by default) decides whether a password is asked for as the desk starts
and when the console opens. With a password set, changing it needs the
current password typed above it. With auto-login off and a password set,
the desk is locked as it starts. A password with Sign in automatically
on still lets you lock by hand: the desk starts signed in and never locks
on its own.
2.4Locking§
Super+L, or Lock Screen in the flower menu, locks the screen. With no password set, a toast says Set a password in Settings → Login to lock.
Settings → Appearance → Idle → Lock when the windows have faded (off by default) locks once the idle fade is complete.
The lock screen (sola-lock) shows the background picture under a dark veil,
the time and the date, and a password field. Return checks the password,
Backspace erases a character, and Escape or Ctrl+U clears the field. Keys
typed while Checking… shows are ignored. A password typed through a
paired KVM works like the local keyboard.
Locking the screen also locks the vault, unless Lock when the screen
locks is off in Vault's settings (Vault): once
the compositor has the screen locked, sola-lock says so on the bus
(ScreenLock), and says it is unlocked once you unlock it
(The bus). A lock screen that dies says nothing, so
the screen still counts as locked.
While locked, the compositor (ext-session-lock-v1) shows and sends input
to the lock screen only. If sola-lock dies, the screen stays locked and the
shell starts it again, up to five times a minute.
2.5The console§
The screen and keyboard (tty1) belong either to the desk or to the
console. Every boot starts with the desk. Exit Sola closes your
applications, ends the desk, and tells Oath's login service that it ended
on purpose; oath desk stop, over SSH, does the same from outside. PID 1
stops the desk's services and starts the console on tty1, which opens a
shell as home when auto-login is on and asks for the password when it is
off. oath desk, from that shell or over SSH, gives the screen back and
starts the desk's services again. Oath's
manual covers the rest.
2.6What stays open§
These hold with or without a password:
homeis root.sudoasks no password, so any program running ashomecan do anything: a terminal, an SSH session, a coding agent in Projects, a bot.- The bus and the call plane trust every process of
home. Any of them can read every bus topic (mail and calendar passwords cross the bus in clear), publish any topic, and call every method: type and click into any window, take screenshots of the screen, and drive Browser and Design.solactland its MCP server reach the same methods. See The bus and The call plane. - The vault keeps its values off the bus and the call plane, and
answers only Sola's own programs on its socket
(Vault). A program running as
homecan still get in by other means: throughsudo, or by reading the master password as you type it. - Design's share links have no password and no expiry: anyone with a link sees its canvas until New Link replaces it.
- The keyboard can always end the desk. Every SysRq key is on, so anyone at the keyboard can kill the desk (Alt+SysRq+K) or restart the machine (Alt+SysRq+B).
- The disk is not encrypted, and Oath's boot menu has no password:
whoever boots another system from a USB stick reads and writes
everything,
/homeincluded. - A serial console, where Oath finds a serial port, is a root shell with no password.
- The lock screen draws without the kit (iced cannot make lock surfaces); it follows the picture but not the theme's colors.
- The lock does not reach through the KVM: it does not cover another computer's screen; lock that computer on its own.
3Architecture§
A Sola session is a set of separate processes on one machine. On Oath,
PID 1 starts them as the desk user, together with the compositor,
rivulet: Oath's port of River 0.4.5, which speaks the same protocols.
Sola's programs are in /bin. The parts talk over three planes: the bus
for facts, the call plane for requests, and Wayland for surfaces and
input. Each Sola process can stop and start again on its own while the
others carry on.
3.1Processes§
PID 1 (Oath)
├─ rivulet Wayland compositor
├─ sola-bus event bus
├─ sola-call call host
├─ sola-river window manager and bus bridge
├─ sola-shell top row, bar, launcher, workspaces, zones
├─ sola-session application launcher and tracker
│ └─ applications its child processes
└─ sola-kvm KVM server
| Process | Owns |
|---|---|
| rivulet | Outputs, rendering, and input devices. Oath runs it, not Sola. See Compositor. |
sola-bus | The bus socket, and the current value of every sticky and persistent topic. See The bus. |
sola-call | The call socket, and the registry of live owners and their methods. See The call plane. |
sola-river | The compositor's window manager (river-window-management-v1). It places, stacks, and focuses windows as the shell asks, binds key chords, and reports windows and outputs on the bus. Call owner compositor. See Compositor. |
sola-shell | The top row, the bar, the launcher, notifications, desktop workspaces, and zones. It decides which windows are shown and where, tells sola-river, and saves the layout that puts windows back after a restart. See Shell. |
sola-session | Launching, tracking, and closing applications, and relaunching the previous session's applications at start. Call owner desk. See Session manager. |
sola-kvm | Sharing this machine's keyboard and mouse with another machine. See KVM. |
| Applications | Their own windows. Sola's applications are built on sola-kit and are Wayland clients and bus clients. Terminal, Browser, Paint, Projects, and Design open a window per tab (a terminal tab, a page, an image, a project) and announce each one to the shell, which lists it in the bar. Projects, Browser, and Design are also call owners. Other programs started through the session manager are plain Wayland or X11 clients. See Applications. |
Some processes run outside that tree:
sola-botsdkeeps named agent sessions, each a directory in~/Botson Grok, Claude, or Gab AI. It is call ownerbotsand serves an HTTP API on the socket$XDG_RUNTIME_DIR/sola-bots. It ships with Sola, but Oath runs no service for it, so it runs only when you start it. See solactl.- Terminal's shells run in a tmux server on the socket
sola, and Projects' onsola-ws. Each application starts its server when it needs one, and the server outlives the application. sola, Sola's own process manager, starts and supervises these processes where nothing else does. On Oath it refuses to start, since PID 1 does that job. See Process manager.
3.2The three planes§
| Plane | Socket | Shape | Carries |
|---|---|---|---|
| Bus | $XDG_RUNTIME_DIR/sola-bus | One sender, every subscriber, no reply | Window lists, focus, outputs, tabs and the bar's layout, workspaces, theme, application menus, launch and close requests, notifications, shutdown |
| Call | $XDG_RUNTIME_DIR/sola-call | One caller, one owner, one reply | solactl commands, agent tools, screenshots, synthetic input, application control |
| Wayland | $XDG_RUNTIME_DIR/wayland-N | Client and compositor | Buffers, windows, layer surfaces, keyboard and pointer input |
The bus carries facts. A message goes to every process subscribed to its topic kind, and the sender gets no answer. A topic is ephemeral, sticky, or persistent. The bus keeps the latest value of each sticky topic and replays it to every new subscriber; it also writes persistent topics to disk. A process that starts late, or restarts, therefore learns the current state as soon as it subscribes.
The call plane carries requests. An owner (compositor, desk,
projects, …) advertises its methods. A caller names an owner and a
method and gets exactly one reply, or an error: <owner> is not running,
<owner> has no method <method>, or timeout (8 seconds unless the caller
sets another). The bus has no reply path, so requests do not travel on it.
Wayland carries pixels and input between the compositor and its clients.
Every Sola window is an ordinary Wayland surface; sola-river is the one
client that also manages everybody else's windows.
The planes are separate processes and sockets, so one can restart without the others: windows stay on screen while the bus restarts, and the bus keeps delivering while the call host restarts.
3.3Programs and files§
3.3.1Packages§
Sola ships on Oath as the desk's core and a package per application.
Their programs appear in /bin, and Sola's icons and cursors are in
/oath/store/pkg/sola/share. Changing an application moves only its own
package, so only that application restarts; changing the core restarts
the desk.
| Package | Programs |
|---|---|
pkg:sola | sola-bus, sola-call, sola-river, sola-shell, sola-session, sola-lock, sola-kvm, solactl |
pkg:sola-oath | Oath |
pkg:sola-terminal | Terminal |
pkg:sola-projects | Projects |
pkg:sola-paint, pkg:sola-preview, pkg:sola-scope | Paint, Preview, Scope |
pkg:sola-monitor | Monitor |
pkg:sola-calendar, pkg:sola-mail | Calendar, Mail |
pkg:sola-settings | Settings |
pkg:sola-bots | Bots (sola-bots, sola-botsd) |
pkg:sola-browser | Browser, which also hosts the web apps |
pkg:sola-design | Design |
SOLA_BIN_DIR, SOLA_SHARE_DIR, and
SOLA_LOG_DIR override the program, share, and log directories.
3.3.2Runtime directory§
If XDG_RUNTIME_DIR is unset, Sola's processes use /run/user/<uid> when
that directory exists, else /tmp. SOLA_BUS_PATH and SOLA_CALL_PATH
override the bus and call socket paths for every process that has them in
its environment.
| Path | What |
|---|---|
$XDG_RUNTIME_DIR/wayland-N | The compositor's Wayland socket. Sola's processes use the first one, by name, that accepts a connection (wayland-x… and .lock files are skipped). |
$XDG_RUNTIME_DIR/sola-bus | The bus |
$XDG_RUNTIME_DIR/sola-call | The call plane (mode 0600) |
$XDG_RUNTIME_DIR/sola-bots | sola-botsd's HTTP API |
~/.local/state/sola/projects/hooks.log | Projects' agent hooks: each computer's agents append their events; every Projects watching its sessions follows it |
$XDG_RUNTIME_DIR/sola-ssh/ | Projects' shared ssh connections |
$XDG_RUNTIME_DIR/sola/ | Single-instance sockets: terminal.sock, projects.sock, paint.lock.sock, design.sock, calendar.sock |
The Browser's single-instance socket is
~/.local/share/sola/browser/chrome.sock (under $XDG_DATA_HOME when that
is set). Where sola runs the compositor, it also writes the files
sola-wayland and sola-display here, naming the Wayland socket and the X
display; on Oath they are absent, and each process finds the sockets
itself.
3.3.3Configuration and state§
~/.config below means $XDG_CONFIG_HOME when that is an absolute path.
| Path | What |
|---|---|
~/.config/sola/state.yaml | Persistent bus topics without a file of their own, one section each: the open-application set, the application catalog, floating window geometry, terminal tabs, the background picture, appearance, and the mail, calendar, terminal, and monitor settings |
~/.config/sola/shell/tabs.yaml | The bar's layout: the workspaces, which one shows, and each window's workspace, zone, and place |
~/.config/sola/theme/current.yaml | The current theme |
~/.config/sola/theme/presets/<name>.yaml | Saved themes |
~/.config/sola/paint.yaml | Paint's open images |
~/.config/sola/calendar/agenda.yaml | Calendar's coming days, for the clock's panel |
~/.config/sola/background/ | Sola's copy of the background picture |
~/.config/sola/<app>/, ~/.local/share/sola/<app>/, ~/.local/state/sola/<app>/, ~/.cache/sola/<app>/ | Application files; each application's notes list its own (Applications) |
~/.config/sola-kvm/config.toml | sola-kvm |
The bus reads its files only when it starts, and rewrites a topic's entry each time a new value is published.
3.3.4Logs§
Sola's log directory on Oath is $XDG_STATE_HOME/sola when that is an
absolute path, else ~/.local/state/sola.
| Path | What |
|---|---|
~/.local/state/sola/sola.log | The log of every Sola process, one line per event, tagged with the process |
~/.local/state/sola/app-<app id>.log | The standard output and standard error of each application the session manager launched |
Every Sola process also writes its log lines to its standard error.
RUST_LOG sets the filter; without it, a process logs its own crate and
Sola's shared libraries at info. Only sola rotates sola.log, when it
starts, so on Oath the file is not rotated.
3.4Start-up§
Sola's processes do not depend on the order they start in: each one waits for what it needs.
- PID 1 starts the compositor and the services
sola-bus,sola-call,sola-river,sola-shell,sola-session, andsola-kvm. sola-busbinds its socket and loads the persistent topics from disk.sola-callbinds its socket with an empty registry.sola-riverwaits up to 20 seconds for a livewayland-Nsocket and exits with an error if none comes. It binds the window-management protocol, reports outputs and windows, and tries the bus every 20 ms until it connects.sola-shellstarts like any kit application (below), maps the top row and the bar, and takes the saved layout when the bus replays it.sola-sessiontries the bus every second until it connects, collects the saved open-application set and the current window list for 750 ms, then launches every saved application that has no window.- Each application reopens its own tabs, and the shell puts each window back in its saved workspace, zone, and place (Session manager).
Call owners connect to sola-call whenever it answers, retrying every
400 ms, so their methods appear as soon as both are up.
3.5When a part stops§
PID 1 starts a service again when it exits with an error. A service that exits cleanly stays down; that is how Exit Sola ends the session.
| What stops | What happens |
|---|---|
sola-bus | Sticky values held only in memory are lost; persistent ones are reloaded from disk. Every client reconnects, subscribes again, and publishes its own sticky values again, so menus, chords, tabs, and window lists return. |
sola-call | Calls in flight fail. Owners reconnect every 400 ms and advertise their methods again. |
sola-river | The compositor and every window keep running. The new sola-river binds the window-management protocol again and reports outputs and windows. |
sola-shell | The top row, the bar, and the overlays go until the new shell maps them; windows stay. The new shell takes the saved layout and pairs the open windows with their tabs again. |
sola-session | The new one launches saved applications that have no window. It did not start the ones still running: their exits are not reported and solactl desk close does not find them, but they stay in the saved set while they have windows. |
| An application | sola-session publishes its exit (UserAppExited) and drops it from the saved set. Nothing restarts it. Its windows' places in the bar wait 30 seconds, so a copy started again within that time gets them back. |
| The compositor | Every Wayland client loses its connection. sola-river and the shell exit with an error, PID 1 starts them again, and they wait for a live Wayland socket. The applications exit with it; sola-session keeps them in the saved set and starts them again once the new compositor has answered for 2 seconds. |
3.5.1Ending the session§
Exit Sola in the flower menu publishes Shutdown, and the shell exits
cleanly. sola-session stops every application it launched (SIGTERM, then
SIGKILL to any still running after 6 seconds) and exits; sola-river
exits. Once sola-session is gone, the shell tells Oath's login service
that the desk ended on purpose, and PID 1 hands the screen to the console
(Security). sola-bus, sola-call, and
sola-kvm do not act on Shutdown and keep running. Restart Computer
and Shut Down are described in Shell.
3.6How an application attaches§
A Sola application is an Iced program built on sola-kit. Before it opens a window, it:
- Calls
startup(app_id), which starts logging, finds the compositor's socket (the firstwayland-Nin the runtime directory that accepts a connection, looked for during up to 20 seconds; thenWAYLAND_DISPLAY; thenwayland-0), waits up to 10 seconds for that socket file, setsWAYLAND_DISPLAY, setsXDG_SESSION_TYPE=waylandunless it is alreadywaylandorx11, setsGSETTINGS_BACKEND=memoryif unset, loads the system fonts, and, unlessSOLA_NO_SELF_WATCHis set, starts watching its own executable. - Connects to the bus with
BusSetup: it identifies with its app id, subscribes to the topic kinds it handles, and publishes its application menu, which its windows' titlebars draw.BusSetupblocks until the bus accepts the connection..calls(owner, methods)on it also advertises call methods;CallSetupadvertises call methods without the bus. - Opens its windows with the same app id as the Wayland
app_id, which is how the shell matches menus, icons, and windows to the application. An application with a window per tab opens each through the kit'sTabWindows, which first announces the tab on the bus (Tab, keyed<app_id>/<tab id>). The shell pairs the announcement with the new window, answers withTabWindow(the window's id and whether it is on screen), and lists it in the bar.
When the bus goes away, the kit reconnects every 250 ms, subscribes again, and republishes the sticky values the application published. The call provider reconnects every 400 ms. Theme and fonts arrive as a persistent bus topic and apply live; see Theme and toolkit.
3.7Updates in place§
A new version of a package becomes live on Oath with oath apply. PID 1
then restarts the services that run from a package that moved. An
application built on sola-kit watches its own executable file; when that
file changes, it executes the new one in place, keeping its process id and
arguments, so sola-session goes on tracking it. The application then
reopens its tabs as it does at start, and the shell puts its windows back
in their places. sola-session removes SOLA_NO_SELF_WATCH from the
environment of the applications it launches, so each of them watches its
own file.
4Process manager§
On Oath, PID 1 is Sola's process manager. Each of Sola's long-running
processes is a service, a svc:* object in Oath's catalog. Oath's desk
profile defines them, and PID 1 (Oath's init) runs them as the desk user,
home (uid 1, home directory /home, runtime directory /run/user/1).
PID 1 starts them at boot, restarts them by their restart policy, restarts
those whose program moved when an apply changes a package, and stops them
when the machine shuts down. Applications are not services: the
session manager starts them.
oath ls --kind svc # every service on the machine
oath get svc:sola-shell # its spec, and actual: state, pid, restarts, last exit, package
oath logs svc:sola-shell # its last 50 lines of output; -n 200 for more
Sola's own process manager, sola, does not run on Oath; see
The sola program.
4.1The services§
The desk profile's services for the session are below. A machine's
catalog can differ from the profile; oath get shows what it runs.
| Service | Runs | Wants | Restart |
|---|---|---|---|
svc:seatd | seatd -u home -g home, as root: opens the GPU and input devices for the session | none | always |
svc:rivulet | rivulet -log-level info -c :, the compositor | svc:seatd | always |
svc:sola-bus | sola-bus, the bus | none | always |
svc:sola-call | sola-call, the call host | svc:sola-bus | always |
svc:sola-river | sola-river, the window manager, a client of rivulet's River protocols (Compositor) | svc:rivulet, svc:sola-bus, svc:sola-call | always |
svc:sola-shell | sola-shell: the top row, the bar, the launcher (Shell) | svc:sola-river, svc:pipewire-pulse, svc:wireplumber | on-failure |
svc:sola-tmux | tmux -L sola -f /home/.config/sola/tmux.conf -D, the tmux server that holds Terminal's shells | none | always |
svc:sola-tmux-ws | The same on socket sola-ws, for Projects' tabs | none | always |
svc:sola-session | sola-session, the session manager | svc:sola-bus, svc:sola-call, svc:sola-tmux, svc:sola-tmux-ws | on-failure |
svc:sola-kvm | sola-kvm listen --bind 0.0.0.0:4242, which takes another machine's keyboard and mouse (KVM); from any machine on the network until it is paired (Security) | svc:sola-river | always |
Each Sola service runs its program from /bin, where pkg:sola links
it. The two tmux servers are services of their own so that shells
outlive a restart of the session manager: the applications it starts
share its cgroup, and PID 1 stops a service's whole cgroup.
Beside these, the desk runs svc:dbus-session, svc:pipewire,
svc:wireplumber, and svc:pipewire-pulse as home, for sound and a
session D-Bus; Oath's own manual covers them.
The bots daemon, sola-botsd, is in pkg:sola, but no service runs it,
so Bots and solactl bots have nothing to talk to on Oath.
4.2Start-up§
At boot, once it has brought up the network, PID 1 starts every enabled
service in wants order: a service starts after the enabled services it
wants. wants orders start-up only; PID 1 does not wait for a service to
be ready. The display drivers load after the services start; rivulet
waits for the GPU, and its restart: always covers one that takes
longer.
Each Sola process waits for what it needs:
| Process | Waits for |
|---|---|
sola-bus, sola-call | Nothing. Each deletes a leftover socket file and binds its socket, /run/user/1/sola-bus or /run/user/1/sola-call. |
sola-river | Rivulet's socket: it tries each wayland-N in /run/user/1 every 500 ms for up to 15 seconds, then waits up to 5 seconds more for the socket file. Without one it exits with status 1, and PID 1 starts it again. Then the bus, retrying. |
sola-shell, and every application | The same socket for up to 20 seconds, then up to 10 seconds for the socket file, then connects anyway. The bus, retrying every 250 ms. |
sola-session | The bus, retrying every second. It then collects saved state for 750 ms before it reopens the applications that were open. |
sola-kvm | Rivulet's socket, for up to 30 seconds. Reads ~/.config/sola-kvm/config.toml, or uses defaults when it is missing. |
Nothing writes $XDG_RUNTIME_DIR/sola-wayland or sola-display on Oath;
those files are the sola program's. Sola's processes find rivulet's
socket by connecting to each wayland-N in name order, skipping
XWayland's wayland-x*, and take the first that answers. Applications find XWayland's display by probing
/tmp/.X11-unix/.
4.3Restarts§
PID 1 restarts a service by its restart:
always: after any exit.on-failure: after an exit with a non-zero status or by a signal. An exit with status 0 leaves the servicestopped.
A run shorter than 10 seconds is a quick failure. Restarts after quick
failures wait 1, 2, 4, then 8 seconds; a run of 10 seconds or more
restarts at once. After five quick failures in a row the service is
failed, and it stays down until its spec or its program's package
changes. oath get svc:<name> --actual shows its state (running,
restarting, stopped, or failed), its restarts this boot, and its
last_exit.
To restart a service by hand, kill its process. An exit by a signal is a failure, so PID 1 starts it again under either policy:
pkill -x sola-shell
When a service's main process exits, PID 1 also kills what is left in its
cgroup — except for a service with keep_children, whose children keep
running. svc:sola-session has it (on novus since 2026-11-01; Oath's desk
profile to follow), and the session manager starts each application in a
session of its own (setsid), out of its process group, which PID 1
signals when it exits or is stopped: killing or restarting sola-session leaves the applications it
started running, a game under Steam included, and the new session
manager keeps the saved ones it finds running and reopens only the listed
ones that have no window.
Not restarted by PID 1: applications. When one exits, the session manager publishes its exit and leaves it closed. A stopped shell or session manager comes back only when PID 1 next starts the services that are not running: at boot, or when an apply that changes a service or a package makes it converge them.
4.4Updates§
Nothing watches Sola's binaries on Oath. PID 1 follows each service's
program through its /bin link into the store, and when an apply moves
that program's package to another hash, it restarts the service. An
apply of pkg:sola (cargo make oath apply does one) therefore restarts
svc:sola-bus, svc:sola-call, svc:sola-river, svc:sola-shell,
svc:sola-session, and svc:sola-kvm. It does not restart rivulet or the
two tmux servers, whose programs are in other packages.
oath update is different. Rivulet, svc:sola-river, svc:sola-shell,
and svc:sola-session hold the seat (seat: desk), and an update leaves
a service that holds the seat on the program it started with until it
next starts: at a reboot, or at Exit Sola and oath desk. The bus,
the call host and the KVM bridge restart on the new programs. An update
that moves pkg:sola therefore leaves the shell and the session manager
of the old build talking to the bus and the applications of the new one.
They agree on which topics each asked for (see
Subscribing), but a topic whose payload changed
between the two builds does not get through until the desk restarts:
from the Sola in Oath's desk output to next today, that is
Appearance, so the shell would miss changes made in Settings →
Appearance. Take such an update with sudo oath update --confirm --reboot, which restarts nothing before the reboot. A PID 1 from Oath's desk output
fdecb1ec3d68 or older restarts the seat's services in an update anyway.
With keep_children on svc:sola-session, its restart leaves the
applications running: they stay on the windows and places they had.
Without it, stopping the service stops the applications in its cgroup,
and the new session manager reopens them. An application does not restart
itself: its program is in the store directory it started from, which
never changes, so an application started before an apply runs the old
program until it is opened again. The session manager does that for each
of Sola's applications whose program changed: as it starts, and when an
apply says it moved packages (PackagesMoved on the bus, from cargo make oath apply when pkg:sola did not move), it compares the program each one
runs (/proc/<pid>/exe) with the one its command names now
(/bin/sola-mail, through its links), stops one that differs, and starts
it again; its windows come back to their places. An
application that isn't Sola's is never restarted for it. sudo oath undo takes an apply back the same
way, restarting the same services on the previous programs.
4.5Environment§
PID 1 gives every service PATH=/bin, HOME, USER, LOGNAME, SHELL,
and XDG_RUNTIME_DIR for its account, then host:local's environment,
then TZ, then the service's own env, which wins on the same name. The
desk profile sets, in host:local, LANG=en_US.UTF-8 (tmux refuses a
locale that is not UTF-8), SSL_CERT_FILE, DBUS_SESSION_BUS_ADDRESS, and
PKG_CONFIG_PATH. Each Sola service's own env has:
| Variable | Value |
|---|---|
XCURSOR_PATH | /oath/store/pkg/sola/share/cursors |
XCURSOR_THEME | McMojave |
FONTCONFIG_FILE | /oath/store/pkg/fontconfig/live/etc/fonts/fonts.conf (Fonts) |
SHELL | /bin/zsh, so terminals start the login shell |
Each Wayland client sets WAYLAND_DISPLAY for itself to the socket it
found, and XDG_SESSION_TYPE=wayland unless it is already wayland or
x11. Applications built on sola-kit also set GSETTINGS_BACKEND=memory
when it is unset. The session manager builds the environment of the
applications it launches from its own; see Session manager.
4.6Ending the session§
| Action | What happens |
|---|---|
| Restart Computer or Shut Down in the flower menu | The shell runs sudo oath set host:local power=reboot (power=halt for Shut Down), then sudo oath apply --confirm host:local. PID 1 stops every service (SIGTERM, up to 5 seconds, then SIGKILL) and reboots or powers off. If either command fails, the shell shows Could not restart the computer or Could not shut down. |
sudo reboot or sudo poweroff, over SSH | The same shutdown by PID 1. |
| Exit Sola in the flower menu | The shell publishes Shutdown on the bus and exits with status 0. The session manager sends SIGTERM to every application, sends SIGKILL to any still running after 6 seconds, and exits with status 0. sola-river exits with status 0. Once the session manager is gone (up to 10 seconds), the shell tells Oath's login service that the desk ended on purpose. |
solactl emit Shutdown null | As Exit Sola, except that the shell keeps running: it does not act on a Shutdown it did not send. |
After Exit Sola, PID 1 stops the desk's services and starts the
console on tty1, signed in or asking for the password as Settings → Login
says; oath desk gives the screen back and starts the desk again
(Security). Without Oath's login service
(before R42), nothing takes the screen: the shell and the session manager
stay stopped, since their policy is on-failure and they exited cleanly,
while sola-river is started again at once and the bus, the call host,
sola-kvm, rivulet, and the tmux servers keep running. Restarting the
machine is then the way back.
Neither Exit Sola nor a shutdown clears the saved set of open applications, so the session manager reopens them when it next starts.
4.7Logs§
| File | Written by | Limit |
|---|---|---|
~/.local/state/sola/sola.log | Every Sola process, through tracing: plain text, UTC times | None on Oath: only the sola program rotates it |
~/.local/state/sola/app-<app id>.log | Standard output and error of each application the session manager launched | Emptied at the next launch once it is larger than 8 MiB |
/oath/log/svc/<service>.log | Standard output and error of each service, as PID 1 records them, each line with a UTC time in front. Sola's processes write their log lines here too, without colour. | Rotated to <service>.log.1 past 256 KiB |
~ is /home. Rivulet's output is only in /oath/log/svc/rivulet.log.
Read a service's file with oath logs svc:<name>, and Sola's with
solactl logs <name>, which prints ~/.local/state/sola/<name>.log
(sola.log when no name is given); -f keeps following it:
oath logs svc:sola-session -n 200
oath logs svc:rivulet
solactl logs -f
solactl logs app-sola-mail -f
A line in sola.log:
2026-10-29T09:12:03.512204Z INFO [river] sola::river sola-river starting
The bracket is the process: the binary name without sola- ([bus],
[call], [river], [shell], [session], [kvm]), or an
application's app id without sola- ([mail]). The next column is the
module: sola::bus is the bus library in any process.
Each process's default filter is its own crate and Sola's shared
libraries (sola_core, sola_bus, sola_call, sola_kit,
sola_browser, sola_assets) at info; other crates log nothing.
RUST_LOG replaces that filter, in tracing filter syntax. On Oath, set
it in a service's env and apply it, which restarts the service.
oath set replaces a field whole, so the new env must repeat the
variables oath get svc:<name> --desired shows:
oath get svc:sola-river --desired
sudo oath set svc:sola-river --from-json '{"env": {…the variables it has…, "RUST_LOG": "info,sola_river=debug"}}'
sudo oath apply svc:sola-river
sudo oath undo # back to the previous env
RUST_LOG=info,sola_bus::traffic=trace on svc:sola-bus logs every bus
message; RUST_LOG=debug logs everything and is very large.
4.8Files and variables§
| Path | Role |
|---|---|
/bin/sola-*, /bin/solactl | Links to the programs in the store; PID 1 follows them to their package |
/oath/store/pkg/sola/share/cursors/ | The McMojave cursor theme |
/run/user/1/sola-bus, /run/user/1/sola-call | The bus and call sockets |
/run/user/1/wayland-N | Rivulet's socket |
~/.config/sola/state.yaml | The bus's saved state, including SessionApps, the open-application set the session manager reopens |
~/.config/sola/tmux.conf | The configuration both tmux services load |
~/.config/sola-kvm/config.toml | sola-kvm settings |
~/.local/state/sola/ | Sola's logs |
/oath/log/svc/ | PID 1's record of each service's output |
| Variable | Effect |
|---|---|
RUST_LOG | Log filter, per service |
SOLA_BIN_DIR, SOLA_SHARE_DIR, SOLA_LOG_DIR | Replace /bin, /oath/store/pkg/sola/share, and ~/.local/state/sola |
XDG_STATE_HOME | When absolute, logs go to $XDG_STATE_HOME/sola |
XDG_RUNTIME_DIR | Where the sockets are. When unset, as in an SSH login on Oath, the user's /run/user/<uid> when it exists, else /tmp |
XDG_CONFIG_HOME | Where sola/ and sola-kvm/ live (~/.config when unset) |
SOLA_BUS_PATH, SOLA_CALL_PATH | Replace the bus and call socket paths |
Sola takes Oath's paths when /oath/INDEX.md is a file.
4.9Troubleshooting§
| Symptom | Cause |
|---|---|
oath get svc:<name> --actual shows failed | It exited within 10 seconds of starting five times in a row. oath logs svc:<name> says why; it stays down until its spec or its package changes. |
restarting, with restarts climbing | It fails at start. See its oath logs and ERROR or panicked lines with its tag in sola.log. |
stopped for svc:sola-shell and svc:sola-session | Exit Sola ended them cleanly, on an Oath without the login service. See Ending the session. |
wayland socket never appeared from [river] | Rivulet opened no socket in /run/user/1 in time. See oath logs svc:rivulet. |
bus connect failed, retrying from [session] | sola-bus is not running. See oath get svc:sola-bus --actual. |
| A new build does not run | It was not applied. oath get pkg:sola shows the live hash; cargo make install writes /opt/sola/bin, which nothing on Oath runs. |
| An open application runs an old build after an apply | It started before the apply, and the session manager did not restart. Close it and open it again. |
Nothing in sola.log | ~/.local/state/sola is not writable by home. Each process then logs only to its standard error, which PID 1 records in the service's file. |
4.10The sola program§
Sola's source also builds sola, a process manager that starts the
compositor and Sola's processes itself (sola-bus, sola-call,
sola-vaultd, sola-botsd, sola-river, sola-shell, sola-session,
and sola-kvm), for a session started by hand from a text console. It is not in pkg:sola. On Oath it prints
sola: not on Oath; PID 1 runs Sola's services (oath ls --kind svc) and
exits with status 1 before it starts anything.
5The bus§
Sola Bus (sola-bus) carries facts between Sola's processes. A process
publishes a typed topic, such as "the window list is now this" or "this
app's menu is this", and the bus copies it to every connected process that
subscribed to that kind of topic. Nothing on the bus is a request: there
are no replies, request ids, or timeouts. Work that needs an answer goes
through the call plane, sola-call (Call plane).
Some topics are sticky: the bus keeps the latest value and hands it to every process that subscribes later. Some sticky topics are also persistent: the bus writes them to disk and restores them when it starts. This is how a restarted shell finds the open windows and their tabs, and how the theme and the background picture survive a reboot. Where the bus sits among Sola's processes is in Architecture.
| Item | Value |
|---|---|
| Process | sola-bus. On Oath, PID 1 runs it as the service svc:sola-bus |
| Socket | $SOLA_BUS_PATH, else sola-bus in the runtime directory: $XDG_RUNTIME_DIR, else /run/user/<uid> when that directory exists, else /tmp |
| Framing | 4-byte little-endian length, then one postcard-encoded message |
| Topics | 63 kinds, fixed when Sola is built: 26 ephemeral, 19 sticky, 18 persistent (topics.rs) |
| Disk | ~/.config/sola/state.yaml and per-topic YAML files |
| Rust client | sola_bus::BusClient; kit apps use sola_kit::app |
| Log | sola.log in Sola's log directory, lines tagged [bus]. On Oath that is ~/.local/state/sola/ ($XDG_STATE_HOME/sola; SOLA_LOG_DIR overrides it) |
At startup the bus deletes any file left at the socket path and binds a new socket; if the bind fails, it logs the error and exits. The bus does no authentication: any process that can open the socket can subscribe to everything, publish any topic under any app id, and overwrite or retract a record another app published. The socket is protected only by the permissions of the runtime directory.
5.1Wire format§
A frame is a 4-byte unsigned length, little-endian, followed by that many
bytes holding one Message encoded with postcard 1.x. Both directions use
the same framing. There is no maximum frame size, version field, or
checksum. When a frame does not decode, the bus logs a read error and
closes the connection.
Postcard writes the Message fields in this order, with no names or tags.
Varints are postcard's unsigned varints (7 bits per byte, low bits first).
| Field | Type | Encoding | Meaning |
|---|---|---|---|
id | UUIDv7 | varint length (16), then 16 bytes | Unique per message; embeds a millisecond timestamp. The bus does not order by it. |
topic | string | varint length, then UTF-8 | A Topic variant name such as Windows or Theme (case-sensitive), or a control name |
payload | optional bytes | 0, or 1 + varint length + bytes | The topic's payload, itself postcard-encoded; absent for topics without one (Shutdown, MouseLeft) |
sticky | bool | one byte | 1 stores the message as a record; 0 on a sticky topic is a retraction |
source | string | varint length, then UTF-8 | The publisher's app id, filled in by BusClient; provenance only |
keys | list of strings | varint count, then each string | The values of the topic's key fields as text (window_id 42 is "42"); empty for unkeyed topics |
Payloads are positional postcard with no field names, so a payload decodes
only as the exact type it was encoded from. A process built from another
sola-bus source reads only the topics whose payloads kept their layout;
only Application and MailConfig also accept older layouts. Keep every
process on the bus on one build.
Three topic names that start with $ are instructions to the bus. It
never forwards or answers them.
| Topic | Payload | Effect |
|---|---|---|
$identify | postcard string: the app id | Names this connection |
$subscribe-names | postcard list of strings: topic names | Replaces this connection's subscription set |
$subscribe | postcard list of integers: topic numbers | The same, for a bus from before $subscribe-names; ignored from a connection that has sent $subscribe-names |
$subscribe-names names each kind as its messages do (TabLayout), so a
bus and a client from different builds agree on what was asked for. The
bus skips names it does not know, a newer client's topics, and applies the
rest.
$subscribe carries each kind as its index in a list frozen at the last
build that sent only numbers (subscribe::NUMBERED, 64 names). That
build's numbers were the kinds' order in the enum sola-bus generates,
which groups them by retention, so adding a topic renumbered others; the
frozen list keeps those numbers, including CustomTheme's, a topic since
removed. The bus skips numbers it cannot place and applies the rest; when
nothing usable remains, the connection keeps its previous set. A kind
added after the freeze has no number, and a bus from before names never
receives a subscription to it.
5.2Connecting and identifying§
A client connects and may send frames at once. There is no greeting, and
the bus sends nothing until the client subscribes. BusClient sends
$identify with its app id first on every connection.
When a connection identifies, the bus adds the app id to its roster and
publishes ClientConnected; when an identified connection closes, it
publishes ClientDisconnected. Identifying again with the same id does
nothing; with a different id, the bus replaces the roster entry and
publishes ClientConnected again. App ids are not checked for uniqueness.
A connection that never identifies still works but is not in the roster.
5.3Subscribing§
Until a connection sends $subscribe, the bus delivers nothing to it. Each
$subscribe replaces the previous set of kinds. For every kind that is new
in the set, the bus immediately sends every stored record of that kind (the
sticky replay), in no defined order, and, for ClientConnected, one
message per identified connection, including the subscriber's own if it has
identified. Kinds already in the set are not replayed, so sending the same
set again does nothing.
Subscription is by kind only, with no filter on keys, payload, or source. A
client subscribed to MenuAction receives every app's menu actions and
keeps those whose app_id is its own.
BusClient::subscribe sends two frames: $subscribe-names, then
$subscribe with the kinds the frozen list has. A current bus applies the
first and ignores the second; a bus from before names logs broadcast dropping unknown topic for the first and applies the second.
5.4Publishing§
Any frame whose topic is not a control name is a publication. The bus:
- Stores it as a record if its
stickyflag is set, or deletes the record if it is a retraction. - Queues it for every connection subscribed to its kind. For sticky and persistent topics the publisher receives its own message too, so it sees its write through the same path as everyone else; for ephemeral topics the publisher is skipped.
- Writes it to disk if the topic is persistent.
A message whose topic name is unknown, or whose payload does not decode,
reaches no one, and the bus logs a warning. If it has sticky set, it is
still stored, replacing any record with the same topic and keys, and
replays to no one.
BusClient::emit(topic) fills in the envelope: a fresh id, the encoded
payload, sticky from the topic's retention, source from the client's
app id, and keys from the payload's key fields. BusClient::retract(topic)
sends the same message with sticky false; for an ephemeral topic it logs
a warning and sends nothing.
5.5Sticky and persistent topics§
| Retention | Kept by the bus | On disk | Replayed to new subscribers |
|---|---|---|---|
| Ephemeral | No | No | No |
| Sticky | Latest value of each record, in memory | No | Yes |
| Persistent | Latest value of each record, in memory | Yes | Yes |
The bus stores sticky messages by record: the topic name plus the key
values. An unkeyed topic has one record. A keyed topic has one per key:
SetAppMenu is keyed by app_id, so each app's menu is its own record. A
new message for a record replaces it, whoever sent it. The bus trusts the
sticky flag; BusClient sets it from the topic's retention.
A record outlives the connection that wrote it: an app's records stay after
it exits, until something retracts them or, for sticky topics, the bus
restarts. Replay sends the stored message as it arrived, with its original
id and source; records restored from disk carry the source sola-bus.
5.5.1Retraction§
A message with sticky false on a sticky or persistent topic is a
retraction. The bus deletes the record named by its topic and keys, deletes
it from disk if the topic is persistent, and delivers the message to
subscribers, the publisher included, so they can drop their copies. A
subscriber tells an update from a retraction by the sticky flag.
5.5.2Persistence§
Persistent records live in $XDG_CONFIG_HOME/sola when XDG_CONFIG_HOME is
an absolute path, otherwise ~/.config/sola.
| Topic | File | Layout |
|---|---|---|
SessionApps, MailConfig, CalendarConfig, Background, Appearance, Zones, TerminalConfig, MonitorConfig | state.yaml | A top-level key named after the topic, holding the payload |
FloatGeometry, Application, TerminalSession | state.yaml | A top-level key holding a list, one entry per record |
Theme | theme/current.yaml | The whole file |
TabLayout | shell/tabs.yaml | The whole file |
CalendarAgenda | calendar/agenda.yaml | The whole file |
PaintSession | paint.yaml | The whole file |
BrowserConfig, BrowserHistory | browser.yaml, browser/history.yaml | The whole file |
BrowserTab | browser/tabs/<id>.yaml | One file per record |
The bus rewrites a file each time one of its records changes (a temporary
file renamed into place), and a retraction removes the entry or deletes the
file. It does not write a record whose key would make an unsafe file name
(empty, or containing /, NUL, or ..); it logs a warning instead.
The bus reads these files only at startup, skipping with a warning anything
that is not a persistent topic or does not match its schema. An edit made
while the bus runs is not seen, and the next write of that topic replaces
it. Unknown keys in state.yaml survive rewrites, but a state.yaml that
does not parse is treated as empty and replaced whole on the next write.
Encrypted fields (mail and calendar passwords and tokens) cross the socket
in clear and are written to disk encrypted with age, using the key in
~/.config/sola/key. If that key is lost, records holding encrypted fields
no longer load; the bus skips them with a warning.
5.6Topic reference§
Acted on by lists the processes that use a topic. Many apps (Shell,
Settings, Monitor, Paint, Calendar, and others) subscribe to every
kind and ignore most. "Apps with a window per tab" are Terminal, Browser
(its pages and web apps), Paint, Projects and Design, which do it through
the kit's TabWindows.
5.6.1Windows, input, and the compositor§
| Topic | Payload | Published by | Acted on by | Kept |
|---|---|---|---|---|
Windows | Every window: window_id, app_id, title, pid (when known) | sola-river | Shell, sola-session, Settings; kit apps, to learn their own window ids | Sticky |
OutputGeometry | The output's logical width and height | sola-river, when the compositor reports the output size and after a bus reconnect | Shell, Browser | Sticky |
WindowGeometry | One window's window_id, x, y, width, height | sola-river, when the window's size or position changes; retracted when it closes | Shell | Sticky by window_id |
WindowParent | A dialog's or picker's window_id and the parent window it belongs to | sola-river, from the compositor's parent event; retracted when the window closes or loses its parent | Shell (no row in the bar; composed above its parent) | Sticky by window_id |
WindowFloating | window_id, floating | Shell; sola-river re-sends its floating windows after a bus reconnect | sola-river (move, resize, shadow); kit apps (a float's frame resizes from its edges) | Sticky by window_id |
WindowChrome | window_id; tiled (in a tiled zone), cinema (covering the output), rim (its app published no menu, so it draws no frame of its own) | Shell; retracted when the window closes | sola-river (tiled state, the rim, rounded corners); kit apps (no frame in Cinema) | Sticky by window_id |
Composition | Window ids to show, bottom to top | Shell | sola-river | Ephemeral |
Frame | One window's window_id, x, y, width, height, and fullscreen flag | Shell | sola-river | Ephemeral |
Focus | The window_id to give the keyboard | Shell | sola-river | Ephemeral |
AppHidden | An app_id left out of Composition | Shell (Hide); retracted to show it again | Shell | Sticky by app_id |
CloseWindow | A window_id | Shell (a bar row's stoplight, for a window that is not a tab) | sola-river: asks that window to close, as its own close button would | Ephemeral |
MouseEntered | The window_id the pointer entered; still when the pointer did not move (the window under it changed) | sola-river | Shell | Ephemeral |
MouseLeft | None | sola-river | Shell | Ephemeral |
MouseClicked | The window_id clicked or touched | sola-river | Shell | Ephemeral |
RegisteredChords | Chords (keysym, modifiers) to route to the bus, not the focused window | Shell | sola-river | Sticky |
Chord | A registered chord was pressed: keysym, modifiers | sola-river | Shell, Browser, Design | Ephemeral |
ChordReleased | A registered chord was released: keysym, modifiers | sola-river | Browser, Design (Super held, for Super+click) | Ephemeral |
What sola-river does with these is in Compositor.
5.6.2The bar, tabs, and workspaces§
| Topic | Payload | Published by | Acted on by | Kept |
|---|---|---|---|---|
Tab | A window an app opens as a tab, announced just before it opens: key (<app_id>/<the app's tab id>, stable across restarts), app_id, pid, seq (how many windows that process has opened, this one included), window_id (once the shell has said, so a restarted shell pairs it directly), background (opened behind the tab its zone shows, without focus), icon (a picture file of the tab's own, a page's favicon; announced again when it changes) | Apps with a window per tab; retracted when the tab closes | Shell (pairs a process's tabs with its new windows, in order) | Sticky by key |
WindowTab | The tab a window is: window_id, tab (the token a kit app's tab window gives in its Wayland app id, <app_id>#<token>) | sola-river | Shell (pairs each window with the tab whose key has that token) | Sticky by window_id; retracted when the window closes |
Replayed | The bus has sent a subscriber every sticky it holds for what it asked for | The bus, to that client only, after each subscribe | Browser (its restored pages wait for Appearance or this; a web app page's window waits for its catalog entry or this) | Ephemeral |
WorkspacePrefs | How the workspaces show and step: islands (an island each in the top row, no switcher in the bar), skipped (the ids Super+Tab passes over while islands) (and stops_at_common, retired with Common, kept for the layout on the wire). Persistent | Settings (islands), the shell (skipped) | The shell, Settings | |
PackagesMoved | A list of package ids (pkg:sola-mail, …) an apply moved | cargo make oath apply (when pkg:sola did not move) | Session manager (starts again each of Sola's apps on an old program) | Ephemeral |
TabOpener | A new tab's key and the opener tab's key, sent just before its Tab | Projects (a session started by a session), through the kit's TabWindows::open_from | Shell: the window lands on the opener's workspace, whichever one is showing | Ephemeral |
TabWindow | A tab's key, its window_id, and shown (its window is on screen) | Shell; sent again when shown changes, retracted when the window closes | The tab's app (a browser page that is not shown stops painting) | Sticky by key |
TabActivity | A tab's key and its activity: Idle, Working, or Waiting (needs a decision) | Projects (a project window with an agent working or waiting), Design (a project window an agent is working in); sent first when the window opens, retracted when the tab closes | Shell (the mark in the row's icon slot) | Sticky by key |
ShowTab | A tab's key | Browser, Paint, Projects, Design (a notification click, a dialog in a page behind, tab.focus) | Shell: shows the tab's window in its zone, on its workspace, and focuses it | Ephemeral |
ShowWindow | A window_id | Calendar (an app bringing its own window forward) | Shell: as for ShowTab | Ephemeral |
CloseTab | A tab's key | Shell (a bar row's stoplight) | The app that opened the tab: closes its window as its own close button would | Ephemeral |
TabLayout | The bar as last saved: zones, its runs in order (each a zone, its apps as tab keys, or app ids for windows that are not tabs, shown, the index of the tab the zone shows, and its workspace); workspaces (id, name); showing; full (the workspaces whose Full zone is on screen) | Shell | Shell, at its next start: returning windows take their places | Persistent |
Workspaces | The workspaces in order (id, name) and the one showing | Shell | Browser (a new page window opens under the showing workspace's profile) | Sticky |
WindowWorkspace | window_id, workspace | Shell, once the window is where it belongs (not while it may still move to its tab's saved place); retracted when the window closes or is pinned | Browser (a page whose workspace has another profile reopens under it; a pinned page, with no workspace, keeps its profile) | Sticky by window_id |
PinnedTabs | Retired with Common; nothing sends it, and it stays because a topic's place in the list is its number on the wire | |||
FocusPinned | pinned: the focused window is on every workspace | Shell | The kit, in every app (the check on the Pin item in a titlebar's Window menu) | Sticky |
FloatGeometry | A floating app's last rectangle: app_id, x, y, width, height | Shell | Shell | Persistent by app_id |
The bar is described in Shell § The bar, its zones in Shell § Zones.
5.6.3Apps, session, and menus§
| Topic | Payload | Published by | Acted on by | Kept |
|---|---|---|---|---|
LaunchApp | app_id, command | Shell (launcher), Browser (--app when no browser is running; Open in Vault when vault.open does not answer), sola-vaultd (vault.open) | sola-session | Ephemeral |
LaunchResult | app_id, command, ok, error | sola-session | Shell | Ephemeral |
UserAppExited | app_id, command, exit code or signal | sola-session | Shell | Ephemeral |
CloseApp | An app_id | Shell (Super+Q); kit apps closing themselves (a single-window app's close button, the Browser's last page) | sola-session (stops it), sola-river (asks its windows to close), the named app (quits), Browser (closes a web app's windows) | Ephemeral |
SessionApps | Open apps: app_id, command | sola-session | sola-session, at its next start | Persistent |
Application | A launcher entry: app_id, label, command, icon, kind, url, profile | Settings; Browser (web apps) | Shell, Settings, Browser | Persistent by app_id |
SetAppMenu | app_id and its menus: labels and items (action id, label, shortcut, disabled, checked) or dividers | Each app with a menu, at start | Shell (registers the focused app's Super shortcuts; an app with no menu gets the compositor's rim) | Sticky by app_id |
MenuAction | app_id, action_id of a chosen item or shortcut | Shell (a Super shortcut of the focused app's menu); a kit app's titlebar Window menu; a second launch of Terminal or Projects, to the running one | The app named by app_id; the Shell for Window-menu actions, which it applies to the focused window | Ephemeral |
Shutdown | None | Shell (Exit Sola, then the shell exits) | sola-session (stops the apps it started, then exits), sola-river (exits) | Ephemeral |
DeskMenuOpen | A right-click: app_id, pid, token (the app's key for this menu), x, y (in the clicked window's logical pixels), context (editable, secret, selection, and site, a page's address without its query or fragment; never the text), items (the app's rows: actions with id, label, shortcut, enabled; separators; submenus), edit (which Edit rows apply) | Kit text fields; Browser (a page); Terminal and Projects (a pane) | Shell (draws the right-click menu) | Ephemeral |
DeskMenuPick | app_id, pid, token, and the pick: Action (a row's id), Edit (Cut, Copy, Paste, SelectAll), VaultInsert (a ticket to collect a value from the vault's socket; never the value), VaultFill (an item's id, for a page), or Dismissed | Shell | The process pid names: the field or pane that opened the menu | Ephemeral |
Launching and closing are described in Session manager and Applications. App menus appear in each window's titlebar; the kit draws them from the app's own copy, not from the bus.
5.6.4The desk's look§
| Topic | Payload | Published by | Acted on by | Kept |
|---|---|---|---|---|
Theme | The active theme (Theme and toolkit) | Shell (at start when no saved theme exists, and the colors rewritten from the picture while Adapt colors to the background is on) | Every kit app; sola-river (the rim's colors) | Persistent |
Background | path (Sola's copy under ~/.config/sola/background/), set_at (unix ms, so setting a file of the same name again is a change) | Paint (File → Set as Background), Browser (Use as Desktop Background), Settings → Appearance (a kept picture), solactl background set; retracted by Settings → Appearance (Clear) and solactl background clear, for the black ground | sola-river (draws it behind the windows), Shell (the theme's colors), Settings | Persistent |
Appearance | adapt_theme (the theme's colors follow the picture), strength (0–100, default 60), variation (which of the picture's schemes), colors (your window, panels, highlight, accent as #rrggbb), customs (your looks: a variation and colors each), custom (the look in use, or none), opacity (the desk at rest, 50–100 %, default 90), fade_idle (default on), fade_after_min (default 5), fade_over_min (default 30), dim_idle (default on), dim_after_min (default 60), dim_over_min (default 30), light (the light theme for the desk and its windows, and web pages are told light; off, dark; default off), lock_idle (lock once the idle fade is complete; default off), pictures (each kept picture's path → the look it was last worn with: adapt_theme, strength, variation, colors, customs, custom) | Settings → Appearance; Shell (pictures, and a kept picture's look put back when it is set again) | Shell (the theme's colors; lock_idle); sola-river (the desk's opacity, the idle fade and dim); Browser (light: every page's prefers-color-scheme); sola-vaultd (fade_idle and fade_after_min, for the vault's idle lock) | Persistent |
ThemeVariations | path (the picture), choices (per scheme: name, and base, raised, accent, and source, the picture's color it comes from, or for a complement the turned color itself) | Shell | Settings (the scheme cards) | Sticky |
IdleKeep | x, y, w, h: output-local logical pixels, the top row's flower and agent islands | Shell | sola-river (that part stays at the desk's rest during the fade) | Sticky |
IdleFade | fading (no input for the fade's delay), out (the fade is full: the windows are out of sight) | sola-river | Projects (a focused session's island shows); Shell (on out, reports every tab window off screen in TabWindow); sola-vaultd (the vault's idle lock counts from it) | Sticky |
WindowVideos | window_id, videos (each x, y, w, h, window-local logical pixels, the part in view, and watched: playing with sound) | Browser (per page window; retracted when it shows none) | sola-river (no overlay over them; a watched one in sight holds the fade; retracts a closed window's) | Sticky, one per window_id |
The fade and the dim are described in Compositor § The idle fade.
5.6.5App state§
| Topic | Payload | Published by | Acted on by | Kept |
|---|---|---|---|---|
MailConfig | Mail accounts, identities, filter rules | Settings | Mail, Settings | Persistent |
MailStatus | inbox_unread | Mail; retracted when Mail quits | Shell (the unread count in the top row) | Sticky |
CalendarConfig | Calendar accounts and the calendar list | Settings, Calendar | Calendar, Settings | Persistent |
CalendarAgenda | days, today through the next week: each a date (YYYY-MM-DD) and its events (id, title, color, all_day, start, end in unix seconds, location) | Calendar | Shell (the clock's panel, also while Calendar is closed) | Persistent |
VaultState | signed_in, unlocked, email, server (its web vault's address), synced_at (unix seconds of the last sync that worked, or of the one kept on disk after an unlock without network), syncing, sync_error, items (the trash not counted; 0 while locked); no item names and no values | sola-vaultd, on every change (Vault) | Shell (the flower menu has Lock Vault while unlocked) | Sticky |
ScreenLock | locked | sola-lock: true once the compositor confirms the lock, false once you unlock it. A lock screen that dies says nothing, so true stands while the compositor keeps the screen locked (Security) | sola-vaultd (locks the vault, unless Lock when the screen locks is off) | Sticky |
TerminalSession | One Terminal tab: id, tmux session, working directory, order, pane layout | Terminal | Terminal | Persistent by id |
PaintSession | The open images' paths and the selected one | Paint | Paint | Persistent |
Zones, TerminalConfig, MonitorConfig, BrowserConfig, BrowserHistory, BrowserTab | Declared only | Nothing | Nothing | Persistent (BrowserTab by id) |
No current app publishes or reads the last six; the bus still restores their files if they exist.
5.6.6Handoff, notifications, and presence§
| Topic | Payload | Published by | Acted on by | Kept |
|---|---|---|---|---|
OpenUrl | url, activate | Nothing in Sola; solactl emit can | Browser (a new page window, in front if activate is set; ignores a message from itself); Shell (starts Browser with the URL when it is not running) | Ephemeral |
OpenImage | path, activate, optional app_id (absent means Paint) | A second Paint launch, handing its paths to the running one | Paint, Preview (each only when addressed) | Ephemeral |
AppToast | text | Browser (after Use as Desktop Background); solactl emit can | Shell (the top row, 5 seconds) | Ephemeral |
AppNotification | id, app_id, source, title, body, optional tag, tab_id, url | Browser (pages and web apps), Calendar (reminders), Projects | Shell (a card on the desk) | Ephemeral |
NotificationActivate | The clicked notification's id, app_id, tab_id, url | Shell | The app named by app_id: Browser (and its web apps), Calendar, Projects | Ephemeral |
AgentIsland | A session's key (its pane), name, project, state (Waiting or Done), title, since | Projects; retracted when the session works again, closes, is looked at, or is dismissed | Shell (its island in the top row) | Sticky by key |
AgentIslandAction | An island's key and what was done: Show, Dismiss | Shell | Projects (brings up its session's window, or lets it go) | Ephemeral |
ClientConnected | An app id | sola-bus on $identify; replayed as the roster | No app; Monitor shows it | Ephemeral |
ClientDisconnected | An app id | sola-bus when an identified connection closes | No app; Monitor shows it | Ephemeral |
5.7Ordering and delivery§
- Each connection is read on its own thread, so one connection's messages are handled in the order sent. Storing, queueing, and persisting happen under one lock, and every subscriber receives messages in the order the bus handled them. Disk writes hold up all traffic until they finish.
- Delivery is at most once. Each connection has an outgoing queue of 256
messages, written by its own thread. The bus never waits for a
subscriber: when a queue is full, the message is dropped for that
subscriber and the bus logs
dropped (queue full). There is no acknowledgement, retry, or redelivery. - Sticky replay uses the same queue, and replayed records that do not fit are dropped without a log line.
- An ephemeral message reaches only the connections subscribed when the bus handles it.
BusClientreads into an unbounded in-memory channel on its own thread;emitwrites on the caller's thread.
5.8When the bus restarts§
A new sola-bus process starts with only what is on disk: persistent
records come back; sticky records, the roster, and every subscription are
gone. BusClient does not reconnect by itself; the program that owns it
does:
| Program | Reconnects |
|---|---|
| Kit apps | Every 250 ms from the kit's bus poller, then re-sends the subscription given to BusSetup |
sola-river | Every 20 ms, then re-subscribes and re-publishes OutputGeometry, Windows, each WindowGeometry, and its floating windows |
sola-session | Blocks, retrying every second, then re-subscribes |
On every connection, BusClient::connect sends $identify and then the
messages queued while it had no connection. On every connection after the
first, it then re-publishes its sticky outbox: the last value of every
sticky or persistent record this client published and has not retracted.
Each process rebuilds its own records as it reconnects, so a client that
subscribes early receives some records by replay and the rest as live
messages. When two processes wrote the same record, the last to reconnect
wins. A message published after the bus died but before the client noticed
fails with a write error and is not queued; ephemeral ones are lost.
5.9Using the bus from Rust§
In a kit app:
BusSetup::new(app_id)takes the subscription (.subscribe(kinds)), the app menu (.app_menu(...),.window_menu()), and call-plane methods (.calls(owner, methods), see Call plane)..install()connects (retrying every 250 ms until the bus answers), identifies, subscribes, publishesSetAppMenu(and keeps it for the window's titlebar), and stores the client process-wide.sola_kit::app::bus_subscription()is an icedSubscriptionofArc<Message>. A thread polls the client every 8 ms and reconnects after a bus restart; while no iced subscription is attached it buffers up to 512 messages, dropping the oldest. The client has one receiver: use this ortry_recv, not both.sola_kit::app::bus()returns the client behind a mutex, foremitandretract; it panics beforeinstall(try_bus()returns anOption).set_bus_kinds(kinds)changes the set re-sent after a reconnect.sola_kit::apply_theme_updateapplies aThememessage (Theme and toolkit);sola_kit::is_self_quitrecognises aMenuActionwith actionquitfor the app, or aCloseAppnaming it.
// main(), before building the iced application:
sola_kit::app::startup(APP_ID);
BusSetup::new(APP_ID)
.subscribe(&[TopicKind::Theme, TopicKind::MenuAction, TopicKind::CloseApp])
.app_menu("Example", [("quit", "Quit Example", KeyCode::Q.meta())])
.install();
// subscription(): sola_kit::app::bus_subscription().map(Msg::Bus)
// update(), Msg::Bus(m):
if sola_kit::apply_theme_update(&m, &mut self.theme) { return Task::none(); }
if sola_kit::is_self_quit(&m, APP_ID) { return iced::exit(); }
if let Some(topic) = Topic::parse(&m) { /* … */ }
// Publishing:
if let Ok(mut b) = sola_kit::app::bus().lock() {
let _ = b.emit(Topic::AppToast(AppToast { text: "Saved".into() }));
}
Without the kit, use sola_bus::BusClient directly:
| Method | Does |
|---|---|
BusClient::new() | An unconnected client; send and emit queue until it connects |
set_app_id(id) | Sets source for later publications and sends $identify |
connect(), connect_to(path), connect_blocking(interval) | Opens the socket and does the reconnect work above; the last retries until it succeeds |
is_connected() | False once the reader thread has seen the socket close |
subscribe(kinds), emit(topic), retract(topic), send(message) | Sends $subscribe, a typed publication or retraction, or a raw Message |
recv(), recv_timeout(d), try_recv() | The next message, waiting without limit, up to d, or not at all; None when nothing arrives or the connection is gone |
notify_fd(), drain_notify() | A descriptor that becomes readable when messages arrive, for event loops such as calloop |
Decode a message with Topic::parse(&message), which returns None for an
unknown topic or a bad payload.
5.10Watching and poking the bus§
Monitor shows live traffic and every stored record.
solactl emit publishes one topic (solactl):
solactl emit AppToast '{"text":"hello"}'
solactl emit OpenUrl '{"url":"https://example.com","activate":true}'
solactl emit CloseApp '"sola-preview"'
The payload is the topic's JSON form, the shape Monitor shows; a topic
without a payload takes any JSON value, such as null. In JSON,
Encrypted fields are age1enc: ciphertext. solactl connects as
solactl, publishes with BusClient::emit, waits 50 ms, prints
emitted <Topic>, and exits. A sticky or persistent topic is stored (and
written to disk) like any other publication and stays after solactl
exits. solactl has no general retract; solactl background clear
retracts Background. An unknown topic (the error lists the valid names),
invalid JSON, a payload that does not match, or an unreachable bus exits
with status 3.
To log every message the bus handles, give sola-bus a RUST_LOG that
includes sola_bus::traffic=trace (for example
RUST_LOG=info,sola_bus::traffic=trace). Each message adds a line with its
connection, topic, sticky flag, source, and keys to sola.log. Nothing
rotates sola.log on Oath, so turn the trace off again.
5.11Limits§
- Topics are compiled in. An app cannot define one at run time, and
processes built from different
sola-bussources can misread each other's subscriptions and payloads. - No authentication or per-app permissions: any process can overwrite or retract any record.
- Delivery is at most once; a full queue drops messages.
- Sticky records do not survive a bus restart unless their owners re-publish them.
- A
state.yamlthat does not parse is replaced on the next persistent write, losing its other entries. - No request/reply; use the call plane.
6The call plane§
sola-call is Sola's request/reply plane. One process, sola-call, holds a
live registry of methods. A process that offers methods (an owner)
connects and advertises them; a caller sends a request to one owner and
gets exactly one reply or an error; an observer watches the registry and
every call. solactl and its MCP server are callers, as is the shell (for
screenshots, and for the launcher's Projects row: window.navigate).
Monitor is an observer.
The command reference for solactl is solactl. This chapter
explains the plane it talks to.
6.1Why it is separate from the bus§
The bus (The bus) fans out facts: anyone may emit a topic, every
subscriber gets it, and there is no request id, no reply, and no way to
tell that nobody was listening. That is right for announcements and sticky
state (windows, focus, theme, menus, LaunchApp).
Some operations need an answer for one caller: take a screenshot and say where it went, click here, open this tab, tell me if it worked. The call plane gives each request an id, a deadline, a named owner, and one reply. When the owner is not connected, the caller gets an error at once. The host never starts an application to answer a call. Screenshots, synthetic input, and app control (Projects, Browser, Design, Bots) are calls; none of them are bus topics.
The bus, the call plane, and Wayland are the three planes of a session (Architecture).
6.2The host§
On Oath, PID 1 runs sola-call as a service and starts it again when it
exits with an error (Architecture).
- Socket.
$SOLA_CALL_PATHif set, else$XDG_RUNTIME_DIR/sola-call. WhenXDG_RUNTIME_DIRis unset, the runtime directory is/run/user/<uid>if that exists, else/tmp. At start the host removes any old socket file, creates the parent directory, binds, and sets mode0600. Any process of the same user can connect; there is no other access control. - State. The registry (owner, app id, methods), the calls in flight,
and the observers live in memory, one thread per connection. Nothing is
saved. After a restart of
sola-callthe registry is empty; owners reconnect and advertise again within about 400 ms. - No confirm. No method asks a person before it runs. Which methods should is an open question (D3).
6.3Roles and the handshake§
Every connection starts with a hello frame that names its role and its
app_id. A connection whose first frame is anything else is closed.
| Role | Hello carries | Then sends | Receives |
|---|---|---|---|
provider | app_id, owner | advertise at once; later reply frames | invoke frames |
caller | app_id | invoke and list, any number | reply and catalog |
observer | app_id | list (optional) | catalog and trace |
The owner is the noun callers use (compositor); the app id is the
process (sola-river). A provider whose hello has no owner, or whose
second frame is not advertise, is closed.
One provider holds an owner name. A later provider that says hello with the same owner replaces the earlier one. Limit: when the replaced connection then closes, the host removes the owner from the registry even though the newer provider is still connected; the owner stays missing until that provider's connection is made again.
6.4Methods and arguments§
A provider advertises a list of method specs.
MethodSpec field | Meaning |
|---|---|
name | Method name within the owner: windows, tab.open, input.click |
summary | One line, shown by solactl <owner> and in MCP tool descriptions |
args | Argument specs (below) |
timeout_ms | Suggested deadline for callers; omitted means 8 s |
Names are lower case; a dotted name groups related methods (pane.list,
pane.send). Monitor and this manual write a call as owner.method
(compositor.screenshot).
ArgSpec field | Meaning |
|---|---|
name | The key in the request's params object |
long | The --flag that solactl accepts |
short | An optional one-letter -f |
type | string, int, float, bool, path, or json |
required | Marks the argument required in help and MCP schemas, and lets solactl fill it from a bare word |
help | Shown by solactl <owner> <method> --help and in the MCP schema |
The host checks only that the method name is in the owner's advertised list. It does not check arguments. Each owner reads the keys it knows, ignores the rest, and answers a missing required argument with an error reply; how it treats a value of the wrong type is up to that owner.
path is a string with one difference: solactl and the MCP server make
it absolute against the caller's working directory before the call (~
expands too), because the owner runs in its own process with its own
working directory.
6.5Keeping the catalog live§
The catalog is the set of connected providers. An owner's entry lasts as long as its connection: when the provider exits, crashes, or its connection breaks, the host removes it and tells the observers.
The client library reconnects a provider every 400 ms while it is not
connected, then sends hello and advertise again. An app that starts
before sola-call, or outlives a restart of it, appears as soon as the
socket answers.
A provider may send advertise again on the same connection to replace its
list. The client library does not, so an owner's methods are fixed for as
long as its process runs. list returns the catalog sorted by owner.
6.6Wire format§
Each frame is a 4-byte little-endian unsigned length followed by that many
bytes of UTF-8 JSON. A reader refuses a frame over 16 MiB and closes that
connection, so large results travel as file paths, not bytes (screenshots
reply with a path).
Every message is a JSON object with a type:
type | Fields | Sent by |
|---|---|---|
hello | role, app_id, owner (providers) | everyone, first |
advertise | methods | provider |
invoke | id, method, params, owner, timeout_ms | caller; host to provider |
reply | id, ok, error or data | provider; host to caller |
list | none | caller or observer |
catalog | owners: owner, app_id, methods | host |
trace | the trace event fields | host to observers |
A call, as the frames' JSON:
caller → host {"type":"hello","role":"caller","app_id":"solactl"}
caller → host {"type":"invoke","id":"0199…","owner":"compositor",
"method":"windows","params":{},"timeout_ms":5000}
host → provider {"type":"invoke","id":"0199…","method":"windows",
"params":{},"timeout_ms":5000}
provider → host {"type":"reply","id":"0199…","ok":true,
"data":{"sola-shell":[{"title":"…","window_id":…,"pid":…}]}}
host → caller {"type":"reply","id":"0199…","ok":true,"data":{…}}
- Ids. The caller chooses the request id; the client library uses a
UUIDv7 string. The host tracks calls in flight by id across all callers,
so ids must be unique. The host forwards the invoke under the same id,
without
owner, with the caller'stimeout_ms. - Params. Any JSON value; owners expect an object.
- Replies.
ok: truewithdata(any JSON,nullincluded), orok: falsewith anerrorstring. Only the first reply for an id counts; later replies for it are discarded.
The host answers some invokes itself:
| Error | When |
|---|---|
invoke missing owner | The invoke has no owner |
<owner> is not running | No provider holds that owner |
<owner> has no method <method> | The owner did not advertise that name |
timeout | The deadline passed with no reply |
6.7Deadlines and failures§
Each invoke carries timeout_ms; without one the host uses 8000. The host
checks deadlines every 200 ms, so a timeout reply arrives up to about
200 ms after the deadline. The owner is not told that the caller gave up;
a reply that comes later is discarded.
| Situation | What the caller sees |
|---|---|
sola-call is not running | Connecting fails: call host is not running (<socket path>) |
| Owner not connected | At once: <owner> is not running |
| Method not advertised | At once: <owner> has no method <method> |
| Owner slow | timeout at the deadline |
| Owner exits or crashes during the call | timeout at the deadline. The owner leaves the catalog at once, but its pending calls wait out their deadlines |
| Kit app before its UI loop is running | The call is dropped: timeout at the deadline |
| Kit app whose UI loop stopped taking calls | At once: the app is running but its UI loop is not draining calls. Close that window (window X or Flower) and launch it again. Do not kill the process from a bot. |
sola-call restarts during the call | call host closed the connection |
How solactl turns these into messages and exit statuses is in
solactl § Output and exit status.
6.8Observers§
An observer has the same privilege as a caller and sees all traffic: every
invoke's params and every reply's data.
- Catalog. The host sends a
catalogon connect, after everyadvertise, and after every owner leaves. An observer may also sendlistfor one. - Traces. The host sends a
tracefor every event:
kind | When | Fields set |
|---|---|---|
invoke | A caller's invoke reaches the host | id, owner, caller, method, params |
reply | A reply is passed back, or the host refuses the invoke because the owner or method is missing (duration_ms 0) | id, owner, caller, method, ok, error, data, duration_ms |
timeout | A deadline passes | id, owner, caller, method, ok: false, error: "timeout", duration_ms |
advertise | A provider advertises | owner, data (its methods) |
unregister | An owner leaves | owner |
caller is the app_id from the caller's hello. Limit: the library's
one-shot invoke always says hello as solactl, so calls from the shell
and the MCP server also trace as solactl.
Monitor is the observer Sola has: its Call view lists invokes with owner, method, caller, status, and duration, and its right rail is the live catalog.
6.9Owners today§
| Owner | Process | Present | Methods |
|---|---|---|---|
compositor | sola-river | Whole session | screenshot, sample, cursor, windows, input.click, input.press, input.release, input.move, input.scroll, input.key |
desk | sola-session | Whole session | launch, close |
bots | sola-botsd | While sola-botsd runs | list, poll, new, setup, send, transcript, rm, cancel |
projects | sola-projects | While Projects runs; it quits when its last project window closes | ps, project.list, window.navigate, project.open, project.close, session.list, session.new, session.end, session.select, session.set, session.exec, pane.list, pane.send, pane.read, pane.wait, whoami |
browser | sola-browser | While the Browser runs | tabs, tab.open, tab.close, tab.focus, goto, back, forward, reload, stop, find.page, snapshot, find, click, hover, key, scroll, type, fill, get, links, select, wait, screenshot |
design | sola-design | While Design runs | project.list, project.add, project.remove, open, canvas.new, canvas.close, status, share, share.rotate, selection, view, wait, reload, screenshot, pdf, inspect |
vault | sola-vaultd | While the vault service runs | status, lock, sync, open; none returns an item or a value (Vault) |
The compositor and desk owners belong to services PID 1 keeps running,
and so does vault: Oath runs sola-vaultd as svc:sola-vault.
sola-botsd ships with Sola, but Oath runs no service for it, so bots
is present only while you run it. The other three exist only while their
application runs; the plane does not start it for you. Every method and
flag is in solactl.
6.10solactl on the call plane§
solactl has two kinds of call commands.
- Compiled commands,
compositoranddesk, are parsed bysolactlitself: fixed flags, defaults, and--helpthat works with nothing running.solactlbuilds the params and invokes the method. - Live-owner commands,
projects,browser,bots,design, and any other name, are read from the catalog.solactl <owner>lists the owner's methods andsolactl <owner> <method> --helpits flags, both from the advertised specs. The name may be the owner or its app id. These needsola-calland the owner running, even for help.
For a live-owner command, solactl turns flags into params by the
method's ArgSpecs: --long VALUE or -s VALUE, bool flags with no
value, a bare word for the next required argument not yet given, or a
single JSON object instead of flags. Values are converted by type, and
path values are made absolute against your working directory. It adds a
little context of its own: in a Projects pane, whoami and
session.new get your pane from $SOLA_PANE_ID (and whoami your
checkout from $SOLA_WS_PATH); design canvas.new without --path uses
your working directory.
The deadline is the method's --timeout argument plus 2 s (at least 8 s)
when you pass one, else its advertised timeout_ms rounded up to whole
seconds, else 8 s. The reply's data is printed as indented JSON. The
full rules are in solactl § Live-owner commands.
6.11MCP (solactl mcp)§
solactl mcp is an MCP server on stdio for agents. It is an adapter over
the same methods, not a second protocol: what a tool may do is what
solactl may do.
- Transport. JSON-RPC 2.0, one message per line. It answers
initialize,ping,tools/list, andtools/call; any other method gets error-32601, and a line that is not JSON gets-32700. It offers protocol versions2025-06-18,2025-03-26, and2024-11-05(the client's if it asks for one of these, else the newest). ItsserverInfo.nameissola, and itsinstructionsexplain the tool names and how to work with Design. - Tools. One tool per live method, named
<owner>_<method>: every character other than a letter, digit, or-becomes_, and the name is cut at 64 characters (browser_tab_open,design_screenshot). The description is the summary, the app id, and the matchingsolactlcommand.--owner NAME(repeatable; owner or app id) limits the tools to those owners. - Schemas. Each
ArgSpecis a property named byname:stringandpathare JSON strings (a path's description ends in(path)),intisinteger,floatisnumber,boolisboolean, andjsonisobject. Required arguments are listed inrequired. - Calls. Arguments must be a JSON object or absent. They are passed as
sent, with no type conversion, except that
patharguments are made absolute against the server's working directory (the agent's). The Projects and Design context, the bot rules for the Browser, and the deadline are the same as forsolactl. A Design page call withoutcanvasgoes to the canvas this server last opened or created. Each call runs on its own thread, sopingandtools/listare answered while a long call waits. - Results. A reply becomes a text item holding the indented JSON (
okfornull). When the reply has apathending in.png,.jpg, or.jpegand the file is at most 3,700,000 bytes, the image is attached as a second, base64 item; a larger file gets a text note with its size instead. An error is a text item withisError: true; a call whose deadline passes saystimeout waiting for <owner>.<method>, with a note that an application still on screen has a wedged call channel and should be closed from its window and launched again. - Changes. The server rereads the catalog every 2 s and sends
notifications/tools/list_changedwhen the set of tool names changes, once the client has sentnotifications/initialized. Whensola-callis down,tools/listis empty; calling a tool whose owner is gone returns this error:no live tool "<name>" — is that app running? (tools follow the apps that are up).
Limit: the compositor's SOLA_BOT=1 refusals belong to the compiled
solactl compositor command, so a bot's MCP server still offers the
compositor tools. Register the server with an agent as a stdio command,
for example claude mcp add sola -- /bin/solactl mcp.
6.12Offering methods from an application§
A kit application advertises its methods when it sets up the bus, then handles calls in its iced loop.
BusSetup::calls(owner, methods)beforeinstall()starts the provider once the bus is connected.CallSetup::new(owner, app_id), then.methods(…)and.install(), does the same without the bus.sola_kit::call_subscription()is aSubscription<Incoming>; fold it into the application's subscription. Use it or drain the receiver yourself, not both; a process has one call receiver.- An
Incominghasmethod,params, andreply. Answer withreply.ok(json)orreply.err(message), exactly once. A reply that is dropped unanswered leaves the caller waiting for its deadline. - A process that is not a kit app calls
sola_call::start_provider(owner, app_id, methods)and drains the returned channel in its own loop, assola-river,sola-session, andsola-botsddo. - A caller uses
sola_call::invoke(owner, method, params, timeout)andsola_call::catalog(); an observer usessola_call::start_observer, or the kit'sinstall_observerandobserve_subscription.
use sola_kit::sola_call::{ArgSpec, ArgType, Incoming, MethodSpec};
fn methods() -> Vec<MethodSpec> {
vec![MethodSpec {
name: "note.add".into(),
summary: "Add a note".into(),
args: vec![ArgSpec {
name: "text".into(),
long: Some("text".into()),
short: Some('t'),
ty: ArgType::String,
required: true,
help: "Note text".into(),
}],
timeout_ms: None,
}]
}
// main, before iced starts
sola_kit::BusSetup::new(APP_ID)
.calls("notes", methods())
.install();
// subscription
sola_kit::call_subscription().map(Msg::Call)
// update
Msg::Call(inc) => match inc.method.as_str() {
"note.add" => match inc.params.get("text").and_then(|v| v.as_str()) {
Some(text) => {
self.add(text);
inc.reply.ok(serde_json::json!({ "ok": true }));
}
None => inc.reply.err("text required"),
},
other => inc.reply.err(format!("unknown method {other}")),
},
With that application open, solactl notes note.add --text hi (or
solactl notes note.add hi) calls it, and an agent sees the tool
notes_note_add.
Some choices keep a method easy to call:
- Keep
longequal toname.solactlsends a--longvalue under the flag's own text, so a differentlongreaches the owner under a key it does not expect. - Do not use
has a short flag;solactlreads-has help. - Give a method that can run longer than 8 s a
timeout_ms, or atimeoutargument in seconds, whichsolactland MCP turn into the deadline. - Reply with a file path for large results.
7Compositor§
On Oath, Sola's compositor is rivulet, Oath's C port of River 0.4.5. The
compositor draws the screen, owns the input devices, and talks to every
application. It does not decide where windows go: that is the job of a
separate window manager client, and in Sola that client is sola-river.
sola-river is also the bridge between the compositor and
the bus: it turns window events into bus facts, and the shell's
decisions into compositor requests.
Policy lives in the Shell: which windows are on screen,
which has the keyboard, which zone a window is in. sola-river applies
those decisions and adds the few rules of its own that this chapter lists.
Architecture shows how the pieces fit together.
7.1The compositor§
rivulet serves River's protocols at the same versions, so sola-river
drives it as it drives River; this chapter says the compositor for
either. River 0.4 is built around the river-window-management-v1
protocol: the compositor keeps the scene, the surfaces, and the input
devices, and one window manager client sets window sizes, positions,
stacking, keyboard focus, key bindings, and fullscreen state. It accepts
one window manager at a time.
On Oath, PID 1 runs the compositor as a service (svc:rivulet), and
Sola's own processes as services too: svc:sola-bus, svc:sola-call,
svc:sola-river, svc:sola-shell, svc:sola-session, and
svc:sola-kvm. Sola's process manager, sola, refuses to start on Oath
(Process manager).
7.1.1What sola-river uses§
| Global | Used for |
|---|---|
river_window_manager_v1 (up to version 4) | Windows, focus, stacking, fullscreen, decorations, and the surfaces sola-river draws itself (the background picture, the idle fade) |
river_xkb_bindings_v1 | The shell's key bindings |
river_layer_shell_v1 | Showing layer-shell clients (Layer shell) |
river_xkb_config_v1, river_input_manager_v1 | NumLock on for each keyboard |
river_libinput_config_v1 | Natural scrolling |
zwlr_output_manager_v1 | Picking each output's mode |
zwlr_screencopy_manager_v1, wl_output, wl_shm | Screenshots and sample |
ext_foreign_toplevel_list_v1, ext_foreign_toplevel_image_capture_source_manager_v1, ext_image_copy_capture_manager_v1 | Screenshots of one window |
zwlr_virtual_pointer_manager_v1, zwp_virtual_keyboard_manager_v1 | Synthesized input (input.*) |
ext_idle_notifier_v1, wp_alpha_modifier_v1 | The desk's opacity, the idle fade, and the dim |
wp_cursor_shape_manager_v1 | The move and resize cursors |
wl_compositor | Shadows, rounded corners, the background picture, the fade |
Only the first is required: without it sola-river exits. Without one of
the others, the feature it serves is missing; sola-river warns at start
when river_xkb_config_v1 is absent.
7.2sola-river§
sola-river is three things in one process: the compositor's window
manager (a Wayland client bound to river_window_manager_v1), a bus
client, and a call-plane provider with the owner name compositor.
At start it looks for the compositor's socket. It reads the name from
$XDG_RUNTIME_DIR/sola-wayland when that file exists (Sola's process
manager writes it; on Oath nothing does). Otherwise it waits up to 15 s
for a wayland-N socket in the runtime directory that accepts a
connection (the lowest name first, skipping wayland-x* and .lock
files), then falls back to $WAYLAND_DISPLAY, then wayland-0. It then
waits up to 5 s for the socket file and exits with status 1, logging
wayland socket never appeared, if it does not come. The runtime
directory is $XDG_RUNTIME_DIR, else /run/user/<uid> when that exists,
else /tmp.
If the compositor does not offer the window-management protocol,
sola-river exits with river_window_manager_v1 not advertised. If
another window manager already holds it, the compositor refuses and
sola-river logs river_window_manager_v1 unavailable.
7.2.1The manage and render cycle§
The compositor batches window management into sequences. When something
changes, it sends the new state followed by manage_start. The window
manager answers with sizes, focus, key bindings, close and fullscreen
requests, and tiled state, then manage_finish. The compositor configures
the windows, sends their new dimensions and render_start, and the window
manager answers with positions, stacking order, visibility, borders, and
its own surfaces, then render_finish. Everything decided in one cycle
reaches the screen in the same frame.
sola-river reads the bus every 20 ms. Stacking, frames, focus, and key
binding changes that arrive between two reads are folded together; if
anything changed, sola-river asks the compositor for a new cycle. A
raise, which the shell sends as a new stack, a frame, and a focus change
at once, lands in one cycle.
sola-river never blocks that loop: screenshot encoding and picture
decoding run on threads of their own, because River drops a window
manager that stops answering for more than 3 s.
7.2.2Who decides what§
| Concern | Decided by | What sola-river does |
|---|---|---|
| Stacking and visibility | Shell (Composition) | Shows the listed windows bottom to top, hides the rest |
| Keyboard focus | Shell (Focus) | Moves the seat's focus to that window |
| Size and position of tiles, of a window in Cinema, and of the shell's own windows | Shell (Frame) | Proposes the size, sets the position |
| Size of a floating window | The application, unless the shell gives it a rectangle (a remembered one, a tile floated in place, gamescope's nest size) | Lets it choose; centers it once if nothing placed it |
| Floating | Shell (WindowFloating) | Allows move and resize, draws the shadow |
| Tiled state and the rim | Shell (WindowChrome) | Tells tiled windows they are tiled; rims windows whose app draws no frame (Frames, rims, and corners) |
| Key bindings | Shell (RegisteredChords; your changes in Keybindings) | Registers them with the compositor, reports presses; registers none while Settings waits for a key (KeyCapture) |
| Fullscreen | The application, or Cinema | Grants it, and tells the shell (FullscreenRequest); ends it when focus moves away |
| The background picture | Whoever sets Background | Draws it behind everything (The background picture) |
| The desk's opacity, the idle fade and dim | Settings → Appearance (Appearance) | Draws the picture, then black, over the desk (The idle fade) |
| Output mode | sola-river | Picks a mode for each output |
7.3Windows§
7.3.1Ids§
sola-river numbers every window, counting from 1 in the order windows
appear. This window_id is not reused while sola-river runs, and a
window keeps it when sola-river restarts: River names each window with
an identifier of its own (river_window_v1.identifier), and sola-river
keeps which id it gave each one in
$XDG_RUNTIME_DIR/sola-river-windows.json (for this compositor only; a
new rivulet starts over), with the size it last gave each: a window found
again is offered that size at once, not 0×0, so it does not resize on the
way back. New windows count on from the highest. Every
window topic on the bus and the windows call use it.
A window joins the Windows list once the compositor has reported both its
app id and its title. Each entry carries window_id, app_id, title,
and pid, the process the compositor attributes the window to. The
protocol marks that pid unreliable; Sola uses it to find a window's
process and to notice when the process is gone.
When the compositor reports that a window belongs to another (a dialog or
a file picker), sola-river publishes WindowParent for it, and retracts
it when the window closes or loses its parent. The shell gives such a
window no row in the bar and stacks it above its parent.
Gamescope's window often reports an empty app id. When a window's app id
is empty and its process's command line names gamescope, sola-river
sets the app id to gamescope, and the title to Gamescope if the title
is empty too.
7.3.2Stacking and visibility§
The shell sends the whole stack as Composition: a list of window ids
from bottom to top. On each render sola-river shows every listed window,
places them on top in list order, and hides every window not in the list.
A hidden window is not drawn and receives no pointer input; its client
keeps running.
A window that appears before the shell lists it stays hidden until the shell does. The shell's overlays (launcher, menus, notifications) rely on this: they stay mapped at 2×2 while dismissed and appear only when the shell adds them to the stack.
The shell builds the stack, bottom to top, as: the top row and the bar; the tab each tiled zone on screen shows, with its dialogs; the floating windows, least recently raised first; any window the bar has not placed; then the open overlays. While a Cinema tab is on screen it is the whole stack (with its dialogs), under the overlays you open; notices wait. It lists only the showing workspace. Every other window (a tab its zone is not showing, a window on another workspace, an app hidden with Super+H) is left out, so the compositor hides it and it keeps running. Shell describes the bar and workspaces.
When the shell restarts, the windows it lists come back in a new order. If
the bottom window (the old top row) is gone and a new one sits at the
bottom, sola-river keeps the previous relative order of the windows that
remain and puts new ones where the shell placed them.
7.3.3Focus§
The shell sends Focus with a window id, and sola-river moves the
seat's keyboard focus to that window. It skips the request when that
window already has focus: asking again makes some clients (Electron)
re-enter focus and flash.
The focus policy is the shell's: focus follows the pointer after a 200 ms
pause, without raising, and only a pointer that moves leads it; a click
shows and focuses the window under it. The shell's own windows never take
focus from the pointer. sola-river supplies the input for it:
MouseEntered and MouseLeft as the pointer crosses windows, and
MouseClicked when a window is clicked or touched.
MouseEntered carries still when the pointer did not move since the
last manage sequence: the window under it changed (a tab shown, a close, a
new window, a workspace switch). The shell leaves focus alone for a still
enter. On the pointer's next move, sola-river sends the enter again, not
still, unless that window has focus by then. It does the same after
keyboard focus goes to a window other than the one under the pointer, so
focus follows the pointer again once it moves, even within that window.
The compositor reports each move's position as it happens
(pointer_position), ahead of the enter it causes.
When the focused window closes, sola-river clears focus before the
window is destroyed. While a layer-shell surface holds exclusive keyboard
focus, the compositor ignores focus requests; sola-river keeps the
latest one and applies it when the surface lets go, and with none it
gives the keyboard back to the window that had it (see
Layer shell).
7.3.4Size and position§
A new window is not drawn until the window manager proposes a size.
sola-river first proposes 0×0, which lets the client choose its own size.
If the shell sends a Frame for a window that has not drawn its first
buffer yet, sola-river holds the size and applies it as an ordinary
resize once the compositor reports the window's first dimensions. Resizing
a client during its first configure can break a GPU client's swapchain.
sola-river sends a size or a position only when it differs from the last
one it sent, so the shell can repeat every frame without each window
receiving a new configure. It ignores a Frame with a zero or negative
width or height.
A window the shell has put in the stack but never positioned is centered on the output once. The centering uses the window's maximum-size hint, or half the output clamped between 800×600 and 1600×1000 when it has none; the window keeps its own size.
Once a window has both a size and a position, sola-river publishes its
rectangle as WindowGeometry, and again whenever it changes. The shell
uses it to remember where floating windows were.
Gamescope host windows are sized differently. Before gamescope's first
frame, sola-river proposes 1920×1080, because gamescope does not draw at
a size it chooses itself. After the first frame, sola-river keeps that
size for 12 s and then accepts a new size at most every 750 ms, so a game
starting inside gamescope has a stable window.
7.3.5Zones and workspaces§
The shell lays the desk out on a grid and sends each window's rectangle as
a Frame. The top row is 38 px tall (a 10 px gap and its 28 px islands).
The bar is docked left, 240 px wide, from y 48. One 10 px gap runs around
the output under the top row, between the bar and the tiles, and between
tiles; the ground shows through it.
The content area is a grid of equal cells, rows × columns
(ZoneLayout, set in Settings → Zones; one row of two columns by
default). A zone is a rectangle of cells.
| Zone | Rectangle | Default key |
|---|---|---|
| Full | The content area right of the bar | Super+KP_0 |
| A grid zone | Its cells, one gap between neighbouring cells; a zone over several takes the gaps inside it | the default layout's two: Super+KP_4, Super+KP_6 |
| Floating | The window's own size, or its remembered rectangle; a tile floated with the key keeps its place, 50 px smaller on each side | Super+KP_* |
| Cinema | The whole output, top row and bar included, in fullscreen state | Super+KP_Enter |
Each tiled zone (Full, the grid's, Cinema) shows one tab at a time. Each
workspace keeps the order its zones were picked; walking it, a zone with
something to show goes on screen unless one already on screen overlaps it
(Full overlaps every grid zone, Cinema everything). The shell frames
every tiled tab to its zone's rectangle, shown or not, so showing one
needs no resize; the tabs a zone is not showing are left out of
Composition. Only the Cinema tab on screen is framed fullscreen; the
rest of Cinema's tabs keep the output's size. Floating windows (Scope,
Preview, gamescope, and windows moved there with Super+KP_*) all show,
above the tiles; a dialog floats above its parent. The shell remembers a
floating app's rectangle (FloatGeometry) and restores it when the app
floats again.
The keypad keys work with NumLock on or off: the shell registers the keysyms of both states. Each kit window's titlebar Window menu offers every zone too (Shell § Window menu). Every key is changeable (Settings → Keybindings; zone keys in Settings → Zones).
Each workspace is a desk of its own (Super+1…9 shows one, Super+Tab steps through them). The shell composes only the showing workspace; the compositor hides the windows on the others, and they keep running. A pinned window is on every workspace (Shell § Pinned windows).
7.3.6Moving and resizing§
Only floating windows move and resize, and only when the application asks:
a drag on a title bar the application draws (xdg_toplevel.move), or on
an edge or corner (xdg_toplevel.resize). sola-river then runs the
compositor's pointer operation. The window follows the pointer; a resize
keeps the opposite edges in place; the pointer shows a move or resize
cursor. A window's top edge cannot go above the top row (y 38), and a
resize stops at 100×100. WindowGeometry is published once, when the
button is released. Move and resize requests from windows that are not
floating are ignored.
There are no modifier-drag bindings. Super with a click or a drag reaches the application: the Browser opens a link behind the page on Super+click, and Design picks the element under it.
7.3.7Fullscreen§
sola-river grants every fullscreen request from an application at once,
on the first output. For Xwayland games (Wine, Proton, SDL) this keeps the
fullscreen window managed: when the request is refused, some of them open
a separate surface that bypasses the window manager. It also tells the
shell (FullscreenRequest), which moves the window's tab to Cinema, and
back to the zone it came from when the application leaves fullscreen. A
window that asks as it maps opens in Cinema, and leaves for the zone it would
have been placed in.
Cinema uses the same state: the Frame of the Cinema tab on screen asks
for fullscreen. While a window in the composition fills the screen the
background picture is taken away, so what shows around or through it is
black. A window fills the screen when it is fullscreen, or when it is as
large as the output without that state, as a game's windowed fullscreen can
be. The shell's own surfaces don't count.
A window leaves fullscreen when it asks to, when the shell sends it a
Frame without fullscreen (another zone's key, or another tab taking the
screen), and when keyboard focus moves to another window. The last rule covers
games that switch to a windowed mode internally without telling the
compositor, and would otherwise stay above everything.
7.3.8Frames, rims, and corners§
sola-river never asks a client for server-side decorations, so every
client draws its own (the protocol's default). Sola's applications draw a
frame: a titlebar with the app's menus, 12 px rounded corners, and a lit
rim. In Cinema they drop it.
The shell publishes each window's WindowChrome. sola-river tells a
window in a tiled zone (not in Cinema) that it is tiled on every edge, so
clients that draw shadows outside their window drop them. A window whose
app published no menu is not a Sola app and draws no Sola frame
(rim); unless it is in Cinema, sola-river frames it:
- Tiled, and not fullscreen: rounded 12 px corners, as the kit draws its own tiles. Four decoration surfaces above the window, which take no input, paint the corners outside the curve with what lies under them (the black ground, or the background picture's own pixels there) and a 1 px rim along the edge. They are painted again when the window's size, its focus, the picture, or the theme changes, and, over a picture, when the window moves. Windows smaller than 24 px either way get none.
- Otherwise, a square 1 px border. A floating window's corners have other windows behind them, so they cannot be painted round.
The rim is opaque: while the window has focus, the theme's highlight a
step lighter, as Sola's own windows draw it; otherwise the theme's base
color 7 % lighter.
Floating windows get a drop shadow: a surface below the window, 14 px larger than the content on each side, with a 12 px corner radius, an 11 px soft edge, a 3 px downward offset, and 28 % black at its darkest. The shadow takes no input and is not part of the window's geometry. Fullscreen windows have no shadow.
7.3.9Closing§
On CloseApp with an app id, sola-river sends the compositor's close
request to every window with exactly that app id; on CloseWindow, to
that one window. This is the polite close: the application may ask to
save, or ignore it. The shell's Super+Q sends CloseApp, and a bar row's
stoplight sends CloseWindow for a window that is not a tab
(Session manager describes the stop that runs alongside
CloseApp).
A client killed outright never tells the compositor its windows closed.
Every 20 ms sola-river drops windows whose process no longer exists and
publishes Windows again. Without this, a restarted shell would stay
invisible behind its dead predecessor's surfaces.
7.3.10Hide§
Super+H hides the focused application without closing it. The shell
records it (AppHidden) and leaves its windows out of Composition, so
sola-river hides them; in a tiled zone, another of the zone's tabs shows
instead. Shell § Hide describes how to bring them
back.
7.4The background picture§
The ground behind the windows is a picture when one is set (Background),
and black otherwise. Paint (File → Set as Background), Browser (Use as
Desktop Background) and solactl background set <image> set it; Settings
→ Appearance (Clear) and solactl background clear take it away. Sola
keeps its own copy under ~/.config/sola/background/, so the original can
move or go.
sola-river decodes the picture off its event loop, turns it upright, and
scales it to fill the output, centre-cropped. It shows it on a surface at
the bottom of the stack, the output's size, at the output's full
resolution when the output's scale is a whole number. The surface takes no
input: clicks on the ground behave as before. The picture is decoded again
when the output's size changes. A picture that fails to decode is logged
(background failed) and not retried until the picture or the output
changes; the ground stays as it was.
7.5The idle fade§
Settings → Appearance sets how the desk sits on the ground (Appearance).
sola-river draws all of it the same way: the picture (or black, without
one) on a surface at the top of the stack, made more or less opaque with
wp_alpha_modifier_v1. Fading the windows into the picture looks the same
as raising the picture over them. The surface takes no input.
| Setting | Default | Range |
|---|---|---|
| Desk → Opacity | 90 % | 50–100 % |
| Idle → Fade the windows when idle | On | On or off |
| Idle → Start after | 5 min | 1–60 min |
| Idle → Fade over | 30 min | 1–120 min |
| Idle → Then dim the background | On | On or off |
| Idle → Dim after | 60 min | 1–240 min |
| Idle → Dim over | 30 min | 1–120 min |
- At rest, the windows, the bar and the top row are Opacity % opaque
over the ground: the picture lies over them at
100 − Opacity%. At 100 % nothing is drawn. - The fade. After Start after minutes with no input, the picture
rises over everything, from the rest to fully opaque, over Fade over
minutes, a step at most once a second. The top row's flower and agent
islands (the rectangle the shell sends as
IdleKeep) stay at the rest. Any key or pointer input brings everything back to the rest over a second. - The dim. Once the fade is full and only the picture shows, after Dim after more minutes a black surface rises over everything, the flower and the islands included, over Dim over minutes, until a tenth of the picture's light is left. Input brings it down with the windows. A settings change that puts the dim later lifts it at once.
While the fade runs, sola-river publishes IdleFade with fading:
Projects then shows the island of a session whose tab has the keyboard,
since nobody is looking at it. Once the fade is full, it adds out: the
shell reports every tab window off screen (TabWindow), so the Browser
rests its pages, until input brings the windows back.
Nothing is drawn over a window that fills the screen (Fullscreen),
at rest or idle: it is fully opaque whatever Opacity says. While one does
the fade does not run; it starts over once none does. Idle
comes from ext-idle-notify, which honors idle inhibitors: an application
that keeps the screen awake stops the fade.
Videos. An app tells sola-river where its windows show videos
(WindowVideos; the Browser does, for videos in a page's top frame). The
surface over the desk is cut around them, so a video is fully opaque while
the rest of its window is at the desk's opacity. sola-river places each
video by its window's position and takes off whatever window is above it.
- At rest, every video on screen is fully opaque.
- While the fade runs, a video being watched (playing with sound) with at least half of it uncovered holds the fade where it is and stays fully opaque. When it stops or leaves sight, the fade goes on from there. Other videos fade with their windows.
- Once the fade is full, no video is cut out and none holds it.
Since the lock follows the full fade, a watched video keeps the desk from
locking too. IdleFade still says fading.
7.6Outputs§
sola-river publishes the output's logical size as OutputGeometry when
the compositor reports it, and again whenever it changes. The shell lays
out the top row, the bar, and the zones against that size.
At start, and whenever the outputs change, sola-river picks a mode for
every enabled output (wlr-output-management):
- Among the modes whose aspect ratio is within 8 % of the panel's physical shape (its size in millimeters), the largest one at 60 Hz or more, or else the largest one.
- When the panel reports no physical size, or no mode matches it, the largest mode at 60 Hz or more.
A mode at 59 Hz or more counts as 60 Hz: many displays report their 60 Hz modes as 59.94 or 59.997 Hz.
Two variables in sola-river's environment change the choice:
| Variable | Effect |
|---|---|
SOLA_OUTPUT_MODE=2560x1440 | That size at its highest refresh rate; if the output has no such mode, the mode closest in area at 60 Hz or more. Sizes under 640×480 are ignored. |
SOLA_OUTPUT_PICK=preferred | The largest mode the display marks as preferred, else the default rule. SOLA_OUTPUT_MODE wins when both are set. |
sola-river does not set output scale, position, or rotation; the
compositor's defaults apply. The log shows the choice as
picked output mode and applying output mode.
7.7Seat, keyboard, and pointer§
sola-river uses the compositor's first seat.
7.7.1Keyboard§
sola-river does not set a keyboard layout or repeat rate; the
compositor's defaults apply.
A keyboard starts with NumLock off, which makes the number pad send
navigation keys. When the compositor offers river_xkb_config_v1,
sola-river turns NumLock on for each keyboard as it appears: at session
start, when one is plugged in, and when you return from another console.
The NumLock key still toggles it. The log shows
numlock enabled by default. Keys typed from another computer through
the KVM come from sola-kvm's own keyboard, which also starts with NumLock
on (KVM).
7.7.2Key bindings§
sola-river binds no keys of its own. The shell publishes the chords it
wants as RegisteredChords, a list of keysym and modifier pairs.
sola-river registers each pair with the compositor
(river-xkb-bindings-v1), removes pairs that left the list, and emits
Chord when one is pressed and ChordReleased when it is released. A
bound chord never reaches the focused application.
The modifier field is a bit set: Shift 1, Ctrl 4, Alt 8, Super 64.
The shell always registers its own chords (Shell § Keyboard), the Super shortcuts of its own menu, and the media keys without modifiers. It adds the Super shortcuts of the focused application's menus while that application has focus, and Escape while an overlay or menu is open.
While a layer-shell surface holds exclusive keyboard focus, sola-river
disables every registered chord so the keys reach that surface, and enables
them again when it lets go.
7.7.3Pointer§
sola-river sets the seat's cursor theme from XCURSOR_THEME and
XCURSOR_SIZE in its environment, McMojave and 24 when they are unset.
Sola's package ships the McMojave cursors in its share directory.
When the compositor offers river_libinput_config_v1, natural scrolling
is on for every input device that supports it. It is not configurable.
There are no pointer bindings.
7.8Layer shell§
The compositor shows wlr-layer-shell surfaces (panels, overlays, input
barriers) only while the window manager holds river_layer_shell_v1.
sola-river binds it, attaches each output and the seat, and marks the
first output as the default for clients that do not choose one. The log
shows bound river_layer_shell_v1 (layer-shell clients enabled).
Layer surfaces are not windows: they have no window id, are not in
Windows, and are not part of Composition.
When a layer surface takes exclusive keyboard focus, the compositor stops
honoring the window manager's focus requests. sola-river holds the
shell's focus requests and disables the shell's chords until the
compositor reports the focus released. The compositor leaves the keyboard
on the layer surface then, so sola-river applies the held request, or,
with none, focuses the window that had the keyboard before.
KVM captures the keyboard and pointer this way.
Exclusive zones are not applied. The shell lays out against the whole output, so a panel that reserves an edge overlaps windows.
7.9Xwayland§
X11 applications (Steam, Wine and Proton games) run through Xwayland.
sola-session gives each application DISPLAY: the name in
$XDG_RUNTIME_DIR/sola-display when that file exists (Sola's process
manager writes it; on Oath nothing does), else the first
/tmp/.X11-unix/X<n> socket that accepts a connection.
X11 windows are ordinary windows. They get window ids, focus, zones, floating, hiding, and closing like Wayland windows, their fullscreen requests are granted, and, drawing no Sola frame, they get the rim.
7.10Bus topics§
The bus documents the encoding and the sticky and persistent rules. Payload fields are listed here.
7.10.1Published by sola-river§
| Topic | Kind | Payload | When |
|---|---|---|---|
Windows | sticky | List of {window_id, app_id, title, pid}, ordered by id | A window's app id, title, or pid arrives or changes; a window closes or is pruned |
OutputGeometry | sticky | {width, height} | The output reports its size |
WindowGeometry | sticky, one per window_id | {window_id, x, y, width, height} | The window's size or position changes; retracted when it closes |
WindowParent | sticky, one per window_id | {window_id, parent} | The compositor reports the window's parent; retracted when it closes or has none |
MouseEntered | event | {window_id, still} | The pointer enters a window, or moves again after a still enter or a focus change |
MouseLeft | event | none | The pointer leaves a window |
MouseClicked | event | {window_id} | A window is clicked or touched |
Chord | event | {keysym, modifiers} | A registered chord is pressed |
ChordReleased | event | {keysym, modifiers} | A registered chord is released |
IdleFade | sticky | {fading, out} | The fade starts, becomes full, or ends |
WindowFloating | sticky, one per window_id | {window_id, floating} | Only after the bus restarts, to restore the floating windows it knows |
7.10.2Read by sola-river§
| Topic | Sent by | Effect |
|---|---|---|
Composition | Shell | List of {window_id}, bottom to top: the stack |
Frame | Shell | {window_id, x, y, width, height, fullscreen}: one window's rectangle |
Focus | Shell | {window_id}: keyboard focus |
WindowFloating | Shell | {window_id, floating}: allows move and resize, adds the shadow |
WindowChrome | Shell | {window_id, tiled, cinema, rim}: tiled state, the rim, rounded corners |
RegisteredChords | Shell | List of {keysym, modifiers}: the key bindings |
CloseApp | Shell, kit applications | An app id: close request to its windows |
CloseWindow | Shell | A window id: close request to that window |
Background | Paint, Browser, solactl, Settings | The picture behind the windows; a retraction for the black ground |
Appearance | Settings | The desk's opacity, the idle fade and dim |
IdleKeep | Shell | {x, y, w, h}: what the fade keeps at the rest |
WindowVideos | Browser | {window_id, videos: [{x, y, w, h, watched}]}, window-local logical pixels: no overlay over them, and a watched one holds the fade (The idle fade). sola-river retracts a window's entry when the window closes |
Theme | Shell | The rim's colors |
Shutdown | Shell | sola-river exits with status 0 |
7.11Call methods§
The owner is compositor. solactl compositor and the MCP
tools named compositor_… reach these methods.
| Method | Parameters | Reply |
|---|---|---|
screenshot | output (path), app, window (title), x, y, width, height (region), format (png or rgba) | {path}; with rgba also width, height, and format: "rgba8" |
sample | size (default 15, at most 65) | The pixels around the pointer |
cursor | visible (default true) | {visible} |
windows | none | {app_id: [{title, window_id, pid}]} |
input.click | x, y, button (left, right, or middle; default left) | {ok: true} |
input.press | button (default left); x, y to move there first | {ok: true} |
input.release | button (default left); x, y to move there first | {ok: true} |
input.move | x, y | {ok: true} |
input.scroll | dx (default 0), dy (default 5; down is positive) | {ok: true} |
input.key | chord, a key-chord object | {ok: true} |
7.11.1screenshot§
With no app and no region, screenshot captures the whole first output
with wlr-screencopy. With all four of x, y, width, and height, it
captures that rectangle of the output, in output coordinates. Neither asks
for the pointer in the image.
With app, it captures one window with ext-image-copy-capture: the
first window whose app id is app and, when window is given, whose title
is exactly window. It copies the window's own buffer, so the window does
not need to be on top; it is not raised or focused. Without a match the
call fails with window not found for capture.
png, the default, writes a PNG with fast compression to output, or to
/tmp/sola/screenshots/<ms>.png when no path is given. rgba writes raw
RGBA8 pixels with no header to output, or to
/dev/shm/sola-freeze-<ms>.rgba (/tmp/sola/screenshots/<ms>.rgba
without /dev/shm). Missing parent directories are created. Conversion
and encoding run on a separate thread.
One screenshot runs at a time; a second request fails with
screenshot already in progress. solactl compositor screenshot waits
10 s by default (--timeout). The shell's screenshot chords use this
method with rgba and copy the image to the clipboard
(Shell § Screenshots).
7.11.2sample§
sample captures a square of size pixels centered on the pointer and
kept inside the output, and returns it in the reply: x and y (the
pointer), left and top (the square's corner), width and height,
hot_x and hot_y (the pointer's pixel inside the square), and pixels
(base64 RGBA8). An even size is rounded up to the next odd one. One sample
runs at a time (sample already in progress), with a 2 s call timeout.
It fails with no pointer position yet until the compositor has reported
where the pointer is. Scope uses it.
7.11.3cursor§
visible: false sets the seat's cursor theme to the reserved name
sola-cursor-hidden, and visible: true to sola-cursor-visible; a
compositor that knows these names stops drawing the pointer, or draws it
again, while the pointer still moves and applications still receive it.
solactl compositor has no command for it; call it through the MCP tool
compositor_cursor.
7.11.4input§
input.click, input.press, input.release, input.move, and
input.scroll drive a virtual pointer (wlr-virtual-pointer) on the seat.
Coordinates are absolute, in the output's logical pixels, and kept inside
the output. A click
moves to the point, then presses and releases the button. A press holds the
button down until a release; moves in between make a drag.
input.key drives a virtual keyboard (virtual-keyboard-unstable-v1)
with a built-in US keymap. Its chord is an object: keycode (an XKB
keycode, the evdev code plus 8) and the booleans meta, alt, ctrl, and
shift. It sets the chord's modifiers, presses and releases the key, then
clears the modifiers, so no modifier stays held.
Synthesized input goes wherever real input would: to the window under the pointer, or to the focused window.
solactl compositor input click 640 400
solactl compositor input drag 300 120 900 120 --steps 20
solactl compositor input press 300 120
solactl compositor input move 600 120
solactl compositor input release 900 120
solactl compositor input key Ctrl+Shift+T
solactl compositor input drag X1 Y1 X2 Y2 presses at the first point,
moves to the second in --steps moves (12 by default) about 16 ms apart,
and releases there; --button picks the button for click, press,
release, and drag. press and release take an optional point.
solactl compositor input key builds the chord from text such as
Meta+Tab. The modifiers are Meta (also Super, Win, Cmd), Alt
(Option), Ctrl (Control), and Shift. The keys are A to Z, 0
to 9, Tab, Esc (Escape), Enter (Return), Backspace (BS),
Space, Left, Right, Backtick (Grave, or the character itself),
and the keypad keys KP0, KP2, KP4, KP5, KP6, KP8, KPEnter,
KP* (KPMultiply), KPAdd, KP= (KPEqual), and KP.
(KPDecimal). Names are not case-sensitive. An unknown key or modifier
exits with status 3.
With SOLA_BOT set to 1, true, or yes in its environment, solactl
refuses compositor screenshot, sample, and input and exits with
status 3, so a bot cannot take over the screen. compositor windows still
works.
7.12Restarts§
| What restarts | What happens |
|---|---|
| The compositor | sola-river's connection ends with it, and sola-river exits with status 1 so that PID 1 starts it again; it then waits up to 15 s for a live Wayland socket. |
sola-river | PID 1 starts it again. The compositor keeps the application windows. The new sola-river numbers them from 1, publishes Windows, and gets the shell's chords, the chrome and floating records, the picture, the appearance, and the theme again from the bus; the shell sends frames and the stack for the new list. While sola-river is down, new windows do not appear and the shell's chords do nothing. |
sola-bus | sola-river reconnects at its next read, subscribes again, and publishes OutputGeometry, Windows, every WindowGeometry, and the floating windows it knows, because the bus lost them. |
sola-shell | The old shell's surfaces are pruned once its process is gone; the stack keeps its order (see Stacking and visibility). |
On Shutdown (Exit Sola), sola-river exits with status 0.
7.13Logs§
sola-river writes to sola.log in Sola's log directory, the log every
Sola process shares, in lines tagged [river]. On Oath that is
$XDG_STATE_HOME/sola/sola.log (~/.local/state/sola/sola.log), unless
SOLA_LOG_DIR names another directory.
solactl logs --follow # the shared log, as it grows
At the default level sola-river logs each new window
(new river window), each Frame it receives (got Frame), held sizes,
fullscreen changes, pruned windows, chord set changes
(applied RegisteredChords), the background picture, the idle fade's
steps (idle: the fade starts, idle: the background dims,
input: the windows come back), and the end of its compositor connection
(wayland event loop ended).
7.14Limits§
- One output.
sola-riversets a mode on every output, but everything else assumes one: the shell lays windows out against one output size (with several outputs, whichever reported its size last), which also sizes the background picture, the fade, and synthesized pointer input; fullscreen, screenshots, and the layer-shell default use the first output. - No keyboard layout, repeat rate, or pointer settings beyond NumLock on and natural scrolling.
- Layer-shell exclusive zones are ignored.
- No server-side title bars. An application that draws no frame gets the rim; its corners are rounded only while it is tiled, and stay square while it floats.
- Only floating windows move or resize, and only by their own title bar or edges. There is no keyboard move and no modifier-drag.
- The zones are fixed: Full, two halves at 50 / 50, Floating, and Cinema.
- A window keeps its id through a restart of
sola-riverbecause River names it as it creates it:sola-rivertells nothing of a new window until then (one River leaves unnamed is told by its new id when River next lays out). A dialog's parent link sent before that names the new id. The firstsola-riverwith the map (2026-11-01) had none to read: the windows up then were numbered anew. - Only the first seat is used.
input.keyuses a US keymap regardless of the keyboard's layout. The method declareschordas text, so a caller that sends text, as the MCP toolcompositor_input_keydoes, gets an error;solactl compositor input keysends the object.- No Sola application keeps the screen awake: the idle fade runs over a video playing in a window.
8Session manager§
The session manager, sola-session, starts, tracks, and stops your
applications: everything opened from the launcher, and the applications
restored when a session starts. It has no window. It is a bus client and a
call-plane provider with the owner name desk (solactl desk). On Oath, PID 1 runs it
as a service and starts it again when it exits with an error
(Architecture).
The session manager does not decide which applications exist or what they
are called. The application catalog belongs to the shell's launcher and to
Settings; sola-session runs whatever command a request carries.
8.1Launching§
8.1.1Requests§
A launch arrives in one of two ways:
| Path | Sent by | Command run |
|---|---|---|
LaunchApp {app_id, command} on the bus | The shell (the launcher — Projects included, when it is not running — and the clock's panel, which opens Calendar); a web app's --app process when no Browser is running | The one in the request |
The launch call: solactl desk launch <app_id> [--command CMD] | You, scripts, agents | --command; else /bin/<app_id> if that file exists; else the app id itself |
The call does not read the catalog: without --command it runs the
program the app id names (solactl desk launch sola-mail runs
/bin/sola-mail). A program that needs arguments needs --command.
solactl desk launch sola-terminal
solactl desk launch foot --command "foot -e htop"
LaunchApp is not kept on the bus. If sola-session is not running when
the launcher sends it, nothing starts, and the launcher's ghost row in
the bar goes after 20 s.
8.1.2The command§
The command is split at whitespace into a program and its arguments.
There is no shell: no quoting, no variables, no ~, no pipes. A program
name without a slash is looked up on PATH. An empty command fails with
empty command.
8.1.3Processes§
On Oath each application is a direct child process of sola-session.
Where a systemd user manager runs ($XDG_RUNTIME_DIR/systemd/private
exists, or systemctl --user is-system-running reports running or
degraded), sola-session instead starts each application in a transient
scope of its own, sola-app-<app_id>-<n>.scope. Oath has no systemd, so
the rest of this chapter describes direct children.
8.1.4Environment and working directory§
Applications inherit sola-session's environment, which on Oath is the
one PID 1 gives the service. sola-session adds:
| Variable | Value |
|---|---|
WAYLAND_DISPLAY | The first wayland-N socket in the runtime directory that accepts a connection, else sola-session's own WAYLAND_DISPLAY |
XDG_SESSION_TYPE | wayland, when it found a Wayland socket. Chromium-based applications and desktop portals treat other values as not Wayland. |
DISPLAY | The first live X socket in /tmp/.X11-unix, as :N, when there is one |
XDG_RUNTIME_DIR | Passed through when set |
SOLA_APP | The app id, for an application that isn't Sola's (below) |
It also removes SOLA_NO_SELF_WATCH, so a kit application watches its own
executable and executes the new file in place when it changes
(Architecture).
Applications start in sola-session's working directory. Standard input
is /dev/null. Standard output and standard error go to
~/.local/state/sola/app-<app_id>.log (Sola's log directory), appended,
and emptied at the next launch once the file passes 8 MiB. In the file
name, characters of the app id other than letters, digits, _, and -
become _. If the file cannot be opened, the output goes to
sola-session's own standard error.
8.1.5The result§
After starting the process, sola-session emits
LaunchResult {app_id, command, ok, error}. ok: true means the process
started, not that it stayed up; an application that fails at once arrives
as an exit (see Tracking). A program that does not exist
fails here, with ok: false and the system's error. The launch call
replies {app_id, command, pid}, or fails with the error.
For ok: false the shell shows Failed to launch <app_id>: <error> in
the top row for 5 s.
8.2Application ids§
An app id names an application in four places:
- the catalog entry, built in or added in Settings → Applications;
- the launch request, which
sola-sessiontracks the process under; - the Wayland app id the program gives its windows, which
sola-riverreports inWindows; CloseApp, which the shell sends with the focused window's app id.
The shell matches windows to catalog entries exactly, then ignoring case,
for icons, names, and the ghost row of an app that is starting.
sola-session and sola-river match exactly. If a catalog id is Orca
and the window says orca, Super+Q still closes the window, but
sola-session does not stop the process it started, and a restart of
sola-session starts the application again although its window is open. Use
the window's own app id as the catalog id: the Running, not in the
launcher list in Settings shows it.
8.3The application catalog§
The launcher lists the catalog: Sola's built-in applications first, then the entries you add, in the order they arrive. It searches the names, ignoring case. Each entry has:
| Field | Meaning |
|---|---|
app_id | The identifier; see Application ids |
label | The name in the launcher and the bar |
command | What LaunchApp carries |
icon | An icon pack name under /oath/store/pkg/sola/share/icons (lucide/terminal, simpleicons/firefox), or a path to an image |
kind | command, or web for a web app |
url | A web app's start page |
profile | The Browser profile a web app runs on |
8.3.1Built-in entries§
The shell defines these in code: Oath, Settings, Monitor, Terminal,
Projects, Browser, Preview, Paint, Mail, Calendar, Bots, Scope, and
Design. Oath is listed only on Oath (where /oath/INDEX.md exists). Each
app id is sola- and the name in lower case (sola-settings,
sola-browser), and each runs /bin/<app_id>.
They are not stored, they do not appear in Settings, and they cannot be
edited there.
An entry you add with the same app id as a built-in replaces the built-in in the launcher.
8.3.2Entries you add§
Settings → Applications edits the other entries. Saving publishes the
entry as Application, a persistent bus topic with one slot per app id.
The bus stores it under Application: in ~/.config/sola/state.yaml and
replays it to the shell and to Settings when they start. Changing an
entry's app id retracts the old entry and publishes the new one;
Remove retracts it. The launcher shows changes at once.
An entry needs an app id, a name, and a command. On save, a first word of
the command that is found on PATH is replaced by its absolute path.
Running, not in the launcher lists open windows whose app id has no
entry (Sola's own applications, every sola-… app id, excepted), with a
command guessed from the app id or from the window's process. Add
starts a new entry with both filled in.
Entries with no command never appear in the launcher.
8.3.3Web apps§
An entry of kind web runs /bin/sola-browser --app <app_id>. Browser
writes these entries when you wrap a page and retracts them when you
remove the app. Settings lists them under Applications → Web apps, where
one can be renamed or removed. A running Browser opens the app's window.
With none running, the --app process sends LaunchApp for
sola-browser, so sola-session starts, tracks, and restores the Browser
itself, then hands the app over and exits. See Web apps.
8.3.4Desktop files§
Sola does not read .desktop files. An application installed on the
system appears in the launcher only once you add it in Settings.
8.4Tracking§
sola-session keeps a record of every process it starts, filed under the
launch's app id; several copies of one application have several records.
At least every 500 ms it checks each one. When a process has exited, it
drops the record and emits UserAppExited {app_id, command, code, signal}.
Exactly one of code (a normal exit) and signal (killed by a signal) is
set.
The shell shows <app_id> exited (code N) for a non-zero exit and
<app_id> killed (signal N) for a signal. A clean exit shows nothing.
A process started any other way is not tracked: from a terminal, by
solactl open, by another application, or by an earlier run of
sola-session. It has no exit fact and no place in the saved set, and
solactl desk close does not find it. Its windows are ordinary
windows, and Super+Q still closes them through sola-river.
8.5Closing§
CloseApp with an app id on the bus, or the close call, sends SIGTERM to
every tracked process of that app id that is not already stopping. Only
the process itself gets the signal, not the processes it started.
solactl desk close sola-mail
A process that is stopping stays tracked until it exits, and then
UserAppExited follows. Closing it again in the meantime does nothing.
The call replies {app_id, stopped} with the number of processes stopped,
or fails with no live children for <app_id> when nothing tracked matches.
Super+Q sends CloseApp for the focused application, which reaches three
receivers at once: sola-session stops the process, sola-river asks
every window with that app id to close (Compositor), and
a kit application quits by itself. solactl desk close does only the
first.
An application that isn't Sola's quits whole instead, as the next section
says; the call replies {app_id, quitting: true}.
8.6Applications that aren't Sola's§
An application whose command runs a program whose name does not start
with sola (Steam, any other X11 or Wayland program in the catalog) is
not Sola's. Kit applications and web apps (sola-browser --app …) are.
Such an application starts with SOLA_APP=<app_id> in its environment,
and every process it starts inherits it, including ones that leave its
process tree: Steam's pressure-vessel containers and webhelpers start
sessions of their own, and its logger ends up under PID 1. That variable
is how sola-session finds all of the application.
It runs only while it has a window on the desk. Sola has no tray, so an
application that hides in one when its window closes, as Steam does, would
otherwise keep running with nothing to show for it. A window counts as
the application's when its app id is the application's, or when its
process, or the nearest process above it, carries the application's
SOLA_APP. A game Steam started (app id steam_app_<id>) is Steam's, so
Steam stays while the game is open. Hidden windows and windows on other
workspaces count.
Closing the last window from Sola quits at once, without the 5 s: Steam answers a close from the compositor by starting its webhelper again, and its window comes back within a second, empty.
| When | sola-session quits it |
|---|---|
Its last window is closed from Sola: the bar's ×, Super+W (CloseWindow) | Yes, at once |
| It had a window and has had none for 5 s (30 s in its first 2 minutes, while it starts): its own close button, its last window crashing | Yes |
| It has shown no window 2 minutes after it started | Yes |
CloseApp: Super+Q, the close call | Yes, at once |
| Exit Sola | Yes, within the 6 s every application gets |
Quitting goes in steps until no process carries its SOLA_APP:
- While it runs, its own quit command, when it has one. Steam's is
/bin/steam -shutdown, which hands the request to the running Steam as Steam → Exit does, so Steam saves its state and ends its games. Steam's command starts Steam when none is running, so it is sent only while one is, and only once. - After 15 s (at once, with no quit command), SIGTERM to every process
that carries its
SOLA_APP. - After 5 s more, SIGKILL to the rest.
It leaves the saved set when it starts quitting, except at Exit Sola, which keeps the set as it was. Picking it in the launcher while it quits starts it again once it is gone.
solactl desk close steam
8.7Ending the session§
On Shutdown (Exit Sola in the flower menu), sola-session sends
SIGTERM to every application it is tracking, waits up to 6 s for them to
exit, sends SIGKILL to any that remain, then exits. Applications that
aren't Sola's get their own quit command first (Steam's -shutdown), and
SIGKILL goes to every process left with their SOLA_APP. It does not change the
saved set, so the applications open at Exit Sola are the ones
restored.
8.8Restoring the last session§
Restore has three parts, each kept by a different process:
| Part | Kept by | Saved in |
|---|---|---|
| Which applications run | sola-session | SessionApps in ~/.config/sola/state.yaml |
| What each application has open: its tabs, a window each | The application | Its own files (below) |
| Where each window goes: its workspace, zone, and place in the bar, which tab each zone shows, the workspaces themselves, and which one shows | The shell | TabLayout in ~/.config/sola/shell/tabs.yaml |
8.8.1Applications§
sola-session publishes the applications it is running as SessionApps,
a persistent, sorted list of {app_id, command} with one entry per app id.
It publishes the list again after every launch, close, and exit. An
application you close, or one that quits or crashes, leaves the list.
An application that exits because its compositor went (rivulet restarted
under the running desk, say by an oath apply that moved pkg:rivulet)
did not
close: it stays on the list, and once a new compositor has answered for
2 s it is started again. sola-session tells by the compositor's Wayland
socket: the one the application was launched on no longer answers, or a
new one has taken its place. The log says app went with its display and
the display is back: starting it again.
When it starts, sola-session spends 750 ms collecting the stored
SessionApps and the current Windows. Then it launches every listed
application that has no window with that app id and is not already
tracked, once, with its stored command. A stored entry for an application
that has since been renamed launches under the new name (sola-workspaces
runs as sola-projects). After a fresh start everything on the list
opens. At boot no compositor may answer yet: then the listed applications
wait, as those that went with their display do, and start once one has
answered for 2 s (session restore: no compositor yet). Started with no
display, an application such as Steam would exit and leave the list.
After a restart of sola-session alone, the applications are still open
and nothing is launched: the listed ones that have windows
stay on the list (keeping the saved apps already running) until their
windows have been gone for 3 s.
8.8.2Tabs§
Each application reopens what it had open by itself, a window per tab:
| Application | Reopens | From |
|---|---|---|
| Terminal | Every saved tab, its panes attached to their tmux sessions again; sessions the tmux server lost (after a reboot) are made again with new shells | TerminalSession entries in ~/.config/sola/state.yaml |
| Browser | Every page of the active profile and of each workspace's profile; a web app's pages open as that app | session.json in each profile's folder under ~/.local/share/sola/browser/profiles/ |
| Paint | Every saved image that still exists | ~/.config/sola/paint.yaml |
| Projects | Every open project, with its selected tab attached | ~/.config/sola/projects/catalog.json |
| Design | A window for each project in its list | ~/.config/sola/design/projects.json |
The other applications open one window. What a tab holds is described in each application's notes (Applications).
8.8.3Places§
A window an application opens as a tab is announced first, with a key
that stays the same across restarts: <app_id>/<tab id>. The window names
its tab too: a kit app gives it the Wayland app id <app_id>#<token> (the
key's token, sola_bus::topics::tab_token); sola-river publishes the app
id without it and the token as WindowTab, and the shell pairs each window
with the tab it names — whatever came first, and in whatever order the
windows were made. A window with no token (an app from before) pairs by
the order its process announced its tabs. Keys:
(sola-terminal/<tab id>, sola-browser/<page id>, <web app id>/<page id>,
sola-paint/<path>, sola-projects/<project id>,
sola-design/<project id>). A window that is not an announced tab goes by
its app id.
The shell saves the bar as TabLayout whenever it changes. When the shell
starts, it takes the saved layout once, and it places the windows already
up (applications kept through its restart) only after the bus has
replayed everything it holds (Replayed): the zone grid and the saved
layout are in first, whatever order they came in. For the next 2 minutes, a window
that arrives takes its saved workspace, zone, and place, matched by its key
and then by its app id, and a zone shows the tab it showed. An app with
windows on several workspaces gets one saved place per window. A window
with no key of its own that finds no place left, while a window of its app
that took the app's place is still open (while the layout is restored), joins that
one: same workspace and zone, right after it in the bar. Steam maps a
short-lived window first, which takes Steam's place, and its main window
after; the main window goes where Steam was. Any other window with no saved
place joins the showing workspace, after the focused tab. A floating window
also gets back its saved rectangle (Compositor).
A window that closes keeps its place for 30 s, so an application that restarts or executes a new version in that time gets its windows back where they were.
8.9One instance per application§
sola-session has no notion of a single instance: every launch starts a
new process. Applications that keep one instance do it themselves:
Browser (with its web apps), Terminal, Paint, Projects, Design, and
Calendar. A second process hands its request to the running one and
exits; sola-session reaps it like any other exit.
| Application | A second process |
|---|---|
| Browser | Hands over its URL, a request for a new page, or a web app to open |
| Terminal | Asks for a new tab, which opens as a new window |
| Paint | Hands over its image paths |
| Projects | Asks the running one to show its selected project's window |
| Design | Asks the running one to come forward |
| Calendar | Hands over its file paths |
The launcher does not start a second copy of an application that has a
window: choosing it brings that application's last-used window forward
(a hidden one is shown again). Terminal is always started again, so its
launch opens a new window. Choosing Projects lists your projects
(Shell). Choosing Browser asks for an address or a search
and opens it as a new page through open_url (Opening links and files); when no
Browser is running, sola-session starts it. Other ways in
(solactl desk launch, opening a file) still reach the application's
own single-instance handling.
8.10Opening links and files§
Every link or file Sola opens goes to the app Sola keeps for its scheme
(mailto:) or its type (the extension: .png): a link clicked in
Terminal, Projects, Mail or Calendar, a link the Browser does not load
itself, the Browser's Open on a download, the Oath app's home link,
solactl open, and on Oath xdg-open from any program (gh auth login,
claude /login, Steam). The library is sola-open
(spec).
8.10.1Handlers§
Apps claim the schemes and types they open. Sola's own apps ship theirs:
| App | Schemes | Types |
|---|---|---|
| Browser | http, https | html, htm, xhtml, pdf, svg, txt, json, xml |
| Paint | — | png, jpg, jpeg, jpe, gif, webp, bmp, tif, tiff, tga |
| Calendar | — | ics, ical, icalendar |
mailto | — |
A kit app claims when it starts (BusSetup::opens); the claim is kept in
~/.config/sola/handlers/claims/<app id>.json with the time it first
claimed. The handler for a scheme or a type is the first that is still
installed of:
- The one you chose, in Settings → Default Apps (kept in
~/.config/sola/handlers/defaults.json:{"defaults": {"mailto:": "sola-mail", ".png": "sola-paint"}}, an app id or a.desktopfile's name). A claim never writes it. - Sola's own claimant (the table).
- Other claimants, the earliest claim first. A later claim never moves ahead of an earlier one, so a new app does not take a scheme or a type from the app that has it.
- For a scheme only:
~/.config/mimeapps.list([Default Applications],x-scheme-handler/<scheme>), then a.desktopfile whoseMimeTypelistsx-scheme-handler/<scheme>, in~/.local/share/applicationsand each$XDG_DATA_DIRSentry'sapplications. That is howclaude-cli://reaches Claude Code.
A type nothing claims has no handler; nothing looks inside a file.
Settings → Default Apps lists every scheme (Links) and type
(Files) something opens, with the app that opens it. Click one for
everything that could: its claimants and, for a scheme, its .desktop
handlers; the one that opens it is marked. Choosing another writes your
choice; the next open uses it.
An app may ask to be the handler (sola_kit::opener::request_default,
the bus event DefaultHandlerRequest {app_id, kind}, kind being
mailto: or .png). The shell asks you with a card
(Shell); only Use writes your choice.
An app that already opens it, or does not claim it, gets no card.
8.10.2Delivery§
A claiming app answers the call-plane method open ({target}: the link,
or the file's absolute path) on its call owner: browser, sola-paint,
sola-calendar, sola-mail. When it is running, the opener calls it.
When it is not, the opener sends LaunchApp with the app's plain command,
so sola-session starts it, tracks it, and brings it back after a desk
restart; it waits up to 20 s for the app's open, then calls it. The
Browser opens the link as a page in front; Paint opens the image in a
window of its own; Calendar drafts the event; Mail opens a new letter with
the link's To, Cc, Subject and body (Mail has no Bcc, so a link's Bcc is
left out).
With no call plane, or an app from before open, the opener starts
<app> --open <target>: that process hands the target to the running app
(the Browser over chrome.sock, Calendar over its socket, Paint by
OpenImage), or has the session start the app and then hands it over;
with no session either, it becomes the app itself. A .desktop handler
runs its Exec, with %u %U %f %F the target and %i %c %k
left out.
8.10.3The Browser§
A link to a scheme the Browser does not load (anything but http,
https, file, about, data, blob, javascript, view-source,
chrome, devtools, filesystem, ws, wss, sola) goes to its handler
when you clicked it: mailto: to Mail. A page that goes there by itself
(no click) gets nothing; the log says without a click: not opened.
Applications that want the shell involved emit OpenUrl {url, activate}
on the bus instead. A running Browser opens the page itself, as the active
page with activate; when the Browser is not running, the shell starts it
through sola_core::open_url.
8.10.4xdg-open and solactl open§
On Oath, pkg:sola puts Sola's opener at /bin/xdg-open, so every
program that calls xdg-open reaches the same handlers. xdg-open <file|URL> exits 0 when opened, 1 on a usage error, 2 when the file is
not there, 3 when nothing opens it, and 4 when its handler failed;
--help and --version print and exit 0. solactl open <target> does the
same and exits 3 on any failure. /bin/x-www-browser is the same opener: the
last name Rust's webbrowser crate tries, and it never calls xdg-open, so
that is how a program built on it (grok's MCP sign-in) reaches a browser.
xdg-open https://example.com
xdg-open ~/Pictures/shot.png
solactl open "mailto:someone@example.com?subject=Hello"
A relative path is taken from the caller's working directory. $BROWSER
is not set on the desk; programs that read it fall back to xdg-open or
x-www-browser.
8.11Bus topics and call methods§
The bus documents the encoding and the persistent rules.
| Topic | Direction | Payload |
|---|---|---|
LaunchApp | Read | {app_id, command} |
CloseApp | Read (also by sola-river and kit applications) | An app id |
Shutdown | Read | None |
SessionApps | Read at start; published | List of {app_id, command}, persistent |
Windows | Read at start | See Compositor |
LaunchResult | Published | {app_id, command, ok, error} |
UserAppExited | Published | {app_id, command, code, signal} |
Call methods, owner desk (also the MCP tools desk_launch and
desk_close):
| Method | Parameters | Reply |
|---|---|---|
launch | app_id; command (optional) | {app_id, command, pid} |
close | app_id | {app_id, stopped} |
8.12Restarts§
| What restarts | What happens |
|---|---|
sola-session | Open applications keep running, but the new sola-session did not start them: no UserAppExited, and solactl desk close does not find them. The listed ones with windows stay in SessionApps while they have windows (3 s grace). Super+Q still closes their windows. Restore launches only listed applications that have no window. A listed one that isn't Sola's is watched as before: once its windows are gone for 5 s, it quits whole. An apply that moves pkg:sola restarts sola-session the same way: svc:sola-session keeps its children (keep_children), so the applications, and a game under Steam, keep running on the program they started with. Without it, PID 1 stops them with the service and the new one opens them again. An application of Sola's whose own package moved is stopped and started again on the new program, alone (its running program against the one its command names: checked as the session manager starts, and when an apply says it moved packages, PackagesMoved); one that isn't Sola's never is. |
sola-bus | sola-session reconnects and subscribes again. A LaunchApp sent while the bus was down is lost. |
| The compositor | The applications lose their display and exit; they stay in SessionApps and start again on the new compositor. See Architecture. |
8.13Logs§
| File | Contents |
|---|---|
~/.local/state/sola/sola.log | sola-session's log, lines tagged [session]: launch, user app launched with the pid, CloseApp: stopping, user app exited, quitting with the reason and quit: … for each step, and what restore decided |
~/.local/state/sola/app-<app_id>.log | Each application's standard output and standard error |
solactl logs app-sola-mail --follow
8.14Limits§
- No
.desktopfiles are read; the launcher lists only built-in entries and entries added in Settings or by Sola applications. - Commands are split at whitespace. There is no quoting and no shell.
- Matching between catalog, launch, windows, and
CloseAppis by exact app id. - A restart of
sola-sessionloses track of the applications it started. - Applications started from a terminal or by another program (not through
sola-sessionoropen_url,open_image,open_calendar) are not tracked and are not restored. - Restore starts one process per application with its launch command. It does not restore a second copy launched with a different command.
- There is no autostart list besides session restore.
- Closing one of Sola's applications sends SIGTERM to its process only, not to the processes it started.
- An application that isn't Sola's and was started some other way (a
terminal, or a
sola-sessionolder than 2026-10-31) has noSOLA_APP, so nothing watches its windows and closing does not find all of it. - An application that isn't Sola's and never shows a window (a command-line tool in the catalog) is quit after 2 minutes.
9Shell§
The Shell (sola-shell) draws the desk's own surfaces: the top row, the
bar, the launcher, the keyboard shortcuts overlay, the right-click menu,
notification cards and the screenshot marquee. It also decides where each window goes
(workspaces and zones) and owns the desktop's keyboard shortcuts.
9.1The desk§
- The top row runs across the top of the output: a row of islands (pills 28 px tall) a 10 px gap in from the edges, with nothing behind them.
- The bar is the column on the left, 240 px wide, under the top row. It lists the windows of the workspace you are on.
- Windows fill the area right of the bar, in zones.
Every gap is 10 px: around the edges, between the bar and the windows, and between windows. What shows through the gaps and the windows' rounded corners is the ground: the background picture, or black without one (Theme).
9.2Keyboard§
These are the default keys. Every one of them can be changed in Settings → Keybindings, which lists each action with its key; a key that another action holds is offered to move, never shared. A system key always includes Super. The zone keys are set in Settings → Zones (Zones). The same page lists the apps' menu keys: the ones every app shares (Undo, Redo, Cut, Copy, Paste, Select All, Find), then each running app's own; two apps may use the same key, but no app a system key.
| Chord | Action |
|---|---|
| Super+Space | Open the launcher |
| Super+K | Keyboard shortcuts overlay (press again to close) |
| Super+Q | Quit the focused app: every window of it. One that isn't Sola's quits whole (Session manager) |
| Super+H | Hide the focused app |
| Super+L | Lock the screen (needs a password: Security) |
| Super+CapsLock | Show the next window in the zone under the pointer (Super+Shift+CapsLock: the previous) |
| Super+Backtick | Show the focused app's next window on this workspace (Super+Shift+Backtick: the previous) |
| Super+1 … Super+9 | Show workspace 1 to 9 |
| Super+Shift+1 … Super+Shift+9 | Send the focused window to workspace 1 to 9 |
| Super+Tab | Next workspace (Super+Shift+Tab: the previous); as islands, past those switched off |
| Super+Keypad 0 | Move the focused window to Full |
| Super+Keypad 4 | Move the focused window to the left column (the default layout's) |
| Super+Keypad 6 | Move the focused window to the right column (the default layout's) |
| Super+Keypad Multiply | Float the focused window |
| Super+Keypad Enter | Move the focused window to Cinema |
| F13 | Screenshot of a selection, to the clipboard |
| F14 | Screenshot of the whole output, to the clipboard |
| F15 | Screenshot of the focused window, to the clipboard |
| Escape | Close the open launcher, overlay, menu or panel; cancel a selection |
| Play/Pause, Next, Previous | Media players (MPRIS) |
| Mute, Volume Up, Volume Down | The default output: mute toggles, volume steps 5% (up to 100%) |
The media keys work whatever has focus; the Shell runs
solactl media <action> for them. Escape belongs to the Shell only while
one of its surfaces is open; otherwise it goes to the focused window.
The keypad chords work with NumLock on or off. Sola turns NumLock on for each keyboard at session start, so the pad types digits; press NumLock to switch it to navigation keys. Caps Lock does nothing: it never locks and its light never comes on (Super+CapsLock steps the windows under the pointer).
Super+CapsLock steps through the windows of the zone under the pointer — the zone of the window you see there — in their order in the bar, starting from that one. Over a floating window, it steps through the floating windows under the pointer. With the pointer over the bar, the top row or a gap, it steps through the focused window's zone. Super+Backtick steps through the windows of the focused app on this workspace, in their order in the bar; the titlebar's Window → Cycle Windows does the same.
While the launcher or the shortcuts overlay is up, every other shortcut except Super+K is ignored; while the screenshot marquee is up, every shortcut but Escape is. Any other shortcut first closes an open menu or panel, except the screenshot chords, which copy the screen as it is.
The focused app's menu shortcuts that use Super (Super+C, Super+T, …) are taken by the Shell while that app has focus and handed to it as the menu choice, so a shortcut and a click on the menu item do the same thing.
Close Window (Super+W) closes the focused window, as its bar row's × does, and an app ends when its last window closes. Quit App (Super+Q) closes every window of the focused app at once, and it ends. Both are in each app's own menu; an app that isn't Sola's has no menu of its own, so the Shell gives it one with these items. Browser pages, Paint and Projects give Super+W their own close, which does the same. Editor alone is different: Super is Emacs's Meta there, and there only, so Super+W copies (its menu has the item, with no key).
9.2.1Focus§
Focus follows the pointer: when the pointer rests on another window for 200 ms, that window takes the keyboard. It does not come forward. Only a moving pointer leads focus: when a keyboard switch, a new window or a close leaves the pointer resting on another window, focus stays where it went until you move the pointer. A click on a window focuses it and brings it forward. The Shell's own surfaces never take focus from the pointer.
9.2.2Shortcuts overlay§
Super+K, or Keyboard Shortcuts in the flower menu, opens a card of every shortcut: the Shell's (groups Shell, Workspaces, Capture and Window), then the focused app's menu items (in groups named after its menus). Type to filter by name, group or chord; ↑ and ↓ move, Enter or a click runs the item. Escape or Super+K closes it, and so does a click on the pad around the card. Exit Sola, Restart Computer and Shut Down are left out, so a filter and Enter cannot end the session.
9.3The top row§
Left to right:
- The flower, in its own circle, opens the flower menu. It is Sola's amber unless the look recolours it (Theme).
- Agent islands, one per Projects session that asks you something or has finished (below).
- A message in the middle of the row, for five seconds, when there is one: a launch that failed ("Failed to launch …"), an app that exited with an error ("… exited (code N)", "… killed (signal N)"), "Screenshot copied".
- On the right, an island per group: mail, the bell, the mouse battery and Bluetooth; the volume spectrum; CPU, GPU, VRAM and MEM; RX and TX; the clock.
Click an item for its panel; its island lifts while the panel is open. Click it again, press Escape, or click a window to close the panel.
9.3.1Flower menu§
| Item | What it does |
|---|---|
| Launch Application… | Opens the launcher |
| Settings… | Brings Settings forward, or starts it |
| Keyboard Shortcuts | Opens the shortcuts overlay (Super+K) |
| Lock Screen | Locks the screen (Super+L; Security) |
| Lock Vault | Locks the vault at once (Vault); there only while the vault is unlocked |
| Exit Sola | Ends the desktop session (Shutdown on the bus); on Oath the console then has the screen, and oath desk brings the desk back |
| Restart Computer | Restarts the machine |
| Shut Down | Powers the machine off |
Restart Computer and Shut Down have no confirmation and no shortcut. On
Oath they run sudo /bin/oath set host:local power=reboot (or
power=halt), then sudo /bin/oath apply --confirm host:local. Sola has
no password prompt, so they work only when sudo lets you run those
commands without one. A failure shows Could not restart the computer
or Could not shut down.
9.3.2Clock and calendar§
The clock reads HH:MM Day YYYY-MM-DD (for example
14:05 Thu 2026-10-29) and updates every ten seconds. Its panel is a
month: six weeks from the Sunday on or before the 1st, the days of the
months either side faint, today on an accent circle. ‹ and › step the
month; Today comes back to this one (dim while this month shows). The
panel always opens on this month.
Under the month, Today lists today's events from Calendar: a bar in the event's calendar colour, the time (or "all day") and the title, with the count at the right. An event that has ended dims; one under way is brighter. Click one and Calendar shows it, starting Calendar first if it is closed. Past five events, N more in Calendar opens Calendar. With none, it says Nothing today. The list is what Calendar last published, so it is there with Calendar closed; it appears once Calendar has run.
9.3.3CPU, GPU, VRAM, MEM, RX and TX§
Each is an LED graph of the last 12 seconds, a column a second, five dots tall. A dot lights once its column reaches its middle. CPU, GPU, VRAM and MEM are drawn in the theme's text colour, its warning colour from 75% and its danger colour from 90%; RX is in the accent and TX in the success colour, each scaled to its own recent peak. GPU is how busy the card's processor is and VRAM how full its memory is; both show only for an NVIDIA GPU that NVML can read.
Click one for its panel: the value in large type, then the last 60 seconds as columns tinted the same way, with the peak.
- CPU: the model and its cores and threads; per-thread load as a column per thread (a gap between threads, two between cores); the top processes with LED bars; load averages and uptime.
- GPU: the card and its memory; the top processes by how much of the processor they used in the last five seconds; temperature, power and fan.
- VRAM: the card and its memory; used and total, with a row of LEDs; the top processes by VRAM.
- MEM: total memory; a row of LEDs (used lit, cache dim, free dark) with the three figures; the top processes by memory; swap and pressure.
- RX and TX: the rate, the interface; its address and the bytes moved this session.
9.3.4Volume§
The volume island is a 12-band LED spectrum of what the default output is
playing (green at the bottom to amber at the top), dimmed while the
output is muted or at zero. It shows while PipeWire runs, even when
WirePlumber stops answering. Click it for
the volume panel: Output and Input, each with its level, a slider,
a mute button and its devices. Click a device to make it the default
(wpctl set-default). While WirePlumber is stuck, the panel keeps the
last level and default it saw, and a change waits until WirePlumber
answers.
9.3.5Mail, the bell, the mouse and Bluetooth§
- Mail: an envelope and the inbox's unread count, in the accent, while Mail has a window and there is unread mail. Click it to bring Mail forward.
- The bell: the missed notifications and their count (Notifications).
- The mouse: a Lamzu mouse on its 4K receiver (
3554:f510) shows its battery: a mouse glyph and a percent, in the warning colour at 20% and below, the danger colour at 10% and below, with a charging glyph while it charges. A sleeping mouse does not answer, so the Shell asks every ten seconds until it does (the chip appears soon after you first move the mouse); after that it asks once a minute. The number dims after ten minutes without an answer (asleep, off, or out of range), and the chip goes when the receiver is unplugged. The Shell needs read and write access to the receiver's hidraw node; without it there is no chip. On Oath, give it withsudo oath set host:local seat_hid='["3554:f510"]'andsudo oath apply host:local(Oath 220c74bb6867 and later). Other receivers and the wired cable are not read. The chip has no panel. - Bluetooth shows while BlueZ has an adapter: dim and crossed out
while it is off, its connected glyph while a device is connected. Its
panel has a switch for Bluetooth, the connected devices
(Disconnect), the paired ones not connected (Connect), and
Add device, which searches and lists named devices nearby (Pair)
until Done. A device that needs a PIN or a passkey asks in the
panel. The Shell reaches BlueZ on the system bus, at
DBUS_SYSTEM_BUS_ADDRESSwhen it is set, else/run/dbus/system_bus_socket. On Oath that issvc:dbus-system, and BlueZ issvc:bluetooth; thedeskprofile has both, and a machine installed before them adds them withoath new(Oath's manual, Services).
9.4Agent islands§
When a Projects session asks you something (a permission, a question) or finishes its turn, an island for it appears in the top row after the flower, the newest first: a diamond for a question, a check for a finished turn, then the project's name and the tab's in parentheses ("Sola (root)"), and for a question what it wants ("wants to run …"). What does not fit in 340 px is cut with an ellipsis. A project on another computer goes by its folder alone, without "on <computer>". When the islands reach the middle of the row, a +N island lists the rest.
Click an island and its session's window comes up, on whichever workspace it is. Hover one and a × takes its mark's place: click that to let the island go without looking; the session carries on, and the island stays gone until the session next asks or finishes. A row of the +N list works the same way, with its × always showing, and the list stays up for the next one.
An island also goes when the session moves on, and when you look at it yourself: its window has the keyboard. While the desk is fading idle you are away, so a session in the focused tab gets an island too.
9.5The bar§
The bar lists every window on the workspace in one order of its own, not grouped by zone. Each row is the app's icon and the window's title (the app's name while it has none); a tab with an icon of its own — a browser page's favicon — shows that instead.
The apps that open a window per document — Browser (its pages), Projects, Terminal, Paint, Design and every web app — keep their windows together in a group once a workspace has two of them: the app's name in small capitals, with its windows a step in under it. One window is a row of its own, as is every window of any other app (Mail, Settings, Calendar, Steam, games), and these rows come first, above every group.
Click a group's name to fold it to one line, and again to open it: folded, the line says how many windows it holds, shows the loudest mark among them (an amber diamond while one waits on a decision, else a spinning ring while one works), and reads brighter while one of them has focus. Each workspace folds its own groups, and they stay folded across restarts. A bar longer than the screen scrolls with the wheel; it has no scrollbar.
A Projects window's row names up to three things, drawn apart: the project, the session in a quieter ink (when it is not the project's main one), and, when the session runs on another computer, that computer as a chip in its prompt color.
- The focused window's row is the bright pill, in the theme's highlight colour.
- A window its zone is showing is lifted a step while that zone is on screen, and in full ink without the lift while it is not (Full while the halves are up, or the halves while Full is).
- Windows behind are muted.
- An app hidden with Super+H says hidden on its rows (beside its group's name, for a group).
- Activity: a window that is working shows a turning ring in place of its icon, and one waiting on a decision a diamond, so the title does not move. Projects marks each session's window by its session, and Design marks a project's window while an agent works in one of its canvases.
Click a row to show its window in its zone and focus it (its workspace and zone come on screen with it, and a hidden app comes back). Hovering a row shows a red close dot: it closes that window as the window's own close dot would. Drag a row to move it in the list; the window stays in its zone and nothing on screen changes. A row in a group moves only within its group; drag a group by its name to move it among the groups. A row of its own moves among the other rows of their own, above the groups. Clicking a group's name does nothing.
While an app starts from the launcher, a faint row with its name and a turning ring stands where its window will land; the window takes its place when it opens (or the row goes after 20 seconds, or when the launch fails).
A new window's row goes right after the row of the window that had focus when that window is in the new one's group: a page opened with a link from a page, a Terminal tab from a terminal. Windows opened behind it from the same window stay in the order you opened them. Otherwise a window of a group goes to the end of its group (a link opened from Terminal puts its page at the end of Browser), and an app's second window starts its group with the first, right after the group that had focus (at the top of the groups, when a row of its own had focus). A window of its own goes right after the row of its own that had focus, else at the end of those rows. A new Projects session goes after its project's other sessions. A dialog has no row: it rides above the window it belongs to.
When a group is down to one window, that window becomes a row of its own. The group keeps its place on that workspace, unseen, and comes back there when the app has two windows again.
The bar's layout (the workspaces, each window's place and zone, and the
places of empty groups) is saved in ~/.config/sola/shell/tabs.yaml. Windows that come back within
two minutes of the Shell starting take their saved places, and a window
whose app restarts within 30 seconds takes the place it left.
9.5.1The head and the workspace menu§
The head of the bar names the workspace you are on. A diamond beside the name means a window on another workspace waits on a decision; a dot, that a Mail window on another workspace has unread mail.
Click the head for the workspace menu: every workspace (the current one checked, Super+1 to Super+9 beside the first nine; click one to show it), then, for the current one:
- New Workspace adds "Workspace N" after the current one and shows it.
- Rename… turns the menu into a field: Return renames, Escape cancels. An empty name keeps the old one.
- Move Left and Move Right move it in the order (dimmed at the ends).
- Delete removes it. Its windows join the workspace before it (the next one, for the first), in the same zones, at the end of its bar (a group's windows in that app's group there). The last workspace cannot be deleted.
9.5.2Workspaces as islands§
Settings → Appearance → Workspaces → Islands in the top row takes the head off the bar and puts each workspace in an island of its own, right of the flower. An island shows the workspace's number (its place: Super+1 to Super+9), its name and a dot. On a workspace you are not on, a diamond means a window there waits on a decision, and a mail icon that you have unread mail and Mail is there. When other islands follow (Projects sessions asking or finished), a thin line sets the workspaces apart from them.
- Click an island to show that workspace. Click the one showing for its menu: New Workspace, Rename…, Delete.
- Drag an island to reorder the workspaces; the numbers follow.
- The dot says whether Super+Tab stops there: lit, it does. Click it to change that. Unlit, Super+Tab and Super+Shift+Tab pass over the workspace; Super+1 to Super+9 and a click still show it. With every other workspace unlit, they stay where they are.
Turning the setting off brings the head back, and Super+Tab steps through every workspace again. The dots are kept for the next time.
9.6Workspaces§
Each workspace has its own windows and its own bar; only the workspace you are on is on screen, along with any pinned windows. A fresh desk starts with Main (number 1).
- Super+1 … Super+9 shows a workspace; its last focused window takes the keyboard.
- Super+Shift+1 … Super+Shift+9 sends the focused window there, into the same zone, at the end of its app's group there (at the end of that workspace's bar, when it has none), shown in its zone. You stay where you are.
- Super+Tab and Super+Shift+Tab step through the workspaces, wrapping.
Showing a window that is on another workspace (from a notification, an agent island, Mail's chip) shows that workspace.
9.6.1Pinned windows§
A pinned window is on every workspace. Window → Pin to All Workspaces pins the focused window, any window, and leaves you where you are; the item shows a check while it is pinned, and choosing it again unpins it. It has no key for now.
- On each workspace it has a row with a small pin, in that app's group there, and it counts toward the group (one Terminal of your own and one pinned Terminal make a group). The row's place and what is on top are that workspace's own: a pinned window landing on a workspace goes to the end of its bar, and does not cover what shows there unless its zone shows nothing. A new workspace starts with the pinned windows. Clicking a pinned row stays on the workspace you are on.
- One zone: the window is in the same zone everywhere. Moving it on one workspace moves it on all, and brings it up only where you are.
- Unpinning leaves it on the workspace you are on, and it leaves the rest.
- Super+Shift+N sends it to workspace N and unpins it.
- Closing works from any workspace, the usual ways, and closes it everywhere.
Each workspace's places for its pinned windows are saved with the rest of the bar, so they come back pinned after a restart. A Browser page keeps the profile it has while pinned, so it stays signed in; unpinned, it runs the profile of the workspace it stays on, like a page sent there (see Profiles). A web app keeps its own.
Each workspace has a browser profile of its own, which goes by the workspace, not its name. Browser pages on a workspace share its sign-ins, and a page sent to another workspace reopens there under that workspace's profile. A web app keeps its own profile wherever it is (Browser).
9.7Zones§
The area right of the bar is a grid of equal cells, one row and two columns by default. A zone is a rectangle of those cells; besides the grid's zones there are always three more:
| Zone | Where it is |
|---|---|
| Full | The whole area right of the bar: every cell |
| A grid zone | Its cells of that area |
| Cinema | The whole screen, over the top row and the bar |
| Floating | Its own rectangle, above the tiles |
A tiled zone (Full, a grid zone, Cinema) shows one window at a time; the others in it wait off screen until you show them. Zones that cover the same cells take turns: showing a window in a zone takes away every zone that overlaps it, and a zone taken away comes back once nothing covers its cells. So showing a window in Full takes the columns away, and showing one in a column brings the columns back. A zone keeps its size when its neighbours are empty. Every floating window on the workspace shows, the last one raised on top.
A new window comes up in the zone you were last looking at. The first window of a workspace goes to Full, and so does one opened while Cinema is up. Scope, Preview and gamescope float from the start, and so does an app whose window you floated, until you tile it again.
A Projects session started by another session opens on that session's
workspace, in the zone last used there, with its row after that session's
and behind what the zone shows; you stay where you are. With --select it
takes you there. A session whose window is pinned, or that has no window
here, starts its new one on the workspace you are on.
- A zone's key (Super with the key you gave it; Keypad 0, 4 and 6 by default) moves the focused window there. It shows there and keeps the keyboard; its row stays where it is. Nothing toggles: to change a window's size, send it to another zone.
- Super+Keypad Multiply floats it, shrunk 50 px in on each side from its tile. Its app's windows then come back floating at that rectangle, and a float you move or resize is remembered for its app. Moving a window out of Floating forgets the rectangle.
- Super+Keypad Enter moves it to Cinema: the whole screen, drawn alone. Nothing of the desk shows over it or through it: no top row, bar or floating windows, and black instead of the background picture. The launcher and Super+K still come up. An app that asks for fullscreen (a game, a video, F11) goes to Cinema, and back to its zone when it leaves fullscreen.
Closing the window a zone shows brings back the one it showed before, so closing a page you just opened returns to the page it came from. When the focused window closes, the keyboard goes to the window now showing in its zone, else to the window in front.
9.7.1Settings → Zones§
The page draws the screen at its shape, with the top row and the bar, and the grid's cells and zones on it.
- Rows and Columns: − and + take away the bottom row or the right column, or add one. Adding keeps every zone on its cells (so they get smaller); taking away a row or column with zones on it marks them and asks before deleting them.
- Add Zone: drag across the cells it should cover (or click one), then press its key; Super is added. Esc leaves it without one.
- A zone's row opens Change Key, Clear Key, Edit Cells (choose its cells again; its key stays) and Delete. Full, Cinema and Floating cannot be deleted; their keys can be changed or cleared.
Each zone shows as its key and a little drawing of it; in lists without a drawing it is named by its place ("Column 1", "Row 2").
9.8Titlebars§
Every Sola window draws its own frame: a titlebar with the red close dot, the app's menus (the first under the app's name in bold), the Window menu, and the window's title at the right. A menu that does not fit is left out. The top row has no app menus. A focused window's frame has a lit rim.
Windows of other programs (XWayland, other Wayland apps) have no Sola titlebar. The compositor gives them the same rim, and rounded corners while they are tiled (a floating one keeps square corners); the shortcuts work on them as on any window.
9.8.1Window menu§
An app's last menu is the Window menu (an app can replace it). It holds Hide (Super+H), Cycle Windows (Super+Backtick), Pin to All Workspaces (pinned windows), Float, the grid's zones (named by their place), Full and Cinema, each with its key now, so each of those keys also has a mouse path. The Shell acts on the focused window.
9.9The right-click menu§
A right-click in a Sola application asks the Shell for the desk's right-click menu. The Shell draws it over the window, in the look of its other menus, and it can reach past the window's edges. The application says what the click landed on (a field, a selection, a link, a picture, a page), never the text itself, and which rows it adds; the Shell adds the Edit rows and the vault's, and sends back what you chose.
It opens in:
- every single-line text field of a Sola application: Settings, Mail, Calendar, Vault, Browser's location bar and vault panel, and the rest;
- a page in Browser, and in a web app's window (Browser);
- a pane in Terminal and in Projects.
It does not open in other programs' windows (XWayland and other Wayland clients), in the Shell's own fields (the launcher, the shortcuts overlay's filter, renaming a workspace), or in multi-line text (a letter in Mail, an item's notes in Vault). In a Terminal or Projects pane whose program has asked for mouse reports (vim with the mouse on, htop), the right-click goes to that program and no menu opens. With the Shell not running, a right-click opens nothing.
9.9.1Rows§
From the top, a hairline between groups:
- The application's rows, when it has any: a page's (see Browser), a pane's Copy (with a selection) and Paste. Each shows the key the application's own menus give it. A row can open one level of submenu.
- Edit, by what the click landed on: Cut with a selection in a field you can type in, Copy with a selection, Paste where you can type, and Select All. Never Cut or Copy out of a password field. Each shows its key from Settings → Keybindings. In a text field, the right-click first gives the field the keyboard and puts the caret where you clicked, unless you clicked in the selection.
- Vault ▸, where you can type.
Vault ▸ follows the vault (Vault):
| The vault | Vault ▸ shows |
|---|---|
| Not running | Vault isn't running |
| Signed out | Sign in to Vault…, which brings Vault forward |
| Locked | Unlock Vault…, which brings Vault forward; right-click again once it is unlocked |
| Unlocked | On a page, the page's logins first, each with its username at the right; then Search vault… |
An item opens its values: every value it has, each under its label, in the order its record in Vault shows them. Every field you can type in gets them all, a password field too.
- Fill, on a page and for the page's own logins only: the whole login into the page, as Browser's vault panel fills it (Filling a page).
- A login: Username, Password, and One-time code (the code at that moment, when the login has one), then each Website.
- A card: Cardholder, Brand, Number, Expiration
(
MM/YY), and Security code. An identity: its name, username, company, numbers, email, phone, and address, a line each. A bank account, a driver's license, or a passport: each of its fields. An SSH key: Fingerprint and Public key, never its private key. - Then the item's custom fields, hidden ones too, by their names, and Notes.
- The column says Reading the item… until the vault service has said which values the item has (the Shell asks it when the column opens), or why it could not.
- An item that asks for your master password shows its values dimmed, under them Asks for your master password, and Open in Vault…, which brings Vault forward on the item.
Search vault… turns the column into a search field over every item, of every kind: type, and the matches show, the most recently used first, as many as fit on the screen up to sixteen (never fewer than eight). ↓ or a click moves into them, and → or Return opens one's values: no Fill, since a match can be any site's. ← in an empty field goes back to the vault's rows. Items in the trash are not searched.
9.9.2Vault values§
A value you pick goes straight in, never through the clipboard: into a
text field at the caret, replacing any selection; into a page at its
caret; into a Terminal or Projects pane typed as keys, with no Return
after, so a sudo or ssh password prompt takes it. A value of more
than one line (notes) goes into a pane as a paste, bracketed when the
program there asks for it, so its lines do not run as commands one by
one.
On its way, only the vault service and that one application see the value. The Shell asks the service to deliver the item's field to the application's process; the service keeps the value against a one-time ticket that only that process can collect, for up to a minute; the Shell sends the application the ticket on the bus; and the application collects the value from the service's socket and puts it in. The Shell itself sees item names, usernames, and the labels of an item's values, never a value (Who may use it).
The service refuses a value from an item that asks for the master password, or from one in the trash, and the top row says why ("Vault: This item asks for your master password — open it in Vault."). Locking the vault drops a ticket not yet collected.
9.9.3Keys and placement§
| Key | Action |
|---|---|
| ↑ / ↓ | The previous or next row, wrapping, over the hairlines |
| → | Open the row's submenu |
| ← | Back to the column before (in Search vault…, once its field is empty) |
| Return | Choose the row |
| Escape | Close the menu, choosing nothing |
Hovering a row highlights it and opens its submenu; a click chooses it. A click anywhere else closes the menu. Afterwards the keyboard goes back to the window.
The menu opens at the pointer, over the window you clicked. When it would pass the screen's right or bottom edge, it opens to the left of the pointer or above it instead. Each submenu opens beside its row, on the left when there is no room on the right. The whole menu stays inside the screen's 10 px edge.
9.10Launcher§
Super+Space opens the launcher; type to filter the apps and press Enter (or click) to open one. An app that is already running comes forward instead of starting again: its last used window is shown in its zone, on its workspace, and takes the keyboard. A hidden app is shown again rather than started twice. Terminal always opens a new tab, in a window of its own, right after the focused window, Editor a new empty file, and a web app opens another window. Projects and Browser ask first (below). Escape closes the launcher; so does a click outside the card.
When no app matches what you typed, the Browser rows take their place, so a search or an address needs only Super+Space, the text, and Enter. Delete back to a match and the apps return.
Super+V pastes into the launcher's field (an address for Browser, say), and Super+C / X / A copy, cut and select in it, whatever app was focused when it opened. The same holds in the Super+K overlay.
9.10.1Projects§
Picking Projects opens a Projects window to pick a project in, starting
Projects first if it is not running; the launcher closes. That window
lists the folders of ~/Projects with their sessions, and another
computer's (Projects).
9.10.2Browser§
Picking Browser turns the launcher into a field scoped to "Browser": type an address or what to search for, as in the browser's location bar.
- With nothing typed, your six latest visits are listed.
- The first row is what Enter does: open the address, or go straight to the top result of the search (Kagi). A search also offers Kagi's results page.
- Under them, the pages you have visited that match: those whose site starts with what you typed first, then those whose site, title or address holds it.
- Picking a row opens it in a new page window right after the focused window; the browser starts if it is not running.
- Escape goes back to the apps; again closes the launcher.
9.11Hide§
Super+H does not close the app. Every window of the focused app drops off the screen (the compositor stops drawing it) and the keyboard goes to the most recent window on the workspace; a zone showing one of its windows shows its next window instead. The process keeps running.
To bring it back: click one of its rows in the bar (or reach it with Super+Backtick), or pick it in the launcher. Clicking one of its notifications, the mail chip or an event in the clock's panel brings it back too, and so does the app showing one of its own windows.
Hiding the last app on screen leaves the top row, the bar and the ground.
9.12Notifications§
A notification arrives as a card at the top right, under the top row: the app's icon, the source, the title and the text. Click it to bring its app forward (the app is told which notification it was); × dismisses it. Up to three cards show at once. A card leaves after six seconds and goes to the missed list; a newer card pushes the oldest out the same way. Calendar's reminders are the exception: they stay until you click or dismiss them (up to six). A notification that updates an earlier one (the same tag) replaces it rather than stacking.
An app that asks to open a kind of link or file in place of the app that does now gets a card above the others, Default app: "Open mailto: links with App instead of Mail?", with Keep Mail and Use App. It stays until you answer; only Use changes it (Opening links and files). An app that already opens it, or does not claim it, gets no card; a newer request replaces one still waiting.
The bell in the top row appears while anything is missed, with the count, in the accent until you open its panel. The panel, Notifications, groups the missed items by app, newest first:
- An app with up to four items lists each of them; one with more is a single row with a count. Click it to expand (the newest 30, then how many more).
- Click an item to bring its app forward. × on an item dismisses it, and × on an app's row dismisses all of that app's items.
There is no Clear All. With nothing missed the panel says Nothing missed.
9.13Screenshots§
F13, F14 and F15 copy a PNG to the clipboard and show Screenshot copied in the top row. They write no file and open no viewer. The clipboard is offered at once, so you can paste right away; the paste waits until encoding finishes.
- F13 first freezes the output as it is (open menus, panels and text selections stay in the still), then shows a marquee on that still at full brightness, with no dim. Drag a rectangle to copy it; Escape, or a drag too small to be a rectangle, cancels. The crop comes from the freeze, not from a second capture.
- F14 copies the whole output.
- F15 copies the focused window's own buffer. The window
does not come forward.
solactl compositor screenshot --appdoes the same but writes a PNG file (solactl).
The keyboard returns to the window that had it.
9.14The idle fade§
The desk's surfaces (the windows, the bar and the top row) are drawn over the background picture at the desk's opacity: 90% at first, 50–100% in Settings → Appearance → Desk. The fade and the dim are set in Settings → Appearance → Idle, and both are on at first:
- Fade the windows when idle: after Start after minutes with no input (5 at first, up to 60), everything fades into the picture over Fade over minutes (30 at first, up to 120).
- Then dim the background: once only the picture shows, it dims after Dim after minutes (60 at first, up to 240) over Dim over minutes (30 at first, up to 120), to a tenth of its light.
Any input brings it all back within a second. The compositor draws both (Compositor).
The flower and the agent islands stay at the desk's opacity while everything else fades, so a session waiting on you can still be seen and answered. Only the dim that follows covers them. Once the fade is full, the Shell tells apps their tab windows are off screen (the browser rests its pages) until you are back. A window that fills the screen (Cinema, a game's windowed fullscreen) is fully opaque and nothing fades while it is up, nor while an app keeps the screen awake.
Videos in the Browser are always fully opaque, while the rest of the page is at the desk's opacity. A video you are watching (playing with sound and mostly in sight) holds the fade where it is until it stops.
9.15Limits§
- Hide works per app: every window of the app goes.
- There is one output layout: Full and two equal halves.
- The Window menu and the shortcuts overlay also list Top, Bottom, Top Middle, Full Middle, Bottom Middle and Middle Right, with keypad chords. On this layout they do nothing, and the Shell does not take those chords.
- Notification cards cannot be answered in place; a click goes to the app.
10Theme and toolkit§
Every Sola application draws its windows with Iced through one shared
toolkit, sola-kit, and takes its colours, fonts, and shell chrome values
from one theme published on the bus. When the theme changes, each running
application applies it on its next frame. The theme styles the
applications' own controls; web pages shown inside Browser, web apps,
and Design are not restyled.
One place changes the theme: Settings → Appearance sets the colours from the background picture, from four colours of your own, or both (Appearance settings). The fonts, sizes and spacing stay at their defaults (Changing the theme).
10.1sola-kit§
sola-kit is a Rust library that every Sola application links. It has
no generic entry point: each application builds its
own iced::application or iced::daemon from the kit's parts.
| Part | What it gives an application |
|---|---|
app::startup | The standard boot: logging, the Wayland socket, the system fonts, and a watch that restarts the application in place when its own binary changes (skipped when SOLA_NO_SELF_WATCH=1) |
BusSetup, app::bus_subscription, app::bus | The bus connection (The bus) |
CallSetup, call_subscription | Call-plane methods (Call plane) |
theme | The palette, the shell style, and conversion from the bus theme |
fonts | System font loading and the five font roles |
components | Shared widgets: buttons, badges, cards, fields and forms, select, colour picker, file picker, popover, context menu, sidebar, status marks, split, scroll, toolbar, prose, JSON view, icons, and the window frame (titlebar, menus, rim) |
menu, float, app::window_settings | The app's menus in its titlebars and the standard Window menu; moving and resizing floating windows; window settings that carry the application's Wayland app_id and turn off server decorations |
tabs | A window per document: tells the Shell which window is which tab, so it keeps its place in the bar, puts an activity mark on its row, and asks the Shell to show it |
10.1.1Text fields§
Every one-line text field in a Sola application is the kit's
(components::text_input), and each one has, with no code in the
application:
- Undo and Redo. A field keeps its own history, up to 100 steps. Ctrl+Z undoes; Ctrl+Shift+Z or Ctrl+Y redoes. A run of typing is one step: it ends after a second's pause, where a new word starts, or when the caret moves. A run of Backspace or Delete is one step the same way. A paste, a cut, a deleted selection, Ctrl+K (delete to the end) and a value from the right-click menu are each a step of their own. A new edit after an Undo drops what Redo had. When the application puts a different value in the field (a cleared search, a reset form), the history starts again, and it goes when the field leaves the window. A password field keeps a history too, wiped from memory as it is dropped.
- The right-click menu (Shell): Cut, Copy, Paste, Select All and the vault's values at the caret.
The Shell takes the ⌘ keys of an application's menus, so the Edit menu's
Undo (⌘Z), Redo (⌘⇧Z), Cut, Copy, Paste and Select
All reach the application, not the field. An application hands them to
the focused field with text_input::edit_focused, which answers with the
field's id and new value for the application to store as typing would
have. Vault, Mail, Settings and Browser do. In an application whose menus
have no Undo or Redo, ⌘Z and ⌘⇧Z reach the field and work as Ctrl+Z and
Ctrl+Shift+Z.
10.2The theme on the bus§
The theme travels as the persistent topic Theme
(topic reference). Its payload is a Theme
value from sola-core:
palette.tokensmaps a token name to a token: akind(Color,FontFamily,TextSize,Space, orRadius), a stringvalue, and a list ofgroups.componentsmaps components to slot bindings. Every theme Sola publishes has this map empty, and no application reads it.
A published theme replaces the previous one whole; there is no update of a
single token. Because Theme is persistent, the bus writes each published
theme to ~/.config/sola/theme/current.yaml, restores it when the bus
starts, and replays it to every application that subscribes. When the shell
starts and that file does not exist, it publishes the built-in default
theme, which creates the file. Each token is stored like this:
palette:
tokens:
accent:
kind: Color
value: '#3dd6f5'
groups:
- accent
The bus reads the file only when it starts, so an edit in the file shows
only after sola-bus restarts. Settings → Appearance changes the colors
through the shell instead.
10.3Palette tokens§
Iced widgets take their colours from palette slots. The kit fills those
slots from ten of the tokens below and keeps the last two, selection and
highlight, beside them. The kit's components and application styles read
these; a colour an application writes into its code does not follow the
theme.
| Token | Default | Iced palette slots | Used for |
|---|---|---|---|
bg-primary | #0b0b0b | background.base, background.weakest | Window canvas |
bg-secondary | #151515 | background.weaker, background.weak | Raised panels: sidebars, cards, titlebars |
bg-hover | #1f1f1f | background.neutral, background.strong, secondary.weak | Hover |
border | #2a2a2a | background.stronger, background.strongest, secondary.base, secondary.strong | Hard edges and chrome |
text-primary | #ececec | Text on every background slot and on secondary.strong | Primary labels |
text-tertiary | #949494 | Text on secondary.base and secondary.weak | Muted labels (see below) |
accent | #3dd6f5 | primary; Iced derives its lighter and darker shades | Focus, selection chrome, key status |
success | #3ecf8e | success | Status |
warning | #e8b84a | warning | Status |
danger | #f07178 | danger | Status |
selection | #292929 | None; read with sola_kit::theme::selection() | Selected rows and text selection |
highlight | #353535 | None; read with sola_kit::theme::highlight() | A sidebar column's active row: the bar's focused window, Settings' page |
text-tertiary has two defaults. A fresh desk writes #949494; the palette's seed value, which the Shell uses whenever it
manages the colours (from the picture, or your own), is #737373.
Themes saved by earlier versions of Sola use a blue-tinted slate base
(#0c0e12, #151922, …). When the shell receives a theme that still has
slate values, it swaps each one for its grey and saves the result. Colours
you picked yourself are kept.
Colour values are #rrggbb or #rrggbbaa. Terminal and Projects also
derive their terminal colours from these tokens: the default foreground and
background, the ANSI colours except magenta and cyan, and the selection
wash (from accent). Colours a program sets with 256-colour or 24-bit
codes are drawn as given, and the cursor colour is fixed.
The palette carries more tokens: bg-tertiary, border-subtle,
text-secondary, text-muted, text-accent, accent-dim, text sizes
(text-caption to text-display), spacing (space-xs to space-xxl),
and radii (radius-sm to radius-lg). They travel and are saved with the
theme, but no application reads them.
10.4Fonts§
10.4.1Roles§
Components never name a font family. They ask for a role each time they draw, and the theme assigns a family to each role.
| Token | Role | Weight | Used for | Default family |
|---|---|---|---|---|
font-ui | fonts::ui() | Regular | Body text, buttons, sidebar items | SF Pro Text |
font-ui-medium | fonts::ui_medium() | Medium (500) | Emphasis: badges, section caps, toolbar labels | SF Pro Text |
font-display | fonts::display() | Medium (500) | Page titles and large headings | SF Pro Text |
font-chrome | fonts::chrome() | Regular | Titlebar menus and titles, the top row's labels and clock | SF Pro Text |
font-mono | fonts::mono() | Regular | Code, JSON, Terminal, Projects | Iosevka Term Slab |
10.4.2How a family is chosen§
Sola bundles no fonts. app::startup loads every family fontconfig knows
into Iced's font database, once per process. A family named in the theme is
used only if it is in that database. When a role's token is missing, empty,
or names a family that is not installed, the role falls back:
- UI roles: SF Pro Text if installed, otherwise Inter, otherwise Noto Sans.
- Mono: Iosevka Term Slab if installed, otherwise JetBrains Mono, otherwise Noto Sans Mono.
Fonts lists the families to install.
Each process keeps the five roles in one table. fonts::install(table)
replaces it, and the next frame draws with the new families; every Theme
message does this. Terminal and Projects also measure the new mono font's
cell size and reflow their panes.
10.5Shell tokens§
The shell's chrome has its own tokens in the same palette, all in the group
shell. Only the shell applies them, and it draws with four of them.
| Token | Default | Controls |
|---|---|---|
shell-flower | #ffb800 | The flower at the left of the top row. A picture's scheme (not a grey picture's) and your Accent set it to the accent |
shell-backdrop-dim | #00000099 | Dimming behind the launcher |
shell-launcher-width | 560px | Launcher width |
shell-launcher-pad | 8px | Row padding in the launcher and the shortcuts overlay |
Eight more are in the theme, but nothing edits them and nothing
draws with them:
shell-menubar-bg (#050505; the top row is transparent), and the
switcher's shell-switcher-bg (#151515e6), shell-switcher-border,
shell-switcher-icon-bg, shell-switcher-icon-fg,
shell-switcher-icon-fg-sel, shell-switcher-pad and
shell-switcher-tile-pad. The switcher they styled is gone.
Colours may carry alpha (#rrggbbaa). Spacing values are a number followed
by px. The shell reads these tokens into its ShellStyle on every Theme
message. The Shell chapter describes the top row and the
launcher.
10.6How an application applies the theme§
On every Theme message, a kit application calls
sola_kit::apply_theme_update(&message, &mut theme), which rebuilds its
Iced theme from the palette tokens (theme::theme_from_bus), installs the
font roles (fonts::install), installs the colours that have no Iced slot
(selection, highlight, and the chrome surface drawn from the base and
raised fills), and returns true when the message was a Theme. The
application returns its stored theme from Iced's theme callback, so the
next frame uses it.
Before a theme arrives (between start and the bus replay, or while the bus
is down), an application draws with sola_kit::default_theme(), the
compile-time defaults in the tables above, with font roles resolved from
the default families.
The compositor follows the theme too: the rims it draws around windows of
other programs come from highlight and bg-primary
(Rims and rows).
10.6.1Fallbacks§
| Missing or malformed | Result |
|---|---|
| No theme on the bus | Compile-time defaults, as before a theme arrives |
A palette token, or a colour not in #rrggbb or #rrggbbaa form | That token's default |
| A font token, or a family that is not installed | The role's fallback family |
A shell token, or a spacing value not in <number>px form | That token's default |
A theme/current.yaml that does not match the theme schema | The bus restores no theme, and the shell publishes none because the file exists; applications draw the compile-time defaults until a theme is published (solactl emit Theme), or the file is removed and the shell starts again |
10.7Appearance settings§
Settings' Appearance page holds, top to bottom:
| Section | Settings |
|---|---|
| Light or dark | Mode: Dark (at first) or Light: the desk and every Sola window wear the light theme, and web pages are told you prefer it (Browser) |
| Background picture | The picture's file name and Clear, or "None — the black ground."; under it every picture set before, each with the look it was worn with (Kept pictures). A picture is set from Paint (File → Set as Background), Browser, or solactl background |
| Colors | Adapt colors to the background, Strength, and a card for each look |
| Your colors | Window, Panels, Highlight, Accent |
| Desk | Opacity of the windows, the bar and the top row over the background: 50–100%, 90% at first |
| Idle | Fade the windows when idle, Start after, Fade over, Then dim the background, Dim after, Dim over, Lock when the windows have faded (Security) |
The settings travel as the persistent topic Appearance, saved in
~/.config/sola/state.yaml. The Shell acts on the colours, rewriting
Theme; the compositor acts on the opacity, the idle fade and the dim
(Shell § The idle fade has their defaults
and ranges; Compositor how they are drawn); the browser
acts on light or dark too. Light gives Sola a light theme: the Shell
writes the light greys into Theme (a near-white base, panels and lines a
step darker, near-black text, a deeper accent and status colors), and with
Adapt colors to the background on, the picture's colors go over them
as over the dark greys: the same hues, a softer tint, an accent dark enough
to read on white. The looks' previews show the mode in use. Lines, lifts
and window rims go darker on a light surface instead of lighter; the
terminal's black is dark and its white light. Dark puts the dark greys
back.
10.8Colors from the background picture§
Adapt colors to the background makes the theme's colours follow the
background picture. The Shell samples the picture and publishes Theme
with new colours, so every application changes at once:
- The surfaces (window fills, titlebars, the bar and other sidebars, the top row's islands, borders, the selection and the highlight) take a tint of one of the picture's colours; the text a fainter one. Each keeps the lightness of its grey, so contrast does not change. The darkest surfaces take a little less of the tint.
- The accent takes another of the picture's colours, at least 60° of hue from the tint where the picture has one (otherwise the tint's own hue), as light as the default accent and as vivid as the picture's own colour there allows.
- The flower in the top row takes the accent too.
- Status colours, fonts, sizes and spacing do not change.
The picture offers a scheme for each colour it is made of: up to four, the best first, as far apart in hue as the picture allows. The colours are the picture's own (the ones many of its pixels share, preferring vivid ones), not averages over it, so a dark picture still gives colourful schemes. A picture of fewer than three colours is filled out to three with complements, after its own: the opposite hue of each of its colours, then the two hues either side of its main colour's opposite. A complement keeps the brightness and colourfulness of the colour it turns, and takes the picture's colour as its accent. Each scheme is named by its hue: Rose, Ember, Amber, Gold, Olive, Green, Teal, Sky, Blue, Violet or Magenta. A picture with no colour of its own offers one scheme, Grey: the greys and the amber flower stay, and the accent is a cyan close to the default.
When the picture offers more than one scheme, each shows as a card under Strength: a small window in its colours, with the picture's colour beside its name. Click one to use it; the one in use is ringed in the accent.
Strength (0–100%, 60% at first) is how much of the picture colour's own colourfulness the surfaces take, up to a fixed limit; at 0% they stay grey. Every window follows as you drag the slider.
Turning Adapt colors to the background off, or clearing the picture, puts the greys back (with your colours over them, if a look of yours is in use).
10.8.1Your colors and your looks§
Your colors go over the greys or over the picture's scheme, adapting on or off. Click a swatch for a picker, then Use (or Cancel).
| Color | Sets |
|---|---|
| Window | bg-primary |
| Panels | bg-secondary (bars, titlebars, sidebars), and shell-switcher-bg at 90% opacity |
| Highlight | highlight (the bar's focused row); selection takes its hue at its own lightness |
| Accent | accent, text-accent, accent-dim at 10%, and the flower (shell-flower) |
Changing a colour while a scheme (or the greys) is in use makes a look of your own from it, named Custom (then Custom 2, …), shown as a card beside the schemes, and leaves the scheme as it was: click the scheme's card to go back to it, and the Custom card to come back to yours. While one of your looks is in use, changes go to it. Reset beside a colour gives that colour back to the look's scheme, and Remove Custom (in the section's heading) forgets the look; its scheme stays in use. You keep up to six looks; with six, remove one to make another.
10.8.2What the Shell rewrites§
While the Shell manages the colours (adapting with a picture, or a look of
yours in use), it makes them from scratch on every change: the seed greys,
then the picture's scheme over them, then your colours over that. It
rewrites these tokens: bg-primary, bg-secondary, bg-tertiary,
bg-hover, selection, highlight, border, border-subtle,
text-primary, text-secondary, text-tertiary, text-muted,
accent, text-accent, accent-dim, shell-menubar-bg,
shell-switcher-bg and shell-flower. A Theme published with other
values for these (solactl emit Theme) is replaced at once; the rest
(status colours, fonts, the other shell tokens) stay. With adapting off
and no look of yours in use, the Shell leaves the theme alone.
10.8.3Rims and rows§
Hover and shown rows in a sidebar are lifts of the column that keep its
hue, so they follow a tinted or custom panel colour instead of turning
grey. A focused window's rim is the highlight a step lighter, so it takes
the scheme's or your highlight's hue (#3d3d3d on the grey theme). An
unfocused rim is opaque: the window's face 7% lighter in its hue. The
compositor draws the same rims around windows of other programs.
10.8.4Kept pictures§
Every picture you set stays in ~/.config/sola/background/, and
Settings → Appearance → Background picture shows them all, newest
first: the picture set has the accent ring, and its file name with
Clear (the black ground) is above them. Click another to set it
again. Hover one for ×, which deletes Sola's copy and what it kept.
Each picture keeps the look you wore with it: adapting on or off, the
strength, the scheme, your colors and your looks
(Appearance.pictures). When the picture changes, from Settings, Paint,
Browser or solactl background, the Shell keeps the outgoing picture's
look and puts the incoming one's back. A picture never set before keeps
the look already in use. Light or dark, the desk's opacity and the idle
settings are the same for every picture.
10.9Changing the theme§
Settings → Appearance is where the theme changes: the colors from the background picture, a scheme, your own colors and looks (above). The fonts, the shell's spacing and the other tokens stay at their defaults. There is no theme editor and there are no named themes.
10.10Following the theme in a new application§
- Call
sola_kit::app::startup(APP_ID)first; it loads the system fonts. - Include
TopicKind::Themein theBusSetupsubscription. - Keep an
iced::Themein the application state, start it assola_kit::default_theme(), and return it from Iced'sthemecallback. A kit window returnssola_kit::theme_for(chrome, &theme)(itsFloatState::chrome), which clears the window fill so the frame's rounded corners show what is under it, except in Cinema. - Map
sola_kit::app::bus_subscription()into the application's subscription and pass each message tosola_kit::apply_theme_update. - Draw with kit components. In custom styles, take colours from
theme.extended_palette(),sola_kit::theme::selection(),sola_kit::theme::highlight()andsola_kit::components::style::chrome_surface(), and fonts from thesola_kit::fontsrole functions. A colour or family written into the application's code does not follow the theme.
The bus has a code sketch of this wiring.
10.11Limits§
- The theme controls twelve colours, five font roles, and the four shell values the shell draws with. Type sizes, spacing, and radii are fixed.
- There is no light theme; Iced treats every theme as dark, whatever its colours.
- The font database is loaded once per process. A family installed while an application runs is not available to it until it restarts.
- Iced's default font, used by widgets that set no font, is chosen when an
application starts, before the theme arrives: the default UI family in
most applications, Inter in the shell, the default mono family in
Terminal and Projects. It does not follow
font-ui. Text that a component draws with a role does follow the theme. - There is no command to change one token.
solactl emit Themecan publish a whole theme as JSON.
11solactl§
solactl is the operator command line. Most of its commands are calls on
the call plane, sola-call (The call plane); emit and
background write to the bus; logs and media work locally; open
finds the app Sola keeps for a link or a file and reaches it over both
(Opening links and files).
On Oath it comes in the sola package as /bin/solactl; elsewhere it is
installed as /opt/sola/bin/solactl. Oath is recognised by
/oath/INDEX.md; the same test picks Oath's directories for logs and
programs below.
11.1Synopsis§
solactl compositor screenshot [-o PATH] [-a APP] [-w TITLE] [--format png|rgba] [-t SECS]
solactl compositor sample [-s N] [-t SECS]
solactl compositor windows
solactl compositor input click X Y [-b left|right|middle] [-t SECS]
solactl compositor input move X Y [-t SECS]
solactl compositor input scroll [-x DX] [-y DY] [-t SECS]
solactl compositor input key CHORD [-t SECS]
solactl compositor input press [X Y] [-b left|right|middle] [-t SECS]
solactl compositor input release [X Y] [-b left|right|middle] [-t SECS]
solactl compositor input drag X1 Y1 X2 Y2 [-b …] [-s STEPS] [-t SECS]
solactl desk launch APP_ID [-c COMMAND] [-t SECS]
solactl desk close APP_ID [-t SECS]
solactl projects [METHOD [ARGS…]]
solactl browser [METHOD [ARGS…]]
solactl bots [METHOD [ARGS…]]
solactl design [METHOD [ARGS…]]
solactl vault [status|lock|sync|open [-i ID]]
solactl OWNER [METHOD [ARGS…]] # any live owner, by owner or app id
solactl mcp [--owner OWNER]…
solactl logs [APP] [-f]
solactl emit KIND PAYLOAD
solactl open TARGET
solactl background set IMAGE
solactl background clear
solactl media play-pause|next|prev|mute|vol-up|vol-down
solactl help [COMMAND…]
| Command | Goes through | Needs |
|---|---|---|
compositor, desk | Call plane; flags compiled into solactl | sola-call and the owner; --help needs nothing |
projects, browser, bots, design, vault, any other name | Call plane; flags read from the owner's advertised methods | sola-call and the owner, also for help |
mcp | Call plane | sola-call; tools appear as owners do |
emit, background | Bus | sola-bus (the picture is drawn by sola-river) |
logs | Files in Sola's log directory | nothing |
open | Opens a link or a file with the app Sola keeps for it, as xdg-open does: its open call, or the session starts it first | sola-call; the bus to start an app |
media | Session D-Bus (MPRIS) and wpctl | a player, PipeWire |
solactl never starts an application to answer a call. There is no
--version, and there are no eval, watch, or generic call commands:
a method is called by its owner's name, and Monitor
shows bus and call traffic.
11.2Output and exit status§
A successful call prints the reply's data to stdout as indented JSON,
and prints nothing when the reply has no data. Method lists and help are
plain text. Errors go to stderr, prefixed solactl: (solactl media:
for media); usage errors in a compiled command are clap's error: ….
Call output written into a closed pipe (solactl browser tabs | head) is
not an error.
| Status | Meaning |
|---|---|
| 0 | Success; also media with no player to act on |
| 1 | The owner or the host answered with an error. This includes timeout, and <owner> is not running for compositor and desk. Also any failure of compositor input drag |
| 2 | Usage error in a compiled command (compositor, desk, mcp, logs, emit, open, background, media): a missing or bad argument or an unknown subcommand; usage is printed |
| 3 | Local failure: sola-call unreachable, a live owner not in the catalog, an unknown method, a flag error, a SOLA_BOT=1 refusal, a bad chord, or a failed logs, emit, open, background, or media |
In a compiled command a negative value must be joined to its flag:
solactl compositor input scroll --dy=-5. Written apart (--dy -5), the
-5 is read as a flag and the command fails with status 2. Live-owner
flags are the other way round: they take a negative number written apart
(--dy -5), and have no --name=value form.
11.2.1When the call host or an owner is down§
| Situation | stderr | Status |
|---|---|---|
sola-call not running | solactl: call host is not running (<socket path>) | 3 |
compositor or desk owner not connected | solactl: compositor is not running | 1 |
Live owner not in the catalog (also for its method list and --help) | solactl: <name> is not running | 3 |
| Method not advertised | solactl: <owner> has no method <method> | 3 |
| Owner did not answer by the deadline, or exited during the call | solactl: timeout | 1 |
sola-call restarted during the call | solactl: call host closed the connection | 3 |
Any command name solactl does not know is taken as an owner name, so a
misspelt command reports <name> is not running.
11.3Environment§
| Variable | Read by | Effect |
|---|---|---|
SOLA_CALL_PATH | call commands, mcp | Call socket. Default $XDG_RUNTIME_DIR/sola-call |
SOLA_BUS_PATH | emit, background | Bus socket. Default $XDG_RUNTIME_DIR/sola-bus |
XDG_RUNTIME_DIR | call commands, mcp, emit, background | Directory of the default sockets. When it is unset or empty (an SSH login on Oath sets none): /run/user/<uid> if that directory exists, else /tmp |
SOLA_BOT | compositor, browser, mcp | 1, true, TRUE, or yes turns on the bot rules |
SOLA_PANE_ID | projects whoami, projects session.new | Default --pane and --parent. Projects sets it in each session |
SOLA_WS_PATH | projects whoami | Default --path. Projects sets it in each pane to the session's checkout path |
HOME | path arguments, open, logs | ~ expansion; default location of chrome.sock and, on Oath, of the logs |
SOLA_LOG_DIR | logs | Log directory. Default $XDG_STATE_HOME/sola on Oath (~/.local/state/sola when that is unset or not absolute), else /opt/sola/log |
SOLA_BIN_DIR | open | Where Sola's programs are. Default /bin on Oath, else /opt/sola/bin |
XDG_DATA_HOME | open | The Browser's handoff socket is $XDG_DATA_HOME/sola/browser/chrome.sock (default ~/.local/share/…) |
SOLA_BROWSER | open | Browser binary to start. Default sola-browser in Sola's bin directory. A path that is not a file is ignored |
SOLA_PAINT | open | Paint binary to start. Default sola-paint in Sola's bin directory. A path that is not a file is ignored |
DBUS_SESSION_BUS_ADDRESS | media | Session D-Bus used to find MPRIS players |
PATH | media | Finds wpctl |
Over SSH, XDG_RUNTIME_DIR is the one that matters: with the fallback to
/run/user/<uid>, solactl run as the desk user from an SSH login reaches
the session's sockets without setting anything.
The working directory matters too: relative path arguments resolve
against it, and design canvas.new makes its canvas in it by default.
11.4Live-owner commands§
solactl projects, browser, bots, design, vault, and
solactl NAME for any other owner take their methods and flags from what the running
owner advertises (The call plane). The
sections below list each owner's methods as they are advertised today.
11.4.1Owners§
| Owner | App id | Process | Present | Section |
|---|---|---|---|---|
compositor | sola-river | sola-river | Whole session | Compositor |
desk | sola-session | sola-session | Whole session | Desk |
bots | sola-botsd | sola-botsd | While the daemon runs | Bots |
projects | sola-projects | sola-projects | While Projects is open | Projects |
browser | sola-browser | sola-browser | While the Browser is open | Browser |
design | sola-design | sola-design | While Design is open | Design |
vault | sola-vaultd | sola-vaultd | While the vault service runs | Vault |
No other application offers methods: Calendar, Mail, Settings, Paint,
Vault (the app; its service is vault), and the Shell have none. The
Shell's desktop workspaces have no solactl command;
solactl workspaces is read as an owner name and fails with
workspaces is not running (status 3).
compositor and desk have compiled commands, but they are live
owners too, so their app ids reach them through the generic parser:
solactl sola-river cursor --visible false (see
Methods with no compiled flags).
11.4.2Listing and help§
solactl OWNER # the owner's methods, one line each
solactl OWNER --help # the same (-h too)
solactl OWNER METHOD --help # the method's flags
OWNER may be the owner or its app id (solactl sola-design status). The
list starts with solactl <name> — <app id>, then each method's name and
summary. A method's help lists each flag as -s, --long <value>, then
its help text and (required) or (optional); bool flags have no
<value>. -h or
--help anywhere after the method prints that help instead of calling.
11.4.3Arguments§
--name VALUEor-s VALUEsets an argument;--name=VALUEis not split. When a flag is given twice, the last one wins.- A bool flag alone is
true.--name true,false,1, or0sets it explicitly. Bool flags may come before other flags. - A value that starts with
-is refused (--name needs a value), except a negative number. - A bare word fills the next required argument not yet given, in the
advertised order:
solactl design open design/home,solactl bots new Suno. A bare word with no required argument left is an error (unexpected argument). - A single JSON object as the only argument is sent as the params,
without conversion or path resolution:
solactl browser tab.open '{"url":"https://example.com"}'. The context is still added and the bot rules still apply. JSON that does not parse is an error (status 3). - Values are converted by the argument's type:
intandfloatare parsed (a value that does not parse is sent as a string and the owner answers with its error),jsonis parsed (else sent as a string), andpathis made absolute (below). - An unknown
--flagis sent as a string argument. An unknown-xis an error (unknown flag -x). solactldoes not check required arguments; the owner answers with its error.
11.4.4Paths§
Arguments of type path are resolved by solactl before the call: ~
and ~/… expand, and a relative path is taken from your working
directory, not the owner's. They are projects --path and
--prompt-file; browser --file and screenshot --path; design
--path, --canvas, and --out; and the compositor's screenshot
--output when you reach it by app id (solactl sola-river screenshot).
Other arguments are sent as typed.
11.4.5Deadline§
- When the method has a
--timeoutargument (seconds) and you pass it, the deadline is that plus 2 s, and at least 8 s. - Otherwise it is the method's advertised deadline, rounded up to whole seconds (listed per owner below).
- Otherwise it is 8 s.
11.4.6Context§
projects whoamiwithout--paneor--pathsends$SOLA_PANE_IDand$SOLA_WS_PATH.projects session.newwithout--parentsends$SOLA_PANE_ID. The new window opens on the parent's workspace, not the one showing.design canvas.newwithout--pathsends your working directory.
11.4.7Bot rules§
With SOLA_BOT=1 (named informational bots), solactl refuses to take
the human's seat. A refusal exits 3 with a message naming the rule.
browser tab.focusis refused, and--selecton anybrowsermethod.browserpage methods need--tab; they never default to the focused tab:snapshot,click,hover,type,fill,get,links,select,wait,screenshot,goto,back,forward,reload,stop,find.page,find,key,scroll,tab.close.compositor screenshot,sample, andinputare refused;compositor windowsis allowed.
The web path for a bot is solactl browser only. The Browser rules go
with the owner, so they hold however the Browser is named (browser or
sola-browser) and over MCP. Limit: the compositor refusals belong to the
solactl compositor command, not to its methods, so they do not apply
over MCP or through the app id (solactl sola-river …).
11.5Compositor (solactl compositor)§
Owner compositor, process sola-river, present for the whole session
(Compositor). The flags are compiled into solactl.
11.5.1screenshot§
| Flag | Default | Meaning |
|---|---|---|
-o, --output PATH | /tmp/sola/screenshots/<ms>.png | Where to write. Sent as given: a relative path is taken from sola-river's working directory, so pass an absolute one. Parent directories are created |
-a, --app APP | the whole output | Capture this app's window (exact app id) |
-w, --window TITLE | the app's first window | With --app, the window whose title is exactly TITLE; ignored without --app |
--format png|rgba | png | rgba (or rgba8) writes packed RGBA8 with no PNG encode; its default path is /dev/shm/sola-freeze-<ms>.rgba (/tmp/sola/screenshots/<ms>.rgba without /dev/shm) |
-t, --timeout SECS | 10 | Deadline |
--app copies that window's own buffer (ext-image-copy-capture). The
window does not need to be on top and is not raised. The default PNG uses
fast compression. The reply is {"path": …}; with rgba it adds width,
height, and "format": "rgba8". Errors:
screenshot already in progress,
window not found for capture: app_id=… title=…, and
unknown screenshot format … (png|rgba).
The Shell's screenshot chords copy to the clipboard instead of writing this file (see Shell § Screenshots). The method also takes a region, which has no flags here (see Methods with no compiled flags).
11.5.2sample§
-s, --size N (default 15) is the side of the square patch around the
pointer: odd, at most 65 (an even size rounds up; 0 or less means 15).
-t, --timeout SECS defaults to 2. The reply has x, y (the
pointer), left, top (the patch's origin), width, height, hot_x,
hot_y (the pointer within the patch), and pixels (base64 RGBA8).
Errors: no pointer position yet, sample already in progress. Scope
uses this method.
11.5.3windows§
No flags; the deadline is 5 s. The reply is an object keyed by app id;
each value is an array of {"title", "window_id", "pid"}.
11.5.4input§
| Command | Arguments | Defaults |
|---|---|---|
input click X Y | -b, --button left|right|middle (also l, r, m; lower case only) | left |
input move X Y | none | |
input scroll | -x, --dx DX; -y, --dy DY (decimal; down is positive; a negative value as --dy=-5) | 0, 5 |
input key CHORD | none | |
input press [X Y] | -b, --button as for click | left |
input release [X Y] | -b, --button as for click | left |
input drag X1 Y1 X2 Y2 | -b, --button; -s, --steps N (moves between press and release) | left, 12 |
Each takes -t, --timeout SECS (default 5) and replies {"ok": true}
(drag prints nothing; on failure it prints solactl: <method>: <error>
for the step that failed, and exits 1). Another button name fails with
unknown button …. X and Y are integer output coordinates. The
events go through the seat like real input, to whatever is under the
pointer or focused. press holds the button until a release; with
X Y either moves there first. drag is a press at X1 Y1, STEPS
moves about 16 ms apart, and a release at X2 Y2, so it is seen as a drag
(a row in the bar, a sidebar reorder) rather than a click.
CHORD is MODIFIER+…+KEY, in any case. Modifiers: Meta (or Super,
Win, Cmd), Alt (or Option), Ctrl (or Control), Shift. Keys:
Tab, Esc or Escape, Enter or Return, Backspace or BS,
Space, Left, Right, A–Z, 0–9, ` (or Grave,
Backtick), and the keypad: KP0, KP2, KP4, KP5, KP6, KP8,
KPEnter, KPMultiply (or KP*), KPAdd, KPEqual (or KP=),
KPDecimal (or KP.). Any other key is refused (unknown key '…',
status 3).
11.5.5Methods with no compiled flags§
cursor(--visible true|false, defaulttrue; deadline 2 s) shows or hides the compositor's pointer and replies{"visible": …}.screenshotalso takes a region:--x,--y,--width,--height, all four, in output coordinates. With--appthe region is ignored.
solactl compositor has no command or flag for either. Reach them by the
compositor's app id, which goes through the generic parser, or over MCP
(compositor_cursor, compositor_screenshot):
solactl sola-river cursor --visible false
solactl sola-river screenshot --x 0 --y 0 --width 640 --height 480 -o shot.png
That way the deadline is the method's own (2 s for cursor, 8 s for
screenshot), -o/--output is a path argument resolved against your
working directory, and the bot refusals do not apply.
input.key expects chord as an object (keycode, an XKB key code, and
meta, alt, ctrl, shift booleans, all five given), which
solactl compositor input key builds from the text form. A text chord
sent to it by app id or over MCP (compositor_input_key) is refused with
chord: invalid type …; the JSON form works:
solactl sola-river input.key '{"chord":{"keycode":23,"meta":true,"alt":false,"ctrl":false,"shift":false}}'
11.6Desk (solactl desk)§
Owner desk, process sola-session, present for the whole session
(Session manager). The flags are compiled into solactl.
| Command | Flags | Reply |
|---|---|---|
launch APP_ID | -c, --command COMMAND; -t, --timeout SECS (5) | {"app_id", "command", "pid"} |
close APP_ID | -t, --timeout SECS (5) | {"app_id", "stopped": N} |
launch runs COMMAND, split on whitespace with no shell quoting. The
default is <APP_ID> in Sola's bin directory (/bin/<APP_ID> on Oath,
/opt/sola/bin/<APP_ID> elsewhere) when that file exists, else APP_ID
itself. Errors: empty command, or the spawn error. close stops the
processes the session launched for APP_ID; with none it fails with
no live children for <APP_ID>.
11.7Bots (solactl bots)§
A bot is a named informational agent session. Each bot is a directory in
~/Bots, as each project is a directory in ~/Projects, and runs on the
AI it was made with: Grok, Claude, or Gab AI. The AI is chosen
when the bot is made and does not change after. solactl bots needs the
bots daemon, sola-botsd (owner bots). It fails if the daemon or
sola-call is down; it does not launch a window. The process manager
sola keeps the daemon running; on Oath no service starts it, and until
it is started by hand solactl bots fails with bots is not running.
solactl bots # list methods
solactl bots list
solactl bots poll [-b BOT]
solactl bots new -n NAME -a AI # AI: grok, claude, or gab
solactl bots setup -b BOT -a AI # a directory made by hand
solactl bots send -b BOT -t TEXT
solactl bots transcript -b BOT
solactl bots rm -b BOT
solactl bots cancel -b BOT
-b, --bot is a bot's id, slug, or name; -n, --name is a display
name; -a, --ai is grok, claude, or gab; -t, --text is the
message. All are required where they appear, except --bot on poll.
Every method has the default 8 s deadline.
| Method | Does |
|---|---|
list | Bots, their AI, and their status |
poll | {"bots", "transcript"} in one call; transcript is null without --bot, or for a bot it does not find |
new | Creates a bot on an AI, and its home |
setup | Gives a bot directory without an AI its AI; fails if it has one |
send | Sends a message and returns while the turn runs; read the answer with transcript or poll |
transcript | The dialog turns |
rm | Deletes the bot's directory (and a Grok bot's session) |
cancel | Cancels the current turn |
11.7.1Homes and the write fence§
Each bot's home is ~/Bots/<slug>/. Its bot.json holds its name, its
AI, an optional model (empty for the AI's default), and its session; its
dialog is dialog.json. A directory made in ~/Bots by hand shows up as
a bot with no AI; it talks once setup (or its header in the Bots
window) gives it one. new and setup seed the home and start a first
turn, an introduction (the orientation prompt is not shown as a user
message). A catalog from before bots were directories,
~/.config/sola/bots/catalog.json, is moved once into each bot's
bot.json (all on Grok); the old file is kept as catalog.json.moved.
The write fence is enforced by the prompt only. By default a bot writes only inside its home. To open the fence for one task, say “drop the write fence” or “work on the whole computer”, or name a path outside the home. Seat steal is refused even then. A bot is not a coding agent, and it does not use Projects unless you ask for that rail.
11.7.2The AIs§
| AI | Runs as | Signs in with |
|---|---|---|
| Grok | grok agent stdio (ACP), one child per bot, all tools approved | the Grok CLI's own login; run grok in a terminal once to sign in |
| Claude | claude -p per message on the bot's session (--resume), all tools approved, no MCP servers | Claude Code's own login, the subscription; an ANTHROPIC_API_KEY is never passed on |
| Gab AI | Gab's chat API (https://gab.ai/v1, model arya), the dialog sent each turn | an API key in GAB_API_KEY or ~/.config/sola/bots/gab.key (Gab AI → Settings → API; Plus plan) |
Grok and Claude bots are agents in their home: shell, files, and the
browser through solactl browser, under AGENTS.md (Claude reads it
through the home's CLAUDE.md). An AGENTS.md that was already in a
directory made by hand is kept as AGENTS.md.orig. A Gab AI bot only
chats: no tools, its CURRENT.md in its system prompt. The daemon runs
$SOLA_GROK / $SOLA_CLAUDE when set, else ~/.local/bin/grok /
~/.local/bin/claude when that file exists, else grok / claude on
PATH.
11.7.3The socket API§
The daemon serves HTTP on a Unix socket, $XDG_RUNTIME_DIR/sola-bots
(mode 0600; SOLA_BOTS_PATH overrides it), with no token: the socket is
the user's alone. There is no TCP port and no access from another
computer.
GET /health
GET /bots
GET /events # SSE: snapshot, bots, transcript, delta, removed
GET /poll?bot={id} # one-shot snapshot (debug)
GET /bots/{id}/transcript
POST /bots { "name": "Suno", "ai": "claude" }
POST /bots/{id}/setup { "ai": "gab" }
DELETE /bots/{id}
POST /bots/{id}/send { "text": "…" }
POST /bots/{id}/cancel
curl --unix-socket "$XDG_RUNTIME_DIR/sola-bots" http://localhost/bots
GET /events is text/event-stream (chunked), with keepalive comments
every 15 s. The Bots window uses this surface (list, send, create,
delete, and live events). solactl bots uses sola-call, not HTTP. The
iPhone app (SolaBot) spoke the old TCP API and cannot reach this one.
11.8Projects (solactl projects)§
Needs Projects running (owner projects, app id sola-projects).
Fails if the app or sola-call is down; it does not launch a window.
A session is a terminal (a shell, or an agent: Grok, Codex, Claude)
in a project. It is a tmux session on the computer it runs on, so it
runs whether or not a window shows it. Every session starts in its
project's folder; a project can have any number. A project is a folder in
~/Projects, here or on another computer.
Each session open here is its own window, a row in the shell's bar. The row names up to three things, drawn apart: the project, the session (when it is not the project's main one), and, when it runs on another computer, that computer as a chip in its prompt color. In place of its icon the row shows the session's activity: an amber diamond while it waits on a decision, a spinning ring while it works.
Super+V pastes the clipboard's text into the session. With a picture and no text on the clipboard, it goes to the agent in the session the way Ctrl+V does: the agent reads the clipboard itself, of the computer it runs on, so a picture copied here reaches a session here. A shell gets nothing.
Under its title, a session's window has a bar: its agent (Claude, Codex,
Grok, or Shell) and model at the left; at the right its turns (prompts
you sent), compactions, and context (what its last request carried, in
tokens; "of 272k" when the agent's files give its window). Claude's come
from its transcript, Codex's from its rollout; Grok's files give only
compactions. A session on another computer reads them there over ssh.
A Claude session shows more, from Claude's status line: its effort level
and fast (when fast mode is on) after the model; its context as a
percent of the window ("45% of 1M context"); and what is left of the
account's 5-hour and weekly limits ("68% left in 5h", "82% left this
week"), or a gateway's spend ("$12 of $50"). Projects installs the status
line command (~/.config/sola/projects/claude-status.sh, or
~/.config/sola/remote/ on another computer) as Claude's statusLine
unless you have one of your own; it writes to the computer's hook log
only when one of those figures changes, and prints nothing, so Claude's
own status line stays empty. The figures come with Claude's next update
of its status line (after a message). In Cinema the window shows only
the terminal.
A Projects window to pick a project in. The launcher's Projects
(window.navigate), Project → Open Project… (⌘O) and New
Project… (⌘N) open one; the launcher starts Projects first if it is not
running. It lists the folders of ~/Projects (type to filter; ↑ ↓ and
Enter, or click), each with its sessions running on this computer under
it, whichever computer started them (main for the project's first);
ones open here say Open.
- Picking a folder opens its running sessions (or starts its main one); picking a session opens it alone. The window closes and the session's window takes its place.
- New project… asks for a name (or takes the one you typed) and makes
~/Projects/<name>, then opens it. - On another computer… asks for a computer: its name,
user@name, or a~/.ssh/configalias; the last eight are listed, latest first. Enter lists that computer's~/Projectsand its sessions over ssh, and the window works as it does here; New makes a folder there. The listing never asks for a password: it needs a key or an ssh agent, and tmux there. If it cannot be listed, the reason shows above the computers. - Esc goes back a step (a computer's projects to the computers, the computers to this one's projects, a new name to the list); at the top it closes the window.
Any computer's Projects can open any computer's sessions: two computers
can watch and type in the same session. Its size follows the computer
that typed last. Closing a window detaches: that computer stops
watching and the session keeps running, as it does when Projects quits.
Project → End Session… (or session.end) ends a session — its tmux is
killed where it runs, for every computer watching; it asks first while
its agent is working or waiting. A session whose shell exits shows
Start new shell.
Projects makes no worktrees. A session makes its own when its work needs
one (git worktree add .worktrees/<slug> from the project root); ending
a session never touches them.
New Session… (⌘T) starts one in the focused session's project, on its
computer, in a window of its own. Closing the last window quits Projects;
started with nothing to open, Projects waits 20 seconds for a call (the
launcher's window.navigate), then quits. Projects was called Workspaces; solactl workspaces and the workspace.* methods are gone. The tab.* methods
and --tab were renamed session.* and --session on 2026-10-31.
solactl projects # list methods
solactl projects ps
solactl projects project.list
solactl projects window.navigate # a window to pick a project in
solactl projects project.open --path ~/Projects/Sola # its sessions here
solactl projects project.open --host <computer> --path /home/<user>/Projects/Sola [--session ws-main]
solactl projects project.close --project Sola # its windows close; sessions run on
solactl projects session.list [--project Sola]
solactl projects session.new --project Sola [--name review] [--title 'fix login'] \
[--agent grok|codex|claude] [--prompt '…' | --prompt-file FILE] [--parent …] [--select]
solactl projects session.set --session review [--name …] [--title 'fix login']
solactl projects session.exec --session review [--agent grok|codex|claude] \
[--prompt '…' | --prompt-file FILE]
solactl projects session.select --session review
solactl projects session.end --session review # end it, for every computer
solactl projects pane.list [--session review]
solactl projects pane.send --text 'follow up' --enter [--pane review]
solactl projects pane.read [--pane review] [--lines 40]
solactl projects pane.wait [--pane review] [--status done] [--timeout 300] [--fresh]
solactl projects whoami # from a Projects pane; or --pane / --path
| Short | Long | Methods |
|---|---|---|
-p | --project | project.close, session.list, session.new |
-p | --path | project.open |
-n | --name | session.new, session.set |
-a | --agent | session.new, session.exec |
-s | --select | session.new |
-s | --session | session.end, session.select, session.set, session.exec, pane.list |
-t | --text | pane.send |
-e | --enter | pane.send |
-l | --lines | pane.read |
-s | --status | pane.wait |
-f | --fresh | pane.wait |
Required arguments: --path on project.open; project.close --project; session.new --project; --session on session.end, session.select,
session.set, session.exec; pane.send --text. --session on pane.list and
--pane on pane.send, pane.read, and pane.wait default to the
focused session. session.list without --project lists every session open
here.
Deadlines: project.open 15 s; session.new and session.exec 60 s;
pane.wait 302 s, or its --timeout plus 2 s; the rest 8 s.
Errors name what went wrong: <path> is not a folder of ~/Projects for
project.open, a session '<name>' is already open in this project, and
no session selected when a default has nothing to fall back on.
11.8.1Open§
project.open opens a project's sessions here, each in its own window:
every session running on its computer in that folder, whichever computer
started it (one already open here comes forward). With none running
anywhere, it starts the project's main session. --session <id> (its id
there, as ws-main) opens that session alone, as a session's row in a
Projects window does. A folder that is not a project here yet is added
first. Its reply is the project with opened (none of its sessions was
open here).
A session says what it is to any computer that lists it: tmux options on
it (@sola-project, @sola-name, @sola-title, @sola-kind). One made
before those existed is placed by the folder its pane is in.
project.close closes the project's windows here and replies
{"project", "closed": true}; its sessions keep running.
11.8.2Start§
session.new starts a session in the project's folder, on the project's
computer, in a window of its own. --name is its name, unique among the
project's sessions open here (default: the next number, 2, 3, …);
--title is its label (default: the name). The reply is the new session:
id, name, title, label, path, kind, parent, project, and
selected.
It runs in the background: its window lands behind, and the shown windows
stay. --select brings it up (as ⌘T does). When you spawn from a Projects
pane, the parent defaults to $SOLA_PANE_ID; --parent takes a session,
pane, or path.
--agent is grok, codex, or claude. Other names are an error
(OpenCode is presence-only). session.new with --prompt and no
--agent starts Grok. session.exec with no --agent sends to the agent
already running in the session (Grok starts when none is); naming another
agent than the one running is an error, not a launch line typed into it.
--prompt and --prompt-file are exclusive. The first Codex session may
need /hooks in its TUI to trust the Sola status hook. Claude hooks are
merged into ~/.claude/settings.json and apply to the next Claude
session. On start, Projects finds its existing Codex and Claude hook
entries by script file name and rewrites them in place (status message
"Sola Projects status"). Its catalog, last status, and hook scripts live
in ~/.config/sola/projects/.
session.set sets a session's --name and --title; its id and its tmux
stay. Target a session by id, name, path, or label. session.select brings
its window up and replies {"id", "selected": true}.
11.8.3Sessions on another computer§
project.open --host <computer> --path <folder> opens a folder on another
computer (an ssh host: a name, user@name, or a ~/.ssh/config alias).
--path is the folder's full path there. solactl resolves --path on
this computer, so ~ becomes this computer's home and a relative path is
taken from your working directory here; write the full path. A Projects
window does this from On another computer….
Its sessions run there, in that computer's own Projects tmux (socket
sola-ws, the one its own Projects uses), reached over ssh: a Projects
restart, or losing the network, leaves them running, and the windows
attach them again. Here such a session's id is <id>@<computer>.
Status lives on the computer a session runs on: its agents' hooks append
their events to that computer's hook log
(~/.local/state/sola/projects/hooks.log), and every Projects watching any
of its sessions follows that log — this computer's by reading it, another
computer's over a connection of its own (the link, ssh … tail -F). So
marks, islands, desk cards and pane.wait work on every computer that has
the session open, whichever one started it; looking at or dismissing an
island on one lets it go on all. The first link installs the same agent
hooks there (Claude's settings.json, Codex's hooks.json, Grok's
sola-status.json; scripts in ~/.config/sola/remote/). pane.send, pane.read and session.exec reach
its panes; a lost session whose Grok or Claude transcript is still there
resumes.
Sessions opened from another computer before 2026-10-30 lived on a socket
of their own there (sola-ws-remote); they are still found and attached
until they end, and nothing new is made there. A session's ssh may ask
for a password in its window. The link and the listing never ask: they
need a key or an agent that logs you in, and tmux and base64 there.
- The connection drops: the window says Can't reach the computer, with ssh's reason, and Reconnect. The session there is left alone, and the link is tried again every 20 seconds; windows come back with it.
- Agents running there cannot drive this Projects (
solactl projectsthere talks to that computer's own Sola).
11.8.4Lists and sessions§
session.list replies {"sessions": […]}; ps replies {"projects", "selected"} with each project's sessions under projects[].sessions[]. A
session has id, name, title, label, path, kind, parent,
status, agent, and project, plus host on another computer; in
ps and the session.set reply it also says selected. A project has id,
name, root, and host on another computer. project.list replies
{"projects": […]}, those with a session open here, each with open.
whoami replies pane, session (id), session_name, project,
project_name, path, kind, status, agent, and agent_session.
session.exec replies {"session", "pane", "started", "sent"}: started when it
launched the agent, sent when it pasted the prompt into one already
running. kind is main (the project's main session), folder (the main
session of a non-git project), or root (any other).
pane.list and whoami include agent_session, the pane's Grok or
Claude session id, when it is known.
11.8.5After a reboot§
Sessions come back by themselves. Each has a record on the computer it
runs on (~/.config/sola/projects/sessions/<id>.json: its project, name,
title, folder, and the agent in it with that agent's session). Any session
recorded there whose tmux is gone is made again, in its folder, with its
agent resumed (claude --resume, grok -r) when its transcript is still
there, else a shell: when Projects starts, when any computer's Projects
connects to that computer, and at boot (sola-projects --restore). A
computer connecting to one with that boot restore has it run there, and
waits for it, so what comes back has that computer's display; one without
it is restored over ssh. Ending a session (End Session…, session.end),
or its shell exiting, removes its record.
11.8.6Panes§
pane.send and session.exec --prompt paste into the live agent (tmux
bracketed paste), so a multiline brief does not submit on the first
newline and a long prompt is not truncated. session.exec then presses
Enter; pane.send does only with --enter. A failed paste returns the
tmux error, not a bare send failed.
pane.send replies {"ok": true, "pane"}. pane.read replies
{"text", "pane"} with the whole scrollback, or the last --lines N
lines.
pane.wait holds until the session's status is --status (working,
waiting, done, or idle; default done), for up to --timeout
seconds (default 300, at most 3600). --fresh waits for a transition
onto that status. It replies {"pane", "status"}, or fails with
timeout waiting for pane <pane> to be <status>.
11.8.7End§
session.end ends a session: its tmux is killed where it runs, for every
computer watching it, and its window here closes. It replies
{"ok": true} first, then ends it on the next tick, so a call from
inside that session can finish instead of hanging; the kill runs off the
UI thread. Its folder and any worktree stay.
11.9Browser (solactl browser)§
Needs the Browser (sola-browser) running (owner browser). Fails if
the Browser or sola-call is down; it does not launch a window. Each tab
is a page with its own window, a row in the shell's bar, the same
pages the human uses.
solactl browser # list methods
solactl browser tabs
solactl browser tab.open --url https://example.com [--select]
solactl browser tab.focus --tab 3
solactl browser tab.close [--tab 3]
solactl browser goto --url https://example.com/path [--tab 3] [--select]
solactl browser back|forward|stop [--tab 3]
solactl browser reload [--tab 3] [--hard]
solactl browser find.page --text needle [--tab 3] [--back]
solactl browser snapshot [--tab 3] [--interactive] [--ref e8] [--json]
solactl browser find --text Submit [--tab 3]
solactl browser click --ref e12
solactl browser click --tab 3 --text "Pay now"
solactl browser click --tab 3 --x 120 --y 80
solactl browser hover --ref e12 | --text NAME | --x 120 --y 80 [--tab 3]
solactl browser type --ref e5 --text more [--submit] [--tab 3]
solactl browser fill --ref e5 --text user@example.com
solactl browser fill --tab 3 --control lyrics --file /path/lyrics.txt
solactl browser get --tab 3 --control lyrics
solactl browser links [--tab 3]
solactl browser key --tab 3 --chord Control+Enter
solactl browser scroll --tab 3 --dy 400 [--dx 0] [--x 100] [--y 100]
solactl browser select --ref e9 --values OptionA,OptionB
solactl browser wait [--tab 3] [--load] [--text done] [--control Create] [--timeout 30]
solactl browser screenshot [--tab 3] [-o PATH]
| Short | Long | Methods |
|---|---|---|
-u | --url | tab.open, goto |
-s | --select | tab.open, goto |
-t | --tab | every method that takes a tab |
-c | --chord | key |
-q | --text | find.page, find, wait |
-x | --text | type, fill, get |
-b | --back | find.page |
-i | --interactive | snapshot |
-j | --json | snapshot |
-r | --ref | click, hover, type, fill, get, select |
-e | --submit | type |
-f | --file | fill |
-v | --values | select |
-l | --load | wait |
-o | --path | screenshot |
reload --hard has no usable short flag: solactl reads -h as help.
Required arguments: --url on tab.open and goto; tab.focus --tab;
--text on find.page and find; key --chord; type --ref --text;
select --ref --values. click and hover need --ref, --text, or
both --x and --y (--text with --x/--y is refused); fill needs
--ref or --control, and --text or --file; get needs --ref or
--control; scroll needs a non-zero --dx or --dy.
--tab is an id, or a substring of one tab's URL or title (any case).
More than one match fails with <n> tabs match …; pass an id. Without
it, page methods use the focused tab, except click, hover, type,
fill, get, and select, which use the tab of the last page call.
tab.open opens the page in its own window behind the shown one in the
focused zone, and does not focus it unless you pass --select;
goto does not focus either. tab.focus and --select show the page's
window and focus it. Closing the last tab opens a blank page
(about:blank) in its place, so one page is always open.
tabs replies {"tabs", "active"}; each tab has id, url, title,
active, loading, and profile. tab.open replies
{"id", "selected"}, tab.focus {"id"}, and tab.close
{"ok": true}.
Deadlines: snapshot, goto, click, hover, key, scroll, type,
fill, get, links, select, and screenshot 15 s; wait 32 s, or
its --timeout plus 2 s; the rest 8 s.
11.9.1Background tabs§
Snapshot, click, fill, type, and browser screenshot target that tab’s
CEF document. They work when the tab is in the background and do not
raise it.
11.9.2Snapshots and refs§
Page verbs use a pruned accessibility snapshot in YAML, with opaque refs
(e12, or f1e3 inside a frame; a leading @ is ignored). snapshot
replies {"tab", "snapshot"}: the text opens with url: and title:
lines (and focused: or dialog: open when they apply), then an
actions: list (role, name, ref); click those. --interactive keeps
controls only, --ref returns one subtree of the last snapshot, and
--json returns debug JSON instead of YAML. click --text "Pay now"
uses the last snapshot and fails with the candidate refs when more than
one control matches. A stale ref fails (the node is gone); take a new
snapshot.
find searches the last snapshot, not the page, and replies
{"tab", "hits"}; with no snapshot of that tab it fails and says to take
one.
find.page is ⌘F (--back searches backwards).
11.9.3Input§
click and hover also take CSS-pixel --x --y in that tab’s
viewport. That needs no ref, and it is still CEF Input.dispatch*, not the
compositor seat.
fill and type write a contenteditable or ProseMirror editor with one
paste of the whole string (newlines kept); they do not assign
textContent. type appends; fill replaces, and takes the control by
--ref or by its actions-list name with --control (from the DOM
aria-label when the last snapshot has no such name). fill --file PATH
reads the string from disk (exclusive with --text; at most 100,000
bytes). The reply includes value and matched: true, or an error if the
field did not keep the text. type --submit presses Enter after.
key --chord (for example Return, Escape, Control+Enter) goes to
that tab, not the seat, and inserts once: one char event, not
keyDown-with-text plus char. A chord is MODIFIER+…+KEY in any case.
Modifiers: Meta (or Super, Win, Cmd, Command), Alt (or
Option), Ctrl (or Control), Shift. Keys: Enter or Return,
Escape or Esc, Tab, Backspace or BS, Space, Delete or
Del, ArrowUp/Up, ArrowDown/Down, ArrowLeft/Left,
ArrowRight/Right, Home, End, PageUp or PgUp, PageDown or
PgDn, and a single letter or digit. With Ctrl, Meta, or Alt held,
no character is typed. scroll --dx --dy (whole CSS pixels) go to the
same tab, from a wheel origin at --x --y (default 100, 100).
select --values takes comma-separated option labels or values.
11.9.4Reading the page§
get reads the current text, value, or href of a ref or a --control
name (from the DOM aria-label if the accessibility tree misses it);
--text is the same as --control. links lists <a href> targets and
audio/video duration from the DOM.
Screenshot is a fallback when the tree is empty (canvas, maps). It
captures that tab’s document even when the tab is in the background,
writes --path (default ~/.cache/sola/browser/shot-<ms>.png, under
$XDG_CACHE_HOME when set), and replies with path.
11.9.5Waiting§
wait and wait --load return when document.readyState is complete
on a committed URL (not when the loading spinner stops). wait --text
snapshots until that string appears. wait --control Create snapshots
until that named control is in the actions list, or times out naming the
missing control; it does not succeed just because CEF fired load.
--timeout defaults to 30 s.
11.9.6Troubleshooting§
If tabs or tab.open time out while the Browser's windows are still
up, the Browser is not answering its calls. Quit it (Browser → Quit
Browser, ⌘Q) and launch it again. Do not kill the process from a bot.
Vault fill and confirm gates are not on this plane. Which methods need a human confirm is an open question (D3).
11.10Design (solactl design)§
Needs Design (sola-design) running (owner design). Fails if Design
or sola-call is down; it does not launch a window.
solactl design # list methods
solactl design project.list # projects + canvases
solactl design project.add --path ~/Workspace/Thoxa
solactl design project.remove --path ~/Workspace/Thoxa # forget it: canvases leave the rail and close; files stay
solactl design canvas.new onboarding # design/onboarding/ in your cwd's project
solactl design canvas.new "Settings v2" --path ~/Workspace/Thoxa --raise
solactl design open design/onboarding # relative to your cwd
solactl design open design/onboarding/canvas.toml --raise
solactl design canvas.close --canvas design/onboarding # done with it: its boards' memory comes back; files stay
solactl design status [--canvas design/home] # canvas (shown, share), artboards (src, url, error, discarded, exits, board_links), annotations (sections, links, notes, flows), linked files, zoom, selection, memory
solactl design share [--canvas design/home] # the canvas's live link
solactl design share.rotate [--canvas …] # a new link; the old one stops working
solactl design selection
solactl design view --fit
solactl design view --artboard phone [--zoom 1]
solactl design view --section sign-in # frame a [[section]]
solactl design view --zoom 0.5
solactl design wait [--artboard phone] [--timeout 15]
solactl design reload [--artboard phone] [--timeout 15]
solactl design screenshot [--artboard phone] [-o shot.png]
solactl design pdf [--artboard landing] [-o landing.pdf]
solactl design inspect [--artboard phone] [--selector 'main .card']
solactl design inspect --artboard light --selector section --all # every match's box
| Short | Long | Methods |
|---|---|---|
-p | --path | project.add, project.remove, open, canvas.new |
-n | --name | canvas.new |
-r | --raise | open, canvas.new |
-c | --canvas | canvas.close and every page call |
-a | --artboard | view, wait, reload, screenshot, pdf, inspect |
-f | --fit | view |
-s | --section, --selector | view, inspect |
-z | --zoom | view |
-o | --out | screenshot, pdf |
-A | --all | inspect |
Required arguments: --path on project.add, project.remove, and
open; canvas.new --name; canvas.close --canvas. Deadlines: wait
and reload 32 s, or their --timeout (default 15) plus 2 s;
screenshot, pdf, and inspect 15 s; the rest 8 s.
share and share.rotate reply {"url", "tunnel"}: the link, and
whether a tunnel token is set (Design § Share).
With sharing off they fail with sharing is off (see the Design log).
11.10.1Canvases are files§
A canvas is files, not app state: <project>/design/<canvas>/canvas.toml
lists the artboards, and each artboard is an HTML page beside it. Edit
those files with ordinary tools. Design watches them (and any outside file
a page loaded; status lists those as linked) and reloads.
After an edit, run wait or reload before screenshot or inspect.
Both make the boards show the files on disk (wait reloads when the files
are newer than the boards) and return once pages have loaded, web fonts are
in, and auto heights are measured.
11.10.2Opening a canvas§
canvas.new NAME makes <project>/design/<slug>/canvas.toml (project =
--path, default your current directory), registers the project if
needed, and opens the canvas. It appears under the project in the rail. An
existing canvas of that name is opened instead (created: false).
open takes the canvas directory, its canvas.toml, or any file inside
it, and registers the project if Design does not know it yet.
open and canvas.new make a canvas live off the human's screen; it
shows in their rail. --raise puts it on their screen.
11.10.3Targeting a canvas and an artboard§
Every page call (status, share, share.rotate, view, wait,
reload, screenshot, pdf, inspect) takes --canvas: the canvas
folder, its canvas.toml, or a file in it, opened in the background if
need be. Without it, the call goes to the canvas on the human's screen.
Over MCP, a page call without canvas goes to the canvas that agent last
opened or made, so agents working at the same time stay on their own.
--artboard defaults to the artboard holding the human's selection, else
the only artboard. inspect without --selector inspects the selected
element. selection is what the human Super+clicked (null when nothing).
11.10.4Annotations§
Annotations are files too: [[section]], [[note]], [[link]], and
[[flow]] in canvas.toml, and data-design-to="<artboard id>" exits in
the pages (see Design § Files). Design draws
them over the canvas, so they are not in screenshots.
status lists them under annotations; each artboard's exits (with an
error when one leads nowhere) and board_links (artboards its plain
links lead to); and a note's placed or error once its artboard has
settled. view --section frames a section.
11.10.5Memory§
Design holds its boards within the machine's free memory; it keeps 15%
free for everything else. When memory runs short, it discards idle
boards: their page closes, their picture stays. It reopens them on the next
call that needs one (screenshot, pdf, and inspect wait for the
reload; wait and reload reopen the boards they cover).
status marks those boards discarded and reports memory: free_mib,
total_mib, keep_free_mib, boards_mib (Design's estimate for its open
boards), and board counts.
When the boards in use leave no room, open, a call on a canvas that is
not open, or a call that reopens boards fails with the reason: the
numbers, each open canvas (boards, memory, why it is kept), and the way
out. Close a canvas you are done with: canvas.close --canvas <dir>
(--canvas is required; the canvas on the human's screen stays open). The
next call on it opens it again.
11.10.6Screenshots, PDFs, and inspect§
screenshot captures the whole page (CDP, at the artboard's raster scale,
normally 2×) and replies with path. The result is the same however the
human is zoomed or panned. screenshot reports the scale it used: each
artboard renders at 2×, else 1×, else under 1× for a page over 8192 px.
pdf writes a one-page PDF of the artboard as it looks on screen (screen
media, backgrounds, no margins). Without --out, both write to
$XDG_RUNTIME_DIR/sola/design/shots/<canvas>-<artboard>-<ms>.<ext>.
wait and reload also wait for a height = "auto" artboard to be
measured.
inspect replies with the viewport (the artboard size), the document size,
cut_off_below (content taller than the artboard), the element's box and
computed styles, and up to 25 issues (overflows-viewport, clipped-x,
clipped-y). --all instead lists every match (up to 300) as
{selector, tag, rect, text}, with no styles; its default selector is
body > *.
In status, each artboard has src (what canvas.toml asks for), url
(what its tab shows), error (missing src, HTTP ≥ 400), and detail
(the part re-rendered at screen scale while the human is zoomed in, else
null).
view moves the human's camera only until they move it themselves on that
canvas; after that it replies moved: false and leaves it. On a canvas
that is not on screen, it frames the canvas for when the human opens it.
Boards are served over http from the project root (/ = the project
folder; a canvas root is looked in first), not file://. There is no raw
eval on this plane.
11.11Vault (solactl vault)§
Owner vault, process sola-vaultd, the vault service Oath runs as
svc:sola-vault (Vault). It fails with vault is not running while the service is down. No method returns an item or a value:
an agent can see the vault's state, lock it, sync it, and open Vault, and
nothing more.
solactl vault # list methods
solactl vault status
solactl vault lock
solactl vault sync
solactl vault open [-i ID]
| Method | Does | Reply |
|---|---|---|
status | The vault's state | {"signed_in", "unlocked", "email", "server", "synced_at", "syncing", "sync_error", "items"}; server is bitwarden.com, bitwarden.eu, or the self-hosted host |
lock | Locks the vault now | The status |
sync | Syncs with Bitwarden; fails when the vault is locked or signed out, or the sync fails. Deadline 120 s | The status |
open | Starts Vault, or brings it forward, on item -i, --id (a UUID), or on its list without one | {"opened": ID} (null without one); id: not an item's id for anything that is not a UUID |
11.12MCP (solactl mcp)§
An MCP server on stdio that exposes the call plane to agents that speak MCP. How it maps methods to tools is in The call plane § MCP.
solactl mcp # every live owner
solactl mcp --owner design # only Design (repeat --owner for more; owner or app id)
claude mcp add sola -- solactl mcp
- Every method a running app advertises is a tool named
<owner>_<method>(dots become_):design_screenshot,browser_tab_open,projects_pane_send, … The list follows the apps that are up: start Design and its tools appear (the server checks every 2 s and sendstools/list_changed). - Each tool takes the same arguments as the
solactl <owner> <method>flags, by their names, with JSON types (integer,number,boolean,string,object); values are passed as sent. Path arguments may be relative to the agent's working directory; that includes the compositor's screenshotoutput, whichsolactl compositor screenshot -osends as given. - A reply with a
pathto a PNG or JPEG (screenshots) also returns the image inline, up to 3,700,000 bytes; larger ones return the path and a note. - The deadline, the Projects and Design context, and the Browser's bot
rules are the same as for
solactl. A Design page call withoutcanvasgoes to the canvas this server last opened or made. - The compositor's bot refusals are part of
solactl compositor, not of the methods, so withSOLA_BOT=1the compositor tools still work.compositor_input_keywantschordas an object (see Methods with no compiled flags). - The server exits when its input closes.
It is an adapter over the same methods, not a second protocol: what a tool
may do is what solactl may do. Which methods need a human confirm is
still open
(D3).
11.13Other commands§
These do not go through sola-call. Screenshot and synthetic input are
calls (solactl compositor), not bus topics.
11.13.1logs§
solactl logs [APP] [-f]
Prints <APP>.log from Sola's log directory and exits: on Oath
~/.local/state/sola/ ($XDG_STATE_HOME/sola/ when that is set and
absolute), elsewhere /opt/sola/log/; SOLA_LOG_DIR overrides both.
APP defaults to sola, the log every Sola process writes to.
app-<app_id> is the output of an application the session launched
(app-sola-browser). river is River's own output where the process
manager sola starts River; on Oath, PID 1 runs River and Sola writes no
river.log. -f, --follow keeps printing what is appended,
checked every 200 ms, until interrupted; a file that shrinks is read again
from the start. A missing file fails with cannot open <path> (status 3).
11.13.2emit§
solactl emit KIND PAYLOAD
solactl emit OpenUrl '{"url":"https://example.com","activate":true}'
Emits one bus topic (The bus) as solactl. KIND is the
topic's name as Monitor shows it (Theme, OpenUrl, LaunchApp, …),
spelt exactly; PAYLOAD is its JSON in the same shape, so a topic seen in
Monitor can be emitted again by copying it. A topic that carries no
payload still needs valid JSON ({}); its content is ignored. On success
it prints emitted KIND (status 0). An unknown kind prints the list of
kinds; invalid JSON, a payload that does not match the topic, or no bus
connection fails with status 3. This is a developer escape hatch: any
topic can be faked.
11.13.3open§
solactl open URL|PATH
Opens a URL or file in the Browser, an image in Paint, or an iCalendar file in Calendar. It does not go through MIME.
- A path or
file://URL whose extension is a raster image (png,jpg,jpeg,jpe,gif,webp,bmp,tif,tiff,tga; any case) startssola-paintwith it ($SOLA_PAINT, elsesola-paintin Sola's bin directory:/binon Oath,/opt/sola/binelsewhere). - A path or
file://URL ending in.ics,.ical, or.icalendar(any case) startssola-calendarfrom Sola's bin directory with it. A running Calendar keeps its window: the new process hands the path over ($XDG_RUNTIME_DIR/sola/calendar.sock) and exits. Calendar opens the event as a new draft. - Anything else goes to the Browser, web URLs to images or
.icsfiles included. An absolute path becomes afile://URL; a relative path does too when the file exists or it starts with./or../. A running Browser gets the URL through itschrome.sock; otherwisesola-browseris started with it ($SOLA_BROWSER, elsesola-browserin Sola's bin directory).
open returns once the handoff or start is done (status 0); a missing
binary or a failed start is status 3, with
open failed: <program> not found at <path> or the spawn error. There is
no fallback to another browser or image viewer.
11.13.4background§
solactl background set IMAGE
solactl background clear
set copies IMAGE into ~/.config/sola/background/ (with the pictures
set before it, which Settings → Appearance lists; a different file of the
same name is kept as name-2.ext; the same file again uses its copy), makes it the picture behind
the windows (Topic::Background, which survives a restart), and prints the
copy's path. The original can then move or go. sola-river draws it
filling the output, centre-cropped. clear goes back to the plain black
ground. A file that cannot be read, or no bus connection, is status 3; a
picture that will not decode is logged by sola-river and the ground stays
as it was. Paint's File → Set as Background does the same for the
image in front.
11.13.5media§
solactl media play-pause|next|prev|mute|vol-up|vol-down
The Shell runs this for the media keys; it works from a terminal too.
| Action | Does |
|---|---|
play-pause, next, prev | Acts on an MPRIS player on the session D-Bus: one that is playing, else the first that can be controlled. next and prev do nothing when the player says it cannot |
mute | Toggles mute on the default audio sink (wpctl) |
vol-up | Raises the default sink by 5%, capped at 100% |
vol-down | Lowers the default sink by 5% |
With no player, a transport action prints no MPRIS player available and
exits 0. A D-Bus or wpctl failure is status 3.
12Applications§
Sola's own applications are Iced programs on sola-kit. Each is its own
process, follows the theme, and is started by the
session manager: from the launcher (Super+Space), from the
shell, or with solactl desk launch <app_id>. On Oath their programs
are in /bin.
Each application window has a titlebar with its application's menus: the first under the application's name, then its others, then the Window menu, and the window's title at the right. Menus show shortcuts with ⌘ for the Super key; the shell takes those chords and hands them to the focused application. Several applications open a window per document, each a row in the shell's bar: Browser a window per page, Terminal a window per tab, Paint a window per image, and Projects and Design a window per project.
The shell is stable in daily use; the applications are early, and each is incomplete. Applications that offer methods on the call plane are driven with solactl.
| Application | Program | Package | What it is | Notes |
|---|---|---|---|---|
| Terminal | sola-terminal | pkg:sola-terminal | A terminal on Alacritty's grid: a window per tab, split panes, each running in tmux | — |
| Projects | sola-projects | pkg:sola-projects | The folders of ~/Projects, here or on another computer over ssh: a window per session (a terminal in tmux, on the computer it runs on), with a bar for its agent, model, turns, compactions and context. The launcher's Projects opens a window to pick a project in. Shows whether the coding agent in a session (Claude Code, Codex, Grok) is working, waiting, or done; its row in the bar marks one working or waiting on a decision, and one that asks or finishes gets an island in the top row. Call owner projects. | solactl |
| Browser | sola-browser | pkg:sola-browser | Chromium's engine (CEF 156) under Sola's own chrome: a window per page, a profile per desktop workspace, a panel that fills pages and signs in with passkeys from Vault, downloads, developer tools. Every Sola application opens web links in it. Call owner browser. | notes |
| Web apps | sola-browser --app <id> | pkg:sola-browser | A website that Browser runs as an application of its own, made from a Browser page: a launcher entry, its own windows, name, and menus | notes |
sola-mail | pkg:sola-mail | IMAP and SMTP for several accounts in one combined set of mailboxes, with rules, drafts, attachments, and safe HTML letters | notes | |
| Calendar | sola-calendar | pkg:sola-calendar | Month, week, and day views of local, Google, iCloud, CalDAV, and feed calendars, with reminders, .ics invites, and today's events in the top row's clock | notes |
| Vault | sola-vault, sola-vaultd | pkg:sola-vault | Sola's own Bitwarden client, for one account on bitwarden.com, bitwarden.eu, or your own server: every kind of item, folders, collections, the trash, the generator, export. Its service, sola-vaultd (svc:sola-vault), alone holds the unlocked vault; Browser's panel and Vault ▸ in the right-click menu ask it. Call owner vault. | notes |
| Editor | sola-editor, edit | pkg:sola-editor | A text editor with Emacs keys and syntax colors, a window per file; edit <file>… opens files in it from anywhere | notes |
| Paint | sola-paint | pkg:sola-paint | The default image viewer and editor, a window per image; sets the desktop's background picture | notes |
| Preview | sola-preview | pkg:sola-preview | A quick viewer for screenshots and images | notes |
| Design | sola-design | pkg:sola-design | HTML artboards on a canvas, written and edited by AI agents: a window per project, its canvases along the top. Call owner design. | notes |
| Settings | sola-settings | pkg:sola-settings | Applications in the launcher, and web apps (a name, where each starts, removing one); Appearance (light or dark for the desk and web pages, the theme's colors, from the background picture or your own, the background picture and every picture set before it, each with the look it was worn with, the desk's opacity, the idle fade and lock); Login (the password and auto-login, Security); Mail accounts and rules; Calendar accounts; Zones; Keybindings; Default Apps (which app opens each kind of link and file, Opening links and files). Also Settings… in the flower menu. | — |
| Bots | sola-bots, sola-botsd | pkg:sola-bots | Named assistant sessions, each a directory in ~/Bots on the AI it was made with (Grok, Claude, or Gab AI); the daemon sola-botsd serves them on a Unix socket. On Oath nothing starts the daemon, so the Bots window has nothing to talk to there. Call owner bots. | solactl |
| Oath | sola-oath | pkg:sola-oath | The packages on this machine and on the origin, for looking: where each stands against the release it runs (current, an update, a local build, not pushed, off, on the origin only, with the audit's advisories over any of them), what it needs and what uses it, and what the daily security audit says of it. Map shows them all at once: Strata by category and how far each stands from the ground, Weight by size on disk or in use. Tree follows one up and down. Check for a newer release asks oath update what the newest release would move, and changes nothing; the answer is kept until the next check. The app changes nothing on the machine. Listed only on Oath. | — |
| Monitor | sola-monitor | pkg:sola-monitor | A live view of bus traffic and of the call plane's methods and calls | notes |
| Scope | sola-scope | pkg:sola-scope | A pixel loupe: the screen under the pointer, magnified | notes |
| KVM | sola-kvm | pkg:sola | One keyboard and mouse across two machines, with a shared clipboard; once paired, each machine drops input it did not seal (Security). A service, not in the launcher. | notes |
There is no separate Wrapper application: a wrapped website is a web app that Browser hosts.
In Terminal and Projects, a program that asks for the mouse gets the left
button, as it already got the wheel. An agent such as Claude Code selects and
copies by itself, and it knows where it wrapped its own lines, so a long URL
dragged over in its pane copies as one line; Copy has nothing to do there. A
plain click on a web link still opens it, on any row of a link that wraps; a
link a program hung on other text opens only if it goes to http or https.
Hold Shift to select with the terminal's own selection instead. A program can
also copy to the clipboard with OSC 52, which is how it copies from another
computer over ssh; no program can read the clipboard that way.
The notes are kept in the repository beside this manual and are not part of the page: they describe each application's keys, menus, and files in detail, and change as often as the applications do.