aboutsummaryrefslogtreecommitdiff
path: root/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/README.md
blob: 7f9cd53e889a0af1c8ac06d90a7f529692fb7082 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# herdr-splits.nvim

Seamless navigation and resizing between Neovim splits and [Herdr](https://herdr.dev) panes. Makes Herdr terminal splits behave like native Neovim windows — move and resize as if they were all part of the same editor.

Inspired by [smart-splits.nvim](https://github.com/mrjones2014/smart-splits.nvim), ported to Herdr's CLI.

## Features

- **Seamless navigation**: Same keys work in Neovim and plain shells — Herdr forwards them to Neovim when appropriate, Neovim delegates to Herdr at edges.
- **Seamless resizing**: `<M-h/j/k/l>` resizes Neovim splits natively, delegates to Herdr when a window fills the terminal.
- **at_edge behaviours**: `wrap` (default), `stop`, `split`, or a custom function.
- **Plugin-aware**: Ignores snacks/neo-tree/dadbod-ui/aerial sidebars and embedded floats (zindex < 50) by default — your keybinds never get trapped inside a picker.
- **Smart unzoom + wrap-around**: Auto-unzooms when leaving a zoomed pane. At a layout edge, navigation wraps to the opposite side — crossing into a sibling Herdr pane when one exists (e.g. `pane | nvim` → `ctrl+l` wraps to the pane), otherwise wrapping within Neovim. Herdr-pane navigation also wraps around at edges (past the last pane → the first). Does not unzoom when moving between Neovim splits in one pane.
- **Count prefix support**: `3<C-h>` moves three splits left; `5<M-l>` resizes five steps right.

## Requirements

- Neovim ≥ 0.10
- [Herdr](https://herdr.dev) ≥ 0.7.0 (for plugin actions)

## Installation

Three steps — install on both the Herdr side and the Neovim side, then add keybinds.

### 1. Install the Herdr plugin

This provides the `nav-*` and `resize-*` actions that Herdr keybinds invoke.

```bash
# From GitHub (once published):
herdr plugin install lmilojevicc/herdr-splits.nvim

# Local development:
herdr plugin link /path/to/herdr-splits
```

### 2. Install the Neovim plugin

## lazy.nvim

```lua
{
  'lmilojevicc/herdr-splits.nvim',
  -- For local development, swap the repo line for `dir = '/path/to/herdr-splits'`
  -- (see "Local development" below).
  cond = vim.env.HERDR_ENV == '1',
  event = 'VeryLazy',
  -- Optional: auto-sync the Herdr-side scripts when lazy updates this plugin.
  -- Requires `auto_sync_herdr = true` in setup() below to take effect.
  -- build = 'lua require("herdr-splits").sync_herdr()',
  config = function()
    require('herdr-splits').setup({
      -- Defaults shown. All fields optional.
      default_amount = 0.03,       -- Herdr resize ratio
      neovim_amount = 3,           -- Neovim resize cells
      at_edge = 'wrap',            -- 'wrap' | 'stop' | 'split' | function
      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,                -- auto-detected from HERDR_BIN_PATH
      floating_zindex_max = 50,       -- floats with zindex < this are treated as embedded sidebars
      ignore_previewwindows = false,  -- opt-in: also treat previewwindow windows (e.g. .dbout) as sidebars
      -- auto_sync_herdr = true,      -- opt-in: sync Herdr-side scripts on update
      -- Managed keys — written to the generated herdr-splits.conf so the
      -- Herdr-side scripts agree. Pass Neovim notation (e.g. <M-Left>).
      nav_keys    = { left = '<C-h>', down = '<C-j>', up = '<C-k>', right = '<C-l>' },
      resize_keys = { left = '<M-h>', down = '<M-j>', up = '<M-k>', right = '<M-l>' },
      unzoom_on_nav = true,   -- auto-unzoom when navigating away from a zoomed pane
      nav_at_edge    = 'wrap', -- 'wrap' | 'stop' — Herdr pane-boundary wrap (distinct from at_edge)
    })
  end,
  keys = {
    { '<C-h>', function() require('herdr-splits').move_cursor_left() end,  desc = 'Navigate left' },
    { '<C-j>', function() require('herdr-splits').move_cursor_down() end,  desc = 'Navigate down' },
    { '<C-k>', function() require('herdr-splits').move_cursor_up() end,    desc = 'Navigate up' },
    { '<C-l>', function() require('herdr-splits').move_cursor_right() end, desc = 'Navigate right' },
    { '<M-h>', function() require('herdr-splits').resize_left() end,  desc = 'Resize left' },
    { '<M-j>', function() require('herdr-splits').resize_down() end,  desc = 'Resize down' },
    { '<M-k>', function() require('herdr-splits').resize_up() end,    desc = 'Resize up' },
    { '<M-l>', function() require('herdr-splits').resize_right() end, desc = 'Resize right' },
  },
}
```

## packer.nvim

```lua
use {
  'lmilojevicc/herdr-splits.nvim',
  config = function()
    require('herdr-splits').setup()
  end,
}
```

### 3. Add keybinds to Herdr config

Add these to `~/.config/herdr/config.toml`, then run `herdr server reload-config`:

```toml
[[keys.command]]
key = "ctrl+h"
type = "plugin_action"
command = "herdr-splits.nav-left"

[[keys.command]]
key = "ctrl+j"
type = "plugin_action"
command = "herdr-splits.nav-down"

[[keys.command]]
key = "ctrl+k"
type = "plugin_action"
command = "herdr-splits.nav-up"

[[keys.command]]
key = "ctrl+l"
type = "plugin_action"
command = "herdr-splits.nav-right"

[[keys.command]]
key = "alt+h"
type = "plugin_action"
command = "herdr-splits.resize-left"

[[keys.command]]
key = "alt+j"
type = "plugin_action"
command = "herdr-splits.resize-down"

[[keys.command]]
key = "alt+k"
type = "plugin_action"
command = "herdr-splits.resize-up"

[[keys.command]]
key = "alt+l"
type = "plugin_action"
command = "herdr-splits.resize-right"
```

> **Note for macOS**: Terminals treat Option as a special character modifier by default. You need to set Option = Alt/Meta:
>
> - **Ghostty**: `macos-option-as-alt = true`
> - **Alacritty**: `option_as_alt = "Both"`
> - **kitty**: `macos_option_as_alt yes`
> - **iTerm**: Profiles → Keys → Left Option key → Esc+
>
> If you're using [smart-splits.nvim](https://github.com/mrjones2014/smart-splits.nvim) for tmux, add `cond = vim.env.HERDR_ENV ~= '1'` to its spec so the two plugins don't conflict.

### Auto-unzoom and wrap-around

**Auto-unzoom.** When you navigate away from a **zoomed** pane, herdr-splits
unzooms first so the destination is visible — whether you're crossing into a
sibling Herdr pane or wrapping at a layout edge. It does not unzoom when
moving between Neovim splits inside the same pane.

**Wrap-around.** When you reach a layout edge in the direction you pressed
(nowhere further to go that way), the default `wrap` behaviour continues on
the opposite side:

- If a Herdr pane sits on the opposite side, focus crosses into it — so
  `pane | nvim` wraps `ctrl+l` back to the pane, and
  `pane | nvim win1 | win2` wraps from win2 to the pane. With `nav_at_edge=stop`
  (see below) this cross-to-opposite-pane is suppressed and the wrap stays
  within Neovim instead.
- Otherwise it wraps within Neovim (smart-splits default).
- Between plain Herdr panes, navigation wraps around at edges too (past the
  last pane → the first).
- Wrap works even when you're on a sidebar (dbui, neo-tree, quickfix, ...),
  so you can leave it at an edge; only embedded floating overlays are gated.

**To disable auto-unzoom entirely**, pass `unzoom_on_nav = false` to `setup()`:

```lua
require('herdr-splits').setup({ unzoom_on_nav = false })
```

These options are written to a generated `herdr-splits.conf` (default
`~/.config/herdr/plugins/config/herdr-splits/herdr-splits.conf`; print the
path with `herdr plugin config-dir herdr-splits`) so the Herdr-side scripts
agree — you configure them once, from `setup()`. **Do not hand-edit that
file:** it is regenerated on every `setup()`. Any values already present are
adopted once the first time `setup()` runs after an upgrade, then
overwritten thereafter. Set `HERDR_SPLITS_CONFIG` to override the path on
both sides.

**To stop at layout edges instead of wrapping**, pass `nav_at_edge = 'stop'`:

```lua
require('herdr-splits').setup({ nav_at_edge = 'stop' })
```

This single switch controls wrap-across-boundary on **both** sides:

- **Plain Herdr panes:** `wrap` (the default) wraps to the opposite pane at an
  edge; `stop` halts.
- **Neovim edge wrap:** when Neovim's `at_edge='wrap'`, `wrap` (the default)
  lets the wrap cross to the Herdr pane on the opposite side (e.g. `ctrl+l` from
  the last nvim split lands on the pane); `stop` keeps the wrap within Neovim
  (e.g. `ctrl+l` from the last split cycles to the first split instead of
  leaving Neovim). `at_edge='stop'` halts regardless of `nav_at_edge`.

Note: `at_edge` (Neovim window-edge behaviour: `wrap`/`stop`/`split`/function)
and `nav_at_edge` (Herdr pane-boundary wrap: `wrap`/`stop`) are two different
things despite the shared name — `at_edge` is Neovim-side only and is not
written to the conf.

Note: when a pane is zoomed and `unzoom_on_nav=false`, the edge flags can't be
trusted so `stop` can't be detected on the plain-pane side — navigation proceeds
in the requested direction in that case. With the default `unzoom_on_nav=true`,
pressing toward an edge while zoomed first unzooms the pane (so the edge can be
detected) and then halts — the pane unzooms even though focus doesn't move.

**To remap the keys forwarded into Neovim**, pass `nav_keys` / `resize_keys`
to `setup()` in Neovim notation (e.g. `<M-Left>`). Override only the
directions you rebind; the rest keep their defaults. By default the scripts
forward `ctrl+h/j/k/l` for navigation and `alt+h/j/k/l` for resizing — the
chords the documented Neovim keymaps bind:

```lua
require('herdr-splits').setup({
  nav_keys    = { left = '<M-Left>', down = '<M-Down>', up = '<M-Up>', right = '<M-Right>' },
  resize_keys = { left = '<M-S-Left>', down = '<M-S-Down>', up = '<M-S-Up>', right = '<M-S-Right>' },
})
```

- Pass Neovim notation in `setup()` (`<C-h>`, `<M-Left>`); it is translated to the Herdr chord notation (`ctrl+h`, `alt+left`) the generated conf writes and `herdr pane send-keys` accepts.
- Any direction left unset keeps its default.
- The chord forwarded into Neovim must match the Neovim keymap that catches it: `<M-Left>` in `setup()` is the same key `alt+left` the script forwards, so keep your `vim.keymap.set` in sync with `nav_keys`.

> **Eager load required for custom keys.** The generated conf is written by
> `setup()`, so custom `nav_keys`/`resize_keys` need `setup()` to run eagerly
> or at `VeryLazy` — do **not** lazy-load this plugin *only* from the same
> custom key mappings it must publish. Otherwise the conf carrying your custom
> chords isn't written before Herdr forwards them, and those keys can't reach
> Neovim to trigger the lazy load. The recommended spec above already uses
> `event = 'VeryLazy'`.

## Local development

Both sides load straight from your clone, so edits take effect immediately
(on the Herdr side; Neovim picks them up on reload/restart).

1. **Link the Herdr plugin** to your local repo:

   ```bash
   herdr plugin link /path/to/herdr-splits
   ```

2. **Point lazy.nvim at the same path** — change the first line of the spec
   from the GitHub repo to `dir`:

   ```lua
   {
     dir = '/path/to/herdr-splits',
     cond = vim.env.HERDR_ENV == '1',
     -- ...rest of the spec unchanged
   }
   ```

Run `herdr plugin list` to confirm it shows `herdr-splits ... [local:/...]`.
To switch back to the published version: `herdr plugin unlink herdr-splits`,
then `herdr plugin install lmilojevicc/herdr-splits.nvim`, and revert the
lazy spec to `'lmilojevicc/herdr-splits.nvim'`.

## Lua API

```lua
require('herdr-splits').setup(opts)

-- Resize (amount defaults to config.default_amount, multiplied by count prefix)
require('herdr-splits').resize_left(amount)
require('herdr-splits').resize_down(amount)
require('herdr-splits').resize_up(amount)
require('herdr-splits').resize_right(amount)

-- Navigate (opts can override same_row and at_edge per-call)
require('herdr-splits').move_cursor_left({ same_row = true, at_edge = 'stop' })
require('herdr-splits').move_cursor_down(opts)
require('herdr-splits').move_cursor_up(opts)
require('herdr-splits').move_cursor_right(opts)
```

## Compatibility

`herdr-splits.nvim` ships with an opinionated default `ignored_filetypes` list so your resize keybinds don't get trapped inside common sidebars and pickers. These filetypes are treated as sidebars: `split`-at-edge and Herdr resize delegation are skipped from them, and the flag is passed to a custom `at_edge` function as `is_sidebar`. `wrap`, however, still works from a sidebar so you can leave it (e.g. `ctrl+l` on a `dadbod-ui` drawer wraps to the opposite side); only embedded floating overlays are gated.

The default `ignored_filetypes` covers:

- `NvimTree` — `nvim-tree/nvim-tree.lua` default file tree (`buftype=nofile`, `winfixwidth`).
- `neo-tree` — `nvim-neo-tree/neo-tree.nvim` LazyVim default (`winfixwidth` sidebar).
- `snacks_dashboard` / `snacks_explorer` / `snacks_picker` — `folke/snacks` sidebar/picker windows (the float case is handled by `is_embedded_floating_window` below).
- `dadbod-ui` / `dbout` — `kristijanhusak/vim-dadbod-ui` drawer and `tpope/vim-dadbod` preview result buffer.
- `aerial` / `Outline` — code-outline sidebars.
- `Trouble` / `quickfix` — diagnostics and quickfix lists.

The default `ignored_buftypes` covers `nofile`, `quickfix`, `prompt`, `help`, and `terminal`.

**Embedded floating windows** (snacks explorer in float mode, neo-tree in float mode, aerial in float mode) are detected via the `zindex < floating_zindex_max` heuristic (default threshold 50, Neovim's default float zindex) and treated as sidebars for navigation/resize decisions. Override the threshold via the `floating_zindex_max` config field.

**Add your own at runtime** from any `VeryLazy` autocmd:

```lua
require('herdr-splits').add_ignored_filetype('lazy')
require('herdr-splits').add_ignored_buftype('help')
```

**Debug with `:checkhealth herdr-splits`** (Neovim ≥ 0.10) to see which filetypes/buftypes are ignored, whether Herdr is in session, and whether the current window is classified as a sidebar.

If you want to **completely replace** the default list (e.g. to opt out of one of the entries), note that `vim.tbl_deep_extend('force', M, opts)` **concatenates** the `ignored_filetypes` / `ignored_buftypes` tables you pass; you keep what you pass AND the defaults are appended. To opt out, assign directly after `setup()`:

```lua
require('herdr-splits').setup({ ignored_filetypes = {} })  -- inherit nothing
require('herdr-splits').add_ignored_filetype('mything')     -- then add back what you want
```

## How It Works

Two sides cooperate for seamless two-way navigation:

```text
You press C-h in a Herdr pane:

  ┌─ Herdr intercepts the key (plugin_action keybind)
  │
  ├─ Is the focused pane running Neovim?
  │   │
  │   ├─ YES → forward "ctrl+h" into that pane
  │   │         └─ Neovim plugin receives it
  │   │              ├─ Has a window to the left? → wincmd h
  │   │              └─ At Neovim edge? → herdr pane focus --direction left
  │   │
  │   └─ NO (plain shell, etc.) → herdr pane focus --direction left
  │
  └─ Result: same keys move you everywhere
```

### Navigation

```text
1. Try moving within Neovim (wincmd h/j/k/l)
   ├─ Window changed → done (stayed within Neovim splits)
   └─ Window didn't change → at Neovim edge
        ├─ Zoomed? → unzoom first (neighbours are reliable once visible)
        ├─ Check if Herdr is running (HERDR_ENV=1)
        ├─ Check if Herdr pane has a neighbour in this direction
        │   └─ Yes → herdr pane focus --direction → done
        └─ No (at both Neovim AND Herdr edge) →
             apply at_edge behaviour (`wrap` crosses to the opposite Herdr
             pane if one exists and nav_at_edge=wrap (the default), else
             wraps within Neovim)
```

### Resizing

```text
1. Only one Neovim window in this dimension AND fills terminal?
   └─ Yes → herdr pane resize --direction --amount <ratio> → done
2. Otherwise:
   └─ Native Neovim resize with position-aware +/- operators
```

### Float and embedded-float classification

Two predicates classify the current window before any nav/resize decision:

- `win.is_floating(winid)` — `nvim_win_get_config(winid).relative ~= ''` (Neovim's built-in float check).
- `win.is_embedded_floating_window(winid)` — true only for floats with `zindex < floating_zindex_max` (default 50); matches snacks/neo-tree/aerial float-mode sidebars.

True floats forward to Herdr; embedded floats behave as sidebars (no movement, no Herdr delegation).

## Herdr Detection

Detected automatically through environment variables Herdr injects into every pane:

- `HERDR_ENV=1` — set when running inside a Herdr-managed pane
- `HERDR_PANE_ID` — the public pane ID (e.g., `w1:p1`)
- `HERDR_BIN_PATH` — path to the herdr binary (for subprocess calls)

## Comparison with vim-herdr-navigation

| Feature                     | vim-herdr-navigation | herdr-splits.nvim            |
| --------------------------- | -------------------- | ---------------------------- |
| Navigation                  | ✓                    | ✓                            |
| Resizing                    | ✗                    | ✓                            |
| at_edge behaviours          | ✗                    | wrap / stop / split / custom |
| Count prefix                | ✗                    | ✓ (3<C-h> = move 3 left)     |
| Auto-unzoom + wrap-around   | ✗                    | ✓                            |
| Floating window handling    | ✗                    | ✓                            |
| Herdr plugin (for keybinds) | ✓                    | ✓                            |
| Neovim plugin               | ✓                    | ✓                            |

## Updating

The Neovim side (lua) auto-updates via lazy.nvim — it pulls `main` on
`LazySync`. The Herdr side (the bash scripts under the Herdr-managed
checkout) does **not** auto-update by default: Herdr v1 has no
`herdr plugin update`, so a managed checkout stays frozen at the commit it
was installed from.

**Option A — opt into automatic sync (recommended):** set
`auto_sync_herdr = true` in `setup()`. The plugin then reinstalls the
Herdr-managed checkout pinned to the exact commit lazy fetched, so the bash
scripts always match the lua side. It is a no-op when already in sync, when
in local-dev (`plugin link`) mode, or when the `herdr` binary is unavailable.

For maximum efficiency, also add a `build` hook so the sync fires only when
lazy actually updates the plugin, instead of on every Neovim startup:

```lua
{
  'lmilojevicc/herdr-splits.nvim',
  build = 'lua require("herdr-splits").sync_herdr()',
  config = function()
    require('herdr-splits').setup({ auto_sync_herdr = true })
  end,
}
```

**Option B — manual:** after an update, re-run
`herdr plugin install lmilojevicc/herdr-splits.nvim` to refresh the
Herdr-side scripts.

Release notes for each version live in [GitHub Releases](https://github.com/lmilojevicc/herdr-splits.nvim/releases).

## License

MIT