From dfd23784fc26989944085cc2398e661d6a954011 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Wed, 4 Jun 2025 13:12:55 +0200 Subject: [PATCH] more theme cleanup --- cspell.json | 2 +- internal/tui/components/chat/chat.go | 4 - internal/tui/components/chat/editor/editor.go | 12 +- .../tui/components/chat/messages/messages.go | 19 +- .../tui/components/chat/messages/renderer.go | 40 ++- internal/tui/components/chat/messages/tool.go | 15 +- .../tui/components/completions/completions.go | 7 +- internal/tui/components/dialog/filepicker.go | 32 +-- internal/tui/components/dialog/help.go | 203 -------------- internal/tui/components/dialog/init.go | 32 +-- internal/tui/components/dialog/permission.go | 90 +++--- internal/tui/components/dialog/theme.go | 201 -------------- .../components/dialogs/commands/arguments.go | 49 ++-- internal/tui/components/dialogs/quit/quit.go | 24 +- internal/tui/layout/overlay.go | 169 ----------- internal/tui/page/logs.go | 2 +- internal/tui/styles/markdown.go | 262 +----------------- internal/tui/styles/styles.go | 155 ----------- 18 files changed, 134 insertions(+), 1184 deletions(-) delete mode 100644 internal/tui/components/dialog/help.go delete mode 100644 internal/tui/components/dialog/theme.go delete mode 100644 internal/tui/layout/overlay.go delete mode 100644 internal/tui/styles/styles.go diff --git a/cspell.json b/cspell.json index c2fdb29fd8f1b777049f2df44c43633a16384245..7a440d8fbdf07a8d8274c707710d1d930dabe787 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"words":["opencode","charmbracelet","lipgloss","bubbletea","textinput","Focusable","lsps"],"version":"0.2","language":"en","flagWords":[]} \ No newline at end of file +{"words":["opencode","charmbracelet","lipgloss","bubbletea","textinput","Focusable","lsps","Sourcegraph"],"version":"0.2","language":"en","flagWords":[]} \ No newline at end of file diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go index 26d5c1c4af70babc614c4709a4e177ef883eb8b8..4abff286babc4c3609371fc084567f7c96d91cd3 100644 --- a/internal/tui/components/chat/chat.go +++ b/internal/tui/components/chat/chat.go @@ -14,7 +14,6 @@ import ( "github.com/opencode-ai/opencode/internal/session" "github.com/opencode-ai/opencode/internal/tui/components/chat/messages" "github.com/opencode-ai/opencode/internal/tui/components/core/list" - "github.com/opencode-ai/opencode/internal/tui/components/dialog" "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -87,9 +86,6 @@ func (m *messageListCmp) Init() tea.Cmd { // Update handles incoming messages and updates the component state. func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case dialog.ThemeChangedMsg: - m.listCmp.ResetView() - return m, nil case SessionSelectedMsg: if msg.ID != m.session.ID { cmd := m.SetSession(msg) diff --git a/internal/tui/components/chat/editor/editor.go b/internal/tui/components/chat/editor/editor.go index c8bf567ad4cab7248f936e1e146ccf4174011a59..e9e565daade4c855c9a50fa7f84cfd0a07d2b016 100644 --- a/internal/tui/components/chat/editor/editor.go +++ b/internal/tui/components/chat/editor/editor.go @@ -22,7 +22,6 @@ import ( "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" ) @@ -138,9 +137,6 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd switch msg := msg.(type) { - case dialog.ThemeChangedMsg: - m.textarea = CreateTextArea(&m.textarea) - return m, cmd case chat.SessionSelectedMsg: if msg.ID != m.session.ID { m.session = msg @@ -300,11 +296,11 @@ func (m *editorCmp) GetSize() (int, int) { func (m *editorCmp) attachmentsContent() string { var styledAttachments []string - t := theme.CurrentTheme() - attachmentStyles := styles.BaseStyle(). + t := styles.CurrentTheme() + attachmentStyles := t.S().Base. MarginLeft(1). - Background(t.TextMuted()). - Foreground(t.Text()) + Background(t.FgMuted). + Foreground(t.FgBase) for i, attachment := range m.attachments { var filename string if len(attachment.FileName) > 10 { diff --git a/internal/tui/components/chat/messages/messages.go b/internal/tui/components/chat/messages/messages.go index f5420bacf923ee2cae5e18ea90c20d7950f36d3e..647db5595978fc44b44d82e4bcf54fddaaebe3f6 100644 --- a/internal/tui/components/chat/messages/messages.go +++ b/internal/tui/components/chat/messages/messages.go @@ -16,7 +16,6 @@ import ( "github.com/opencode-ai/opencode/internal/tui/components/anim" "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" ) @@ -124,7 +123,7 @@ func (m *messageCmp) textWidth() int { // style returns the lipgloss style for the message component. // Applies different border colors and styles based on message role and focus state. func (msg *messageCmp) style() lipgloss.Style { - t := theme.CurrentTheme() + t := styles.CurrentTheme() var borderColor color.Color borderStyle := lipgloss.NormalBorder() if msg.focused { @@ -133,17 +132,16 @@ func (msg *messageCmp) style() lipgloss.Style { switch msg.message.Role { case message.User: - borderColor = t.Secondary() + borderColor = t.Secondary case message.Assistant: - borderColor = t.Primary() + borderColor = t.Primary default: // Tool call - borderColor = t.TextMuted() + borderColor = t.BgSubtle } - return styles.BaseStyle(). + return t.S().Muted. BorderLeft(true). - Foreground(t.TextMuted()). BorderForeground(borderColor). BorderStyle(borderStyle) } @@ -182,14 +180,13 @@ func (m *messageCmp) renderAssistantMessage() string { // renderUserMessage renders user messages with file attachments. // Displays message content and any attached files with appropriate icons. func (m *messageCmp) renderUserMessage() string { - t := theme.CurrentTheme() + t := styles.CurrentTheme() parts := []string{ m.markdownContent(), } - attachmentStyles := styles.BaseStyle(). + attachmentStyles := t.S().Text. MarginLeft(1). - Background(t.BackgroundSecondary()). - Foreground(t.Text()) + Background(t.BgSubtle) attachments := []string{} for _, attachment := range m.message.BinaryContent() { file := filepath.Base(attachment.Path) diff --git a/internal/tui/components/chat/messages/renderer.go b/internal/tui/components/chat/messages/renderer.go index 3c86c2be1c063f45f36ac2cdd84b600da820856a..ee8d679529505e8e984a0b5fa5c57ccb9d266b57 100644 --- a/internal/tui/components/chat/messages/renderer.go +++ b/internal/tui/components/chat/messages/renderer.go @@ -15,7 +15,6 @@ import ( "github.com/opencode-ai/opencode/internal/llm/agent" "github.com/opencode-ai/opencode/internal/llm/tools" "github.com/opencode-ai/opencode/internal/tui/styles" - "github.com/opencode-ai/opencode/internal/tui/theme" ) // responseContextHeight limits the number of lines displayed in tool output @@ -107,7 +106,7 @@ func (br baseRenderer) renderWithParams(v *toolCallCmp, toolName string, args [] return joinHeaderBody(header, body) } -// unmarshalParams safely unmarshals JSON parameters +// unmarshalParams safely unmarshal JSON parameters func (br baseRenderer) unmarshalParams(input string, target any) error { return json.Unmarshal([]byte(input), target) } @@ -593,7 +592,7 @@ func joinHeaderBody(header, body string) string { } func renderPlainContent(v *toolCallCmp, content string) string { - t := theme.CurrentTheme() + t := styles.CurrentTheme() content = strings.TrimSpace(content) lines := strings.Split(content, "\n") @@ -606,58 +605,55 @@ func renderPlainContent(v *toolCallCmp, content string) string { if len(ln) > v.textWidth() { ln = v.fit(ln, v.textWidth()) } - out = append(out, lipgloss.NewStyle(). + out = append(out, t.S().Muted. Width(v.textWidth()). - Background(t.BackgroundSecondary()). - Foreground(t.TextMuted()). + Background(t.BgSubtle). Render(ln)) } if len(lines) > responseContextHeight { - out = append(out, lipgloss.NewStyle(). - Background(t.BackgroundSecondary()). - Foreground(t.TextMuted()). + out = append(out, t.S().Muted. + Background(t.BgSubtle). Render(fmt.Sprintf("... (%d lines)", len(lines)-responseContextHeight))) } return strings.Join(out, "\n") } func renderCodeContent(v *toolCallCmp, path, content string, offset int) string { - t := theme.CurrentTheme() + t := styles.CurrentTheme() truncated := truncateHeight(content, responseContextHeight) - highlighted, _ := highlight.SyntaxHighlight(truncated, path, t.BackgroundSecondary()) + highlighted, _ := highlight.SyntaxHighlight(truncated, path, t.BgSubtle) lines := strings.Split(highlighted, "\n") if len(strings.Split(content, "\n")) > responseContextHeight { - lines = append(lines, lipgloss.NewStyle(). - Background(t.BackgroundSecondary()). - Foreground(t.TextMuted()). + lines = append(lines, t.S().Muted. + Background(t.BgSubtle). Render(fmt.Sprintf("... (%d lines)", len(strings.Split(content, "\n"))-responseContextHeight))) } for i, ln := range lines { - num := lipgloss.NewStyle(). - PaddingLeft(4).PaddingRight(2). - Background(t.BackgroundSecondary()). - Foreground(t.TextMuted()). + num := t.S().Muted. + Background(t.BgSubtle). + PaddingLeft(4). + PaddingRight(2). Render(fmt.Sprintf("%d", i+1+offset)) w := v.textWidth() - lipgloss.Width(num) lines[i] = lipgloss.JoinHorizontal(lipgloss.Left, num, - lipgloss.NewStyle(). + t.S().Base. Width(w). - Background(t.BackgroundSecondary()). + Background(t.BgSubtle). Render(v.fit(ln, w))) } return lipgloss.JoinVertical(lipgloss.Left, lines...) } func (v *toolCallCmp) renderToolError() string { - t := theme.CurrentTheme() + t := styles.CurrentTheme() err := strings.ReplaceAll(v.result.Content, "\n", " ") err = fmt.Sprintf("Error: %s", err) - return styles.BaseStyle().Foreground(t.Error()).Render(v.fit(err, v.textWidth())) + return t.S().Base.Foreground(t.Error).Render(v.fit(err, v.textWidth())) } func removeWorkingDirPrefix(path string) string { diff --git a/internal/tui/components/chat/messages/tool.go b/internal/tui/components/chat/messages/tool.go index 2f23146a26ba204f895bcac9ee61cb8360f91e2b..33d711f3941af28c233242d4e4f94783358d1031 100644 --- a/internal/tui/components/chat/messages/tool.go +++ b/internal/tui/components/chat/messages/tool.go @@ -11,7 +11,6 @@ import ( "github.com/opencode-ai/opencode/internal/tui/components/anim" "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" ) @@ -216,19 +215,17 @@ func (m *toolCallCmp) renderPending() string { // style returns the lipgloss style for the tool call component. // Applies muted colors and focus-dependent border styles. func (m *toolCallCmp) style() lipgloss.Style { - t := theme.CurrentTheme() + t := styles.CurrentTheme() if m.isNested { - return styles.BaseStyle(). - Foreground(t.TextMuted()) + return t.S().Muted } borderStyle := lipgloss.NormalBorder() if m.focused { borderStyle = lipgloss.DoubleBorder() } - return styles.BaseStyle(). + return t.S().Muted. BorderLeft(true). - Foreground(t.TextMuted()). - BorderForeground(t.TextMuted()). + BorderForeground(t.Border). BorderStyle(borderStyle) } @@ -240,8 +237,8 @@ func (m *toolCallCmp) textWidth() int { // fit truncates content to fit within the specified width with ellipsis func (m *toolCallCmp) fit(content string, width int) string { - t := theme.CurrentTheme() - lineStyle := lipgloss.NewStyle().Background(t.BackgroundSecondary()).Foreground(t.TextMuted()) + t := styles.CurrentTheme() + lineStyle := t.S().Muted.Background(t.BgSubtle) dots := lineStyle.Render("...") return ansi.Truncate(content, width, dots) } diff --git a/internal/tui/components/completions/completions.go b/internal/tui/components/completions/completions.go index 9ad622a9e5fca17665f93d0d1c7c6bcef4575329..392af550050407cf321578fa6906740ee13c1169 100644 --- a/internal/tui/components/completions/completions.go +++ b/internal/tui/components/completions/completions.go @@ -6,7 +6,6 @@ import ( "github.com/charmbracelet/lipgloss/v2" "github.com/opencode-ai/opencode/internal/tui/components/core/list" "github.com/opencode-ai/opencode/internal/tui/styles" - "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -172,11 +171,11 @@ func (c *completionsCmp) View() tea.View { } func (c *completionsCmp) style() lipgloss.Style { - t := theme.CurrentTheme() - return styles.BaseStyle(). + t := styles.CurrentTheme() + return t.S().Base. Width(c.width). Height(c.height). - Background(t.BackgroundSecondary()) + Background(t.BgSubtle) } func (c *completionsCmp) Open() bool { diff --git a/internal/tui/components/dialog/filepicker.go b/internal/tui/components/dialog/filepicker.go index d6d5e10112ad13cc8b93ebd54731610a6c4b8eea..fdb23f4d3afafcd9a548eb254afd076fe22212d9 100644 --- a/internal/tui/components/dialog/filepicker.go +++ b/internal/tui/components/dialog/filepicker.go @@ -19,7 +19,6 @@ import ( "github.com/opencode-ai/opencode/internal/message" "github.com/opencode-ai/opencode/internal/tui/image" "github.com/opencode-ai/opencode/internal/tui/styles" - "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -258,7 +257,8 @@ func (f *filepickerCmp) addAttachmentToMessage() (tea.Model, tea.Cmd) { } func (f *filepickerCmp) View() tea.View { - t := theme.CurrentTheme() + t := styles.CurrentTheme() + baseStyle := t.S().Base const maxVisibleDirs = 20 const maxWidth = 80 @@ -286,12 +286,11 @@ func (f *filepickerCmp) View() tea.View { for i := startIdx; i < endIdx; i++ { file := f.dirs[i] - itemStyle := styles.BaseStyle().Width(adjustedWidth) + itemStyle := t.S().Text.Width(adjustedWidth) if i == f.cursor { itemStyle = itemStyle. - Background(t.Primary()). - Foreground(t.Background()). + Background(t.Primary). Bold(true) } filename := file.Name() @@ -309,20 +308,18 @@ func (f *filepickerCmp) View() tea.View { // Pad to always show exactly 21 lines for len(files) < maxVisibleDirs { - files = append(files, styles.BaseStyle().Width(adjustedWidth).Render("")) + files = append(files, baseStyle.Width(adjustedWidth).Render("")) } - currentPath := styles.BaseStyle(). + currentPath := baseStyle. Height(1). Width(adjustedWidth). Render(f.cwd.View()) - viewportstyle := lipgloss.NewStyle(). + viewportstyle := baseStyle. Width(f.viewport.Width()). - Background(t.Background()). Border(lipgloss.RoundedBorder()). - BorderForeground(t.TextMuted()). - BorderBackground(t.Background()). + BorderForeground(t.BorderFocus). Padding(2). Render(f.viewport.View()) var insertExitText string @@ -335,17 +332,16 @@ func (f *filepickerCmp) View() tea.View { content := lipgloss.JoinVertical( lipgloss.Left, currentPath, - styles.BaseStyle().Width(adjustedWidth).Render(""), - styles.BaseStyle().Width(adjustedWidth).Render(lipgloss.JoinVertical(lipgloss.Left, files...)), - styles.BaseStyle().Width(adjustedWidth).Render(""), - styles.BaseStyle().Foreground(t.TextMuted()).Width(adjustedWidth).Render(insertExitText), + baseStyle.Width(adjustedWidth).Render(""), + baseStyle.Width(adjustedWidth).Render(lipgloss.JoinVertical(lipgloss.Left, files...)), + baseStyle.Width(adjustedWidth).Render(""), + t.S().Muted.Width(adjustedWidth).Render(insertExitText), ) f.cwd.SetValue(f.cwd.Value()) - contentStyle := styles.BaseStyle().Padding(1, 2). + contentStyle := baseStyle.Padding(1, 2). Border(lipgloss.RoundedBorder()). - BorderBackground(t.Background()). - BorderForeground(t.TextMuted()). + BorderForeground(t.BorderFocus). Width(lipgloss.Width(content) + 4) return tea.NewView( diff --git a/internal/tui/components/dialog/help.go b/internal/tui/components/dialog/help.go deleted file mode 100644 index 549c2a476fa43da14a36c5d942e894d83b6f79ac..0000000000000000000000000000000000000000 --- a/internal/tui/components/dialog/help.go +++ /dev/null @@ -1,203 +0,0 @@ -package dialog - -import ( - "strings" - - "github.com/charmbracelet/bubbles/v2/key" - tea "github.com/charmbracelet/bubbletea/v2" - "github.com/charmbracelet/lipgloss/v2" - "github.com/opencode-ai/opencode/internal/tui/styles" - "github.com/opencode-ai/opencode/internal/tui/theme" - "github.com/opencode-ai/opencode/internal/tui/util" -) - -type helpCmp struct { - width int - height int - keys []key.Binding -} - -func (h *helpCmp) Init() tea.Cmd { - return nil -} - -func (h *helpCmp) SetBindings(k []key.Binding) { - h.keys = k -} - -func (h *helpCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.WindowSizeMsg: - h.width = 90 - h.height = msg.Height - } - return h, nil -} - -func removeDuplicateBindings(bindings []key.Binding) []key.Binding { - seen := make(map[string]struct{}) - result := make([]key.Binding, 0, len(bindings)) - - // Process bindings in reverse order - for i := len(bindings) - 1; i >= 0; i-- { - b := bindings[i] - k := strings.Join(b.Keys(), " ") - if _, ok := seen[k]; ok { - // duplicate, skip - continue - } - seen[k] = struct{}{} - // Add to the beginning of result to maintain original order - result = append([]key.Binding{b}, result...) - } - - return result -} - -func (h *helpCmp) render() string { - 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 ( - pairs []string - width int - rows = 12 - 2 - ) - - for i := 0; i < len(bindings); i += rows { - var ( - keys []string - descs []string - ) - for j := i; j < min(i+rows, len(bindings)); j++ { - 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{baseStyle.Render(" ")} - } - - maxDescWidth := 0 - for _, desc := range descs { - if maxDescWidth < lipgloss.Width(desc) { - maxDescWidth = lipgloss.Width(desc) - } - } - for i := range descs { - remainingWidth := maxDescWidth - lipgloss.Width(descs[i]) - if remainingWidth > 0 { - descs[i] = descs[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth)) - } - } - maxKeyWidth := 0 - for _, key := range keys { - if maxKeyWidth < lipgloss.Width(key) { - maxKeyWidth = lipgloss.Width(key) - } - } - for i := range keys { - remainingWidth := maxKeyWidth - lipgloss.Width(keys[i]) - if remainingWidth > 0 { - keys[i] = keys[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth)) - } - } - - cols = append(cols, - strings.Join(keys, "\n"), - strings.Join(descs, "\n"), - ) - - 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) - if width > h.width-2 { - break - } - pairs = append(pairs, pair) - } - - // https://github.com/charmbracelet/lipgloss/v2/issues/209 - if len(pairs) > 1 { - prefix := pairs[:len(pairs)-1] - lastPair := pairs[len(pairs)-1] - prefix = append(prefix, lipgloss.Place( - lipgloss.Width(lastPair), // width - lipgloss.Height(prefix[0]), // height - lipgloss.Left, // x - lipgloss.Top, // y - lastPair, // content - lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())), - )) - content := baseStyle.Width(h.width).Render( - lipgloss.JoinHorizontal( - lipgloss.Top, - prefix..., - ), - ) - return content - } - - // Join pairs of columns and enclose in a border - content := baseStyle.Width(h.width).Render( - lipgloss.JoinHorizontal( - lipgloss.Top, - pairs..., - ), - ) - return content -} - -func (h *helpCmp) View() tea.View { - t := theme.CurrentTheme() - baseStyle := styles.BaseStyle() - - content := h.render() - header := baseStyle. - Bold(true). - Width(lipgloss.Width(content)). - Foreground(t.Primary()). - Render("Keyboard Shortcuts") - - return tea.NewView( - baseStyle.Padding(1). - Border(lipgloss.RoundedBorder()). - BorderForeground(t.TextMuted()). - Width(h.width). - BorderBackground(t.Background()). - Render( - lipgloss.JoinVertical(lipgloss.Center, - header, - baseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))), - content, - ), - ), - ) -} - -type HelpCmp interface { - util.Model - SetBindings([]key.Binding) -} - -func NewHelpCmp() HelpCmp { - return &helpCmp{} -} diff --git a/internal/tui/components/dialog/init.go b/internal/tui/components/dialog/init.go index d4ef8c523f842ac979969596271b6538efe6af2b..261516b787cca0a9b0142146652b7a41ec7d41c0 100644 --- a/internal/tui/components/dialog/init.go +++ b/internal/tui/components/dialog/init.go @@ -6,7 +6,6 @@ import ( "github.com/charmbracelet/lipgloss/v2" "github.com/opencode-ai/opencode/internal/tui/styles" - "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -93,51 +92,45 @@ 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() + t := styles.CurrentTheme() + baseStyle := t.S().Base // Calculate width needed for content maxWidth := 60 // Width for explanation text title := baseStyle. - Foreground(t.Primary()). + Foreground(t.Primary). Bold(true). Width(maxWidth). Padding(0, 1). Render("Initialize Project") - explanation := baseStyle. - Foreground(t.Text()). + explanation := t.S().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 := baseStyle. - Foreground(t.Text()). + question := t.S().Text. Width(maxWidth). Padding(1, 1). Render("Would you like to initialize this project?") maxWidth = min(maxWidth, m.width-10) - yesStyle := baseStyle - noStyle := baseStyle + yesStyle := t.S().Text + noStyle := yesStyle if m.selected == 0 { yesStyle = yesStyle. - Background(t.Primary()). - Foreground(t.Background()). + Background(t.Primary). Bold(true) noStyle = noStyle. - Background(t.Background()). - Foreground(t.Primary()) + Background(t.BgSubtle) } else { noStyle = noStyle. - Background(t.Primary()). - Foreground(t.Background()). + Background(t.Primary). Bold(true) yesStyle = yesStyle. - Background(t.Background()). - Foreground(t.Primary()) + Background(t.BgSubtle) } yes := yesStyle.Padding(0, 3).Render("Yes") @@ -161,8 +154,7 @@ func (m InitDialogCmp) View() string { return baseStyle.Padding(1, 2). Border(lipgloss.RoundedBorder()). - BorderBackground(t.Background()). - BorderForeground(t.TextMuted()). + BorderForeground(t.BorderFocus). Width(lipgloss.Width(content) + 4). Render(content) } diff --git a/internal/tui/components/dialog/permission.go b/internal/tui/components/dialog/permission.go index e258dbc24099414309faef78ac8a6aefe204c989..33332e44eaae9de25653f328ad9de457a121c48a 100644 --- a/internal/tui/components/dialog/permission.go +++ b/internal/tui/components/dialog/permission.go @@ -13,7 +13,6 @@ import ( "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,28 +148,26 @@ func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd { } func (p *permissionDialogCmp) renderButtons() string { - t := theme.CurrentTheme() - baseStyle := styles.BaseStyle() + t := styles.CurrentTheme() - allowStyle := baseStyle - allowSessionStyle := baseStyle - denyStyle := baseStyle - spacerStyle := baseStyle.Background(t.Background()) + allowStyle := t.S().Text + allowSessionStyle := allowStyle + denyStyle := allowStyle // Style the selected button switch p.selectedOption { case 0: - allowStyle = allowStyle.Background(t.Primary()).Foreground(t.Background()) - allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary()) - denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary()) + allowStyle = allowStyle.Background(t.Primary) + allowSessionStyle = allowSessionStyle.Background(t.BgSubtle) + denyStyle = denyStyle.Background(t.BgSubtle) case 1: - allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary()) - allowSessionStyle = allowSessionStyle.Background(t.Primary()).Foreground(t.Background()) - denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary()) + allowStyle = allowStyle.Background(t.BgSubtle) + allowSessionStyle = allowSessionStyle.Background(t.Primary) + denyStyle = denyStyle.Background(t.BgSubtle) case 2: - allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary()) - allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary()) - denyStyle = denyStyle.Background(t.Primary()).Foreground(t.Background()) + allowStyle = allowStyle.Background(t.BgSubtle) + allowSessionStyle = allowSessionStyle.Background(t.BgSubtle) + denyStyle = denyStyle.Background(t.Primary) } allowButton := allowStyle.Padding(0, 1).Render("Allow (a)") @@ -180,33 +177,31 @@ func (p *permissionDialogCmp) renderButtons() string { content := lipgloss.JoinHorizontal( lipgloss.Left, allowButton, - spacerStyle.Render(" "), + " ", allowSessionButton, - spacerStyle.Render(" "), + " ", denyButton, - spacerStyle.Render(" "), + " ", ) remainingWidth := p.width - lipgloss.Width(content) if remainingWidth > 0 { - content = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + content + content = strings.Repeat(" ", remainingWidth) + content } return content } func (p *permissionDialogCmp) renderHeader() string { - t := theme.CurrentTheme() - baseStyle := styles.BaseStyle() + t := styles.CurrentTheme() + baseStyle := t.S().Base - toolKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Tool") - toolValue := baseStyle. - Foreground(t.Text()). + toolKey := t.S().Muted.Bold(true).Render("Tool") + toolValue := t.S().Text. Width(p.width - lipgloss.Width(toolKey)). Render(fmt.Sprintf(": %s", p.permission.ToolName)) - pathKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Path") - pathValue := baseStyle. - Foreground(t.Text()). + pathKey := t.S().Muted.Bold(true).Render("Path") + pathValue := t.S().Text. Width(p.width - lipgloss.Width(pathKey)). Render(fmt.Sprintf(": %s", p.permission.Path)) @@ -228,12 +223,11 @@ func (p *permissionDialogCmp) renderHeader() string { // Add tool-specific header information switch p.permission.ToolName { case tools.BashToolName: - headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Command")) + headerParts = append(headerParts, t.S().Muted.Width(p.width).Bold(true).Render("Command")) case tools.EditToolName: params := p.permission.Params.(tools.EditPermissionsParams) - fileKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("File") - filePath := baseStyle. - Foreground(t.Text()). + fileKey := t.S().Muted.Bold(true).Render("File") + filePath := t.S().Text. Width(p.width - lipgloss.Width(fileKey)). Render(fmt.Sprintf(": %s", params.FilePath)) headerParts = append(headerParts, @@ -247,9 +241,8 @@ func (p *permissionDialogCmp) renderHeader() string { case tools.WriteToolName: params := p.permission.Params.(tools.WritePermissionsParams) - fileKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("File") - filePath := baseStyle. - Foreground(t.Text()). + fileKey := t.S().Muted.Bold(true).Render("File") + filePath := t.S().Text. Width(p.width - lipgloss.Width(fileKey)). Render(fmt.Sprintf(": %s", params.FilePath)) headerParts = append(headerParts, @@ -261,15 +254,14 @@ func (p *permissionDialogCmp) renderHeader() string { baseStyle.Render(strings.Repeat(" ", p.width)), ) case tools.FetchToolName: - headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("URL")) + headerParts = append(headerParts, t.S().Muted.Width(p.width).Bold(true).Render("URL")) } - return lipgloss.NewStyle().Background(t.Background()).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...)) + return baseStyle.Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...)) } func (p *permissionDialogCmp) renderBashContent() string { - baseStyle := styles.BaseStyle() - + baseStyle := styles.CurrentTheme().S().Base if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok { content := fmt.Sprintf("```bash\n%s\n```", pr.Command) @@ -327,8 +319,7 @@ func (p *permissionDialogCmp) renderWriteContent() string { } func (p *permissionDialogCmp) renderFetchContent() string { - baseStyle := styles.BaseStyle() - + baseStyle := styles.CurrentTheme().S().Base if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok { content := fmt.Sprintf("```bash\n%s\n```", pr.URL) @@ -349,7 +340,7 @@ func (p *permissionDialogCmp) renderFetchContent() string { } func (p *permissionDialogCmp) renderDefaultContent() string { - baseStyle := styles.BaseStyle() + baseStyle := styles.CurrentTheme().S().Base content := p.permission.Description @@ -373,21 +364,19 @@ func (p *permissionDialogCmp) renderDefaultContent() string { } func (p *permissionDialogCmp) styleViewport() string { - t := theme.CurrentTheme() - contentStyle := lipgloss.NewStyle(). - Background(t.Background()) + t := styles.CurrentTheme() - return contentStyle.Render(p.contentViewPort.View()) + return t.S().Base.Render(p.contentViewPort.View()) } func (p *permissionDialogCmp) render() string { - t := theme.CurrentTheme() - baseStyle := styles.BaseStyle() + t := styles.CurrentTheme() + baseStyle := t.S().Base title := baseStyle. Bold(true). Width(p.width - 4). - Foreground(t.Primary()). + Foreground(t.Primary). Render("Permission Required") // Render header headerContent := p.renderHeader() @@ -428,8 +417,7 @@ func (p *permissionDialogCmp) render() string { return baseStyle. Padding(1, 0, 0, 1). Border(lipgloss.RoundedBorder()). - BorderBackground(t.Background()). - BorderForeground(t.TextMuted()). + BorderForeground(t.BorderFocus). Width(p.width). Height(p.height). Render( diff --git a/internal/tui/components/dialog/theme.go b/internal/tui/components/dialog/theme.go deleted file mode 100644 index c5faf6c902d6bdf2f935abb2418d3adb4558def9..0000000000000000000000000000000000000000 --- a/internal/tui/components/dialog/theme.go +++ /dev/null @@ -1,201 +0,0 @@ -package dialog - -import ( - "github.com/charmbracelet/bubbles/v2/key" - tea "github.com/charmbracelet/bubbletea/v2" - "github.com/charmbracelet/lipgloss/v2" - "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 { - util.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.KeyPressMsg: - 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() tea.View { - currentTheme := theme.CurrentTheme() - baseStyle := styles.BaseStyle() - - if len(t.themes) == 0 { - return tea.NewView( - 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 tea.NewView( - 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/dialogs/commands/arguments.go b/internal/tui/components/dialogs/commands/arguments.go index 730c50c490bbd4fec98653b82e81589f6e2bfeda..f08436d299a1f825dd7f525dd5290e7af9a8ed14 100644 --- a/internal/tui/components/dialogs/commands/arguments.go +++ b/internal/tui/components/dialogs/commands/arguments.go @@ -11,7 +11,6 @@ import ( "github.com/charmbracelet/lipgloss/v2" "github.com/opencode-ai/opencode/internal/tui/components/dialogs" "github.com/opencode-ai/opencode/internal/tui/styles" - "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -54,7 +53,7 @@ type commandArgumentsDialogCmp struct { } func NewCommandArgumentsDialog(commandID, content string, argNames []string) CommandArgumentsDialog { - t := theme.CurrentTheme() + t := styles.CurrentTheme() inputs := make([]textinput.Model, len(argNames)) for i, name := range argNames { @@ -63,15 +62,8 @@ func NewCommandArgumentsDialog(commandID, content string, argNames []string) Com ti.SetWidth(40) ti.SetVirtualCursor(false) ti.Prompt = "" - ds := ti.Styles() - - ds.Blurred.Placeholder = ds.Blurred.Placeholder.Background(t.Background()).Foreground(t.TextMuted()) - ds.Blurred.Prompt = ds.Blurred.Prompt.Background(t.Background()).Foreground(t.TextMuted()) - ds.Blurred.Text = ds.Blurred.Text.Background(t.Background()).Foreground(t.TextMuted()) - ds.Focused.Placeholder = ds.Blurred.Placeholder.Background(t.Background()).Foreground(t.TextMuted()) - ds.Focused.Prompt = ds.Blurred.Prompt.Background(t.Background()).Foreground(t.Text()) - ds.Focused.Text = ds.Blurred.Text.Background(t.Background()).Foreground(t.Text()) - ti.SetStyles(ds) + + ti.SetStyles(t.S().TextInput) // Only focus the first input initially if i == 0 { ti.Focus() @@ -148,42 +140,36 @@ func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // View implements CommandArgumentsDialog. func (c *commandArgumentsDialogCmp) View() tea.View { - t := theme.CurrentTheme() - baseStyle := styles.BaseStyle() + t := styles.CurrentTheme() + baseStyle := t.S().Base title := lipgloss.NewStyle(). - Foreground(t.Primary()). + Foreground(t.Primary). Bold(true). Padding(0, 1). - Background(t.Background()). Render("Command Arguments") - explanation := lipgloss.NewStyle(). - Foreground(t.Text()). + explanation := t.S().Text. Padding(0, 1). - Background(t.Background()). Render("This command requires arguments.") // Create input fields for each argument inputFields := make([]string, len(c.inputs)) for i, input := range c.inputs { // Highlight the label of the focused input - labelStyle := lipgloss.NewStyle(). - Padding(1, 1, 0, 1). - Background(t.Background()) + labelStyle := baseStyle. + Padding(1, 1, 0, 1) if i == c.focusIndex { - labelStyle = labelStyle.Foreground(t.Text()).Bold(true) + labelStyle = labelStyle.Foreground(t.FgBase).Bold(true) } else { - labelStyle = labelStyle.Foreground(t.TextMuted()) + labelStyle = labelStyle.Foreground(t.FgMuted) } label := labelStyle.Render(c.argNames[i] + ":") - field := lipgloss.NewStyle(). - Foreground(t.Text()). + field := t.S().Text. Padding(0, 1). - Background(t.Background()). Render(input.View()) inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field) @@ -205,9 +191,7 @@ func (c *commandArgumentsDialogCmp) View() tea.View { view := tea.NewView( baseStyle.Padding(1, 1, 0, 1). Border(lipgloss.RoundedBorder()). - BorderBackground(t.Background()). - BorderForeground(t.TextMuted()). - Background(t.Background()). + BorderForeground(t.BorderFocus). Width(c.width). Render(content), ) @@ -228,13 +212,12 @@ func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor { } func (c *commandArgumentsDialogCmp) style() lipgloss.Style { - t := theme.CurrentTheme() - return styles.BaseStyle(). + t := styles.CurrentTheme() + return t.S().Base. Width(c.width). Padding(1). Border(lipgloss.RoundedBorder()). - BorderBackground(t.Background()). - BorderForeground(t.TextMuted()) + BorderForeground(t.BorderFocus) } func (c *commandArgumentsDialogCmp) Position() (int, int) { diff --git a/internal/tui/components/dialogs/quit/quit.go b/internal/tui/components/dialogs/quit/quit.go index 211bac3c88258dae43b791080e6570b58192b5db..987edbd6874c29a58c442a238258a0ea68f90360 100644 --- a/internal/tui/components/dialogs/quit/quit.go +++ b/internal/tui/components/dialogs/quit/quit.go @@ -7,7 +7,6 @@ import ( "github.com/opencode-ai/opencode/internal/tui/components/dialogs" "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" ) @@ -69,26 +68,24 @@ func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // View renders the quit dialog with Yes/No buttons. func (q *quitDialogCmp) View() tea.View { - t := theme.CurrentTheme() - baseStyle := styles.BaseStyle() - - yesStyle := baseStyle - noStyle := baseStyle - spacerStyle := baseStyle.Background(t.Background()) + t := styles.CurrentTheme() + baseStyle := t.S().Base + yesStyle := t.S().Text + noStyle := yesStyle if q.selectedNo { - noStyle = noStyle.Background(t.Primary()).Foreground(t.Background()) - yesStyle = yesStyle.Background(t.Background()).Foreground(t.Primary()) + noStyle = noStyle.Background(t.Primary) + yesStyle = yesStyle.Background(t.BgSubtle) } else { - yesStyle = yesStyle.Background(t.Primary()).Foreground(t.Background()) - noStyle = noStyle.Background(t.Background()).Foreground(t.Primary()) + yesStyle = yesStyle.Background(t.Primary) + noStyle = noStyle.Background(t.BgSubtle) } yesButton := yesStyle.Padding(0, 1).Render("Yes") noButton := noStyle.Padding(0, 1).Render("No") buttons := baseStyle.Width(lipgloss.Width(question)).Align(lipgloss.Right).Render( - lipgloss.JoinHorizontal(lipgloss.Center, yesButton, spacerStyle.Render(" "), noButton), + lipgloss.JoinHorizontal(lipgloss.Center, yesButton, " ", noButton), ) content := baseStyle.Render( @@ -103,8 +100,7 @@ func (q *quitDialogCmp) View() tea.View { quitDialogStyle := baseStyle. Padding(1, 2). Border(lipgloss.RoundedBorder()). - BorderBackground(t.Background()). - BorderForeground(t.TextMuted()) + BorderForeground(t.BorderFocus) return tea.NewView( quitDialogStyle.Render(content), diff --git a/internal/tui/layout/overlay.go b/internal/tui/layout/overlay.go deleted file mode 100644 index 4c9dd5a9435e7d3d9a58a0b0829526532d76f2e0..0000000000000000000000000000000000000000 --- a/internal/tui/layout/overlay.go +++ /dev/null @@ -1,169 +0,0 @@ -package layout - -import ( - "strings" - - "github.com/charmbracelet/lipgloss/v2" - chAnsi "github.com/charmbracelet/x/ansi" - "github.com/muesli/ansi" - "github.com/muesli/reflow/truncate" - "github.com/muesli/termenv" - "github.com/opencode-ai/opencode/internal/tui/styles" - "github.com/opencode-ai/opencode/internal/tui/theme" - "github.com/opencode-ai/opencode/internal/tui/util" -) - -// Most of this code is borrowed from -// https://github.com/charmbracelet/lipgloss/v2/pull/102 -// as well as the lipgloss library, with some modification for what I needed. - -// Split a string into lines, additionally returning the size of the widest -// line. -func getLines(s string) (lines []string, widest int) { - lines = strings.Split(s, "\n") - - for _, l := range lines { - w := ansi.PrintableRuneWidth(l) - if widest < w { - widest = w - } - } - - return lines, widest -} - -// PlaceOverlay places fg on top of bg. -func PlaceOverlay( - x, y int, - fg, bg string, - shadow bool, opts ...WhitespaceOption, -) string { - fgLines, fgWidth := getLines(fg) - bgLines, bgWidth := getLines(bg) - bgHeight := len(bgLines) - fgHeight := len(fgLines) - - if shadow { - t := theme.CurrentTheme() - baseStyle := styles.BaseStyle() - - var shadowbg string = "" - shadowchar := lipgloss.NewStyle(). - Background(t.BackgroundDarker()). - Foreground(t.Background()). - Render("░") - bgchar := baseStyle.Render(" ") - for i := 0; i <= fgHeight; i++ { - if i == 0 { - shadowbg += bgchar + strings.Repeat(bgchar, fgWidth) + "\n" - } else { - shadowbg += bgchar + strings.Repeat(shadowchar, fgWidth) + "\n" - } - } - - fg = PlaceOverlay(0, 0, fg, shadowbg, false, opts...) - fgLines, fgWidth = getLines(fg) - fgHeight = len(fgLines) - } - - if fgWidth >= bgWidth && fgHeight >= bgHeight { - // FIXME: return fg or bg? - return fg - } - // TODO: allow placement outside of the bg box? - x = util.Clamp(x, 0, bgWidth-fgWidth) - y = util.Clamp(y, 0, bgHeight-fgHeight) - - ws := &whitespace{} - for _, opt := range opts { - opt(ws) - } - - var b strings.Builder - for i, bgLine := range bgLines { - if i > 0 { - b.WriteByte('\n') - } - if i < y || i >= y+fgHeight { - b.WriteString(bgLine) - continue - } - - pos := 0 - if x > 0 { - left := truncate.String(bgLine, uint(x)) - pos = ansi.PrintableRuneWidth(left) - b.WriteString(left) - if pos < x { - b.WriteString(ws.render(x - pos)) - pos = x - } - } - - fgLine := fgLines[i-y] - b.WriteString(fgLine) - pos += ansi.PrintableRuneWidth(fgLine) - - right := cutLeft(bgLine, pos) - bgWidth := ansi.PrintableRuneWidth(bgLine) - rightWidth := ansi.PrintableRuneWidth(right) - if rightWidth <= bgWidth-pos { - b.WriteString(ws.render(bgWidth - rightWidth - pos)) - } - - b.WriteString(right) - } - - return b.String() -} - -// cutLeft cuts printable characters from the left. -// This function is heavily based on muesli's ansi and truncate packages. -func cutLeft(s string, cutWidth int) string { - return chAnsi.Cut(s, cutWidth, lipgloss.Width(s)) -} - -func max(a, b int) int { - if a > b { - return a - } - return b -} - -type whitespace struct { - style termenv.Style - chars string -} - -// Render whitespaces. -func (w whitespace) render(width int) string { - if w.chars == "" { - w.chars = " " - } - - r := []rune(w.chars) - j := 0 - b := strings.Builder{} - - // Cycle through runes and print them into the whitespace. - for i := 0; i < width; { - b.WriteRune(r[j]) - j++ - if j >= len(r) { - j = 0 - } - i += ansi.PrintableRuneWidth(string(r[j])) - } - - // Fill any extra gaps white spaces. This might be necessary if any runes - // are more than one cell wide, which could leave a one-rune gap. - short := width - ansi.PrintableRuneWidth(b.String()) - if short > 0 { - b.WriteString(strings.Repeat(" ", short)) - } - - return w.style.Styled(b.String()) -} - -// WhitespaceOption sets a styling rule for rendering whitespace. -type WhitespaceOption func(*whitespace) diff --git a/internal/tui/page/logs.go b/internal/tui/page/logs.go index 63613d02d9c222e6f998b571779f43ae13e74668..e94fa5d12837a4d823804f8c8617ec42cc3a25ba 100644 --- a/internal/tui/page/logs.go +++ b/internal/tui/page/logs.go @@ -43,7 +43,7 @@ func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (p *logsPage) View() tea.View { - style := styles.BaseStyle().Width(p.width).Height(p.height) + style := styles.CurrentTheme().S().Base.Width(p.width).Height(p.height) return tea.NewView( style.Render( lipgloss.JoinVertical(lipgloss.Top, diff --git a/internal/tui/styles/markdown.go b/internal/tui/styles/markdown.go index df6b91bd8e876b4ae44700e7daeb28484d120bd6..deda517add19a306d41320fdaab0c8895f63919e 100644 --- a/internal/tui/styles/markdown.go +++ b/internal/tui/styles/markdown.go @@ -1,12 +1,7 @@ package styles import ( - "fmt" - "image/color" - "github.com/charmbracelet/glamour/v2" - "github.com/charmbracelet/glamour/v2/ansi" - "github.com/opencode-ai/opencode/internal/tui/theme" ) // Helper functions for style pointers @@ -16,263 +11,10 @@ func uintPtr(u uint) *uint { return &u } // returns a glamour TermRenderer configured with the current theme func GetMarkdownRenderer(width int) *glamour.TermRenderer { + t := CurrentTheme() r, _ := glamour.NewTermRenderer( - glamour.WithStyles(generateMarkdownStyleConfig()), + glamour.WithStyles(t.S().Markdown), glamour.WithWordWrap(width), ) return r } - -// creates an ansi.StyleConfig for markdown rendering -// using adaptive colors from the provided theme. -func generateMarkdownStyleConfig() ansi.StyleConfig { - t := theme.CurrentTheme() - - return ansi.StyleConfig{ - Document: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.MarkdownText())), - }, - Margin: uintPtr(defaultMargin), - }, - BlockQuote: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.MarkdownBlockQuote())), - Italic: boolPtr(true), - Prefix: "┃ ", - }, - Indent: uintPtr(1), - IndentToken: stringPtr(BaseStyle().Render(" ")), - }, - List: ansi.StyleList{ - LevelIndent: defaultMargin, - StyleBlock: ansi.StyleBlock{ - IndentToken: stringPtr(BaseStyle().Render(" ")), - StylePrimitive: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.MarkdownText())), - }, - }, - }, - Heading: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - BlockSuffix: "\n", - Color: stringPtr(colorToString(t.MarkdownHeading())), - Bold: boolPtr(true), - }, - }, - H1: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "# ", - Color: stringPtr(colorToString(t.MarkdownHeading())), - Bold: boolPtr(true), - }, - }, - H2: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "## ", - Color: stringPtr(colorToString(t.MarkdownHeading())), - Bold: boolPtr(true), - }, - }, - H3: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "### ", - Color: stringPtr(colorToString(t.MarkdownHeading())), - Bold: boolPtr(true), - }, - }, - H4: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "#### ", - Color: stringPtr(colorToString(t.MarkdownHeading())), - Bold: boolPtr(true), - }, - }, - H5: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "##### ", - Color: stringPtr(colorToString(t.MarkdownHeading())), - Bold: boolPtr(true), - }, - }, - H6: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "###### ", - Color: stringPtr(colorToString(t.MarkdownHeading())), - Bold: boolPtr(true), - }, - }, - Strikethrough: ansi.StylePrimitive{ - CrossedOut: boolPtr(true), - Color: stringPtr(colorToString(t.TextMuted())), - }, - Emph: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.MarkdownEmph())), - Italic: boolPtr(true), - }, - Strong: ansi.StylePrimitive{ - Bold: boolPtr(true), - Color: stringPtr(colorToString(t.MarkdownStrong())), - }, - HorizontalRule: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.MarkdownHorizontalRule())), - Format: "\n─────────────────────────────────────────\n", - }, - Item: ansi.StylePrimitive{ - BlockPrefix: "• ", - Color: stringPtr(colorToString(t.MarkdownListItem())), - }, - Enumeration: ansi.StylePrimitive{ - BlockPrefix: ". ", - Color: stringPtr(colorToString(t.MarkdownListEnumeration())), - }, - Task: ansi.StyleTask{ - StylePrimitive: ansi.StylePrimitive{}, - Ticked: "[✓] ", - Unticked: "[ ] ", - }, - Link: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.MarkdownLink())), - Underline: boolPtr(true), - }, - LinkText: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.MarkdownLinkText())), - Bold: boolPtr(true), - }, - Image: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.MarkdownImage())), - Underline: boolPtr(true), - Format: "🖼 {{.text}}", - }, - ImageText: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.MarkdownImageText())), - Format: "{{.text}}", - }, - Code: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.MarkdownCode())), - Prefix: "", - Suffix: "", - }, - }, - CodeBlock: ansi.StyleCodeBlock{ - StyleBlock: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: " ", - Color: stringPtr(colorToString(t.MarkdownCodeBlock())), - }, - Margin: uintPtr(defaultMargin), - }, - Chroma: &ansi.Chroma{ - Text: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.MarkdownText())), - }, - Error: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.Error())), - }, - Comment: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.SyntaxComment())), - }, - CommentPreproc: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.SyntaxKeyword())), - }, - Keyword: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.SyntaxKeyword())), - }, - KeywordReserved: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.SyntaxKeyword())), - }, - KeywordNamespace: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.SyntaxKeyword())), - }, - KeywordType: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.SyntaxType())), - }, - Operator: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.SyntaxOperator())), - }, - Punctuation: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.SyntaxPunctuation())), - }, - Name: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.SyntaxVariable())), - }, - NameBuiltin: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.SyntaxVariable())), - }, - NameTag: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.SyntaxKeyword())), - }, - NameAttribute: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.SyntaxFunction())), - }, - NameClass: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.SyntaxType())), - }, - NameConstant: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.SyntaxVariable())), - }, - NameDecorator: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.SyntaxFunction())), - }, - NameFunction: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.SyntaxFunction())), - }, - LiteralNumber: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.SyntaxNumber())), - }, - LiteralString: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.SyntaxString())), - }, - LiteralStringEscape: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.SyntaxKeyword())), - }, - GenericDeleted: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.DiffRemoved())), - }, - GenericEmph: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.MarkdownEmph())), - Italic: boolPtr(true), - }, - GenericInserted: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.DiffAdded())), - }, - GenericStrong: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.MarkdownStrong())), - Bold: boolPtr(true), - }, - GenericSubheading: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.MarkdownHeading())), - }, - }, - }, - Table: ansi.StyleTable{ - StyleBlock: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - BlockPrefix: "\n", - BlockSuffix: "\n", - }, - }, - CenterSeparator: stringPtr("┼"), - ColumnSeparator: stringPtr("│"), - RowSeparator: stringPtr("─"), - }, - DefinitionDescription: ansi.StylePrimitive{ - BlockPrefix: "\n ❯ ", - Color: stringPtr(colorToString(t.MarkdownLinkText())), - }, - Text: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.MarkdownText())), - }, - Paragraph: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Color: stringPtr(colorToString(t.MarkdownText())), - }, - }, - } -} - -func colorToString(c color.Color) string { - rgba := color.RGBAModel.Convert(c).(color.RGBA) - return fmt.Sprintf("#%02x%02x%02x", rgba.R, rgba.G, rgba.B) -} diff --git a/internal/tui/styles/styles.go b/internal/tui/styles/styles.go deleted file mode 100644 index a502e411c74c52b6f4521164f18710213ed62b03..0000000000000000000000000000000000000000 --- a/internal/tui/styles/styles.go +++ /dev/null @@ -1,155 +0,0 @@ -package styles - -import ( - "image/color" - - "github.com/charmbracelet/lipgloss/v2" - "github.com/opencode-ai/opencode/internal/tui/theme" -) - -var ImageBakcground = "#212121" - -// Style generation functions that use the current theme - -// BaseStyle returns the base style with background and foreground colors -func BaseStyle() lipgloss.Style { - t := theme.CurrentTheme() - return lipgloss.NewStyle(). - Background(t.Background()). - Foreground(t.Text()) -} - -// Regular returns a basic unstyled lipgloss.Style -func Regular() lipgloss.Style { - return lipgloss.NewStyle() -} - -// Bold returns a bold style -func Bold() lipgloss.Style { - return Regular().Bold(true) -} - -// Padded returns a style with horizontal padding -func Padded() lipgloss.Style { - return Regular().Padding(0, 1) -} - -// Border returns a style with a normal border -func Border() lipgloss.Style { - t := theme.CurrentTheme() - return Regular(). - Border(lipgloss.NormalBorder()). - BorderForeground(t.BorderNormal()) -} - -// ThickBorder returns a style with a thick border -func ThickBorder() lipgloss.Style { - t := theme.CurrentTheme() - return Regular(). - Border(lipgloss.ThickBorder()). - BorderForeground(t.BorderNormal()) -} - -// DoubleBorder returns a style with a double border -func DoubleBorder() lipgloss.Style { - t := theme.CurrentTheme() - return Regular(). - Border(lipgloss.DoubleBorder()). - BorderForeground(t.BorderNormal()) -} - -// FocusedBorder returns a style with a border using the focused border color -func FocusedBorder() lipgloss.Style { - t := theme.CurrentTheme() - return Regular(). - Border(lipgloss.NormalBorder()). - BorderForeground(t.BorderFocused()) -} - -// DimBorder returns a style with a border using the dim border color -func DimBorder() lipgloss.Style { - t := theme.CurrentTheme() - return Regular(). - Border(lipgloss.NormalBorder()). - BorderForeground(t.BorderDim()) -} - -// PrimaryColor returns the primary color from the current theme -func PrimaryColor() color.Color { - return theme.CurrentTheme().Primary() -} - -// SecondaryColor returns the secondary color from the current theme -func SecondaryColor() color.Color { - return theme.CurrentTheme().Secondary() -} - -// AccentColor returns the accent color from the current theme -func AccentColor() color.Color { - return theme.CurrentTheme().Accent() -} - -// ErrorColor returns the error color from the current theme -func ErrorColor() color.Color { - return theme.CurrentTheme().Error() -} - -// WarningColor returns the warning color from the current theme -func WarningColor() color.Color { - return theme.CurrentTheme().Warning() -} - -// SuccessColor returns the success color from the current theme -func SuccessColor() color.Color { - return theme.CurrentTheme().Success() -} - -// InfoColor returns the info color from the current theme -func InfoColor() color.Color { - return theme.CurrentTheme().Info() -} - -// TextColor returns the text color from the current theme -func TextColor() color.Color { - return theme.CurrentTheme().Text() -} - -// TextMutedColor returns the muted text color from the current theme -func TextMutedColor() color.Color { - return theme.CurrentTheme().TextMuted() -} - -// TextEmphasizedColor returns the emphasized text color from the current theme -func TextEmphasizedColor() color.Color { - return theme.CurrentTheme().TextEmphasized() -} - -// BackgroundColor returns the background color from the current theme -func BackgroundColor() color.Color { - return theme.CurrentTheme().Background() -} - -// BackgroundSecondaryColor returns the secondary background color from the current theme -func BackgroundSecondaryColor() color.Color { - return theme.CurrentTheme().BackgroundSecondary() -} - -// BackgroundDarkerColor returns the darker background color from the current theme -func BackgroundDarkerColor() color.Color { - return theme.CurrentTheme().BackgroundDarker() -} - -// BorderNormalColor returns the normal border color from the current theme -func BorderNormalColor() color.Color { - return theme.CurrentTheme().BorderNormal() -} - -// BorderFocusedColor returns the focused border color from the current theme -func BorderFocusedColor() color.Color { - return theme.CurrentTheme().BorderFocused() -} - -// BorderDimColor returns the dim border color from the current theme -func BorderDimColor() color.Color { - return theme.CurrentTheme().BorderDim() -}