diff --git a/flake.lock b/flake.lock index 8a9b682..ea7a838 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,26 @@ { "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "nixvim", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1778716662, + "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, "home-manager": { "inputs": { "nixpkgs": [ @@ -37,10 +58,34 @@ "type": "github" } }, + "nixvim": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": [ + "nixpkgs" + ], + "systems": "systems" + }, + "locked": { + "lastModified": 1782919967, + "narHash": "sha256-pRwjfB5HQJ3m8J8bOR43pPHtHI7VUJSqwLA3P06cOY0=", + "owner": "nix-community", + "repo": "nixvim", + "rev": "667c8471f4a0fb24d702d1a61af8609f1a5f1ba6", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "nixos-26.05", + "repo": "nixvim", + "type": "github" + } + }, "root": { "inputs": { "home-manager": "home-manager", "nixpkgs": "nixpkgs", + "nixvim": "nixvim", "rust-overlay": "rust-overlay" } }, @@ -63,6 +108,21 @@ "repo": "rust-overlay", "type": "github" } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 2c5b8ec..af6b7cc 100644 --- a/flake.nix +++ b/flake.nix @@ -17,9 +17,20 @@ url = "github:oxalica/rust-overlay"; inputs.nixpkgs.follows = "nixpkgs"; }; + + # nixvim: declarative Neovim as home-manager options — the whole editor + # (plugins, LSP, keymaps) lives in modules/nvim.nix, pinned by this flake's + # lock. Track the release branch matching nixpkgs (a mismatched branch fails + # with `vimPlugins. not found`). `follows` keeps one nixpkgs: both are + # on the same stable channel, so the version skew nixvim warns about (only an + # issue against unstable) does not arise here. + nixvim = { + url = "github:nix-community/nixvim/nixos-26.05"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; - outputs = inputs@{ self, nixpkgs, home-manager, rust-overlay, ... }: + outputs = inputs@{ self, nixpkgs, home-manager, rust-overlay, nixvim, ... }: let system = "x86_64-linux"; in { @@ -28,7 +39,11 @@ # imports = [ config.nixosModules.default ]; # home-manager.users..imports = [ config.homeModules.default ]; nixosModules.default = import ./modules/nixos.nix; - homeModules.default = import ./modules/home.nix; + # nixvim's home-manager module rides along so `modules/nvim.nix` can set + # `programs.nixvim.*`; consumers get the editor by importing this one module. + homeModules.default = { + imports = [ nixvim.homeModules.nixvim ./modules/home.nix ]; + }; # ---- The example / template host ----------------------------------- # Real and buildable; dogfoods the modules above. Clone, replace diff --git a/modules/home.nix b/modules/home.nix index d26874f..42a9765 100644 --- a/modules/home.nix +++ b/modules/home.nix @@ -122,7 +122,7 @@ let ''; in { - imports = [ ./gpg.nix ./media.nix ./texlive.nix ./librewolf.nix ./mc.nix ./mime.nix ./hyprland-workspaces.nix ]; + imports = [ ./gpg.nix ./media.nix ./texlive.nix ./librewolf.nix ./mc.nix ./mime.nix ./hyprland-workspaces.nix ./nvim.nix ]; # Put ~/.local/bin (the XDG user-bin dir) on PATH so hand-dropped scripts run # by name without wiring each through nix. diff --git a/modules/nvim.nix b/modules/nvim.nix new file mode 100644 index 0000000..24d920e --- /dev/null +++ b/modules/nvim.nix @@ -0,0 +1,272 @@ +# Neovim, configured declaratively via nixvim. The editor — plugins, LSP, +# completion, keymaps — is defined here and pinned by the flake lock; nothing is +# downloaded at runtime, unlike an imperative distro (NvChad/LazyVim). The nixvim +# home-manager module that provides `programs.nixvim` is imported for consumers by +# `homeModules.default` in flake.nix, so this file only sets options. +# +# Consumer knobs (the option-with-default contract): `local.nvim.enable` to opt +# out wholesale, `local.nvim.colorscheme` to swap theme in one line, and +# `local.nvim.lspServers` to reshape the language set. Everything else is +# opinionated — but since nixvim exposes every plugin as an option, a consumer can +# still override any `programs.nixvim.plugins.*` directly from their own config. + +{ config, lib, pkgs, ... }: + +let + cfg = config.local.nvim; +in +{ + options.local.nvim = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to enable the nixvim-based Neovim setup."; + }; + + colorscheme = lib.mkOption { + type = lib.types.str; + default = "catppuccin"; + example = "tokyonight"; + description = '' + nixvim colorscheme module to enable (the attr under `colorschemes.*`, + e.g. "catppuccin", "tokyonight", "gruvbox"). Catppuccin is tuned below + (mocha flavour); for another scheme set its `colorschemes..settings` + from your own config if you want to tweak it. + ''; + }; + + lspServers = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + "nixd" # Nix — this repo's own language, so a sensible public baseline + "bashls" # Shell — likewise generic enough to default on + ]; + example = [ "nixd" "bashls" "rust_analyzer" "basedpyright" ]; + description = '' + lspconfig server names to enable under `plugins.lsp.servers.`. + Each must exist in nixvim's server list and its binary in nixpkgs. The + public default is deliberately minimal (Nix + shell); a consumer sets its + own language set here — and any per-server tuning a pick needs (e.g. + rust_analyzer's `installRustc`) via `programs.nixvim.plugins.lsp.servers`. + Note: standalone Lua wants "lua_ls", but Lua embedded in Nix strings is + invisible to any LSP, so it is not needed just to edit this config. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + programs.nixvim = { + enable = true; + + # Point nixvim at our (followed) nixpkgs instead of the rev it pins. Both + # track nixos-26.05, so the version skew nixvim warns about does not arise, + # and this keeps a single nixpkgs in the closure. Setting it explicitly is + # also what silences that warning. + nixpkgs.source = pkgs.path; + + # System clipboard on Wayland: yanks go to / puts read from wl-clipboard, + # so copy/paste crosses the nvim boundary. Enabling the provider pulls the + # wl-clipboard binaries in as a dependency. + clipboard = { + register = "unnamedplus"; + providers.wl-copy.enable = true; + }; + + globals = { + mapleader = " "; + maplocalleader = " "; + }; + + opts = { + number = true; + relativenumber = true; # relative line numbers for quick j/k motions + signcolumn = "yes"; # always show the gutter so text doesn't jump + cursorline = true; + termguicolors = true; # 24-bit colour, required by catppuccin & co. + scrolloff = 8; # keep 8 lines of context around the cursor + + expandtab = true; # spaces, not tabs + shiftwidth = 2; + tabstop = 2; + smartindent = true; + + ignorecase = true; # case-insensitive search… + smartcase = true; # …unless the query has a capital + undofile = true; # persistent undo across sessions + splitright = true; + splitbelow = true; + + # What :mksession stores (persistence.nvim). Drops "blank" and "terminal" + # from nvim's default so empty/plugin windows (neo-tree, toggleterm) aren't + # serialised — they restore as broken empty splits otherwise. Adds + # "globals", which some plugins rely on. Pairs with the PersistenceSavePre + # neo-tree close below. + sessionoptions = "buffers,curdir,folds,globals,tabpages,winsize"; + }; + + # Theme. Swap via local.nvim.colorscheme; catppuccin gets the mocha flavour. + # mkMerge keeps the computed `${cfg.colorscheme}` key from colliding with the + # literal `catppuccin` one when they happen to be the same attr. + colorschemes = lib.mkMerge [ + { ${cfg.colorscheme}.enable = true; } + (lib.mkIf (cfg.colorscheme == "catppuccin") { + catppuccin.settings.flavour = "mocha"; + }) + ]; + + plugins = { + # Shared icon set for the tree, statusline, completion menu. + web-devicons.enable = true; + + # Syntax via tree-sitter. The default grammar set is broad, so most + # languages get highlighting without an LSP or any per-language wiring. + treesitter = { + enable = true; + settings = { + highlight.enable = true; + indent.enable = true; + }; + }; + + # LSP. Servers come from local.nvim.lspServers; blink-cmp feeds its + # completion capabilities in automatically (setupLspCapabilities). + lsp = { + enable = true; + servers = lib.genAttrs cfg.lspServers (_: { enable = true; }); + keymaps = { + silent = true; + lspBuf = { + gd = "definition"; + gD = "declaration"; + K = "hover"; + "rn" = "rename"; + "ca" = "code_action"; + }; + # gr/gi (references, implementations) are wired to Telescope pickers in + # the global keymaps below instead — fuzzy + preview browsing beats the + # quickfix list that vim.lsp.buf.references would populate. gd stays a + # direct jump here (usually one target; Ctrl-o returns). + diagnostic = { + # Idiomatic vim bracket pair for stepping a list, kept as the public + # default. A consumer whose layout makes brackets awkward (they need + # AltGr on QWERTZ) can add comfortable aliases from their own config: + # this option is attrsOf, so extra keys merge in without conflict. + "[d" = "goto_prev"; + "]d" = "goto_next"; + "ds" = "open_float"; # show the diagnostic under the cursor + }; + }; + }; + + # Completion. blink.cmp — faster and lighter to configure than nvim-cmp. + blink-cmp = { + enable = true; + settings = { + keymap.preset = "default"; # C-space open, C-y confirm, C-n/C-p move + appearance.nerd_font_variant = "mono"; + sources.default = [ "lsp" "path" "snippets" "buffer" ]; + completion.documentation.auto_show = true; + }; + }; + + # Fuzzy finder — files, live grep, open buffers (buffer switching lives + # here rather than a bufferline; see the S-h/S-l binds below too). + telescope.enable = true; + + # File tree. + neo-tree.enable = true; + + # Statusline, NvChad-ish blocks: powerline separators, one global bar, + # theme following the colorscheme. + lualine = { + enable = true; + settings.options = { + theme = "auto"; + globalstatus = true; + section_separators = { left = ""; right = ""; }; + component_separators = { left = ""; right = ""; }; + }; + }; + + # Inline colour swatches for #rrggbb / colour names. + colorizer.enable = true; + + # Git signs in the gutter. + gitsigns.enable = true; + + # Session persistence: auto-saves a session per project directory into XDG + # state, restorable later. S restores the current dir's session; a + # consumer wanting auto-restore on a bare `nvim` adds a VimEnter autocmd. + persistence.enable = true; + + # Floating terminal toggled with Ctrl-\ (shares nvim's cwd). Try it; if + # a tiled foot next to nvim serves you better, drop this block. + toggleterm = { + enable = true; + settings = { + open_mapping = { __raw = "[[]]"; }; + direction = "float"; + }; + }; + + # Popup of the pending keybinds after leader — a live, always-accurate + # cheatsheet. The group labels name each leader subtree so the popup reads + # by category; ? opens the full list on demand (keymaps below). + which-key = { + enable = true; + settings.spec = [ + { __unkeyed-1 = "f"; group = "Find (Telescope)"; } + { __unkeyed-1 = "d"; group = "Diagnostics"; } + { __unkeyed-1 = "b"; group = "Buffer"; } + { __unkeyed-1 = "c"; group = "Code"; } + { __unkeyed-1 = "r"; group = "Refactor"; } + { __unkeyed-1 = "s"; group = "Search"; } + ]; + }; + }; + + # Global (non-LSP) keymaps. Deliberately small — a starting point to grow. + keymaps = [ + { mode = "n"; key = "w"; action = "w"; options.desc = "Save"; } + { mode = "n"; key = "q"; action = "q"; options.desc = "Quit window"; } + { mode = "n"; key = "Q"; action = "qa"; options.desc = "Quit all (exit nvim)"; } + # Restore the saved session for the current directory (persistence.nvim). + { mode = "n"; key = "S"; action.__raw = "function() require('persistence').load() end"; options.desc = "Restore session"; } + { mode = "n"; key = "e"; action = "Neotree toggle"; options.desc = "File tree"; } + { mode = "n"; key = ""; action = "nohlsearch"; } + + # Telescope + { mode = "n"; key = "ff"; action = "Telescope find_files"; options.desc = "Find files"; } + { mode = "n"; key = "fg"; action = "Telescope live_grep"; options.desc = "Live grep"; } + { mode = "n"; key = "fb"; action = "Telescope buffers"; options.desc = "Buffers"; } + { mode = "n"; key = "fh"; action = "Telescope help_tags"; options.desc = "Help tags"; } + + # LSP navigation via Telescope pickers (fuzzy + preview) for the + # many-result lookups; gd/gD stay direct jumps (plugins.lsp.keymaps above). + { mode = "n"; key = "gr"; action = "Telescope lsp_references"; options.desc = "References"; } + { mode = "n"; key = "gi"; action = "Telescope lsp_implementations"; options.desc = "Implementations"; } + + # Buffer navigation (no bufferline: cycle with Shift-h/l, list via fb) + { mode = "n"; key = ""; action = "bnext"; options.desc = "Next buffer"; } + { mode = "n"; key = ""; action = "bprevious"; options.desc = "Prev buffer"; } + { mode = "n"; key = "bd"; action = "bdelete"; options.desc = "Close buffer"; } + + # Cheatsheet: ? pops the full which-key list; sk fuzzy- + # searches every active mapping via Telescope (catches non-leader binds too). + { mode = "n"; key = "?"; action = "WhichKey"; options.desc = "All keybinds (which-key)"; } + { mode = "n"; key = "sk"; action = "Telescope keymaps"; options.desc = "Search keymaps"; } + ]; + + # persistence saves the window layout via :mksession, but neo-tree's special + # buffer doesn't serialise — a saved tree window restores as a blank split. + # Close neo-tree just before a session is written so the layout stays clean. + autoCmd = [ + { + event = "User"; + pattern = "PersistenceSavePre"; + callback.__raw = ''function() pcall(vim.cmd, "Neotree close") end''; + } + ]; + }; + }; +}