sidebar.go

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