Compare commits
2 Commits
c70cbeaedf
...
b618e3a382
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b618e3a382 | ||
|
|
76fa55440e |
@ -4,12 +4,17 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/program"
|
"git.gophernest.net/azpect/TextEditor/internal/program"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/theme"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
// main: Entry point for the Gim text editor. Creates a buffer and window,
|
// main: Entry point for the Gim text editor. Creates a buffer and window,
|
||||||
// initializes the editor model, and runs the BubbleTea TUI program.
|
// initializes the editor model, and runs the BubbleTea TUI program.
|
||||||
func main() {
|
func main() {
|
||||||
|
if err := theme.RegisterAll(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
// <exe> <filename>
|
// <exe> <filename>
|
||||||
args := os.Args[1:]
|
args := os.Args[1:]
|
||||||
|
|
||||||
|
|||||||
2
go.mod
2
go.mod
@ -3,6 +3,7 @@ module git.gophernest.net/azpect/TextEditor
|
|||||||
go 1.25.5
|
go 1.25.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/alecthomas/chroma/v2 v2.23.1
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/charmbracelet/x/exp/teatest v0.0.0-20260209132835-6b065b8ba62c
|
github.com/charmbracelet/x/exp/teatest v0.0.0-20260209132835-6b065b8ba62c
|
||||||
@ -19,6 +20,7 @@ require (
|
|||||||
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
||||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
|||||||
10
go.sum
10
go.sum
@ -1,3 +1,9 @@
|
|||||||
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||||
|
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||||
|
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||||
@ -24,8 +30,12 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfa
|
|||||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||||
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
||||||
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/style"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ==================================================
|
// ==================================================
|
||||||
@ -22,6 +23,7 @@ type mockModel struct {
|
|||||||
commandCursor int
|
commandCursor int
|
||||||
commandOutput *core.CommandOutput
|
commandOutput *core.CommandOutput
|
||||||
lastFind core.LastFindCommand
|
lastFind core.LastFindCommand
|
||||||
|
styles style.Styles
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMockModel() *mockModel {
|
func newMockModel() *mockModel {
|
||||||
@ -103,6 +105,8 @@ func (m *mockModel) Mode() core.Mode { return m.mode }
|
|||||||
func (m *mockModel) SetMode(mode core.Mode) { m.mode = mode }
|
func (m *mockModel) SetMode(mode core.Mode) { m.mode = mode }
|
||||||
func (m *mockModel) Settings() core.EditorSettings { return m.settings }
|
func (m *mockModel) Settings() core.EditorSettings { return m.settings }
|
||||||
func (m *mockModel) SetSettings(s core.EditorSettings) { m.settings = s }
|
func (m *mockModel) SetSettings(s core.EditorSettings) { m.settings = s }
|
||||||
|
func (m *mockModel) Styles() style.Styles { return m.styles }
|
||||||
|
func (m *mockModel) SetStyles(s style.Styles) { m.styles = s }
|
||||||
|
|
||||||
// Registers
|
// Registers
|
||||||
func (m *mockModel) Registers() map[rune]core.Register { return m.registers }
|
func (m *mockModel) Registers() map[rune]core.Register { return m.registers }
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package action
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/style"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ type Model interface {
|
|||||||
CommandCursor() int
|
CommandCursor() int
|
||||||
SetCommandCursor(cur int)
|
SetCommandCursor(cur int)
|
||||||
CommandOutput() *core.CommandOutput
|
CommandOutput() *core.CommandOutput
|
||||||
|
// DO NOT FORGET TO CALL SetMode()
|
||||||
SetCommandOutput(out *core.CommandOutput)
|
SetCommandOutput(out *core.CommandOutput)
|
||||||
|
|
||||||
// ==================================================
|
// ==================================================
|
||||||
@ -48,6 +50,8 @@ type Model interface {
|
|||||||
|
|
||||||
Settings() core.EditorSettings
|
Settings() core.EditorSettings
|
||||||
SetSettings(s core.EditorSettings)
|
SetSettings(s core.EditorSettings)
|
||||||
|
Styles() style.Styles
|
||||||
|
SetStyles(s style.Styles)
|
||||||
|
|
||||||
// ==================================================
|
// ==================================================
|
||||||
// Registers
|
// Registers
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import (
|
|||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/style"
|
||||||
|
"github.com/alecthomas/chroma/v2/styles"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -854,3 +856,58 @@ func parseSetOption(m action.Model, opt string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// Colorscheme Commands
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
func cmdColorscheme(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
|
_ = force
|
||||||
|
|
||||||
|
// No args, just print the current scheme
|
||||||
|
if len(args) == 0 {
|
||||||
|
s := m.Styles().ChromaStyle
|
||||||
|
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m.SetCommandOutput(&core.CommandOutput{
|
||||||
|
Lines: []string{s.Name},
|
||||||
|
Inline: true,
|
||||||
|
IsError: false,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Args given, set the scheme
|
||||||
|
name := strings.Join(args, " ")
|
||||||
|
|
||||||
|
chromaStyle := styles.Registry[name]
|
||||||
|
if chromaStyle == nil {
|
||||||
|
m.SetCommandOutput(&core.CommandOutput{
|
||||||
|
Lines: []string{fmt.Sprintf("colorscheme not found: %s", name)},
|
||||||
|
Inline: true,
|
||||||
|
IsError: true,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.SetStyles(style.ChromaStyles(chromaStyle))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdListColorschemes(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
|
_, _ = args, force
|
||||||
|
|
||||||
|
colors := styles.Names()
|
||||||
|
|
||||||
|
m.SetMode(core.CommandOutputMode)
|
||||||
|
m.SetCommandOutput(&core.CommandOutput{
|
||||||
|
Title: ":colorschemes",
|
||||||
|
Lines: colors,
|
||||||
|
Inline: false,
|
||||||
|
IsError: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import (
|
|||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/style"
|
||||||
|
cStyles "github.com/alecthomas/chroma/v2/styles"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -28,6 +30,7 @@ type mockModel struct {
|
|||||||
commandCursor int
|
commandCursor int
|
||||||
commandOutput *core.CommandOutput
|
commandOutput *core.CommandOutput
|
||||||
lastFind core.LastFindCommand
|
lastFind core.LastFindCommand
|
||||||
|
styles style.Styles
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMockModel() *mockModel {
|
func newMockModel() *mockModel {
|
||||||
@ -98,6 +101,8 @@ func (m *mockModel) Mode() core.Mode { return m.mode }
|
|||||||
func (m *mockModel) SetMode(mode core.Mode) { m.mode = mode }
|
func (m *mockModel) SetMode(mode core.Mode) { m.mode = mode }
|
||||||
func (m *mockModel) Settings() core.EditorSettings { return m.settings }
|
func (m *mockModel) Settings() core.EditorSettings { return m.settings }
|
||||||
func (m *mockModel) SetSettings(s core.EditorSettings) { m.settings = s }
|
func (m *mockModel) SetSettings(s core.EditorSettings) { m.settings = s }
|
||||||
|
func (m *mockModel) Styles() style.Styles { return m.styles }
|
||||||
|
func (m *mockModel) SetStyles(s style.Styles) { m.styles = s }
|
||||||
|
|
||||||
// Registers
|
// Registers
|
||||||
func (m *mockModel) Registers() map[rune]core.Register { return m.registers }
|
func (m *mockModel) Registers() map[rune]core.Register { return m.registers }
|
||||||
@ -5601,3 +5606,305 @@ func TestCmdDeleteBuffer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// TestCmdColorscheme Tests
|
||||||
|
// ==================================================
|
||||||
|
|
||||||
|
func TestCmdColorscheme(t *testing.T) {
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// Group 1: Valid name — styles are updated
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
t.Run("valid name updates styles on model", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmdColorscheme(m, []string{"onedark"}, false)
|
||||||
|
|
||||||
|
name := m.Styles().ChromaStyle.Name
|
||||||
|
if name != "onedark" {
|
||||||
|
t.Error("expected styles to change after setting a valid colorscheme")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("same valid name applied twice produces same styles", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmdColorscheme(m, []string{"monokai"}, false)
|
||||||
|
first := m.styles.BackgroundStyle.Render(" ")
|
||||||
|
|
||||||
|
cmdColorscheme(m, []string{"monokai"}, false)
|
||||||
|
second := m.styles.BackgroundStyle.Render(" ")
|
||||||
|
|
||||||
|
if first != second {
|
||||||
|
t.Error("expected applying the same colorscheme twice to produce identical styles")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid name sets no error output", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmdColorscheme(m, []string{"monokai"}, false)
|
||||||
|
|
||||||
|
if m.commandOutput != nil && m.commandOutput.IsError {
|
||||||
|
t.Error("expected no error output for a valid colorscheme name")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid name returns nil tea.Cmd", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmd := cmdColorscheme(m, []string{"monokai"}, false)
|
||||||
|
|
||||||
|
if cmd != nil {
|
||||||
|
t.Error("expected nil tea.Cmd for colorscheme command")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// Group 2: Invalid name — error is reported
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
t.Run("unknown name sets error output", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmdColorscheme(m, []string{"not-a-real-theme"}, false)
|
||||||
|
|
||||||
|
if m.commandOutput == nil {
|
||||||
|
t.Fatal("expected commandOutput to be set for unknown colorscheme")
|
||||||
|
}
|
||||||
|
if !m.commandOutput.IsError {
|
||||||
|
t.Error("expected commandOutput.IsError to be true for unknown colorscheme")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unknown name does not change styles", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
before := m.styles.BackgroundStyle.Render(" ")
|
||||||
|
|
||||||
|
cmdColorscheme(m, []string{"not-a-real-theme"}, false)
|
||||||
|
|
||||||
|
if m.styles.BackgroundStyle.Render(" ") != before {
|
||||||
|
t.Error("expected styles to remain unchanged after unknown colorscheme")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty string name sets error output", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmdColorscheme(m, []string{""}, false)
|
||||||
|
|
||||||
|
if m.commandOutput == nil || !m.commandOutput.IsError {
|
||||||
|
t.Error("expected error output for empty colorscheme name")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unknown name error output is inline", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmdColorscheme(m, []string{"not-a-real-theme"}, false)
|
||||||
|
|
||||||
|
if m.commandOutput == nil {
|
||||||
|
t.Fatal("expected commandOutput to be set")
|
||||||
|
}
|
||||||
|
if !m.commandOutput.Inline {
|
||||||
|
t.Error("expected error commandOutput to be inline")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// Group 3: No args — print current scheme
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
t.Run("no args sets no error output", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmdColorscheme(m, []string{}, false)
|
||||||
|
|
||||||
|
if m.commandOutput != nil && m.commandOutput.IsError {
|
||||||
|
t.Error("expected no error output when called with no args")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no args does not change styles", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
before := m.styles.BackgroundStyle.Render(" ")
|
||||||
|
|
||||||
|
cmdColorscheme(m, []string{}, false)
|
||||||
|
|
||||||
|
if m.styles.BackgroundStyle.Render(" ") != before {
|
||||||
|
t.Error("expected styles to remain unchanged when no args given")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no args returns nil tea.Cmd", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmd := cmdColorscheme(m, []string{}, false)
|
||||||
|
|
||||||
|
if cmd != nil {
|
||||||
|
t.Error("expected nil tea.Cmd when called with no args")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// Group 4: Extra args — only first arg used, no panic
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
t.Run("extra args beyond name do not panic", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmdColorscheme(m, []string{"monokai", "extra", "args"}, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// Group 5: Force flag — has no effect on outcome
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
t.Run("force flag with valid name still sets styles", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmdColorscheme(m, []string{"monokai"}, true)
|
||||||
|
|
||||||
|
name := m.Styles().ChromaStyle.Name
|
||||||
|
if name != "monokai" {
|
||||||
|
t.Error("expected styles to change with force=true and valid name")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("force flag with invalid name still sets error output", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmdColorscheme(m, []string{"not-a-real-theme"}, true)
|
||||||
|
|
||||||
|
if m.commandOutput == nil || !m.commandOutput.IsError {
|
||||||
|
t.Error("expected error output for unknown colorscheme even with force=true")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// TestCmdListColorschemes Tests
|
||||||
|
// ==================================================
|
||||||
|
|
||||||
|
func TestCmdListColorschemes(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("sets mode to CommandOutputMode", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmdListColorschemes(m, []string{}, false)
|
||||||
|
|
||||||
|
if m.mode != core.CommandOutputMode {
|
||||||
|
t.Errorf("expected mode CommandOutputMode, got %v", m.mode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sets commandOutput", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmdListColorschemes(m, []string{}, false)
|
||||||
|
|
||||||
|
if m.commandOutput == nil {
|
||||||
|
t.Fatal("expected commandOutput to be set")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("commandOutput is not an error", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmdListColorschemes(m, []string{}, false)
|
||||||
|
|
||||||
|
if m.commandOutput.IsError {
|
||||||
|
t.Error("expected commandOutput.IsError to be false")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("commandOutput is not inline", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmdListColorschemes(m, []string{}, false)
|
||||||
|
|
||||||
|
if m.commandOutput.Inline {
|
||||||
|
t.Error("expected commandOutput.Inline to be false")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("commandOutput title is :colorschemes", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmdListColorschemes(m, []string{}, false)
|
||||||
|
|
||||||
|
if m.commandOutput.Title != ":colorschemes" {
|
||||||
|
t.Errorf("expected title ':colorschemes', got %q", m.commandOutput.Title)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("commandOutput lines contains known built-in styles", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmdListColorschemes(m, []string{}, false)
|
||||||
|
|
||||||
|
lines := m.commandOutput.Lines
|
||||||
|
known := []string{"monokai", "github-dark", "dracula"}
|
||||||
|
for _, name := range known {
|
||||||
|
found := false
|
||||||
|
for _, l := range lines {
|
||||||
|
if l == name {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected style %q to appear in colorschemes list", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("commandOutput lines matches styles.Names()", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmdListColorschemes(m, []string{}, false)
|
||||||
|
|
||||||
|
expected := cStyles.Names()
|
||||||
|
if len(m.commandOutput.Lines) != len(expected) {
|
||||||
|
t.Errorf("expected %d colorschemes, got %d", len(expected), len(m.commandOutput.Lines))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("commandOutput lines are non-empty strings", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmdListColorschemes(m, []string{}, false)
|
||||||
|
|
||||||
|
for i, l := range m.commandOutput.Lines {
|
||||||
|
if strings.TrimSpace(l) == "" {
|
||||||
|
t.Errorf("commandOutput.Lines[%d] is blank", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("args and force are ignored", func(t *testing.T) {
|
||||||
|
m1 := newMockModel()
|
||||||
|
m2 := newMockModel()
|
||||||
|
|
||||||
|
cmdListColorschemes(m1, []string{}, false)
|
||||||
|
cmdListColorschemes(m2, []string{"monokai", "extra"}, true)
|
||||||
|
|
||||||
|
if len(m1.commandOutput.Lines) != len(m2.commandOutput.Lines) {
|
||||||
|
t.Error("expected args and force to have no effect on list output")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns nil tea.Cmd", func(t *testing.T) {
|
||||||
|
m := newMockModel()
|
||||||
|
|
||||||
|
cmd := cmdListColorschemes(m, []string{}, false)
|
||||||
|
|
||||||
|
if cmd != nil {
|
||||||
|
t.Error("expected nil tea.Cmd")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = style.DefaultStyles
|
||||||
|
|||||||
@ -217,4 +217,16 @@ func (r *Registry) registerDefaults() {
|
|||||||
ShortForm: "e",
|
ShortForm: "e",
|
||||||
Handler: cmdEdit,
|
Handler: cmdEdit,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Color scheme commands
|
||||||
|
r.Register(Command{
|
||||||
|
Name: "colorscheme",
|
||||||
|
ShortForm: "colo",
|
||||||
|
Handler: cmdColorscheme,
|
||||||
|
})
|
||||||
|
r.Register(Command{
|
||||||
|
Name: "colorschemes",
|
||||||
|
ShortForm: "colorschemes",
|
||||||
|
Handler: cmdListColorschemes,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,108 +4,108 @@ package core
|
|||||||
var CurrentWindowId int = 1000
|
var CurrentWindowId int = 1000
|
||||||
|
|
||||||
type WindowBuilder struct {
|
type WindowBuilder struct {
|
||||||
window Window
|
window Window
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWindowBuilder: Creates a new window builder. The window builder implements a
|
// NewWindowBuilder: Creates a new window builder. The window builder implements a
|
||||||
// builder pattern to create a window with the defined properties and values.
|
// builder pattern to create a window with the defined properties and values.
|
||||||
func NewWindowBuilder() *WindowBuilder {
|
func NewWindowBuilder() *WindowBuilder {
|
||||||
return &WindowBuilder{
|
return &WindowBuilder{
|
||||||
window: Window{
|
window: Window{
|
||||||
Id: 0, // This is set when built
|
Id: 0, // This is set when built
|
||||||
Number: 1, // Ignored for now, will be used for splits
|
Number: 1, // Ignored for now, will be used for splits
|
||||||
Buffer: nil,
|
Buffer: nil,
|
||||||
Cursor: Position{Line: 0, Col: 0},
|
Cursor: Position{Line: 0, Col: 0},
|
||||||
Anchor: Position{Line: 0, Col: 0},
|
Anchor: Position{Line: 0, Col: 0},
|
||||||
ScrollY: 0,
|
ScrollY: 0,
|
||||||
Height: 0,
|
Height: 0,
|
||||||
Width: 0,
|
Width: 0,
|
||||||
Options: NewDefaultWinOptions(),
|
Options: NewDefaultWinOptions(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WindowBuilder.WithNumber: Attaches a window number to the window that is being built.
|
// WindowBuilder.WithNumber: Attaches a window number to the window that is being built.
|
||||||
// Window numbers are position-based and change when windows are rearranged. This is
|
// Window numbers are position-based and change when windows are rearranged. This is
|
||||||
// ignored for now, but will be used when splits are implemented.
|
// ignored for now, but will be used when splits are implemented.
|
||||||
func (w *WindowBuilder) WithNumber(number int) *WindowBuilder {
|
func (w *WindowBuilder) WithNumber(number int) *WindowBuilder {
|
||||||
w.window.Number = number
|
w.window.Number = number
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// WindowBuilder.WithBuffer: Attaches a buffer to the window that is being built. The
|
// WindowBuilder.WithBuffer: Attaches a buffer to the window that is being built. The
|
||||||
// window will display and edit the content of this buffer.
|
// window will display and edit the content of this buffer.
|
||||||
func (w *WindowBuilder) WithBuffer(buffer *Buffer) *WindowBuilder {
|
func (w *WindowBuilder) WithBuffer(buffer *Buffer) *WindowBuilder {
|
||||||
w.window.Buffer = buffer
|
w.window.Buffer = buffer
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// WindowBuilder.WithCursor: Sets the cursor position in the window that is being built.
|
// WindowBuilder.WithCursor: Sets the cursor position in the window that is being built.
|
||||||
func (w *WindowBuilder) WithCursor(cursor Position) *WindowBuilder {
|
func (w *WindowBuilder) WithCursor(cursor Position) *WindowBuilder {
|
||||||
w.window.Cursor = cursor
|
w.window.Cursor = cursor
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// WindowBuilder.WithCursorPos: Sets the cursor position in the window that is being built.
|
// WindowBuilder.WithCursorPos: Sets the cursor position in the window that is being built.
|
||||||
// This is an alias for WithCursor that accepts line and column separately.
|
// This is an alias for WithCursor that accepts line and column separately.
|
||||||
func (w *WindowBuilder) WithCursorPos(line, col int) *WindowBuilder {
|
func (w *WindowBuilder) WithCursorPos(line, col int) *WindowBuilder {
|
||||||
w.window.Cursor = Position{Line: line, Col: col}
|
w.window.Cursor = Position{Line: line, Col: col}
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// WindowBuilder.WithAnchor: Sets the anchor position in the window that is being built.
|
// WindowBuilder.WithAnchor: Sets the anchor position in the window that is being built.
|
||||||
// The anchor is used for visual mode selections.
|
// The anchor is used for visual mode selections.
|
||||||
func (w *WindowBuilder) WithAnchor(anchor Position) *WindowBuilder {
|
func (w *WindowBuilder) WithAnchor(anchor Position) *WindowBuilder {
|
||||||
w.window.Anchor = anchor
|
w.window.Anchor = anchor
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// WindowBuilder.WithAnchorPos: Sets the anchor position in the window that is being built.
|
// WindowBuilder.WithAnchorPos: Sets the anchor position in the window that is being built.
|
||||||
// This is an alias for WithAnchor that accepts line and column separately.
|
// This is an alias for WithAnchor that accepts line and column separately.
|
||||||
func (w *WindowBuilder) WithAnchorPos(line, col int) *WindowBuilder {
|
func (w *WindowBuilder) WithAnchorPos(line, col int) *WindowBuilder {
|
||||||
w.window.Anchor = Position{Line: line, Col: col}
|
w.window.Anchor = Position{Line: line, Col: col}
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// WindowBuilder.WithScrollY: Sets the vertical scroll offset of the window that is being built.
|
// WindowBuilder.WithScrollY: Sets the vertical scroll offset of the window that is being built.
|
||||||
func (w *WindowBuilder) WithScrollY(scrollY int) *WindowBuilder {
|
func (w *WindowBuilder) WithScrollY(scrollY int) *WindowBuilder {
|
||||||
w.window.ScrollY = scrollY
|
w.window.ScrollY = scrollY
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// WindowBuilder.WithHeight: Sets the height of the window that is being built.
|
// WindowBuilder.WithHeight: Sets the height of the window that is being built.
|
||||||
func (w *WindowBuilder) WithHeight(height int) *WindowBuilder {
|
func (w *WindowBuilder) WithHeight(height int) *WindowBuilder {
|
||||||
w.window.Height = height
|
w.window.Height = height
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// WindowBuilder.WithWidth: Sets the width of the window that is being built.
|
// WindowBuilder.WithWidth: Sets the width of the window that is being built.
|
||||||
func (w *WindowBuilder) WithWidth(width int) *WindowBuilder {
|
func (w *WindowBuilder) WithWidth(width int) *WindowBuilder {
|
||||||
w.window.Width = width
|
w.window.Width = width
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// WindowBuilder.WithDimensions: Sets both width and height of the window that is being built.
|
// WindowBuilder.WithDimensions: Sets both width and height of the window that is being built.
|
||||||
// This is a convenience method for setting dimensions in one call.
|
// This is a convenience method for setting dimensions in one call.
|
||||||
func (w *WindowBuilder) WithDimensions(width, height int) *WindowBuilder {
|
func (w *WindowBuilder) WithDimensions(width, height int) *WindowBuilder {
|
||||||
w.window.Width = width
|
w.window.Width = width
|
||||||
w.window.Height = height
|
w.window.Height = height
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// WindowBuilder.WithOptions: Applies the options to the window that is being built.
|
// WindowBuilder.WithOptions: Applies the options to the window that is being built.
|
||||||
// This is a convenience method for setting all options in one call.
|
// This is a convenience method for setting all options in one call.
|
||||||
func (w *WindowBuilder) WithOptions(options WinOptions) *WindowBuilder {
|
func (w *WindowBuilder) WithOptions(options WinOptions) *WindowBuilder {
|
||||||
w.window.Options = options
|
w.window.Options = options
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// WindowBuilder.Build: Build the final window and return it to the caller. Final
|
// WindowBuilder.Build: Build the final window and return it to the caller. Final
|
||||||
// step in the process. This is where the ID is set, so many windows can be "in-progress"
|
// step in the process. This is where the ID is set, so many windows can be "in-progress"
|
||||||
// but the ID will be set when they are built. Meaning, this is not thread safe.
|
// but the ID will be set when they are built. Meaning, this is not thread safe.
|
||||||
func (w *WindowBuilder) Build() Window {
|
func (w *WindowBuilder) Build() Window {
|
||||||
w.window.Id = CurrentWindowId
|
w.window.Id = CurrentWindowId
|
||||||
CurrentWindowId++
|
CurrentWindowId++
|
||||||
|
|
||||||
return w.window
|
return w.window
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,13 +4,86 @@ import (
|
|||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/input"
|
"git.gophernest.net/azpect/TextEditor/internal/input"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/style"
|
"git.gophernest.net/azpect/TextEditor/internal/style"
|
||||||
|
"github.com/alecthomas/chroma/v2/styles"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelBuilder struct {
|
type ModelBuilder struct {
|
||||||
model Model
|
model Model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RPGLE
|
||||||
|
// abap
|
||||||
|
// algol
|
||||||
|
// algol_nu
|
||||||
|
// arduino
|
||||||
|
// ashen
|
||||||
|
// aura-theme-dark
|
||||||
|
// aura-theme-dark-soft
|
||||||
|
// autumn
|
||||||
|
// average
|
||||||
|
// base16-snazzy
|
||||||
|
// borland
|
||||||
|
// bw
|
||||||
|
// catppuccin-frappe
|
||||||
|
// catppuccin-latte
|
||||||
|
// catppuccin-macchiato
|
||||||
|
// catppuccin-mocha
|
||||||
|
// colorful
|
||||||
|
// doom-one
|
||||||
|
// doom-one2
|
||||||
|
// dracula
|
||||||
|
// emacs
|
||||||
|
// evergarden
|
||||||
|
// friendly
|
||||||
|
// fruity
|
||||||
|
// github
|
||||||
|
// github-dark
|
||||||
|
// gruvbox
|
||||||
|
// gruvbox-light
|
||||||
|
// hr_high_contrast
|
||||||
|
// hrdark
|
||||||
|
// igor
|
||||||
|
// lovelace
|
||||||
|
// manni
|
||||||
|
// modus-operandi
|
||||||
|
// modus-vivendi
|
||||||
|
// monokai
|
||||||
|
// monokailight
|
||||||
|
// murphy
|
||||||
|
// native
|
||||||
|
// nord
|
||||||
|
// nordic
|
||||||
|
// onedark
|
||||||
|
// onesenterprise
|
||||||
|
// paraiso-dark
|
||||||
|
// paraiso-light
|
||||||
|
// pastie
|
||||||
|
// perldoc
|
||||||
|
// pygments
|
||||||
|
// rainbow_dash
|
||||||
|
// rose-pine
|
||||||
|
// rose-pine-dawn
|
||||||
|
// rose-pine-moon
|
||||||
|
// rrt
|
||||||
|
// solarized-dark
|
||||||
|
// solarized-dark256
|
||||||
|
// solarized-light
|
||||||
|
// swapoff
|
||||||
|
// tango
|
||||||
|
// tokyonight-day
|
||||||
|
// tokyonight-moon
|
||||||
|
// tokyonight-night
|
||||||
|
// tokyonight-storm
|
||||||
|
// trac
|
||||||
|
// vim
|
||||||
|
// vs
|
||||||
|
// vulcan
|
||||||
|
// witchhazel
|
||||||
|
// xcode
|
||||||
|
// xcode-dark
|
||||||
func NewModelBuilder() *ModelBuilder {
|
func NewModelBuilder() *ModelBuilder {
|
||||||
|
chromaStyle := styles.Get("kanagawa-wave")
|
||||||
|
|
||||||
return &ModelBuilder{
|
return &ModelBuilder{
|
||||||
model: Model{
|
model: Model{
|
||||||
buffers: []*core.Buffer{},
|
buffers: []*core.Buffer{},
|
||||||
@ -28,7 +101,7 @@ func NewModelBuilder() *ModelBuilder {
|
|||||||
commandOutput: nil,
|
commandOutput: nil,
|
||||||
settings: core.NewDefaultSettings(),
|
settings: core.NewDefaultSettings(),
|
||||||
registers: core.DefaultRegisters(),
|
registers: core.DefaultRegisters(),
|
||||||
styles: style.DefaultStyles(),
|
styles: style.ChromaStyles(chromaStyle),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import (
|
|||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/style"
|
"git.gophernest.net/azpect/TextEditor/internal/style"
|
||||||
|
"github.com/alecthomas/chroma/v2"
|
||||||
|
"github.com/alecthomas/chroma/v2/lexers"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -49,17 +51,28 @@ func viewWindow(w *core.Window, styles style.Styles, options core.WinOptions, mo
|
|||||||
start := w.ScrollY
|
start := w.ScrollY
|
||||||
end := w.ScrollY + w.ViewportHeight()
|
end := w.ScrollY + w.ViewportHeight()
|
||||||
|
|
||||||
|
// Chroma stuff
|
||||||
|
name := strings.ReplaceAll(buf.Filetype, ".", "")
|
||||||
|
lexer := lexers.Get(name)
|
||||||
|
if lexer == nil {
|
||||||
|
lexer = lexers.Fallback
|
||||||
|
}
|
||||||
|
lexer = chroma.Coalesce(lexer) // Merge tokens together
|
||||||
|
|
||||||
// Draw buffer lines
|
// Draw buffer lines
|
||||||
for lineNum := start; lineNum < end; lineNum++ {
|
for lineNum := start; lineNum < end; lineNum++ {
|
||||||
if lineNum < buf.LineCount() {
|
if lineNum < buf.LineCount() {
|
||||||
line := drawLine(w, styles, options, mode, buf.Line(lineNum), lineNum)
|
styleMap := styles.MakeStyleMap(lexer, buf.Line(lineNum))
|
||||||
|
line := drawLine(w, styles, options, mode, buf.Line(lineNum), lineNum, styleMap)
|
||||||
view.WriteString(line)
|
view.WriteString(line)
|
||||||
|
} else {
|
||||||
|
view.WriteString(strings.Repeat(styles.BackgroundStyle.Render(" "), w.Width))
|
||||||
}
|
}
|
||||||
view.WriteRune('\n')
|
view.WriteRune('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw status line
|
// Draw status line
|
||||||
statusBar := drawStatusBar(w, mode)
|
statusBar := drawStatusBar(w, mode, styles)
|
||||||
view.WriteString(statusBar + "\n")
|
view.WriteString(statusBar + "\n")
|
||||||
|
|
||||||
return view.String()
|
return view.String()
|
||||||
@ -67,13 +80,9 @@ func viewWindow(w *core.Window, styles style.Styles, options core.WinOptions, mo
|
|||||||
|
|
||||||
// drawLine: Renders a single line with syntax highlighting, cursor, and visual selection.
|
// drawLine: Renders a single line with syntax highlighting, cursor, and visual selection.
|
||||||
// Handles gutter, cursor rendering, and visual mode highlighting.
|
// Handles gutter, cursor rendering, and visual mode highlighting.
|
||||||
func drawLine(w *core.Window, styles style.Styles, options core.WinOptions, mode core.Mode, line string, lineNumber int) string {
|
func drawLine(w *core.Window, styles style.Styles, options core.WinOptions, mode core.Mode, line string, lineNumber int, styleMap []lipgloss.Style) string {
|
||||||
runes := []rune(line)
|
|
||||||
|
|
||||||
curStyle := styles.CursorStyle(mode)
|
|
||||||
visStyle := styles.VisualHighlight
|
|
||||||
|
|
||||||
var view strings.Builder
|
var view strings.Builder
|
||||||
|
runes := []rune(line)
|
||||||
|
|
||||||
// Draw gutter first
|
// Draw gutter first
|
||||||
gutter := drawGutter(w, styles, options, lineNumber)
|
gutter := drawGutter(w, styles, options, lineNumber)
|
||||||
@ -84,24 +93,33 @@ func drawLine(w *core.Window, styles style.Styles, options core.WinOptions, mode
|
|||||||
// Current char is cursor
|
// Current char is cursor
|
||||||
if col == w.Cursor.Col && lineNumber == w.Cursor.Line {
|
if col == w.Cursor.Col && lineNumber == w.Cursor.Line {
|
||||||
if col < len(runes) {
|
if col < len(runes) {
|
||||||
view.WriteString(curStyle.Render(string(runes[col])))
|
cur := styles.CursorStyle(mode, styleMap[col])
|
||||||
|
view.WriteString(cur.Render(string(runes[col])))
|
||||||
} else {
|
} else {
|
||||||
view.WriteString(curStyle.Render(" "))
|
view.WriteString(styles.DefaultCursorStyle(mode).Render(" "))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not cursor, but not end
|
// Not cursor, but not end
|
||||||
} else if col < len(runes) {
|
} else if col < len(runes) {
|
||||||
|
s := styleMap[col]
|
||||||
if mode.IsVisualMode() && posInsideSelection(w, mode, col, lineNumber) {
|
if mode.IsVisualMode() && posInsideSelection(w, mode, col, lineNumber) {
|
||||||
view.WriteString(visStyle.Render(string(runes[col])))
|
vis := styles.VisualHighlightWithTextColor(s)
|
||||||
|
view.WriteString(vis.Render(string(runes[col])))
|
||||||
} else {
|
} else {
|
||||||
view.WriteRune(runes[col])
|
view.WriteString(s.Render(string(runes[col])))
|
||||||
}
|
}
|
||||||
// Allow highlight on blank lines or chars
|
// Allow highlight on blank lines or chars
|
||||||
} else if mode.IsVisualMode() && posInsideSelection(w, mode, col, lineNumber) {
|
} else if mode.IsVisualMode() && posInsideSelection(w, mode, col, lineNumber) {
|
||||||
view.WriteString(visStyle.Render(" "))
|
view.WriteString(styles.VisualHighlight.Render(" "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pad remainder of line to window width with background color
|
||||||
|
dif := w.Width - lipgloss.Width(view.String())
|
||||||
|
if dif > 0 {
|
||||||
|
view.WriteString(strings.Repeat(styles.BackgroundStyle.Render(" "), dif))
|
||||||
|
}
|
||||||
|
|
||||||
return view.String()
|
return view.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,9 +171,9 @@ func drawGutter(w *core.Window, styles style.Styles, options core.WinOptions, cu
|
|||||||
|
|
||||||
// drawStatusBar: Renders the status bar with mode and cursor position,
|
// drawStatusBar: Renders the status bar with mode and cursor position,
|
||||||
// padding the middle with spaces to fill the terminal width.
|
// padding the middle with spaces to fill the terminal width.
|
||||||
func drawStatusBar(w *core.Window, mode core.Mode) string {
|
func drawStatusBar(w *core.Window, mode core.Mode, styles style.Styles) string {
|
||||||
left := leftBar(w, mode)
|
left := leftBar(w, mode, styles)
|
||||||
right := rightBar(w, mode)
|
right := rightBar(w, mode, styles)
|
||||||
|
|
||||||
diff := w.Width - (lipgloss.Width(left) + lipgloss.Width(right))
|
diff := w.Width - (lipgloss.Width(left) + lipgloss.Width(right))
|
||||||
|
|
||||||
@ -164,12 +182,12 @@ func drawStatusBar(w *core.Window, mode core.Mode) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
middle := strings.Repeat(" ", diff)
|
middle := strings.Repeat(styles.BackgroundStyle.Render(" "), diff)
|
||||||
return left + middle + right
|
return left + middle + right
|
||||||
}
|
}
|
||||||
|
|
||||||
// leftBar: Returns the left side of the status bar showing the current mode.
|
// leftBar: Returns the left side of the status bar showing the current mode.
|
||||||
func leftBar(w *core.Window, mode core.Mode) string {
|
func leftBar(w *core.Window, mode core.Mode, styles style.Styles) string {
|
||||||
buf := w.Buffer
|
buf := w.Buffer
|
||||||
|
|
||||||
var flags []string
|
var flags []string
|
||||||
@ -185,12 +203,13 @@ func leftBar(w *core.Window, mode core.Mode) string {
|
|||||||
flagStr = "(" + strings.Join(flags, "") + ")"
|
flagStr = "(" + strings.Join(flags, "") + ")"
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf(" %s %s %s", mode.ToString(), buf.Filename, flagStr)
|
bar := fmt.Sprintf(" %s %s %s", mode.ToString(), buf.Filename, flagStr)
|
||||||
|
return styles.LineStyle.Render(bar)
|
||||||
}
|
}
|
||||||
|
|
||||||
// rightBar: Returns the right side of the status bar showing cursor position
|
// rightBar: Returns the right side of the status bar showing cursor position
|
||||||
// and selection count in visual mode.
|
// and selection count in visual mode.
|
||||||
func rightBar(w *core.Window, mode core.Mode) (bar string) {
|
func rightBar(w *core.Window, mode core.Mode, styles style.Styles) (bar string) {
|
||||||
if mode.IsVisualMode() {
|
if mode.IsVisualMode() {
|
||||||
lineCount := max(w.Anchor.Line, w.Cursor.Line) - min(w.Anchor.Line, w.Cursor.Line) + 1
|
lineCount := max(w.Anchor.Line, w.Cursor.Line) - min(w.Anchor.Line, w.Cursor.Line) + 1
|
||||||
bar = fmt.Sprintf("%d:%d <%d>", w.Cursor.Line+1, w.Cursor.Col+1, lineCount)
|
bar = fmt.Sprintf("%d:%d <%d>", w.Cursor.Line+1, w.Cursor.Col+1, lineCount)
|
||||||
@ -198,41 +217,44 @@ func rightBar(w *core.Window, mode core.Mode) (bar string) {
|
|||||||
bar = fmt.Sprintf("%d:%d ", w.Cursor.Line+1, w.Cursor.Col+1)
|
bar = fmt.Sprintf("%d:%d ", w.Cursor.Line+1, w.Cursor.Col+1)
|
||||||
}
|
}
|
||||||
buf := w.Buffer
|
buf := w.Buffer
|
||||||
bar = fmt.Sprintf("%s %s", buf.Filetype, bar)
|
bar = styles.LineStyle.Render(fmt.Sprintf("%s %s", buf.Filetype, bar))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawCommandBar: Renders the command line showing command input, errors, or
|
// drawCommandBar: Renders the command line showing command input, errors, or
|
||||||
// output depending on the current mode and state.
|
// output depending on the current mode and state.
|
||||||
func drawCommandBar(m Model) string {
|
func drawCommandBar(m Model) string {
|
||||||
|
styles := m.Styles()
|
||||||
|
|
||||||
// Compute left bar (command side)
|
// Compute left bar (command side)
|
||||||
var leftBar string
|
var leftBar string
|
||||||
if m.Mode() == core.CommandMode {
|
if m.Mode() == core.CommandMode {
|
||||||
leftBar = ":"
|
leftBar = styles.LineStyle.Render(":")
|
||||||
cmd := []rune(m.Command())
|
cmd := []rune(m.Command())
|
||||||
cur := m.CommandCursor()
|
cur := m.CommandCursor()
|
||||||
for i, r := range cmd {
|
for i, r := range cmd {
|
||||||
if i == cur {
|
if i == cur {
|
||||||
leftBar += m.Styles().CursorStyle(m.Mode()).Render(string(r))
|
leftBar += styles.DefaultCursorStyle(m.Mode()).Render(string(r))
|
||||||
} else {
|
} else {
|
||||||
leftBar += string(r)
|
leftBar += styles.LineStyle.Render(string(r))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Cursor at end of command
|
// Cursor at end of command
|
||||||
if cur >= len(cmd) {
|
if cur >= len(cmd) {
|
||||||
leftBar += m.Styles().CursorStyle(m.Mode()).Render(" ")
|
leftBar += styles.DefaultCursorStyle(m.Mode()).Render(" ")
|
||||||
}
|
}
|
||||||
// bar = fmt.Sprintf("%s %d", bar, cur)
|
// bar = fmt.Sprintf("%s %d", bar, cur)
|
||||||
} else if out := m.CommandOutput(); out != nil && len(out.Lines) > 0 && out.Inline {
|
} else if out := m.CommandOutput(); out != nil && len(out.Lines) > 0 && out.Inline {
|
||||||
// TODO: This is not perfect, temporary
|
// TODO: This is not perfect, temporary
|
||||||
text := strings.Join(out.Lines, " ")
|
text := strings.Join(out.Lines, " ")
|
||||||
if out.IsError {
|
if out.IsError {
|
||||||
leftBar = m.Styles().CommandError.Render(text)
|
leftBar = styles.CommandError.Render(text)
|
||||||
} else {
|
} else {
|
||||||
leftBar = text
|
leftBar = styles.LineStyle.Render(text)
|
||||||
}
|
}
|
||||||
} else if strings.TrimSpace(m.Command()) != "" {
|
} else if strings.TrimSpace(m.Command()) != "" {
|
||||||
leftBar = fmt.Sprintf(":%s", m.Command())
|
content := fmt.Sprintf(":%s", m.Command())
|
||||||
|
leftBar = styles.LineStyle.Render(content) //
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute right bar
|
// Compute right bar
|
||||||
@ -240,12 +262,13 @@ func drawCommandBar(m Model) string {
|
|||||||
var rightBar string
|
var rightBar string
|
||||||
if len(m.input.Pending()) > 0 {
|
if len(m.input.Pending()) > 0 {
|
||||||
width := 10 // Size of the block to display
|
width := 10 // Size of the block to display
|
||||||
rightBar = fmt.Sprintf("%-*s", width, m.input.Pending())
|
content := fmt.Sprintf("%-*s", width, m.input.Pending())
|
||||||
|
rightBar = styles.LineStyle.Render(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
dif := m.termWidth - (lipgloss.Width(leftBar) + lipgloss.Width(rightBar))
|
dif := m.termWidth - (lipgloss.Width(leftBar) + lipgloss.Width(rightBar))
|
||||||
|
|
||||||
bar := leftBar + strings.Repeat(" ", max(0, dif)) + rightBar
|
bar := leftBar + strings.Repeat(styles.BackgroundStyle.Render(" "), max(0, dif)) + rightBar
|
||||||
return bar
|
return bar
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,10 +333,12 @@ func overlayCommandOutputWindow(view string, cmd *core.CommandOutput, styles sty
|
|||||||
overlay = append(overlay, styles.CommandOutputBorder.Render(strings.Repeat(" ", termWidth)))
|
overlay = append(overlay, styles.CommandOutputBorder.Render(strings.Repeat(" ", termWidth)))
|
||||||
|
|
||||||
if strings.TrimSpace(cmd.Title) != "" {
|
if strings.TrimSpace(cmd.Title) != "" {
|
||||||
overlay = append(overlay, cmd.Title)
|
title := styles.LineStyle.Render(cmd.Title)
|
||||||
|
overlay = append(overlay, title)
|
||||||
}
|
}
|
||||||
for _, l := range cmd.Lines {
|
for _, l := range cmd.Lines {
|
||||||
overlay = append(overlay, strings.ReplaceAll(l, "\n", "\\n"))
|
content := styles.LineStyle.Render(strings.ReplaceAll(l, "\n", "\\n"))
|
||||||
|
overlay = append(overlay, content)
|
||||||
}
|
}
|
||||||
overlay = append(overlay, styles.CommandContinueMessage.Render(core.CommandOutputExitMessage))
|
overlay = append(overlay, styles.CommandContinueMessage.Render(core.CommandOutputExitMessage))
|
||||||
|
|
||||||
@ -321,6 +346,12 @@ func overlayCommandOutputWindow(view string, cmd *core.CommandOutput, styles sty
|
|||||||
// which would cause Lipgloss to embed newlines internally and corrupt the line count.
|
// which would cause Lipgloss to embed newlines internally and corrupt the line count.
|
||||||
// If block-level styles are ever added, this approach must be replaced.
|
// If block-level styles are ever added, this approach must be replaced.
|
||||||
|
|
||||||
|
// Add background color to end of each line
|
||||||
|
for i, l := range overlay {
|
||||||
|
dif := termWidth - lipgloss.Width(l)
|
||||||
|
overlay[i] += styles.BackgroundStyle.Render(strings.Repeat(" ", dif))
|
||||||
|
}
|
||||||
|
|
||||||
// Remove 'h' lines from back of view and append overlay
|
// Remove 'h' lines from back of view and append overlay
|
||||||
h := len(overlay)
|
h := len(overlay)
|
||||||
final := lines[:max(0, len(lines)-h)]
|
final := lines[:max(0, len(lines)-h)]
|
||||||
|
|||||||
@ -46,6 +46,7 @@ func (p *ProgramBuilder) FileProgram(filename string) *ProgramBuilder {
|
|||||||
WithType(core.FileBuffer).
|
WithType(core.FileBuffer).
|
||||||
WithFilename(filename).
|
WithFilename(filename).
|
||||||
WithFiletype(ext).
|
WithFiletype(ext).
|
||||||
|
Listed().
|
||||||
Build()
|
Build()
|
||||||
|
|
||||||
win := core.NewWindowBuilder().
|
win := core.NewWindowBuilder().
|
||||||
|
|||||||
158
internal/style/style.go
Normal file → Executable file
158
internal/style/style.go
Normal file → Executable file
@ -2,6 +2,7 @@ package style
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
|
"github.com/alecthomas/chroma/v2"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -28,9 +29,16 @@ type Styles struct {
|
|||||||
CommandError lipgloss.Style
|
CommandError lipgloss.Style
|
||||||
CommandOutputBorder lipgloss.Style
|
CommandOutputBorder lipgloss.Style
|
||||||
CommandContinueMessage lipgloss.Style
|
CommandContinueMessage lipgloss.Style
|
||||||
|
|
||||||
|
// General Styles
|
||||||
|
LineStyle lipgloss.Style // This is a simple background with no text coloring
|
||||||
|
BackgroundStyle lipgloss.Style // This is just the background
|
||||||
|
|
||||||
|
// Chroma data
|
||||||
|
ChromaStyle *chroma.Style
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultStyles returns the default editor color scheme.
|
// DefaultStyles: Returns the default editor color scheme.
|
||||||
func DefaultStyles() Styles {
|
func DefaultStyles() Styles {
|
||||||
return Styles{
|
return Styles{
|
||||||
CursorNormal: lipgloss.NewStyle().Reverse(true),
|
CursorNormal: lipgloss.NewStyle().Reverse(true),
|
||||||
@ -67,11 +75,86 @@ func DefaultStyles() Styles {
|
|||||||
|
|
||||||
CommandContinueMessage: lipgloss.NewStyle().
|
CommandContinueMessage: lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#546fba")),
|
Foreground(lipgloss.Color("#546fba")),
|
||||||
|
|
||||||
|
ChromaStyle: nil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CursorStyle returns the appropriate cursor style for the given mode.
|
func ChromaStyles(chromaStyle *chroma.Style) Styles {
|
||||||
func (s Styles) CursorStyle(mode core.Mode) lipgloss.Style {
|
bgString := chromaStyle.Get(chroma.Background).Background.String()
|
||||||
|
lineNumbers := chromaStyle.Get(chroma.LineTableTD)
|
||||||
|
lineHighlight := chromaStyle.Get(chroma.LineHighlight)
|
||||||
|
|
||||||
|
return Styles{
|
||||||
|
CursorNormal: lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color(bgString)).
|
||||||
|
Reverse(true),
|
||||||
|
|
||||||
|
CursorInsert: lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color(bgString)).
|
||||||
|
Underline(true),
|
||||||
|
|
||||||
|
CursorCommand: lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color(bgString)).
|
||||||
|
Reverse(true),
|
||||||
|
|
||||||
|
Gutter: lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color(
|
||||||
|
darkenColor(lineNumbers.Background, 0.9).String()),
|
||||||
|
).
|
||||||
|
Foreground(lipgloss.Color(lineNumbers.Colour.String())),
|
||||||
|
|
||||||
|
GutterCurrentLine: lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color(
|
||||||
|
darkenColor(lineNumbers.Background, 0.9).String()),
|
||||||
|
).
|
||||||
|
Foreground(lipgloss.Color(lineNumbers.Colour.String())),
|
||||||
|
|
||||||
|
VisualHighlight: lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color(lineHighlight.Background.String())).
|
||||||
|
Foreground(lipgloss.Color(lineHighlight.Colour.String())),
|
||||||
|
|
||||||
|
VisualAnchor: lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color(lineHighlight.Background.String())).
|
||||||
|
Foreground(lipgloss.Color(lineHighlight.Colour.String())),
|
||||||
|
|
||||||
|
StatusBar: lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color(bgString)).
|
||||||
|
Foreground(lipgloss.Color("243")),
|
||||||
|
|
||||||
|
StatusBarActive: lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color(bgString)).
|
||||||
|
Foreground(lipgloss.Color("230")),
|
||||||
|
|
||||||
|
CommandError: lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color(bgString)).
|
||||||
|
Foreground(lipgloss.Color("#e3203a")),
|
||||||
|
|
||||||
|
CommandOutputBorder: lipgloss.NewStyle().
|
||||||
|
Background(
|
||||||
|
lipgloss.Color(
|
||||||
|
darkenColor(
|
||||||
|
chromaStyle.Get(chroma.Background).Background, 0.5).
|
||||||
|
String(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
CommandContinueMessage: lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color(bgString)).
|
||||||
|
Foreground(lipgloss.Color("#546fba")),
|
||||||
|
|
||||||
|
LineStyle: lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color(chromaStyle.Get(chroma.Line).Colour.String())).
|
||||||
|
Background(lipgloss.Color(bgString)),
|
||||||
|
|
||||||
|
BackgroundStyle: lipgloss.NewStyle().Background(lipgloss.Color(bgString)),
|
||||||
|
|
||||||
|
ChromaStyle: chromaStyle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styles.DefaultCursorStyle: Returns the appropriate cursor style for the given mode.
|
||||||
|
func (s Styles) DefaultCursorStyle(mode core.Mode) lipgloss.Style {
|
||||||
switch mode {
|
switch mode {
|
||||||
case core.InsertMode:
|
case core.InsertMode:
|
||||||
return s.CursorInsert
|
return s.CursorInsert
|
||||||
@ -81,3 +164,72 @@ func (s Styles) CursorStyle(mode core.Mode) lipgloss.Style {
|
|||||||
return s.CursorNormal
|
return s.CursorNormal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Styles.CursorStyle: Returns a cursor style derived from a chroma style. This function should preferred
|
||||||
|
// over the DefaultCursorStyle, but in cases where there is no style to apply, the DefaultCursorStyle
|
||||||
|
// will always work.
|
||||||
|
func (s Styles) CursorStyle(mode core.Mode, style lipgloss.Style) lipgloss.Style {
|
||||||
|
switch mode {
|
||||||
|
case core.NormalMode, core.VisualLineMode, core.VisualBlockMode, core.VisualMode:
|
||||||
|
return lipgloss.NewStyle().
|
||||||
|
Background(style.GetForeground()).
|
||||||
|
Foreground(style.GetBackground())
|
||||||
|
default:
|
||||||
|
return lipgloss.NewStyle().
|
||||||
|
Background(s.BackgroundStyle.GetBackground()).
|
||||||
|
Foreground(style.GetForeground()).
|
||||||
|
Underline(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styles.VisualHighlightWithTextColor: Works analogously to CursorStyle vs DefaultCursorStyle. When a
|
||||||
|
// style is available, this function should be used, so the text color will be rendered in front
|
||||||
|
// of the background. Otherwise, the VisualHighlight property will always work.
|
||||||
|
func (s Styles) VisualHighlightWithTextColor(style lipgloss.Style) lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().
|
||||||
|
Background(s.VisualHighlight.GetBackground()).
|
||||||
|
Foreground(style.GetForeground())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styles.MakeStyleMap: Generates a style map for a single line. A style map is a mapping from
|
||||||
|
// column a lipgloss style. Cursor styles are not handled by this map, but they can be derived
|
||||||
|
// by inverting the background and foreground (and rolling back to the default).
|
||||||
|
func (s Styles) MakeStyleMap(lexer chroma.Lexer, line string) []lipgloss.Style {
|
||||||
|
m := make([]lipgloss.Style, len(line))
|
||||||
|
|
||||||
|
if s.ChromaStyle == nil {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
iter, err := lexer.Tokenise(nil, line)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
col := 0
|
||||||
|
for _, token := range iter.Tokens() {
|
||||||
|
entry := s.ChromaStyle.Get(token.Type)
|
||||||
|
s := lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color(entry.Background.String())).
|
||||||
|
Foreground(lipgloss.Color(entry.Colour.String()))
|
||||||
|
for _, char := range token.Value {
|
||||||
|
if char == '\n' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if col < len(m) {
|
||||||
|
m[col] = s
|
||||||
|
}
|
||||||
|
col++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// darkenColor: Uses a factor (0.0 to 1.0) to darken a color using its opacity.
|
||||||
|
func darkenColor(c chroma.Colour, factor float64) chroma.Colour {
|
||||||
|
r := uint8(float64(c.Red()) * factor)
|
||||||
|
g := uint8(float64(c.Green()) * factor)
|
||||||
|
b := uint8(float64(c.Blue()) * factor)
|
||||||
|
return chroma.NewColour(r, g, b)
|
||||||
|
}
|
||||||
|
|||||||
43
internal/theme/theme.go
Normal file
43
internal/theme/theme.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package theme
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/alecthomas/chroma/v2"
|
||||||
|
"github.com/alecthomas/chroma/v2/styles"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed themes/*
|
||||||
|
var themeFS embed.FS
|
||||||
|
|
||||||
|
// RegisterAll: Registers all XML theme files embedded in the themes/ directory
|
||||||
|
// with chroma's style registry. After calling this, styles.Get() will recognize
|
||||||
|
// any theme defined in those files.
|
||||||
|
func RegisterAll() error {
|
||||||
|
entries, err := themeFS.ReadDir("themes")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read embedded themes directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := themeFS.Open("themes/" + entry.Name())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open theme %s: %w", entry.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
style, err := chroma.NewXMLStyle(f)
|
||||||
|
f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse theme %s: %w", entry.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
styles.Register(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
83
internal/theme/themes/kanagawa-dragon.xml
Normal file
83
internal/theme/themes/kanagawa-dragon.xml
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<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>
|
||||||
83
internal/theme/themes/kanagawa-lotus.xml
Normal file
83
internal/theme/themes/kanagawa-lotus.xml
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<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>
|
||||||
83
internal/theme/themes/kanagawa-wave.xml
Normal file
83
internal/theme/themes/kanagawa-wave.xml
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<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>
|
||||||
Loading…
x
Reference in New Issue
Block a user