summaryrefslogtreecommitdiffhomepage
path: root/internal/tui/components
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
parent61d9dc95111d2645a49816f6d9d6cc1014be1a22 (diff)
downloadopencode-61b605e724eb4cc50ab831534fcdd18e031d68eb.tar.gz
opencode-61b605e724eb4cc50ab831534fcdd18e031d68eb.zip
feat: themes
Diffstat (limited to 'internal/tui/components')
-rw-r--r--internal/tui/components/chat/chat.go72
-rw-r--r--internal/tui/components/chat/editor.go62
-rw-r--r--internal/tui/components/chat/list.go76
-rw-r--r--internal/tui/components/chat/message.go183
-rw-r--r--internal/tui/components/chat/sidebar.go72
-rw-r--r--internal/tui/components/core/status.go88
-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
-rw-r--r--internal/tui/components/logs/details.go28
-rw-r--r--internal/tui/components/logs/table.go7
16 files changed, 774 insertions, 357 deletions
diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go
index f4c055903..ca094ca7c 100644
--- a/internal/tui/components/chat/chat.go
+++ b/internal/tui/components/chat/chat.go
@@ -9,6 +9,7 @@ import (
"github.com/opencode-ai/opencode/internal/config"
"github.com/opencode-ai/opencode/internal/session"
"github.com/opencode-ai/opencode/internal/tui/styles"
+ "github.com/opencode-ai/opencode/internal/tui/theme"
"github.com/opencode-ai/opencode/internal/version"
)
@@ -22,12 +23,29 @@ type SessionClearedMsg struct{}
type EditorFocusMsg bool
+func header(width int) string {
+ return lipgloss.JoinVertical(
+ lipgloss.Top,
+ logo(width),
+ repo(width),
+ "",
+ cwd(width),
+ )
+}
+
func lspsConfigured(width int) string {
cfg := config.Get()
title := "LSP Configuration"
title = ansi.Truncate(title, width, "…")
- lsps := styles.BaseStyle.Width(width).Foreground(styles.PrimaryColor).Bold(true).Render(title)
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ lsps := baseStyle.
+ Width(width).
+ Foreground(t.Primary()).
+ Bold(true).
+ Render(title)
// Get LSP names and sort them for consistent ordering
var lspNames []string
@@ -39,16 +57,19 @@ func lspsConfigured(width int) string {
var lspViews []string
for _, name := range lspNames {
lsp := cfg.LSP[name]
- lspName := styles.BaseStyle.Foreground(styles.Forground).Render(
- fmt.Sprintf("• %s", name),
- )
+ lspName := baseStyle.
+ Foreground(t.Text()).
+ Render(fmt.Sprintf("• %s", name))
+
cmd := lsp.Command
cmd = ansi.Truncate(cmd, width-lipgloss.Width(lspName)-3, "…")
- lspPath := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(
- fmt.Sprintf(" (%s)", cmd),
- )
+
+ lspPath := baseStyle.
+ Foreground(t.TextMuted()).
+ Render(fmt.Sprintf(" (%s)", cmd))
+
lspViews = append(lspViews,
- styles.BaseStyle.
+ baseStyle.
Width(width).
Render(
lipgloss.JoinHorizontal(
@@ -59,7 +80,8 @@ func lspsConfigured(width int) string {
),
)
}
- return styles.BaseStyle.
+
+ return baseStyle.
Width(width).
Render(
lipgloss.JoinVertical(
@@ -75,10 +97,14 @@ func lspsConfigured(width int) string {
func logo(width int) string {
logo := fmt.Sprintf("%s %s", styles.OpenCodeIcon, "OpenCode")
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
- version := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(version.Version)
+ versionText := baseStyle.
+ Foreground(t.TextMuted()).
+ Render(version.Version)
- return styles.BaseStyle.
+ return baseStyle.
Bold(true).
Width(width).
Render(
@@ -86,34 +112,28 @@ func logo(width int) string {
lipgloss.Left,
logo,
" ",
- version,
+ versionText,
),
)
}
func repo(width int) string {
repo := "https://github.com/opencode-ai/opencode"
- return styles.BaseStyle.
- Foreground(styles.ForgroundDim).
+ t := theme.CurrentTheme()
+
+ return styles.BaseStyle().
+ Foreground(t.TextMuted()).
Width(width).
Render(repo)
}
func cwd(width int) string {
cwd := fmt.Sprintf("cwd: %s", config.WorkingDirectory())
- return styles.BaseStyle.
- Foreground(styles.ForgroundDim).
+ t := theme.CurrentTheme()
+
+ return styles.BaseStyle().
+ Foreground(t.TextMuted()).
Width(width).
Render(cwd)
}
-func header(width int) string {
- header := lipgloss.JoinVertical(
- lipgloss.Top,
- logo(width),
- repo(width),
- "",
- cwd(width),
- )
- return header
-}
diff --git a/internal/tui/components/chat/editor.go b/internal/tui/components/chat/editor.go
index 88ac3e759..3548cbb0b 100644
--- a/internal/tui/components/chat/editor.go
+++ b/internal/tui/components/chat/editor.go
@@ -10,8 +10,10 @@ import (
"github.com/charmbracelet/lipgloss"
"github.com/opencode-ai/opencode/internal/app"
"github.com/opencode-ai/opencode/internal/session"
+ "github.com/opencode-ai/opencode/internal/tui/components/dialog"
"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"
)
@@ -100,6 +102,9 @@ func (m *editorCmp) send() tea.Cmd {
func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
+ case dialog.ThemeChangedMsg:
+ m.textarea = CreateTextArea(&m.textarea)
+ return m, nil
case SessionSelectedMsg:
if msg.ID != m.session.ID {
m.session = msg
@@ -134,7 +139,13 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (m *editorCmp) View() string {
- style := lipgloss.NewStyle().Padding(0, 0, 0, 1).Bold(true)
+ t := theme.CurrentTheme()
+
+ // Style the prompt with theme colors
+ style := lipgloss.NewStyle().
+ Padding(0, 0, 0, 1).
+ Bold(true).
+ Foreground(t.Primary())
return lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"), m.textarea.View())
}
@@ -155,23 +166,42 @@ func (m *editorCmp) BindingKeys() []key.Binding {
return bindings
}
+func CreateTextArea(existing *textarea.Model) textarea.Model {
+ t := theme.CurrentTheme()
+ bgColor := t.Background()
+ textColor := t.Text()
+ textMutedColor := t.TextMuted()
+
+ ta := textarea.New()
+ ta.BlurredStyle.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor)
+ ta.BlurredStyle.CursorLine = styles.BaseStyle().Background(bgColor)
+ ta.BlurredStyle.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor)
+ ta.BlurredStyle.Text = styles.BaseStyle().Background(bgColor).Foreground(textColor)
+ ta.FocusedStyle.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor)
+ ta.FocusedStyle.CursorLine = styles.BaseStyle().Background(bgColor)
+ ta.FocusedStyle.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor)
+ ta.FocusedStyle.Text = styles.BaseStyle().Background(bgColor).Foreground(textColor)
+
+ ta.Prompt = " "
+ ta.ShowLineNumbers = false
+ ta.CharLimit = -1
+
+ if existing != nil {
+ ta.SetValue(existing.Value())
+ ta.SetWidth(existing.Width())
+ ta.SetHeight(existing.Height())
+ }
+
+ ta.Focus()
+ return ta
+}
+
func NewEditorCmp(app *app.App) tea.Model {
- ti := textarea.New()
- ti.Prompt = " "
- ti.ShowLineNumbers = false
- ti.BlurredStyle.Base = ti.BlurredStyle.Base.Background(styles.Background)
- ti.BlurredStyle.CursorLine = ti.BlurredStyle.CursorLine.Background(styles.Background)
- ti.BlurredStyle.Placeholder = ti.BlurredStyle.Placeholder.Background(styles.Background)
- ti.BlurredStyle.Text = ti.BlurredStyle.Text.Background(styles.Background)
-
- ti.FocusedStyle.Base = ti.FocusedStyle.Base.Background(styles.Background)
- ti.FocusedStyle.CursorLine = ti.FocusedStyle.CursorLine.Background(styles.Background)
- ti.FocusedStyle.Placeholder = ti.FocusedStyle.Placeholder.Background(styles.Background)
- ti.FocusedStyle.Text = ti.BlurredStyle.Text.Background(styles.Background)
- ti.CharLimit = -1
- ti.Focus()
+ ta := CreateTextArea(nil)
+
return &editorCmp{
app: app,
- textarea: ti,
+ textarea: ta,
}
}
+
diff --git a/internal/tui/components/chat/list.go b/internal/tui/components/chat/list.go
index fa7332d5f..12f1681fa 100644
--- a/internal/tui/components/chat/list.go
+++ b/internal/tui/components/chat/list.go
@@ -14,7 +14,9 @@ import (
"github.com/opencode-ai/opencode/internal/message"
"github.com/opencode-ai/opencode/internal/pubsub"
"github.com/opencode-ai/opencode/internal/session"
+ "github.com/opencode-ai/opencode/internal/tui/components/dialog"
"github.com/opencode-ai/opencode/internal/tui/styles"
+ "github.com/opencode-ai/opencode/internal/tui/theme"
"github.com/opencode-ai/opencode/internal/tui/util"
)
@@ -69,7 +71,9 @@ func (m *messagesCmp) Init() tea.Cmd {
func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
-
+ case dialog.ThemeChangedMsg:
+ m.rerender()
+ return m, nil
case SessionSelectedMsg:
if msg.ID != m.session.ID {
cmd := m.SetSession(msg)
@@ -174,6 +178,7 @@ func formatTimeDifference(unixTime1, unixTime2 int64) string {
func (m *messagesCmp) renderView() {
m.uiMessages = make([]uiMessage, 0)
pos := 0
+ baseStyle := styles.BaseStyle()
if m.width == 0 {
return
@@ -225,15 +230,13 @@ func (m *messagesCmp) renderView() {
messages := make([]string, 0)
for _, v := range m.uiMessages {
messages = append(messages, v.content,
- styles.BaseStyle.
+ baseStyle.
Width(m.width).
- Render(
- "",
- ),
+ Render(""),
)
}
m.viewport.SetContent(
- styles.BaseStyle.
+ baseStyle.
Width(m.width).
Render(
lipgloss.JoinVertical(
@@ -245,8 +248,10 @@ func (m *messagesCmp) renderView() {
}
func (m *messagesCmp) View() string {
+ baseStyle := styles.BaseStyle()
+
if m.rendering {
- return styles.BaseStyle.
+ return baseStyle.
Width(m.width).
Render(
lipgloss.JoinVertical(
@@ -258,14 +263,14 @@ func (m *messagesCmp) View() string {
)
}
if len(m.messages) == 0 {
- content := styles.BaseStyle.
+ content := baseStyle.
Width(m.width).
Height(m.height - 1).
Render(
m.initialScreen(),
)
- return styles.BaseStyle.
+ return baseStyle.
Width(m.width).
Render(
lipgloss.JoinVertical(
@@ -277,7 +282,7 @@ func (m *messagesCmp) View() string {
)
}
- return styles.BaseStyle.
+ return baseStyle.
Width(m.width).
Render(
lipgloss.JoinVertical(
@@ -328,6 +333,9 @@ func hasUnfinishedToolCalls(messages []message.Message) bool {
func (m *messagesCmp) working() string {
text := ""
if m.IsAgentWorking() && len(m.messages) > 0 {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
task := "Thinking..."
lastMessage := m.messages[len(m.messages)-1]
if hasToolsWithoutResponse(m.messages) {
@@ -338,42 +346,49 @@ func (m *messagesCmp) working() string {
task = "Generating..."
}
if task != "" {
- text += styles.BaseStyle.Width(m.width).Foreground(styles.PrimaryColor).Bold(true).Render(
- fmt.Sprintf("%s %s ", m.spinner.View(), task),
- )
+ text += baseStyle.
+ Width(m.width).
+ Foreground(t.Primary()).
+ Bold(true).
+ Render(fmt.Sprintf("%s %s ", m.spinner.View(), task))
}
}
return text
}
func (m *messagesCmp) help() string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
text := ""
if m.app.CoderAgent.IsBusy() {
text += lipgloss.JoinHorizontal(
lipgloss.Left,
- styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
- styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("esc"),
- styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to exit cancel"),
+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
+ baseStyle.Foreground(t.Text()).Bold(true).Render("esc"),
+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to exit cancel"),
)
} else {
text += lipgloss.JoinHorizontal(
lipgloss.Left,
- styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
- styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("enter"),
- styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to send the message,"),
- styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" write"),
- styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render(" \\"),
- styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" and enter to add a new line"),
+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
+ baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send the message,"),
+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" write"),
+ baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"),
+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" and enter to add a new line"),
)
}
- return styles.BaseStyle.
+ return baseStyle.
Width(m.width).
Render(text)
}
func (m *messagesCmp) initialScreen() string {
- return styles.BaseStyle.Width(m.width).Render(
+ baseStyle := styles.BaseStyle()
+
+ return baseStyle.Width(m.width).Render(
lipgloss.JoinVertical(
lipgloss.Top,
header(m.width),
@@ -383,6 +398,13 @@ func (m *messagesCmp) initialScreen() string {
)
}
+func (m *messagesCmp) rerender() {
+ for _, msg := range m.messages {
+ delete(m.cachedContent, msg.ID)
+ }
+ m.renderView()
+}
+
func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
if m.width == width && m.height == height {
return nil
@@ -391,11 +413,7 @@ func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
m.height = height
m.viewport.Width = width
m.viewport.Height = height - 2
- for _, msg := range m.messages {
- delete(m.cachedContent, msg.ID)
- }
- m.uiMessages = make([]uiMessage, 0)
- m.renderView()
+ m.rerender()
return nil
}
diff --git a/internal/tui/components/chat/message.go b/internal/tui/components/chat/message.go
index 53ec7ea3d..715e00d66 100644
--- a/internal/tui/components/chat/message.go
+++ b/internal/tui/components/chat/message.go
@@ -6,10 +6,8 @@ import (
"fmt"
"path/filepath"
"strings"
- "sync"
"time"
- "github.com/charmbracelet/glamour"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
"github.com/opencode-ai/opencode/internal/config"
@@ -19,6 +17,7 @@ import (
"github.com/opencode-ai/opencode/internal/llm/tools"
"github.com/opencode-ai/opencode/internal/message"
"github.com/opencode-ai/opencode/internal/tui/styles"
+ "github.com/opencode-ai/opencode/internal/tui/theme"
)
type uiMessageType int
@@ -31,7 +30,10 @@ const (
maxResultHeight = 10
)
-var diffStyle = diff.NewStyleConfig(diff.WithShowHeader(false), diff.WithShowHunkHeader(false))
+// getDiffWidth returns the width for the diff formatting
+func getDiffWidth(width int) int {
+ return width
+}
type uiMessage struct {
ID string
@@ -41,46 +43,37 @@ type uiMessage struct {
content string
}
-type renderCache struct {
- mutex sync.Mutex
- cache map[string][]uiMessage
-}
-
func toMarkdown(content string, focused bool, width int) string {
- r, _ := glamour.NewTermRenderer(
- glamour.WithStyles(styles.MarkdownTheme(false)),
- glamour.WithWordWrap(width),
- )
- if focused {
- r, _ = glamour.NewTermRenderer(
- glamour.WithStyles(styles.MarkdownTheme(true)),
- glamour.WithWordWrap(width),
- )
- }
+ r := styles.GetMarkdownRenderer(width)
rendered, _ := r.Render(content)
return rendered
}
func renderMessage(msg string, isUser bool, isFocused bool, width int, info ...string) string {
- style := styles.BaseStyle.
+ t := theme.CurrentTheme()
+
+ style := styles.BaseStyle().
Width(width - 1).
BorderLeft(true).
- Foreground(styles.ForgroundDim).
- BorderForeground(styles.PrimaryColor).
+ Foreground(t.TextMuted()).
+ BorderForeground(t.Primary()).
BorderStyle(lipgloss.ThickBorder())
+
if isUser {
- style = style.
- BorderForeground(styles.Blue)
+ style = style.BorderForeground(t.Secondary())
}
+
+ // Apply markdown formatting and handle background color
parts := []string{
- styles.ForceReplaceBackgroundWithLipgloss(toMarkdown(msg, isFocused, width), styles.Background),
+ styles.ForceReplaceBackgroundWithLipgloss(toMarkdown(msg, isFocused, width), t.Background()),
}
- // remove newline at the end
+ // Remove newline at the end
parts[0] = strings.TrimSuffix(parts[0], "\n")
if len(info) > 0 {
parts = append(parts, info...)
}
+
rendered := style.Render(
lipgloss.JoinVertical(
lipgloss.Left,
@@ -121,26 +114,37 @@ func renderAssistantMessage(
finishData := msg.FinishPart()
info := []string{}
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
// Add finish info if available
if finished {
switch finishData.Reason {
case message.FinishReasonEndTurn:
- took := formatTimeDifference(msg.CreatedAt, finishData.Time)
- info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
- fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took),
- ))
+ took := formatTimestampDiff(msg.CreatedAt, finishData.Time)
+ info = append(info, baseStyle.
+ Width(width-1).
+ Foreground(t.TextMuted()).
+ Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took)),
+ )
case message.FinishReasonCanceled:
- info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
- fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "canceled"),
- ))
+ info = append(info, baseStyle.
+ Width(width-1).
+ Foreground(t.TextMuted()).
+ Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "canceled")),
+ )
case message.FinishReasonError:
- info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
- fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "error"),
- ))
+ info = append(info, baseStyle.
+ Width(width-1).
+ Foreground(t.TextMuted()).
+ Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "error")),
+ )
case message.FinishReasonPermissionDenied:
- info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
- fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "permission denied"),
- ))
+ info = append(info, baseStyle.
+ Width(width-1).
+ Foreground(t.TextMuted()).
+ Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "permission denied")),
+ )
}
}
if content != "" || (finished && finishData.Reason == message.FinishReasonEndTurn) {
@@ -414,32 +418,36 @@ func truncateHeight(content string, height int) string {
}
func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, width int) string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
if response.IsError {
errContent := fmt.Sprintf("Error: %s", strings.ReplaceAll(response.Content, "\n", " "))
errContent = ansi.Truncate(errContent, width-1, "...")
- return styles.BaseStyle.
+ return baseStyle.
Width(width).
- Foreground(styles.Error).
+ Foreground(t.Error()).
Render(errContent)
}
+
resultContent := truncateHeight(response.Content, maxResultHeight)
switch toolCall.Name {
case agent.AgentToolName:
return styles.ForceReplaceBackgroundWithLipgloss(
toMarkdown(resultContent, false, width),
- styles.Background,
+ t.Background(),
)
case tools.BashToolName:
resultContent = fmt.Sprintf("```bash\n%s\n```", resultContent)
return styles.ForceReplaceBackgroundWithLipgloss(
toMarkdown(resultContent, true, width),
- styles.Background,
+ t.Background(),
)
case tools.EditToolName:
metadata := tools.EditResponseMetadata{}
json.Unmarshal([]byte(response.Metadata), &metadata)
truncDiff := truncateHeight(metadata.Diff, maxResultHeight)
- formattedDiff, _ := diff.FormatDiff(truncDiff, diff.WithTotalWidth(width), diff.WithStyle(diffStyle))
+ formattedDiff, _ := diff.FormatDiff(truncDiff, diff.WithTotalWidth(width))
return formattedDiff
case tools.FetchToolName:
var params tools.FetchParams
@@ -454,16 +462,16 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
resultContent = fmt.Sprintf("```%s\n%s\n```", mdFormat, resultContent)
return styles.ForceReplaceBackgroundWithLipgloss(
toMarkdown(resultContent, true, width),
- styles.Background,
+ t.Background(),
)
case tools.GlobToolName:
- return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent)
+ return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
case tools.GrepToolName:
- return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent)
+ return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
case tools.LSToolName:
- return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent)
+ return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
case tools.SourcegraphToolName:
- return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent)
+ return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
case tools.ViewToolName:
metadata := tools.ViewResponseMetadata{}
json.Unmarshal([]byte(response.Metadata), &metadata)
@@ -476,7 +484,7 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(metadata.Content, maxResultHeight))
return styles.ForceReplaceBackgroundWithLipgloss(
toMarkdown(resultContent, true, width),
- styles.Background,
+ t.Background(),
)
case tools.WriteToolName:
params := tools.WriteParams{}
@@ -492,13 +500,13 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(params.Content, maxResultHeight))
return styles.ForceReplaceBackgroundWithLipgloss(
toMarkdown(resultContent, true, width),
- styles.Background,
+ t.Background(),
)
default:
resultContent = fmt.Sprintf("```text\n%s\n```", resultContent)
return styles.ForceReplaceBackgroundWithLipgloss(
toMarkdown(resultContent, true, width),
- styles.Background,
+ t.Background(),
)
}
}
@@ -515,39 +523,31 @@ func renderToolMessage(
if nested {
width = width - 3
}
- style := styles.BaseStyle.
+
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ style := baseStyle.
Width(width - 1).
BorderLeft(true).
BorderStyle(lipgloss.ThickBorder()).
PaddingLeft(1).
- BorderForeground(styles.ForgroundDim)
+ BorderForeground(t.TextMuted())
response := findToolResponse(toolCall.ID, allMessages)
- toolName := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf("%s: ", toolName(toolCall.Name)))
+ toolNameText := baseStyle.Foreground(t.TextMuted()).
+ Render(fmt.Sprintf("%s: ", toolName(toolCall.Name)))
if !toolCall.Finished {
// Get a brief description of what the tool is doing
toolAction := getToolAction(toolCall.Name)
- // toolInput := strings.ReplaceAll(toolCall.Input, "\n", " ")
- // truncatedInput := toolInput
- // if len(truncatedInput) > 10 {
- // truncatedInput = truncatedInput[len(truncatedInput)-10:]
- // }
- //
- // truncatedInput = styles.BaseStyle.
- // Italic(true).
- // Width(width - 2 - lipgloss.Width(toolName)).
- // Background(styles.BackgroundDim).
- // Foreground(styles.ForgroundMid).
- // Render(truncatedInput)
-
- progressText := styles.BaseStyle.
- Width(width - 2 - lipgloss.Width(toolName)).
- Foreground(styles.ForgroundDim).
+ progressText := baseStyle.
+ Width(width - 2 - lipgloss.Width(toolNameText)).
+ Foreground(t.TextMuted()).
Render(fmt.Sprintf("%s", toolAction))
- content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolName, progressText))
+ content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, progressText))
toolMsg := uiMessage{
messageType: toolMessageType,
position: position,
@@ -556,37 +556,39 @@ func renderToolMessage(
}
return toolMsg
}
- params := renderToolParams(width-2-lipgloss.Width(toolName), toolCall)
+
+ params := renderToolParams(width-2-lipgloss.Width(toolNameText), toolCall)
responseContent := ""
if response != nil {
responseContent = renderToolResponse(toolCall, *response, width-2)
responseContent = strings.TrimSuffix(responseContent, "\n")
} else {
- responseContent = styles.BaseStyle.
+ responseContent = baseStyle.
Italic(true).
Width(width - 2).
- Foreground(styles.ForgroundDim).
+ Foreground(t.TextMuted()).
Render("Waiting for response...")
}
parts := []string{}
if !nested {
- params := styles.BaseStyle.
- Width(width - 2 - lipgloss.Width(toolName)).
- Foreground(styles.ForgroundDim).
+ formattedParams := baseStyle.
+ Width(width - 2 - lipgloss.Width(toolNameText)).
+ Foreground(t.TextMuted()).
Render(params)
- parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolName, params))
+ parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, formattedParams))
} else {
- prefix := styles.BaseStyle.
- Foreground(styles.ForgroundDim).
+ prefix := baseStyle.
+ Foreground(t.TextMuted()).
Render(" └ ")
- params := styles.BaseStyle.
- Width(width - 2 - lipgloss.Width(toolName)).
- Foreground(styles.ForgroundMid).
+ formattedParams := baseStyle.
+ Width(width - 2 - lipgloss.Width(toolNameText)).
+ Foreground(t.TextMuted()).
Render(params)
- parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolName, params))
+ parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolNameText, formattedParams))
}
+
if toolCall.Name == agent.AgentToolName {
taskMessages, _ := messagesService.List(context.Background(), toolCall.ID)
toolCalls := []message.ToolCall{}
@@ -622,3 +624,16 @@ func renderToolMessage(
}
return toolMsg
}
+
+// Helper function to format the time difference between two Unix timestamps
+func formatTimestampDiff(start, end int64) string {
+ diffSeconds := float64(end-start) / 1000.0 // Convert to seconds
+ if diffSeconds < 1 {
+ return fmt.Sprintf("%dms", int(diffSeconds*1000))
+ }
+ if diffSeconds < 60 {
+ return fmt.Sprintf("%.1fs", diffSeconds)
+ }
+ return fmt.Sprintf("%.1fm", diffSeconds/60)
+}
+
diff --git a/internal/tui/components/chat/sidebar.go b/internal/tui/components/chat/sidebar.go
index 5baac3cd4..a66249b36 100644
--- a/internal/tui/components/chat/sidebar.go
+++ b/internal/tui/components/chat/sidebar.go
@@ -14,6 +14,7 @@ import (
"github.com/opencode-ai/opencode/internal/pubsub"
"github.com/opencode-ai/opencode/internal/session"
"github.com/opencode-ai/opencode/internal/tui/styles"
+ "github.com/opencode-ai/opencode/internal/tui/theme"
)
type sidebarCmp struct {
@@ -81,7 +82,9 @@ func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (m *sidebarCmp) View() string {
- return styles.BaseStyle.
+ baseStyle := styles.BaseStyle()
+
+ return baseStyle.
Width(m.width).
PaddingLeft(4).
PaddingRight(2).
@@ -101,11 +104,19 @@ func (m *sidebarCmp) View() string {
}
func (m *sidebarCmp) sessionSection() string {
- sessionKey := styles.BaseStyle.Foreground(styles.PrimaryColor).Bold(true).Render("Session")
- sessionValue := styles.BaseStyle.
- Foreground(styles.Forground).
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ sessionKey := baseStyle.
+ Foreground(t.Primary()).
+ Bold(true).
+ Render("Session")
+
+ sessionValue := baseStyle.
+ Foreground(t.Text()).
Width(m.width - lipgloss.Width(sessionKey)).
Render(fmt.Sprintf(": %s", m.session.Title))
+
return lipgloss.JoinHorizontal(
lipgloss.Left,
sessionKey,
@@ -114,22 +125,40 @@ func (m *sidebarCmp) sessionSection() string {
}
func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
stats := ""
if additions > 0 && removals > 0 {
- additions := styles.BaseStyle.Foreground(styles.Green).PaddingLeft(1).Render(fmt.Sprintf("+%d", additions))
- removals := styles.BaseStyle.Foreground(styles.Red).PaddingLeft(1).Render(fmt.Sprintf("-%d", removals))
- content := lipgloss.JoinHorizontal(lipgloss.Left, additions, removals)
- stats = styles.BaseStyle.Width(lipgloss.Width(content)).Render(content)
+ additionsStr := baseStyle.
+ Foreground(t.Success()).
+ PaddingLeft(1).
+ Render(fmt.Sprintf("+%d", additions))
+
+ removalsStr := baseStyle.
+ Foreground(t.Error()).
+ PaddingLeft(1).
+ Render(fmt.Sprintf("-%d", removals))
+
+ content := lipgloss.JoinHorizontal(lipgloss.Left, additionsStr, removalsStr)
+ stats = baseStyle.Width(lipgloss.Width(content)).Render(content)
} else if additions > 0 {
- additions := fmt.Sprintf(" %s", styles.BaseStyle.PaddingLeft(1).Foreground(styles.Green).Render(fmt.Sprintf("+%d", additions)))
- stats = styles.BaseStyle.Width(lipgloss.Width(additions)).Render(additions)
+ additionsStr := fmt.Sprintf(" %s", baseStyle.
+ PaddingLeft(1).
+ Foreground(t.Success()).
+ Render(fmt.Sprintf("+%d", additions)))
+ stats = baseStyle.Width(lipgloss.Width(additionsStr)).Render(additionsStr)
} else if removals > 0 {
- removals := fmt.Sprintf(" %s", styles.BaseStyle.PaddingLeft(1).Foreground(styles.Red).Render(fmt.Sprintf("-%d", removals)))
- stats = styles.BaseStyle.Width(lipgloss.Width(removals)).Render(removals)
+ removalsStr := fmt.Sprintf(" %s", baseStyle.
+ PaddingLeft(1).
+ Foreground(t.Error()).
+ Render(fmt.Sprintf("-%d", removals)))
+ stats = baseStyle.Width(lipgloss.Width(removalsStr)).Render(removalsStr)
}
- filePathStr := styles.BaseStyle.Render(filePath)
- return styles.BaseStyle.
+ filePathStr := baseStyle.Render(filePath)
+
+ return baseStyle.
Width(m.width).
Render(
lipgloss.JoinHorizontal(
@@ -141,7 +170,14 @@ func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) stri
}
func (m *sidebarCmp) modifiedFiles() string {
- modifiedFiles := styles.BaseStyle.Width(m.width).Foreground(styles.PrimaryColor).Bold(true).Render("Modified Files:")
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ modifiedFiles := baseStyle.
+ Width(m.width).
+ Foreground(t.Primary()).
+ Bold(true).
+ Render("Modified Files:")
// If no modified files, show a placeholder message
if m.modFiles == nil || len(m.modFiles) == 0 {
@@ -150,13 +186,13 @@ func (m *sidebarCmp) modifiedFiles() string {
if remainingWidth > 0 {
message += strings.Repeat(" ", remainingWidth)
}
- return styles.BaseStyle.
+ return baseStyle.
Width(m.width).
Render(
lipgloss.JoinVertical(
lipgloss.Top,
modifiedFiles,
- styles.BaseStyle.Foreground(styles.ForgroundDim).Render(message),
+ baseStyle.Foreground(t.TextMuted()).Render(message),
),
)
}
@@ -175,7 +211,7 @@ func (m *sidebarCmp) modifiedFiles() string {
fileViews = append(fileViews, m.modifiedFile(path, stats.additions, stats.removals))
}
- return styles.BaseStyle.
+ return baseStyle.
Width(m.width).
Render(
lipgloss.JoinVertical(
diff --git a/internal/tui/components/core/status.go b/internal/tui/components/core/status.go
index 9fefdbabd..75fafff05 100644
--- a/internal/tui/components/core/status.go
+++ b/internal/tui/components/core/status.go
@@ -15,6 +15,7 @@ import (
"github.com/opencode-ai/opencode/internal/session"
"github.com/opencode-ai/opencode/internal/tui/components/chat"
"github.com/opencode-ai/opencode/internal/tui/styles"
+ "github.com/opencode-ai/opencode/internal/tui/theme"
"github.com/opencode-ai/opencode/internal/tui/util"
)
@@ -70,7 +71,21 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
-var helpWidget = styles.Padded.Background(styles.ForgroundMid).Foreground(styles.BackgroundDarker).Bold(true).Render("ctrl+? help")
+var helpWidget = ""
+
+// getHelpWidget returns the help widget with current theme colors
+func getHelpWidget(helpText string) string {
+ t := theme.CurrentTheme()
+ if helpText == "" {
+ helpText = "ctrl+? help"
+ }
+
+ return styles.Padded().
+ Background(t.TextMuted()).
+ Foreground(t.BackgroundDarker()).
+ Bold(true).
+ Render(helpText)
+}
func formatTokensAndCost(tokens int64, cost float64) string {
// Format tokens in human-readable format (e.g., 110K, 1.2M)
@@ -99,29 +114,38 @@ func formatTokensAndCost(tokens int64, cost float64) string {
}
func (m statusCmp) View() string {
- status := helpWidget
+ t := theme.CurrentTheme()
+
+ // Initialize the help widget
+ status := getHelpWidget("")
+
if m.session.ID != "" {
tokens := formatTokensAndCost(m.session.PromptTokens+m.session.CompletionTokens, m.session.Cost)
- tokensStyle := styles.Padded.
- Background(styles.Forground).
- Foreground(styles.BackgroundDim).
+ tokensStyle := styles.Padded().
+ Background(t.Text()).
+ Foreground(t.BackgroundSecondary()).
Render(tokens)
status += tokensStyle
}
- diagnostics := styles.Padded.Background(styles.BackgroundDarker).Render(m.projectDiagnostics())
+ diagnostics := styles.Padded().
+ Background(t.BackgroundDarker()).
+ Render(m.projectDiagnostics())
+
if m.info.Msg != "" {
- infoStyle := styles.Padded.
- Foreground(styles.Base).
+ infoStyle := styles.Padded().
+ Foreground(t.Background()).
Width(m.availableFooterMsgWidth(diagnostics))
+
switch m.info.Type {
case util.InfoTypeInfo:
- infoStyle = infoStyle.Background(styles.BorderColor)
+ infoStyle = infoStyle.Background(t.Info())
case util.InfoTypeWarn:
- infoStyle = infoStyle.Background(styles.Peach)
+ infoStyle = infoStyle.Background(t.Warning())
case util.InfoTypeError:
- infoStyle = infoStyle.Background(styles.Red)
+ infoStyle = infoStyle.Background(t.Error())
}
+
// Truncate message if it's longer than available width
msg := m.info.Msg
availWidth := m.availableFooterMsgWidth(diagnostics) - 10
@@ -130,9 +154,9 @@ func (m statusCmp) View() string {
}
status += infoStyle.Render(msg)
} else {
- status += styles.Padded.
- Foreground(styles.Base).
- Background(styles.BackgroundDim).
+ status += styles.Padded().
+ Foreground(t.Text()).
+ Background(t.BackgroundSecondary()).
Width(m.availableFooterMsgWidth(diagnostics)).
Render("")
}
@@ -143,6 +167,8 @@ func (m statusCmp) View() string {
}
func (m *statusCmp) projectDiagnostics() string {
+ t := theme.CurrentTheme()
+
// Check if any LSP server is still initializing
initializing := false
for _, client := range m.lspClients {
@@ -155,8 +181,8 @@ func (m *statusCmp) projectDiagnostics() string {
// If any server is initializing, show that status
if initializing {
return lipgloss.NewStyle().
- Background(styles.BackgroundDarker).
- Foreground(styles.Peach).
+ Background(t.BackgroundDarker()).
+ Foreground(t.Warning()).
Render(fmt.Sprintf("%s Initializing LSP...", styles.SpinnerIcon))
}
@@ -189,29 +215,29 @@ func (m *statusCmp) projectDiagnostics() string {
if len(errorDiagnostics) > 0 {
errStr := lipgloss.NewStyle().
- Background(styles.BackgroundDarker).
- Foreground(styles.Error).
+ Background(t.BackgroundDarker()).
+ Foreground(t.Error()).
Render(fmt.Sprintf("%s %d", styles.ErrorIcon, len(errorDiagnostics)))
diagnostics = append(diagnostics, errStr)
}
if len(warnDiagnostics) > 0 {
warnStr := lipgloss.NewStyle().
- Background(styles.BackgroundDarker).
- Foreground(styles.Warning).
+ Background(t.BackgroundDarker()).
+ Foreground(t.Warning()).
Render(fmt.Sprintf("%s %d", styles.WarningIcon, len(warnDiagnostics)))
diagnostics = append(diagnostics, warnStr)
}
if len(hintDiagnostics) > 0 {
hintStr := lipgloss.NewStyle().
- Background(styles.BackgroundDarker).
- Foreground(styles.Text).
+ Background(t.BackgroundDarker()).
+ Foreground(t.Text()).
Render(fmt.Sprintf("%s %d", styles.HintIcon, len(hintDiagnostics)))
diagnostics = append(diagnostics, hintStr)
}
if len(infoDiagnostics) > 0 {
infoStr := lipgloss.NewStyle().
- Background(styles.BackgroundDarker).
- Foreground(styles.Peach).
+ Background(t.BackgroundDarker()).
+ Foreground(t.Info()).
Render(fmt.Sprintf("%s %d", styles.InfoIcon, len(infoDiagnostics)))
diagnostics = append(diagnostics, infoStr)
}
@@ -230,6 +256,8 @@ func (m statusCmp) availableFooterMsgWidth(diagnostics string) int {
}
func (m statusCmp) model() string {
+ t := theme.CurrentTheme()
+
cfg := config.Get()
coder, ok := cfg.Agents[config.AgentCoder]
@@ -237,14 +265,22 @@ func (m statusCmp) model() string {
return "Unknown"
}
model := models.SupportedModels[coder.Model]
- return styles.Padded.Background(styles.Grey).Foreground(styles.Text).Render(model.Name)
+
+ return styles.Padded().
+ Background(t.Secondary()).
+ Foreground(t.Background()).
+ Render(model.Name)
}
func (m statusCmp) SetHelpMsg(s string) {
- helpWidget = styles.Padded.Background(styles.Forground).Foreground(styles.BackgroundDarker).Bold(true).Render(s)
+ // Update the help widget text using the getHelpWidget function
+ helpWidget = getHelpWidget(s)
}
func NewStatusCmp(lspClients map[string]*lsp.Client) StatusCmp {
+ // Initialize the help widget with default text
+ helpWidget = getHelpWidget("")
+
return &statusCmp{
messageTTL: 10 * time.Second,
lspClients: lspClients,
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: "",
+ }
+}
+
diff --git a/internal/tui/components/logs/details.go b/internal/tui/components/logs/details.go
index 8aaa7a41c..9d7713bbf 100644
--- a/internal/tui/components/logs/details.go
+++ b/internal/tui/components/logs/details.go
@@ -12,6 +12,7 @@ import (
"github.com/opencode-ai/opencode/internal/logging"
"github.com/opencode-ai/opencode/internal/tui/layout"
"github.com/opencode-ai/opencode/internal/tui/styles"
+ "github.com/opencode-ai/opencode/internal/tui/theme"
)
type DetailComponent interface {
@@ -49,9 +50,10 @@ func (i *detailCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (i *detailCmp) updateContent() {
var content strings.Builder
+ t := theme.CurrentTheme()
// Format the header with timestamp and level
- timeStyle := lipgloss.NewStyle().Foreground(styles.SubText0)
+ timeStyle := lipgloss.NewStyle().Foreground(t.TextMuted())
levelStyle := getLevelStyle(i.currentLog.Level)
header := lipgloss.JoinHorizontal(
@@ -65,7 +67,7 @@ func (i *detailCmp) updateContent() {
content.WriteString("\n\n")
// Message with styling
- messageStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Text)
+ messageStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text())
content.WriteString(messageStyle.Render("Message:"))
content.WriteString("\n")
content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(i.currentLog.Message))
@@ -73,13 +75,13 @@ func (i *detailCmp) updateContent() {
// Attributes section
if len(i.currentLog.Attributes) > 0 {
- attrHeaderStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Text)
+ attrHeaderStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text())
content.WriteString(attrHeaderStyle.Render("Attributes:"))
content.WriteString("\n")
// Create a table-like display for attributes
- keyStyle := lipgloss.NewStyle().Foreground(styles.Primary).Bold(true)
- valueStyle := lipgloss.NewStyle().Foreground(styles.Text)
+ keyStyle := lipgloss.NewStyle().Foreground(t.Primary()).Bold(true)
+ valueStyle := lipgloss.NewStyle().Foreground(t.Text())
for _, attr := range i.currentLog.Attributes {
attrLine := fmt.Sprintf("%s: %s",
@@ -96,23 +98,25 @@ func (i *detailCmp) updateContent() {
func getLevelStyle(level string) lipgloss.Style {
style := lipgloss.NewStyle().Bold(true)
-
+ t := theme.CurrentTheme()
+
switch strings.ToLower(level) {
case "info":
- return style.Foreground(styles.Blue)
+ return style.Foreground(t.Info())
case "warn", "warning":
- return style.Foreground(styles.Warning)
+ return style.Foreground(t.Warning())
case "error", "err":
- return style.Foreground(styles.Error)
+ return style.Foreground(t.Error())
case "debug":
- return style.Foreground(styles.Green)
+ return style.Foreground(t.Success())
default:
- return style.Foreground(styles.Text)
+ return style.Foreground(t.Text())
}
}
func (i *detailCmp) View() string {
- return styles.ForceReplaceBackgroundWithLipgloss(i.viewport.View(), styles.Background)
+ t := theme.CurrentTheme()
+ return styles.ForceReplaceBackgroundWithLipgloss(i.viewport.View(), t.Background())
}
func (i *detailCmp) GetSize() (int, int) {
diff --git a/internal/tui/components/logs/table.go b/internal/tui/components/logs/table.go
index bffa7b1ad..b9386cb73 100644
--- a/internal/tui/components/logs/table.go
+++ b/internal/tui/components/logs/table.go
@@ -11,6 +11,7 @@ import (
"github.com/opencode-ai/opencode/internal/pubsub"
"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"
)
@@ -61,7 +62,8 @@ func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (i *tableCmp) View() string {
- return styles.ForceReplaceBackgroundWithLipgloss(i.table.View(), styles.Background)
+ t := theme.CurrentTheme()
+ return styles.ForceReplaceBackgroundWithLipgloss(i.table.View(), t.Background())
}
func (i *tableCmp) GetSize() (int, int) {
@@ -114,6 +116,7 @@ func (i *tableCmp) setRows() {
}
func NewLogsTable() TableComponent {
+ t := theme.CurrentTheme()
columns := []table.Column{
{Title: "ID", Width: 4},
{Title: "Time", Width: 4},
@@ -122,7 +125,7 @@ func NewLogsTable() TableComponent {
{Title: "Attributes", Width: 10},
}
defaultStyles := table.DefaultStyles()
- defaultStyles.Selected = defaultStyles.Selected.Foreground(styles.Primary)
+ defaultStyles.Selected = defaultStyles.Selected.Foreground(t.Primary())
tableModel := table.New(
table.WithColumns(columns),
table.WithStyles(defaultStyles),