summaryrefslogtreecommitdiffhomepage
path: root/internal/tui/components/dialog
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-04-28 08:46:09 -0500
committeradamdottv <[email protected]>2025-04-30 07:46:34 -0500
commit61b605e724eb4cc50ab831534fcdd18e031d68eb (patch)
treeb15b074a8fed1931e179a8d3df08d05b753c7aa3 /internal/tui/components/dialog
parent61d9dc95111d2645a49816f6d9d6cc1014be1a22 (diff)
downloadopencode-61b605e724eb4cc50ab831534fcdd18e031d68eb.tar.gz
opencode-61b605e724eb4cc50ab831534fcdd18e031d68eb.zip
feat: themes
Diffstat (limited to 'internal/tui/components/dialog')
-rw-r--r--internal/tui/components/dialog/commands.go38
-rw-r--r--internal/tui/components/dialog/help.go48
-rw-r--r--internal/tui/components/dialog/init.go50
-rw-r--r--internal/tui/components/dialog/models.go29
-rw-r--r--internal/tui/components/dialog/permission.go122
-rw-r--r--internal/tui/components/dialog/quit.go26
-rw-r--r--internal/tui/components/dialog/session.go32
-rw-r--r--internal/tui/components/dialog/theme.go198
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: "",
+ }
+}
+