Skip to content

Commit a7eb280

Browse files
fix(client): do not use pager for short paginated responses
1 parent 96361e1 commit a7eb280

3 files changed

Lines changed: 95 additions & 30 deletions

File tree

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ require (
1717
github.com/urfave/cli-docs/v3 v3.0.0-alpha6
1818
github.com/urfave/cli/v3 v3.3.2
1919
golang.org/x/sys v0.38.0
20-
golang.org/x/term v0.37.0
2120
)
2221

2322
require (

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,6 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc
8181
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
8282
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
8383
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
84-
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
85-
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
8684
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
8785
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
8886
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

pkg/cmd/cmdutil.go

Lines changed: 95 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"fmt"
67
"io"
@@ -16,11 +17,12 @@ import (
1617
"github.com/browserbase/stagehand-cli/internal/jsonview"
1718
"github.com/browserbase/stagehand-go/option"
1819

20+
"github.com/charmbracelet/x/term"
1921
"github.com/itchyny/json2yaml"
22+
"github.com/muesli/reflow/wrap"
2023
"github.com/tidwall/gjson"
2124
"github.com/tidwall/pretty"
2225
"github.com/urfave/cli/v3"
23-
"golang.org/x/term"
2426
)
2527

2628
var OutputFormats = []string{"auto", "explore", "json", "jsonl", "pretty", "raw", "yaml"}
@@ -71,7 +73,7 @@ func isInputPiped() bool {
7173
func isTerminal(w io.Writer) bool {
7274
switch v := w.(type) {
7375
case *os.File:
74-
return term.IsTerminal(int(v.Fd()))
76+
return term.IsTerminal(v.Fd())
7577
default:
7678
return false
7779
}
@@ -115,7 +117,7 @@ func streamToPagerWithPipe(label string, generateOutput func(w *os.File) error)
115117
cmd.Stdout = os.Stdout
116118
cmd.Stderr = os.Stderr
117119
cmd.Env = append(os.Environ(),
118-
"LESS=-r -P "+label,
120+
"LESS=-X -r -P "+label,
119121
"MORE=-r -P "+label,
120122
)
121123

@@ -164,8 +166,7 @@ func shouldUseColors(w io.Writer) bool {
164166
return isTerminal(w)
165167
}
166168

167-
// Display JSON to the user in various different formats
168-
func ShowJSON(out *os.File, title string, res gjson.Result, format string, transform string) error {
169+
func formatJSON(expectedOutput *os.File, title string, res gjson.Result, format string, transform string) ([]byte, error) {
169170
if format != "raw" && transform != "" {
170171
transformed := res.Get(transform)
171172
if transformed.Exists() {
@@ -174,57 +175,124 @@ func ShowJSON(out *os.File, title string, res gjson.Result, format string, trans
174175
}
175176
switch strings.ToLower(format) {
176177
case "auto":
177-
return ShowJSON(out, title, res, "json", "")
178-
case "explore":
179-
return jsonview.ExploreJSON(title, res)
178+
return formatJSON(expectedOutput, title, res, "json", "")
180179
case "pretty":
181-
_, err := out.WriteString(jsonview.RenderJSON(title, res) + "\n")
182-
return err
180+
return []byte(jsonview.RenderJSON(title, res) + "\n"), nil
183181
case "json":
184182
prettyJSON := pretty.Pretty([]byte(res.Raw))
185-
if shouldUseColors(out) {
186-
_, err := out.Write(pretty.Color(prettyJSON, pretty.TerminalStyle))
187-
return err
183+
if shouldUseColors(expectedOutput) {
184+
return pretty.Color(prettyJSON, pretty.TerminalStyle), nil
188185
} else {
189-
_, err := out.Write(prettyJSON)
190-
return err
186+
return prettyJSON, nil
191187
}
192188
case "jsonl":
193189
// @ugly is gjson syntax for "no whitespace", so it fits on one line
194190
oneLineJSON := res.Get("@ugly").Raw
195-
if shouldUseColors(out) {
191+
if shouldUseColors(expectedOutput) {
196192
bytes := append(pretty.Color([]byte(oneLineJSON), pretty.TerminalStyle), '\n')
197-
_, err := out.Write(bytes)
198-
return err
193+
return bytes, nil
199194
} else {
200-
_, err := out.Write([]byte(oneLineJSON + "\n"))
201-
return err
195+
return []byte(oneLineJSON + "\n"), nil
202196
}
203197
case "raw":
204-
if _, err := out.Write([]byte(res.Raw + "\n")); err != nil {
205-
return err
206-
}
207-
return nil
198+
return []byte(res.Raw + "\n"), nil
208199
case "yaml":
209200
input := strings.NewReader(res.Raw)
210201
var yaml strings.Builder
211202
if err := json2yaml.Convert(&yaml, input); err != nil {
203+
return nil, err
204+
}
205+
_, err := expectedOutput.Write([]byte(yaml.String()))
206+
return nil, err
207+
default:
208+
return nil, fmt.Errorf("Invalid format: %s, valid formats are: %s", format, strings.Join(OutputFormats, ", "))
209+
}
210+
}
211+
212+
// Display JSON to the user in various different formats
213+
func ShowJSON(out *os.File, title string, res gjson.Result, format string, transform string) error {
214+
if format != "raw" && transform != "" {
215+
transformed := res.Get(transform)
216+
if transformed.Exists() {
217+
res = transformed
218+
}
219+
}
220+
221+
switch strings.ToLower(format) {
222+
case "auto":
223+
return ShowJSON(out, title, res, "json", "")
224+
case "explore":
225+
return jsonview.ExploreJSON(title, res)
226+
default:
227+
bytes, err := formatJSON(out, title, res, format, transform)
228+
if err != nil {
212229
return err
213230
}
214-
_, err := out.Write([]byte(yaml.String()))
231+
232+
_, err = out.Write(bytes)
215233
return err
216-
default:
217-
return fmt.Errorf("Invalid format: %s, valid formats are: %s", format, strings.Join(OutputFormats, ", "))
218234
}
219235
}
220236

237+
// Get the number of lines that would be output by writing the data to the terminal
238+
func countTerminalLines(data []byte, terminalWidth int) int {
239+
return bytes.Count([]byte(wrap.String(string(data), terminalWidth)), []byte("\n"))
240+
}
241+
221242
// For an iterator over different value types, display its values to the user in
222243
// different formats.
223244
func ShowJSONIterator[T any](stdout *os.File, title string, iter jsonview.Iterator[T], format string, transform string) error {
224245
if format == "explore" {
225246
return jsonview.ExploreJSONStream(title, iter)
226247
}
248+
249+
terminalWidth, terminalHeight, err := term.GetSize(os.Stdout.Fd())
250+
if err != nil {
251+
return err
252+
}
253+
254+
// Decide whether or not to use a pager based on whether it's a short output or a long output
255+
usePager := false
256+
output := []byte{}
257+
numberOfNewlines := 0
258+
for iter.Next() {
259+
item := iter.Current()
260+
jsonData, err := json.Marshal(item)
261+
if err != nil {
262+
return err
263+
}
264+
obj := gjson.ParseBytes(jsonData)
265+
json, err := formatJSON(stdout, title, obj, format, transform)
266+
if err != nil {
267+
return err
268+
}
269+
270+
output = append(output, json...)
271+
numberOfNewlines += countTerminalLines(json, terminalWidth)
272+
273+
// If the output won't fit in the terminal window, stream it to a pager
274+
if numberOfNewlines >= terminalHeight-3 {
275+
usePager = true
276+
break
277+
}
278+
}
279+
280+
if !usePager {
281+
_, err := stdout.Write(output)
282+
if err != nil {
283+
return err
284+
}
285+
286+
return iter.Err()
287+
}
288+
227289
return streamOutput(title, func(pager *os.File) error {
290+
// Write the output we used during the initial terminal size computation
291+
_, err := pager.Write(output)
292+
if err != nil {
293+
return err
294+
}
295+
228296
for iter.Next() {
229297
item := iter.Current()
230298
jsonData, err := json.Marshal(item)

0 commit comments

Comments
 (0)