@@ -10,10 +10,13 @@ import (
1010 "math/big"
1111 "net/http"
1212 "os"
13+ "os/signal"
1314 "path/filepath"
1415 "regexp"
1516 "strconv"
1617 "strings"
18+ "syscall"
19+ "time"
1720
1821 "github.com/onkernel/cli/pkg/termimg"
1922 "github.com/onkernel/cli/pkg/util"
@@ -173,6 +176,8 @@ type BrowsersDeleteInput struct {
173176
174177type BrowsersViewInput struct {
175178 Identifier string
179+ Live bool
180+ Interval time.Duration
176181}
177182
178183type BrowsersGetInput struct {
@@ -457,6 +462,11 @@ func (b BrowsersCmd) Delete(ctx context.Context, in BrowsersDeleteInput) error {
457462}
458463
459464func (b BrowsersCmd ) View (ctx context.Context , in BrowsersViewInput ) error {
465+ // If live view requested, run the live view loop
466+ if in .Live {
467+ return b .LiveView (ctx , in )
468+ }
469+
460470 browser , err := b .browsers .Get (ctx , in .Identifier )
461471 if err != nil {
462472 return util.CleanedUpSdkError {Err : err }
@@ -474,6 +484,102 @@ func (b BrowsersCmd) View(ctx context.Context, in BrowsersViewInput) error {
474484 return nil
475485}
476486
487+ // LiveView displays a continuously updating view of the browser in the terminal.
488+ func (b BrowsersCmd ) LiveView (ctx context.Context , in BrowsersViewInput ) error {
489+ if b .computer == nil {
490+ pterm .Error .Println ("computer service not available" )
491+ return nil
492+ }
493+
494+ // Check terminal supports inline images
495+ if ! termimg .IsSupported () {
496+ pterm .Error .Printf ("Terminal does not support inline images (detected: %s). Try using iTerm2, Kitty, or Ghostty.\n " , termimg .DetectTerminal ())
497+ return nil
498+ }
499+
500+ // Verify browser exists and get session ID
501+ browser , err := b .browsers .Get (ctx , in .Identifier )
502+ if err != nil {
503+ return util.CleanedUpSdkError {Err : err }
504+ }
505+
506+ // Set default interval if not specified
507+ interval := in .Interval
508+ if interval == 0 {
509+ interval = 100 * time .Millisecond
510+ }
511+
512+ // Set up signal handling for graceful exit
513+ sigChan := make (chan os.Signal , 1 )
514+ signal .Notify (sigChan , syscall .SIGINT , syscall .SIGTERM )
515+ defer signal .Stop (sigChan )
516+
517+ // Create a cancellable context
518+ ctx , cancel := context .WithCancel (ctx )
519+ defer cancel ()
520+
521+ // Handle signals in a goroutine
522+ go func () {
523+ <- sigChan
524+ cancel ()
525+ }()
526+
527+ // Hide cursor and set up cleanup
528+ termimg .HideCursor (os .Stdout )
529+ defer termimg .CleanupLiveView (os .Stdout , false )
530+
531+ // Clear screen initially
532+ termimg .ClearScreen (os .Stdout )
533+
534+ pterm .Info .Println ("Live view started. Press Ctrl+C to exit." )
535+ fmt .Println () // Add space before image
536+
537+ ticker := time .NewTicker (interval )
538+ defer ticker .Stop ()
539+
540+ // Capture and display first frame immediately
541+ if err := b .captureAndDisplayFrame (ctx , browser .SessionID ); err != nil {
542+ // If first frame fails, show error and exit
543+ pterm .Error .Printf ("Failed to capture screenshot: %v\n " , err )
544+ return nil
545+ }
546+
547+ // Main loop
548+ for {
549+ select {
550+ case <- ctx .Done ():
551+ return nil
552+ case <- ticker .C :
553+ if err := b .captureAndDisplayFrame (ctx , browser .SessionID ); err != nil {
554+ // Log error but continue trying
555+ // The browser session may have ended
556+ if ctx .Err () != nil {
557+ return nil
558+ }
559+ // Browser session likely ended
560+ pterm .Warning .Println ("\n Browser session ended or screenshot failed" )
561+ return nil
562+ }
563+ }
564+ }
565+ }
566+
567+ // captureAndDisplayFrame captures a screenshot and displays it in the terminal.
568+ func (b BrowsersCmd ) captureAndDisplayFrame (ctx context.Context , sessionID string ) error {
569+ res , err := b .computer .CaptureScreenshot (ctx , sessionID , kernel.BrowserComputerCaptureScreenshotParams {})
570+ if err != nil {
571+ return err
572+ }
573+ defer res .Body .Close ()
574+
575+ imgData , err := io .ReadAll (res .Body )
576+ if err != nil {
577+ return err
578+ }
579+
580+ return termimg .ClearAndDisplayImage (os .Stdout , imgData )
581+ }
582+
477583func (b BrowsersCmd ) Get (ctx context.Context , in BrowsersGetInput ) error {
478584 if in .Output != "" && in .Output != "json" {
479585 pterm .Error .Println ("unsupported --output value: use 'json'" )
@@ -1756,7 +1862,7 @@ var browsersDeleteCmd = &cobra.Command{
17561862
17571863var browsersViewCmd = & cobra.Command {
17581864 Use : "view <id>" ,
1759- Short : "Get the live view URL for a browser" ,
1865+ Short : "Get the live view URL for a browser, or show a live terminal view " ,
17601866 Args : cobra .ExactArgs (1 ),
17611867 RunE : runBrowsersView ,
17621868}
@@ -1779,6 +1885,10 @@ func init() {
17791885 // get flags
17801886 browsersGetCmd .Flags ().StringP ("output" , "o" , "" , "Output format: json for raw API response" )
17811887
1888+ // view flags
1889+ browsersViewCmd .Flags ().Bool ("live" , false , "Show live terminal view of browser (requires iTerm2, Kitty, or Ghostty)" )
1890+ browsersViewCmd .Flags ().Duration ("interval" , 100 * time .Millisecond , "Refresh interval for live view" )
1891+
17821892 browsersCmd .AddCommand (browsersListCmd )
17831893 browsersCmd .AddCommand (browsersCreateCmd )
17841894 browsersCmd .AddCommand (browsersDeleteCmd )
@@ -2161,10 +2271,16 @@ func runBrowsersView(cmd *cobra.Command, args []string) error {
21612271 client := getKernelClient (cmd )
21622272
21632273 identifier := args [0 ]
2274+ live , _ := cmd .Flags ().GetBool ("live" )
2275+ interval , _ := cmd .Flags ().GetDuration ("interval" )
21642276
2165- in := BrowsersViewInput {Identifier : identifier }
2277+ in := BrowsersViewInput {
2278+ Identifier : identifier ,
2279+ Live : live ,
2280+ Interval : interval ,
2281+ }
21662282 svc := client .Browsers
2167- b := BrowsersCmd {browsers : & svc }
2283+ b := BrowsersCmd {browsers : & svc , computer : & svc . Computer }
21682284 return b .View (cmd .Context (), in )
21692285}
21702286
0 commit comments