package bonfire import ( "context" "errors" tea "charm.land/bubbletea/v2" ) // consoleState is the optional side of an Output that knows the running // command's global options. The bonfire console implements it; other // Output implementations give the zero values. type consoleState interface { consoleGlobals() Globals consoleInteractive() bool } func (c *console) consoleGlobals() Globals { return c.globals } func (c *console) consoleInteractive() bool { return c.canPrompt() } // GlobalsOf returns the global options of the command out belongs to, or // the zero Globals when out is not a bonfire console. func GlobalsOf(out Output) Globals { if s, ok := out.(consoleState); ok { return s.consoleGlobals() } return Globals{} } // VerbosityOf returns the -v level (0 to 3) of the command out belongs to. func VerbosityOf(out Output) int { return GlobalsOf(out).Verbosity } // IsQuiet reports whether the command runs with -q. func IsQuiet(out Output) bool { return GlobalsOf(out).Quiet } // Interactive reports whether out can ask a person: stdin and stdout are // terminals and neither -n nor -q is set. It is false for other Output // implementations and inside bonfire.Call. func Interactive(out Output) bool { if s, ok := out.(consoleState); ok { return s.consoleInteractive() } return false } // ConfirmProceed asks question before a destructive step, defaulting to // no, only when out is Interactive. Off a terminal, with -n or -q and in // bonfire.Call it returns true without asking, so deploys over SSH and // scheduled runs never block (D-28). func ConfirmProceed(out Output, question string) (bool, error) { if !Interactive(out) { return true, nil } return out.Confirm(question, false) } // Current tells p which item is being worked on, when p is an ItemProgress. func Current(p Progress, item string) { if ip, ok := p.(ItemProgress); ok { ip.Current(item) } } // Failed records a failed item on p, when p is an ItemProgress. func Failed(p Progress, item string, err error) { if ip, ok := p.(ItemProgress); ok { ip.Fail(item, err) } } // Process runs fn for each item in turn. On a live console it shows SSU's // process view: one row per item (waiting, running, done or failed with its // error) and a progress bar labelled operation. Otherwise it prints one line // per item, "✓ item" through Output.Success or "✗ item: error" through // Output.Error, so -q prints only the failures. It stops early when ctx is // cancelled and returns errors.Join of the failures. func Process(ctx context.Context, out Output, operation string, items []string, fn func(ctx context.Context, item string) error) error { if ctx == nil { ctx = context.Background() } if c, ok := out.(*console); ok && c.live() && len(items) > 0 { var errs []error _ = runLive(ctx, c.out, c, newProcessModel(c.palette, operation, items), func(send func(tea.Msg)) error { for i, item := range items { if err := ctx.Err(); err != nil { errs = append(errs, err) return nil } send(stepMsg{item: item, state: itemRunning, done: i}) if err := fn(ctx, item); err != nil { errs = append(errs, failure(item, err)) send(stepMsg{item: item, state: itemFailed, err: err, done: i + 1}) continue } send(stepMsg{item: item, state: itemDone, done: i + 1}) } return nil }) return errors.Join(errs...) } var errs []error for _, item := range items { if err := ctx.Err(); err != nil { errs = append(errs, err) break } if err := fn(ctx, item); err != nil { errs = append(errs, failure(item, err)) out.Error(failureLine(item, err)) continue } out.Success(item) } return errors.Join(errs...) } // failure wraps err with the item it belongs to. func failure(item string, err error) error { return &itemError{item: item, err: err} } type itemError struct { item string err error } func (e *itemError) Error() string { return failureLine(e.item, e.err) } func (e *itemError) Unwrap() error { return e.err } // PrintAbove prints line above the running live view (spinner, progress or // process view) and reports whether one was running. diary's console // channel uses it so log lines from other goroutines never tear a view; it // writes directly when PrintAbove returns false. func PrintAbove(line string) bool { return printAboveLines(line) }