sidebar.go

  1package sidebar
  2
  3import (
  4	"os"
  5	"strings"
  6
  7	tea "github.com/charmbracelet/bubbletea/v2"
  8	"github.com/charmbracelet/lipgloss/v2"
  9	"github.com/opencode-ai/opencode/internal/config"
 10	"github.com/opencode-ai/opencode/internal/pubsub"
 11	"github.com/opencode-ai/opencode/internal/session"
 12	"github.com/opencode-ai/opencode/internal/tui/components/chat"
 13	"github.com/opencode-ai/opencode/internal/tui/components/core"
 14	"github.com/opencode-ai/opencode/internal/tui/components/logo"
 15	"github.com/opencode-ai/opencode/internal/tui/layout"
 16	"github.com/opencode-ai/opencode/internal/tui/styles"
 17	"github.com/opencode-ai/opencode/internal/tui/util"
 18	"github.com/opencode-ai/opencode/internal/version"
 19)
 20
 21const (
 22	logoBreakpoint = 65
 23)
 24
 25type Sidebar interface {
 26	util.Model
 27	layout.Sizeable
 28}
 29
 30type sidebarCmp struct {
 31	width, height int
 32	session       session.Session
 33	logo          string
 34	cwd           string
 35}
 36
 37func NewSidebarCmp() Sidebar {
 38	return &sidebarCmp{}
 39}
 40
 41func (m *sidebarCmp) Init() tea.Cmd {
 42	m.logo = m.logoBlock(false)
 43	m.cwd = cwd()
 44	return nil
 45}
 46
 47func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 48	switch msg := msg.(type) {
 49	case chat.SessionSelectedMsg:
 50		if msg.ID != m.session.ID {
 51			m.session = msg
 52		}
 53	case pubsub.Event[session.Session]:
 54		if msg.Type == pubsub.UpdatedEvent {
 55			if m.session.ID == msg.Payload.ID {
 56				m.session = msg.Payload
 57			}
 58		}
 59	}
 60	return m, nil
 61}
 62
 63func (m *sidebarCmp) View() tea.View {
 64	t := styles.CurrentTheme()
 65	parts := []string{
 66		m.logo,
 67	}
 68
 69	if m.session.ID != "" {
 70		parts = append(parts, t.S().Muted.Render(m.session.Title), "")
 71	}
 72
 73	parts = append(parts,
 74		m.cwd,
 75		"",
 76		m.lspBlock(),
 77		"",
 78		m.mcpBlock(),
 79	)
 80
 81	return tea.NewView(
 82		lipgloss.JoinVertical(lipgloss.Left, parts...),
 83	)
 84}
 85
 86func (m *sidebarCmp) SetSize(width, height int) tea.Cmd {
 87	if width < logoBreakpoint && m.width >= logoBreakpoint {
 88		m.logo = m.logoBlock(true)
 89	} else if width >= logoBreakpoint && m.width < logoBreakpoint {
 90		m.logo = m.logoBlock(false)
 91	}
 92
 93	m.width = width
 94	m.height = height
 95	return nil
 96}
 97
 98func (m *sidebarCmp) GetSize() (int, int) {
 99	return m.width, m.height
100}
101
102func (m *sidebarCmp) logoBlock(compact bool) string {
103	t := styles.CurrentTheme()
104	return logo.Render(version.Version, compact, logo.Opts{
105		FieldColor:   t.Primary,
106		TitleColorA:  t.Secondary,
107		TitleColorB:  t.Primary,
108		CharmColor:   t.Secondary,
109		VersionColor: t.Primary,
110	})
111}
112
113func (m *sidebarCmp) lspBlock() string {
114	maxWidth := min(m.width, 58)
115	t := styles.CurrentTheme()
116
117	section := t.S().Muted.Render(
118		core.Section("LSPs", maxWidth),
119	)
120
121	lspList := []string{section, ""}
122
123	lsp := config.Get().LSP
124	if len(lsp) == 0 {
125		return lipgloss.JoinVertical(
126			lipgloss.Left,
127			section,
128			"",
129			t.S().Base.Foreground(t.Border).Render("None"),
130		)
131	}
132
133	for n, l := range lsp {
134		iconColor := t.Success
135		if l.Disabled {
136			iconColor = t.FgMuted
137		}
138		lspList = append(lspList,
139			core.Status(
140				core.StatusOpts{
141					IconColor:   iconColor,
142					Title:       n,
143					Description: l.Command,
144				},
145				m.width,
146			),
147		)
148	}
149
150	return lipgloss.JoinVertical(
151		lipgloss.Left,
152		lspList...,
153	)
154}
155
156func (m *sidebarCmp) mcpBlock() string {
157	maxWidth := min(m.width, 58)
158	t := styles.CurrentTheme()
159
160	section := t.S().Muted.Render(
161		core.Section("MCPs", maxWidth),
162	)
163
164	mcpList := []string{section, ""}
165
166	mcp := config.Get().MCPServers
167	if len(mcp) == 0 {
168		return lipgloss.JoinVertical(
169			lipgloss.Left,
170			section,
171			"",
172			t.S().Base.Foreground(t.Border).Render("None"),
173		)
174	}
175
176	for n, l := range mcp {
177		iconColor := t.Success
178		mcpList = append(mcpList,
179			core.Status(
180				core.StatusOpts{
181					IconColor:   iconColor,
182					Title:       n,
183					Description: l.Command,
184				},
185				m.width,
186			),
187		)
188	}
189
190	return lipgloss.JoinVertical(
191		lipgloss.Left,
192		mcpList...,
193	)
194}
195
196func cwd() string {
197	cwd := config.WorkingDirectory()
198	t := styles.CurrentTheme()
199	// replace home directory with ~
200	homeDir, err := os.UserHomeDir()
201	if err == nil {
202		cwd = strings.ReplaceAll(cwd, homeDir, "~")
203	}
204	return t.S().Muted.Render(cwd)
205}