From 19c67f7ffc90d1470a4de5644ac78733f4020a42 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Tue, 13 Jan 2026 16:58:00 +0100 Subject: [PATCH] refactor: compact mode (#1850) --- internal/ui/dialog/commands.go | 2 +- internal/ui/model/header.go | 112 +++++++++++++ internal/ui/model/lsp.go | 3 + internal/ui/model/mcp.go | 3 + internal/ui/model/session.go | 11 +- internal/ui/model/sidebar.go | 2 +- internal/ui/model/ui.go | 277 +++++++++++++++++++++++++-------- internal/ui/styles/styles.go | 34 +++- 8 files changed, 366 insertions(+), 78 deletions(-) create mode 100644 internal/ui/model/header.go diff --git a/internal/ui/dialog/commands.go b/internal/ui/dialog/commands.go index ae9574e2d76b74ccc7465b59a2cafce6f7d9fd0e..03707a54775992992a36e90e6857b0f55ce3c8e3 100644 --- a/internal/ui/dialog/commands.go +++ b/internal/ui/dialog/commands.go @@ -227,11 +227,11 @@ func (c *Commands) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor { width := max(0, min(defaultDialogMaxWidth, area.Dx())) height := max(0, min(defaultDialogHeight, area.Dy())) if area.Dx() != c.windowWidth && c.selected == SystemCommands { + c.windowWidth = area.Dx() // since some items in the list depend on width (e.g. toggle sidebar command), // we need to reset the command items when width changes c.setCommandItems(c.selected) } - c.windowWidth = area.Dx() innerWidth := width - c.com.Styles.Dialog.View.GetHorizontalFrameSize() heightOffset := t.Dialog.Title.GetVerticalFrameSize() + titleContentHeight + t.Dialog.InputPrompt.GetVerticalFrameSize() + inputContentHeight + diff --git a/internal/ui/model/header.go b/internal/ui/model/header.go new file mode 100644 index 0000000000000000000000000000000000000000..e01a19143c20e0d3e2c6753b719c28092077ac91 --- /dev/null +++ b/internal/ui/model/header.go @@ -0,0 +1,112 @@ +package model + +import ( + "fmt" + "strings" + + "charm.land/lipgloss/v2" + "github.com/charmbracelet/crush/internal/config" + "github.com/charmbracelet/crush/internal/csync" + "github.com/charmbracelet/crush/internal/fsext" + "github.com/charmbracelet/crush/internal/lsp" + "github.com/charmbracelet/crush/internal/session" + "github.com/charmbracelet/crush/internal/ui/common" + "github.com/charmbracelet/crush/internal/ui/styles" + "github.com/charmbracelet/x/ansi" +) + +const ( + headerDiag = "╱" + minHeaderDiags = 3 + leftPadding = 1 + rightPadding = 1 +) + +// renderCompactHeader renders the compact header for the given session. +func renderCompactHeader( + com *common.Common, + session *session.Session, + lspClients *csync.Map[string, *lsp.Client], + detailsOpen bool, + width int, +) string { + if session == nil || session.ID == "" { + return "" + } + + t := com.Styles + + var b strings.Builder + + b.WriteString(t.Header.Charm.Render("Charm™")) + b.WriteString(" ") + b.WriteString(styles.ApplyBoldForegroundGrad(t, "CRUSH", t.Secondary, t.Primary)) + b.WriteString(" ") + + availDetailWidth := width - leftPadding - rightPadding - lipgloss.Width(b.String()) - minHeaderDiags + details := renderHeaderDetails(com, session, lspClients, detailsOpen, availDetailWidth) + + remainingWidth := width - + lipgloss.Width(b.String()) - + lipgloss.Width(details) - + leftPadding - + rightPadding + + if remainingWidth > 0 { + b.WriteString(t.Header.Diagonals.Render( + strings.Repeat(headerDiag, max(minHeaderDiags, remainingWidth)), + )) + b.WriteString(" ") + } + + b.WriteString(details) + + return t.Base.Padding(0, rightPadding, 0, leftPadding).Render(b.String()) +} + +// renderHeaderDetails renders the details section of the header. +func renderHeaderDetails( + com *common.Common, + session *session.Session, + lspClients *csync.Map[string, *lsp.Client], + detailsOpen bool, + availWidth int, +) string { + t := com.Styles + + var parts []string + + errorCount := 0 + for l := range lspClients.Seq() { + errorCount += l.GetDiagnosticCounts().Error + } + + if errorCount > 0 { + parts = append(parts, t.LSP.ErrorDiagnostic.Render(fmt.Sprintf("%s%d", styles.ErrorIcon, errorCount))) + } + + agentCfg := config.Get().Agents[config.AgentCoder] + model := config.Get().GetModelByType(agentCfg.Model) + percentage := (float64(session.CompletionTokens+session.PromptTokens) / float64(model.ContextWindow)) * 100 + formattedPercentage := t.Header.Percentage.Render(fmt.Sprintf("%d%%", int(percentage))) + parts = append(parts, formattedPercentage) + + const keystroke = "ctrl+d" + if detailsOpen { + parts = append(parts, t.Header.Keystroke.Render(keystroke)+t.Header.KeystrokeTip.Render(" close")) + } else { + parts = append(parts, t.Header.Keystroke.Render(keystroke)+t.Header.KeystrokeTip.Render(" open ")) + } + + dot := t.Header.Separator.Render(" • ") + metadata := strings.Join(parts, dot) + metadata = dot + metadata + + const dirTrimLimit = 4 + cfg := com.Config() + cwd := fsext.DirTrim(fsext.PrettyPath(cfg.WorkingDir()), dirTrimLimit) + cwd = ansi.Truncate(cwd, max(0, availWidth-lipgloss.Width(metadata)), "…") + cwd = t.Header.WorkingDir.Render(cwd) + + return cwd + metadata +} diff --git a/internal/ui/model/lsp.go b/internal/ui/model/lsp.go index 1f13b5afc3c8a90b6ca14e304636e31fbedddbfc..61e9f75d478ef51daee465ca7eeca109acd6c64b 100644 --- a/internal/ui/model/lsp.go +++ b/internal/ui/model/lsp.go @@ -72,6 +72,9 @@ func lspDiagnostics(t *styles.Styles, diagnostics map[protocol.DiagnosticSeverit // lspList renders a list of LSP clients with their status and diagnostics, // truncating to maxItems if needed. func lspList(t *styles.Styles, lsps []LSPInfo, width, maxItems int) string { + if maxItems <= 0 { + return "" + } var renderedLsps []string for _, l := range lsps { var icon string diff --git a/internal/ui/model/mcp.go b/internal/ui/model/mcp.go index 4100907d2c58f4238eb080356a069cf9bd0a2da6..40be8619133268edbc53cf2bee863ed89a2af00f 100644 --- a/internal/ui/model/mcp.go +++ b/internal/ui/model/mcp.go @@ -49,6 +49,9 @@ func mcpCounts(t *styles.Styles, counts mcp.Counts) string { // mcpList renders a list of MCP clients with their status and counts, // truncating to maxItems if needed. func mcpList(t *styles.Styles, mcps []mcp.ClientInfo, width, maxItems int) string { + if maxItems <= 0 { + return "" + } var renderedMcps []string for _, m := range mcps { diff --git a/internal/ui/model/session.go b/internal/ui/model/session.go index 065a17ad49b7d14092fc6bb868390e522e5eeaa8..38fd718db9cf2b44eb48538a9debb25870b90a7d 100644 --- a/internal/ui/model/session.go +++ b/internal/ui/model/session.go @@ -169,9 +169,13 @@ func (m *UI) handleFileEvent(file history.File) tea.Cmd { // filesInfo renders the modified files section for the sidebar, showing files // with their addition/deletion counts. -func (m *UI) filesInfo(cwd string, width, maxItems int) string { +func (m *UI) filesInfo(cwd string, width, maxItems int, isSection bool) string { t := m.com.Styles - title := common.Section(t, "Modified Files", width) + + title := t.Subtle.Render("Modified Files") + if isSection { + title = common.Section(t, "Modified Files", width) + } list := t.Subtle.Render("None") if len(m.sessionFiles) > 0 { @@ -184,6 +188,9 @@ func (m *UI) filesInfo(cwd string, width, maxItems int) string { // fileList renders a list of files with their diff statistics, truncating to // maxItems and showing a "...and N more" message if needed. func fileList(t *styles.Styles, cwd string, files []SessionFile, width, maxItems int) string { + if maxItems <= 0 { + return "" + } var renderedFiles []string filesShown := 0 diff --git a/internal/ui/model/sidebar.go b/internal/ui/model/sidebar.go index 11d7b73baee60fbf68514ae34fb5aeaf459a16d9..c0e46eb31530bc9b9d4f62fbfb020afdd7abc009 100644 --- a/internal/ui/model/sidebar.go +++ b/internal/ui/model/sidebar.go @@ -133,7 +133,7 @@ func (m *UI) drawSidebar(scr uv.Screen, area uv.Rectangle) { lspSection := m.lspInfo(width, maxLSPs, true) mcpSection := m.mcpInfo(width, maxMCPs, true) - filesSection := m.filesInfo(m.com.Config().WorkingDir(), width, maxFiles) + filesSection := m.filesInfo(m.com.Config().WorkingDir(), width, maxFiles, true) uv.NewStyledString( lipgloss.NewStyle(). diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index 23c7f0d7b8811aa598eb71a0c42a73a2b17e76cf..6a92f5bb9c7c4f856cd83b38272a63120fea929f 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -53,6 +53,15 @@ const maxAttachmentSize = int64(5 * 1024 * 1024) // Allowed image formats. var allowedImageTypes = []string{".jpg", ".jpeg", ".png"} +// Compact mode breakpoints. +const ( + compactModeWidthBreakpoint = 120 + compactModeHeightBreakpoint = 30 +) + +// Session details panel max height. +const sessionDetailsMaxHeight = 20 + // uiFocusState represents the current focus state of the UI. type uiFocusState uint8 @@ -71,7 +80,6 @@ const ( uiInitialize uiLanding uiChat - uiChatCompact ) type openEditorMsg struct { @@ -161,6 +169,16 @@ type UI struct { // custom commands & mcp commands customCommands []commands.CustomCommand mcpCustomCommands []commands.MCPCustomCommand + + // forceCompactMode tracks whether compact mode is forced by user toggle + forceCompactMode bool + + // isCompact tracks whether we're currently in compact layout mode (either + // by user toggle or auto-switch based on window size) + isCompact bool + + // detailsOpen tracks whether the details panel is open (in compact mode) + detailsOpen bool } // New creates a new instance of the [UI] model. @@ -233,6 +251,9 @@ func New(com *common.Common) *UI { ui.textarea.Placeholder = ui.readyPlaceholder ui.status = status + // Initialize compact mode from config + ui.forceCompactMode = com.Config().Options.TUI.CompactMode + return ui } @@ -284,6 +305,9 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case loadSessionMsg: m.state = uiChat + if m.forceCompactMode { + m.isCompact = true + } m.session = msg.session m.sessionFiles = msg.files msgs, err := m.com.App.Messages.List(context.Background(), m.session.ID) @@ -370,6 +394,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case tea.WindowSizeMsg: m.width, m.height = msg.Width, msg.Height + m.handleCompactMode(m.width, m.height) m.updateLayoutAndSize() case tea.KeyboardEnhancementsMsg: m.keyenh = msg @@ -840,6 +865,9 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd { case dialog.ActionToggleHelp: m.status.ToggleHelp() m.dialog.CloseDialog(dialog.CommandsID) + case dialog.ActionToggleCompactMode: + cmds = append(cmds, m.toggleCompactMode()) + m.dialog.CloseDialog(dialog.CommandsID) case dialog.ActionQuit: cmds = append(cmds, tea.Quit) case dialog.ActionInitializeProject: @@ -938,6 +966,10 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { cmds = append(cmds, cmd) } return true + case key.Matches(msg, m.keyMap.Chat.Details) && m.isCompact: + m.detailsOpen = !m.detailsOpen + m.updateLayoutAndSize() + return true } return false } @@ -972,7 +1004,7 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { case uiInitialize: cmds = append(cmds, m.updateInitializeView(msg)...) return tea.Batch(cmds...) - case uiChat, uiLanding, uiChatCompact: + case uiChat, uiLanding: switch m.focus { case uiFocusEditor: // Handle completions if open. @@ -1070,6 +1102,12 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { } } + // remove the details if they are open when user starts typing + if m.detailsOpen { + m.detailsOpen = false + m.updateLayoutAndSize() + } + ta, cmd := m.textarea.Update(msg) m.textarea = ta cmds = append(cmds, cmd) @@ -1220,28 +1258,26 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor { editor.Draw(scr, layout.editor) case uiChat: - m.chat.Draw(scr, layout.main) + if m.isCompact { + header := uv.NewStyledString(m.header) + header.Draw(scr, layout.header) + } else { + m.drawSidebar(scr, layout.sidebar) + } - header := uv.NewStyledString(m.header) - header.Draw(scr, layout.header) - m.drawSidebar(scr, layout.sidebar) + m.chat.Draw(scr, layout.main) - editor := uv.NewStyledString(m.renderEditorView(scr.Bounds().Dx() - layout.sidebar.Dx())) + editorWidth := scr.Bounds().Dx() + if !m.isCompact { + editorWidth -= layout.sidebar.Dx() + } + editor := uv.NewStyledString(m.renderEditorView(editorWidth)) editor.Draw(scr, layout.editor) - case uiChatCompact: - header := uv.NewStyledString(m.header) - header.Draw(scr, layout.header) - - mainView := lipgloss.NewStyle().Width(layout.main.Dx()). - Height(layout.main.Dy()). - Background(lipgloss.ANSIColor(rand.Intn(256))). - Render(" Compact Chat Messages ") - main := uv.NewStyledString(mainView) - main.Draw(scr, layout.main) - - editor := uv.NewStyledString(m.renderEditorView(scr.Bounds().Dx())) - editor.Draw(scr, layout.editor) + // Draw details overlay in compact mode when open + if m.isCompact && m.detailsOpen { + m.drawSessionDetails(scr, layout.sessionDetails) + } } // Add status and help layer @@ -1290,6 +1326,10 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor { // Don't show cursor if editor is not visible return nil } + if m.detailsOpen && m.isCompact { + // Don't show cursor if details overlay is open + return nil + } if m.textarea.Focused() { cur := m.textarea.Cursor() @@ -1537,6 +1577,36 @@ func (m *UI) FullHelp() [][]key.Binding { return binds } +// toggleCompactMode toggles compact mode between uiChat and uiChatCompact states. +func (m *UI) toggleCompactMode() tea.Cmd { + m.forceCompactMode = !m.forceCompactMode + + err := m.com.Config().SetCompactMode(m.forceCompactMode) + if err != nil { + return uiutil.ReportError(err) + } + + m.handleCompactMode(m.width, m.height) + m.updateLayoutAndSize() + + return nil +} + +// handleCompactMode updates the UI state based on window size and compact mode setting. +func (m *UI) handleCompactMode(newWidth, newHeight int) { + if m.state == uiChat { + if m.forceCompactMode { + m.isCompact = true + return + } + if newWidth < compactModeWidthBreakpoint || newHeight < compactModeHeightBreakpoint { + m.isCompact = true + } else { + m.isCompact = false + } + } +} + // updateLayoutAndSize updates the layout and sizes of UI components. func (m *UI) updateLayoutAndSize() { m.layout = m.generateLayout(m.width, m.height) @@ -1558,11 +1628,11 @@ func (m *UI) updateSize() { m.renderHeader(false, m.layout.header.Dx()) case uiChat: - m.renderSidebarLogo(m.layout.sidebar.Dx()) - - case uiChatCompact: - // TODO: set the width and heigh of the chat component - m.renderHeader(true, m.layout.header.Dx()) + if m.isCompact { + m.renderHeader(true, m.layout.header.Dx()) + } else { + m.renderSidebarLogo(m.layout.sidebar.Dx()) + } } } @@ -1579,8 +1649,7 @@ func (m *UI) generateLayout(w, h int) layout { // The sidebar width sidebarWidth := 30 // The header height - // TODO: handle compact - headerHeight := 4 + const landingHeaderHeight = 4 var helpKeyMap help.KeyMap = m if m.status.ShowingAll() { @@ -1619,7 +1688,7 @@ func (m *UI) generateLayout(w, h int) layout { // ------ // help - headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(headerHeight)) + headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(landingHeaderHeight)) layout.header = headerRect layout.main = mainRect @@ -1633,7 +1702,7 @@ func (m *UI) generateLayout(w, h int) layout { // editor // ------ // help - headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(headerHeight)) + headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(landingHeaderHeight)) mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight)) // Remove extra padding from editor (but keep it for header and main) editorRect.Min.X -= 1 @@ -1643,41 +1712,52 @@ func (m *UI) generateLayout(w, h int) layout { layout.editor = editorRect case uiChat: - // Layout - // - // ------|--- - // main | - // ------| side - // editor| - // ---------- - // help - - mainRect, sideRect := uv.SplitHorizontal(appRect, uv.Fixed(appRect.Dx()-sidebarWidth)) - // Add padding left - sideRect.Min.X += 1 - mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight)) - mainRect.Max.X -= 1 // Add padding right - // Add bottom margin to main - mainRect.Max.Y -= 1 - layout.sidebar = sideRect - layout.main = mainRect - layout.editor = editorRect - - case uiChatCompact: - // Layout - // - // compact-header - // ------ - // main - // ------ - // editor - // ------ - // help - headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(appRect.Dy()-headerHeight)) - mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight)) - layout.header = headerRect - layout.main = mainRect - layout.editor = editorRect + if m.isCompact { + // Layout + // + // compact-header + // ------ + // main + // ------ + // editor + // ------ + // help + const compactHeaderHeight = 1 + headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(compactHeaderHeight)) + detailsHeight := min(sessionDetailsMaxHeight, area.Dy()-1) // One row for the header + sessionDetailsArea, _ := uv.SplitVertical(appRect, uv.Fixed(detailsHeight)) + layout.sessionDetails = sessionDetailsArea + layout.sessionDetails.Min.Y += compactHeaderHeight // adjust for header + // Add one line gap between header and main content + mainRect.Min.Y += 1 + mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight)) + mainRect.Max.X -= 1 // Add padding right + // Add bottom margin to main + mainRect.Max.Y -= 1 + layout.header = headerRect + layout.main = mainRect + layout.editor = editorRect + } else { + // Layout + // + // ------|--- + // main | + // ------| side + // editor| + // ---------- + // help + + mainRect, sideRect := uv.SplitHorizontal(appRect, uv.Fixed(appRect.Dx()-sidebarWidth)) + // Add padding left + sideRect.Min.X += 1 + mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight)) + mainRect.Max.X -= 1 // Add padding right + // Add bottom margin to main + mainRect.Max.Y -= 1 + layout.sidebar = sideRect + layout.main = mainRect + layout.editor = editorRect + } } if !layout.editor.Empty() { @@ -1711,6 +1791,9 @@ type layout struct { // status is the area for the status view. status uv.Rectangle + + // session details is the area for the session details overlay in compact mode. + sessionDetails uv.Rectangle } func (m *UI) openEditor(value string) tea.Cmd { @@ -1916,8 +1999,11 @@ func (m *UI) renderEditorView(width int) string { // renderHeader renders and caches the header logo at the specified width. func (m *UI) renderHeader(compact bool, width int) { - // TODO: handle the compact case differently - m.header = renderLogo(m.com.Styles, compact, width) + if compact && m.session != nil && m.com.App != nil { + m.header = renderCompactHeader(m.com, m.session, m.com.App.LSPClients, m.detailsOpen, width) + } else { + m.header = renderLogo(m.com.Styles, compact, width) + } } // renderSidebarLogo renders and caches the sidebar logo at the specified @@ -1939,8 +2025,13 @@ func (m *UI) sendMessage(content string, attachments []message.Attachment) tea.C return uiutil.ReportError(err) } m.state = uiChat - m.session = &newSession - cmds = append(cmds, m.loadSession(newSession.ID)) + if m.forceCompactMode { + m.isCompact = true + } + if newSession.ID != "" { + m.session = &newSession + cmds = append(cmds, m.loadSession(newSession.ID)) + } } // Capture session ID to avoid race with main goroutine updating m.session. @@ -2252,6 +2343,56 @@ func (m *UI) pasteIdx() int { return result + 1 } +// drawSessionDetails draws the session details in compact mode. +func (m *UI) drawSessionDetails(scr uv.Screen, area uv.Rectangle) { + if m.session == nil { + return + } + + s := m.com.Styles + + width := area.Dx() - s.CompactDetails.View.GetHorizontalFrameSize() + height := area.Dy() - s.CompactDetails.View.GetVerticalFrameSize() + + title := s.CompactDetails.Title.Width(width).MaxHeight(2).Render(m.session.Title) + blocks := []string{ + title, + "", + m.modelInfo(width), + "", + } + + detailsHeader := lipgloss.JoinVertical( + lipgloss.Left, + blocks..., + ) + + version := s.CompactDetails.Version.Foreground(s.Border).Width(width).AlignHorizontal(lipgloss.Right).Render(version.Version) + + remainingHeight := height - lipgloss.Height(detailsHeader) - lipgloss.Height(version) + + const maxSectionWidth = 50 + sectionWidth := min(maxSectionWidth, width/3-2) // account for 2 spaces + maxItemsPerSection := remainingHeight - 3 // Account for section title and spacing + + lspSection := m.lspInfo(sectionWidth, maxItemsPerSection, false) + mcpSection := m.mcpInfo(sectionWidth, maxItemsPerSection, false) + filesSection := m.filesInfo(m.com.Config().WorkingDir(), sectionWidth, maxItemsPerSection, false) + sections := lipgloss.JoinHorizontal(lipgloss.Top, filesSection, " ", lspSection, " ", mcpSection) + uv.NewStyledString( + s.CompactDetails.View. + Width(area.Dx()). + Render( + lipgloss.JoinVertical( + lipgloss.Left, + detailsHeader, + sections, + version, + ), + ), + ).Draw(scr, area) +} + // renderLogo renders the Crush logo with the given styles and dimensions. func renderLogo(t *styles.Styles, compact bool, width int) string { return logo.Render(version.Version, compact, logo.Opts{ diff --git a/internal/ui/styles/styles.go b/internal/ui/styles/styles.go index 1bb6648117e1413950fb8a68dc9a9b3f3b90b89d..442e3f78a449baae2c99868ae9434d69debce40e 100644 --- a/internal/ui/styles/styles.go +++ b/internal/ui/styles/styles.go @@ -69,9 +69,22 @@ type Styles struct { TagError lipgloss.Style TagInfo lipgloss.Style - // Headers - HeaderTool lipgloss.Style - HeaderToolNested lipgloss.Style + // Header + Header struct { + Charm lipgloss.Style // Style for "Charm™" label + Diagonals lipgloss.Style // Style for diagonal separators (╱) + Percentage lipgloss.Style // Style for context percentage + Keystroke lipgloss.Style // Style for keystroke hints (e.g., "ctrl+d") + KeystrokeTip lipgloss.Style // Style for keystroke action text (e.g., "open", "close") + WorkingDir lipgloss.Style // Style for current working directory + Separator lipgloss.Style // Style for separator dots (•) + } + + CompactDetails struct { + View lipgloss.Style + Version lipgloss.Style + Title lipgloss.Style + } // Panels PanelMuted lipgloss.Style @@ -995,9 +1008,18 @@ func DefaultStyles() Styles { s.TagError = s.TagBase.Background(redDark) s.TagInfo = s.TagBase.Background(blueLight) - // headers - s.HeaderTool = lipgloss.NewStyle().Foreground(blue) - s.HeaderToolNested = lipgloss.NewStyle().Foreground(fgHalfMuted) + // Compact header styles + s.Header.Charm = base.Foreground(secondary) + s.Header.Diagonals = base.Foreground(primary) + s.Header.Percentage = s.Muted + s.Header.Keystroke = s.Muted + s.Header.KeystrokeTip = s.Subtle + s.Header.WorkingDir = s.Muted + s.Header.Separator = s.Subtle + + s.CompactDetails.Title = s.Base + s.CompactDetails.View = s.Base.Padding(0, 1, 1, 1).Border(lipgloss.RoundedBorder()).BorderForeground(borderFocus) + s.CompactDetails.Version = s.Muted // panels s.PanelMuted = s.Muted.Background(bgBaseLighter)