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/core/layout"
14 "github.com/charmbracelet/crush/internal/tui/components/logo"
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}