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:
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user