diff --git a/internal/editor/model_builder.go b/internal/editor/model_builder.go index 9e8c920..2b1b85a 100644 --- a/internal/editor/model_builder.go +++ b/internal/editor/model_builder.go @@ -15,6 +15,7 @@ type ModelBuilder struct { // NewModelBuilder: Builds and returns a new model, using the default color scheme (kanagawa-wave). func NewModelBuilder() *ModelBuilder { editorStyles := style.DefaultStyles() + editorTheme := themes.NewDefaultTheme() return &ModelBuilder{ model: Model{ @@ -34,8 +35,8 @@ func NewModelBuilder() *ModelBuilder { settings: core.NewDefaultSettings(), registers: core.DefaultRegisters(), styles: editorStyles, - syntax: syntax.NewTreeSitterEngine(editorStyles), - theme: themes.NewDefaultTheme(), + syntax: syntax.NewTreeSitterEngine(editorTheme), + theme: editorTheme, }, } } diff --git a/internal/editor/view.go b/internal/editor/view.go index a47ff6c..f13cd72 100644 --- a/internal/editor/view.go +++ b/internal/editor/view.go @@ -31,8 +31,8 @@ func (m Model) View() string { // Draw window view := viewWindow(win, t, options, m.Mode(), m.Syntax()) - // Command bar is seperate - cmdBar := drawCommandBar(m) + // Command bar is separate + cmdBar := drawCommandBar(m, t) view += cmdBar // Handle command output, draw on top @@ -225,38 +225,37 @@ func rightBar(w *core.Window, mode core.Mode, t theme.EditorTheme) (bar string) // drawCommandBar: Renders the command line showing command input, errors, or // output depending on the current mode and state. -func drawCommandBar(m Model) string { - styles := m.Styles() +func drawCommandBar(m Model, t theme.EditorTheme) string { // Compute left bar (command side) var leftBar string if m.Mode() == core.CommandMode { - leftBar = styles.LineStyle.Render(":") + leftBar = t.Line.Render(":") cmd := []rune(m.Command()) cur := m.CommandCursor() for i, r := range cmd { if i == cur { - leftBar += styles.DefaultCursorStyle(m.Mode()).Render(string(r)) + leftBar += t.DefaultCursor(m.Mode()).Render(string(r)) } else { - leftBar += styles.LineStyle.Render(string(r)) + leftBar += t.Line.Render(string(r)) } } // Cursor at end of command if cur >= len(cmd) { - leftBar += styles.DefaultCursorStyle(m.Mode()).Render(" ") + leftBar += t.DefaultCursor(m.Mode()).Render(" ") } // bar = fmt.Sprintf("%s %d", bar, cur) } else if out := m.CommandOutput(); out != nil && len(out.Lines) > 0 && out.Inline { // TODO: This is not perfect, temporary text := strings.Join(out.Lines, " ") if out.IsError { - leftBar = styles.CommandError.Render(text) + leftBar = t.CommandLine.Error.Render(text) } else { - leftBar = styles.LineStyle.Render(text) + leftBar = t.Line.Render(text) } } else if strings.TrimSpace(m.Command()) != "" { content := fmt.Sprintf(":%s", m.Command()) - leftBar = styles.LineStyle.Render(content) // + leftBar = t.Line.Render(content) } // Compute right bar @@ -265,12 +264,12 @@ func drawCommandBar(m Model) string { if len(m.input.Pending()) > 0 { width := 10 // Size of the block to display content := fmt.Sprintf("%-*s", width, m.input.Pending()) - rightBar = styles.LineStyle.Render(content) + rightBar = t.Line.Render(content) } dif := m.termWidth - (lipgloss.Width(leftBar) + lipgloss.Width(rightBar)) - bar := leftBar + strings.Repeat(styles.BackgroundStyle.Render(" "), max(0, dif)) + rightBar + bar := leftBar + strings.Repeat(t.Background.Render(" "), max(0, dif)) + rightBar return bar } diff --git a/internal/syntax/treesitter.go b/internal/syntax/treesitter.go index 258f948..adf8ba6 100644 --- a/internal/syntax/treesitter.go +++ b/internal/syntax/treesitter.go @@ -5,7 +5,7 @@ import ( "sort" "git.gophernest.net/azpect/TextEditor/internal/core" - "git.gophernest.net/azpect/TextEditor/internal/style" + "git.gophernest.net/azpect/TextEditor/internal/theme" "github.com/charmbracelet/lipgloss" sitter "github.com/tree-sitter/go-tree-sitter" ) @@ -20,8 +20,8 @@ import ( // // Cached styles are represented as one style per rune for each line. type TreeSitterEngine struct { - styles style.Styles - registry *languageRegistry + editorTheme theme.EditorTheme + registry *languageRegistry cache map[*core.Buffer]*bufferCache } @@ -72,11 +72,11 @@ type captureRange struct { // // Language support is resolved through the language registry, so the engine can // work with any language/query pair registered there. -func NewTreeSitterEngine(styles style.Styles) *TreeSitterEngine { +func NewTreeSitterEngine(t theme.EditorTheme) *TreeSitterEngine { return &TreeSitterEngine{ - styles: styles, - registry: newLanguageRegistry(), - cache: map[*core.Buffer]*bufferCache{}, + editorTheme: t, + registry: newLanguageRegistry(), + cache: map[*core.Buffer]*bufferCache{}, } } @@ -138,7 +138,7 @@ func (e *TreeSitterEngine) LineStyleMap(buf *core.Buffer, line int) []lipgloss.S runes := []rune(buf.Line(line)) out := make([]lipgloss.Style, len(runes)) for i := range out { - out[i] = e.styles.LineStyle + out[i] = e.editorTheme.Line } bc.lines[line] = out return out @@ -312,13 +312,13 @@ func (e *TreeSitterEngine) buildFullBuffer(buf *core.Buffer, bc *bufferCache) { if fullRebuild { bc.lines = map[int][]lipgloss.Style{} for i := range lineCount { - bc.lines[i] = defaultLineStyles(lines[i], e.styles.LineStyle) + bc.lines[i] = defaultLineStyles(lines[i], e.editorTheme.Line) } } else { dirty := normalizedDirtyRanges(bc.dirty, lineCount) for _, r := range dirty { for i := r.start; i <= r.end; i++ { - bc.lines[i] = defaultLineStyles(lines[i], e.styles.LineStyle) + bc.lines[i] = defaultLineStyles(lines[i], e.editorTheme.Line) } } } @@ -397,7 +397,7 @@ func (e *TreeSitterEngine) buildFullBuffer(buf *core.Buffer, bc *bufferCache) { // rewrites. targetDirty := normalizedDirtyRanges(bc.dirty, lineCount) for _, c := range captures { - sty := style.CaptureStyle(e.styles.LineStyle, c.name) + sty := e.editorTheme.CaptureStyle(c.name) for row := c.startRow; row <= c.endRow; row++ { if int(row) >= len(lines) { break diff --git a/internal/theme/theme.go b/internal/theme/theme.go index 26263c2..86d4bc7 100644 --- a/internal/theme/theme.go +++ b/internal/theme/theme.go @@ -1,6 +1,8 @@ package theme import ( + "strings" + "git.gophernest.net/azpect/TextEditor/internal/core" "github.com/charmbracelet/lipgloss" ) @@ -13,6 +15,7 @@ type EditorTheme struct { CommandLine CommandLineTheme Line lipgloss.Style Background lipgloss.Style + Syntax SyntaxTheme } type CursorTheme struct { @@ -37,6 +40,11 @@ type CommandLineTheme struct { ContinueMessage lipgloss.Style } +type SyntaxTheme struct { + Exact map[string]lipgloss.Style + Group map[string]lipgloss.Style +} + func (t EditorTheme) Cursor(mode core.Mode, textStyle lipgloss.Style) lipgloss.Style { bg := textStyle.GetBackground() fg := textStyle.GetForeground() @@ -75,3 +83,23 @@ func (t EditorTheme) VisualHighlightWithTextColor(textStyle lipgloss.Style) lipg return t.VisualHightlight. Foreground(textStyle.GetForeground()) } + +// Use base (Line) as fallback. Every style will use the background from the base (Line). +// +// NOTE: Maybe we keep background on the mapping? Not sure for now +func (t EditorTheme) CaptureStyle(capture string) lipgloss.Style { + base := t.Line + + exact := strings.ToLower(strings.TrimSpace(capture)) + group := strings.Split(exact, ".")[0] + + if sty, ok := t.Syntax.Exact[exact]; ok { + return sty.Background(base.GetBackground()) + } + + if sty, ok := t.Syntax.Group[group]; ok { + return sty.Background(base.GetBackground()) + } + + return base +} diff --git a/internal/theme/themes/default.go b/internal/theme/themes/default.go index f368803..7e584d3 100644 --- a/internal/theme/themes/default.go +++ b/internal/theme/themes/default.go @@ -1,12 +1,14 @@ package themes import ( + "strings" + "git.gophernest.net/azpect/TextEditor/internal/theme" "github.com/charmbracelet/lipgloss" ) -const background = lipgloss.Color("#1f2335") -const foreground = lipgloss.Color("#dcd7ba") +const background = lipgloss.Color("#111418") +const foreground = lipgloss.Color("#d4d8e1") func NewDefaultTheme() theme.EditorTheme { hightlight := lipgloss.NewStyle(). @@ -27,6 +29,7 @@ func NewDefaultTheme() theme.EditorTheme { CommandLine: newDefaultCommandLineTheme(), Line: line, Background: background, + Syntax: newDefaultSyntaxTheme(), } } @@ -51,19 +54,19 @@ func newDefaultCursorTheme() theme.CursorTheme { func newDefaultGutterTheme() theme.GutterTheme { base := lipgloss.NewStyle(). - Background(lipgloss.Color("#181b2a")). - Foreground(lipgloss.Color("#7e8399")) + Background(lipgloss.Color("#0d1014")). + Foreground(lipgloss.Color("#6b7280")) return theme.GutterTheme{ Default: base, - CurrentLine: base.Foreground(lipgloss.Color("#f6c384")), + CurrentLine: base.Foreground(lipgloss.Color("#c0c8d8")), } } func newDefaultStatusBarTheme() theme.StatusBarTheme { bar := lipgloss.NewStyle(). - Background(lipgloss.Color("#181b2a")). - Foreground(lipgloss.Color("#8ea4a2")) + Background(lipgloss.Color("#0d1014")). + Foreground(lipgloss.Color("#8f99aa")) return theme.StatusBarTheme{ Default: bar, @@ -76,8 +79,118 @@ func newDefaultCommandLineTheme() theme.CommandLineTheme { Background(background) return theme.CommandLineTheme{ - Error: base.Foreground(lipgloss.Color("#e82424")), - OutputBorder: base.Background(lipgloss.Color("#11131d")), - ContinueMessage: base.Foreground(lipgloss.Color("#7aa2f7")), + Error: base.Foreground(lipgloss.Color("#bf616a")), + OutputBorder: base.Background(lipgloss.Color("#0d1014")), + ContinueMessage: base.Foreground(lipgloss.Color("#81a1c1")), } } + +func newDefaultSyntaxTheme() theme.SyntaxTheme { + exact := map[string]lipgloss.Style{ + "attribute.builtin": color("#ebcb8b"), + "character.special": color("#d08770"), + "comment.documentation": color("#8f99aa"), + "constant.builtin": color("#88c0d0"), + "constant.macro": color("#d08770"), + "function.builtin": color("#88c0d0"), + "function.call": color("#81a1c1"), + "function.macro": color("#d08770"), + "function.method": color("#81a1c1"), + "function.method.call": color("#81a1c1"), + "keyword.conditional": color("#b48ead"), + "keyword.conditional.ternary": color("#b48ead"), + "keyword.coroutine": color("#b48ead"), + "keyword.debug": color("#b48ead"), + "keyword.directive": color("#b48ead"), + "keyword.directive.define": color("#b48ead"), + "keyword.exception": color("#b48ead"), + "keyword.function": color("#b48ead"), + "keyword.import": color("#b48ead"), + "keyword.modifier": color("#b48ead"), + "keyword.operator": color("#d08770"), + "keyword.repeat": color("#b48ead"), + "keyword.return": color("#b48ead"), + "keyword.type": color("#ebcb8b"), + "markup.heading": color("#ebcb8b"), + "markup.heading.1": color("#ebcb8b"), + "markup.heading.2": color("#e5c68a"), + "markup.heading.3": color("#ddbe88"), + "markup.heading.4": color("#d5b686"), + "markup.heading.5": color("#cdaf84"), + "markup.heading.6": color("#c5a883"), + "markup.italic": color("#c0c8d8"), + "markup.link.label": color("#81a1c1"), + "markup.raw": color("#a3be8c"), + "markup.strikethrough": color("#7f8795"), + "markup.strong": color("#ebcb8b"), + "markup.underline": color("#88c0d0"), + "module.builtin": color("#88c0d0"), + "number.float": color("#88c0d0"), + "punctuation.bracket": color("#9aa4b2"), + "punctuation.delimiter": color("#9aa4b2"), + "punctuation.special": color("#d08770"), + "string.documentation": color("#a3be8c"), + "string.escape": color("#d08770"), + "string.regexp": color("#88c0d0"), + "string.special.path": color("#a3be8c"), + "string.special.symbol": color("#88c0d0"), + "string.special.url": color("#88c0d0"), + "tag.attribute": color("#ebcb8b"), + "tag.attribute.url": color("#88c0d0"), + "tag.builtin": color("#81a1c1"), + "tag.delimiter": color("#9aa4b2"), + "type.builtin": color("#ebcb8b"), + "type.definition": color("#ebcb8b"), + "variable.builtin": color("#8fbcbb"), + "variable.member": color("#c0c8d8"), + "variable.parameter": color("#c0c8d8"), + } + + group := map[string]lipgloss.Style{ + "attribute": color("#ebcb8b"), + "boolean": color("#88c0d0"), + "character": color("#a3be8c"), + "charset": color("#ebcb8b"), + "comment": color("#7f8795"), + "conceal": color("#7f8795"), + "constant": color("#88c0d0"), + "constructor": color("#ebcb8b"), + "error": color("#bf616a"), + "function": color("#81a1c1"), + "import": color("#b48ead"), + "interface": color("#ebcb8b"), + "keyframes": color("#d08770"), + "keyword": color("#b48ead"), + "label": color("#d08770"), + "media": color("#d08770"), + "module": color("#81a1c1"), + "namespace": color("#81a1c1"), + "none": color("#d4d8e1"), + "nospell": color("#d4d8e1"), + "number": color("#88c0d0"), + "operator": color("#9aa4b2"), + "property": color("#c0c8d8"), + "spell": color("#d4d8e1"), + "string": color("#a3be8c"), + "supports": color("#d08770"), + "tag": color("#81a1c1"), + "type": color("#ebcb8b"), + "variable": color("#d4d8e1"), + } + + return theme.SyntaxTheme{ + Exact: exact, + Group: group, + } +} + +// Simple helper to create a lipgloss style with the provided foreground +func color(c string) lipgloss.Style { + col := foreground + if strings.TrimSpace(c) != "" { + col = lipgloss.Color(c) + } + return lipgloss.NewStyle(). + Background(background). + Foreground(col) +}