aboutsummaryrefslogtreecommitdiffstats
path: root/docs/layouts.md
blob: cdb57a09ea57c1864e7bd85f3c2b259bdd61c15b (plain) (blame)
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
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# 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`** — a project's is `slosh.layout`. The syntax is still the same
KDL subset the config uses, one parser reads both files, and for a while the
files were `*.layout.kdl` on the argument that the extension should say so.
The argument lost to a collision: zellij speaks the same syntax under that
same extension with a different schema, and to anyone arriving from there
`.kdl` said *whose* file it is — naming the wrong owner is worse than not
naming the parser. The name says what the file is; `slosh --check` says how
it was read. (Point your editor's KDL highlighting at `*.layout` — it *is*
still KDL underneath.) The old 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 of it is checked as a layout, and a
config handed to the same flag is still checked as a config. A file too broken to
parse at all is judged by its name, because by then there is nothing else left to
read. One flag rather than a `--check-layout` beside it: naming the schema is the
part you wanted the checker to do, and the answer it used to give — `this is a
layout, not a config` — replied to a question about a file with the name of
another flag.

```
$ 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: a property no pane
has, a `split=` that is neither direction, a `weight=` that is not a number the
engine will honour, a `true`/`false` written some other way, a `purpose=` on a
split — which would tag nothing, because a split runs nothing — and a child node
that is not a `pane`. Last comes `this layout declares no tabs`, reported at the
end 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. Both answers come
from the same list of settings the loader reads, so the two cannot disagree about
which file is which.

## 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 (absent, it takes the centred
  default). 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 for configs, applied to the
other half of the same syntax — the two documents share a parser, so they should
not disagree about what a relative path is. 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`. The alternative — absolute
paths, or a `$PROJECT` of our own to expand — makes the file personal to one
machine, or makes you learn a variable to say "here".

A layout that arrives as *text* over the socket has no file, and so no directory
to be relative to. Relative paths in it keep the meaning they had before there
was a base: relative to wherever the session is. Inventing one for text would
mean guessing on behalf of whoever sent it.

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. It used to ask the session's current tab which pane was focused whichever tab
it was writing, so restoring a six-tab session put you back where you were in one
tab and wherever the file happened to begin in the other five.

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

`C-a P` gives the focused pane one from the keyboard — see
[keys](keys.md#defaults) — and **a purpose an operator typed counts as declared,
so it locks like one from a file**. The lock is not about which door the label
came through, it is about the label coming from outside the pane at all: you named
that pane `service:web` on purpose, and the shell in it printing a title is not an
argument for renaming it.

**Setting a purpose to nothing unlocks it**, from a layout, the API or `C-a P`
with the field cleared, and hands the label back to the program to describe
itself again. Before that, a lock held over an empty string was the one state
nothing could get out of: the pane carried no label, and the lock was what kept
the program from supplying one. It is the same shape as clearing a pane's name,
and worth more than a second verb whose whole job is taking a lock off.

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