Commit 7000aa47b1

7000aa47b179487348f77be1510104056de32258

parent: 3d834e058d

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-24 01:16 UTC

control: the next page as a command on stderr at a terminal

Ref #254
internal/control/control.go +4
@@ -43,6 +43,9 @@ type Ctx struct {
4343 // Cmd is the command being run, set by Dispatch, so a usage error can
4444 // print the registered usage rather than a copy of it.
4545 Cmd Command
46 // Argv is the command's arguments after its path, global flags
47 // removed, so output can print a command to run next.
48 Argv []string
4649 // Done, when the surface has one, closes when nobody is reading any
4750 // more: the SSH channel closed or the HTTP request ended. A command
4851 // that runs until something happens (build log --follow) stops on it.
@@ -142,6 +145,7 @@ func Dispatch(c *Ctx, argv []string) int {
142145 }
143146 args = append(args, a)
144147 }
148 c.Argv = args
145149 // A runner-scoped key reaches the runner protocol and nothing else, so
146150 // the key a CI host holds cannot administer the instance.
147151 if c.Scope != "full" && !(c.Scope == "runner" && cmd.Path[0] == "runner") {
internal/control/cursor.go +14 −1
@@ -120,8 +120,21 @@ func (c *Ctx) emitPage(p page, items any, next string, plain func(w io.Writer))
120120 }
121121 return c.emit(out{items, next}, func(w io.Writer) {
122122 plain(w)
123 if next != "" {
123 if next == "" {
124 return
125 }
126 if c.Term.Cols == 0 {
124127 fmt.Fprintf(w, "next\t%s\n", next)
128 return
129 }
130 var again []string
131 for i := 0; i < len(c.Argv); i++ {
132 if c.Argv[i] == "--cursor" {
133 i++
134 continue
135 }
136 again = append(again, c.Argv[i])
125137 }
138 fmt.Fprintf(c.Stderr, "more: gitbay %s %s --cursor %s\n", joinPath(c.Cmd.Path), strings.Join(again, " "), next)
126139 })
127140}
internal/control/cursor_test.go +29 −1
@@ -1,6 +1,13 @@
11package control
22
3import "testing"
3import (
4 "bytes"
5 "strings"
6 "testing"
7
8 "gitbay.org/gitbay/internal/protocol"
9 "gitbay.org/gitbay/internal/store"
10)
411
512func TestCursorRoundTrip(t *testing.T) {
613 cur := encodeCursor("issue", "42")
@@ -37,3 +44,24 @@ func TestTrimPage(t *testing.T) {
3744 t.Fatalf("unpaged: %v next=%q", items, next)
3845 }
3946}
47
48func TestEmitPageHintsTheNextPageAtATerminal(t *testing.T) {
49 st, repo, uid := newQueueTestRepo(t)
50 for i := 0; i < 3; i++ {
51 if _, err := st.CreateBuild(repo.ID, "unit", "aaa", "main", `["true"]`, "", "", true); err != nil {
52 t.Fatal(err)
53 }
54 }
55 c, errOut := pruneCtx(st, t.TempDir(), store.User{ID: uid})
56 c.Term = Term{Cols: 100}
57 if code := Dispatch(c, []string{"build", "list", repo.Path(), "--limit", "2"}); code != protocol.ExitOK {
58 t.Fatalf("exit %d: %s", code, errOut)
59 }
60 if strings.Contains(c.Stdout.(*bytes.Buffer).String(), "next\t") {
61 t.Errorf("cursor row on stdout at a terminal")
62 }
63 want := "more: gitbay build list " + repo.Path() + " --limit 2 --cursor "
64 if !strings.Contains(errOut.String(), want) {
65 t.Errorf("stderr = %q, want %q…", errOut.String(), want)
66 }
67}