1package modes
2
3import (
4 "context"
5 "fmt"
6 "strings"
7 "time"
8
9 "github.com/bnema/zut/packages/agent/extensions"
10 "github.com/bnema/zut/packages/tui"
11)
12
13const reloadStatusDuration = 5 * time.Second
14
15func formatReloadStatus(stats extensions.ReloadStats) (string, bool) {
16 msg := fmt.Sprintf("reloaded: %d stopped, %d loaded (%d ready)", stats.Stopped, stats.Loaded, stats.Ready)
17 if len(stats.Errors) == 0 {
18 return msg, false
19 }
20 details := make([]string, 0, len(stats.Errors))
21 for _, err := range stats.Errors {
22 if err != nil {
23 details = append(details, err.Error())
24 }
25 }
26 msg += fmt.Sprintf(", %d error(s)", len(stats.Errors))
27 if len(details) > 0 {
28 msg += ": " + strings.Join(details, "; ")
29 }
30 return msg, true
31}
32
33func (i *Interactive) setReloadStatus(msg string, failed bool) uint64 {
34 i.mu.Lock()
35 defer i.mu.Unlock()
36 i.reloadStatusSeq++
37 if failed {
38 i.statusOK = ""
39 i.statusErr = msg
40 i.reloadErrors = append(i.reloadErrors, msg)
41 return i.reloadStatusSeq
42 }
43 i.statusOK = msg
44 i.statusErr = ""
45 i.reloadErrors = nil
46 return i.reloadStatusSeq
47}
48
49func renderReloadErrors(theme tui.Theme, errors []string, width int) []string {
50 const marker = "✖ "
51 const indent = " "
52 wrapWidth := width - len([]rune(marker))
53 if wrapWidth < 8 {
54 wrapWidth = 8
55 }
56 var out []string
57 for _, msg := range errors {
58 // Split explicit newlines before wrapping. Passing a multiline string
59 // to WrapANSILine would leave the terminal to interpret the embedded
60 // newline, which can misalign the continuation text. Normalize CRLF
61 // first so no raw CR survives inside a row.
62 for lineIndex, paragraph := range strings.Split(strings.ReplaceAll(msg, "\r\n", "\n"), "\n") {
63 wrapped := tui.WrapANSILine(paragraph, wrapWidth)
64 if len(wrapped) == 0 {
65 wrapped = []string{""}
66 }
67 for wrapIndex, line := range wrapped {
68 prefix := indent
69 if lineIndex == 0 && wrapIndex == 0 {
70 prefix = marker
71 }
72 out = append(out, theme.FGColor(theme.Error, prefix+line))
73 }
74 }
75 }
76 return out
77}
78
79func (i *Interactive) clearReloadStatus(seq uint64, msg string, failed bool) bool {
80 i.mu.Lock()
81 defer i.mu.Unlock()
82 if i.reloadStatusSeq != seq {
83 return false
84 }
85 if failed {
86 if i.statusErr != msg {
87 return false
88 }
89 i.statusErr = ""
90 return true
91 }
92 if i.statusOK != msg {
93 return false
94 }
95 i.statusOK = ""
96 return true
97}
98
99func (i *Interactive) dismissReloadStatus(ctx context.Context, seq uint64, msg string, failed bool) {
100 timer := time.NewTimer(reloadStatusDuration)
101 defer timer.Stop()
102 select {
103 case <-ctx.Done():
104 return
105 case <-timer.C:
106 if i.clearReloadStatus(seq, msg, failed) {
107 i.invalidate()
108 }
109 }
110}