1package core
2
3import (
4 "fmt"
5 "strings"
6 "time"
7
8 tea "github.com/charmbracelet/bubbletea"
9 "github.com/charmbracelet/lipgloss"
10 "github.com/kujtimiihoxha/termai/internal/config"
11 "github.com/kujtimiihoxha/termai/internal/llm/models"
12 "github.com/kujtimiihoxha/termai/internal/lsp"
13 "github.com/kujtimiihoxha/termai/internal/lsp/protocol"
14 "github.com/kujtimiihoxha/termai/internal/tui/styles"
15 "github.com/kujtimiihoxha/termai/internal/tui/util"
16)
17
18type statusCmp struct {
19 info util.InfoMsg
20 width int
21 messageTTL time.Duration
22 lspClients map[string]*lsp.Client
23}
24
25// clearMessageCmd is a command that clears status messages after a timeout
26func (m statusCmp) clearMessageCmd(ttl time.Duration) tea.Cmd {
27 return tea.Tick(ttl, func(time.Time) tea.Msg {
28 return util.ClearStatusMsg{}
29 })
30}
31
32func (m statusCmp) Init() tea.Cmd {
33 return nil
34}
35
36func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
37 switch msg := msg.(type) {
38 case tea.WindowSizeMsg:
39 m.width = msg.Width
40 return m, nil
41 case util.InfoMsg:
42 m.info = msg
43 ttl := msg.TTL
44 if ttl == 0 {
45 ttl = m.messageTTL
46 }
47 return m, m.clearMessageCmd(ttl)
48 case util.ClearStatusMsg:
49 m.info = util.InfoMsg{}
50 }
51 return m, nil
52}
53
54var helpWidget = styles.Padded.Background(styles.ForgroundMid).Foreground(styles.BackgroundDarker).Bold(true).Render("ctrl+? help")
55
56func (m statusCmp) View() string {
57 status := helpWidget
58 diagnostics := styles.Padded.Background(styles.BackgroundDarker).Render(m.projectDiagnostics())
59 if m.info.Msg != "" {
60 infoStyle := styles.Padded.
61 Foreground(styles.Base).
62 Width(m.availableFooterMsgWidth(diagnostics))
63 switch m.info.Type {
64 case util.InfoTypeInfo:
65 infoStyle = infoStyle.Background(styles.BorderColor)
66 case util.InfoTypeWarn:
67 infoStyle = infoStyle.Background(styles.Peach)
68 case util.InfoTypeError:
69 infoStyle = infoStyle.Background(styles.Red)
70 }
71 // Truncate message if it's longer than available width
72 msg := m.info.Msg
73 availWidth := m.availableFooterMsgWidth(diagnostics) - 10
74 if len(msg) > availWidth && availWidth > 0 {
75 msg = msg[:availWidth] + "..."
76 }
77 status += infoStyle.Render(msg)
78 } else {
79 status += styles.Padded.
80 Foreground(styles.Base).
81 Background(styles.BackgroundDim).
82 Width(m.availableFooterMsgWidth(diagnostics)).
83 Render("")
84 }
85 status += diagnostics
86 status += m.model()
87 return status
88}
89
90func (m *statusCmp) projectDiagnostics() string {
91 errorDiagnostics := []protocol.Diagnostic{}
92 warnDiagnostics := []protocol.Diagnostic{}
93 hintDiagnostics := []protocol.Diagnostic{}
94 infoDiagnostics := []protocol.Diagnostic{}
95 for _, client := range m.lspClients {
96 for _, d := range client.GetDiagnostics() {
97 for _, diag := range d {
98 switch diag.Severity {
99 case protocol.SeverityError:
100 errorDiagnostics = append(errorDiagnostics, diag)
101 case protocol.SeverityWarning:
102 warnDiagnostics = append(warnDiagnostics, diag)
103 case protocol.SeverityHint:
104 hintDiagnostics = append(hintDiagnostics, diag)
105 case protocol.SeverityInformation:
106 infoDiagnostics = append(infoDiagnostics, diag)
107 }
108 }
109 }
110 }
111
112 if len(errorDiagnostics) == 0 && len(warnDiagnostics) == 0 && len(hintDiagnostics) == 0 && len(infoDiagnostics) == 0 {
113 return "No diagnostics"
114 }
115
116 diagnostics := []string{}
117
118 if len(errorDiagnostics) > 0 {
119 errStr := lipgloss.NewStyle().Foreground(styles.Error).Render(fmt.Sprintf("%s %d", styles.ErrorIcon, len(errorDiagnostics)))
120 diagnostics = append(diagnostics, errStr)
121 }
122 if len(warnDiagnostics) > 0 {
123 warnStr := lipgloss.NewStyle().Foreground(styles.Warning).Render(fmt.Sprintf("%s %d", styles.WarningIcon, len(warnDiagnostics)))
124 diagnostics = append(diagnostics, warnStr)
125 }
126 if len(hintDiagnostics) > 0 {
127 hintStr := lipgloss.NewStyle().Foreground(styles.Text).Render(fmt.Sprintf("%s %d", styles.HintIcon, len(hintDiagnostics)))
128 diagnostics = append(diagnostics, hintStr)
129 }
130 if len(infoDiagnostics) > 0 {
131 infoStr := lipgloss.NewStyle().Foreground(styles.Peach).Render(fmt.Sprintf("%s %d", styles.InfoIcon, len(infoDiagnostics)))
132 diagnostics = append(diagnostics, infoStr)
133 }
134
135 return strings.Join(diagnostics, " ")
136}
137
138func (m statusCmp) availableFooterMsgWidth(diagnostics string) int {
139 return max(0, m.width-lipgloss.Width(helpWidget)-lipgloss.Width(m.model())-lipgloss.Width(diagnostics))
140}
141
142func (m statusCmp) model() string {
143 cfg := config.Get()
144
145 coder, ok := cfg.Agents[config.AgentCoder]
146 if !ok {
147 return "Unknown"
148 }
149 model := models.SupportedModels[coder.Model]
150 return styles.Padded.Background(styles.Grey).Foreground(styles.Text).Render(model.Name)
151}
152
153func NewStatusCmp(lspClients map[string]*lsp.Client) tea.Model {
154 return &statusCmp{
155 messageTTL: 10 * time.Second,
156 lspClients: lspClients,
157 }
158}