aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-19 13:46:57 +0200
committeruser <user@clank>2026-08-19 13:46:57 +0200
commit42b3c10995eb67778d9dd3aa2fd80624896acd5f (patch)
tree751bcb8604967dc8687cf23fbeb1033aa26ca427 /src
parentslosh: a website that is a page, not a mock session (diff)
downloadslosh-42b3c10995eb67778d9dd3aa2fd80624896acd5f.tar.gz
slosh-42b3c10995eb67778d9dd3aa2fd80624896acd5f.tar.xz
slosh-42b3c10995eb67778d9dd3aa2fd80624896acd5f.zip
slosh: defaults most people can live in, said by shaders
The out-of-the-box look, rethought as one piece: - the compiled-in palette calms down: muted blue #7aa2f7 on the same neutral darks, with the colours that mean something kept semantic -- amber bell, red-orange dead. sl0p stays one include away as the house style, and contrib/themes/default.kdl is the starting point written down (theme-tour shows it). - derived state defaults now actually follow the theme: the scrolled wash is rebuilt from scroll_bg after the theme block (it used to bake the compiled accent), and declaring a state -- in any file of an include chain -- still wins. The dump writes derived states as comments so C-a e cannot pin what is meant to keep following. - a bell you can see: the frame blinks three times in theme's bell colour then breathes until answered, and a soft diagonal shimmer -- the bell colour halved towards default_fg, width breathing on a sine -- crosses the body for the same 900ms. bell_indicator false silences all of it; states { bell { } } replaces it like any state. since carries the clock dependency, so an idle session pays nothing. - scrollback says where it continues: while scrolled, the viewport's top and bottom rows melt towards an edge with content past it and turn solid at the buffer's end. New expression variables above/below (lines hidden past each edge, own dependency bit, cached per scroll position), carried in shade_ctx_t -- SLOSH_SHADER_ABI 3 -> 4. - expressions survive the dump: a program keeps its source and cb_chain writes it back, so amount="(y % 2) * 40" no longer dumps as a number. - headers move to include/, -Iinclude everywhere; the plugin Makefile's SLOSH var becomes SLOSH_DIR because slosh exports SLOSH=1 into every pane it runs, and building from inside a session read that as a path. - comment drift culled where features outran the prose, and the derivation boilerplate folded into derived_pass()/ctx_scroll().
Diffstat (limited to 'src')
-rw-r--r--src/app.c15
-rw-r--r--src/app.h322
-rw-r--r--src/cmd.h13
-rw-r--r--src/config.c238
-rw-r--r--src/config.h500
-rw-r--r--src/expr.c24
-rw-r--r--src/expr.h91
-rw-r--r--src/graphics.h88
-rw-r--r--src/input.h94
-rw-r--r--src/json.h28
-rw-r--r--src/jsonval.h45
-rw-r--r--src/kdl.h50
-rw-r--r--src/osc5577.h56
-rw-r--r--src/png.h22
-rw-r--r--src/project.h64
-rw-r--r--src/proto.h68
-rw-r--r--src/server.h28
-rw-r--r--src/shader.c2
-rw-r--r--src/shader.h83
-rw-r--r--src/shader_abi.h176
-rw-r--r--src/slosh.h282
21 files changed, 250 insertions, 2039 deletions
diff --git a/src/app.c b/src/app.c
index 9a01781..67cada3 100644
--- a/src/app.c
+++ b/src/app.c
@@ -4249,6 +4249,19 @@ static void note_animation(app_t *a, const shader_t *chain, size_t n) {
}
}
+/* Scrollback as the viewport sees it, for content and chrome passes alike, so
+ * an expression can say "there is more this way": lines hidden above the top
+ * edge, and below the bottom. `total` from pane_scroll_pos is already the
+ * scrollable overhang (history minus the viewport), so the split needs no
+ * geometry of its own. */
+static void ctx_scroll(shade_ctx_t *base, const node_t *n) {
+ if (!n->pane) return;
+ uint32_t off = 0, total = 0;
+ pane_scroll_pos(n->pane, &off, &total);
+ base->above = off;
+ base->below = total > off ? total - off : 0;
+}
+
/* A pane with nothing to apply costs nothing: no context is built and no cell
* is visited, so an unshaded session emits the same bytes it did before. */
static void shade_leaf(app_t *a, screen_t *s, node_t *n) {
@@ -4276,6 +4289,7 @@ static void shade_leaf(app_t *a, screen_t *s, node_t *n) {
.default_fg = CFG.default_fg,
.default_bg = CFG.default_bg,
};
+ ctx_scroll(&base, n);
/* The screen's cursor belongs to whichever pane is focused, so it is only
* this pane's cursor when this pane is the focused one. Handing it to any
* other pane would have its spotlight chasing a cursor somewhere else. */
@@ -4328,6 +4342,7 @@ static void shade_chrome(app_t *a, screen_t *s, node_t *n, rect_t r,
.default_fg = CFG.default_fg,
.default_bg = CFG.default_bg,
};
+ ctx_scroll(&base, n);
note_animation(a, chain, nc);
shade_apply(s, chain, nc, r, hole, &base);
}
diff --git a/src/app.h b/src/app.h
deleted file mode 100644
index 2a3435e..0000000
--- a/src/app.h
+++ /dev/null
@@ -1,322 +0,0 @@
-/* The session: a layout tree of panes, focus, and the chrome around them.
- *
- * Both front ends (the interactive client and the headless driver) drive this
- * same object, so there is exactly one implementation of what a key does. */
-#ifndef SLOSH_APP_H
-#define SLOSH_APP_H
-
-#include "kdl.h"
-#include "project.h"
-#include "shader.h"
-#include "slosh.h"
-
-typedef enum { SPLIT_COLS, SPLIT_ROWS } split_dir_t;
-
-typedef struct node node_t;
-typedef struct app app_t;
-
-app_t *app_new(const char *const argv[], uint16_t cols, uint16_t rows);
-void app_free(app_t *a);
-
-void app_event(app_t *a, const input_event_t *ev);
-/* Kitty graphics for this frame: the bytes the client's terminal needs, and
- * the same thing as JSON for tests. Bytes are borrowed until the next call. */
-const char *app_graphics(app_t *a, size_t *len);
-void app_graphics_reset(app_t *a);
-char *app_graphics_json(app_t *a);
-
-/* Transient announcements, drawn bottom-right and expiring on their own. */
-void app_toast(app_t *a, const char *text);
-size_t app_toast_count(app_t *a);
-/* Milliseconds until something needs repainting on its own — a toast expiring,
- * a hover guide arming, or a shader whose amount reads the clock having run
- * over the frame just composed — or -1 when nothing does. A property of that
- * frame, so it is asked after composing one. */
-int app_next_deadline_ms(app_t *a);
-
-/* Text the session has copied, and the copy the client has not been told
- * about yet (which the front end sends on as OSC 52). Caller frees the take. */
-const char *app_clipboard(const app_t *a);
-char *app_take_clipboard(app_t *a);
-
-/* Re-read the config file. Keeps the working one if the new file is broken. */
-bool app_reload_config(char *err, size_t errcap);
-/* The files the config in force was read from: the one that was loaded and
- * everything it included, existing or not. What the watcher watches. */
-size_t app_config_files(const char **out, size_t max);
-/* What the config in force complained about while loading, or "". A complaint is
- * not a failure -- an include that is not there, a shader nobody has heard of --
- * so the session is running either way; this is for saying so. */
-const char *app_config_complaint(void);
-void app_resize(app_t *a, uint16_t cols, uint16_t rows);
-/* The attached client's cell size in pixels, passed on to every pane's pty and
- * terminal. Images need it: a placement that does not say how many cells it
- * covers is sized from the image's pixels and this. Ignored if either is 0,
- * so a client that cannot find out leaves the default standing. */
-void app_set_cell_px(app_t *a, uint16_t w, uint16_t h);
-void app_cell_px(const app_t *a, uint16_t *w, uint16_t *h);
-
-/* Pump every pane that has data. Returns true if anything changed. */
-bool app_pump_fd(app_t *a, int fd);
-/* Fill `out` with every live pane fd; returns the count. */
-size_t app_fds(app_t *a, int *out, size_t max);
-/* Close panes whose process exited, collapsing the tree — only under
- * `keep_dead false`. By default a dead pane stays until it is dismissed, so
- * this does nothing and the pane offers [re-run] and [close] in its frame. */
-void app_reap(app_t *a);
-
-void app_compose(app_t *a, screen_t *s);
-/* Bytes straight into the focused pane's pty (the harness's `raw`). */
-void app_write_focused(app_t *a, const void *buf, size_t len);
-bool app_should_quit(const app_t *a);
-/* C-a d asks the client to leave while the session keeps running. */
-bool app_detach_requested(const app_t *a);
-void app_clear_detach(app_t *a);
-size_t app_pane_count(const app_t *a);
-
-/* Tabs. A tab is a layout tree; panes in every tab keep running. */
-uint32_t app_new_tab(app_t *a, const char *name);
-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);
-/* A pane's name, which outranks the title its program sets. 0 is the focused
- * pane; an empty name clears it and hands the label back to the program. */
-bool app_set_pane_name(app_t *a, uint32_t id, const char *name);
-/* Put a tab at a different position in the strip, 0-based. The strip is the
- * order, so this is what dragging one does; you stay in the tab you were in. */
-bool app_move_tab(app_t *a, uint32_t id, size_t index);
-
-/* Purposes (D8). `declared` means "from a layout/control API": it outranks an
- * in-band purpose and locks it, so a pane cannot relabel itself afterwards --
- * except by being cleared, which unlocks the slot and hands the label back,
- * because a lock over an empty string is a state nothing can escape.
- * `id` 0 is the focused pane and the current tab, as everywhere else.
- * Returns false when a locked purpose refuses an in-band change. */
-bool app_set_pane_purpose(app_t *a, uint32_t id, const char *purpose,
- bool declared);
-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). A relative `cwd=` in a *file*
- * resolves against that file's directory, so a project's layout can be checked
- * in beside the project; text has no directory and keeps meaning what it said. */
-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);
-/* The same, with an explicit base -- for a layout whose text came from one place
- * and whose relative paths mean another: a shared project layout in ~/.config
- * opening a project in ~/dev. */
-bool app_apply_layout_text_at(app_t *a, const char *text, bool replace,
- const char *base, char *err, size_t errcap);
-
-/* What a session would not honour in a layout document, one problem per entry,
- * `file:line: text` -- the same shape `--check` prints for a config, because a
- * project's layout is a file people edit by hand and `cmd=` where `command=`
- * was meant is silently a shell. Returns how many it filled in; `dropped` is
- * how many more there were, so a summary can say so. Neither allocates. */
-#define LAYOUT_MSGS_MAX 32
-typedef char layout_msg_t[192];
-size_t layout_check(const kdl_node_t *root, const char *file,
- layout_msg_t *msgs, size_t max, size_t *dropped);
-size_t layout_check_file(const char *path, layout_msg_t *msgs, size_t max,
- size_t *dropped);
-
-/* ---- workspaces ---------------------------------------------------------- *
- *
- * A project is a directory (project.h). A workspace is the tab it occupies in
- * this session, and membership is that tab's `purpose` in the `project:`
- * namespace -- so there is no second answer to "what is this tab", and a dumped
- * layout restores membership for free because a dump already writes tab purposes
- * and applying one already locks them (D8).
- */
-
-/* Whether the config names anywhere to look. False means the feature is dormant
- * and every verb below says so rather than answering with nothing. */
-bool app_project_roots_set(void);
-/* One project by name, or by a path under a root. False when no root holds it,
- * which is also the containment check: a path this session will not write to. */
-bool app_workspace_find(const char *name, project_t *out);
-/* Every project the configured roots hold, sorted by name. */
-size_t app_projects(project_t *out, size_t max);
-/* The tab holding a workspace, or 0. */
-uint32_t app_workspace_tab(app_t *a, const char *slug);
-
-typedef struct {
- uint32_t tab; /* the tab to land in */
- char purpose[64]; /* the workspace's identity */
- char path[512]; /* the project on disk */
- size_t tabs; /* tabs it adopted */
- size_t honoured; /* tabs whose own declared purpose was left alone */
- bool created; /* false when it was already open and was focused */
-} app_workspace_open_t;
-
-/* Open a project's layout as a workspace, or focus the one already open --
- * idempotent, so the same request twice is one workspace and a caller never has
- * to ask first. `name` is a project name or a path under a root. `suspended`
- * starts every pane asleep whatever the layout said, which is the "open ten
- * projects, run zero processes" case. */
-bool app_workspace_open(app_t *a, const char *name, bool suspended,
- app_workspace_open_t *out, char *err, size_t errcap);
-
-/* Close every tab carrying this workspace's purpose; returns how many. */
-size_t app_workspace_close(app_t *a, const char *slug);
-
-typedef struct {
- char path[512]; /* the file written */
- char purpose[64]; /* the workspace the tab now carries */
- size_t panes, suspended;
- bool replaced; /* there was a layout there before */
-} app_workspace_save_t;
-
-/* Write one tab out as a project's layout: the same dump `dump-layout` answers
- * with, relative to the project, into `slosh.layout.kdl` beside it. `tab` 0 is
- * the current one; `path` names the project for a tab that is not yet a
- * workspace (and saving adopts it, which is the whole of onboarding one).
- * Refused without `force` when the project already has a layout: that file is
- * checked in. */
-bool app_workspace_save(app_t *a, uint32_t tab, const char *path, int suspend,
- bool force, app_workspace_save_t *out, char *err,
- size_t errcap);
-
-/* What this session is called, for the status line. The server knows it
- * because it opened the socket under that name. */
-void app_set_session(app_t *a, const char *name);
-
-/* Put a pane away into the strip along the bottom. 0 means the focused one.
- * False if it would leave the tab with nothing on screen. Restoring is done by
- * focusing it, which is what clicking its row does. */
-bool app_minimize(app_t *a, uint32_t id);
-
-/* Move a pane into another tab, beside whatever that tab has focused. 0 means the
- * focused pane; `rows` puts it under rather than beside. The destination is a tab
- * *id*, not an index: emptying the source tab removes it and shifts every index
- * after it, which an id survives.
- *
- * The pane keeps running -- nothing is re-spawned, which is the point of moving one
- * rather than opening another and closing this. False when there is nothing to do:
- * no such pane, no such tab, or it is already there. Which tab you are looking at
- * does not change.
- *
- * A move drops what the old tab thought about the pane: a zoom naming 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. */
-bool app_move_pane_to_tab(app_t *a, uint32_t pane_id, uint32_t tab_id,
- bool rows);
-
-/* The same, into a tab of its own; returns that tab's id, or 0 -- including for a
- * pane that is already alone in its tab, which has nowhere to go. */
-uint32_t app_move_pane_to_new_tab(app_t *a, uint32_t pane_id, const char *name);
-
-/* Fill the tab with one pane, or put it back. 0 means the focused one. */
-bool app_toggle_zoom(app_t *a, uint32_t id);
-bool app_pane_zoomed(app_t *a, uint32_t id);
-
-/* Give every visible pane in the current tab an even share of the rows and
- * columns it competes for: each split's children weighted by how many visible
- * panes are behind them, so "even" means the same thing at every depth. False
- * when the tab is a single pane and there is nothing to divide. */
-bool app_equalize_splits(app_t *a);
-
-/* 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);
-bool app_close_pane(app_t *a, uint32_t id);
-/* Run a dead (or not-yet-started) pane's command again, in the same pane and
- * on top of the same scrollback. 0 means the focused one. */
-bool app_rerun_pane(app_t *a, uint32_t id);
-
-/* Turn the current tab's layout a quarter turn clockwise: every split changes
- * axis, and the children of a row split reverse, because the pane on top of a
- * stack is the pane on the right once you turn it. Four turns are the identity.
- * False when the tab is a single pane, which looks the same from every angle. */
-bool app_rotate_layout(app_t *a);
-/* Open the config file in $EDITOR, in a pane of its own, so the edit/save/
- * watch loop happens without leaving the session. The pane is ephemeral. */
-bool app_edit_config(app_t *a);
-uint32_t app_focused_pane_id(app_t *a);
-uint32_t app_current_tab_id(app_t *a);
-
-/* Colour passes over one pane (shader.h), set by the program running in it over
- * OSC 5577. `text` is a *document* in the config's own syntax -- one entry, several
- * separated by `;`, or a whole `shaders { }` block -- and each pass goes where its
- * own `where=` says, so both chains are set from one call. `default_chrome` is what
- * a pass that does not say means, which is how a prompt aimed at the frame works
- * without every line repeating itself. Empty text clears both.
- *
- * Counts come back so a caller can say what happened: "ok" alone cannot tell a
- * block that filled both chains from one whose entries were all dropped. Refused
- * with a reason in `err`: `in_band_shaders` off, no such pane, or text that does
- * not read. */
-bool app_set_pane_shaders(app_t *a, uint32_t pane_id, bool default_chrome,
- const char *text, size_t *nchrome, size_t *ncontent,
- char *err, size_t errcap);
-
-/* A preset file -- `contrib/chrome/sine-comet.kdl` and its neighbours -- applied
- * to one pane, both chains at once, routed by each entry's own `where=`. The
- * session reads the file so that nothing else has to know this format; a relative
- * path resolves against the session's working directory. Counts come back so a
- * caller can say what happened. Gated on `in_band_shaders` like setting a chain by
- * hand: it is the same act with a shorter spelling. */
-bool app_load_pane_shaders(app_t *a, uint32_t pane_id, const char *path,
- size_t *nchrome, size_t *ncontent, char *err,
- size_t errcap);
-
-/* Undo both of one pane's own chains. 0 means the focused pane. False when there
- * was nothing to undo, so a caller can say so rather than flash a frame nobody
- * changed.
- *
- * Deliberately not gated on `in_band_shaders`: a chain can outlive the consent
- * that allowed it, and the way out must not be the thing the setting controls.
- * Leaves the config's chains and the session's policy passes alone -- neither is
- * this pane's doing. */
-bool app_clear_pane_shaders(app_t *a, uint32_t pane_id);
-
-/* What to write when a session is written back out. `tab` 0 is every tab;
- * `base` relativises every `cwd=` under it, which is what makes a project's
- * layout the same file on another machine; `suspend` decides which panes are
- * written as not-yet-started. `panes` and `suspended` come back filled in, so
- * a caller can say what it wrote. NULL means all of it, verbatim, absolute. */
-typedef enum {
- DUMP_SUSPEND_ASIS = 0, /* what is suspended now */
- DUMP_SUSPEND_NONE,
- DUMP_SUSPEND_COMMANDS, /* every pane that was given a command */
- DUMP_SUSPEND_ALL
-} dump_suspend_t;
-
-typedef struct {
- uint32_t tab;
- const char *base;
- int suspend;
- /* This dump *is* a project's own layout file, so leave out what the project's
- * location already says: a tab's `project:` purpose is derived from the path,
- * and a checked-in file carrying one would hand another checkout of the same
- * repo the original's identity. Every other purpose is somebody's label and is
- * written as usual. */
- bool for_project;
- /* out. `tabs` is how a caller tells "that tab has no panes" from "there is no
- * such tab", which are the same empty document. */
- size_t tabs, panes, suspended;
-} dump_layout_t;
-
-/* The session written back out as a layout file: tabs, splits, proportions,
- * directories and commands. The inverse of app_apply_layout_text(), and what
- * `dump-layout` and `save-workspace` both answer with -- one of them writes it
- * to a file, which is the only difference between them.
- *
- * What a dump can honestly restore is the shape. Not the state inside a
- * program: a shell's history, a running editor. A pane running the session's
- * default shell is dumped as a pane with no command, so restoring it gives a
- * fresh one, and a pane whose program has exited is dumped as what it ran. It
- * does not pretend otherwise. */
-char *app_dump_layout(app_t *a, dump_layout_t *o);
-
-/* State as JSON, for the control API and the harness. Caller frees. */
-char *app_panes_json(app_t *a);
-char *app_tabs_json(app_t *a);
-
-#endif /* SLOSH_APP_H */
diff --git a/src/cmd.h b/src/cmd.h
deleted file mode 100644
index a54505d..0000000
--- a/src/cmd.h
+++ /dev/null
@@ -1,13 +0,0 @@
-/* The command vocabulary, shared by the headless driver and the control
- * socket, so a script written against one works against the other. */
-#ifndef SLOSH_CMD_H
-#define SLOSH_CMD_H
-
-#include "app.h"
-
-/* Execute one command line. Returns a freshly allocated reply (possibly an
- * empty string), or NULL if the command is unknown. Sets *quit for "quit". */
-char *cmd_exec(app_t *a, screen_t *s, input_parser_t *in, const char *line,
- bool *quit);
-
-#endif /* SLOSH_CMD_H */
diff --git a/src/config.c b/src/config.c
index 48622b2..2d65998 100644
--- a/src/config.c
+++ b/src/config.c
@@ -687,6 +687,120 @@ static void apply_dim_unfocused(config_t *c, bool declared) {
c->state_n[PSTATE_UNFOCUSED] = 1;
}
+/* One derived pass: a shader whose amount is a compiled-in expression,
+ * written into `sh` with its program registered on the config, which owns
+ * every program. The sources are string literals in this file, so a compile
+ * failure is a can't-happen guard rather than a path with a story -- the
+ * caller just stops deriving and ships what it has. */
+static bool derived_pass(config_t *c, shader_t *sh, const char *kind,
+ color_t color, uint8_t channels, const char *src) {
+ shader_make(sh, kind, color, 128);
+ sh->channels = channels;
+ char eerr[128] = {0};
+ expr_prog_t *p = expr_compile(src, eerr, sizeof eerr);
+ if (!p) return false;
+ sh->amount_expr = p;
+ c->exprs = realloc(c->exprs, (c->nexprs + 1) * sizeof *c->exprs);
+ c->exprs[c->nexprs++] = p;
+ return true;
+}
+
+/* The scrolled default, derived *after* the theme block is read rather than
+ * baked from the compiled palette a theme was about to replace. `declared` is
+ * whether the config named `scrolled` itself, in which case its chain --
+ * including an empty one -- stands.
+ *
+ * The wash: the scroll indicator's own colour (theme's scroll_bg), weak
+ * (about 9%) because scrollback is something you are reading through it.
+ *
+ * The edge fades: the viewport's top and bottom rows melt towards an edge
+ * while there is content past it -- `above`/`below` are the lines hidden past
+ * each edge, so `(above > 0)` snaps the top fade off at the top of the
+ * buffer: an edge you cannot scroll past renders solid, which is the whole
+ * message. (Reaching the bottom ends the state itself, same statement.) Up to
+ * three rows a side and never more than a quarter of the viewport each,
+ * because two three-row fades on a six-row viewport is every row faded and
+ * nothing said. They ride after the wash so they dim washed cells rather
+ * than racing it. */
+#define SCROLL_FADE_TOP "(above > 0) * max(0, (min(3, rows / 4) - y) * 45)"
+#define SCROLL_FADE_BOTTOM \
+ "(below > 0) * max(0, (y - rows + min(3, rows / 4) + 1) * 45)"
+
+static void apply_scrolled(config_t *c, bool declared) {
+ if (declared) return;
+ shader_t *chain = c->state_shaders[PSTATE_SCROLLED];
+ c->state_n[PSTATE_SCROLLED] = 0;
+ shader_make(&chain[0], "tint", c->scroll_bg, 22);
+ c->state_n[PSTATE_SCROLLED] = 1;
+ if (!derived_pass(c, &chain[1], "dim", (color_t){0}, 0, SCROLL_FADE_TOP))
+ return;
+ c->state_n[PSTATE_SCROLLED] = 2;
+ if (!derived_pass(c, &chain[2], "dim", (color_t){0}, 0, SCROLL_FADE_BOTTOM))
+ return;
+ c->state_n[PSTATE_SCROLLED] = 3;
+}
+
+/* What an unanswered bell does to the frame of the pane that rang: three
+ * full-strength blinks over the first 900ms -- the part that catches the eye
+ * the moment it rings -- then a slow breathe, so a bell nobody answered stays
+ * findable without nagging. The breathe's phase constant starts it at its
+ * dimmest (sin(since/8 + 158) is about -1 at since=900), so the hand-off from
+ * the last dark blink is a fade-in rather than a pop. */
+#define BELL_FLASH_AMOUNT \
+ "since < 900 ? (since / 150 % 2 == 0) * 255 : 120 + sin(since / 8 + 158) / " \
+ "3"
+
+/* ...and to its body, for those same 900ms and not a millisecond longer: a
+ * shimmer -- one soft diagonal sheen gliding across, the way light crosses
+ * something glossy. Deliberately quiet next to the frame: peak 95 where the
+ * blinks are 255, with a fade slow enough (since / 64) that the band keeps
+ * its strength across the whole sweep and the 900ms cut is what ends it. `x + 2 * y` keeps the band at a true diagonal on
+ * a terminal's 1:2 cells, and motion is the whole trick: the eye is pulled by
+ * something *moving* long before it is pulled by something bright. The band's
+ * width breathes on a sine while it travels -- the falloff slope swings 6±2
+ * over roughly one swell per lifetime (sin is -255..255, so /96 is ±2), about
+ * ±14 to ±27 columns -- which is what makes it read as a living gleam rather
+ * than a ruled stripe. The shimmer is the announcement, the frame the
+ * reminder. */
+#define BELL_SHIMMER_AMOUNT \
+ "(since < 900) * " \
+ "max(0, 95 - abs(x + 2 * y - since / 4) * (6 + sin(since / 2) / 96) - " \
+ "since / 64)"
+
+/* The shimmer's colour: the bell colour softened halfway towards the theme's
+ * default_fg -- light crossing the pane rather than paint laid on it. Derived
+ * from both ends of the theme, so it follows either being changed. */
+static color_t bell_sheen(const config_t *c) {
+ return (color_t){.set = true,
+ .r = (uint8_t)((c->bell.r + c->default_fg.r) / 2),
+ .g = (uint8_t)((c->bell.g + c->default_fg.g) / 2),
+ .b = (uint8_t)((c->bell.b + c->default_fg.b) / 2)};
+}
+
+/* Derived like the scrolled wash, and for the same reason: both passes are in
+ * theme's `bell` colour, so they are built after the theme is read and a
+ * theme moves the mark, the flash and the shimmer together. The frame pass is
+ * `channel="fg"`, because a frame's background is the terminal's own default
+ * and colouring glyphs is the whole job; the body pass keeps both channels,
+ * because most of a body is blank cells whose background is the only thing a
+ * shimmer can travel across. It follows `bell_indicator` off -- that knob's
+ * promise is "silent *and* invisible" -- and a `states { bell { } }` chain
+ * somebody wrote replaces it, like every state. The expressions read `since`,
+ * so the frame clock this costs runs only while a rung pane is on screen. */
+static void apply_bell(config_t *c, bool declared) {
+ if (declared) return;
+ c->state_n[PSTATE_BELL] = 0;
+ c->chrome_state_n[PSTATE_BELL] = 0;
+ if (!c->bell_indicator) return;
+
+ if (derived_pass(c, &c->chrome_state_shaders[PSTATE_BELL][0], "tint", c->bell,
+ SHADE_FG, BELL_FLASH_AMOUNT))
+ c->chrome_state_n[PSTATE_BELL] = 1;
+ if (derived_pass(c, &c->state_shaders[PSTATE_BELL][0], "tint", bell_sheen(c),
+ 0, BELL_SHIMMER_AMOUNT))
+ c->state_n[PSTATE_BELL] = 1;
+}
+
void config_defaults(config_t *c) {
memset(c, 0, sizeof *c);
c->gap = 1;
@@ -777,9 +891,8 @@ void config_defaults(config_t *c) {
c->default_fg = rgb(0xff, 0xff, 0xff);
c->default_bg = rgb(0x00, 0x00, 0x00);
- /* The one state with an opinion out of the box: while a pane is being
- * dragged, everything it could be dropped onto is pushed back so the pane in
- * your hand stands out. Everything else is empty until asked for. */
+ /* While a pane is being dragged, everything it could be dropped onto is
+ * pushed back so the pane in your hand stands out. */
shader_make(&c->state_shaders[PSTATE_DROP_TARGET][0], "grayscale",
(color_t){0}, 200);
shader_make(&c->state_shaders[PSTATE_DROP_TARGET][1], "dim", (color_t){0},
@@ -790,21 +903,12 @@ void config_defaults(config_t *c) {
shader_make(&c->state_shaders[PSTATE_DROP_HOVER][1], "dim", (color_t){0},
140);
c->state_n[PSTATE_DROP_HOVER] = 2;
- /* The states with an opinion, and the line between those and the ones
- * without it:
- *
- * a pane that is *not live* gets one — dead, suspended, scrolled. In all
- * three the cells are showing something other than a running program's
- * present: output from something that has exited, a pane that never
- * started, or the past. You cannot discover any of that by looking unless
- * something says so, and that is the whole argument for the feature;
- *
- * a pane that is merely *not the one you are in* does not — unfocused,
- * dragging. That is ambient contrast, which is a taste, and shipping a
- * taste as a default is how a tool gets a reputation for fighting you.
- *
- * All three are gentle on purpose. A dead pane, a suspended one and
- * scrollback are all things you still want to read. */
+ /* Which states ship a default, and why the line falls where it does, is
+ * argued in full in config/config.kdl (the states section) -- one copy of
+ * that essay is one that cannot drift. The short form: not-live states
+ * (dead, suspended, scrolled) and news (bell) get one; unfocused gets one
+ * through the dim_unfocused knob; dragging gets none. All gentle, because
+ * every one of them is a pane you still want to read. */
shader_make(&c->state_shaders[PSTATE_DEAD][0], "grayscale", (color_t){0},
200);
shader_make(&c->state_shaders[PSTATE_DEAD][1], "dim", (color_t){0}, 90);
@@ -819,7 +923,13 @@ void config_defaults(config_t *c) {
apply_dim_unfocused(c, false);
- const color_t accent = rgb(0xff, 0x5f, 0xd7);
+ /* Muted blue on neutral dark: the accent has to say "this one" on the
+ * focused frame, the guide and the active tab without shouting over the
+ * text it frames, and blue is the one hue that reads as focus/selection
+ * everywhere else, sits apart from the red-orange reserved for `dead`, and
+ * survives the common colour-vision deficiencies doing it. The house pink
+ * lives on as contrib/themes/sl0p.kdl, one include away. */
+ const color_t accent = rgb(0x7a, 0xa2, 0xf7);
const color_t ink = rgb(0x14, 0x14, 0x18);
const color_t dim = rgb(0x45, 0x45, 0x4a);
const color_t bright = rgb(0xff, 0xff, 0xff);
@@ -840,13 +950,9 @@ void config_defaults(config_t *c) {
c->scroll_fg = ink;
c->scroll_bg = accent;
- /* Looking at the past, said in colour as well as in the ▲ count: a wash of
- * the same accent the scroll indicator uses, so a theme moves both together
- * rather than leaving a hardcoded hue nobody can reach. Weak (about 9%) —
- * it has to survive being read through. Attached here rather than beside
- * the other states because it is the one that needs the palette. */
- shader_make(&c->state_shaders[PSTATE_SCROLLED][0], "tint", c->scroll_bg, 22);
- c->state_n[PSTATE_SCROLLED] = 1;
+ /* After scroll_bg above, and re-derived after a config's theme block: the
+ * rationale lives at apply_scrolled. */
+ apply_scrolled(c, false);
c->header = dim;
c->header_hover = accent;
@@ -871,9 +977,17 @@ void config_defaults(config_t *c) {
c->finder_sel_fg = ink;
c->finder_sel_bg = accent;
- c->bell = accent;
+ /* The two colours that mean something rather than match something: a bell
+ * is attention, so amber, not the accent -- painted in the focus colour it
+ * said nothing -- and a dead pane is warm red-orange. Both sit apart from
+ * the blue accent and from each other. */
+ c->bell = rgb(0xe0, 0xaf, 0x68);
c->dead = rgb(0xff, 0x87, 0x5f);
+ /* After c->bell above, and re-derived after a config's theme block: the
+ * rationale lives at apply_bell. */
+ apply_bell(c, false);
+
/* A surface, not a hole: lighter than the dimmed screen behind it, with a
* border in the accent so the edge is never in doubt. The button is a grey
* that is *visible* on that surface — the first version used the pane
@@ -1307,7 +1421,15 @@ static void cb_chain(cfgbuf_t *b, const char *indent, const shader_t *sh,
size_t n, bool chrome) {
for (size_t i = 0; i < n; i++) {
if (!sh[i].kind) continue;
- cb_add(b, "%s%s amount=%u", indent, sh[i].kind, sh[i].amount);
+ /* An expression is written back as the line somebody wrote; only a
+ * numeric amount is a number. Pretending otherwise is how a dumped
+ * config used to lose every expression it contained. */
+ if (sh[i].amount_expr && expr_source(sh[i].amount_expr)) {
+ cb_add(b, "%s%s amount=", indent, sh[i].kind);
+ cb_qval(b, expr_source(sh[i].amount_expr));
+ } else {
+ cb_add(b, "%s%s amount=%u", indent, sh[i].kind, sh[i].amount);
+ }
if (sh[i].color.set)
cb_add(b, " color=\"#%02x%02x%02x\"", sh[i].color.r, sh[i].color.g,
sh[i].color.b);
@@ -1321,6 +1443,33 @@ static void cb_chain(cfgbuf_t *b, const char *indent, const shader_t *sh,
}
}
+/* Whether a state's chain is still the one the loader derives rather than one
+ * somebody wrote: `unfocused` from the dim_unfocused knob, `scrolled` from
+ * theme's scroll_bg. Field-wise rather than memcmp, because a struct copy
+ * carries padding nobody initialised. */
+static bool state_is_derived(const config_t *c, int st) {
+ switch (st) {
+ /* bell and scrolled carry compiled expressions, which cannot be compared
+ * structurally -- but they also cannot be written by a config, so the
+ * declared flags (persisted across an include chain) are the whole test. */
+ case PSTATE_BELL: return !c->bell_declared;
+ case PSTATE_SCROLLED: return !c->scrolled_declared;
+ /* unfocused has no flag -- the knob is re-applied per file -- so it is
+ * compared against what the knob would build: exactly one plain dim at the
+ * knob's strength, or nothing when the knob is 0. */
+ case PSTATE_UNFOCUSED: {
+ if (c->chrome_state_n[st]) return false;
+ if (!c->dim_unfocused) return c->state_n[st] == 0;
+ if (c->state_n[st] != 1) return false;
+ const shader_t *got = &c->state_shaders[st][0];
+ return got->kind && strcmp(got->kind, "dim") == 0 && !got->amount_expr &&
+ got->amount == c->dim_unfocused && !got->color.set && !got->param &&
+ !got->channels;
+ }
+ default: return false;
+ }
+}
+
char *config_render(const config_t *c) {
cfgbuf_t b = {0};
@@ -1462,6 +1611,32 @@ char *config_render(const config_t *c) {
cb_add(&b,
"\n// ---- what a pane looks like in a given state ----\nstates {\n");
for (int st = 0; st < PSTATE_COUNT; st++) {
+ /* The derived states are written as comments while they still match
+ * their derivation: declaring a state replaces its default outright, so a
+ * dump that declared these would pin what is meant to keep following the
+ * dim_unfocused knob and the theme -- and `C-a e` seeds a fresh config
+ * with exactly this text, which is the moment nobody has declared
+ * anything yet. The round trip is unharmed, because loading the comment
+ * derives the identical chain. A chain somebody replaced no longer
+ * matches, and is written out declared, as it must be. */
+ if (state_is_derived(c, st)) {
+ const char *why =
+ st == PSTATE_BELL ? "a body shimmer, and a frame "
+ "blink-then-breathe, in theme's bell "
+ "colour"
+ : st == PSTATE_SCROLLED ? "a wash of theme's scroll_bg, and edge "
+ "fades where more content is"
+ : "the dim_unfocused knob above";
+ cb_add(&b, " // %s { // derived: %s\n",
+ pane_state_name((pane_state_t)st), why);
+ cb_chain(&b, " // ", c->state_shaders[st], c->state_n[st], false);
+ cb_chain(&b, " // ", c->chrome_state_shaders[st],
+ c->chrome_state_n[st], true);
+ if (st == PSTATE_BELL && !c->state_n[st] && !c->chrome_state_n[st])
+ cb_add(&b, " // // nothing: bell_indicator is false\n");
+ cb_add(&b, " // }\n");
+ continue;
+ }
cb_add(&b, " %s {\n", pane_state_name((pane_state_t)st));
cb_chain(&b, " ", c->state_shaders[st], c->state_n[st], false);
cb_chain(&b, " ", c->chrome_state_shaders[st], c->chrome_state_n[st],
@@ -2036,6 +2211,8 @@ static bool load_into(config_t *c, const char *path, int depth, char *err,
continue;
}
if (st == PSTATE_UNFOCUSED) unfocused_declared = true;
+ if (st == PSTATE_SCROLLED) c->scrolled_declared = true;
+ if (st == PSTATE_BELL) c->bell_declared = true;
parse_shader_list(c, k, c->state_shaders[st], &c->state_n[st],
c->chrome_state_shaders[st], &c->chrome_state_n[st],
err, errcap);
@@ -2055,6 +2232,11 @@ static bool load_into(config_t *c, const char *path, int depth, char *err,
"bad colour for %s: %s", THEME_COLORS[i].name, v);
}
}
+ /* After the theme, so the wash follows whatever scroll_bg the theme just
+ * chose -- a config that wrote its own `scrolled` chain keeps it, whether
+ * the chain was written here or in a file this one included. */
+ apply_scrolled(c, c->scrolled_declared);
+ apply_bell(c, c->bell_declared);
const kdl_node_t *keys = kdl_child(root, "keys");
if (keys) {
diff --git a/src/config.h b/src/config.h
deleted file mode 100644
index a8b94f5..0000000
--- a/src/config.h
+++ /dev/null
@@ -1,500 +0,0 @@
-/* The opinionated defaults, and the file that overrides them.
- *
- * Everything here has a working value compiled in, so a missing or broken
- * config file costs you a warning, never a terminal (the fail-open property
- * D9 asks of the CLI, applied to the mux itself).
- */
-#ifndef SLOSH_CONFIG_H
-#define SLOSH_CONFIG_H
-
-#include <stdbool.h>
-#include <stddef.h>
-#include <stdint.h>
-
-#include "expr.h"
-#include "shader.h"
-#include "slosh.h"
-
-/* Pane states a shader can be hung off, in the order they are tested. Only
- * states a pane can actually be seen in — which `dead` now is: a pane whose
- * program exits is kept until it is dismissed, so there is a frame to draw.
- * It ranks just under the drag states, above every ambient one: nothing about
- * a pane is more worth knowing than that it is over. */
-typedef enum {
- PSTATE_DRAGGING, /* the pane you have hold of */
- PSTATE_DROP_HOVER, /* the one under the pointer, where it would land */
- PSTATE_DROP_TARGET, /* the others, all of them somewhere it could go */
- PSTATE_DEAD, /* its program exited; waiting to be re-run or closed */
- PSTATE_SUSPENDED, /* laid out, never started */
- /* A BEL arrived and nobody has looked since. Above the ambient states
- * because it is *news* — the whole point of a bell is to be noticed on a
- * pane you were not watching — and below `dead`, because a pane that rang
- * and then exited is a pane that exited. It ends when you look at the pane,
- * which is what answering a bell is. */
- PSTATE_BELL,
- PSTATE_SCROLLED, /* looking at scrollback rather than the present */
- PSTATE_UNFOCUSED,
- PSTATE_COUNT,
-} pane_state_t;
-
-/* The config name for a state, e.g. "drop_target". */
-const char *pane_state_name(pane_state_t s);
-
-typedef enum {
- ACT_NONE = 0,
- ACT_SPLIT,
- ACT_SPLIT_COLS,
- ACT_SPLIT_ROWS,
- ACT_CLOSE_PANE,
- ACT_RERUN,
- ACT_ZOOM,
- ACT_MINIMIZE,
- ACT_SET_PURPOSE,
- ACT_ROTATE_LAYOUT,
- ACT_FOCUS_LEFT,
- ACT_FOCUS_RIGHT,
- ACT_FOCUS_UP,
- ACT_FOCUS_DOWN,
- ACT_FOCUS_NEXT,
- ACT_NEW_TAB,
- ACT_CLOSE_TAB,
- ACT_NEXT_TAB,
- ACT_PREV_TAB,
- ACT_FINDER,
- ACT_PALETTE,
- ACT_WORKSPACES,
- ACT_SAVE_WORKSPACE,
- ACT_SCROLL_UP,
- ACT_SCROLL_DOWN,
- ACT_SCROLL_PAGE_UP,
- ACT_SCROLL_PAGE_DOWN,
- ACT_SCROLL_TOP,
- ACT_SCROLL_BOTTOM,
- ACT_RESIZE_LEFT,
- ACT_RESIZE_RIGHT,
- ACT_RESIZE_UP,
- ACT_RESIZE_DOWN,
- ACT_EQUALIZE,
- ACT_CLEAR_SHADERS,
- ACT_PANE_TO_NEXT_TAB,
- ACT_PANE_TO_PREV_TAB,
- ACT_PANE_TO_NEW_TAB,
- ACT_DETACH,
- ACT_QUIT,
- ACT_HELP,
- ACT_EDIT_CONFIG,
- ACT_LITERAL_PREFIX,
- ACT_SELECT_TAB_1, /* .. +8, so digits stay one entry each */
-} action_t;
-
-typedef struct {
- int key; /* GhosttyKey */
- uint16_t mods; /* MOD_* */
- action_t action;
- /* Fires without the leader, taking that chord away from every program in
- * every pane. Deliberately possible and deliberately opt-in: it is the
- * user's terminal and their keyboard, and a multiplexer that refuses to get
- * out of the way is its own kind of wrong. */
- bool direct;
-} binding_t;
-
-typedef enum { ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT } align_t;
-
-/* How many files one config may be built from: itself plus its includes. Small
- * and fixed, because a config assembled from more than this many pieces is a
- * thing nobody can read. */
-#define CONFIG_FILES_MAX 16
-
-/* How many project roots one config may name. `~/dev` and `~/work` is the case;
- * a person with more than this many places they keep code has a search problem
- * rather than a list problem. */
-#define PROJECT_ROOTS_MAX 8
-
-/* How many complaints one load keeps. A config with more than this many
- * problems in it has one problem, and the list is long enough to say so. */
-#define CONFIG_MSGS_MAX 32
-
-typedef struct {
- /* geometry */
- uint16_t gap, gap_aspect;
- /* Space between a pane's frame and its contents, per side, in the same unit
- * `gap` uses: ROWS. A horizontal one is multiplied by `gap_aspect` on the way
- * out, so `padding 1` is a square-looking ring rather than a squashed one —
- * and so a number means the same thing wherever it appears, however many of
- * them were written. Written as 1, 2 or 4 values, CSS order. */
- uint16_t pad_top, pad_right, pad_bottom, pad_left;
- bool rounded;
- align_t title_align;
- /* Cells between a frame's corner and the start of its title. The title
- * always carries a space on each side on top of this, so a title can never
- * end up welded to the rule beside it. */
- uint16_t title_inset;
- /* Mark a pane's titlebar when a BEL arrives there and nobody has looked
- * since. Off makes a bell silent *and* invisible, which is a real choice. */
- /* Buttons in the top-right of a pane's frame: zoom, and close. Off leaves
- * the keyboard bindings alone; it is about the affordance, not the verb. */
- /* A word in the middle of the status line for whatever the pointer is on.
- * Discoverability for a frame whose affordances are mostly one character
- * wide; off if you already know them. */
- bool hints;
- /* What slosh this is, in the middle of the status line, whenever there is
- * no hint to put there. The slot is otherwise empty most of the time, and
- * "which build am I attached to" is the first question when a session is
- * behaving oddly -- a session keeps the binary it started with, so the
- * answer is not whatever was built last. */
- bool version_banner;
- bool pane_buttons;
- /* Marks of similar visual weight, so the even spacing between the buttons
- * reads as even: a glyph that floats in a mostly-empty cell donates its own
- * whitespace to the gap beside it and looks further away than its
- * neighbours, which is what an en dash did here. */
- /* 32 bytes because a mark is a grapheme cluster, not a character: a
- family emoji is eighteen bytes on its own. Truncating one mid-
- codepoint used to put invalid UTF-8 on the wire. */
- char zoom_mark[32], zoom_on_mark[32], close_mark[32], min_mark[32];
- /* The new-tab button at the end of the strip. Drawn with a space each side,
- * so a one-character mark is still a three-cell target. */
- char newtab_mark[32];
- bool bell_indicator;
- /* What to draw. A short string rather than a fixed glyph: the obvious
- * choice is an emoji bell, and emoji are drawn two columns wide by many
- * terminals while chrome here is booked as one — so the default is narrow
- * and anyone who knows their terminal can say otherwise. */
- char bell_mark[32];
- /* Which dead panes stay, showing what they printed and offering to run
- * again, and which just go.
- *
- * The line is whether the pane was given a *command*. A pane told to run
- * `npm run dev` is part of the session's shape and its death is news: you
- * want the error, and you want the button that runs it again. A pane that
- * is a shell -- one you split off to do something in -- is finished when
- * you type `exit`, and leaving its corpse on screen to be dismissed is
- * exactly the fussiness a terminal should not have.
- *
- * Not *who made it*: a shell is a shell whether it came from a layout file
- * or from C-a \. That also means a session restored from dump-layout
- * behaves like the one it was dumped from, since the dump writes `command=`
- * only for panes that had one. */
- enum { KEEP_DEAD_NONE, KEEP_DEAD_COMMANDS, KEEP_DEAD_ALL } keep_dead;
- uint16_t min_pane_cols, min_pane_rows;
- /* The smallest pane a split is allowed to *produce*. min_pane is the point
- * below which the layout gives up and collapses a pane; this is the point
- * below which splitting stopped being worth offering, which is a larger
- * number and a different question. Never used below min_pane: refusing to
- * offer something the layout would collapse anyway is the floor. */
- uint16_t min_split_cols, min_split_rows;
- uint16_t scroll_lines; /* rows per wheel notch */
- /* How much history a pane keeps. `scrollback` is the number people mean --
- * lines -- and `scrollback_bytes` is the ceiling that keeps one very wide,
- * heavily styled pane from spending the machine to honour it: lib-vt applies
- * whichever limit is reached first, and both depend on things a line count
- * cannot see. 0 lines is no scrollback; 0 bytes is no ceiling.
- *
- * Not a `uint16_t`, because 65,535 is a number somebody will want to exceed
- * and a silently wrapped limit is worse than a refused one. */
- size_t scrollback;
- size_t scrollback_bytes;
- uint16_t toast_ms; /* how long an announcement stays up */
- uint16_t hover_delay_ms; /* how long the pointer must rest to arm a guide */
- uint16_t double_click_ms; /* how close two clicks must be to be a double */
- /* What a double-click's word stops at, listed as *separators* rather than as
- * word characters: anything unlisted belongs to a word, so text nobody
- * enumerated -- CJK, accents, emoji -- needs no table. Whitespace always
- * separates, listed or not. */
- char word_separators[64];
- /* How far everything behind a modal is pushed back, 0..255, 0 being off.
- * A modal that floats over a screen as bright as it is reads as another
- * pane rather than as something in front. */
- uint8_t modal_scrim;
- /* How far the panes you are *not* in are pushed back, 0..255 and 0 for not
- * at all. A knob rather than only a `states { unfocused { } }` chain,
- * because "dim the other panes" is a thing people want by name and should
- * not have to know the shader vocabulary to ask for -- and because the
- * argument against shipping it on was that turning it off should be
- * obvious, which a single number is and a chain is not.
- *
- * Writing the state chain by hand still wins: the knob only fills it in
- * when the config has not. */
- uint8_t dim_unfocused;
-
- /* Colour passes every pane gets, in the order they were written. Ordinary
- * shaders rather than policy: the session has no opinion about these, you
- * asked for them. */
- shader_t shaders[SHADE_MAX];
- size_t nshaders;
-
- /* What a pane looks like when it is in a particular state. A pane is
- * usually in several at once — unfocused *and* scrolled, suspended *and*
- * something you could drop onto — so exactly one wins, the first that
- * matches in the order below. That order is fixed rather than taken from
- * the config, because it is a ranking by urgency and not a preference: a
- * pane you are holding should not be recoloured by anything, and a mode the
- * whole screen is in outranks an ambient hint about one pane.
- *
- * Stacking them instead would let two reasons to be grey compound into one
- * muddy grey that reads as neither, which is the mistake the drag policy
- * already had to avoid by hand. */
- shader_t state_shaders[PSTATE_COUNT][SHADE_MAX];
- size_t state_n[PSTATE_COUNT];
-
- /* The same two things again, over a pane's *frame* rather than its contents:
- * any entry in `shaders` or in a state's block carrying `where="chrome"`
- * lands here instead. Separate chains rather than a flag on each shader,
- * because a pass is per rect: the contents and the frame are two rects and
- * therefore two passes, and a chain is exactly the list one pass runs. It
- * also keeps the flag out of the plugin ABI, which has no business knowing
- * where a session chose to run an effect. */
- shader_t chrome_shaders[SHADE_MAX];
- size_t nchrome_shaders;
- shader_t chrome_state_shaders[PSTATE_COUNT][SHADE_MAX];
- size_t chrome_state_n[PSTATE_COUNT];
- /* How often to repaint while something on screen is animated -- which means
- * a shader whose amount reads the clock. Nothing else needs a frame clock:
- * every other repaint has an event behind it. 0 turns animation off, in the
- * sense that an animated shader then only advances when something else
- * causes a frame. */
- uint16_t anim_ms;
- bool status_bar; /* the strip along the top: tabs, prefix, pane count */
- bool status_line; /* the line along the bottom: what you are looking at */
- /* How far the strip and the line are held off the left and right edges.
- * Separate from `gap`, which is the space around the *panes*: the two happen
- * to match by default and there is no reason they must. */
- uint16_t status_pad;
- bool focus_follows_mouse;
-
- /* Whether the program running in a pane may set that pane's shader chains
- * over OSC 5577. Off, because a program restyling the session it happens to
- * be running in is a hazard and not a feature (D13) -- and on for anyone
- * prototyping a chain, because the alternative is edit, save, look, and the
- * whole point of a colour pass is that you can see it.
- *
- * Scoped to the pane that asked: it cannot touch another pane, the chrome
- * outside itself, or anything the config said about everybody else. */
- bool in_band_shaders;
-
- /* What a cell's "terminal default" colour resolves to when a shader has to
- * compute on it. We cannot know the client's real default, and most text is
- * default-coloured, so a shader would otherwise leave nearly everything
- * alone. Only consulted while shading: an unshaded pane still defers to the
- * terminal exactly as before. */
- color_t default_fg, default_bg;
-
- /* theme
- *
- * One name per decision. These began as six names doing thirty jobs, which
- * meant the split guide could not be recoloured without also recolouring the
- * focused frame, because they were the same entry. Several still share a
- * default — that is a statement about what looks right together, not about
- * them being the same thing. */
-
- /* panes */
- color_t frame_focus, frame_idle;
- color_t title_focus, title_idle;
-
- /* things you can press: the pane's own OSC 5577 buttons */
- color_t button_fg, button_bg, button_bg_idle;
-
- /* affordances that appear under the pointer */
- color_t guide; /* the split guide: armed edge and dashed boundary */
- color_t resize; /* the handle in the gap between two panes */
- color_t drop_target; /* the frame of a pane a drag could land on */
-
- /* the scrollback indicator in a pane's frame */
- color_t scroll_fg, scroll_bg;
-
- /* rows of a tab that has collapsed into a list */
- color_t header, header_hover, header_hover_title;
-
- /* the strip along the top */
- color_t tab_active_fg, tab_active_bg;
- /* The active tab keeps its fill while the pointer is on it -- it is still
- * the tab you are in -- so its hover shows in the text instead. */
- color_t tab_active_hover_fg;
- color_t tab_idle, tab_hover;
- color_t prefix_fg, prefix_bg; /* the "C-a" badge */
- color_t tab_count; /* how many panes in the session */
-
- /* the line along the bottom */
- color_t status, status_state;
-
- /* the pane finder */
- color_t finder_fg, finder_bg, finder_sel_fg, finder_sel_bg;
-
- /* the in-place editor a double-click opens on a pane's title */
- color_t rename_fg, rename_bg;
-
- /* the bell mark in a pane's titlebar */
- color_t bell;
-
- /* what a dead pane says: the line in its backlog, and the word in the
- * status line and its own frame */
- color_t dead;
-
- /* the frame's own buttons */
- color_t pane_button, pane_button_hover;
-
- /* the row of minimised panes along the bottom */
- color_t minbar, minbar_hover;
-
- /* the hint in the middle of the status line */
- color_t hint;
-
- /* Modals: a surface that floats over the layout, with its own frame. Named
- * separately from the pane colours it resembles because it is a different
- * *surface* — it sits on top of everything, it is opaque, and it has to
- * stay legible against a background that has been dimmed underneath it.
- * Borrowing the pane button colour here is what made the close button
- * invisible: dim on dim. */
- color_t modal_fg, modal_bg;
- color_t modal_border, modal_title;
- color_t modal_button, modal_button_hover;
-
- /* announcements */
- color_t toast_fg, toast_bg;
-
- /* keys */
- int prefix_key;
- uint16_t prefix_mods;
- binding_t *binds;
- size_t nbinds;
-
- /* Every amount expression this config compiled. A shader_t is copied by
- * value all over the place and several copies can point at one program, so
- * ownership lives here — with the config that parsed them, freed when it is
- * replaced. */
- expr_prog_t **exprs;
- size_t nexprs;
-
- /* what a new pane runs; NULL means $SHELL */
- char *shell;
- /* What `edit-config` opens the file with; NULL means $EDITOR, then vi. */
- char *editor;
-
- /* Where to look for shader plugins (`*.so`). NULL means the default: a
- * `shaders` directory beside the config file. Read before the `shaders` and
- * `states` blocks are parsed, because what they are allowed to name depends
- * on what has been loaded. */
- char *shader_dir;
-
- /* Where projects live: `project_roots "~/dev" "~/work" depth=2`. A project is
- * a subdirectory of one of these with a `slosh.layout.kdl` in it -- or a
- * `.git`, which gets `project_layout` instead. Empty means the feature is
- * dormant and costs nothing.
- *
- * `depth` is levels below each root, and a directory that *is* a project is
- * never descended into, so 2 covers `~/dev/work/api` without walking any
- * checkout's insides. */
- char *project_roots[PROJECT_ROOTS_MAX];
- size_t nproject_roots;
- int project_depth;
-
- /* The layout a project with no file of its own opens as. Yours rather than a
- * guess about your stack: relative paths in it bind to whichever project is
- * being opened, so one file is the shape you start every project in. NULL is
- * one pane running your shell. */
- char *project_layout;
-
- /* Every file this config was built from, in the order they were read: the one
- * that was loaded and everything it included, whether or not each existed.
- * Kept because the watcher needs it — a theme you can include is a theme you
- * expect to reload when you save it — and recorded even for a file that is
- * not there, so a session started before you wrote one still notices it
- * appearing. */
- char *files[CONFIG_FILES_MAX];
- size_t nfiles;
-
- /* Everything this load had to complain about, in the order it found them,
- * each already carrying the file and line it happened at. A session shows the
- * first (there is one status line); `--check` shows them all, which is the
- * whole difference between a warning and a linter. */
- char msgs[CONFIG_MSGS_MAX][192];
- size_t nmsgs;
-
- /* Loader scratch: the file being read right now, so a complaint can say which
- * one it came from without every call site being handed the path. Meaningless
- * once loading has finished. */
- const char *loading;
-} config_t;
-
-/* Defaults, then <config dir>/config.kdl on top. Never fails: on a bad file
- * it keeps the defaults and writes the reason to `err`. */
-void config_defaults(config_t *c);
-bool config_load(config_t *c, const char *path, char *err, size_t errcap);
-/* $SLOSH_CONFIG, else $XDG_CONFIG_HOME/slosh/config.kdl, else ~/.config/… */
-const char *config_default_path(void);
-/* Every complaint from the last load, oldest first: "config.kdl:12: ...".
- * Returns how many were written. */
-size_t config_messages(const config_t *c, const char **out, size_t max);
-void config_free(config_t *c);
-/* The files this config was read from, oldest first: `files[0]` is the one that
- * was loaded, the rest are what it included. Returns how many were written. */
-size_t config_files(const config_t *c, const char **out, size_t max);
-
-/* Shader chains from text rather than from a file: one entry, several separated by
- * `;`, or a whole `shaders { ... }` block. Every shape a config file has, because
- * the point of using the config's syntax at a prompt is that the two directions
- * match -- what you prototype is what you paste, and what you paste is something
- * you can type back.
- *
- * Each pass goes where its own `where=` says; `default_chrome` is only what a pass
- * that keeps quiet means. Here because the parser belongs with the config: a chain
- * typed at a running pane and a chain in a config file have to mean the same
- * thing, or prototyping teaches you something that then does not work.
- *
- * Both arrays hold SHADE_MAX and `exprs` twice that; the compiled programs belong
- * to the caller, since a pane's chain outlives no load in particular. Returns how
- * many entries were understood, writing the first refusal to `err`. */
-size_t config_parse_chain_doc(const char *text, color_t default_color,
- bool default_chrome, shader_t *content,
- size_t *ncontent, shader_t *chrome,
- size_t *nchrome, expr_prog_t **exprs,
- size_t *nexprs, char *err, size_t errcap);
-
-/* The same, from a file: a `shaders { }` block as `contrib/chrome` and
- * `contrib/shaders` ship them, read with the parser that already knows this
- * format. Entries are routed to the two chains by their own `where=`, defaulting
- * to content as a config file does, so a preset lands where its author said.
- * Anything else at the top level is ignored -- hand it a whole config.kdl and
- * only the shaders apply. Both arrays hold SHADE_MAX; `exprs` holds twice that.
- * Returns how many entries were understood. */
-size_t config_parse_chain_file(const char *path, color_t default_color,
- shader_t *content, size_t *ncontent,
- shader_t *chrome, size_t *nchrome,
- expr_prog_t **exprs, size_t *nexprs, char *err,
- size_t errcap);
-
-/* Whether a name is one of the config's own settings. For the layout loader, which
- * shares this syntax and needs to tell a config apart from a session file -- one
- * list, so the two answers cannot disagree. */
-bool config_is_setting(const char *name);
-
-/* What a chord does after the leader. Direct bindings answer here too, so
- * pressing the leader first never makes a binding stop working. */
-action_t config_lookup(const config_t *c, int key, uint16_t mods);
-/* What a chord does on its own, with no leader pressed. ACT_NONE for the
- * overwhelming majority of keys, which is what lets them reach the pane. */
-action_t config_lookup_direct(const config_t *c, int key, uint16_t mods);
-/* Whether any binding fires without the leader, so the cheatsheet knows
- * whether it has a section to draw. */
-bool config_has_direct(const config_t *c);
-/* The reverse of config_parse_chord: what to call a binding on screen. A
- * shifted letter is written the way you would type it ("H"), because "S-h" is
- * a description of a keystroke rather than a keystroke. */
-void config_chord_name(int key, uint16_t mods, char *out, size_t cap);
-/* A phrase for the cheatsheet, and which group it belongs under. `group` is
- * one of a fixed set, in the order they should be shown. */
-const char *config_action_label(action_t a);
-/* The name this action is written as in a config file. */
-const char *config_action_name(action_t a);
-/* The whole config as a file you could have written, with the values it
- * currently holds. Generated rather than kept on disk, because a checked-in
- * copy of the defaults is a second source of truth and drifts. Caller frees. */
-char *config_render(const config_t *c);
-/* The same, for a config nobody has edited: what a fresh install would do.
- * Caller frees. */
-char *config_dump_defaults(void);
-const char *config_action_group(action_t a);
-/* "ctrl+a", "alt+left", "\\", "f" -> key + mods. False if unparseable. */
-bool config_parse_chord(const char *text, int *out_key, uint16_t *out_mods);
-
-#endif /* SLOSH_CONFIG_H */
diff --git a/src/expr.c b/src/expr.c
index 0df4a97..578a71b 100644
--- a/src/expr.c
+++ b/src/expr.c
@@ -57,7 +57,9 @@ enum {
V_CURSOR,
V_FOCUSED,
V_T,
- V_SINCE
+ V_SINCE,
+ V_ABOVE,
+ V_BELOW
};
/* Angles are counted in degrees here (see `isin`), and pi is not a number in an
@@ -99,6 +101,12 @@ static const struct {
/* Also a clock, so it carries the same dependency: never cached, and the
* session keeps painting while something reads it. */
{"since", V_SINCE, EXPR_DEP_TIME},
+ /* Scrollback, as the viewport sees it: lines hidden above its top edge,
+ * and below its bottom one. Both 0 at the present. A dependency of their
+ * own rather than a clock: they change per scroll event, not per frame,
+ * so the map is reused while the position holds still. */
+ {"above", V_ABOVE, EXPR_DEP_SCROLL},
+ {"below", V_BELOW, EXPR_DEP_SCROLL},
};
/* cols/rows are a dependency even though the map's key always carries the
@@ -127,10 +135,12 @@ struct expr_prog {
* one place that outlives a frame. The key is every input the compiler says
* the program reads, which is what makes a stale map unrepresentable rather
* than merely unlikely. */
+ char *src; /* what was compiled, for the dump to render back out */
uint8_t *map;
size_t map_cap;
bool map_valid;
int k_cols, k_rows, k_curx, k_cury, k_cursor, k_focused;
+ int k_above, k_below;
};
/* ---- lexer -------------------------------------------------------------- */
@@ -511,6 +521,8 @@ static int vm_run(const expr_prog_t *pr, const expr_env_t *env) {
[V_SINCE] = (int32_t)(env->since < 0 ? 0
: env->since > 0x7fffffff ? 0x7fffffff
: env->since),
+ [V_ABOVE] = env->above,
+ [V_BELOW] = env->below,
};
for (size_t i = 0; i < pr->n; i++) {
@@ -735,15 +747,21 @@ expr_prog_t *expr_compile(const char *src, char *err, size_t errcap) {
pr->value = vm_run(pr, &zero);
pr->constant = true;
}
+ /* The source, kept: a program lives in configs that are dumped back out,
+ * and bytecode cannot be un-compiled into the line somebody wrote. */
+ pr->src = strdup(src);
return pr;
}
void expr_free(expr_prog_t *p) {
if (!p) return;
+ free(p->src);
free(p->map);
free(p);
}
+const char *expr_source(const expr_prog_t *p) { return p ? p->src : NULL; }
+
unsigned expr_deps(const expr_prog_t *p) { return p ? p->deps : 0; }
int expr_constant(const expr_prog_t *p) { return p ? p->value : 0; }
@@ -769,6 +787,8 @@ bool expr_amount_map(expr_prog_t *p, const expr_env_t *env, uint8_t **out) {
same = p->k_curx == env->curx && p->k_cury == env->cury &&
p->k_cursor == env->cursor;
if (same && (p->deps & EXPR_DEP_FOCUS)) same = p->k_focused == env->focused;
+ if (same && (p->deps & EXPR_DEP_SCROLL))
+ same = p->k_above == env->above && p->k_below == env->below;
if (!same) {
if (p->map_cap < cells) {
@@ -792,6 +812,8 @@ bool expr_amount_map(expr_prog_t *p, const expr_env_t *env, uint8_t **out) {
p->k_cury = env->cury;
p->k_cursor = env->cursor;
p->k_focused = env->focused;
+ p->k_above = env->above;
+ p->k_below = env->below;
}
*out = p->map;
return true;
diff --git a/src/expr.h b/src/expr.h
deleted file mode 100644
index c409ded..0000000
--- a/src/expr.h
+++ /dev/null
@@ -1,91 +0,0 @@
-/* Shader expressions: a tiny integer language for the one number a shader
- * takes, compiled to bytecode at config load.
- *
- * The idea is smaller than "shaders in a language". Every built-in shader here
- * is already `colour_op(amount)` where the amount is a pure function of where
- * the cell is:
- *
- * vignette amount = f(x, y) ruler amount = (x == col) * amount
- * zebra amount = f(y) margin amount = (x >= col) * amount
- *
- * So the part worth handing to a config is the *amount*, not the colour maths.
- * That keeps the mixing in C where it is fast and correct, gives an expression
- * no way to produce an invalid cell (it produces one byte), and \u2014 because the
- * result is a pure function of position \u2014 lets the whole program be evaluated
- * once into a per-cell map and then reused, which is what makes an interpreter
- * affordable at all. Measured: 56ns/cell interpreted, 2ns/cell from the map.
- *
- * The language is deliberately total. No loops, no recursion, no jumps: even
- * `?:` compiles to evaluating both sides and selecting, which is safe because
- * nothing in here has an effect. Evaluation is therefore O(program length),
- * a bad expression cannot hang a session, and there is nothing to sandbox.
- *
- * variables x y cols rows curx cury cursor focused t since
- * operators + - * / % < > <= >= == != && || ! ?:
- * functions min max abs clamp dist
- *
- * `x`/`y` are cell positions inside the rect the pass runs over (a pane's
- * contents, or its frame for a chrome pass), `t` is a millisecond clock for
- * animation, `since` is how many milliseconds the pane has been in the state
- * it is in — which is what a one-shot effect needs, because `t` says what time
- * it is and not how long ago something happened. `dist` corrects for a cell
- * being about twice as tall as it is wide. Division and modulo by zero are 0
- * rather than a trap: an expression that is wrong should look wrong, not take
- * the session with it.
- */
-#ifndef SLOSH_EXPR_H
-#define SLOSH_EXPR_H
-
-#include <stdbool.h>
-#include <stddef.h>
-#include <stdint.h>
-
-typedef struct expr_prog expr_prog_t;
-
-/* What a program reads, worked out by the compiler rather than declared. This
- * is what decides whether its output can be cached and against what, so it is
- * derived from the source and can never disagree with it. */
-enum {
- EXPR_DEP_POS = 1 << 0, /* x, y */
- EXPR_DEP_CURSOR = 1 << 1, /* curx, cury, cursor */
- EXPR_DEP_TIME = 1 << 2, /* t: recomputed every frame, never cached */
- EXPR_DEP_FOCUS = 1 << 3, /* focused */
- /* cols, rows. Not part of the cache key -- the key always carries the rect,
- * because a map is per rect by construction -- but it still has to be a
- * dependency, or a program reading only the size would look constant and be
- * folded at compile time, where the size is not known yet and is zero. */
- EXPR_DEP_SIZE = 1 << 4,
-};
-
-/* Compile, or NULL with a reason in `err`. A program that reads nothing is
- * folded to its value at compile time and still returned, so callers have one
- * shape to handle. */
-expr_prog_t *expr_compile(const char *src, char *err, size_t errcap);
-void expr_free(expr_prog_t *p);
-
-/* What it reads (EXPR_DEP_*). 0 means the result is a constant. */
-unsigned expr_deps(const expr_prog_t *p);
-/* The value of a program that reads nothing; undefined otherwise. */
-int expr_constant(const expr_prog_t *p);
-
-/* Everything a program can see. Filled by the shader pass. */
-typedef struct {
- int x, y, cols, rows;
- int curx, cury, cursor;
- int focused;
- int64_t t;
- /* Milliseconds since the pane entered its current state, clamped at 0. */
- int64_t since;
-} expr_env_t;
-
-/* Evaluate for one cell. Clamped to 0..255 by the caller, not here: the
- * language has no opinion about what its number is for. */
-int expr_eval(const expr_prog_t *p, const expr_env_t *env);
-
-/* Evaluate for every cell of a cols x rows rect into `out` (cols*rows bytes,
- * clamped to 0..255), reusing the last result when nothing it reads has
- * changed. Returns false when the program reads the clock, which is the one
- * case there is nothing to reuse. */
-bool expr_amount_map(expr_prog_t *p, const expr_env_t *env, uint8_t **out);
-
-#endif /* SLOSH_EXPR_H */
diff --git a/src/graphics.h b/src/graphics.h
deleted file mode 100644
index b3cd6ec..0000000
--- a/src/graphics.h
+++ /dev/null
@@ -1,88 +0,0 @@
-/* Kitty graphics passthrough.
- *
- * libghostty-vt parses the protocol and tracks every image and placement, so
- * a pane's images exist as data; what a multiplexer has to add is *re-emitting*
- * them to the client's terminal at the right place, with ids that cannot
- * collide between panes.
- *
- * This is why pi has to fall back to sixel under tmux and zellij: they drop
- * the images entirely. Here the native path works.
- */
-#ifndef SLOSH_GRAPHICS_H
-#define SLOSH_GRAPHICS_H
-
-#include <stdbool.h>
-#include <stddef.h>
-#include <stdint.h>
-
-typedef struct graphics graphics_t;
-
-/* One image, as this session knows it. */
-typedef struct {
- uint32_t pane; /* which pane transmitted it */
- uint32_t src_id; /* the id the program chose, unique only within its pane */
- uint32_t out_id; /* the id we give the client, unique across the session */
- uint64_t gen; /* image generation, so a changed image is re-sent */
- bool sent;
-} gfx_image_t;
-
-/* One placement on screen this frame. */
-typedef struct {
- uint32_t out_id;
- uint32_t place_id;
- uint16_t col, row; /* screen cells */
- uint16_t cols, rows;
- /* Where the image starts *inside* its first cell, in pixels. This is what
- * makes motion smooth instead of snapping a cell at a time, and dropping it
- * is invisible in a still picture and obvious the moment anything moves. */
- uint32_t x_off, y_off;
- /* Cells to scale into, or 0 to draw at natural size. Only sent on when the
- * program asked for it. */
- uint16_t scale_cols, scale_rows;
- uint32_t sx, sy, sw, sh; /* source rectangle, in image pixels */
- bool live;
-} gfx_place_t;
-
-graphics_t *gfx_new(void);
-void gfx_free(graphics_t *g);
-
-/* Start a frame: everything placed last frame is marked stale. */
-void gfx_begin(graphics_t *g);
-/* Everything gfx_place() needs: where it goes, what it crops to, and the
- * image itself for the first time it is seen.
- *
- * A struct rather than the twenty positional arguments this used to be. Half
- * of them are uint32_t pixel quantities, so a transposed pair compiles
- * perfectly and shows up as a picture in slightly the wrong place — which is
- * exactly the bug class this file is for. */
-typedef struct {
- uint32_t pane; /* which pane it belongs to */
- uint32_t src_id; /* the id the program chose */
- uint64_t gen; /* image generation, so a changed image is re-sent */
- uint32_t place_id;
- uint16_t col, row; /* screen cells */
- uint16_t cols, rows; /* cells covered */
- uint16_t scale_cols, scale_rows; /* cells to scale into, 0 for natural */
- uint32_t x_off, y_off; /* offset within the first cell, in pixels */
- uint32_t sx, sy, sw, sh; /* source rectangle, in image pixels */
- uint32_t px_w, px_h; /* the image's own size */
- int format, compression;
- const uint8_t *data; /* NULL once the client has it */
- size_t data_len;
-} gfx_req_t;
-
-/* Record a visible placement, transmitting the image if the client has not
- * seen it yet. `data` may be NULL when the image is already transmitted. */
-void gfx_place(graphics_t *g, const gfx_req_t *req);
-/* End a frame: emit transmits, placements and deletions. Caller frees. */
-char *gfx_flush(graphics_t *g, size_t *out_len);
-
-/* Forget what the client has seen (it is a different client now). */
-void gfx_reset(graphics_t *g);
-/* Drop everything belonging to a pane that is gone. */
-void gfx_forget_pane(graphics_t *g, uint32_t pane);
-
-size_t gfx_placements(const graphics_t *g, const gfx_place_t **out);
-size_t gfx_images(const graphics_t *g, const gfx_image_t **out);
-
-#endif /* SLOSH_GRAPHICS_H */
diff --git a/src/input.h b/src/input.h
deleted file mode 100644
index adda27a..0000000
--- a/src/input.h
+++ /dev/null
@@ -1,94 +0,0 @@
-/* Input decoding: the bytes the *outer* terminal sends us -> semantic events.
- *
- * libghostty-vt encodes events outward to a pane, but nothing decodes inward,
- * so this half is ours (DESIGN.md). Decoding to a semantic event and
- * re-encoding per pane against that pane's own modes is what makes kitty
- * keyboard passthrough correct instead of approximate.
- *
- * The event carries GhosttyKey / GHOSTTY_MODS_* values as plain integers so
- * this header stays free of libghostty-vt.
- */
-#ifndef SLOSH_INPUT_H
-#define SLOSH_INPUT_H
-
-#include <stdbool.h>
-#include <stddef.h>
-#include <stdint.h>
-
-typedef enum {
- EV_NONE = 0,
- EV_KEY,
- EV_MOUSE,
- EV_PASTE,
- EV_FOCUS,
-} ev_kind_t;
-
-/* mirrors GhosttyKeyAction */
-enum { KEY_RELEASE = 0, KEY_PRESS = 1, KEY_REPEAT = 2 };
-/* mirrors GhosttyMouseAction */
-enum { MOUSE_PRESS = 0, MOUSE_RELEASE = 1, MOUSE_MOTION = 2 };
-/* mirrors GhosttyMouseButton */
-enum {
- MBTN_UNKNOWN = 0,
- MBTN_LEFT = 1,
- MBTN_RIGHT = 2,
- MBTN_MIDDLE = 3,
- MBTN_FOUR = 4,
- MBTN_FIVE = 5,
- MBTN_SIX = 6,
- MBTN_SEVEN = 7,
-};
-/* mirrors GHOSTTY_MODS_* */
-enum {
- MOD_SHIFT = 1 << 0,
- MOD_CTRL = 1 << 1,
- MOD_ALT = 1 << 2,
- MOD_SUPER = 1 << 3,
- MOD_CAPS = 1 << 4,
- MOD_NUM = 1 << 5,
-};
-
-typedef struct {
- ev_kind_t kind;
-
- /* EV_KEY */
- int key; /* GhosttyKey */
- uint16_t mods; /* MOD_* */
- uint8_t action; /* KEY_* */
- char text[16]; /* UTF-8 the key produced, if any */
- uint8_t text_len;
- uint32_t unshifted; /* unshifted codepoint, 0 if unknown */
-
- /* EV_MOUSE */
- uint8_t button; /* MBTN_* */
- uint8_t maction; /* MOUSE_* */
- uint16_t mx, my; /* 0-indexed cell coordinates */
-
- /* EV_PASTE */
- const char *paste;
- size_t paste_len;
-
- /* EV_FOCUS */
- bool focused;
-} input_event_t;
-
-typedef struct input_parser input_parser_t;
-typedef void (*input_cb_t)(const input_event_t *ev, void *ud);
-
-input_parser_t *input_new(void);
-void input_free(input_parser_t *p);
-
-/* Feed bytes; complete events are dispatched to cb. Partial sequences are
- * retained for the next call, so split reads are safe. */
-void input_feed(input_parser_t *p, const uint8_t *data, size_t len,
- input_cb_t cb, void *ud);
-
-/* True when a bare ESC (or an unterminated sequence) is buffered. Call
- * input_timeout() after ~50ms of quiet to resolve it as the Escape key. */
-bool input_pending(const input_parser_t *p);
-void input_timeout(input_parser_t *p, input_cb_t cb, void *ud);
-
-/* Debug/test rendering of an event, e.g. "key ARROW_UP mods=CTRL press". */
-void input_event_describe(const input_event_t *ev, char *buf, size_t cap);
-
-#endif /* SLOSH_INPUT_H */
diff --git a/src/json.h b/src/json.h
deleted file mode 100644
index f9442ad..0000000
--- a/src/json.h
+++ /dev/null
@@ -1,28 +0,0 @@
-/* A minimal JSON writer. No parser: we only ever emit. */
-#ifndef SLOSH_JSON_H
-#define SLOSH_JSON_H
-
-#include <stdbool.h>
-#include <stddef.h>
-#include <stdint.h>
-
-typedef struct {
- char *buf;
- size_t len, cap;
- bool need_comma;
-} json_t;
-
-void json_init(json_t *j);
-void json_free(json_t *j);
-
-void json_obj_open(json_t *j, const char *key); /* key may be NULL in arrays */
-void json_obj_close(json_t *j);
-void json_arr_open(json_t *j, const char *key);
-void json_arr_close(json_t *j);
-
-void json_str(json_t *j, const char *key, const char *val, size_t len);
-void json_int(json_t *j, const char *key, long long val);
-void json_bool(json_t *j, const char *key, bool val);
-void json_null(json_t *j, const char *key);
-
-#endif /* SLOSH_JSON_H */
diff --git a/src/jsonval.h b/src/jsonval.h
deleted file mode 100644
index 1eedb4c..0000000
--- a/src/jsonval.h
+++ /dev/null
@@ -1,45 +0,0 @@
-/* A small JSON reader, for the control API (D3). We already emit JSON; this
- * is the other half. Deliberately minimal: no streaming, no comments, no
- * duplicate-key policy beyond "last wins". */
-#ifndef SLOSH_JSONVAL_H
-#define SLOSH_JSONVAL_H
-
-#include <stdbool.h>
-#include <stddef.h>
-
-typedef enum {
- JV_NULL,
- JV_BOOL,
- JV_NUM,
- JV_STR,
- JV_ARR,
- JV_OBJ,
-} jv_kind_t;
-
-typedef struct jv jv_t;
-
-struct jv {
- jv_kind_t kind;
- bool b;
- double num;
- char *str; /* JV_STR: NUL-terminated, UTF-8 */
- size_t len; /* JV_STR bytes; JV_ARR/JV_OBJ element count */
- jv_t **kids; /* JV_ARR/JV_OBJ values */
- char **keys; /* JV_OBJ keys */
-};
-
-/* NULL on malformed input. */
-jv_t *jv_parse(const char *text);
-void jv_free(jv_t *v);
-
-const jv_t *jv_get(const jv_t *obj, const char *key);
-const char *jv_str(const jv_t *v, const char *fallback);
-long jv_int(const jv_t *v, long fallback);
-bool jv_bool(const jv_t *v, bool fallback);
-
-/* Convenience: obj.key as a string/int/bool, with a fallback. */
-const char *jv_gets(const jv_t *obj, const char *key, const char *fallback);
-long jv_geti(const jv_t *obj, const char *key, long fallback);
-bool jv_getb(const jv_t *obj, const char *key, bool fallback);
-
-#endif /* SLOSH_JSONVAL_H */
diff --git a/src/kdl.h b/src/kdl.h
deleted file mode 100644
index 9787fd9..0000000
--- a/src/kdl.h
+++ /dev/null
@@ -1,50 +0,0 @@
-/* A hand-rolled KDL subset (D2).
- *
- * Enough of https://kdl.dev for configuration and layouts, and no more:
- *
- * name arg1 "arg two" key=value key2=3 {
- * child "x"; child "y" // single-line children need ; terminators
- * }
- * // line comments, and block comments
- *
- * Values are kept as strings and converted on read, which keeps the parser
- * small and puts type decisions where the meaning is.
- */
-#ifndef SLOSH_KDL_H
-#define SLOSH_KDL_H
-
-#include <stdbool.h>
-#include <stddef.h>
-
-typedef struct {
- char *key;
- char *val;
-} kdl_prop_t;
-
-typedef struct kdl_node {
- char *name;
- char **args;
- size_t nargs;
- kdl_prop_t *props;
- size_t nprops;
- struct kdl_node **kids;
- size_t nkids;
- int line;
-} kdl_node_t;
-
-/* Parses a document into a synthetic root node whose children are the
- * top-level nodes. Returns NULL and fills err (if given) on a syntax error. */
-kdl_node_t *kdl_parse(const char *text, char *err, size_t errcap);
-kdl_node_t *kdl_parse_file(const char *path, char *err, size_t errcap);
-void kdl_free(kdl_node_t *n);
-
-const kdl_node_t *kdl_child(const kdl_node_t *n, const char *name);
-const char *kdl_arg(const kdl_node_t *n, size_t i, const char *fallback);
-long kdl_arg_int(const kdl_node_t *n, size_t i, long fallback);
-bool kdl_arg_bool(const kdl_node_t *n, size_t i, bool fallback);
-const char *kdl_prop(const kdl_node_t *n, const char *key,
- const char *fallback);
-long kdl_prop_int(const kdl_node_t *n, const char *key, long fallback);
-bool kdl_prop_bool(const kdl_node_t *n, const char *key, bool fallback);
-
-#endif /* SLOSH_KDL_H */
diff --git a/src/osc5577.h b/src/osc5577.h
deleted file mode 100644
index e6033f1..0000000
--- a/src/osc5577.h
+++ /dev/null
@@ -1,56 +0,0 @@
-/* OSC 5577: the pane status bar and action buttons.
- *
- * Byte-compatible with the sl0ppi fork (D1), so the pi extensions written
- * against zellij work here unmodified:
- *
- * printf '\033]5577;1;status;building 3/7\033\\'
- * printf '\033]5577;1;buttons;approve:Approve;cancel:Cancel\033\\'
- * printf '\033]5577;1;clear\033\\'
- *
- * and a click comes back on the pane's stdin as
- *
- * \033]5577;1;click;approve\033\\
- *
- * A reply is never a request. Everything the session sends back to a program
- * ends its verb in `-reply` (`hello-reply`, `shader-reply`), and no request verb
- * may, because a pane that echoes what it is sent -- `cat`, a shell with echo
- * on, a REPL waiting for a line -- would otherwise be answered into a loop. The
- * fork's `click` predates the rule and is safe for the weaker reason that
- * nothing answers a click.
- *
- * libghostty-vt's UNKNOWN_SEQUENCE effect reports APC only, not unknown OSC,
- * so we scan the pty stream ourselves. The bytes still go to lib-vt, which
- * discards an OSC it does not know — nothing is drawn, and we need no
- * buffering that could stall a pane's output.
- */
-#ifndef SLOSH_OSC5577_H
-#define SLOSH_OSC5577_H
-
-#include <stdbool.h>
-#include <stddef.h>
-#include <stdint.h>
-
-#define OSC5577_MAX 4096
-
-typedef struct {
- enum { OS_GROUND, OS_ESC, OS_BODY, OS_BODY_ESC } state;
- char buf[OSC5577_MAX];
- size_t len;
- bool overflow;
-} osc_scan_t;
-
-/* verb and payload are NUL-terminated and valid for the callback only. */
-typedef void (*osc5577_fn)(const char *verb, const char *payload, void *ud);
-
-void osc_scan_reset(osc_scan_t *s);
-void osc_scan_feed(osc_scan_t *s, const uint8_t *data, size_t len,
- osc5577_fn cb, void *ud);
-
-/* %3B %3A %25 and friends. Writes at most cap-1 bytes plus a NUL. */
-size_t osc5577_unescape(const char *in, size_t in_len, char *out, size_t cap);
-/* A button id is [A-Za-z0-9_-]{1,32}. Deliberately narrow: ids are echoed
- * back to the program, and a program that trusts its own ids should not have
- * to defend against what a hostile label could smuggle through. */
-bool osc5577_valid_id(const char *id);
-
-#endif /* SLOSH_OSC5577_H */
diff --git a/src/png.h b/src/png.h
deleted file mode 100644
index ddd1b72..0000000
--- a/src/png.h
+++ /dev/null
@@ -1,22 +0,0 @@
-/* PNG decoding for the kitty graphics protocol.
- *
- * libghostty-vt ships no image codec: built as a library it sets its
- * `decode_png` hook to null, and a null hook means every `f=100` transmission
- * is rejected outright — no image stored, no placement, nothing for the
- * graphics layer to re-emit. A program that uploads a PNG (which is what a
- * program with a picture on disk naturally does) therefore drew nothing at
- * all, silently, while raw RGB worked. Installing this hook is the whole fix.
- *
- * PNG is the only format this can be about: the protocol has exactly three
- * (`f=24` RGB, `f=32` RGBA, `f=100` PNG) and the first two are raw pixels the
- * library already handles. Decoding only, never encoding — we hand the client
- * RGBA.
- */
-#ifndef SLOSH_PNG_H
-#define SLOSH_PNG_H
-
-/* Install the process-global PNG decoder. Must be called before any terminal
- * exists, and is idempotent. */
-void png_init(void);
-
-#endif /* SLOSH_PNG_H */
diff --git a/src/project.h b/src/project.h
deleted file mode 100644
index 6bf3021..0000000
--- a/src/project.h
+++ /dev/null
@@ -1,64 +0,0 @@
-/* Projects on disk, and the workspaces they open as.
- *
- * A *project* is a directory somebody works in. A *workspace* is the tab it
- * currently occupies in a session. This file is about the first: finding them,
- * naming them, and saying where each one's layout lives. The second lives in
- * app.c, because a workspace is tabs.
- *
- * The list is derived when it is asked for rather than watched. One readdir per
- * root plus two faccessat per entry is forty checkouts for eighty-one syscalls,
- * which is nothing next to the keystroke that asked -- and an answer nobody
- * remembered cannot be stale, which no watcher can promise on a bind mount.
- */
-#ifndef SLOSH_PROJECT_H
-#define SLOSH_PROJECT_H
-
-#include <stdbool.h>
-#include <stddef.h>
-#include <stdint.h>
-
-/* The file a project uses to say what it needs. One name, so there is no
- * precedence question: a layout file by D2's rule, checked by `--check`, and
- * highlighted by anything that knows KDL. */
-#define PROJECT_LAYOUT_FILE "slosh.layout.kdl"
-
-/* How many projects one session will look at. A picker is not a filesystem
- * browser: past this many the answer to "which project" is a search, not a
- * list, and the list is what this is for. */
-#define PROJECTS_MAX 128
-
-typedef struct {
- char name[64]; /* the directory's own name, which is what people call it */
- char path[512]; /* absolute, resolved */
- char slug[64]; /* `project:name.hash8` -- the purpose a workspace carries */
- /* The project's own layout file, or "" for one inferred from a `.git`. The
- * distinction is the whole of what "declared" means here, and the picker
- * shows it: a project with no layout is an invitation to save one. */
- char layout[512];
- int64_t mtime; /* of `layout`, 0 when there is none. Drift is derivable from
- * this and a pane's `since` without anything storing it. */
-} project_t;
-
-/* Every project under `roots`, sorted by name. `roots` is a NUL-terminated
- * array of directories; `depth` is how many levels below each to look, and a
- * directory that *is* a project is never descended into -- which is what keeps
- * this off node_modules without a rule about node_modules.
- *
- * A root that does not exist is not an error. Neither is one with nothing in
- * it: the setting is a statement of where you keep things, not a promise that
- * you have any. */
-size_t project_scan(const char *const *roots, int depth, project_t *out,
- size_t max);
-
-/* One project by name, or by path when `name` looks like one (it has a `/`).
- * False when no root holds it -- which is also the containment check, since a
- * path that no root holds is a path this session will not write to. */
-bool project_find(const char *const *roots, int depth, const char *name,
- project_t *out);
-
-/* The purpose a workspace for this directory carries: `project:<name>.<hash>`
- * of the resolved path. Hashed on the path rather than the name so that two
- * worktrees of one repo are two workspaces, which is what they are. */
-void project_slug(const char *path, const char *name, char *out, size_t cap);
-
-#endif /* SLOSH_PROJECT_H */
diff --git a/src/proto.h b/src/proto.h
deleted file mode 100644
index e6cd42c..0000000
--- a/src/proto.h
+++ /dev/null
@@ -1,68 +0,0 @@
-/* The client/server wire, and where sessions live.
- *
- * Frames are `u8 type, u32 length (big endian), payload`. Deliberately dull:
- * the interesting protocol is the JSON control channel (M3) that rides on
- * MSG_CMD, and it speaks the same vocabulary as the headless driver.
- */
-#ifndef SLOSH_PROTO_H
-#define SLOSH_PROTO_H
-
-#include <stdbool.h>
-#include <stddef.h>
-#include <stdint.h>
-
-/* MSG_HELLO and MSG_RESIZE carry u16 cols, u16 rows, and since the graphics
- * fix two more: u16 cell_w, u16 cell_h, the client's cell size in pixels.
- * A four-byte payload is still accepted and means "I did not say", which is
- * what an older client sends and what a terminal that reports no pixel size
- * leads to. */
-enum {
- MSG_HELLO = 1, /* client -> server: u16 cols, rows[, cell_w, cell_h] */
- MSG_INPUT = 2, /* client -> server: raw bytes from the terminal */
- MSG_RESIZE = 3, /* client -> server: u16 cols, rows[, cell_w, cell_h] */
- MSG_DETACH = 4, /* client -> server: leave, but keep running */
- MSG_OUTPUT = 5, /* server -> client: bytes for the terminal */
- MSG_EXIT = 6, /* server -> client: u8 reason */
- MSG_CMD = 7, /* client -> server: one JSON line */
- MSG_REPLY = 8, /* server -> client: one JSON line */
-};
-
-enum {
- EXIT_SESSION_ENDED = 0, /* the last pane closed */
- EXIT_DETACHED = 1, /* the user detached */
- EXIT_REPLACED = 2, /* another client attached */
-};
-
-typedef struct {
- uint8_t type;
- uint8_t *data;
- uint32_t len;
-} msg_t;
-
-/* Incremental framing: feed bytes, take whole messages out. */
-typedef struct {
- uint8_t *buf;
- size_t len, cap;
- uint8_t *msg; /* the payload handed to the caller, lifted out of buf */
- size_t msg_cap;
-} msg_reader_t;
-
-void msg_reader_init(msg_reader_t *r);
-void msg_reader_free(msg_reader_t *r);
-void msg_reader_feed(msg_reader_t *r, const uint8_t *data, size_t len);
-/* Returns true and fills `out` (data points into the reader's buffer, valid
- * until the next call) when a whole message is available. */
-bool msg_reader_next(msg_reader_t *r, msg_t *out);
-
-/* Blocking send of one frame. Returns 0 on success, -1 if the peer is gone. */
-int msg_send(int fd, uint8_t type, const void *data, size_t len);
-
-/* $XDG_RUNTIME_DIR/slosh/<name>.sock, or /tmp/slosh-<uid>/<name>.sock.
- * Creates the directory (0700). Returns 0 on success. */
-int session_socket_path(const char *name, char *out, size_t cap);
-int session_log_path(const char *name, char *out, size_t cap);
-/* Names of live sessions (sockets that answer). Caller frees each and the
- * array. Returns the count. */
-size_t session_list(char ***out_names);
-
-#endif /* SLOSH_PROTO_H */
diff --git a/src/server.h b/src/server.h
deleted file mode 100644
index b5d114a..0000000
--- a/src/server.h
+++ /dev/null
@@ -1,28 +0,0 @@
-/* Server and client entry points. See proto.h for the wire. */
-#ifndef SLOSH_SERVER_H
-#define SLOSH_SERVER_H
-
-#include <stdbool.h>
-#include <stdint.h>
-
-#include "slosh.h"
-
-/* Run the session in this process (the daemonised half). `watch` re-reads the
- * config when the file changes; see server.c for why that is a flag and not a
- * config setting. */
-/* What panes are told about the session around them: its name, or NULL for a
- * mode that has no socket, which must clear it rather than inherit a lie. */
-void session_env(const char *name);
-
-int server_run(const char *name, const char *const argv[], uint16_t cols,
- uint16_t rows, const char *layout, bool watch);
-/* 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, const char *layout, bool watch);
-/* Connect to an existing session, or -1. */
-int server_connect(const char *name);
-
-int client_run(int fd);
-int client_control(int fd, const char *line);
-
-#endif /* SLOSH_SERVER_H */
diff --git a/src/shader.c b/src/shader.c
index b748e21..2cd5375 100644
--- a/src/shader.c
+++ b/src/shader.c
@@ -409,6 +409,8 @@ void shade_apply(screen_t *s, const shader_t *shaders, size_t n, rect_t r,
env.focused = ctx.focused;
env.t = ctx.now_ms;
env.since = ctx.state_ms;
+ env.above = (int)ctx.above;
+ env.below = (int)ctx.below;
if (expr_deps(sh->amount_expr) == 0) {
local.amount = (uint8_t)clamp255(expr_constant(sh->amount_expr));
} else {
diff --git a/src/shader.h b/src/shader.h
deleted file mode 100644
index a6d9f65..0000000
--- a/src/shader.h
+++ /dev/null
@@ -1,83 +0,0 @@
-/* Terminal shaders: per-cell colour computation over a pane's CONTENTS.
- *
- * A shader is a pure function from (cell, position, parameters) to that cell's
- * colours. It runs after pane_compose() has written the pane's viewport into
- * the screen and before the split guide is drawn over it, which is what makes
- * "contents, not chrome" fall out of the existing paint order rather than
- * needing a rule: the frame is painted before and lives outside the content
- * rect, and an affordance drawn after stays legible on top of a dimmed pane.
- *
- * Cells only. A shader may write fg, bg and attrs; it may not touch text, len
- * or width. Rewriting text would desync selection and copy (which read the
- * terminal, not the screen) and could break the wide-cell invariant.
- *
- * Each cell is shaded independently — no neighbours — so effects are limited
- * to what position and colour can express. That is most of them: tint, dim,
- * grayscale, gradients, vignettes, anything positional. Convolutions (blur,
- * bloom) would need a second buffer and are deliberately not possible here.
- *
- * Shaders chain: each is a complete pass, so `grayscale` then `tint` differs
- * from `tint` then `grayscale`, and both are expressible.
- */
-#ifndef SLOSH_SHADER_H
-#define SLOSH_SHADER_H
-
-#include <stdbool.h>
-#include <stddef.h>
-#include <stdint.h>
-
-#include "shader_abi.h"
-#include "slosh.h"
-
-/* Per pane, and separately per config. Small and fixed: a stack this deep is
- * already a lot of passes over the same cells. */
-#define SHADE_MAX 4
-/* What one pane can end up running: the ones every pane gets from the config,
- * the ones attached to this pane, and the one or two the session derived for
- * this frame. */
-#define SHADE_CHAIN_MAX (SHADE_MAX * 2 + 2)
-
-/* shade_ctx_t, shader_t and shade_fn are the ABI (shader_abi.h). */
-
-/* Build a shader by registry name. False if the name is not known — a
- * built-in, or one a loaded plugin added. */
-bool shader_make(shader_t *out, const char *kind, color_t color,
- uint8_t amount);
-/* The same, for the shaders that take a number of their own. */
-bool shader_make_p(shader_t *out, const char *kind, color_t color,
- uint8_t amount, uint16_t param);
-/* Iterate the registry: the i'th name, or NULL past the end. Built-ins first,
- * then anything plugins added, in load order. */
-const char *shader_kind(size_t i);
-
-/* Load every `*.so` in `dir` that exports a shader plugin, adding what they
- * declare to the registry. Returns how many shaders were added; a directory
- * that does not exist is not an error (it is the normal case). Failures are
- * per file: a library that cannot be opened, does not export the symbol, or
- * was built against another ABI is reported in `err` and skipped, and the
- * rest still load.
- *
- * Idempotent by path: a file already loaded is not loaded again, so calling
- * this on a config reload picks up newly added plugins and disturbs nothing.
- * Nothing is ever unloaded — a shader's function pointer may be sitting in a
- * live config or on a pane, and dlclose() would turn it into a jump into an
- * unmapped page. Replacing a plugin therefore needs a new session.
- *
- * A plugin is native code with the privileges of the session that loads it.
- * This is deliberately a directory you own, not a config value pointing
- * anywhere: the trust decision is "put a file here". */
-size_t shader_load_dir(const char *dir, char *err, size_t errcap);
-
-/* Run `n` shaders in order over `r`, skipping the cells inside `hole` (NULL
- * for none). `base` supplies everything in the context except the per-cell
- * position and the size.
- *
- * The hole is what makes a pane's *frame* a rect: chrome is the pane's rect
- * minus its contents, and an effect has to see the whole frame's coordinates
- * or it could not travel round one — four separate passes over the four sides
- * would each start counting from zero. Skipped cells are not visited at all,
- * so a chrome pass cannot touch a cell the content pass owns. */
-void shade_apply(screen_t *s, const shader_t *shaders, size_t n, rect_t r,
- const rect_t *hole, const shade_ctx_t *base);
-
-#endif /* SLOSH_SHADER_H */
diff --git a/src/shader_abi.h b/src/shader_abi.h
deleted file mode 100644
index be737c4..0000000
--- a/src/shader_abi.h
+++ /dev/null
@@ -1,176 +0,0 @@
-/* The shader ABI: everything a shader needs to see, and nothing else.
- *
- * This header exists so a shader can be compiled *outside* this tree — a
- * plugin includes only this file, with no libghostty-vt, no pty, no layout.
- * That is also the contract: a shader gets a cell and where it sits, and it
- * cannot reach anything else, because nothing else is declared here.
- *
- * Everything in here is ABI. Changing the layout of these structs breaks
- * every plugin built against the old one, so SLOSH_SHADER_ABI is bumped
- * when they change, and a plugin that reports the wrong version (or the wrong
- * sizes, which catches a plugin built against an edited copy of this file) is
- * refused at load rather than trusted into a crash.
- */
-#ifndef SLOSH_SHADER_ABI_H
-#define SLOSH_SHADER_ABI_H
-
-#include <stdbool.h>
-#include <stddef.h>
-#include <stdint.h>
-
-/* 3: shade_ctx_t gained `state_ms`, so an effect can know how long the pane has
- * been as it is and not only what time it is; struct shader gained
- * `channels`, which is the pass's business rather than a shader's. */
-#define SLOSH_SHADER_ABI 3
-
-/* Which of a cell's two colours a pass is allowed to keep. Enforced by the
- * pass, not by the shader: every shader writes whatever it writes, and the
- * cell's other colour is put back afterwards — so this works for a built-in
- * and for a loaded one, and no shader has to grow a variant of itself.
- *
- * Put back to what it *was*, which for a cell the terminal is drawing in its
- * own default colour means back to unset. That is the difference between
- * recolouring a border glyph and painting a dark rectangle behind it. */
-enum {
- SHADE_FG = 1 << 0,
- SHADE_BG = 1 << 1,
- SHADE_BOTH = SHADE_FG | SHADE_BG, /* and 0 means this, so zeroed is normal */
-};
-
-enum {
- ATTR_BOLD = 1 << 0,
- ATTR_DIM = 1 << 1,
- ATTR_ITALIC = 1 << 2,
- ATTR_UNDERLINE = 1 << 3,
- ATTR_BLINK = 1 << 4,
- ATTR_INVERSE = 1 << 5,
- ATTR_INVISIBLE = 1 << 6,
- ATTR_STRIKE = 1 << 7,
-};
-
-typedef struct {
- bool set; /* false = terminal default */
- uint8_t r, g, b;
-} color_t;
-
-/* A composited cell. `len` bytes of UTF-8; a grapheme cluster may be several
- * codepoints, hence the buffer rather than a codepoint. width 0 means this is
- * the tail half of a wide cell and must not be painted.
- *
- * A shader may write fg, bg and attrs. It may not write text, len or width:
- * selection and copy read the terminal rather than the screen, so rewriting
- * text here would desync them, and changing width would break the invariant
- * the compositor relies on. */
-typedef struct {
- char text[16];
- uint8_t len;
- uint8_t width;
- uint16_t attrs;
- color_t fg, bg;
-} cell_t;
-
-/* Where a cell sits, and what else the effect is allowed to know. */
-typedef struct {
- uint16_t x, y; /* cell position within the content rect, 0-based */
- uint16_t cols, rows; /* content size, so an effect can be positional */
- int64_t now_ms; /* for anything animated */
- /* How long this pane has been in the state it is in, in milliseconds — the
- * bell that just rang, the pane that died a moment ago, the one you have not
- * been in for a while. `now_ms` says what time it is; a one-shot effect needs
- * to know how long ago something happened, and only the session can say. 0
- * when there is nothing to date the state from. */
- int64_t state_ms;
- bool focused;
-
- /* Where the cursor is, in the same rect-relative space as x/y. Only ever
- * set for the pane that owns it, so an effect that follows the cursor does
- * not chase another pane's. */
- bool has_cursor;
- uint16_t cursor_x, cursor_y;
-
- /* What "terminal default" means while shading. A cell whose colour is unset
- * is drawn in whatever the client's terminal calls default, and we never
- * learn that RGB — but most terminal text is default-coloured, so a shader
- * that skipped those cells would visibly do nothing. The pass materialises
- * them to these before running the chain. Configured, because guessing is
- * the one thing that would be worse. */
- color_t default_fg, default_bg;
-} shade_ctx_t;
-
-typedef struct shader shader_t;
-typedef void (*shade_fn)(const shader_t *sh, const shade_ctx_t *ctx, cell_t *c);
-
-/* Opaque to a shader: a compiled amount expression, owned by the config. When
- * one is attached the pass computes `amount` per cell before calling the
- * shader, so an effect never has to know whether its strength was a number in
- * a config file or a function of where the cell is. */
-typedef struct expr_prog expr_prog_t;
-
-struct shader {
- const char *kind; /* registry name, NULL for an empty slot */
- shade_fn fn;
- expr_prog_t *amount_expr; /* NULL when `amount` is just a number */
- color_t color; /* the target colour, for shaders that have one */
- /* Strength, 0..255; 0 is identity, 255 is fully applied. A cell whose
- * strength works out to 0 is skipped by the pass rather than handed to the
- * shader, so a plugin is never called to do nothing -- and a cell the
- * terminal was drawing in its own default colour stays that way. */
- uint8_t amount;
- /* One number whose meaning is the shader's own, because a second parameter
- * that is a column for one effect and a radius for another is not really
- * two things:
- * ruler the column to mark margin first column to dim
- * zebra rows per band spotlight radius in columns
- * gradient 0 down, 1 up, 2 right, 3 left
- */
- uint16_t param;
- /* SHADE_FG / SHADE_BG / SHADE_BOTH, and 0 for both. Opaque to a shader, like
- * `amount_expr` above: it is the pass that keeps a channel, so a shader
- * cannot get it wrong and does not have to know. */
- uint8_t channels;
-};
-
-/* ---- plugins ------------------------------------------------------------
- *
- * A shared library dropped in the shader directory, exporting one symbol:
- *
- * const shader_plugin_t *slosh_shader_plugin(void);
- *
- * SLOSH_SHADER_PLUGIN() writes that for you. See
- * contrib/shader-plugin/ for a complete one.
- */
-
-typedef struct {
- const char *name; /* what to call it in the config */
- shade_fn fn;
-} shader_def_t;
-
-typedef struct {
- uint32_t abi; /* SLOSH_SHADER_ABI it was built against */
- uint32_t cell_size; /* the three sizes catch a plugin built against a */
- uint32_t ctx_size; /* copy of this header that has drifted, which a */
- uint32_t shader_size; /* version number alone would not */
- const char *name; /* the bundle's name, for the log line */
- size_t count;
- const shader_def_t *shaders;
-} shader_plugin_t;
-
-#define SLOSH_SHADER_PLUGIN_SYM "slosh_shader_plugin"
-
-/* Define the entry point. `defs` is a static array of shader_def_t. */
-#define SLOSH_SHADER_PLUGIN(bundle_name, defs) \
- const shader_plugin_t *slosh_shader_plugin(void); \
- const shader_plugin_t *slosh_shader_plugin(void) { \
- static const shader_plugin_t table = { \
- .abi = SLOSH_SHADER_ABI, \
- .cell_size = sizeof(cell_t), \
- .ctx_size = sizeof(shade_ctx_t), \
- .shader_size = sizeof(shader_t), \
- .name = (bundle_name), \
- .count = sizeof(defs) / sizeof((defs)[0]), \
- .shaders = (defs), \
- }; \
- return &table; \
- }
-
-#endif /* SLOSH_SHADER_ABI_H */
diff --git a/src/slosh.h b/src/slosh.h
deleted file mode 100644
index 834879f..0000000
--- a/src/slosh.h
+++ /dev/null
@@ -1,282 +0,0 @@
-/* slosh — common types. See DESIGN.md. */
-#ifndef SLOSH_H
-#define SLOSH_H
-
-#include <stdbool.h>
-#include <stddef.h>
-#include <stdint.h>
-#include <sys/types.h>
-
-#include "input.h"
-/* cell_t, color_t and the ATTR_* flags live in the shader ABI, because that is
- * the one place they are a *contract* with code compiled outside this tree. */
-#include "shader_abi.h"
-
-/* A rectangle of cells. Here rather than in app.h because the layout is not
- * the only thing that has one: a shader pass is given a rect to run over, and
- * the frame pass is given the rect it must skip. */
-typedef struct {
- uint16_t x, y, w, h;
-} rect_t;
-
-/* ---- screen: our composited cell buffer + diff emitter ---------------- */
-
-/* One geometry (DESIGN.md): every painted interactive element records its rect
- * here as it is painted, and a click is a lookup. Drawing and hit-testing
- * cannot disagree because there is only one of them. */
-typedef struct {
- uint16_t x, y, w, h;
- char action[48];
-} hit_t;
-
-typedef struct {
- hit_t *items;
- size_t len, cap;
-} hitlist_t;
-
-void hit_reset(hitlist_t *hl);
-void hit_add(hitlist_t *hl, uint16_t x, uint16_t y, uint16_t w, uint16_t h,
- const char *action);
-/* The action at a point, or NULL. Later entries win: painted last is on top. */
-const char *hit_test(const hitlist_t *hl, uint16_t x, uint16_t y);
-/* The rect a named action was registered with, or NULL: for a caller that must
- * grow or paint over an entry some other pass placed. */
-const hit_t *hit_find(const hitlist_t *hl, const char *action);
-
-typedef struct {
- uint16_t cols, rows;
- cell_t *cur; /* what we want on screen */
- cell_t *prev; /* what the terminal last received */
- bool force_full;
- hitlist_t hits;
-
- /* where the real cursor goes after a flush */
- bool cursor_visible;
- uint16_t cursor_x, cursor_y;
- /* what the terminal was last told, so an unchanged frame emits nothing */
- bool shown_cursor_visible;
- uint16_t shown_cursor_x, shown_cursor_y;
-
- /* output accumulator, flushed in one write() */
- char *out;
- size_t out_len, out_cap;
-} screen_t;
-
-void screen_init(screen_t *s, uint16_t cols, uint16_t rows);
-void screen_free(screen_t *s);
-void screen_resize(screen_t *s, uint16_t cols, uint16_t rows);
-void screen_clear(screen_t *s);
-cell_t *screen_at(screen_t *s, uint16_t x, uint16_t y);
-void screen_put_utf8(screen_t *s, uint16_t x, uint16_t y, const char *txt,
- size_t len, color_t fg, color_t bg, uint16_t attrs);
-/* Columns one grapheme cluster occupies, from lib-vt's own width table. */
-uint8_t screen_width_of(const char *txt, size_t len);
-/* Columns a whole string occupies. What chrome must budget with. */
-uint16_t screen_cells(const char *txt);
-/* Write a UTF-8 string; returns the *columns* written, which is not the number
- * of characters when any of them is wide. */
-uint16_t screen_text(screen_t *s, uint16_t x, uint16_t y, const char *txt,
- color_t fg, color_t bg, uint16_t attrs);
-/* Diff cur against prev into s->out (the minimal byte stream for a terminal). */
-void screen_render(screen_t *s);
-/* screen_render, then write it to fd. */
-void screen_flush(screen_t *s, int fd);
-/* Plain-text dump of the composited screen; caller frees. (headless tests) */
-char *screen_dump(screen_t *s);
-/* The same screen as JSON: rows of text, style runs, cursor, hit-list. */
-char *screen_dump_json(screen_t *s);
-
-/* ---- paths -------------------------------------------------------------- */
-
-/* Expand a leading `~`, using `buf` when it has to. Returns `path` itself when
- * there is nothing to do, so `buf` only has to outlive the result's use. */
-const char *path_expand(const char *path, char *buf, size_t cap);
-/* `~`, then a relative path against `base` -- the directory the file that wrote
- * it lives in, so a layout checked in with a project can say `cwd="."`. A NULL
- * or empty `base` leaves a relative path alone, which is what it meant before
- * there was a base to be relative to. */
-const char *path_resolve(const char *path, const char *base, char *buf,
- size_t cap);
-/* The directory part of a path: the base `path_resolve` wants. `.` when there
- * is no `/` in it. */
-const char *path_dir(const char *path, char *buf, size_t cap);
-/* The inverse of `path_resolve` for writing one back out: a path under `base`
- * relative to it, `.` for `base` itself, and anything not under it unchanged.
- * Points into `path` or a literal, so it needs no buffer. */
-const char *path_relative(const char *path, const char *base);
-/* mkdir -p, for the directory a config or a session socket wants to live in. */
-bool path_mkdirs(const char *dir);
-
-/* ---- pty --------------------------------------------------------------- */
-
-typedef struct {
- int fd;
- pid_t pid;
-} pty_t;
-
-/* `cell_w`/`cell_h` are the client's cell size in pixels, so the pty's
- * winsize carries real pixel dimensions: a program that draws images asks the
- * tty how big a cell is, and zeroes there mean it cannot size one. */
-int pty_spawn(pty_t *p, const char *const argv[], uint16_t cols, uint16_t rows,
- const char *cwd, uint16_t cell_w, uint16_t cell_h);
-int pty_resize(pty_t *p, uint16_t cols, uint16_t rows, uint16_t cell_w,
- uint16_t cell_h);
-void pty_close(pty_t *p);
-
-/* ---- pane: a pty + a libghostty-vt terminal ---------------------------- */
-
-typedef struct pane pane_t;
-
-/* An OSC 5577 action button drawn in a pane's frame. */
-typedef struct {
- char id[33];
- char label[33];
-} pane_button_t;
-
-/* Verbs pane.c does not handle itself (purpose, and whatever comes later). */
-typedef void (*pane_osc_fn)(pane_t *p, const char *verb, const char *payload,
- void *ud);
-/* A program in the pane wrote the clipboard (OSC 52). Takes ownership. */
-typedef void (*pane_clip_fn)(pane_t *p, char *text, void *ud);
-/* A program in the pane sent a notification (OSC 9 / OSC 777). */
-typedef void (*pane_notify_fn)(pane_t *p, const char *title, const char *body,
- void *ud);
-
-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);
-/* A pane opened to do one thing, rather than one the session is made of: an
- * editor you popped open, not the dev server a layout declared. It runs a
- * command like any other, but when that command ends it is finished — so it
- * keeps no corpse and is not written into a dumped layout. */
-void pane_set_ephemeral(pane_t *p, bool yes);
-bool pane_ephemeral(const pane_t *p);
-const char *pane_label(const pane_t *p);
-/* The pid of the program in the pane, or -1. Used to ask the kernel where a
- * shell has got to, which is the only honest answer to "what is this pane's
- * directory" once somebody has typed `cd`. */
-pid_t pane_pid(const pane_t *p);
-/* Where it was started, which is not where it *is*: see pane_pid(). */
-const char *pane_start_cwd(const pane_t *p);
-/* What the pane's terminal is running now, as a command line, or NULL when the
- * answer is "your shell, at a prompt". Read from the pty's foreground process
- * group, so it is the job that owns the terminal rather than a guess: `label`
- * only knows what a layout said, which left a pane you split and typed a command
- * into with nothing to write down. Shell-quoted, because `command=` is run
- * through `/bin/sh -c`. */
-const char *pane_foreground(const pane_t *p, char *buf, size_t cap);
-/* How much history the pane keeps: a line count, and a byte ceiling that stops
- * one wide, heavily styled pane from spending the machine to honour it. 0 lines
- * is no scrollback; 0 bytes is no ceiling. Both are estimates -- lib-vt prunes a
- * page at a time, so a pane keeps a little more than it was told, never less --
- * and lowering either drops history at once, which is what makes this safe to
- * apply to a running pane. */
-void pane_set_scrollback(pane_t *p, size_t lines, size_t bytes);
-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);
-/* How the program ended, for a pane that is no longer alive. `code` is the
- * exit status, or the signal number when `signaled`. False while it is still
- * running, and also when the status could not be collected — a pane can be
- * known to be gone without it being known why. */
-bool pane_exit(const pane_t *p, int *code, bool *signaled);
-/* Run the pane's command again, in the same pty-less pane: same argv, same
- * cwd, same terminal, so the previous run stays above in the scrollback.
- * False if the pane is still alive or the spawn failed. */
-bool pane_restart(pane_t *p);
-/* Write a line into the pane's own terminal — the backlog, not the pty (which
- * by the time this is wanted is closed). It scrolls with the output it
- * followed, which is the point: it is a thing that happened to this pane. */
-void pane_note(pane_t *p, const char *text, color_t fg);
-/* Read available pty output into the terminal. Returns bytes read, 0 on EOF,
- * -1 on error (EAGAIN is reported as 0 bytes with alive still true). A pane
- * that reaches EOF stops being alive here and closes its pty, but keeps its
- * terminal: a dead pane is still a pane with contents you can read. */
-ssize_t pane_pump(pane_t *p);
-void pane_write(pane_t *p, const void *buf, size_t len);
-/* Re-encode a decoded event against this pane's own negotiated modes. */
-void pane_send_key(pane_t *p, const input_event_t *ev);
-void pane_send_mouse(pane_t *p, const input_event_t *ev);
-void pane_send_paste(pane_t *p, const char *text, size_t len);
-void pane_resize(pane_t *p, uint16_t cols, uint16_t rows);
-/* The client's cell size in pixels. Everything about images depends on it:
- * without it a placement that asks for its natural size covers zero cells and
- * is dropped, which is a picture that silently does not appear. */
-void pane_set_cell_px(pane_t *p, uint16_t w, uint16_t h);
-bool pane_dirty(pane_t *p);
-/* Ask for a repaint without having changed a cell: what is drawn *over* the
- * pane changed, and composing is the only thing that can see that. */
-void pane_touch(pane_t *p);
-/* Scrollback. Negative delta scrolls up (towards older output). */
-void pane_scroll(pane_t *p, int delta);
-void pane_scroll_edge(pane_t *p, bool top);
-bool pane_scrolled(const pane_t *p);
-void pane_scroll_pos(const pane_t *p, uint32_t *above, uint32_t *total);
-bool pane_alt_screen(const pane_t *p);
-bool pane_wants_mouse(const pane_t *p);
-const char *pane_title(const pane_t *p);
-/* A BEL arrived and the pane has not been looked at since. */
-bool pane_bell(const pane_t *p);
-void pane_clear_bell(pane_t *p);
-/* The user-assigned name, "" when the program's title is showing through. */
-const char *pane_name(const pane_t *p);
-/* "" clears the name and gives the pane back to the program's title. */
-void pane_set_name(pane_t *p, const char *name);
-const char *pane_status(const pane_t *p);
-size_t pane_buttons(const pane_t *p, const pane_button_t **out);
-void pane_click_button(pane_t *p, const char *id);
-void pane_set_osc_handler(pane_t *p, pane_osc_fn fn, void *ud);
-void pane_set_clipboard_handler(pane_t *p, pane_clip_fn fn, void *ud);
-void pane_set_notify_handler(pane_t *p, pane_notify_fn fn, void *ud);
-
-/* One visible kitty graphics placement, in the pane's viewport coordinates. */
-typedef struct {
- uint32_t image_id, place_id;
- uint16_t col, row, cols, rows;
- /* Where the image starts inside its first cell, in pixels. Sub-cell
- * positioning: what a program moving something smoothly relies on, and the
- * difference between motion and a slideshow. */
- uint32_t x_off, y_off;
- /* What the program asked to scale into, or 0 for "draw it at its natural
- * size". The difference matters on the way out: naming a cell count tells
- * the terminal to *scale* the image into it, and the count a natural
- * placement happens to cover changes by one as it slides across a cell
- * boundary — so passing it on makes a moving image change size. */
- uint16_t req_cols, req_rows;
- uint32_t px_w, px_h; /* rendered size */
- uint32_t src_w, src_h; /* the image's own size */
- /* The part of the image to draw, in pixels. Clipping has to move this:
- * asking for fewer columns alone *scales* the image into them, it does not
- * crop it. */
- uint32_t sx, sy, sw, sh;
- uint32_t cell_px_w, cell_px_h; /* derived: rendered pixels per cell */
- int format, compression;
- uint64_t generation;
- const uint8_t *data; /* borrowed for the callback only */
- size_t data_len;
-} pane_gfx_t;
-
-typedef void (*pane_gfx_fn)(pane_t *p, const pane_gfx_t *g, void *ud);
-size_t pane_graphics(pane_t *p, pane_gfx_fn cb, void *ud);
-
-/* Selection, in the pane's viewport coordinates. */
-void pane_select_start(pane_t *p, uint16_t x, uint16_t y);
-void pane_select_extend(pane_t *p, uint16_t x, uint16_t y);
-void pane_select_clear(pane_t *p);
-/* Select the word under a viewport cell, with `seps` naming what a word stops
- * at. False when there is no word there -- blank, a separator, past the end of
- * the row -- and then the selection is left alone. */
-bool pane_select_word(pane_t *p, uint16_t x, uint16_t y, const char *seps);
-void pane_select_done(pane_t *p);
-bool pane_selecting(const pane_t *p);
-char *pane_selection_text(pane_t *p);
-/* Compose this pane's viewport into the screen at (x0,y0). Clears dirty. */
-void pane_compose(pane_t *p, screen_t *s, uint16_t x0, uint16_t y0,
- bool focused);
-
-#endif /* SLOSH_H */