1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
|
# Layouts
A session can be a file, so a project's window layout is checked in with the
project.
```kdl
layout {
tab name="api" cwd="~/dev/api" {
pane command="nvim"
pane split="rows" {
pane command="npm run dev" suspended=true
pane
}
}
}
```
```bash
slosh --layout project.layout
```
## What to call them
**`*.layout`**, and a project's is `slosh.layout`. The syntax is the same KDL
subset the config uses — one parser reads both files — but the extension is
not `.kdl`: zellij speaks KDL layouts with a different schema, and naming the
wrong owner is worse than not naming the parser. Point your editor's KDL
highlighting at `*.layout`. The old `*.layout.kdl` spelling is still found by
the project scan, never written.
A filename cannot enforce anything, so **the document decides which schema it is
held to, not the flag that read it**. `slosh --check` reads what is in front of
it: a file with `layout` or `tab` at the top is checked as a layout, a config
handed to the same flag is still checked as a config, and a file too broken to
parse at all is judged by its name, there being nothing else left to read.
```
$ slosh --check project.layout
project.layout: ok, a layout
```
A clean layout says which schema it passed, because otherwise a layout nothing
could open and a layout with nothing wrong in it read the same. A broken one
reports every problem it found, one indented `file:line: what` per line, then a
summary, and exits 1: the shape [checking a config](config.md#checking-one)
already has, so one editor compile step and one pre-commit hook read both:
```
$ slosh --check broken.layout
broken.layout:2: unknown pane property: cmd
broken.layout:3: split is cols or rows, not `diagonal`
broken.layout:4: weight is a number of 150 or more, not `40`
broken.layout:5: suspended takes true or false, not `yes`
broken.layout:6: purpose is ignored on a pane with panes in it
broken.layout:8: a pane holds panes, not `wobble`
broken.layout: 6 problems
```
Those are the mistakes a layout can make that still parses. `this layout
declares no tabs` comes last rather than first, so it reads as the summary it
is.
Loading is the other half of the same answer:
```
$ slosh --layout ~/.config/slosh/config.kdl
slosh: config.kdl: this is a config, not a layout: `gap` is a setting
```
A config handed to `--layout` is named for what it is instead of refused for
having no tabs, which is true of it in the least useful way.
## The shape
- `tab`: one per tab, in strip order. `name=`, `cwd=`, `purpose=`,
`active=true` for the one you land in.
- `pane`: a leaf, or a split when it has `pane` children. `split="cols"|"rows"`
says which way it divides; `weight=` is its share of the parent (even shares
are equal weights, so leaving it out means "even").
- `command=`: what it runs. Without one, a pane runs your shell.
- `cwd=`: where it starts. Inherited from the tab when the pane does not say.
- `suspended=true`: laid out but running nothing until you touch it, so twelve
checked-out projects are not twelve running dev servers. The pane shows what it
_would_ run.
- `focus=true`: the pane you start in, within its tab.
- `floating=true`: the pane starts [floating](panes.md#floating-a-pane), with
`x=` `y=` `w=` `h=` as its wanted rect in cells. All four absent takes the
centred default; `w=` `h=` alone is that size, centred — and centred again
on every resize, until an explicit move or resize pins it. The rect is
intent: a smaller screen clamps it, and the place you asked for survives. It still holds a seat in the tree: its `weight=` and
position are where un-floating lands it. A file whose every pane floats
lands one as the backdrop, because an overlay needs something to be over.
- `purpose=`: a label for tooling; see below.
**A relative `cwd=` in a layout _file_ resolves against that file's own
directory**, never against the directory you started the session from. It is
the rule [`include`](config.md#include) already follows, applied to the other
half of the same syntax. A layout with no `cwd=` anywhere
starts in the file's directory too, so the common case needs no `cwd=` at all. An
absolute path is unchanged, and `~` is still your home directory.
That is what lets a layout be checked in beside the project it describes and mean
the same thing in everybody's clone:
```kdl
// ~/dev/api/slosh.layout, no absolute path in it anywhere
layout {
tab name="api" cwd="." {
pane command="nvim"
pane split="rows" {
pane cwd="src"
pane command="npm run dev" suspended=true
}
}
}
```
Clone that repo somewhere else and the tab still opens on the checkout, with the
pane that said `src` still in the checkout's `src`.
A layout that arrives as _text_ over the socket has no file, and so no
directory to be relative to; relative paths in it resolve against wherever the
session is.
A full annotated example is
`config/example.layout`.
## Writing one back out
```bash
slosh -s work cmd '{"cmd":"dump-layout"}' > project.layout
```
writes the session as a layout file: tabs, splits, proportions, directories,
commands, which pane you were in. So a session can be checked in, or put back
after a restart.
`contrib/slosh-dev`
is that loop, for when the thing you are rebuilding is slosh itself.
**A pane is dumped with the command it is running**, whether a layout gave it one
or you typed it. `label`, what a layout said, comes first and outlives the
program that ran it, so a project's `command="npm run dev"` stays that in the file
after the server has exited. Failing that, the pane's terminal is asked what owns
it: a pty has a foreground process group, and that group _is_ the job running in
the pane. So arranging a project by hand and writing it down are one job rather
than two: split, start the dev server and the log tailer, `C-a W`, done.
Three things are deliberately not a command. **A shell at a prompt**, because
`command="zsh"` on every idle pane would restore a session where each shell runs
inside a shell. **A background job**, because it does not own the terminal and a
layout that resurrected `&` jobs in the foreground would describe a session nobody
had. And **an [ephemeral](panes.md#panes-that-were-given-a-command) pane** (the
editor `C-a e` opened) because restoring one reopens a file somebody finished
with.
Argv is joined with shell quoting, since `command=` is handed to `/bin/sh -c`:
`python3 -c 'import x; x.go()'` comes back as one argument rather than three.
What a dump can honestly restore is the _shape_, and the command that made it.
What it cannot is the state inside a program (a shell's history, a half-written
commit message, an editor's undo) and it does not pretend otherwise. A pane
running the session's default shell is dumped as a pane with no command, so
restoring gives you a fresh one.
Three arguments shape what comes out:
```bash
slosh -s work cmd '{"cmd":"dump-layout","tab":3}'
slosh -s work cmd '{"cmd":"dump-layout","relative_to":"~/dev/api","suspend":"commands"}'
```
| argument | what it says |
| ------------- | -------------------------------------------------------- |
| `tab` | a tab **id**, or `0` (the default) for the whole session |
| `relative_to` | a directory every `cwd=` is written relative to |
| `suspend` | which panes come back laid out but not running |
`relative_to` is the writing end of the reading rule above: point it at the
project and the dump comes out with `cwd="."` and `cwd="src"` in it rather than
your home directory, which is what makes the file committable without an edit
afterwards. `suspend` takes four words:
| word | means |
| ---------- | ---------------------------------------------------------------------------------------------------------------- |
| `as-is` | what is suspended in the session right now, the default here, because a session dump should describe the session |
| `none` | everything comes back running |
| `commands` | every pane that was given a command |
| `all` | nothing starts until you touch it |
`commands` is the one you want for a layout you will open often, and it is what
[workspaces](workspaces.md) saves with: the pane running this morning's dev server
belongs in the file asleep, or opening the project starts a dev server every time.
The reply carries the document as `kdl` and counts beside it (`panes` and
`suspended`) so a caller can report "4 panes, 2 suspended" without parsing back
the document it was handed. A `tab` that does not exist is an error rather than an
empty document, because an empty layout is a plausible answer for a tab you closed
a minute ago and a silent one for a typo in an id.
A dump records `focus=true` in **every** tab, not only the one you were looking
at, so restoring a six-tab session puts you back where you were in all six.
## Applying one to a running session
```bash
slosh -s work cmd '{"cmd":"apply-layout","path":"project.layout"}'
slosh -s work cmd '{"cmd":"apply-layout","kdl":"layout { tab { pane } }","replace":true}'
```
Without `replace`, the tabs the file describes are added to what is already
there.
## Moving a pane between tabs
```bash
slosh cmd '{"cmd":"move-pane","id":3,"tab":2}' # beside that tab's focus
slosh cmd '{"cmd":"move-pane","id":3,"tab":2,"dir":"rows"}' # under it instead
slosh cmd '{"cmd":"move-pane","id":3,"tab":0,"name":"logs"}' # into a tab of its own
```
The pane keeps running: same pty, same scrollback, same process. A move is tree
surgery, not a new pane and a funeral. The destination is a tab **id** rather than
an index, because a tab whose last pane leaves is removed and every index after it
shifts; an id survives that.
Two things a move deliberately drops, both of them the old tab's opinion rather
than the pane's: a zoom that named it, and its minimised flag. Carried across, the
first would zoom a pane that has left and the second would file the arrival in a
strip nobody asked for. Which tab you are _looking_ at does not change.
`C-a >` and `C-a <` push the focused pane one tab along, `C-a b` gives it a tab of
its own, and a pane can be dragged onto a tab in the strip; see
[keys](keys.md#defaults) and [the mouse](panes.md#the-mouse).
## Purposes
A pane or tab can carry a `purpose=` label (`agent:main`, `logs`, `db`) for
tooling to find it by. A purpose declared in a layout or over the control API is
_locked_: a program inside the pane cannot overwrite it, so identity comes from
the layout rather than from whatever the program decides to print.
The [finder](panes.md#finding-a-pane) matches on purposes as well as titles, and
`{"cmd":"panes"}` reports them.
## When the layout is found rather than named
Everything above assumes you name the file. When it lives _in_ the project,
`slosh.layout` beside the `.git`, you do not have to: `C-a w` lists the
projects under the roots you configured and opening one applies the layout it
found there, or a default layout bound to that directory when the project has
none, and `C-a W` writes this tab back out as that file. Same syntax, same
checker, same dump; the only difference is that the path comes from the project
instead of from your command line. That is [workspaces](workspaces.md).
|