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
  1. 1Introduction
    1. 1.1Requirements
    2. 1.2Installing
    3. 1.3Building from source
    4. 1.4Starting a session
    5. 1.5Updating and rolling back
    6. 1.6Fonts
    7. 1.7Where things are
    8. 1.8First-run problems
  2. 2Security
    1. 2.1Ways in
    2. 2.2Pairing the KVM
    3. 2.3The password
    4. 2.4Locking
    5. 2.5The console
    6. 2.6What stays open
  3. 3Architecture
    1. 3.1Processes
    2. 3.2The three planes
    3. 3.3Programs and files
    4. 3.4Start-up
    5. 3.5When a part stops
    6. 3.6How an application attaches
    7. 3.7Updates in place
  4. 4Process manager
    1. 4.1The services
    2. 4.2Start-up
    3. 4.3Restarts
    4. 4.4Updates
    5. 4.5Environment
    6. 4.6Ending the session
    7. 4.7Logs
    8. 4.8Files and variables
    9. 4.9Troubleshooting
    10. 4.10The sola program
  5. 5The bus
    1. 5.1Wire format
    2. 5.2Connecting and identifying
    3. 5.3Subscribing
    4. 5.4Publishing
    5. 5.5Sticky and persistent topics
    6. 5.6Topic reference
    7. 5.7Ordering and delivery
    8. 5.8When the bus restarts
    9. 5.9Using the bus from Rust
    10. 5.10Watching and poking the bus
    11. 5.11Limits
  6. 6The call plane
    1. 6.1Why it is separate from the bus
    2. 6.2The host
    3. 6.3Roles and the handshake
    4. 6.4Methods and arguments
    5. 6.5Keeping the catalog live
    6. 6.6Wire format
    7. 6.7Deadlines and failures
    8. 6.8Observers
    9. 6.9Owners today
    10. 6.10solactl on the call plane
    11. 6.11MCP (solactl mcp)
    12. 6.12Offering methods from an application
  7. 7Compositor
    1. 7.1The compositor
    2. 7.2sola-river
    3. 7.3Windows
    4. 7.4The background picture
    5. 7.5The idle fade
    6. 7.6Outputs
    7. 7.7Seat, keyboard, and pointer
    8. 7.8Layer shell
    9. 7.9Xwayland
    10. 7.10Bus topics
    11. 7.11Call methods
    12. 7.12Restarts
    13. 7.13Logs
    14. 7.14Limits
  8. 8Session manager
    1. 8.1Launching
    2. 8.2Application ids
    3. 8.3The application catalog
    4. 8.4Tracking
    5. 8.5Closing
    6. 8.6Applications that aren't Sola's
    7. 8.7Ending the session
    8. 8.8Restoring the last session
    9. 8.9One instance per application
    10. 8.10Opening links and files
    11. 8.11Bus topics and call methods
    12. 8.12Restarts
    13. 8.13Logs
    14. 8.14Limits
  9. 9Shell
    1. 9.1The desk
    2. 9.2Keyboard
    3. 9.3The top row
    4. 9.4Agent islands
    5. 9.5The bar
    6. 9.6Workspaces
    7. 9.7Zones
    8. 9.8Titlebars
    9. 9.9The right-click menu
    10. 9.10Launcher
    11. 9.11Hide
    12. 9.12Notifications
    13. 9.13Screenshots
    14. 9.14The idle fade
    15. 9.15Limits
  10. 10Theme and toolkit
    1. 10.1sola-kit
    2. 10.2The theme on the bus
    3. 10.3Palette tokens
    4. 10.4Fonts
    5. 10.5Shell tokens
    6. 10.6How an application applies the theme
    7. 10.7Appearance settings
    8. 10.8Colors from the background picture
    9. 10.9Changing the theme
    10. 10.10Following the theme in a new application
    11. 10.11Limits
  11. 11solactl
    1. 11.1Synopsis
    2. 11.2Output and exit status
    3. 11.3Environment
    4. 11.4Live-owner commands
    5. 11.5Compositor (solactl compositor)
    6. 11.6Desk (solactl desk)
    7. 11.7Bots (solactl bots)
    8. 11.8Projects (solactl projects)
    9. 11.9Browser (solactl browser)
    10. 11.10Design (solactl design)
    11. 11.11Vault (solactl vault)
    12. 11.12MCP (solactl mcp)
    13. 11.13Other commands
  12. 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 desk profile. 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, has sudo without a password, which cargo make oath apply uses.

    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
    CommandWhat it doesWhat it changes
    cargo make oath stageBuilds 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 versionThe checkout. If ~/.cache/sola/cef-<version> holds no engine, it becomes a link to pkg:cef's.
    cargo make oath applyStages, 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 thoseThe 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 publishFrom 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 nothingThis 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.

    PathWhat
    /bin/sola-*, /bin/solactlLinks to Sola's programs in the store (/oath/store/pkg/sola/<hash>/bin/, and the Browser and Design packages)
    /oath/store/pkg/sola/shareIcons 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>.logEach 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§

    SymptomCause
    No top row or bar after boot, on an Intel GPU or in QEMUOath'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 treecargo make oath publish was run with uncommitted changes.
    this machine's oath has no publishcargo 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 needThe release was built against a package the machine has at another hash. Build Sola on the machine.
    A new build does not runcargo 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 daemonThe desk profile runs no service for sola-botsd.
    No desktop and no console after Exit SolaOath 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 growingOnly 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 inBy defaultTo close it
    The network, through the KVMsola-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 keyNothing more to close; to open it, pair (Pairing the KVM)
    The network, over SSHOath's sshd takes keys only (no passwords, no root login)Nothing more to close
    The keyboard, at bootThe desk starts signed inA password, with Sign in automatically off (The password)
    The keyboard, while you are awayNothing locks the deskSuper+L, or Lock when the windows have faded (Locking)
    The keyboard, at the consoleThe console that Exit Sola or oath desk stop puts on the screen opens a shell as home without askingA 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:

    • home is root. sudo asks no password, so any program running as home can 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. solactl and 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 home can still get in by other means: through sudo, 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, /home included.
    • 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
    ProcessOwns
    rivuletOutputs, rendering, and input devices. Oath runs it, not Sola. See Compositor.
    sola-busThe bus socket, and the current value of every sticky and persistent topic. See The bus.
    sola-callThe call socket, and the registry of live owners and their methods. See The call plane.
    sola-riverThe 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-shellThe 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-sessionLaunching, tracking, and closing applications, and relaunching the previous session's applications at start. Call owner desk. See Session manager.
    sola-kvmSharing this machine's keyboard and mouse with another machine. See KVM.
    ApplicationsTheir 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-botsd keeps named agent sessions, each a directory in ~/Bots on Grok, Claude, or Gab AI. It is call owner bots and 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' on sola-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§

    PlaneSocketShapeCarries
    Bus$XDG_RUNTIME_DIR/sola-busOne sender, every subscriber, no replyWindow lists, focus, outputs, tabs and the bar's layout, workspaces, theme, application menus, launch and close requests, notifications, shutdown
    Call$XDG_RUNTIME_DIR/sola-callOne caller, one owner, one replysolactl commands, agent tools, screenshots, synthetic input, application control
    Wayland$XDG_RUNTIME_DIR/wayland-NClient and compositorBuffers, 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.

    PackagePrograms
    pkg:solasola-bus, sola-call, sola-river, sola-shell, sola-session, sola-lock, sola-kvm, solactl
    pkg:sola-oathOath
    pkg:sola-terminalTerminal
    pkg:sola-projectsProjects
    pkg:sola-paint, pkg:sola-preview, pkg:sola-scopePaint, Preview, Scope
    pkg:sola-monitorMonitor
    pkg:sola-calendar, pkg:sola-mailCalendar, Mail
    pkg:sola-settingsSettings
    pkg:sola-botsBots (sola-bots, sola-botsd)
    pkg:sola-browserBrowser, which also hosts the web apps
    pkg:sola-designDesign

    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.

    PathWhat
    $XDG_RUNTIME_DIR/wayland-NThe 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-busThe bus
    $XDG_RUNTIME_DIR/sola-callThe call plane (mode 0600)
    $XDG_RUNTIME_DIR/sola-botssola-botsd's HTTP API
    ~/.local/state/sola/projects/hooks.logProjects' 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.

    PathWhat
    ~/.config/sola/state.yamlPersistent 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.yamlThe bar's layout: the workspaces, which one shows, and each window's workspace, zone, and place
    ~/.config/sola/theme/current.yamlThe current theme
    ~/.config/sola/theme/presets/<name>.yamlSaved themes
    ~/.config/sola/paint.yamlPaint's open images
    ~/.config/sola/calendar/agenda.yamlCalendar'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.tomlsola-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.

    PathWhat
    ~/.local/state/sola/sola.logThe log of every Sola process, one line per event, tagged with the process
    ~/.local/state/sola/app-<app id>.logThe 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.

    1. PID 1 starts the compositor and the services sola-bus, sola-call, sola-river, sola-shell, sola-session, and sola-kvm.
    2. sola-bus binds its socket and loads the persistent topics from disk. sola-call binds its socket with an empty registry.
    3. sola-river waits up to 20 seconds for a live wayland-N socket 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.
    4. sola-shell starts like any kit application (below), maps the top row and the bar, and takes the saved layout when the bus replays it.
    5. sola-session tries 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.
    6. 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 stopsWhat happens
    sola-busSticky 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-callCalls in flight fail. Owners reconnect every 400 ms and advertise their methods again.
    sola-riverThe compositor and every window keep running. The new sola-river binds the window-management protocol again and reports outputs and windows.
    sola-shellThe 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-sessionThe 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 applicationsola-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 compositorEvery 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:

    1. Calls startup(app_id), which starts logging, finds the compositor's socket (the first wayland-N in the runtime directory that accepts a connection, looked for during up to 20 seconds; then WAYLAND_DISPLAY; then wayland-0), waits up to 10 seconds for that socket file, sets WAYLAND_DISPLAY, sets XDG_SESSION_TYPE=wayland unless it is already wayland or x11, sets GSETTINGS_BACKEND=memory if unset, loads the system fonts, and, unless SOLA_NO_SELF_WATCH is set, starts watching its own executable.
    2. 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. BusSetup blocks until the bus accepts the connection. .calls(owner, methods) on it also advertises call methods; CallSetup advertises call methods without the bus.
    3. 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's TabWindows, which first announces the tab on the bus (Tab, keyed <app_id>/<tab id>). The shell pairs the announcement with the new window, answers with TabWindow (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.

    ServiceRunsWantsRestart
    svc:seatdseatd -u home -g home, as root: opens the GPU and input devices for the sessionnonealways
    svc:rivuletrivulet -log-level info -c :, the compositorsvc:seatdalways
    svc:sola-bussola-bus, the busnonealways
    svc:sola-callsola-call, the call hostsvc:sola-busalways
    svc:sola-riversola-river, the window manager, a client of rivulet's River protocols (Compositor)svc:rivulet, svc:sola-bus, svc:sola-callalways
    svc:sola-shellsola-shell: the top row, the bar, the launcher (Shell)svc:sola-river, svc:pipewire-pulse, svc:wireplumberon-failure
    svc:sola-tmuxtmux -L sola -f /home/.config/sola/tmux.conf -D, the tmux server that holds Terminal's shellsnonealways
    svc:sola-tmux-wsThe same on socket sola-ws, for Projects' tabsnonealways
    svc:sola-sessionsola-session, the session managersvc:sola-bus, svc:sola-call, svc:sola-tmux, svc:sola-tmux-wson-failure
    svc:sola-kvmsola-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-riveralways

    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:

    ProcessWaits for
    sola-bus, sola-callNothing. Each deletes a leftover socket file and binds its socket, /run/user/1/sola-bus or /run/user/1/sola-call.
    sola-riverRivulet'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 applicationThe 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-sessionThe bus, retrying every second. It then collects saved state for 750 ms before it reopens the applications that were open.
    sola-kvmRivulet'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 service stopped.

    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:

    VariableValue
    XCURSOR_PATH/oath/store/pkg/sola/share/cursors
    XCURSOR_THEMEMcMojave
    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§

    ActionWhat happens
    Restart Computer or Shut Down in the flower menuThe 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 SSHThe same shutdown by PID 1.
    Exit Sola in the flower menuThe 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 nullAs 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§

    FileWritten byLimit
    ~/.local/state/sola/sola.logEvery Sola process, through tracing: plain text, UTC timesNone on Oath: only the sola program rotates it
    ~/.local/state/sola/app-<app id>.logStandard output and error of each application the session manager launchedEmptied at the next launch once it is larger than 8 MiB
    /oath/log/svc/<service>.logStandard 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§

    PathRole
    /bin/sola-*, /bin/solactlLinks 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-callThe bus and call sockets
    /run/user/1/wayland-NRivulet's socket
    ~/.config/sola/state.yamlThe bus's saved state, including SessionApps, the open-application set the session manager reopens
    ~/.config/sola/tmux.confThe configuration both tmux services load
    ~/.config/sola-kvm/config.tomlsola-kvm settings
    ~/.local/state/sola/Sola's logs
    /oath/log/svc/PID 1's record of each service's output
    VariableEffect
    RUST_LOGLog filter, per service
    SOLA_BIN_DIR, SOLA_SHARE_DIR, SOLA_LOG_DIRReplace /bin, /oath/store/pkg/sola/share, and ~/.local/state/sola
    XDG_STATE_HOMEWhen absolute, logs go to $XDG_STATE_HOME/sola
    XDG_RUNTIME_DIRWhere 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_HOMEWhere sola/ and sola-kvm/ live (~/.config when unset)
    SOLA_BUS_PATH, SOLA_CALL_PATHReplace the bus and call socket paths

    Sola takes Oath's paths when /oath/INDEX.md is a file.

    4.9Troubleshooting§

    SymptomCause
    oath get svc:<name> --actual shows failedIt 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 climbingIt 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-sessionExit 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 runIt 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 applyIt 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.

    ItemValue
    Processsola-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
    Framing4-byte little-endian length, then one postcard-encoded message
    Topics63 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 clientsola_bus::BusClient; kit apps use sola_kit::app
    Logsola.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).

    FieldTypeEncodingMeaning
    idUUIDv7varint length (16), then 16 bytesUnique per message; embeds a millisecond timestamp. The bus does not order by it.
    topicstringvarint length, then UTF-8A Topic variant name such as Windows or Theme (case-sensitive), or a control name
    payloadoptional bytes0, or 1 + varint length + bytesThe topic's payload, itself postcard-encoded; absent for topics without one (Shutdown, MouseLeft)
    stickyboolone byte1 stores the message as a record; 0 on a sticky topic is a retraction
    sourcestringvarint length, then UTF-8The publisher's app id, filled in by BusClient; provenance only
    keyslist of stringsvarint count, then each stringThe 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.

    TopicPayloadEffect
    $identifypostcard string: the app idNames this connection
    $subscribe-namespostcard list of strings: topic namesReplaces this connection's subscription set
    $subscribepostcard list of integers: topic numbersThe 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:

    1. Stores it as a record if its sticky flag is set, or deletes the record if it is a retraction.
    2. 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.
    3. 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§

    RetentionKept by the busOn diskReplayed to new subscribers
    EphemeralNoNoNo
    StickyLatest value of each record, in memoryNoYes
    PersistentLatest value of each record, in memoryYesYes

    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.

    TopicFileLayout
    SessionApps, MailConfig, CalendarConfig, Background, Appearance, Zones, TerminalConfig, MonitorConfigstate.yamlA top-level key named after the topic, holding the payload
    FloatGeometry, Application, TerminalSessionstate.yamlA top-level key holding a list, one entry per record
    Themetheme/current.yamlThe whole file
    TabLayoutshell/tabs.yamlThe whole file
    CalendarAgendacalendar/agenda.yamlThe whole file
    PaintSessionpaint.yamlThe whole file
    BrowserConfig, BrowserHistorybrowser.yaml, browser/history.yamlThe whole file
    BrowserTabbrowser/tabs/<id>.yamlOne 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§

    TopicPayloadPublished byActed on byKept
    WindowsEvery window: window_id, app_id, title, pid (when known)sola-riverShell, sola-session, Settings; kit apps, to learn their own window idsSticky
    OutputGeometryThe output's logical width and heightsola-river, when the compositor reports the output size and after a bus reconnectShell, BrowserSticky
    WindowGeometryOne window's window_id, x, y, width, heightsola-river, when the window's size or position changes; retracted when it closesShellSticky by window_id
    WindowParentA dialog's or picker's window_id and the parent window it belongs tosola-river, from the compositor's parent event; retracted when the window closes or loses its parentShell (no row in the bar; composed above its parent)Sticky by window_id
    WindowFloatingwindow_id, floatingShell; sola-river re-sends its floating windows after a bus reconnectsola-river (move, resize, shadow); kit apps (a float's frame resizes from its edges)Sticky by window_id
    WindowChromewindow_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 closessola-river (tiled state, the rim, rounded corners); kit apps (no frame in Cinema)Sticky by window_id
    CompositionWindow ids to show, bottom to topShellsola-riverEphemeral
    FrameOne window's window_id, x, y, width, height, and fullscreen flagShellsola-riverEphemeral
    FocusThe window_id to give the keyboardShellsola-riverEphemeral
    AppHiddenAn app_id left out of CompositionShell (Hide); retracted to show it againShellSticky by app_id
    CloseWindowA window_idShell (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 wouldEphemeral
    MouseEnteredThe window_id the pointer entered; still when the pointer did not move (the window under it changed)sola-riverShellEphemeral
    MouseLeftNonesola-riverShellEphemeral
    MouseClickedThe window_id clicked or touchedsola-riverShellEphemeral
    RegisteredChordsChords (keysym, modifiers) to route to the bus, not the focused windowShellsola-riverSticky
    ChordA registered chord was pressed: keysym, modifierssola-riverShell, Browser, DesignEphemeral
    ChordReleasedA registered chord was released: keysym, modifierssola-riverBrowser, Design (Super held, for Super+click)Ephemeral

    What sola-river does with these is in Compositor.

    5.6.2The bar, tabs, and workspaces§

    TopicPayloadPublished byActed on byKept
    TabA 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 closesShell (pairs a process's tabs with its new windows, in order)Sticky by key
    WindowTabThe 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-riverShell (pairs each window with the tab whose key has that token)Sticky by window_id; retracted when the window closes
    ReplayedThe bus has sent a subscriber every sticky it holds for what it asked forThe bus, to that client only, after each subscribeBrowser (its restored pages wait for Appearance or this; a web app page's window waits for its catalog entry or this)Ephemeral
    WorkspacePrefsHow 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). PersistentSettings (islands), the shell (skipped)The shell, Settings
    PackagesMovedA list of package ids (pkg:sola-mail, …) an apply movedcargo make oath apply (when pkg:sola did not move)Session manager (starts again each of Sola's apps on an old program)Ephemeral
    TabOpenerA new tab's key and the opener tab's key, sent just before its TabProjects (a session started by a session), through the kit's TabWindows::open_fromShell: the window lands on the opener's workspace, whichever one is showingEphemeral
    TabWindowA tab's key, its window_id, and shown (its window is on screen)Shell; sent again when shown changes, retracted when the window closesThe tab's app (a browser page that is not shown stops painting)Sticky by key
    TabActivityA 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 closesShell (the mark in the row's icon slot)Sticky by key
    ShowTabA tab's keyBrowser, 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 itEphemeral
    ShowWindowA window_idCalendar (an app bringing its own window forward)Shell: as for ShowTabEphemeral
    CloseTabA tab's keyShell (a bar row's stoplight)The app that opened the tab: closes its window as its own close button wouldEphemeral
    TabLayoutThe 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)ShellShell, at its next start: returning windows take their placesPersistent
    WorkspacesThe workspaces in order (id, name) and the one showingShellBrowser (a new page window opens under the showing workspace's profile)Sticky
    WindowWorkspacewindow_id, workspaceShell, 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 pinnedBrowser (a page whose workspace has another profile reopens under it; a pinned page, with no workspace, keeps its profile)Sticky by window_id
    PinnedTabsRetired with Common; nothing sends it, and it stays because a topic's place in the list is its number on the wire
    FocusPinnedpinned: the focused window is on every workspaceShellThe kit, in every app (the check on the Pin item in a titlebar's Window menu)Sticky
    FloatGeometryA floating app's last rectangle: app_id, x, y, width, heightShellShellPersistent by app_id

    The bar is described in Shell § The bar, its zones in Shell § Zones.

    5.6.3Apps, session, and menus§

    TopicPayloadPublished byActed on byKept
    LaunchAppapp_id, commandShell (launcher), Browser (--app when no browser is running; Open in Vault when vault.open does not answer), sola-vaultd (vault.open)sola-sessionEphemeral
    LaunchResultapp_id, command, ok, errorsola-sessionShellEphemeral
    UserAppExitedapp_id, command, exit code or signalsola-sessionShellEphemeral
    CloseAppAn app_idShell (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
    SessionAppsOpen apps: app_id, commandsola-sessionsola-session, at its next startPersistent
    ApplicationA launcher entry: app_id, label, command, icon, kind, url, profileSettings; Browser (web apps)Shell, Settings, BrowserPersistent by app_id
    SetAppMenuapp_id and its menus: labels and items (action id, label, shortcut, disabled, checked) or dividersEach app with a menu, at startShell (registers the focused app's Super shortcuts; an app with no menu gets the compositor's rim)Sticky by app_id
    MenuActionapp_id, action_id of a chosen item or shortcutShell (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 oneThe app named by app_id; the Shell for Window-menu actions, which it applies to the focused windowEphemeral
    ShutdownNoneShell (Exit Sola, then the shell exits)sola-session (stops the apps it started, then exits), sola-river (exits)Ephemeral
    DeskMenuOpenA 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
    DeskMenuPickapp_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 DismissedShellThe process pid names: the field or pane that opened the menuEphemeral

    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§

    TopicPayloadPublished byActed on byKept
    ThemeThe 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
    Backgroundpath (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 groundsola-river (draws it behind the windows), Shell (the theme's colors), SettingsPersistent
    Appearanceadapt_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
    ThemeVariationspath (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)ShellSettings (the scheme cards)Sticky
    IdleKeepx, y, w, h: output-local logical pixels, the top row's flower and agent islandsShellsola-river (that part stays at the desk's rest during the fade)Sticky
    IdleFadefading (no input for the fade's delay), out (the fade is full: the windows are out of sight)sola-riverProjects (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
    WindowVideoswindow_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§

    TopicPayloadPublished byActed on byKept
    MailConfigMail accounts, identities, filter rulesSettingsMail, SettingsPersistent
    MailStatusinbox_unreadMail; retracted when Mail quitsShell (the unread count in the top row)Sticky
    CalendarConfigCalendar accounts and the calendar listSettings, CalendarCalendar, SettingsPersistent
    CalendarAgendadays, 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)CalendarShell (the clock's panel, also while Calendar is closed)Persistent
    VaultStatesigned_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 valuessola-vaultd, on every change (Vault)Shell (the flower menu has Lock Vault while unlocked)Sticky
    ScreenLocklockedsola-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
    TerminalSessionOne Terminal tab: id, tmux session, working directory, order, pane layoutTerminalTerminalPersistent by id
    PaintSessionThe open images' paths and the selected onePaintPaintPersistent
    Zones, TerminalConfig, MonitorConfig, BrowserConfig, BrowserHistory, BrowserTabDeclared onlyNothingNothingPersistent (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§

    TopicPayloadPublished byActed on byKept
    OpenUrlurl, activateNothing in Sola; solactl emit canBrowser (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
    OpenImagepath, activate, optional app_id (absent means Paint)A second Paint launch, handing its paths to the running onePaint, Preview (each only when addressed)Ephemeral
    AppToasttextBrowser (after Use as Desktop Background); solactl emit canShell (the top row, 5 seconds)Ephemeral
    AppNotificationid, app_id, source, title, body, optional tag, tab_id, urlBrowser (pages and web apps), Calendar (reminders), ProjectsShell (a card on the desk)Ephemeral
    NotificationActivateThe clicked notification's id, app_id, tab_id, urlShellThe app named by app_id: Browser (and its web apps), Calendar, ProjectsEphemeral
    AgentIslandA session's key (its pane), name, project, state (Waiting or Done), title, sinceProjects; retracted when the session works again, closes, is looked at, or is dismissedShell (its island in the top row)Sticky by key
    AgentIslandActionAn island's key and what was done: Show, DismissShellProjects (brings up its session's window, or lets it go)Ephemeral
    ClientConnectedAn app idsola-bus on $identify; replayed as the rosterNo app; Monitor shows itEphemeral
    ClientDisconnectedAn app idsola-bus when an identified connection closesNo app; Monitor shows itEphemeral

    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.
    • BusClient reads into an unbounded in-memory channel on its own thread; emit writes 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:

    ProgramReconnects
    Kit appsEvery 250 ms from the kit's bus poller, then re-sends the subscription given to BusSetup
    sola-riverEvery 20 ms, then re-subscribes and re-publishes OutputGeometry, Windows, each WindowGeometry, and its floating windows
    sola-sessionBlocks, 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, publishes SetAppMenu (and keeps it for the window's titlebar), and stores the client process-wide.
    • sola_kit::app::bus_subscription() is an iced Subscription of Arc<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 or try_recv, not both.
    • sola_kit::app::bus() returns the client behind a mutex, for emit and retract; it panics before install (try_bus() returns an Option). set_bus_kinds(kinds) changes the set re-sent after a reconnect.
    • sola_kit::apply_theme_update applies a Theme message (Theme and toolkit); sola_kit::is_self_quit recognises a MenuAction with action quit for the app, or a CloseApp naming 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:

    MethodDoes
    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-bus sources 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.yaml that 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_PATH if set, else $XDG_RUNTIME_DIR/sola-call. When XDG_RUNTIME_DIR is 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 mode 0600. 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-call the 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.

    RoleHello carriesThen sendsReceives
    providerapp_id, owneradvertise at once; later reply framesinvoke frames
    callerapp_idinvoke and list, any numberreply and catalog
    observerapp_idlist (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 fieldMeaning
    nameMethod name within the owner: windows, tab.open, input.click
    summaryOne line, shown by solactl <owner> and in MCP tool descriptions
    argsArgument specs (below)
    timeout_msSuggested 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 fieldMeaning
    nameThe key in the request's params object
    longThe --flag that solactl accepts
    shortAn optional one-letter -f
    typestring, int, float, bool, path, or json
    requiredMarks the argument required in help and MCP schemas, and lets solactl fill it from a bare word
    helpShown 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:

    typeFieldsSent by
    hellorole, app_id, owner (providers)everyone, first
    advertisemethodsprovider
    invokeid, method, params, owner, timeout_mscaller; host to provider
    replyid, ok, error or dataprovider; host to caller
    listnonecaller or observer
    catalogowners: owner, app_id, methodshost
    tracethe trace event fieldshost 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's timeout_ms.
    • Params. Any JSON value; owners expect an object.
    • Replies. ok: true with data (any JSON, null included), or ok: false with an error string. Only the first reply for an id counts; later replies for it are discarded.

    The host answers some invokes itself:

    ErrorWhen
    invoke missing ownerThe invoke has no owner
    <owner> is not runningNo provider holds that owner
    <owner> has no method <method>The owner did not advertise that name
    timeoutThe 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.

    SituationWhat the caller sees
    sola-call is not runningConnecting fails: call host is not running (<socket path>)
    Owner not connectedAt once: <owner> is not running
    Method not advertisedAt once: <owner> has no method <method>
    Owner slowtimeout at the deadline
    Owner exits or crashes during the calltimeout 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 runningThe call is dropped: timeout at the deadline
    Kit app whose UI loop stopped taking callsAt 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 callcall 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 catalog on connect, after every advertise, and after every owner leaves. An observer may also send list for one.
    • Traces. The host sends a trace for every event:
    kindWhenFields set
    invokeA caller's invoke reaches the hostid, owner, caller, method, params
    replyA 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
    timeoutA deadline passesid, owner, caller, method, ok: false, error: "timeout", duration_ms
    advertiseA provider advertisesowner, data (its methods)
    unregisterAn owner leavesowner

    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§

    OwnerProcessPresentMethods
    compositorsola-riverWhole sessionscreenshot, sample, cursor, windows, input.click, input.press, input.release, input.move, input.scroll, input.key
    desksola-sessionWhole sessionlaunch, close
    botssola-botsdWhile sola-botsd runslist, poll, new, setup, send, transcript, rm, cancel
    projectssola-projectsWhile Projects runs; it quits when its last project window closesps, 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
    browsersola-browserWhile the Browser runstabs, 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
    designsola-designWhile Design runsproject.list, project.add, project.remove, open, canvas.new, canvas.close, status, share, share.rotate, selection, view, wait, reload, screenshot, pdf, inspect
    vaultsola-vaultdWhile the vault service runsstatus, 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, compositor and desk, are parsed by solactl itself: fixed flags, defaults, and --help that works with nothing running. solactl builds 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 and solactl <owner> <method> --help its flags, both from the advertised specs. The name may be the owner or its app id. These need sola-call and 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, and tools/call; any other method gets error -32601, and a line that is not JSON gets -32700. It offers protocol versions 2025-06-18, 2025-03-26, and 2024-11-05 (the client's if it asks for one of these, else the newest). Its serverInfo.name is sola, and its instructions explain 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 matching solactl command. --owner NAME (repeatable; owner or app id) limits the tools to those owners.
    • Schemas. Each ArgSpec is a property named by name: string and path are JSON strings (a path's description ends in (path)), int is integer, float is number, bool is boolean, and json is object. Required arguments are listed in required.
    • Calls. Arguments must be a JSON object or absent. They are passed as sent, with no type conversion, except that path arguments 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 for solactl. A Design page call without canvas goes to the canvas this server last opened or created. Each call runs on its own thread, so ping and tools/list are answered while a long call waits.
    • Results. A reply becomes a text item holding the indented JSON (ok for null). When the reply has a path ending in .png, .jpg, or .jpeg and 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 with isError: true; a call whose deadline passes says timeout 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_changed when the set of tool names changes, once the client has sent notifications/initialized. When sola-call is down, tools/list is 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) before install() 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 a Subscription<Incoming>; fold it into the application's subscription. Use it or drain the receiver yourself, not both; a process has one call receiver.
    • An Incoming has method, params, and reply. Answer with reply.ok(json) or reply.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, as sola-river, sola-session, and sola-botsd do.
    • A caller uses sola_call::invoke(owner, method, params, timeout) and sola_call::catalog(); an observer uses sola_call::start_observer, or the kit's install_observer and observe_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 long equal to name. solactl sends a --long value under the flag's own text, so a different long reaches the owner under a key it does not expect.
    • Do not use h as a short flag; solactl reads -h as help.
    • Give a method that can run longer than 8 s a timeout_ms, or a timeout argument in seconds, which solactl and 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§

    GlobalUsed 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_v1The shell's key bindings
    river_layer_shell_v1Showing layer-shell clients (Layer shell)
    river_xkb_config_v1, river_input_manager_v1NumLock on for each keyboard
    river_libinput_config_v1Natural scrolling
    zwlr_output_manager_v1Picking each output's mode
    zwlr_screencopy_manager_v1, wl_output, wl_shmScreenshots and sample
    ext_foreign_toplevel_list_v1, ext_foreign_toplevel_image_capture_source_manager_v1, ext_image_copy_capture_manager_v1Screenshots of one window
    zwlr_virtual_pointer_manager_v1, zwp_virtual_keyboard_manager_v1Synthesized input (input.*)
    ext_idle_notifier_v1, wp_alpha_modifier_v1The desk's opacity, the idle fade, and the dim
    wp_cursor_shape_manager_v1The move and resize cursors
    wl_compositorShadows, 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§

    ConcernDecided byWhat sola-river does
    Stacking and visibilityShell (Composition)Shows the listed windows bottom to top, hides the rest
    Keyboard focusShell (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 windowsShell (Frame)Proposes the size, sets the position
    Size of a floating windowThe 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
    FloatingShell (WindowFloating)Allows move and resize, draws the shadow
    Tiled state and the rimShell (WindowChrome)Tells tiled windows they are tiled; rims windows whose app draws no frame (Frames, rims, and corners)
    Key bindingsShell (RegisteredChords; your changes in Keybindings)Registers them with the compositor, reports presses; registers none while Settings waits for a key (KeyCapture)
    FullscreenThe application, or CinemaGrants it, and tells the shell (FullscreenRequest); ends it when focus moves away
    The background pictureWhoever sets BackgroundDraws it behind everything (The background picture)
    The desk's opacity, the idle fade and dimSettings → Appearance (Appearance)Draws the picture, then black, over the desk (The idle fade)
    Output modesola-riverPicks 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.

    ZoneRectangleDefault key
    FullThe content area right of the barSuper+KP_0
    A grid zoneIts cells, one gap between neighbouring cells; a zone over several takes the gaps inside itthe default layout's two: Super+KP_4, Super+KP_6
    FloatingThe window's own size, or its remembered rectangle; a tile floated with the key keeps its place, 50 px smaller on each sideSuper+KP_*
    CinemaThe whole output, top row and bar included, in fullscreen stateSuper+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.

    SettingDefaultRange
    Desk → Opacity90 %50–100 %
    Idle → Fade the windows when idleOnOn or off
    Idle → Start after5 min1–60 min
    Idle → Fade over30 min1–120 min
    Idle → Then dim the backgroundOnOn or off
    Idle → Dim after60 min1–240 min
    Idle → Dim over30 min1–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):

    1. 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.
    2. 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:

    VariableEffect
    SOLA_OUTPUT_MODE=2560x1440That 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=preferredThe 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§

    TopicKindPayloadWhen
    WindowsstickyList of {window_id, app_id, title, pid}, ordered by idA window's app id, title, or pid arrives or changes; a window closes or is pruned
    OutputGeometrysticky{width, height}The output reports its size
    WindowGeometrysticky, one per window_id{window_id, x, y, width, height}The window's size or position changes; retracted when it closes
    WindowParentsticky, one per window_id{window_id, parent}The compositor reports the window's parent; retracted when it closes or has none
    MouseEnteredevent{window_id, still}The pointer enters a window, or moves again after a still enter or a focus change
    MouseLefteventnoneThe pointer leaves a window
    MouseClickedevent{window_id}A window is clicked or touched
    Chordevent{keysym, modifiers}A registered chord is pressed
    ChordReleasedevent{keysym, modifiers}A registered chord is released
    IdleFadesticky{fading, out}The fade starts, becomes full, or ends
    WindowFloatingsticky, 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§

    TopicSent byEffect
    CompositionShellList of {window_id}, bottom to top: the stack
    FrameShell{window_id, x, y, width, height, fullscreen}: one window's rectangle
    FocusShell{window_id}: keyboard focus
    WindowFloatingShell{window_id, floating}: allows move and resize, adds the shadow
    WindowChromeShell{window_id, tiled, cinema, rim}: tiled state, the rim, rounded corners
    RegisteredChordsShellList of {keysym, modifiers}: the key bindings
    CloseAppShell, kit applicationsAn app id: close request to its windows
    CloseWindowShellA window id: close request to that window
    BackgroundPaint, Browser, solactl, SettingsThe picture behind the windows; a retraction for the black ground
    AppearanceSettingsThe desk's opacity, the idle fade and dim
    IdleKeepShell{x, y, w, h}: what the fade keeps at the rest
    WindowVideosBrowser{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
    ThemeShellThe rim's colors
    ShutdownShellsola-river exits with status 0

    7.11Call methods§

    The owner is compositor. solactl compositor and the MCP tools named compositor_… reach these methods.

    MethodParametersReply
    screenshotoutput (path), app, window (title), x, y, width, height (region), format (png or rgba){path}; with rgba also width, height, and format: "rgba8"
    samplesize (default 15, at most 65)The pixels around the pointer
    cursorvisible (default true){visible}
    windowsnone{app_id: [{title, window_id, pid}]}
    input.clickx, y, button (left, right, or middle; default left){ok: true}
    input.pressbutton (default left); x, y to move there first{ok: true}
    input.releasebutton (default left); x, y to move there first{ok: true}
    input.movex, y{ok: true}
    input.scrolldx (default 0), dy (default 5; down is positive){ok: true}
    input.keychord, 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 restartsWhat happens
    The compositorsola-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-riverPID 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-bussola-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-shellThe 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-river sets 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-river because River names it as it creates it: sola-river tells 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 first sola-river with the map (2026-11-01) had none to read: the windows up then were numbered anew.
    • Only the first seat is used.
    • input.key uses a US keymap regardless of the keyboard's layout. The method declares chord as text, so a caller that sends text, as the MCP tool compositor_input_key does, gets an error; solactl compositor input key sends 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:

    PathSent byCommand run
    LaunchApp {app_id, command} on the busThe 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 runningThe 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:

    VariableValue
    WAYLAND_DISPLAYThe first wayland-N socket in the runtime directory that accepts a connection, else sola-session's own WAYLAND_DISPLAY
    XDG_SESSION_TYPEwayland, when it found a Wayland socket. Chromium-based applications and desktop portals treat other values as not Wayland.
    DISPLAYThe first live X socket in /tmp/.X11-unix, as :N, when there is one
    XDG_RUNTIME_DIRPassed through when set
    SOLA_APPThe 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-session tracks the process under;
    • the Wayland app id the program gives its windows, which sola-river reports in Windows;
    • 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:

    FieldMeaning
    app_idThe identifier; see Application ids
    labelThe name in the launcher and the bar
    commandWhat LaunchApp carries
    iconAn icon pack name under /oath/store/pkg/sola/share/icons (lucide/terminal, simpleicons/firefox), or a path to an image
    kindcommand, or web for a web app
    urlA web app's start page
    profileThe 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.

    Whensola-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 crashingYes
    It has shown no window 2 minutes after it startedYes
    CloseApp: Super+Q, the close callYes, at once
    Exit SolaYes, within the 6 s every application gets

    Quitting goes in steps until no process carries its SOLA_APP:

    1. 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.
    2. After 15 s (at once, with no quit command), SIGTERM to every process that carries its SOLA_APP.
    3. 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:

    PartKept bySaved in
    Which applications runsola-sessionSessionApps in ~/.config/sola/state.yaml
    What each application has open: its tabs, a window eachThe applicationIts 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 showsThe shellTabLayout 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:

    ApplicationReopensFrom
    TerminalEvery saved tab, its panes attached to their tmux sessions again; sessions the tmux server lost (after a reboot) are made again with new shellsTerminalSession entries in ~/.config/sola/state.yaml
    BrowserEvery page of the active profile and of each workspace's profile; a web app's pages open as that appsession.json in each profile's folder under ~/.local/share/sola/browser/profiles/
    PaintEvery saved image that still exists~/.config/sola/paint.yaml
    ProjectsEvery open project, with its selected tab attached~/.config/sola/projects/catalog.json
    DesignA 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.

    ApplicationA second process
    BrowserHands over its URL, a request for a new page, or a web app to open
    TerminalAsks for a new tab, which opens as a new window
    PaintHands over its image paths
    ProjectsAsks the running one to show its selected project's window
    DesignAsks the running one to come forward
    CalendarHands 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.

    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:

    AppSchemesTypes
    Browserhttp, httpshtml, htm, xhtml, pdf, svg, txt, json, xml
    Paint—png, jpg, jpeg, jpe, gif, webp, bmp, tif, tiff, tga
    Calendar—ics, ical, icalendar
    Mailmailto—

    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:

    1. 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 .desktop file's name). A claim never writes it.
    2. Sola's own claimant (the table).
    3. 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.
    4. For a scheme only: ~/.config/mimeapps.list ([Default Applications], x-scheme-handler/<scheme>), then a .desktop file whose MimeType lists x-scheme-handler/<scheme>, in ~/.local/share/applications and each $XDG_DATA_DIRS entry's applications. That is how claude-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.

    TopicDirectionPayload
    LaunchAppRead{app_id, command}
    CloseAppRead (also by sola-river and kit applications)An app id
    ShutdownReadNone
    SessionAppsRead at start; publishedList of {app_id, command}, persistent
    WindowsRead at startSee Compositor
    LaunchResultPublished{app_id, command, ok, error}
    UserAppExitedPublished{app_id, command, code, signal}

    Call methods, owner desk (also the MCP tools desk_launch and desk_close):

    MethodParametersReply
    launchapp_id; command (optional){app_id, command, pid}
    closeapp_id{app_id, stopped}

    8.12Restarts§

    What restartsWhat happens
    sola-sessionOpen 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-bussola-session reconnects and subscribes again. A LaunchApp sent while the bus was down is lost.
    The compositorThe applications lose their display and exit; they stay in SessionApps and start again on the new compositor. See Architecture.

    8.13Logs§

    FileContents
    ~/.local/state/sola/sola.logsola-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>.logEach application's standard output and standard error
    solactl logs app-sola-mail --follow

    8.14Limits§

    • No .desktop files 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 CloseApp is by exact app id.
    • A restart of sola-session loses track of the applications it started.
    • Applications started from a terminal or by another program (not through sola-session or open_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-session older than 2026-10-31) has no SOLA_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.

    ChordAction
    Super+SpaceOpen the launcher
    Super+KKeyboard shortcuts overlay (press again to close)
    Super+QQuit the focused app: every window of it. One that isn't Sola's quits whole (Session manager)
    Super+HHide the focused app
    Super+LLock the screen (needs a password: Security)
    Super+CapsLockShow the next window in the zone under the pointer (Super+Shift+CapsLock: the previous)
    Super+BacktickShow the focused app's next window on this workspace (Super+Shift+Backtick: the previous)
    Super+1 … Super+9Show workspace 1 to 9
    Super+Shift+1 … Super+Shift+9Send the focused window to workspace 1 to 9
    Super+TabNext workspace (Super+Shift+Tab: the previous); as islands, past those switched off
    Super+Keypad 0Move the focused window to Full
    Super+Keypad 4Move the focused window to the left column (the default layout's)
    Super+Keypad 6Move the focused window to the right column (the default layout's)
    Super+Keypad MultiplyFloat the focused window
    Super+Keypad EnterMove the focused window to Cinema
    F13Screenshot of a selection, to the clipboard
    F14Screenshot of the whole output, to the clipboard
    F15Screenshot of the focused window, to the clipboard
    EscapeClose the open launcher, overlay, menu or panel; cancel a selection
    Play/Pause, Next, PreviousMedia players (MPRIS)
    Mute, Volume Up, Volume DownThe 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§

    ItemWhat it does
    Launch Application…Opens the launcher
    Settings…Brings Settings forward, or starts it
    Keyboard ShortcutsOpens the shortcuts overlay (Super+K)
    Lock ScreenLocks the screen (Super+L; Security)
    Lock VaultLocks the vault at once (Vault); there only while the vault is unlocked
    Exit SolaEnds the desktop session (Shutdown on the bus); on Oath the console then has the screen, and oath desk brings the desk back
    Restart ComputerRestarts the machine
    Shut DownPowers 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 with sudo oath set host:local seat_hid='["3554:f510"]' and sudo 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_ADDRESS when it is set, else /run/dbus/system_bus_socket. On Oath that is svc:dbus-system, and BlueZ is svc:bluetooth; the desk profile has both, and a machine installed before them adds them with oath 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:

    ZoneWhere it is
    FullThe whole area right of the bar: every cell
    A grid zoneIts cells of that area
    CinemaThe whole screen, over the top row and the bar
    FloatingIts 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:

    1. 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.
    2. 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.
    3. Vault ▸, where you can type.

    Vault ▸ follows the vault (Vault):

    The vaultVault ▸ shows
    Not runningVault isn't running
    Signed outSign in to Vault…, which brings Vault forward
    LockedUnlock Vault…, which brings Vault forward; right-click again once it is unlocked
    UnlockedOn 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§

    KeyAction
    ↑ / ↓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)
    ReturnChoose the row
    EscapeClose 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 --app does 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.

    PartWhat it gives an application
    app::startupThe 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::busThe bus connection (The bus)
    CallSetup, call_subscriptionCall-plane methods (Call plane)
    themeThe palette, the shell style, and conversion from the bus theme
    fontsSystem font loading and the five font roles
    componentsShared 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_settingsThe 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
    tabsA 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.tokens maps a token name to a token: a kind (Color, FontFamily, TextSize, Space, or Radius), a string value, and a list of groups.
    • components maps 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.

    TokenDefaultIced palette slotsUsed for
    bg-primary#0b0b0bbackground.base, background.weakestWindow canvas
    bg-secondary#151515background.weaker, background.weakRaised panels: sidebars, cards, titlebars
    bg-hover#1f1f1fbackground.neutral, background.strong, secondary.weakHover
    border#2a2a2abackground.stronger, background.strongest, secondary.base, secondary.strongHard edges and chrome
    text-primary#ecececText on every background slot and on secondary.strongPrimary labels
    text-tertiary#949494Text on secondary.base and secondary.weakMuted labels (see below)
    accent#3dd6f5primary; Iced derives its lighter and darker shadesFocus, selection chrome, key status
    success#3ecf8esuccessStatus
    warning#e8b84awarningStatus
    danger#f07178dangerStatus
    selection#292929None; read with sola_kit::theme::selection()Selected rows and text selection
    highlight#353535None; 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.

    TokenRoleWeightUsed forDefault family
    font-uifonts::ui()RegularBody text, buttons, sidebar itemsSF Pro Text
    font-ui-mediumfonts::ui_medium()Medium (500)Emphasis: badges, section caps, toolbar labelsSF Pro Text
    font-displayfonts::display()Medium (500)Page titles and large headingsSF Pro Text
    font-chromefonts::chrome()RegularTitlebar menus and titles, the top row's labels and clockSF Pro Text
    font-monofonts::mono()RegularCode, JSON, Terminal, ProjectsIosevka 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.

    TokenDefaultControls
    shell-flower#ffb800The 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#00000099Dimming behind the launcher
    shell-launcher-width560pxLauncher width
    shell-launcher-pad8pxRow 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 malformedResult
    No theme on the busCompile-time defaults, as before a theme arrives
    A palette token, or a colour not in #rrggbb or #rrggbbaa formThat token's default
    A font token, or a family that is not installedThe role's fallback family
    A shell token, or a spacing value not in <number>px formThat token's default
    A theme/current.yaml that does not match the theme schemaThe 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:

    SectionSettings
    Light or darkMode: 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 pictureThe 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
    ColorsAdapt colors to the background, Strength, and a card for each look
    Your colorsWindow, Panels, Highlight, Accent
    DeskOpacity of the windows, the bar and the top row over the background: 50–100%, 90% at first
    IdleFade 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).

    ColorSets
    Windowbg-primary
    Panelsbg-secondary (bars, titlebars, sidebars), and shell-switcher-bg at 90% opacity
    Highlighthighlight (the bar's focused row); selection takes its hue at its own lightness
    Accentaccent, 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§

    1. Call sola_kit::app::startup(APP_ID) first; it loads the system fonts.
    2. Include TopicKind::Theme in the BusSetup subscription.
    3. Keep an iced::Theme in the application state, start it as sola_kit::default_theme(), and return it from Iced's theme callback. A kit window returns sola_kit::theme_for(chrome, &theme) (its FloatState::chrome), which clears the window fill so the frame's rounded corners show what is under it, except in Cinema.
    4. Map sola_kit::app::bus_subscription() into the application's subscription and pass each message to sola_kit::apply_theme_update.
    5. Draw with kit components. In custom styles, take colours from theme.extended_palette(), sola_kit::theme::selection(), sola_kit::theme::highlight() and sola_kit::components::style::chrome_surface(), and fonts from the sola_kit::fonts role 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 Theme can 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…]
    CommandGoes throughNeeds
    compositor, deskCall plane; flags compiled into solactlsola-call and the owner; --help needs nothing
    projects, browser, bots, design, vault, any other nameCall plane; flags read from the owner's advertised methodssola-call and the owner, also for help
    mcpCall planesola-call; tools appear as owners do
    emit, backgroundBussola-bus (the picture is drawn by sola-river)
    logsFiles in Sola's log directorynothing
    openOpens a link or a file with the app Sola keeps for it, as xdg-open does: its open call, or the session starts it firstsola-call; the bus to start an app
    mediaSession D-Bus (MPRIS) and wpctla 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.

    StatusMeaning
    0Success; also media with no player to act on
    1The 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
    2Usage 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
    3Local 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§

    SituationstderrStatus
    sola-call not runningsolactl: call host is not running (<socket path>)3
    compositor or desk owner not connectedsolactl: compositor is not running1
    Live owner not in the catalog (also for its method list and --help)solactl: <name> is not running3
    Method not advertisedsolactl: <owner> has no method <method>3
    Owner did not answer by the deadline, or exited during the callsolactl: timeout1
    sola-call restarted during the callsolactl: call host closed the connection3

    Any command name solactl does not know is taken as an owner name, so a misspelt command reports <name> is not running.

    11.3Environment§

    VariableRead byEffect
    SOLA_CALL_PATHcall commands, mcpCall socket. Default $XDG_RUNTIME_DIR/sola-call
    SOLA_BUS_PATHemit, backgroundBus socket. Default $XDG_RUNTIME_DIR/sola-bus
    XDG_RUNTIME_DIRcall commands, mcp, emit, backgroundDirectory 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_BOTcompositor, browser, mcp1, true, TRUE, or yes turns on the bot rules
    SOLA_PANE_IDprojects whoami, projects session.newDefault --pane and --parent. Projects sets it in each session
    SOLA_WS_PATHprojects whoamiDefault --path. Projects sets it in each pane to the session's checkout path
    HOMEpath arguments, open, logs~ expansion; default location of chrome.sock and, on Oath, of the logs
    SOLA_LOG_DIRlogsLog directory. Default $XDG_STATE_HOME/sola on Oath (~/.local/state/sola when that is unset or not absolute), else /opt/sola/log
    SOLA_BIN_DIRopenWhere Sola's programs are. Default /bin on Oath, else /opt/sola/bin
    XDG_DATA_HOMEopenThe Browser's handoff socket is $XDG_DATA_HOME/sola/browser/chrome.sock (default ~/.local/share/…)
    SOLA_BROWSERopenBrowser binary to start. Default sola-browser in Sola's bin directory. A path that is not a file is ignored
    SOLA_PAINTopenPaint binary to start. Default sola-paint in Sola's bin directory. A path that is not a file is ignored
    DBUS_SESSION_BUS_ADDRESSmediaSession D-Bus used to find MPRIS players
    PATHmediaFinds 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§

    OwnerApp idProcessPresentSection
    compositorsola-riversola-riverWhole sessionCompositor
    desksola-sessionsola-sessionWhole sessionDesk
    botssola-botsdsola-botsdWhile the daemon runsBots
    projectssola-projectssola-projectsWhile Projects is openProjects
    browsersola-browsersola-browserWhile the Browser is openBrowser
    designsola-designsola-designWhile Design is openDesign
    vaultsola-vaultdsola-vaultdWhile the vault service runsVault

    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 VALUE or -s VALUE sets an argument; --name=VALUE is not split. When a flag is given twice, the last one wins.
    • A bool flag alone is true. --name true, false, 1, or 0 sets 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: int and float are parsed (a value that does not parse is sent as a string and the owner answers with its error), json is parsed (else sent as a string), and path is made absolute (below).
    • An unknown --flag is sent as a string argument. An unknown -x is an error (unknown flag -x).
    • solactl does 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 --timeout argument (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 whoami without --pane or --path sends $SOLA_PANE_ID and $SOLA_WS_PATH.
    • projects session.new without --parent sends $SOLA_PANE_ID. The new window opens on the parent's workspace, not the one showing.
    • design canvas.new without --path sends 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.focus is refused, and --select on any browser method.
    • browser page 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, and input are refused; compositor windows is 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§

    FlagDefaultMeaning
    -o, --output PATH/tmp/sola/screenshots/<ms>.pngWhere 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 APPthe whole outputCapture this app's window (exact app id)
    -w, --window TITLEthe app's first windowWith --app, the window whose title is exactly TITLE; ignored without --app
    --format png|rgbapngrgba (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 SECS10Deadline

    --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§

    CommandArgumentsDefaults
    input click X Y-b, --button left|right|middle (also l, r, m; lower case only)left
    input move X Ynone
    input scroll-x, --dx DX; -y, --dy DY (decimal; down is positive; a negative value as --dy=-5)0, 5
    input key CHORDnone
    input press [X Y]-b, --button as for clickleft
    input release [X Y]-b, --button as for clickleft
    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, default true; deadline 2 s) shows or hides the compositor's pointer and replies {"visible": …}.
    • screenshot also takes a region: --x, --y, --width, --height, all four, in output coordinates. With --app the 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.

    CommandFlagsReply
    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.

    MethodDoes
    listBots, their AI, and their status
    poll{"bots", "transcript"} in one call; transcript is null without --bot, or for a bot it does not find
    newCreates a bot on an AI, and its home
    setupGives a bot directory without an AI its AI; fails if it has one
    sendSends a message and returns while the turn runs; read the answer with transcript or poll
    transcriptThe dialog turns
    rmDeletes the bot's directory (and a Grok bot's session)
    cancelCancels 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§

    AIRuns asSigns in with
    Grokgrok agent stdio (ACP), one child per bot, all tools approvedthe Grok CLI's own login; run grok in a terminal once to sign in
    Claudeclaude -p per message on the bot's session (--resume), all tools approved, no MCP serversClaude Code's own login, the subscription; an ANTHROPIC_API_KEY is never passed on
    Gab AIGab's chat API (https://gab.ai/v1, model arya), the dialog sent each turnan 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/config alias; the last eight are listed, latest first. Enter lists that computer's ~/Projects and 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
    ShortLongMethods
    -p--projectproject.close, session.list, session.new
    -p--pathproject.open
    -n--namesession.new, session.set
    -a--agentsession.new, session.exec
    -s--selectsession.new
    -s--sessionsession.end, session.select, session.set, session.exec, pane.list
    -t--textpane.send
    -e--enterpane.send
    -l--linespane.read
    -s--statuspane.wait
    -f--freshpane.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 projects there 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]
    ShortLongMethods
    -u--urltab.open, goto
    -s--selecttab.open, goto
    -t--tabevery method that takes a tab
    -c--chordkey
    -q--textfind.page, find, wait
    -x--texttype, fill, get
    -b--backfind.page
    -i--interactivesnapshot
    -j--jsonsnapshot
    -r--refclick, hover, type, fill, get, select
    -e--submittype
    -f--filefill
    -v--valuesselect
    -l--loadwait
    -o--pathscreenshot

    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
    ShortLongMethods
    -p--pathproject.add, project.remove, open, canvas.new
    -n--namecanvas.new
    -r--raiseopen, canvas.new
    -c--canvascanvas.close and every page call
    -a--artboardview, wait, reload, screenshot, pdf, inspect
    -f--fitview
    -s--section, --selectorview, inspect
    -z--zoomview
    -o--outscreenshot, pdf
    -A--allinspect

    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]
    MethodDoesReply
    statusThe 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
    lockLocks the vault nowThe status
    syncSyncs with Bitwarden; fails when the vault is locked or signed out, or the sync fails. Deadline 120 sThe status
    openStarts 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 sends tools/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 screenshot output, which solactl compositor screenshot -o sends as given.
    • A reply with a path to 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 without canvas goes to the canvas this server last opened or made.
    • The compositor's bot refusals are part of solactl compositor, not of the methods, so with SOLA_BOT=1 the compositor tools still work. compositor_input_key wants chord as 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) starts sola-paint with it ($SOLA_PAINT, else sola-paint in Sola's bin directory: /bin on Oath, /opt/sola/bin elsewhere).
    • A path or file:// URL ending in .ics, .ical, or .icalendar (any case) starts sola-calendar from 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 .ics files included. An absolute path becomes a file:// URL; a relative path does too when the file exists or it starts with ./ or ../. A running Browser gets the URL through its chrome.sock; otherwise sola-browser is started with it ($SOLA_BROWSER, else sola-browser in 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.

    ActionDoes
    play-pause, next, prevActs 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
    muteToggles mute on the default audio sink (wpctl)
    vol-upRaises the default sink by 5%, capped at 100%
    vol-downLowers 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.

    ApplicationProgramPackageWhat it isNotes
    Terminalsola-terminalpkg:sola-terminalA terminal on Alacritty's grid: a window per tab, split panes, each running in tmux—
    Projectssola-projectspkg:sola-projectsThe 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
    Browsersola-browserpkg:sola-browserChromium'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 appssola-browser --app <id>pkg:sola-browserA website that Browser runs as an application of its own, made from a Browser page: a launcher entry, its own windows, name, and menusnotes
    Mailsola-mailpkg:sola-mailIMAP and SMTP for several accounts in one combined set of mailboxes, with rules, drafts, attachments, and safe HTML lettersnotes
    Calendarsola-calendarpkg:sola-calendarMonth, 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 clocknotes
    Vaultsola-vault, sola-vaultdpkg:sola-vaultSola'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
    Editorsola-editor, editpkg:sola-editorA text editor with Emacs keys and syntax colors, a window per file; edit <file>… opens files in it from anywherenotes
    Paintsola-paintpkg:sola-paintThe default image viewer and editor, a window per image; sets the desktop's background picturenotes
    Previewsola-previewpkg:sola-previewA quick viewer for screenshots and imagesnotes
    Designsola-designpkg:sola-designHTML artboards on a canvas, written and edited by AI agents: a window per project, its canvases along the top. Call owner design.notes
    Settingssola-settingspkg:sola-settingsApplications 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.—
    Botssola-bots, sola-botsdpkg:sola-botsNamed 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
    Oathsola-oathpkg:sola-oathThe 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.—
    Monitorsola-monitorpkg:sola-monitorA live view of bus traffic and of the call plane's methods and callsnotes
    Scopesola-scopepkg:sola-scopeA pixel loupe: the screen under the pointer, magnifiednotes
    KVMsola-kvmpkg:solaOne 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.