From 2723e193d23182f3f7a1dd490ef2c15da215cb20 Mon Sep 17 00:00:00 2001 From: Federico <16429579+rawnly@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:15:02 +0200 Subject: [PATCH] feat: report blocked prompts via Program Status Protocol Also guard against empty photo results and drop a duplicate config write. --- cmd/auth/login.go | 4 ++-- cmd/root.go | 18 ++++++++++---- lib/terminal/status.go | 48 +++++++++++++++++++++++++++++++++++++ lib/terminal/status_test.go | 23 ++++++++++++++++++ 4 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 lib/terminal/status.go create mode 100644 lib/terminal/status_test.go diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 2422fbac..0c766518 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -33,6 +33,7 @@ var loginCmd = &cobra.Command{ ctx := cmd.Context() api := keys.GetAPIInstance(ctx) + resume := terminal.Blocked("auth", "Log in to Unsplash in your browser") fmt.Println("Please visit the following URL to login:") authenticationURL := api.BuildAuthenticationUrl( "read_user", @@ -53,6 +54,7 @@ var loginCmd = &cobra.Command{ sp.Suffix = " Authenticating..." code := <-codeChan + resume() res, err := api.Authenticate(code) if err != nil { sp.FinalMSG = "An error occured while authenticating\n" @@ -68,8 +70,6 @@ var loginCmd = &cobra.Command{ tokens.SetRefreshToken(res.RefreshToken) logrus.Debug("writing config") - cobra.CheckErr(viper.WriteConfig()) - if err := viper.WriteConfig(); err != nil { logrus.WithField("error", err).Error("Error while saving config") diff --git a/cmd/root.go b/cmd/root.go index 0f405a65..bb20e909 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "errors" "fmt" "os" "time" @@ -119,7 +120,10 @@ var rootCmd = &cobra.Command{ } confirm := true - if err := survey.AskOne(prompt, &confirm); err != nil { + resume := terminal.Blocked("permission", prompt.Message) + err := survey.AskOne(prompt, &confirm) + resume() + if err != nil { return } @@ -201,9 +205,6 @@ var rootCmd = &cobra.Command{ cobra.CheckErr(err) } - if err = viper.WriteConfig(); err != nil { - handleSpinnerError(err, connectionSpinner, cmd, ConnectionSpinnerSuffix[2]) - } } handleSpinnerError(err, connectionSpinner, cmd, ConnectionSpinnerSuffix[1]) @@ -236,6 +237,11 @@ var rootCmd = &cobra.Command{ } } + if photo == nil { + connectionSpinner.Stop() + cobra.CheckErr(errors.New("no photos found")) + } + connectionSpinner.FinalMSG = ConnectionSpinnerSuffix[2] connectionSpinner.Stop() @@ -290,7 +296,9 @@ var rootCmd = &cobra.Command{ fmt.Println("") var key keyboard.Key + resume := terminal.Blocked("permission", "Download failed. Set wallpaper from the URL?") _, key, err = keyboard.GetSingleKey() + resume() if err != nil { eventID := sentry.CaptureException(err) log.Fatal().Any("event_id", eventID).Err(err).Send() @@ -372,10 +380,12 @@ var rootCmd = &cobra.Command{ shouldLike := false + resume := terminal.Blocked("question", "Do you like this photo?") err := survey.AskOne(&survey.Confirm{ Default: false, Message: "Do you like this photo?", }, &shouldLike) + resume() cobra.CheckErr(err) if shouldLike { diff --git a/lib/terminal/status.go b/lib/terminal/status.go new file mode 100644 index 00000000..e4b127bb --- /dev/null +++ b/lib/terminal/status.go @@ -0,0 +1,48 @@ +package terminal + +import ( + "encoding/base64" + "fmt" + "os" +) + +// Blocked reports (OSC 7501, Program Status Protocol) that the program is +// waiting for user input. The returned func reports it is working again. +// +// Reports are best-effort and only sent to interactive terminals; terminals +// that don't support the protocol ignore the sequence. +// +// defer terminal.Blocked("question", "Do you like this photo?")() +func Blocked(kind, message string) (resume func()) { + report("blocked", kind, message) + return func() { report("working", "", "") } +} + +func report(state, kind, message string) { + if !isInteractive() { + return + } + + body := "state=" + state + ":app=splash-cli" + if kind != "" { + body += ":kind=" + kind + } + if message != "" { + body += ":msg=" + base64.StdEncoding.EncodeToString([]byte(truncate(message))) + } + + fmt.Fprint(os.Stdout, "\x1b]7501;"+body+"\x1b\\") +} + +func isInteractive() bool { + info, err := os.Stdout.Stat() + return err == nil && info.Mode()&os.ModeCharDevice != 0 && os.Getenv("TERM") != "dumb" +} + +// messages are limited to 2048 bytes +func truncate(s string) string { + if len(s) <= 2048 { + return s + } + return s[:2048] +} diff --git a/lib/terminal/status_test.go b/lib/terminal/status_test.go new file mode 100644 index 00000000..1256e5a4 --- /dev/null +++ b/lib/terminal/status_test.go @@ -0,0 +1,23 @@ +package terminal + +import ( + "encoding/base64" + "strings" + "testing" +) + +func TestTruncate(t *testing.T) { + if got := truncate(strings.Repeat("a", 3000)); len(got) != 2048 { + t.Fatalf("len = %d, want 2048", len(got)) + } + if got := truncate("hi"); got != "hi" { + t.Fatalf("got %q", got) + } +} + +func TestMessageEncoding(t *testing.T) { + enc := base64.StdEncoding.EncodeToString([]byte("Do you like this photo?")) + if enc != "RG8geW91IGxpa2UgdGhpcyBwaG90bz8=" { + t.Fatalf("unexpected encoding %s", enc) + } +}