Skip to content
gwmgwmgwmv1.10.0

v1.8.0

  • One width policy for every modal (#550). Overlays used to size themselves with four different rules, two of which branched on term_width <= 80 to spend a bigger percentage on a small terminal. Width was therefore not monotonic: dragging a pane past 80 columns made the link prompt 16 columns narrower and the exec / clean / detail overlay 22. Four others sized on a bare percentage with no ceiling, so on a 200-column terminal the delete confirmation reached 124 columns for a four-row detail grid, and the help, config and command-palette overlays 120.

    Every bounded overlay now resolves its width through a single modal_width(term_width, pct, min, max): its own knobs, one rule. Never narrower as the terminal widens, never past its ceiling, always two columns of margin per side. The floor also makes 80 columns, the width the docs advertise, a size the surfaces were actually sized for. The confirm modal was 49 columns wide there and its hint row read Enter activa, cut mid-word with no ellipsis and the n cancel hint entirely off screen.

    The PTY overlay, the command-log transcript, the note editor and the bootstrap report keep spending a percentage of the frame: they are text canvases, and the width is content. render_section hard-clips by design (one logical row, one visual row), so on those surfaces every column the frame gives up is a column of a hook’s error nobody can reach.

  • The TUI is compact by default (#545). Panes and sidebar sections no longer draw box rules; each is delimited by a filled one-line header instead, which buys back two rows and two columns per section. The title keeps its bracketed keybinding and goes uppercase ([1] WORKTREES, ISSUE / PR [F]), the counter moves to the right of that same line rather than into a bottom rule, a muted rule marks the boundary between the two panes, and the worktrees pane sizes itself to its row count instead of reserving its share of the stacked split. Focus reads on the header: the active pane takes the focus role and the selection_bg fill. Modal titles moved into the top rule as part of the same pass (#549), two rows back per overlay.

    [tui] layout = "bordered" restores the previous lazygit-style boxes. That mode is left untouched by the compact refinements (no dimming, no separator rule) so it stays a faithful restore rather than a third look. Overlays and modals keep their border under either value.

  • [tui] status_one_line (#547): folds the sidebar’s Status block onto a single row (branch, head, state badges, diff and age joined by ·) where it used to spend one labelled row per value. On by default, which frees three rows for the panes below; status_one_line = false restores the labelled block.

    A knob rather than a compact-mode behaviour, so it applies under bordered too. The Path row never folds in: a path is the one value long enough that sharing a row would clip both halves. Segment order is the width policy: the sidebar does not wrap, so a row wider than the pane is clipped on the right, and the fold puts identity first and the age last. Reachable from the Settings panel’s TUI tab, where the edit now drops the sidebar cache so the new shape is visible immediately (a live theme change had the same staleness and is fixed with it).

  • [tui] dim_unfocused (#545): dims the body of whichever pane does not hold focus, in either layout. Off by default: it trades contrast for a stronger focus cue, and the inactive pane’s content is still readable information. Uses the terminal’s DIM attribute, so semantic colours survive.

  • section_bg theme role (#545): the compact header fill. An indexed colour rather than a translucent white, so the mode stays readable on a terminal without truecolor; each preset takes the tone its own palette reserves for chrome bands, and keeps it distinct from selection_bg, which is also what separates a focused header from an unfocused one.

  • The header, footer and statusbar stay inside the terminal (#563). The rows that pin something to their right edge computed the padding before it by counting characters, so anything wider than one column per character under-counted by half and pushed that pinned element off the terminal. The header carries the repo directory name and the working path, the footer and statusbar carry the action log, and none of those three is a value gwm picks: they are wherever you cloned, and whatever the last command had to say. Measured, an 80-column header painted 102 cells and the version chip went with the overflow; the footer painted 100.

    Table columns are sized the same way now. #560 made the cell cut in cells and left the column it is cut to sized in characters, so a branch of wide glyphs got a column half the width it needed and rendered as nine glyphs, an ellipsis, and eleven blank columns. With a wide name and a wide branch both claiming their ceiling, the path column gives up the width they take, which is the intended trade and a better one than spending it on blanks.

    The row arithmetic joins the three truncators (#554, #560, #562) on ratatui’s own CellWidth per grapheme. ASCII renders exactly as before, which is what kept this standing for so long.

  • The header, footer and statusbar neutralise bidi control characters (#563). All three replaced what char::is_control matches, which is the Cc block only. The Bidi_Control characters are Cf, which is why they had to be named explicitly in the first place: they reorder how a terminal renders the text around them, so a row can read in an order its bytes do not have. The sanitiser sits at the funnel every width-constrained cell passes through, and these three rows reach the terminal without going through it whenever their text fits, which is the ordinary case.

    Neither vector is exotic. The header shows the repo directory name and the working path, and a directory on disk can carry one of these without anyone fetching anything. The action log carries branch names and paths, and git’s ref rules refuse the ASCII controls and ~^:?*[ but not the format characters, so a ref carrying one arrives with a fetch. All three call the same sanitiser as the rest of the codebase now, so the replacement shows as ? rather than a blank.

  • The statusbar no longer paints one column past its width (#563). Unrelated to the measure and visible on plain ASCII: the … marking a cut hint list is drawn as a space then the glyph, so it costs two columns wherever anything precedes it, and only one was ever reserved. Any terminal narrow enough to truncate its hints overflowed. Where the context chip alone fills the row, the marker is now dropped rather than drawn past the edge, its leading space first.

  • Table cells and pane headers are cut by the room their text takes (#560, #562). The two width funnels left standing after #554, now on the same measure as the ellipsizer.

    trunc clips every width-constrained table cell, and it counted characters where its callers hand it a column width. A branch of 20 ideographs is 20 characters and 40 columns, so it was judged short and passed through whole, and the table then hard-clipped it at the column edge: the tail vanished with no … to say the name shown is not the branch you are on. That marker is the reason the funnel exists.

    compact_header_line budgeted its title through Span::width, which is unicode-width over the whole string, and that is not the measure ratatui applies when it paints. The two agree on CJK, which is where #546 was measured, and disagree on text that is not exotic: a title reading لالالا is 3 columns to unicode-width and 6 on screen, ガガガ likewise. An undercounted title means padding computed against the undercount, so the right-aligned counter was pushed off the pane. A filter query reaches this directly, being arbitrary typed text. The truncation branch stepped character by character on top of that, where a variation selector reads zero and the sequence it completes paints two, so every one of those was free.

    Both now measure with ratatui’s own CellWidth per extended grapheme, the walk Buffer::set_stringn performs, through the cells helper #554 left behind; the prefix walk the three of them share is one function rather than three. Nothing in src/ measures with unicode-width any more, so it moves to a dev-dependency, where the width tests keep it as the contrast measure that proves a fixture is one the two disagree on.

  • A path of wide glyphs is ellipsized by the room it actually takes (#554). ellipsize_middle counted characters while all eleven of its callers hand it a budget in terminal cells, the width of a ratatui rect. For anything but narrow Latin the two disagree: a path of 40 CJK glyphs is 40 characters and 80 columns, so the helper called it short and returned it whole. In the delete confirmation the row then overflowed its frame and wrapped, dropping the path off its Path label and breaking the aligned grid; in a table cell ratatui simply clipped the tail, which is the half a middle ellipsis exists to keep.

    The budget, the head/tail split and the padding that fills a picker or reclaim row are all measured in cells now, and the measure is ratatui’s own CellWidth summed per extended grapheme, which is exactly what Buffer::set_stringn walks. Anything less agrees with the renderer on CJK and disagrees elsewhere: unicode-width reads لالالا as 3 columns where 6 get painted, because lam-alef counts as a ligature, and ガガガ as 3 where 6 get painted, because a halfwidth dakuten is Grapheme_Extend yet terminals give it a cell. Walking graphemes also puts the cut where a glyph ends rather than between a base and its combining mark, and skips control characters the way the renderer does. A glyph that would straddle the last column is dropped whole rather than half-drawn, so a result is at most the budget rather than exactly it. unicode-segmentation, already in the tree through crossterm, is now a direct dependency.

  • The create and rename forms keep their focused field on screen (#553). Both modals size themselves to their content, and a content box taller than the terminal is clamped to the frame: ratatui then cut the tail off with no indicator. The rename form wants 18 rows, so a 16-row terminal lost its Desc row, the field the modal opens focused on. It stayed reachable by Tab and typed into blind; free-form mode lost its only input the same way at 12 rows.

    The body now scrolls to whichever field has focus, derived from the focus itself rather than kept as scroll state: the forms have no scroll cursor, focus is the only thing that moves. A form that fits renders exactly as before; one that does not gets the Settings panel’s scrollbar, so the overflow is visible instead of silent.

  • Returning from a fullscreen surface no longer ends the session when the terminal is slow to answer (#548). Coming back from the PTY overlay, an exec run or a review launch, gwm could exit with error: io error: The cursor position could not be read within a normal duration. That message is crossterm giving up on a DSR report: Terminal::clear snapshots the cursor with ESC [ 6 n before wiping the screen, and the return path from a fullscreen child is exactly when the terminal is least likely to answer in time.

    The three call sites now clear without asking. The snapshot was dead weight here: each one repaints the whole frame on the next loop iteration, so the position it restored was overwritten before anyone could see it. What the callers needed was the other half of clear, wiping the screen and resetting the back buffer so the next draw is a full repaint rather than a diff against stale content; a fix that only wiped would have traded the crash for a blank TUI.

  • Every capture regenerated, narrower and fitted to its content (#544). Acting on the design read ratatui’s maintainer sent back on the awesome-ratatui listing. The set used to sit at 1500px wide for 14 of 24 tapes, 50% above the 800-1000px he suggests and 25% past the widest he would go, which renders the TUI’s text smaller than the prose around it and makes the reader change scale to read a screenshot. Full-TUI captures are now 1000px (103 columns), the narrow-layout one 800px (81 columns, one above the width the docs advertise as the floor).

    Four exceed that band, each because the surface it documents does not fit below it: side-by-side needs 121 columns before the sidebar stops stacking and about 148 before the STATUS column stops reading STA, so it sits at 1400; the ? help overlay, the Settings panel and the command palette clamp their modal to a 64-column floor, which cuts the descriptions that are the point of the shot, so they sit at 1240; gwm list prints 140 columns and gwm agents 126, and below that the shell wraps every row and scrolls the header away. The rest of the set is inside the band.

    Heights were cut to the content in the same pass, which is the other half of the same feedback. The hero was carrying twelve empty rows under the commit graph, the narrow capture twenty-one; the o terminal overlay was around 85% empty. Measured per capture rather than eyeballed, by finding the band of rows between the last content and the status bar.

    The terminal background goes from #2b2b2b to #141414. Compact paints its section headers with section_bg, a band a few steps off the terminal background, and against a mid-grey it barely read as a band at all; the frames also sat in the docs page rather than on it, which is the contrast half of the same feedback.

  • generate.sh stops reporting success over captures it did not write. vhs exits 0 whether or not it produced what the tape asked for, so the script was checking the wrong thing. Two failures hid behind that zero: a tape that errors out, and the screenshot write racing process exit. The second is the common one and it is a coin flip: measured, a bare tape landed its PNG on one run in three, leaving the previous asset in place, so the set went stale one capture at a time while the script printed a tick. Every tape now carries a Sleep after its Screenshot, and every declared target must be newer than the run that claims to have written it, with one retry and a non-zero exit naming whatever still did not land. It caught a real miss on its first run.

    bootstrap.tape is torn down at both ends as part of this. It is the one tape that mutates the fixture, and gwm remove keeps the branch on purpose, so a second run in the same fixture captured branch already exists instead of the bootstrap report. The retry above is what surfaced it, by shipping exactly that PNG.

  • The github-linking capture points at a branch that exists. It is the one shot outside generate.sh’s loop, because the Issue/PR pane needs a live remote and the demo fixture has none, and it was still filtering for a docs/#206 worktree deleted several releases ago. The filter matched nothing, so the image was frozen at the old width, the old background and a gwm 1.7.1 chip: the only capture in the docs not following the rest. The filter is gone rather than repointed at another number that would rot the same way; the pane follows the selected row, so the tape now works from any branch with an open PR. Its caption promised an auto-linked issue that the pane does not render, in both languages, and now says what is in the image.

  • The repo banner reads v1.8.0, with the test count the suite reports and a branch graph carrying five branches from this cut.