Skip to content

Commit 2ef239b

Browse files
committed
pretty needs tty & add pretty for process
1 parent 79afb0b commit 2ef239b

3 files changed

Lines changed: 185 additions & 17 deletions

File tree

cmd/ask.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func runAsk(cmd *cobra.Command, args []string) error {
5353

5454
var stopSpinner chan struct{}
5555
var spinnerDone chan struct{}
56-
if prettyOutput {
56+
if prettyOutput && isStderrTTY() {
5757
stopSpinner = make(chan struct{})
5858
spinnerDone = make(chan struct{})
5959
go func() {
@@ -83,7 +83,7 @@ func runAsk(cmd *cobra.Command, args []string) error {
8383

8484
result, err := ask.Ask(context.Background(), client, question)
8585

86-
if prettyOutput {
86+
if stopSpinner != nil {
8787
close(stopSpinner)
8888
<-spinnerDone
8989
}

cmd/output.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ func loadGlamourStyle() string {
2222
}
2323

2424
// printMarkdown writes text to cmd's output stream, rendering it through
25-
// glamour when the global --pretty flag is set.
25+
// glamour when the global --pretty flag is set and stdout is a TTY.
26+
// When stdout is not a TTY the rendered ANSI codes would be meaningless
27+
// (e.g. piped to a file or another process), so plain text is used instead.
2628
func printMarkdown(cmd *cobra.Command, text string) {
2729
// --pretty-no-pager takes priority over --pretty
28-
if prettyNoPager {
30+
if prettyNoPager && isTTY() {
2931
text = render.Markdown(text, loadGlamourStyle())
3032
fmt.Fprint(cmd.OutOrStdout(), text)
31-
} else if prettyOutput {
33+
} else if prettyOutput && isTTY() {
3234
text = render.Markdown(text, loadGlamourStyle())
3335
printWithPager(cmd, text)
3436
} else {
@@ -41,10 +43,10 @@ func printMarkdown(cmd *cobra.Command, text string) {
4143
// so glamour does not collapse them into a single line per item.
4244
func printMarkdownAnswer(cmd *cobra.Command, text string) {
4345
// --pretty-no-pager takes priority over --pretty
44-
if prettyNoPager {
46+
if prettyNoPager && isTTY() {
4547
text = render.MarkdownAnswer(text, loadGlamourStyle())
4648
fmt.Fprint(cmd.OutOrStdout(), text)
47-
} else if prettyOutput {
49+
} else if prettyOutput && isTTY() {
4850
text = render.MarkdownAnswer(text, loadGlamourStyle())
4951
printWithPager(cmd, text)
5052
} else {
@@ -90,3 +92,8 @@ func printWithPager(cmd *cobra.Command, text string) {
9092
func isTTY() bool {
9193
return term.IsTerminal(1) // fd 1 = stdout
9294
}
95+
96+
// isStderrTTY returns true if stderr is a terminal.
97+
func isStderrTTY() bool {
98+
return term.IsTerminal(2) // fd 2 = stderr
99+
}

cmd/process.go

Lines changed: 171 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"strings"
8+
"sync"
79
"time"
810

911
"github.com/hardhacker/podwise-cli/internal/api"
@@ -119,8 +121,13 @@ func runProcess(cmd *cobra.Command, args []string) error {
119121
if result.Progress != nil {
120122
initialProgress = *result.Progress
121123
}
122-
printProcessStatus(result, initialProgress)
123124

125+
if (prettyOutput || prettyNoPager) && isTTY() {
126+
return runProcessPretty(ctx, client, seq, result.Status, initialProgress, startTime)
127+
}
128+
129+
// Plain (non-pretty) path.
130+
printProcessStatus(result, initialProgress)
124131
if result.Status == "done" {
125132
printProcessDoneHint(seq, time.Since(startTime))
126133
return nil
@@ -151,7 +158,95 @@ func runProcess(cmd *cobra.Command, args []string) error {
151158
case "done":
152159
printProcessDoneHint(seq, time.Since(startTime))
153160
return nil
154-
case "failed":
161+
case "failed", "not_requested":
162+
return fmt.Errorf("processing failed for episode %s", episode.BuildEpisodeURL(seq))
163+
}
164+
}
165+
return nil
166+
}
167+
168+
// runProcessPretty runs the polling loop with an animated spinner.
169+
// A goroutine redraws the status line at 100 ms so the spinner animates
170+
// continuously, while API polls happen on the normal (≥30 s) interval.
171+
func runProcessPretty(ctx context.Context, client *api.Client, seq int, initialStatus string, initialProgress float64, startTime time.Time) error {
172+
// Shared state between the spinner goroutine and the poll loop.
173+
var mu sync.Mutex
174+
curStatus := initialStatus
175+
curProgress := initialProgress
176+
spinIdx := 0
177+
178+
// Initial render.
179+
prettyPrintProcessStatus(&episode.ProcessResult{Status: curStatus}, curProgress, spinIdx)
180+
181+
if curStatus == "done" {
182+
prettyPrintProcessDoneHint(seq, time.Since(startTime))
183+
return nil
184+
}
185+
186+
stopCh := make(chan struct{})
187+
var wg sync.WaitGroup
188+
wg.Add(1)
189+
190+
// Spinner goroutine: redraws the current line at ~100 ms.
191+
go func() {
192+
defer wg.Done()
193+
t := time.NewTicker(100 * time.Millisecond)
194+
defer t.Stop()
195+
for {
196+
select {
197+
case <-stopCh:
198+
return
199+
case <-t.C:
200+
mu.Lock()
201+
spinIdx++
202+
prettyPrintProcessStatus(&episode.ProcessResult{Status: curStatus}, curProgress, spinIdx)
203+
mu.Unlock()
204+
}
205+
}
206+
}()
207+
208+
stopSpinner := func() {
209+
select {
210+
case <-stopCh:
211+
default:
212+
close(stopCh)
213+
}
214+
wg.Wait()
215+
}
216+
217+
deadline := time.Now().Add(processTimeout)
218+
pollTicker := time.NewTicker(processPollInterval)
219+
defer pollTicker.Stop()
220+
221+
for range pollTicker.C {
222+
if time.Now().After(deadline) {
223+
stopSpinner()
224+
fmt.Println()
225+
return fmt.Errorf("timed out after %s waiting for episode %s to finish processing", processTimeout, episode.BuildEpisodeURL(seq))
226+
}
227+
status, err := episode.FetchStatus(ctx, client, seq)
228+
if err != nil {
229+
stopSpinner()
230+
fmt.Println()
231+
return err
232+
}
233+
234+
mu.Lock()
235+
if status.Progress != nil && *status.Progress > curProgress {
236+
curProgress = *status.Progress
237+
}
238+
curStatus = status.Status
239+
mu.Unlock()
240+
241+
switch status.Status {
242+
case "done":
243+
stopSpinner()
244+
prettyPrintProcessStatus(status, curProgress, 0)
245+
prettyPrintProcessDoneHint(seq, time.Since(startTime))
246+
return nil
247+
case "failed", "not_requested":
248+
stopSpinner()
249+
prettyPrintProcessStatus(status, curProgress, 0)
155250
return fmt.Errorf("processing failed for episode %s", episode.BuildEpisodeURL(seq))
156251
}
157252
}
@@ -166,18 +261,13 @@ func printProcessStatus(r *episode.ProcessResult, maxProgress float64) {
166261
switch r.Status {
167262
case "waiting":
168263
fmt.Printf(" [%s] → waiting episode is queued for processing\n", ts)
169-
case "processing":
170-
if maxProgress >= 0.0 {
171-
fmt.Printf(" [%s] → processing %.0f%% complete\n", ts, maxProgress)
172-
}
173264
case "done":
174265
fmt.Printf(" [%s] ✓ done processing complete (100%%)\n", ts)
175-
case "not_requested":
176-
fmt.Printf(" [%s] → not_requested transcription has not been requested yet\n", ts)
177-
case "failed":
266+
case "failed", "not_requested":
178267
fmt.Printf(" [%s] ✗ failed transcription failed\n", ts)
179268
default:
180-
fmt.Printf(" [%s] ? %s\n", ts, r.Status)
269+
// "processing" and any other transitional status are all treated as in-progress.
270+
fmt.Printf(" [%s] → processing %.0f%% complete\n", ts, maxProgress)
181271
}
182272
}
183273

@@ -195,3 +285,74 @@ func printProcessDoneHint(seq int, elapsed time.Duration) {
195285
fmt.Printf(" podwise get summary %s\n", episodeURL)
196286
fmt.Printf(" podwise get --help to see all available commands\n")
197287
}
288+
289+
// prettySpinnerFrames are the braille spinner frames used in pretty mode.
290+
var prettySpinnerFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
291+
292+
// prettyPrintProcessStatus overwrites the current terminal line with a styled
293+
// status. spinnerIdx is incremented by the caller on each poll to advance the
294+
// spinner. done/failed states end the line with \n so subsequent output is clean.
295+
func prettyPrintProcessStatus(r *episode.ProcessResult, maxProgress float64, spinnerIdx int) {
296+
const (
297+
reset = "\033[0m"
298+
bold = "\033[1m"
299+
dim = "\033[2m"
300+
green = "\033[32m"
301+
yellow = "\033[33m"
302+
blue = "\033[34m"
303+
red = "\033[31m"
304+
cyan = "\033[36m"
305+
barW = 24
306+
)
307+
spinner := prettySpinnerFrames[spinnerIdx%len(prettySpinnerFrames)]
308+
309+
buildBar := func(pct float64) string {
310+
n := int(pct / 100.0 * barW)
311+
if n > barW {
312+
n = barW
313+
}
314+
return strings.Repeat("█", n) + strings.Repeat("░", barW-n)
315+
}
316+
317+
var line string
318+
switch r.Status {
319+
case "waiting":
320+
line = fmt.Sprintf("\r\033[K %s%s%s %swaiting%s queued for processing",
321+
yellow, spinner, reset, yellow, reset)
322+
case "done":
323+
bar := buildBar(100)
324+
line = fmt.Sprintf("\r\033[K %s✓%s %sdone%s %s[%s]%s %s100%%%s\n",
325+
green, reset, green+bold, reset, dim, bar, reset, bold, reset)
326+
case "failed", "not_requested":
327+
line = fmt.Sprintf("\r\033[K %s✗%s %sfailed%s transcription failed\n",
328+
red, reset, red+bold, reset)
329+
default:
330+
// "processing" and any other transitional status are all treated as in-progress.
331+
bar := buildBar(maxProgress)
332+
line = fmt.Sprintf("\r\033[K %s%s%s %sprocessing%s %s[%s]%s %s%.0f%%%s",
333+
blue, spinner, reset, blue, reset, dim, bar, reset, bold, maxProgress, reset)
334+
}
335+
fmt.Print(line)
336+
}
337+
338+
func prettyPrintProcessDoneHint(seq int, elapsed time.Duration) {
339+
const (
340+
reset = "\033[0m"
341+
bold = "\033[1m"
342+
dim = "\033[2m"
343+
green = "\033[32m"
344+
cyan = "\033[36m"
345+
)
346+
episodeURL := episode.BuildEpisodeURL(seq)
347+
sep := "─────────────────────────────────────────────────────────"
348+
fmt.Printf("\n%s%s%s\n", dim, sep, reset)
349+
fmt.Printf(" %s✓ Processing Complete%s\n", green+bold, reset)
350+
fmt.Printf("%s%s%s\n", dim, sep, reset)
351+
fmt.Printf(" %sEpisode URL:%s %s%s%s\n", dim, reset, cyan, episodeURL, reset)
352+
fmt.Printf(" %sDuration :%s %s\n", dim, reset, utils.FormatDuration(elapsed))
353+
fmt.Printf("\n")
354+
fmt.Printf(" %sNext steps:%s\n", bold, reset)
355+
fmt.Printf(" %spodwise get transcript%s %s\n", cyan, reset, episodeURL)
356+
fmt.Printf(" %spodwise get summary%s %s\n", cyan, reset, episodeURL)
357+
fmt.Printf(" %spodwise get --help%s to see all available commands\n", cyan, reset)
358+
}

0 commit comments

Comments
 (0)