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 }