status.go

  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/opencode/internal/config"
 11	"github.com/kujtimiihoxha/opencode/internal/llm/models"
 12	"github.com/kujtimiihoxha/opencode/internal/lsp"
 13	"github.com/kujtimiihoxha/opencode/internal/lsp/protocol"
 14	"github.com/kujtimiihoxha/opencode/internal/tui/styles"
 15	"github.com/kujtimiihoxha/opencode/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}