Add dev_env role: zsh/tmux/nvim for workstation-class hosts
A new role (separate from base) that gives workstation-class hosts (ubongo now, mamba later) a clean interactive environment: zsh + oh-my-zsh + oh-my-posh, tmux + TPM plugins, and neovim. Dotfiles are real files deployed via GNU stow (not templated); pinned nvim v0.12.2 + oh-my-posh 29.0.1. Configs re-derived (ADR-013) from AnsibleBaobabV4 + the operator's fisi setup on boma's terms: no Nerd Font (headless host), no system LSP suite (nvim uses mason), versions pinned (V4 tracks latest). Applied via playbooks/workstation.yml to the control group for users sjat + claude. Lint + Molecule (idempotent) green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b9daf2a0ad
commit
f3f382ae69
36 changed files with 1807 additions and 0 deletions
58
docs/superpowers/plans/2026-06-11-dev-env-role.md
Normal file
58
docs/superpowers/plans/2026-06-11-dev-env-role.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# `dev_env` Role — Implementation Plan (iteration 1)
|
||||
|
||||
> Built in the same 2026-06-11 session as the `ubongo` bring-up. A developer
|
||||
> interactive environment (zsh/tmux/nvim) for **workstation-class** hosts.
|
||||
|
||||
**Goal:** Give `ubongo` (and future `mamba`) a clean interactive shell/editor setup,
|
||||
reproducibly, as a boma-native Ansible role — so the operator (and the `claude` agent
|
||||
user) can work comfortably over SSH.
|
||||
|
||||
## Decisions
|
||||
|
||||
- **Separate role, never part of `base`.** `base` is the security/infra baseline for
|
||||
*every* host; a dev environment is only for human workstation-class hosts. Servers and
|
||||
service VMs must never get it.
|
||||
- **Stow, not templating.** Dotfiles are **real files** under `files/dotfiles/{zsh,tmux,nvim}/`
|
||||
(re-derived `$HOME`-relative from `fisi`'s live configs), symlinked into `~` with GNU
|
||||
stow. No Jinja-templated dotfiles (they rot; you'd edit templates not configs).
|
||||
- **Users:** `dev_env__users` (default `[]`). Set to `[sjat, claude]` for `ubongo` in
|
||||
`group_vars/control`.
|
||||
- **V4 (ADR-013):** configs/package-lists/install-mechanism *consulted* from V4 and
|
||||
**re-derived on boma's terms** — not its structure. V4 identifiers stripped from the
|
||||
dotfiles.
|
||||
|
||||
## Re-derivations vs V4
|
||||
|
||||
- **No Nerd Font** on `ubongo` — it's headless; fonts are a client-side concern.
|
||||
- **No system-wide LSP suite** — the operator's nvim uses **mason**, which self-installs
|
||||
LSPs/formatters inside nvim (needs only nvim + git + a C compiler + node).
|
||||
- **Pinned versions** (ADR-014): nvim `v0.12.2`, oh-my-posh `29.0.1` (V4 tracks "latest").
|
||||
- **Plugins self-bootstrap**: lazy.nvim installs nvim plugins on first launch; the role
|
||||
only lays down config + pre-clones omz/tmux plugins.
|
||||
|
||||
## Tasks (role: `roles/dev_env/`)
|
||||
|
||||
- `tasks/main.yml` — apt packages (`packages` tag) → include `neovim.yml`, `oh_my_posh.yml`
|
||||
→ loop `per_user.yml` over `dev_env__users`.
|
||||
- `tasks/neovim.yml` — install pinned nvim release to `/opt`, symlink, version sentinel.
|
||||
- `tasks/oh_my_posh.yml` — install pinned oh-my-posh binary + deploy `zen.toml` to `/etc`.
|
||||
- `tasks/per_user.yml` — set login shell to zsh (`users`); clone oh-my-zsh + custom
|
||||
plugins + tmux/TPM plugins; copy dotfiles to `~/.dotfiles`; `stow` into `~` (`config`).
|
||||
- `defaults/main.yml`, `meta/main.yml`, `README.md`, `requirements.yml`.
|
||||
- `molecule/default/{converge,verify}.yml` — create a `tester` user, apply, assert
|
||||
packages + nvim/omp/zen present + shell=zsh + dotfiles stowed (symlinks).
|
||||
- `playbooks/workstation.yml` — apply `dev_env` to the `control` group (ubongo).
|
||||
- `inventories/production/group_vars/control/vars.yml` — `dev_env__users: [sjat, claude]`.
|
||||
|
||||
## Verify / apply
|
||||
|
||||
- `make lint`; `make test ROLE=dev_env` (Molecule, Debian 13) must pass.
|
||||
- Apply to `ubongo`: `make check`/`deploy PLAYBOOK=workstation` from a host that can SSH
|
||||
to `ubongo` as `sjat` with `--ask-become-pass` (the Ansible-manages-ubongo connection
|
||||
isn't bootstrapped yet — handle at apply time).
|
||||
|
||||
## Deferred (iteration 2+)
|
||||
|
||||
- A proper `workstations` inventory group (when `mamba` joins) instead of reusing `control`.
|
||||
- lazygit, extra CLI tooling, any system LSP/formatters mason can't cover.
|
||||
- Pinning tmux plugins to commits (currently `master` except catppuccin `v1.0.3`).
|
||||
7
inventories/production/group_vars/control/vars.yml
Normal file
7
inventories/production/group_vars/control/vars.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
# Workstation-class control node (ubongo, ADR-015) — developer-environment users.
|
||||
# The operator and the dedicated AI-worker user both get the dev_env role (dotfiles,
|
||||
# zsh/tmux/nvim), so `sudo -iu claude` lands in the same clean shell.
|
||||
dev_env__users:
|
||||
- sjat
|
||||
- claude
|
||||
10
playbooks/workstation.yml
Normal file
10
playbooks/workstation.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
# workstation.yml — developer environment for workstation-class hosts.
|
||||
# Targets the `control` group (ubongo) for now; generalizes to a dedicated
|
||||
# `workstations` group when mamba joins. Run: make deploy PLAYBOOK=workstation
|
||||
- name: Apply developer environment to workstation-class hosts
|
||||
hosts: control
|
||||
become: true
|
||||
roles:
|
||||
- role: dev_env
|
||||
tags: [dev_env]
|
||||
50
roles/dev_env/README.md
Normal file
50
roles/dev_env/README.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# dev_env
|
||||
|
||||
Interactive developer environment for **workstation-class** boma hosts (`ubongo`, and
|
||||
later `mamba`). Gives the operator — and the `claude` agent user — a clean shell/editor
|
||||
setup over SSH: zsh + oh-my-zsh + oh-my-posh, tmux, and neovim.
|
||||
|
||||
**This is not part of `base`.** `base` is the security/infra baseline every host gets;
|
||||
`dev_env` is only for human workstation-class hosts and must never be applied to servers
|
||||
or service VMs.
|
||||
|
||||
## What it does
|
||||
|
||||
- Installs packages: `zsh, tmux, git, stow, build-essential, curl, ca-certificates,
|
||||
fzf, ripgrep, direnv, nodejs, npm` (`dev_env__packages`).
|
||||
- Installs **pinned** neovim (`dev_env__nvim_version`) and oh-my-posh
|
||||
(`dev_env__omp_version`) from GitHub releases, and the system-wide oh-my-posh theme
|
||||
`/etc/oh-my-posh/zen.toml`.
|
||||
- For each user in `dev_env__users`: sets the login shell to zsh, clones oh-my-zsh +
|
||||
custom plugins and the tmux/TPM plugins, and **stows** the dotfiles into `~`.
|
||||
|
||||
## Dotfiles
|
||||
|
||||
Real files under `files/dotfiles/{zsh,tmux,nvim}/`, deployed to `~/.dotfiles/` and
|
||||
symlinked into the home directory with GNU **stow** (not Jinja templates — so they stay
|
||||
editable as live configs). nvim plugins self-bootstrap via lazy.nvim on first launch;
|
||||
LSPs/formatters self-install via mason (no system LSP packages needed).
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `dev_env__users` | `[]` | Users to configure. Set per group, e.g. `group_vars/control → [sjat, claude]`. Empty = no per-user work. |
|
||||
| `dev_env__nvim_version` | `v0.12.2` | Pinned neovim release. |
|
||||
| `dev_env__omp_version` | `29.0.1` | Pinned oh-my-posh release. |
|
||||
| `dev_env__packages` | see defaults | APT packages. |
|
||||
| `dev_env__omz_custom_plugins` | autosuggestions, syntax-highlighting | Cloned into `~/.oh-my-zsh/custom/plugins`. |
|
||||
| `dev_env__tmux_plugins` | tpm, tmux-sensible, vim-tmux-navigator, catppuccin@v1.0.3 | Cloned into `~/.tmux/plugins`. |
|
||||
|
||||
## Apply
|
||||
|
||||
```bash
|
||||
make test ROLE=dev_env # Molecule (Debian 13)
|
||||
make deploy PLAYBOOK=workstation # applies to the control group (ubongo)
|
||||
```
|
||||
|
||||
## Provenance
|
||||
|
||||
Configs re-derived (ADR-013) from the heritage `AnsibleBaobabV4` repo and the operator's
|
||||
live `fisi` setup, on boma's terms — V4's structure was not imported. No Nerd Font is
|
||||
installed (headless host; fonts are a client-side concern).
|
||||
51
roles/dev_env/defaults/main.yml
Normal file
51
roles/dev_env/defaults/main.yml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
# dev_env — interactive developer environment (zsh/tmux/nvim) for workstation-class
|
||||
# hosts. Its own role, never part of `base`. See the role README and ADR-015.
|
||||
|
||||
# Users who receive the environment. Empty = no-op. Set in group_vars for the
|
||||
# workstation-class group (e.g. group_vars/control → [sjat, claude]).
|
||||
dev_env__users: []
|
||||
|
||||
# APT packages. nvim + oh-my-posh are installed separately from pinned releases.
|
||||
# (nvim uses mason internally for LSPs, so no system LSP packages are needed; node is
|
||||
# present so mason's node-based servers work. direnv is referenced by the .zshrc.)
|
||||
dev_env__packages:
|
||||
- zsh
|
||||
- tmux
|
||||
- git
|
||||
- stow
|
||||
- build-essential
|
||||
- curl
|
||||
- ca-certificates
|
||||
- fzf
|
||||
- ripgrep
|
||||
- direnv
|
||||
- nodejs
|
||||
- npm
|
||||
|
||||
# Pinned tool versions (ADR-014 — pin, don't track "latest").
|
||||
dev_env__nvim_version: "v0.12.2"
|
||||
dev_env__omp_version: "29.0.1"
|
||||
|
||||
# oh-my-zsh custom plugins (cloned per user into ~/.oh-my-zsh/custom/plugins).
|
||||
dev_env__omz_custom_plugins:
|
||||
- name: zsh-autosuggestions
|
||||
repo: "https://github.com/zsh-users/zsh-autosuggestions.git"
|
||||
- name: zsh-syntax-highlighting
|
||||
repo: "https://github.com/zsh-users/zsh-syntax-highlighting.git"
|
||||
|
||||
# tmux plugins (pre-cloned per user into ~/.tmux/plugins; TPM loads them on tmux start).
|
||||
# catppuccin is pinned to the v1 API the .tmux.conf targets.
|
||||
dev_env__tmux_plugins:
|
||||
- name: tpm
|
||||
repo: "https://github.com/tmux-plugins/tpm.git"
|
||||
version: master
|
||||
- name: tmux-sensible
|
||||
repo: "https://github.com/tmux-plugins/tmux-sensible.git"
|
||||
version: master
|
||||
- name: vim-tmux-navigator
|
||||
repo: "https://github.com/christoomey/vim-tmux-navigator.git"
|
||||
version: master
|
||||
- name: tmux
|
||||
repo: "https://github.com/catppuccin/tmux.git"
|
||||
version: v1.0.3
|
||||
26
roles/dev_env/files/dotfiles/nvim/.config/nvim/init.lua
Normal file
26
roles/dev_env/files/dotfiles/nvim/.config/nvim/init.lua
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
-- Neovim configuration — managed by boma dev_env role (stow)
|
||||
-- Bootstrap lazy.nvim plugin manager
|
||||
|
||||
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
|
||||
if not vim.loop.fs_stat(lazypath) then
|
||||
vim.fn.system({
|
||||
"git",
|
||||
"clone",
|
||||
"--filter=blob:none",
|
||||
"https://github.com/folke/lazy.nvim.git",
|
||||
"--branch=stable",
|
||||
lazypath,
|
||||
})
|
||||
end
|
||||
vim.opt.rtp:prepend(lazypath)
|
||||
|
||||
-- Set leader key before loading plugins
|
||||
vim.g.mapleader = " "
|
||||
vim.g.maplocalleader = " "
|
||||
|
||||
-- Load core configuration
|
||||
require("options")
|
||||
require("keymaps")
|
||||
|
||||
-- Load lazy.nvim and plugin configuration
|
||||
require("lazy-setup")
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"Comment.nvim": { "branch": "master", "commit": "e30b7f2008e52442154b66f7c519bfd2f1e32acb" },
|
||||
"LuaSnip": { "branch": "master", "commit": "0abc8f390b278c3b4aabc4c004ac8a088b65cf24" },
|
||||
"catppuccin": { "branch": "main", "commit": "8edd468af4d63212b84d69b2ddb5ffc9023ef5eb" },
|
||||
"cmp-buffer": { "branch": "main", "commit": "b74fab3656eea9de20a9b8116afa3cfc4ec09657" },
|
||||
"cmp-nvim-lsp": { "branch": "main", "commit": "cbc7b02bb99fae35cb42f514762b89b5126651ef" },
|
||||
"cmp-path": { "branch": "main", "commit": "c642487086dbd9a93160e1679a1327be111cbc25" },
|
||||
"cmp_luasnip": { "branch": "master", "commit": "98d9cb5c2c38532bd9bdb481067b20fea8f32e90" },
|
||||
"conform.nvim": { "branch": "master", "commit": "619363c30309d29ffa631e67c8183f2a72caa373" },
|
||||
"gitsigns.nvim": { "branch": "main", "commit": "dd3f588bacbeb041be6facf1742e42097f62165d" },
|
||||
"indent-blankline.nvim": { "branch": "master", "commit": "d28a3f70721c79e3c5f6693057ae929f3d9c0a03" },
|
||||
"lazy.nvim": { "branch": "main", "commit": "306a05526ada86a7b30af95c5cc81ffba93fef97" },
|
||||
"lualine.nvim": { "branch": "master", "commit": "131a558e13f9f28b15cd235557150ccb23f89286" },
|
||||
"neo-tree.nvim": { "branch": "v3.x", "commit": "ebd66767191714e008ce73b769518a763ff31bdc" },
|
||||
"nui.nvim": { "branch": "main", "commit": "de740991c12411b663994b2860f1a4fd0937c130" },
|
||||
"nvim-autopairs": { "branch": "master", "commit": "7b9923abad60b903ece7c52940e1321d39eccc79" },
|
||||
"nvim-cmp": { "branch": "main", "commit": "a1d504892f2bc56c2e79b65c6faded2fd21f3eca" },
|
||||
"nvim-lspconfig": { "branch": "master", "commit": "a4ed4e761c400849e8c9f8bda33e5083f890268c" },
|
||||
"nvim-surround": { "branch": "main", "commit": "2e93e154de9ff326def6480a4358bfc149d5da2c" },
|
||||
"nvim-tmux-navigation": { "branch": "main", "commit": "4898c98702954439233fdaf764c39636681e2861" },
|
||||
"nvim-treesitter": { "branch": "master", "commit": "cf12346a3414fa1b06af75c79faebe7f76df080a" },
|
||||
"nvim-web-devicons": { "branch": "master", "commit": "0d7d35fa946837b8738b17c18d1faa1ac351e7f9" },
|
||||
"plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" },
|
||||
"telescope.nvim": { "branch": "0.1.x", "commit": "a0bbec21143c7bc5f8bb02e0005fa0b982edc026" },
|
||||
"toggleterm.nvim": { "branch": "main", "commit": "50ea089fc548917cc3cc16b46a8211833b9e3c7c" },
|
||||
"which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" },
|
||||
"zen-mode.nvim": { "branch": "main", "commit": "8564ce6d29ec7554eb9df578efa882d33b3c23a7" }
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
-- Neovim keymaps configuration
|
||||
-- Ported from vimrc.j2 and enhanced
|
||||
|
||||
local keymap = vim.keymap.set
|
||||
local opts = { noremap = true, silent = true }
|
||||
|
||||
-- Set leader key
|
||||
vim.g.mapleader = " "
|
||||
vim.g.maplocalleader = " "
|
||||
|
||||
-- Quick save (Ctrl+s in normal and insert mode)
|
||||
keymap('n', '<C-s>', ':w<CR>', opts)
|
||||
keymap('i', '<C-s>', '<Esc>:w<CR>a', opts)
|
||||
|
||||
-- Leader shortcuts
|
||||
keymap('n', '<leader>w', ':w<CR>', opts) -- Save
|
||||
keymap('n', '<leader>q', ':q<CR>', opts) -- Quit
|
||||
keymap('n', '<leader>e', ':Ex<CR>', opts) -- File explorer (netrw)
|
||||
|
||||
-- Clear search highlighting
|
||||
keymap('n', '<Esc>', ':nohlsearch<CR>', opts)
|
||||
|
||||
-- Better indenting (keep selection in visual mode)
|
||||
keymap('v', '<', '<gv', opts)
|
||||
keymap('v', '>', '>gv', opts)
|
||||
|
||||
-- Move text up and down in visual mode
|
||||
keymap('v', 'J', ":m '>+1<CR>gv=gv", opts)
|
||||
keymap('v', 'K', ":m '<-2<CR>gv=gv", opts)
|
||||
|
||||
-- Keep cursor centered when scrolling
|
||||
keymap('n', '<C-d>', '<C-d>zz', opts)
|
||||
keymap('n', '<C-u>', '<C-u>zz', opts)
|
||||
keymap('n', 'n', 'nzzzv', opts)
|
||||
keymap('n', 'N', 'Nzzzv', opts)
|
||||
|
||||
-- Buffer cycling (inner layer — mirrors tmux window cycling)
|
||||
keymap('n', '<C-Tab>', ':bnext<CR>', opts)
|
||||
keymap('n', '<C-PageDown>', ':bnext<CR>', opts)
|
||||
keymap('n', '<C-PageUp>', ':bprevious<CR>', opts)
|
||||
|
||||
-- Better paste (don't yank replaced text in visual mode)
|
||||
keymap('x', '<leader>p', '"_dP', opts)
|
||||
|
||||
-- Explicit yank-to-system-clipboard. Default y/Y already mirror to + via
|
||||
-- the TextYankPost autocmd in options.lua, so these are redundant but kept
|
||||
-- as muscle-memory aliases. Paste counterparts removed — `p` already pastes
|
||||
-- correctly and `"+p` would route through OSC52 paste, which most terminals
|
||||
-- refuse (silent E353).
|
||||
keymap('n', '<leader>y', '"+y', { noremap = true, silent = true, desc = 'Yank to system clipboard' })
|
||||
keymap('v', '<leader>y', '"+y', { noremap = true, silent = true, desc = 'Yank to system clipboard' })
|
||||
keymap('n', '<leader>Y', '"+Y', { noremap = true, silent = true, desc = 'Yank line to system clipboard' })
|
||||
|
||||
-- Delete to black hole register
|
||||
keymap('n', '<leader>d', '"_d', opts)
|
||||
keymap('v', '<leader>d', '"_d', opts)
|
||||
|
||||
-- Window management — arrow direction = where new pane opens (mirrors kitty and tmux)
|
||||
keymap('n', '<leader><Right>', ':vsplit<CR>', opts)
|
||||
keymap('n', '<leader><Down>', ':split<CR>', opts)
|
||||
keymap('n', '<leader>sx', ':close<CR>', opts)
|
||||
keymap('n', '<leader>sv', ':vsplit<CR>', opts)
|
||||
keymap('n', '<leader>sh', ':split<CR>', opts)
|
||||
|
||||
-- Buffer navigation
|
||||
keymap('n', '<leader>bn', ':bnext<CR>', opts) -- Next buffer
|
||||
keymap('n', '<leader>bp', ':bprevious<CR>', opts) -- Previous buffer
|
||||
keymap('n', '<leader>bd', ':bdelete<CR>', opts) -- Delete buffer
|
||||
|
||||
-- Quick fix list navigation
|
||||
keymap('n', '<leader>j', ':cnext<CR>', opts)
|
||||
keymap('n', '<leader>k', ':cprev<CR>', opts)
|
||||
|
||||
-- Diagnostic keymaps (will be overridden by LSP if active)
|
||||
keymap('n', '<A-k>', vim.diagnostic.goto_prev, opts) -- Alt+k = up = prev diagnostic
|
||||
keymap('n', '<A-j>', vim.diagnostic.goto_next, opts) -- Alt+j = down = next diagnostic
|
||||
keymap('n', '<leader>dl', vim.diagnostic.setloclist, opts)
|
||||
keymap('n', '<leader>df', vim.diagnostic.open_float, opts)
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
-- Lazy.nvim setup
|
||||
-- Loads all plugins from lua/plugins/ directory
|
||||
|
||||
require("lazy").setup("plugins", {
|
||||
-- Use lockfile for deterministic installations
|
||||
lockfile = vim.fn.stdpath("config") .. "/lazy-lock.json",
|
||||
|
||||
-- Disable automatic checking for plugin updates (managed by Ansible)
|
||||
checker = {
|
||||
enabled = false, -- Updates only via Ansible
|
||||
notify = false,
|
||||
},
|
||||
|
||||
-- Disable change detection (we control updates via Ansible)
|
||||
change_detection = {
|
||||
enabled = false,
|
||||
notify = false,
|
||||
},
|
||||
|
||||
-- Performance optimizations
|
||||
performance = {
|
||||
cache = {
|
||||
enabled = true,
|
||||
},
|
||||
rtp = {
|
||||
-- Debian puts bundled treesitter parsers (vimdoc etc.) under /usr/lib/nvim
|
||||
-- which lazy's rtp reset would otherwise drop.
|
||||
paths = vim.fn.isdirectory("/usr/lib/nvim") == 1 and { "/usr/lib/nvim" } or {},
|
||||
-- Disable unused built-in plugins
|
||||
disabled_plugins = {
|
||||
"gzip",
|
||||
"matchit",
|
||||
"matchparen",
|
||||
"netrwPlugin",
|
||||
"tarPlugin",
|
||||
"tohtml",
|
||||
"tutor",
|
||||
"zipPlugin",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
-- UI configuration
|
||||
ui = {
|
||||
border = "rounded",
|
||||
icons = {
|
||||
cmd = "⌘",
|
||||
config = "🛠",
|
||||
event = "📅",
|
||||
ft = "📂",
|
||||
init = "⚙",
|
||||
keys = "🗝",
|
||||
plugin = "🔌",
|
||||
runtime = "💻",
|
||||
source = "📄",
|
||||
start = "🚀",
|
||||
task = "📌",
|
||||
lazy = "💤 ",
|
||||
},
|
||||
},
|
||||
})
|
||||
119
roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/options.lua
Normal file
119
roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/options.lua
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
-- Neovim options configuration
|
||||
-- Ported from vimrc.j2 and enhanced with Neovim best practices
|
||||
|
||||
local opt = vim.opt
|
||||
|
||||
-- Line numbers
|
||||
opt.number = true -- Show line numbers
|
||||
opt.relativenumber = true -- Show relative line numbers
|
||||
opt.cursorline = true -- Highlight the current line
|
||||
|
||||
-- Tabs and indentation
|
||||
opt.expandtab = true -- Use spaces instead of tabs
|
||||
opt.tabstop = 4 -- Number of spaces for a tab
|
||||
opt.shiftwidth = 4 -- Number of spaces for indentation
|
||||
opt.softtabstop = 4 -- Number of spaces for soft tab
|
||||
opt.autoindent = true -- Enable auto-indentation
|
||||
opt.smartindent = true -- Smart indentation
|
||||
|
||||
-- Search
|
||||
opt.hlsearch = true -- Highlight search results
|
||||
opt.incsearch = true -- Incremental search
|
||||
opt.ignorecase = true -- Case-insensitive search
|
||||
opt.smartcase = true -- Override ignorecase if search contains uppercase
|
||||
|
||||
-- UI
|
||||
opt.showmatch = true -- Show matching brackets
|
||||
opt.mouse = 'a' -- Enable mouse support
|
||||
opt.termguicolors = true -- Enable 24-bit RGB colors
|
||||
opt.laststatus = 2 -- Always show status line
|
||||
opt.ruler = true -- Show cursor position
|
||||
opt.wildmenu = true -- Enhanced command-line completion
|
||||
opt.linebreak = true -- Wrap lines at word boundaries
|
||||
|
||||
-- Clipboard — OSC52 tunnels clipboard through SSH/tmux to local kitty.
|
||||
-- We only wire the *copy* half: OSC52 paste requires the terminal to read
|
||||
-- back its own clipboard, which most terminals refuse for security. Pairing
|
||||
-- it with unnamedplus made `p` route through OSC52 paste and silently fail
|
||||
-- (E353). Instead, leave the unnamed register alone so `p` uses vim's
|
||||
-- internal buffer, and mirror every yank into `+` via TextYankPost so the
|
||||
-- system clipboard still gets updated.
|
||||
vim.g.clipboard = {
|
||||
name = 'OSC 52',
|
||||
copy = {
|
||||
['+'] = require('vim.ui.clipboard.osc52').copy('+'),
|
||||
['*'] = require('vim.ui.clipboard.osc52').copy('*'),
|
||||
},
|
||||
paste = {
|
||||
['+'] = require('vim.ui.clipboard.osc52').paste('+'),
|
||||
['*'] = require('vim.ui.clipboard.osc52').paste('*'),
|
||||
},
|
||||
}
|
||||
vim.api.nvim_create_autocmd('TextYankPost', {
|
||||
pattern = '*',
|
||||
callback = function()
|
||||
if vim.v.event.operator == 'y' then
|
||||
vim.fn.setreg('+', vim.v.event.regcontents, vim.v.event.regtype)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
-- Spell-check for markdown buffers (English + Danish). Nvim auto-downloads
|
||||
-- missing spellfiles on first use from neovim's spell repo.
|
||||
vim.api.nvim_create_autocmd('FileType', {
|
||||
pattern = 'markdown',
|
||||
callback = function()
|
||||
vim.opt_local.spell = true
|
||||
vim.opt_local.spelllang = { 'en', 'da' }
|
||||
end,
|
||||
})
|
||||
|
||||
-- Files and backups
|
||||
opt.backup = false -- Disable backup files
|
||||
opt.swapfile = false -- Disable swap files
|
||||
opt.undofile = true -- Enable persistent undo
|
||||
opt.undodir = vim.fn.expand('~/.local/state/nvim/undo') -- Neovim undo directory
|
||||
|
||||
-- Auto-reload buffers when files change on disk (e.g. external edits by Claude Code).
|
||||
-- Nvim only checks on certain events; the autocmd below pokes it more aggressively.
|
||||
opt.autoread = true
|
||||
vim.api.nvim_create_autocmd({ 'FocusGained', 'BufEnter', 'CursorHold', 'CursorHoldI' }, {
|
||||
pattern = '*',
|
||||
command = 'checktime',
|
||||
})
|
||||
vim.api.nvim_create_autocmd('FileChangedShellPost', {
|
||||
pattern = '*',
|
||||
callback = function()
|
||||
vim.notify('File changed on disk — buffer reloaded', vim.log.levels.WARN)
|
||||
end,
|
||||
})
|
||||
|
||||
-- Performance
|
||||
opt.updatetime = 300 -- Faster completion and updates
|
||||
|
||||
-- Encoding
|
||||
opt.encoding = 'utf-8' -- Set encoding to UTF-8
|
||||
opt.fileformat = 'unix' -- Use Unix file format
|
||||
|
||||
-- Folding
|
||||
opt.foldmethod = 'indent' -- Fold based on indentation
|
||||
opt.foldlevel = 99 -- Start with all folds open
|
||||
|
||||
-- Split behavior
|
||||
opt.splitbelow = true -- Horizontal splits below
|
||||
opt.splitright = true -- Vertical splits to the right
|
||||
|
||||
-- Sign column (for LSP diagnostics and git signs)
|
||||
opt.signcolumn = 'yes' -- Always show sign column
|
||||
|
||||
-- Completion menu
|
||||
opt.completeopt = { 'menu', 'menuone', 'noselect' } -- Better completion experience
|
||||
|
||||
-- Scroll offset
|
||||
opt.scrolloff = 8 -- Keep 8 lines visible above/below cursor
|
||||
opt.sidescrolloff = 8 -- Keep 8 columns visible left/right of cursor
|
||||
|
||||
-- Command line
|
||||
opt.cmdheight = 1 -- Command line height
|
||||
|
||||
-- Color scheme is set by catppuccin plugin in lua/plugins/ui.lua
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
-- Autocompletion (nvim-cmp)
|
||||
return {
|
||||
"hrsh7th/nvim-cmp",
|
||||
event = "InsertEnter",
|
||||
dependencies = {
|
||||
"hrsh7th/cmp-nvim-lsp", -- LSP source
|
||||
"hrsh7th/cmp-buffer", -- Buffer words source
|
||||
"hrsh7th/cmp-path", -- Filesystem path source
|
||||
"L3MON4D3/LuaSnip", -- Snippet engine
|
||||
"saadparwaiz1/cmp_luasnip", -- Snippet source
|
||||
},
|
||||
config = function()
|
||||
local cmp = require("cmp")
|
||||
local luasnip = require("luasnip")
|
||||
|
||||
cmp.setup({
|
||||
snippet = {
|
||||
expand = function(args)
|
||||
luasnip.lsp_expand(args.body)
|
||||
end,
|
||||
},
|
||||
mapping = cmp.mapping.preset.insert({
|
||||
["<C-j>"] = cmp.mapping.select_next_item(),
|
||||
["<C-k>"] = cmp.mapping.select_prev_item(),
|
||||
["<C-b>"] = cmp.mapping.scroll_docs(-4),
|
||||
["<C-f>"] = cmp.mapping.scroll_docs(4),
|
||||
["<C-e>"] = cmp.mapping.abort(),
|
||||
["<CR>"] = cmp.mapping.confirm({ select = false }),
|
||||
["<Tab>"] = cmp.mapping(function(fallback)
|
||||
if cmp.visible() then
|
||||
cmp.select_next_item()
|
||||
elseif luasnip.expand_or_jumpable() then
|
||||
luasnip.expand_or_jump()
|
||||
else
|
||||
fallback()
|
||||
end
|
||||
end, { "i", "s" }),
|
||||
["<S-Tab>"] = cmp.mapping(function(fallback)
|
||||
if cmp.visible() then
|
||||
cmp.select_prev_item()
|
||||
elseif luasnip.jumpable(-1) then
|
||||
luasnip.jump(-1)
|
||||
else
|
||||
fallback()
|
||||
end
|
||||
end, { "i", "s" }),
|
||||
}),
|
||||
sources = cmp.config.sources({
|
||||
{ name = "nvim_lsp", priority = 1000 },
|
||||
{ name = "luasnip", priority = 750 },
|
||||
{ name = "buffer", priority = 500 },
|
||||
{ name = "path", priority = 250 },
|
||||
}),
|
||||
formatting = {
|
||||
format = function(entry, item)
|
||||
local source_labels = {
|
||||
nvim_lsp = "[LSP]",
|
||||
luasnip = "[Snip]",
|
||||
buffer = "[Buf]",
|
||||
path = "[Path]",
|
||||
}
|
||||
item.menu = source_labels[entry.source.name] or ""
|
||||
return item
|
||||
end,
|
||||
},
|
||||
window = {
|
||||
completion = cmp.config.window.bordered(),
|
||||
documentation = cmp.config.window.bordered(),
|
||||
},
|
||||
})
|
||||
end,
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
-- File browser (neo-tree.nvim)
|
||||
return {
|
||||
"nvim-neo-tree/neo-tree.nvim",
|
||||
branch = "v3.x",
|
||||
dependencies = {
|
||||
"nvim-lua/plenary.nvim",
|
||||
"nvim-tree/nvim-web-devicons",
|
||||
"MunifTanjim/nui.nvim",
|
||||
},
|
||||
cmd = "Neotree",
|
||||
keys = {
|
||||
{ "<leader>ee", "<cmd>Neotree toggle<cr>", desc = "Toggle file browser" },
|
||||
{ "<leader>ef", "<cmd>Neotree focus<cr>", desc = "Focus file browser" },
|
||||
{ "<leader>eg", "<cmd>Neotree git_status<cr>", desc = "Git status tree" },
|
||||
},
|
||||
config = function()
|
||||
require("neo-tree").setup({
|
||||
close_if_last_window = true,
|
||||
window = {
|
||||
width = 30,
|
||||
mappings = {
|
||||
["<space>"] = "none", -- don't steal leader
|
||||
},
|
||||
},
|
||||
filesystem = {
|
||||
filtered_items = {
|
||||
hide_dotfiles = false,
|
||||
hide_gitignored = true,
|
||||
},
|
||||
follow_current_file = {
|
||||
enabled = true,
|
||||
},
|
||||
},
|
||||
git_status = {
|
||||
symbols = {
|
||||
added = "✚",
|
||||
modified = "",
|
||||
deleted = "✖",
|
||||
renamed = "",
|
||||
untracked = "",
|
||||
ignored = "",
|
||||
unstaged = "",
|
||||
staged = "",
|
||||
conflict = "",
|
||||
},
|
||||
},
|
||||
})
|
||||
end,
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
-- Formatting configuration (conform.nvim)
|
||||
return {
|
||||
"stevearc/conform.nvim",
|
||||
event = { "BufWritePre" },
|
||||
cmd = { "ConformInfo" },
|
||||
config = function()
|
||||
local conform = require("conform")
|
||||
|
||||
conform.setup({
|
||||
formatters_by_ft = {
|
||||
python = function(bufnr)
|
||||
-- Prefer ruff if available, fallback to black
|
||||
if vim.fn.executable("ruff") == 1 then
|
||||
return { "ruff_format", "ruff_fix" }
|
||||
elseif vim.fn.executable("black") == 1 then
|
||||
return { "black" }
|
||||
end
|
||||
return {}
|
||||
end,
|
||||
javascript = { "prettier" },
|
||||
typescript = { "prettier" },
|
||||
javascriptreact = { "prettier" },
|
||||
typescriptreact = { "prettier" },
|
||||
html = { "prettier" },
|
||||
css = { "prettier" },
|
||||
scss = { "prettier" },
|
||||
json = { "prettier" },
|
||||
markdown = { "prettier" },
|
||||
php = { "php_cs_fixer" },
|
||||
yaml = { "yamlfmt" },
|
||||
},
|
||||
|
||||
-- Format on save configuration
|
||||
format_on_save = function(bufnr)
|
||||
-- Graceful fallback: disable if no formatter available
|
||||
local formatters = conform.list_formatters(bufnr)
|
||||
if #formatters == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
return {
|
||||
timeout_ms = 500,
|
||||
lsp_fallback = true,
|
||||
async = false,
|
||||
quiet = false,
|
||||
}
|
||||
end,
|
||||
|
||||
-- Custom formatters
|
||||
formatters = {
|
||||
ruff_format = {
|
||||
command = "ruff",
|
||||
args = { "format", "--stdin-filename", "$FILENAME", "-" },
|
||||
stdin = true,
|
||||
},
|
||||
ruff_fix = {
|
||||
command = "ruff",
|
||||
args = { "check", "--fix", "--stdin-filename", "$FILENAME", "-" },
|
||||
stdin = true,
|
||||
},
|
||||
php_cs_fixer = {
|
||||
command = "php-cs-fixer",
|
||||
args = { "fix", "$FILENAME" },
|
||||
stdin = false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
-- Keymap for manual formatting
|
||||
vim.keymap.set({ "n", "v" }, "<leader>fm", function()
|
||||
conform.format({
|
||||
lsp_fallback = true,
|
||||
async = false,
|
||||
timeout_ms = 500,
|
||||
})
|
||||
end, { desc = "Format file or range (in visual mode)" })
|
||||
|
||||
-- Command to show formatter info
|
||||
vim.api.nvim_create_user_command("FormatInfo", function()
|
||||
local formatters = conform.list_formatters(0)
|
||||
if #formatters == 0 then
|
||||
print("No formatters available for this filetype")
|
||||
else
|
||||
print("Available formatters:")
|
||||
for _, formatter in ipairs(formatters) do
|
||||
print(" - " .. formatter.name)
|
||||
end
|
||||
end
|
||||
end, {})
|
||||
end,
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
-- Git integration (gitsigns.nvim)
|
||||
return {
|
||||
"lewis6991/gitsigns.nvim",
|
||||
event = { "BufReadPre", "BufNewFile" },
|
||||
config = function()
|
||||
require("gitsigns").setup({
|
||||
signs = {
|
||||
add = { text = '│' },
|
||||
change = { text = '│' },
|
||||
delete = { text = '_' },
|
||||
topdelete = { text = '‾' },
|
||||
changedelete = { text = '~' },
|
||||
untracked = { text = '┆' },
|
||||
},
|
||||
signcolumn = true, -- Toggle with `:Gitsigns toggle_signs`
|
||||
numhl = false, -- Toggle with `:Gitsigns toggle_numhl`
|
||||
linehl = false, -- Toggle with `:Gitsigns toggle_linehl`
|
||||
word_diff = false, -- Toggle with `:Gitsigns toggle_word_diff`
|
||||
watch_gitdir = {
|
||||
interval = 1000,
|
||||
follow_files = true,
|
||||
},
|
||||
attach_to_untracked = true,
|
||||
current_line_blame = false, -- Toggle with `:Gitsigns toggle_current_line_blame`
|
||||
current_line_blame_opts = {
|
||||
virt_text = true,
|
||||
virt_text_pos = 'eol', -- 'eol' | 'overlay' | 'right_align'
|
||||
delay = 1000,
|
||||
ignore_whitespace = false,
|
||||
},
|
||||
sign_priority = 6,
|
||||
update_debounce = 100,
|
||||
status_formatter = nil, -- Use default
|
||||
max_file_length = 40000,
|
||||
preview_config = {
|
||||
-- Options passed to nvim_open_win
|
||||
border = 'rounded',
|
||||
style = 'minimal',
|
||||
relative = 'cursor',
|
||||
row = 0,
|
||||
col = 1,
|
||||
},
|
||||
on_attach = function(bufnr)
|
||||
local gs = package.loaded.gitsigns
|
||||
|
||||
local function map(mode, l, r, opts)
|
||||
opts = opts or {}
|
||||
opts.buffer = bufnr
|
||||
vim.keymap.set(mode, l, r, opts)
|
||||
end
|
||||
|
||||
-- Navigation (j = down = next, k = up = prev)
|
||||
map('n', '<leader>hj', function()
|
||||
if vim.wo.diff then return ']c' end
|
||||
vim.schedule(function() gs.next_hunk() end)
|
||||
return '<Ignore>'
|
||||
end, {expr=true, desc="Next git hunk"})
|
||||
|
||||
map('n', '<leader>hk', function()
|
||||
if vim.wo.diff then return '[c' end
|
||||
vim.schedule(function() gs.prev_hunk() end)
|
||||
return '<Ignore>'
|
||||
end, {expr=true, desc="Previous git hunk"})
|
||||
|
||||
-- Actions
|
||||
map('n', '<leader>hs', gs.stage_hunk, {desc="Stage hunk"})
|
||||
map('n', '<leader>hr', gs.reset_hunk, {desc="Reset hunk"})
|
||||
map('v', '<leader>hs', function() gs.stage_hunk {vim.fn.line('.'), vim.fn.line('v')} end, {desc="Stage hunk"})
|
||||
map('v', '<leader>hr', function() gs.reset_hunk {vim.fn.line('.'), vim.fn.line('v')} end, {desc="Reset hunk"})
|
||||
map('n', '<leader>hS', gs.stage_buffer, {desc="Stage buffer"})
|
||||
map('n', '<leader>hu', gs.undo_stage_hunk, {desc="Undo stage hunk"})
|
||||
map('n', '<leader>hR', gs.reset_buffer, {desc="Reset buffer"})
|
||||
map('n', '<leader>hp', gs.preview_hunk, {desc="Preview hunk"})
|
||||
map('n', '<leader>hb', function() gs.blame_line{full=true} end, {desc="Blame line"})
|
||||
map('n', '<leader>tb', gs.toggle_current_line_blame, {desc="Toggle line blame"})
|
||||
map('n', '<leader>hd', gs.diffthis, {desc="Diff this"})
|
||||
map('n', '<leader>hD', function() gs.diffthis('~') end, {desc="Diff this ~"})
|
||||
map('n', '<leader>td', gs.toggle_deleted, {desc="Toggle deleted"})
|
||||
|
||||
-- Text object
|
||||
map({'o', 'x'}, 'ih', ':<C-U>Gitsigns select_hunk<CR>', {desc="Select hunk"})
|
||||
end,
|
||||
})
|
||||
end,
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
-- Visual indent guides (indent-blankline.nvim)
|
||||
return {
|
||||
"lukas-reineke/indent-blankline.nvim",
|
||||
main = "ibl",
|
||||
event = { "BufReadPost", "BufNewFile" },
|
||||
config = function()
|
||||
require("ibl").setup({
|
||||
indent = {
|
||||
char = "│",
|
||||
},
|
||||
scope = {
|
||||
enabled = true,
|
||||
show_start = false,
|
||||
show_end = false,
|
||||
},
|
||||
exclude = {
|
||||
filetypes = {
|
||||
"help",
|
||||
"neo-tree",
|
||||
"lazy",
|
||||
"mason",
|
||||
"TelescopePrompt",
|
||||
},
|
||||
},
|
||||
})
|
||||
end,
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
-- LSP configuration (nvim 0.11+ native API)
|
||||
return {
|
||||
{
|
||||
"neovim/nvim-lspconfig",
|
||||
config = function()
|
||||
|
||||
-- Diagnostic configuration
|
||||
vim.diagnostic.config({
|
||||
virtual_text = true,
|
||||
signs = {
|
||||
text = {
|
||||
[vim.diagnostic.severity.ERROR] = "✘",
|
||||
[vim.diagnostic.severity.WARN] = "▲",
|
||||
[vim.diagnostic.severity.HINT] = "⚑",
|
||||
[vim.diagnostic.severity.INFO] = "»",
|
||||
},
|
||||
},
|
||||
underline = true,
|
||||
update_in_insert = false,
|
||||
severity_sort = true,
|
||||
float = {
|
||||
border = "rounded",
|
||||
source = true,
|
||||
},
|
||||
})
|
||||
|
||||
-- Global LSP keybindings applied when any server attaches
|
||||
vim.lsp.config('*', {
|
||||
on_attach = function(client, bufnr)
|
||||
local opts = { buffer = bufnr, noremap = true, silent = true }
|
||||
vim.keymap.set('n', 'gD', vim.lsp.buf.declaration, opts)
|
||||
vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts)
|
||||
vim.keymap.set('n', 'K', vim.lsp.buf.hover, opts)
|
||||
vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, opts)
|
||||
vim.keymap.set('n', '<leader>K', vim.lsp.buf.signature_help, opts)
|
||||
vim.keymap.set('n', '<leader>rn', vim.lsp.buf.rename, opts)
|
||||
vim.keymap.set('n', '<leader>ca', vim.lsp.buf.code_action, opts)
|
||||
vim.keymap.set('n', 'gr', vim.lsp.buf.references, opts)
|
||||
vim.keymap.set('n', '<leader>f', function() vim.lsp.buf.format({ async = true }) end, opts)
|
||||
end,
|
||||
})
|
||||
|
||||
-- Python: pyright (prefer) or pylsp (fallback)
|
||||
if vim.fn.executable("pyright-langserver") == 1 then
|
||||
vim.lsp.config('pyright', {
|
||||
settings = {
|
||||
python = {
|
||||
analysis = {
|
||||
typeCheckingMode = "basic",
|
||||
autoSearchPaths = true,
|
||||
useLibraryCodeForTypes = true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
vim.lsp.enable('pyright')
|
||||
elseif vim.fn.executable("pylsp") == 1 then
|
||||
vim.lsp.enable('pylsp')
|
||||
end
|
||||
|
||||
-- PHP: intelephense
|
||||
if vim.fn.executable("intelephense") == 1 then
|
||||
vim.lsp.enable('intelephense')
|
||||
end
|
||||
|
||||
-- JavaScript/TypeScript: typescript-language-server
|
||||
if vim.fn.executable("typescript-language-server") == 1 then
|
||||
vim.lsp.enable('ts_ls')
|
||||
end
|
||||
|
||||
-- HTML
|
||||
if vim.fn.executable("vscode-html-language-server") == 1 then
|
||||
vim.lsp.enable('html')
|
||||
end
|
||||
-- CSS
|
||||
if vim.fn.executable("vscode-css-language-server") == 1 then
|
||||
vim.lsp.enable('cssls')
|
||||
end
|
||||
-- JSON
|
||||
if vim.fn.executable("vscode-json-language-server") == 1 then
|
||||
vim.lsp.enable('jsonls')
|
||||
end
|
||||
|
||||
-- YAML
|
||||
if vim.fn.executable("yaml-language-server") == 1 then
|
||||
vim.lsp.config('yamlls', {
|
||||
settings = {
|
||||
yaml = {
|
||||
schemas = {
|
||||
["https://json.schemastore.org/github-workflow.json"] = "/.github/workflows/*",
|
||||
["https://raw.githubusercontent.com/ansible/ansible-lint/main/src/ansiblelint/schemas/ansible.json#/$defs/playbook"] = "/*play*.{yml,yaml}",
|
||||
["https://raw.githubusercontent.com/ansible/ansible-lint/main/src/ansiblelint/schemas/ansible.json#/$defs/tasks"] = "/tasks/*.{yml,yaml}",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
vim.lsp.enable('yamlls')
|
||||
end
|
||||
|
||||
-- Markdown: marksman
|
||||
if vim.fn.executable("marksman") == 1 then
|
||||
vim.lsp.enable('marksman')
|
||||
end
|
||||
|
||||
-- Bash: bash-language-server
|
||||
if vim.fn.executable("bash-language-server") == 1 then
|
||||
vim.lsp.enable('bashls')
|
||||
end
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
-- tadmccorkle/markdown.nvim — buffer-local helpers for markdown editing.
|
||||
-- Defaults already wire emphasis (gs/gss/ds/cs), links (gl/gx) and heading
|
||||
-- nav (]c/]p/]]/[[). List-item/checkbox features are user commands only,
|
||||
-- so on_attach adds <localleader> bindings for them.
|
||||
return {
|
||||
"tadmccorkle/markdown.nvim",
|
||||
ft = "markdown",
|
||||
opts = {
|
||||
on_attach = function(bufnr)
|
||||
local map = function(mode, lhs, rhs, desc)
|
||||
vim.keymap.set(mode, lhs, rhs, { buffer = bufnr, silent = true, desc = desc })
|
||||
end
|
||||
map("n", "<localleader>o", "<Cmd>MDListItemBelow<CR>", "Insert list item below")
|
||||
map("n", "<localleader>O", "<Cmd>MDListItemAbove<CR>", "Insert list item above")
|
||||
map("n", "<localleader>ch", "<Cmd>MDTaskToggle<CR>", "Toggle checkbox")
|
||||
-- <localleader>n (not rn): the LSP's on_attach binds <leader>rn to
|
||||
-- vim.lsp.buf.rename buffer-local and overrides ours.
|
||||
map("n", "<localleader>n", "<Cmd>%MDResetListNumbering<CR>", "Renumber ordered list")
|
||||
map("x", "<localleader>ch", ":MDTaskToggle<CR>", "Toggle checkbox (range)")
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
-- echasnovski/mini.move — move lines/selections with Alt+arrows in any
|
||||
-- filetype. Uses arrow keys to avoid clashing with existing <A-j>/<A-k>
|
||||
-- diagnostic mappings in keymaps.lua.
|
||||
return {
|
||||
"echasnovski/mini.move",
|
||||
event = "VeryLazy",
|
||||
opts = {
|
||||
mappings = {
|
||||
-- visual selection
|
||||
left = "<M-Left>",
|
||||
right = "<M-Right>",
|
||||
down = "<M-Down>",
|
||||
up = "<M-Up>",
|
||||
-- current line in normal mode
|
||||
line_left = "<M-Left>",
|
||||
line_right = "<M-Right>",
|
||||
line_down = "<M-Down>",
|
||||
line_up = "<M-Up>",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
return {
|
||||
"alexghergh/nvim-tmux-navigation",
|
||||
event = "VeryLazy",
|
||||
config = function()
|
||||
local nav = require("nvim-tmux-navigation")
|
||||
nav.setup({ disable_when_zoomed = true })
|
||||
vim.keymap.set("n", "<C-h>", nav.NvimTmuxNavigateLeft, { noremap = true, silent = true, desc = "Navigate left" })
|
||||
vim.keymap.set("n", "<C-j>", nav.NvimTmuxNavigateDown, { noremap = true, silent = true, desc = "Navigate down" })
|
||||
vim.keymap.set("n", "<C-k>", nav.NvimTmuxNavigateUp, { noremap = true, silent = true, desc = "Navigate up" })
|
||||
vim.keymap.set("n", "<C-l>", nav.NvimTmuxNavigateRight, { noremap = true, silent = true, desc = "Navigate right" })
|
||||
end,
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
-- Telescope fuzzy finder configuration
|
||||
return {
|
||||
"nvim-telescope/telescope.nvim",
|
||||
branch = "0.1.x",
|
||||
dependencies = {
|
||||
"nvim-lua/plenary.nvim",
|
||||
},
|
||||
cmd = "Telescope",
|
||||
keys = {
|
||||
{ "<leader>ff", "<cmd>Telescope find_files<cr>", desc = "Find files" },
|
||||
{ "<leader>fg", "<cmd>Telescope live_grep<cr>", desc = "Live grep" },
|
||||
{ "<leader>fb", "<cmd>Telescope buffers<cr>", desc = "Find buffers" },
|
||||
{ "<leader>fh", "<cmd>Telescope help_tags<cr>", desc = "Help tags" },
|
||||
{ "<leader>fr", "<cmd>Telescope oldfiles<cr>", desc = "Recent files" },
|
||||
{ "<leader>fc", "<cmd>Telescope commands<cr>", desc = "Commands" },
|
||||
{ "<leader>fk", "<cmd>Telescope keymaps<cr>", desc = "Keymaps" },
|
||||
},
|
||||
config = function()
|
||||
local telescope = require("telescope")
|
||||
local actions = require("telescope.actions")
|
||||
|
||||
telescope.setup({
|
||||
defaults = {
|
||||
prompt_prefix = "🔍 ",
|
||||
selection_caret = "➤ ",
|
||||
path_display = { "truncate" },
|
||||
sorting_strategy = "ascending",
|
||||
layout_config = {
|
||||
horizontal = {
|
||||
prompt_position = "top",
|
||||
preview_width = 0.55,
|
||||
results_width = 0.8,
|
||||
},
|
||||
vertical = {
|
||||
mirror = false,
|
||||
},
|
||||
width = 0.87,
|
||||
height = 0.80,
|
||||
preview_cutoff = 120,
|
||||
},
|
||||
mappings = {
|
||||
i = {
|
||||
["<C-k>"] = actions.move_selection_previous,
|
||||
["<C-j>"] = actions.move_selection_next,
|
||||
["<C-q>"] = actions.send_selected_to_qflist + actions.open_qflist,
|
||||
["<Esc>"] = actions.close,
|
||||
},
|
||||
n = {
|
||||
["q"] = actions.close,
|
||||
},
|
||||
},
|
||||
},
|
||||
pickers = {
|
||||
find_files = {
|
||||
hidden = false,
|
||||
find_command = { "rg", "--files", "--hidden", "--glob", "!.git/*" },
|
||||
},
|
||||
},
|
||||
})
|
||||
end,
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
-- Terminal management (toggleterm.nvim)
|
||||
-- <leader>tt floating shell
|
||||
-- <leader>gg lazygit
|
||||
return {
|
||||
"akinsho/toggleterm.nvim",
|
||||
version = "*",
|
||||
keys = {
|
||||
{ "<leader>tt", desc = "Toggle floating terminal" },
|
||||
{ "<leader>gg", desc = "Open lazygit" },
|
||||
},
|
||||
config = function()
|
||||
require("toggleterm").setup({
|
||||
direction = "float",
|
||||
float_opts = {
|
||||
border = "curved",
|
||||
width = function() return math.floor(vim.o.columns * 0.9) end,
|
||||
height = function() return math.floor(vim.o.lines * 0.9) end,
|
||||
},
|
||||
close_on_exit = true,
|
||||
})
|
||||
|
||||
local Terminal = require("toggleterm.terminal").Terminal
|
||||
|
||||
local shell_term = Terminal:new({ direction = "float" })
|
||||
vim.keymap.set("n", "<leader>tt", function()
|
||||
shell_term:toggle()
|
||||
end, { desc = "Toggle floating terminal" })
|
||||
|
||||
local lazygit = Terminal:new({
|
||||
cmd = "lazygit",
|
||||
direction = "float",
|
||||
close_on_exit = true,
|
||||
on_open = function(_)
|
||||
vim.cmd("startinsert!")
|
||||
end,
|
||||
})
|
||||
vim.keymap.set("n", "<leader>gg", function()
|
||||
lazygit:toggle()
|
||||
end, { desc = "Open lazygit" })
|
||||
end,
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
-- Treesitter configuration
|
||||
-- Pin to legacy `master` branch: upstream made `main` the default, but `main`
|
||||
-- is a rewrite that removed the `nvim-treesitter.configs` module this config
|
||||
-- depends on. `master` is still maintained and exposes the classic API.
|
||||
return {
|
||||
"nvim-treesitter/nvim-treesitter",
|
||||
branch = "master",
|
||||
build = ":TSUpdate",
|
||||
config = function()
|
||||
-- Check if treesitter is available
|
||||
local status_ok, treesitter_configs = pcall(require, "nvim-treesitter.configs")
|
||||
if not status_ok then
|
||||
vim.notify("nvim-treesitter not loaded", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
treesitter_configs.setup({
|
||||
-- Install parsers for these languages.
|
||||
-- Omit parsers bundled with nvim 0.10 (c, lua, markdown, markdown_inline,
|
||||
-- vim, vimdoc) — they live in /usr/lib/nvim/parser/ and do not need building.
|
||||
ensure_installed = {
|
||||
"bash",
|
||||
"css",
|
||||
"html",
|
||||
"javascript",
|
||||
"json",
|
||||
"php",
|
||||
"python",
|
||||
"typescript",
|
||||
"yaml",
|
||||
},
|
||||
|
||||
-- Install parsers synchronously (only applied to `ensure_installed`)
|
||||
sync_install = false,
|
||||
|
||||
-- Automatically install missing parsers when entering buffer
|
||||
auto_install = true,
|
||||
|
||||
highlight = {
|
||||
enable = true,
|
||||
additional_vim_regex_highlighting = false,
|
||||
},
|
||||
|
||||
indent = {
|
||||
enable = true,
|
||||
},
|
||||
|
||||
-- Incremental selection
|
||||
incremental_selection = {
|
||||
enable = true,
|
||||
keymaps = {
|
||||
init_selection = "<C-space>",
|
||||
node_incremental = "<C-space>",
|
||||
scope_incremental = false,
|
||||
node_decremental = "<C-backspace>",
|
||||
},
|
||||
},
|
||||
})
|
||||
end,
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
-- UI and editing quality-of-life plugins
|
||||
return {
|
||||
-- ---- Catppuccin colorscheme (priority 1000 — loads before other UI) ----
|
||||
{
|
||||
"catppuccin/nvim",
|
||||
name = "catppuccin",
|
||||
priority = 1000,
|
||||
config = function()
|
||||
require("catppuccin").setup({
|
||||
flavour = "mocha",
|
||||
transparent_background = false,
|
||||
term_colors = true,
|
||||
integrations = {
|
||||
treesitter = true,
|
||||
telescope = { enabled = true },
|
||||
gitsigns = true,
|
||||
which_key = true,
|
||||
cmp = true,
|
||||
native_lsp = { enabled = true },
|
||||
},
|
||||
})
|
||||
vim.cmd.colorscheme("catppuccin-mocha")
|
||||
end,
|
||||
},
|
||||
|
||||
-- ---- Lualine status line ----
|
||||
{
|
||||
"nvim-lualine/lualine.nvim",
|
||||
dependencies = { "nvim-tree/nvim-web-devicons" },
|
||||
event = "VeryLazy",
|
||||
config = function()
|
||||
require("lualine").setup({
|
||||
options = {
|
||||
theme = "catppuccin-mocha",
|
||||
globalstatus = true,
|
||||
component_separators = { left = "", right = "" },
|
||||
section_separators = { left = "", right = "" },
|
||||
},
|
||||
})
|
||||
end,
|
||||
},
|
||||
|
||||
-- ---- Zen mode ----
|
||||
{
|
||||
"folke/zen-mode.nvim",
|
||||
cmd = "ZenMode",
|
||||
keys = { { "<leader>z", "<cmd>ZenMode<cr>", desc = "Zen Mode" } },
|
||||
config = function()
|
||||
require("zen-mode").setup({
|
||||
window = {
|
||||
width = 90,
|
||||
height = 1,
|
||||
options = { signcolumn = "no", number = false, relativenumber = false },
|
||||
},
|
||||
plugins = { tmux = { enabled = true } },
|
||||
})
|
||||
end,
|
||||
},
|
||||
|
||||
-- ---- Which-key (v3 API) ----
|
||||
{
|
||||
"folke/which-key.nvim",
|
||||
event = "VeryLazy",
|
||||
config = function()
|
||||
local wk = require("which-key")
|
||||
wk.setup({ preset = "modern", delay = 500 })
|
||||
wk.add({
|
||||
{ "<leader>f", group = "Find (Telescope)" },
|
||||
{ "<leader>h", group = "Git Hunks" },
|
||||
{ "<leader>z", desc = "Zen Mode" },
|
||||
})
|
||||
end,
|
||||
},
|
||||
|
||||
-- Auto-close brackets, quotes, etc.
|
||||
{
|
||||
"windwp/nvim-autopairs",
|
||||
event = "InsertEnter",
|
||||
config = function()
|
||||
require("nvim-autopairs").setup({
|
||||
check_ts = true, -- Enable treesitter integration
|
||||
ts_config = {
|
||||
lua = { "string" }, -- Don't add pairs in lua string treesitter nodes
|
||||
javascript = { "template_string" },
|
||||
java = false, -- Don't check treesitter on java
|
||||
},
|
||||
disable_filetype = { "TelescopePrompt", "vim" },
|
||||
fast_wrap = {
|
||||
map = "<M-e>",
|
||||
chars = { "{", "[", "(", '"', "'" },
|
||||
pattern = [=[[%'%"%>%]%)%}%,]]=],
|
||||
end_key = "$",
|
||||
keys = "qwertyuiopzxcvbnmasdfghjkl",
|
||||
check_comma = true,
|
||||
highlight = "Search",
|
||||
highlight_grey = "Comment",
|
||||
},
|
||||
})
|
||||
end,
|
||||
},
|
||||
|
||||
-- Surround text objects
|
||||
{
|
||||
"kylechui/nvim-surround",
|
||||
event = "VeryLazy",
|
||||
config = function()
|
||||
require("nvim-surround").setup()
|
||||
end,
|
||||
},
|
||||
|
||||
-- Smart commenting
|
||||
{
|
||||
"numToStr/Comment.nvim",
|
||||
event = "VeryLazy",
|
||||
config = function()
|
||||
require("Comment").setup({
|
||||
padding = true,
|
||||
sticky = true,
|
||||
ignore = "^$", -- Ignore empty lines
|
||||
toggler = {
|
||||
line = "gcc",
|
||||
block = "gbc",
|
||||
},
|
||||
opleader = {
|
||||
line = "gc",
|
||||
block = "gb",
|
||||
},
|
||||
extra = {
|
||||
above = "gcO",
|
||||
below = "gco",
|
||||
eol = "gcA",
|
||||
},
|
||||
mappings = {
|
||||
basic = true,
|
||||
extra = true,
|
||||
},
|
||||
})
|
||||
end,
|
||||
},
|
||||
}
|
||||
88
roles/dev_env/files/dotfiles/tmux/.tmux.conf
Normal file
88
roles/dev_env/files/dotfiles/tmux/.tmux.conf
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# tmux configuration — managed by boma dev_env role (stow)
|
||||
|
||||
# Prefix
|
||||
unbind C-b
|
||||
set-option -g prefix C-a
|
||||
bind-key C-a send-prefix
|
||||
|
||||
# General
|
||||
set -g default-terminal "screen-256color"
|
||||
set -ag terminal-overrides ",xterm-256color:RGB"
|
||||
set -g mouse on
|
||||
set -g history-limit 50000
|
||||
set -g display-time 4000
|
||||
set -g status-interval 5
|
||||
set -g focus-events on
|
||||
set -s extended-keys off
|
||||
set -s set-clipboard on
|
||||
set -g allow-passthrough on
|
||||
set -as terminal-features ',xterm-256color:clipboard'
|
||||
|
||||
# Base index
|
||||
set -g base-index 1
|
||||
set -g pane-base-index 1
|
||||
set-window-option -g pane-base-index 1
|
||||
set-option -g renumber-windows on
|
||||
|
||||
# Vi mode in copy mode
|
||||
set-window-option -g mode-keys vi
|
||||
bind-key -T copy-mode-vi v send-keys -X begin-selection
|
||||
bind-key -T copy-mode-vi C-v send-keys -X rectangle-toggle
|
||||
bind-key -T copy-mode-vi y send-keys -X copy-selection-and-cancel
|
||||
|
||||
# Splits — arrow direction = where new pane opens (mirrors kitty and nvim)
|
||||
bind Right split-window -h -c "#{pane_current_path}"
|
||||
bind Down split-window -v -c "#{pane_current_path}"
|
||||
unbind '"'
|
||||
unbind %
|
||||
unbind v
|
||||
unbind -
|
||||
|
||||
# Pane resize (repeatable)
|
||||
bind -r H resize-pane -L 5
|
||||
bind -r J resize-pane -D 5
|
||||
bind -r K resize-pane -U 5
|
||||
bind -r L resize-pane -R 5
|
||||
|
||||
# Window cycling — no prefix needed; mirrors nvim buffer cycling (Ctrl = inner layer)
|
||||
bind -n C-Tab next-window
|
||||
bind -n C-NPage next-window
|
||||
bind -n C-PPage previous-window
|
||||
|
||||
# New window — t = tab (mirrors kitty Ctrl+Shift+T); keep c as alias
|
||||
bind t new-window -c "#{pane_current_path}"
|
||||
bind c new-window -c "#{pane_current_path}"
|
||||
|
||||
# Close pane without confirmation prompt
|
||||
bind x kill-pane
|
||||
|
||||
# Reload config
|
||||
bind r source-file ~/.tmux.conf \; display "Config reloaded!"
|
||||
|
||||
# =============================================================
|
||||
# Plugins (TPM)
|
||||
# Pin catppuccin to v1 API to avoid v2 breaking changes
|
||||
# =============================================================
|
||||
set -g @plugin 'tmux-plugins/tpm'
|
||||
set -g @plugin 'tmux-plugins/tmux-sensible'
|
||||
set -g @plugin 'christoomey/vim-tmux-navigator'
|
||||
set -g @plugin 'catppuccin/tmux#v1.0.3'
|
||||
|
||||
# Catppuccin theme (v1 API)
|
||||
set -g @catppuccin_flavour 'mocha'
|
||||
set -g @catppuccin_window_left_separator ""
|
||||
set -g @catppuccin_window_right_separator " "
|
||||
set -g @catppuccin_window_middle_separator " █"
|
||||
set -g @catppuccin_window_number_position "right"
|
||||
set -g @catppuccin_window_default_fill "number"
|
||||
set -g @catppuccin_window_current_fill "number"
|
||||
set -g @catppuccin_window_current_text "#{pane_current_path}"
|
||||
set -g @catppuccin_status_modules_right "directory session"
|
||||
set -g @catppuccin_status_left_separator " "
|
||||
set -g @catppuccin_status_right_separator ""
|
||||
set -g @catppuccin_status_fill "icon"
|
||||
set -g @catppuccin_status_connect_separator "no"
|
||||
set -g @catppuccin_directory_text "#{pane_current_path}"
|
||||
|
||||
# Initialize TPM (keep at very bottom)
|
||||
run '~/.tmux/plugins/tpm/tpm'
|
||||
55
roles/dev_env/files/dotfiles/zsh/.zshrc
Normal file
55
roles/dev_env/files/dotfiles/zsh/.zshrc
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# ---- managed by boma dev_env role (stow) ----
|
||||
# If Oh My Zsh is installed, point ZSH and load it; otherwise pure zsh.
|
||||
export ZSH="$HOME/.oh-my-zsh"
|
||||
|
||||
ZSH_THEME="robbyrussell"
|
||||
plugins=(git history sudo zsh-autosuggestions zsh-syntax-highlighting)
|
||||
|
||||
if [ -d "$ZSH" ]; then
|
||||
source "$ZSH/oh-my-zsh.sh"
|
||||
fi
|
||||
|
||||
# Oh My Posh prompt (preferred)
|
||||
if command -v oh-my-posh >/dev/null 2>&1; then
|
||||
eval "$(oh-my-posh init zsh --config /etc/oh-my-posh/zen.toml)"
|
||||
fi
|
||||
|
||||
# Terminal title
|
||||
function set_terminal_title {
|
||||
echo -ne "\033]0;${USER}@${HOSTNAME}: ${PWD}\007"
|
||||
}
|
||||
precmd() { set_terminal_title }
|
||||
|
||||
# Aliases and basics
|
||||
alias ll="ls -lh"
|
||||
alias la="ls -lha"
|
||||
alias ..="cd .."
|
||||
alias update="sudo apt update && sudo apt upgrade -y"
|
||||
alias rclone="/usr/bin/rclone"
|
||||
|
||||
# Use neovim for vim/vi commands
|
||||
alias vim='nvim'
|
||||
alias vi='nvim'
|
||||
|
||||
# History
|
||||
HISTFILE="$HOME/.zsh_history"
|
||||
HISTSIZE=10000
|
||||
SAVEHIST=10000
|
||||
setopt inc_append_history share_history
|
||||
|
||||
# Completion/globbing
|
||||
autoload -Uz compinit
|
||||
compinit
|
||||
setopt autocd correct globdots no_beep
|
||||
|
||||
# Editor & PATH
|
||||
export EDITOR="/usr/local/bin/nvim"
|
||||
export VISUAL="/usr/local/bin/nvim"
|
||||
export PATH="$HOME/.local/bin:$HOME/bin:$PATH"
|
||||
|
||||
# Ensure USER is set (edge cases)
|
||||
export USER=$(whoami)
|
||||
|
||||
# Extras from inventory
|
||||
# Enable direnv for automatic virtualenv activation
|
||||
eval "$(direnv hook zsh)"
|
||||
78
roles/dev_env/files/oh-my-posh/zen.toml
Normal file
78
roles/dev_env/files/oh-my-posh/zen.toml
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#:schema https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json
|
||||
version = 3
|
||||
final_space = true
|
||||
console_title_template = '{{ .Shell }} in {{ .Folder }}'
|
||||
|
||||
[[blocks]]
|
||||
type = 'prompt'
|
||||
alignment = 'left'
|
||||
newline = true
|
||||
|
||||
[[blocks.segments]]
|
||||
type = 'text'
|
||||
style = 'plain'
|
||||
foreground = 'cyan'
|
||||
background = 'transparent'
|
||||
template = '{{ .UserName }}@{{ .HostName }}:'
|
||||
|
||||
[[blocks.segments]]
|
||||
type = 'path'
|
||||
style = 'plain'
|
||||
foreground = 'blue'
|
||||
background = 'transparent'
|
||||
template = '{{ .Path }}'
|
||||
[blocks.segments.properties]
|
||||
style = 'full'
|
||||
|
||||
[[blocks.segments]]
|
||||
type = 'text'
|
||||
style = 'plain'
|
||||
foreground = 'magenta'
|
||||
background = 'transparent'
|
||||
template = '>'
|
||||
|
||||
[[blocks.segments]]
|
||||
type = 'git'
|
||||
style = 'plain'
|
||||
foreground = 'p:grey'
|
||||
background = 'transparent'
|
||||
template = ' {{ .HEAD }}{{ if or (.Working.Changed) (.Staging.Changed) }}*{{ end }} <cyan>{{ if gt .Behind 0 }}⇣{{ end }}{{ if gt .Ahead 0 }}⇡{{ end }}</>'
|
||||
|
||||
[[blocks]]
|
||||
type = 'rprompt'
|
||||
overflow = 'hidden'
|
||||
[[blocks.segments]]
|
||||
type = 'executiontime'
|
||||
style = 'plain'
|
||||
foreground = 'yellow'
|
||||
background = 'transparent'
|
||||
template = '{{ .FormattedMs }}'
|
||||
[blocks.segments.properties]
|
||||
threshold = 5000
|
||||
|
||||
[[blocks]]
|
||||
type = 'prompt'
|
||||
alignment = 'left'
|
||||
newline = true
|
||||
[[blocks.segments]]
|
||||
type = 'text'
|
||||
style = 'plain'
|
||||
foreground_templates = [
|
||||
"{{if gt .Code 0}}red{{end}}",
|
||||
"{{if eq .Code 0}}magenta{{end}}",
|
||||
]
|
||||
background = 'transparent'
|
||||
template = '❯'
|
||||
|
||||
[transient_prompt]
|
||||
foreground_templates = [
|
||||
"{{if gt .Code 0}}red{{end}}",
|
||||
"{{if eq .Code 0}}magenta{{end}}",
|
||||
]
|
||||
background = 'transparent'
|
||||
template = '❯ '
|
||||
|
||||
[secondary_prompt]
|
||||
foreground = 'magenta'
|
||||
background = 'transparent'
|
||||
template = '❯❯ '
|
||||
1
roles/dev_env/handlers/main.yml
Normal file
1
roles/dev_env/handlers/main.yml
Normal file
|
|
@ -0,0 +1 @@
|
|||
---
|
||||
13
roles/dev_env/meta/main.yml
Normal file
13
roles/dev_env/meta/main.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
galaxy_info:
|
||||
author: sjat
|
||||
description: >-
|
||||
Interactive developer environment (zsh/oh-my-zsh/oh-my-posh, tmux, neovim) for
|
||||
workstation-class boma hosts (Debian 13). Dotfiles deployed via GNU stow.
|
||||
license: MIT
|
||||
min_ansible_version: "2.17"
|
||||
platforms:
|
||||
- name: Debian
|
||||
versions:
|
||||
- trixie
|
||||
dependencies: []
|
||||
15
roles/dev_env/molecule/default/converge.yml
Normal file
15
roles/dev_env/molecule/default/converge.yml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
- name: Converge
|
||||
hosts: all
|
||||
become: true
|
||||
gather_facts: true
|
||||
vars:
|
||||
dev_env__users:
|
||||
- tester
|
||||
pre_tasks:
|
||||
- name: Create a test user to receive the environment
|
||||
ansible.builtin.user:
|
||||
name: tester
|
||||
create_home: true
|
||||
roles:
|
||||
- role: dev_env
|
||||
31
roles/dev_env/molecule/default/molecule.yml
Normal file
31
roles/dev_env/molecule/default/molecule.yml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
dependency:
|
||||
name: galaxy
|
||||
options:
|
||||
requirements-file: ../../requirements.yml
|
||||
|
||||
driver:
|
||||
name: docker
|
||||
|
||||
platforms:
|
||||
- name: instance
|
||||
# Project-owned image built from .docker/molecule-debian13/Dockerfile
|
||||
# and hosted in the Forgejo container registry.
|
||||
# Build/push with: make molecule-image / make molecule-image-push
|
||||
image: forgejo.nyumbani.baobab.band/sjat/molecule-debian13:latest
|
||||
pre_build_image: true
|
||||
privileged: true # required for systemd
|
||||
cgroupns_mode: host
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
command: /lib/systemd/systemd
|
||||
|
||||
provisioner:
|
||||
name: ansible
|
||||
inventory:
|
||||
host_vars:
|
||||
instance:
|
||||
ansible_user: root
|
||||
|
||||
verifier:
|
||||
name: ansible
|
||||
73
roles/dev_env/molecule/default/verify.yml
Normal file
73
roles/dev_env/molecule/default/verify.yml
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
- name: Verify
|
||||
hosts: all
|
||||
become: true
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Gather installed-package facts
|
||||
ansible.builtin.package_facts:
|
||||
manager: apt
|
||||
|
||||
- name: Assert core packages are present
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "'zsh' in ansible_facts.packages"
|
||||
- "'tmux' in ansible_facts.packages"
|
||||
- "'stow' in ansible_facts.packages"
|
||||
- "'direnv' in ansible_facts.packages"
|
||||
fail_msg: core dev_env packages missing
|
||||
|
||||
- name: Stat system binaries and theme
|
||||
ansible.builtin.stat:
|
||||
path: "{{ item }}"
|
||||
loop:
|
||||
- /usr/local/bin/nvim
|
||||
- /usr/local/bin/oh-my-posh
|
||||
- /etc/oh-my-posh/zen.toml
|
||||
register: dev_env__sys
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
|
||||
- name: Assert system tools are installed
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- dev_env__sys.results[0].stat.exists
|
||||
- dev_env__sys.results[1].stat.exists
|
||||
- dev_env__sys.results[2].stat.exists
|
||||
fail_msg: nvim/oh-my-posh/zen.toml missing
|
||||
|
||||
- name: Look up the test user
|
||||
ansible.builtin.getent:
|
||||
database: passwd
|
||||
key: tester
|
||||
|
||||
- name: Assert tester login shell is zsh
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "getent_passwd['tester'][5] == '/usr/bin/zsh'"
|
||||
fail_msg: tester login shell was not set to zsh
|
||||
|
||||
- name: Stat tester dotfiles and frameworks
|
||||
ansible.builtin.stat:
|
||||
path: "{{ item }}"
|
||||
loop:
|
||||
- /home/tester/.zshrc
|
||||
- /home/tester/.tmux.conf
|
||||
- /home/tester/.config/nvim/init.lua
|
||||
- /home/tester/.oh-my-zsh
|
||||
- /home/tester/.tmux/plugins/tpm
|
||||
register: dev_env__dots
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
|
||||
- name: Assert dotfiles are stowed (symlinks) and frameworks cloned
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- dev_env__dots.results[0].stat.exists
|
||||
- dev_env__dots.results[0].stat.islnk
|
||||
- dev_env__dots.results[1].stat.exists
|
||||
- dev_env__dots.results[1].stat.islnk
|
||||
- dev_env__dots.results[2].stat.exists
|
||||
- dev_env__dots.results[3].stat.exists
|
||||
- dev_env__dots.results[4].stat.exists
|
||||
fail_msg: dotfiles not stowed or omz/tpm not cloned
|
||||
4
roles/dev_env/requirements.yml
Normal file
4
roles/dev_env/requirements.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
# No extra Galaxy collections beyond the repo-level requirements.yml — dev_env uses
|
||||
# only ansible.builtin modules. Present so the Molecule galaxy dependency step resolves.
|
||||
collections: []
|
||||
23
roles/dev_env/tasks/main.yml
Normal file
23
roles/dev_env/tasks/main.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
- name: Install developer-environment packages
|
||||
ansible.builtin.apt:
|
||||
name: "{{ dev_env__packages }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
cache_valid_time: 3600
|
||||
tags: [packages]
|
||||
|
||||
- name: Install Neovim (pinned release)
|
||||
ansible.builtin.include_tasks: neovim.yml
|
||||
tags: [packages]
|
||||
|
||||
- name: Install oh-my-posh prompt (pinned release)
|
||||
ansible.builtin.include_tasks: oh_my_posh.yml
|
||||
tags: [packages]
|
||||
|
||||
- name: Configure each developer user
|
||||
ansible.builtin.include_tasks: per_user.yml
|
||||
loop: "{{ dev_env__users }}"
|
||||
loop_control:
|
||||
loop_var: dev_env__user
|
||||
label: "{{ dev_env__user }}"
|
||||
52
roles/dev_env/tasks/neovim.yml
Normal file
52
roles/dev_env/tasks/neovim.yml
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
- name: Neovim | Read installed-version sentinel
|
||||
ansible.builtin.slurp:
|
||||
src: /etc/nvim_installed_version
|
||||
register: dev_env__nvim_sentinel
|
||||
failed_when: false
|
||||
|
||||
- name: Neovim | Determine installed version
|
||||
ansible.builtin.set_fact:
|
||||
dev_env__nvim_installed: >-
|
||||
{{ (dev_env__nvim_sentinel.content | b64decode | trim)
|
||||
if dev_env__nvim_sentinel.content is defined else '' }}
|
||||
|
||||
- name: Neovim | Install pinned release
|
||||
when: dev_env__nvim_installed != dev_env__nvim_version
|
||||
block:
|
||||
- name: Neovim | Download release tarball
|
||||
ansible.builtin.get_url:
|
||||
url: "https://github.com/neovim/neovim/releases/download/{{ dev_env__nvim_version }}/nvim-linux-x86_64.tar.gz"
|
||||
dest: "/tmp/nvim-{{ dev_env__nvim_version }}.tar.gz"
|
||||
mode: "0644"
|
||||
|
||||
- name: Neovim | Create versioned install directory
|
||||
ansible.builtin.file:
|
||||
path: "/opt/nvim-{{ dev_env__nvim_version }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Neovim | Extract tarball
|
||||
ansible.builtin.unarchive:
|
||||
src: "/tmp/nvim-{{ dev_env__nvim_version }}.tar.gz"
|
||||
dest: "/opt/nvim-{{ dev_env__nvim_version }}"
|
||||
remote_src: true
|
||||
extra_opts: ["--strip-components=1"]
|
||||
|
||||
- name: Neovim | Symlink into PATH
|
||||
ansible.builtin.file:
|
||||
src: "/opt/nvim-{{ dev_env__nvim_version }}/bin/nvim"
|
||||
dest: /usr/local/bin/nvim
|
||||
state: link
|
||||
force: true
|
||||
|
||||
- name: Neovim | Write version sentinel
|
||||
ansible.builtin.copy:
|
||||
content: "{{ dev_env__nvim_version }}"
|
||||
dest: /etc/nvim_installed_version
|
||||
mode: "0644"
|
||||
|
||||
- name: Neovim | Remove downloaded tarball
|
||||
ansible.builtin.file:
|
||||
path: "/tmp/nvim-{{ dev_env__nvim_version }}.tar.gz"
|
||||
state: absent
|
||||
25
roles/dev_env/tasks/oh_my_posh.yml
Normal file
25
roles/dev_env/tasks/oh_my_posh.yml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
- name: Oh-my-posh | Check installed version
|
||||
ansible.builtin.command: oh-my-posh --version
|
||||
register: dev_env__omp_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Oh-my-posh | Install pinned binary
|
||||
ansible.builtin.get_url:
|
||||
url: "https://github.com/JanDeDobbeleer/oh-my-posh/releases/download/v{{ dev_env__omp_version }}/posh-linux-amd64"
|
||||
dest: /usr/local/bin/oh-my-posh
|
||||
mode: "0755"
|
||||
when: (dev_env__omp_check.stdout | default('') | trim) != dev_env__omp_version
|
||||
|
||||
- name: Oh-my-posh | Ensure theme directory
|
||||
ansible.builtin.file:
|
||||
path: /etc/oh-my-posh
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Oh-my-posh | Deploy zen.toml theme (system-wide)
|
||||
ansible.builtin.copy:
|
||||
src: oh-my-posh/zen.toml
|
||||
dest: /etc/oh-my-posh/zen.toml
|
||||
mode: "0644"
|
||||
69
roles/dev_env/tasks/per_user.yml
Normal file
69
roles/dev_env/tasks/per_user.yml
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
- name: Look up account for {{ dev_env__user }}
|
||||
ansible.builtin.getent:
|
||||
database: passwd
|
||||
key: "{{ dev_env__user }}"
|
||||
|
||||
- name: Resolve home directory for {{ dev_env__user }}
|
||||
ansible.builtin.set_fact:
|
||||
dev_env__home: "{{ getent_passwd[dev_env__user][4] }}"
|
||||
|
||||
- name: Set login shell to zsh for {{ dev_env__user }}
|
||||
ansible.builtin.user:
|
||||
name: "{{ dev_env__user }}"
|
||||
shell: /usr/bin/zsh
|
||||
tags: [users]
|
||||
|
||||
- name: Clone oh-my-zsh for {{ dev_env__user }}
|
||||
become: true
|
||||
become_user: "{{ dev_env__user }}"
|
||||
ansible.builtin.git:
|
||||
repo: https://github.com/ohmyzsh/ohmyzsh.git
|
||||
dest: "{{ dev_env__home }}/.oh-my-zsh"
|
||||
version: master
|
||||
depth: 1
|
||||
update: false
|
||||
|
||||
- name: Clone oh-my-zsh custom plugins for {{ dev_env__user }}
|
||||
become: true
|
||||
become_user: "{{ dev_env__user }}"
|
||||
ansible.builtin.git:
|
||||
repo: "{{ item.repo }}"
|
||||
dest: "{{ dev_env__home }}/.oh-my-zsh/custom/plugins/{{ item.name }}"
|
||||
version: master
|
||||
depth: 1
|
||||
update: false
|
||||
loop: "{{ dev_env__omz_custom_plugins }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
|
||||
- name: Clone tmux plugins (incl. TPM) for {{ dev_env__user }}
|
||||
become: true
|
||||
become_user: "{{ dev_env__user }}"
|
||||
ansible.builtin.git:
|
||||
repo: "{{ item.repo }}"
|
||||
dest: "{{ dev_env__home }}/.tmux/plugins/{{ item.name }}"
|
||||
version: "{{ item.version }}"
|
||||
depth: 1
|
||||
update: false
|
||||
loop: "{{ dev_env__tmux_plugins }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
|
||||
- name: Install dotfiles into ~/.dotfiles for {{ dev_env__user }}
|
||||
become: true
|
||||
become_user: "{{ dev_env__user }}"
|
||||
ansible.builtin.copy:
|
||||
src: dotfiles/
|
||||
dest: "{{ dev_env__home }}/.dotfiles/"
|
||||
mode: preserve
|
||||
tags: [config]
|
||||
|
||||
- name: Stow dotfiles into home for {{ dev_env__user }}
|
||||
become: true
|
||||
become_user: "{{ dev_env__user }}"
|
||||
ansible.builtin.command:
|
||||
cmd: "stow --no-folding -v -d {{ dev_env__home }}/.dotfiles -t {{ dev_env__home }} zsh tmux nvim"
|
||||
register: dev_env__stow
|
||||
changed_when: "'LINK:' in dev_env__stow.stderr or 'LINK:' in dev_env__stow.stdout"
|
||||
tags: [config]
|
||||
Loading…
Add table
Reference in a new issue