From f3f382ae69d27e7068e3fc30ce4ddc14cd0d07f7 Mon Sep 17 00:00:00 2001 From: sjat Date: Thu, 11 Jun 2026 13:50:11 +0200 Subject: [PATCH] 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) --- .../plans/2026-06-11-dev-env-role.md | 58 ++++++++ .../production/group_vars/control/vars.yml | 7 + playbooks/workstation.yml | 10 ++ roles/dev_env/README.md | 50 +++++++ roles/dev_env/defaults/main.yml | 51 +++++++ .../files/dotfiles/nvim/.config/nvim/init.lua | 26 ++++ .../dotfiles/nvim/.config/nvim/lazy-lock.json | 28 ++++ .../nvim/.config/nvim/lua/keymaps.lua | 78 ++++++++++ .../nvim/.config/nvim/lua/lazy-setup.lua | 61 ++++++++ .../nvim/.config/nvim/lua/options.lua | 119 +++++++++++++++ .../.config/nvim/lua/plugins/completion.lua | 72 +++++++++ .../.config/nvim/lua/plugins/filebrowser.lua | 49 ++++++ .../.config/nvim/lua/plugins/formatting.lua | 91 ++++++++++++ .../nvim/.config/nvim/lua/plugins/git.lua | 85 +++++++++++ .../.config/nvim/lua/plugins/indentguides.lua | 27 ++++ .../nvim/.config/nvim/lua/plugins/lsp.lua | 111 ++++++++++++++ .../.config/nvim/lua/plugins/markdown.lua | 22 +++ .../nvim/.config/nvim/lua/plugins/move.lua | 21 +++ .../.config/nvim/lua/plugins/navigation.lua | 12 ++ .../.config/nvim/lua/plugins/telescope.lua | 61 ++++++++ .../.config/nvim/lua/plugins/terminal.lua | 41 +++++ .../.config/nvim/lua/plugins/treesitter.lua | 60 ++++++++ .../nvim/.config/nvim/lua/plugins/ui.lua | 140 ++++++++++++++++++ roles/dev_env/files/dotfiles/tmux/.tmux.conf | 88 +++++++++++ roles/dev_env/files/dotfiles/zsh/.zshrc | 55 +++++++ roles/dev_env/files/oh-my-posh/zen.toml | 78 ++++++++++ roles/dev_env/handlers/main.yml | 1 + roles/dev_env/meta/main.yml | 13 ++ roles/dev_env/molecule/default/converge.yml | 15 ++ roles/dev_env/molecule/default/molecule.yml | 31 ++++ roles/dev_env/molecule/default/verify.yml | 73 +++++++++ roles/dev_env/requirements.yml | 4 + roles/dev_env/tasks/main.yml | 23 +++ roles/dev_env/tasks/neovim.yml | 52 +++++++ roles/dev_env/tasks/oh_my_posh.yml | 25 ++++ roles/dev_env/tasks/per_user.yml | 69 +++++++++ 36 files changed, 1807 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-11-dev-env-role.md create mode 100644 inventories/production/group_vars/control/vars.yml create mode 100644 playbooks/workstation.yml create mode 100644 roles/dev_env/README.md create mode 100644 roles/dev_env/defaults/main.yml create mode 100644 roles/dev_env/files/dotfiles/nvim/.config/nvim/init.lua create mode 100644 roles/dev_env/files/dotfiles/nvim/.config/nvim/lazy-lock.json create mode 100644 roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/keymaps.lua create mode 100644 roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/lazy-setup.lua create mode 100644 roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/options.lua create mode 100644 roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/completion.lua create mode 100644 roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/filebrowser.lua create mode 100644 roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/formatting.lua create mode 100644 roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/git.lua create mode 100644 roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/indentguides.lua create mode 100644 roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/lsp.lua create mode 100644 roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/markdown.lua create mode 100644 roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/move.lua create mode 100644 roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/navigation.lua create mode 100644 roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/telescope.lua create mode 100644 roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/terminal.lua create mode 100644 roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/treesitter.lua create mode 100644 roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/ui.lua create mode 100644 roles/dev_env/files/dotfiles/tmux/.tmux.conf create mode 100644 roles/dev_env/files/dotfiles/zsh/.zshrc create mode 100644 roles/dev_env/files/oh-my-posh/zen.toml create mode 100644 roles/dev_env/handlers/main.yml create mode 100644 roles/dev_env/meta/main.yml create mode 100644 roles/dev_env/molecule/default/converge.yml create mode 100644 roles/dev_env/molecule/default/molecule.yml create mode 100644 roles/dev_env/molecule/default/verify.yml create mode 100644 roles/dev_env/requirements.yml create mode 100644 roles/dev_env/tasks/main.yml create mode 100644 roles/dev_env/tasks/neovim.yml create mode 100644 roles/dev_env/tasks/oh_my_posh.yml create mode 100644 roles/dev_env/tasks/per_user.yml diff --git a/docs/superpowers/plans/2026-06-11-dev-env-role.md b/docs/superpowers/plans/2026-06-11-dev-env-role.md new file mode 100644 index 0000000..49a2f44 --- /dev/null +++ b/docs/superpowers/plans/2026-06-11-dev-env-role.md @@ -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`). diff --git a/inventories/production/group_vars/control/vars.yml b/inventories/production/group_vars/control/vars.yml new file mode 100644 index 0000000..eead269 --- /dev/null +++ b/inventories/production/group_vars/control/vars.yml @@ -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 diff --git a/playbooks/workstation.yml b/playbooks/workstation.yml new file mode 100644 index 0000000..071b7e0 --- /dev/null +++ b/playbooks/workstation.yml @@ -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] diff --git a/roles/dev_env/README.md b/roles/dev_env/README.md new file mode 100644 index 0000000..070147c --- /dev/null +++ b/roles/dev_env/README.md @@ -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). diff --git a/roles/dev_env/defaults/main.yml b/roles/dev_env/defaults/main.yml new file mode 100644 index 0000000..70f2e97 --- /dev/null +++ b/roles/dev_env/defaults/main.yml @@ -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 diff --git a/roles/dev_env/files/dotfiles/nvim/.config/nvim/init.lua b/roles/dev_env/files/dotfiles/nvim/.config/nvim/init.lua new file mode 100644 index 0000000..d9fbfdc --- /dev/null +++ b/roles/dev_env/files/dotfiles/nvim/.config/nvim/init.lua @@ -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") diff --git a/roles/dev_env/files/dotfiles/nvim/.config/nvim/lazy-lock.json b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lazy-lock.json new file mode 100644 index 0000000..03a44fd --- /dev/null +++ b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lazy-lock.json @@ -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" } +} diff --git a/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/keymaps.lua b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/keymaps.lua new file mode 100644 index 0000000..3fb0388 --- /dev/null +++ b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/keymaps.lua @@ -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', '', ':w', opts) +keymap('i', '', ':wa', opts) + +-- Leader shortcuts +keymap('n', 'w', ':w', opts) -- Save +keymap('n', 'q', ':q', opts) -- Quit +keymap('n', 'e', ':Ex', opts) -- File explorer (netrw) + +-- Clear search highlighting +keymap('n', '', ':nohlsearch', opts) + +-- Better indenting (keep selection in visual mode) +keymap('v', '<', '', '>gv', opts) + +-- Move text up and down in visual mode +keymap('v', 'J', ":m '>+1gv=gv", opts) +keymap('v', 'K', ":m '<-2gv=gv", opts) + +-- Keep cursor centered when scrolling +keymap('n', '', 'zz', opts) +keymap('n', '', 'zz', opts) +keymap('n', 'n', 'nzzzv', opts) +keymap('n', 'N', 'Nzzzv', opts) + +-- Buffer cycling (inner layer — mirrors tmux window cycling) +keymap('n', '', ':bnext', opts) +keymap('n', '', ':bnext', opts) +keymap('n', '', ':bprevious', opts) + +-- Better paste (don't yank replaced text in visual mode) +keymap('x', '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', 'y', '"+y', { noremap = true, silent = true, desc = 'Yank to system clipboard' }) +keymap('v', 'y', '"+y', { noremap = true, silent = true, desc = 'Yank to system clipboard' }) +keymap('n', 'Y', '"+Y', { noremap = true, silent = true, desc = 'Yank line to system clipboard' }) + +-- Delete to black hole register +keymap('n', 'd', '"_d', opts) +keymap('v', 'd', '"_d', opts) + +-- Window management — arrow direction = where new pane opens (mirrors kitty and tmux) +keymap('n', '', ':vsplit', opts) +keymap('n', '', ':split', opts) +keymap('n', 'sx', ':close', opts) +keymap('n', 'sv', ':vsplit', opts) +keymap('n', 'sh', ':split', opts) + +-- Buffer navigation +keymap('n', 'bn', ':bnext', opts) -- Next buffer +keymap('n', 'bp', ':bprevious', opts) -- Previous buffer +keymap('n', 'bd', ':bdelete', opts) -- Delete buffer + +-- Quick fix list navigation +keymap('n', 'j', ':cnext', opts) +keymap('n', 'k', ':cprev', opts) + +-- Diagnostic keymaps (will be overridden by LSP if active) +keymap('n', '', vim.diagnostic.goto_prev, opts) -- Alt+k = up = prev diagnostic +keymap('n', '', vim.diagnostic.goto_next, opts) -- Alt+j = down = next diagnostic +keymap('n', 'dl', vim.diagnostic.setloclist, opts) +keymap('n', 'df', vim.diagnostic.open_float, opts) diff --git a/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/lazy-setup.lua b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/lazy-setup.lua new file mode 100644 index 0000000..e7ad5b5 --- /dev/null +++ b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/lazy-setup.lua @@ -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 = "💤 ", + }, + }, +}) diff --git a/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/options.lua b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/options.lua new file mode 100644 index 0000000..725c0f6 --- /dev/null +++ b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/options.lua @@ -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 diff --git a/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/completion.lua b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/completion.lua new file mode 100644 index 0000000..fbbb28e --- /dev/null +++ b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/completion.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({ + [""] = cmp.mapping.select_next_item(), + [""] = cmp.mapping.select_prev_item(), + [""] = cmp.mapping.scroll_docs(-4), + [""] = cmp.mapping.scroll_docs(4), + [""] = cmp.mapping.abort(), + [""] = cmp.mapping.confirm({ select = false }), + [""] = 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" }), + [""] = 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, +} diff --git a/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/filebrowser.lua b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/filebrowser.lua new file mode 100644 index 0000000..2e0e6d2 --- /dev/null +++ b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/filebrowser.lua @@ -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 = { + { "ee", "Neotree toggle", desc = "Toggle file browser" }, + { "ef", "Neotree focus", desc = "Focus file browser" }, + { "eg", "Neotree git_status", desc = "Git status tree" }, + }, + config = function() + require("neo-tree").setup({ + close_if_last_window = true, + window = { + width = 30, + mappings = { + [""] = "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, +} diff --git a/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/formatting.lua b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/formatting.lua new file mode 100644 index 0000000..f7a702a --- /dev/null +++ b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/formatting.lua @@ -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" }, "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, +} diff --git a/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/git.lua b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/git.lua new file mode 100644 index 0000000..e680779 --- /dev/null +++ b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/git.lua @@ -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', 'hj', function() + if vim.wo.diff then return ']c' end + vim.schedule(function() gs.next_hunk() end) + return '' + end, {expr=true, desc="Next git hunk"}) + + map('n', 'hk', function() + if vim.wo.diff then return '[c' end + vim.schedule(function() gs.prev_hunk() end) + return '' + end, {expr=true, desc="Previous git hunk"}) + + -- Actions + map('n', 'hs', gs.stage_hunk, {desc="Stage hunk"}) + map('n', 'hr', gs.reset_hunk, {desc="Reset hunk"}) + map('v', 'hs', function() gs.stage_hunk {vim.fn.line('.'), vim.fn.line('v')} end, {desc="Stage hunk"}) + map('v', 'hr', function() gs.reset_hunk {vim.fn.line('.'), vim.fn.line('v')} end, {desc="Reset hunk"}) + map('n', 'hS', gs.stage_buffer, {desc="Stage buffer"}) + map('n', 'hu', gs.undo_stage_hunk, {desc="Undo stage hunk"}) + map('n', 'hR', gs.reset_buffer, {desc="Reset buffer"}) + map('n', 'hp', gs.preview_hunk, {desc="Preview hunk"}) + map('n', 'hb', function() gs.blame_line{full=true} end, {desc="Blame line"}) + map('n', 'tb', gs.toggle_current_line_blame, {desc="Toggle line blame"}) + map('n', 'hd', gs.diffthis, {desc="Diff this"}) + map('n', 'hD', function() gs.diffthis('~') end, {desc="Diff this ~"}) + map('n', 'td', gs.toggle_deleted, {desc="Toggle deleted"}) + + -- Text object + map({'o', 'x'}, 'ih', ':Gitsigns select_hunk', {desc="Select hunk"}) + end, + }) + end, +} diff --git a/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/indentguides.lua b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/indentguides.lua new file mode 100644 index 0000000..acb89e9 --- /dev/null +++ b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/indentguides.lua @@ -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, +} diff --git a/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/lsp.lua b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/lsp.lua new file mode 100644 index 0000000..55e93da --- /dev/null +++ b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/lsp.lua @@ -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', 'K', vim.lsp.buf.signature_help, opts) + vim.keymap.set('n', 'rn', vim.lsp.buf.rename, opts) + vim.keymap.set('n', 'ca', vim.lsp.buf.code_action, opts) + vim.keymap.set('n', 'gr', vim.lsp.buf.references, opts) + vim.keymap.set('n', '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, + }, +} diff --git a/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/markdown.lua b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/markdown.lua new file mode 100644 index 0000000..cb39624 --- /dev/null +++ b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/markdown.lua @@ -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 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", "o", "MDListItemBelow", "Insert list item below") + map("n", "O", "MDListItemAbove", "Insert list item above") + map("n", "ch", "MDTaskToggle", "Toggle checkbox") + -- n (not rn): the LSP's on_attach binds rn to + -- vim.lsp.buf.rename buffer-local and overrides ours. + map("n", "n", "%MDResetListNumbering", "Renumber ordered list") + map("x", "ch", ":MDTaskToggle", "Toggle checkbox (range)") + end, + }, +} diff --git a/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/move.lua b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/move.lua new file mode 100644 index 0000000..3bb9339 --- /dev/null +++ b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/move.lua @@ -0,0 +1,21 @@ +-- echasnovski/mini.move — move lines/selections with Alt+arrows in any +-- filetype. Uses arrow keys to avoid clashing with existing / +-- diagnostic mappings in keymaps.lua. +return { + "echasnovski/mini.move", + event = "VeryLazy", + opts = { + mappings = { + -- visual selection + left = "", + right = "", + down = "", + up = "", + -- current line in normal mode + line_left = "", + line_right = "", + line_down = "", + line_up = "", + }, + }, +} diff --git a/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/navigation.lua b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/navigation.lua new file mode 100644 index 0000000..c5e925f --- /dev/null +++ b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/navigation.lua @@ -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", "", nav.NvimTmuxNavigateLeft, { noremap = true, silent = true, desc = "Navigate left" }) + vim.keymap.set("n", "", nav.NvimTmuxNavigateDown, { noremap = true, silent = true, desc = "Navigate down" }) + vim.keymap.set("n", "", nav.NvimTmuxNavigateUp, { noremap = true, silent = true, desc = "Navigate up" }) + vim.keymap.set("n", "", nav.NvimTmuxNavigateRight, { noremap = true, silent = true, desc = "Navigate right" }) + end, +} diff --git a/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/telescope.lua b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/telescope.lua new file mode 100644 index 0000000..9a50234 --- /dev/null +++ b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/telescope.lua @@ -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 = { + { "ff", "Telescope find_files", desc = "Find files" }, + { "fg", "Telescope live_grep", desc = "Live grep" }, + { "fb", "Telescope buffers", desc = "Find buffers" }, + { "fh", "Telescope help_tags", desc = "Help tags" }, + { "fr", "Telescope oldfiles", desc = "Recent files" }, + { "fc", "Telescope commands", desc = "Commands" }, + { "fk", "Telescope keymaps", 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 = { + [""] = actions.move_selection_previous, + [""] = actions.move_selection_next, + [""] = actions.send_selected_to_qflist + actions.open_qflist, + [""] = actions.close, + }, + n = { + ["q"] = actions.close, + }, + }, + }, + pickers = { + find_files = { + hidden = false, + find_command = { "rg", "--files", "--hidden", "--glob", "!.git/*" }, + }, + }, + }) + end, +} diff --git a/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/terminal.lua b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/terminal.lua new file mode 100644 index 0000000..df81468 --- /dev/null +++ b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/terminal.lua @@ -0,0 +1,41 @@ +-- Terminal management (toggleterm.nvim) +-- tt floating shell +-- gg lazygit +return { + "akinsho/toggleterm.nvim", + version = "*", + keys = { + { "tt", desc = "Toggle floating terminal" }, + { "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", "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", "gg", function() + lazygit:toggle() + end, { desc = "Open lazygit" }) + end, +} diff --git a/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/treesitter.lua b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/treesitter.lua new file mode 100644 index 0000000..2f29ce0 --- /dev/null +++ b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/treesitter.lua @@ -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 = "", + node_incremental = "", + scope_incremental = false, + node_decremental = "", + }, + }, + }) + end, +} diff --git a/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/ui.lua b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/ui.lua new file mode 100644 index 0000000..ee5fe6a --- /dev/null +++ b/roles/dev_env/files/dotfiles/nvim/.config/nvim/lua/plugins/ui.lua @@ -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 = { { "z", "ZenMode", 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({ + { "f", group = "Find (Telescope)" }, + { "h", group = "Git Hunks" }, + { "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 = "", + 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, + }, +} diff --git a/roles/dev_env/files/dotfiles/tmux/.tmux.conf b/roles/dev_env/files/dotfiles/tmux/.tmux.conf new file mode 100644 index 0000000..91e2915 --- /dev/null +++ b/roles/dev_env/files/dotfiles/tmux/.tmux.conf @@ -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' diff --git a/roles/dev_env/files/dotfiles/zsh/.zshrc b/roles/dev_env/files/dotfiles/zsh/.zshrc new file mode 100644 index 0000000..51566ff --- /dev/null +++ b/roles/dev_env/files/dotfiles/zsh/.zshrc @@ -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)" diff --git a/roles/dev_env/files/oh-my-posh/zen.toml b/roles/dev_env/files/oh-my-posh/zen.toml new file mode 100644 index 0000000..5122d81 --- /dev/null +++ b/roles/dev_env/files/oh-my-posh/zen.toml @@ -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 }} {{ 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 = '❯❯ ' diff --git a/roles/dev_env/handlers/main.yml b/roles/dev_env/handlers/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/dev_env/handlers/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/dev_env/meta/main.yml b/roles/dev_env/meta/main.yml new file mode 100644 index 0000000..ea072fb --- /dev/null +++ b/roles/dev_env/meta/main.yml @@ -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: [] diff --git a/roles/dev_env/molecule/default/converge.yml b/roles/dev_env/molecule/default/converge.yml new file mode 100644 index 0000000..cf2227a --- /dev/null +++ b/roles/dev_env/molecule/default/converge.yml @@ -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 diff --git a/roles/dev_env/molecule/default/molecule.yml b/roles/dev_env/molecule/default/molecule.yml new file mode 100644 index 0000000..b23d8da --- /dev/null +++ b/roles/dev_env/molecule/default/molecule.yml @@ -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 diff --git a/roles/dev_env/molecule/default/verify.yml b/roles/dev_env/molecule/default/verify.yml new file mode 100644 index 0000000..b2a6600 --- /dev/null +++ b/roles/dev_env/molecule/default/verify.yml @@ -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 diff --git a/roles/dev_env/requirements.yml b/roles/dev_env/requirements.yml new file mode 100644 index 0000000..93f0b4f --- /dev/null +++ b/roles/dev_env/requirements.yml @@ -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: [] diff --git a/roles/dev_env/tasks/main.yml b/roles/dev_env/tasks/main.yml new file mode 100644 index 0000000..1d11bf0 --- /dev/null +++ b/roles/dev_env/tasks/main.yml @@ -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 }}" diff --git a/roles/dev_env/tasks/neovim.yml b/roles/dev_env/tasks/neovim.yml new file mode 100644 index 0000000..ab44ffe --- /dev/null +++ b/roles/dev_env/tasks/neovim.yml @@ -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 diff --git a/roles/dev_env/tasks/oh_my_posh.yml b/roles/dev_env/tasks/oh_my_posh.yml new file mode 100644 index 0000000..99b3f13 --- /dev/null +++ b/roles/dev_env/tasks/oh_my_posh.yml @@ -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" diff --git a/roles/dev_env/tasks/per_user.yml b/roles/dev_env/tasks/per_user.yml new file mode 100644 index 0000000..6a69705 --- /dev/null +++ b/roles/dev_env/tasks/per_user.yml @@ -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]