Repository navigation
fix(tui): stop rows wrapping and shearing the layout - #69
Merged
Merged
Conversation
The TUI wrapped rows whenever a repository supplied long paths, long branches or long subjects, and again whenever the theme changed. Every cause was structural, so each one is fixed where it starts instead of being papered over in View(): - Panel.View rendered Width(p.Width) while lipgloss draws the border outside Width, so every panel was two cells wider than the layout had reserved for it. Width is now the inner width, and the title and every body line are capped to it. - The header never adapted to the terminal: it measured ~170 cells on a 60-column terminal. It is now a density ladder (counts, then short labels, then drop the meta block, then padding-free tabs) that renders the first strip which fits. - The AI sidebar drew panelW+4 cells and sliced its input row by byte offset. It sizes itself from the inner width, keeps the tail of its input rune-wise, and clamps every row. - Dialogs were pinned at a stale huh form width, so the form burst out of its border. views/dialog.go now owns the budget (border, padding, form field frame) and every dialog clamps its body to it. - Rows went through per-view selection loops that sliced styled strings by byte or rune index. components.RowLine is now the single path: it charges the style frame, truncates with the shared width law, then draws the cursor. - components.CutLeft/TruncateLeft replace ansi.TruncateLeft, which cannot cut into a wide grapheme: asking it to drop one cell in front of a 2-cell character returns the string untouched, so a caller looping until the tail fits never terminates. - The modal compositor places overlays by cell column, not rune index, so a CJK or emoji background no longer shifts them. - Light theme: diff backgrounds are tokens instead of dark literals, and no style carries a hardcoded Width(80) any more. View() still clamps each line as a last line of defence, but the new tests assert that the raw header + content layout fits at eight widths, nine screens and both themes without it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Reported: the TUI "breaks" on a big repository, and again when the theme is
changed — rows wrap, the layout shears sideways. Both were the same class of
bug: rendered lines were allowed to be wider than the cells reserved for them,
so a real terminal wrapped the row and every row below it shifted.
What was actually wrong
Panel.ViewusedWidth(p.Width); lipgloss draws the border outsideWidth, so each panel renderedp.Width + 2Width(innerW), title/body capped toinnerWpanelW + 4; input row sliced by byte offsetviews/dialog.goowns the budget; every dialog clamps its bodycomponents.RowLineis the single row path (charges style frame, truncates, draws cursor)ansi.TruncateLeftcannot cut into a wide grapheme —TruncateLeft(s, 1, "")in front of a 2-cell character returnssunchangedcomponents.CutLeft/TruncateLeftwalk the input withansi.DecodeSequenceansi.Strip+Truncate/CutLeft)Width(80)DiffAddBg/DiffDelBgtokens; fixed widths removedBug 6 is worth calling out: the first attempt at a left-truncation loop
("shave one cell until the tail fits") hung the test suite because the
library hands the string straight back in that case. The loop is gone.
Guarantees now tested
TestLayoutFitsWithoutTheSafetyNet— renders header + content raw, withclampFrameout of the path: 8 widths × 9 screens × 2 themes. This is thetest with teeth; temporarily restoring
Width(p.Width)inPanel.Viewfails it immediately (
line 1 is 64 cellsat a 60-cell terminal).TestViewNeverWraps,TestViewNeverWrapsWithSidebar— every line of thereal
View()stays inside the terminal.TestPanelOccupiesExactWidth,TestRowLineChargesForTheFrame— containersand rows occupy exactly their budget.
TestDialogsFitTerminal— each modal's raw output fits at 7 widths.TestOverlayCenterIsColumnAccurate— the modal lands on the same columnover ASCII, CJK and emoji backgrounds.
TestTruncateLeftKeepsTailsweeps every width (a hang would fail the run),TestCutLeftDropsWholeGraphemes,TestCursorLineKeepsLeadingEscapes.TestRenderHeaderAdapts/TestRenderHeaderNarrowShrinks.TestThemeSwitchRepaintsStyles/TestThemeTokensStayLegible.View()keeps a per-line clamp as defence in depth — but it is no longer whatmakes the layout correct.
Checks
go test ./... -count=1greengo vet ./...clean,gofmt -l .emptyrg 'line\[1:\]|runes\[1:\]' pkg/tui/ -g '!zlist.go'empty (CI width-law step)