From 273be90d42923a20fdb1d010134ad5bce80a553b Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Wed, 8 Apr 2026 11:59:49 -0700 Subject: [PATCH] feat: HUGE refactor of colorschemes, untested. Now we can load them in via JSON files at launch time. They are embded in the final exe though... --- cmd/theme-loader/main.go | 28 ++++ internal/action/interface.go | 10 +- internal/action/mock.go | 67 +++++--- internal/command/handlers.go | 24 +-- internal/editor/model.go | 40 ++++- internal/editor/model_builder.go | 14 +- internal/editor/view.go | 6 +- internal/syntax/engine.go | 5 +- internal/syntax/treesitter.go | 26 ++-- internal/syntax/treesitter_behavior_test.go | 56 ++++--- internal/syntax/treesitter_bench_test.go | 7 +- internal/syntax/treesitter_engine_test.go | 12 +- internal/syntax/treesitter_sequences_test.go | 16 +- internal/theme/loader.go | 154 +++++++++++++++++++ internal/theme/theme_json.go | 53 +++++++ internal/theme/themes/README.md | 81 ++++++++++ internal/theme/themes/kanagawa-dragon.json | 116 ++++++++++++++ internal/theme/themes/kanagawa-dragon.xml | 83 ---------- internal/theme/themes/kanagawa-lotus.json | 116 ++++++++++++++ internal/theme/themes/kanagawa-lotus.xml | 83 ---------- internal/theme/themes/kanagawa-wave.xml | 83 ---------- internal/theme/themes/kanagawa.json | 153 ++++++++++++++++++ 22 files changed, 883 insertions(+), 350 deletions(-) create mode 100644 cmd/theme-loader/main.go create mode 100644 internal/theme/loader.go create mode 100644 internal/theme/theme_json.go create mode 100644 internal/theme/themes/README.md create mode 100644 internal/theme/themes/kanagawa-dragon.json delete mode 100644 internal/theme/themes/kanagawa-dragon.xml create mode 100644 internal/theme/themes/kanagawa-lotus.json delete mode 100644 internal/theme/themes/kanagawa-lotus.xml delete mode 100644 internal/theme/themes/kanagawa-wave.xml create mode 100644 internal/theme/themes/kanagawa.json diff --git a/cmd/theme-loader/main.go b/cmd/theme-loader/main.go new file mode 100644 index 0000000..5f8919b --- /dev/null +++ b/cmd/theme-loader/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "sort" + + "git.gophernest.net/azpect/TextEditor/internal/theme" +) + +func main() { + themes, err := theme.LoadEmbeddedThemesJSON() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", themes) + + names := make([]string, 0, len(themes)) + for name := range themes { + names = append(names, name) + } + sort.Strings(names) + + fmt.Printf("loaded %d embedded themes:\n", len(names)) + for _, name := range names { + fmt.Printf("- %s\n", name) + } +} diff --git a/internal/action/interface.go b/internal/action/interface.go index acabf24..095d608 100644 --- a/internal/action/interface.go +++ b/internal/action/interface.go @@ -54,8 +54,14 @@ type Model interface { Settings() core.EditorSettings SetSettings(s core.EditorSettings) - Theme() theme.EditorTheme - SetTheme(t theme.EditorTheme) + + // ================================================== + // Themes + // ================================================== + Theme() (string, theme.EditorTheme) + SetTheme(name string) + Themes() map[string]theme.EditorTheme + SetThemes(t map[string]theme.EditorTheme) // ================================================== // Registers diff --git a/internal/action/mock.go b/internal/action/mock.go index e33c6e9..6380b1b 100644 --- a/internal/action/mock.go +++ b/internal/action/mock.go @@ -23,7 +23,8 @@ type MockModel struct { CommandHistoryList []string CommandHistoryCur int LastFindVal core.LastFindCommand - ThemeVal theme.EditorTheme + CurrentThemeName string + ThemesMap map[string]theme.EditorTheme LastChangeKeysList []string } @@ -40,12 +41,14 @@ func NewMockModel() *MockModel { Build() return &MockModel{ - WindowsList: []*core.Window{&win}, - ActiveWindowVal: &win, - BuffersList: []*core.Buffer{&buf}, - SettingsVal: core.NewDefaultSettings(), - ModeVal: core.NormalMode, - RegistersMap: core.DefaultRegisters(), + WindowsList: []*core.Window{&win}, + ActiveWindowVal: &win, + BuffersList: []*core.Buffer{&buf}, + SettingsVal: core.NewDefaultSettings(), + ModeVal: core.NormalMode, + RegistersMap: core.DefaultRegisters(), + CurrentThemeName: "default", + ThemesMap: map[string]theme.EditorTheme{"default": {}}, } } @@ -58,24 +61,28 @@ func NewMockModelWithBuffer(buf *core.Buffer) *MockModel { Build() return &MockModel{ - WindowsList: []*core.Window{&win}, - ActiveWindowVal: &win, - BuffersList: []*core.Buffer{buf}, - SettingsVal: core.NewDefaultSettings(), - ModeVal: core.NormalMode, - RegistersMap: core.DefaultRegisters(), + WindowsList: []*core.Window{&win}, + ActiveWindowVal: &win, + BuffersList: []*core.Buffer{buf}, + SettingsVal: core.NewDefaultSettings(), + ModeVal: core.NormalMode, + RegistersMap: core.DefaultRegisters(), + CurrentThemeName: "default", + ThemesMap: map[string]theme.EditorTheme{"default": {}}, } } // NewMockModelWithWindow creates a mock with a custom window. func NewMockModelWithWindow(win *core.Window) *MockModel { return &MockModel{ - WindowsList: []*core.Window{win}, - ActiveWindowVal: win, - BuffersList: []*core.Buffer{win.Buffer}, - SettingsVal: core.NewDefaultSettings(), - ModeVal: core.NormalMode, - RegistersMap: core.DefaultRegisters(), + WindowsList: []*core.Window{win}, + ActiveWindowVal: win, + BuffersList: []*core.Buffer{win.Buffer}, + SettingsVal: core.NewDefaultSettings(), + ModeVal: core.NormalMode, + RegistersMap: core.DefaultRegisters(), + CurrentThemeName: "default", + ThemesMap: map[string]theme.EditorTheme{"default": {}}, } } @@ -117,8 +124,26 @@ func (m *MockModel) Mode() core.Mode { return m.ModeVal } func (m *MockModel) SetMode(mode core.Mode) { m.ModeVal = mode } func (m *MockModel) Settings() core.EditorSettings { return m.SettingsVal } func (m *MockModel) SetSettings(s core.EditorSettings) { m.SettingsVal = s } -func (m *MockModel) Theme() theme.EditorTheme { return m.ThemeVal } -func (m *MockModel) SetTheme(t theme.EditorTheme) { m.ThemeVal = t } +func (m *MockModel) Theme() (string, theme.EditorTheme) { + if m.ThemesMap != nil { + if t, ok := m.ThemesMap[m.CurrentThemeName]; ok { + return m.CurrentThemeName, t + } + if t, ok := m.ThemesMap["default"]; ok { + return "default", t + } + } + + return m.CurrentThemeName, theme.EditorTheme{} +} +func (m *MockModel) SetTheme(name string) { m.CurrentThemeName = name } +func (m *MockModel) Themes() map[string]theme.EditorTheme { + if m.ThemesMap == nil { + m.ThemesMap = map[string]theme.EditorTheme{} + } + return m.ThemesMap +} +func (m *MockModel) SetThemes(t map[string]theme.EditorTheme) { m.ThemesMap = t } // Registers func (m *MockModel) Registers() map[rune]core.Register { return m.RegistersMap } diff --git a/internal/command/handlers.go b/internal/command/handlers.go index 6dbc4a2..2a0d751 100644 --- a/internal/command/handlers.go +++ b/internal/command/handlers.go @@ -887,8 +887,9 @@ func cmdColorscheme(m action.Model, args []string, force bool) tea.Cmd { // No args, just print the current scheme if len(args) == 0 { + name, _ := m.Theme() m.SetCommandOutput(&core.CommandOutput{ - Lines: []string{"default"}, + Lines: []string{name}, Inline: true, IsError: false, }) @@ -897,15 +898,9 @@ func cmdColorscheme(m action.Model, args []string, force bool) tea.Cmd { // Theme switching is disabled while migrating away from Chroma. name := strings.TrimSpace(strings.Join(args, " ")) - if name == "" { - m.SetCommandOutput(&core.CommandOutput{ - Lines: []string{"colorscheme not found: "}, - Inline: true, - IsError: true, - }) - return nil - } - if name != "" && strings.ToLower(name) != "default" { + _, found := m.Themes()[name] + + if name == "" || !found { m.SetCommandOutput(&core.CommandOutput{ Lines: []string{fmt.Sprintf("colorscheme not found: %s", name)}, Inline: true, @@ -914,14 +909,19 @@ func cmdColorscheme(m action.Model, args []string, force bool) tea.Cmd { return nil } - // m.SetStyles(style.DefaultStyles()) + m.SetTheme(name) return nil } func cmdListColorschemes(m action.Model, args []string, force bool) tea.Cmd { _, _ = args, force - colors := []string{"default"} + var colors []string + for k := range m.Themes() { + colors = append(colors, k) + } + + slices.Sort(colors) m.SetMode(core.CommandOutputMode) m.SetCommandOutput(&core.CommandOutput{ diff --git a/internal/editor/model.go b/internal/editor/model.go index 9dcd388..edf4f47 100644 --- a/internal/editor/model.go +++ b/internal/editor/model.go @@ -51,8 +51,9 @@ type Model struct { registers map[rune]core.Register // name -> register // Visual styles - theme theme.EditorTheme - syntax syntax.Engine + currentTheme string // Name of current theme + themes map[string]theme.EditorTheme + syntax syntax.Engine // Dot operator state lastChangeKeys []string @@ -341,12 +342,39 @@ func (m *Model) SetSettings(s core.EditorSettings) { m.settings = s } -func (m *Model) Theme() theme.EditorTheme { - return m.theme +// ================================================== +// Themes +// ================================================== +func (m *Model) Theme() (string, theme.EditorTheme) { + t, ok := m.themes[m.currentTheme] + if ok { + return m.currentTheme, t + } + return "default", m.themes["default"] } -func (m *Model) SetTheme(t theme.EditorTheme) { - m.theme = t +func (m *Model) SetTheme(name string) { + m.currentTheme = name + + if m.syntax == nil { + return + } + + // Need to invalidate the buffers to force a redraw + for _, buf := range m.buffers { + if buf == nil { + continue + } + m.syntax.InvalidateBuffer(buf) + } +} + +func (m *Model) Themes() map[string]theme.EditorTheme { + return m.themes +} + +func (m *Model) SetThemes(t map[string]theme.EditorTheme) { + m.themes = t } func (m *Model) Syntax() syntax.Engine { diff --git a/internal/editor/model_builder.go b/internal/editor/model_builder.go index f0af1a5..1f02664 100644 --- a/internal/editor/model_builder.go +++ b/internal/editor/model_builder.go @@ -4,6 +4,7 @@ import ( "git.gophernest.net/azpect/TextEditor/internal/core" "git.gophernest.net/azpect/TextEditor/internal/input" "git.gophernest.net/azpect/TextEditor/internal/syntax" + "git.gophernest.net/azpect/TextEditor/internal/theme" "git.gophernest.net/azpect/TextEditor/internal/theme/themes" ) @@ -15,6 +16,16 @@ type ModelBuilder struct { func NewModelBuilder() *ModelBuilder { editorTheme := themes.NewDefaultTheme() + // Embed the themes + var embededThemes map[string]theme.EditorTheme + embededThemesJson, err := theme.LoadEmbeddedThemesJSON() + if err == nil { + embededThemes = theme.MapEmbededThemeToEditorTheme(embededThemesJson) + } + + // Always have a default theme + embededThemes["default"] = themes.NewDefaultTheme() + return &ModelBuilder{ model: Model{ buffers: []*core.Buffer{}, @@ -33,7 +44,8 @@ func NewModelBuilder() *ModelBuilder { settings: core.NewDefaultSettings(), registers: core.DefaultRegisters(), syntax: syntax.NewTreeSitterEngine(editorTheme), - theme: editorTheme, + currentTheme: "default", + themes: embededThemes, }, } } diff --git a/internal/editor/view.go b/internal/editor/view.go index f13cd72..3cd4888 100644 --- a/internal/editor/view.go +++ b/internal/editor/view.go @@ -21,7 +21,7 @@ func (m Model) View() string { // Each window has its own line numbers and gutter // Each window has its own status bar and mode - t := m.Theme() + _, t := m.Theme() options := win.Options // Adjust gutter to fit line len @@ -51,7 +51,7 @@ func viewWindow(w *core.Window, t theme.EditorTheme, options core.WinOptions, mo buf := w.Buffer var view strings.Builder if sx != nil { - sx.PrepareBuffer(buf) + sx.PrepareBuffer(buf, t) } // Compute window size (y) @@ -63,7 +63,7 @@ func viewWindow(w *core.Window, t theme.EditorTheme, options core.WinOptions, mo if lineNum < buf.LineCount() { styleMap := make([]lipgloss.Style, len([]rune(buf.Line(lineNum)))) if sx != nil { - styleMap = sx.LineStyleMap(buf, lineNum) + styleMap = sx.LineStyleMap(buf, lineNum, t) } line := drawLine(w, t, options, mode, buf.Line(lineNum), lineNum, styleMap) view.WriteString(line) diff --git a/internal/syntax/engine.go b/internal/syntax/engine.go index 674af74..b8f9d77 100644 --- a/internal/syntax/engine.go +++ b/internal/syntax/engine.go @@ -2,6 +2,7 @@ package syntax import ( "git.gophernest.net/azpect/TextEditor/internal/core" + "git.gophernest.net/azpect/TextEditor/internal/theme" "github.com/charmbracelet/lipgloss" ) @@ -11,13 +12,13 @@ import ( // directly. type Engine interface { // Engine.PrepareBuffer: Ensure syntax state for a buffer is ready. - PrepareBuffer(buf *core.Buffer) + PrepareBuffer(buf *core.Buffer, t theme.EditorTheme) // Engine.ApplyEdit: Apply an incremental text edit to syntax state. ApplyEdit(buf *core.Buffer, edit *core.BufferEdit) // Engine.LineStyleMap: Returns per-rune styles for a line. - LineStyleMap(buf *core.Buffer, line int) []lipgloss.Style + LineStyleMap(buf *core.Buffer, line int, t theme.EditorTheme) []lipgloss.Style // Engine.InvalidateBuffer: Marks all syntax state for a buffer as stale. InvalidateBuffer(buf *core.Buffer) diff --git a/internal/syntax/treesitter.go b/internal/syntax/treesitter.go index adf8ba6..05ac49d 100644 --- a/internal/syntax/treesitter.go +++ b/internal/syntax/treesitter.go @@ -20,8 +20,7 @@ import ( // // Cached styles are represented as one style per rune for each line. type TreeSitterEngine struct { - editorTheme theme.EditorTheme - registry *languageRegistry + registry *languageRegistry cache map[*core.Buffer]*bufferCache } @@ -74,9 +73,8 @@ type captureRange struct { // work with any language/query pair registered there. func NewTreeSitterEngine(t theme.EditorTheme) *TreeSitterEngine { return &TreeSitterEngine{ - editorTheme: t, - registry: newLanguageRegistry(), - cache: map[*core.Buffer]*bufferCache{}, + registry: newLanguageRegistry(), + cache: map[*core.Buffer]*bufferCache{}, } } @@ -88,7 +86,7 @@ func NewTreeSitterEngine(t theme.EditorTheme) *TreeSitterEngine { // // If the buffer language is unsupported or resolution fails, it still marks the // cache as built with an empty style map so callers can safely continue. -func (e *TreeSitterEngine) PrepareBuffer(buf *core.Buffer) { +func (e *TreeSitterEngine) PrepareBuffer(buf *core.Buffer, t theme.EditorTheme) { // Cannot prepare a nil buffer if buf == nil { return @@ -115,7 +113,7 @@ func (e *TreeSitterEngine) PrepareBuffer(buf *core.Buffer) { } _ = lang - e.buildFullBuffer(buf, bc) + e.buildFullBuffer(buf, bc, t) } // LineStyleMap returns the style row for a specific line in buf. @@ -123,12 +121,12 @@ func (e *TreeSitterEngine) PrepareBuffer(buf *core.Buffer) { // It first guarantees buffer preparation, then returns cached styles when // available. Missing lines are lazily initialized to the base line style and // stored in cache to keep downstream rendering logic simple. -func (e *TreeSitterEngine) LineStyleMap(buf *core.Buffer, line int) []lipgloss.Style { +func (e *TreeSitterEngine) LineStyleMap(buf *core.Buffer, line int, t theme.EditorTheme) []lipgloss.Style { if buf == nil { return nil } - e.PrepareBuffer(buf) + e.PrepareBuffer(buf, t) bc := e.getCache(buf) if s, ok := bc.lines[line]; ok { @@ -138,7 +136,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.editorTheme.Line + out[i] = t.Line } bc.lines[line] = out return out @@ -297,7 +295,7 @@ func (e *TreeSitterEngine) getCache(buf *core.Buffer) *bufferCache { // It (re)parses source when needed, collects query captures, sorts captures by // precedence order, then writes styles onto per-rune line slices. After a // successful pass it clears dirty flags and marks the cache as built. -func (e *TreeSitterEngine) buildFullBuffer(buf *core.Buffer, bc *bufferCache) { +func (e *TreeSitterEngine) buildFullBuffer(buf *core.Buffer, bc *bufferCache, t theme.EditorTheme) { lineCount := buf.LineCount() // Load the lines into memory. There is no method for this due to the buffers @@ -312,13 +310,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.editorTheme.Line) + bc.lines[i] = defaultLineStyles(lines[i], t.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.editorTheme.Line) + bc.lines[i] = defaultLineStyles(lines[i], t.Line) } } } @@ -397,7 +395,7 @@ func (e *TreeSitterEngine) buildFullBuffer(buf *core.Buffer, bc *bufferCache) { // rewrites. targetDirty := normalizedDirtyRanges(bc.dirty, lineCount) for _, c := range captures { - sty := e.editorTheme.CaptureStyle(c.name) + sty := t.CaptureStyle(c.name) for row := c.startRow; row <= c.endRow; row++ { if int(row) >= len(lines) { break diff --git a/internal/syntax/treesitter_behavior_test.go b/internal/syntax/treesitter_behavior_test.go index e421f37..44ddfa6 100644 --- a/internal/syntax/treesitter_behavior_test.go +++ b/internal/syntax/treesitter_behavior_test.go @@ -23,13 +23,14 @@ func TestTreeSitterEngineHighlightsGoKeywordAndString(t *testing.T) { Build() buf := &b - engine := NewTreeSitterEngine(themes.NewDefaultTheme()) - engine.PrepareBuffer(buf) + editorTheme := themes.NewDefaultTheme() + engine := NewTreeSitterEngine(editorTheme) + engine.PrepareBuffer(buf, editorTheme) - base := engine.editorTheme.Line + base := editorTheme.Line line0 := buf.Line(0) - map0 := engine.LineStyleMap(buf, 0) + map0 := engine.LineStyleMap(buf, 0, editorTheme) if len(map0) != len([]rune(line0)) { t.Fatalf("line 0 style map length mismatch") } @@ -42,7 +43,7 @@ func TestTreeSitterEngineHighlightsGoKeywordAndString(t *testing.T) { if stringStart < 0 { t.Fatalf("test setup failed: string literal not found") } - map2 := engine.LineStyleMap(buf, 2) + map2 := engine.LineStyleMap(buf, 2, editorTheme) if styleEquivalent(map2[stringStart+1], base) { t.Fatalf("expected string contents to be highlighted") } @@ -63,11 +64,12 @@ func TestTreeSitterEngineHighlightsMultilineRawString(t *testing.T) { Build() buf := &b - engine := NewTreeSitterEngine(themes.NewDefaultTheme()) - engine.PrepareBuffer(buf) + editorTheme := themes.NewDefaultTheme() + engine := NewTreeSitterEngine(editorTheme) + engine.PrepareBuffer(buf, editorTheme) - base := engine.editorTheme.Line - map3 := engine.LineStyleMap(buf, 3) + base := editorTheme.Line + map3 := engine.LineStyleMap(buf, 3, editorTheme) if len(map3) == 0 { t.Fatalf("expected style map on multiline raw string line") } @@ -89,15 +91,16 @@ func TestTreeSitterEngineApplyEditUpdatesStyleCategory(t *testing.T) { Build() buf := &b - engine := NewTreeSitterEngine(themes.NewDefaultTheme()) - engine.PrepareBuffer(buf) + editorTheme := themes.NewDefaultTheme() + engine := NewTreeSitterEngine(editorTheme) + engine.PrepareBuffer(buf, editorTheme) oldLine := buf.Line(2) oldIdx := strings.Index(oldLine, "123") if oldIdx < 0 { t.Fatalf("test setup failed: number not found") } - oldMap := engine.LineStyleMap(buf, 2) + oldMap := engine.LineStyleMap(buf, 2, editorTheme) oldStyle := oldMap[oldIdx] var edit *core.BufferEdit @@ -111,17 +114,17 @@ func TestTreeSitterEngineApplyEditUpdatesStyleCategory(t *testing.T) { } engine.ApplyEdit(buf, edit) - engine.PrepareBuffer(buf) + engine.PrepareBuffer(buf, editorTheme) newLine := buf.Line(2) newIdx := strings.Index(newLine, "abc") if newIdx < 0 { t.Fatalf("test setup failed: string not found") } - newMap := engine.LineStyleMap(buf, 2) + newMap := engine.LineStyleMap(buf, 2, editorTheme) newStyle := newMap[newIdx] - if styleEquivalent(newStyle, engine.editorTheme.Line) { + if styleEquivalent(newStyle, editorTheme.Line) { t.Fatalf("expected updated string to be highlighted") } if styleEquivalent(oldStyle, newStyle) { @@ -137,8 +140,9 @@ func TestTreeSitterEngineApplyEditLineCountChangeForcesFullDirty(t *testing.T) { Build() buf := &b - engine := NewTreeSitterEngine(themes.NewDefaultTheme()) - engine.PrepareBuffer(buf) + editorTheme := themes.NewDefaultTheme() + engine := NewTreeSitterEngine(editorTheme) + engine.PrepareBuffer(buf, editorTheme) bc := engine.getCache(buf) var edit *core.BufferEdit @@ -156,7 +160,7 @@ func TestTreeSitterEngineApplyEditLineCountChangeForcesFullDirty(t *testing.T) { t.Fatalf("expected line count change to set dirtyAll") } - engine.PrepareBuffer(buf) + engine.PrepareBuffer(buf, editorTheme) if !bc.built { t.Fatalf("expected cache rebuilt after prepare") } @@ -176,12 +180,13 @@ func TestTreeSitterEngineUnsupportedBufferFallsBackToDefaultStyles(t *testing.T) Build() buf := &b - engine := NewTreeSitterEngine(themes.NewDefaultTheme()) - engine.PrepareBuffer(buf) + editorTheme := themes.NewDefaultTheme() + engine := NewTreeSitterEngine(editorTheme) + engine.PrepareBuffer(buf, editorTheme) - base := engine.editorTheme.Line + base := editorTheme.Line line := buf.Line(0) - m := engine.LineStyleMap(buf, 0) + m := engine.LineStyleMap(buf, 0, editorTheme) if len(m) != len([]rune(line)) { t.Fatalf("style map length mismatch on fallback buffer") } @@ -200,8 +205,9 @@ func TestTreeSitterEngineLastLineEditDoesNotPanicAndRebuilds(t *testing.T) { Build() buf := &b - engine := NewTreeSitterEngine(themes.NewDefaultTheme()) - engine.PrepareBuffer(buf) + editorTheme := themes.NewDefaultTheme() + engine := NewTreeSitterEngine(editorTheme) + engine.PrepareBuffer(buf, editorTheme) bc := engine.getCache(buf) var edit *core.BufferEdit @@ -215,7 +221,7 @@ func TestTreeSitterEngineLastLineEditDoesNotPanicAndRebuilds(t *testing.T) { } engine.ApplyEdit(buf, edit) - engine.PrepareBuffer(buf) + engine.PrepareBuffer(buf, editorTheme) if !bc.built { t.Fatalf("expected cache built after last-line edit") diff --git a/internal/syntax/treesitter_bench_test.go b/internal/syntax/treesitter_bench_test.go index 6fc4c97..c211801 100644 --- a/internal/syntax/treesitter_bench_test.go +++ b/internal/syntax/treesitter_bench_test.go @@ -18,9 +18,10 @@ func BenchmarkTreeSitterPrepareAndIncrementalEdit(b *testing.B) { bld := core.NewBufferBuilder().WithFilename("bench.go").WithFiletype("go").WithLines(lines).Build() buf := &bld - eng := NewTreeSitterEngine(themes.NewDefaultTheme()) + editorTheme := themes.NewDefaultTheme() + eng := NewTreeSitterEngine(editorTheme) - eng.PrepareBuffer(buf) + eng.PrepareBuffer(buf, editorTheme) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -32,6 +33,6 @@ func BenchmarkTreeSitterPrepareAndIncrementalEdit(b *testing.B) { // Synthetic direct invalidate path benchmark (current API entrypoints) eng.InvalidateLines(buf, lineIdx, lineIdx) - eng.PrepareBuffer(buf) + eng.PrepareBuffer(buf, editorTheme) } } diff --git a/internal/syntax/treesitter_engine_test.go b/internal/syntax/treesitter_engine_test.go index 096d358..ecc36e4 100644 --- a/internal/syntax/treesitter_engine_test.go +++ b/internal/syntax/treesitter_engine_test.go @@ -15,8 +15,9 @@ func TestTreeSitterEngineApplyEditMarksDirtyWithoutFullInvalidation(t *testing.T Build() buf := &b - engine := NewTreeSitterEngine(themes.NewDefaultTheme()) - engine.PrepareBuffer(buf) + editorTheme := themes.NewDefaultTheme() + engine := NewTreeSitterEngine(editorTheme) + engine.PrepareBuffer(buf, editorTheme) bc := engine.getCache(buf) if !bc.built { @@ -45,7 +46,7 @@ func TestTreeSitterEngineApplyEditMarksDirtyWithoutFullInvalidation(t *testing.T t.Fatalf("expected dirty ranges after apply edit") } - engine.PrepareBuffer(buf) + engine.PrepareBuffer(buf, editorTheme) if !bc.built { t.Fatalf("expected cache rebuilt after prepare") } @@ -62,8 +63,9 @@ func TestTreeSitterEngineInvalidateLinesAndBuffer(t *testing.T) { Build() buf := &b - engine := NewTreeSitterEngine(themes.NewDefaultTheme()) - engine.PrepareBuffer(buf) + editorTheme := themes.NewDefaultTheme() + engine := NewTreeSitterEngine(editorTheme) + engine.PrepareBuffer(buf, editorTheme) bc := engine.getCache(buf) engine.InvalidateLines(buf, 1, 1) diff --git a/internal/syntax/treesitter_sequences_test.go b/internal/syntax/treesitter_sequences_test.go index 4aae529..1353bbc 100644 --- a/internal/syntax/treesitter_sequences_test.go +++ b/internal/syntax/treesitter_sequences_test.go @@ -5,6 +5,7 @@ import ( "testing" "git.gophernest.net/azpect/TextEditor/internal/core" + "git.gophernest.net/azpect/TextEditor/internal/theme" "git.gophernest.net/azpect/TextEditor/internal/theme/themes" ) @@ -66,7 +67,8 @@ func TestTreeSitterEngineEditSequences(t *testing.T) { w := core.NewWindowBuilder().WithBuffer(buf).WithDimensions(120, 40).Build() win := &w - engine := NewTreeSitterEngine(themes.NewDefaultTheme()) + editorTheme := themes.NewDefaultTheme() + engine := NewTreeSitterEngine(editorTheme) buf.OnChange = func(change core.BufferChange) { if change.Edit != nil { @@ -76,19 +78,19 @@ func TestTreeSitterEngineEditSequences(t *testing.T) { } } - engine.PrepareBuffer(buf) - assertEngineInvariants(t, engine, buf, "initial") + engine.PrepareBuffer(buf, editorTheme) + assertEngineInvariants(t, engine, buf, editorTheme, "initial") for i, op := range tc.opList { op(buf, win) - engine.PrepareBuffer(buf) - assertEngineInvariants(t, engine, buf, fmt.Sprintf("after op %d", i+1)) + engine.PrepareBuffer(buf, editorTheme) + assertEngineInvariants(t, engine, buf, editorTheme, fmt.Sprintf("after op %d", i+1)) } }) } } -func assertEngineInvariants(t *testing.T, engine *TreeSitterEngine, buf *core.Buffer, phase string) { +func assertEngineInvariants(t *testing.T, engine *TreeSitterEngine, buf *core.Buffer, editorTheme theme.EditorTheme, phase string) { t.Helper() bc := engine.getCache(buf) @@ -104,7 +106,7 @@ func assertEngineInvariants(t *testing.T, engine *TreeSitterEngine, buf *core.Bu for i := 0; i < buf.LineCount(); i++ { line := buf.Line(i) - m := engine.LineStyleMap(buf, i) + m := engine.LineStyleMap(buf, i, editorTheme) if len(m) != len([]rune(line)) { t.Fatalf("%s: line %d style length mismatch: got %d want %d", phase, i, len(m), len([]rune(line))) } diff --git a/internal/theme/loader.go b/internal/theme/loader.go new file mode 100644 index 0000000..f7f6219 --- /dev/null +++ b/internal/theme/loader.go @@ -0,0 +1,154 @@ +package theme + +import ( + "embed" + "encoding/json" + "fmt" + "io/fs" + "path/filepath" + "sort" + "strings" + + "github.com/charmbracelet/lipgloss" +) + +//go:embed themes/*.json +var embeddedThemes embed.FS + +// LoadEmbeddedThemesJSON reads all embedded theme JSON files and unmarshals +// them into ThemeJSON objects keyed by theme name. +func LoadEmbeddedThemesJSON() (map[string]ThemeJSON, error) { + paths, err := fs.Glob(embeddedThemes, "themes/*.json") + if err != nil { + return nil, err + } + sort.Strings(paths) + + out := make(map[string]ThemeJSON, len(paths)) + for _, path := range paths { + b, readErr := embeddedThemes.ReadFile(path) + if readErr != nil { + return nil, fmt.Errorf("read embedded theme %q: %w", path, readErr) + } + + var th ThemeJSON + if unmarshalErr := json.Unmarshal(b, &th); unmarshalErr != nil { + return nil, fmt.Errorf("decode embedded theme %q: %w", path, unmarshalErr) + } + + if strings.TrimSpace(th.Name) == "" { + th.Name = strings.TrimSuffix(filepath.Base(path), ".json") + } + + out[th.Name] = th + } + + return out, nil +} + +func MapEmbededThemeToEditorTheme(em map[string]ThemeJSON) map[string]EditorTheme { + out := make(map[string]EditorTheme, len(em)) + + for name, in := range em { + line := styleFromJSON(in.Line) + lineBg := colorString(in.Line.BG) + + syntaxExact := make(map[string]lipgloss.Style, len(in.Syntax.Exact)) + for capture, col := range in.Syntax.Exact { + c := normalizeCaptureKey(capture) + if c == "" { + continue + } + syntaxExact[c] = syntaxColorStyle(col, lineBg) + } + + syntaxGroup := make(map[string]lipgloss.Style, len(in.Syntax.Group)) + for group, col := range in.Syntax.Group { + g := normalizeCaptureKey(group) + if g == "" { + continue + } + syntaxGroup[g] = syntaxColorStyle(col, lineBg) + } + + key := strings.TrimSpace(name) + if key == "" { + key = strings.TrimSpace(in.Name) + } + if key == "" { + continue + } + + out[key] = EditorTheme{ + Cursors: CursorTheme{ + Normal: styleFromJSON(in.Cursors.Normal), + Insert: styleFromJSON(in.Cursors.Insert), + Command: styleFromJSON(in.Cursors.Command), + Replace: styleFromJSON(in.Cursors.Replace), + }, + Gutter: GutterTheme{ + Default: styleFromJSON(in.Gutter.Default), + CurrentLine: styleFromJSON(in.Gutter.CurrentLine), + }, + VisualHightlight: styleFromJSON(in.VisualHighlight), + StatusBar: StatusBarTheme{ + Default: styleFromJSON(in.StatusBar.Default), + }, + CommandLine: CommandLineTheme{ + Error: styleFromJSON(in.CommandLine.Error), + OutputBorder: styleFromJSON(in.CommandLine.OutputBorder), + ContinueMessage: styleFromJSON(in.CommandLine.ContinueMessage), + }, + Line: line, + Background: styleFromJSON(in.Background), + Syntax: SyntaxTheme{ + Exact: syntaxExact, + Group: syntaxGroup, + }, + } + } + + return out +} + +// MapEmbeddedThemeToEditorTheme is a correctly spelled alias for +// MapEmbededThemeToEditorTheme. +func MapEmbeddedThemeToEditorTheme(em map[string]ThemeJSON) map[string]EditorTheme { + return MapEmbededThemeToEditorTheme(em) +} + +func styleFromJSON(in ColorStyleJSON) lipgloss.Style { + out := lipgloss.NewStyle() + + if fg := colorString(in.FG); fg != "" { + out = out.Foreground(lipgloss.Color(fg)) + } + if bg := colorString(in.BG); bg != "" { + out = out.Background(lipgloss.Color(bg)) + } + + return out +} + +func colorString(c string) string { + return strings.TrimSpace(c) +} + +func normalizeCaptureKey(k string) string { + k = strings.TrimSpace(strings.ToLower(k)) + k = strings.TrimPrefix(k, "@") + return k +} + +func syntaxColorStyle(fg, bg string) lipgloss.Style { + out := lipgloss.NewStyle() + + if f := colorString(fg); f != "" { + out = out.Foreground(lipgloss.Color(f)) + } + if b := colorString(bg); b != "" { + out = out.Background(lipgloss.Color(b)) + } + + return out +} diff --git a/internal/theme/theme_json.go b/internal/theme/theme_json.go new file mode 100644 index 0000000..07969b8 --- /dev/null +++ b/internal/theme/theme_json.go @@ -0,0 +1,53 @@ +package theme + +// ThemeJSON is the file-backed theme DTO used for JSON unmarshalling. +// +// This mirrors the format documented in internal/theme/themes/README.md. +// It is intentionally string-based so values can be validated and compiled +// into EditorTheme styles in a separate step. +type ThemeJSON struct { + Name string `json:"name"` + Line ColorStyleJSON `json:"line"` + Background ColorStyleJSON `json:"background"` + VisualHighlight ColorStyleJSON `json:"visual_highlight"` + Cursors CursorJSON `json:"cursors"` + Gutter GutterJSON `json:"gutter"` + StatusBar StatusBarJSON `json:"status_bar"` + CommandLine CommandLineJSON `json:"command_line"` + Syntax SyntaxJSON `json:"syntax"` +} + +// ColorStyleJSON represents a simple fg/bg style entry. +// +// For v1 themes, only color values are supported. +type ColorStyleJSON struct { + FG string `json:"fg,omitempty"` + BG string `json:"bg,omitempty"` +} + +type CursorJSON struct { + Normal ColorStyleJSON `json:"normal"` + Insert ColorStyleJSON `json:"insert"` + Command ColorStyleJSON `json:"command"` + Replace ColorStyleJSON `json:"replace"` +} + +type GutterJSON struct { + Default ColorStyleJSON `json:"default"` + CurrentLine ColorStyleJSON `json:"current_line"` +} + +type StatusBarJSON struct { + Default ColorStyleJSON `json:"default"` +} + +type CommandLineJSON struct { + Error ColorStyleJSON `json:"error"` + OutputBorder ColorStyleJSON `json:"output_border"` + ContinueMessage ColorStyleJSON `json:"continue_message"` +} + +type SyntaxJSON struct { + Group map[string]string `json:"group"` + Exact map[string]string `json:"exact"` +} diff --git a/internal/theme/themes/README.md b/internal/theme/themes/README.md new file mode 100644 index 0000000..44f6a48 --- /dev/null +++ b/internal/theme/themes/README.md @@ -0,0 +1,81 @@ +# Theme JSON Format (v1) + +This document defines the JSON structure for editor themes. + +- All color values are 6-digit hex strings (for example `#d4d8e1`). +- Capture keys must be lowercase and must not include `@`. +- `syntax.exact` overrides `syntax.group`. **These can be any values!** +- If a capture is missing from both maps, the editor should fall back to the base `line` style. + +## Full structure + +```json +{ + "name": "default", + "line": { "fg": "#d4d8e1", "bg": "#111418" }, + "background": { "bg": "#111418" }, + "visual_highlight": { "bg": "#2f334d" }, + + "cursors": { + "normal": { "fg": "#111418", "bg": "#d4d8e1" }, + "insert": { "fg": "#d4d8e1", "bg": "#111418" }, + "command": { "fg": "#111418", "bg": "#d4d8e1" }, + "replace": { "fg": "#d4d8e1", "bg": "#111418" } + }, + + "gutter": { + "default": { "fg": "#6b7280", "bg": "#0d1014" }, + "current_line": { "fg": "#c0c8d8", "bg": "#0d1014" } + }, + + "status_bar": { + "default": { "fg": "#8f99aa", "bg": "#0d1014" } + }, + + "command_line": { + "error": { "fg": "#bf616a", "bg": "#111418" }, + "output_border": { "fg": "#d4d8e1", "bg": "#0d1014" }, + "continue_message": { "fg": "#81a1c1", "bg": "#111418" } + }, + + "syntax": { + "group": { + "comment": "#7f8795", + "function": "#81a1c1", + "keyword": "#b48ead", + "number": "#88c0d0", + "string": "#a3be8c", + "type": "#ebcb8b", + "variable": "#d4d8e1" + ... + }, + "exact": { + "comment.documentation": "#8f99aa", + "function.call": "#81a1c1", + "keyword.return": "#b48ead", + "string.escape": "#d08770", + "variable.parameter": "#c0c8d8", + ... + } + } +} +``` + +## Field notes + +- `name`: theme name shown by `:colorscheme`. +- `line`: base text style used as the default fallback. +- `background`: background fill style used for empty space. +- `visual_highlight`: selection background style. +- `syntax.group`: fallback colors by capture group (`keyword`, `string`, `comment`, etc.). +- `syntax.exact`: exact capture overrides (`keyword.function`, `string.escape`, etc.). + +## Future ideas + +For now, styles only support foreground/background colors. + +In a future version we may add optional text attributes such as: + +- `bold` +- `italic` +- `underline` diff --git a/internal/theme/themes/kanagawa-dragon.json b/internal/theme/themes/kanagawa-dragon.json new file mode 100644 index 0000000..79f3f10 --- /dev/null +++ b/internal/theme/themes/kanagawa-dragon.json @@ -0,0 +1,116 @@ +{ + "name": "kanagawa-dragon", + "line": { "fg": "#c5c9c5", "bg": "#181616" }, + "background": { "bg": "#181616" }, + "visual_highlight": { "bg": "#223249" }, + "cursors": { + "normal": { "fg": "#181616", "bg": "#c5c9c5" }, + "insert": { "fg": "#181616", "bg": "#c5c9c5" }, + "command": { "fg": "#181616", "bg": "#c5c9c5" }, + "replace": { "fg": "#181616", "bg": "#c5c9c5" } + }, + "gutter": { + "default": { "fg": "#625e5a", "bg": "#282727" }, + "current_line": { "fg": "#c4b28a", "bg": "#282727" } + }, + "status_bar": { + "default": { "fg": "#c5c9c5", "bg": "#282727" } + }, + "command_line": { + "error": { "fg": "#e82424", "bg": "#181616" }, + "output_border": { "fg": "#c5c9c5", "bg": "#0d0c0c" }, + "continue_message": { "fg": "#8ba4b0", "bg": "#181616" } + }, + "syntax": { + "group": { + "attribute": "#c4b28a", + "boolean": "#a292a3", + "character": "#8a9a7b", + "charset": "#8ea4a2", + "comment": "#737c73", + "conceal": "#737c73", + "constant": "#b6927b", + "constructor": "#c4b28a", + "error": "#e82424", + "function": "#8ba4b0", + "import": "#8992a7", + "interface": "#8ea4a2", + "keyframes": "#c4746e", + "keyword": "#8992a7", + "label": "#949fb5", + "media": "#c4746e", + "module": "#949fb5", + "namespace": "#949fb5", + "none": "#c5c9c5", + "nospell": "#c5c9c5", + "number": "#a292a3", + "operator": "#c4746e", + "property": "#c4b28a", + "spell": "#c5c9c5", + "string": "#8a9a7b", + "supports": "#c4746e", + "tag": "#8ba4b0", + "type": "#8ea4a2", + "variable": "#c5c9c5" + }, + "exact": { + "attribute.builtin": "#c4b28a", + "character.special": "#c4746e", + "comment.documentation": "#a6a69c", + "constant.builtin": "#b6927b", + "constant.macro": "#c4746e", + "function.builtin": "#949fb5", + "function.call": "#8ba4b0", + "function.macro": "#c4746e", + "function.method": "#8ba4b0", + "function.method.call": "#8ba4b0", + "keyword.conditional": "#8992a7", + "keyword.conditional.ternary": "#8992a7", + "keyword.coroutine": "#8992a7", + "keyword.debug": "#8992a7", + "keyword.directive": "#c4746e", + "keyword.directive.define": "#c4746e", + "keyword.exception": "#8992a7", + "keyword.function": "#8992a7", + "keyword.import": "#8992a7", + "keyword.modifier": "#8992a7", + "keyword.operator": "#c4746e", + "keyword.repeat": "#8992a7", + "keyword.return": "#8992a7", + "keyword.type": "#8ea4a2", + "markup.heading": "#c4b28a", + "markup.heading.1": "#c4b28a", + "markup.heading.2": "#b6927b", + "markup.heading.3": "#a292a3", + "markup.heading.4": "#949fb5", + "markup.heading.5": "#8ba4b0", + "markup.heading.6": "#8ea4a2", + "markup.italic": "#a6a69c", + "markup.link.label": "#8ba4b0", + "markup.raw": "#8a9a7b", + "markup.strikethrough": "#737c73", + "markup.strong": "#c5c9c5", + "markup.underline": "#949fb5", + "module.builtin": "#949fb5", + "number.float": "#a292a3", + "punctuation.bracket": "#9e9b93", + "punctuation.delimiter": "#9e9b93", + "punctuation.special": "#c4746e", + "string.documentation": "#a6a69c", + "string.escape": "#c4746e", + "string.regexp": "#c4746e", + "string.special.path": "#8a9a7b", + "string.special.symbol": "#b6927b", + "string.special.url": "#8ba4b0", + "tag.attribute": "#c4b28a", + "tag.attribute.url": "#8ba4b0", + "tag.builtin": "#949fb5", + "tag.delimiter": "#9e9b93", + "type.builtin": "#8ea4a2", + "type.definition": "#8ea4a2", + "variable.builtin": "#b6927b", + "variable.member": "#c4b28a", + "variable.parameter": "#a6a69c" + } + } +} diff --git a/internal/theme/themes/kanagawa-dragon.xml b/internal/theme/themes/kanagawa-dragon.xml deleted file mode 100644 index 114d165..0000000 --- a/internal/theme/themes/kanagawa-dragon.xml +++ /dev/null @@ -1,83 +0,0 @@ - diff --git a/internal/theme/themes/kanagawa-lotus.json b/internal/theme/themes/kanagawa-lotus.json new file mode 100644 index 0000000..8fbba2c --- /dev/null +++ b/internal/theme/themes/kanagawa-lotus.json @@ -0,0 +1,116 @@ +{ + "name": "kanagawa-lotus", + "line": { "fg": "#545464", "bg": "#f2ecbc" }, + "background": { "bg": "#f2ecbc" }, + "visual_highlight": { "bg": "#c9cbd1" }, + "cursors": { + "normal": { "fg": "#f2ecbc", "bg": "#545464" }, + "insert": { "fg": "#f2ecbc", "bg": "#545464" }, + "command": { "fg": "#f2ecbc", "bg": "#545464" }, + "replace": { "fg": "#f2ecbc", "bg": "#545464" } + }, + "gutter": { + "default": { "fg": "#a09cac", "bg": "#e7dba0" }, + "current_line": { "fg": "#77713f", "bg": "#e7dba0" } + }, + "status_bar": { + "default": { "fg": "#43436c", "bg": "#e7dba0" } + }, + "command_line": { + "error": { "fg": "#e82424", "bg": "#f2ecbc" }, + "output_border": { "fg": "#545464", "bg": "#e7dba0" }, + "continue_message": { "fg": "#4d699b", "bg": "#f2ecbc" } + }, + "syntax": { + "group": { + "attribute": "#77713f", + "boolean": "#b35b79", + "character": "#6f894e", + "charset": "#597b75", + "comment": "#8a8980", + "conceal": "#8a8980", + "constant": "#cc6d00", + "constructor": "#77713f", + "error": "#e82424", + "function": "#4d699b", + "import": "#624c83", + "interface": "#597b75", + "keyframes": "#c84053", + "keyword": "#624c83", + "label": "#6693bf", + "media": "#c84053", + "module": "#6693bf", + "namespace": "#6693bf", + "none": "#545464", + "nospell": "#545464", + "number": "#b35b79", + "operator": "#836f4a", + "property": "#77713f", + "spell": "#545464", + "string": "#6f894e", + "supports": "#c84053", + "tag": "#4d699b", + "type": "#597b75", + "variable": "#545464" + }, + "exact": { + "attribute.builtin": "#77713f", + "character.special": "#836f4a", + "comment.documentation": "#716e61", + "constant.builtin": "#cc6d00", + "constant.macro": "#c84053", + "function.builtin": "#6693bf", + "function.call": "#4d699b", + "function.macro": "#c84053", + "function.method": "#4d699b", + "function.method.call": "#4d699b", + "keyword.conditional": "#624c83", + "keyword.conditional.ternary": "#624c83", + "keyword.coroutine": "#624c83", + "keyword.debug": "#624c83", + "keyword.directive": "#c84053", + "keyword.directive.define": "#c84053", + "keyword.exception": "#624c83", + "keyword.function": "#624c83", + "keyword.import": "#624c83", + "keyword.modifier": "#624c83", + "keyword.operator": "#836f4a", + "keyword.repeat": "#624c83", + "keyword.return": "#624c83", + "keyword.type": "#597b75", + "markup.heading": "#77713f", + "markup.heading.1": "#77713f", + "markup.heading.2": "#836f4a", + "markup.heading.3": "#cc6d00", + "markup.heading.4": "#4d699b", + "markup.heading.5": "#624c83", + "markup.heading.6": "#6693bf", + "markup.italic": "#716e61", + "markup.link.label": "#4d699b", + "markup.raw": "#6f894e", + "markup.strikethrough": "#8a8980", + "markup.strong": "#545464", + "markup.underline": "#6693bf", + "module.builtin": "#6693bf", + "number.float": "#b35b79", + "punctuation.bracket": "#4e8ca2", + "punctuation.delimiter": "#4e8ca2", + "punctuation.special": "#836f4a", + "string.documentation": "#716e61", + "string.escape": "#836f4a", + "string.regexp": "#836f4a", + "string.special.path": "#6f894e", + "string.special.symbol": "#cc6d00", + "string.special.url": "#4d699b", + "tag.attribute": "#77713f", + "tag.attribute.url": "#4d699b", + "tag.builtin": "#6693bf", + "tag.delimiter": "#4e8ca2", + "type.builtin": "#597b75", + "type.definition": "#597b75", + "variable.builtin": "#c84053", + "variable.member": "#77713f", + "variable.parameter": "#5d57a3" + } + } +} diff --git a/internal/theme/themes/kanagawa-lotus.xml b/internal/theme/themes/kanagawa-lotus.xml deleted file mode 100644 index dde3bc8..0000000 --- a/internal/theme/themes/kanagawa-lotus.xml +++ /dev/null @@ -1,83 +0,0 @@ - diff --git a/internal/theme/themes/kanagawa-wave.xml b/internal/theme/themes/kanagawa-wave.xml deleted file mode 100644 index cebcda1..0000000 --- a/internal/theme/themes/kanagawa-wave.xml +++ /dev/null @@ -1,83 +0,0 @@ - diff --git a/internal/theme/themes/kanagawa.json b/internal/theme/themes/kanagawa.json new file mode 100644 index 0000000..33ff303 --- /dev/null +++ b/internal/theme/themes/kanagawa.json @@ -0,0 +1,153 @@ +{ + "name": "kanagawa", + "line": { + "fg": "#dcd7ba", + "bg": "#1f1f28" + }, + "background": { + "bg": "#1f1f28" + }, + "visual_highlight": { + "bg": "#223249" + }, + "cursors": { + "normal": { + "fg": "#1f1f28", + "bg": "#dcd7ba" + }, + "insert": { + "fg": "#1f1f28", + "bg": "#dcd7ba" + }, + "command": { + "fg": "#1f1f28", + "bg": "#dcd7ba" + }, + "replace": { + "fg": "#1f1f28", + "bg": "#dcd7ba" + } + }, + "gutter": { + "default": { + "fg": "#727169", + "bg": "#2a2a37" + }, + "current_line": { + "fg": "#e6c384", + "bg": "#2a2a37" + } + }, + "status_bar": { + "default": { + "fg": "#c8c093", + "bg": "#2a2a37" + } + }, + "command_line": { + "error": { + "fg": "#e82424", + "bg": "#1f1f28" + }, + "output_border": { + "fg": "#dcd7ba", + "bg": "#16161d" + }, + "continue_message": { + "fg": "#7e9cd8", + "bg": "#1f1f28" + } + }, + "syntax": { + "group": { + "attribute": "#e6c384", + "boolean": "#d27e99", + "character": "#98bb6c", + "charset": "#7aa89f", + "comment": "#727169", + "conceal": "#727169", + "constant": "#ffa066", + "constructor": "#e6c384", + "error": "#e82424", + "function": "#7e9cd8", + "import": "#957fb8", + "interface": "#7aa89f", + "keyframes": "#e46876", + "keyword": "#957fb8", + "label": "#e46876", + "media": "#e46876", + "module": "#7fb4ca", + "namespace": "#7fb4ca", + "none": "#dcd7ba", + "nospell": "#dcd7ba", + "number": "#d27e99", + "operator": "#c0a36e", + "property": "#e6c384", + "spell": "#dcd7ba", + "string": "#98bb6c", + "supports": "#e46876", + "tag": "#7fb4ca", + "type": "#7aa89f", + "variable": "#dcd7ba" + }, + "exact": { + "attribute.builtin": "#e6c384", + "character.special": "#c0a36e", + "comment.documentation": "#c8c093", + "constant.builtin": "#ffa066", + "constant.macro": "#e46876", + "function.builtin": "#7fb4ca", + "function.call": "#7e9cd8", + "function.macro": "#e46876", + "function.method": "#7e9cd8", + "function.method.call": "#7e9cd8", + "keyword.conditional": "#957fb8", + "keyword.conditional.ternary": "#957fb8", + "keyword.coroutine": "#957fb8", + "keyword.debug": "#957fb8", + "keyword.directive": "#e46876", + "keyword.directive.define": "#e46876", + "keyword.exception": "#957fb8", + "keyword.function": "#957fb8", + "keyword.import": "#957fb8", + "keyword.modifier": "#957fb8", + "keyword.operator": "#c0a36e", + "keyword.repeat": "#957fb8", + "keyword.return": "#957fb8", + "keyword.type": "#7aa89f", + "markup.heading": "#e6c384", + "markup.heading.1": "#e6c384", + "markup.heading.2": "#dca561", + "markup.heading.3": "#c0a36e", + "markup.heading.4": "#b6927b", + "markup.heading.5": "#957fb8", + "markup.heading.6": "#7e9cd8", + "markup.italic": "#b8b4d0", + "markup.link.label": "#7e9cd8", + "markup.raw": "#98bb6c", + "markup.strikethrough": "#727169", + "markup.strong": "#c8c093", + "markup.underline": "#7fb4ca", + "module.builtin": "#7fb4ca", + "number.float": "#d27e99", + "punctuation.bracket": "#9cabca", + "punctuation.delimiter": "#9cabca", + "punctuation.special": "#c0a36e", + "string.documentation": "#c8c093", + "string.escape": "#c0a36e", + "string.regexp": "#c0a36e", + "string.special.path": "#98bb6c", + "string.special.symbol": "#ffa066", + "string.special.url": "#7e9cd8", + "tag.attribute": "#e6c384", + "tag.attribute.url": "#7e9cd8", + "tag.builtin": "#7fb4ca", + "tag.delimiter": "#9cabca", + "type.builtin": "#7aa89f", + "type.definition": "#7aa89f", + "variable.builtin": "#ffa066", + "variable.member": "#e6c384", + "variable.parameter": "#b8b4d0" + } + } +}