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...
This commit is contained in:
Hayden Hargreaves 2026-04-08 11:59:49 -07:00
parent be13f8838d
commit 273be90d42
22 changed files with 883 additions and 350 deletions

28
cmd/theme-loader/main.go Normal file
View File

@ -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)
}
}

View File

@ -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

View File

@ -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
}
@ -46,6 +47,8 @@ func NewMockModel() *MockModel {
SettingsVal: core.NewDefaultSettings(),
ModeVal: core.NormalMode,
RegistersMap: core.DefaultRegisters(),
CurrentThemeName: "default",
ThemesMap: map[string]theme.EditorTheme{"default": {}},
}
}
@ -64,6 +67,8 @@ func NewMockModelWithBuffer(buf *core.Buffer) *MockModel {
SettingsVal: core.NewDefaultSettings(),
ModeVal: core.NormalMode,
RegistersMap: core.DefaultRegisters(),
CurrentThemeName: "default",
ThemesMap: map[string]theme.EditorTheme{"default": {}},
}
}
@ -76,6 +81,8 @@ func NewMockModelWithWindow(win *core.Window) *MockModel {
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 }

View File

@ -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{

View File

@ -51,7 +51,8 @@ type Model struct {
registers map[rune]core.Register // name -> register
// Visual styles
theme theme.EditorTheme
currentTheme string // Name of current theme
themes map[string]theme.EditorTheme
syntax syntax.Engine
// Dot operator state
@ -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 {

View File

@ -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,
},
}
}

View File

@ -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)

View File

@ -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)

View File

@ -20,7 +20,6 @@ import (
//
// Cached styles are represented as one style per rune for each line.
type TreeSitterEngine struct {
editorTheme theme.EditorTheme
registry *languageRegistry
cache map[*core.Buffer]*bufferCache
@ -74,7 +73,6 @@ 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{},
}
@ -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

View File

@ -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")

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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)))
}

154
internal/theme/loader.go Normal file
View File

@ -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
}

View File

@ -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"`
}

View File

@ -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`

View File

@ -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"
}
}
}

View File

@ -1,83 +0,0 @@
<style name="kanagawa-dragon">
<entry type="Background" style="bg:#181616 #c5c9c5" />
<entry type="CodeLine" style="#c5c9c5" />
<entry type="Error" style="#e82424" />
<entry type="Other" style="#c5c9c5" />
<entry type="LineTableTD" style="" />
<entry type="LineTable" style="" />
<entry type="LineHighlight" style="bg:#393836" />
<entry type="LineNumbersTable" style="#625e5a" />
<entry type="LineNumbers" style="#625e5a" />
<entry type="Keyword" style="#8992a7" />
<entry type="KeywordReserved" style="#8992a7" />
<entry type="KeywordPseudo" style="#8992a7" />
<entry type="KeywordConstant" style="#b6927b" />
<entry type="KeywordDeclaration" style="#8992a7" />
<entry type="KeywordNamespace" style="#c4b28a" />
<entry type="KeywordType" style="#8ea4a2" />
<entry type="Name" style="#c5c9c5" />
<entry type="NameClass" style="#8ea4a2" />
<entry type="NameConstant" style="#b6927b" />
<entry type="NameDecorator" style="bold #b6927b" />
<entry type="NameEntity" style="#c4b28a" />
<entry type="NameException" style="#b6927b" />
<entry type="NameFunction" style="#8ba4b0" />
<entry type="NameFunctionMagic" style="#8ba4b0" />
<entry type="NameLabel" style="#949fb5" />
<entry type="NameNamespace" style="#c4b28a" />
<entry type="NameProperty" style="#c4b28a" />
<entry type="NameTag" style="#8ba4b0" />
<entry type="NameVariable" style="#c5c9c5" />
<entry type="NameVariableClass" style="#c5c9c5" />
<entry type="NameVariableGlobal" style="#c5c9c5" />
<entry type="NameVariableInstance" style="#c5c9c5" />
<entry type="NameVariableMagic" style="#c5c9c5" />
<entry type="NameAttribute" style="#c4b28a" />
<entry type="NameBuiltin" style="#c4746e" />
<entry type="NameBuiltinPseudo" style="#c4746e" />
<entry type="NameOther" style="#c5c9c5" />
<entry type="Literal" style="#c5c9c5" />
<entry type="LiteralDate" style="#c5c9c5" />
<entry type="LiteralString" style="#8a9a7b" />
<entry type="LiteralStringChar" style="#8a9a7b" />
<entry type="LiteralStringSingle" style="#8a9a7b" />
<entry type="LiteralStringDouble" style="#8a9a7b" />
<entry type="LiteralStringBacktick" style="#8a9a7b" />
<entry type="LiteralStringOther" style="#8a9a7b" />
<entry type="LiteralStringSymbol" style="#8a9a7b" />
<entry type="LiteralStringInterpol" style="#949fb5" />
<entry type="LiteralStringAffix" style="#c4746e" />
<entry type="LiteralStringDelimiter" style="#949fb5" />
<entry type="LiteralStringEscape" style="#c4746e" />
<entry type="LiteralStringRegex" style="#c4746e" />
<entry type="LiteralStringDoc" style="#737c73" />
<entry type="LiteralStringHeredoc" style="#737c73" />
<entry type="LiteralNumber" style="#a292a3" />
<entry type="LiteralNumberBin" style="#a292a3" />
<entry type="LiteralNumberHex" style="#a292a3" />
<entry type="LiteralNumberInteger" style="#a292a3" />
<entry type="LiteralNumberFloat" style="#a292a3" />
<entry type="LiteralNumberIntegerLong" style="#a292a3" />
<entry type="LiteralNumberOct" style="#a292a3" />
<entry type="Operator" style="bold #c4746e" />
<entry type="OperatorWord" style="bold #c4746e" />
<entry type="Comment" style="italic #737c73" />
<entry type="CommentSingle" style="italic #737c73" />
<entry type="CommentMultiline" style="italic #737c73" />
<entry type="CommentSpecial" style="italic #737c73" />
<entry type="CommentHashbang" style="italic #737c73" />
<entry type="CommentPreproc" style="italic #c4746e" />
<entry type="CommentPreprocFile" style="bold #c4746e" />
<entry type="Generic" style="#c5c9c5" />
<entry type="GenericInserted" style="bg:#2b3328 #76946a" />
<entry type="GenericDeleted" style="bg:#43242b #c34043" />
<entry type="GenericEmph" style="italic #c5c9c5" />
<entry type="GenericStrong" style="bold #c5c9c5" />
<entry type="GenericUnderline" style="underline #c5c9c5" />
<entry type="GenericHeading" style="bold #8ba4b0" />
<entry type="GenericSubheading" style="bold #8ba4b0" />
<entry type="GenericOutput" style="#c5c9c5" />
<entry type="GenericPrompt" style="#c5c9c5" />
<entry type="GenericError" style="#e82424" />
<entry type="GenericTraceback" style="#e82424" />
</style>

View File

@ -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"
}
}
}

View File

@ -1,83 +0,0 @@
<style name="kanagawa-lotus">
<entry type="Background" style="bg:#f2ecbc #545464" />
<entry type="CodeLine" style="#545464" />
<entry type="Error" style="#e82424" />
<entry type="Other" style="#545464" />
<entry type="LineTableTD" style="" />
<entry type="LineTable" style="" />
<entry type="LineHighlight" style="bg:#e4d794" />
<entry type="LineNumbersTable" style="#a09cac" />
<entry type="LineNumbers" style="#a09cac" />
<entry type="Keyword" style="#624c83" />
<entry type="KeywordReserved" style="#624c83" />
<entry type="KeywordPseudo" style="#624c83" />
<entry type="KeywordConstant" style="#cc6d00" />
<entry type="KeywordDeclaration" style="#624c83" />
<entry type="KeywordNamespace" style="#77713f" />
<entry type="KeywordType" style="#597b75" />
<entry type="Name" style="#545464" />
<entry type="NameClass" style="#597b75" />
<entry type="NameConstant" style="#cc6d00" />
<entry type="NameDecorator" style="bold #cc6d00" />
<entry type="NameEntity" style="#77713f" />
<entry type="NameException" style="#cc6d00" />
<entry type="NameFunction" style="#4d699b" />
<entry type="NameFunctionMagic" style="#4d699b" />
<entry type="NameLabel" style="#6693bf" />
<entry type="NameNamespace" style="#77713f" />
<entry type="NameProperty" style="#77713f" />
<entry type="NameTag" style="#4d699b" />
<entry type="NameVariable" style="#545464" />
<entry type="NameVariableClass" style="#545464" />
<entry type="NameVariableGlobal" style="#545464" />
<entry type="NameVariableInstance" style="#545464" />
<entry type="NameVariableMagic" style="#545464" />
<entry type="NameAttribute" style="#77713f" />
<entry type="NameBuiltin" style="#c84053" />
<entry type="NameBuiltinPseudo" style="#c84053" />
<entry type="NameOther" style="#545464" />
<entry type="Literal" style="#545464" />
<entry type="LiteralDate" style="#545464" />
<entry type="LiteralString" style="#6f894e" />
<entry type="LiteralStringChar" style="#6f894e" />
<entry type="LiteralStringSingle" style="#6f894e" />
<entry type="LiteralStringDouble" style="#6f894e" />
<entry type="LiteralStringBacktick" style="#6f894e" />
<entry type="LiteralStringOther" style="#6f894e" />
<entry type="LiteralStringSymbol" style="#6f894e" />
<entry type="LiteralStringInterpol" style="#6693bf" />
<entry type="LiteralStringAffix" style="#c84053" />
<entry type="LiteralStringDelimiter" style="#6693bf" />
<entry type="LiteralStringEscape" style="#836f4a" />
<entry type="LiteralStringRegex" style="#836f4a" />
<entry type="LiteralStringDoc" style="#8a8980" />
<entry type="LiteralStringHeredoc" style="#8a8980" />
<entry type="LiteralNumber" style="#b35b79" />
<entry type="LiteralNumberBin" style="#b35b79" />
<entry type="LiteralNumberHex" style="#b35b79" />
<entry type="LiteralNumberInteger" style="#b35b79" />
<entry type="LiteralNumberFloat" style="#b35b79" />
<entry type="LiteralNumberIntegerLong" style="#b35b79" />
<entry type="LiteralNumberOct" style="#b35b79" />
<entry type="Operator" style="bold #836f4a" />
<entry type="OperatorWord" style="bold #836f4a" />
<entry type="Comment" style="italic #8a8980" />
<entry type="CommentSingle" style="italic #8a8980" />
<entry type="CommentMultiline" style="italic #8a8980" />
<entry type="CommentSpecial" style="italic #8a8980" />
<entry type="CommentHashbang" style="italic #8a8980" />
<entry type="CommentPreproc" style="italic #c84053" />
<entry type="CommentPreprocFile" style="bold #c84053" />
<entry type="Generic" style="#545464" />
<entry type="GenericInserted" style="bg:#b7d0ae #6e915f" />
<entry type="GenericDeleted" style="bg:#d9a594 #d7474b" />
<entry type="GenericEmph" style="italic #545464" />
<entry type="GenericStrong" style="bold #545464" />
<entry type="GenericUnderline" style="underline #545464" />
<entry type="GenericHeading" style="bold #4d699b" />
<entry type="GenericSubheading" style="bold #4d699b" />
<entry type="GenericOutput" style="#545464" />
<entry type="GenericPrompt" style="#545464" />
<entry type="GenericError" style="#e82424" />
<entry type="GenericTraceback" style="#e82424" />
</style>

View File

@ -1,83 +0,0 @@
<style name="kanagawa-wave">
<entry type="Background" style="bg:#1f1f28 #dcd7ba" />
<entry type="CodeLine" style="#dcd7ba" />
<entry type="Error" style="#e82424" />
<entry type="Other" style="#dcd7ba" />
<entry type="LineTableTD" style="" />
<entry type="LineTable" style="" />
<entry type="LineHighlight" style="bg:#363646" />
<entry type="LineNumbersTable" style="#54546d" />
<entry type="LineNumbers" style="#54546d" />
<entry type="Keyword" style="#957fb8" />
<entry type="KeywordReserved" style="#957fb8" />
<entry type="KeywordPseudo" style="#957fb8" />
<entry type="KeywordConstant" style="#ffa066" />
<entry type="KeywordDeclaration" style="#957fb8" />
<entry type="KeywordNamespace" style="#e6c384" />
<entry type="KeywordType" style="#7aa89f" />
<entry type="Name" style="#dcd7ba" />
<entry type="NameClass" style="#7aa89f" />
<entry type="NameConstant" style="#ffa066" />
<entry type="NameDecorator" style="bold #ffa066" />
<entry type="NameEntity" style="#e6c384" />
<entry type="NameException" style="#ffa066" />
<entry type="NameFunction" style="#7e9cd8" />
<entry type="NameFunctionMagic" style="#7e9cd8" />
<entry type="NameLabel" style="#7fb4ca" />
<entry type="NameNamespace" style="#e6c384" />
<entry type="NameProperty" style="#e6c384" />
<entry type="NameTag" style="#7e9cd8" />
<entry type="NameVariable" style="#dcd7ba" />
<entry type="NameVariableClass" style="#dcd7ba" />
<entry type="NameVariableGlobal" style="#dcd7ba" />
<entry type="NameVariableInstance" style="#dcd7ba" />
<entry type="NameVariableMagic" style="#dcd7ba" />
<entry type="NameAttribute" style="#e6c384" />
<entry type="NameBuiltin" style="#e46876" />
<entry type="NameBuiltinPseudo" style="#e46876" />
<entry type="NameOther" style="#dcd7ba" />
<entry type="Literal" style="#dcd7ba" />
<entry type="LiteralDate" style="#dcd7ba" />
<entry type="LiteralString" style="#98bb6c" />
<entry type="LiteralStringChar" style="#98bb6c" />
<entry type="LiteralStringSingle" style="#98bb6c" />
<entry type="LiteralStringDouble" style="#98bb6c" />
<entry type="LiteralStringBacktick" style="#98bb6c" />
<entry type="LiteralStringOther" style="#98bb6c" />
<entry type="LiteralStringSymbol" style="#98bb6c" />
<entry type="LiteralStringInterpol" style="#7fb4ca" />
<entry type="LiteralStringAffix" style="#ff5d62" />
<entry type="LiteralStringDelimiter" style="#7fb4ca" />
<entry type="LiteralStringEscape" style="#c0a36e" />
<entry type="LiteralStringRegex" style="#c0a36e" />
<entry type="LiteralStringDoc" style="#727169" />
<entry type="LiteralStringHeredoc" style="#727169" />
<entry type="LiteralNumber" style="#d27e99" />
<entry type="LiteralNumberBin" style="#d27e99" />
<entry type="LiteralNumberHex" style="#d27e99" />
<entry type="LiteralNumberInteger" style="#d27e99" />
<entry type="LiteralNumberFloat" style="#d27e99" />
<entry type="LiteralNumberIntegerLong" style="#d27e99" />
<entry type="LiteralNumberOct" style="#d27e99" />
<entry type="Operator" style="bold #c0a36e" />
<entry type="OperatorWord" style="bold #c0a36e" />
<entry type="Comment" style="italic #727169" />
<entry type="CommentSingle" style="italic #727169" />
<entry type="CommentMultiline" style="italic #727169" />
<entry type="CommentSpecial" style="italic #727169" />
<entry type="CommentHashbang" style="italic #727169" />
<entry type="CommentPreproc" style="italic #e46876" />
<entry type="CommentPreprocFile" style="bold #e46876" />
<entry type="Generic" style="#dcd7ba" />
<entry type="GenericInserted" style="bg:#2b3328 #76946a" />
<entry type="GenericDeleted" style="bg:#43242b #c34043" />
<entry type="GenericEmph" style="italic #dcd7ba" />
<entry type="GenericStrong" style="bold #dcd7ba" />
<entry type="GenericUnderline" style="underline #dcd7ba" />
<entry type="GenericHeading" style="bold #7e9cd8" />
<entry type="GenericSubheading" style="bold #7e9cd8" />
<entry type="GenericOutput" style="#dcd7ba" />
<entry type="GenericPrompt" style="#dcd7ba" />
<entry type="GenericError" style="#e82424" />
<entry type="GenericTraceback" style="#e82424" />
</style>

View File

@ -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"
}
}
}