on main help sign in
chore(styles): use hypercrush theme when hyper is selected
9d4653eChristian Rocha
internal/app/app.go+1−1
@@ -233,7 +233,7 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt,
233 progress = app.config.Config().Options.Progress == nil || *app.config.Config().Options.Progress
233 progress = app.config.Config().Options.Progress == nil || *app.config.Config().Options.Progress
234
234
235 if !hideSpinner && stderrTTY {
235 if !hideSpinner && stderrTTY {
236− t := styles.CharmtonePantera()
236+ t := styles.ThemeForProvider(app.config.Config().Models[config.SelectedModelTypeLarge].Provider)
237
237
238 // Detect background color to set the appropriate color for the
238 // Detect background color to set the appropriate color for the
239 // spinner's 'Generating...' text. Without this, that text would be
239 // spinner's 'Generating...' text. Without this, that text would be
internal/cmd/run.go+1−1
@@ -189,7 +189,7 @@ func runNonInteractive(
189 progress = ws.Config.Options.Progress == nil || *ws.Config.Options.Progress
189 progress = ws.Config.Options.Progress == nil || *ws.Config.Options.Progress
190
190
191 if !hideSpinner && stderrTTY {
191 if !hideSpinner && stderrTTY {
192− t := styles.CharmtonePantera()
192+ t := styles.ThemeForProvider(ws.Config.Models[config.SelectedModelTypeLarge].Provider)
193
193
194 hasDarkBG := true
194 hasDarkBG := true
195 if stdinTTY && stdoutTTY {
195 if stdinTTY && stdoutTTY {
internal/cmd/session.go+10−4
@@ -101,6 +101,7 @@ func init() {
101type sessionServices struct {
101type sessionServices struct {
102 sessions session.Service
102 sessions session.Service
103 messages message.Service
103 messages message.Service
104+ cfg *config.ConfigStore
104}
105}
105
106
106func sessionSetup(cmd *cobra.Command) (context.Context, *sessionServices, func(), error) {
107func sessionSetup(cmd *cobra.Command) (context.Context, *sessionServices, func(), error) {
@@ -127,6 +128,7 @@ func sessionSetup(cmd *cobra.Command) (context.Context, *sessionServices, func()
127 svc := &sessionServices{
128 svc := &sessionServices{
128 sessions: session.NewService(queries, conn),
129 sessions: session.NewService(queries, conn),
129 messages: message.NewService(queries),
130 messages: message.NewService(queries),
131+ cfg: cfg,
130 }
132 }
131 return ctx, svc, func() { conn.Close() }, nil
133 return ctx, svc, func() { conn.Close() }, nil
132}
134}
@@ -280,7 +282,7 @@ func runSessionShow(cmd *cobra.Command, args []string) error {
280 if sessionShowJSON {
282 if sessionShowJSON {
281 return outputSessionJSON(cmd.OutOrStdout(), sess, msgPtrs)
283 return outputSessionJSON(cmd.OutOrStdout(), sess, msgPtrs)
282 }
284 }
283− return outputSessionHuman(ctx, sess, msgPtrs)
285+ return outputSessionHuman(ctx, svc.cfg, sess, msgPtrs)
284}
286}
285
287
286func runSessionDelete(cmd *cobra.Command, args []string) error {
288func runSessionDelete(cmd *cobra.Command, args []string) error {
@@ -387,7 +389,7 @@ func runSessionLast(cmd *cobra.Command, _ []string) error {
387 if sessionLastJSON {
389 if sessionLastJSON {
388 return outputSessionJSON(cmd.OutOrStdout(), sess, msgPtrs)
390 return outputSessionJSON(cmd.OutOrStdout(), sess, msgPtrs)
389 }
391 }
390− return outputSessionHuman(ctx, sess, msgPtrs)
392+ return outputSessionHuman(ctx, svc.cfg, sess, msgPtrs)
391}
393}
392
394
393const (
395const (
@@ -437,8 +439,12 @@ func outputSessionJSON(w io.Writer, sess session.Session, msgs []*message.Messag
437 return enc.Encode(output)
439 return enc.Encode(output)
438}
440}
439
441
440−func outputSessionHuman(ctx context.Context, sess session.Session, msgs []*message.Message) error {
442+func outputSessionHuman(ctx context.Context, cfg *config.ConfigStore, sess session.Session, msgs []*message.Message) error {
441− styles := styles.CharmtonePantera()
443+ var providerID string
444+ if cfg != nil {
445+ providerID = cfg.Config().Models[config.SelectedModelTypeLarge].Provider
446+ }
447+ styles := styles.ThemeForProvider(providerID)
442 toolResults := chat.BuildToolResultMap(msgs)
448 toolResults := chat.BuildToolResultMap(msgs)
443
449
444 width := sessionOutputWidth
450 width := sessionOutputWidth
internal/ui/attachments/attachments.go+12
@@ -78,6 +78,10 @@ func (m *Attachments) Render(width int) string {
78 return m.renderer.Render(m.list, m.deleting, width)
78 return m.renderer.Render(m.list, m.deleting, width)
79}
79}
80
80
81+// Renderer returns the attachment renderer so callers can update its
82+// styles in place.
83+func (m *Attachments) Renderer() *Renderer { return m.renderer }
84+
81func NewRenderer(normalStyle, deletingStyle, imageStyle, textStyle lipgloss.Style) *Renderer {
85func NewRenderer(normalStyle, deletingStyle, imageStyle, textStyle lipgloss.Style) *Renderer {
82 return &Renderer{
86 return &Renderer{
83 normalStyle: normalStyle,
87 normalStyle: normalStyle,
@@ -87,6 +91,14 @@ func NewRenderer(normalStyle, deletingStyle, imageStyle, textStyle lipgloss.Styl
87 }
91 }
88}
92}
89
93
94+// SetStyles updates the renderer styles in place.
95+func (r *Renderer) SetStyles(normalStyle, deletingStyle, imageStyle, textStyle lipgloss.Style) {
96+ r.normalStyle = normalStyle
97+ r.textStyle = textStyle
98+ r.imageStyle = imageStyle
99+ r.deletingStyle = deletingStyle
100+}
101+
90type Renderer struct {
102type Renderer struct {
91 normalStyle, textStyle, imageStyle, deletingStyle lipgloss.Style
103 normalStyle, textStyle, imageStyle, deletingStyle lipgloss.Style
92}
104}
internal/ui/chat/messages.go+16
@@ -128,6 +128,22 @@ func defaultHighlighter(sty *styles.Styles) *highlightableMessageItem {
128 }
128 }
129}
129}
130
130
131+// cacheClearable is implemented by message items that cache rendered
132+// output and can be asked to drop the cache.
133+type cacheClearable interface {
134+ clearCache()
135+}
136+
137+// ClearItemCaches drops any cached rendered output on each item so the
138+// next render uses the current styles.
139+func ClearItemCaches(items []MessageItem) {
140+ for _, item := range items {
141+ if cc, ok := item.(cacheClearable); ok {
142+ cc.clearCache()
143+ }
144+ }
145+}
146+
131// cachedMessageItem caches rendered message content to avoid re-rendering.
147// cachedMessageItem caches rendered message content to avoid re-rendering.
132//
148//
133// This should be used by any message that can store a cached version of its render. e.x user,assistant... and so on
149// This should be used by any message that can store a cached version of its render. e.x user,assistant... and so on
internal/ui/common/common.go+17−2
@@ -31,15 +31,30 @@ func (c *Common) Config() *config.Config {
31 return c.Workspace.Config()
31 return c.Workspace.Config()
32}
32}
33
33
34−// DefaultCommon returns the default common UI configurations.
34+// DefaultCommon returns the default common UI configurations. When the
35+// workspace has a large model selected, the theme is chosen based on its
36+// provider; otherwise the default theme is used.
35func DefaultCommon(ws workspace.Workspace) *Common {
37func DefaultCommon(ws workspace.Workspace) *Common {
36− s := styles.CharmtonePantera()
38+ s := styles.ThemeForProvider(largeModelProviderID(ws))
37 return &Common{
39 return &Common{
38 Workspace: ws,
40 Workspace: ws,
39 Styles: &s,
41 Styles: &s,
40 }
42 }
41}
43}
42
44
45+// largeModelProviderID returns the provider ID of the currently selected
46+// large model, or the empty string if none is set or the workspace is nil.
47+func largeModelProviderID(ws workspace.Workspace) string {
48+ if ws == nil {
49+ return ""
50+ }
51+ cfg := ws.Config()
52+ if cfg == nil {
53+ return ""
54+ }
55+ return cfg.Models[config.SelectedModelTypeLarge].Provider
56+}
57+
43// CenterRect returns a new [Rectangle] centered within the given area with the
58// CenterRect returns a new [Rectangle] centered within the given area with the
44// specified width and height.
59// specified width and height.
45func CenterRect(area uv.Rectangle, width, height int) uv.Rectangle {
60func CenterRect(area uv.Rectangle, width, height int) uv.Rectangle {
internal/ui/completions/completions.go+9
@@ -110,6 +110,15 @@ func New(normalStyle, focusedStyle, matchStyle lipgloss.Style) *Completions {
110 }
110 }
111}
111}
112
112
113+// SetStyles updates the styles used when rendering completion items.
114+// Existing items are not restyled; subsequent SetItems calls pick up the
115+// new styles.
116+func (c *Completions) SetStyles(normalStyle, focusedStyle, matchStyle lipgloss.Style) {
117+ c.normalStyle = normalStyle
118+ c.focusedStyle = focusedStyle
119+ c.matchStyle = matchStyle
120+}
121+
113// IsOpen returns whether the completions popup is open.
122// IsOpen returns whether the completions popup is open.
114func (c *Completions) IsOpen() bool {
123func (c *Completions) IsOpen() bool {
115 return c.open
124 return c.open
internal/ui/model/chat.go+12
@@ -107,6 +107,18 @@ func (m *Chat) Len() int {
107 return m.list.Len()
107 return m.list.Len()
108}
108}
109
109
110+// InvalidateRenderCaches drops cached rendered output on every message
111+// item so the next draw re-renders with the current styles.
112+func (m *Chat) InvalidateRenderCaches() {
113+ items := make([]chat.MessageItem, 0, m.list.Len())
114+ for i := range m.list.Len() {
115+ if item, ok := m.list.ItemAt(i).(chat.MessageItem); ok {
116+ items = append(items, item)
117+ }
118+ }
119+ chat.ClearItemCaches(items)
120+}
121+
110// SetMessages sets the chat messages to the provided list of message items.
122// SetMessages sets the chat messages to the provided list of message items.
111func (m *Chat) SetMessages(msgs ...chat.MessageItem) {
123func (m *Chat) SetMessages(msgs ...chat.MessageItem) {
112 m.idInxMap = make(map[string]int)
124 m.idInxMap = make(map[string]int)
internal/ui/model/header.go+11−2
@@ -37,10 +37,19 @@ func newHeader(com *common.Common) *header {
37 h := &header{
37 h := &header{
38 com: com,
38 com: com,
39 }
39 }
40+ h.refresh()
41+ return h
42+}
43+
44+// refresh rebuilds cached logo strings using the current styles. Call
45+// after the theme changes.
46+func (h *header) refresh() {
40− t := com.Styles
47+ t := h.com.Styles
41 h.compactLogo = t.Header.Charm.Render("Charm™") + " " +
48 h.compactLogo = t.Header.Charm.Render("Charm™") + " " +
42 styles.ApplyBoldForegroundGrad(t.Header.LogoGradCanvas, "CRUSH", t.Header.LogoGradFromColor, t.Header.LogoGradToColor) + " "
49 styles.ApplyBoldForegroundGrad(t.Header.LogoGradCanvas, "CRUSH", t.Header.LogoGradFromColor, t.Header.LogoGradToColor) + " "
43− return h
50+ // Force drawHeader to re-render the wide logo on the next frame.
51+ h.width = 0
52+ h.logo = ""
44}
53}
45
54
46// drawHeader draws the header for the given session.
55// drawHeader draws the header for the given session.
internal/ui/model/ui.go+40−5
@@ -1461,11 +1461,18 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
1461
1461
1462 if err := m.com.Workspace.UpdatePreferredModel(config.ScopeGlobal, msg.ModelType, msg.Model); err != nil {
1462 if err := m.com.Workspace.UpdatePreferredModel(config.ScopeGlobal, msg.ModelType, msg.Model); err != nil {
1463 cmds = append(cmds, util.ReportError(err))
1463 cmds = append(cmds, util.ReportError(err))
1464+ } else {
1465+ if msg.ModelType == config.SelectedModelTypeLarge {
1466+ // Swap the theme live based on the newly selected large
1467+ // model's provider.
1468+ m.applyTheme(styles.ThemeForProvider(providerID))
1469+ }
1464− } else if _, ok := cfg.Models[config.SelectedModelTypeSmall]; !ok {
1470+ if _, ok := cfg.Models[config.SelectedModelTypeSmall]; !ok {
1465− // Ensure small model is set is unset.
1471+ // Ensure small model is set is unset.
1466− smallModel := m.com.Workspace.GetDefaultSmallModel(providerID)
1472+ smallModel := m.com.Workspace.GetDefaultSmallModel(providerID)
1467− if err := m.com.Workspace.UpdatePreferredModel(config.ScopeGlobal, config.SelectedModelTypeSmall, smallModel); err != nil {
1473+ if err := m.com.Workspace.UpdatePreferredModel(config.ScopeGlobal, config.SelectedModelTypeSmall, smallModel); err != nil {
1468− cmds = append(cmds, util.ReportError(err))
1474+ cmds = append(cmds, util.ReportError(err))
1475+ }
1469 }
1476 }
1470 }
1477 }
1471
1478
@@ -2980,6 +2987,34 @@ func (m *UI) cacheSidebarLogo(width int) {
2980 m.sidebarLogo = renderLogo(m.com.Styles, true, width)
2987 m.sidebarLogo = renderLogo(m.com.Styles, true, width)
2981}
2988}
2982
2989
2990+// applyTheme replaces the active styles with the given theme and
2991+// refreshes every component that caches style data.
2992+func (m *UI) applyTheme(s styles.Styles) {
2993+ *m.com.Styles = s
2994+ m.refreshStyles()
2995+}
2996+
2997+// refreshStyles pushes the current *m.com.Styles into every subcomponent
2998+// that copies or pre-renders style-dependent values at construction time.
2999+func (m *UI) refreshStyles() {
3000+ t := m.com.Styles
3001+ m.header.refresh()
3002+ if m.layout.sidebar.Dx() > 0 {
3003+ m.cacheSidebarLogo(m.layout.sidebar.Dx())
3004+ }
3005+ m.textarea.SetStyles(t.Editor.Textarea)
3006+ m.completions.SetStyles(t.Completions.Normal, t.Completions.Focused, t.Completions.Match)
3007+ m.attachments.Renderer().SetStyles(
3008+ t.Attachments.Normal,
3009+ t.Attachments.Deleting,
3010+ t.Attachments.Image,
3011+ t.Attachments.Text,
3012+ )
3013+ m.todoSpinner.Style = t.Pills.TodoSpinner
3014+ m.status.help.Styles = t.Help
3015+ m.chat.InvalidateRenderCaches()
3016+}
3017+
2983// sendMessage sends a message with the given content and attachments.
3018// sendMessage sends a message with the given content and attachments.
2984func (m *UI) sendMessage(content string, attachments ...message.Attachment) tea.Cmd {
3019func (m *UI) sendMessage(content string, attachments ...message.Attachment) tea.Cmd {
2985 if !m.com.Workspace.AgentIsReady() {
3020 if !m.com.Workspace.AgentIsReady() {
internal/ui/styles/themes.go+49
@@ -2,6 +2,18 @@ package styles
2
2
3import "github.com/charmbracelet/x/exp/charmtone"
3import "github.com/charmbracelet/x/exp/charmtone"
4
4
5+// ThemeForProvider returns the Styles associated with the given provider
6+// ID. Unknown or empty provider IDs yield the default Charmtone Pantera
7+// theme.
8+func ThemeForProvider(providerID string) Styles {
9+ switch providerID {
10+ case "hyper":
11+ return HypercrushObsidiana()
12+ default:
13+ return CharmtonePantera()
14+ }
15+}
16+
5// CharmtonePantera returns the Charmtone dark theme. It's the default style
17// CharmtonePantera returns the Charmtone dark theme. It's the default style
6// for the UI.
18// for the UI.
7func CharmtonePantera() Styles {
19func CharmtonePantera() Styles {
@@ -39,3 +51,40 @@ func CharmtonePantera() Styles {
39 successMuted: charmtone.Guac,
51 successMuted: charmtone.Guac,
40 })
52 })
41}
53}
54+
55+// HypercrushObsidiana returns the Hypercrush dark theme.
56+func HypercrushObsidiana() Styles {
57+ return quickStyle(quickStyleOpts{
58+ primary: charmtone.Charple,
59+ secondary: charmtone.Dolly,
60+ tertiary: charmtone.Bok,
61+
62+ fgBase: charmtone.Ash,
63+ fgMuted: charmtone.Squid,
64+ fgHalfMuted: charmtone.Smoke,
65+ fgSubtle: charmtone.Oyster,
66+
67+ onPrimary: charmtone.Salt,
68+ onAccent: charmtone.Butter,
69+
70+ bgBase: charmtone.Pepper,
71+ bgBaseLighter: charmtone.BBQ,
72+ bgSubtle: charmtone.Charcoal,
73+ bgOverlay: charmtone.Iron,
74+
75+ border: charmtone.Charcoal,
76+ borderFocus: charmtone.Charple,
77+
78+ danger: charmtone.Coral,
79+ error: charmtone.Sriracha,
80+ warning: charmtone.Zest,
81+ warningStrong: charmtone.Mustard,
82+ busy: charmtone.Citron,
83+ info: charmtone.Malibu,
84+ infoSubtle: charmtone.Sardine,
85+ infoMuted: charmtone.Damson,
86+ success: charmtone.Julep,
87+ successSubtle: charmtone.Bok,
88+ successMuted: charmtone.Guac,
89+ })
90+}