v1.7: stateful home with git-backed snapshot/rollback

This commit is contained in:
Wayne Hayes 2026-04-27 11:41:59 -04:00
commit f61e99a2eb
19 changed files with 1880 additions and 0 deletions

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# ── arch-dev repo .gitignore ──────────────────────────────────────────────────
# Docker build artifacts
*.tar.gz
*.tar
# Environment / secrets
.env
.env.*
!.env.example
# Editor
.DS_Store
*.swp
*.swo
*~
# Python
__pycache__/
*.pyc
*.pyo
# Workspace contents (bind mount — user's project files, not ours)
workspace/*
!workspace/.gitkeep

89
Dockerfile Normal file
View File

@ -0,0 +1,89 @@
FROM archlinux:latest
# ── Rolling release: full system update first, always ─────────────────────────
RUN pacman -Syu --noconfirm
# ── Crown Jewel #1: pacman ────────────────────────────────────────────────────
RUN pacman -S --noconfirm --needed \
base-devel git curl wget unzip zip \
zsh tmux screen mosh \
zsh-syntax-highlighting zsh-autosuggestions zsh-history-substring-search \
zsh-completions \
neovim \
starship \
python python-pip python-pynvim \
perl \
pyright \
bash-language-server \
python-black ruff shellcheck shfmt \
python-pylint \
ripgrep fd bat eza fzf zoxide \
git-delta lazygit \
btop \
ttf-nerd-fonts-symbols ttf-jetbrains-mono-nerd \
man-db man-pages \
jq tree wget \
rsync \
&& pacman -Scc --noconfirm
# ── Crown Jewel #2: AUR ───────────────────────────────────────────────────────
RUN useradd -m -s /bin/zsh -G wheel aurbuild && \
echo 'aurbuild ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers.d/aurbuild
RUN cd /tmp && \
git clone --depth=1 https://aur.archlinux.org/yay-bin.git && \
chown -R aurbuild:aurbuild yay-bin && \
cd yay-bin && \
sudo -u aurbuild makepkg -si --noconfirm && \
cd / && rm -rf /tmp/yay-bin
RUN sudo -u aurbuild yay -S --noconfirm --needed \
eza \
wl-clipboard \
trash-cli \
&& sudo -u aurbuild yay -Scc --noconfirm
# ── Dev user ──────────────────────────────────────────────────────────────────
RUN useradd -m -s /bin/zsh -G wheel dev && \
echo 'dev ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers.d/dev
# ── Skeleton: bake dotfiles into /etc/skel-arch-dev/ ──────────────────────────
# This is the SOURCE OF TRUTH. The volume gets seeded from here on first run.
COPY --chown=dev:dev dotfiles/ /etc/skel-arch-dev/
# ── Initial seed of /home/dev so plugin bake works at build time ──────────────
RUN cp -an /etc/skel-arch-dev/. /home/dev/ && \
chown -R dev:dev /home/dev
# ── Python tools ──────────────────────────────────────────────────────────────
RUN pip install --break-system-packages pynvim httpx requests
# ── Bake neovim plugins into /etc/skel-arch-dev so they seed too ─────────────
RUN sudo -u dev HOME=/home/dev XDG_DATA_HOME=/home/dev/.local/share \
nvim --headless +"Lazy! sync" +qa 2>/dev/null; exit 0
RUN sudo -u dev HOME=/home/dev XDG_DATA_HOME=/home/dev/.local/share \
nvim --headless \
+"TSUpdateSync python bash lua json yaml toml markdown vim vimdoc regex" \
+qa 2>/dev/null; exit 0
# Copy the fully-baked /home/dev back into the skel template
RUN cp -an /home/dev/.local /etc/skel-arch-dev/ && \
cp -an /home/dev/.cache /etc/skel-arch-dev/ 2>/dev/null || true && \
chown -R dev:dev /etc/skel-arch-dev
# ── Cleanup AUR build user ────────────────────────────────────────────────────
RUN userdel -r aurbuild && rm -f /etc/sudoers.d/aurbuild
# ── Entrypoint script ─────────────────────────────────────────────────────────
COPY entrypoint.sh /usr/local/bin/arch-dev-entrypoint
RUN chmod +x /usr/local/bin/arch-dev-entrypoint
# ── Final permissions ─────────────────────────────────────────────────────────
RUN chown -R dev:dev /home/dev
USER dev
WORKDIR /workspace
ENTRYPOINT ["/usr/local/bin/arch-dev-entrypoint"]
CMD ["/bin/zsh"]

118
README.md Normal file
View File

@ -0,0 +1,118 @@
# arch-dev v1.7
### Riced Neovim IDE · Arch Linux · Stateful · Mobile-Aware
> *"Like Gentoo without the compiling."*
> Kanagawa Wave · rolling release · AUR-powered · git-snapshotted home
---
## What's new in v1.7
- **Stateful `/home/dev`** — your shell history, plugin state, and config tweaks survive container exit
- **Git-backed snapshot system** — commit good states, roll back when things break
- **Auto-snapshot on dotfile updates** — image upgrades preserve your history
- **Mobile improvements** — bufferline disabled on mobile, dashboard uses thin-line ASCII
---
## The Snapshot System
Your `/home/dev` is a git repo (state in `~/.arch-dev-state/`). When something works, snapshot it. When something breaks, roll back.
```bash
# Working state — save it
snap node-working "NodeJS env with nvm + pnpm"
# List your snapshots
snaps
# See what's changed since last snapshot
snapd
# Try something risky, breaks things... no problem
snapr node-working
# Show what's in a snapshot
show-snapshot node-working
# Remove a snapshot you don't need
unsnapshot old-thing
```
What's tracked: dotfiles, configs, neovim plugins, anything in `~`.
What's ignored: `.cache`, `.zsh_history`, build artifacts, log files.
Snapshots use real git so you can:
- Branch (`_archdev_git checkout -b experimenting`)
- Push to remote (`_archdev_git remote add origin git@...`)
- Diff between snapshots (`_archdev_git diff snap-a snap-b`)
---
## Quick Start
```bash
docker compose build
docker compose run --rm arch-dev
```
First run seeds the home volume from a baked-in skeleton and creates a `skeleton` tag you can always roll back to.
---
## Volume Architecture
| Path | Type | Purpose |
|---|---|---|
| `/workspace` | bind mount → `./workspace` | Project files, host-visible |
| `/home/dev` | named volume `arch-dev-home` | Stateful user home, survives `--rm` |
| `/etc/skel-arch-dev/` | image layer | Read-only template, used to seed and update |
**Reset home to factory:**
```bash
docker volume rm arch-dev_arch-dev-home
docker compose run --rm arch-dev
```
**Or roll back inside the container:**
```bash
rollback skeleton
```
---
## Image Updates
When you rebuild the image with new dotfiles:
1. Container starts, entrypoint compares image dotfiles to home
2. If image is newer, takes an auto-snapshot of current home
3. Updates dotfiles, leaving user data alone (history, project state)
4. You can `rollback` to the auto-snapshot if you don't like the new dotfiles
---
## Known Caveats
- **AUR/pacman packages** are NOT in snapshots (they live in `/usr/`, not `~`)
- **Docker volume size** grows with snapshots — `_archdev_git gc` periodically
- First `snap` after major changes can take a few seconds (lots to hash)
---
## Mobile (Termius)
`MOBILE=1` activates:
- Minimal starship prompt
- Auto-attach screen on connect
- Bufferline disabled (was showing as purple bar)
- Habamax colorscheme (kanagawa needs truecolor which Termius mangles)
---
## v2 Roadmap
- nvm + nodejs (snapshot it after install!)
- gemini-cli for AI in the terminal
- copilot.nvim or avante.nvim
- nvim-dap (debugger)

32
docker-compose.yml Normal file
View File

@ -0,0 +1,32 @@
services:
arch-dev:
build: .
image: arch-dev:latest
container_name: arch-dev
hostname: arch-dev
stdin_open: true
tty: true
volumes:
# Project files — bind mount, host-visible
- ./workspace:/workspace
# Stateful home — named volume, survives --rm
# Reset with: docker volume rm arch-dev_arch-dev-home
- arch-dev-home:/home/dev
environment:
- TERM=xterm-256color
- MOBILE=${MOBILE:-0}
- GIT_AUTHOR_NAME=${GIT_NAME:-dev}
- GIT_AUTHOR_EMAIL=${GIT_EMAIL:-dev@localhost}
- GIT_COMMITTER_NAME=${GIT_NAME:-dev}
- GIT_COMMITTER_EMAIL=${GIT_EMAIL:-dev@localhost}
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
volumes:
arch-dev-home:

89
dotfiles/.aliases Normal file
View File

@ -0,0 +1,89 @@
# ╔══════════════════════════════════════════════════════════════════╗
# ║ arch-dev :: aliases ║
# ╚══════════════════════════════════════════════════════════════════╝
# ── Navigation ────────────────────────────────────────────────────────────────
alias ..='cd ..'
alias ...='cd ../..'
alias ....='cd ../../..'
alias ~='cd ~'
alias -- -='cd -'
# ── ls / eza ──────────────────────────────────────────────────────────────────
alias ls='eza --icons --group-directories-first --color=always'
alias ll='eza -la --icons --git --group-directories-first --color=always'
alias lt='eza --tree --icons --level=2 --color=always'
alias ltt='eza --tree --icons --level=3 --color=always'
# ── bat (better cat) ──────────────────────────────────────────────────────────
alias cat='bat --style=plain --paging=never --color=never'
alias catn='bat --style=numbers --paging=never --color=never'
alias catp='bat --style=full --paging=never'
# ── Editor ────────────────────────────────────────────────────────────────────
alias v='nvim'
alias vi='nvim'
alias vim='nvim'
alias vdiff='nvim -d'
# ── Git (tearless mobile ref bindings) ────────────────────────────────────────
alias g='git'
alias gs='git status'
alias gl='git log --oneline --graph --decorate --color'
alias glo='git log --oneline -20'
alias gd='git diff'
alias gds='git diff --staged'
alias ga='git add'
alias gaa='git add -A'
alias gc='git commit'
alias gcm='git commit -m'
alias gp='git push'
alias gpl='git pull'
alias gco='git checkout'
alias gcb='git checkout -b'
alias gst='git stash'
alias gstp='git stash pop'
alias lg='lazygit'
# ── Shell utils ───────────────────────────────────────────────────────────────
alias grep='grep --color=auto'
alias rg='ripgrep'
alias src='source ~/.zshrc && echo "↻ zshrc reloaded"'
alias cls='clear'
alias path='echo $PATH | tr ":" "\n"'
# ── System (btop > top) ───────────────────────────────────────────────────────
alias top='btop'
alias df='df -h'
alias du='du -h'
alias free='free -h'
# ── Safety nets ───────────────────────────────────────────────────────────────
# trash-cli installed from AUR — rm is still available for scripts
alias rm='echo "Use trash or /bin/rm"; false'
alias tp='trash-put'
alias tl='trash-list'
# ── Python ────────────────────────────────────────────────────────────────────
alias py='python'
alias py3='python3'
alias pip='pip3'
alias venv='python -m venv'
alias va='source .venv/bin/activate'
alias vd='deactivate'
# ── Perl ──────────────────────────────────────────────────────────────────────
alias pl='perl'
alias plcheck='perl -wc' # syntax check a file
alias plcritic='perlcritic'
# ── arch-dev meta ─────────────────────────────────────────────────────────────
alias pacman-list='pacman -Qqe' # list explicitly installed
alias pacman-orphans='pacman -Qtdq' # list orphaned packages
alias aur-update='yay -Syu --aur'
# ── Snapshot shortcuts ────────────────────────────────────────────────────────
alias snap='snapshot'
alias snaps='snapshots'
alias snapd='diff-state'
alias snapr='rollback'

View File

@ -0,0 +1,8 @@
-- ╔══════════════════════════════════════════════════════════════════╗
-- ║ arch-dev :: neovim ║
-- ║ Kanagawa Wave · lazy.nvim · LSP/lint/format · riced IDE ║
-- ╚══════════════════════════════════════════════════════════════════╝
require("options")
require("keymaps")
require("plugins")

View File

@ -0,0 +1,103 @@
-- ── keymaps.lua ───────────────────────────────────────────────────────────────
local map = vim.keymap.set
local opts = { silent = true }
-- ── Escape (tearless mobile ref: jj) ─────────────────────────────────────────
map("i", "jj", "<Esc>", { desc = "Escape insert mode" })
map("i", "jk", "<Esc>", { desc = "Escape insert mode" })
-- ── File ops (tearless mobile ref) ───────────────────────────────────────────
map("n", "<leader>w", "<cmd>w<cr>", { desc = "Save" })
map("n", "<leader>q", "<cmd>q<cr>", { desc = "Quit" })
map("n", "<leader>x", "<cmd>wq<cr>", { desc = "Save + quit" })
map("n", "<leader>Q", "<cmd>qa!<cr>",{ desc = "Force quit all" })
-- ── Clear search highlight ────────────────────────────────────────────────────
map("n", "<Esc>", "<cmd>noh<cr>", opts)
-- ── Better movement ───────────────────────────────────────────────────────────
map("n", "j", "gj", opts) -- visual line movement
map("n", "k", "gk", opts)
map("n", "H", "^", opts) -- start/end of line
map("n", "L", "$", opts)
map("n", "<C-d>", "<C-d>zz", opts) -- keep cursor centred on scroll
map("n", "<C-u>", "<C-u>zz", opts)
map("n", "n", "nzzzv", opts) -- keep cursor centred on search
map("n", "N", "Nzzzv", opts)
-- ── Window navigation (matches tmux bindings) ─────────────────────────────────
map("n", "<C-h>", "<C-w>h", opts)
map("n", "<C-j>", "<C-w>j", opts)
map("n", "<C-k>", "<C-w>k", opts)
map("n", "<C-l>", "<C-w>l", opts)
-- ── Window resize ─────────────────────────────────────────────────────────────
map("n", "<C-Up>", "<cmd>resize +2<cr>", opts)
map("n", "<C-Down>", "<cmd>resize -2<cr>", opts)
map("n", "<C-Left>", "<cmd>vertical resize -2<cr>", opts)
map("n", "<C-Right>", "<cmd>vertical resize +2<cr>", opts)
-- ── Buffer navigation ─────────────────────────────────────────────────────────
map("n", "<S-h>", "<cmd>bprev<cr>", opts)
map("n", "<S-l>", "<cmd>bnext<cr>", opts)
map("n", "<leader>bd", "<cmd>bdelete<cr>", { desc = "Delete buffer" })
-- ── Indenting keeps selection (tearless mobile ref) ──────────────────────────
map("v", "<", "<gv", opts)
map("v", ">", ">gv", opts)
-- ── Move lines ────────────────────────────────────────────────────────────────
map("v", "J", ":m '>+1<cr>gv=gv", opts)
map("v", "K", ":m '<-2<cr>gv=gv", opts)
-- ── Paste without losing register ─────────────────────────────────────────────
map("v", "p", '"_dP', opts)
-- ── File explorer (tearless mobile ref: Ctrl+n) ───────────────────────────────
map("n", "<C-n>", "<cmd>Oil<cr>", { desc = "File explorer (oil)" })
map("n", "<leader>e", "<cmd>Oil<cr>", { desc = "File explorer (oil)" })
-- ── Telescope (tearless mobile ref) ──────────────────────────────────────────
map("n", "<leader>ff", "<cmd>Telescope find_files<cr>", { desc = "Find files" })
map("n", "<leader>fg", "<cmd>Telescope live_grep<cr>", { desc = "Live grep" })
map("n", "<leader>fb", "<cmd>Telescope buffers<cr>", { desc = "Buffers" })
map("n", "<leader>fh", "<cmd>Telescope help_tags<cr>", { desc = "Help tags" })
map("n", "<leader>fd", "<cmd>Telescope diagnostics<cr>", { desc = "Diagnostics" })
map("n", "<leader>fr", "<cmd>Telescope oldfiles<cr>", { desc = "Recent files" })
map("n", "<leader>fs", "<cmd>Telescope lsp_document_symbols<cr>",{ desc = "Symbols" })
-- ── LSP (set globally; lsp.lua on_attach adds buffer-local too) ───────────────
map("n", "gd", vim.lsp.buf.definition, { desc = "Go to definition" })
map("n", "gD", vim.lsp.buf.declaration, { desc = "Go to declaration" })
map("n", "gr", "<cmd>Telescope lsp_references<cr>",{ desc = "References" })
map("n", "gi", vim.lsp.buf.implementation, { desc = "Implementation" })
map("n", "K", vim.lsp.buf.hover, { desc = "Hover docs" })
map("n", "<C-k>", vim.lsp.buf.signature_help, { desc = "Signature help" })
map("n", "<leader>rn", vim.lsp.buf.rename, { desc = "Rename symbol" })
map("n", "<leader>ca", vim.lsp.buf.code_action, { desc = "Code action" })
map("n", "<leader>f", function()
vim.lsp.buf.format({ async = true })
end, { desc = "Format buffer" })
-- ── Diagnostics ───────────────────────────────────────────────────────────────
map("n", "[d", vim.diagnostic.goto_prev, { desc = "Prev diagnostic" })
map("n", "]d", vim.diagnostic.goto_next, { desc = "Next diagnostic" })
map("n", "<leader>dl", vim.diagnostic.open_float, { desc = "Line diagnostics" })
-- ── Leap (info only — sets its own mappings) ─────────────────────────────────
-- s → leap (n/x/o)
-- S → leap-from-window (n)
-- Surround defaults (v4): ys{motion}{char}, ds{char}, cs{old}{new}
-- ── LazyGit ───────────────────────────────────────────────────────────────────
map("n", "<leader>g", "<cmd>LazyGit<cr>", { desc = "LazyGit" })
-- ── Which-key group labels ────────────────────────────────────────────────────
local ok, wk = pcall(require, "which-key")
if ok then
wk.add({
{ "<leader>f", group = "find / telescope" },
{ "<leader>d", group = "diagnostics" },
{ "<leader>b", group = "buffer" },
})
end

View File

@ -0,0 +1,91 @@
-- ── options.lua ───────────────────────────────────────────────────────────────
local o = vim.opt
local g = vim.g
-- Leader (set before plugins load)
g.mapleader = " "
g.maplocalleader = "\\"
-- ── Appearance ────────────────────────────────────────────────────────────────
-- termguicolors emits RGB escape codes. Termius on mobile renders these
-- as purple/magenta backgrounds. Disable when MOBILE=1.
o.termguicolors = (vim.env.MOBILE ~= "1")
o.number = true
o.relativenumber = true
o.cursorline = true
o.cursorcolumn = false
o.signcolumn = "yes:1"
o.scrolloff = 8
o.sidescrolloff = 8
o.wrap = false
o.showmode = false -- lualine handles this
o.showcmd = false
o.cmdheight = 1
o.pumheight = 12 -- max completion menu items
o.conceallevel = 0
-- ── Editing ───────────────────────────────────────────────────────────────────
o.expandtab = true
o.shiftwidth = 4
o.tabstop = 4
o.softtabstop = 4
o.smartindent = true
o.autoindent = true
o.breakindent = true
-- ── Search ────────────────────────────────────────────────────────────────────
o.ignorecase = true
o.smartcase = true
o.hlsearch = true
o.incsearch = true
-- ── Files ─────────────────────────────────────────────────────────────────────
o.undofile = true
o.undodir = vim.fn.stdpath("data") .. "/undo"
o.backup = false
o.swapfile = false
o.autoread = true
-- ── Behavior ──────────────────────────────────────────────────────────────────
o.clipboard = "unnamedplus"
o.updatetime = 250
o.timeoutlen = 300
o.splitright = true
o.splitbelow = true
o.completeopt = "menu,menuone,noselect"
o.mouse = "a"
o.virtualedit = "block"
-- ── Folding (treesitter) ──────────────────────────────────────────────────────
o.foldmethod = "expr"
o.foldexpr = "nvim_treesitter#foldexpr()"
o.foldenable = false -- open by default, fold manually
-- ── Netrw (disabled — using oil.nvim) ────────────────────────────────────────
g.loaded_netrw = 1
g.loaded_netrwPlugin = 1
-- ── Per-filetype tweaks ───────────────────────────────────────────────────────
vim.api.nvim_create_autocmd("FileType", {
pattern = { "lua", "json", "yaml", "toml" },
callback = function()
vim.opt_local.shiftwidth = 2
vim.opt_local.tabstop = 2
end,
})
-- Perl: use perltidy indenting convention
vim.api.nvim_create_autocmd("FileType", {
pattern = { "perl" },
callback = function()
vim.opt_local.shiftwidth = 4
vim.opt_local.tabstop = 4
end,
})
-- ── Highlight on yank ─────────────────────────────────────────────────────────
vim.api.nvim_create_autocmd("TextYankPost", {
callback = function()
vim.highlight.on_yank({ higroup = "IncSearch", timeout = 150 })
end,
})

View File

@ -0,0 +1,48 @@
-- ── plugins.lua — lazy.nvim bootstrap ────────────────────────────────────────
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)
require("lazy").setup({
{ import = "plugins.ui" },
{ import = "plugins.lsp" },
{ import = "plugins.treesitter" },
{ import = "plugins.tools" },
}, {
-- Baked into the image — don't check for updates at runtime
checker = { enabled = false },
change_detection = { enabled = false, notify = false },
-- Performance
performance = {
rtp = {
disabled_plugins = {
"gzip", "matchit", "matchparen",
"netrwPlugin", "tarPlugin", "tohtml",
"tutor", "zipPlugin",
},
},
},
ui = {
border = "rounded",
-- Kanagawa Wave colors in lazy UI
icons = {
cmd = "",
config = " ",
event = " ",
ft = " ",
init = " ",
plugin = " ",
source = " ",
start = "",
task = "",
},
},
})

View File

@ -0,0 +1,269 @@
-- ── plugins/lsp.lua ───────────────────────────────────────────────────────────
-- nvim 0.11+ native LSP API: vim.lsp.config / vim.lsp.enable
-- lspconfig still used as a config data source (server cmd/root detection)
-- but we drive it through vim.lsp.config, not lspconfig.setup()
-- No vim.lsp.with() — border config goes through vim.lsp.config globals
return {
-- ── LSP ───────────────────────────────────────────────────────────────────
{
"neovim/nvim-lspconfig",
dependencies = {
"hrsh7th/nvim-cmp",
"hrsh7th/cmp-nvim-lsp",
"hrsh7th/cmp-buffer",
"hrsh7th/cmp-path",
"hrsh7th/cmp-cmdline",
"L3MON4D3/LuaSnip",
"saadparwaiz1/cmp_luasnip",
"rafamadriz/friendly-snippets",
"onsails/lspkind.nvim",
},
config = function()
-- ── Diagnostics ───────────────────────────────────────────────────
vim.diagnostic.config({
virtual_text = { prefix = "", spacing = 4 },
signs = true,
underline = true,
update_in_insert = false,
severity_sort = true,
float = { border = "rounded", source = true },
})
-- Diagnostic gutter icons
local signs = { Error = " ", Warn = " ", Hint = "󰌵 ", Info = " " }
for type, icon in pairs(signs) do
local hl = "DiagnosticSign" .. type
vim.fn.sign_define(hl, { text = icon, texthl = hl, numhl = hl })
end
-- ── Borders on hover/signature (0.11 way, no vim.lsp.with) ───────
vim.lsp.config("*", {
handlers = {
["textDocument/hover"] = function(err, result, ctx, config)
config = config or {}
config.border = "rounded"
vim.lsp.handlers.hover(err, result, ctx, config)
end,
["textDocument/signatureHelp"] = function(err, result, ctx, config)
config = config or {}
config.border = "rounded"
vim.lsp.handlers.signature_help(err, result, ctx, config)
end,
},
})
-- ── Capabilities (nvim-cmp) ───────────────────────────────────────
local caps = vim.tbl_deep_extend(
"force",
vim.lsp.protocol.make_client_capabilities(),
require("cmp_nvim_lsp").default_capabilities()
)
-- ── on_attach via LspAttach autocmd (0.11 pattern) ───────────────
vim.api.nvim_create_autocmd("LspAttach", {
group = vim.api.nvim_create_augroup("arch-dev-lsp", { clear = true }),
callback = function(event)
local buf = event.buf
local m = function(k, fn, d)
vim.keymap.set("n", k, fn, { buffer = buf, desc = d, silent = true })
end
m("gd", vim.lsp.buf.definition, "Definition")
m("gD", vim.lsp.buf.declaration, "Declaration")
m("gi", vim.lsp.buf.implementation, "Implementation")
m("gt", vim.lsp.buf.type_definition, "Type definition")
m("K", vim.lsp.buf.hover, "Hover docs")
m("<C-k>", vim.lsp.buf.signature_help, "Signature")
m("<leader>rn", vim.lsp.buf.rename, "Rename")
m("<leader>ca", vim.lsp.buf.code_action, "Code action")
m("<leader>f", function() vim.lsp.buf.format({ async = true }) end, "Format")
m("gr", "<cmd>Telescope lsp_references<cr>", "References")
m("<leader>ds", "<cmd>Telescope lsp_document_symbols<cr>", "Doc symbols")
end,
})
-- ── Servers ───────────────────────────────────────────────────────
-- pyright — from pacman
vim.lsp.config("pyright", {
capabilities = caps,
settings = {
python = {
analysis = {
typeCheckingMode = "basic",
autoSearchPaths = true,
useLibraryCodeForTypes = true,
diagnosticMode = "openFilesOnly",
},
},
},
})
vim.lsp.enable("pyright")
-- bash-language-server — from pacman
vim.lsp.config("bashls", {
capabilities = caps,
filetypes = { "sh", "bash", "zsh" },
})
vim.lsp.enable("bashls")
-- lua_ls — for editing neovim config inside arch-dev
vim.lsp.config("lua_ls", {
capabilities = caps,
settings = {
Lua = {
runtime = { version = "LuaJIT" },
diagnostics = { globals = { "vim" } },
workspace = {
library = vim.api.nvim_get_runtime_file("", true),
checkThirdParty = false,
},
telemetry = { enable = false },
},
},
})
vim.lsp.enable("lua_ls")
end,
},
-- ── Completion ────────────────────────────────────────────────────────────
{
"hrsh7th/nvim-cmp",
event = { "InsertEnter", "CmdlineEnter" },
dependencies = {
"L3MON4D3/LuaSnip",
"saadparwaiz1/cmp_luasnip",
"rafamadriz/friendly-snippets",
"onsails/lspkind.nvim",
"hrsh7th/cmp-nvim-lsp",
"hrsh7th/cmp-buffer",
"hrsh7th/cmp-path",
"hrsh7th/cmp-cmdline",
},
config = function()
local cmp = require("cmp")
local luasnip = require("luasnip")
local lspkind = require("lspkind")
require("luasnip.loaders.from_vscode").lazy_load()
cmp.setup({
snippet = {
expand = function(args) luasnip.lsp_expand(args.body) end,
},
window = {
completion = cmp.config.window.bordered(),
documentation = cmp.config.window.bordered(),
},
mapping = cmp.mapping.preset.insert({
["<C-b>"] = cmp.mapping.scroll_docs(-4),
["<C-f>"] = cmp.mapping.scroll_docs(4),
["<C-Space>"] = cmp.mapping.complete(),
["<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 = lspkind.cmp_format({
mode = "symbol_text",
maxwidth = 50,
ellipsis_char = "",
menu = {
nvim_lsp = "[LSP]",
luasnip = "[Snip]",
buffer = "[Buf]",
path = "[Path]",
},
}),
},
})
cmp.setup.cmdline({ "/", "?" }, {
mapping = cmp.mapping.preset.cmdline(),
sources = { { name = "buffer" } },
})
cmp.setup.cmdline(":", {
mapping = cmp.mapping.preset.cmdline(),
sources = cmp.config.sources(
{ { name = "path" } },
{ { name = "cmdline" } }
),
})
end,
},
-- ── Linting ───────────────────────────────────────────────────────────────
{
"mfussenegger/nvim-lint",
event = { "BufReadPre", "BufNewFile" },
config = function()
local lint = require("lint")
lint.linters_by_ft = {
python = { "ruff" },
sh = { "shellcheck" },
bash = { "shellcheck" },
zsh = { "shellcheck" },
}
vim.api.nvim_create_autocmd(
{ "BufWritePost", "BufReadPost", "InsertLeave" },
{ callback = function() lint.try_lint() end }
)
end,
},
-- ── Formatting ────────────────────────────────────────────────────────────
{
"stevearc/conform.nvim",
event = { "BufWritePre" },
config = function()
require("conform").setup({
formatters_by_ft = {
python = { "black" },
sh = { "shfmt" },
bash = { "shfmt" },
lua = { "stylua" },
},
format_on_save = { timeout_ms = 1000, lsp_fallback = true },
notify_on_error = false,
})
end,
},
-- ── Trouble: diagnostics panel ────────────────────────────────────────────
{
"folke/trouble.nvim",
dependencies = { "nvim-tree/nvim-web-devicons" },
config = function()
require("trouble").setup({ use_diagnostic_signs = true })
vim.keymap.set("n", "<leader>xx", "<cmd>TroubleToggle<cr>",
{ desc = "Toggle trouble" })
vim.keymap.set("n", "<leader>xd", "<cmd>TroubleToggle document_diagnostics<cr>",
{ desc = "Document diagnostics" })
end,
},
}

View File

@ -0,0 +1,104 @@
-- ── plugins/tools.lua ─────────────────────────────────────────────────────────
return {
-- ── LazyGit inside neovim ─────────────────────────────────────────────────
{
"kdheepak/lazygit.nvim",
dependencies = { "nvim-lua/plenary.nvim" },
},
-- ── Diffview ──────────────────────────────────────────────────────────────
{
"sindrets/diffview.nvim",
dependencies = { "nvim-lua/plenary.nvim" },
config = function()
require("diffview").setup()
vim.keymap.set("n", "<leader>gd", "<cmd>DiffviewOpen<cr>", { desc = "Diff view" })
vim.keymap.set("n", "<leader>gh", "<cmd>DiffviewFileHistory<cr>", { desc = "File history" })
end,
},
-- ── Leap: 2-char jump ─────────────────────────────────────────────────────
-- Repo moved GitHub → Codeberg Jan 2026. url= required.
-- API: plain <Plug> mappings. No setup(), no add/set_default_mappings().
{
url = "https://codeberg.org/andyg/leap.nvim",
config = function()
vim.keymap.set({ "n", "x", "o" }, "s", "<Plug>(leap)")
vim.keymap.set("n", "S", "<Plug>(leap-from-window)")
end,
},
-- ── Auto pairs ────────────────────────────────────────────────────────────
{
"windwp/nvim-autopairs",
event = "InsertEnter",
config = function()
require("nvim-autopairs").setup({
check_ts = true,
fast_wrap = { map = "<M-e>" },
})
local cmp_autopairs = require("nvim-autopairs.completion.cmp")
require("cmp").event:on("confirm_done", cmp_autopairs.on_confirm_done())
end,
},
-- ── Comment toggling ──────────────────────────────────────────────────────
{
"numToStr/Comment.nvim",
event = "BufReadPost",
dependencies = { "JoosepAlviste/nvim-ts-context-commentstring" },
config = function()
require("Comment").setup({
pre_hook = require("ts_context_commentstring.integrations.comment_nvim").create_pre_hook(),
})
end,
},
-- ── Surround ──────────────────────────────────────────────────────────────
-- v4: keymaps table removed from setup(). Use default ys/ds/cs mappings.
-- leap owns `s` but ys/ds/cs don't conflict with it.
{
"kylechui/nvim-surround",
event = "VeryLazy",
config = function()
require("nvim-surround").setup()
end,
},
-- ── Todo comments ─────────────────────────────────────────────────────────
{
"folke/todo-comments.nvim",
dependencies = { "nvim-lua/plenary.nvim" },
config = function()
require("todo-comments").setup()
vim.keymap.set("n", "<leader>ft", "<cmd>TodoTelescope<cr>", { desc = "Find TODOs" })
end,
},
-- ── Marks ─────────────────────────────────────────────────────────────────
{
"chentoast/marks.nvim",
event = "BufReadPost",
config = true,
},
-- ── Better quickfix ───────────────────────────────────────────────────────
{
"kevinhwang91/nvim-bqf",
ft = "qf",
config = true,
},
-- ── Zen mode ──────────────────────────────────────────────────────────────
{
"folke/zen-mode.nvim",
config = function()
require("zen-mode").setup({ window = { width = 0.85 } })
vim.keymap.set("n", "<leader>z", "<cmd>ZenMode<cr>", { desc = "Zen mode" })
end,
},
-- ── Plenary ───────────────────────────────────────────────────────────────
{ "nvim-lua/plenary.nvim", lazy = true },
}

View File

@ -0,0 +1,81 @@
-- ── plugins/treesitter.lua ────────────────────────────────────────────────────
-- nvim-treesitter rewrote its API (Oct 2025).
-- - nvim-treesitter.configs is GONE
-- - New API: require("nvim-treesitter").setup{}
-- - Does NOT support lazy loading: lazy = false required
-- - treesitter-textobjects is now a fully separate module
return {
-- ── Core treesitter ───────────────────────────────────────────────────────
{
"nvim-treesitter/nvim-treesitter",
branch = "main",
lazy = false,
build = ":TSUpdate",
config = function()
require("nvim-treesitter").setup({
ensure_installed = {
"python",
"bash", "lua", "luadoc",
"json", "yaml", "toml",
"markdown", "markdown_inline",
"regex",
"vim", "vimdoc",
"diff", "gitcommit", "git_rebase",
"ini", "comment",
},
auto_install = false, -- stateless image, no runtime installs
})
-- Highlight: enable per filetype via autocmd (new API pattern)
vim.api.nvim_create_autocmd("FileType", {
callback = function(ev)
-- Skip very large files
local ok, stats = pcall(vim.loop.fs_stat, vim.api.nvim_buf_get_name(ev.buf))
if ok and stats and stats.size > 500 * 1024 then return end
pcall(vim.treesitter.start)
end,
})
end,
},
-- ── Textobjects: separate plugin, separate module ─────────────────────────
{
"nvim-treesitter/nvim-treesitter-textobjects",
branch = "main",
lazy = false,
dependencies = {
{ "nvim-treesitter/nvim-treesitter", branch = "main" },
},
config = function()
require("nvim-treesitter-textobjects").setup({
select = {
enable = true,
lookahead = true,
keymaps = {
["af"] = "@function.outer",
["if"] = "@function.inner",
["ac"] = "@class.outer",
["ic"] = "@class.inner",
["aa"] = "@parameter.outer",
["ia"] = "@parameter.inner",
["ab"] = "@block.outer",
["ib"] = "@block.inner",
},
},
move = {
enable = true,
set_jumps = true,
goto_next_start = {
["]f"] = "@function.outer",
["]c"] = "@class.outer",
},
goto_previous_start = {
["[f"] = "@function.outer",
["[c"] = "@class.outer",
},
},
})
end,
},
}

View File

@ -0,0 +1,285 @@
-- ── plugins/ui.lua ────────────────────────────────────────────────────────────
return {
-- ── Colorscheme: Kanagawa Wave (desktop) / habamax (mobile) ───────────────
-- kanagawa requires termguicolors which Termius mangles as purple.
-- habamax is built-in, renders cleanly in 256-color (retrobox caused
-- redraw corruption on Termius, habamax does not).
{
"rebelot/kanagawa.nvim",
priority = 1000,
config = function()
if vim.env.MOBILE == "1" then
vim.cmd("colorscheme habamax")
return
end
require("kanagawa").setup({
compile = true,
undercurl = true,
theme = "wave",
background = {
dark = "wave",
light = "lotus",
},
colors = {
theme = {
wave = {
ui = {
bg_gutter = "none",
},
},
},
},
overrides = function(colors)
local theme = colors.theme
return {
Pmenu = { fg = theme.ui.shade0, bg = theme.ui.bg_p1 },
PmenuSbar = { bg = theme.ui.bg_m1 },
PmenuThumb = { bg = theme.ui.bg_p2 },
CursorLine = { bg = theme.ui.bg_p1 },
}
end,
})
vim.cmd("colorscheme kanagawa-wave")
end,
},
-- ── Statusline: lualine ───────────────────────────────────────────────────
-- theme = "auto" derives from active colorscheme — works for both
-- kanagawa (desktop) and habamax (mobile) without hex colors
{
"nvim-lualine/lualine.nvim",
dependencies = { "nvim-tree/nvim-web-devicons" },
config = function()
require("lualine").setup({
options = {
theme = "auto",
component_separators = { left = "", right = "" },
section_separators = { left = "", right = "" },
globalstatus = true,
},
sections = {
lualine_a = { { "mode", icon = "" } },
lualine_b = {
{ "branch", icon = "" },
{ "diff",
symbols = { added = " ", modified = " ", removed = " " },
},
},
lualine_c = {
{ "filename", path = 1, symbols = { modified = "", readonly = "" } },
},
lualine_x = {
{ "diagnostics",
symbols = { error = " ", warn = " ", info = " ", hint = "󰌵 " },
},
"filetype",
},
lualine_y = { "progress" },
lualine_z = { { "location", icon = "" } },
},
})
end,
},
-- ── Bufferline (desktop only) ─────────────────────────────────────────────
-- Renders truecolor backgrounds for tabs which Termius shows as purple bars.
{
"akinsho/bufferline.nvim",
enabled = function() return vim.env.MOBILE ~= "1" end,
dependencies = { "nvim-tree/nvim-web-devicons" },
config = function()
require("bufferline").setup({
options = {
mode = "buffers",
separator_style = "slant",
show_close_icon = false,
show_buffer_close_icons = false,
diagnostics = "nvim_lsp",
diagnostics_indicator = function(_, _, diag)
local icons = { error = " ", warning = " " }
local ret = (diag.error and icons.error .. diag.error .. " " or "")
.. (diag.warning and icons.warning .. diag.warning or "")
return vim.trim(ret)
end,
offsets = {
{ filetype = "oil", text = "File Explorer", separator = true },
},
},
})
end,
},
-- ── File explorer: oil.nvim ───────────────────────────────────────────────
-- Edit the filesystem like a buffer. Very Arch: does one thing, perfectly.
{
"stevearc/oil.nvim",
dependencies = { "nvim-tree/nvim-web-devicons" },
config = function()
require("oil").setup({
default_file_explorer = true,
columns = {
"icon",
"permissions",
"size",
"mtime",
},
view_options = {
show_hidden = true,
},
keymaps = {
["<C-h>"] = false, -- don't hijack window nav
["<C-l>"] = false,
["q"] = "actions.close",
},
})
end,
},
-- ── Fuzzy finder: Telescope ───────────────────────────────────────────────
{
"nvim-telescope/telescope.nvim",
dependencies = {
"nvim-lua/plenary.nvim",
-- Native FZF sorter — faster than the lua one
{ "nvim-telescope/telescope-fzf-native.nvim", build = "make" },
},
config = function()
local telescope = require("telescope")
local actions = require("telescope.actions")
telescope.setup({
defaults = {
prompt_prefix = " ",
selection_caret = "",
-- Kanagawa-inspired layout
layout_strategy = "horizontal",
layout_config = { prompt_position = "top", width = 0.9 },
sorting_strategy = "ascending",
mappings = {
i = {
["<Esc>"] = actions.close,
["<C-j>"] = actions.move_selection_next,
["<C-k>"] = actions.move_selection_previous,
},
},
},
})
telescope.load_extension("fzf")
end,
},
-- ── Which-key ─────────────────────────────────────────────────────────────
{
"folke/which-key.nvim",
event = "VeryLazy",
config = function()
require("which-key").setup({
preset = "modern",
delay = 400,
icons = { separator = "" },
})
end,
},
-- ── Git signs in gutter ───────────────────────────────────────────────────
{
"lewis6991/gitsigns.nvim",
config = function()
require("gitsigns").setup({
signs = {
add = { text = "" },
change = { text = "" },
delete = { text = "" },
topdelete = { text = "" },
changedelete = { text = "" },
untracked = { text = "" },
},
on_attach = function(buf)
local gs = require("gitsigns")
local m = function(k, fn, d)
vim.keymap.set("n", k, fn, { buffer = buf, desc = d })
end
m("]c", gs.next_hunk, "Next hunk")
m("[c", gs.prev_hunk, "Prev hunk")
m("<leader>hs", gs.stage_hunk, "Stage hunk")
m("<leader>hr", gs.reset_hunk, "Reset hunk")
m("<leader>hp", gs.preview_hunk, "Preview hunk")
m("<leader>hb", gs.blame_line, "Blame line")
end,
})
end,
},
-- ── Indent guides ─────────────────────────────────────────────────────────
{
"lukas-reineke/indent-blankline.nvim",
main = "ibl",
config = function()
require("ibl").setup({
indent = { char = "" },
scope = { char = "" },
})
-- Link to existing highlight groups instead of hex (mobile-safe)
vim.cmd("hi! link IblIndent Comment")
vim.cmd("hi! link IblScope LineNr")
end,
},
-- ── Dashboard ─────────────────────────────────────────────────────────────
{
"goolord/alpha-nvim",
event = "VimEnter",
config = function()
local alpha = require("alpha")
local dashboard = require("alpha.themes.dashboard")
-- Mobile-friendly ASCII (heavy blocks render as red bars in Termius)
dashboard.section.header.val = {
" ",
" ┌─┐┬─┐┌─┐┬ ┬ ┌┬┐┌─┐┬ ┬ ",
" ├─┤├┬┘│ ├─┤───│││├┤ └┐┌┘ ",
" ┴ ┴┴└─└─┘┴ ┴ ─┴┘└─┘ └┘ ",
" ",
" riced neovim ide · arch linux · v1.7 ",
}
dashboard.section.buttons.val = {
dashboard.button("f", " Find file", "<cmd>Telescope find_files<cr>"),
dashboard.button("r", " Recent files", "<cmd>Telescope oldfiles<cr>"),
dashboard.button("g", " Live grep", "<cmd>Telescope live_grep<cr>"),
dashboard.button("e", " Explorer", "<cmd>Oil<cr>"),
dashboard.button("l", " Lazy", "<cmd>Lazy<cr>"),
dashboard.button("q", " Quit", "<cmd>qa<cr>"),
}
dashboard.section.header.opts.hl = "Comment"
dashboard.section.footer.opts.hl = "NonText"
alpha.setup(dashboard.opts)
end,
},
-- ── Smooth scrolling ──────────────────────────────────────────────────────
{
"karb94/neoscroll.nvim",
config = function()
require("neoscroll").setup({ mappings = { "<C-u>","<C-d>","<C-b>","<C-f>" } })
end,
},
-- ── Color previews inline (desktop only) ──────────────────────────────────
-- Renders hex as RGB which Termius mangles. Skip on mobile.
{
"NvChad/nvim-colorizer.lua",
enabled = function() return vim.env.MOBILE ~= "1" end,
config = function()
require("colorizer").setup({ "*" }, { mode = "virtualtext" })
end,
},
-- Web devicons (used everywhere)
{ "nvim-tree/nvim-web-devicons", lazy = true },
}

View File

@ -0,0 +1,45 @@
# ╔══════════════════════════════════════════════════════════════════╗
# ║ arch-dev :: starship-mobile.toml ║
# ║ Single line · no icons · 256-color · Termius-safe ║
# ╚══════════════════════════════════════════════════════════════════╝
format = "$directory$git_branch$git_status$python $character"
right_format = "$cmd_duration"
[directory]
style = "bold fg:173"
truncation_length = 3
truncate_to_repo = true
read_only = "[ro]"
format = "[$path]($style)[$read_only](fg:167) "
[git_branch]
symbol = ""
style = "fg:106"
format = "[$symbol$branch]($style) "
[git_status]
style = "fg:167"
conflicted = "!"
ahead = "+${count}"
behind = "-${count}"
modified = "*"
untracked = "?"
staged = "+"
format = "([$all_status$ahead_behind]($style)) "
[python]
symbol = "py:"
style = "fg:66"
format = "[$symbol$version]($style) "
detect_files = ["*.py", "requirements.txt", "pyproject.toml"]
[cmd_duration]
min_time = 3000
style = "fg:242"
format = "[$duration]($style)"
[character]
success_symbol = "[>](bold fg:106)"
error_symbol = "[>](bold fg:167)"
vimcmd_symbol = "[<](bold fg:139)"

View File

@ -0,0 +1,76 @@
# ╔══════════════════════════════════════════════════════════════════╗
# ║ arch-dev :: starship.toml (desktop) ║
# ║ Kanagawa Wave · 256-color only · no hex RGB ║
# ╚══════════════════════════════════════════════════════════════════╝
# 256-color Kanagawa approximations:
# 110 = crystalBlue 106 = springGreen 139 = oniViolet
# 173 = boatYellow 167 = peachRed 66 = waveAqua
# 242 = fujiGray 250 = fujiWhite 236 = waveBlue
format = """
$os$username$hostname$directory$git_branch$git_status$python
$character"""
right_format = """$cmd_duration$time"""
[os]
disabled = false
style = "bold fg:139"
[os.symbols]
Arch = " "
[username]
show_always = true
style_user = "bold fg:110"
style_root = "bold fg:167"
format = "[$user]($style)"
[hostname]
ssh_only = false
style = "fg:242"
format = "@[$hostname]($style) "
[directory]
style = "bold fg:173"
truncation_length = 5
truncate_to_repo = true
read_only = " "
format = "in [$path]($style)[$read_only](fg:167) "
[git_branch]
symbol = " "
style = "bold fg:106"
format = "on [$symbol$branch]($style) "
[git_status]
style = "fg:167"
conflicted = "✖ "
ahead = "⇡${count} "
behind = "⇣${count} "
modified = "✎${count} "
untracked = "?${count} "
staged = "✔${count} "
format = "([$all_status$ahead_behind]($style))"
[python]
symbol = " "
style = "fg:66"
format = " [$symbol$version( $virtualenv)]($style)"
detect_files = ["*.py", "requirements.txt", "pyproject.toml"]
[cmd_duration]
min_time = 2000
style = "fg:242"
format = " took [$duration]($style)"
[time]
disabled = false
style = "fg:236"
format = " [$time]($style)"
time_format = "%H:%M"
[character]
success_symbol = "[](bold fg:106)"
error_symbol = "[](bold fg:167)"
vimcmd_symbol = "[](bold fg:139)"

View File

@ -0,0 +1,92 @@
# ╔══════════════════════════════════════════════════════════════════╗
# ║ arch-dev :: tmux.conf (desktop) ║
# ║ Kanagawa Wave · 256-color · Ctrl+Space prefix · vim-nav ║
# ╚══════════════════════════════════════════════════════════════════╝
# ── Prefix ────────────────────────────────────────────────────────────────────
unbind C-b
set -g prefix C-Space
bind C-Space send-prefix
# ── Core behavior ─────────────────────────────────────────────────────────────
set -g mouse on
set -g base-index 1
setw -g pane-base-index 1
set -g renumber-windows on
set -sg escape-time 5
set -g history-limit 50000
set -g focus-events on
set -g display-time 2000
# ── 256 color (no truecolor — Termius mangles it as purple) ───────────────────
set -g default-terminal "tmux-256color"
# ── Clipboard (OSC 52) ────────────────────────────────────────────────────────
set -g set-clipboard on
# ── Splits in cwd ─────────────────────────────────────────────────────────────
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
bind c new-window -c "#{pane_current_path}"
unbind '"'
unbind %
# ── Vim-style pane navigation ─────────────────────────────────────────────────
bind h select-pane -L
bind j select-pane -D
bind k select-pane -U
bind l select-pane -R
# ── Alt+number window switching ───────────────────────────────────────────────
bind -n M-1 select-window -t 1
bind -n M-2 select-window -t 2
bind -n M-3 select-window -t 3
bind -n M-4 select-window -t 4
bind -n M-5 select-window -t 5
# ── Pane resize ───────────────────────────────────────────────────────────────
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
# ── Zoom & scroll mode ────────────────────────────────────────────────────────
bind z resize-pane -Z
bind Enter copy-mode
bind -T copy-mode-vi v send -X begin-selection
bind -T copy-mode-vi y send -X copy-selection-and-cancel
setw -g mode-keys vi
# ── Reload & detach ───────────────────────────────────────────────────────────
bind r source-file ~/.tmux.conf \; display "↻ tmux reloaded"
bind d detach-client
# ── Status bar: 256-color Kanagawa Wave approximations ────────────────────────
# 234=sumiInk0, 236=sumiInk3, 237=waveBlue1, 110=crystalBlue
# 173=boatYellow2, 106=springGreen, 242=fujiGray
# 250=fujiWhite, 139=oniViolet
set -g status on
set -g status-interval 5
set -g status-position bottom
set -g status-style "bg=colour234,fg=colour250"
set -g status-left-length 40
set -g status-left \
"#[bold,fg=colour139,bg=colour234] arch-dev#[fg=colour236] │ #[default]"
set -g status-right-length 60
set -g status-right \
"#[fg=colour106]%H:%M #[fg=colour236]│ #[fg=colour110]%a %d %b"
# Window tabs
set -g window-status-format " #I #W "
set -g window-status-current-format "#[bold,fg=colour173,bg=colour237] #I #W #[default]"
set -g window-status-style "fg=colour242"
set -g window-status-current-style "fg=colour173,bg=colour237"
# Pane borders
set -g pane-border-style "fg=colour236"
set -g pane-active-border-style "fg=colour110"
# Message bar
set -g message-style "bg=colour237,fg=colour250,bold"

57
dotfiles/.screenrc Normal file
View File

@ -0,0 +1,57 @@
# ╔══════════════════════════════════════════════════════════════════╗
# ║ arch-dev :: screenrc (mobile multiplexer) ║
# ║ Termius-optimized · clean status · keyboard-friendly ║
# ╚══════════════════════════════════════════════════════════════════╝
# ── Basics ────────────────────────────────────────────────────────────────────
startup_message off
vbell off
defscrollback 10000
defutf8 on
defencoding utf8
autodetach on
shell -zsh
# ── 256 color (no truecolor — Termius mangles RGB as purple) ─────────────────
term xterm-256color
termcapinfo xterm-256color|xterm 'Co#256:AB=\E[48;5;%dm:AF=\E[38;5;%dm'
termcapinfo xterm-256color ti@:te@
# ── Status bar ────────────────────────────────────────────────────────────────
# Keep it readable on a small screen
hardstatus alwayslastline
hardstatus string '%{= kw}[ %{= kc}arch-dev%{= kw} ][ %{= kg}%w%{= kw} ]%=%{= kw}[ %{= kY}%D %d %M%{= kw} ][ %{= kc}%0c%{= kw} ]'
# ── Keybindings (tearless mobile ref) ────────────────────────────────────────
# Prefix stays Ctrl+a (default) — familiar and easy on mobile
# New window: Ctrl+a c
# Next window: Ctrl+a n (or Ctrl+a Space)
# Prev window: Ctrl+a p
# List windows: Ctrl+a "
# Scroll mode: Ctrl+a [
# Exit scroll: q
# Detach: Ctrl+a d
# Reattach: screen -RD (alias: mux)
# Make scroll mode vi-friendly
markkeys h=^H:l=^L:$=^E
# ── Mouse ─────────────────────────────────────────────────────────────────────
# Disabled — Termius touch handles this better directly
# mousetrack on
# ── Windows ───────────────────────────────────────────────────────────────────
# Number pad for quick window switch
bind 1 select 1
bind 2 select 2
bind 3 select 3
bind 4 select 4
bind 5 select 5
# Reload
bind r source ~/.screenrc
# ── Logging ───────────────────────────────────────────────────────────────────
# deflog on
# logfile $HOME/.screen/screen-%Y%m%d-%n.log

185
dotfiles/.zshrc Normal file
View File

@ -0,0 +1,185 @@
# ╔══════════════════════════════════════════════════════════════════╗
# ║ arch-dev :: zshrc ║
# ║ Kanagawa Wave · 256-color · mobile-aware · AUR-powered ║
# ╚══════════════════════════════════════════════════════════════════╝
# ── Environment ───────────────────────────────────────────────────────────────
export EDITOR=nvim
export VISUAL=nvim
export PAGER="less"
export MANPAGER="nvim +Man!"
export TERM=xterm-256color
export PATH="$HOME/.local/bin:$HOME/perl5/bin:/usr/local/bin:$PATH"
export DOCKER_BUILDKIT=1
# NO truecolor — Termius renders hex RGB as purple
unset COLORTERM
# ── History ───────────────────────────────────────────────────────────────────
HISTSIZE=50000
SAVEHIST=50000
HISTFILE="${HISTFILE:-$HOME/.zsh_history}"
setopt share_history hist_ignore_dups hist_ignore_space
setopt hist_reduce_blanks extended_history
# ── ZSH Options ───────────────────────────────────────────────────────────────
setopt auto_cd correct interactive_comments no_beep
# ── Completion ────────────────────────────────────────────────────────────────
autoload -Uz compinit && compinit
zstyle ':completion:*' menu select
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'
zstyle ':completion:*:descriptions' format '%F{yellow}── %d ──%f'
# ── Plugins ───────────────────────────────────────────────────────────────────
source /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh
source /usr/share/zsh/plugins/zsh-history-substring-search/zsh-history-substring-search.zsh
bindkey '^[[A' history-substring-search-up
bindkey '^[[B' history-substring-search-down
# ── zsh-syntax-highlighting: 256-color only (no hex/truecolor) ────────────────
# Kanagawa Wave approximations in 256-color palette
# 110 = steel blue (crystalBlue) 106 = olive green (springGreen)
# 139 = medium purple (oniViolet) 173 = tan (boatYellow)
# 167 = indian red (peachRed) 66 = cadet blue (waveAqua)
ZSH_HIGHLIGHT_STYLES[default]='none'
ZSH_HIGHLIGHT_STYLES[unknown-token]='fg=167,bold'
ZSH_HIGHLIGHT_STYLES[reserved-word]='fg=139'
ZSH_HIGHLIGHT_STYLES[command]='fg=110'
ZSH_HIGHLIGHT_STYLES[builtin]='fg=110'
ZSH_HIGHLIGHT_STYLES[function]='fg=110'
ZSH_HIGHLIGHT_STYLES[alias]='fg=106'
ZSH_HIGHLIGHT_STYLES[path]='fg=66'
ZSH_HIGHLIGHT_STYLES[single-quoted-argument]='fg=173'
ZSH_HIGHLIGHT_STYLES[double-quoted-argument]='fg=173'
ZSH_HIGHLIGHT_STYLES[comment]='fg=242'
ZSH_HIGHLIGHT_STYLES[globbing]='fg=139'
# ── Autosuggestion style ──────────────────────────────────────────────────────
ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=242'
ZSH_AUTOSUGGEST_STRATEGY=(history completion)
ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE=20
# ── Smart Tools ───────────────────────────────────────────────────────────────
eval "$(zoxide init zsh)"
source <(fzf --zsh)
# FZF — 256-color theme
export FZF_DEFAULT_OPTS="
--color=bg+:236,bg:234,spinner:139,hl:173
--color=fg:250,header:242,info:110,pointer:167
--color=marker:139,fg+:250,prompt:110,hl+:167
--border=rounded --prompt=' ' --pointer='▶' --marker='✔'
"
export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git'
# ── Mobile Detection ──────────────────────────────────────────────────────────
if [[ "${MOBILE:-0}" == "1" ]]; then
export STARSHIP_CONFIG="$HOME/.config/starship-mobile.toml"
alias mux='screen -RD'
alias muxls='screen -ls'
if [[ -z "$STY" && -z "$TMUX" ]]; then
screen -RD arch-dev 2>/dev/null || screen -S arch-dev
fi
else
export STARSHIP_CONFIG="$HOME/.config/starship.toml"
alias mux='tmux new-session -A -s main'
alias muxls='tmux ls'
fi
# ── Prompt ────────────────────────────────────────────────────────────────────
eval "$(starship init zsh)"
# ── Aliases ───────────────────────────────────────────────────────────────────
source ~/.aliases
# ── arch-dev snapshot system ──────────────────────────────────────────────────
# Git-backed rollback for /home/dev. State lives in ~/.arch-dev-state/
# (separate git dir so it doesn't conflict with project git repos).
export ARCH_DEV_GITDIR="$HOME/.arch-dev-state"
_archdev_git() {
git --git-dir="$ARCH_DEV_GITDIR" --work-tree="$HOME" "$@"
}
# Take a named snapshot
snapshot() {
if [[ -z "$1" ]]; then
echo "usage: snapshot <name> [message]"
echo " e.g. snapshot node-working 'NodeJS environment with nvm + pnpm'"
return 1
fi
local name="$1"
local msg="${2:-snapshot: $name}"
_archdev_git add -A
_archdev_git commit -q -m "$msg" || { echo "nothing to snapshot"; return 1; }
_archdev_git tag -f "$name"
echo "✓ snapshot '$name' saved"
}
# List snapshots
snapshots() {
echo "── arch-dev snapshots ──"
_archdev_git tag -l --format=' %(refname:short) %(taggerdate:short) %(subject)'
echo ""
echo "── recent commits ──"
_archdev_git log --oneline -10
}
# Roll back to a snapshot
rollback() {
if [[ -z "$1" ]]; then
echo "usage: rollback <name>"
snapshots
return 1
fi
local name="$1"
if ! _archdev_git rev-parse "$name" >/dev/null 2>&1; then
echo "✗ snapshot '$name' not found"
snapshots
return 1
fi
echo "⚠ rolling back to '$name' will discard uncommitted changes"
echo -n " proceed? [y/N] "
read -r reply
[[ "$reply" =~ ^[Yy]$ ]] || { echo "cancelled"; return 1; }
# Auto-snapshot current state before rollback
_archdev_git add -A 2>/dev/null
_archdev_git commit -q -m "auto: pre-rollback $(date +%Y%m%d-%H%M%S)" 2>/dev/null || true
_archdev_git reset --hard "$name"
echo "✓ rolled back to '$name'"
}
# What's changed since last snapshot
diff-state() {
_archdev_git status --short
echo ""
_archdev_git diff --stat HEAD
}
# Delete a snapshot
unsnapshot() {
if [[ -z "$1" ]]; then
echo "usage: unsnapshot <name>"
return 1
fi
_archdev_git tag -d "$1" && echo "✓ snapshot '$1' removed"
}
# Show what's in a snapshot
show-snapshot() {
if [[ -z "$1" ]]; then
echo "usage: show-snapshot <name>"
return 1
fi
_archdev_git show --stat "$1"
}
# ── Key Bindings ──────────────────────────────────────────────────────────────
bindkey -e
bindkey '^[[1;5C' forward-word
bindkey '^[[1;5D' backward-word

83
entrypoint.sh Normal file
View File

@ -0,0 +1,83 @@
#!/bin/bash
# ╔══════════════════════════════════════════════════════════════════╗
# ║ arch-dev :: entrypoint ║
# ║ Seeds /home/dev from skel template on first run. ║
# ║ Updates dotfiles when image is newer than home. ║
# ║ Initializes git for snapshot/rollback functionality. ║
# ╚══════════════════════════════════════════════════════════════════╝
set -e
SKEL=/etc/skel-arch-dev
HOME_DIR=/home/dev
MARKER="$HOME_DIR/.arch-dev-initialized"
GITDIR="$HOME_DIR/.arch-dev-state"
# ── First run: seed everything from skel ──────────────────────────────────────
if [[ ! -f "$MARKER" ]]; then
echo "🌱 arch-dev: first run — seeding home from skeleton..."
# Copy everything except things that should not exist yet
rsync -a --chown=dev:dev "$SKEL/" "$HOME_DIR/"
# Initialize git repo for snapshots
cd "$HOME_DIR"
git init --quiet --separate-git-dir="$GITDIR" .
git --git-dir="$GITDIR" --work-tree="$HOME_DIR" config user.name "arch-dev"
git --git-dir="$GITDIR" --work-tree="$HOME_DIR" config user.email "dev@arch-dev.local"
# Write .gitignore to exclude noise
cat > "$HOME_DIR/.gitignore" <<'GITIGNORE'
# Caches and logs
.cache/
.local/state/
.local/share/nvim/log
.local/share/nvim/lazy-lock.json.bak
.npm/
__pycache__/
*.pyc
# History (intentional — too noisy to track)
.zsh_history
.lesshst
.bash_history
.python_history
# Compiled neovim plugin caches
.local/share/nvim/lazy/*/plugin/packer_compiled.lua
# Trash
.local/share/Trash/
GITIGNORE
git --git-dir="$GITDIR" --work-tree="$HOME_DIR" add -A
git --git-dir="$GITDIR" --work-tree="$HOME_DIR" commit -q -m "initial: arch-dev skeleton baked"
git --git-dir="$GITDIR" --work-tree="$HOME_DIR" tag -a "skeleton" -m "factory state"
touch "$MARKER"
echo "✓ arch-dev: home initialized with snapshot 'skeleton'"
# ── Subsequent runs: refresh dotfiles only if image is newer ──────────────────
else
SKEL_TIME=$(stat -c %Y "$SKEL/.zshrc" 2>/dev/null || echo 0)
HOME_TIME=$(stat -c %Y "$HOME_DIR/.zshrc" 2>/dev/null || echo 0)
if [[ "$SKEL_TIME" -gt "$HOME_TIME" ]]; then
echo "🔄 arch-dev: image dotfiles newer than home — updating..."
# Auto-snapshot before updating, in case user wants to revert
if [[ -d "$GITDIR" ]]; then
git --git-dir="$GITDIR" --work-tree="$HOME_DIR" add -A 2>/dev/null
git --git-dir="$GITDIR" --work-tree="$HOME_DIR" \
commit -q -m "auto: pre-update snapshot $(date +%Y%m%d-%H%M%S)" 2>/dev/null || true
fi
# Refresh only the dotfiles, not user data
rsync -a --chown=dev:dev \
--exclude='.local/share/nvim/lazy' \
--exclude='.cache' \
"$SKEL/" "$HOME_DIR/"
echo "✓ arch-dev: dotfiles updated (auto-snapshot taken first)"
fi
fi
# ── Hand off ──────────────────────────────────────────────────────────────────
cd "$HOME_DIR"
exec "$@"