From 2ed553fab8180fa5fea630c8de96d85b73311fe4 Mon Sep 17 00:00:00 2001 From: Chris Birch Date: Sun, 8 Nov 2026 03:49:13 -0500 Subject: [PATCH 1/3] feat: draw each pull request as two lines, ruled apart and spread across the width The board drew each pull request on one line, with its branch and checkout hidden until f showed them. Each now takes two lines, as the first version of prs drew them. The first holds the state, the title, the diff, the age and the branch, spread across the width. The second holds the checks under the state, then the checkout, the number, the review, the newest Claude round and the app that opened it. A dotted rule sits between rows. The board keeps a margin of 12 columns a side, narrower under 150 columns, with rules under the titles and over the keys. The cursor's two lines are bold on the selection, and rows draw no chevron, because every row opens its document. prs list keeps its one-line table. gotermboard is pinned at the commit that adds this look. Agent-Name: claude-code-unattended Agent-Session: 69e19fe4-568c-41ef-9af1-316d086cd270 Agent-Run: 01a1b9f7-d1e5-73a0-a189-4f76a78eeb51 Worklog: 2568.1 --- CLAUDE.md | 22 ++++-- README.md | 41 +++++++---- cmd/prs/tui.go | 30 ++++---- cmd/prs/tui_test.go | 39 +++++++--- go.mod | 2 +- go.sum | 4 +- internal/board/board.go | 5 +- internal/board/rows.go | 142 +++++++++++++++++++++++++++---------- internal/board/sections.go | 5 +- internal/board/table.go | 8 +-- 10 files changed, 204 insertions(+), 94 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index fbbca1e..ac152cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ sources, the frame and the `--json` snapshot, `sections.go` the people, bots and closed groups, one record per pull request and the row held under the cursor, `listing_cache.go` the last listing kept on disk for the next start, `table.go` what `prs list` prints, `document.go` each pull -request's markdown document, a section per heading, `review_rounds.go` the CLAUDE cell, +request's markdown document, a section per heading, `review_rounds.go` the round's cell, the Review section and the tmux windows that run a round, `diff_files.go` the document and review threads `d` writes for nvim, and `opened_on.go` the pull request `prs tui --pr` names. `internal/reviewrounds` reads a pull request's Claude review rounds out of its @@ -129,9 +129,16 @@ is the shape of the threads file `d` hands nvim, held the same way. - **A round record's statuses stay apart.** A skipped reviewer was skipped on purpose and reads as skipped. One not run or not posted counts as a gap beside the findings, and a round none ran reads `claude did not run`, never as a clean one. -- **The CLAUDE column draws only while a row fills it**, so a board on repos no round - reviews never has it. `rowColumns` holds every column with the cell it draws, so a +- **`prs list`'s CLAUDE column draws only while a row fills it**, so a list of repos no + round reviews never has it. On the board the round follows the review on a row's second + line. `listColumns` and `boardColumns` hold every column with the cell it draws, so a column's header and its cells cannot drift apart. +- **A pull request is two lines on the board, and a field keeps its line.** The first is + `boardColumns`: state, title, diff, age and branch. The second is `secondLine`: checks + under the state, then the checkout, the number, the review, the round and the bot. A new + field goes at the end of `secondLine`'s flow before the bot, or into `boardColumns`. The + second line is cut at the table's edge, so the checkout's length decides what a narrow + screen loses. - **A window marked `runs_review_round` reads running only until a round finishes after prs opened it.** A session that ran a round keeps its window afterwards, so one prs did not open reads running only where no round has finished. tmux is asked every @@ -183,7 +190,14 @@ is the shape of the threads file `d` hands nvim, held the same way. request's markdown until its row, its detail state or the minute moves. - **A row's `ReaderTitle` is its pull request's title**, because the table's enter would otherwise title the Reader with the row's first cells, which here are its state words. -- **A row's `Under` line shows only once `f` turns summaries on.** +- **`Board.Look` is gotermboard's `LookBoard`**: a margin, the columns spread across the + width, rules under the titles and over the keys, the cursor's row bold on the selection, + and no row chevrons. `PanelFrom` is measured inside the margin, so the side panel shows + from 184 columns. +- **A column with no title continues the one before it under `Look.Spread`**, so the + diff's three counts take two untitled columns after DIFF and stay together. +- **`f` applies only where a row or group sets `Under`**, and no prs row does, so `f` + stays out of the footer and `?`. - **A group header that is not `Still` takes the cursor**, so `g` lands on the people header and enter there folds the group. The cursor starts on the first row holding cells, of whichever group draws first, so recently closed waits for the first open listing. diff --git a/README.md b/README.md index e2eba83..6648ce0 100644 --- a/README.md +++ b/README.md @@ -122,23 +122,33 @@ field it filters on drops a repo silently, and every list it prints still looks moved to another group stays under the cursor where it was, in its new state, and one no read lists any more stays there marked `gone`, each until the cursor moves off it. `m` and `x` refuse a `gone` one. +- **Each pull request takes two lines, with a dotted rule between it and the next.** The + first line holds where it stands, its title, its diff, its age and its branch, its + columns spread across the width. The second holds its checks, its checkout, its number + and its review. A glyph marks each field, and a field's glyph is the same in every + state. - **State is a word, never a color alone.** The first column says where a pull request stands: `ready`, `behind`, `conflicts`, `blocked`, `draft` or `unknown` while open, or `mergeable` where it can merge with its checks not passing, and `merged` or `closed` - after. A Bitbucket row reads `open`, because prs reads no merge state from Bitbucket. `checks - passing`, `review required` and `review 2 posted` follow it. Each reads under every - theme and under `NO_COLOR`, and the colors are the terminal's own 16. -- **A Claude review round reads on its row.** Where one reviewed the pull request, a - `CLAUDE` cell after the review gives the newest round. Once its round comment posts, the - cell is its findings by section, as `claude 2 correctness · 1 design`. It adds `1 not - run` or `1 unread` where a reviewer did not run or its review could not be read, and - reads `claude did not run` where none ran. Before the round comment, it reads `claude - running` while the reviews post, and `claude unfinished` once they have stopped for an - hour. It reads `claude reviewed` where the newest review is of a version prs does not - read. The column shows only while a row fills it. + after. A Bitbucket row reads `open`, because prs reads no merge state from Bitbucket. + `checks passing` sits under the state, and `review required` or `review 2 posted` + follows the number. Each reads under every theme and under `NO_COLOR`, and the colors + are the terminal's own 16. +- **A Claude review round reads on its row.** Where one reviewed the pull request, its + second line gives the newest round after the review. Once its round comment posts, that + is its findings by section, as `claude 2 correctness · 1 design`. It adds `1 not run` or + `1 unread` where a reviewer did not run or its review could not be read, and reads + `claude did not run` where none ran. Before the round comment, it reads `claude running` + while the reviews post, and `claude unfinished` once they have stopped for an hour. It + reads `claude reviewed` where the newest review is of a version prs does not read. - **A pull request whose base is another open one's branch names that one**, as `↑ #12` - after its title. A pull request an app opened names the app. -- **The branch and the checkout are under the row**, dim, where `f` shows them. + after its number. A pull request an app opened names the app at the end of its second + line. +- **A checkout reads from `~`**, and a repo not cloned here reads as its owner/name, `not + cloned here` beside it. +- **The board keeps a margin of 12 columns a side**, and a terminal under 150 columns wide + gives up a column a side for every two it lacks. The side panel wants 160 columns inside + the margin, so it shows from 184. - **The glyphs are Nerd Font glyphs**, so the board wants a Nerd Font in the terminal. ## Each pull request's document @@ -287,8 +297,9 @@ prs tui --pr https://bitbucket.example.com/projects/FIN/repos/ledger/pull-reques ## Reading it from a program -`prs list` reads each provider once and prints the board's open rows as a table, the -people's then the bots', under a line naming any provider it could not read. It reads no +`prs list` reads each provider once and prints the board's open rows as a table, one line +a pull request, the people's then the bots', under a line naming any provider it could not +read. A `CLAUDE` column shows the newest review round while a row fills it. It reads no closed pull requests. `prs list --json` prints them as a JSON array, one object per pull request, in the shape [`cmd/prs/list.schema.json`](cmd/prs/list.schema.json) gives. Beside the row's columns it carries each pull request's state, its merge state, its diff --git a/cmd/prs/tui.go b/cmd/prs/tui.go index 6a3a096..8cb1ea8 100644 --- a/cmd/prs/tui.go +++ b/cmd/prs/tui.go @@ -40,21 +40,21 @@ show names, in three groups. people holds those a person opened, and bots those opened, each oldest first. recently closed holds those merged or closed within --since, newest first. Each header counts its rows, and enter on it folds them. -Each row gives where it stands in one word: ready, behind, conflicts, blocked, mergeable -with checks not passing, draft or unknown while open, and merged or closed after. Then its -checks and review in words, its age, or how long ago it closed, its diff, its repo and -number, and its title. f shows its branch and checkout under it. A Bitbucket row reads -open, and only one merged here reaches recently closed, because prs reads no merge state -or close time from Bitbucket. - -A pull request a Claude review round reviewed gains a CLAUDE cell after its review, from -the newest round: its findings by section once the round comment posts, as in claude 2 -correctness · 1 design, with any reviewer that did not run counted beside them. Before the -round comment, it reads claude running while the reviews post and claude unfinished once -they have stopped for an hour. It reads claude reviewed where the newest review is of a -version prs does not read. The column shows only while a row fills it. A window action -marked runs_review_round reads running while its window is open and the round has not -finished. +Each pull request takes two lines. The first gives where it stands in one word: ready, +behind, conflicts, blocked, mergeable with checks not passing, draft or unknown while open, +and merged or closed after. Then its title, its diff, its age, or how long ago it closed, +and its branch. The second gives its checks in words, its checkout, its number and its +review, and names the app that opened it. A Bitbucket row reads open, and only one merged +here reaches recently closed, because prs reads no merge state or close time from +Bitbucket. + +A pull request a Claude review round reviewed reads the newest round after its review: +its findings by section once the round comment posts, as in claude 2 correctness · 1 +design, with any reviewer that did not run counted beside them. Before the round comment, +it reads claude running while the reviews post and claude unfinished once they have +stopped for an hour. It reads claude reviewed where the newest review is of a version prs +does not read. A window action marked runs_review_round reads running while its window is +open and the round has not finished. It opens on the rows the last run on the same repos read, kept under $XDG_CACHE_HOME/prs, with their age on the first line, and reads each provider behind them. It reads the open diff --git a/cmd/prs/tui_test.go b/cmd/prs/tui_test.go index f76793c..0ebfc17 100644 --- a/cmd/prs/tui_test.go +++ b/cmd/prs/tui_test.go @@ -307,6 +307,21 @@ func gitHubStandIn(t *testing.T, listing *gitHub) string { return config } +// listSide is the list's half of each line of a frame, left of the side panel's border. +func listSide(frame string) string { + var list strings.Builder + for line := range strings.SplitSeq(frame, "\n") { + left, _, _ := strings.Cut(line, "│") + list.WriteString(left + "\n") + } + return list.String() +} + +// underRow matches a row whose first line holds title and whose second holds below. +func underRow(title, below string) string { + return regexp.QuoteMeta(title) + `[^\n]*\n[^\n]*` + regexp.QuoteMeta(below) +} + func fileExists(path string) bool { _, err := os.Stat(path) return err == nil @@ -393,6 +408,7 @@ window_actions: } } s.env = map[string]string{ + "HOME": dir, "PATH": bin + string(os.PathListSeparator) + os.Getenv("PATH"), "XDG_CONFIG_HOME": filepath.Join(dir, "config"), "TMUX": filepath.Join(dir, "tmux") + ",1,0", @@ -421,7 +437,7 @@ func (s standIn) ran(t *testing.T, lines ...string) bool { func (s standIn) run(t *testing.T, args ...string) (string, string, int) { t.Helper() cmd := exec.Command(tuidrive.Build(t, "."), args...) - cmd.Env = []string{"HOME=" + t.TempDir()} + cmd.Env = []string{} for name, value := range s.env { cmd.Env = append(cmd.Env, name+"="+value) } @@ -587,8 +603,8 @@ func TestTheBoardSortsPeopleAboveBotsAndClosedBelowInWordsShowsTheDocumentBeside } { term.WaitForPattern(pattern) } - text := term.Frame().Text() - order := []string{"#12 Cache the widget lookups", "#13 Count cache hits", "#9 Log the widget misses", "#8 Bump the gadget's linter"} + text := listSide(term.Frame().Text()) + order := []string{"Cache the widget lookups", "Count cache hits", "Log the widget misses", "Bump the gadget's linter"} for i := 1; i < len(order); i++ { if strings.Index(text, order[i-1]) > strings.Index(text, order[i]) { t.Errorf("%q should sit above %q:\n%s", order[i-1], order[i], text) @@ -602,7 +618,8 @@ func TestTheBoardSortsPeopleAboveBotsAndClosedBelowInWordsShowsTheDocumentBeside term.WaitFor("mona approved at 9e8d7c6") beside := false for line := range strings.SplitSeq(term.Frame().Text(), "\n") { - beside = beside || strings.Contains(line, "review ") && strings.Contains(line, "acme/widget #12 · opened by octo") + list, document, found := strings.Cut(line, "│") + beside = beside || found && strings.TrimSpace(list) != "" && strings.Contains(document, "acme/widget #12 · opened by octo") } if !beside { t.Errorf("#12's document should share a line of the screen with a row of the table:\n%s", term.Frame().Text()) @@ -648,9 +665,9 @@ func TestEachRowReadsItsNewestReviewRoundAndItsDocumentHoldsTheRound(t *testing. t.Fatal(err) } term := tuidrive.Start(t, tuidrive.Program{Path: tuidrive.Build(t, "."), Args: []string{"tui"}, Env: s.env, Cols: 300, Rows: 40}) - term.WaitForPattern(`claude 5 correctness · 3 design · 4 prose .*Count cache hits`) - term.WaitForPattern(`claude unfinished .*Tidy the gadget`) - term.WaitForPattern(`claude reviewed .*Cache the widget lookups`) + term.WaitForPattern(underRow("Count cache hits", "claude 5 correctness · 3 design · 4 prose")) + term.WaitForPattern(underRow("Tidy the gadget", "claude unfinished")) + term.WaitForPattern(underRow("Cache the widget lookups", "claude reviewed")) // d hands nvim the whole document, which the stand-in nvim keeps. The people's rows run // ledger #4, widget #12 and gadget #5, then widget #13 under the bots' header. @@ -705,11 +722,11 @@ func TestEachRowReadsItsNewestReviewRoundAndItsDocumentHoldsTheRound(t *testing. // The cursor is on #13, whose round finished before r opened a window for another. term.Press("r") term.WaitFor("opened review widget #13 in a tmux window") - term.WaitForPattern(`claude running .*Count cache hits`) + term.WaitForPattern(underRow("Count cache hits", "claude running")) if err := os.WriteFile(s.windows, nil, 0o644); err != nil { t.Fatal(err) } - term.WaitForPattern(`claude 5 correctness · 3 design · 4 prose .*Count cache hits`) + term.WaitForPattern(underRow("Count cache hits", "claude 5 correctness · 3 design · 4 prose")) term.Press("q") if code := term.WaitExit(); code != 0 { t.Errorf("q exited %d", code) @@ -722,7 +739,7 @@ func TestEachRowReadsItsNewestReviewRoundAndItsDocumentHoldsTheRound(t *testing. func TestTheDiffSectionShowsASmallDiffWholeAndNamesALargeOnesSize(t *testing.T) { t.Parallel() s := newStandIn(t) - term := tuidrive.Start(t, tuidrive.Program{Path: tuidrive.Build(t, "."), Args: []string{"tui"}, Env: s.env, Cols: 340, Rows: 40}) + term := tuidrive.Start(t, tuidrive.Program{Path: tuidrive.Build(t, "."), Args: []string{"tui"}, Env: s.env, Cols: 360, Rows: 40}) term.WaitFor("people · 3") term.WaitFor(" #5") @@ -923,7 +940,7 @@ func TestOneQuestionMergesEveryMarkedGreenPullRequestInTurnEachAfterItsOwnRead(t t.Fatal(err) } term.Press("enter") - term.WaitFor("merged 3 of 4: acme/widget #21, acme/widget #23, acme/widget #25. Left acme/widget #22: GitHub reads its merge state as behind, not ready") + term.WaitForPattern(`merged 3 of 4: acme/widget #21, acme/widget #23, acme/widget #25\. Left acme/widget #22: GitHub reads its merge state as behind,\s+not\s+ready`) recorded, _ := os.ReadFile(s.recorded) var events []string diff --git a/go.mod b/go.mod index 97ed4a8..2bb8f06 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/cli/go-gh/v2 v2.17.0 github.com/datapointchris/goclikit v0.5.3 github.com/datapointchris/goselfupdate v0.11.2 - github.com/datapointchris/gotermboard v0.30.0 + github.com/datapointchris/gotermboard v0.30.1-0.20261108084638-374b01f425e6 github.com/datapointchris/gotermstyle v0.4.0 github.com/datapointchris/tuidrive v0.1.3 github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 diff --git a/go.sum b/go.sum index 5a9965b..32176ed 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ github.com/datapointchris/goclikit v0.5.3 h1:6ec9nuswLXeIqZLa0YVEQeqsCnrjkqF/3gD github.com/datapointchris/goclikit v0.5.3/go.mod h1:hEbplQyvrWq5m5UepXVNKfv8jvvlmQshNMrWdoHBb6Y= github.com/datapointchris/goselfupdate v0.11.2 h1:7aiYmeatQnfLqHiFawNlFHUohxAyOsiKyio8Vosje4I= github.com/datapointchris/goselfupdate v0.11.2/go.mod h1:k3gVv/y3AwoAyGGn5sN/UcAxhANB2W+GdN9rYw/oob4= -github.com/datapointchris/gotermboard v0.30.0 h1:UOeJm18Ma3a9s6crk0eBxePNhAwjybgBLhCP6g1jMkQ= -github.com/datapointchris/gotermboard v0.30.0/go.mod h1:gzSG1LtOC2McInuKV5mSFJOLZ3EnFIY/1chPyivuxe0= +github.com/datapointchris/gotermboard v0.30.1-0.20261108084638-374b01f425e6 h1:eY7+7muo0O8bWbnUYU+sowJN0cOt73dyjnyaJERjNKo= +github.com/datapointchris/gotermboard v0.30.1-0.20261108084638-374b01f425e6/go.mod h1:gzSG1LtOC2McInuKV5mSFJOLZ3EnFIY/1chPyivuxe0= github.com/datapointchris/gotermstyle v0.4.0 h1:kOovTrAVF4oI/P6ptNg5VTuU9woNpPyjrhcKd6ndJsI= github.com/datapointchris/gotermstyle v0.4.0/go.mod h1:pcTXWC/xls5Riw0rFtTueCQGlnszB3sqlepRItgdgeg= github.com/datapointchris/tuidrive v0.1.3 h1:Gmu8Lye5+UMMJB30AbMk0EHosWk6AOW+PxgiEWi7dMA= diff --git a/internal/board/board.go b/internal/board/board.go index 09b4b9c..747d318 100644 --- a/internal/board/board.go +++ b/internal/board/board.go @@ -71,8 +71,6 @@ type Board struct { // their windows are open, nil until ReadDetails is called inside tmux. reviewActions []config.WindowAction roundWindows *roundWindows - // columns are the columns the frame being built draws. - columns []rowColumn // working is every command a key started that has not ended, oldest first. working []underWay workCount int @@ -377,7 +375,6 @@ func (b *Board) Frame(now time.Time, _ string) gotermboard.Frame { } prs := b.records(now, extra...) parents := stackParents(prs) - b.columns = b.drawnColumns(prs, now) rows := ordered(b.hold(placedBy(prs))) clear(b.shown) clear(b.drawnIn) @@ -395,7 +392,7 @@ func (b *Board) Frame(now time.Time, _ string) gotermboard.Frame { maps.DeleteFunc(b.rounds, func(key string, _ roundsMemo) bool { return !b.listed(key) }) return gotermboard.Frame{ Strip: b.StripAt(now), - Columns: headers(b.columns), + Columns: headers(boardColumns), Groups: groups, StartAt: b.startAt(), } diff --git a/internal/board/rows.go b/internal/board/rows.go index 1c2e270..4148260 100644 --- a/internal/board/rows.go +++ b/internal/board/rows.go @@ -142,9 +142,9 @@ func stateMeaning(pr pullrequests.PullRequest, state rowState) meaning { // words is an enum as words, CHANGES_REQUESTED as "changes requested". func words(enum string) string { return strings.ToLower(strings.ReplaceAll(enum, "_", " ")) } -// rowColumn is one column of the board: its header, and the cell it draws on a pull request's -// row. The last column, the title, wraps under itself and draws no cell of its own. An optional -// column draws only while a row fills it. +// rowColumn is one column of a table of pull requests: its header, and the cell it draws on a +// pull request's line. A column with no cell is the line's last, which the table fills itself. +// An optional column draws only while a row fills it. type rowColumn struct { gotermboard.Column cell func(b *Board, at rowAt) gotermboard.Cell @@ -158,13 +158,26 @@ type rowAt struct { now time.Time } -// rowColumns carry the state, the checks, the review, the newest Claude review round, the age -// and the sizes, then the repo and the number, then the title wrapping under itself. The -// round's column is optional, so a board on repos no round reviews never has it. The three -// diff counts each right-align in a column of their own, so the signs stack. The branch and -// the checkout are read when acting rather than when scanning, so they sit dim under the row, -// where f shows them. -var rowColumns = []rowColumn{ +// boardColumns are a pull request's first line on the board: where it stands, its title, its +// diff, its age and its branch, spread across the width. The diff's three counts each +// right-align on a column of their own, so the signs stack, and the two after the first carry +// no title, so the spread keeps them beside it. Its second line, under the first column, is +// secondLine. +var boardColumns = []rowColumn{ + {Column: gotermboard.Column{Title: "STATUS"}, cell: func(_ *Board, at rowAt) gotermboard.Cell { return stateCell(at.pr, at.state) }}, + {Column: gotermboard.Column{Title: "TITLE"}, cell: func(_ *Board, at rowAt) gotermboard.Cell { return titleCell(at.pr, at.state) }}, + {Column: gotermboard.Column{Title: "DIFF", Right: true}, cell: diffCell(0)}, + {Column: gotermboard.Column{Right: true}, cell: diffCell(1)}, + {Column: gotermboard.Column{Right: true}, cell: diffCell(2)}, + {Column: gotermboard.Column{Title: "AGE", Right: true}, cell: func(_ *Board, at rowAt) gotermboard.Cell { return ageCell(at.pr, at.state, at.now) }}, + {Column: gotermboard.Column{Title: "BRANCH"}, cell: func(_ *Board, at rowAt) gotermboard.Cell { return branchCell(at.pr) }}, +} + +// listColumns are what prs list prints of a pull request on one line: the state, the checks, +// the review, the newest Claude review round, the age and the sizes, then the repo and the +// number, then the title. The round's column is optional, so a list of repos no round reviews +// never has it. +var listColumns = []rowColumn{ {Column: gotermboard.Column{Title: "STATE"}, cell: func(_ *Board, at rowAt) gotermboard.Cell { return stateCell(at.pr, at.state) }}, {Column: gotermboard.Column{Title: "CHECKS"}, cell: func(_ *Board, at rowAt) gotermboard.Cell { return checksCell(at.pr) }}, {Column: gotermboard.Column{Title: "REVIEW"}, cell: func(_ *Board, at rowAt) gotermboard.Cell { return reviewCell(at.pr) }}, @@ -184,9 +197,9 @@ var rowColumns = []rowColumn{ {Column: gotermboard.Column{Title: "PULL REQUEST"}}, } -// drawnColumns is rowColumns less each optional one no row of prs fills. -func (b *Board) drawnColumns(prs []pullrequests.PullRequest, now time.Time) []rowColumn { - return slices.DeleteFunc(slices.Clone(rowColumns), func(column rowColumn) bool { +// listedColumns is listColumns less each optional one no row of prs fills. +func (b *Board) listedColumns(prs []pullrequests.PullRequest, now time.Time) []rowColumn { + return slices.DeleteFunc(slices.Clone(listColumns), func(column rowColumn) bool { return column.optional && !slices.ContainsFunc(prs, func(pr pullrequests.PullRequest) bool { return column.cell(b, rowAt{pr: pr, state: b.stateOf(pr), now: now}).Text != "" }) @@ -227,19 +240,23 @@ func parentOf(pr pullrequests.PullRequest, parents map[[2]string]int) int { return parent } -func (b *Board) row(pr pullrequests.PullRequest, parents map[[2]string]int, now time.Time) gotermboard.Row { - at := rowAt{pr: pr, state: b.stateOf(pr), now: now} +// cells are the cells columns draw on the pull request's line. +func (b *Board) cells(columns []rowColumn, at rowAt) []gotermboard.Cell { var cells []gotermboard.Cell - for _, column := range b.columns { + for _, column := range columns { if column.cell != nil { cells = append(cells, column.cell(b, at)) } } + return cells +} + +func (b *Board) row(pr pullrequests.PullRequest, parents map[[2]string]int, now time.Time) gotermboard.Row { + at := rowAt{pr: pr, state: b.stateOf(pr), now: now} return gotermboard.Row{ Key: pr.Key(), - Cells: cells, - WrapLast: titleLine(pr, parentOf(pr, parents), at.state), - Under: gotermboard.C(identity(pr), dim), + Cells: b.cells(boardColumns, at), + Second: b.secondLine(at, parentOf(pr, parents)), Document: b.document(pr), ReaderTitle: pr.Title, StartsOpen: b.isOpenedOn(pr), @@ -342,15 +359,19 @@ func repoCell(pr pullrequests.PullRequest) gotermboard.Cell { return gotermboard.C(pathGlyph+" "+pr.Repo, gotermboard.Cyan) } -// titleLine is the title, the open pull request it is stacked on, and the app that opened it. -// A pull request a person opened carries no mark, so the mark sets an app's row apart. A -// draft's title and one with a row state are dim, because neither waits on a reviewer. -func titleLine(pr pullrequests.PullRequest, parent int, state rowState) gotermboard.Line { - code := "" +// titleCell is the title. A draft's and one with a row state are dim, because neither waits +// on a reviewer. +func titleCell(pr pullrequests.PullRequest, state rowState) gotermboard.Cell { if state != "" || pr.Draft { - code = dim + return gotermboard.C(pr.Title, dim) } - line := gotermboard.Line{gotermboard.C(pr.Title, code)} + return gotermboard.C(pr.Title, "") +} + +// titleLine is the title, the open pull request it is stacked on, and the app that opened it. +// A pull request a person opened carries no mark, so the mark sets an app's row apart. +func titleLine(pr pullrequests.PullRequest, parent int, state rowState) gotermboard.Line { + line := gotermboard.Line{titleCell(pr, state)} if parent != 0 { line = append(line, gotermboard.C(fmt.Sprintf(" %s #%d", stackGlyph, parent), gotermboard.Yellow)) } @@ -360,19 +381,66 @@ func titleLine(pr pullrequests.PullRequest, parent int, state rowState) gotermbo return line } -// identity is the branch, the base it targets, and the checkout on this disk, written -// ~-rooted as it would be typed. -func identity(pr pullrequests.PullRequest) string { - where := "not cloned here" - if pr.Path != "" { - where = pr.Path - if home, err := os.UserHomeDir(); err == nil { - if rest, ok := strings.CutPrefix(where, home); ok { - where = "~" + rest - } +// branchCell is the branch the pull request merges from. +func branchCell(pr pullrequests.PullRequest) gotermboard.Cell { + return gotermboard.C(branchGlyph+" "+orDash(pr.Branch), gotermboard.Blue) +} + +// fieldGap is the space between two fields of a second line, wider than a field's own glyph +// and words are apart, so each reads as a field. +var fieldGap = gotermboard.C(" ", "") + +// secondLine is a pull request's second line on the board: its checks under where it stands, +// then where it is checked out, its number and the open pull request it is stacked on, its +// review, its newest Claude review round, and the app that opened it. A field a pull request +// does not have takes no room. The line is cut at the table's edge, so a narrow screen loses +// its end first. +func (b *Board) secondLine(at rowAt, parent int) gotermboard.Line { + pr := at.pr + line := gotermboard.Line{checksCell(pr)} + line = append(line, whereCells(pr)...) + line = append(line, fieldGap, gotermboard.C(fmt.Sprintf("%s #%d", numberGlyph, pr.Number), gotermboard.Yellow)) + if parent != 0 { + line = append(line, gotermboard.C(fmt.Sprintf(" %s #%d", stackGlyph, parent), gotermboard.Yellow)) + } + line = append(line, fieldGap, reviewCell(pr)) + if claude := claudeCell(b.reviewOf(pr, at.now)); claude.Text != "" { + line = append(line, fieldGap, claude) + } + if pr.Bot != nil && *pr.Bot { + line = append(line, fieldGap, gotermboard.C(botGlyph+" bot "+pr.Author, gotermboard.Magenta)) + } + return line +} + +// whereCells are where the pull request is checked out on this disk, written ~-rooted as it +// would be typed, or its repo where it is not cloned here, which d and the window keys refuse. +func whereCells(pr pullrequests.PullRequest) gotermboard.Line { + if pr.Path == "" { + return gotermboard.Line{gotermboard.C(pathGlyph+" "+pr.Slug, gotermboard.Cyan), gotermboard.C(" not cloned here", dim)} + } + where := pr.Path + if home, err := os.UserHomeDir(); err == nil { + if rest, ok := strings.CutPrefix(where, home); ok { + where = "~" + rest } } - return fmt.Sprintf("%s %s into %s %s %s", branchGlyph, orDash(pr.Branch), orDash(pr.Base), pathGlyph, where) + return gotermboard.Line{gotermboard.C(pathGlyph+" "+where, gotermboard.Cyan)} +} + +// sideMargin is the columns of margin at each side of the board from marginFrom columns +// wide, which narrower screens give up a column a side for every two they lack. +const ( + sideMargin = 12 + marginFrom = 150 +) + +// Look is gotermboard.LookBoard's: a margin around the screen, the columns spread across the +// width, rules under the titles and above the keys, and the cursor's pull request bold on the +// selection. Every row opens its document, so no row draws a chevron. +func (b *Board) Look(width int) gotermboard.Look { + sides := min(sideMargin, max(sideMargin-(marginFrom-width+1)/2, 1)) + return gotermboard.Look{Sides: sides, Ends: 1, Spread: true, Rules: true, BoldCursor: true, NoRowChevrons: true} } func orDash(text string) string { diff --git a/internal/board/sections.go b/internal/board/sections.go index 55edd42..0c4a6b0 100644 --- a/internal/board/sections.go +++ b/internal/board/sections.go @@ -235,7 +235,10 @@ func (b *Board) groups(rows []placed, parents map[[2]string]int, now time.Time) if s.in == recentlyClosed && b.unread() && !b.failed() { continue } - group := gotermboard.Group{Key: s.key, Title: gotermboard.Line{gotermboard.C(s.title, gotermboard.Bold)}, Counted: true, Gap: true} + group := gotermboard.Group{ + Key: s.key, Title: gotermboard.Line{gotermboard.C(s.title, gotermboard.Bold)}, Counted: true, Gap: true, + BetweenRows: gotermboard.RuleDotted, + } for _, p := range rows { if p.in != s.in { continue diff --git a/internal/board/table.go b/internal/board/table.go index 606cf34..3be0aa0 100644 --- a/internal/board/table.go +++ b/internal/board/table.go @@ -17,8 +17,8 @@ const columnGap = " " func (b *Board) Table(now time.Time) []gotermboard.Line { prs := b.records(now) parents := stackParents(prs) - b.columns = b.drawnColumns(prs, now) - columns := headers(b.columns) + listed := b.listedColumns(prs, now) + columns := headers(listed) lines := []gotermboard.Line{gotermboard.ReadStatus(b.statuses(), now)} if len(prs) == 0 { return append(lines, gotermboard.Line{gotermboard.C(b.emptyReason(), dim)}) @@ -29,8 +29,8 @@ func (b *Board) Table(now time.Time) []gotermboard.Line { } rows := [][]gotermboard.Cell{header} for _, p := range ordered(placedBy(prs)) { - row := b.row(p.pr, parents, now) - rows = append(rows, append(row.Cells, row.WrapLast...)) + at := rowAt{pr: p.pr, state: b.stateOf(p.pr), now: now} + rows = append(rows, append(b.cells(listed, at), titleLine(p.pr, parentOf(p.pr, parents), at.state)...)) } widths := make([]int, len(columns)-1) for _, cells := range rows { From 853c293ee5929ac92807a57e8ef8ce46a10844b2 Mon Sep 17 00:00:00 2001 From: Chris Birch Date: Sun, 8 Nov 2026 04:13:27 -0500 Subject: [PATCH 2/3] fix: give the board's margin up to the side panel, and root ~ at a path segment gotermboard measures the side panel's threshold inside the margin, so a terminal from 160 to 183 columns wide lost the panel, and prs tui --once at its default width drew no document. The margin now narrows as the screen nears the panel's width plus both sides, so every screen wide enough for the panel with no margin keeps it. A checkout reads ~-rooted only where it sits inside the home directory at a path segment, so a sibling of home keeps its whole path. The number, the stacked parent and the bot each have one cell builder, which the board's second line and prs list's title line both place. prs list's number and bot take two spaces after their glyph, as every other field with words does. The README and CLAUDE.md say where the margin and panel come from rather than copying their numbers. gotermboard is pinned at the commit that keeps a look whole under tabs, on an empty frame and beside a wrapped last column. Agent-Name: claude-code-unattended Agent-Session: 69e19fe4-568c-41ef-9af1-316d086cd270 Agent-Run: 01a1ba5c-b70b-7c95-89fd-537c2603d8a5 Worklog: 2568.1 --- CLAUDE.md | 17 +++++---- README.md | 9 ++--- go.mod | 2 +- go.sum | 4 +- internal/board/rows.go | 75 +++++++++++++++++++++++++++---------- internal/board/rows_test.go | 18 +++++++++ internal/board/sections.go | 2 +- 7 files changed, 90 insertions(+), 37 deletions(-) create mode 100644 internal/board/rows_test.go diff --git a/CLAUDE.md b/CLAUDE.md index ac152cb..49677fe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -133,12 +133,13 @@ is the shape of the threads file `d` hands nvim, held the same way. round reviews never has it. On the board the round follows the review on a row's second line. `listColumns` and `boardColumns` hold every column with the cell it draws, so a column's header and its cells cannot drift apart. -- **A pull request is two lines on the board, and a field keeps its line.** The first is - `boardColumns`: state, title, diff, age and branch. The second is `secondLine`: checks - under the state, then the checkout, the number, the review, the round and the bot. A new - field goes at the end of `secondLine`'s flow before the bot, or into `boardColumns`. The - second line is cut at the table's edge, so the checkout's length decides what a narrow - screen loses. +- **A pull request is two lines on the board, and a field keeps its line.** The first line + is `boardColumns` and the second is `secondLine`. A new field goes at the end of + `secondLine`'s flow before the bot, or into `boardColumns`. The second line is cut at the + table's edge, so the checkout's length decides what a narrow screen loses. +- **A field the board and `prs list` both show has one cell builder**, such as `numberCell` + or `botCell`, so the two layouts choose only where it sits. `prs list` places them through + `listColumns` and `titleLine`. - **A window marked `runs_review_round` reads running only until a round finishes after prs opened it.** A session that ran a round keeps its window afterwards, so one prs did not open reads running only where no round has finished. tmux is asked every @@ -192,8 +193,8 @@ is the shape of the threads file `d` hands nvim, held the same way. otherwise title the Reader with the row's first cells, which here are its state words. - **`Board.Look` is gotermboard's `LookBoard`**: a margin, the columns spread across the width, rules under the titles and over the keys, the cursor's row bold on the selection, - and no row chevrons. `PanelFrom` is measured inside the margin, so the side panel shows - from 184 columns. + and no row chevrons. `PanelFrom` is measured inside the margin, so the margin gives way + from `PanelFrom` columns and the panel shows wherever it would with no margin. - **A column with no title continues the one before it under `Look.Spread`**, so the diff's three counts take two untitled columns after DIFF and stay together. - **`f` applies only where a row or group sets `Under`**, and no prs row does, so `f` diff --git a/README.md b/README.md index 6648ce0..7943707 100644 --- a/README.md +++ b/README.md @@ -125,8 +125,8 @@ field it filters on drops a repo silently, and every list it prints still looks - **Each pull request takes two lines, with a dotted rule between it and the next.** The first line holds where it stands, its title, its diff, its age and its branch, its columns spread across the width. The second holds its checks, its checkout, its number - and its review. A glyph marks each field, and a field's glyph is the same in every - state. + and its review. A glyph marks each field but the title, and a field's glyph is the same + in every state. - **State is a word, never a color alone.** The first column says where a pull request stands: `ready`, `behind`, `conflicts`, `blocked`, `draft` or `unknown` while open, or `mergeable` where it can merge with its checks not passing, and `merged` or `closed` @@ -146,9 +146,8 @@ field it filters on drops a repo silently, and every list it prints still looks line. - **A checkout reads from `~`**, and a repo not cloned here reads as its owner/name, `not cloned here` beside it. -- **The board keeps a margin of 12 columns a side**, and a terminal under 150 columns wide - gives up a column a side for every two it lacks. The side panel wants 160 columns inside - the margin, so it shows from 184. +- **The board keeps a margin at each side**, which a narrow terminal gives up first. The + margin also gives way to the side panel, so a terminal wide enough for the panel shows it. - **The glyphs are Nerd Font glyphs**, so the board wants a Nerd Font in the terminal. ## Each pull request's document diff --git a/go.mod b/go.mod index 2bb8f06..f91a2bb 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/cli/go-gh/v2 v2.17.0 github.com/datapointchris/goclikit v0.5.3 github.com/datapointchris/goselfupdate v0.11.2 - github.com/datapointchris/gotermboard v0.30.1-0.20261108084638-374b01f425e6 + github.com/datapointchris/gotermboard v0.30.1-0.20261108091225-2602f096bd67 github.com/datapointchris/gotermstyle v0.4.0 github.com/datapointchris/tuidrive v0.1.3 github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 diff --git a/go.sum b/go.sum index 32176ed..a43a7db 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ github.com/datapointchris/goclikit v0.5.3 h1:6ec9nuswLXeIqZLa0YVEQeqsCnrjkqF/3gD github.com/datapointchris/goclikit v0.5.3/go.mod h1:hEbplQyvrWq5m5UepXVNKfv8jvvlmQshNMrWdoHBb6Y= github.com/datapointchris/goselfupdate v0.11.2 h1:7aiYmeatQnfLqHiFawNlFHUohxAyOsiKyio8Vosje4I= github.com/datapointchris/goselfupdate v0.11.2/go.mod h1:k3gVv/y3AwoAyGGn5sN/UcAxhANB2W+GdN9rYw/oob4= -github.com/datapointchris/gotermboard v0.30.1-0.20261108084638-374b01f425e6 h1:eY7+7muo0O8bWbnUYU+sowJN0cOt73dyjnyaJERjNKo= -github.com/datapointchris/gotermboard v0.30.1-0.20261108084638-374b01f425e6/go.mod h1:gzSG1LtOC2McInuKV5mSFJOLZ3EnFIY/1chPyivuxe0= +github.com/datapointchris/gotermboard v0.30.1-0.20261108091225-2602f096bd67 h1:OuXtn4j+lrLtQmiziaNFMU7V5dAQt8o7Q43mjHwmtiQ= +github.com/datapointchris/gotermboard v0.30.1-0.20261108091225-2602f096bd67/go.mod h1:gzSG1LtOC2McInuKV5mSFJOLZ3EnFIY/1chPyivuxe0= github.com/datapointchris/gotermstyle v0.4.0 h1:kOovTrAVF4oI/P6ptNg5VTuU9woNpPyjrhcKd6ndJsI= github.com/datapointchris/gotermstyle v0.4.0/go.mod h1:pcTXWC/xls5Riw0rFtTueCQGlnszB3sqlepRItgdgeg= github.com/datapointchris/tuidrive v0.1.3 h1:Gmu8Lye5+UMMJB30AbMk0EHosWk6AOW+PxgiEWi7dMA= diff --git a/internal/board/rows.go b/internal/board/rows.go index 4148260..26e2191 100644 --- a/internal/board/rows.go +++ b/internal/board/rows.go @@ -3,6 +3,7 @@ package board import ( "fmt" "os" + "path/filepath" "slices" "strconv" "strings" @@ -191,9 +192,7 @@ var listColumns = []rowColumn{ {Column: gotermboard.Column{Right: true}, cell: diffCell(1)}, {Column: gotermboard.Column{Right: true}, cell: diffCell(2)}, {Column: gotermboard.Column{Title: "REPO", Max: repoWidth}, cell: func(_ *Board, at rowAt) gotermboard.Cell { return repoCell(at.pr) }}, - {Column: gotermboard.Column{Title: "#", Right: true}, cell: func(_ *Board, at rowAt) gotermboard.Cell { - return gotermboard.C(fmt.Sprintf("%s #%d", numberGlyph, at.pr.Number), gotermboard.Yellow) - }}, + {Column: gotermboard.Column{Title: "#", Right: true}, cell: func(_ *Board, at rowAt) gotermboard.Cell { return numberCell(at.pr) }}, {Column: gotermboard.Column{Title: "PULL REQUEST"}}, } @@ -373,19 +372,39 @@ func titleCell(pr pullrequests.PullRequest, state rowState) gotermboard.Cell { func titleLine(pr pullrequests.PullRequest, parent int, state rowState) gotermboard.Line { line := gotermboard.Line{titleCell(pr, state)} if parent != 0 { - line = append(line, gotermboard.C(fmt.Sprintf(" %s #%d", stackGlyph, parent), gotermboard.Yellow)) + line = append(line, titleGap, stackCell(parent)) } - if pr.Bot != nil && *pr.Bot { - line = append(line, gotermboard.C(" "+botGlyph+" bot "+pr.Author, gotermboard.Magenta)) + if isBot(pr) { + line = append(line, titleGap, botCell(pr)) } return line } +// titleGap is the space between the title and each mark after it in prs list. +var titleGap = gotermboard.C(" ", "") + // branchCell is the branch the pull request merges from. func branchCell(pr pullrequests.PullRequest) gotermboard.Cell { return gotermboard.C(branchGlyph+" "+orDash(pr.Branch), gotermboard.Blue) } +// numberCell is the pull request's number. +func numberCell(pr pullrequests.PullRequest) gotermboard.Cell { + return gotermboard.C(fmt.Sprintf("%s #%d", numberGlyph, pr.Number), gotermboard.Yellow) +} + +// stackCell is the number of the open pull request this one is stacked on. +func stackCell(parent int) gotermboard.Cell { + return gotermboard.C(fmt.Sprintf("%s #%d", stackGlyph, parent), gotermboard.Yellow) +} + +// botCell names the app that opened the pull request. +func botCell(pr pullrequests.PullRequest) gotermboard.Cell { + return gotermboard.C(botGlyph+" bot "+pr.Author, gotermboard.Magenta) +} + +func isBot(pr pullrequests.PullRequest) bool { return pr.Bot != nil && *pr.Bot } + // fieldGap is the space between two fields of a second line, wider than a field's own glyph // and words are apart, so each reads as a field. var fieldGap = gotermboard.C(" ", "") @@ -399,37 +418,48 @@ func (b *Board) secondLine(at rowAt, parent int) gotermboard.Line { pr := at.pr line := gotermboard.Line{checksCell(pr)} line = append(line, whereCells(pr)...) - line = append(line, fieldGap, gotermboard.C(fmt.Sprintf("%s #%d", numberGlyph, pr.Number), gotermboard.Yellow)) + line = append(line, fieldGap, numberCell(pr)) if parent != 0 { - line = append(line, gotermboard.C(fmt.Sprintf(" %s #%d", stackGlyph, parent), gotermboard.Yellow)) + line = append(line, gotermboard.C(" ", ""), stackCell(parent)) } line = append(line, fieldGap, reviewCell(pr)) if claude := claudeCell(b.reviewOf(pr, at.now)); claude.Text != "" { line = append(line, fieldGap, claude) } - if pr.Bot != nil && *pr.Bot { - line = append(line, fieldGap, gotermboard.C(botGlyph+" bot "+pr.Author, gotermboard.Magenta)) + if isBot(pr) { + line = append(line, fieldGap, botCell(pr)) } return line } -// whereCells are where the pull request is checked out on this disk, written ~-rooted as it -// would be typed, or its repo where it is not cloned here, which d and the window keys refuse. +// whereCells are where the pull request is checked out on this disk, or its repo where it is +// not cloned here, which d and the window keys refuse. func whereCells(pr pullrequests.PullRequest) gotermboard.Line { if pr.Path == "" { return gotermboard.Line{gotermboard.C(pathGlyph+" "+pr.Slug, gotermboard.Cyan), gotermboard.C(" not cloned here", dim)} } - where := pr.Path - if home, err := os.UserHomeDir(); err == nil { - if rest, ok := strings.CutPrefix(where, home); ok { - where = "~" + rest - } + return gotermboard.Line{gotermboard.C(pathGlyph+" "+homeAsTilde(pr.Path), gotermboard.Cyan)} +} + +// homeAsTilde is path for a person to read, with the home directory written ~ where path sits +// inside it. The cut falls at a path segment, so a sibling of home keeps its whole path. +func homeAsTilde(path string) string { + home, err := os.UserHomeDir() + if err != nil || home == "" { + return path } - return gotermboard.Line{gotermboard.C(pathGlyph+" "+where, gotermboard.Cyan)} + if path == home { + return "~" + } + if rest, ok := strings.CutPrefix(path, home+string(filepath.Separator)); ok { + return "~" + string(filepath.Separator) + rest + } + return path } // sideMargin is the columns of margin at each side of the board from marginFrom columns -// wide, which narrower screens give up a column a side for every two they lack. +// wide. A narrower screen gives up a column a side for every two columns it lacks, rounded +// up, and keeps one. const ( sideMargin = 12 marginFrom = 150 @@ -437,9 +467,14 @@ const ( // Look is gotermboard.LookBoard's: a margin around the screen, the columns spread across the // width, rules under the titles and above the keys, and the cursor's pull request bold on the -// selection. Every row opens its document, so no row draws a chevron. +// selection. The margin gives way to the side panel, which gotermboard measures inside it, so +// a screen wide enough for the panel keeps it. Every row opens its document, so no row draws +// a chevron. func (b *Board) Look(width int) gotermboard.Look { sides := min(sideMargin, max(sideMargin-(marginFrom-width+1)/2, 1)) + if width >= gotermboard.PanelFrom { + sides = min(sides, (width-gotermboard.PanelFrom)/2) + } return gotermboard.Look{Sides: sides, Ends: 1, Spread: true, Rules: true, BoldCursor: true, NoRowChevrons: true} } diff --git a/internal/board/rows_test.go b/internal/board/rows_test.go new file mode 100644 index 0000000..2a4db5c --- /dev/null +++ b/internal/board/rows_test.go @@ -0,0 +1,18 @@ +package board + +import ( + "testing" + + "github.com/datapointchris/gotermboard" +) + +// A terminal PanelFrom columns wide or wider shows the side panel beside the list, because +// the margin gives way: the width inside it still reaches PanelFrom. +func TestATerminalWideEnoughForTheSidePanelKeepsItInsideTheMargin(t *testing.T) { + var b Board + for width := gotermboard.PanelFrom; width <= 2*gotermboard.PanelFrom; width++ { + if inside := width - 2*b.Look(width).Sides; inside < gotermboard.PanelFrom { + t.Errorf("at %d columns the margin leaves %d, under PanelFrom's %d", width, inside, gotermboard.PanelFrom) + } + } +} diff --git a/internal/board/sections.go b/internal/board/sections.go index 0c4a6b0..5f2aaf8 100644 --- a/internal/board/sections.go +++ b/internal/board/sections.go @@ -38,7 +38,7 @@ func sectionOf(pr pullrequests.PullRequest) section { if standing, err := pr.Standing(); err == nil && standing != pullrequests.Open { return recentlyClosed } - if pr.Bot != nil && *pr.Bot { + if isBot(pr) { return bots } return people From 495d8200e022113b2095327e993bb3d5336f2489 Mon Sep 17 00:00:00 2001 From: Chris Birch Date: Sun, 8 Nov 2026 10:03:07 -0500 Subject: [PATCH 3/3] build: pin gotermboard v0.35.0, the release that draws two-line rows ruled apart The board's look needs Row.Second, Group.BetweenRows and LookBoard, which gotermboard first tags in v0.35.0. The pin moves off the untagged commit it was built against. Agent-Name: claude-code Agent-Session: 60bee577-11ae-4c13-9517-75a25f6211a6 Worklog: 1786.1 --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index f91a2bb..553f1fb 100644 --- a/go.mod +++ b/go.mod @@ -10,8 +10,8 @@ require ( github.com/cli/go-gh/v2 v2.17.0 github.com/datapointchris/goclikit v0.5.3 github.com/datapointchris/goselfupdate v0.11.2 - github.com/datapointchris/gotermboard v0.30.1-0.20261108091225-2602f096bd67 - github.com/datapointchris/gotermstyle v0.4.0 + github.com/datapointchris/gotermboard v0.35.0 + github.com/datapointchris/gotermstyle v0.5.0 github.com/datapointchris/tuidrive v0.1.3 github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 github.com/spf13/cobra v1.10.2 diff --git a/go.sum b/go.sum index a43a7db..465abdf 100644 --- a/go.sum +++ b/go.sum @@ -52,10 +52,10 @@ github.com/datapointchris/goclikit v0.5.3 h1:6ec9nuswLXeIqZLa0YVEQeqsCnrjkqF/3gD github.com/datapointchris/goclikit v0.5.3/go.mod h1:hEbplQyvrWq5m5UepXVNKfv8jvvlmQshNMrWdoHBb6Y= github.com/datapointchris/goselfupdate v0.11.2 h1:7aiYmeatQnfLqHiFawNlFHUohxAyOsiKyio8Vosje4I= github.com/datapointchris/goselfupdate v0.11.2/go.mod h1:k3gVv/y3AwoAyGGn5sN/UcAxhANB2W+GdN9rYw/oob4= -github.com/datapointchris/gotermboard v0.30.1-0.20261108091225-2602f096bd67 h1:OuXtn4j+lrLtQmiziaNFMU7V5dAQt8o7Q43mjHwmtiQ= -github.com/datapointchris/gotermboard v0.30.1-0.20261108091225-2602f096bd67/go.mod h1:gzSG1LtOC2McInuKV5mSFJOLZ3EnFIY/1chPyivuxe0= -github.com/datapointchris/gotermstyle v0.4.0 h1:kOovTrAVF4oI/P6ptNg5VTuU9woNpPyjrhcKd6ndJsI= -github.com/datapointchris/gotermstyle v0.4.0/go.mod h1:pcTXWC/xls5Riw0rFtTueCQGlnszB3sqlepRItgdgeg= +github.com/datapointchris/gotermboard v0.35.0 h1:FSy9u9Mi30sfjBGmmc7lFDd71SdIBOPt8IGlNtSt9d4= +github.com/datapointchris/gotermboard v0.35.0/go.mod h1:A/9phVumidQPEVvHBLHTIWGgMfwbyDT+1/0iOHtjmao= +github.com/datapointchris/gotermstyle v0.5.0 h1:xdpuNASa4vS0jSMpBDQOO0rRFGY7jaft5H3hTHOrREE= +github.com/datapointchris/gotermstyle v0.5.0/go.mod h1:pcTXWC/xls5Riw0rFtTueCQGlnszB3sqlepRItgdgeg= github.com/datapointchris/tuidrive v0.1.3 h1:Gmu8Lye5+UMMJB30AbMk0EHosWk6AOW+PxgiEWi7dMA= github.com/datapointchris/tuidrive v0.1.3/go.mod h1:gkHhDQs6zOLaRUP9MFKNTh+IkegUq0RedcBeavctBYY= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=