diff options
| author | adamdottv <[email protected]> | 2025-04-28 08:46:09 -0500 |
|---|---|---|
| committer | adamdottv <[email protected]> | 2025-04-30 07:46:34 -0500 |
| commit | 61b605e724eb4cc50ab831534fcdd18e031d68eb (patch) | |
| tree | b15b074a8fed1931e179a8d3df08d05b753c7aa3 /internal/tui/components/dialog | |
| parent | 61d9dc95111d2645a49816f6d9d6cc1014be1a22 (diff) | |
| download | opencode-61b605e724eb4cc50ab831534fcdd18e031d68eb.tar.gz opencode-61b605e724eb4cc50ab831534fcdd18e031d68eb.zip | |
feat: themes
Diffstat (limited to 'internal/tui/components/dialog')
| -rw-r--r-- | internal/tui/components/dialog/commands.go | 38 | ||||
| -rw-r--r-- | internal/tui/components/dialog/help.go | 48 | ||||
| -rw-r--r-- | internal/tui/components/dialog/init.go | 50 | ||||
| -rw-r--r-- | internal/tui/components/dialog/models.go | 29 | ||||
| -rw-r--r-- | internal/tui/components/dialog/permission.go | 122 | ||||
| -rw-r--r-- | internal/tui/components/dialog/quit.go | 26 | ||||
| -rw-r--r-- | internal/tui/components/dialog/session.go | 32 | ||||
| -rw-r--r-- | internal/tui/components/dialog/theme.go | 198 |
8 files changed, 399 insertions, 144 deletions
diff --git a/internal/tui/components/dialog/commands.go b/internal/tui/components/dialog/commands.go index 5a1888cd2..c725f020c 100644 --- a/internal/tui/components/dialog/commands.go +++ b/internal/tui/components/dialog/commands.go @@ -6,6 +6,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -112,11 +113,14 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (c *commandDialogCmp) View() string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + if len(c.commands) == 0 { - return styles.BaseStyle.Padding(1, 2). + return baseStyle.Padding(1, 2). Border(lipgloss.RoundedBorder()). - BorderBackground(styles.Background). - BorderForeground(styles.ForgroundDim). + BorderBackground(t.Background()). + BorderForeground(t.TextMuted()). Width(40). Render("No commands available") } @@ -154,17 +158,17 @@ func (c *commandDialogCmp) View() string { for i := startIdx; i < endIdx; i++ { cmd := c.commands[i] - itemStyle := styles.BaseStyle.Width(maxWidth) - descStyle := styles.BaseStyle.Width(maxWidth).Foreground(styles.ForgroundDim) + itemStyle := baseStyle.Width(maxWidth) + descStyle := baseStyle.Width(maxWidth).Foreground(t.TextMuted()) if i == c.selectedIdx { itemStyle = itemStyle. - Background(styles.PrimaryColor). - Foreground(styles.Background). + Background(t.Primary()). + Foreground(t.Background()). Bold(true) descStyle = descStyle. - Background(styles.PrimaryColor). - Foreground(styles.Background) + Background(t.Primary()). + Foreground(t.Background()) } title := itemStyle.Padding(0, 1).Render(cmd.Title) @@ -177,8 +181,8 @@ func (c *commandDialogCmp) View() string { } } - title := styles.BaseStyle. - Foreground(styles.PrimaryColor). + title := baseStyle. + Foreground(t.Primary()). Bold(true). Width(maxWidth). Padding(0, 1). @@ -187,15 +191,15 @@ func (c *commandDialogCmp) View() string { content := lipgloss.JoinVertical( lipgloss.Left, title, - styles.BaseStyle.Width(maxWidth).Render(""), - styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, commandItems...)), - styles.BaseStyle.Width(maxWidth).Render(""), + baseStyle.Width(maxWidth).Render(""), + baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, commandItems...)), + baseStyle.Width(maxWidth).Render(""), ) - return styles.BaseStyle.Padding(1, 2). + return baseStyle.Padding(1, 2). Border(lipgloss.RoundedBorder()). - BorderBackground(styles.Background). - BorderForeground(styles.ForgroundDim). + BorderBackground(t.Background()). + BorderForeground(t.TextMuted()). Width(lipgloss.Width(content) + 4). Render(content) } diff --git a/internal/tui/components/dialog/help.go b/internal/tui/components/dialog/help.go index ef3ab3d78..1f161c7d2 100644 --- a/internal/tui/components/dialog/help.go +++ b/internal/tui/components/dialog/help.go @@ -7,6 +7,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" ) type helpCmp struct { @@ -53,10 +54,21 @@ func removeDuplicateBindings(bindings []key.Binding) []key.Binding { } func (h *helpCmp) render() string { - helpKeyStyle := styles.Bold.Background(styles.Background).Foreground(styles.Forground).Padding(0, 1, 0, 0) - helpDescStyle := styles.Regular.Background(styles.Background).Foreground(styles.ForgroundMid) + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + helpKeyStyle := styles.Bold(). + Background(t.Background()). + Foreground(t.Text()). + Padding(0, 1, 0, 0) + + helpDescStyle := styles.Regular(). + Background(t.Background()). + Foreground(t.TextMuted()) + // Compile list of bindings to render bindings := removeDuplicateBindings(h.keys) + // Enumerate through each group of bindings, populating a series of // pairs of columns, one for keys, one for descriptions var ( @@ -64,6 +76,7 @@ func (h *helpCmp) render() string { width int rows = 10 - 2 ) + for i := 0; i < len(bindings); i += rows { var ( keys []string @@ -73,11 +86,12 @@ func (h *helpCmp) render() string { keys = append(keys, helpKeyStyle.Render(bindings[j].Help().Key)) descs = append(descs, helpDescStyle.Render(bindings[j].Help().Desc)) } + // Render pair of columns; beyond the first pair, render a three space // left margin, in order to visually separate the pairs. var cols []string if len(pairs) > 0 { - cols = []string{styles.BaseStyle.Render(" ")} + cols = []string{baseStyle.Render(" ")} } maxDescWidth := 0 @@ -89,7 +103,7 @@ func (h *helpCmp) render() string { for i := range descs { remainingWidth := maxDescWidth - lipgloss.Width(descs[i]) if remainingWidth > 0 { - descs[i] = descs[i] + styles.BaseStyle.Render(strings.Repeat(" ", remainingWidth)) + descs[i] = descs[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth)) } } maxKeyWidth := 0 @@ -101,7 +115,7 @@ func (h *helpCmp) render() string { for i := range keys { remainingWidth := maxKeyWidth - lipgloss.Width(keys[i]) if remainingWidth > 0 { - keys[i] = keys[i] + styles.BaseStyle.Render(strings.Repeat(" ", remainingWidth)) + keys[i] = keys[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth)) } } @@ -110,7 +124,7 @@ func (h *helpCmp) render() string { strings.Join(descs, "\n"), ) - pair := styles.BaseStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, cols...)) + pair := baseStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, cols...)) // check whether it exceeds the maximum width avail (the width of the // terminal, subtracting 2 for the borders). width += lipgloss.Width(pair) @@ -130,9 +144,9 @@ func (h *helpCmp) render() string { lipgloss.Left, // x lipgloss.Top, // y lastPair, // content - lipgloss.WithWhitespaceBackground(styles.Background), // background + lipgloss.WithWhitespaceBackground(t.Background()), )) - content := styles.BaseStyle.Width(h.width).Render( + content := baseStyle.Width(h.width).Render( lipgloss.JoinHorizontal( lipgloss.Top, prefix..., @@ -140,8 +154,9 @@ func (h *helpCmp) render() string { ) return content } + // Join pairs of columns and enclose in a border - content := styles.BaseStyle.Width(h.width).Render( + content := baseStyle.Width(h.width).Render( lipgloss.JoinHorizontal( lipgloss.Top, pairs..., @@ -151,22 +166,25 @@ func (h *helpCmp) render() string { } func (h *helpCmp) View() string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + content := h.render() - header := styles.BaseStyle. + header := baseStyle. Bold(true). Width(lipgloss.Width(content)). - Foreground(styles.PrimaryColor). + Foreground(t.Primary()). Render("Keyboard Shortcuts") - return styles.BaseStyle.Padding(1). + return baseStyle.Padding(1). Border(lipgloss.RoundedBorder()). - BorderForeground(styles.ForgroundDim). + BorderForeground(t.TextMuted()). Width(h.width). - BorderBackground(styles.Background). + BorderBackground(t.Background()). Render( lipgloss.JoinVertical(lipgloss.Center, header, - styles.BaseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))), + baseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))), content, ), ) diff --git a/internal/tui/components/dialog/init.go b/internal/tui/components/dialog/init.go index bfe2323fd..77c76584d 100644 --- a/internal/tui/components/dialog/init.go +++ b/internal/tui/components/dialog/init.go @@ -6,6 +6,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -92,55 +93,58 @@ func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // View implements tea.Model. func (m InitDialogCmp) View() string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + // Calculate width needed for content maxWidth := 60 // Width for explanation text - title := styles.BaseStyle. - Foreground(styles.PrimaryColor). + title := baseStyle. + Foreground(t.Primary()). Bold(true). Width(maxWidth). Padding(0, 1). Render("Initialize Project") - explanation := styles.BaseStyle. - Foreground(styles.Forground). + explanation := baseStyle. + Foreground(t.Text()). Width(maxWidth). Padding(0, 1). Render("Initialization generates a new OpenCode.md file that contains information about your codebase, this file serves as memory for each project, you can freely add to it to help the agents be better at their job.") - question := styles.BaseStyle. - Foreground(styles.Forground). + question := baseStyle. + Foreground(t.Text()). Width(maxWidth). Padding(1, 1). Render("Would you like to initialize this project?") maxWidth = min(maxWidth, m.width-10) - yesStyle := styles.BaseStyle - noStyle := styles.BaseStyle + yesStyle := baseStyle + noStyle := baseStyle if m.selected == 0 { yesStyle = yesStyle. - Background(styles.PrimaryColor). - Foreground(styles.Background). + Background(t.Primary()). + Foreground(t.Background()). Bold(true) noStyle = noStyle. - Background(styles.Background). - Foreground(styles.PrimaryColor) + Background(t.Background()). + Foreground(t.Primary()) } else { noStyle = noStyle. - Background(styles.PrimaryColor). - Foreground(styles.Background). + Background(t.Primary()). + Foreground(t.Background()). Bold(true) yesStyle = yesStyle. - Background(styles.Background). - Foreground(styles.PrimaryColor) + Background(t.Background()). + Foreground(t.Primary()) } yes := yesStyle.Padding(0, 3).Render("Yes") no := noStyle.Padding(0, 3).Render("No") - buttons := lipgloss.JoinHorizontal(lipgloss.Center, yes, styles.BaseStyle.Render(" "), no) - buttons = styles.BaseStyle. + buttons := lipgloss.JoinHorizontal(lipgloss.Center, yes, baseStyle.Render(" "), no) + buttons = baseStyle. Width(maxWidth). Padding(1, 0). Render(buttons) @@ -148,17 +152,17 @@ func (m InitDialogCmp) View() string { content := lipgloss.JoinVertical( lipgloss.Left, title, - styles.BaseStyle.Width(maxWidth).Render(""), + baseStyle.Width(maxWidth).Render(""), explanation, question, buttons, - styles.BaseStyle.Width(maxWidth).Render(""), + baseStyle.Width(maxWidth).Render(""), ) - return styles.BaseStyle.Padding(1, 2). + return baseStyle.Padding(1, 2). Border(lipgloss.RoundedBorder()). - BorderBackground(styles.Background). - BorderForeground(styles.ForgroundDim). + BorderBackground(t.Background()). + BorderForeground(t.TextMuted()). Width(lipgloss.Width(content) + 4). Render(content) } diff --git a/internal/tui/components/dialog/models.go b/internal/tui/components/dialog/models.go index d10d5c8cc..48b7ce03f 100644 --- a/internal/tui/components/dialog/models.go +++ b/internal/tui/components/dialog/models.go @@ -12,6 +12,7 @@ import ( "github.com/opencode-ai/opencode/internal/llm/models" "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -185,10 +186,13 @@ func (m *modelDialogCmp) switchProvider(offset int) { } func (m *modelDialogCmp) View() string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + // Capitalize first letter of provider name providerName := strings.ToUpper(string(m.provider)[:1]) + string(m.provider[1:]) - title := styles.BaseStyle. - Foreground(styles.PrimaryColor). + title := baseStyle. + Foreground(t.Primary()). Bold(true). Width(maxDialogWidth). Padding(0, 0, 1). @@ -199,10 +203,10 @@ func (m *modelDialogCmp) View() string { modelItems := make([]string, 0, endIdx-m.scrollOffset) for i := m.scrollOffset; i < endIdx; i++ { - itemStyle := styles.BaseStyle.Width(maxDialogWidth) + itemStyle := baseStyle.Width(maxDialogWidth) if i == m.selectedIdx { - itemStyle = itemStyle.Background(styles.PrimaryColor). - Foreground(styles.Background).Bold(true) + itemStyle = itemStyle.Background(t.Primary()). + Foreground(t.Background()).Bold(true) } modelItems = append(modelItems, itemStyle.Render(m.models[i].Name)) } @@ -212,14 +216,14 @@ func (m *modelDialogCmp) View() string { content := lipgloss.JoinVertical( lipgloss.Left, title, - styles.BaseStyle.Width(maxDialogWidth).Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)), + baseStyle.Width(maxDialogWidth).Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)), scrollIndicator, ) - return styles.BaseStyle.Padding(1, 2). + return baseStyle.Padding(1, 2). Border(lipgloss.RoundedBorder()). - BorderBackground(styles.Background). - BorderForeground(styles.ForgroundDim). + BorderBackground(t.Background()). + BorderForeground(t.TextMuted()). Width(lipgloss.Width(content) + 4). Render(content) } @@ -249,8 +253,11 @@ func (m *modelDialogCmp) getScrollIndicators(maxWidth int) string { return "" } - return styles.BaseStyle. - Foreground(styles.PrimaryColor). + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + return baseStyle. + Foreground(t.Primary()). Width(maxWidth). Align(lipgloss.Right). Bold(true). diff --git a/internal/tui/components/dialog/permission.go b/internal/tui/components/dialog/permission.go index 1a1e0783b..fb12a2cd5 100644 --- a/internal/tui/components/dialog/permission.go +++ b/internal/tui/components/dialog/permission.go @@ -7,13 +7,13 @@ import ( "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/glamour" "github.com/charmbracelet/lipgloss" "github.com/opencode-ai/opencode/internal/diff" "github.com/opencode-ai/opencode/internal/llm/tools" "github.com/opencode-ai/opencode/internal/permission" "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -149,25 +149,28 @@ func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd { } func (p *permissionDialogCmp) renderButtons() string { - allowStyle := styles.BaseStyle - allowSessionStyle := styles.BaseStyle - denyStyle := styles.BaseStyle - spacerStyle := styles.BaseStyle.Background(styles.Background) + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + allowStyle := baseStyle + allowSessionStyle := baseStyle + denyStyle := baseStyle + spacerStyle := baseStyle.Background(t.Background()) // Style the selected button switch p.selectedOption { case 0: - allowStyle = allowStyle.Background(styles.PrimaryColor).Foreground(styles.Background) - allowSessionStyle = allowSessionStyle.Background(styles.Background).Foreground(styles.PrimaryColor) - denyStyle = denyStyle.Background(styles.Background).Foreground(styles.PrimaryColor) + allowStyle = allowStyle.Background(t.Primary()).Foreground(t.Background()) + allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary()) + denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary()) case 1: - allowStyle = allowStyle.Background(styles.Background).Foreground(styles.PrimaryColor) - allowSessionStyle = allowSessionStyle.Background(styles.PrimaryColor).Foreground(styles.Background) - denyStyle = denyStyle.Background(styles.Background).Foreground(styles.PrimaryColor) + allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary()) + allowSessionStyle = allowSessionStyle.Background(t.Primary()).Foreground(t.Background()) + denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary()) case 2: - allowStyle = allowStyle.Background(styles.Background).Foreground(styles.PrimaryColor) - allowSessionStyle = allowSessionStyle.Background(styles.Background).Foreground(styles.PrimaryColor) - denyStyle = denyStyle.Background(styles.PrimaryColor).Foreground(styles.Background) + allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary()) + allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary()) + denyStyle = denyStyle.Background(t.Primary()).Foreground(t.Background()) } allowButton := allowStyle.Padding(0, 1).Render("Allow (a)") @@ -192,15 +195,18 @@ func (p *permissionDialogCmp) renderButtons() string { } func (p *permissionDialogCmp) renderHeader() string { - toolKey := styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("Tool") - toolValue := styles.BaseStyle. - Foreground(styles.Forground). + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + toolKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Tool") + toolValue := baseStyle. + Foreground(t.Text()). Width(p.width - lipgloss.Width(toolKey)). Render(fmt.Sprintf(": %s", p.permission.ToolName)) - pathKey := styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("Path") - pathValue := styles.BaseStyle. - Foreground(styles.Forground). + pathKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Path") + pathValue := baseStyle. + Foreground(t.Text()). Width(p.width - lipgloss.Width(pathKey)). Render(fmt.Sprintf(": %s", p.permission.Path)) @@ -210,45 +216,45 @@ func (p *permissionDialogCmp) renderHeader() string { toolKey, toolValue, ), - styles.BaseStyle.Render(strings.Repeat(" ", p.width)), + baseStyle.Render(strings.Repeat(" ", p.width)), lipgloss.JoinHorizontal( lipgloss.Left, pathKey, pathValue, ), - styles.BaseStyle.Render(strings.Repeat(" ", p.width)), + baseStyle.Render(strings.Repeat(" ", p.width)), } // Add tool-specific header information switch p.permission.ToolName { case tools.BashToolName: - headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Command")) + headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Command")) case tools.EditToolName: - headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Diff")) + headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Diff")) case tools.WriteToolName: - headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Diff")) + headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Diff")) case tools.FetchToolName: - headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("URL")) + headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("URL")) } - return lipgloss.NewStyle().Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...)) + return lipgloss.NewStyle().Background(t.Background()).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...)) } func (p *permissionDialogCmp) renderBashContent() string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok { content := fmt.Sprintf("```bash\n%s\n```", pr.Command) // Use the cache for markdown rendering renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) { - r, _ := glamour.NewTermRenderer( - glamour.WithStyles(styles.MarkdownTheme(true)), - glamour.WithWordWrap(p.width-10), - ) + r := styles.GetMarkdownRenderer(p.width-10) s, err := r.Render(content) - return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err + return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err }) - finalContent := styles.BaseStyle. + finalContent := baseStyle. Width(p.contentViewPort.Width). Render(renderedContent) p.contentViewPort.SetContent(finalContent) @@ -295,39 +301,45 @@ func (p *permissionDialogCmp) renderWriteContent() string { } func (p *permissionDialogCmp) renderFetchContent() string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok { content := fmt.Sprintf("```bash\n%s\n```", pr.URL) // Use the cache for markdown rendering renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) { - r, _ := glamour.NewTermRenderer( - glamour.WithStyles(styles.MarkdownTheme(true)), - glamour.WithWordWrap(p.width-10), - ) + r := styles.GetMarkdownRenderer(p.width-10) s, err := r.Render(content) - return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err + return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err }) - p.contentViewPort.SetContent(renderedContent) + finalContent := baseStyle. + Width(p.contentViewPort.Width). + Render(renderedContent) + p.contentViewPort.SetContent(finalContent) return p.styleViewport() } return "" } func (p *permissionDialogCmp) renderDefaultContent() string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + content := p.permission.Description // Use the cache for markdown rendering renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) { - r, _ := glamour.NewTermRenderer( - glamour.WithStyles(styles.CatppuccinMarkdownStyle()), - glamour.WithWordWrap(p.width-10), - ) + r := styles.GetMarkdownRenderer(p.width-10) s, err := r.Render(content) - return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err + return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err }) - p.contentViewPort.SetContent(renderedContent) + finalContent := baseStyle. + Width(p.contentViewPort.Width). + Render(renderedContent) + p.contentViewPort.SetContent(finalContent) if renderedContent == "" { return "" @@ -337,17 +349,21 @@ func (p *permissionDialogCmp) renderDefaultContent() string { } func (p *permissionDialogCmp) styleViewport() string { + t := theme.CurrentTheme() contentStyle := lipgloss.NewStyle(). - Background(styles.Background) + Background(t.Background()) return contentStyle.Render(p.contentViewPort.View()) } func (p *permissionDialogCmp) render() string { - title := styles.BaseStyle. + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + title := baseStyle. Bold(true). Width(p.width - 4). - Foreground(styles.PrimaryColor). + Foreground(t.Primary()). Render("Permission Required") // Render header headerContent := p.renderHeader() @@ -378,18 +394,18 @@ func (p *permissionDialogCmp) render() string { content := lipgloss.JoinVertical( lipgloss.Top, title, - styles.BaseStyle.Render(strings.Repeat(" ", lipgloss.Width(title))), + baseStyle.Render(strings.Repeat(" ", lipgloss.Width(title))), headerContent, contentFinal, buttons, - styles.BaseStyle.Render(strings.Repeat(" ", p.width-4)), + baseStyle.Render(strings.Repeat(" ", p.width-4)), ) - return styles.BaseStyle. + return baseStyle. Padding(1, 0, 0, 1). Border(lipgloss.RoundedBorder()). - BorderBackground(styles.Background). - BorderForeground(styles.ForgroundDim). + BorderBackground(t.Background()). + BorderForeground(t.TextMuted()). Width(p.width). Height(p.height). Render( diff --git a/internal/tui/components/dialog/quit.go b/internal/tui/components/dialog/quit.go index 38c7dc1a1..f755fa272 100644 --- a/internal/tui/components/dialog/quit.go +++ b/internal/tui/components/dialog/quit.go @@ -8,6 +8,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -81,16 +82,19 @@ func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (q *quitDialogCmp) View() string { - yesStyle := styles.BaseStyle - noStyle := styles.BaseStyle - spacerStyle := styles.BaseStyle.Background(styles.Background) + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + yesStyle := baseStyle + noStyle := baseStyle + spacerStyle := baseStyle.Background(t.Background()) if q.selectedNo { - noStyle = noStyle.Background(styles.PrimaryColor).Foreground(styles.Background) - yesStyle = yesStyle.Background(styles.Background).Foreground(styles.PrimaryColor) + noStyle = noStyle.Background(t.Primary()).Foreground(t.Background()) + yesStyle = yesStyle.Background(t.Background()).Foreground(t.Primary()) } else { - yesStyle = yesStyle.Background(styles.PrimaryColor).Foreground(styles.Background) - noStyle = noStyle.Background(styles.Background).Foreground(styles.PrimaryColor) + yesStyle = yesStyle.Background(t.Primary()).Foreground(t.Background()) + noStyle = noStyle.Background(t.Background()).Foreground(t.Primary()) } yesButton := yesStyle.Padding(0, 1).Render("Yes") @@ -104,7 +108,7 @@ func (q *quitDialogCmp) View() string { buttons = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + buttons } - content := styles.BaseStyle.Render( + content := baseStyle.Render( lipgloss.JoinVertical( lipgloss.Center, question, @@ -113,10 +117,10 @@ func (q *quitDialogCmp) View() string { ), ) - return styles.BaseStyle.Padding(1, 2). + return baseStyle.Padding(1, 2). Border(lipgloss.RoundedBorder()). - BorderBackground(styles.Background). - BorderForeground(styles.ForgroundDim). + BorderBackground(t.Background()). + BorderForeground(t.TextMuted()). Width(lipgloss.Width(content) + 4). Render(content) } diff --git a/internal/tui/components/dialog/session.go b/internal/tui/components/dialog/session.go index 90a07358f..a29fa7131 100644 --- a/internal/tui/components/dialog/session.go +++ b/internal/tui/components/dialog/session.go @@ -7,6 +7,7 @@ import ( "github.com/opencode-ai/opencode/internal/session" "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -105,11 +106,14 @@ func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (s *sessionDialogCmp) View() string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + if len(s.sessions) == 0 { - return styles.BaseStyle.Padding(1, 2). + return baseStyle.Padding(1, 2). Border(lipgloss.RoundedBorder()). - BorderBackground(styles.Background). - BorderForeground(styles.ForgroundDim). + BorderBackground(t.Background()). + BorderForeground(t.TextMuted()). Width(40). Render("No sessions available") } @@ -146,20 +150,20 @@ func (s *sessionDialogCmp) View() string { for i := startIdx; i < endIdx; i++ { sess := s.sessions[i] - itemStyle := styles.BaseStyle.Width(maxWidth) + itemStyle := baseStyle.Width(maxWidth) if i == s.selectedIdx { itemStyle = itemStyle. - Background(styles.PrimaryColor). - Foreground(styles.Background). + Background(t.Primary()). + Foreground(t.Background()). Bold(true) } sessionItems = append(sessionItems, itemStyle.Padding(0, 1).Render(sess.Title)) } - title := styles.BaseStyle. - Foreground(styles.PrimaryColor). + title := baseStyle. + Foreground(t.Primary()). Bold(true). Width(maxWidth). Padding(0, 1). @@ -168,15 +172,15 @@ func (s *sessionDialogCmp) View() string { content := lipgloss.JoinVertical( lipgloss.Left, title, - styles.BaseStyle.Width(maxWidth).Render(""), - styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)), - styles.BaseStyle.Width(maxWidth).Render(""), + baseStyle.Width(maxWidth).Render(""), + baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)), + baseStyle.Width(maxWidth).Render(""), ) - return styles.BaseStyle.Padding(1, 2). + return baseStyle.Padding(1, 2). Border(lipgloss.RoundedBorder()). - BorderBackground(styles.Background). - BorderForeground(styles.ForgroundDim). + BorderBackground(t.Background()). + BorderForeground(t.TextMuted()). Width(lipgloss.Width(content) + 4). Render(content) } diff --git a/internal/tui/components/dialog/theme.go b/internal/tui/components/dialog/theme.go new file mode 100644 index 000000000..d35d3e2b6 --- /dev/null +++ b/internal/tui/components/dialog/theme.go @@ -0,0 +1,198 @@ +package dialog + +import ( + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/opencode-ai/opencode/internal/tui/layout" + "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" + "github.com/opencode-ai/opencode/internal/tui/util" +) + +// ThemeChangedMsg is sent when the theme is changed +type ThemeChangedMsg struct { + ThemeName string +} + +// CloseThemeDialogMsg is sent when the theme dialog is closed +type CloseThemeDialogMsg struct{} + +// ThemeDialog interface for the theme switching dialog +type ThemeDialog interface { + tea.Model + layout.Bindings +} + +type themeDialogCmp struct { + themes []string + selectedIdx int + width int + height int + currentTheme string +} + +type themeKeyMap struct { + Up key.Binding + Down key.Binding + Enter key.Binding + Escape key.Binding + J key.Binding + K key.Binding +} + +var themeKeys = themeKeyMap{ + Up: key.NewBinding( + key.WithKeys("up"), + key.WithHelp("↑", "previous theme"), + ), + Down: key.NewBinding( + key.WithKeys("down"), + key.WithHelp("↓", "next theme"), + ), + Enter: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "select theme"), + ), + Escape: key.NewBinding( + key.WithKeys("esc"), + key.WithHelp("esc", "close"), + ), + J: key.NewBinding( + key.WithKeys("j"), + key.WithHelp("j", "next theme"), + ), + K: key.NewBinding( + key.WithKeys("k"), + key.WithHelp("k", "previous theme"), + ), +} + +func (t *themeDialogCmp) Init() tea.Cmd { + // Load available themes and update selectedIdx based on current theme + t.themes = theme.AvailableThemes() + t.currentTheme = theme.CurrentThemeName() + + // Find the current theme in the list + for i, name := range t.themes { + if name == t.currentTheme { + t.selectedIdx = i + break + } + } + + return nil +} + +func (t *themeDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, themeKeys.Up) || key.Matches(msg, themeKeys.K): + if t.selectedIdx > 0 { + t.selectedIdx-- + } + return t, nil + case key.Matches(msg, themeKeys.Down) || key.Matches(msg, themeKeys.J): + if t.selectedIdx < len(t.themes)-1 { + t.selectedIdx++ + } + return t, nil + case key.Matches(msg, themeKeys.Enter): + if len(t.themes) > 0 { + previousTheme := theme.CurrentThemeName() + selectedTheme := t.themes[t.selectedIdx] + if previousTheme == selectedTheme { + return t, util.CmdHandler(CloseThemeDialogMsg{}) + } + if err := theme.SetTheme(selectedTheme); err != nil { + return t, util.ReportError(err) + } + return t, util.CmdHandler(ThemeChangedMsg{ + ThemeName: selectedTheme, + }) + } + case key.Matches(msg, themeKeys.Escape): + return t, util.CmdHandler(CloseThemeDialogMsg{}) + } + case tea.WindowSizeMsg: + t.width = msg.Width + t.height = msg.Height + } + return t, nil +} + +func (t *themeDialogCmp) View() string { + currentTheme := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + if len(t.themes) == 0 { + return baseStyle.Padding(1, 2). + Border(lipgloss.RoundedBorder()). + BorderBackground(currentTheme.Background()). + BorderForeground(currentTheme.TextMuted()). + Width(40). + Render("No themes available") + } + + // Calculate max width needed for theme names + maxWidth := 40 // Minimum width + for _, themeName := range t.themes { + if len(themeName) > maxWidth-4 { // Account for padding + maxWidth = len(themeName) + 4 + } + } + + maxWidth = max(30, min(maxWidth, t.width-15)) // Limit width to avoid overflow + + // Build the theme list + themeItems := make([]string, 0, len(t.themes)) + for i, themeName := range t.themes { + itemStyle := baseStyle.Width(maxWidth) + + if i == t.selectedIdx { + itemStyle = itemStyle. + Background(currentTheme.Primary()). + Foreground(currentTheme.Background()). + Bold(true) + } + + themeItems = append(themeItems, itemStyle.Padding(0, 1).Render(themeName)) + } + + title := baseStyle. + Foreground(currentTheme.Primary()). + Bold(true). + Width(maxWidth). + Padding(0, 1). + Render("Select Theme") + + content := lipgloss.JoinVertical( + lipgloss.Left, + title, + baseStyle.Width(maxWidth).Render(""), + baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, themeItems...)), + baseStyle.Width(maxWidth).Render(""), + ) + + return baseStyle.Padding(1, 2). + Border(lipgloss.RoundedBorder()). + BorderBackground(currentTheme.Background()). + BorderForeground(currentTheme.TextMuted()). + Width(lipgloss.Width(content) + 4). + Render(content) +} + +func (t *themeDialogCmp) BindingKeys() []key.Binding { + return layout.KeyMapToSlice(themeKeys) +} + +// NewThemeDialogCmp creates a new theme switching dialog +func NewThemeDialogCmp() ThemeDialog { + return &themeDialogCmp{ + themes: []string{}, + selectedIdx: 0, + currentTheme: "", + } +} + |
