aboutsummaryrefslogtreecommitdiffstats
path: root/src/app_ui.c (follow)
Commit message (Collapse)AuthorAge
* panes: the focus a closed tab left behinduser2026-10-09
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A tab's focus outlived the tree it pointed into. close_leaf's last-pane branch freed the leaf and removed the tab but never cleared `t->focus`, and tab_remove only shifts the slots *after* the one it removes -- so with the last tab gone, `a->tabs[0]` kept the stale tab_t while `ntabs` was 0 and `cur` was 0. `cur(a)` is `&a->tabs[a->cur]` with no bounds check, and `a->quit` is a request rather than a barrier: the loop keeps serving commands, and the next one to ask for the focused pane was handed freed memory. From outside it looked like a random crash, because what the freed chunk held depended on what had been allocated since. Two from one afternoon: a pane_t recycled into a `panes` JSON reply, whose `term` pointer was then the ASCII of `k":true,` and faulted as a general protection fault; and the same node read back zeroed, which left pane_clear_bell testing a flag at NULL+0x5d. Typing `exit` in the last pane is the whole recipe. close_leaf drops the focus with the tree tab_remove blanks the slot the shift vacated app_compose tests ->pane as well as ->focus, the way the search beside it already did app_apply_layout clears restore_focus up front -- it is a node pointer, and an apply that fails before consuming it leaves it naming a tree the next apply frees Two more the same reading turned up, both in close_leaf: an emptied split set the focus to the node just freed, and a leaf whose parent disagreed with that parent's child list underflowed `p->nkids - at - 1` into a memmove of nearly SIZE_MAX. FOCUS_CHECK -- behind -DSLOSH_FOCUS_CHECK, compiled out otherwise -- is the invariant all of this turns on: every tab's focus is a leaf of that tab's own tree. It is what found this one. Committed with --no-verify: the files are deliberately partly staged, and the staged blobs were checked against clang-format directly.
* themes: a picker that wears them as you browse (C-a t)user2026-09-22
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | `theme_name` is a line in a file, and a name in a list tells you nothing about a colour scheme. So the picker previews by *applying*: moving the selection puts that theme on, which is the only honest preview of a palette meant to dress a whole session -- a swatch in a list would be a small lie about it, and the session is already on screen behind the box. It opens on the theme being worn rather than at the top, so the first arrow press moves off it instead of silently changing the session, and the row your config names says so, because "which one is this" and "which one comes back on restart" are two different facts the moment you switch. Escape puts back what was worn, since a picker that changed your session by being looked at is one you stop opening. Enter keeps it for the session and says as much. Only C-s writes `theme_name` into your config, which is the one place in the UI where a keystroke edits it, so it is a deliberate chord and the footer says so: "C-s keeps it, esc undoes". Reuses the picker the finder, the palette and the projects list already wear -- one machine with a subject, so the query, the selection, the mouse, the frame and the scroll behaviour are the ones already tested. The list is scanned on the keystroke, like the projects are: the moment it opens is the moment it has to be right, and a theme written since the session started is exactly the one being looked for. With nothing to list it toasts the directory it looked in rather than opening an empty box. `t` was free: tabs cycle on tab, and new-tab has had `c` since the first day.
* tabs: the strip can stand on its side (tab_bar_side left/right)user2026-09-10
| | | | | | | | | | | | | | | | A sidebar lists one tab per row down the chosen edge, tab_bar_width columns wide; the panes trade the strip's row for the sidebar's columns in tab_area(), the one place that arithmetic lives. The per-tab cell drawing is extracted from draw_tab_strip (label, rename editor, bell, hover, drop states, hit) and shared, so every mouse verb -- click, double-click rename, drag to a tab, drop on + -- works in both shapes because the hits move with the cells. The strip's indicators (pane count, prefix badge) take the sidebar's bottom rows, reserved first under the same budgeting rule the strip applies from the right. The status line below keeps the full width. A terminal too narrow to give up the columns falls back to top, re-derived per frame, so a resize moves the bar without anyone storing state.
* search: a scrollback search bar on /, matches highlighteduser2026-09-08
| | | | | | | | | | | | | | | | | | | | | | | | C-a / opens an inline search bar on the focused pane's status row, on lib-vt's GhosttySearch (D4's pattern: the scrollback walk, reflow and alt-screen survival are the library's, the UI and the wiring are ours). Type to search live; the viewport follows the current match; up/down (or C-n/C-p) step through them with wraparound; Enter keeps your place, Escape snaps back. A k-of-n counter reads on the right, or "no matches". Every match on screen is tinted, and the one the bar is on is tinted apart -- warm gold by default, painted after the shader pass so the scrolled wash does not dilute a match you are hunting for. Four themeable colours (search_fg/bg, search_cur_fg/bg). Bare / took the key from help, which keeps ?; that needed the legacy decoder to read the shift out of a punctuation byte the way it already did for capitals -- without it ? and / decode identically (SLASH, no mods) and cannot be two keys on a client without the kitty protocol. tests/test_search.py drives it end to end (39 checks): the bar, the counter, stepping and wrap, all-match vs current highlighting, Enter/ Escape, new output updating the count, case-insensitivity, the keyboard belonging to the bar, click-away, and that / and ? no longer collide.
* app: cut the modals, toasts and splash out of the images under themIngmar Steen2026-09-06
| | | | | | | | | | | | | | | | | | | | | | | | Overlays are painted over the panes and the compositor needs no more than that: painted later is on top. A placement is not painted, though -- the client's terminal draws the image over the cells, in whatever order they arrived -- so an overlay that is only drawn is an overlay an image sits on top of. The cheatsheet came up under a picture and was unreadable through it, in the terminal and in the mirror both. So a frame now records where its overlays landed, and the graphics pass takes them out of a placement exactly as it takes out a float: the modal's box (the cheatsheet and the picker wear the same frame), each toast, and the splash. The list is rebuilt every compose, so a rect that is not registered again is one that is no longer on screen. Only the opaque things are registered. The scrim behind a modal tints what is behind it rather than hiding it, and an image cannot be tinted by us at all -- so a picture beside the cheatsheet stays bright while the text around it is pushed back, which is wrong-looking but a great deal better than the image disappearing while a modal is up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRz3zwZJvBvzsR9k4cj6dd
* pane: drop the render snapshot of panes on hidden tabs -- 154K -> 66K eachuser2026-09-03
| | | | | | | | | | | | | | | | | | | | An updated GhosttyRenderState retains a full copy of the pane's viewport (rows, cells, per-row arenas, styles) so renders can read it without the terminal lock. Measured with bpftrace and a lib-vt micro-benchmark, that copy is ~140KB for an 80x24 pane -- more than twice the terminal it mirrors (~57KB) -- and every pane kept one forever, composed or not. Only the current tab is ever composed, so app_compose now drops the snapshot of every pane on the other tabs (a pointer test once dropped), and pane_new no longer creates one at all: pane_compose and pane_select_word build it on first use. Both already begin with ghostty_render_state_update(), so a dropped snapshot is rebuilt, never missed, and the tab switch already forces a full repaint. Server memory per empty 80x24 terminal drops from 154K to 66K anon (Superlogical reports 68K on macOS; tmux 4K, zellij 1.2-6.2M). Fifty background tabs: 8.2M -> 3.8M, reclaimed on close, and cycling through tabs does not ratchet. What remains is the lib-vt terminal itself.
* compact: shared borders instead of gaps (D23)user2026-08-25
| | | | | | | | | | | | | | | | | | | | | | One knob, `compact true`: panes pack flush against 1-cell divider lines that meet in real junctions, one frame rings the tab, and a pane's title rides the line above it -- a boundary costs one cell where the default look spends four. The lines are strokes: each write says which edges of its cell it needs, unions with what is there, and never clobbers text, so ├ ┬ ┼ fall out of paint order and the focus ring recolours without inventing arms. The divider *is* the gap (eff_gap, one answer for the layout, split_fits and the resize clamp), so gap hits, hover hints and corner drags work unchanged. Interior edges give up click-to-split -- one cell cannot hold two verbs -- and a pane's status row overlays its last content row. Floats, zoom and the D6 flatten keep classic frames through place_hidden: zero new tree cases. Underneath it, mode or no mode: a border drag now moves the boundary on that side, converted on the first motion so the press that never moves keeps its old meaning -- the handle's click still splits, the rim's is still deliberately nothing (drag.rim). Which is also the fix for gap 0, where flush panes leave no gap cell to grab and the resize gesture had silently vanished with it.
* slosh: floating panes and the drop grammaruser2026-08-20
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A pane can float: lifted out of the layout, drawn over the tiled panes and under the modals, freely moved and shaped. The model is a flag and a rect on the leaf, exactly like `minimized` -- the seat in the tree is kept, layout skips it, the siblings absorb its share, and un-floating returns it home. The drawn rect is the wanted one clamped to the tab every frame, never written back, so a terminal squeeze round-trips exactly; an explicit move or resize *does* write back, because the hand is the author of the intent. Floating pops the pane to the centre: the jump is what says it happened. Painted after everything tiled, so the hit list -- searched backwards -- resolves an overlap to the float you can see: the z-order and the click order are the same list. It casts a shadow (float_shadow, one number), and `floating` joins the states table above `unfocused`, so a float is never dimmed as merely-unfocused. The hands: the title moves it, any other border resizes it (a bottom corner on both axes; the grabbed edge pins at walls and floors, never sliding the window), H J K L move a focused float, the =/+ key grows it about its centre, `-` shrinks it (its split was a refusal anyway), and hovering shows which edges a grab would take, from the same derivation the press uses. Equalize moved to `0` to free the key; `C-a f` floats (finder went to `s`, where it reads as search); `C-a F` opens a new floating shell -- the throwaway terminal; the function row f1..f12 became nameable in configs along the way. Guards throughout: the last tiled pane cannot float or minimize away, closing it lands the top float, splits are refused with their own words, floats are not swap targets. Dropping got a grammar: while a pane is carried, the pane under the pointer subdivides into a swap centre and four edge bands meaning "insert me beside this pane, on this side" -- detach_leaf + place_beside, the surgery cross-tab moves already had. Bands are thirds (the first map of quarters showed the swap owning half of each axis), the hovered band fills the half the drop would hand over, the release resolves against the painted frame's hits so it cannot disagree with the fill, and a drop between siblings along the axis is a reorder that always fits -- refusing it was refusing the most ordinary drag there is. Also: rename-pane/rename-tab as palette actions (renaming was a double-click and nothing else, the one verb the palette could not reach), and C-u clears the rename editor like it clears the picker's query. Design log in PLAN-FLOATING-PANES.md; to fold into DESIGN.md as D22/M10-11.
* slosh: greet an attach with the logo, wearing a random shaderuser2026-08-19
| | | | | | | | | | | | | | | | | | | | | | | | logo.txt is the source of truth; the Makefile bakes it into build/logo.h the way version.h is baked. When a client attaches (MSG_HELLO turning a connection into the display), the logo appears centered over everything for splash_ms (900, 0 for never), on a cleared backdrop, in the accent -- and one of six effects runs over its rect through the ordinary shader pass: a diagonal sheen, plasma, rings, sparkle, a left-to-right reveal, a breathe. Which one is picked by the splash's own timestamp, so every attach looks a little different without anybody keeping a counter. Every effect reads the clock, so the frame clock runs only while the splash is up (note_animation, same as any animated chain), and `since` in a splash context is milliseconds since it appeared, so one-shot effects sweep exactly once. Any key or click ends it and still lands where it was aimed: a greeting is not a modal. A screen the box does not fit on gets no greeting rather than a cropped one. Headless and scripted sessions never attach, so the whole fast suite never sees it; {"cmd":"splash"} triggers it on demand, which is what makes it testable from a scripted session -- and lets you replay the greeting whenever you like.
* slosh: split app.c into five units, mechanicallyuser2026-08-19
app.c was 7k lines wearing section markers; now the markers are files, cut exactly along them, no behaviour change: app.c the session core: config in force, pane and tab lifecycle, walking, input routing, introspection app_layout.c layout, tree edits, resizing, equalising, rotating, focus app_session.c layout files, checking, workspaces, dumping a session app_draw.c drawing, the shader passes, kitty graphics app_ui.c the finder, modals, the palette, the project picker, renaming include/app_internal.h carries what the five share: the tree/tab/app structs, CFG and the palette macros, and the 50-odd formerly static functions the cut made cross-unit, declared where they are defined. They keep their names; only the five units see the header. One test moved with the code: test_portable_layouts' staleness check greps the source for the layouts section, which now lives in app_session.c. Mechanical, so blame for anything in the four new files continues in app.c before this commit.