diff options
| author | Dax Raad <[email protected]> | 2025-05-18 22:30:41 -0400 |
|---|---|---|
| committer | Dax Raad <[email protected]> | 2025-05-26 12:40:17 -0400 |
| commit | 99af6146d5def31c59993636d60eb75a483a283b (patch) | |
| tree | eb57ed227c15cf9be54bc9f231c83895d812f881 /pkg/tui/styles | |
| parent | 020e0ca039287b73fa33041fbd1bb214e6ccb396 (diff) | |
| download | opencode-99af6146d5def31c59993636d60eb75a483a283b.tar.gz opencode-99af6146d5def31c59993636d60eb75a483a283b.zip | |
openapi
Diffstat (limited to 'pkg/tui/styles')
| -rw-r--r-- | pkg/tui/styles/background.go | 123 | ||||
| -rw-r--r-- | pkg/tui/styles/icons.go | 12 | ||||
| -rw-r--r-- | pkg/tui/styles/markdown.go | 284 | ||||
| -rw-r--r-- | pkg/tui/styles/styles.go | 159 |
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() +} |
