aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorsl0p <sl0p@sl0p.foo>2026-08-13 23:27:47 +0200
committersl0p <sl0p@sl0p.foo>2026-08-13 23:27:47 +0200
commit58afd137373fb395fc0479d091c4da9ffa60324a (patch)
tree93839e3c20ff722819444ed22deaef07dc6e6041 /src
parentsl0ptty: M6 — the config file, and a README (diff)
downloadslosh-58afd137373fb395fc0479d091c4da9ffa60324a.tar.gz
slosh-58afd137373fb395fc0479d091c4da9ffa60324a.tar.xz
slosh-58afd137373fb395fc0479d091c4da9ffa60324a.zip
sl0ptty: M7 — layout files and suspended panes
A session can be a file: layout { tab name="api" purpose="project:api.a1b2c3" cwd="~/dev/api" { pane purpose="agent:main" command="pi" pane split="rows" { pane purpose="service:web" command="npm run dev" suspended=true pane purpose="shell:scratch" } } } built by --layout at startup or applied to a running session with {"cmd":"apply-layout", path|kdl, replace?}. Idempotency stays with the caller, which is where sl0ppi's CLI design already puts it: diff list-tabs against the projects on disk, then apply what is missing. close-tab completes that loop. Purposes a layout declares are locked, so identity comes from the file rather than from whatever the program inside prints — tested by having a pane try to take one in-band and be refused. Suspended panes: a pane that exists, is laid out, and has run nothing, showing what it would run and starting on its first keystroke (which is consumed, not forwarded). This is sl0ppi's D6 — a root with twelve projects must not become twelve running servers — and it is the reason can be cheap. Also fixes a test-isolation bug latent since M2: the live tests attached to the session called "main", which is whatever happens to exist on the box rather than one they created. They now use a unique session per run, which is how a run that had somebody else's htop in it became reproducible. 284 checks green (257 headless + 27 live).
Diffstat (limited to 'src')
-rw-r--r--src/app.c199
-rw-r--r--src/app.h8
-rw-r--r--src/cmd.c18
-rw-r--r--src/headless.c7
-rw-r--r--src/main.c13
-rw-r--r--src/pane.c59
-rw-r--r--src/server.c11
-rw-r--r--src/server.h4
-rw-r--r--src/sl0ptty.h7
9 files changed, 314 insertions, 12 deletions
diff --git a/src/app.c b/src/app.c
index ee8e548..0de6781 100644
--- a/src/app.c
+++ b/src/app.c
@@ -10,6 +10,7 @@
#include <string.h>
#include "json.h"
+#include "kdl.h"
/* ---- config ------------------------------------------------------------- */
@@ -142,6 +143,18 @@ static void on_pane_osc(pane_t *p, const char *verb, const char *payload,
if (b.found) app_set_pane_purpose(a, b.found->id, payload, false);
}
+static node_t *leaf_new_ex(app_t *a, const char *const argv[], const char *cwd,
+ bool suspended, const char *label) {
+ pane_t *p = pane_new_ex(argv, 1, 1, cwd, suspended, label);
+ if (!p) return NULL;
+ node_t *n = calloc(1, sizeof *n);
+ n->kind = NODE_LEAF;
+ n->pane = p;
+ n->id = ++a->next_id;
+ pane_set_osc_handler(p, on_pane_osc, a);
+ return n;
+}
+
static node_t *leaf_new(app_t *a) {
pane_t *p = pane_new(a->argv, 1, 1, NULL);
if (!p) return NULL;
@@ -593,6 +606,17 @@ void app_cycle_tab(app_t *a, int delta) {
size_t app_tab_count(const app_t *a) { return a->ntabs; }
+bool app_close_tab(app_t *a, uint32_t id) {
+ for (size_t i = 0; i < a->ntabs; i++) {
+ if (a->tabs[i].id != id) continue;
+ tab_remove(a, i);
+ if (a->ntabs == 0) a->quit = true;
+ else layout(a);
+ return true;
+ }
+ return false;
+}
+
/* D8's trust model: a purpose declared by a layout outranks an in-band one and
* cannot be overridden, so `cat hostile.txt` in a pane cannot relabel a
* project tab. `declared` is only ever true on the control path. */
@@ -667,6 +691,150 @@ uint32_t app_focused_pane_id(app_t *a) {
uint32_t app_current_tab_id(app_t *a) { return a->ntabs ? cur(a)->id : 0; }
+/* ---- layouts ------------------------------------------------------------ *
+ *
+ * layout {
+ * tab name="api" purpose="project:api.a1b2" cwd="/home/user/dev/api" {
+ * pane purpose="agent:main" command="pi" suspended=true
+ * pane split="rows" {
+ * pane command="npm run dev" suspended=true
+ * pane
+ * }
+ * }
+ * }
+ *
+ * A `pane` with children is a split; `split` on it picks the direction. Every
+ * purpose a layout declares is locked, which is the point: identity comes from
+ * the layout, not from whatever the program inside decides to print (D8).
+ */
+
+static node_t *build_pane(app_t *a, const kdl_node_t *node, const char *cwd) {
+ const char *node_cwd = kdl_prop(node, "cwd", cwd);
+
+ /* a split: children, in order, in one direction */
+ size_t kids = 0;
+ for (size_t i = 0; i < node->nkids; i++)
+ if (strcmp(node->kids[i]->name, "pane") == 0) kids++;
+
+ if (kids) {
+ node_t *sp = calloc(1, sizeof *sp);
+ sp->kind = NODE_SPLIT;
+ sp->dir = strcmp(kdl_prop(node, "split", "cols"), "rows") == 0 ? SPLIT_ROWS
+ : SPLIT_COLS;
+ for (size_t i = 0; i < node->nkids; i++) {
+ if (strcmp(node->kids[i]->name, "pane") != 0) continue;
+ node_t *kid = build_pane(a, node->kids[i], node_cwd);
+ if (!kid) continue;
+ sp->kids = realloc(sp->kids, (sp->nkids + 1) * sizeof *sp->kids);
+ sp->kids[sp->nkids++] = kid;
+ kid->parent = sp;
+ }
+ if (sp->nkids == 0) {
+ free(sp);
+ return NULL;
+ }
+ if (sp->nkids == 1) { /* a split of one is just the pane */
+ node_t *only = sp->kids[0];
+ only->parent = NULL;
+ free(sp->kids);
+ free(sp);
+ return only;
+ }
+ return sp;
+ }
+
+ const char *command = kdl_prop(node, "command", NULL);
+ bool suspended = kdl_prop_bool(node, "suspended", false);
+ const char *argv[4];
+ if (command) {
+ argv[0] = "/bin/sh";
+ argv[1] = "-c";
+ argv[2] = command;
+ argv[3] = NULL;
+ } else {
+ argv[0] = a->argv[0];
+ argv[1] = NULL;
+ for (size_t i = 1; a->argv[i] && i < 3; i++) argv[i] = a->argv[i];
+ }
+
+ node_t *leaf = leaf_new_ex(a, command ? argv : a->argv, node_cwd, suspended,
+ command ? command : "");
+ if (!leaf) return NULL;
+ const char *purpose = kdl_prop(node, "purpose", NULL);
+ if (purpose) {
+ sanitise_purpose(purpose, leaf->purpose, sizeof leaf->purpose);
+ leaf->purpose_locked = true; /* declared by a layout: in-band cannot win */
+ }
+ return leaf;
+}
+
+bool app_apply_layout(app_t *a, const kdl_node_t *root, bool replace, char *err,
+ size_t errcap) {
+ const kdl_node_t *lay = kdl_child(root, "layout");
+ if (!lay) lay = root; /* allow a bare list of tabs */
+
+ size_t before = a->ntabs;
+ size_t made = 0;
+ for (size_t i = 0; i < lay->nkids; i++) {
+ const kdl_node_t *t = lay->kids[i];
+ if (strcmp(t->name, "tab") != 0) continue;
+
+ tab_t *tab = tab_add(a, kdl_prop(t, "name", ""));
+ const char *purpose = kdl_prop(t, "purpose", NULL);
+ if (purpose) {
+ sanitise_purpose(purpose, tab->purpose, sizeof tab->purpose);
+ tab->purpose_locked = true;
+ }
+
+ /* the tab body is a split of its pane children */
+ kdl_node_t body = {.name = (char *)"pane", .kids = t->kids,
+ .nkids = t->nkids, .props = t->props,
+ .nprops = t->nprops};
+ node_t *tree = build_pane(a, &body, kdl_prop(t, "cwd", NULL));
+ if (!tree) tree = leaf_new(a);
+ if (!tree) {
+ a->ntabs--;
+ if (err) snprintf(err, errcap, "cannot create panes for tab %zu", i + 1);
+ return false;
+ }
+ tab->root = tree;
+ tab->focus = first_leaf_of(tree);
+ made++;
+ }
+
+ if (!made) {
+ if (err) snprintf(err, errcap, "layout declares no tabs");
+ return false;
+ }
+
+ if (replace) { /* drop the tabs that existed before this layout */
+ for (size_t i = 0; i < before; i++) node_free(a->tabs[i].root);
+ memmove(&a->tabs[0], &a->tabs[before], made * sizeof *a->tabs);
+ a->ntabs = made;
+ }
+ a->cur = replace ? 0 : before;
+ layout(a);
+ return true;
+}
+
+bool app_apply_layout_text(app_t *a, const char *text, bool replace, char *err,
+ size_t errcap) {
+ kdl_node_t *root = kdl_parse(text, err, errcap);
+ if (!root) return false;
+ bool ok = app_apply_layout(a, root, replace, err, errcap);
+ kdl_free(root);
+ return ok;
+}
+
+bool app_apply_layout_file(app_t *a, const char *path, bool replace, char *err,
+ size_t errcap) {
+ kdl_node_t *root = kdl_parse_file(path, err, errcap);
+ if (!root) return false;
+ bool ok = app_apply_layout(a, root, replace, err, errcap);
+ kdl_free(root);
+ return ok;
+}
+
/* ---- focus -------------------------------------------------------------- */
struct dirsearch {
@@ -869,6 +1037,31 @@ static void draw_cb(node_t *n, void *ud) {
action);
draw_frame(d->a, d->s, n);
+ if (pane_suspended(n->pane)) {
+ /* The pane exists, is laid out, and has run nothing. Say what it would. */
+ const char *label = pane_label(n->pane);
+ char line[256];
+ snprintf(line, sizeof line, "press a key to run: %s",
+ label && *label ? label : "shell");
+ /* Truncate rather than vanish: a hint that only appears on wide panes is
+ * worse than a clipped one, and the pane may be narrow precisely because
+ * there are twelve of them. */
+ size_t w = strlen(line);
+ if (w > n->content.w) {
+ w = n->content.w;
+ if (w > 1) {
+ line[w - 1] = 0;
+ line[w - 2] = 0xe2; /* fall back to a plain dot rather than a cut UTF-8 */
+ line[w - 2] = '.';
+ } else {
+ line[w] = 0;
+ }
+ }
+ screen_text(d->s, (uint16_t)(n->content.x + (n->content.w - w) / 2),
+ (uint16_t)(n->content.y + n->content.h / 2), line, FRAME_IDLE,
+ NO_COLOR, 0);
+ return;
+ }
pane_compose(n->pane, d->s, n->content.x, n->content.y, n == cur(d->a)->focus);
}
@@ -1200,6 +1393,11 @@ void app_event(app_t *a, const input_event_t *ev) {
switch (ev->kind) {
case EV_KEY:
+ /* the first keystroke starts a suspended pane, and is not forwarded */
+ if (pane_suspended(cur(a)->focus->pane)) {
+ pane_start(cur(a)->focus->pane);
+ break;
+ }
pane_send_key(cur(a)->focus->pane, ev);
break;
case EV_MOUSE: {
@@ -1239,6 +1437,7 @@ static void panes_cb(node_t *n, void *ud) {
json_int(j, "content_h", n->content.h);
json_bool(j, "focused", n == cur(pj->a)->focus);
json_bool(j, "hidden", n->hidden);
+ json_bool(j, "suspended", pane_suspended(n->pane));
json_str(j, "purpose", n->purpose, strlen(n->purpose));
json_bool(j, "purpose_declared", n->purpose_locked);
json_int(j, "tab", (long long)tab_of(pj->a, n) + 1);
diff --git a/src/app.h b/src/app.h
index e8c00cd..cccfb60 100644
--- a/src/app.h
+++ b/src/app.h
@@ -46,6 +46,7 @@ bool app_select_tab(app_t *a, size_t index);
bool app_select_tab_id(app_t *a, uint32_t id);
void app_cycle_tab(app_t *a, int delta);
size_t app_tab_count(const app_t *a);
+bool app_close_tab(app_t *a, uint32_t id);
bool app_set_tab_name(app_t *a, uint32_t id, const char *name);
/* Purposes (D8). `declared` means "from a layout/control API": it outranks an
@@ -56,6 +57,13 @@ bool app_set_pane_purpose(app_t *a, uint32_t id, const char *purpose,
bool app_set_tab_purpose(app_t *a, uint32_t id, const char *purpose,
bool declared);
+/* Build tabs and panes from a KDL layout. `replace` drops what was there.
+ * Purposes a layout declares are locked (D8). */
+bool app_apply_layout_text(app_t *a, const char *text, bool replace, char *err,
+ size_t errcap);
+bool app_apply_layout_file(app_t *a, const char *path, bool replace, char *err,
+ size_t errcap);
+
/* Address panes by id (the control API), not "the focused one". */
bool app_focus_pane(app_t *a, uint32_t id);
bool app_split_pane(app_t *a, uint32_t id, bool rows);
diff --git a/src/cmd.c b/src/cmd.c
index 5cc25b2..df367db 100644
--- a/src/cmd.c
+++ b/src/cmd.c
@@ -158,6 +158,11 @@ static char *cmd_json(app_t *a, screen_t *s, input_parser_t *in,
: app_select_tab(a, (size_t)(jv_geti(req, "index", 1) - 1));
return ok ? jok_int("id", app_current_tab_id(a)) : jerr("no such tab");
}
+ if (strcmp(cmd, "close-tab") == 0) {
+ return app_close_tab(a, (uint32_t)jv_geti(req, "id", 0))
+ ? jok_int(NULL, 0)
+ : jerr("no such tab");
+ }
if (strcmp(cmd, "set-name") == 0) {
return app_set_tab_name(a, (uint32_t)jv_geti(req, "id", 0),
jv_gets(req, "name", ""))
@@ -176,6 +181,19 @@ static char *cmd_json(app_t *a, screen_t *s, input_parser_t *in,
: app_set_pane_purpose(a, id, purpose, declared);
return ok ? jok_int(NULL, 0) : jerr("refused");
}
+ if (strcmp(cmd, "apply-layout") == 0) {
+ const char *path = jv_gets(req, "path", NULL);
+ const char *text = jv_gets(req, "kdl", NULL);
+ bool replace = jv_getb(req, "replace", false);
+ char err[256] = {0};
+ bool ok = text ? app_apply_layout_text(a, text, replace, err, sizeof err)
+ : path ? app_apply_layout_file(a, path, replace, err, sizeof err)
+ : false;
+ if (!ok) return jerr(err[0] ? err : "need a path or kdl");
+ app_resize(a, s->cols, s->rows);
+ s->force_full = true;
+ return jok_raw("tabs", app_tabs_json(a));
+ }
if (strcmp(cmd, "reload") == 0) {
char err[256] = {0};
if (!app_reload_config(err, sizeof err)) return jerr(err[0] ? err : "reload failed");
diff --git a/src/headless.c b/src/headless.c
index 8be9f87..8fafd05 100644
--- a/src/headless.c
+++ b/src/headless.c
@@ -51,12 +51,17 @@ static void settle(app_t *a, int quiet) {
}
int run_headless(const char *const argv[], uint16_t cols, uint16_t rows,
- int idle_ms, bool script) {
+ int idle_ms, bool script, const char *layout) {
app_t *a = app_new(argv, cols, rows);
if (!a) {
fprintf(stderr, "sl0ptty: cannot spawn pane\n");
return 1;
}
+ if (layout) {
+ char err[256] = {0};
+ if (!app_apply_layout_file(a, layout, true, err, sizeof err))
+ fprintf(stderr, "sl0ptty: %s: %s\n", layout, err[0] ? err : "bad layout");
+ }
app_resize(a, cols, rows);
screen_t s;
screen_init(&s, cols, rows);
diff --git a/src/main.c b/src/main.c
index b36c524..95ea517 100644
--- a/src/main.c
+++ b/src/main.c
@@ -22,7 +22,7 @@
#include "server.h"
int run_headless(const char *const argv[], uint16_t cols, uint16_t rows,
- int idle_ms, bool script);
+ int idle_ms, bool script, const char *layout);
static void term_size(uint16_t *cols, uint16_t *rows) {
struct winsize ws;
@@ -36,7 +36,8 @@ static void term_size(uint16_t *cols, uint16_t *rows) {
}
static void usage(void) {
- fputs("usage: sl0ptty [-s NAME] [ls | cmd LINE | -- CMD...]\n", stderr);
+ fputs("usage: sl0ptty [-s NAME] [--layout FILE] [ls | cmd LINE | -- CMD...]\n",
+ stderr);
}
int main(int argc, char **argv) {
@@ -45,6 +46,7 @@ int main(int argc, char **argv) {
int idle_ms = 300;
const char *name = "main";
const char *cmd_line = NULL;
+ const char *layout = NULL;
bool list = false;
const char *cmd_argv[64];
int cmd_n = 0;
@@ -55,6 +57,7 @@ int main(int argc, char **argv) {
else if (strcmp(a, "--script") == 0) headless = script = true;
else if (strcmp(a, "--server") == 0) server = true;
else if (strcmp(a, "-s") == 0 && i + 1 < argc) name = argv[++i];
+ else if (strcmp(a, "--layout") == 0 && i + 1 < argc) layout = argv[++i];
else if (strcmp(a, "ls") == 0) list = true;
else if (strcmp(a, "cmd") == 0 && i + 1 < argc) cmd_line = argv[++i];
else if (strcmp(a, "--cols") == 0 && i + 1 < argc) cols = (uint16_t)atoi(argv[++i]);
@@ -101,14 +104,14 @@ int main(int argc, char **argv) {
return client_control(fd, cmd_line);
}
- if (headless) return run_headless(cmd_argv, cols, rows, idle_ms, script);
+ if (headless) return run_headless(cmd_argv, cols, rows, idle_ms, script, layout);
if (isatty(STDOUT_FILENO)) term_size(&cols, &rows);
- if (server) return server_run(name, cmd_argv, cols, rows);
+ if (server) return server_run(name, cmd_argv, cols, rows, layout);
/* attach, creating the session if nobody is home */
int fd = server_connect(name);
- if (fd < 0) fd = server_spawn(name, cmd_argv, cols, rows);
+ if (fd < 0) fd = server_spawn(name, cmd_argv, cols, rows, layout);
if (fd < 0) {
fprintf(stderr, "sl0ptty: cannot start session %s\n", name);
return 1;
diff --git a/src/pane.c b/src/pane.c
index 1129665..ac03134 100644
--- a/src/pane.c
+++ b/src/pane.c
@@ -29,6 +29,14 @@ struct pane {
char title[256];
/* OSC 5577 state: what this pane asked us to draw in its frame */
+ /* A suspended pane is real, sized and laid out, but has not run anything:
+ * a root with twelve projects must not become twelve running servers. It
+ * starts on the first keystroke it is given. */
+ bool suspended;
+ char **argv;
+ char *cwd;
+ char label[128];
+
osc_scan_t scan;
char status[256];
pane_button_t buttons[8];
@@ -132,6 +140,49 @@ static void on_title_changed(GhosttyTerminal t, void *ud) {
p->dirty = true;
}
+bool pane_suspended(const pane_t *p) { return p->suspended; }
+const char *pane_label(const pane_t *p) { return p->label; }
+
+/* Spawn what a suspended pane was created to run. */
+bool pane_start(pane_t *p) {
+ if (!p->suspended) return false;
+ p->suspended = false;
+ if (pty_spawn(&p->pty, (const char *const *)p->argv, p->cols, p->rows_n,
+ p->cwd) != 0) {
+ p->alive = false;
+ return false;
+ }
+ p->alive = true;
+ p->dirty = true;
+ return true;
+}
+
+static char **argv_dup(const char *const argv[]) {
+ size_t n = 0;
+ while (argv[n]) n++;
+ char **out = calloc(n + 1, sizeof *out);
+ for (size_t i = 0; i < n; i++) out[i] = strdup(argv[i]);
+ return out;
+}
+
+pane_t *pane_new_ex(const char *const argv[], uint16_t cols, uint16_t rows,
+ const char *cwd, bool suspended, const char *label) {
+ pane_t *p = pane_new(argv, cols, rows, cwd);
+ if (!p) return NULL;
+ snprintf(p->label, sizeof p->label, "%s", label ? label : "");
+ if (suspended) {
+ /* pane_new already spawned; a suspended pane must not have. Close it and
+ * keep the arguments for later, rather than duplicating the constructor. */
+ pty_close(&p->pty);
+ p->pty.fd = -1;
+ p->suspended = true;
+ p->alive = true;
+ p->argv = argv_dup(argv);
+ p->cwd = cwd ? strdup(cwd) : NULL;
+ }
+ return p;
+}
+
pane_t *pane_new(const char *const argv[], uint16_t cols, uint16_t rows,
const char *cwd) {
pane_t *p = calloc(1, sizeof *p);
@@ -190,6 +241,11 @@ fail:
void pane_free(pane_t *p) {
if (!p) return;
+ if (p->argv) {
+ for (size_t i = 0; p->argv[i]; i++) free(p->argv[i]);
+ free(p->argv);
+ }
+ free(p->cwd);
if (p->pty.fd >= 0) pty_close(&p->pty);
if (p->mev) ghostty_mouse_event_free(p->mev);
if (p->menc) ghostty_mouse_encoder_free(p->menc);
@@ -208,6 +264,7 @@ bool pane_dirty(pane_t *p) { return p->dirty; }
const char *pane_title(const pane_t *p) { return p->title; }
ssize_t pane_pump(pane_t *p) {
+ if (p->pty.fd < 0) return 0;
uint8_t buf[65536];
ssize_t total = 0;
for (;;) {
@@ -315,7 +372,7 @@ void pane_resize(pane_t *p, uint16_t cols, uint16_t rows) {
p->cols = cols;
p->rows_n = rows;
ghostty_terminal_resize(p->term, cols, rows, 0, 0);
- pty_resize(&p->pty, cols, rows);
+ if (p->pty.fd >= 0) pty_resize(&p->pty, cols, rows);
p->dirty = true;
}
diff --git a/src/server.c b/src/server.c
index 4326fb2..55696d1 100644
--- a/src/server.c
+++ b/src/server.c
@@ -121,7 +121,7 @@ static void drop_display(server_t *s, uint8_t reason) {
}
int server_run(const char *name, const char *const argv[], uint16_t cols,
- uint16_t rows) {
+ uint16_t rows, const char *layout) {
char path[512];
if (session_socket_path(name, path, sizeof path) != 0) return 1;
int lfd = listen_socket(path);
@@ -139,6 +139,11 @@ int server_run(const char *name, const char *const argv[], uint16_t cols,
server_t s = {0};
s.app = app_new(argv, cols, rows);
if (!s.app) return 1;
+ if (layout) {
+ char err[256] = {0};
+ if (!app_apply_layout_file(s.app, layout, true, err, sizeof err))
+ fprintf(stderr, "sl0ptty: %s: %s\n", layout, err[0] ? err : "bad layout");
+ }
app_resize(s.app, cols, rows);
screen_init(&s.screen, cols, rows);
s.in = input_new();
@@ -299,7 +304,7 @@ int server_run(const char *name, const char *const argv[], uint16_t cols,
/* Fork a server into the background and wait for its socket to answer. */
int server_spawn(const char *name, const char *const argv[], uint16_t cols,
- uint16_t rows) {
+ uint16_t rows, const char *layout) {
char path[512], logp[512];
if (session_socket_path(name, path, sizeof path) != 0) return -1;
session_log_path(name, logp, sizeof logp);
@@ -316,7 +321,7 @@ int server_spawn(const char *name, const char *const argv[], uint16_t cols,
dup2(log >= 0 ? log : null, STDERR_FILENO);
if (null > 2) close(null);
if (log > 2) close(log);
- _exit(server_run(name, argv, cols, rows));
+ _exit(server_run(name, argv, cols, rows, layout));
}
waitpid(pid, NULL, 0); /* the intermediate exits immediately */
diff --git a/src/server.h b/src/server.h
index c81dd33..e2b9224 100644
--- a/src/server.h
+++ b/src/server.h
@@ -9,10 +9,10 @@
/* Run the session in this process (the daemonised half). */
int server_run(const char *name, const char *const argv[], uint16_t cols,
- uint16_t rows);
+ uint16_t rows, const char *layout);
/* Fork a server, then wait for its socket to answer. Returns a connected fd. */
int server_spawn(const char *name, const char *const argv[], uint16_t cols,
- uint16_t rows);
+ uint16_t rows, const char *layout);
/* Connect to an existing session, or -1. */
int server_connect(const char *name);
diff --git a/src/sl0ptty.h b/src/sl0ptty.h
index 828de0e..eab209c 100644
--- a/src/sl0ptty.h
+++ b/src/sl0ptty.h
@@ -123,6 +123,13 @@ typedef void (*pane_osc_fn)(pane_t *p, const char *verb, const char *payload,
pane_t *pane_new(const char *const argv[], uint16_t cols, uint16_t rows,
const char *cwd);
+/* `suspended`: create the pane but run nothing until pane_start(). `label` is
+ * what to show in the meantime (usually the command line). */
+pane_t *pane_new_ex(const char *const argv[], uint16_t cols, uint16_t rows,
+ const char *cwd, bool suspended, const char *label);
+bool pane_suspended(const pane_t *p);
+const char *pane_label(const pane_t *p);
+bool pane_start(pane_t *p);
void pane_free(pane_t *p);
int pane_fd(const pane_t *p);
bool pane_alive(const pane_t *p);