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
change lzlkxounpwklyoxxnzuloptuzyqvlyml
commit c652acc12dd1882437767ec3644bd7cd8e2d6ccf
author Alpha Chen <alpha@kejadlen.dev>
date
parent plxvvtrv
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