sloshUsing it

Workspaces#

A project is a directory on disk. A workspace is the tab it occupies in a session. Two words rather than one because the two have different lifetimes: the project outlives every session and is checked into git, the workspace lasts until you close the tab.

Finding a pane starts from the complaint this answers — tabs stop being navigation somewhere around six projects. The finder answers where is that pane; this answers the question before it: which of the things you work on is this tab about, and what does opening one mean. C-a w, a name, enter, and the tab arrives arranged, because the project itself said how.

What counts as a project#

A subdirectory of a configured root with one of two markers in it:

markerwhat it is
slosh.layouta declared project — it says what it needs open
.gitan inferred one — you work here, but you have not said how

Anything else is not a project, and nothing is guessed from what is inside it.

.git counts because otherwise the list is empty on the day you configure the feature. A picker that only knows declared projects shows nothing until you have written a layout, and there is nowhere to write one from — you would need the picker to get there. The inferred half is also the invitation: such a project reads .git · no layout in the picker, and C-a W is how it stops being one.

A directory that is a project is never descended into. That is what keeps the scan out of node_modules, a vendored checkout or a submodule without a rule about any of their names — they are under a project, so the walk stops above them. A skip list is a list of the directories that were fashionable when it was written; this holds for the ones nobody has invented yet.

Where they live#

Two settings, both in your config:

project_roots "~/dev" "~/work" depth=2
project_layout "~/.config/slosh/project.layout"

project_roots takes more than one directory because people have more than one — work in one tree, everything else in another — and depth is how many levels below each root to look, 2 by default, 1 to 8. Two is ~/dev/thing and ~/dev/org/thing, which is where checkouts actually sit.

No roots configured and the feature is dormant. Nothing is scanned, C-a w says which setting is missing rather than opening an empty picker, and workspaces answers roots: false — because "you have no projects" and "you never said where they are" are different facts, and a tool that cannot tell them apart reports the wrong one.

project_layout is what a project with no file of its own opens as. Relative paths in it bind to the project being opened, not to the directory the file lives in — deliberately the opposite of the rule for a layout you name yourself, where a relative cwd= is relative to that file (layouts). The whole point of one shared layout is that cwd="." means this project; bound to itself it would open every project in ~/.config/slosh. Unset, a project opens as one pane running your shell in it.

Nothing sniffs package.json or Cargo.toml to guess what to run. A table mapping manifests to commands is wrong for somebody the day it ships — the repo with two package managers, the one whose dev server is behind a make target — and stale for everybody a year later. project_layout is that table with your name on it, covering the projects you have rather than the ones a heuristic imagined, and a project that disagrees with it carries its own file.

From the keyboard#

keydoes
wthe projects picker
Wwrite this tab as this project's layout
Pset this pane's purpose

w and W are the same shifted pair as p/P: go to one, write one down. All three are bindable as workspaces, save-workspace and set-purpose, and all three are in the palette under projects, so they work before you have rebound anything — see keys and every action.

The picker narrows on a project's name, its path and which marker it has, so typing .git gives you the projects with no layout yet — which is the list you want when you are about to write one. A project that is already open shows its purpose instead, and a dot marks the workspace you are in.

The flow the feature is shaped around, once through:

C-a w                       picker: ~/work  newthing   .git · no layout
Enter                       your project_layout, cwd bound to newthing
C-a Enter, npm run dev      arrange it, run things
C-a P  service:web  Enter   tag the pane that matters
C-a W                       "wrote slosh.layout · 4 panes, 2 suspended"

Tomorrow, C-a w Enter rebuilds that tab with the dev server in the pane you put it in and not running. A tab with no name of its own takes the project's, because a strip reading 1 2 3 is the thing workspaces are for.

Closing one is C-a X, which closes the tab and the workspace with it — membership is the tab's purpose, so there is nothing else to put away and no second key to learn (keys). close-workspace over the socket is the same thing for a workspace whose layout opened several tabs: it closes every tab carrying that purpose and says how many.

Saving one#

C-a W writes the focused tab to slosh.layout in the project's directory, and the toast says what happened: wrote slosh.layout · 4 panes, 2 suspended.

What each pane is running goes in with it. That is what makes writing a layout the same act as arranging one: split the tab, start the dev server, start the log tailer, tag the ones that matter, C-a W. A pane's terminal has a foreground process group and that group is the job running in it, so nothing has to be declared up front for it to be recorded — see writing one back out for what is deliberately not counted as a command (a shell at a prompt, a background job, an ephemeral pane).

A project's layout is not a verbatim dump of the session, and suspend is where the two part company. It takes one of four values:

suspendwrites suspended=true on
as-iswhatever is suspended right now — the default for dump-layout
nonenothing
commandsevery pane that has a command — the default for save-workspace
allevery pane

The defaults differ because the two verbs answer different questions. dump-layout is put this session back, so it reports the state it found. save-workspace is this is what this project needs open, and the pane running this morning's dev server is a pane that should be laid out tomorrow rather than started — twelve checked-out projects are not twelve running dev servers (layouts). A suspended pane still shows what it would run, so the layout stays readable as a record of the commands.

Neither can restore what is inside a program, and the file does not pretend otherwise: a shell's history and an editor's buffers are not in it.

Over the API, writing over a layout that is already there is refused without force; from the keyboard C-a W replaces it and the toast says replaced. That asymmetry is not an oversight in either direction. A key cannot pass a flag, and the operator pressing it is looking at the tab being written; the file is committed text, so the diff is the confirmation and the replacement stays reviewable after the fact. A script has none of that — no witness, no glance at the screen — so it says so in the request.

save-workspace also takes path, for a tab that is not a workspace yet: that is how a hand-arranged tab becomes a project's layout without opening it through the picker first.

slosh --check slosh.layout holds the result to the layout schema and prints one file:line: what per problem, so a project's layout goes through the same check as everything else you commit — see layouts.

Identity#

A workspace is its tab's purpose:

project:api.9f3c1d20

The hash is of the resolved absolute path, not of the name, so two git worktrees of one repo are two workspaces — which is what they are, and a name-keyed session would have merged them and then focused the wrong one. Membership is that string and nothing else: no side table of open projects to fall out of step with the tabs.

Because the session declared it, it is locked — no program running in a pane can relabel a project tab, however chatty its title. That is the same rule any declared purpose gets, and it is what makes the purpose safe for a tool to address.

A tab that a layout gave some other purpose keeps it and is not a member. Honouring it is the point: overwriting a declared purpose is precisely what the lock forbids, and a project layout that names a tab agent:main meant it. open-workspace counts those in honoured, so a caller can see that a tab was built and deliberately not claimed.

Nothing watches#

Discovery is derived, never remembered: one readdir per root plus two stats per entry, run when the picker opens rather than per frame. Nothing is stored, so nothing can be stale — and no watcher can promise that. Inotify does not fire for a bind mount, an sshfs share or a checkout another machine wrote, so a cache that trusted it would be wrong exactly when the answer mattered.

One thing a scan cannot see is a project's layout file changing under a workspace that is already open. workspaces reports each layout's mtime, so a tool that keeps the mtime it opened with can compare and notice. Re-applying the new file over the running panes is deliberately not offered — it would kill them.

From another program#

Open one and act on what is in it, over the control socket:

$ slosh -s work cmd '{"cmd":"open-workspace","name":"api"}'
{"ok":true,"tab":3,"purpose":"project:api.9f3c1d20","created":true,...}
$ slosh -s work cmd '{"cmd":"panes"}'      # keep the ones whose tab_id is 3
verbtakesanswers
workspacesroots, and a workspaces[] of name path purpose layout (the file, or "") mtime tab (0 when it is not open)
open-workspacename or path; suspendedtab purpose path created tabs honoured. Already open, it focuses that tab and answers created:false
close-workspacename or purposeclosed — how many tabs went
save-workspacetab (0 for the current one), path for a tab that is not a workspace yet, suspend, forcepath purpose panes suspended replaced

Every one of them is in the bare dispatcher too, so slosh -s work cmd workspaces and slosh -s work cmd open-workspace api do the same from a shell without quoting JSON (scripting).

open-workspace is idempotent, which is the property that makes it safe to call from a script that does not know whether it ran already: asking twice focuses the tab rather than building a second one. suspended:true opens the layout with every pane suspended whatever the file said — the open ten projects, run zero processes case, which is a different question from the one the project answered about which of its own panes are expensive.

The useful part is the last step. Having opened a workspace, filter panes by that tab_id and act on the purposes the project's own layout declared: the project decided that service:web is its dev server and that it starts suspended, and neither the session nor the program driving it had to know that beforehand.