11package cmd
22
33import (
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
2628var OutputFormats = []string {"auto" , "explore" , "json" , "jsonl" , "pretty" , "raw" , "yaml" }
@@ -71,7 +73,7 @@ func isInputPiped() bool {
7173func 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.
223244func 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