strand

Running a shell

This page is the day-to-day loop: make a shell, start it, edit it while it runs, and check it from a terminal. Level 1 of the guide walks through the same loop with the hello bar.

Where the config lives

A shell is a directory of .strand files. By default it is $XDG_CONFIG_HOME/strand, or ~/.config/strand when XDG_CONFIG_HOME is not set. Every command on this page that takes a directory uses that one when you give none.

Every .strand file in the directory is part of one shell, down to three directories below it. There is no entry file and nothing to import. Files and directories whose names start with . are skipped, and symlinks are followed, so a config kept in a dotfiles repository and linked into place works. strand check warns about .strand files that sit deeper than three directories, since they are not loaded.

Make a shell

strand new

strand new writes one file, bar.strand, the guide's hello bar, and prints where it went:

wrote /home/you/.config/strand/bar.strand
start it with `strand run`

It makes the directory if it is missing. It refuses a directory that already holds anything, even a hidden file, and writes nothing there. strand new ~/shells/test writes into another directory instead.

Start it

strand run

strand run stays in the foreground until the compositor goes away or it gets SIGINT or SIGTERM (Ctrl-C in its terminal). Before it ends it writes the persist state and settings it has not saved yet. strand run ~/shells/test runs another directory.

If the directory has no .strand files yet, the shell starts with nothing to show and waits: the first file you save there appears. It says so once as it starts, in its log: "… nothing to run yet: no .strand file".

Save to change it

Leave strand run running. Strand watches the config directory, and each save changes the running shell instead of restarting it: a changed value animates to its new one, and state you did not touch is kept, such as an open popup or the text in a launcher. Files saved together, as "save all" does, count as one change.

A save that does not check leaves the last good shell running. A quarter of a second later an overlay lists the problems, and the fix takes it away. Troubleshooting says what the overlay does.

Strand keeps the last config that checked in $XDG_CACHE_HOME/strand/last-good (~/.cache/strand/last-good by default). A config that is broken when the shell starts runs that last good version, with the overlay up. With no last good version yet, only the overlay shows until you fix the file.

Check it

strand check

strand check reads every .strand file of the config as one program, type-checks it without running it, and prints what is wrong, with the file, the line and often a "did you mean" fix. It exits with an error status when it finds an error, so it works in scripts and before you start the shell. It ends with a summary line:

strand check: 1 file in /home/you/.config/strand: 0 errors, 0 warnings

strand check ~/shells/test checks another directory. Given a file, strand check bar.strand checks it together with the rest of its config and prints only the problems in that file.

Format it

strand fmt

strand fmt rewrites .strand files in Strand's one style. With no path it formats the config directory; it also takes directories and files. With --check it writes nothing, lists each file that is not formatted and exits with an error status when there is one:

not formatted: /home/you/.config/strand/bar.strand
strand fmt: 2 files, 1 not formatted

A file with syntax errors is reported and left as it was. A formatted file replaces the old one through a rename, so a symlinked file stays a link. Your editor's format command uses the same formatter (see Editor setup).

Watch what it does

strand watch

strand watch connects to the running shell and prints each event until the shell ends: every reload with its time, what kind of edit it was, which files it committed or held back and which state it kept, reset cells, runtime faults and notices. A save of the hello bar with a new height, then a typo in it, looks like this:

reload (20.6 ms): prop
  committed: /home/you/.config/strand/bar.strand
reload (18.0 ms): no change
  held back: /home/you/.config/strand/bar.strand
  /home/you/.config/strand/bar.strand:5:14: error[check::unknown_prop]: unknown prop "heigth" on "bar"; did you mean "height"?

strand watch --json prints the same events as one JSON object per line, for scripts. Its first line, {"event":"watching","v":1}, says that no event after it is missed.

Reload by hand

strand reload
strand reload --hard

You rarely need strand reload: saving already reloads. It rescans the config now and prints that reload's event as strand watch does.

strand reload --hard also drops every state that is not persist and makes every surface again. Use it when the shell's state has gone somewhere you cannot get back from. While the lock screen is shown, a hard reload waits until you unlock.

Change values from a terminal

strand set theme.look mocha
strand set audio.sink.volume +5%
strand toggle launcher.open
strand get theme.look

These talk to the running shell:

  • strand set <path> <value> writes an exported state, a settings field, a service field the service lets you write, or a token. The value is read as the target's type. For a service's number, a signed number or percentage, such as +5%, steps from its current value.
  • strand toggle <path> flips an exported bool state.
  • strand get <path> prints an exported value, a settings field or a token.

A path is file.name for a value a file exports (launcher.open is the export state open of launcher.strand), service.field for a service (audio.sink.volume) and $name for a token. strand set '$accent' '#ff5555' lays a token over the theme until strand clear '$accent' takes it off. Bind these commands to keys in your compositor (Start it and bind keys shows how).

Logs

The shell logs to its standard error. STRAND_LOG sets how much: a level, one of off, error, warn, info, debug and trace, with warn the default. Each line names its level and where it came from:

strand: WARN strand::run::logic: no IPC socket (strand reload, strand watch): …
STRAND_LOG=info strand run

info adds what the shell does as it runs, such as each surface mapped, each reload and each service starting or stopping. The topic damage (STRAND_LOG=damage, or STRAND_LOG=info,damage with a level) prints one line per painted frame with the rectangles it repainted.

Start it and bind keys

Start strand run from your compositor's startup commands, so it runs in the session's environment. Strand does not grab keys either: your compositor keeps them, and a key runs a strand command. Every compositor works this way; these are the lines for three of them, starting the shell and opening the launcher on Super+D.

sway, in ~/.config/sway/config:

exec strand run
bindsym $mod+d exec strand toggle launcher.open

Hyprland, in its Lua config (~/.config/hypr/hyprland.lua, Hyprland 0.55 and later):

hl.on("hyprland.start", function()
    hl.exec_cmd("strand run")
end)
hl.bind("SUPER + D", hl.dsp.exec_cmd("strand toggle launcher.open"))

niri, in ~/.config/niri/config.kdl:

spawn-at-startup "strand" "run"
binds {
    Mod+D { spawn "strand" "toggle" "launcher.open"; }
}

Keys that should work while the screen is locked, such as volume and brightness, take a flag: bindsym --locked in sway, { locked = true } as hl.bind's third argument in Hyprland (with repeating = true there to repeat while held, which sway and niri do already), and allow-when-locked=true in niri. Your compositor's own documentation has the rest.

The shell connects to the display WAYLAND_DISPLAY names, and the commands above reach it through a socket in $XDG_RUNTIME_DIR named for that display (strand-wayland-1.sock). Its log then goes wherever your compositor sends its programs' standard error.

A second strand run on the same display finds the socket taken: it runs, but with no socket, so strand watch and the other commands keep reaching the first one. To run a second shell on purpose, give it its own STRAND_SOCKET and STRAND_STATE_DIR.

This page on GitHubThis page as Markdownllms.txt