diff options
| author | user <user@clank> | 2026-08-15 23:06:32 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-15 23:06:32 +0200 |
| commit | f6281fbec93d92bfc7e9164492e436d0236d9179 (patch) | |
| tree | c78b435dec7f62c948e2e8144f29f47ccab2fa7d /src/app.c | |
| parent | sl0ppty: a command palette, on a key you choose (diff) | |
| download | slosh-f6281fbec93d92bfc7e9164492e436d0236d9179.tar.gz slosh-f6281fbec93d92bfc7e9164492e436d0236d9179.tar.xz slosh-f6281fbec93d92bfc7e9164492e436d0236d9179.zip | |
sl0ppty: shaders on the chrome, and two keys for the arrangement
Five things, in one commit because they land in the same few files --
the action table, the shader pass, config.kdl -- and splitting them by
hunk would buy a tidier log at the price of commits that do not build.
Each is verified end to end; the suite is 46 files green.
Chrome shaders. `where="chrome"` on any entry in `shaders` or in a
`states` block runs that pass over a pane's frame -- border, padding,
title, buttons -- instead of its contents. Nothing else changes: same
registry, same `amount`, same expressions, same chain order, one new
word, because a frame is another rect and so it is another pass. It runs
over the frame's whole rect with the contents punched out as a hole,
rather than four passes over four sides: an effect has to be able to
travel round a border, and four passes would each count x/y from zero
and restart at every corner. The hole keeps the two honest -- a skipped
cell is never visited, so a full-strength chrome tint cannot reach your
text. It is also the only kind of pass that can colour a pane a small
window has collapsed to a single row (D6), which is the state where a
colour is worth the most.
`channel="fg"|"bg"|"both"` came out of looking at the first border
flash: `tint` mixes both of a cell's colours, and a frame's background
is the terminal's own default, so the pass materialised it to our guess
at that default and mixed towards the colour -- a dark rectangle behind
the glyphs, left there until the pane changed state. The mask is
enforced by the pass, not the shader: the other colour is snapshotted
before materialising and put back after, so an unset colour goes back to
unset, it works for a built-in and a plugin alike, and no shader grows
an fg-only twin of itself.
A `bell` pane state and a `since` variable, because "flash the border
when a pane rings" needed both and neither existed. `since` is
milliseconds since the pane entered its current state, stamped in the
one function that decides the state -- a transition is the one thing a
frame cannot tell you, so it is remembered where it cannot disagree with
the state it dates. `bell` ranks above the ambient states and below
`dead`: a pane that rang while you were elsewhere is news, and a pane
that rang and then exited is a pane that exited. It ends when you look
at the pane, because that is what answering a bell is.
Animation needed a clock and did not have one: a shader reading `t` only
advanced when something else caused a frame, so a pulse froze the moment
you stopped typing. `app_next_deadline_ms` now reports `anim_ms` while a
chain that reads the clock ran over the last frame -- derived from what
was applied, so a pulse hung off `dead` costs a clock while a pane is
dead and nothing for the rest of the session. A `deadline` verb makes
that observable; the server's poll loop already lived on the number.
contrib/chrome has seven presets and the perimeter trick they are built
on: on a frame the coordinate you want is distance along the ring, which
the language can work out, and `min(d, P - d)` is what makes an effect
cross the corner it started from. Hand-written, unlike contrib/shaders,
because the toy previews contents and a frame has no preview to give --
so tests/test_chrome_presets.py checks each one the only way left: load
it into a real session, assert the frame moves and the contents do not.
`equalize` (C-a =) puts every share back to even: each split's children
weighted by how many visible panes are behind them, so one pane beside a
stack of three is a quarter of the width and "even" means the same thing
at every depth. Exactly equal cell counts are not something a tree can
always express, so the action promises the even division it can.
`rotate-layout` (C-a Space) turns the whole tab a quarter turn
clockwise. Every split changes axis and a row split's children reverse,
because the pane on top of a stack is the pane on the right once you
turn it; get that asymmetry wrong and you have a mirror image, which
looks almost right until four turns fail to come back. They do come
back, exactly, which is why the inverse is three more turns rather than
a second mirrored routine with its own chance of being wrong -- and why
a key as easy as space is the right one rather than a hazard. A turn is
refused when it would not fit: a share of the height is not always a
share the width can afford, and a node that cannot give its children
their floor takes the whole tab down to a list. So the turn is tried,
the layout is asked what it made of it, and one that flattened the tab
is put back with a word. Same rule `split_fits` applies to splitting.
Keys: the palette moves from `shift+p` to `p`, and cycling tabs from
`n`/`p` to tab/shift+tab. `p` is pressed far more often than "the tab
before this one" and was the only shifted letter in the defaults; tab is
what every other tabbed thing cycles with, and `\e[Z` decodes as
shift+tab on a plain terminal, so this costs nothing on a client without
the kitty protocol. `n` is free again. The actions are untouched: two
lines of config put the old binds back.
The shader ABI goes to 3: `shade_ctx_t` gained `state_ms` and struct
shader gained `channels`, both the pass's business rather than a
shader's. Plugins need a rebuild, which is what the version is for.
Diffstat (limited to 'src/app.c')
| -rw-r--r-- | src/app.c | 314 |
1 files changed, 299 insertions, 15 deletions
@@ -12,6 +12,7 @@ #include <stdlib.h> #include <string.h> +#include "expr.h" #include "json.h" #include "version.h" #include "graphics.h" @@ -141,6 +142,16 @@ struct node { char purpose[64]; bool purpose_locked; /* declared by a layout: in-band cannot override */ + /* When this pane last became what it is, for `since` (D20). A *transition* + * cannot be derived from the frame in front of you — the frame only says + * what is true now — so this is remembered, and it is remembered in the one + * place that decides the state, which is what keeps the timestamp and the + * state it belongs to from disagreeing. `last_state` is the state it was in + * when `state_since` was stamped; PSTATE_COUNT is "in none of them", which + * is a state to have been in for a while like any other. */ + pane_state_t last_state; + int64_t state_since; + /* split */ split_dir_t dir; node_t **kids; @@ -296,11 +307,20 @@ struct app { * socket under that name; the app only knows it because it is worth saying * out loud when several are running. */ char session[64]; + /* The last layout pass made this tab a list rather than a layout (D6). Same + * rule as `animating`: derived every pass, never remembered across one. */ + bool flattened; /* the screen we last composed into: its hit list is what a click resolves * against, so routing can never consult geometry the user never saw */ corner_t corners[16]; size_t ncorners; const screen_t *painted; + /* Something on the frame we last composed is animated: a shader whose amount + * reads the clock ran over it. Derived every frame from the chains actually + * applied, like the rect and the collapsed flag and for the same reason — + * remembering "this session animates" would keep a clock running for a pulse + * that was hung off a state no pane is in any more. */ + bool animating; }; static tab_t *cur(app_t *a) { return &a->tabs[a->cur]; } @@ -386,10 +406,13 @@ static void toasts_expire(app_t *a) { a->ntoasts = keep; } -/* When something needs repainting on its own: a toast expiring, or a hover - * guide arming. Without the second one the guide would appear on the next - * event rather than when the dwell is up, which for a resting pointer means - * "never". */ +/* When something needs repainting on its own: a toast expiring, a hover guide + * arming, or an animated shader — the three things on screen that change + * without an event behind them. Without the second one the guide would appear + * on the next event rather than when the dwell is up, which for a resting + * pointer means "never"; without the third a pulse would only advance when + * something else happened to cause a frame, which for an idle pane means a + * border frozen mid-pulse. */ int app_next_deadline_ms(app_t *a) { int64_t soonest = -1; for (size_t i = 0; i < a->ntoasts; i++) @@ -409,6 +432,16 @@ int app_next_deadline_ms(app_t *a) { } } + /* An animated shader ran over the last frame, so the next thing that changes + * is the clock. Bounded by a config knob rather than painting flat out: this + * is a terminal, and 20fps of one pane's border is not worth a core. The + * flag comes from the frame we composed, so the cost lands only while + * something animated is actually on screen. */ + if (a->animating && CFG.anim_ms) { + int64_t due = now_ms_() + CFG.anim_ms; + if (soonest < 0 || due < soonest) soonest = due; + } + if (soonest < 0) return -1; int64_t in = soonest - now_ms_(); return in <= 0 ? 0 : (int)in; @@ -1187,6 +1220,11 @@ static size_t tab_of(app_t *a, node_t *n); static void layout(app_t *a) { if (!a->ntabs) return; + /* Re-derived below, like everything else about a layout pass: whether this + * tab is a layout or a list (D6). Recorded because a tree edit needs to be + * able to ask what its edit did — a turn that flattens the tab is a turn + * nobody asked for. */ + a->flattened = false; uint16_t gx = (uint16_t)(CFG.gap * CFG.gap_aspect), gy = CFG.gap; uint16_t top = (uint16_t)(gy + STRIP_ROWS); rect_t r = {.x = gx, @@ -1263,6 +1301,7 @@ static void layout(app_t *a) { collapse_leaf(mins[i], (rect_t){r.x, r.y, 0, 0}); return; } + a->flattened = true; cur(a)->min_bar = (rect_t){0, 0, 0, 0}; /* a list has no separate bar */ if (r.h >= (uint16_t)(count_leaves(root) + 2)) { /* One row per pane, plus a body worth opening into: a frame is two rows. */ @@ -2060,6 +2099,139 @@ static void resize_focus(app_t *a, int dx, int dy) { layout(a); } +/* ---- equalising --------------------------------------------------------- + * + * Every visible pane an even share of the rows and columns it competes for. + * + * A split's children are weighted by *how many visible panes are behind each + * of them*, not given a weight each: one pane beside a column of three is a + * quarter of the width, not half of it. Weighting per child instead would make + * "even" mean something different at every depth — the pane on its own would + * get as much room as the three sharing the other side, which is the layout a + * person asks for this action to get *away* from. + * + * Exactly equal cell counts are not something a tree can always express: a + * nested split pays for its own gaps out of its share, and a share that is not + * a whole number of cells lands where the layout's remainder loop puts it. So + * this is the even division the tree *can* express, which is why the action is + * "give every pane an even share" and not "make every pane the same size". + * + * Minimised panes are not counted. They are out of the layout entirely and + * cost a row in the strip rather than a share of the tree, so counting one + * would hand its share to a pane nobody can see. Their own weight is still + * evened, because that is the share they come back to. + * + * Returns the number of visible panes under `n`. */ +static size_t equalize_node(node_t *n) { + if (n->kind == NODE_LEAF) { + n->weight = WEIGHT_UNIT; + return n->minimized ? 0 : 1; + } + size_t vis = 0; + for (size_t i = 0; i < n->nkids; i++) vis += equalize_node(n->kids[i]); + /* A subtree with nothing visible in it is not laid out at all, so its weight + * is never divided by — but it must not be zero either, or restoring the + * pane inside it would give it no share to come back to. */ + n->weight = (int)(WEIGHT_UNIT * (vis ? vis : 1)); + return vis; +} + +/* The current tab, like zooming and minimising: this is about the arrangement + * you are looking at. False when there are no boundaries to move, so the + * caller can say so rather than leaving a keystroke looking broken. */ +bool app_equalize_splits(app_t *a) { + node_t *root = cur(a)->root; + if (!root || root->kind == NODE_LEAF) return false; + equalize_node(root); + layout(a); + return true; +} + +/* ---- rotating ----------------------------------------------------------- + * + * A quarter turn clockwise, of the whole tab. + * + * Every split changes axis, and one of the two directions also reverses its + * children — because that is what a quarter turn does to a stack. Turn a column + * of panes clockwise and the one that was on top is now the one on the *right*, + * so a row split becomes a column split read backwards; turn a row of them and + * the leftmost becomes the top, so the order stands. Getting that asymmetry + * wrong gives you a mirror image, which looks almost right and is not a + * rotation: four turns would not come back. + * + * Four turns *do* come back, exactly, which is what makes this safe to press: + * it is a permutation of the tree and nothing else. Weights travel with the + * children they belong to, so a pane that had two thirds of the width has two + * thirds of the height afterwards. + * + * Depth first, so a subtree is turned before the split above it reorders it — + * the two operations are independent, but doing the children first keeps the + * reversal reading as one thing rather than as something that has to be + * reasoned about twice. */ +static void rotate_node(node_t *n) { + if (!n || n->kind == NODE_LEAF) return; + for (size_t i = 0; i < n->nkids; i++) rotate_node(n->kids[i]); + + if (n->dir == SPLIT_ROWS) { + for (size_t i = 0, j = n->nkids ? n->nkids - 1 : 0; i < j; i++, j--) { + node_t *t = n->kids[i]; + n->kids[i] = n->kids[j]; + n->kids[j] = t; + } + n->dir = SPLIT_COLS; + } else { + n->dir = SPLIT_ROWS; + } +} + +/* The current tab, like zooming and equalising. False when there is nothing to + * turn — one pane looks the same from every angle — and false when the turn + * would not fit, which is the interesting case. + * + * A pane's share of one axis is not a share the other axis can always afford: + * two rows split 19:6 are fine as rows and put the smaller one under the column + * floor as columns, and a node that cannot give its children their floor takes + * the *whole tab* down to a list (D6). So the turn is tried, the layout is asked + * what it made of it, and a turn that flattened the tab is put back — the same + * rule `split_fits` applies to splitting, and for the same reason: an action a + * person just asked for should not be the thing that breaks the arrangement. + * + * Put back by turning three more times rather than by a second, mirrored + * rotation. Four turns are the identity, so this *is* the inverse, and the + * asymmetry between the two directions — the part that is easy to get wrong — + * stays written down exactly once. + * + * A tab that was *already* a list is turned anyway: there is nothing to break, + * and refusing would mean a window too small to lay out could never be turned + * back into one that is. */ +bool app_rotate_layout(app_t *a) { + node_t *root = cur(a)->root; + if (!root || root->kind == NODE_LEAF) return false; + + bool was_flat = a->flattened; + rotate_node(root); + layout(a); + if (a->flattened && !was_flat) { + rotate_node(root); + rotate_node(root); + rotate_node(root); + layout(a); + return false; + } + return true; +} + +/* Turning because a person just asked for it, which is the only case that needs + * telling why nothing happened. The two refusals are different facts and get + * different words: one pane has no arrangement to turn, and a turn that would + * flatten the tab was put back. Same split of responsibility as + * `split_focus_ui`: the tree edit is quiet and the UI explains itself. */ +static void rotate_layout_ui(app_t *a) { + bool one_pane = cur(a)->root && cur(a)->root->kind == NODE_LEAF; + if (!app_rotate_layout(a)) + app_toast(a, one_pane ? "nothing to turn" : "no room to turn it"); +} + /* Reordering is a swap of two leaves, in place: their positions, weights and * parents trade, and everything else about them is untouched. */ static bool swap_panes(app_t *a, uint32_t id_a, uint32_t id_b) { @@ -2503,6 +2675,12 @@ struct draw { screen_t *s; }; +/* Defined with the other shader passes below, because it is one. A collapsed + * row needs it here: the row is chrome, and it is the only thing a flattened + * tab draws of a pane. */ +static void shade_chrome(app_t *a, screen_t *s, node_t *n, rect_t r, + const rect_t *hole); + /* A collapsed subtree: one row, the title of the pane it stands for, and a hit * that focuses it — which expands it on the next layout pass, because the * expanded child is simply the one holding focus. */ @@ -2519,11 +2697,13 @@ static void draw_collapsed(app_t *a, screen_t *s, node_t *n) { * gone or was never started. * * This row is the only thing a flattened tab draws of a pane, and the - * shader pass deliberately never reaches it: shaders colour *contents*, and - * a header is chrome (D13). So the states that get a colour everywhere else - * have to be carried by the words here, in the same order the status line - * ranks them. Without this, collapsing a tab hides exactly the facts the - * colour exists to show. */ + * *content* shader pass deliberately never reaches it: those colour + * contents, and a header is chrome (D13). So the states that get a colour + * everywhere else have to be carried by the words here, in the same order + * the status line ranks them. Without this, collapsing a tab hides exactly + * the facts the colour exists to show. A `where="chrome"` chain does reach + * this row — it is a frame — but that is a thing you may have asked for, + * not something the words can rely on. */ bool dead = !pane_alive(leaf->pane) && !pane_suspended(leaf->pane); char words[64]; if (dead) { @@ -2586,6 +2766,12 @@ static void draw_collapsed(app_t *a, screen_t *s, node_t *n) { char action[48]; snprintf(action, sizeof action, "focus:%u", leaf->id); hit_add(&s->hits, r.x, r.y, r.w, 1, action); + + /* Last, so the pass sees the finished row: rule, bell, title and all. The + * state comes from the leaf this row stands for, not from the node that + * happens to own the rect — a collapsed subtree is drawn as its first pane + * and should be coloured as that pane. */ + shade_chrome(a, s, leaf, r, NULL); } /* ---- shaders ------------------------------------------------------------ */ @@ -2626,7 +2812,7 @@ size_t app_shade_count(app_t *a, uint32_t pane_id) { * should not be recoloured by anything, a mode the whole screen is in outranks * a hint about one pane, and "not focused" is the weakest thing that can be * true of a pane. */ -static pane_state_t pane_state(app_t *a, node_t *n) { +static pane_state_t pane_state_of(app_t *a, node_t *n) { /* Only once the pointer has actually moved: a press that turns out to be a * click would otherwise flash the whole session on its way to nothing. */ if (a->drag.kind == DRAG_TITLE && a->drag.moved) { @@ -2636,11 +2822,31 @@ static pane_state_t pane_state(app_t *a, node_t *n) { } if (!pane_alive(n->pane) && !pane_suspended(n->pane)) return PSTATE_DEAD; if (pane_suspended(n->pane)) return PSTATE_SUSPENDED; + /* A rung pane outranks a scrolled or unfocused one, and *replaces* it rather + * than stacking with it: states do not stack (D13), and of the things true + * of a pane that rang while you were elsewhere, the ringing is the one you + * needed telling. The focused pane never reaches here with a bell — looking + * at it answers the bell before anything is drawn. */ + if (pane_bell(n->pane)) return PSTATE_BELL; if (pane_scrolled(n->pane)) return PSTATE_SCROLLED; if (n != cur(a)->focus) return PSTATE_UNFOCUSED; return PSTATE_COUNT; } +/* ...and when it last became that, which is the part a frame cannot tell you. + * Stamped here, in the one place that decides the state, so `since` cannot end + * up describing a state the pane is no longer in. Idempotent within a frame: + * the content pass and the chrome pass both ask, and the second sees no + * change. */ +static pane_state_t pane_state(app_t *a, node_t *n) { + pane_state_t st = pane_state_of(a, n); + if (st != n->last_state || !n->state_since) { + n->last_state = st; + n->state_since = now_ms_(); + } + return st; +} + static size_t policy_shaders(app_t *a, node_t *n, shader_t *out, size_t cap) { pane_state_t st = pane_state(a, n); if (st >= PSTATE_COUNT) return 0; @@ -2650,6 +2856,30 @@ static size_t policy_shaders(app_t *a, node_t *n, shader_t *out, size_t cap) { return k; } +static size_t chrome_policy_shaders(app_t *a, node_t *n, shader_t *out, + size_t cap) { + pane_state_t st = pane_state(a, n); + if (st >= PSTATE_COUNT) return 0; + size_t k = CFG.chrome_state_n[st]; + if (k > cap) k = cap; + for (size_t i = 0; i < k; i++) out[i] = CFG.chrome_state_shaders[st][i]; + return k; +} + +/* Does anything in this chain read the clock? If so the session has to keep + * painting on its own, because the thing that will change next is the time. + * Asked of the chain that was actually applied rather than of the config: a + * pulse hung off `dead` costs a frame clock while a pane is dead and nothing + * for the rest of the session. */ +static void note_animation(app_t *a, const shader_t *chain, size_t n) { + for (size_t i = 0; i < n; i++) + if (chain[i].amount_expr && + (expr_deps(chain[i].amount_expr) & EXPR_DEP_TIME)) { + a->animating = true; + return; + } +} + /* 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) { @@ -2668,8 +2898,11 @@ static void shade_leaf(app_t *a, screen_t *s, node_t *n) { if (!nc) return; bool focused = n == cur(a)->focus; + /* After policy_shaders, which is what stamps the transition. */ + int64_t now = now_ms_(); shade_ctx_t base = { - .now_ms = now_ms_(), + .now_ms = now, + .state_ms = n->state_since ? now - n->state_since : 0, .focused = focused, .default_fg = CFG.default_fg, .default_bg = CFG.default_bg, @@ -2683,8 +2916,46 @@ static void shade_leaf(app_t *a, screen_t *s, node_t *n) { base.cursor_x = (uint16_t)(s->cursor_x - n->content.x); base.cursor_y = (uint16_t)(s->cursor_y - n->content.y); } - shade_apply(s, chain, nc, n->content.x, n->content.y, n->content.w, - n->content.h, &base); + note_animation(a, chain, nc); + shade_apply(s, chain, nc, n->content, NULL, &base); +} + +/* Chrome: a pass over `r`, skipping `hole` (the contents, when the thing being + * drawn has any). One pass over the whole frame rather than one per side, so + * that an effect can travel round it — four passes would each count from zero + * and a sweep would restart at every corner. + * + * The rect is passed in rather than taken from the node because the two things + * that wear chrome are not the same shape: an open pane is its rect minus its + * contents, and a collapsed pane is one row that is chrome all the way through + * — its `content` is the size the program inside still believes it has, which + * is somewhere else entirely, and punching that out of the row would remove + * cells the row does not even overlap. `n` is only asked which state it is in, + * so a collapsed row is coloured by the pane it stands for. + * + * No cursor. The cursor is in the contents by construction, so a frame pass + * carrying one would hand an effect a position outside its own rect; `cursor` + * reads 0 here, which is the truth about a frame. */ +static void shade_chrome(app_t *a, screen_t *s, node_t *n, rect_t r, + const rect_t *hole) { + shader_t chain[SHADE_CHAIN_MAX]; + size_t nc = 0; + + for (size_t i = 0; i < CFG.nchrome_shaders && nc < SHADE_CHAIN_MAX; i++) + chain[nc++] = CFG.chrome_shaders[i]; + nc += chrome_policy_shaders(a, n, &chain[nc], SHADE_CHAIN_MAX - nc); + if (!nc) return; + + int64_t now = now_ms_(); + shade_ctx_t base = { + .now_ms = now, + .state_ms = n->state_since ? now - n->state_since : 0, + .focused = n == cur(a)->focus, + .default_fg = CFG.default_fg, + .default_bg = CFG.default_bg, + }; + note_animation(a, chain, nc); + shade_apply(s, chain, nc, r, hole, &base); } /* Drawn after the panes, so the corner's hit is registered after the two gap @@ -2761,8 +3032,15 @@ static void draw_cb(node_t *n, void *ud) { } /* Between the contents and the chrome that goes over them: the frame was * painted before this and lies outside the content rect, and the split guide - * is painted after, so it stays legible on top of a shaded pane. */ + * is painted after, so it stays legible on top of a shaded pane. + * + * The chrome pass runs here too, and for the same reason: the frame it + * recolours is already painted, and the guide, the resize hints and the + * corners are painted after it and stay in their own colours. An affordance + * is not decoration and must not be dimmed along with the thing it is + * offered on. */ shade_leaf(d->a, d->s, n); + shade_chrome(d->a, d->s, n, n->rect, &n->content); draw_split_guide(d->a, d->s, n); } @@ -3287,7 +3565,7 @@ static void draw_scrim(app_t *a, screen_t *s) { .default_fg = CFG.default_fg, .default_bg = CFG.default_bg, }; - shade_apply(s, &dim, 1, 0, 0, s->cols, s->rows, &base); + shade_apply(s, &dim, 1, (rect_t){0, 0, s->cols, s->rows}, NULL, &base); } /* The frame every modal wears: an opaque box with a pane's corners, a title @@ -3909,6 +4187,8 @@ void app_compose(app_t *a, screen_t *s) { hit_reset(&s->hits); s->cursor_visible = false; a->painted = s; + /* Re-derived by the shader passes below, every frame, from what they ran. */ + a->animating = false; if (!a->ntabs || !cur(a)->root) return; /* Looking at a pane is the acknowledgement, and it happens before anything @@ -4385,6 +4665,10 @@ static bool run_action(app_t *a, action_t act) { case ACT_RESIZE_RIGHT: resize_focus(a, 1, 0); return true; case ACT_RESIZE_UP: resize_focus(a, 0, -1); return true; case ACT_RESIZE_DOWN: resize_focus(a, 0, 1); return true; + case ACT_EQUALIZE: + if (!app_equalize_splits(a)) app_toast(a, "nothing to even out"); + return true; + case ACT_ROTATE_LAYOUT: rotate_layout_ui(a); return true; case ACT_SCROLL_UP: pane_scroll(cur(a)->focus->pane, -CFG.scroll_lines); return true; case ACT_SCROLL_DOWN: pane_scroll(cur(a)->focus->pane, CFG.scroll_lines); return true; case ACT_SCROLL_PAGE_UP: |
