From 4c257d506560d01f4e4c5cf68bb5f382ae4f81ea Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sat, 8 Nov 2025 13:04:35 -0500 Subject: [PATCH] initial commit --- LICENSE | 21 ++++ README.md | 64 ++++++++++ colors/midnight.lua | 1 + lua/midnight/init.lua | 24 ++++ lua/midnight/palette.lua | 96 +++++++++++++++ lua/midnight/theme.lua | 248 +++++++++++++++++++++++++++++++++++++++ selene.toml | 1 + stylua.toml | 8 ++ vim.toml | 30 +++++ 9 files changed, 493 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 colors/midnight.lua create mode 100644 lua/midnight/init.lua create mode 100644 lua/midnight/palette.lua create mode 100644 lua/midnight/theme.lua create mode 100644 selene.toml create mode 100644 stylua.toml create mode 100644 vim.toml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5ae4fde --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Raphael + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..348948f --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# midnight.nvim + +A minimal, scientifically-optimized colorscheme with automatic dark/light variants. + +## Features + +- **Single theme, dual modes**: Automatically detects `vim.o.background` and switches between midnight (dark) and daylight (light) palettes +- **Ultra-minimal syntax**: Only 3 semantic colors - blue for keywords, green for literals, grey for comments, white for everything else +- **Scientific optimization**: Desaturated colors in the 200-50 range, WCAG AAA contrast ratios +- **Eye strain reduction**: No pure black (#121212) or pure white (#e0e0e0) +- **Full terminal support**: Complete 16 ANSI color support +- **Plugin compatibility**: Built-in support for fzf-lua, nvim-cmp, gitsigns, and more + +## Installation + +### lazy.nvim + +```lua +{ + 'barrett-ruth/midnight.nvim', + lazy = false, + priority = 1000, + config = function() + require('midnight').setup() + vim.cmd('colorscheme midnight') + end, +} +``` + +## Usage + +```vim +colorscheme midnight +``` + +The theme automatically adapts to your background setting: + +```lua +vim.o.background = 'dark' -- uses midnight palette +vim.o.background = 'light' -- uses daylight palette +``` + +## Design Philosophy + +- **Minimal colors**: Only color what's semantically necessary +- **High contrast**: Meet WCAG AAA standards for readability +- **Consistent with terminal**: Matches Ghostty, tmux, and FZF themes +- **No AI comments**: Clean, human-written code + +## Color Palette + +### Midnight (Dark) +- Background: `#121212` (optimal dark gray) +- Foreground: `#e0e0e0` (87% white) +- Blue: `#7aa2f7` (keywords/control) +- Green: `#98c379` (literals/data) +- Grey: `#666666` (comments) + +### Daylight (Light) +- Optimized light variant with similar principles + +## License + +MIT diff --git a/colors/midnight.lua b/colors/midnight.lua new file mode 100644 index 0000000..605553d --- /dev/null +++ b/colors/midnight.lua @@ -0,0 +1 @@ +require('midnight').load() diff --git a/lua/midnight/init.lua b/lua/midnight/init.lua new file mode 100644 index 0000000..06b43aa --- /dev/null +++ b/lua/midnight/init.lua @@ -0,0 +1,24 @@ +local M = {} + +---@param opts? table +function M.setup(opts) + opts = opts or {} +end + +function M.load() + if vim.g.colors_name then + vim.cmd('hi clear') + end + + if vim.fn.exists('syntax_on') then + vim.cmd('syntax reset') + end + + vim.o.termguicolors = true + vim.g.colors_name = 'midnight' + + local theme = require('midnight.theme') + theme.apply() +end + +return M diff --git a/lua/midnight/palette.lua b/lua/midnight/palette.lua new file mode 100644 index 0000000..b9213f5 --- /dev/null +++ b/lua/midnight/palette.lua @@ -0,0 +1,96 @@ +local M = {} + +---@class Palette +---@field black string +---@field red string +---@field green string +---@field yellow string +---@field blue string +---@field magenta string +---@field cyan string +---@field orange? string +---@field white string +---@field bright_black string +---@field bright_red string +---@field bright_green string +---@field bright_yellow string +---@field bright_blue string +---@field bright_magenta string +---@field bright_cyan string +---@field bright_white string +---@field light_black string +---@field grey string +---@field med_grey? string +---@field dark_grey? string +---@field light_grey? string +---@field light_red? string +---@field light_green? string +---@field light_yellow? string +---@field light_blue? string +---@field light_magenta? string +---@field light_cyan? string +---@field light_white? string +---@field medium_emphasis? string + +---@type Palette +M.midnight = { + black = '#121212', + red = '#f48771', + green = '#98c379', + yellow = '#e5c07b', + blue = '#7aa2f7', + magenta = '#c678dd', + cyan = '#56b6c2', + orange = '#e5a56b', + white = '#e0e0e0', + + bright_black = '#666666', + bright_red = '#ff6b6b', + bright_green = '#b5e890', + bright_yellow = '#f0d197', + bright_blue = '#9db8f7', + bright_magenta = '#e298ff', + bright_cyan = '#7dd6e0', + bright_white = '#ffffff', + + light_black = '#666666', + grey = '#3d3d3d', + med_grey = '#2d2d2d', + dark_grey = '#1e1e1e', + + medium_emphasis = '#999999', +} + +---@type Palette +M.daylight = { + black = '#000000', + grey = '#666666', + red = '#B22222', + green = '#228B22', + yellow = '#B8860B', + blue = '#27408B', + magenta = '#8B008B', + cyan = '#00BFFF', + white = '#ffffff', + + light_black = '#555555', + light_grey = '#ECECEC', + light_red = '#ff0000', + light_green = '#00ff00', + light_yellow = '#ffa500', + light_blue = '#0000ff', + light_magenta = '#ff00ff', + light_cyan = '#00ffff', + light_white = '#ffffff', +} + +---@return Palette +function M.get_palette() + if vim.o.background == 'light' then + return M.daylight + else + return M.midnight + end +end + +return M diff --git a/lua/midnight/theme.lua b/lua/midnight/theme.lua new file mode 100644 index 0000000..4d24774 --- /dev/null +++ b/lua/midnight/theme.lua @@ -0,0 +1,248 @@ +local M = {} + +---@class Highlight +---@field fg? string +---@field bg? string +---@field bold? boolean +---@field italic? boolean +---@field underline? boolean +---@field undercurl? boolean +---@field reverse? boolean +---@field special? string +---@field none? boolean + +---@param from string +---@param tos string|string[] +local function link(from, tos) + tos = type(tos) == 'string' and { tos } or tos + for _, to in ipairs(tos) do + vim.api.nvim_set_hl(0, to, { link = from }) + end +end + +---@param group string +---@param highlights Highlight +---@param links? string[] +local function hi(group, highlights, links) + if highlights.none then + highlights.none = nil + highlights.undercurl = false + highlights.italic = false + highlights.bold = false + highlights.fg = 'NONE' + highlights.bg = 'NONE' + end + vim.api.nvim_set_hl(0, group, highlights) + for _, other in ipairs(links or {}) do + link(group, other) + end +end + +---@param group string +---@param highlights Highlight +---@param links? string[] +local function tshi(group, highlights, links) + hi(group, highlights) + + local tsgroup = '@' .. group:gsub('^%L', string.lower) + link(group, tsgroup) + + for _, to in ipairs(links or {}) do + link(group, to) + end +end + +---@return nil +function M.apply() + local palette = require('midnight.palette') + local cs = palette.get_palette() + + for color, hexcode in pairs(cs) do + hi(color, { fg = hexcode }) + end + + hi( + 'Normal', + { fg = cs.white, bg = cs.black }, + { 'Identifier', 'Special', 'StatusLine', 'StatusLineNC', 'Winbar', 'WinbarNC' } + ) + hi('NonText', { fg = cs.grey }, { 'SpecialKey' }) + + hi('LineNr', { fg = cs.light_black }, { 'SignColumn' }) + hi('CursorLine', { bg = cs.dark_grey }, { 'ColorColumn', 'Folded', 'Visual' }) + hi('CursorLineNr', { fg = cs.medium_emphasis }) + + hi('Conceal', { fg = cs.light_black, bg = cs.bg }) + hi('Directory', { fg = cs.white }) + hi('Error', { fg = cs.red }) + hi('ErrorMsg', { bold = true, underline = true, fg = cs.red }) + hi('MoreMsg', { fg = cs.yellow }, { 'WarningMsg' }) + hi('MatchParen', { bg = cs.med_grey }) + hi('NormalFloat', { bg = cs.bg }, { + 'LspInfoBorder', + 'FloatBorder', + 'FloatShadow', + 'FloatShadowThrough', + }) + hi('Search', { reverse = true }, { 'IncSearch' }) + hi('Whitespace', { fg = cs.grey }) + + tshi('Boolean', { fg = cs.green }, { '@constant.builtin' }) + tshi('Comment', { fg = cs.light_black }) + tshi('Constant', { fg = cs.green }) + tshi('Define', { fg = cs.blue }) + tshi('Function', { fg = cs.white }, { '@function.builtin', '@function.macro' }) + tshi('Include', { fg = cs.blue }) + tshi('Keyword', { fg = cs.blue }, { 'Statement' }) + tshi('Namespace', { fg = cs.white }) + tshi('Number', { fg = cs.green }) + tshi( + 'Operator', + { fg = cs.white }, + { '@keyword.operator', '@conditional.ternary' } + ) + hi('Delimiter', { none = true }) + hi('@punctuation.delimiter', { fg = cs.white }) + tshi('PreProc', { fg = cs.blue }) + tshi('String', { fg = cs.green }, { '@character' }) + hi('@string.escape', { fg = cs.green }) + tshi('Title', { bold = true, fg = cs.white }) + + hi('@tag', { fg = cs.white }) + hi('@tag.attribute', { fg = cs.white }) + hi('@tag.delimiter', { fg = cs.white }) + + tshi('Type', { fg = cs.white }) + hi('@type.qualifier', { fg = cs.white }, { '@storageclass' }) + hi('@lsp.type.enum', { fg = cs.white }, { '@lsp.type.class' }) + hi('@lsp.type.comment', { none = true }, { '@lsp.type.macro' }) + + hi('@text.emphasis', { italic = true }) + hi('@text.strong', { bold = true }) + hi('@text.underline', { underline = true }) + hi('@text.uri', { fg = cs.white, underline = true }, { '@text.reference' }) + + hi('@text.danger', { fg = cs.red, bold = true, italic = true }) + hi('@text.note', { fg = cs.green, bold = true, italic = true }) + hi('@text.todo', { fg = cs.yellow, bold = true, italic = true }, { 'Todo' }) + hi('@text.warning', { fg = cs.yellow, bold = true, italic = true }) + + hi('@variable', { none = true }) + + hi('Pmenu', { bg = cs.med_grey }, { 'PmenuSbar' }) + hi('PmenuSel', { fg = cs.med_grey, bg = cs.medium_emphasis }) + hi('PmenuThumb', { bg = cs.medium_emphasis }) + + hi('LspInlayHint', { fg = cs.light_black }) + hi('LspSignatureActiveParameter', { underline = true, italic = true }) + hi( + 'DiagnosticError', + { fg = cs.red }, + { 'DiagnosticFloatingError', 'DiagnosticSignError' } + ) + hi( + 'DiagnosticWarn', + { fg = cs.yellow }, + { 'DiagnosticFloatingWarn', 'DiagnosticSignWarn' } + ) + hi( + 'DiagnosticHint', + { fg = cs.white }, + { 'DiagnosticFloatingHint', 'DiagnosticSignHint' } + ) + hi( + 'DiagnosticOk', + { fg = cs.green }, + { 'DiagnosticFloatingOk', 'DiagnosticSignOk' } + ) + hi( + 'DiagnosticInfo', + { fg = cs.white }, + { 'DiagnosticFloatingInfo', 'DiagnosticSignInfo' } + ) + hi('DiagnosticUnderlineError', { undercurl = true, special = cs.red }) + hi('DiagnosticUnderlineWarn', { undercurl = true, special = cs.yellow }) + hi('DiagnosticUnderlineHint', { undercurl = true, special = cs.white }) + hi('DiagnosticUnderlineOk', { undercurl = true, special = cs.green }) + hi('DiagnosticUnderlineInfo', { undercurl = true, special = cs.white }) + + hi('SpellBad', { underline = true, special = cs.red }) + hi('SpellRare', { underline = true, special = cs.white }) + hi('SpellCap', { underline = true, special = cs.white }) + hi('SpellLocal', { underline = true, special = cs.white }) + + hi('gitCommitSummary', { fg = cs.white }) + + hi('@attribute.diff', { fg = cs.white }) + hi('DiffAdd', { fg = cs.green }, { '@text.diff.add', 'diffAdded' }) + hi('DiffDelete', { fg = cs.red }, { '@text.diff.delete', 'diffRemoved' }) + hi('DiffChange', { fg = cs.yellow }) + + hi('@constructor.lua', { fg = cs.white }) + hi('@markup.heading.gitcommit', { none = true }) + + hi('GitSignsCurrentLineBlame', { italic = true, fg = cs.light_black }) + link('DiffAdd', 'GitSignsAdd') + link('DiffChange', 'GitSignsChange') + link('DiffDelete', 'GitSignsDelete') + + link('Search', 'HighlightUndo') + + hi('CmpItemAbbrMatch', { fg = cs.white, bold = true }) + + for hlgroup, color in pairs({ + Key = 'white', + Keyword = 'blue', + Folder = 'white', + File = 'white', + Boolean = 'green', + Class = 'white', + Constant = 'green', + Constructor = 'white', + Enum = 'white', + EnumMember = 'white', + Field = 'white', + Function = 'white', + Interface = 'white', + Method = 'white', + Namespace = 'white', + Null = 'white', + Number = 'green', + Operator = 'white', + Property = 'white', + String = 'green', + Struct = 'white', + Text = 'white', + TypeParameter = 'white', + Variable = 'white', + }) do + hi('CmpItemKind' .. hlgroup, { fg = cs[color] }) + end + + link('NormalFloat', 'NullLsInfoBorder') + + link('Directory', 'OilDir') + + link('NormalFloat', 'FzfLuaBorder') + hi('FzfLuaHeaderText', { fg = cs.white }, { 'FzfLuaBufFlagCur' }) + hi('FzfLuaBufFlagAlt', { fg = cs.white }) + + vim.g.terminal_color_0 = cs.black + vim.g.terminal_color_1 = cs.red + vim.g.terminal_color_2 = cs.green + vim.g.terminal_color_3 = cs.yellow + vim.g.terminal_color_4 = cs.blue + vim.g.terminal_color_5 = cs.magenta + vim.g.terminal_color_6 = cs.cyan + vim.g.terminal_color_7 = cs.white + vim.g.terminal_color_8 = cs.bright_black + vim.g.terminal_color_9 = cs.bright_red + vim.g.terminal_color_10 = cs.bright_green + vim.g.terminal_color_11 = cs.bright_yellow + vim.g.terminal_color_12 = cs.bright_blue + vim.g.terminal_color_13 = cs.bright_magenta + vim.g.terminal_color_14 = cs.bright_cyan + vim.g.terminal_color_15 = cs.bright_white +end + +return M diff --git a/selene.toml b/selene.toml new file mode 100644 index 0000000..96cf5ab --- /dev/null +++ b/selene.toml @@ -0,0 +1 @@ +std = 'vim' diff --git a/stylua.toml b/stylua.toml new file mode 100644 index 0000000..01ded03 --- /dev/null +++ b/stylua.toml @@ -0,0 +1,8 @@ +column_width = 100 +line_endings = "Unix" +indent_type = "Spaces" +indent_width = 2 +quote_style = "AutoPreferSingle" +call_parentheses = "Always" +[sort_requires] +enabled = true diff --git a/vim.toml b/vim.toml new file mode 100644 index 0000000..8bf26ea --- /dev/null +++ b/vim.toml @@ -0,0 +1,30 @@ +[selene] +base = "lua51" +name = "vim" + +[vim] +any = true + +[jit] +any = true + +[assert] +any = true + +[describe] +any = true + +[it] +any = true + +[before_each] +any = true + +[after_each] +any = true + +[spy] +any = true + +[stub] +any = true