diff options
| author | user <user@clank> | 2026-08-19 13:46:57 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-19 13:46:57 +0200 |
| commit | 42b3c10995eb67778d9dd3aa2fd80624896acd5f (patch) | |
| tree | 751bcb8604967dc8687cf23fbeb1033aa26ca427 /src | |
| parent | slosh: a website that is a page, not a mock session (diff) | |
| download | slosh-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.c | 15 | ||||
| -rw-r--r-- | src/app.h | 322 | ||||
| -rw-r--r-- | src/cmd.h | 13 | ||||
| -rw-r--r-- | src/config.c | 238 | ||||
| -rw-r--r-- | src/config.h | 500 | ||||
| -rw-r--r-- | src/expr.c | 24 | ||||
| -rw-r--r-- | src/expr.h | 91 | ||||
| -rw-r--r-- | src/graphics.h | 88 | ||||
| -rw-r--r-- | src/input.h | 94 | ||||
| -rw-r--r-- | src/json.h | 28 | ||||
| -rw-r--r-- | src/jsonval.h | 45 | ||||
| -rw-r--r-- | src/kdl.h | 50 | ||||
| -rw-r--r-- | src/osc5577.h | 56 | ||||
| -rw-r--r-- | src/png.h | 22 | ||||
| -rw-r--r-- | src/project.h | 64 | ||||
| -rw-r--r-- | src/proto.h | 68 | ||||
| -rw-r--r-- | src/server.h | 28 | ||||
| -rw-r--r-- | src/shader.c | 2 | ||||
| -rw-r--r-- | src/shader.h | 83 | ||||
| -rw-r--r-- | src/shader_abi.h | 176 | ||||
| -rw-r--r-- | src/slosh.h | 282 |
21 files changed, 250 insertions, 2039 deletions
@@ -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 */ @@ -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 */ |
