feat: Add alphabaster colorscheme
Minimal highlighting based on Tonsky's Alabaster philosophy: only
comments, strings, constants, and definitions get color. Everything
else uses default foreground. Colors sourced from Ghostty config.
Assisted-by: Claude Opus 4.5 via Claude Code
diff --git a/.config/nvim/colors/alphabaster.lua b/.config/nvim/colors/alphabaster.lua
new file mode 100644
index 00000000..c7e415c3
--- /dev/null
+++ b/.config/nvim/colors/alphabaster.lua
@@ -0,0 +1,6 @@
+-- colors/alphabaster.lua
+--
+-- Entry point for :colorscheme alphabaster
+-- Hotpot compiles the Fennel modules on demand.
+
+require("alphabaster").setup()
diff --git a/.config/nvim/fnl/alphabaster/README.md b/.config/nvim/fnl/alphabaster/README.md
new file mode 100644
index 00000000..17e72389
--- /dev/null
+++ b/.config/nvim/fnl/alphabaster/README.md
@@ -0,0 +1,78 @@
+# Alphabaster
+
+A minimal Neovim colorscheme based on [Tonsky's Alabaster philosophy](https://tonsky.me/blog/syntax-highlighting/).
+
+## Philosophy
+
+> If everything is highlighted, nothing stands out. Your eye adapts and considers it a new norm.
+
+Traditional colorschemes assign unique colors to keywords, operators, types, punctuation, variables, functions, and more. The result: visual noise that adds no comprehension value.
+
+Alphabaster highlights only **4 semantic categories**:
+
+| Category | Color | Rationale |
+|----------|-------|-----------|
+| Comments | Yellow | Important explanations, not noise. The tradition of dimming comments comes from when people were paid by line. |
+| Strings | Lavender | Reference points. Logic often starts from string literals. Highlighting also makes unclosed strings obvious. |
+| Constants | Lavender | Numbers, booleans, symbols. Also reference points that anchor understanding. |
+| Definitions | Cyan | Where functions/types/classes are *defined*, not called. Definitions are rare (~5% of code); calls are everywhere. |
+
+Everything else (keywords, operators, punctuation, variables, function calls) uses the default foreground. This is intentional.
+
+## Color Decisions
+
+### Why not red for comments?
+
+Initial design used red for comments (matching original Alabaster). In practice, red comments looked like errors. Yellow feels like sticky notes — visible but not alarming.
+
+### Why lavender for strings and constants?
+
+Lavender/purple for strings matches the paramount colorscheme, preserving muscle memory. Strings and constants are conceptually similar anyway: literal values that serve as reference points. Using the same color for both simplifies the mental model from 4 categories to 3 visual colors.
+
+### Colors sourced from Ghostty
+
+All colors come from `~/.config/ghostty/config` to maintain consistency between terminal and editor:
+
+```
+Background: #121218
+Foreground: #F0F0E4
+Yellow: #E6E6A1 (comments)
+Lavender: #C4AAEA (strings, constants)
+Cyan: #A2D6E0 (definitions)
+```
+
+## File Structure
+
+```
+alphabaster/
+├── init.fnl # Highlight definitions
+├── palette.fnl # Color values and semantic mappings
+├── lightline.fnl # Statusline theme
+└── README.md # This file
+```
+
+Entry point: `colors/alphabaster.lua` (required by `:colorscheme` command)
+
+## Usage
+
+```fennel
+(vim.cmd.colorscheme :alphabaster)
+```
+
+Or test without changing config:
+
+```vim
+:colorscheme alphabaster
+```
+
+## Treesitter and LSP
+
+The same 4-category rule applies to Treesitter (`@`-prefixed groups) and LSP semantic tokens (`@lsp.*`). Most groups link to `Normal`; only comments, strings, constants, and definitions get color.
+
+LSP semantic tokens have higher priority (125) than Treesitter (100). We clear most LSP highlights to let Treesitter rules dominate, except for `@lsp.mod.definition` where LSP is more accurate at identifying true definition sites.
+
+## References
+
+- [Tonsky: "I am sorry, but everyone is getting syntax highlighting wrong"](https://tonsky.me/blog/syntax-highlighting/)
+- [alabaster.nvim](https://github.com/p00f/alabaster.nvim)
+- [vscode-theme-alabaster](https://github.com/tonsky/vscode-theme-alabaster)
diff --git a/.config/nvim/fnl/alphabaster/init.fnl b/.config/nvim/fnl/alphabaster/init.fnl
new file mode 100644
index 00000000..891298e6
--- /dev/null
+++ b/.config/nvim/fnl/alphabaster/init.fnl
@@ -0,0 +1,294 @@
+;; fnl/alphabaster/init.fnl
+;;
+;; Alphabaster: A minimal colorscheme based on Tonsky's Alabaster philosophy.
+;; https://tonsky.me/blog/syntax-highlighting/
+;;
+;; Core insight: If everything is highlighted, nothing stands out.
+;; Your eye adapts and considers it a new norm.
+;;
+;; Only 4 semantic categories get color:
+;; 1. Comments — important explanations, not "visual noise"
+;; 2. Strings — reference points, easy to scan for
+;; 3. Constants — numbers, booleans, symbols (logic starts from constants)
+;; 4. Definitions — where functions/types/classes are defined (not called)
+;;
+;; Everything else (keywords, punctuation, variables, function calls)
+;; uses the default foreground. This is intentional.
+
+(local {: colors : semantic} (require :alphabaster.palette))
+(local api vim.api)
+
+(fn hi [group opts]
+ (api.nvim_set_hl 0 group opts))
+
+(fn link [group target]
+ (api.nvim_set_hl 0 group {:link target}))
+
+(fn setup []
+ (vim.cmd "highlight clear")
+ (set vim.g.colors_name :alphabaster)
+
+ ;;; Base
+ (hi :Normal {:fg colors.fg :bg colors.bg})
+ (hi :NormalFloat {:fg colors.fg :bg colors.black})
+ (hi :FloatBorder {:fg colors.bright-black})
+
+ ;;; === THE 4 SEMANTIC CATEGORIES ===
+
+ ;; Comments are important. The tradition of dimming comments comes from
+ ;; when people were paid by line. If code was complex enough to deserve
+ ;; an explanation, that explanation should be read first.
+ (hi :Comment {:fg semantic.comment})
+
+ ;; Strings are reference points. Logic often starts from string literals.
+ ;; Highlighting them also makes unclosed strings immediately obvious.
+ (hi :String {:fg semantic.string})
+
+ ;; Constants (numbers, booleans, symbols) are also reference points.
+ ;; These anchor your understanding of what code does.
+ (hi :Constant {:fg semantic.constant})
+
+ ;; Definitions mark where things are created, not where they're used.
+ ;; Function calls are everywhere (~75% of code); definitions are rare
+ ;; and worth highlighting.
+ (hi :Function {:fg semantic.definition})
+
+ ;;; === DELIBERATELY UNHIGHLIGHTED ===
+ ;;
+ ;; Keywords, operators, types, etc. are not highlighted.
+ ;; They're structural scaffolding — you learn to parse them unconsciously.
+ ;; Highlighting them adds noise without aiding comprehension.
+
+ (link :Identifier :Normal)
+ (link :Statement :Normal)
+ (link :Keyword :Normal)
+ (link :Operator :Normal)
+ (link :Type :Normal)
+ (link :PreProc :Normal)
+ (link :Special :Normal)
+ (link :Delimiter :Normal)
+
+ ;;; Constants family (all inherit the constant color)
+ (link :Number :Constant)
+ (link :Boolean :Constant)
+ (link :Float :Constant)
+ (link :Character :Constant)
+
+ ;;; UI elements
+ (hi :Visual {:bg colors.selection-bg :fg colors.selection-fg})
+ (hi :CursorLine {:bg colors.black})
+ (hi :CursorLineNr {:fg semantic.definition :bg colors.black})
+ (hi :LineNr {:fg colors.bright-black})
+ (hi :SignColumn {:bg colors.bg})
+ (hi :VertSplit {:fg colors.bright-black})
+ (hi :WinSeparator {:fg colors.bright-black})
+ (hi :StatusLine {:bg colors.black})
+ (hi :StatusLineNC {:bg colors.black :fg colors.bright-black})
+ (hi :Pmenu {:fg colors.fg :bg colors.black})
+ (hi :PmenuSel {:fg colors.fg :bg colors.bright-black})
+ (hi :PmenuSbar {:bg colors.black})
+ (hi :PmenuThumb {:bg colors.bright-black})
+ (hi :TabLine {:fg colors.fg :bg colors.black})
+ (hi :TabLineSel {:fg semantic.definition :bg colors.bright-black :bold true})
+ (hi :TabLineFill {:bg colors.black})
+
+ ;;; Search — high contrast, needs to pop
+ (hi :Search {:fg colors.bg :bg colors.yellow})
+ (hi :IncSearch {:fg colors.bg :bg colors.bright-yellow})
+ (hi :CurSearch {:fg colors.bg :bg colors.bright-yellow})
+
+ ;;; Diagnostics — these are actionable, worth color
+ (hi :DiagnosticError {:fg semantic.error})
+ (hi :DiagnosticWarn {:fg semantic.warn})
+ (hi :DiagnosticInfo {:fg semantic.info})
+ (hi :DiagnosticHint {:fg semantic.hint})
+ (hi :DiagnosticUnderlineError {:undercurl true :sp semantic.error})
+ (hi :DiagnosticUnderlineWarn {:undercurl true :sp semantic.warn})
+ (hi :DiagnosticUnderlineInfo {:undercurl true :sp semantic.info})
+ (hi :DiagnosticUnderlineHint {:undercurl true :sp semantic.hint})
+
+ ;;; Diff
+ (hi :DiffAdd {:fg semantic.added})
+ (hi :DiffDelete {:fg semantic.removed})
+ (hi :DiffChange {:fg semantic.changed})
+ (hi :DiffText {:fg semantic.changed :bold true})
+
+ ;;; === TREESITTER ===
+ ;;
+ ;; Treesitter captures use @prefix. We apply the same 4-category rule:
+ ;; comments, strings, constants, and definitions get color.
+ ;; Everything else inherits Normal.
+
+ ;; Comments
+ (link "@comment" :Comment)
+ (link "@comment.documentation" :Comment)
+
+ ;; Strings
+ (link "@string" :String)
+ (link "@string.documentation" :String)
+ (link "@string.escape" :Constant) ;; escape sequences are constants
+ (link "@string.regexp" :String)
+ (link "@character" :String)
+
+ ;; Constants
+ (link "@constant" :Constant)
+ (link "@constant.builtin" :Constant) ;; true, false, nil
+ (link "@constant.macro" :Constant)
+ (link "@number" :Constant)
+ (link "@number.float" :Constant)
+ (link "@boolean" :Constant)
+
+ ;; Definitions — only where things are *defined*, not used
+ (link "@function" :Normal) ;; function calls → no highlight
+ (link "@function.call" :Normal)
+ (link "@function.builtin" :Normal)
+ (link "@function.method" :Normal)
+ (link "@function.method.call" :Normal)
+ (hi "@function.definition" {:fg semantic.definition})
+ (hi "@type.definition" {:fg semantic.definition})
+ (hi "@variable.definition" {:fg semantic.definition})
+ (hi "@parameter" {:fg semantic.definition}) ;; parameter names in signatures
+
+ ;; Deliberately unhighlighted (structural scaffolding)
+ (link "@keyword" :Normal)
+ (link "@keyword.function" :Normal)
+ (link "@keyword.return" :Normal)
+ (link "@keyword.operator" :Normal)
+ (link "@keyword.conditional" :Normal)
+ (link "@keyword.repeat" :Normal)
+ (link "@keyword.import" :Normal)
+ (link "@keyword.exception" :Normal)
+ (link "@operator" :Normal)
+ (link "@punctuation" :Normal)
+ (link "@punctuation.bracket" :Normal)
+ (link "@punctuation.delimiter" :Normal)
+ (link "@punctuation.special" :Normal)
+ (link "@variable" :Normal)
+ (link "@variable.builtin" :Normal)
+ (link "@variable.parameter" :Normal)
+ (link "@variable.member" :Normal)
+ (link "@type" :Normal)
+ (link "@type.builtin" :Normal)
+ (link "@attribute" :Normal)
+ (link "@property" :Normal)
+ (link "@field" :Normal)
+ (link "@module" :Normal)
+ (link "@label" :Normal)
+ (link "@tag" :Normal)
+ (link "@tag.attribute" :Normal)
+ (link "@tag.delimiter" :Normal)
+
+ ;; Markup (for markdown, etc.)
+ (link "@markup.heading" :Normal)
+ (link "@markup.strong" :Normal)
+ (link "@markup.italic" :Normal)
+ (link "@markup.link" :Constant)
+ (link "@markup.link.url" :String)
+ (link "@markup.raw" :String) ;; code blocks
+
+ ;;; === LSP SEMANTIC TOKENS ===
+ ;;
+ ;; LSP provides deeper semantic understanding than Treesitter.
+ ;; Priority: semantic tokens (125) > treesitter (100).
+ ;;
+ ;; We mostly clear these to let our Treesitter rules dominate,
+ ;; except for definition-related tokens where LSP is more accurate.
+
+ ;; Clear type highlights — we don't want LSP overriding our choices
+ (link "@lsp.type.class" :Normal)
+ (link "@lsp.type.decorator" :Normal)
+ (link "@lsp.type.enum" :Normal)
+ (link "@lsp.type.enumMember" :Constant)
+ (link "@lsp.type.function" :Normal)
+ (link "@lsp.type.interface" :Normal)
+ (link "@lsp.type.keyword" :Normal)
+ (link "@lsp.type.macro" :Normal)
+ (link "@lsp.type.method" :Normal)
+ (link "@lsp.type.namespace" :Normal)
+ (link "@lsp.type.operator" :Normal)
+ (link "@lsp.type.parameter" :Normal)
+ (link "@lsp.type.property" :Normal)
+ (link "@lsp.type.struct" :Normal)
+ (link "@lsp.type.type" :Normal)
+ (link "@lsp.type.typeParameter" :Normal)
+ (link "@lsp.type.variable" :Normal)
+
+ ;; Strings and comments — let LSP confirm these
+ (link "@lsp.type.string" :String)
+ (link "@lsp.type.comment" :Comment)
+
+ ;; Definitions — LSP knows exactly where things are defined
+ ;; The "definition" modifier marks declaration sites
+ (hi "@lsp.mod.definition" {:fg semantic.definition})
+ (hi "@lsp.mod.declaration" {:fg semantic.definition})
+
+ ;; Readonly/constant modifiers — these are constants
+ (hi "@lsp.mod.readonly" {:fg semantic.constant})
+ (hi "@lsp.typemod.variable.readonly" {:fg semantic.constant})
+ (hi "@lsp.typemod.property.readonly" {:fg semantic.constant})
+
+ ;; Documentation comments — still comments, still important
+ (link "@lsp.type.comment.documentation" :Comment)
+
+ ;;; === PLUGIN SUPPORT ===
+
+ ;; indent-blankline
+ ;; Indent guides should be subtle — they're structural, not semantic
+ (hi :IblIndent {:fg colors.black})
+ (hi :IblScope {:fg colors.bright-black}) ;; current scope slightly brighter
+
+ ;; treesitter-context
+ ;; The sticky context header at top of window
+ (hi :TreesitterContext {:bg colors.black})
+ (hi :TreesitterContextLineNumber {:fg semantic.definition :bg colors.black})
+
+ ;; fidget (LSP progress)
+ (hi :FidgetTitle {:fg semantic.definition})
+ (hi :FidgetTask {:fg colors.bright-black})
+
+ ;; fzf.vim
+ ;; Matched characters should stand out
+ (hi :fzf1 {:fg colors.fg :bg colors.black})
+ (hi :fzf2 {:fg colors.fg :bg colors.black})
+ (hi :fzf3 {:fg colors.fg :bg colors.black})
+
+ ;; dirvish (file browser)
+ (link :DirvishPathTail :Normal)
+ (hi :DirvishArg {:fg semantic.definition})
+
+ ;; git diff (for fugitive, dirvish-git, etc.)
+ (hi :diffAdded {:fg semantic.added})
+ (hi :diffRemoved {:fg semantic.removed})
+ (hi :diffChanged {:fg semantic.changed})
+ (hi :diffFile {:fg semantic.definition})
+ (hi :diffLine {:fg colors.bright-black})
+
+ ;; Spelling
+ (hi :SpellBad {:undercurl true :sp semantic.error})
+ (hi :SpellCap {:undercurl true :sp semantic.info})
+ (hi :SpellRare {:undercurl true :sp semantic.hint})
+ (hi :SpellLocal {:undercurl true :sp semantic.warn})
+
+ ;; Misc UI
+ (hi :MatchParen {:bg colors.bright-black})
+ (hi :Folded {:fg colors.bright-black})
+ (hi :FoldColumn {:fg colors.bright-black})
+ (hi :NonText {:fg colors.bright-black})
+ (hi :SpecialKey {:fg colors.bright-black})
+ (hi :Whitespace {:fg colors.black}) ;; listchars (tabs, trailing spaces)
+ (hi :Directory {:fg semantic.definition})
+ (hi :Title {:fg semantic.definition})
+ (hi :Question {:fg semantic.info})
+ (hi :MoreMsg {:fg semantic.info})
+ (hi :WarningMsg {:fg semantic.warn})
+ (hi :ErrorMsg {:fg semantic.error :bold true})
+ (hi :Todo {:fg semantic.comment :bold true})
+ (hi :Underlined {:fg colors.fg :underline true})
+ (hi :Ignore {:fg colors.bg})
+ (hi :Error {:fg colors.bright-white :bg semantic.error})
+ (hi :Conceal {:fg colors.bright-black})
+ (hi :CursorColumn {:bg colors.black})
+ (hi :ColorColumn {:bg colors.black})
+ (hi :QuickFixLine {:bg colors.black}))
+
+{: setup}
diff --git a/.config/nvim/fnl/alphabaster/lightline.fnl b/.config/nvim/fnl/alphabaster/lightline.fnl
new file mode 100644
index 00000000..2440dbc3
--- /dev/null
+++ b/.config/nvim/fnl/alphabaster/lightline.fnl
@@ -0,0 +1,34 @@
+;; fnl/alphabaster/lightline.fnl
+;;
+;; Lightline theme for alphabaster.
+;; Minimal: same philosophy as the colorscheme.
+
+(local {: colors : semantic} (require :alphabaster.palette))
+
+;; Lightline expects: [fg, bg, cterm_fg, cterm_bg]
+;; We only use GUI colors, so cterm values are placeholders.
+(fn ll [fg bg]
+ [fg bg 0 0])
+
+(local normal (ll colors.fg colors.black))
+(local inactive (ll colors.bright-black colors.black))
+(local insert (ll colors.bg semantic.string))
+(local visual (ll colors.bg colors.selection-bg))
+(local replace (ll colors.bg semantic.error))
+
+;; Middle section is transparent (matches your current config)
+(local middle [[:NONE :NONE :NONE :NONE]])
+
+(local palette
+ {:normal {:left [normal normal] :middle middle :right [normal normal]}
+ :insert {:left [insert normal] :middle middle :right [normal normal]}
+ :visual {:left [visual normal] :middle middle :right [normal normal]}
+ :replace {:left [replace normal] :middle middle :right [normal normal]}
+ :inactive {:left [inactive inactive] :middle middle :right [inactive inactive]}
+ :tabline {:left [normal] :tabsel [insert] :middle middle :right [normal]}})
+
+;; Register with lightline
+(fn setup []
+ (tset vim.g "lightline#colorscheme#alphabaster#palette" palette))
+
+{: setup : palette}
diff --git a/.config/nvim/fnl/alphabaster/palette.fnl b/.config/nvim/fnl/alphabaster/palette.fnl
new file mode 100644
index 00000000..0e1fed9a
--- /dev/null
+++ b/.config/nvim/fnl/alphabaster/palette.fnl
@@ -0,0 +1,54 @@
+;; fnl/alphabaster/palette.fnl
+;;
+;; Color definitions sourced from ~/.config/ghostty/config
+
+;; Ghostty terminal colors (named)
+(local colors
+ {:bg "#121218"
+ :fg "#F0F0E4"
+
+ ;; ANSI normal
+ :black "#282A36"
+ :red "#E97C7C"
+ :green "#7BC275"
+ :yellow "#E6E6A1"
+ :blue "#C4AAEA"
+ :magenta "#E2A6D4"
+ :cyan "#A2D6E0"
+ :white "#F0F0E4"
+
+ ;; ANSI bright
+ :bright-black "#44475A"
+ :bright-red "#F09292"
+ :bright-green "#96D988"
+ :bright-yellow "#EFE9B0"
+ :bright-blue "#D0BCF0"
+ :bright-magenta "#EDBBE0"
+ :bright-cyan "#B6E2EA"
+ :bright-white "#FFFFFF"
+
+ ;; Special
+ :cursor "#f2d5cf"
+ :selection-bg "#626880"
+ :selection-fg "#c6d0f5"})
+
+;; Semantic roles (Alabaster philosophy)
+;; Only 4 categories get color. Everything else is default fg.
+(local semantic
+ {:comment colors.yellow ;; warm, like sticky notes (not red — looks like errors)
+ :string colors.blue ;; lavender/purple
+ :constant colors.blue ;; same as strings — all literal values are lavender
+ :definition colors.cyan
+
+ ;; Diagnostics
+ :error colors.red
+ :warn colors.yellow
+ :info colors.cyan
+ :hint colors.magenta
+
+ ;; Diff
+ :added colors.green
+ :removed colors.red
+ :changed colors.blue})
+
+{: colors : semantic}
diff --git a/.config/nvim/fnl/init.fnl b/.config/nvim/fnl/init.fnl
index 21570292..12f4bf4d 100644
--- a/.config/nvim/fnl/init.fnl
+++ b/.config/nvim/fnl/init.fnl
@@ -1,8 +1,12 @@
(local {:nvim_create_autocmd create-autocmd
:nvim_create_augroup create-augroup} vim.api)
+;;; colorscheme (toggle between these to compare)
+;; Option 1: paramount
(vim.pack.add ["https://git.kejadlen.dev/alpha/vim-colors-paramount.git"])
-(vim.cmd.colorscheme :paramount)
+; (vim.cmd.colorscheme :paramount)
+;; Option 2: alphabaster
+(vim.cmd.colorscheme :alphabaster)
(set vim.o.cmdheight 0)
@@ -164,6 +168,7 @@
;;; lightline
;; https://github.com/itchyny/lightline.vim/issues/168#issuecomment-232183744
+;; Option 1: powerline theme (for paramount)
(let [colorscheme :powerline
component {:filename "%{expand(\"%:~:.\")}"} ; relative path
palette-key (.. "lightline#colorscheme#" colorscheme "#palette")
@@ -172,6 +177,11 @@
(each [_ f (ipairs [:normal :inactive :tabline])]
(tset palette f :middle [[:NONE :NONE :NONE :NONE]]))
(tset vim.g palette-key palette))
+;; Option 2: alphabaster theme (uncomment when using alphabaster colorscheme)
+; (let [alphabaster-ll (require :alphabaster.lightline)]
+; (alphabaster-ll.setup)
+; (set vim.g.lightline {:colorscheme :alphabaster
+; :component {:filename "%{expand(\"%:~:.\")}"}}))
;;; netrw