on main help sign in
fix(ui): context percentage updates (#2115)
f53402dAyman Bagabas
internal/ui/model/header.go+47−15
@@ -12,6 +12,7 @@ import (
12 "github.com/charmbracelet/crush/internal/session"
12 "github.com/charmbracelet/crush/internal/session"
13 "github.com/charmbracelet/crush/internal/ui/common"
13 "github.com/charmbracelet/crush/internal/ui/common"
14 "github.com/charmbracelet/crush/internal/ui/styles"
14 "github.com/charmbracelet/crush/internal/ui/styles"
15+ uv "github.com/charmbracelet/ultraviolet"
15 "github.com/charmbracelet/x/ansi"
16 "github.com/charmbracelet/x/ansi"
16)
17)
17
18
@@ -22,29 +23,58 @@ const (
22 rightPadding = 1
23 rightPadding = 1
23)
24)
24
25
26+type header struct {
27+ // cached logo and compact logo
28+ logo string
29+ compactLogo string
30+
31+ com *common.Common
32+ width int
33+ compact bool
34+}
35+
36+// newHeader creates a new header model.
37+func newHeader(com *common.Common) *header {
38+ h := &header{
39+ com: com,
40+ }
41+ t := com.Styles
42+ h.compactLogo = t.Header.Charm.Render("Charm™") + " " +
43+ styles.ApplyBoldForegroundGrad(t, "CRUSH", t.Secondary, t.Primary) + " "
44+ return h
45+}
46+
25−// renderCompactHeader renders the compact header for the given session.
47+// drawHeader draws the header for the given session.
26−func renderCompactHeader(
48+func (h *header) drawHeader(
27− com *common.Common,
49+ scr uv.Screen,
50+ area uv.Rectangle,
28 session *session.Session,
51 session *session.Session,
29− lspClients *csync.Map[string, *lsp.Client],
52+ compact bool,
30 detailsOpen bool,
53 detailsOpen bool,
31 width int,
54 width int,
32−) string {
55+) {
33− if session == nil || session.ID == "" {
56+ t := h.com.Styles
34− return ""
57+ if width != h.width || compact != h.compact {
58+ h.logo = renderLogo(h.com.Styles, compact, width)
35 }
59 }
36
60
37− t := com.Styles
61+ h.width = width
62+ h.compact = compact
38
63
39− var b strings.Builder
64+ if !compact || session == nil || h.com.App == nil {
65+ uv.NewStyledString(h.logo).Draw(scr, area)
66+ return
67+ }
68+
69+ if session.ID == "" {
70+ return
71+ }
40
72
41− b.WriteString(t.Header.Charm.Render("Charm™"))
73+ var b strings.Builder
42− b.WriteString(" ")
43− b.WriteString(styles.ApplyBoldForegroundGrad(t, "CRUSH", t.Secondary, t.Primary))
44− b.WriteString(" ")
74+ b.WriteString(h.compactLogo)
45
75
46 availDetailWidth := width - leftPadding - rightPadding - lipgloss.Width(b.String()) - minHeaderDiags
76 availDetailWidth := width - leftPadding - rightPadding - lipgloss.Width(b.String()) - minHeaderDiags
47− details := renderHeaderDetails(com, session, lspClients, detailsOpen, availDetailWidth)
77+ details := renderHeaderDetails(h.com, session, h.com.App.LSPClients, detailsOpen, availDetailWidth)
48
78
49 remainingWidth := width -
79 remainingWidth := width -
50 lipgloss.Width(b.String()) -
80 lipgloss.Width(b.String()) -
@@ -61,7 +91,9 @@ func renderCompactHeader(
61
91
62 b.WriteString(details)
92 b.WriteString(details)
63
93
94+ view := uv.NewStyledString(
64− return t.Base.Padding(0, rightPadding, 0, leftPadding).Render(b.String())
95+ t.Base.Padding(0, rightPadding, 0, leftPadding).Render(b.String()))
96+ view.Draw(scr, area)
65}
97}
66
98
67// renderHeaderDetails renders the details section of the header.
99// renderHeaderDetails renders the details section of the header.
internal/ui/model/ui.go+28−29
@@ -141,8 +141,7 @@ type UI struct {
141 // isCanceling tracks whether the user has pressed escape once to cancel.
141 // isCanceling tracks whether the user has pressed escape once to cancel.
142 isCanceling bool
142 isCanceling bool
143
143
144− // header is the last cached header logo
144+ header *header
145− header string
146
145
147 // sendProgressBar instructs the TUI to send progress bar updates to the
146 // sendProgressBar instructs the TUI to send progress bar updates to the
148 // terminal.
147 // terminal.
@@ -261,12 +260,15 @@ func New(com *common.Common) *UI {
261 },
260 },
262 )
261 )
263
262
263+ header := newHeader(com)
264+
264 ui := &UI{
265 ui := &UI{
265 com: com,
266 com: com,
266 dialog: dialog.NewOverlay(),
267 dialog: dialog.NewOverlay(),
267 keyMap: keyMap,
268 keyMap: keyMap,
268 textarea: ta,
269 textarea: ta,
269 chat: ch,
270 chat: ch,
271+ header: header,
270 completions: comp,
272 completions: comp,
271 attachments: attachments,
273 attachments: attachments,
272 todoSpinner: todoSpinner,
274 todoSpinner: todoSpinner,
@@ -325,6 +327,10 @@ func (m *UI) Init() tea.Cmd {
325
327
326// setState changes the UI state and focus.
328// setState changes the UI state and focus.
327func (m *UI) setState(state uiState, focus uiFocusState) {
329func (m *UI) setState(state uiState, focus uiFocusState) {
330+ if state == uiLanding {
331+ // Always turn off compact mode when going to landing
332+ m.isCompact = false
333+ }
328 m.state = state
334 m.state = state
329 m.focus = focus
335 m.focus = focus
330 // Changing the state may change layout, so update it.
336 // Changing the state may change layout, so update it.
@@ -1761,6 +1767,18 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
1761 return tea.Batch(cmds...)
1767 return tea.Batch(cmds...)
1762}
1768}
1763
1769
1770+// drawHeader draws the header section of the UI.
1771+func (m *UI) drawHeader(scr uv.Screen, area uv.Rectangle) {
1772+ m.header.drawHeader(
1773+ scr,
1774+ area,
1775+ m.session,
1776+ m.isCompact,
1777+ m.detailsOpen,
1778+ m.width,
1779+ )
1780+}
1781+
1764// Draw implements [uv.Drawable] and draws the UI model.
1782// Draw implements [uv.Drawable] and draws the UI model.
1765func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
1783func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
1766 layout := m.generateLayout(area.Dx(), area.Dy())
1784 layout := m.generateLayout(area.Dx(), area.Dy())
@@ -1775,22 +1793,19 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
1775
1793
1776 switch m.state {
1794 switch m.state {
1777 case uiOnboarding:
1795 case uiOnboarding:
1778− header := uv.NewStyledString(m.header)
1779− header.Draw(scr, layout.header)
1796+ m.drawHeader(scr, layout.header)
1780
1797
1781 // NOTE: Onboarding flow will be rendered as dialogs below, but
1798 // NOTE: Onboarding flow will be rendered as dialogs below, but
1782 // positioned at the bottom left of the screen.
1799 // positioned at the bottom left of the screen.
1783
1800
1784 case uiInitialize:
1801 case uiInitialize:
1785− header := uv.NewStyledString(m.header)
1786− header.Draw(scr, layout.header)
1802+ m.drawHeader(scr, layout.header)
1787
1803
1788 main := uv.NewStyledString(m.initializeView())
1804 main := uv.NewStyledString(m.initializeView())
1789 main.Draw(scr, layout.main)
1805 main.Draw(scr, layout.main)
1790
1806
1791 case uiLanding:
1807 case uiLanding:
1792− header := uv.NewStyledString(m.header)
1793− header.Draw(scr, layout.header)
1808+ m.drawHeader(scr, layout.header)
1794 main := uv.NewStyledString(m.landingView())
1809 main := uv.NewStyledString(m.landingView())
1795 main.Draw(scr, layout.main)
1810 main.Draw(scr, layout.main)
1796
1811
@@ -1799,8 +1814,7 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
1799
1814
1800 case uiChat:
1815 case uiChat:
1801 if m.isCompact {
1816 if m.isCompact {
1802− header := uv.NewStyledString(m.header)
1803− header.Draw(scr, layout.header)
1817+ m.drawHeader(scr, layout.header)
1804 } else {
1818 } else {
1805 m.drawSidebar(scr, layout.sidebar)
1819 m.drawSidebar(scr, layout.sidebar)
1806 }
1820 }
@@ -2177,14 +2191,9 @@ func (m *UI) updateSize() {
2177
2191
2178 // Handle different app states
2192 // Handle different app states
2179 switch m.state {
2193 switch m.state {
2180− case uiOnboarding, uiInitialize, uiLanding:
2181− m.renderHeader(false, m.layout.header.Dx())
2182−
2183 case uiChat:
2194 case uiChat:
2184− if m.isCompact {
2195+ if !m.isCompact {
2185− m.renderHeader(true, m.layout.header.Dx())
2186− } else {
2187− m.renderSidebarLogo(m.layout.sidebar.Dx())
2196+ m.cacheSidebarLogo(m.layout.sidebar.Dx())
2188 }
2197 }
2189 }
2198 }
2190}
2199}
@@ -2590,18 +2599,8 @@ func (m *UI) renderEditorView(width int) string {
2590 )
2599 )
2591}
2600}
2592
2601
2593−// renderHeader renders and caches the header logo at the specified width.
2594−func (m *UI) renderHeader(compact bool, width int) {
2595− if compact && m.session != nil && m.com.App != nil {
2596− m.header = renderCompactHeader(m.com, m.session, m.com.App.LSPClients, m.detailsOpen, width)
2597− } else {
2598− m.header = renderLogo(m.com.Styles, compact, width)
2599− }
2600−}
2601−
2602−// renderSidebarLogo renders and caches the sidebar logo at the specified
2602+// cacheSidebarLogo renders and caches the sidebar logo at the specified width.
2603−// width.
2604−func (m *UI) renderSidebarLogo(width int) {
2603+func (m *UI) cacheSidebarLogo(width int) {
2605 m.sidebarLogo = renderLogo(m.com.Styles, true, width)
2604 m.sidebarLogo = renderLogo(m.com.Styles, true, width)
2606}
2605}
2607
2606