Commit 583b480f3a
583b480f3a46a5c596cf64b85210e33b09a9e875
parent: b19c86712e
Verified · cmc
cmc <hello@cleberg.net> · 2026-10-02 03:16 UTC
control: emitView and emitPageView; next page as a legend line
Ref #317
Layout: unified · split
internal/control/control.go
+10
| @@ -360,6 +360,16 @@ type emptyReader struct{} |
| 360 | |
360 | |
| 361 | func (emptyReader) Read([]byte) (int, error) { return 0, io.EOF } |
361 | func (emptyReader) Read([]byte) (int, error) { return 0, io.EOF } |
| 362 | |
362 | |
| |
363 | // emitView is emit for a command with a terminal screen: --json and |
| |
364 | // piped output are emit's, and at a terminal build's screen is drawn |
| |
365 | // in place of plain. |
| |
366 | func (c *Ctx) emitView(data any, plain func(w io.Writer), build func() screen) int { |
| |
367 | if c.Term.Cols == 0 || c.JSON { |
| |
368 | return c.emit(data, plain) |
| |
369 | } |
| |
370 | return c.emit(data, func(w io.Writer) { c.render(w, build()) }) |
| |
371 | } |
| |
372 | |
| 363 | // emit writes data as the command result: a JSON envelope under --json, |
373 | // emit writes data as the command result: a JSON envelope under --json, |
| 364 | // otherwise via the plain formatter. |
374 | // otherwise via the plain formatter. |
| 365 | func (c *Ctx) emit(data any, plain func(w io.Writer)) int { |
375 | func (c *Ctx) emit(data any, plain func(w io.Writer)) int { |
internal/control/cursor.go
+24 −14
| @@ -7,6 +7,7 @@ import ( |
| 7 | "io" |
7 | "io" |
| 8 | "reflect" |
8 | "reflect" |
| 9 | "regexp" |
9 | "regexp" |
| |
10 | "slices" |
| 10 | "strconv" |
11 | "strconv" |
| 11 | "strings" |
12 | "strings" |
| 12 | |
13 | |
| @@ -141,19 +142,28 @@ func (c *Ctx) emitPage(p page, items any, next string, plain func(w io.Writer)) |
| 141 | fmt.Fprintf(w, "next\t%s\n", next) |
142 | fmt.Fprintf(w, "next\t%s\n", next) |
| 142 | return |
143 | return |
| 143 | } |
144 | } |
| 144 | var again []string |
145 | fmt.Fprintln(c.Stderr, c.Term.paint(sgrBold, "Next page")+" "+c.Term.paint(sgrBlue, c.cmdline(c.nextArgv(next)))) |
| 145 | for i := 0; i < len(c.Argv); i++ { |
| |
| 146 | if c.Argv[i] == "--cursor" { |
| |
| 147 | i++ |
| |
| 148 | continue |
| |
| 149 | } |
| |
| 150 | again = append(again, c.Argv[i]) |
| |
| 151 | } |
| |
| 152 | cmd := []string{"gitbay", joinPath(c.Cmd.Path)} |
| |
| 153 | for _, a := range again { |
| |
| 154 | cmd = append(cmd, shellWord(a)) |
| |
| 155 | } |
| |
| 156 | cmd = append(cmd, "--cursor", next) |
| |
| 157 | fmt.Fprintf(c.Stderr, "more: %s\n", strings.Join(cmd, " ")) |
| |
| 158 | }) |
146 | }) |
| 159 | } |
147 | } |
| |
148 | |
| |
149 | // emitPageView is emitPage for a list with a terminal screen. |
| |
150 | func (c *Ctx) emitPageView(p page, items any, next string, plain func(w io.Writer), build func() screen) int { |
| |
151 | if c.Term.Cols == 0 || c.JSON { |
| |
152 | return c.emitPage(p, items, next, plain) |
| |
153 | } |
| |
154 | return c.emitPage(p, items, next, func(w io.Writer) { c.render(w, build()) }) |
| |
155 | } |
| |
156 | |
| |
157 | // nextArgv is the command that fetches the page after this one: this |
| |
158 | // command's argv with its cursor replaced. |
| |
159 | func (c *Ctx) nextArgv(next string) []string { |
| |
160 | argv := slices.Clone(c.Cmd.Path) |
| |
161 | for i := 0; i < len(c.Argv); i++ { |
| |
162 | if c.Argv[i] == "--cursor" { |
| |
163 | i++ |
| |
164 | continue |
| |
165 | } |
| |
166 | argv = append(argv, c.Argv[i]) |
| |
167 | } |
| |
168 | return append(argv, "--cursor", next) |
| |
169 | } |
internal/control/cursor_test.go
+10 −6
| @@ -81,12 +81,16 @@ func TestEmitPageHintsTheNextPageAtATerminal(t *testing.T) { |
| 81 | t.Errorf("cursor row on stdout at a terminal") |
81 | t.Errorf("cursor row on stdout at a terminal") |
| 82 | } |
82 | } |
| 83 | stderr := errOut.String() |
83 | stderr := errOut.String() |
| 84 | want := "more: gitbay build list " + repo.Path() + " --limit 2 --cursor " |
84 | cmd, ok := strings.CutPrefix(stderr, "Next page ") |
| 85 | if !strings.Contains(stderr, want) { |
85 | if !ok { |
| 86 | t.Errorf("stderr = %q, want %q…", stderr, want) |
86 | t.Fatalf("stderr = %q, want a Next page line", stderr) |
| 87 | } |
87 | } |
| 88 | // Ensure no double space in the output. |
88 | want := "gitbay build list " + repo.Path() + " --limit 2 --cursor " |
| 89 | if strings.Contains(stderr, " ") { |
89 | if !strings.HasPrefix(cmd, want) { |
| 90 | t.Errorf("stderr contains double space: %q", stderr) |
90 | t.Errorf("command = %q, want %q…", cmd, want) |
| |
91 | } |
| |
92 | // No empty word in the command. |
| |
93 | if strings.Contains(cmd, " ") { |
| |
94 | t.Errorf("command contains double space: %q", cmd) |
| 91 | } |
95 | } |
| 92 | } |
96 | } |
internal/control/screen_test.go
+29
| @@ -1,7 +1,9 @@ |
| 1 | package control |
1 | package control |
| 2 | |
2 | |
| 3 | import ( |
3 | import ( |
| |
4 | "bytes" |
| 4 | "fmt" |
5 | "fmt" |
| |
6 | "io" |
| 5 | "strings" |
7 | "strings" |
| 6 | "testing" |
8 | "testing" |
| 7 | ) |
9 | ) |
| @@ -177,3 +179,30 @@ func TestLegendColour(t *testing.T) { |
| 177 | t.Error("colour legend differs beyond SGR") |
179 | t.Error("colour legend differs beyond SGR") |
| 178 | } |
180 | } |
| 179 | } |
181 | } |
| |
182 | |
| |
183 | func TestEmitViewRoutes(t *testing.T) { |
| |
184 | built := false |
| |
185 | build := func() screen { built = true; return screen{fields: []field{{"Repo", []cell{cText("a/b")}}}} } |
| |
186 | plain := func(w io.Writer) { io.WriteString(w, "plain\n") } |
| |
187 | |
| |
188 | var out bytes.Buffer |
| |
189 | c := &Ctx{Stdout: &out, Stderr: io.Discard} |
| |
190 | c.emitView(map[string]string{"k": "v"}, plain, build) |
| |
191 | if out.String() != "plain\n" || built { |
| |
192 | t.Errorf("piped: %q built=%v", out.String(), built) |
| |
193 | } |
| |
194 | |
| |
195 | out.Reset() |
| |
196 | c = &Ctx{Stdout: &out, Stderr: io.Discard, JSON: true, Term: Term{Cols: 80}} |
| |
197 | c.emitView(map[string]string{"k": "v"}, plain, build) |
| |
198 | if !strings.Contains(out.String(), `"k":"v"`) || built { |
| |
199 | t.Errorf("json: %q built=%v", out.String(), built) |
| |
200 | } |
| |
201 | |
| |
202 | out.Reset() |
| |
203 | c = &Ctx{Stdout: &out, Stderr: io.Discard, Term: Term{Cols: 80}} |
| |
204 | c.emitView(map[string]string{"k": "v"}, plain, build) |
| |
205 | if out.String() != "Repo: a/b\n" || !built { |
| |
206 | t.Errorf("terminal: %q built=%v", out.String(), built) |
| |
207 | } |
| |
208 | } |