feat: public dotfiles tier — no credential, no identity, one installer

Fresh history. This is the repo a throwaway VM clones anonymously: it brings a
machine to a working baseline and carries nothing that makes it mine.

56 files. 50 land in $HOME, 3 are chezmoi metadata, 2 are repo documentation,
1 is the manifest, and a 15-file test harness stays behind in .tests/.

What did not travel, and why:

  encrypted_private_bws-token.age   a real credential; age is dropped entirely
  .chezmoidata/bws.toml             env-var -> secret-id map; belongs with the
                                    tier that can use it
  SECRETS.md                        documentation of the rules, not config
  finish-setup.sh.tmpl              superseded by dotup
  nvim/init.lua.backup              dead file
  dot_claude/**, dot_codex/**,      120 files of agent config, private tier
  dot_pi/**

De-identified rather than dropped:

  .gitconfig   [user], the GitHub ssh rewrite and both Gitea host rewrites are
               identity, not configuration. They move behind an [include] of
               ~/.config/git/config.local, which the private tier writes. Git
               treats a missing include as a no-op, so a public-only machine
               reads the file and stops.
  .zshrc       the two gitea aliases carried a personal domain and a LAN IP.
               They move behind a guarded source of ~/.config/zsh/local.zsh,
               the sibling of the secrets.zsh seam phase 2 established.
  nvim         a commented-out LM Studio endpoint naming a LAN address.
  ghostty      a stale auto-generated header naming an absolute home directory.

Newly captured, never tracked before: ~/.zshenv, ~/.config/gh/config.yml. The
former sourced ~/.cargo/env unguarded, so every zsh on a machine without rustup
printed an error -- the same shape as the unguarded oh-my-zsh source phase 2
fixed. It is guarded now.

.chezmoiexternal.toml grows from one entry to six. oh-my-zsh, powerlevel10k,
zsh-autosuggestions, zsh-ai and tpm were hand-installed and declared nowhere,
which is why `chezmoi init --apply` on a clean box produced a .zshrc that broke
the shell it configures. The theme and both plugins nest under
.oh-my-zsh/custom/, which is what $ZSH_CUSTOM resolves to.

dotup gains an install engine. It resolves each selected package to a channel
(apt, brew, npm, uv, snap, deb, flatpak, tarball, script, builtin) through one
function every consumer reads, probes apt-cache before batching so a name apt
does not know moves to brew instead of failing all thirty, and retries
individually if a batch still fails -- which earned its keep on the first real
container run, where mermaid-cli's puppeteer dependency failed and the other
twelve npm packages installed anyway. --unattended computes safe defaults fresh
from the manifest rather than inheriting a state file, and refuses private and
invasive rows outright even when a stale state file ticks them.

The manifest gains @spec, a second directive kind alongside @needs, carrying the
argument a channel needs but a package name cannot supply -- the scoped npm
name, the flatpak app id, the .deb source. The TSV stays five columns wide.

Three bugs the container runs found, all fixed here:

  * `apt install nodejs` gives you node WITHOUT npm on Ubuntu, so all thirteen
    npm packages failed on a fresh box. The manifest asks apt for both names.
  * A tool installed a moment ago is not on this process's PATH -- uv lands in
    ~/.local/bin, npm -g honours the ~/.npmrc prefix, linuxbrew is outside a
    non-login PATH. Resolved by looking in the places we just wrote to, never by
    exporting a modified PATH.
  * `A || { B && C; }` is one || list, so when `command -v sudo` failed the list
    failed and `set -e` killed dotup at load. On a non-root machine with no
    sudo it died before printing anything. There is a regression test.

.zshenv and .p10k.zsh are marked private_. Both are shell code the login shell
executes and both applied at 664, group-writable. Third occurrence of the class
of bug phase 1 found on .pi/agent/auth.json and phase 2 found on .zshrc; the
first one found on purpose rather than by accident.

Verification: 81 assertions, 81/81 on this box and in ubuntu:24.04, ubuntu:22.04
and debian:12. The installer is driven against a directory of fake package
managers that record what they were asked to do and install nothing, so the
engine is exercised end to end without a package landing on the test machine.
`gitleaks detect` over the full history and the working tree: no leaks found,
with no allowlist and no .gitleaks.toml.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
bcherb2
2026-08-17 00:11:52 -04:00
commit b487b0e855
71 changed files with 7824 additions and 0 deletions
@@ -0,0 +1,35 @@
return {
dir = vim.fn.expand("~/code/nvim-ai-assist"),
name = "nvim_ai_assist",
lazy = false,
cmd = { "AI", "AIHealth", "AIRefreshContext" },
keys = {
{ "<leader>ai", ":AI<CR>", desc = "AI command (prompt)", mode = "n" },
{ "<leader>aA", ":AI!<CR>", desc = "AI command (auto-execute)", mode = "n" },
{ "<leader>ah", ":AIHealth<CR>", desc = "AI health + context stats", mode = "n" },
},
config = function()
require("nvim_ai_assist").setup({
-- Active: Z.AI Coding Plan (uses ZAI_API_KEY from env)
provider = "zai-coding",
model = "glm-4.7",
max_tokens = 300,
temperature = 0.05,
debug = true, -- writes request/response to ~/.cache/nvim/nvim_ai_assist.log; :AILog to view
context = {
enabled = true,
max_chars = 1500,
refresh_seconds = 60,
},
-- Local LM Studio fallback — uncomment to use, and comment out the
-- provider/model lines above.
-- The host is deliberately not written down here: this repo is public,
-- and a LAN address is one of the things the tier split exists to keep
-- out of it. Put yours in and do not commit it back.
-- endpoint = "http://<lm-studio-host>:1234/v1/chat/completions",
-- api_key = "lm-studio",
-- model = "qwen3-almost-human-y1-7b",
})
end,
}
@@ -0,0 +1,32 @@
-- Tame blink.cmp completions for prose:
-- * drop the `buffer` (next-word) source everywhere
-- * markdown/md-render buffers use LSP+path only (no snippet callout noise)
-- * disable friendly-snippets' global snippets (datetime/dateMDY/lorem/...)
-- Inline AI suggestions are handled by minuet instead (see minuet.lua).
return {
"saghen/blink.cmp",
opts = function(_, opts)
opts.sources = opts.sources or {}
-- Drop the buffer (next-word) source everywhere.
opts.sources.default = vim.tbl_filter(function(source)
return source ~= "buffer"
end, opts.sources.default or { "lsp", "path", "snippets" })
-- Markdown editing (ft=markdown) and md-render's render buffer (ft=md-render):
-- LSP+path only, so callout snippets like !WARNING don't pop up. md-render
-- flips the buffer filetype between these two as you edit, so cover both.
opts.sources.per_filetype = opts.sources.per_filetype or {}
opts.sources.per_filetype.markdown = { "lsp", "path" }
opts.sources.per_filetype["md-render"] = { "lsp", "path" }
-- Kill friendly-snippets' GLOBAL snippets (global.json: datetime, dateMDY,
-- lorem, etc.). These load in every filetype regardless of the per_filetype
-- rules above, which is why they leaked through while editing markdown.
-- Per-language snippets (friendly_snippets) are unaffected.
opts.sources.providers = opts.sources.providers or {}
opts.sources.providers.snippets = opts.sources.providers.snippets or {}
opts.sources.providers.snippets.opts = opts.sources.providers.snippets.opts or {}
opts.sources.providers.snippets.opts.global_snippets = {}
end,
}
@@ -0,0 +1,14 @@
return {
"sindrets/diffview.nvim",
cmd = { "DiffviewOpen", "DiffviewFileHistory", "DiffviewClose" },
keys = {
{ "<leader>dv", "<cmd>DiffviewOpen<cr>", desc = "Open diffview" },
{ "<leader>ds", "<cmd>DiffviewOpen --staged<cr>", desc = "Diffview staged" },
{ "<leader>dh", "<cmd>DiffviewFileHistory %<cr>", desc = "File history (current)" },
{ "<leader>dH", "<cmd>DiffviewFileHistory<cr>", desc = "File history (all)" },
},
config = function()
require("diffview").setup({})
-- removed the vim.keymap.set lines from here
end,
}
@@ -0,0 +1,197 @@
-- since this is just an example spec, don't actually load anything here and return an empty spec
-- stylua: ignore
if true then return {} end
-- every spec file under the "plugins" directory will be loaded automatically by lazy.nvim
--
-- In your plugin files, you can:
-- * add extra plugins
-- * disable/enabled LazyVim plugins
-- * override the configuration of LazyVim plugins
return {
-- add gruvbox
{ "ellisonleao/gruvbox.nvim" },
-- Configure LazyVim to load gruvbox
{
"LazyVim/LazyVim",
opts = {
colorscheme = "gruvbox",
},
},
-- change trouble config
{
"folke/trouble.nvim",
-- opts will be merged with the parent spec
opts = { use_diagnostic_signs = true },
},
-- disable trouble
{ "folke/trouble.nvim", enabled = false },
-- override nvim-cmp and add cmp-emoji
{
"hrsh7th/nvim-cmp",
dependencies = { "hrsh7th/cmp-emoji" },
---@param opts cmp.ConfigSchema
opts = function(_, opts)
table.insert(opts.sources, { name = "emoji" })
end,
},
-- change some telescope options and a keymap to browse plugin files
{
"nvim-telescope/telescope.nvim",
keys = {
-- add a keymap to browse plugin files
-- stylua: ignore
{
"<leader>fp",
function() require("telescope.builtin").find_files({ cwd = require("lazy.core.config").options.root }) end,
desc = "Find Plugin File",
},
},
-- change some options
opts = {
defaults = {
layout_strategy = "horizontal",
layout_config = { prompt_position = "top" },
sorting_strategy = "ascending",
winblend = 0,
},
},
},
-- add pyright to lspconfig
{
"neovim/nvim-lspconfig",
---@class PluginLspOpts
opts = {
---@type lspconfig.options
servers = {
-- pyright will be automatically installed with mason and loaded with lspconfig
pyright = {},
},
},
},
-- add tsserver and setup with typescript.nvim instead of lspconfig
{
"neovim/nvim-lspconfig",
dependencies = {
"jose-elias-alvarez/typescript.nvim",
init = function()
require("lazyvim.util").lsp.on_attach(function(_, buffer)
-- stylua: ignore
vim.keymap.set( "n", "<leader>co", "TypescriptOrganizeImports", { buffer = buffer, desc = "Organize Imports" })
vim.keymap.set("n", "<leader>cR", "TypescriptRenameFile", { desc = "Rename File", buffer = buffer })
end)
end,
},
---@class PluginLspOpts
opts = {
---@type lspconfig.options
servers = {
-- tsserver will be automatically installed with mason and loaded with lspconfig
tsserver = {},
},
-- you can do any additional lsp server setup here
-- return true if you don't want this server to be setup with lspconfig
---@type table<string, fun(server:string, opts:_.lspconfig.options):boolean?>
setup = {
-- example to setup with typescript.nvim
tsserver = function(_, opts)
require("typescript").setup({ server = opts })
return true
end,
-- Specify * to use this function as a fallback for any server
-- ["*"] = function(server, opts) end,
},
},
},
-- for typescript, LazyVim also includes extra specs to properly setup lspconfig,
-- treesitter, mason and typescript.nvim. So instead of the above, you can use:
{ import = "lazyvim.plugins.extras.lang.typescript" },
-- add more treesitter parsers
{
"nvim-treesitter/nvim-treesitter",
opts = {
ensure_installed = {
"bash",
"html",
"javascript",
"json",
"lua",
"markdown",
"markdown_inline",
"python",
"query",
"regex",
"tsx",
"typescript",
"vim",
"yaml",
},
},
},
-- since `vim.tbl_deep_extend`, can only merge tables and not lists, the code above
-- would overwrite `ensure_installed` with the new value.
-- If you'd rather extend the default config, use the code below instead:
{
"nvim-treesitter/nvim-treesitter",
opts = function(_, opts)
-- add tsx and treesitter
vim.list_extend(opts.ensure_installed, {
"tsx",
"typescript",
})
end,
},
-- the opts function can also be used to change the default opts:
{
"nvim-lualine/lualine.nvim",
event = "VeryLazy",
opts = function(_, opts)
table.insert(opts.sections.lualine_x, {
function()
return "😄"
end,
})
end,
},
-- or you can return new options to override all the defaults
{
"nvim-lualine/lualine.nvim",
event = "VeryLazy",
opts = function()
return {
--[[add your custom lualine config here]]
}
end,
},
-- use mini.starter instead of alpha
{ import = "lazyvim.plugins.extras.ui.mini-starter" },
-- add jsonls and schemastore packages, and setup treesitter for json, json5 and jsonc
{ import = "lazyvim.plugins.extras.lang.json" },
-- add any tools you want to have installed below
{
"williamboman/mason.nvim",
opts = {
ensure_installed = {
"stylua",
"shellcheck",
"shfmt",
"flake8",
},
},
},
}
@@ -0,0 +1,36 @@
-- Filetype-specific plugins
return {
-- CSV file viewer with column alignment
-- Automatically formats CSV files for better readability
-- Toggle with :CsvViewEnable, :CsvViewDisable
{
"hat0uma/csvview.nvim",
ft = { "csv", "tsv" },
enabled = true, -- Set to false to disable
opts = {
parser = {
async = true,
delimiter = {
default = ",",
ft = {
tsv = "\t",
},
},
comments = {
-- Example: Set comment prefix for specific filetypes
-- ft = {
-- csv = "#",
-- },
},
},
view = {
min_column_width = 5,
spacing = 2,
display_mode = "border", -- Options: "highlight", "border"
},
},
keys = {
{ "<leader>cv", "<cmd>CsvViewToggle<cr>", desc = "Toggle CSV View", ft = { "csv", "tsv" } },
},
},
}
@@ -0,0 +1,36 @@
-- Git enhancement plugins
return {
-- Git blame annotations in virtual text
-- Shows git blame info at the end of each line
-- Toggle with :GitBlameToggle
{
"f-person/git-blame.nvim",
event = "VeryLazy",
opts = {
enabled = true, -- Start enabled
message_template = " <summary> • <date> • <author>",
date_format = "%r",
virtual_text_column = 80,
},
},
-- Advanced git diff viewer
-- Better git diff interface with file history
-- Open with :DiffviewOpen, :DiffviewFileHistory
{
"sindrets/diffview.nvim",
cmd = { "DiffviewOpen", "DiffviewClose", "DiffviewToggleFiles", "DiffviewFocusFiles", "DiffviewFileHistory" },
opts = {
enhanced_diff_hl = true,
view = {
default = { layout = "diff2_horizontal" },
file_history = { layout = "diff2_horizontal" },
},
},
keys = {
{ "<leader>gd", "<cmd>DiffviewOpen<cr>", desc = "Open Diffview" },
{ "<leader>gh", "<cmd>DiffviewFileHistory %<cr>", desc = "File History" },
{ "<leader>gH", "<cmd>DiffviewFileHistory<cr>", desc = "Repo History" },
},
},
}
@@ -0,0 +1,861 @@
-- Markdown rendering with inline mermaid via md-render.nvim.
--
-- Why not snacks.image / diagram.nvim / image.nvim?
-- Those all try to overlay the rendered image on top of the source fenced
-- code block. snacks issue #1384 ("mermaid blocks text behind it") was
-- closed as "not planned" — overlap-in-source is a fundamental limitation
-- of the current kitty-graphics-protocol design in nvim.
--
-- md-render takes the opposite approach: render mode is a *separate buffer*.
-- Normal mode -> auto-swap to render (full diagrams, no source text in the way).
-- Insert mode -> swap back to source so we can edit. The hop is per-buffer.
--
-- Ghostty itself is verified by the plugin; the SSH transport shim below is
-- local glue so the terminal never has to read remote-only image paths.
local function use_direct_kitty_bytes()
return true
end
local function image_debug(message)
if vim.env.MD_RENDER_IMAGE_DEBUG ~= "1" then return end
local path = vim.fn.stdpath("cache") .. "/md-render-image-debug.log"
local line = os.date("%Y-%m-%d %H:%M:%S") .. " " .. message .. "\n"
vim.fn.writefile({ line }, path, "a")
end
return {
{
"delphinus/md-render.nvim",
version = "*",
ft = { "markdown" },
dependencies = {
{ "nvim-tree/nvim-web-devicons", version = "*" },
},
init = function()
-- Ghostty does not normally forward TERM_PROGRAM /
-- GHOSTTY_RESOURCES_DIR into the remote shell. Neovim 0.12 therefore
-- cannot prove Kitty graphics support from the remote side on its own.
-- Herdr panes expose HERDR_ENV=1, but the underlying display path is
-- still Ghostty-compatible when kitty_graphics is enabled.
--
-- Keep the terminal identity as Ghostty. md-render uses that identity to
-- avoid Ghostty's flaky stored-image re-placement path during redraws.
if use_direct_kitty_bytes() and not vim.env.TERM_PROGRAM then
vim.env.TERM_PROGRAM = "ghostty"
end
-- In auto mode the render view is a separate, nomodifiable buffer, so
-- normal-mode editing (dd/x/ciw/p/…) does nothing — you'd have to toggle
-- to source first. md-render already solves this for insert-entry keys
-- (i/I/a/A/o/O): pressing one hops to the source at the mapped line and
-- replays the key (see AUTO_INSERT_KEYS in the plugin). It intentionally
-- leaves operator/normal-mode keys alone.
--
-- We extend that same hop to editing keys, scoped buffer-locally to the
-- render buffer (ft=md-render) only — the real source buffer is never
-- touched. Press dd/x/p/etc. while viewing the render and it transparently
-- drops to source on the right line and performs the edit. (Pure-motion
-- keys h/j/k/w/G/… are deliberately NOT mapped, so you can still navigate
-- the render freely.) Yank keys stay in the read-only render buffer so
-- copying text does not dismiss rendered diagrams. Visual-entry keys hop
-- to source so the resulting selection can be changed safely.
-- NOTE: the plugin sets the render buffer's filetype under
-- `eventignore=all` (preview.lua, so markdown ftplugins/treesitter don't
-- clobber the pre-rendered content), so a `FileType md-render` autocmd
-- never fires. Hook `BufEnter` instead — it fires when `toggle` swaps the
-- render buffer into the window — and guard on filetype + a buffer flag so
-- the maps install exactly once per render buffer.
local HOP_KEYS = {
"d", "D", "c", "C", "x", "X", "s", "S", "r", "R",
"p", "P", "J", ">", "<", "=", "u", "v", "V", ".",
}
local hop_group = vim.api.nvim_create_augroup("MdRenderEditHop", { clear = true })
vim.api.nvim_create_autocmd({ "BufEnter", "BufWinEnter" }, {
group = hop_group,
desc = "md-render: editing keys hop to source then replay",
callback = function(ev)
if vim.bo[ev.buf].filetype ~= "md-render" then return end
if vim.b[ev.buf].md_render_hop_installed then return end
vim.b[ev.buf].md_render_hop_installed = true
for _, key in ipairs(HOP_KEYS) do
vim.keymap.set("n", key, function()
-- Preserve a leading count and explicit register (e.g. 3dd, "ayy)
-- so the replayed operator behaves like the keystroke you typed.
local count = vim.v.count
local reg = vim.v.register
local prefix = (reg and reg ~= '"') and ('"' .. reg) or ""
if count > 0 then prefix = prefix .. tostring(count) end
-- toggle() is synchronous: on return the window already shows the
-- source buffer with the cursor on the mapped line. Feed the key
-- *synchronously* (no vim.schedule) so it enters the typeahead
-- before any follow-up keystroke. That keeps multi-key operators
-- in order — `d` becomes operator-pending and your next key (e.g.
-- the second `d`, or `w`, `ip`) completes it natively in source.
-- (The plugin schedules its insert-entry hop because those are
-- single keys that immediately switch to Insert mode; operators
-- need the ordering guarantee a sync feed provides.)
require("md-render").preview.toggle()
if key == "v" or key == "V" then
-- Enter Visual mode before the user's next queued key arrives.
-- feedkeys() would put v/V behind that key, turning `Vd` into
-- `dV`; :normal! establishes the selection synchronously.
local visual_count = count > 0 and tostring(count) or ""
vim.cmd("normal! " .. visual_count .. key)
else
vim.api.nvim_feedkeys(prefix .. key, "n", false)
end
end, {
buffer = ev.buf,
noremap = true,
silent = true,
desc = "md-render: hop to source then " .. key,
})
end
end,
})
end,
keys = {
{
"<leader>mp",
function() require("md-render").preview.show({ max_width = 140 }) end,
ft = "markdown",
desc = "Markdown preview (wide float toggle)",
},
{
"<leader>mt",
function() require("md-render").preview.show_tab({ max_width = 140 }) end,
ft = "markdown",
desc = "Markdown preview (wide tab toggle)",
},
{
"<leader>ms",
function() require("md-render").preview.split({ max_width = 140 }) end,
ft = "markdown",
desc = "Markdown source/render split",
},
{
"<leader>mr",
function() require("md-render").preview.toggle({ max_width = 140 }) end,
ft = "markdown",
desc = "Markdown render toggle in-place",
},
{ "<leader>md", "<Plug>(md-render-demo)", desc = "Markdown render demo" },
},
config = function()
local function patch_mermaid_rendering()
local img = require("md-render.image")
if img._thembones_mermaid_patch then return end
img._thembones_mermaid_patch = true
local MERMAID_SCALE = "4"
local MERMAID_ROWS = 45
local original_calc_display_size = img.calc_display_size
img.calc_display_size = function(img_w, img_h, max_cols, max_rows)
if max_rows == 25 then
max_rows = MERMAID_ROWS
end
return original_calc_display_size(img_w, img_h, max_cols, max_rows)
end
local function mermaid_cache_dir()
local dir = vim.fn.stdpath("cache") .. "/md-render/mermaid"
vim.fn.mkdir(dir, "p")
return dir
end
local function mermaid_theme_args()
local bg = vim.o.background
local hl = vim.api.nvim_get_hl(0, { name = "NormalFloat", link = false })
if not hl.bg then
hl = vim.api.nvim_get_hl(0, { name = "Normal", link = false })
end
local bg_color = hl.bg
if bg == "dark" then
return "dark", bg_color and string.format("#%06x", bg_color) or "#1e1e2e"
end
return "default", bg_color and string.format("#%06x", bg_color) or "#ffffff"
end
local function mermaid_cache_path(source)
local theme, bg_hex = mermaid_theme_args()
local key = table.concat({ source, theme, bg_hex, "scale=" .. MERMAID_SCALE }, "|")
local hash = vim.fn.sha256(key):sub(1, 16)
return mermaid_cache_dir() .. "/" .. hash .. ".png"
end
local function mmdc_cmd(input_path, output_path)
local cmd
if vim.fn.executable("mmdc") == 1 then
cmd = { "mmdc" }
elseif vim.fn.executable("npx") == 1 then
cmd = { "npx", "-y", "@mermaid-js/mermaid-cli" }
else
return nil
end
local theme, bg_hex = mermaid_theme_args()
vim.list_extend(cmd, {
"-i", input_path,
"-o", output_path,
"-t", theme,
"-b", bg_hex,
"-s", MERMAID_SCALE,
})
return cmd
end
img.get_mermaid_cached = function(source)
local path = mermaid_cache_path(source)
if vim.fn.filereadable(path) == 1 then
return path
end
return nil
end
img.render_mermaid = function(source)
local cache_path = mermaid_cache_path(source)
if vim.fn.filereadable(cache_path) == 1 then
return cache_path
end
local tmp_input = vim.fn.tempname() .. ".mmd"
local f = io.open(tmp_input, "w")
if not f then return nil end
f:write(source)
f:close()
local cmd = mmdc_cmd(tmp_input, cache_path)
if not cmd then
os.remove(tmp_input)
return nil
end
vim.system(cmd, { text = true, timeout = 30000 }):wait()
os.remove(tmp_input)
if vim.fn.filereadable(cache_path) == 1 then
return cache_path
end
return nil
end
-- Callbacks per in-flight cache path, so concurrent requests for the
-- same diagram share one mmdc process instead of racing on the output.
local mermaid_inflight = {}
img.render_mermaid_async = function(source, callback)
local cache_path = mermaid_cache_path(source)
if vim.fn.filereadable(cache_path) == 1 then
callback(cache_path)
return
end
if mermaid_inflight[cache_path] then
table.insert(mermaid_inflight[cache_path], callback)
return
end
mermaid_inflight[cache_path] = { callback }
local function finish(result)
local callbacks = mermaid_inflight[cache_path]
mermaid_inflight[cache_path] = nil
for _, cb in ipairs(callbacks) do
cb(result)
end
end
local tmp_input = vim.fn.tempname() .. ".mmd"
local f = io.open(tmp_input, "w")
if not f then
finish(nil)
return
end
f:write(source)
f:close()
local cmd = mmdc_cmd(tmp_input, cache_path)
if not cmd then
os.remove(tmp_input)
finish(nil)
return
end
vim.system(cmd, { text = true, timeout = 30000 }, function()
vim.schedule(function()
os.remove(tmp_input)
if vim.fn.filereadable(cache_path) == 1 then
finish(cache_path)
else
finish(nil)
end
end)
end)
end
end
patch_mermaid_rendering()
-- md-render only processes placements near the viewport on first paint,
-- and its WinScrolled retry loop skips placements without a `path`.
-- Uncached mermaid placements carry only `mermaid_source`, so a diagram
-- that starts off-screen never renders at all, no matter how far you
-- scroll. Eagerly kick off rendering for every mermaid placement when
-- images are set up: the plugin's own rebuild-on-complete then swaps in
-- a normal `path` placement, which its retry loop handles.
local function patch_offscreen_mermaid()
local du = require("md-render.display_utils")
if du._thembones_offscreen_mermaid_patch then return end
du._thembones_offscreen_mermaid_patch = true
local original_setup_images = du.setup_images
du.setup_images = function(win, content, ns, opts)
local state = original_setup_images(win, content, ns, opts)
if state and state.placements and state.process_placement then
vim.schedule(function()
for _, placement in ipairs(state.placements) do
if placement.mermaid_source then
local ok, err = pcall(state.process_placement, placement)
if not ok then
image_debug("offscreen mermaid eager render err=" .. tostring(err))
end
end
end
end)
end
return state
end
end
patch_offscreen_mermaid()
-- md-render's Ghostty path intentionally re-transmits images on every
-- placement so redraws stay visible. Its stock transport uses `t=f`,
-- which means "the terminal reads this file path". Over SSH that path is
-- remote-only, so Ghostty cannot load it. In Herdr/Ghostty, file-path
-- transfer can reserve the image area without painting the pixels, so
-- avoid that path there too. Keep Ghostty's placement model, but switch
-- transport to direct PNG bytes (`t=d`).
if use_direct_kitty_bytes() then
image_debug("direct-byte override enabled")
local img = require("md-render.image")
local tty = require("md-render.tty")
local original_begin_batch = img.begin_batch
local original_flush_batch = img.flush_batch
local original_clear_all = img.clear_all
local original_delete_image = img.delete_image
local original_delete_images = img.delete_images
local original_put_image = img.put_image
local original_get_tty_path = tty.get_tty_path
local CHUNK = 4096
local next_image_id = 800000
local direct_images = {}
local batch_depth = 0
local after_flush = {}
tty.get_tty_path = function()
if vim.env.SSH_TTY and vim.fn.filereadable(vim.env.SSH_TTY) == 1 then
return vim.env.SSH_TTY
end
return original_get_tty_path()
end
local function queue_terminal(data)
image_debug("queue_terminal bytes=" .. tostring(#data) .. " batch_depth=" .. tostring(batch_depth))
if batch_depth > 0 then
table.insert(after_flush, data)
else
vim.api.nvim_ui_send(data)
end
end
local function flush_direct_queue()
if batch_depth == 0 and #after_flush > 0 then
image_debug("flush_direct_queue chunks=" .. tostring(#after_flush))
for _, data in ipairs(after_flush) do
vim.api.nvim_ui_send(data)
end
after_flush = {}
end
end
img.begin_batch = function(...)
batch_depth = batch_depth + 1
return original_begin_batch(...)
end
img.flush_batch = function(...)
local result = original_flush_batch(...)
batch_depth = math.max(0, batch_depth - 1)
flush_direct_queue()
return result
end
local function next_direct_id()
next_image_id = next_image_id + 1
return next_image_id
end
local function read_png_base64(path)
local f = io.open(path, "rb")
if not f then
return nil
end
local data = f:read("*a")
f:close()
return vim.base64.encode(data)
end
local function send_chunked(header, payload)
image_debug("send_chunked header=" .. header .. " payload_len=" .. tostring(#payload))
local i = 1
local first = true
while i <= #payload do
local piece = payload:sub(i, i + CHUNK - 1)
i = i + CHUNK
local more = i <= #payload and 1 or 0
local hdr
if first then
hdr = string.format("%s,m=%d", header, more)
first = false
else
hdr = string.format("m=%d,q=2", more)
end
queue_terminal("\27_G" .. hdr .. ";" .. piece .. "\27\\")
end
end
local function crop_params_for(win, row, col, display_cols, display_rows, img_w, img_h)
if not vim.api.nvim_win_is_valid(win) then return nil end
local win_pos = vim.api.nvim_win_get_position(win)
local wininfo = vim.fn.getwininfo(win)[1]
if not wininfo then return nil end
local win_height = wininfo.height
local topline = wininfo.topline - 1
local leftcol = wininfo.leftcol or 0
local textoff = wininfo.textoff or 0
local img_end_row = row + display_rows - 1
if img_end_row < topline or row >= topline + win_height then return nil end
local visual_row = row - topline
local border_left_width = 0
local border_top_height = 0
local ok_cfg, win_cfg = pcall(vim.api.nvim_win_get_config, win)
if ok_cfg and win_cfg.border then
local border = win_cfg.border
if type(border) == "table" then
local left = border[8]
if type(left) == "table" then left = left[1] end
if left and left ~= "" then
border_left_width = vim.api.nvim_strwidth(left)
end
local top = border[2]
if type(top) == "table" then top = top[1] end
if top and top ~= "" then
border_top_height = 1
end
elseif border ~= "none" and border ~= "" then
border_left_width = vim.api.nvim_strwidth("|")
border_top_height = 1
end
end
local visual_col = col - leftcol
local visible_text_cols = vim.api.nvim_win_get_width(win) - textoff
local img_end_col = col + display_cols - 1
if img_end_col < leftcol or col >= leftcol + visible_text_cols then return nil end
local screen_col = win_pos[2] + visual_col + border_left_width + textoff + 1
local src_x, src_y, src_w, src_h
if visual_col < 0 then
local hidden_cols = -visual_col
if img_w then
src_x = math.floor(img_w * hidden_cols / display_cols)
src_w = img_w - src_x
end
display_cols = display_cols - hidden_cols
visual_col = 0
screen_col = win_pos[2] + border_left_width + textoff + 1
end
if visual_row < 0 then
local hidden_rows = -visual_row
if img_h then
src_y = math.floor(img_h * hidden_rows / display_rows)
src_h = img_h - src_y
end
display_rows = display_rows - hidden_rows
visual_row = 0
end
local winbar_height = 0
local ok_wb, wb = pcall(function() return vim.wo[win].winbar end)
if ok_wb and wb and wb ~= "" then
winbar_height = 1
end
local screen_row = wininfo.winrow + visual_row + border_top_height + winbar_height
local visible_rows = win_height - visual_row
if visible_rows <= 0 then return nil end
if display_rows > visible_rows and img_h then
local remaining_h = src_h or img_h
src_h = math.floor(remaining_h * visible_rows / display_rows)
display_rows = visible_rows
end
local visible_cols = visible_text_cols - visual_col
if visible_cols <= 0 then return nil end
if display_cols > visible_cols and img_w then
local remaining_w = src_w or img_w
src_w = math.floor(remaining_w * visible_cols / display_cols)
display_cols = visible_cols
end
local crop_params = ""
if (src_x or src_y or src_w or src_h) and img_w and img_h then
crop_params = string.format(
",x=%d,y=%d,w=%d,h=%d",
src_x or 0,
src_y or 0,
src_w or img_w,
src_h or img_h
)
end
return {
screen_col = screen_col,
screen_row = screen_row,
display_cols = display_cols,
display_rows = display_rows,
crop_params = crop_params,
}
end
local function transmit_direct(path, callback)
image_debug("transmit_direct path=" .. tostring(path))
local payload = read_png_base64(path)
if not payload then
image_debug("transmit_direct no payload")
callback(nil)
return
end
local id = next_direct_id()
local img_w, img_h = img.image_dimensions(path)
image_debug("transmit_direct id=" .. tostring(id) .. " dims=" .. tostring(img_w) .. "x" .. tostring(img_h))
direct_images[id] = {
path = path,
payload = payload,
width = img_w,
height = img_h,
}
send_chunked(
string.format("a=t,f=100,t=d,i=%d,q=2", id),
payload
)
callback(id, img_w, img_h)
end
-- Herdr re-encodes every visible placement as base64 RGBA of the FULL
-- source image (bytes ≈ w*h*4*4/3, ~16x the PNG size) when forwarding
-- frames to its client, and drops ALL graphics for any frame whose
-- total exceeds 32MiB ("dropping oversized graphics payload" in
-- herdr-server.log). A scale-4 mermaid PNG alone is ~60MB on that
-- wire, so nothing paints. Cap source pixels inside herdr panes; the
-- render view is ≤45 rows so the downscale is visually lossless.
local HERDR_MAX_PIXELS = 1500000
local function herdr_scaled_path(png_path, max_dim)
local mtime = vim.fn.getftime(png_path)
local key = png_path .. "|" .. tostring(mtime) .. "|" .. tostring(max_dim)
local dir = vim.fn.stdpath("cache") .. "/md-render/herdr-scaled"
vim.fn.mkdir(dir, "p")
return dir .. "/" .. vim.fn.sha256(key):sub(1, 16) .. ".png"
end
-- Downscale tool detection, same priority as md-render's own
-- find_convert_tool(): sips (macOS) → ffmpeg → magick (IM7) →
-- convert (IM6, Linux distro ImageMagick installs).
local _downscale_tool = nil
local _downscale_checked = false
local function find_downscale_tool()
if _downscale_checked then return _downscale_tool end
_downscale_checked = true
if vim.fn.has("mac") == 1 and vim.fn.executable("sips") == 1 then
_downscale_tool = "sips"
elseif vim.fn.executable("ffmpeg") == 1 then
_downscale_tool = "ffmpeg"
elseif vim.fn.executable("magick") == 1 then
_downscale_tool = "magick"
elseif vim.fn.executable("convert") == 1 then
_downscale_tool = "convert"
end
return _downscale_tool
end
local function build_downscale_cmd(tool, src, dst, max_dim)
local dim = tostring(max_dim)
if tool == "sips" then
return { "sips", "-Z", dim, src, "--out", dst }
elseif tool == "ffmpeg" then
return {
"ffmpeg", "-y", "-i", src,
"-vframes", "1",
"-vf", "scale='min(" .. dim .. ",iw)':'min(" .. dim .. ",ih)':force_original_aspect_ratio=decrease",
dst,
}
else -- magick / convert share the same argument shape
return { tool, src, "-resize", dim .. "x" .. dim .. ">", dst }
end
end
local function downscale_for_herdr(png_path, callback)
if vim.env.HERDR_ENV ~= "1" then
callback(png_path)
return
end
local w, h = img.image_dimensions(png_path)
if not w or not h or w * h <= HERDR_MAX_PIXELS then
callback(png_path)
return
end
local scale = math.sqrt(HERDR_MAX_PIXELS / (w * h))
local max_dim = math.floor(math.max(w, h) * scale)
local out = herdr_scaled_path(png_path, max_dim)
if vim.fn.filereadable(out) == 1 then
image_debug("downscale_for_herdr cached " .. out)
callback(out)
return
end
-- If no tool is available or it fails, fall back to the original
-- (which herdr will drop, same as before this patch).
local tool = find_downscale_tool()
if not tool then
image_debug("downscale_for_herdr no downscale tool")
callback(png_path)
return
end
vim.system(
build_downscale_cmd(tool, png_path, out, max_dim),
{ text = true, timeout = 15000 },
function()
vim.schedule(function()
if vim.fn.filereadable(out) == 1 then
image_debug("downscale_for_herdr " .. tool .. " " .. w .. "x" .. h .. " -> max_dim=" .. max_dim)
callback(out)
else
image_debug("downscale_for_herdr " .. tool .. " failed")
callback(png_path)
end
end)
end
)
end
img.transmit_image_async = function(path, callback)
image_debug("transmit_image_async path=" .. tostring(path))
if not img.supports_kitty() then
image_debug("transmit_image_async no kitty support")
callback(nil)
return
end
img.ensure_png_async(path, function(png_path)
if not png_path then
image_debug("transmit_image_async no png_path")
callback(nil)
return
end
image_debug("transmit_image_async png_path=" .. tostring(png_path))
downscale_for_herdr(png_path, function(final_path)
transmit_direct(final_path, callback)
end)
end)
end
img.put_image = function(image_id, win, row, col, display_cols, display_rows, anim_path, img_w, img_h)
image_debug(
"put_image id=" .. tostring(image_id)
.. " row=" .. tostring(row)
.. " col=" .. tostring(col)
.. " cols=" .. tostring(display_cols)
.. " rows=" .. tostring(display_rows)
.. " anim=" .. tostring(anim_path)
)
local direct = direct_images[image_id]
if not direct or anim_path then
image_debug("put_image fallback direct=" .. tostring(direct ~= nil) .. " anim=" .. tostring(anim_path))
return original_put_image(image_id, win, row, col, display_cols, display_rows, anim_path, img_w, img_h)
end
if not img.supports_kitty() then return end
local placement = crop_params_for(
win,
row,
col,
display_cols,
display_rows,
img_w or direct.width,
img_h or direct.height
)
if not placement then
image_debug("put_image no placement")
return
end
image_debug(
"put_image placement screen="
.. tostring(placement.screen_row)
.. ","
.. tostring(placement.screen_col)
.. " cells="
.. tostring(placement.display_cols)
.. "x"
.. tostring(placement.display_rows)
.. " crop="
.. tostring(placement.crop_params)
)
queue_terminal("\27[s")
queue_terminal(string.format("\27[%d;%dH", placement.screen_row, placement.screen_col))
send_chunked(
string.format(
"a=T,f=100,t=d,i=%d,c=%d,r=%d%s,C=1,q=2",
image_id,
placement.display_cols,
placement.display_rows,
placement.crop_params
),
direct.payload
)
queue_terminal("\27[u")
end
img.clear_all = function(...)
direct_images = {}
return original_clear_all(...)
end
img.delete_image = function(image_id, ...)
direct_images[image_id] = nil
return original_delete_image(image_id, ...)
end
img.delete_images = function(image_ids, ...)
for _, image_id in ipairs(image_ids or {}) do
direct_images[image_id] = nil
end
return original_delete_images(image_ids, ...)
end
end
local function mermaid_sources(buf)
local sources = {}
local in_mermaid = false
local block = {}
for _, line in ipairs(vim.api.nvim_buf_get_lines(buf, 0, -1, false)) do
if not in_mermaid then
if line:match("^%s*```%s*mermaid%s*$") then
in_mermaid = true
block = {}
end
elseif line:match("^%s*```%s*$") then
if #block > 0 then
table.insert(sources, table.concat(block, "\n"))
end
in_mermaid = false
block = {}
else
table.insert(block, line)
end
end
return sources
end
local function prime_mermaid_cache(buf)
if not vim.api.nvim_buf_is_valid(buf) then return end
local ok, image = pcall(require, "md-render.image")
if not ok or not image.has_mmdc() then return end
for _, source in ipairs(mermaid_sources(buf)) do
if not image.get_mermaid_cached(source) then
image.render_mermaid(source)
end
end
end
local function ensure_md_render_auto(buf)
if not vim.api.nvim_buf_is_valid(buf) then return end
if vim.bo[buf].filetype ~= "markdown" then return end
if not vim.b[buf].md_render_auto then
local cache_group = vim.api.nvim_create_augroup("MdRenderMermaidCache" .. buf, { clear = true })
vim.api.nvim_create_autocmd({ "InsertLeave", "BufWritePost" }, {
group = cache_group,
buffer = buf,
callback = function()
prime_mermaid_cache(buf)
end,
desc = "Prime md-render Mermaid cache before redraw",
})
end
prime_mermaid_cache(buf)
if vim.b[buf].md_render_auto then return end
local ok, md = pcall(require, "md-render")
if ok then
local win = vim.api.nvim_get_current_win()
local ok_state, state = pcall(vim.api.nvim_win_get_var, win, "md_render_state")
if ok_state and type(state) == "table" and state.source_buf ~= buf then
pcall(vim.api.nvim_win_del_var, win, "md_render_state")
end
pcall(md.preview.auto_on, { max_width = 140 })
end
end
local group = vim.api.nvim_create_augroup("MdRenderMarkdownAuto", { clear = true })
vim.api.nvim_create_autocmd("FileType", {
group = group,
pattern = { "markdown", "*.md", "*.markdown" },
callback = function(ev)
-- Defer so the FileType pass that loads md-render completes first.
vim.schedule(function()
if vim.api.nvim_get_current_buf() == ev.buf then
ensure_md_render_auto(ev.buf)
end
end)
end,
desc = "Enable md-render auto mode for markdown buffers",
})
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
vim.schedule(function()
ensure_md_render_auto(buf)
end)
end
end,
},
-- Disable the previous attempts so they don't fight md-render for the same
-- terminal cells (the "1 frame then gone" / "behind the text" symptoms).
{ "3rd/image.nvim", enabled = false },
{ "3rd/diagram.nvim", enabled = false },
{
"folke/snacks.nvim",
opts = function(_, opts)
opts.image = vim.tbl_deep_extend("force", opts.image or {}, {
enabled = false,
})
return opts
end,
},
}
@@ -0,0 +1,17 @@
-- Override LazyVim's indent-blankline for VS Code-like indent guides
return {
{
"lukas-reineke/indent-blankline.nvim",
main = "ibl", -- v3 module name
opts = {
indent = {
char = "│",
},
scope = {
enabled = true,
show_start = false,
show_end = false,
},
},
},
}
@@ -0,0 +1,12 @@
return {
{
"mfussenegger/nvim-lint",
optional = true,
opts = {
linters_by_ft = {
markdown = {}, -- no linters for .md
["markdown.mdx"] = {}, -- no linters for .mdx
},
},
},
}
@@ -0,0 +1,27 @@
return {
-- Disable markdown diagnostics/linting but keep LSP for formatting
{
"neovim/nvim-lspconfig",
opts = {
servers = {
marksman = {
-- Keep marksman for markdown LSP features like formatting
handlers = {
-- Disable diagnostics from marksman
["textDocument/publishDiagnostics"] = function() end,
},
},
},
},
},
-- Also disable any lint-related diagnostics for markdown files
{
"mfussenegger/nvim-lint",
optional = true,
opts = {
linters_by_ft = {
markdown = {},
},
},
},
}
@@ -0,0 +1,52 @@
-- VS Code-style minimap using neominimap.nvim
return {
{
"Isrothy/neominimap.nvim",
version = "v3.*.*",
lazy = false,
keys = {
{ "<leader>um", "<cmd>Neominimap Toggle<cr>", desc = "Toggle minimap" },
{ "<leader>uf", "<cmd>Neominimap Focus<cr>", desc = "Focus minimap" },
},
init = function()
vim.g.neominimap = {
auto_enable = true,
-- Higher resolution (lower = denser)
x_multiplier = 2,
y_multiplier = 1,
layout = "float",
float = {
minimap_width = 8,
window_border = "none",
},
-- Enable mouse clicks
click = {
enabled = true,
auto_switch_focus = true,
},
-- Annotations
diagnostic = {
enabled = true,
mode = "line", -- Highlight full line
},
git = {
enabled = true,
mode = "line", -- Full line highlight for git changes
},
search = {
enabled = true,
mode = "line",
},
treesitter = {
enabled = true,
},
exclude_filetypes = { "help", "neo-tree", "lazy", "mason", "dashboard" },
}
end,
},
}
@@ -0,0 +1,115 @@
-- Inline LLM (ghost-text) completion tuned for writing markdown/prose.
-- Reuses ZAI_API_KEY from the environment (same key the :AI command uses).
-- Suggestions are virtual text, auto-triggered behind a debounce delay.
local PROSE_FT = {
markdown = true,
["md-render"] = true,
text = true,
rst = true,
tex = true,
gitcommit = true,
mail = true,
}
return {
"milanglacier/minuet-ai.nvim",
dependencies = { "nvim-lua/plenary.nvim" },
-- Load eagerly: minuet only sets its buffer-local auto-trigger flag from a
-- FileType autocmd registered in setup(). Lazy-loading on InsertEnter would
-- register that autocmd *after* the current buffer's FileType already fired,
-- so virtualtext would never auto-trigger on the buffer you're editing.
lazy = false,
opts = {
provider = "openai_compatible",
-- One natural continuation, not three alternatives — prose has a single
-- "next sentence", and fewer candidates means less to read past.
n_completions = 1,
-- Give the model more of the text *before* the cursor (default 0.75).
context_ratio = 0.85,
-- Cap how much surrounding text is sent. Smaller = lower, steadier latency
-- (the full default of 16000 chars is overkill for prose continuation and
-- inflates time-to-first-token). 4000 chars ≈ plenty of preceding context.
context_window = 4000,
-- Delay knobs: wait for an idle pause, and rate-limit requests. Groq is fast
-- and deterministic, so we can afford a snappier debounce than the cloud
-- defaults; throttle stays moderate to respect the free-tier rate limit.
throttle = 800, -- min ms between requests
debounce = 400, -- ms of idle typing before a request fires
provider_options = {
openai_compatible = {
api_key = "GROQ_API_KEY", -- env var NAME; minuet reads it at runtime
name = "Groq",
-- Groq's LPU inference gives ~80-100ms time-to-first-token with very low
-- variance — that consistency is the whole reason for moving off Z.AI's
-- hit-or-miss coding endpoint. OpenAI-compatible, so the schema is unchanged.
end_point = "https://api.groq.com/openai/v1/chat/completions",
-- llama-3.3-70b is Groq's flagship general model: strong prose, still fast
-- on their hardware. If you ever want the absolute lowest latency, swap to
-- "llama-3.1-8b-instant" (faster, slightly weaker prose).
model = "llama-3.3-70b-versatile",
-- Swap minuet's "code completion engine" system prompt for a prose one
-- in writing filetypes; keep the default (string) for code. Guidelines
-- and few-shots are left at defaults so minuet's <endCompletion> parsing
-- still works.
system = {
prompt = function()
if PROSE_FT[vim.bo.filetype] then
return [[
You are a prose writing assistant embedded in a text editor. Continue the
author's text at the <cursorPosition> marker naturally and fluently, matching
their voice, tone, and sentence rhythm.
- Output ONLY the continuation text — no preamble, no explanation.
- Never wrap the output in markdown code fences or backticks.
- Do not add headings, bullets, or numbering unless the surrounding text
already uses them.
- Keep it to at most 1-3 sentences and stop at a natural boundary.]]
end
return require("minuet.config").default_system_prefix_first.prompt
end,
},
optional = {
-- Short, focused completions. (No `thinking` field here — that was a
-- Z.AI-specific param; Groq's Llama models 400 on unknown body fields.)
max_tokens = 96,
temperature = 0.3,
},
},
},
virtualtext = {
auto_trigger_ft = { "*" },
-- Alt keymaps are kept as a secondary path, but the primary accept key is
-- <Tab>, wired in config() below (Alt+* doesn't fire in Ghostty without
-- macos-option-as-alt, and Tab is the natural "accept" key for prose).
keymap = {
accept = "<A-y>", -- accept full suggestion
accept_line = "<A-l>", -- accept one line
prev = "<A-[>",
next = "<A-]>",
dismiss = "<A-e>",
},
},
},
config = function(_, opts)
require("minuet").setup(opts)
-- Smart <Tab>: accept a visible minuet ghost-text suggestion; otherwise fall
-- back to the exact super-tab behavior we had before (jump an active snippet,
-- else insert a literal Tab). Blink's menu navigates with <C-n>/<C-p>, not
-- Tab, so this doesn't fight the completion popup.
local vt = require("minuet.virtualtext").action
vim.keymap.set("i", "<Tab>", function()
if vt.is_visible() then
vt.accept()
elseif vim.snippet.active({ direction = 1 }) then
vim.snippet.jump(1)
else
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes("<Tab>", true, false, true),
"n",
false
)
end
end, { desc = "minuet: accept suggestion, else snippet jump / Tab" })
end,
}
@@ -0,0 +1,3 @@
-- Navigation enhancement plugins
-- (neoscroll removed due to conflicts with smear-cursor)
return {}
@@ -0,0 +1,14 @@
return {
"nvim-neo-tree/neo-tree.nvim",
opts = function(_, opts)
opts.filesystem = opts.filesystem or {}
opts.filesystem.filtered_items = opts.filesystem.filtered_items or {}
-- make sure .log files are visible
opts.filesystem.filtered_items.hide_gitignored = false
opts.filesystem.filtered_items.hide_dotfiles = false
opts.filesystem.filtered_items.always_show = opts.filesystem.filtered_items.always_show or {}
table.insert(opts.filesystem.filtered_items.always_show, "*.log")
end,
}
@@ -0,0 +1,4 @@
-- Disable noice.nvim (causing treesitter errors)
return {
{ "folke/noice.nvim", enabled = false },
}
@@ -0,0 +1,12 @@
return {
"nvim-tree/nvim-tree.lua",
dependencies = {
"nvim-tree/nvim-web-devicons",
},
keys = {
{ "<leader>e", "<cmd>NvimTreeToggle<cr>", desc = "Toggle file explorer" },
},
config = function()
require("nvim-tree").setup({})
end,
}
@@ -0,0 +1,35 @@
return {
"nvim-telescope/telescope.nvim",
opts = {
defaults = {
-- Don't respect .gitignore - show logs and temp files
file_ignore_patterns = {
"node_modules/",
"__pycache__/",
".git/",
".pytest_cache/",
"%.pyc$",
"%.pyo$",
},
-- Make sure to not use git_files by default
vimgrep_arguments = {
"rg",
"--color=never",
"--no-heading",
"--with-filename",
"--line-number",
"--column",
"--smart-case",
-- Don't respect gitignore
"--no-ignore",
},
},
pickers = {
find_files = {
-- Show hidden files and don't respect gitignore
hidden = true,
no_ignore = true,
},
},
},
}
@@ -0,0 +1,14 @@
return {
{
"ThePrimeagen/refactoring.nvim",
dependencies = {
"nvim-lua/plenary.nvim",
"nvim-treesitter/nvim-treesitter",
"lewis6991/async.nvim",
},
ft = { "lua", "python", "javascript", "typescript", "go", "c", "cpp", "java", "php", "ruby" },
config = function()
require("refactoring").setup()
end,
},
}
@@ -0,0 +1,58 @@
-- Enhanced search and replace functionality
return {
-- Better search highlighting with match counts
-- Shows "X/Y" match indicator and improves n/N navigation
{
"kevinhwang91/nvim-hlslens",
event = "VeryLazy",
enabled = true, -- Set to false to disable
keys = {
{ "n", [[<Cmd>execute('normal! ' . v:count1 . 'n')<CR><Cmd>lua require('hlslens').start()<CR>]], desc = "Next search result" },
{ "N", [[<Cmd>execute('normal! ' . v:count1 . 'N')<CR><Cmd>lua require('hlslens').start()<CR>]], desc = "Previous search result" },
{ "*", [[*<Cmd>lua require('hlslens').start()<CR>]], desc = "Search word under cursor" },
{ "#", [[#<Cmd>lua require('hlslens').start()<CR>]], desc = "Search word under cursor (backward)" },
{ "g*", [[g*<Cmd>lua require('hlslens').start()<CR>]], desc = "Search word under cursor (no boundary)" },
{ "g#", [[g#<Cmd>lua require('hlslens').start()<CR>]], desc = "Search word under cursor (no boundary, backward)" },
},
opts = {
calm_down = true,
nearest_only = true,
nearest_float_when = "always",
},
},
-- Advanced search and replace with multiple strategies
-- Provides visual search/replace interface
-- Use :SearchReplaceSingleBuffer, :SearchReplaceMultiBuffer
{
"roobert/search-replace.nvim",
cmd = {
"SearchReplaceSingleBuffer",
"SearchReplaceMultiBuffer",
"SearchReplaceWithinVisualSelection",
"SearchReplaceWithinVisualSelectionCWord",
},
enabled = true, -- Set to false to disable
opts = {
default_replace_single_buffer_options = "gcI",
default_replace_multi_buffer_options = "egcI",
},
keys = {
-- Single buffer replacements
{ "<leader>rs", "<CMD>SearchReplaceSingleBufferSelections<CR>", desc = "Search/Replace Selections", mode = "v" },
{ "<leader>ro", "<CMD>SearchReplaceSingleBufferOpen<CR>", desc = "Search/Replace Open" },
{ "<leader>rw", "<CMD>SearchReplaceSingleBufferCWord<CR>", desc = "Search/Replace Word" },
{ "<leader>rW", "<CMD>SearchReplaceSingleBufferCWORD<CR>", desc = "Search/Replace WORD" },
{ "<leader>re", "<CMD>SearchReplaceSingleBufferCExpr<CR>", desc = "Search/Replace Expr" },
{ "<leader>rf", "<CMD>SearchReplaceSingleBufferCFile<CR>", desc = "Search/Replace File" },
-- Multi buffer replacements
{ "<leader>rbs", "<CMD>SearchReplaceMultiBufferSelections<CR>", desc = "Search/Replace Selections (Multi)", mode = "v" },
{ "<leader>rbo", "<CMD>SearchReplaceMultiBufferOpen<CR>", desc = "Search/Replace Open (Multi)" },
{ "<leader>rbw", "<CMD>SearchReplaceMultiBufferCWord<CR>", desc = "Search/Replace Word (Multi)" },
{ "<leader>rbW", "<CMD>SearchReplaceMultiBufferCWORD<CR>", desc = "Search/Replace WORD (Multi)" },
{ "<leader>rbe", "<CMD>SearchReplaceMultiBufferCExpr<CR>", desc = "Search/Replace Expr (Multi)" },
{ "<leader>rbf", "<CMD>SearchReplaceMultiBufferCFile<CR>", desc = "Search/Replace File (Multi)" },
},
},
}
@@ -0,0 +1,9 @@
return {
"sphamba/smear-cursor.nvim",
opts = {
smear_between_buffers = true,
smear_between_neighbor_lines = true,
scroll_buffer_space = true,
smear_insert_mode = true,
},
}
@@ -0,0 +1,273 @@
-- Colorscheme/theme plugins
return {
-- Catppuccin
{
"catppuccin/nvim",
name = "catppuccin",
lazy = false,
priority = 1000,
},
-- Tokyo Night
{
"folke/tokyonight.nvim",
lazy = false,
priority = 1000,
},
-- Kanagawa
{
"rebelot/kanagawa.nvim",
lazy = false,
priority = 1000,
},
-- Rose Pine
{
"rose-pine/neovim",
name = "rose-pine",
lazy = false,
priority = 1000,
},
-- Nightfox family (Nightfox, Nordfox, Dawnfox, Duskfox, Carbonfox, Terafox)
{
"EdenEast/nightfox.nvim",
lazy = false,
priority = 1000,
},
-- Gruvbox
{
"ellisonleao/gruvbox.nvim",
lazy = false,
priority = 1000,
},
-- Gruvbox Material
{
"sainnhe/gruvbox-material",
lazy = false,
priority = 1000,
},
-- Nord
{
"shaunsingh/nord.nvim",
lazy = false,
priority = 1000,
},
-- Nordic
{
"AlexvZyl/nordic.nvim",
lazy = false,
priority = 1000,
},
-- Everforest
{
"neanias/everforest-nvim",
lazy = false,
priority = 1000,
},
-- Dracula
{
"Mofiqul/dracula.nvim",
lazy = false,
priority = 1000,
},
-- OneDark
{
"navarasu/onedark.nvim",
lazy = false,
priority = 1000,
},
-- One Dark Pro
{
"olimorris/onedarkpro.nvim",
lazy = false,
priority = 1000,
},
-- Solarized
{
"maxmx03/solarized.nvim",
lazy = false,
priority = 1000,
},
-- Monokai Pro
{
"loctvl842/monokai-pro.nvim",
lazy = false,
priority = 1000,
},
-- Cyberdream
{
"scottmckendry/cyberdream.nvim",
lazy = false,
priority = 1000,
},
-- Oxocarbon
{
"nyoom-engineering/oxocarbon.nvim",
lazy = false,
priority = 1000,
},
-- Melange
{
"savq/melange-nvim",
lazy = false,
priority = 1000,
},
-- Nightfly
{
"bluz71/vim-nightfly-colors",
name = "nightfly",
lazy = false,
priority = 1000,
},
-- Moonfly
{
"bluz71/vim-moonfly-colors",
name = "moonfly",
lazy = false,
priority = 1000,
},
-- Sonokai
{
"sainnhe/sonokai",
lazy = false,
priority = 1000,
},
-- Edge
{
"sainnhe/edge",
lazy = false,
priority = 1000,
},
-- Ayu
{
"Shatur/neovim-ayu",
lazy = false,
priority = 1000,
},
-- Material
{
"marko-cerovac/material.nvim",
lazy = false,
priority = 1000,
},
-- Palenight
{
"drewtempelmeyer/palenight.vim",
lazy = false,
priority = 1000,
},
-- GitHub theme
{
"projekt0n/github-nvim-theme",
lazy = false,
priority = 1000,
},
-- Tokyodark
{
"tiagovla/tokyodark.nvim",
lazy = false,
priority = 1000,
},
-- Darkplus (VS Code dark theme)
{
"lunarvim/darkplus.nvim",
lazy = false,
priority = 1000,
},
-- Poimandres
{
"olivercederborg/poimandres.nvim",
lazy = false,
priority = 1000,
},
-- Flow
{
"0xstepit/flow.nvim",
lazy = false,
priority = 1000,
},
-- Modus themes
{
"miikanissi/modus-themes.nvim",
lazy = false,
priority = 1000,
},
-- Zenbones
{
"mcchrish/zenbones.nvim",
dependencies = "rktjmp/lush.nvim",
lazy = false,
priority = 1000,
},
-- Apprentice
{
"romainl/Apprentice",
lazy = false,
priority = 1000,
},
-- Jellybeans
{
"nanotech/jellybeans.vim",
lazy = false,
priority = 1000,
},
-- Tender
{
"jacoborus/tender.vim",
lazy = false,
priority = 1000,
},
-- Horizon
{
"ntk148v/vim-horizon",
lazy = false,
priority = 1000,
},
-- Moonlight
{
"shaunsingh/moonlight.nvim",
lazy = false,
priority = 1000,
},
-- Lackluster
{
"slugbyte/lackluster.nvim",
lazy = false,
priority = 1000,
},
}
@@ -0,0 +1,162 @@
-- UI enhancement plugins for better visual experience
return {
-- Render markdown with better formatting in buffers
{
"MeanderingProgrammer/render-markdown.nvim",
ft = "markdown",
enabled = true,
dependencies = { "nvim-treesitter/nvim-treesitter", "nvim-tree/nvim-web-devicons" },
opts = {
render_modes = { "n", "i", "c", "t" },
heading = {
enabled = true,
sign = true,
icons = { "󰲡 ", "󰲣 ", "󰲥 ", "󰲧 ", "󰲩 ", "󰲫 " },
},
code = {
enabled = true,
sign = true,
style = "normal",
width = "block",
},
bullet = {
enabled = true,
icons = { "●", "○", "◆", "◇" },
},
pipe_table = {
cell = "trimmed",
},
},
},
-- Highlight function arguments with different colors
-- Makes it easier to distinguish parameters
{
"m-demare/hlargs.nvim",
event = "VeryLazy",
enabled = true, -- Set to false to disable
dependencies = { "nvim-treesitter/nvim-treesitter" },
opts = {
color = "#ef9062",
highlight = {},
excluded_filetypes = {},
paint_arg_declarations = true,
paint_arg_usages = true,
performance = {
parse_delay = 1,
slow_parse_delay = 50,
max_iterations = 400,
max_concurrent_partial_parses = 30,
},
},
},
-- Dim inactive portions of code
-- Great for focusing on specific functions/blocks
-- Toggle with :Twilight
{
"folke/twilight.nvim",
cmd = { "Twilight", "TwilightEnable", "TwilightDisable" },
enabled = true, -- Set to false to disable
opts = {
dimming = {
alpha = 0.25,
color = { "Normal", "#ffffff" },
term_bg = "#000000",
inactive = false,
},
context = 10,
treesitter = true,
expand = {
"function",
"method",
"table",
"if_statement",
},
},
keys = {
{ "<leader>ut", "<cmd>Twilight<cr>", desc = "Toggle Twilight" },
},
},
-- Theme switcher with live preview
-- Browse and switch between colorschemes with instant preview
-- Open with :Themery
{
"zaldih/themery.nvim",
lazy = false, -- Load on startup to apply saved theme
priority = 1000, -- Load before other plugins
enabled = true,
keys = {
{ "<leader>uT", "<cmd>Themery<cr>", desc = "Theme Picker" },
},
config = function()
require("themery").setup({
themes = {
-- Catppuccin variants
{ name = "Catppuccin Latte", colorscheme = "catppuccin-latte" },
{ name = "Catppuccin Frappe", colorscheme = "catppuccin-frappe" },
{ name = "Catppuccin Macchiato", colorscheme = "catppuccin-macchiato" },
{ name = "Catppuccin Mocha", colorscheme = "catppuccin-mocha" },
-- Tokyo Night variants
{ name = "Tokyo Night", colorscheme = "tokyonight" },
{ name = "Tokyo Night - Night", colorscheme = "tokyonight-night" },
{ name = "Tokyo Night - Storm", colorscheme = "tokyonight-storm" },
{ name = "Tokyo Night - Day", colorscheme = "tokyonight-day" },
{ name = "Tokyo Night - Moon", colorscheme = "tokyonight-moon" },
-- Kanagawa variants
{ name = "Kanagawa", colorscheme = "kanagawa" },
{ name = "Kanagawa Wave", colorscheme = "kanagawa-wave" },
{ name = "Kanagawa Dragon", colorscheme = "kanagawa-dragon" },
{ name = "Kanagawa Lotus", colorscheme = "kanagawa-lotus" },
-- Rose Pine variants
{ name = "Rose Pine", colorscheme = "rose-pine" },
{ name = "Rose Pine Moon", colorscheme = "rose-pine-moon" },
{ name = "Rose Pine Dawn", colorscheme = "rose-pine-dawn" },
-- Nightfox variants
{ name = "Nightfox", colorscheme = "nightfox" },
{ name = "Nordfox", colorscheme = "nordfox" },
{ name = "Dawnfox", colorscheme = "dawnfox" },
{ name = "Duskfox", colorscheme = "duskfox" },
{ name = "Carbonfox", colorscheme = "carbonfox" },
{ name = "Terafox", colorscheme = "terafox" },
-- Gruvbox variants
{ name = "Gruvbox Dark", colorscheme = "gruvbox" },
{ name = "Gruvbox Material", colorscheme = "gruvbox-material" },
-- Popular themes that actually work
{ name = "Nord", colorscheme = "nord" },
{ name = "Everforest", colorscheme = "everforest" },
{ name = "Dracula", colorscheme = "dracula" },
{ name = "OneDark", colorscheme = "onedark" },
{ name = "Solarized", colorscheme = "solarized" },
{ name = "Monokai Pro", colorscheme = "monokai-pro" },
{ name = "Cyberdream", colorscheme = "cyberdream" },
{ name = "Oxocarbon", colorscheme = "oxocarbon" },
{ name = "Melange", colorscheme = "melange" },
{ name = "Nightfly", colorscheme = "nightfly" },
{ name = "Moonfly", colorscheme = "moonfly" },
{ name = "Sonokai", colorscheme = "sonokai" },
{ name = "Edge", colorscheme = "edge" },
{ name = "Ayu", colorscheme = "ayu" },
{ name = "Material", colorscheme = "material" },
{ name = "Palenight", colorscheme = "palenight" },
{ name = "GitHub Dark", colorscheme = "github_dark" },
{ name = "GitHub Dark Dimmed", colorscheme = "github_dark_dimmed" },
{ name = "GitHub Light", colorscheme = "github_light" },
{ name = "Tokyodark", colorscheme = "tokyodark" },
{ name = "Darkplus", colorscheme = "darkplus" },
{ name = "Poimandres", colorscheme = "poimandres" },
{ name = "Modus Vivendi", colorscheme = "modus" },
{ name = "Zenbones", colorscheme = "zenbones" },
{ name = "Nordic", colorscheme = "nordic" },
{ name = "Jellybeans", colorscheme = "jellybeans" },
{ name = "Tender", colorscheme = "tender" },
{ name = "Horizon", colorscheme = "horizon" },
{ name = "Moonlight", colorscheme = "moonlight" },
{ name = "Lackluster", colorscheme = "lackluster" },
},
livePreview = true, -- Apply theme while navigating
})
end,
},
}
@@ -0,0 +1,14 @@
-- Yanky is enabled via LazyVim's `coding.yanky` extra. Its default
-- `system_clipboard.sync_with_ring = true` reads the `+` register on every yank.
-- Over SSH/herdr the clipboard provider is copy-only OSC 52 (see
-- config/options.lua), so a ring<->clipboard sync is pointless and historically
-- threw when OSC 52 reads were attempted. Disable it on remote sessions only;
-- keep full sync locally where wl-copy/pbcopy make reads cheap and useful.
return {
"gbprod/yanky.nvim",
opts = {
system_clipboard = {
sync_with_ring = vim.env.SSH_CONNECTION == nil,
},
},
}