Gim/internal/style/style.go
Hayden Hargreaves 43b3992522
All checks were successful
Run Test Suite / test (push) Successful in 15s
Run Test Suite / test (pull_request) Successful in 15s
feat: implemented replace mode, tested!
Looking great, maybe I will actually use this lol
2026-04-05 23:53:24 -07:00

283 lines
8.0 KiB
Go
Executable File

package style
import (
"strings"
"git.gophernest.net/azpect/TextEditor/internal/core"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/charmbracelet/lipgloss"
)
// Styles holds all the visual styling for the editor.
type Styles struct {
// Cursor styles by mode
CursorNormal lipgloss.Style
CursorInsert lipgloss.Style
CursorCommand lipgloss.Style
CursorReplace lipgloss.Style
// Gutter (line numbers)
Gutter lipgloss.Style
GutterCurrentLine lipgloss.Style
// Visual mode
VisualHighlight lipgloss.Style
VisualAnchor lipgloss.Style // debugging
// Status bar
StatusBar lipgloss.Style
StatusBarActive lipgloss.Style
// Command line
CommandError lipgloss.Style
CommandOutputBorder 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.
func DefaultStyles() Styles {
return Styles{
CursorNormal: lipgloss.NewStyle().Reverse(true),
CursorInsert: lipgloss.NewStyle().Underline(true),
CursorCommand: lipgloss.NewStyle().Reverse(true),
CursorReplace: lipgloss.NewStyle().Underline(true),
Gutter: lipgloss.NewStyle().
Background(lipgloss.Color("236")).
Foreground(lipgloss.Color("243")),
GutterCurrentLine: lipgloss.NewStyle().
Background(lipgloss.Color("236")).
Foreground(lipgloss.Color("#d69d00")),
VisualHighlight: lipgloss.NewStyle().
Background(lipgloss.Color("#7a6a00")),
VisualAnchor: lipgloss.NewStyle().
Background(lipgloss.Color("#a89020")),
StatusBar: lipgloss.NewStyle().
Background(lipgloss.Color("236")).
Foreground(lipgloss.Color("243")),
StatusBarActive: lipgloss.NewStyle().
Background(lipgloss.Color("62")).
Foreground(lipgloss.Color("230")),
CommandError: lipgloss.NewStyle().
Foreground(lipgloss.Color("#e3203a")),
CommandOutputBorder: lipgloss.NewStyle().
Background(lipgloss.Color("#000000")),
CommandContinueMessage: lipgloss.NewStyle().
Foreground(lipgloss.Color("#546fba")),
ChromaStyle: nil,
}
}
func ChromaStyles(chromaStyle *chroma.Style) Styles {
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)).
Bold(true).
Underline(true),
CursorCommand: lipgloss.NewStyle().
Background(lipgloss.Color(bgString)).
Reverse(true),
CursorReplace: lipgloss.NewStyle().
Background(lipgloss.Color(bgString)).
Underline(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 {
case core.InsertMode:
return s.CursorInsert
case core.CommandMode:
return s.CursorCommand
case core.ReplaceMode:
return s.CursorReplace
default:
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())
case core.ReplaceMode, core.WaitingMode:
return lipgloss.NewStyle().
Background(style.GetBackground()).
Foreground(style.GetForeground()).
Underline(true)
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)
}
// GetLexer: Uses buffer meta data or content to pick a lexer for use in applying
// highlights.
func GetLexer(buf *core.Buffer) chroma.Lexer {
var lexer chroma.Lexer
if buf.Filetype != "" {
lexer = lexers.Get(strings.TrimPrefix(buf.Filetype, "."))
}
if lexer == nil && buf.Filename != "" {
lexer = lexers.Match(buf.Filename)
}
if lexer == nil && len(buf.Lines) > 0 {
// Get first few lines for content analysis
var content strings.Builder
for i := 0; i < min(len(buf.Lines), 10); i++ {
content.WriteString(buf.Lines[i].String() + "\n")
}
lexer = lexers.Analyse(content.String())
}
if lexer == nil {
lexer = lexers.Fallback
}
lexer = chroma.Coalesce(lexer) // Merge tokens together
return lexer
}