From 75f2079ddf60f8901bd436e1cb908c189a8a9037 Mon Sep 17 00:00:00 2001 From: Pablu Date: Wed, 29 Jul 2026 08:36:58 +0200 Subject: Add jj and herdr --- .../lua/herdr-splits/conf.lua | 236 +++++++++++++++++++++ .../lua/herdr-splits/config.lua | 146 +++++++++++++ .../lua/herdr-splits/health.lua | 46 ++++ .../lua/herdr-splits/herdr.lua | 160 ++++++++++++++ .../lua/herdr-splits/init.lua | 162 ++++++++++++++ .../lua/herdr-splits/nav.lua | 206 ++++++++++++++++++ .../lua/herdr-splits/resize.lua | 108 ++++++++++ .../lua/herdr-splits/sync.lua | 107 ++++++++++ .../lua/herdr-splits/win.lua | 154 ++++++++++++++ 9 files changed, 1325 insertions(+) create mode 100644 dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/conf.lua create mode 100644 dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/config.lua create mode 100644 dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/health.lua create mode 100644 dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/herdr.lua create mode 100644 dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/init.lua create mode 100644 dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/nav.lua create mode 100644 dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/resize.lua create mode 100644 dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/sync.lua create mode 100644 dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/win.lua (limited to 'dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua') diff --git a/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/conf.lua b/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/conf.lua new file mode 100644 index 0000000..d17b920 --- /dev/null +++ b/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/conf.lua @@ -0,0 +1,236 @@ +---herdr-splits shared config file (generated artifact) + notation translator. +---Owns everything about `herdr-splits.conf`: path resolution, reading existing +---managed values, translating Neovim key notation to Herdr chord notation, +---and writing the resolved config back (atomically). Requires nothing else +---from this plugin (avoiding a require-cycle with config.lua). +---@class HerdrSplitsConf +local M = {} + +-- Neovim modifier prefix -> Herdr modifier name. +local MOD = { C = 'ctrl', M = 'alt', A = 'alt', S = 'shift', D = 'cmd' } +-- Herdr's documented specials: lowercase the terminal key when present. +local SPECIAL = { + left = 'left', right = 'right', up = 'up', down = 'down', + enter = 'enter', ['return'] = 'enter', tab = 'tab', + esc = 'esc', escape = 'esc', space = 'space', backspace = 'backspace', + del = 'delete', delete = 'delete', home = 'home', + ['end'] = 'end', pageup = 'pageup', pagedown = 'pagedown', +} + +-- Marker line stamped into every generated conf. Its presence means the file +-- is the plugin's own output: setup() is authoritative, so read_managed() does +-- NOT adopt it back (removing an opt reverts to the default on the next start). +local MARKER = '# herdr-splits-generated-v1' + +local uv = vim.uv or vim.loop + +-- Lazily-resolved cache of the shared config path, so setup() shells out to +-- `herdr plugin config-dir` at most once per process. +local resolved_path + +---Resolve the herdr binary: a config.herdr_bin override (when the config +---module is loaded), then HERDR_BIN_PATH, then 'herdr'. pcall-required so +---conf.lua never hard-requires config.lua at load time (cycle-safe). +---@return string +local function resolve_herdr_bin() + local ok, cfg = pcall(require, 'herdr-splits.config') + if ok and cfg.herdr_bin then return cfg.herdr_bin end + return vim.env.HERDR_BIN_PATH or 'herdr' +end + +---Best-effort `herdr plugin config-dir herdr-splits` query. Returns the +---directory printed by herdr (trimmed), or nil on any failure / empty output. +---@return string|nil +local function query_config_dir() + local ok, obj = pcall(vim.system, { resolve_herdr_bin(), 'plugin', 'config-dir', 'herdr-splits' }, { text = true }) + if not ok or not obj then return nil end + local wok, res = pcall(obj.wait, obj) + if not wok or not res or res.code ~= 0 then return nil end + local out = (res.stdout or ''):gsub('^%s+', ''):gsub('%s+$', '') + return out ~= '' and out or nil +end + +---Path to the shared herdr-splits config file (same file the Herdr-side +---scripts read). Precedence: (1) HERDR_SPLITS_CONFIG; (2) HERDR_PLUGIN_CONFIG_DIR +---+ `/herdr-splits.conf`; (3) `herdr plugin config-dir herdr-splits` (via the +---herdr binary) + `/herdr-splits.conf`; (4) XDG_CONFIG_HOME / ~/.config +---fallback. Resolved once and cached, so setup() shells out at most once. +---@return string +function M.path() + if resolved_path then return resolved_path end + if vim.env.HERDR_SPLITS_CONFIG and vim.env.HERDR_SPLITS_CONFIG ~= '' then + resolved_path = vim.env.HERDR_SPLITS_CONFIG + return resolved_path + end + if vim.env.HERDR_PLUGIN_CONFIG_DIR and vim.env.HERDR_PLUGIN_CONFIG_DIR ~= '' then + resolved_path = vim.env.HERDR_PLUGIN_CONFIG_DIR .. '/herdr-splits.conf' + return resolved_path + end + local dir = query_config_dir() + if dir then + resolved_path = dir .. '/herdr-splits.conf' + return resolved_path + end + local xdg = vim.env.XDG_CONFIG_HOME + local base = (xdg and xdg:sub(1, 1) == '/') and xdg + or ((vim.env.HOME or '~') .. '/.config') + resolved_path = base .. '/herdr/plugins/config/herdr-splits/herdr-splits.conf' + return resolved_path +end + +---Translate a Neovim key notation (e.g. ``, ``, `h`) into the +---Herdr chord notation the scripts and `herdr pane send-keys` expect +---(e.g. `ctrl+h`, `alt+left`, `h`). Returns nil for modifier-only / empty +---input so callers can fall back to a per-direction default. +---@param nvim_key string +---@return string|nil +function M.to_herdr(nvim_key) + if type(nvim_key) ~= 'string' or nvim_key == '' then return nil end + if not nvim_key:match('^<.+>$') then return nvim_key:lower() end -- plain "h" -> "h" + local inner = nvim_key:sub(2, -2) + local mods, key = {}, nil + for part in inner:gmatch('[^-]+') do + local m = MOD[part:upper()] + if m then + mods[#mods + 1] = m + else + key = SPECIAL[part:lower()] or part:lower() + end + end + if not key then return nil end + mods[#mods + 1] = key + return table.concat(mods, '+') +end + +---Parse the shared config file for the managed keys only. When the file +---carries the generated MARKER it is the plugin's own output and is NOT read +---back (setup() is authoritative — removing an opt reverts to default). When +---the MARKER is absent (a legacy/hand-edited conf) the managed values are +---parsed and adopted once; the second return value is true only when at +---least one managed value was found, so the caller can emit a one-time +---migration notice. Returns `({}, false)` for a missing/marker-tagged file; +---last occurrence wins (matches the scripts' `tail -n 1`). +---@return table values, boolean adopted +function M.read_managed() + local f = io.open(M.path(), 'r') + if not f then return {}, false end + local out = { nav_keys = {}, resize_keys = {} } + for line in f:lines() do + -- Generated marker anywhere => our own output; do not adopt it back. + if line:match('^%s*' .. vim.pesc(MARKER) .. '%s*$') then + f:close() + return {}, false + end + local k, v = line:match('^%s*([%w_]+)%s*=%s*([^%s#]+)') + if k and v then + local dir = k:match('^nav_key_(%a+)$') + if dir then + out.nav_keys[dir] = v + else + dir = k:match('^resize_key_(%a+)$') + if dir then + out.resize_keys[dir] = v + elseif k == 'unzoom_on_nav' then + out.unzoom_on_nav = (v ~= 'false') + elseif k == 'nav_at_edge' then + out.nav_at_edge = (v == 'stop' and 'stop' or 'wrap') + end + end + end + end + f:close() + local adopted = false + for _, t in pairs({ out.nav_keys, out.resize_keys }) do + for _ in pairs(t) do adopted = true; break end + end + if out.unzoom_on_nav ~= nil or out.nav_at_edge ~= nil then adopted = true end + return out, adopted +end + +---Write the full managed key set to the shared config with a generated-file +---header (including the MARKER), atomically via a unique same-dir temp + +---os.rename so a script never observes a partial write and two concurrent +---Neovim instances don't share a temp inode. Skips the write entirely when the +---content is unchanged (preserves mtime, reduces multi-instance contention). +---Returns (true, nil) on success or (false, err) on failure; callers wrap in +---pcall so a write failure never crashes startup. +---@param resolved table resolved managed config (nav_keys, resize_keys, unzoom_on_nav, nav_at_edge) +---@return boolean ok, string|nil err +function M.write(resolved) + local path = M.path() + local lines = { + MARKER, + '# herdr-splits plugin config — GENERATED by herdr-splits.nvim setup().', + '# Regenerated from defaults + setup() opts on every setup(); do not edit by hand.', + '# A headerless/legacy conf is adopted once on migration (see the startup', + '# notice), then overwritten thereafter — remove an opt in setup() to revert.', + 'nav_key_left=' .. resolved.nav_keys.left, + 'nav_key_down=' .. resolved.nav_keys.down, + 'nav_key_up=' .. resolved.nav_keys.up, + 'nav_key_right=' .. resolved.nav_keys.right, + 'resize_key_left=' .. resolved.resize_keys.left, + 'resize_key_down=' .. resolved.resize_keys.down, + 'resize_key_up=' .. resolved.resize_keys.up, + 'resize_key_right=' .. resolved.resize_keys.right, + 'unzoom_on_nav=' .. (resolved.unzoom_on_nav == false and 'false' or 'true'), + 'nav_at_edge=' .. (resolved.nav_at_edge == 'stop' and 'stop' or 'wrap'), + } + local content = table.concat(lines, '\n') .. '\n' + + -- Skip when unchanged: only when the existing file's bytes match exactly. + local existing = io.open(path, 'r') + if existing then + local prev = existing:read('*a') + existing:close() + if prev == content then return true end + end + + vim.fn.mkdir(vim.fn.fnamemodify(path, ':h'), 'p') + -- Unique same-dir temp so os.rename stays same-filesystem (atomic) and two + -- Neovim instances don't collide on a shared temp inode. + local tmp = path .. '.tmp.' .. vim.fn.getpid() .. '.' .. tostring(uv.hrtime()) + local f, err = io.open(tmp, 'w') + if not f then + pcall(os.remove, tmp) + vim.notify('herdr-splits: failed to open conf temp file: ' .. tostring(err), vim.log.levels.WARN) + return false, err + end + local wok, werr = f:write(content) + -- file:close() flushes buffered output and can itself fail with a delayed + -- write error (disk full, NFS, quota). Its result MUST be checked before + -- os.rename: otherwise an incomplete temp replaces the live config and the + -- atomic-write guarantee is lost. LuaJIT returns true on success or + -- (nil, err) on a flush failure — same form as file:write, checked here too. + local cok, cerr = f:close() + if (not wok) or (not cok) then + local why = (not wok) and werr or cerr + pcall(os.remove, tmp) + vim.notify('herdr-splits: failed to write conf temp file: ' .. tostring(why), vim.log.levels.WARN) + return false, why + end + local ok, rerr = os.rename(tmp, path) + if not ok then + pcall(os.remove, tmp) + vim.notify('herdr-splits: failed to write conf: ' .. tostring(rerr), vim.log.levels.WARN) + end + return ok, rerr +end + +---Runnable self-check for the notation translator. Asserts on the documented +---mapping table; returns true on success. Intended for headless validation: +---`nvim --headless -c "lua assert(require('herdr-splits.conf')._selfcheck())" -c "qa"`. +---@return boolean +function M._selfcheck() + local cases = { + { '', 'ctrl+h' }, { '', 'ctrl+j' }, { '', 'ctrl+k' }, { '', 'ctrl+l' }, + { '', 'alt+h' }, { '', 'alt+left' }, { '', 'shift+left' }, + { '', 'cmd+x' }, { '', 'ctrl+alt+h' }, { 'h', 'h' }, { '', 'esc' }, + } + for _, c in ipairs(cases) do + local got = M.to_herdr(c[1]) + assert(got == c[2], ('to_herdr(%q)=%q want %q'):format(c[1], tostring(got), c[2])) + end + return true +end + +return M diff --git a/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/config.lua b/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/config.lua new file mode 100644 index 0000000..1763803 --- /dev/null +++ b/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/config.lua @@ -0,0 +1,146 @@ +---@class HerdrSplitsConfig +---@field default_amount number Resize amount as Herdr ratio (float, e.g. 0.03 = 3%) +---@field neovim_amount number Resize amount for native Neovim resizes (integer cells, default 3) +---@field at_edge 'wrap'|'stop'|'split'|function Neovim window-edge behavior (distinct from nav_at_edge below) +---@field ignored_buftypes string[] Buffer types ignored during resize +---@field ignored_filetypes string[] Filetypes ignored during resize +---@field move_cursor_same_row boolean Keep cursor on same screen row when moving left/right +---@field herdr_bin string|nil Path to herdr binary (auto-detected if nil) +---@field ignored_events string[] Autocmd events to ignore during resize operations +---@field auto_sync_herdr boolean|nil If true, auto-sync the Herdr-managed checkout to match this lazy commit (opt-in; default false) +---@field floating_zindex_max number Threshold below which a floating window's zindex classifies it as an embedded sidebar (default 50; Neovim's default float zindex) +---@field ignore_previewwindows boolean If true, vim.wo[winid].previewwindow windows (e.g. dadbod `.dbout`) are treated as sidebars (opt-in; default false) +--- Managed config (written to herdr-splits.conf by setup()). Pass Neovim +--- notation (e.g. ``, ``); stored/written in Herdr notation. +---@field nav_keys table Forward chords per direction: {left,down,up,right} (Neovim notation in opts; Herdr notation internally) +---@field resize_keys table Resize forward chords per direction: {left,down,up,right} (Neovim notation in opts; Herdr notation internally) +---@field unzoom_on_nav boolean If true, auto-unzoom when navigating away from a zoomed pane (default true) +---@field nav_at_edge 'wrap'|'stop' Herdr pane-boundary wrap behavior (distinct from at_edge, which is the Neovim window-edge behavior) + +-- Managed config defaults (Herdr notation). The single source of truth for +-- the key set written to herdr-splits.conf; deep-copied into M below and into +-- the setup() merge base so the two cannot drift. +local managed_defaults = { + nav_keys = { left = 'ctrl+h', down = 'ctrl+j', up = 'ctrl+k', right = 'ctrl+l' }, + resize_keys = { left = 'alt+h', down = 'alt+j', up = 'alt+k', right = 'alt+l' }, + unzoom_on_nav = true, + nav_at_edge = 'wrap', +} + +local M = { + default_amount = 0.03, + neovim_amount = 3, + at_edge = 'wrap', + ignored_buftypes = { 'nofile', 'quickfix', 'prompt', 'help', 'terminal' }, + ignored_filetypes = { + 'NvimTree', + -- sidebars + 'neo-tree', + 'snacks_dashboard', + 'snacks_explorer', + 'snacks_picker', + -- DB / REPL / data sidebars + 'dadbod-ui', + 'dbout', + -- outlines / symbols + 'aerial', + 'Outline', + -- diagnostics / quick lists + 'Trouble', + 'quickfix', + }, + move_cursor_same_row = false, + herdr_bin = nil, + ignored_events = { 'BufEnter', 'WinEnter' }, + auto_sync_herdr = false, + floating_zindex_max = 50, + ignore_previewwindows = false, + -- Managed config: deep-copied from managed_defaults (single source). + -- Stored in Herdr chord notation; setup() translates user Neovim notation + -- and writes these to herdr-splits.conf for the scripts. + nav_keys = vim.deepcopy(managed_defaults.nav_keys), + resize_keys = vim.deepcopy(managed_defaults.resize_keys), + unzoom_on_nav = managed_defaults.unzoom_on_nav, + nav_at_edge = managed_defaults.nav_at_edge, +} + +local conf = require('herdr-splits.conf') + +---Apply user configuration on top of defaults and publish the managed keys +---to the shared `herdr-splits.conf` (so the Herdr-side scripts agree). +---Idempotent in outcome: managed keys (nav_keys/resize_keys/unzoom_on_nav/nav_at_edge) +---rebuild from defaults + opts each call; non-managed opts merge into existing config. +--- +---Managed keys (nav_keys/resize_keys/unzoom_on_nav/nav_at_edge) resolve with +---precedence: defaults -> existing conf value (adopt-once) -> explicit opt. +---User chords are given in Neovim notation and translated to Herdr notation +---before merging; the resolved set is written to the conf atomically (write +---failures never crash startup). +---@param opts table|nil +function M.setup(opts) + opts = opts or {} + + -- Apply herdr_bin from opts before the first conf.path() resolution so the + -- documented precedence (config.herdr_bin -> HERDR_BIN_PATH -> 'herdr') holds. + if opts.herdr_bin ~= nil then M.herdr_bin = opts.herdr_bin end + + -- Managed opts: translate user chords nvim -> herdr, capture only what was passed. + local user = {} + if opts.nav_keys then + user.nav_keys = {} + for dir, k in pairs(opts.nav_keys) do user.nav_keys[dir] = conf.to_herdr(k) end + end + if opts.resize_keys then + user.resize_keys = {} + for dir, k in pairs(opts.resize_keys) do user.resize_keys[dir] = conf.to_herdr(k) end + end + if opts.unzoom_on_nav ~= nil then user.unzoom_on_nav = opts.unzoom_on_nav end + if opts.nav_at_edge ~= nil then user.nav_at_edge = opts.nav_at_edge end + + -- Precedence: defaults -> legacy/headerless conf (adopt once) -> explicit + -- user opts. read_managed returns {} (no adoption) once the conf carries + -- our generated marker, so removing an opt reverts to the default next start. + local existing, migrated = conf.read_managed() + local resolved = vim.deepcopy(managed_defaults) + resolved = vim.tbl_deep_extend('force', resolved, existing) + resolved = vim.tbl_deep_extend('force', resolved, user) + + M.nav_keys, M.resize_keys = resolved.nav_keys, resolved.resize_keys + M.unzoom_on_nav, M.nav_at_edge = resolved.unzoom_on_nav, resolved.nav_at_edge + + -- One-time migration notice: a headerless/legacy conf was adopted. Fires + -- once — after this setup() the conf carries the marker, so later setups + -- don't adopt. Tell the user to move these into setup() to persist them. + if migrated then + local parts = {} + for _, kind in ipairs({ 'nav_keys', 'resize_keys' }) do + for dir, v in pairs(existing[kind] or {}) do + parts[#parts + 1] = ('%s.%s=%s'):format(kind, dir, v) + end + end + if existing.unzoom_on_nav ~= nil then + parts[#parts + 1] = 'unzoom_on_nav=' .. tostring(existing.unzoom_on_nav) + end + if existing.nav_at_edge ~= nil then + parts[#parts + 1] = 'nav_at_edge=' .. existing.nav_at_edge + end + vim.notify( + 'herdr-splits: adopted legacy conf values (' .. table.concat(parts, ', ') + .. '). Add them to setup() (Neovim notation) to keep them — the conf is ' + .. 'now regenerated on every setup().', + vim.log.levels.INFO + ) + end + + -- Non-managed opts (default_amount, at_edge, ignored_filetypes, …) merge as before. + local other = vim.deepcopy(opts) + other.nav_keys, other.resize_keys = nil, nil + other.unzoom_on_nav, other.nav_at_edge = nil, nil + local merged = vim.tbl_deep_extend('force', M, other) + for k, v in pairs(merged) do M[k] = v end + + -- Publish to the shared conf for the Herdr scripts (never fatal). + pcall(conf.write, resolved) +end + +return M diff --git a/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/health.lua b/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/health.lua new file mode 100644 index 0000000..3751026 --- /dev/null +++ b/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/health.lua @@ -0,0 +1,46 @@ +---:checkhealth herdr-splits integration. +---Discovered automatically by Neovim's built-in `:checkhealth` via the +---runtimefile lookup in `lua//health.lua`. +---Requires Neovim ≥ 0.10 for `vim.health`. + +local M = {} + +function M.check() + local config = require('herdr-splits.config') + local herdr = require('herdr-splits.herdr') + local win = require('herdr-splits.win') + + vim.health.start('herdr-splits') + + if herdr.is_in_session() then + vim.health.ok( + string.format( + 'Running inside a Herdr session (HERDR_ENV=1, HERDR_PANE_ID=%s)', + tostring(vim.env.HERDR_PANE_ID) + ) + ) + else + vim.health.warn( + 'Not inside a Herdr session (HERDR_ENV=' + .. tostring(vim.env.HERDR_ENV) + .. ') — nav/resize will not cross pane boundaries.' + ) + end + + vim.health.info(string.format('ignored_buftypes: %s', vim.inspect(config.ignored_buftypes))) + vim.health.info(string.format('ignored_filetypes: %s', vim.inspect(config.ignored_filetypes))) + vim.health.info(string.format('floating_zindex_max: %s', tostring(config.floating_zindex_max))) + vim.health.info(string.format('ignore_previewwindows: %s', tostring(config.ignore_previewwindows))) + + if win.is_embedded_floating_window() then + vim.health.warn('Current window is an embedded floating window (zindex < 50) — treat as sidebar.') + end + if win.is_ignored_win() then + vim.health.info('Current window matches an ignored filetype/buftype.') + end + if win.is_floating() and not win.is_embedded_floating_window() then + vim.health.info('Current window is a true floating popup — nav/resize will forward to Herdr.') + end +end + +return M diff --git a/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/herdr.lua b/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/herdr.lua new file mode 100644 index 0000000..a5485ad --- /dev/null +++ b/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/herdr.lua @@ -0,0 +1,160 @@ +---Herdr CLI wrapper. All Herdr subprocess calls go through this module. +---@class HerdrSplitsHerdr +local M = {} + +local config = require('herdr-splits.config') + +---Resolve the herdr binary path. +---@return string +function M.herdr_bin() + return config.herdr_bin or vim.env.HERDR_BIN_PATH or 'herdr' +end + +---Check if Neovim is running inside a Herdr session. +---@return boolean +function M.is_in_session() + return vim.env.HERDR_ENV == '1' and vim.env.HERDR_PANE_ID ~= nil and #vim.env.HERDR_PANE_ID > 0 +end + +---Get the current Herdr pane ID. +---@return string|nil +function M.current_pane_id() + if not M.is_in_session() then + return nil + end + return vim.env.HERDR_PANE_ID +end + +---Run a herdr CLI command and return stdout, stderr, and exit code. +---@param args string[] +---@return string stdout, string stderr, number exit_code +local function herdr_exec(args) + local cmd = vim.list_extend({ M.herdr_bin() }, args, 1, #args) + local obj = vim.system(cmd, { text = true }):wait() + return obj.stdout, obj.stderr, obj.code +end + +---Check if the current Herdr pane is at the layout boundary in the given direction. +---Calls `herdr pane edges --current` and parses the JSON response. +---@param direction '"left"'|'"right"'|'"up"'|'"down"' +---@return boolean|nil true if at edge, false if neighbor exists, nil on error +function M.current_pane_at_edge(direction) + if not M.is_in_session() then + return nil + end + + local edge_key = direction + local stdout, _, code = herdr_exec({ 'pane', 'edges', '--current' }) + + if code ~= 0 or not stdout or #stdout == 0 then + return nil + end + + local ok, data = pcall(vim.json.decode, stdout) + if + not ok + or type(data) ~= 'table' + or type(data.result) ~= 'table' + or type(data.result.edges) ~= 'table' + then + return nil + end + + local at_edge = data.result.edges[edge_key] + if type(at_edge) ~= 'boolean' then + return nil + end + return at_edge +end + +---Check if the current Herdr pane is zoomed. +---Calls `herdr pane layout --current` and parses JSON. +---@return boolean|nil true if zoomed, false if not, nil on error +function M.current_pane_is_zoomed() + if not M.is_in_session() then + return nil + end + + local stdout, _, code = herdr_exec({ 'pane', 'layout', '--current' }) + + if code ~= 0 or not stdout or #stdout == 0 then + return nil + end + + local ok, data = pcall(vim.json.decode, stdout) + if + not ok + or type(data) ~= 'table' + or type(data.result) ~= 'table' + or type(data.result.layout) ~= 'table' + then + return nil + end + + local zoomed = data.result.layout.zoomed + if type(zoomed) ~= 'boolean' then + return nil + end + return zoomed +end + +---Focus a Herdr pane in the given direction. +---Calls `herdr pane focus --direction --current`. +---@param direction '"left"'|'"right"'|'"up"'|'"down"' +---@return boolean true on success +function M.focus_pane(direction) + if not M.is_in_session() then + return false + end + + local _, _, code = herdr_exec({ 'pane', 'focus', '--direction', direction, '--current' }) + return code == 0 +end + +---Unzoom the current Herdr pane (turn off zoom). +---Calls `herdr pane zoom --off --current`. +---@return boolean true on success +function M.unzoom() + if not M.is_in_session() then + return false + end + + local _, _, code = herdr_exec({ 'pane', 'zoom', '--off', '--current' }) + return code == 0 +end + +---Check whether auto-unzoom is enabled. +---Reads `config.unzoom_on_nav` (resolved from setup(); default: enabled). +---@return boolean +function M.unzoom_enabled() + return config.unzoom_on_nav ~= false +end + +---Edge-wrap behavior across the Herdr pane boundary, read from the resolved +---in-memory config (the same value written to the shared conf that the +---Herdr-side scripts read). `stop` suppresses wrap-across-boundary on both +---the plain-pane side (handled by the Herdr script) and the Neovim +---edge-wrap side (handled here). Anything else defaults to `wrap`. +---@return '"wrap"'|'"stop"' +function M.nav_at_edge() + return config.nav_at_edge == 'stop' and 'stop' or 'wrap' +end + +---Resize the current Herdr pane in the given direction. +---Calls `herdr pane resize --direction --amount --current`. +---@param direction '"left"'|'"right"'|'"up"'|'"down"' +---@param amount number Float ratio (e.g., 0.03 = 3% of terminal dimension) +---@return boolean true on success +function M.resize_pane(direction, amount) + if not M.is_in_session() then + return false + end + + local amount_str = string.format('%.4f', amount) + local _, _, code = herdr_exec({ + 'pane', 'resize', '--direction', direction, '--amount', amount_str, '--current', + }) + return code == 0 +end + +return M diff --git a/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/init.lua b/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/init.lua new file mode 100644 index 0000000..3782090 --- /dev/null +++ b/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/init.lua @@ -0,0 +1,162 @@ +---herdr-splits.nvim — Seamless navigation and resizing between Neovim splits +---and Herdr panes. Makes Herdr terminal splits behave like native Neovim windows. +--- +---Usage: +--- require('herdr-splits').setup({ ... }) +--- vim.keymap.set('n', '', require('herdr-splits').move_cursor_left) +--- vim.keymap.set('n', '', require('herdr-splits').resize_left) + +local config = require('herdr-splits.config') +local nav = require('herdr-splits.nav') +local resize_mod = require('herdr-splits.resize') +local win = require('herdr-splits.win') +local sync = require('herdr-splits.sync') + +local M = {} + +---Apply configuration. Idempotent — calling again merges into existing config. +---@param opts table|nil Configuration options (see config.lua for defaults) +function M.setup(opts) + config.setup(opts) + pcall(sync.sync) +end + +---Sync the Herdr-managed checkout to match this lazy commit. +---Intended as a lazy.nvim `build` hook entry point; respects `auto_sync_herdr`. +---Safe to call at any time; no-op unless opted in. +function M.sync_herdr() + pcall(sync.sync) +end + +---Resize the current split to the left. +---Amount defaults to config.default_amount. Multiplied by vim.v.count1. +---@param amount number|nil +function M.resize_left(amount) + local eventignore_orig = vim.o.eventignore + vim.o.eventignore = table.concat(config.ignored_events, ',') + resize_mod.is_resizing = true + local cur_win = vim.api.nvim_get_current_win() + local ok, err = pcall(resize_mod.resize, 'left', amount) + if not ok then + vim.notify('herdr-splits: resize_left failed: ' .. tostring(err), vim.log.levels.ERROR) + end + pcall(vim.api.nvim_set_current_win, cur_win) + resize_mod.is_resizing = false + vim.o.eventignore = eventignore_orig +end + +---Resize the current split downward. +---@param amount number|nil +function M.resize_down(amount) + local eventignore_orig = vim.o.eventignore + vim.o.eventignore = table.concat(config.ignored_events, ',') + resize_mod.is_resizing = true + local cur_win = vim.api.nvim_get_current_win() + local ok, err = pcall(resize_mod.resize, 'down', amount) + if not ok then + vim.notify('herdr-splits: resize_down failed: ' .. tostring(err), vim.log.levels.ERROR) + end + pcall(vim.api.nvim_set_current_win, cur_win) + resize_mod.is_resizing = false + vim.o.eventignore = eventignore_orig +end + +---Resize the current split upward. +---@param amount number|nil +function M.resize_up(amount) + local eventignore_orig = vim.o.eventignore + vim.o.eventignore = table.concat(config.ignored_events, ',') + resize_mod.is_resizing = true + local cur_win = vim.api.nvim_get_current_win() + local ok, err = pcall(resize_mod.resize, 'up', amount) + if not ok then + vim.notify('herdr-splits: resize_up failed: ' .. tostring(err), vim.log.levels.ERROR) + end + pcall(vim.api.nvim_set_current_win, cur_win) + resize_mod.is_resizing = false + vim.o.eventignore = eventignore_orig +end + +---Resize the current split to the right. +---@param amount number|nil +function M.resize_right(amount) + local eventignore_orig = vim.o.eventignore + vim.o.eventignore = table.concat(config.ignored_events, ',') + resize_mod.is_resizing = true + local cur_win = vim.api.nvim_get_current_win() + local ok, err = pcall(resize_mod.resize, 'right', amount) + if not ok then + vim.notify('herdr-splits: resize_right failed: ' .. tostring(err), vim.log.levels.ERROR) + end + pcall(vim.api.nvim_set_current_win, cur_win) + resize_mod.is_resizing = false + vim.o.eventignore = eventignore_orig +end + +---Move cursor to the left, crossing into Herdr pane if at Neovim edge. +---@param opts table|nil { same_row: boolean|nil, at_edge: string|function|nil } +function M.move_cursor_left(opts) + resize_mod.is_resizing = false + local ok, err = pcall(nav.move_cursor, 'left', opts) + if not ok then + vim.notify('herdr-splits: move_cursor_left failed: ' .. tostring(err), vim.log.levels.ERROR) + end +end + +---Move cursor downward, crossing into Herdr pane if at Neovim edge. +---@param opts table|nil +function M.move_cursor_down(opts) + resize_mod.is_resizing = false + local ok, err = pcall(nav.move_cursor, 'down', opts) + if not ok then + vim.notify('herdr-splits: move_cursor_down failed: ' .. tostring(err), vim.log.levels.ERROR) + end +end + +---Move cursor upward, crossing into Herdr pane if at Neovim edge. +---@param opts table|nil +function M.move_cursor_up(opts) + resize_mod.is_resizing = false + local ok, err = pcall(nav.move_cursor, 'up', opts) + if not ok then + vim.notify('herdr-splits: move_cursor_up failed: ' .. tostring(err), vim.log.levels.ERROR) + end +end + +---Move cursor to the right, crossing into Herdr pane if at Neovim edge. +---@param opts table|nil +function M.move_cursor_right(opts) + resize_mod.is_resizing = false + local ok, err = pcall(nav.move_cursor, 'right', opts) + if not ok then + vim.notify('herdr-splits: move_cursor_right failed: ' .. tostring(err), vim.log.levels.ERROR) + end +end + +---Append a filetype to ignored_filetypes at runtime (de-duped). +---@param name string Filetype name, e.g. 'fugitive' +---@return boolean true if added, false if already present +function M.add_ignored_filetype(name) + for _, v in ipairs(config.ignored_filetypes) do + if v == name then + return false + end + end + table.insert(config.ignored_filetypes, name) + return true +end + +---Append a buftype to ignored_buftypes at runtime (de-duped). +---@param name string Buffer type, e.g. 'help' +---@return boolean true if added, false if already present +function M.add_ignored_buftype(name) + for _, v in ipairs(config.ignored_buftypes) do + if v == name then + return false + end + end + table.insert(config.ignored_buftypes, name) + return true +end + +return M diff --git a/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/nav.lua b/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/nav.lua new file mode 100644 index 0000000..56ecca3 --- /dev/null +++ b/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/nav.lua @@ -0,0 +1,206 @@ +---Navigation logic: seamless movement between Neovim splits and Herdr panes. +---@class HerdrSplitsNav +local M = {} + +local config = require('herdr-splits.config') +local herdr = require('herdr-splits.herdr') +local win = require('herdr-splits.win') + +---Treat the current window as a sidebar that should not be navigated through. +---Combines the configured ignore lists with the embedded-float heuristic. +---@return boolean +local function is_sidebar() + return win.is_ignored_or_preview() or win.is_embedded_floating_window() +end + +---Split a new Neovim window using the user's placement preferences. +---@param direction '"left"'|'"right"'|'"up"'|'"down"' +local function split_edge(direction) + if direction == 'left' or direction == 'right' then + vim.cmd('vsp') + else + vim.cmd('sp') + end +end + +---Move cursor between Neovim splits, falling through to Herdr at edges. +---This is the core navigation function. +---@param direction '"left"'|'"right"'|'"up"'|'"down"' +---@param opts table|nil { same_row: boolean|nil, at_edge: string|function|nil } +function M.move_cursor(direction, opts) + local same_row = config.move_cursor_same_row + local at_edge_behavior = config.at_edge + + if type(opts) == 'table' then + if opts.same_row ~= nil then + same_row = opts.same_row + end + if opts.at_edge ~= nil then + at_edge_behavior = opts.at_edge + end + end + + -- Handle floating windows: just forward to Herdr, + -- EXCEPT for embedded sidebars (snacks/neo-tree float/aerial float): + -- those should stay in Neovim so the user can keep interacting + -- with the picker. + if win.is_floating() and not win.is_embedded_floating_window() then + herdr.focus_pane(direction) + return + end + + local dir_key = win.dir_keys[direction] + local offset = vim.fn.winline() + vim.api.nvim_win_get_position(0)[1] + + -- Save current window to detect if wincmd changes it + local prev_win = vim.api.nvim_get_current_win() + + -- Try moving within Neovim first + local will_wrap = false + local count = vim.v.count1 + local target_winnr = vim.fn.winnr(count .. dir_key) + if count > 1 then + local prev_winnr = vim.fn.winnr((count - 1) .. dir_key) + will_wrap = target_winnr == prev_winnr + else + will_wrap = target_winnr == vim.fn.winnr() + end + + -- Command-line window (q:, q/, q?): Neovim forbids all window commands + -- (E11). Never wincmd; at a Neovim screen edge, delegate to Herdr + -- (subprocess-safe, does not close the cmdwin); otherwise silent no-op. + -- Mirrors smart-splits.nvim PR #464. + if win.is_command_line_window() then + if will_wrap and herdr.is_in_session() then + local at_herdr_edge = herdr.current_pane_at_edge(direction) + if at_herdr_edge == false then + herdr.focus_pane(direction) + elseif at_herdr_edge == true and herdr.nav_at_edge() ~= 'stop' then + herdr.focus_pane(win.reverse_direction[direction]) + end + end + return + end + + -- Execute the wincmd + if will_wrap and count == 1 then + vim.cmd('wincmd ' .. dir_key) + else + vim.cmd(count .. 'wincmd ' .. dir_key) + end + + if vim.api.nvim_get_current_win() ~= prev_win then + -- Moved within Neovim. Restore same-row if configured. + if (direction == 'left' or direction == 'right') and same_row then + local row = offset - vim.api.nvim_win_get_position(0)[1] + if row > 0 then + vim.cmd('normal! ' .. row .. 'H') + end + end + return + end + + -- We're at a Neovim edge. Try to cross into Herdr. + if not herdr.is_in_session() then + if will_wrap and count == 1 then + local sidebar = is_sidebar() + if type(at_edge_behavior) == 'function' then + at_edge_behavior({ + direction = direction, + split = function() split_edge(direction) end, + is_sidebar = sidebar, + wrap = function() + vim.cmd('wincmd ' .. win.dir_keys_reverse[direction]) + end, + }) + elseif at_edge_behavior == 'stop' then + return + elseif at_edge_behavior == 'split' then + if not sidebar then + split_edge(direction) + end + else -- 'wrap' (default) + -- Wrap is allowed even from filetype-based sidebars (dbui, neo-tree, + -- ...) so the user can leave them; only embedded floats stay gated. + if not win.is_embedded_floating_window() then + vim.cmd('wincmd ' .. win.dir_keys_reverse[direction]) + end + end + end + return + end + + -- Check zoom state: unzoom first, then retry Neovim navigation. + -- Must happen BEFORE the at_herdr_edge check — when zoomed, the pane fills + -- the screen so herdr reports it as being at every edge, making + -- at_herdr_edge useless until we unzoom. + if herdr.unzoom_enabled() and herdr.current_pane_is_zoomed() then + herdr.unzoom() + -- Retry wincmd — other Neovim splits may now be visible + vim.cmd('wincmd ' .. dir_key) + if vim.api.nvim_get_current_win() ~= prev_win then + if (direction == 'left' or direction == 'right') and same_row then + local row = offset - vim.api.nvim_win_get_position(0)[1] + if row > 0 then + vim.cmd('normal! ' .. row .. 'H') + end + end + return + end + -- Still at edge after unzoom; fall through to Herdr edge check. + end + + -- Check if we're at the Herdr edge too + local at_herdr_edge = herdr.current_pane_at_edge(direction) + if at_herdr_edge == nil then + if will_wrap and count == 1 and not win.is_embedded_floating_window() then + vim.cmd('wincmd ' .. win.dir_keys_reverse[direction]) + end + return + end + + if not at_herdr_edge then + -- There's a Herdr pane in this direction. Cross the boundary. + local moved = herdr.focus_pane(direction) + if not moved and will_wrap and count == 1 and not win.is_embedded_floating_window() then + vim.cmd('wincmd ' .. win.dir_keys_reverse[direction]) + end + return + end + + -- At both Neovim AND Herdr edges (no herdr pane to cross into). + -- Apply at_edge behavior. (Any needed unzoom already happened above.) + if type(at_edge_behavior) == 'function' then + at_edge_behavior({ + direction = direction, + split = function() split_edge(direction) end, + is_sidebar = is_sidebar(), + wrap = function() + vim.cmd('wincmd ' .. win.dir_keys_reverse[direction]) + end, + }) + elseif at_edge_behavior == 'stop' then + return + elseif at_edge_behavior == 'split' then + if not is_sidebar() then + split_edge(direction) + end + else -- 'wrap' (default) + -- Wrap is allowed from filetype-based sidebars (dbui, neo-tree, ...) so + -- you can leave them at an edge; only embedded floating overlays stay gated. + if will_wrap and count == 1 and not win.is_embedded_floating_window() then + -- Wrap to the opposite side. If a Herdr pane exists there AND nav_at_edge + -- allows wrap-across-boundary (the default), cross into it; otherwise + -- wrap within Neovim. nav_at_edge=stop keeps the wrap inside Neovim. + if herdr.nav_at_edge() ~= 'stop' + and herdr.current_pane_at_edge(win.reverse_direction[direction]) == false + then + herdr.focus_pane(win.reverse_direction[direction]) + else + vim.cmd('wincmd ' .. win.dir_keys_reverse[direction]) + end + end + end +end + +return M diff --git a/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/resize.lua b/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/resize.lua new file mode 100644 index 0000000..ff94999 --- /dev/null +++ b/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/resize.lua @@ -0,0 +1,108 @@ +---Resize logic: seamless resize between Neovim splits and Herdr panes. +--- +---Mental model (same as smart-splits.nvim): +--- The direction (up/down/left/right) is which way to move the split border. +--- The +/- operator depends on whether current window is above/below or +--- left/right of the neighbor being resized against. +--- +---When at a Neovim edge with no neighbor in that direction, and the window +---fills the terminal, the resize is forwarded to Herdr. +---@class HerdrSplitsResize +local M = {} + +local config = require('herdr-splits.config') +local herdr = require('herdr-splits.herdr') +local win = require('herdr-splits.win') + +M.is_resizing = false + +---Compute the +/- operator for a vertical resize. +---At start/middle: resize_up shrinks (-), resize_down grows (+). +---At last (bottom): inverted — resize_up grows (+), resize_down shrinks (-). +---@param direction '"up"'|'"down"' +---@return '"+""'|'"-""' +local function compute_vertical(direction) + local pos = win.win_position(direction) + if pos == 'start' or pos == 'middle' then + return direction == 'down' and '+' or '-' + end + return direction == 'down' and '-' or '+' +end + +---Compute the +/- operator for a horizontal resize. +---At start/middle: resize_left shrinks (-), resize_right grows (+). +---At last (right): inverted — resize_left grows (+), resize_right shrinks (-). +---@param direction '"left"'|'"right"' +---@return '"+""'|'"-""' +local function compute_horizontal(direction) + local pos = win.win_position(direction) + if pos == 'start' or pos == 'middle' then + return direction == 'right' and '+' or '-' + end + return direction == 'right' and '-' or '+' +end + +---Resize in a direction. +---@param direction '"left"'|'"right"'|'"up"'|'"down"' +---@param amount number|nil Override amount in Neovim cells. When nil, uses +--- vim.v.count1 * neovim_amount for native or count * default_amount for Herdr. +function M.resize(direction, amount) + local count = vim.v.count1 + local has_explicit = amount ~= nil + + -- Embedded floating sidebars (snacks float, neo-tree float, aerial float): + -- refuse to resize; the picker owns its own dimensions. + if win.is_embedded_floating_window() then + return + end + + -- Floating windows: forward to Herdr + if win.is_floating() then + local ratio = has_explicit and amount or (count * config.default_amount) + herdr.resize_pane(direction, ratio) + return + end + + -- Delegate to Herdr ONLY when at both Neovim edges in this dimension + -- AND the window fills the terminal (full_width or full_height). + local delegate = false + -- Never delegate from inside a sidebar; the ignore list applies to resize + -- too. The command-line window is exempted: it is `nofile` (so it trips this + -- rule) yet a full-width cmdwin should still resize the Herdr pane sideways. + local in_sidebar = win.is_ignored_or_preview() + if direction == 'left' or direction == 'right' then + delegate = win.is_full_width() and win.at_left_edge() and win.at_right_edge() and herdr.is_in_session() + else + delegate = win.is_full_height() and win.at_top_edge() and win.at_bottom_edge() and herdr.is_in_session() + end + if delegate and in_sidebar and not win.is_command_line_window() then + delegate = false + end + + if delegate and herdr.current_pane_is_zoomed() then + delegate = false + end + + if delegate then + local ratio = (has_explicit and amount < 1) and amount or (count * config.default_amount) + herdr.resize_pane(direction, ratio) + return + end + + -- Native Neovim resize + local cells = has_explicit and math.floor(amount) or (count * config.neovim_amount) + if cells <= 0 then + cells = 1 + end + + local is_horiz = direction == 'left' or direction == 'right' + local op = is_horiz and compute_horizontal(direction) or compute_vertical(direction) + + if is_horiz then + pcall(vim.cmd, 'vertical resize ' .. op .. cells) + else + pcall(vim.cmd, 'resize ' .. op .. cells) + end +end + +return M diff --git a/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/sync.lua b/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/sync.lua new file mode 100644 index 0000000..ddef394 --- /dev/null +++ b/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/sync.lua @@ -0,0 +1,107 @@ +---Auto-sync the Herdr-managed checkout to match the lazy.nvim checkout. +--- +---Herdr has no `plugin update` command in v1; the only sanctioned refresh is +---reinstall from GitHub. When lazy.nvim pulls a new commit, the bash scripts +---under the Herdr-managed checkout stay frozen at the old commit unless the +---user manually reinstalls. This module closes that gap: it reinstalls the +---Herdr-managed checkout pinned to the exact commit lazy fetched, so the bash +---scripts match the lua side byte-for-byte. +--- +---Entirely a no-op unless `auto_sync_herdr = true` is set in setup(). Also a +---no-op when: the herdr binary is unavailable, the plugin is installed as a +---local link (dev mode — reinstall would be refused anyway), the plugin is not +---installed, or the commits already match. +local M = {} + +local config = require('herdr-splits.config') + +---Plugin id and GitHub source as used by `herdr plugin install`. +local PLUGIN_ID = 'herdr-splits' +local PLUGIN_SOURCE = 'lmilojevicc/herdr-splits.nvim' + +---Run the sync. Safe to call at any time; never throws. +function M.sync() + if config.auto_sync_herdr ~= true then + return + end + + local ok, err = pcall(function() + local bin = config.herdr_bin or vim.env.HERDR_BIN_PATH or 'herdr' + if vim.fn.executable(bin) ~= 1 then + return + end + + -- Resolve this plugin's own checkout root (the lazy clone). + local source = debug.getinfo(1, 'S').source -- "@/lua/herdr-splits/sync.lua" + local this_file = source:sub(2) -- strip leading '@' + local plugin_root = vim.fn.fnamemodify(this_file, ':p:h:h:h') -- up 3: file<-herdr-splits<-lua<-root + if vim.fn.isdirectory(plugin_root .. '/.git') == 0 + and vim.fn.filereadable(plugin_root .. '/herdr-plugin.toml') == 0 then + return -- not a real checkout + end + + -- Get the lazy checkout HEAD. + local obj = vim.system({ 'git', '-C', plugin_root, 'rev-parse', 'HEAD' }, { text = true }):wait() + if obj.code ~= 0 then + return + end + local lazy_sha = vim.trim(obj.stdout or '') + if lazy_sha == '' then + return + end + + -- Get the managed install info. + local obj2 = vim.system({ bin, 'plugin', 'list', '--plugin', PLUGIN_ID, '--json' }, { text = true }):wait() + if obj2.code ~= 0 then + return + end + + local decoded, data = pcall(vim.json.decode, obj2.stdout or '') + if not decoded or type(data) ~= 'table' then + return + end + + -- Output may be a single object or an array; find our entry. + local entry = data + if data[1] ~= nil then + for _, e in ipairs(data) do + if e.plugin_id == PLUGIN_ID then + entry = e + break + end + end + end + + local src = entry and entry.source + if not src then + return + end + -- Dev mode (plugin link) or unknown kind: nothing to do. + if src.kind ~= 'github' then + return + end + + -- Already in sync? + local managed_sha = vim.trim(src.resolved_commit or '') + if managed_sha ~= '' and managed_sha:lower() == lazy_sha:lower() then + return + end + + -- Reinstall pinned to the lazy commit. + local obj3 = vim.system( + { bin, 'plugin', 'install', PLUGIN_SOURCE, '--ref', lazy_sha, '--yes' }, + { text = true } + ):wait() + if obj3.code == 0 then + vim.notify('herdr-splits: synced Herdr-side scripts to ' .. lazy_sha:sub(1, 7), vim.log.levels.INFO) + else + vim.notify('herdr-splits: Herdr-side sync skipped (install failed; local link or offline)', vim.log.levels.INFO) + end + end) + + if not ok then + vim.notify('herdr-splits: sync error: ' .. tostring(err), vim.log.levels.DEBUG) + end +end + +return M diff --git a/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/win.lua b/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/win.lua new file mode 100644 index 0000000..47f6821 --- /dev/null +++ b/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/win.lua @@ -0,0 +1,154 @@ +---Window utility functions for Neovim split detection. +---@class HerdrSplitsWin +local M = {} + +local config = require('herdr-splits.config') + +---Check if the current window spans the full terminal width. +---@param winid number|nil window ID, defaults to current +---@return boolean +function M.is_full_width(winid) + return vim.api.nvim_win_get_width(winid or 0) == vim.o.columns +end + +---Check if the current window spans the full terminal height. +---Accounts for cmdheight, statusline, and tabline. +---@param winid number|nil window ID, defaults to current +---@return boolean +function M.is_full_height(winid) + local height = vim.o.lines - vim.o.cmdheight + local tabpages = #vim.api.nvim_list_tabpages() + local wins = #vim.api.nvim_tabpage_list_wins(0) + + if (vim.o.laststatus == 1 and wins > 1) or vim.o.laststatus > 1 then + height = height - 1 + end + if (vim.o.showtabline == 1 and tabpages > 1) or vim.o.showtabline == 2 then + height = height - 1 + end + + return vim.api.nvim_win_get_height(winid or 0) == height +end + +---@return boolean +function M.at_left_edge() + return vim.fn.winnr() == vim.fn.winnr('h') +end + +---@return boolean +function M.at_right_edge() + return vim.fn.winnr() == vim.fn.winnr('l') +end + +---@return boolean +function M.at_top_edge() + return vim.fn.winnr() == vim.fn.winnr('k') +end + +---@return boolean +function M.at_bottom_edge() + return vim.fn.winnr() == vim.fn.winnr('j') +end + +---Determine where the current window sits in the Neovim split layout +---for a given direction (horizontal or vertical). +---@param direction '"left"'|'"right"'|'"up"'|'"down"' +---@return '"start"'|'"middle"'|'"last"' +function M.win_position(direction) + if direction == 'left' or direction == 'right' then + if M.at_left_edge() then + return 'start' + end + if M.at_right_edge() then + return 'last' + end + return 'middle' + end + + if M.at_top_edge() then + return 'start' + end + if M.at_bottom_edge() then + return 'last' + end + return 'middle' +end + +---Check if a window should be ignored during resize operations. +---@param winid number|nil window ID, defaults to current +---@return boolean +function M.is_ignored_win(winid) + local bufnr = vim.api.nvim_win_get_buf(winid or 0) + return vim.tbl_contains(config.ignored_buftypes, vim.api.nvim_get_option_value('buftype', { buf = bufnr })) + or vim.tbl_contains(config.ignored_filetypes, vim.api.nvim_get_option_value('filetype', { buf = bufnr })) +end + +---Check if the current window is a floating window. +---@param winid number|nil +---@return boolean +function M.is_floating(winid) + return vim.api.nvim_win_get_config(winid or 0).relative ~= '' +end + +---Check if a window is an "embedded" floating window — one that is technically +---floating (relative ~= '') but visually behaves like a sidebar (e.g. snacks +---explorer). Neovim's default floating zindex is 50; anything explicitly set +---below that signals the window is meant to coexist with normal splits. +---@param winid number|nil window ID, defaults to current +---@return boolean +function M.is_embedded_floating_window(winid) + if not M.is_floating(winid) then + return false + end + local cfg = vim.api.nvim_win_get_config(winid or 0) + local threshold = config.floating_zindex_max or 50 + return cfg.zindex ~= nil and cfg.zindex < threshold +end + +---Same as M.is_ignored_win but also checks previewwindow when opt-in. +---@param winid number|nil window ID, defaults to current +---@return boolean +function M.is_ignored_or_preview(winid) + if M.is_ignored_win(winid) then + return true + end + if config.ignore_previewwindows then + local ok, pw = pcall(vim.api.nvim_win_get_option, winid or 0, 'previewwindow') + if ok and pw then + return true + end + end + return false +end + +---Returns true while Neovim's command-line window (q:, q/, q?) is open. +---Inside it all window commands raise E11, so callers must short-circuit. +---@return boolean +function M.is_command_line_window() + return vim.fn.getcmdwintype() ~= '' +end + +---Direction key shorthand for wincmd. +M.dir_keys = { + left = 'h', + right = 'l', + up = 'k', + down = 'j', +} + +M.dir_keys_reverse = { + left = 'l', + right = 'h', + up = 'j', + down = 'k', +} + +---Reverse of a direction name: left<->right, up<->down. +M.reverse_direction = { + left = 'right', + right = 'left', + up = 'down', + down = 'up', +} + +return M -- cgit 1.3.1