summaryrefslogtreecommitdiffhomepage
path: root/pkg/tui/styles
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-05-18 22:30:41 -0400
committerDax Raad <[email protected]>2025-05-26 12:40:17 -0400
commit99af6146d5def31c59993636d60eb75a483a283b (patch)
treeeb57ed227c15cf9be54bc9f231c83895d812f881 /pkg/tui/styles
parent020e0ca039287b73fa33041fbd1bb214e6ccb396 (diff)
downloadopencode-99af6146d5def31c59993636d60eb75a483a283b.tar.gz
opencode-99af6146d5def31c59993636d60eb75a483a283b.zip
openapi
Diffstat (limited to 'pkg/tui/styles')
-rw-r--r--pkg/tui/styles/background.go123
-rw-r--r--pkg/tui/styles/icons.go12
-rw-r--r--pkg/tui/styles/markdown.go284
-rw-r--r--pkg/tui/styles/styles.go159
4 files changed, 578 insertions, 0 deletions
diff --git a/pkg/tui/styles/background.go b/pkg/tui/styles/background.go
new file mode 100644
index 000000000..2fbb34efb
--- /dev/null
+++ b/pkg/tui/styles/background.go
@@ -0,0 +1,123 @@
+package styles
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+
+ "github.com/charmbracelet/lipgloss"
+)
+
+var ansiEscape = regexp.MustCompile("\x1b\\[[0-9;]*m")
+
+func getColorRGB(c lipgloss.TerminalColor) (uint8, uint8, uint8) {
+ r, g, b, a := c.RGBA()
+
+ // Un-premultiply alpha if needed
+ if a > 0 && a < 0xffff {
+ r = (r * 0xffff) / a
+ g = (g * 0xffff) / a
+ b = (b * 0xffff) / a
+ }
+
+ // Convert from 16-bit to 8-bit color
+ return uint8(r >> 8), uint8(g >> 8), uint8(b >> 8)
+}
+
+// ForceReplaceBackgroundWithLipgloss replaces any ANSI background color codes
+// in `input` with a single 24‑bit background (48;2;R;G;B).
+func ForceReplaceBackgroundWithLipgloss(input string, newBgColor lipgloss.TerminalColor) string {
+ // Precompute our new-bg sequence once
+ r, g, b := getColorRGB(newBgColor)
+ newBg := fmt.Sprintf("48;2;%d;%d;%d", r, g, b)
+
+ return ansiEscape.ReplaceAllStringFunc(input, func(seq string) string {
+ const (
+ escPrefixLen = 2 // "\x1b["
+ escSuffixLen = 1 // "m"
+ )
+
+ raw := seq
+ start := escPrefixLen
+ end := len(raw) - escSuffixLen
+
+ var sb strings.Builder
+ // reserve enough space: original content minus bg codes + our newBg
+ sb.Grow((end - start) + len(newBg) + 2)
+
+ // scan from start..end, token by token
+ for i := start; i < end; {
+ // find the next ';' or end
+ j := i
+ for j < end && raw[j] != ';' {
+ j++
+ }
+ token := raw[i:j]
+
+ // fast‑path: skip "48;5;N" or "48;2;R;G;B"
+ if len(token) == 2 && token[0] == '4' && token[1] == '8' {
+ k := j + 1
+ if k < end {
+ // find next token
+ l := k
+ for l < end && raw[l] != ';' {
+ l++
+ }
+ next := raw[k:l]
+ if next == "5" {
+ // skip "48;5;N"
+ m := l + 1
+ for m < end && raw[m] != ';' {
+ m++
+ }
+ i = m + 1
+ continue
+ } else if next == "2" {
+ // skip "48;2;R;G;B"
+ m := l + 1
+ for count := 0; count < 3 && m < end; count++ {
+ for m < end && raw[m] != ';' {
+ m++
+ }
+ m++
+ }
+ i = m
+ continue
+ }
+ }
+ }
+
+ // decide whether to keep this token
+ // manually parse ASCII digits to int
+ isNum := true
+ val := 0
+ for p := i; p < j; p++ {
+ c := raw[p]
+ if c < '0' || c > '9' {
+ isNum = false
+ break
+ }
+ val = val*10 + int(c-'0')
+ }
+ keep := !isNum ||
+ ((val < 40 || val > 47) && (val < 100 || val > 107) && val != 49)
+
+ if keep {
+ if sb.Len() > 0 {
+ sb.WriteByte(';')
+ }
+ sb.WriteString(token)
+ }
+ // advance past this token (and the semicolon)
+ i = j + 1
+ }
+
+ // append our new background
+ if sb.Len() > 0 {
+ sb.WriteByte(';')
+ }
+ sb.WriteString(newBg)
+
+ return "\x1b[" + sb.String() + "m"
+ })
+}
diff --git a/pkg/tui/styles/icons.go b/pkg/tui/styles/icons.go
new file mode 100644
index 000000000..6f9af6a5b
--- /dev/null
+++ b/pkg/tui/styles/icons.go
@@ -0,0 +1,12 @@
+package styles
+
+const (
+ OpenCodeIcon string = "ⓒ"
+
+ ErrorIcon string = "ⓔ"
+ WarningIcon string = "ⓦ"
+ InfoIcon string = "ⓘ"
+ HintIcon string = "ⓗ"
+ SpinnerIcon string = "⟳"
+ DocumentIcon string = "🖼"
+)
diff --git a/pkg/tui/styles/markdown.go b/pkg/tui/styles/markdown.go
new file mode 100644
index 000000000..9693cfbba
--- /dev/null
+++ b/pkg/tui/styles/markdown.go
@@ -0,0 +1,284 @@
+package styles
+
+import (
+ "github.com/charmbracelet/glamour"
+ "github.com/charmbracelet/glamour/ansi"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/tui/theme"
+)
+
+const defaultMargin = 1
+
+// Helper functions for style pointers
+func boolPtr(b bool) *bool { return &b }
+func stringPtr(s string) *string { return &s }
+func uintPtr(u uint) *uint { return &u }
+
+// returns a glamour TermRenderer configured with the current theme
+func GetMarkdownRenderer(width int) *glamour.TermRenderer {
+ r, _ := glamour.NewTermRenderer(
+ glamour.WithStyles(generateMarkdownStyleConfig()),
+ 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{
+ BlockPrefix: "",
+ BlockSuffix: "",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
+ },
+ Margin: uintPtr(defaultMargin),
+ },
+ BlockQuote: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(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(adaptiveColorToString(t.MarkdownText())),
+ },
+ },
+ },
+ Heading: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BlockSuffix: "\n",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
+ Bold: boolPtr(true),
+ },
+ },
+ H1: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "# ",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
+ Bold: boolPtr(true),
+ },
+ },
+ H2: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "## ",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
+ Bold: boolPtr(true),
+ },
+ },
+ H3: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "### ",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
+ Bold: boolPtr(true),
+ },
+ },
+ H4: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "#### ",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
+ Bold: boolPtr(true),
+ },
+ },
+ H5: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "##### ",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
+ Bold: boolPtr(true),
+ },
+ },
+ H6: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "###### ",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
+ Bold: boolPtr(true),
+ },
+ },
+ Strikethrough: ansi.StylePrimitive{
+ CrossedOut: boolPtr(true),
+ Color: stringPtr(adaptiveColorToString(t.TextMuted())),
+ },
+ Emph: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownEmph())),
+ Italic: boolPtr(true),
+ },
+ Strong: ansi.StylePrimitive{
+ Bold: boolPtr(true),
+ Color: stringPtr(adaptiveColorToString(t.MarkdownStrong())),
+ },
+ HorizontalRule: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownHorizontalRule())),
+ Format: "\n─────────────────────────────────────────\n",
+ },
+ Item: ansi.StylePrimitive{
+ BlockPrefix: "• ",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownListItem())),
+ },
+ Enumeration: ansi.StylePrimitive{
+ BlockPrefix: ". ",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownListEnumeration())),
+ },
+ Task: ansi.StyleTask{
+ StylePrimitive: ansi.StylePrimitive{},
+ Ticked: "[✓] ",
+ Unticked: "[ ] ",
+ },
+ Link: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownLink())),
+ Underline: boolPtr(true),
+ },
+ LinkText: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownLinkText())),
+ Bold: boolPtr(true),
+ },
+ Image: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownImage())),
+ Underline: boolPtr(true),
+ Format: "🖼 {{.text}}",
+ },
+ ImageText: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownImageText())),
+ Format: "{{.text}}",
+ },
+ Code: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownCode())),
+ Prefix: "",
+ Suffix: "",
+ },
+ },
+ CodeBlock: ansi.StyleCodeBlock{
+ StyleBlock: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: " ",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownCodeBlock())),
+ },
+ Margin: uintPtr(defaultMargin),
+ },
+ Chroma: &ansi.Chroma{
+ Text: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
+ },
+ Error: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.Error())),
+ },
+ Comment: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxComment())),
+ },
+ CommentPreproc: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
+ },
+ Keyword: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
+ },
+ KeywordReserved: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
+ },
+ KeywordNamespace: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
+ },
+ KeywordType: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxType())),
+ },
+ Operator: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxOperator())),
+ },
+ Punctuation: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxPunctuation())),
+ },
+ Name: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxVariable())),
+ },
+ NameBuiltin: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxVariable())),
+ },
+ NameTag: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
+ },
+ NameAttribute: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxFunction())),
+ },
+ NameClass: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxType())),
+ },
+ NameConstant: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxVariable())),
+ },
+ NameDecorator: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxFunction())),
+ },
+ NameFunction: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxFunction())),
+ },
+ LiteralNumber: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxNumber())),
+ },
+ LiteralString: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxString())),
+ },
+ LiteralStringEscape: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
+ },
+ GenericDeleted: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.DiffRemoved())),
+ },
+ GenericEmph: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownEmph())),
+ Italic: boolPtr(true),
+ },
+ GenericInserted: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.DiffAdded())),
+ },
+ GenericStrong: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownStrong())),
+ Bold: boolPtr(true),
+ },
+ GenericSubheading: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(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(adaptiveColorToString(t.MarkdownLinkText())),
+ },
+ Text: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
+ },
+ Paragraph: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
+ },
+ },
+ }
+}
+
+// adaptiveColorToString converts a lipgloss.AdaptiveColor to the appropriate
+// hex color string based on the current terminal background
+func adaptiveColorToString(color lipgloss.AdaptiveColor) string {
+ if lipgloss.HasDarkBackground() {
+ return color.Dark
+ }
+ return color.Light
+}
diff --git a/pkg/tui/styles/styles.go b/pkg/tui/styles/styles.go
new file mode 100644
index 000000000..d1b5a92bf
--- /dev/null
+++ b/pkg/tui/styles/styles.go
@@ -0,0 +1,159 @@
+package styles
+
+import (
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/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()
+}
+
+func Muted() lipgloss.Style {
+ return lipgloss.NewStyle().Foreground(theme.CurrentTheme().TextMuted())
+}
+
+// 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() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().Primary()
+}
+
+// SecondaryColor returns the secondary color from the current theme
+func SecondaryColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().Secondary()
+}
+
+// AccentColor returns the accent color from the current theme
+func AccentColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().Accent()
+}
+
+// ErrorColor returns the error color from the current theme
+func ErrorColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().Error()
+}
+
+// WarningColor returns the warning color from the current theme
+func WarningColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().Warning()
+}
+
+// SuccessColor returns the success color from the current theme
+func SuccessColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().Success()
+}
+
+// InfoColor returns the info color from the current theme
+func InfoColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().Info()
+}
+
+// TextColor returns the text color from the current theme
+func TextColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().Text()
+}
+
+// TextMutedColor returns the muted text color from the current theme
+func TextMutedColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().TextMuted()
+}
+
+// TextEmphasizedColor returns the emphasized text color from the current theme
+func TextEmphasizedColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().TextEmphasized()
+}
+
+// BackgroundColor returns the background color from the current theme
+func BackgroundColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().Background()
+}
+
+// BackgroundSecondaryColor returns the secondary background color from the current theme
+func BackgroundSecondaryColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().BackgroundSecondary()
+}
+
+// BackgroundDarkerColor returns the darker background color from the current theme
+func BackgroundDarkerColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().BackgroundDarker()
+}
+
+// BorderNormalColor returns the normal border color from the current theme
+func BorderNormalColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().BorderNormal()
+}
+
+// BorderFocusedColor returns the focused border color from the current theme
+func BorderFocusedColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().BorderFocused()
+}
+
+// BorderDimColor returns the dim border color from the current theme
+func BorderDimColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().BorderDim()
+}