remove herdr overflow

This commit is contained in:
2026-07-29 08:38:45 +02:00
parent 75f2079ddf
commit b2ea1bd456
104 changed files with 0 additions and 5626 deletions

View File

@@ -1,4 +0,0 @@
2026-07-24T20:15:47.544152Z INFO herdr::logging: herdr starting event="app.startup" subsystem="client" outcome="started" pid=8768
2026-07-24T20:15:47.544192Z INFO herdr::client: connecting to server path=/Users/zamp/.config/herdr/herdr-client.sock
2026-07-24T20:15:47.774089Z INFO herdr::client: handshake succeeded version=17 encoding=SemanticFrame
2026-07-27T08:17:29.705493Z INFO herdr::logging: herdr exiting event="app.shutdown" subsystem="client" outcome="completed" pid=8768

View File

@@ -1,670 +0,0 @@
2026-07-24T20:15:47.495927Z INFO herdr::platform::macos: raised server file descriptor soft limit previous=256 target=8192
2026-07-24T20:15:47.496202Z INFO herdr::api::server: api server listening path=/Users/zamp/.config/herdr/herdr.sock
2026-07-24T20:15:47.496722Z INFO herdr::app: using pane scrollback configuration pane_scrollback_limit_bytes=10000000
2026-07-24T20:15:47.510661Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-24T20:15:47.515100Z INFO herdr::logging: spawning pane terminal event="pane.spawn.start" subsystem="pane" outcome="started" pane_id=1 rows=24 cols=80 scrollback_limit_bytes=10000000
2026-07-24T20:15:47.517632Z INFO herdr::logging: pane child spawned event="pane.spawned" subsystem="pane" outcome="ok" pane_id=1 pid=8794
2026-07-24T20:15:47.517974Z INFO herdr::logging: workspace created event="workspace.create" subsystem="workspace" outcome="ok" workspace_id="w1" pane_id=1
2026-07-24T20:15:47.517985Z INFO herdr::logging: workspace focused event="workspace.focus" subsystem="workspace" outcome="ok" workspace_id="w1"
2026-07-24T20:15:47.518010Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:15:47.518044Z INFO herdr::server::headless: created startup workspace cwd=/Users/zamp
2026-07-24T20:15:47.518254Z INFO herdr::server::headless: client protocol socket listening path=/Users/zamp/.config/herdr/herdr-client.sock
2026-07-24T20:15:47.518289Z INFO herdr::server::headless: herdr server started api_socket=/Users/zamp/.config/herdr/herdr.sock client_socket=/Users/zamp/.config/herdr/herdr-client.sock
2026-07-24T20:15:47.518602Z INFO herdr::logging: herdr starting event="app.startup" subsystem="server" outcome="started" pid=8769
2026-07-24T20:15:47.774119Z INFO herdr::server::headless: client connected client_id=2 cols=216 rows=61 cell_width_px=0 cell_height_px=0 render_encoding=SemanticFrame
2026-07-24T20:15:52.523465Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:16:19.387859Z INFO herdr::pane: agent changed pane=1 previous_agent=None agent=Some(Codex) process=codex pgid=Some(9125)
2026-07-24T20:16:24.558399Z INFO herdr::pane: agent changed pane=1 previous_agent=Some(Codex) agent=None pgid=Some(8794)
2026-07-24T20:16:53.702239Z INFO herdr::logging: spawning pane terminal event="pane.spawn.start" subsystem="pane" outcome="started" pane_id=2 rows=60 cols=190 scrollback_limit_bytes=10000000
2026-07-24T20:16:53.707432Z INFO herdr::logging: pane child spawned event="pane.spawned" subsystem="pane" outcome="ok" pane_id=2 pid=9204
2026-07-24T20:16:53.707741Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:16:58.718080Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:17:00.098164Z INFO herdr::logging: pane child exited event="pane.exit" subsystem="pane" outcome="completed" pane_id=2 status="ExitStatus { code: 0, signal: None }"
2026-07-24T20:17:00.098404Z INFO herdr::pane: pane session terminated pane=2 pid=9204 signal=Hangup
2026-07-24T20:17:05.103386Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:19:32.845531Z INFO herdr::logging: spawning pane terminal event="pane.spawn.start" subsystem="pane" outcome="started" pane_id=3 rows=60 cols=190 scrollback_limit_bytes=10000000
2026-07-24T20:19:32.853251Z INFO herdr::logging: pane child spawned event="pane.spawned" subsystem="pane" outcome="ok" pane_id=3 pid=9357
2026-07-24T20:19:32.853741Z INFO herdr::logging: workspace created event="workspace.create" subsystem="workspace" outcome="ok" workspace_id="w2" pane_id=3
2026-07-24T20:19:32.853805Z INFO herdr::logging: workspace focused event="workspace.focus" subsystem="workspace" outcome="ok" workspace_id="w2"
2026-07-24T20:19:32.853986Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w2" tab_id="w2:t1"
2026-07-24T20:19:35.669009Z INFO herdr::logging: workspace focused event="workspace.focus" subsystem="workspace" outcome="ok" workspace_id="w1"
2026-07-24T20:19:35.669587Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:19:36.322288Z INFO herdr::logging: workspace focused event="workspace.focus" subsystem="workspace" outcome="ok" workspace_id="w2"
2026-07-24T20:19:36.322643Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w2" tab_id="w2:t1"
2026-07-24T20:19:36.894143Z INFO herdr::logging: workspace focused event="workspace.focus" subsystem="workspace" outcome="ok" workspace_id="w1"
2026-07-24T20:19:36.894634Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:19:40.710704Z INFO herdr::logging: workspace focused event="workspace.focus" subsystem="workspace" outcome="ok" workspace_id="w2"
2026-07-24T20:19:40.711158Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w2" tab_id="w2:t1"
2026-07-24T20:19:40.885895Z INFO herdr::logging: workspace focused event="workspace.focus" subsystem="workspace" outcome="ok" workspace_id="w2"
2026-07-24T20:19:40.886509Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w2" tab_id="w2:t1"
2026-07-24T20:19:41.652479Z INFO herdr::logging: workspace focused event="workspace.focus" subsystem="workspace" outcome="ok" workspace_id="w2"
2026-07-24T20:19:41.652930Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w2" tab_id="w2:t1"
2026-07-24T20:19:44.824051Z INFO herdr::logging: workspace closed event="workspace.close" subsystem="workspace" outcome="ok" workspace_id="w2"
2026-07-24T20:19:44.825788Z INFO herdr::logging: pane child exited event="pane.exit" subsystem="pane" outcome="completed" pane_id=3 status="ExitStatus { code: 1, signal: None }"
2026-07-24T20:19:44.846478Z INFO herdr::pane: pane session terminated pane=3 pid=9357 signal=Hangup
2026-07-24T20:19:44.846780Z WARN herdr::app::actions: PaneDied for unknown pane pane=3
2026-07-24T20:19:49.853275Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:23:03.120646Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:server:reload-config" method="server.reload_config" changes_ui=true
2026-07-24T20:23:03.121645Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:server:reload-config" method="server.reload_config"
2026-07-24T20:27:56.051742Z INFO herdr::logging: spawning pane terminal event="pane.spawn.start" subsystem="pane" outcome="started" pane_id=4 rows=61 cols=188 scrollback_limit_bytes=10000000
2026-07-24T20:27:56.059988Z INFO herdr::logging: pane child spawned event="pane.spawned" subsystem="pane" outcome="ok" pane_id=4 pid=10184
2026-07-24T20:27:56.060708Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:01.068501Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:28:03.484689Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:03.484817Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:03.484853Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:03.484898Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:03.854267Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:03.854376Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:03.854411Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:03.854440Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:03.988757Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:03.988858Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:03.988891Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:03.988918Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:04.140185Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:04.140300Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:04.140337Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:04.140365Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:04.219212Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:04.219322Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:04.219353Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:04.219384Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:04.349179Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:04.349296Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:04.349332Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:04.349361Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:04.396051Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:04.396151Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:04.396179Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:04.396227Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:04.512651Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:04.512742Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:04.512771Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:04.512801Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:04.595812Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:04.595897Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:04.595931Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:04.595960Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:04.736016Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:04.736118Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:04.736151Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:04.736180Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:05.059772Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:05.059874Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:05.059910Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:05.059937Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:05.329296Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:05.329399Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:05.329433Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:05.329460Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:05.572759Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:05.572861Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:05.572894Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:05.572932Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:06.126137Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:06.126241Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:06.126275Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:06.126302Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:06.367841Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:06.367947Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:06.367981Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:06.368007Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:08.880422Z INFO herdr::logging: spawning pane terminal event="pane.spawn.start" subsystem="pane" outcome="started" pane_id=5 rows=61 cols=94 scrollback_limit_bytes=10000000
2026-07-24T20:28:08.888439Z INFO herdr::logging: pane child spawned event="pane.spawned" subsystem="pane" outcome="ok" pane_id=5 pid=10510
2026-07-24T20:28:08.889393Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t2"
2026-07-24T20:28:10.493117Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:11.439894Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t2"
2026-07-24T20:28:12.083552Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:14.070228Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t2"
2026-07-24T20:28:14.842639Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:16.199704Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:16.199808Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:16.199844Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:16.199875Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:18.846218Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:18.846633Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:18.846675Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:18.846704Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:19.555043Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:19.555154Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:19.555190Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:19.555220Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:19.887077Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:19.887175Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:19.887211Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:19.887240Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:24.892830Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:28:43.487460Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:43.487559Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:43.487593Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:43.487620Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:44.050842Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:44.050945Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:44.050980Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:44.051008Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:44.367429Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:44.367515Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:44.367549Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:44.367577Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:44.567157Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:44.567267Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:44.567301Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:44.567332Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:44.755395Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:44.755487Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:44.755520Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:44.755553Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:44.883669Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:44.883761Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:44.883794Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:44.883821Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:45.109183Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:45.109278Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:45.109314Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:45.109345Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:45.358467Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:45.358568Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:45.358602Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:45.358629Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:47.622638Z INFO herdr::logging: spawning pane terminal event="pane.spawn.start" subsystem="pane" outcome="started" pane_id=6 rows=60 cols=94 scrollback_limit_bytes=10000000
2026-07-24T20:28:47.627635Z INFO herdr::logging: pane child spawned event="pane.spawned" subsystem="pane" outcome="ok" pane_id=6 pid=11042
2026-07-24T20:28:47.627926Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:51.147656Z INFO herdr::logging: pane child exited event="pane.exit" subsystem="pane" outcome="completed" pane_id=6 status="ExitStatus { code: 1, signal: None }"
2026-07-24T20:28:51.167453Z INFO herdr::pane: pane session terminated pane=6 pid=11042 signal=Hangup
2026-07-24T20:28:51.168762Z WARN herdr::app::actions: PaneDied for unknown pane pane=6
2026-07-24T20:28:52.245707Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:28:52.245804Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:52.245839Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:28:52.245886Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:28:57.255281Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:29:01.690454Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:29:01.690556Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:29:01.690591Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:29:01.690617Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:29:02.356107Z INFO herdr::logging: pane child exited event="pane.exit" subsystem="pane" outcome="completed" pane_id=4 status="ExitStatus { code: 1, signal: None }"
2026-07-24T20:29:02.378950Z INFO herdr::pane: pane session terminated pane=4 pid=10184 signal=Hangup
2026-07-24T20:29:02.380356Z WARN herdr::app::actions: PaneDied for unknown pane pane=4
2026-07-24T20:29:07.384503Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:29:08.117360Z INFO herdr::logging: workspace focused event="workspace.focus" subsystem="workspace" outcome="ok" workspace_id="w1"
2026-07-24T20:29:08.117845Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:29:08.118208Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t2"
2026-07-24T20:29:08.121378Z INFO herdr::logging: pane child exited event="pane.exit" subsystem="pane" outcome="completed" pane_id=5 status="ExitStatus { code: 1, signal: None }"
2026-07-24T20:29:08.140690Z INFO herdr::pane: pane session terminated pane=5 pid=10510 signal=Hangup
2026-07-24T20:29:08.142026Z WARN herdr::app::actions: PaneDied for unknown pane pane=5
2026-07-24T20:29:13.144294Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:29:14.368664Z INFO herdr::server::headless: client resize client_id=2 cols=216 rows=63 cell_width_px=0 cell_height_px=0
2026-07-24T20:29:55.026465Z INFO herdr::logging: spawning pane terminal event="pane.spawn.start" subsystem="pane" outcome="started" pane_id=7 rows=63 cols=188 scrollback_limit_bytes=10000000
2026-07-24T20:29:55.034370Z INFO herdr::logging: pane child spawned event="pane.spawned" subsystem="pane" outcome="ok" pane_id=7 pid=11194
2026-07-24T20:29:55.035200Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:30:00.260708Z INFO herdr::logging: pane child exited event="pane.exit" subsystem="pane" outcome="completed" pane_id=7 status="ExitStatus { code: 1, signal: None }"
2026-07-24T20:30:00.283775Z INFO herdr::pane: pane session terminated pane=7 pid=11194 signal=Hangup
2026-07-24T20:30:00.285320Z WARN herdr::app::actions: PaneDied for unknown pane pane=7
2026-07-24T20:30:05.292500Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:37:31.320771Z INFO herdr::logging: spawning pane terminal event="pane.spawn.start" subsystem="pane" outcome="started" pane_id=8 rows=63 cols=188 scrollback_limit_bytes=10000000
2026-07-24T20:37:31.323406Z INFO herdr::logging: pane child spawned event="pane.spawned" subsystem="pane" outcome="ok" pane_id=8 pid=14997
2026-07-24T20:37:31.323602Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:37:36.330791Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:39:29.429357Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:39:29.429466Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:39:29.429500Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:39:29.429532Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:39:34.437047Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:39:49.443605Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:39:49.443713Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:39:49.443751Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:39:49.443780Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:39:50.986759Z INFO herdr::logging: pane child exited event="pane.exit" subsystem="pane" outcome="completed" pane_id=8 status="ExitStatus { code: 1, signal: None }"
2026-07-24T20:39:51.009203Z INFO herdr::pane: pane session terminated pane=8 pid=14997 signal=Hangup
2026-07-24T20:39:51.010458Z WARN herdr::app::actions: PaneDied for unknown pane pane=8
2026-07-24T20:39:56.013104Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:45:47.507377Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-24T20:48:10.440858Z INFO herdr::logging: spawning pane terminal event="pane.spawn.start" subsystem="pane" outcome="started" pane_id=9 rows=63 cols=188 scrollback_limit_bytes=10000000
2026-07-24T20:48:10.448173Z INFO herdr::logging: pane child spawned event="pane.spawned" subsystem="pane" outcome="ok" pane_id=9 pid=18572
2026-07-24T20:48:10.448699Z INFO herdr::logging: tab renamed event="tab.rename" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t3"
2026-07-24T20:48:10.448952Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t3"
2026-07-24T20:48:15.460482Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:48:16.971538Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:48:21.981885Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:48:25.785934Z INFO herdr::logging: spawning pane terminal event="pane.spawn.start" subsystem="pane" outcome="started" pane_id=10 rows=62 cols=188 scrollback_limit_bytes=10000000
2026-07-24T20:48:25.794666Z INFO herdr::logging: pane child spawned event="pane.spawned" subsystem="pane" outcome="ok" pane_id=10 pid=18695
2026-07-24T20:48:25.795478Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:48:27.872492Z INFO herdr::pane: agent changed pane=10 previous_agent=None agent=Some(Codex) process=codex pgid=Some(18777)
2026-07-24T20:48:30.805081Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:48:45.389622Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:48:45.389730Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:48:45.389768Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:48:45.389796Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:48:50.398066Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:49:29.975339Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:49:29.975447Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:49:29.975483Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:49:29.975513Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:49:34.981613Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:51:22.915523Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:51:22.915626Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:51:22.915659Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:51:22.915691Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:51:23.760451Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:51:23.760555Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:51:23.760592Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:51:23.760621Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:51:24.474175Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:51:24.474303Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:51:24.474344Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:51:24.474376Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:51:24.979965Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:51:24.980079Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:51:24.980112Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:51:24.980139Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:51:29.985734Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:57:01.048251Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:57:06.060548Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:57:46.869917Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T20:57:46.870032Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:57:46.870089Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:57:46.870122Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T20:57:51.877083Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T20:57:53.710265Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T20:57:58.719990Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:00:26.212153Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:02:39.786251Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:06:20.511195Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:06:21.779877Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:06:26.789479Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:06:49.204781Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:06:54.213071Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:07:02.222091Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t3"
2026-07-24T21:07:07.230383Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:07:18.660887Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:07:22.499473Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t3"
2026-07-24T21:07:27.332116Z INFO herdr::logging: spawning pane terminal event="pane.spawn.start" subsystem="pane" outcome="started" pane_id=11 rows=62 cols=212 scrollback_limit_bytes=10000000
2026-07-24T21:07:27.339837Z INFO herdr::logging: pane child spawned event="pane.spawned" subsystem="pane" outcome="ok" pane_id=11 pid=20255
2026-07-24T21:07:27.340448Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t3"
2026-07-24T21:07:32.347272Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:07:43.146396Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:07:44.572325Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T21:07:44.572390Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:07:44.572399Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:07:44.572426Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T21:07:49.580441Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:08:14.437071Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t3"
2026-07-24T21:08:15.777810Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T21:08:15.777886Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t3"
2026-07-24T21:08:15.777902Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t3"
2026-07-24T21:08:15.777937Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T21:08:20.781601Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:11:07.825989Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:11:11.140914Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T21:11:11.140989Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:11:11.141005Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:11:11.141033Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T21:11:16.146935Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:12:09.439364Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t3"
2026-07-24T21:12:14.443736Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:12:18.452091Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:12:20.209770Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t3"
2026-07-24T21:12:25.220470Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:14:02.865464Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:14:07.873823Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:14:37.745692Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T21:14:37.745757Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:14:37.745767Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:14:37.745797Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T21:14:42.751030Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:15:47.514610Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-24T21:19:07.660869Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:19:12.668963Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:26:03.469834Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:26:08.480447Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:27:50.532495Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:27:55.544999Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:32:14.768807Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T21:32:14.768872Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:32:14.768887Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:32:14.768917Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T21:32:19.777694Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:33:04.426832Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:33:09.439697Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:36:01.598714Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t3"
2026-07-24T21:36:06.613697Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:38:49.966643Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:38:51.093872Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T21:38:51.093930Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:38:51.093942Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:38:51.093969Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T21:38:56.103494Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:41:27.064655Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:resize" method="pane.resize" changes_ui=true
2026-07-24T21:41:27.064757Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:resize" method="pane.resize"
2026-07-24T21:41:28.837157Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:resize" method="pane.resize" changes_ui=true
2026-07-24T21:41:28.837256Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:resize" method="pane.resize"
2026-07-24T21:41:29.581641Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T21:41:29.581693Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:41:29.581705Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:41:29.581730Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T21:41:34.587134Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:43:30.874776Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:43:35.889111Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:45:47.516638Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-24T21:45:54.626666Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T21:45:54.626748Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:45:54.626762Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:45:54.626793Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T21:45:59.635321Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:49:08.994570Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T21:49:08.994650Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:49:08.994679Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:49:08.994712Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T21:49:14.002985Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:52:13.003183Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T21:52:13.003242Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:52:13.003253Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:52:13.003277Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T21:52:18.012305Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:53:26.751038Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T21:53:26.751109Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:53:26.751124Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:53:26.751152Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T21:53:31.757250Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T21:57:46.030420Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T21:57:46.030493Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:57:46.030507Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T21:57:46.030537Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T21:57:51.039160Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T22:01:40.805870Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T22:01:45.817930Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T22:01:54.710020Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T22:01:54.710073Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T22:01:54.710082Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T22:01:54.710105Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T22:01:59.529421Z INFO herdr::app::actions: copied selection to clipboard
2026-07-24T22:01:59.720718Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T22:02:00.407938Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T22:02:05.417311Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T22:04:30.290820Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T22:04:35.301373Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T22:05:06.520174Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T22:05:06.520243Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T22:05:06.520256Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T22:05:06.520284Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T22:05:11.530318Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T22:05:19.850836Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t3"
2026-07-24T22:05:24.858198Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T22:05:32.018640Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T22:05:37.029531Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T22:05:48.426692Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t3"
2026-07-24T22:05:53.436963Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T22:06:01.164550Z INFO herdr::app::actions: copied selection to clipboard
2026-07-24T22:06:02.103445Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T22:06:03.184718Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T22:06:08.197923Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T22:07:10.241276Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T22:07:10.241340Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T22:07:10.241349Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T22:07:10.241380Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T22:07:15.244260Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T22:07:33.896022Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t3"
2026-07-24T22:07:36.790544Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T22:07:41.800511Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-24T22:09:20.154594Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-24T22:09:20.154659Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T22:09:20.154669Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-24T22:09:20.154699Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-24T22:09:25.163183Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T03:20:52.664554Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-25T08:33:40.425668Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T08:33:40.425733Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:33:40.425746Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:33:40.425779Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T08:33:45.439002Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T08:34:14.220647Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T08:34:14.221769Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:34:14.221794Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:34:14.221831Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T08:34:18.468246Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T08:34:18.468321Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:34:18.468338Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:34:18.468368Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T08:34:23.476684Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T08:35:23.015877Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:35:28.032512Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T08:37:58.545558Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:38:02.481869Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T08:38:02.482722Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:38:02.482738Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:38:02.482770Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T08:38:07.492669Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T08:38:10.772436Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:38:15.780131Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T08:41:53.205279Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:41:58.222280Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T08:42:00.144382Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T08:42:00.144449Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:42:00.144458Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:42:00.144487Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T08:42:05.151595Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T08:47:43.263886Z INFO herdr::logging: spawning pane terminal event="pane.spawn.start" subsystem="pane" outcome="started" pane_id=12 rows=62 cols=106 scrollback_limit_bytes=10000000
2026-07-25T08:47:43.269625Z INFO herdr::logging: pane child spawned event="pane.spawned" subsystem="pane" outcome="ok" pane_id=12 pid=28595
2026-07-25T08:47:43.269860Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:47:48.277627Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T08:47:50.558718Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T08:47:50.558780Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:47:50.558791Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:47:50.558825Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T08:47:55.564254Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T08:48:08.070987Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T08:48:08.071053Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:48:08.071065Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:48:08.071093Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T08:48:09.887251Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T08:48:09.887303Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:48:09.887313Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:48:09.887345Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T08:48:14.891509Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T08:48:23.083718Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T08:48:23.083794Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:48:23.083809Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:48:23.083840Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T08:48:28.089241Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T08:48:39.175711Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:48:44.186460Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T08:48:47.336661Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:48:52.346317Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T08:48:59.110141Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T08:48:59.110210Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:48:59.110224Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:48:59.110254Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T08:49:04.117759Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T08:49:19.315625Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T08:49:19.315717Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:49:19.315735Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:49:19.315771Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T08:49:24.322972Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T08:56:00.518594Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T08:56:00.518693Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:56:00.518713Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:56:00.518746Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T08:56:05.523856Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T08:56:13.147947Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-25T08:57:22.557892Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T08:57:22.557968Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:57:22.557982Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:57:22.558016Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T08:57:23.381280Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T08:57:23.381337Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:57:23.381347Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T08:57:23.381377Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T08:57:28.384413Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:01:14.040058Z INFO herdr::logging: spawning pane terminal event="pane.spawn.start" subsystem="pane" outcome="started" pane_id=13 rows=31 cols=106 scrollback_limit_bytes=10000000
2026-07-25T09:01:14.049736Z INFO herdr::logging: pane child spawned event="pane.spawned" subsystem="pane" outcome="ok" pane_id=13 pid=29177
2026-07-25T09:01:14.050307Z INFO herdr::logging: tab renamed event="tab.rename" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:01:14.050404Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:01:17.569986Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T09:01:21.332959Z INFO herdr::logging: pane child exited event="pane.exit" subsystem="pane" outcome="completed" pane_id=12 status="ExitStatus { code: 1, signal: None }"
2026-07-25T09:01:21.352047Z INFO herdr::pane: pane session terminated pane=12 pid=28595 signal=Hangup
2026-07-25T09:01:21.353403Z WARN herdr::app::actions: PaneDied for unknown pane pane=12
2026-07-25T09:01:22.456339Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:01:27.464643Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:04:38.947773Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T09:04:43.958494Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:04:44.920527Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t3"
2026-07-25T09:04:46.791110Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:04:49.847732Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T09:04:51.464113Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:04:56.474945Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:05:05.746218Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T09:05:07.638231Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:05:12.645021Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:06:29.197143Z INFO herdr::logging: spawning pane terminal event="pane.spawn.start" subsystem="pane" outcome="started" pane_id=14 rows=62 cols=212 scrollback_limit_bytes=10000000
2026-07-25T09:06:29.204615Z INFO herdr::logging: pane child spawned event="pane.spawned" subsystem="pane" outcome="ok" pane_id=14 pid=29271
2026-07-25T09:06:29.205045Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:06:34.210446Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:06:37.525010Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T09:06:37.525078Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:06:37.525096Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:06:37.525125Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T09:06:42.533462Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:06:55.876025Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T09:06:55.876117Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:06:55.876133Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:06:55.876168Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T09:07:00.881839Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:07:22.906891Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T09:07:22.906967Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:07:22.906983Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:07:22.907020Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T09:07:27.910421Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:07:56.330518Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T09:07:56.330585Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:07:56.330596Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:07:56.330627Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T09:08:01.340699Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:08:02.853921Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T09:08:02.853990Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:08:02.854001Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:08:02.854030Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T09:08:07.862980Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:15:47.482698Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t3"
2026-07-25T09:15:52.490243Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:17:15.436479Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T09:17:19.487520Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T09:17:19.487587Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T09:17:19.487596Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T09:17:19.487626Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T09:17:24.495824Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:19:07.888874Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T09:19:09.664094Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T09:19:13.805505Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T09:19:15.805554Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T09:19:20.814809Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:19:37.170473Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T09:19:37.170556Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T09:19:37.170571Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T09:19:37.170603Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T09:19:42.177066Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:22:31.663169Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t3"
2026-07-25T09:22:36.672066Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:23:33.068860Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T09:23:38.081380Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:26:13.183920Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-25T09:27:18.062214Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T09:27:23.072259Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:28:02.470711Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:28:07.480177Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:30:03.106417Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T09:30:03.106487Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:30:03.106502Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:30:03.106528Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T09:30:05.528427Z INFO herdr::pane: agent changed pane=14 previous_agent=None agent=Some(Codex) process=codex pgid=Some(30779)
2026-07-25T09:30:08.110643Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:30:11.304221Z INFO herdr::pane: agent changed pane=14 previous_agent=Some(Codex) agent=None pgid=Some(29271)
2026-07-25T09:30:15.845291Z INFO herdr::pane: agent changed pane=14 previous_agent=None agent=Some(Codex) process=codex pgid=Some(30989)
2026-07-25T09:31:36.914954Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:31:41.919085Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T09:32:23.005800Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-25T09:32:23.005867Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:32:23.005876Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t4"
2026-07-25T09:32:23.005908Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-25T09:32:24.769763Z INFO herdr::pane: agent changed pane=14 previous_agent=Some(Codex) agent=None pgid=Some(29271)
2026-07-25T09:32:25.294189Z INFO herdr::logging: pane child exited event="pane.exit" subsystem="pane" outcome="completed" pane_id=14 status="ExitStatus { code: 1, signal: None }"
2026-07-25T09:32:25.314788Z INFO herdr::pane: pane session terminated pane=14 pid=29271 signal=Hangup
2026-07-25T09:32:25.316478Z WARN herdr::app::actions: PaneDied for unknown pane pane=14
2026-07-25T09:32:26.951276Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-25T09:32:31.961966Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-25T10:13:30.232263Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-25T11:16:23.997646Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-25T11:46:24.000054Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-25T12:16:24.002870Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-25T21:27:21.050030Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-26T05:41:02.085859Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-26T21:23:23.272165Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-27T08:17:12.366384Z INFO herdr::logging: api request received event="api.request.start" subsystem="api" outcome="started" request_id="cli:pane:focus" method="pane.focus_direction" changes_ui=true
2026-07-27T08:17:12.366679Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-27T08:17:12.366693Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w1" tab_id="w1:t1"
2026-07-27T08:17:12.366729Z INFO herdr::logging: api request completed event="api.request.complete" subsystem="api" outcome="ok" request_id="cli:pane:focus" method="pane.focus_direction"
2026-07-27T08:17:13.176887Z INFO herdr::logging: pane child exited event="pane.exit" subsystem="pane" outcome="completed" pane_id=10 status="ExitStatus { code: 1, signal: None }"
2026-07-27T08:17:13.195909Z INFO herdr::pane: pane session terminated pane=10 pid=18695 signal=Hangup
2026-07-27T08:17:13.196693Z WARN herdr::app::actions: PaneDied for unknown pane pane=10
2026-07-27T08:17:14.600139Z INFO herdr::logging: pane child exited event="pane.exit" subsystem="pane" outcome="completed" pane_id=1 status="ExitStatus { code: 1, signal: None }"
2026-07-27T08:17:14.620176Z INFO herdr::pane: pane session terminated pane=1 pid=8794 signal=Hangup
2026-07-27T08:17:14.621568Z WARN herdr::app::actions: PaneDied for unknown pane pane=1
2026-07-27T08:17:15.556356Z INFO herdr::logging: pane child exited event="pane.exit" subsystem="pane" outcome="completed" pane_id=9 status="ExitStatus { code: 1, signal: None }"
2026-07-27T08:17:15.577929Z INFO herdr::pane: pane session terminated pane=9 pid=18572 signal=Hangup
2026-07-27T08:17:15.579241Z WARN herdr::app::actions: PaneDied for unknown pane pane=9
2026-07-27T08:17:16.216807Z INFO herdr::logging: pane child exited event="pane.exit" subsystem="pane" outcome="completed" pane_id=11 status="ExitStatus { code: 1, signal: None }"
2026-07-27T08:17:16.238969Z INFO herdr::pane: pane session terminated pane=11 pid=20255 signal=Hangup
2026-07-27T08:17:16.240401Z WARN herdr::app::actions: PaneDied for unknown pane pane=11
2026-07-27T08:17:16.875575Z INFO herdr::logging: workspace closed event="workspace.close" subsystem="workspace" outcome="ok" workspace_id="w1"
2026-07-27T08:17:16.879383Z INFO herdr::logging: pane child exited event="pane.exit" subsystem="pane" outcome="completed" pane_id=13 status="ExitStatus { code: 1, signal: None }"
2026-07-27T08:17:16.898617Z INFO herdr::pane: pane session terminated pane=13 pid=29177 signal=Hangup
2026-07-27T08:17:16.900560Z WARN herdr::app::actions: PaneDied for unknown pane pane=13
2026-07-27T08:17:16.900799Z INFO herdr::logging: spawning pane terminal event="pane.spawn.start" subsystem="pane" outcome="started" pane_id=15 rows=63 cols=212 scrollback_limit_bytes=10000000
2026-07-27T08:17:16.910328Z INFO herdr::logging: pane child spawned event="pane.spawned" subsystem="pane" outcome="ok" pane_id=15 pid=37908
2026-07-27T08:17:16.910899Z INFO herdr::logging: workspace created event="workspace.create" subsystem="workspace" outcome="ok" workspace_id="w3" pane_id=15
2026-07-27T08:17:16.910964Z INFO herdr::logging: workspace focused event="workspace.focus" subsystem="workspace" outcome="ok" workspace_id="w3"
2026-07-27T08:17:16.910994Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w3" tab_id="w3:t1"
2026-07-27T08:17:19.902825Z INFO herdr::logging: pane child exited event="pane.exit" subsystem="pane" outcome="completed" pane_id=15 status="ExitStatus { code: 0, signal: None }"
2026-07-27T08:17:19.903131Z INFO herdr::pane: pane session terminated pane=15 pid=37908 signal=Hangup
2026-07-27T08:17:19.903230Z INFO herdr::logging: spawning pane terminal event="pane.spawn.start" subsystem="pane" outcome="started" pane_id=16 rows=63 cols=212 scrollback_limit_bytes=10000000
2026-07-27T08:17:19.906898Z INFO herdr::logging: pane child spawned event="pane.spawned" subsystem="pane" outcome="ok" pane_id=16 pid=37972
2026-07-27T08:17:19.907051Z INFO herdr::logging: workspace created event="workspace.create" subsystem="workspace" outcome="ok" workspace_id="w4" pane_id=16
2026-07-27T08:17:19.907058Z INFO herdr::logging: workspace focused event="workspace.focus" subsystem="workspace" outcome="ok" workspace_id="w4"
2026-07-27T08:17:19.907064Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w4" tab_id="w4:t1"
2026-07-27T08:17:21.725815Z INFO herdr::logging: pane child exited event="pane.exit" subsystem="pane" outcome="completed" pane_id=16 status="ExitStatus { code: 0, signal: None }"
2026-07-27T08:17:21.726361Z INFO herdr::pane: pane session terminated pane=16 pid=37972 signal=Hangup
2026-07-27T08:17:21.726515Z INFO herdr::logging: spawning pane terminal event="pane.spawn.start" subsystem="pane" outcome="started" pane_id=17 rows=63 cols=212 scrollback_limit_bytes=10000000
2026-07-27T08:17:21.732198Z INFO herdr::logging: pane child spawned event="pane.spawned" subsystem="pane" outcome="ok" pane_id=17 pid=38019
2026-07-27T08:17:21.732424Z INFO herdr::logging: workspace created event="workspace.create" subsystem="workspace" outcome="ok" workspace_id="w5" pane_id=17
2026-07-27T08:17:21.732435Z INFO herdr::logging: workspace focused event="workspace.focus" subsystem="workspace" outcome="ok" workspace_id="w5"
2026-07-27T08:17:21.732443Z INFO herdr::logging: tab focused event="tab.focus" subsystem="tab" outcome="ok" workspace_id="w5" tab_id="w5:t1"
2026-07-27T08:17:26.742162Z INFO herdr::logging: session saved event="persist.save" subsystem="persist" outcome="ok" path=/Users/zamp/.config/herdr/session.json workspaces=1
2026-07-27T08:17:29.704424Z INFO herdr::server::headless: client detach requested via keybind client_id=2
2026-07-27T08:17:29.709419Z INFO herdr::server::headless: client disconnected client_id=2
2026-07-27T08:22:06.703297Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-27T09:32:36.426244Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-27T10:56:16.883118Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-27T11:26:16.857274Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-27T11:56:16.872766Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-27T12:26:16.877180Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-27T22:00:33.686857Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T07:19:40.075642Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T07:49:40.084916Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T08:19:40.096188Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T08:49:40.089294Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T09:19:40.067308Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T09:49:40.090768Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T10:54:13.991411Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T11:32:26.492804Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T12:02:26.482344Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T12:32:26.474783Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T13:02:26.488631Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T13:32:26.464326Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T14:02:26.496166Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T15:54:34.086319Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T18:11:57.017781Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T18:44:07.003930Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T19:14:06.972607Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T19:44:06.959635Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T20:14:06.999784Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T20:44:07.011166Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"
2026-07-28T21:34:00.934106Z INFO herdr::logging: checking for updates event="update.check.start" subsystem="update" outcome="started"

View File

@@ -1,122 +0,0 @@
[
{
"plugin_id": "herdr-splits",
"name": "Herdr Splits",
"version": "0.5.1",
"min_herdr_version": "0.7.0",
"description": "Seamless navigation and resizing between Herdr panes and Neovim splits",
"manifest_path": "/Users/zamp/.config/herdr/plugins/github/herdr-splits-d880c4924f2b/herdr-plugin.toml",
"plugin_root": "/Users/zamp/.config/herdr/plugins/github/herdr-splits-d880c4924f2b",
"enabled": true,
"platforms": [
"linux",
"macos"
],
"actions": [
{
"id": "nav-down",
"title": "Navigate down (Neovim/Herder)",
"contexts": [
"global"
],
"command": [
"bash",
"scripts/herdr-nav.sh",
"down"
]
},
{
"id": "nav-left",
"title": "Navigate left (Neovim/Herder)",
"contexts": [
"global"
],
"command": [
"bash",
"scripts/herdr-nav.sh",
"left"
]
},
{
"id": "nav-right",
"title": "Navigate right (Neovim/Herder)",
"contexts": [
"global"
],
"command": [
"bash",
"scripts/herdr-nav.sh",
"right"
]
},
{
"id": "nav-up",
"title": "Navigate up (Neovim/Herder)",
"contexts": [
"global"
],
"command": [
"bash",
"scripts/herdr-nav.sh",
"up"
]
},
{
"id": "resize-down",
"title": "Resize down (Neovim/Herder)",
"contexts": [
"global"
],
"command": [
"bash",
"scripts/herdr-resize.sh",
"down"
]
},
{
"id": "resize-left",
"title": "Resize left (Neovim/Herder)",
"contexts": [
"global"
],
"command": [
"bash",
"scripts/herdr-resize.sh",
"left"
]
},
{
"id": "resize-right",
"title": "Resize right (Neovim/Herder)",
"contexts": [
"global"
],
"command": [
"bash",
"scripts/herdr-resize.sh",
"right"
]
},
{
"id": "resize-up",
"title": "Resize up (Neovim/Herder)",
"contexts": [
"global"
],
"command": [
"bash",
"scripts/herdr-resize.sh",
"up"
]
}
],
"source": {
"kind": "github",
"owner": "lmilojevicc",
"repo": "herdr-splits.nvim",
"resolved_commit": "107273e004e4f7ef07f13c83164d2cb2c51df65d",
"managed_path": "/Users/zamp/.config/herdr/plugins/github/herdr-splits-d880c4924f2b",
"installed_unix_ms": 1784924593009
}
}
]

View File

@@ -1,15 +0,0 @@
# herdr-splits-generated-v1
# 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=ctrl+h
nav_key_down=ctrl+j
nav_key_up=ctrl+k
nav_key_right=ctrl+l
resize_key_left=alt+h
resize_key_down=alt+j
resize_key_up=alt+k
resize_key_right=alt+l
unzoom_on_nav=true
nav_at_edge=wrap

View File

@@ -1,126 +0,0 @@
# Contributing to herdr-splits.nvim
Thanks for your interest in improving herdr-splits.nvim! This guide covers how
to set up a local development environment and submit changes.
herdr-splits.nvim is a Neovim plugin (Lua) that also ships a Herdr-side plugin
(the bash scripts under `scripts/` plus the `herdr-plugin.toml` manifest). The
two halves cooperate — please consider both when making changes to navigation
or resize behaviour.
## Requirements
- Neovim ≥ 0.10
- [Herdr](https://herdr.dev) ≥ 0.7.0 (only needed to exercise cross-pane
behaviour; you can develop the Lua side without a running Herdr session)
- A Herdr-compatible terminal with Option/Alt enabled (see the README's macOS
notes if you're on macOS)
## Repository layout
```
plugin/herdr-splits.lua Neovim entrypoint
lua/herdr-splits/ Neovim plugin source
init.lua setup() + public API
nav.lua resize.lua navigation and resizing
win.lua float / sidebar classification
sync.lua Herdr-side script syncing
config.lua herdr.lua config + core
health.lua :checkhealth herdr-splits
scripts/herdr-nav.sh Herdr-side navigation action
scripts/herdr-resize.sh Herdr-side resize action
scripts/minimal_init.lua Isolated Neovim test bootstrap
tests/ mini.test suites
Makefile Local and CI validation commands
herdr-plugin.toml Herdr plugin manifest (actions + metadata)
```
## Local development setup
### Neovim side
Point your plugin manager at a local clone instead of the published repo:
```lua
-- lazy.nvim
{
dir = '~/Projects/herdr-splits',
cond = vim.env.HERDR_ENV == '1',
config = function()
require('herdr-splits').setup({})
end,
}
```
Changes to files under `lua/` take effect after `:source` or a Neovim restart
(unless your plugin manager hot-reloads).
### Herdr side
Link the clone so Herdr loads its scripts from your working tree:
```bash
herdr plugin link /path/to/herdr-splits
```
After changing `scripts/*.sh` or `herdr-plugin.toml`, run
`herdr server reload-config` to pick them up.
## Coding style
- Match the surrounding Lua style — indentation, quoting, and table layout in
the file you're editing are the source of truth.
- Run [StyLua](https://github.com/JohnnyMorganz/StyLua) if you use it, but note
that no `.stylua.toml` is committed yet; when one is added it will be the
canonical formatter config.
- Run [luacheck](https://github.com/mpeterv/luacheck) where practical to catch
undefined globals and unused variables.
- Keep the bash scripts POSIX-friendly where possible; they run on both Linux
and macOS as declared in `herdr-plugin.toml`.
## Validation
The Make targets initialize and update the pinned test dependency automatically.
To prepare it separately, run `make deps`.
Run the automated checks with:
```bash
make test # isolated mini.test suite in headless Neovim
make check-shell # bash syntax checks for both shipped scripts
make check # all of the above
```
Cross-pane behaviour still requires manual validation in a Herdr session:
1. Load the plugin in Neovim inside a Herdr session (`HERDR_ENV=1`).
2. Run `:checkhealth herdr-splits` and confirm it reports no failures.
3. Exercise navigation (`<C-h/j/k/l>`) and resizing (`<M-h/j/k/l>`) across both
Neovim splits and Herdr panes, including the edge cases you changed:
- `at_edge` behaviour (`wrap` / `stop` / `split` / custom)
- count prefixes (e.g. `3<C-h>`)
- floating windows and embedded-sidebar floats
- auto-unzoom when crossing into a sibling Herdr pane
## Submitting changes
1. Fork the repo and create a branch from `main`.
2. Make focused commits with descriptive messages.
3. Open a pull request against `main` and fill in the PR template — include the
manual validation steps you ran.
4. If your change affects user-facing behaviour or config, update `README.md`
and, for manifest-level changes, `herdr-plugin.toml`.
Keep PRs scoped to one concern. Large changes are easier to review when split
into smaller, independently understandable commits.
## Reporting bugs and ideas
Use the GitHub issue templates (bug report / feature request). Include the
output of `nvim --version`, your plugin manager, the Herdr version
(`herdr --version`), and a minimal reproduction.
## License
By contributing you agree that your contributions are licensed under the MIT
License, the same terms as the rest of the project (see `LICENSE`).

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 Luka
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,21 +0,0 @@
ROOT := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
NVIM ?= nvim
.PHONY: deps test check-shell check
deps:
@if git -C "$(ROOT)" ls-files --error-unmatch deps/mini.test >/dev/null 2>&1; then \
git -C "$(ROOT)" submodule update --init --depth 1 --checkout deps/mini.test; \
else \
test -f "$(ROOT)/deps/mini.test/lua/mini/test.lua"; \
fi
test: deps
cd "$(ROOT)" && "$(NVIM)" --headless --noplugin -i NONE \
-u "$(ROOT)/scripts/minimal_init.lua" -c "lua MiniTest.run()"
check-shell:
bash -n "$(ROOT)/scripts/herdr-nav.sh"
bash -n "$(ROOT)/scripts/herdr-resize.sh"
check: check-shell test

View File

@@ -1,445 +0,0 @@
# 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

View File

@@ -1 +0,0 @@
107273e004e4f7ef07f13c83164d2cb2c51df65d https://github.com/lmilojevicc/herdr-splits.nvim

View File

@@ -1 +0,0 @@
107273e004e4f7ef07f13c83164d2cb2c51df65d

View File

@@ -1,10 +0,0 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = https://github.com/lmilojevicc/herdr-splits.nvim.git
fetch = +refs/heads/*:refs/remotes/origin/*

View File

@@ -1 +0,0 @@
Unnamed repository; edit this file 'description' to name the repository.

View File

@@ -1,15 +0,0 @@
#!/bin/sh
#
# An example hook script to check the commit log message taken by
# applypatch from an e-mail message.
#
# The hook should exit with non-zero status after issuing an
# appropriate message if it wants to stop the commit. The hook is
# allowed to edit the commit message file.
#
# To enable this hook, rename this file to "applypatch-msg".
. git-sh-setup
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
:

View File

@@ -1,24 +0,0 @@
#!/bin/sh
#
# An example hook script to check the commit log message.
# Called by "git commit" with one argument, the name of the file
# that has the commit message. The hook should exit with non-zero
# status after issuing an appropriate message if it wants to stop the
# commit. The hook is allowed to edit the commit message file.
#
# To enable this hook, rename this file to "commit-msg".
# Uncomment the below to add a Signed-off-by line to the message.
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
# hook is more suited to it.
#
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
# This example catches duplicate Signed-off-by lines.
test "" = "$(grep '^Signed-off-by: ' "$1" |
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
echo >&2 Duplicate Signed-off-by lines.
exit 1
}

View File

@@ -1,174 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use IPC::Open2;
# An example hook script to integrate Watchman
# (https://facebook.github.io/watchman/) with git to speed up detecting
# new and modified files.
#
# The hook is passed a version (currently 2) and last update token
# formatted as a string and outputs to stdout a new update token and
# all files that have been modified since the update token. Paths must
# be relative to the root of the working tree and separated by a single NUL.
#
# To enable this hook, rename this file to "query-watchman" and set
# 'git config core.fsmonitor .git/hooks/query-watchman'
#
my ($version, $last_update_token) = @ARGV;
# Uncomment for debugging
# print STDERR "$0 $version $last_update_token\n";
# Check the hook interface version
if ($version ne 2) {
die "Unsupported query-fsmonitor hook version '$version'.\n" .
"Falling back to scanning...\n";
}
my $git_work_tree = get_working_dir();
my $retry = 1;
my $json_pkg;
eval {
require JSON::XS;
$json_pkg = "JSON::XS";
1;
} or do {
require JSON::PP;
$json_pkg = "JSON::PP";
};
launch_watchman();
sub launch_watchman {
my $o = watchman_query();
if (is_work_tree_watched($o)) {
output_result($o->{clock}, @{$o->{files}});
}
}
sub output_result {
my ($clockid, @files) = @_;
# Uncomment for debugging watchman output
# open (my $fh, ">", ".git/watchman-output.out");
# binmode $fh, ":utf8";
# print $fh "$clockid\n@files\n";
# close $fh;
binmode STDOUT, ":utf8";
print $clockid;
print "\0";
local $, = "\0";
print @files;
}
sub watchman_clock {
my $response = qx/watchman clock "$git_work_tree"/;
die "Failed to get clock id on '$git_work_tree'.\n" .
"Falling back to scanning...\n" if $? != 0;
return $json_pkg->new->utf8->decode($response);
}
sub watchman_query {
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
or die "open2() failed: $!\n" .
"Falling back to scanning...\n";
# In the query expression below we're asking for names of files that
# changed since $last_update_token but not from the .git folder.
#
# To accomplish this, we're using the "since" generator to use the
# recency index to select candidate nodes and "fields" to limit the
# output to file names only. Then we're using the "expression" term to
# further constrain the results.
my $last_update_line = "";
if (substr($last_update_token, 0, 1) eq "c") {
$last_update_token = "\"$last_update_token\"";
$last_update_line = qq[\n"since": $last_update_token,];
}
my $query = <<" END";
["query", "$git_work_tree", {$last_update_line
"fields": ["name"],
"expression": ["not", ["dirname", ".git"]]
}]
END
# Uncomment for debugging the watchman query
# open (my $fh, ">", ".git/watchman-query.json");
# print $fh $query;
# close $fh;
print CHLD_IN $query;
close CHLD_IN;
my $response = do {local $/; <CHLD_OUT>};
# Uncomment for debugging the watch response
# open ($fh, ">", ".git/watchman-response.json");
# print $fh $response;
# close $fh;
die "Watchman: command returned no output.\n" .
"Falling back to scanning...\n" if $response eq "";
die "Watchman: command returned invalid output: $response\n" .
"Falling back to scanning...\n" unless $response =~ /^\{/;
return $json_pkg->new->utf8->decode($response);
}
sub is_work_tree_watched {
my ($output) = @_;
my $error = $output->{error};
if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
$retry--;
my $response = qx/watchman watch "$git_work_tree"/;
die "Failed to make watchman watch '$git_work_tree'.\n" .
"Falling back to scanning...\n" if $? != 0;
$output = $json_pkg->new->utf8->decode($response);
$error = $output->{error};
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
# Uncomment for debugging watchman output
# open (my $fh, ">", ".git/watchman-output.out");
# close $fh;
# Watchman will always return all files on the first query so
# return the fast "everything is dirty" flag to git and do the
# Watchman query just to get it over with now so we won't pay
# the cost in git to look up each individual file.
my $o = watchman_clock();
$error = $output->{error};
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
output_result($o->{clock}, ("/"));
$last_update_token = $o->{clock};
eval { launch_watchman() };
return 0;
}
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
return 1;
}
sub get_working_dir {
my $working_dir;
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
$working_dir = Win32::GetCwd();
$working_dir =~ tr/\\/\//;
} else {
require Cwd;
$working_dir = Cwd::cwd();
}
return $working_dir;
}

View File

@@ -1,8 +0,0 @@
#!/bin/sh
#
# An example hook script to prepare a packed repository for use over
# dumb transports.
#
# To enable this hook, rename this file to "post-update".
exec git update-server-info

View File

@@ -1,14 +0,0 @@
#!/bin/sh
#
# An example hook script to verify what is about to be committed
# by applypatch from an e-mail message.
#
# The hook should exit with non-zero status after issuing an
# appropriate message if it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-applypatch".
. git-sh-setup
precommit="$(git rev-parse --git-path hooks/pre-commit)"
test -x "$precommit" && exec "$precommit" ${1+"$@"}
:

View File

@@ -1,49 +0,0 @@
#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-commit".
if git rev-parse --verify HEAD >/dev/null 2>&1
then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=$(git hash-object -t tree /dev/null)
fi
# If you want to allow non-ASCII filenames set this variable to true.
allownonascii=$(git config --type=bool hooks.allownonascii)
# Redirect output to stderr.
exec 1>&2
# Cross platform projects tend to avoid non-ASCII filenames; prevent
# them from being added to the repository. We exploit the fact that the
# printable range starts at the space character and ends with tilde.
if [ "$allownonascii" != "true" ] &&
# Note that the use of brackets around a tr range is ok here, (it's
# even required, for portability to Solaris 10's /usr/bin/tr), since
# the square bracket bytes happen to fall in the designated range.
test $(git diff-index --cached --name-only --diff-filter=A -z $against |
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
then
cat <<\EOF
Error: Attempt to add a non-ASCII file name.
This can cause problems if you want to work with people on other platforms.
To be portable it is advisable to rename the file.
If you know what you are doing you can disable this check using:
git config hooks.allownonascii true
EOF
exit 1
fi
# If there are whitespace errors, print the offending file names and fail.
exec git diff-index --check --cached $against --

View File

@@ -1,13 +0,0 @@
#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git merge" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message to
# stderr if it wants to stop the merge commit.
#
# To enable this hook, rename this file to "pre-merge-commit".
. git-sh-setup
test -x "$GIT_DIR/hooks/pre-commit" &&
exec "$GIT_DIR/hooks/pre-commit"
:

View File

@@ -1,53 +0,0 @@
#!/bin/sh
# An example hook script to verify what is about to be pushed. Called by "git
# push" after it has checked the remote status, but before anything has been
# pushed. If this script exits with a non-zero status nothing will be pushed.
#
# This hook is called with the following parameters:
#
# $1 -- Name of the remote to which the push is being done
# $2 -- URL to which the push is being done
#
# If pushing without using a named remote those arguments will be equal.
#
# Information about the commits which are being pushed is supplied as lines to
# the standard input in the form:
#
# <local ref> <local oid> <remote ref> <remote oid>
#
# This sample shows how to prevent push of commits where the log message starts
# with "WIP" (work in progress).
remote="$1"
url="$2"
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
while read local_ref local_oid remote_ref remote_oid
do
if test "$local_oid" = "$zero"
then
# Handle delete
:
else
if test "$remote_oid" = "$zero"
then
# New branch, examine all commits
range="$local_oid"
else
# Update to existing branch, examine new commits
range="$remote_oid..$local_oid"
fi
# Check for WIP commit
commit=$(git rev-list -n 1 --grep '^WIP' "$range")
if test -n "$commit"
then
echo >&2 "Found WIP commit in $local_ref, not pushing"
exit 1
fi
fi
done
exit 0

View File

@@ -1,169 +0,0 @@
#!/bin/sh
#
# Copyright (c) 2006, 2008 Junio C Hamano
#
# The "pre-rebase" hook is run just before "git rebase" starts doing
# its job, and can prevent the command from running by exiting with
# non-zero status.
#
# The hook is called with the following parameters:
#
# $1 -- the upstream the series was forked from.
# $2 -- the branch being rebased (or empty when rebasing the current branch).
#
# This sample shows how to prevent topic branches that are already
# merged to 'next' branch from getting rebased, because allowing it
# would result in rebasing already published history.
publish=next
basebranch="$1"
if test "$#" = 2
then
topic="refs/heads/$2"
else
topic=`git symbolic-ref HEAD` ||
exit 0 ;# we do not interrupt rebasing detached HEAD
fi
case "$topic" in
refs/heads/??/*)
;;
*)
exit 0 ;# we do not interrupt others.
;;
esac
# Now we are dealing with a topic branch being rebased
# on top of master. Is it OK to rebase it?
# Does the topic really exist?
git show-ref -q "$topic" || {
echo >&2 "No such branch $topic"
exit 1
}
# Is topic fully merged to master?
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
if test -z "$not_in_master"
then
echo >&2 "$topic is fully merged to master; better remove it."
exit 1 ;# we could allow it, but there is no point.
fi
# Is topic ever merged to next? If so you should not be rebasing it.
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
only_next_2=`git rev-list ^master ${publish} | sort`
if test "$only_next_1" = "$only_next_2"
then
not_in_topic=`git rev-list "^$topic" master`
if test -z "$not_in_topic"
then
echo >&2 "$topic is already up to date with master"
exit 1 ;# we could allow it, but there is no point.
else
exit 0
fi
else
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
/usr/bin/perl -e '
my $topic = $ARGV[0];
my $msg = "* $topic has commits already merged to public branch:\n";
my (%not_in_next) = map {
/^([0-9a-f]+) /;
($1 => 1);
} split(/\n/, $ARGV[1]);
for my $elem (map {
/^([0-9a-f]+) (.*)$/;
[$1 => $2];
} split(/\n/, $ARGV[2])) {
if (!exists $not_in_next{$elem->[0]}) {
if ($msg) {
print STDERR $msg;
undef $msg;
}
print STDERR " $elem->[1]\n";
}
}
' "$topic" "$not_in_next" "$not_in_master"
exit 1
fi
<<\DOC_END
This sample hook safeguards topic branches that have been
published from being rewound.
The workflow assumed here is:
* Once a topic branch forks from "master", "master" is never
merged into it again (either directly or indirectly).
* Once a topic branch is fully cooked and merged into "master",
it is deleted. If you need to build on top of it to correct
earlier mistakes, a new topic branch is created by forking at
the tip of the "master". This is not strictly necessary, but
it makes it easier to keep your history simple.
* Whenever you need to test or publish your changes to topic
branches, merge them into "next" branch.
The script, being an example, hardcodes the publish branch name
to be "next", but it is trivial to make it configurable via
$GIT_DIR/config mechanism.
With this workflow, you would want to know:
(1) ... if a topic branch has ever been merged to "next". Young
topic branches can have stupid mistakes you would rather
clean up before publishing, and things that have not been
merged into other branches can be easily rebased without
affecting other people. But once it is published, you would
not want to rewind it.
(2) ... if a topic branch has been fully merged to "master".
Then you can delete it. More importantly, you should not
build on top of it -- other people may already want to
change things related to the topic as patches against your
"master", so if you need further changes, it is better to
fork the topic (perhaps with the same name) afresh from the
tip of "master".
Let's look at this example:
o---o---o---o---o---o---o---o---o---o "next"
/ / / /
/ a---a---b A / /
/ / / /
/ / c---c---c---c B /
/ / / \ /
/ / / b---b C \ /
/ / / / \ /
---o---o---o---o---o---o---o---o---o---o---o "master"
A, B and C are topic branches.
* A has one fix since it was merged up to "next".
* B has finished. It has been fully merged up to "master" and "next",
and is ready to be deleted.
* C has not merged to "next" at all.
We would want to allow C to be rebased, refuse A, and encourage
B to be deleted.
To compute (1):
git rev-list ^master ^topic next
git rev-list ^master next
if these match, topic has not merged in next at all.
To compute (2):
git rev-list master..topic
if this is empty, it is fully merged to "master".
DOC_END

View File

@@ -1,24 +0,0 @@
#!/bin/sh
#
# An example hook script to make use of push options.
# The example simply echoes all push options that start with 'echoback='
# and rejects all pushes when the "reject" push option is used.
#
# To enable this hook, rename this file to "pre-receive".
if test -n "$GIT_PUSH_OPTION_COUNT"
then
i=0
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
do
eval "value=\$GIT_PUSH_OPTION_$i"
case "$value" in
echoback=*)
echo "echo from the pre-receive-hook: ${value#*=}" >&2
;;
reject)
exit 1
esac
i=$((i + 1))
done
fi

View File

@@ -1,42 +0,0 @@
#!/bin/sh
#
# An example hook script to prepare the commit log message.
# Called by "git commit" with the name of the file that has the
# commit message, followed by the description of the commit
# message's source. The hook's purpose is to edit the commit
# message file. If the hook fails with a non-zero status,
# the commit is aborted.
#
# To enable this hook, rename this file to "prepare-commit-msg".
# This hook includes three examples. The first one removes the
# "# Please enter the commit message..." help message.
#
# The second includes the output of "git diff --name-status -r"
# into the message, just before the "git status" output. It is
# commented because it doesn't cope with --amend or with squashed
# commits.
#
# The third example adds a Signed-off-by line to the message, that can
# still be edited. This is rarely a good idea.
COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2
SHA1=$3
/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
# case "$COMMIT_SOURCE,$SHA1" in
# ,|template,)
# /usr/bin/perl -i.bak -pe '
# print "\n" . `git diff --cached --name-status -r`
# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
# *) ;;
# esac
# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
# if test -z "$COMMIT_SOURCE"
# then
# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
# fi

View File

@@ -1,78 +0,0 @@
#!/bin/sh
# An example hook script to update a checked-out tree on a git push.
#
# This hook is invoked by git-receive-pack(1) when it reacts to git
# push and updates reference(s) in its repository, and when the push
# tries to update the branch that is currently checked out and the
# receive.denyCurrentBranch configuration variable is set to
# updateInstead.
#
# By default, such a push is refused if the working tree and the index
# of the remote repository has any difference from the currently
# checked out commit; when both the working tree and the index match
# the current commit, they are updated to match the newly pushed tip
# of the branch. This hook is to be used to override the default
# behaviour; however the code below reimplements the default behaviour
# as a starting point for convenient modification.
#
# The hook receives the commit with which the tip of the current
# branch is going to be updated:
commit=$1
# It can exit with a non-zero status to refuse the push (when it does
# so, it must not modify the index or the working tree).
die () {
echo >&2 "$*"
exit 1
}
# Or it can make any necessary changes to the working tree and to the
# index to bring them to the desired state when the tip of the current
# branch is updated to the new commit, and exit with a zero status.
#
# For example, the hook can simply run git read-tree -u -m HEAD "$1"
# in order to emulate git fetch that is run in the reverse direction
# with git push, as the two-tree form of git read-tree -u -m is
# essentially the same as git switch or git checkout that switches
# branches while keeping the local changes in the working tree that do
# not interfere with the difference between the branches.
# The below is a more-or-less exact translation to shell of the C code
# for the default behaviour for git's push-to-checkout hook defined in
# the push_to_deploy() function in builtin/receive-pack.c.
#
# Note that the hook will be executed from the repository directory,
# not from the working tree, so if you want to perform operations on
# the working tree, you will have to adapt your code accordingly, e.g.
# by adding "cd .." or using relative paths.
if ! git update-index -q --ignore-submodules --refresh
then
die "Up-to-date check failed"
fi
if ! git diff-files --quiet --ignore-submodules --
then
die "Working directory has unstaged changes"
fi
# This is a rough translation of:
#
# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX
if git cat-file -e HEAD 2>/dev/null
then
head=HEAD
else
head=$(git hash-object -t tree --stdin </dev/null)
fi
if ! git diff-index --quiet --cached --ignore-submodules $head --
then
die "Working directory has staged changes"
fi
if ! git read-tree -u -m "$commit"
then
die "Could not update working tree to new HEAD"
fi

View File

@@ -1,77 +0,0 @@
#!/bin/sh
# An example hook script to validate a patch (and/or patch series) before
# sending it via email.
#
# The hook should exit with non-zero status after issuing an appropriate
# message if it wants to prevent the email(s) from being sent.
#
# To enable this hook, rename this file to "sendemail-validate".
#
# By default, it will only check that the patch(es) can be applied on top of
# the default upstream branch without conflicts in a secondary worktree. After
# validation (successful or not) of the last patch of a series, the worktree
# will be deleted.
#
# The following config variables can be set to change the default remote and
# remote ref that are used to apply the patches against:
#
# sendemail.validateRemote (default: origin)
# sendemail.validateRemoteRef (default: HEAD)
#
# Replace the TODO placeholders with appropriate checks according to your
# needs.
validate_cover_letter () {
file="$1"
# TODO: Replace with appropriate checks (e.g. spell checking).
true
}
validate_patch () {
file="$1"
# Ensure that the patch applies without conflicts.
git am -3 "$file" || return
# TODO: Replace with appropriate checks for this patch
# (e.g. checkpatch.pl).
true
}
validate_series () {
# TODO: Replace with appropriate checks for the whole series
# (e.g. quick build, coding style checks, etc.).
true
}
# main -------------------------------------------------------------------------
if test "$GIT_SENDEMAIL_FILE_COUNTER" = 1
then
remote=$(git config --default origin --get sendemail.validateRemote) &&
ref=$(git config --default HEAD --get sendemail.validateRemoteRef) &&
worktree=$(mktemp --tmpdir -d sendemail-validate.XXXXXXX) &&
git worktree add -fd --checkout "$worktree" "refs/remotes/$remote/$ref" &&
git config --replace-all sendemail.validateWorktree "$worktree"
else
worktree=$(git config --get sendemail.validateWorktree)
fi || {
echo "sendemail-validate: error: failed to prepare worktree" >&2
exit 1
}
unset GIT_DIR GIT_WORK_TREE
cd "$worktree" &&
if grep -q "^diff --git " "$1"
then
validate_patch "$1"
else
validate_cover_letter "$1"
fi &&
if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL"
then
git config --unset-all sendemail.validateWorktree &&
trap 'git worktree remove -ff "$worktree"' EXIT &&
validate_series
fi

View File

@@ -1,128 +0,0 @@
#!/bin/sh
#
# An example hook script to block unannotated tags from entering.
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
#
# To enable this hook, rename this file to "update".
#
# Config
# ------
# hooks.allowunannotated
# This boolean sets whether unannotated tags will be allowed into the
# repository. By default they won't be.
# hooks.allowdeletetag
# This boolean sets whether deleting tags will be allowed in the
# repository. By default they won't be.
# hooks.allowmodifytag
# This boolean sets whether a tag may be modified after creation. By default
# it won't be.
# hooks.allowdeletebranch
# This boolean sets whether deleting branches will be allowed in the
# repository. By default they won't be.
# hooks.denycreatebranch
# This boolean sets whether remotely creating branches will be denied
# in the repository. By default this is allowed.
#
# --- Command line
refname="$1"
oldrev="$2"
newrev="$3"
# --- Safety check
if [ -z "$GIT_DIR" ]; then
echo "Don't run this script from the command line." >&2
echo " (if you want, you could supply GIT_DIR then run" >&2
echo " $0 <ref> <oldrev> <newrev>)" >&2
exit 1
fi
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
exit 1
fi
# --- Config
allowunannotated=$(git config --type=bool hooks.allowunannotated)
allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
# check for no description
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
case "$projectdesc" in
"Unnamed repository"* | "")
echo "*** Project description file hasn't been set" >&2
exit 1
;;
esac
# --- Check types
# if $newrev is 0000...0000, it's a commit to delete a ref.
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
if [ "$newrev" = "$zero" ]; then
newrev_type=delete
else
newrev_type=$(git cat-file -t $newrev)
fi
case "$refname","$newrev_type" in
refs/tags/*,commit)
# un-annotated tag
short_refname=${refname##refs/tags/}
if [ "$allowunannotated" != "true" ]; then
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
exit 1
fi
;;
refs/tags/*,delete)
# delete tag
if [ "$allowdeletetag" != "true" ]; then
echo "*** Deleting a tag is not allowed in this repository" >&2
exit 1
fi
;;
refs/tags/*,tag)
# annotated tag
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
then
echo "*** Tag '$refname' already exists." >&2
echo "*** Modifying a tag is not allowed in this repository." >&2
exit 1
fi
;;
refs/heads/*,commit)
# branch
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
echo "*** Creating a branch is not allowed in this repository" >&2
exit 1
fi
;;
refs/heads/*,delete)
# delete branch
if [ "$allowdeletebranch" != "true" ]; then
echo "*** Deleting a branch is not allowed in this repository" >&2
exit 1
fi
;;
refs/remotes/*,commit)
# tracking branch
;;
refs/remotes/*,delete)
# delete tracking branch
if [ "$allowdeletebranch" != "true" ]; then
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
exit 1
fi
;;
*)
# Anything else (is there anything else?)
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
exit 1
;;
esac
# --- Finished
exit 0

View File

@@ -1,6 +0,0 @@
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~

View File

@@ -1 +0,0 @@
0000000000000000000000000000000000000000 107273e004e4f7ef07f13c83164d2cb2c51df65d Pablu <me@pablu.de> 1784924592 +0200 checkout: moving from main to FETCH_HEAD

View File

@@ -1 +0,0 @@
x<01><53>0<><EE99A7>kB<>Ӫ=<3D><><17><><EFBFBD><EFBFBD>Cpcl<63><6C>d<EFBFBD>վ{<7B>&R6Z<36>Ro<52><6F>0<EFBFBD><30><EFBFBD>g><3E><><EFBFBD><EFBFBD>}<7D><><EFBFBD><06><>kQX3<14><><EFBFBD><EFBFBD><EFBFBD>I<EFBFBD>p܌3<DC8C><33>|_<>2?rS<72><53>~G<>a(<28>ܢ<>W<EFBFBD><57>42Z<13>p<>eQ<65><51><18>3^R<><52>l\<07><><EFBFBD><EFBFBD>T<><54>(<28><>'m<><6D>兾*;<3B><><EFBFBD>"<01>T<19>:{p<>qup<11><><EFBFBD><19>Ϡ<EFBFBD>Fw<13><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <09> <10><><EFBFBD><14><>(<28> <09>j<1E>{n<05><><EFBFBD><EFBFBD>.<2E>D,d<><64>><3E><>w<EFBFBD>qJ<71>O-><3E><><EFBFBD><01><18>8<EFBFBD>X7<58>X<EFBFBD><58>[Q <09>ƺ<EFBFBD><C6BA><EFBFBD><EFBFBD>;<3B>%kAHV<48><56><EFBFBD>:<3A>#<23>ɩ<EFBFBD>{Ӻ3<D3BA><33>ئ74<1B>W><3E>сD<D181>׈5<D788>/\Gh0<><30><1F>dl<64><17>N:^+<2B><06><><EFBFBD>l<EFBFBD>ƒSO<53><4F>zJ <20>l<EFBFBD><6C><EFBFBD>#<23><>Z<EFBFBD>жC<D0B6>b<EFBFBD>=q<><71><EFBFBD><11><><EFBFBD>{<7B>w<EFBFBD><77>1:U%<25><><EFBFBD>#<23><>

View File

@@ -1 +0,0 @@
x<01>PKj<4B>0 <0C>ڧ<EFBFBD><DAA7>N )<29>&<10>z<><7A><EFBFBD>r<EFBFBD><72><EFBFBD><EFBFBD>r<EFBFBD><72>w<EFBFBD><77> <09><>+I<><49><EFBFBD>66xߞl<DF9E>W 9<><39>#̠p+<2B><<3C><><EFBFBD>A<EFBFBD>=<3D><0F>ɫ<EFBFBD>֜1<D69C><31><EFBFBD>)<29><><01><0E><19>;<3B><>=<14><><07>FK<08>x<EFBFBD>V<EFBFBD>ԏ<EFBFBD>-+<2B><18>S<><53><19><><EFBFBD><EFBFBD>׷:tA<19><>H<EFBFBD><48>U<EFBFBD>)N<><4E>V'6D<36><44>x2<0E>J<EFBFBD>)&<26><><EFBFBD>ht<68>#@J<><4A>˜<EFBFBD><C298>Lfj<66>~<16>P<EFBFBD><50><EFBFBD><EFBFBD>H%<25><06>kqov[<06><>W<EFBFBD>6s-<2D><>w2<77><32><EFBFBD><EFBFBD><01>:<3A><><EFBFBD>fG=<3D><><EFBFBD><EFBFBD>n<EFBFBD>yhqy><0F><03><>W<EFBFBD><18>:}<7D><><EFBFBD><EFBFBD>ޱ_@$<24><>

View File

@@ -1,2 +0,0 @@
xu<><75>R<EFBFBD>0D<><44><15>%)<29><><19><>hh<68><68>'rt<72>o"<22><>Ir<49><72>G<EFBFBD>a<EFBFBD>*<2A>n<EFBFBD>n<EFBFBD>n<17><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>1́s<CD81>q<EFBFBD> s(Gt"9eu<65>5Uv9<><39>@<40>o<10>ɣ<EFBFBD><C9A3>;I[<5B><><14>G<EFBFBD><47>mZ)<29>C<EFBFBD><43><EFBFBD><EFBFBD>y<EFBFBD><79><EFBFBD>q<EFBFBD><71><EFBFBD><EFBFBD><EFBFBD>'Mp!<21>.zC\<5C>>
+<2B>w<EFBFBD><77>v<EFBFBD><76>5<EFBFBD>{T#;<3B><><EFBFBD><EFBFBD>*<2A>vs<76>P<>E<EFBFBD><45><EFBFBD><EFBFBD><EFBFBD><EFBFBD>d-dZ<64>׆<EFBFBD><D786><EFBFBD>8<EFBFBD>!<06>TG<54>J<EFBFBD><4A><EFBFBD><EFBFBD>y<79>(8.r<>J<EFBFBD><4A><EFBFBD>kf<6B>ߥ&W<><57>ė<EFBFBD><1A>8<0E><><1F>*Пy<D09F><79><02>4<EFBFBD><34>

View File

@@ -1,2 +0,0 @@
x<01><>[K1<10>}m~Ű<><C5B0>><3E>k}hiU<69>b<EFBFBD><17>"B<>n<EFBFBD>&<26><>.<2E>T<EFBFBD><54>;I<><49>܇0<DC87>M<EFBFBD>sN<73><4E>u<17><>'<27><><EFBFBD><1A>chwY<77><1A>%V\Y\5s<35><73><EFBFBD><1C><><EFBFBD><EFBFBD><EFBFBD>v6<76>n泧u?<3F>|<7C>-<2D>r f<>*B<>w<><77><EFBFBD><10>hx<><<3C>R<EFBFBD>o<EFBFBD>I<EFBFBD><49><EFBFBD>&$<24><1C>֕<EFBFBD><D695>Ny<4E>7<EFBFBD><37><EFBFBD>Q<EFBFBD><51><EFBFBD>vt<76><74>p@<40><><EFBFBD><EFBFBD>4<EFBFBD><34><EFBFBD>R&b^)<29><><><C9B9>& <20>l<EFBFBD><19><>Ka<4B><61><EFBFBD>Z<5A>.μ@4bP<62><50><11>9<EFBFBD>*<2A><><EFBFBD><EFBFBD>%3<>D<EFBFBD><44>H<EFBFBD>$L<>G<EFBFBD>h<EFBFBD>q_<71>rX<><58>x<EFBFBD><78>]<5D><>o!q<><71>*<2A>U<EFBFBD>#!<21>N'6<>1<<3C>R0<52><30>c|S7:<3A><><01>`<60>ZN<5A><1A>wsWZ<57><5A><EFBFBD><EFBFBD><EFBFBD><EFBFBD>M <0C>T<EFBFBD><54><EFBFBD><EFBFBD><,pk<1D><>`<60><><EFBFBD><EFBFBD>o<EFBFBD><6F><14>I<EFBFBD><49>/Q
<EFBFBD>-5l<35>w2<77><32><EFBFBD>N}<7D>t<EFBFBD> 0<>1<EFBFBD><18>|E<1B>w

View File

@@ -1 +0,0 @@
107273e004e4f7ef07f13c83164d2cb2c51df65d

View File

@@ -1,100 +0,0 @@
name: Bug report
description: Report something that isn't working as expected
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to file a bug report. Please fill in the
fields below so it can be reproduced.
- type: textarea
id: description
attributes:
label: Description
description: A clear description of the bug.
validations:
required: true
- type: input
id: nvim-version
attributes:
label: Neovim version
description: Output of `nvim --version` (first line is enough).
placeholder: "NVIM v0.10.0"
validations:
required: true
- type: dropdown
id: plugin-manager
attributes:
label: Plugin manager
description: How is herdr-splits.nvim installed?
options:
- lazy.nvim
- packer.nvim
- None / manual (local path)
- Other
validations:
required: true
- type: input
id: herdr-version
attributes:
label: Herdr version
description: Output of `herdr --version`.
placeholder: "herdr 0.7.0"
validations:
required: true
- type: input
id: herdr-splits-version
attributes:
label: herdr-splits.nvim version
description: The plugin version / commit you're on (check with your plugin manager).
placeholder: "0.3.0"
validations:
required: false
- type: textarea
id: repro
attributes:
label: Reproduction steps
description: Minimal, numbered steps that trigger the bug.
placeholder: |
1. Open Neovim inside a Herdr session (`HERDR_ENV=1`)
2. Create two vertical splits
3. Press `<C-l>`
4. ...
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behaviour
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual behaviour
validations:
required: true
- type: textarea
id: checkhealth
attributes:
label: ":checkhealth herdr-splits output"
description: Paste the relevant output of `:checkhealth herdr-splits` (Neovim ≥ 0.10).
render: shell
validations:
required: false
- type: textarea
id: config
attributes:
label: Setup / config
description: Your `setup({})` call and any relevant keybinds.
render: lua
validations:
required: false
- type: textarea
id: os
attributes:
label: OS / terminal
description: Operating system and terminal emulator (note if Option-as-Alt is enabled on macOS).
placeholder: "macOS 14 / Ghostty (macos-option-as-alt = true)"
validations:
required: false

View File

@@ -1,32 +0,0 @@
name: Feature request
description: Suggest an idea or improvement for herdr-splits.nvim
labels: [enhancement]
body:
- type: textarea
id: problem
attributes:
label: Problem
description: What problem does this solve? What's currently hard or impossible to do?
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed solution
description: What would you like herdr-splits.nvim to do instead?
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Any workarounds or other approaches you've tried.
validations:
required: false
- type: textarea
id: context
attributes:
label: Additional context
description: Anything else relevant — links, screenshots, related config.
validations:
required: false

View File

@@ -1,31 +0,0 @@
<!-- Thanks for the PR! Fill in the sections below. -->
## Summary
<!-- What does this change do, and why? One or two sentences. -->
## Changes
<!-- Bullet list of the notable changes. Mention any user-facing changes. -->
-
## Validation
<!-- Check the automated commands you ran and describe any manual coverage. -->
- [ ] `make test` passes
- [ ] `make check-shell` passes
- [ ] Plugin loads with no errors in a Neovim + Herdr session (`HERDR_ENV=1`)
- [ ] `:checkhealth herdr-splits` reports no failures
- [ ] Navigation (`<C-h/j/k/l>`) works across Neovim splits and Herdr panes
- [ ] Resizing (`<M-h/j/k/l>`) works across Neovim splits and Herdr panes
- [ ] Edge cases relevant to this change exercised (at_edge, count prefix,
floats, auto-unzoom)
- [ ] `README.md` / `herdr-plugin.toml` updated (if user-facing)
## Notes for review
<!-- Anything reviewers should pay attention to, or follow-up work. -->
Closes #NN

View File

@@ -1,43 +0,0 @@
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
shell:
name: Bash syntax
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- run: make check-shell
test:
name: Neovim ${{ matrix.neovim-version }}
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
neovim-version: [v0.10.0, stable]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
submodules: true
- uses: rhysd/action-setup-vim@febef33995d6649302e9d88dda81e071b68f16a7 # v1.6.1
with:
neovim: true
version: ${{ matrix.neovim-version }}
- run: make test

View File

@@ -1,3 +0,0 @@
[submodule "deps/mini.test"]
path = deps/mini.test
url = https://github.com/nvim-mini/mini.test.git

View File

@@ -1,62 +0,0 @@
id = "herdr-splits"
name = "Herdr Splits"
version = "0.5.1"
min_herdr_version = "0.7.0"
description = "Seamless navigation and resizing between Herdr panes and Neovim splits"
platforms = ["linux", "macos"]
# Herdr-side navigation actions. These check if the focused pane is running
# Neovim and either forward the key chord or move Herdr pane focus.
# Bind them in ~/.config/herdr/config.toml:
# [[keys.command]]
# key = "ctrl+h"
# type = "plugin_action"
# command = "herdr-splits.nav-left"
[[actions]]
id = "nav-left"
title = "Navigate left (Neovim/Herder)"
contexts = ["global"]
command = ["bash", "scripts/herdr-nav.sh", "left"]
[[actions]]
id = "nav-down"
title = "Navigate down (Neovim/Herder)"
contexts = ["global"]
command = ["bash", "scripts/herdr-nav.sh", "down"]
[[actions]]
id = "nav-up"
title = "Navigate up (Neovim/Herder)"
contexts = ["global"]
command = ["bash", "scripts/herdr-nav.sh", "up"]
[[actions]]
id = "nav-right"
title = "Navigate right (Neovim/Herder)"
contexts = ["global"]
command = ["bash", "scripts/herdr-nav.sh", "right"]
[[actions]]
id = "resize-left"
title = "Resize left (Neovim/Herder)"
contexts = ["global"]
command = ["bash", "scripts/herdr-resize.sh", "left"]
[[actions]]
id = "resize-down"
title = "Resize down (Neovim/Herder)"
contexts = ["global"]
command = ["bash", "scripts/herdr-resize.sh", "down"]
[[actions]]
id = "resize-up"
title = "Resize up (Neovim/Herder)"
contexts = ["global"]
command = ["bash", "scripts/herdr-resize.sh", "up"]
[[actions]]
id = "resize-right"
title = "Resize right (Neovim/Herder)"
contexts = ["global"]
command = ["bash", "scripts/herdr-resize.sh", "right"]

View File

@@ -1,236 +0,0 @@
---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. `<C-h>`, `<M-Left>`, `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 = {
{ '<C-h>', 'ctrl+h' }, { '<C-j>', 'ctrl+j' }, { '<C-k>', 'ctrl+k' }, { '<C-l>', 'ctrl+l' },
{ '<M-h>', 'alt+h' }, { '<M-Left>', 'alt+left' }, { '<S-Left>', 'shift+left' },
{ '<D-x>', 'cmd+x' }, { '<C-M-h>', 'ctrl+alt+h' }, { 'h', 'h' }, { '<Esc>', '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

View File

@@ -1,146 +0,0 @@
---@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. `<C-h>`, `<M-Left>`); stored/written in Herdr notation.
---@field nav_keys table<string,string> Forward chords per direction: {left,down,up,right} (Neovim notation in opts; Herdr notation internally)
---@field resize_keys table<string,string> 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

View File

@@ -1,46 +0,0 @@
---:checkhealth herdr-splits integration.
---Discovered automatically by Neovim's built-in `:checkhealth` via the
---runtimefile lookup in `lua/<plugin>/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

View File

@@ -1,160 +0,0 @@
---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 <dir> --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 <dir> --amount <float> --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

View File

@@ -1,162 +0,0 @@
---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', '<C-h>', require('herdr-splits').move_cursor_left)
--- vim.keymap.set('n', '<M-h>', 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

View File

@@ -1,206 +0,0 @@
---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

View File

@@ -1,108 +0,0 @@
---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

View File

@@ -1,107 +0,0 @@
---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 -- "@<abs>/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

View File

@@ -1,154 +0,0 @@
---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

View File

@@ -1,11 +0,0 @@
-- herdr-splits.nvim plugin bootstrap.
-- This file is loaded by Neovim when the plugin is found on the runtimepath.
-- It defers all work to `require('herdr-splits')`.
if vim.g.loaded_herdr_splits then
return
end
vim.g.loaded_herdr_splits = true
-- Defer loading until user calls setup() or invokes a function.
-- The main module is available as require('herdr-splits').

View File

@@ -1,104 +0,0 @@
#!/usr/bin/env bash
# Herdr navigation helper — used by herdr keybinds for seamless two-way nav.
#
# When a navigation key is pressed in Herdr:
# 1. Check if the focused pane is running Neovim in the foreground.
# - If yes: forward the key chord into that pane. The Neovim plugin owns
# in-split movement, edge crossing, and unzoom — do NOT unzoom here, or
# moving between Neovim splits would incorrectly unzoom the pane.
# 2. If no (a plain Herdr pane): unzoom if needed, then move Herdr pane focus.
# At a layout edge, focus wraps around to the opposite side (smart-splits
# style), so navigating past the last pane lands on the first — unless
# `nav_at_edge=stop`, in which case it halts at the edge.
#
# Auto-unzoom is configurable via `unzoom_on_nav=false` in
# `~/.config/herdr/plugins/config/herdr-splits/herdr-splits.conf`
# (same file the Neovim plugin reads). Default: enabled.
#
# Edge behaviour for plain Herdr panes is configurable via `nav_at_edge` in
# the same conf file: `wrap` (default; wraps to the opposite side at an edge)
# or `stop` (navigation halts at the edge instead of wrapping).
#
# Usage: herdr-nav.sh <left|down|up|right>
set -euo pipefail
dir="${1:?usage: herdr-nav.sh <left|down|up|right>}"
herdr="${HERDR_BIN_PATH:-herdr}"
case "$dir" in
left) key="ctrl+h"; config_key="nav_key_left"; opp="right" ;;
down) key="ctrl+j"; config_key="nav_key_down"; opp="up" ;;
up) key="ctrl+k"; config_key="nav_key_up"; opp="down" ;;
right) key="ctrl+l"; config_key="nav_key_right"; opp="left" ;;
*) echo "herdr-nav.sh: unknown direction: $dir" >&2; exit 2 ;;
esac
# Resolve `unzoom_on_nav` and `nav_at_edge` from the shared config file.
# Defaults: unzoom enabled, nav_at_edge=wrap (backward compatible).
unzoom=1
nav_at_edge=wrap
config_path="${HERDR_SPLITS_CONFIG:-${HERDR_PLUGIN_CONFIG_DIR:-$HOME/.config/herdr/plugins/config/herdr-splits}/herdr-splits.conf}"
if [ -r "$config_path" ]; then
configured_key=$(sed -n -E "s/^[[:space:]]*${config_key}[[:space:]]*=[[:space:]]*([^[:space:]#]+).*$/\\1/p" "$config_path" | tail -n 1)
if [ -n "$configured_key" ]; then
key="$configured_key"
fi
if grep -Eq '^[[:space:]]*unzoom_on_nav[[:space:]]*=[[:space:]]*false' "$config_path"; then
unzoom=0
fi
if grep -Eq '^[[:space:]]*nav_at_edge[[:space:]]*=[[:space:]]*stop' "$config_path"; then
nav_at_edge=stop
fi
fi
# Get focused pane ID from server (best-effort; empty is fine — we fall
# through to the non-vim path below, which uses --current directly).
pane_id=$("$herdr" pane current --current 2>/dev/null | grep -o '"pane_id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/' || true)
# Check if focused pane is running vim/nvim as foreground process.
is_vim=0
if [ -n "$pane_id" ] && pane_info=$("$herdr" pane process-info --current 2>/dev/null); then
if echo "$pane_info" | grep -qiE '"name"[[:space:]]*:[[:space:]]*"(g?(view|l?n?vim?x?)(diff)?)"' 2>/dev/null; then
is_vim=1
fi
fi
# Vim pane: forward the chord; the Neovim plugin decides movement + unzoom.
if [ "$is_vim" -eq 1 ]; then
exec "$herdr" pane send-keys "$pane_id" "$key"
fi
# --- Non-vim Herdr pane: unzoom if needed, then focus (wrapping at edges) ---
# `pane edges` reports both edge flags and the zoomed state in one call.
edges_out=$("$herdr" pane edges --current 2>/dev/null || true)
# A zoomed pane fills the tab and reports itself at every edge, so the edge
# flags are useless for the wrap decision. Unzoom (if enabled) so we can
# trust them. If unzoom is disabled we leave zoom untouched and must NOT use
# the (all-true) edge flags — wrap is skipped below in that case.
edges_trusted=1
if printf '%s' "$edges_out" | grep -q '"zoomed"[[:space:]]*:[[:space:]]*true'; then
if [ "$unzoom" -eq 1 ]; then
"$herdr" pane zoom --off --current 2>/dev/null || true
# Layout changed; re-read edges so the wrap check is accurate.
edges_out=$("$herdr" pane edges --current 2>/dev/null || true)
else
edges_trusted=0
fi
fi
# Move to the neighbor in the requested direction. When already at the
# requested edge (no neighbor there): wrap to the opposite side, or — when
# nav_at_edge=stop — do nothing. Skip the edge check when the flags are
# unreliable (still zoomed, unzoom disabled); in that case we can't tell
# whether we're at an edge, so we just focus in the requested direction.
if [ "$edges_trusted" -eq 1 ] && printf '%s' "$edges_out" | grep -q "\"$dir\"[[:space:]]*:[[:space:]]*true"; then
if [ "$nav_at_edge" = stop ]; then
exit 0
fi
exec "$herdr" pane focus --direction "$opp" --current
else
exec "$herdr" pane focus --direction "$dir" --current
fi

View File

@@ -1,58 +0,0 @@
#!/usr/bin/env bash
# Herdr resize helper — used by herdr keybinds for seamless two-way resize.
#
# When a resize key (alt+h/j/k/l) is pressed in Herdr:
# 1. Check if the focused pane is running Neovim in the foreground
# 2. If yes: forward the key chord into that pane (Neovim's plugin handles it)
# 3. If no: resize Herdr pane directly
#
# Zoom state is left untouched (matches smart-splits.nvim's
# disable_multiplexer_nav_when_zoomed default).
#
# Usage: herdr-resize.sh <left|down|up|right> [amount]
set -euo pipefail
dir="${1:?usage: herdr-resize.sh <left|down|up|right> [amount]}"
amount="${2:-0.03}"
herdr="${HERDR_BIN_PATH:-herdr}"
case "$dir" in
left) key="alt+h"; config_key="resize_key_left" ;;
down) key="alt+j"; config_key="resize_key_down" ;;
up) key="alt+k"; config_key="resize_key_up" ;;
right) key="alt+l"; config_key="resize_key_right" ;;
*) echo "herdr-resize.sh: unknown direction: $dir" >&2; exit 2 ;;
esac
# Resolve the forward chord from the shared config file.
# Unset values keep the alt+hjkl default.
config_path="${HERDR_SPLITS_CONFIG:-${HERDR_PLUGIN_CONFIG_DIR:-$HOME/.config/herdr/plugins/config/herdr-splits}/herdr-splits.conf}"
if [ -r "$config_path" ]; then
configured_key=$(sed -n -E "s/^[[:space:]]*${config_key}[[:space:]]*=[[:space:]]*([^[:space:]#]+).*$/\\1/p" "$config_path" | tail -n 1)
if [ -n "$configured_key" ]; then
key="$configured_key"
fi
fi
# Get focused pane ID from server
pane_id=$("$herdr" pane current --current 2>/dev/null | grep -o '"pane_id"\s*:\s*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
if [ -z "$pane_id" ]; then
exec "$herdr" pane resize --direction "$dir" --amount "$amount" --current
fi
# Check if focused pane is running vim/nvim as foreground process.
# When it is, the key is forwarded into Neovim so the plugin can decide
# whether to resize a Neovim split or hand off to Herdr.
is_vim=0
if pane_info=$("$herdr" pane process-info --current 2>/dev/null); then
if echo "$pane_info" | grep -qiE '"name"\s*:\s*"(g?(view|l?n?vim?x?)(diff)?)"' 2>/dev/null; then
is_vim=1
fi
fi
if [ "$is_vim" -eq 1 ]; then
exec "$herdr" pane send-keys "$pane_id" "$key"
else
exec "$herdr" pane resize --direction "$dir" --amount "$amount" --current
fi

View File

@@ -1,14 +0,0 @@
local source = debug.getinfo(1, 'S').source
assert(source:sub(1, 1) == '@', 'could not locate minimal_init.lua')
local root = vim.fn.fnamemodify(source:sub(2), ':p:h:h')
vim.opt.runtimepath:prepend(root)
vim.opt.runtimepath:prepend(root .. '/deps/mini.test')
require('mini.test').setup({
collect = {
find_files = function()
return vim.fn.glob(root .. '/tests/test_*.lua', true, true)
end,
},
})

View File

@@ -1,196 +0,0 @@
local T
local expect = MiniTest.expect
local child = MiniTest.new_child_neovim()
local source = debug.getinfo(1, 'S').source
local root = vim.fn.fnamemodify(source:sub(2), ':p:h:h')
local function write_config(lines)
child.lua_func(function(config_lines)
local path = vim.env.HERDR_SPLITS_CONFIG
vim.fn.mkdir(vim.fn.fnamemodify(path, ':h'), 'p')
vim.fn.writefile(config_lines, path)
end, lines)
end
T = MiniTest.new_set({
hooks = {
pre_case = function()
child.restart({ '--noplugin', '-u', 'NONE', '-i', 'NONE' })
child.cmd('set runtimepath^=' .. vim.fn.fnameescape(root))
child.lua([[
local test_dir = vim.fn.tempname()
vim.fn.mkdir(test_dir, 'p')
vim.g.herdr_splits_test_dir = test_dir
vim.env.HOME = test_dir .. '/home'
vim.env.XDG_CONFIG_HOME = test_dir .. '/xdg'
vim.env.HERDR_SPLITS_CONFIG = test_dir .. '/nested/herdr-splits.conf'
vim.env.HERDR_BIN_PATH = test_dir .. '/missing-herdr'
vim.env.HERDR_PLUGIN_CONFIG_DIR = nil
]])
end,
post_case = function()
local test_dir = child.g.herdr_splits_test_dir
child.stop()
vim.fn.delete(test_dir, 'rf')
end,
},
})
T['to_herdr translates key notation'] = function()
local translate = function(value)
return child.lua_func(function(key)
return require('herdr-splits.conf').to_herdr(key)
end, value)
end
expect.equality(translate('<C-M-Left>'), 'ctrl+alt+left')
expect.equality(translate('<A-Return>'), 'alt+enter')
expect.equality(translate('<S-Space>'), 'shift+space')
expect.equality(translate('H'), 'h')
expect.equality(translate('<Escape>'), 'esc')
expect.equality(translate(''), vim.NIL)
expect.equality(translate(false), vim.NIL)
expect.equality(translate('<C>'), vim.NIL)
end
T['read_managed handles a missing file'] = function()
local result = child.lua_func(function()
local values, adopted = require('herdr-splits.conf').read_managed()
return { adopted = adopted, count = vim.tbl_count(values) }
end)
expect.equality(result, { adopted = false, count = 0 })
end
T['read_managed adopts managed legacy values with last value winning'] = function()
write_config({
'# legacy config',
' nav_key_left = ctrl+old',
'nav_key_left=ctrl+new # last value wins',
'resize_key_up = alt+up',
'unzoom_on_nav=false',
'nav_at_edge=stop',
'unknown_key=ignored',
})
local result = child.lua_func(function()
local values, adopted = require('herdr-splits.conf').read_managed()
return {
adopted = adopted,
nav_left = values.nav_keys.left,
resize_up = values.resize_keys.up,
unzoom_on_nav = values.unzoom_on_nav,
nav_at_edge = values.nav_at_edge,
unknown = values.unknown_key,
}
end)
expect.equality(result, {
adopted = true,
nav_left = 'ctrl+new',
resize_up = 'alt+up',
unzoom_on_nav = false,
nav_at_edge = 'stop',
})
end
T['write creates generated content without leftover temporary files'] = function()
local result = child.lua_func(function()
local conf = require('herdr-splits.conf')
local ok, err = conf.write({
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 = false,
nav_at_edge = 'stop',
})
local values, adopted = conf.read_managed()
return {
ok = ok,
err = err,
adopted = adopted,
adopted_count = vim.tbl_count(values),
}
end)
expect.equality(result, { ok = true, adopted = false, adopted_count = 0 })
local content = child.lua_func(function()
local file = assert(io.open(vim.env.HERDR_SPLITS_CONFIG, 'r'))
local bytes = file:read('*a')
file:close()
return bytes
end)
expect.equality(content, table.concat({
'# herdr-splits-generated-v1',
'# 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=ctrl+h',
'nav_key_down=ctrl+j',
'nav_key_up=ctrl+k',
'nav_key_right=ctrl+l',
'resize_key_left=alt+h',
'resize_key_down=alt+j',
'resize_key_up=alt+k',
'resize_key_right=alt+l',
'unzoom_on_nav=false',
'nav_at_edge=stop',
'',
}, '\n'))
expect.equality(child.lua_get([[vim.fn.glob(vim.env.HERDR_SPLITS_CONFIG .. '.tmp.*', false, true)]]), {})
end
T['config setup applies precedence and resets omitted managed options'] = function()
write_config({
'nav_key_left=ctrl+legacy',
'resize_key_left=alt+legacy',
'unzoom_on_nav=false',
'nav_at_edge=stop',
})
local result = child.lua_func(function()
vim.notify = function() end
local config = require('herdr-splits.config')
config.setup({
nav_keys = { left = '<C-x>' },
unzoom_on_nav = true,
})
local first = {
nav_left = config.nav_keys.left,
resize_left = config.resize_keys.left,
unzoom_on_nav = config.unzoom_on_nav,
nav_at_edge = config.nav_at_edge,
}
config.setup({ nav_keys = { right = '<C-r>' } })
local second = {
nav_left = config.nav_keys.left,
nav_right = config.nav_keys.right,
resize_left = config.resize_keys.left,
unzoom_on_nav = config.unzoom_on_nav,
nav_at_edge = config.nav_at_edge,
}
return { first = first, second = second }
end)
expect.equality(result, {
first = {
nav_left = 'ctrl+x',
resize_left = 'alt+legacy',
unzoom_on_nav = true,
nav_at_edge = 'stop',
},
second = {
nav_left = 'ctrl+h',
nav_right = 'ctrl+r',
resize_left = 'alt+h',
unzoom_on_nav = true,
nav_at_edge = 'wrap',
},
})
end
return T

View File

@@ -1,267 +0,0 @@
local T
local expect = MiniTest.expect
local child = MiniTest.new_child_neovim()
local source = debug.getinfo(1, 'S').source
local root = vim.fn.fnamemodify(source:sub(2), ':p:h:h')
T = MiniTest.new_set({
hooks = {
pre_case = function()
child.restart({ '--noplugin', '-u', 'NONE', '-i', 'NONE' })
child.cmd('set runtimepath^=' .. vim.fn.fnameescape(root))
end,
post_case = function()
child.stop()
end,
},
})
T['resolves the binary and requires a complete session environment'] = function()
local result = child.lua_func(function()
local config = require('herdr-splits.config')
local herdr = require('herdr-splits.herdr')
vim.env.HERDR_BIN_PATH = '/env/herdr'
config.herdr_bin = nil
local from_env = herdr.herdr_bin()
config.herdr_bin = '/config/herdr'
local from_config = herdr.herdr_bin()
config.herdr_bin = nil
vim.env.HERDR_BIN_PATH = nil
local fallback = herdr.herdr_bin()
vim.env.HERDR_ENV = nil
vim.env.HERDR_PANE_ID = nil
local absent = { herdr.is_in_session(), herdr.current_pane_id() or '<nil>' }
vim.env.HERDR_ENV = '1'
vim.env.HERDR_PANE_ID = ''
local empty = { herdr.is_in_session(), herdr.current_pane_id() or '<nil>' }
vim.env.HERDR_PANE_ID = 'pane-7'
local present = { herdr.is_in_session(), herdr.current_pane_id() }
return {
binaries = { from_env, from_config, fallback },
absent = absent,
empty = empty,
present = present,
}
end)
expect.equality(result, {
binaries = { '/env/herdr', '/config/herdr', 'herdr' },
absent = { false, '<nil>' },
empty = { false, '<nil>' },
present = { true, 'pane-7' },
})
end
T['parses edge responses and rejects invalid command output'] = function()
local result = child.lua_func(function()
vim.env.HERDR_ENV = '1'
vim.env.HERDR_PANE_ID = 'pane-7'
local config = require('herdr-splits.config')
config.herdr_bin = '/fake/herdr'
local queue = {
{ stdout = vim.json.encode({ result = { edges = { left = true } } }), stderr = '', code = 0 },
{ stdout = vim.json.encode({ result = { edges = { left = false } } }), stderr = '', code = 0 },
{ stdout = '{bad', stderr = '', code = 0 },
{ stdout = '', stderr = '', code = 0 },
{ stdout = vim.json.encode({ result = {} }), stderr = '', code = 0 },
{ stdout = '{}', stderr = 'failed', code = 2 },
{ stdout = vim.json.encode({ result = { edges = {} } }), stderr = '', code = 0 },
{ stdout = '{"result":{"edges":{"left":null}}}', stderr = '', code = 0 },
{ stdout = vim.json.encode({ result = { edges = { left = 'yes' } } }), stderr = '', code = 0 },
}
local calls = {}
vim.system = function(argv, opts)
calls[#calls + 1] = { argv = vim.deepcopy(argv), opts = vim.deepcopy(opts) }
local item = table.remove(queue, 1)
return { wait = function() return item end }
end
package.loaded['herdr-splits.herdr'] = nil
local herdr = require('herdr-splits.herdr')
local values = {}
for i = 1, 9 do
local value = herdr.current_pane_at_edge('left')
values[i] = value == nil and '<nil>' or value
end
return { values = values, calls = calls }
end)
expect.equality(result.values, {
true, false, '<nil>', '<nil>', '<nil>', '<nil>', '<nil>', '<nil>', '<nil>',
})
expect.equality(#result.calls, 9)
for _, call in ipairs(result.calls) do
expect.equality(call, {
argv = { '/fake/herdr', 'pane', 'edges', '--current' },
opts = { text = true },
})
end
end
T['parses zoom responses and rejects invalid command output'] = function()
local result = child.lua_func(function()
vim.env.HERDR_ENV = '1'
vim.env.HERDR_PANE_ID = 'pane-7'
local queue = {
{ stdout = vim.json.encode({ result = { layout = { zoomed = true } } }), stderr = '', code = 0 },
{ stdout = vim.json.encode({ result = { layout = { zoomed = false } } }), stderr = '', code = 0 },
{ stdout = '{bad', stderr = '', code = 0 },
{ stdout = vim.json.encode({ result = {} }), stderr = '', code = 0 },
{ stdout = '{}', stderr = '', code = 1 },
{ stdout = vim.json.encode({ result = { layout = {} } }), stderr = '', code = 0 },
{ stdout = '{"result":{"layout":{"zoomed":null}}}', stderr = '', code = 0 },
{ stdout = vim.json.encode({ result = { layout = { zoomed = 1 } } }), stderr = '', code = 0 },
}
local calls = {}
vim.system = function(argv, opts)
calls[#calls + 1] = { argv = vim.deepcopy(argv), opts = vim.deepcopy(opts) }
local item = table.remove(queue, 1)
return { wait = function() return item end }
end
package.loaded['herdr-splits.herdr'] = nil
local herdr = require('herdr-splits.herdr')
local values = {}
for i = 1, 8 do
local value = herdr.current_pane_is_zoomed()
values[i] = value == nil and '<nil>' or value
end
return { values = values, calls = calls }
end)
expect.equality(result.values, {
true, false, '<nil>', '<nil>', '<nil>', '<nil>', '<nil>', '<nil>',
})
expect.equality(#result.calls, 8)
for _, call in ipairs(result.calls) do
expect.equality(call.argv, { 'herdr', 'pane', 'layout', '--current' })
expect.equality(call.opts, { text = true })
end
end
T['rejects JSON null for pane queries'] = function()
local result = child.lua_func(function()
vim.env.HERDR_ENV = '1'
vim.env.HERDR_PANE_ID = 'pane-7'
local queue = {
'null',
'{"result":null}',
'{"result":{"edges":null}}',
'null',
'{"result":null}',
'{"result":{"layout":null}}',
}
vim.system = function()
local stdout = table.remove(queue, 1)
return {
wait = function()
return { stdout = stdout, stderr = '', code = 0 }
end,
}
end
package.loaded['herdr-splits.herdr'] = nil
local herdr = require('herdr-splits.herdr')
local values = {}
for _ = 1, 3 do
local ok, value = pcall(herdr.current_pane_at_edge, 'left')
values[#values + 1] = { ok, value == nil and '<nil>' or value }
end
for _ = 1, 3 do
local ok, value = pcall(herdr.current_pane_is_zoomed)
values[#values + 1] = { ok, value == nil and '<nil>' or value }
end
return values
end)
expect.equality(result, {
{ true, '<nil>' },
{ true, '<nil>' },
{ true, '<nil>' },
{ true, '<nil>' },
{ true, '<nil>' },
{ true, '<nil>' },
})
end
T['focus and unzoom use exact commands and report exit status'] = function()
local result = child.lua_func(function()
vim.env.HERDR_ENV = '1'
vim.env.HERDR_PANE_ID = 'pane-7'
local queue = {
{ stdout = '', stderr = '', code = 0 },
{ stdout = '', stderr = 'no neighbor', code = 1 },
{ stdout = '', stderr = '', code = 0 },
{ stdout = '', stderr = 'failed', code = 1 },
}
local calls = {}
vim.system = function(argv, opts)
calls[#calls + 1] = { argv = vim.deepcopy(argv), opts = vim.deepcopy(opts) }
local item = table.remove(queue, 1)
return { wait = function() return item end }
end
package.loaded['herdr-splits.herdr'] = nil
local herdr = require('herdr-splits.herdr')
return {
values = {
herdr.focus_pane('right'),
herdr.focus_pane('left'),
herdr.unzoom(),
herdr.unzoom(),
},
calls = calls,
}
end)
expect.equality(result.values, { true, false, true, false })
expect.equality(result.calls, {
{ argv = { 'herdr', 'pane', 'focus', '--direction', 'right', '--current' }, opts = { text = true } },
{ argv = { 'herdr', 'pane', 'focus', '--direction', 'left', '--current' }, opts = { text = true } },
{ argv = { 'herdr', 'pane', 'zoom', '--off', '--current' }, opts = { text = true } },
{ argv = { 'herdr', 'pane', 'zoom', '--off', '--current' }, opts = { text = true } },
})
end
T['resize formats ratios and reports exit status'] = function()
local result = child.lua_func(function()
vim.env.HERDR_ENV = '1'
vim.env.HERDR_PANE_ID = 'pane-7'
local queue = {
{ stdout = '', stderr = '', code = 0 },
{ stdout = '', stderr = 'failed', code = 1 },
}
local calls = {}
vim.system = function(argv, opts)
calls[#calls + 1] = { argv = vim.deepcopy(argv), opts = vim.deepcopy(opts) }
local item = table.remove(queue, 1)
return { wait = function() return item end }
end
package.loaded['herdr-splits.herdr'] = nil
local herdr = require('herdr-splits.herdr')
return {
values = { herdr.resize_pane('down', 0.03), herdr.resize_pane('left', 1 / 3) },
calls = calls,
}
end)
expect.equality(result.values, { true, false })
expect.equality(result.calls, {
{
argv = { 'herdr', 'pane', 'resize', '--direction', 'down', '--amount', '0.0300', '--current' },
opts = { text = true },
},
{
argv = { 'herdr', 'pane', 'resize', '--direction', 'left', '--amount', '0.3333', '--current' },
opts = { text = true },
},
})
end
return T

View File

@@ -1,520 +0,0 @@
local T
local expect = MiniTest.expect
local child = MiniTest.new_child_neovim()
local source = debug.getinfo(1, 'S').source
local root = vim.fn.fnamemodify(source:sub(2), ':p:h:h')
T = MiniTest.new_set({
hooks = {
pre_case = function()
child.restart({ '--noplugin', '-u', 'NONE', '-i', 'NONE' })
child.cmd('set runtimepath^=' .. vim.fn.fnameescape(root))
end,
post_case = function()
child.stop()
end,
},
})
T['moves through native horizontal and vertical splits without delegation'] = function()
local result = child.lua_func(function()
local calls = {}
package.loaded['herdr-splits.herdr'] = {
focus_pane = function(direction) calls[#calls + 1] = direction end,
is_in_session = function() return false end,
}
package.loaded['herdr-splits.nav'] = nil
local nav = require('herdr-splits.nav')
vim.o.splitright = true
vim.cmd('vsplit')
local horizontal = vim.api.nvim_tabpage_list_wins(0)
table.sort(horizontal, function(a, b)
return vim.api.nvim_win_get_position(a)[2] < vim.api.nvim_win_get_position(b)[2]
end)
vim.api.nvim_set_current_win(horizontal[1])
nav.move_cursor('right')
local moved_right = vim.api.nvim_get_current_win() == horizontal[2]
vim.cmd('only')
vim.o.splitbelow = true
vim.cmd('split')
local vertical = vim.api.nvim_tabpage_list_wins(0)
table.sort(vertical, function(a, b)
return vim.api.nvim_win_get_position(a)[1] < vim.api.nvim_win_get_position(b)[1]
end)
vim.api.nvim_set_current_win(vertical[1])
nav.move_cursor('down')
local moved_down = vim.api.nvim_get_current_win() == vertical[2]
return { moved_right = moved_right, moved_down = moved_down, calls = calls }
end)
expect.equality(result, { moved_right = true, moved_down = true, calls = {} })
end
T['applies typed navigation counts without wrapping or delegation'] = function()
child.lua_func(function()
_G.nav_delegations = {}
package.loaded['herdr-splits.herdr'] = {
is_in_session = function() return false end,
focus_pane = function(direction)
_G.nav_delegations[#_G.nav_delegations + 1] = direction
return true
end,
}
package.loaded['herdr-splits.nav'] = nil
local nav = require('herdr-splits.nav')
vim.o.splitright = true
for _ = 1, 3 do
vim.cmd('vsplit')
end
_G.nav_count_wins = vim.api.nvim_tabpage_list_wins(0)
table.sort(_G.nav_count_wins, function(a, b)
return vim.api.nvim_win_get_position(a)[2] < vim.api.nvim_win_get_position(b)[2]
end)
vim.api.nvim_set_current_win(_G.nav_count_wins[1])
vim.keymap.set('n', 'x', function()
nav.move_cursor('right', { at_edge = 'stop' })
end)
end)
child.type_keys('2x')
local after_two = child.lua_get('vim.api.nvim_get_current_win() == _G.nav_count_wins[3]')
child.type_keys('5x')
local exhausted_at_last = child.lua_get('vim.api.nvim_get_current_win() == _G.nav_count_wins[4]')
child.type_keys('5x')
local stayed_at_last = child.lua_get('vim.api.nvim_get_current_win() == _G.nav_count_wins[4]')
local delegations = child.lua_get('_G.nav_delegations')
expect.equality({
after_two = after_two,
exhausted_at_last = exhausted_at_last,
stayed_at_last = stayed_at_last,
delegations = delegations,
}, {
after_two = true,
exhausted_at_last = true,
stayed_at_last = true,
delegations = {},
})
end
T['delegates normal floats but not embedded floats'] = function()
local result = child.lua_func(function()
local calls = {}
package.loaded['herdr-splits.herdr'] = {
focus_pane = function(direction)
calls[#calls + 1] = direction
return true
end,
is_in_session = function() return false end,
}
package.loaded['herdr-splits.nav'] = nil
local nav = require('herdr-splits.nav')
local normal = vim.api.nvim_open_win(vim.api.nvim_create_buf(false, true), true, {
relative = 'editor', row = 1, col = 1, width = 12, height = 3, zindex = 50,
})
nav.move_cursor('right')
local normal_stayed = vim.api.nvim_get_current_win() == normal
vim.api.nvim_open_win(vim.api.nvim_create_buf(false, true), true, {
relative = 'editor', row = 2, col = 2, width = 12, height = 3, zindex = 49,
})
nav.move_cursor('left')
return { calls = calls, normal_stayed = normal_stayed }
end)
expect.equality(result, {
calls = { 'right' },
normal_stayed = true,
})
end
T['applies no-session stop, wrap, and callback edge behavior'] = function()
local result = child.lua_func(function()
package.loaded['herdr-splits.herdr'] = {
is_in_session = function() return false end,
}
package.loaded['herdr-splits.nav'] = nil
local nav = require('herdr-splits.nav')
local function horizontal_pair()
vim.cmd('only')
vim.o.splitright = true
vim.cmd('vsplit')
local wins = vim.api.nvim_tabpage_list_wins(0)
table.sort(wins, function(a, b)
return vim.api.nvim_win_get_position(a)[2] < vim.api.nvim_win_get_position(b)[2]
end)
vim.api.nvim_set_current_win(wins[1])
return wins
end
local stop_wins = horizontal_pair()
nav.move_cursor('left', { at_edge = 'stop' })
local stopped = vim.api.nvim_get_current_win() == stop_wins[1]
local wrap_wins = horizontal_pair()
nav.move_cursor('left', { at_edge = 'wrap' })
local wrapped = vim.api.nvim_get_current_win() == wrap_wins[2]
local callback_wins = horizontal_pair()
local callback = {}
nav.move_cursor('left', {
at_edge = function(ctx)
callback.direction = ctx.direction
callback.is_sidebar = ctx.is_sidebar
callback.has_split = type(ctx.split) == 'function'
callback.has_wrap = type(ctx.wrap) == 'function'
ctx.wrap()
end,
})
callback.wrapped = vim.api.nvim_get_current_win() == callback_wins[2]
return {
stopped = stopped,
wrapped = wrapped,
callback = callback,
}
end)
expect.equality(result, {
stopped = true,
wrapped = true,
callback = {
direction = 'left',
is_sidebar = false,
has_split = true,
has_wrap = true,
wrapped = true,
},
})
end
T['creates local edge splits and preserves split preferences'] = function()
local result = child.lua_func(function()
package.loaded['herdr-splits.herdr'] = {
is_in_session = function() return false end,
}
package.loaded['herdr-splits.nav'] = nil
local nav = require('herdr-splits.nav')
local cases = {}
local function exercise(direction, preference)
vim.cmd('only')
vim.o.splitright = preference
vim.o.splitbelow = preference
local before = #vim.api.nvim_tabpage_list_wins(0)
local original = vim.api.nvim_get_current_win()
nav.move_cursor(direction, { at_edge = 'split' })
local current = vim.api.nvim_get_current_win()
local original_position = vim.api.nvim_win_get_position(original)
local current_position = vim.api.nvim_win_get_position(current)
local placement_matches_preference
if direction == 'left' or direction == 'right' then
if preference then
placement_matches_preference = current_position[2] > original_position[2]
else
placement_matches_preference = current_position[2] < original_position[2]
end
elseif preference then
placement_matches_preference = current_position[1] > original_position[1]
else
placement_matches_preference = current_position[1] < original_position[1]
end
cases[#cases + 1] = {
direction = direction,
preference = preference,
created_one = #vim.api.nvim_tabpage_list_wins(0) == before + 1,
focused_new = current ~= original,
placement_matches_preference = placement_matches_preference,
splitright_restored = vim.o.splitright == preference,
splitbelow_restored = vim.o.splitbelow == preference,
}
end
for _, direction in ipairs({ 'left', 'right', 'up', 'down' }) do
exercise(direction, false)
exercise(direction, true)
end
return cases
end)
local expected = {}
for _, direction in ipairs({ 'left', 'right', 'up', 'down' }) do
for _, preference in ipairs({ false, true }) do
expected[#expected + 1] = {
direction = direction,
preference = preference,
created_one = true,
focused_new = true,
placement_matches_preference = true,
splitright_restored = true,
splitbelow_restored = true,
}
end
end
expect.equality(result, expected)
end
T['uses Herdr neighbors and falls back locally on focus or edge failures'] = function()
local result = child.lua_func(function()
local edge_queue = { false, false, '<nil>' }
local focus_queue = { true, false }
local calls = {}
package.loaded['herdr-splits.herdr'] = {
is_in_session = function() return true end,
unzoom_enabled = function() return false end,
current_pane_at_edge = function(direction)
calls[#calls + 1] = 'edge:' .. direction
local value = table.remove(edge_queue, 1)
if value == '<nil>' then return nil end
return value
end,
focus_pane = function(direction)
calls[#calls + 1] = 'focus:' .. direction
return table.remove(focus_queue, 1)
end,
}
package.loaded['herdr-splits.nav'] = nil
local nav = require('herdr-splits.nav')
local function at_left()
vim.cmd('only')
vim.o.splitright = true
vim.cmd('vsplit')
local wins = vim.api.nvim_tabpage_list_wins(0)
table.sort(wins, function(a, b)
return vim.api.nvim_win_get_position(a)[2] < vim.api.nvim_win_get_position(b)[2]
end)
vim.api.nvim_set_current_win(wins[1])
return wins
end
local success_wins = at_left()
nav.move_cursor('left')
local focus_success_stays = vim.api.nvim_get_current_win() == success_wins[1]
local failure_wins = at_left()
nav.move_cursor('left')
local focus_failure_wraps = vim.api.nvim_get_current_win() == failure_wins[2]
local unknown_wins = at_left()
nav.move_cursor('left')
local unknown_wraps = vim.api.nvim_get_current_win() == unknown_wins[2]
return {
focus_success_stays = focus_success_stays,
focus_failure_wraps = focus_failure_wraps,
unknown_wraps = unknown_wraps,
calls = calls,
}
end)
expect.equality(result, {
focus_success_stays = true,
focus_failure_wraps = true,
unknown_wraps = true,
calls = {
'edge:left', 'focus:left',
'edge:left', 'focus:left',
'edge:left',
},
})
end
T['unzooms and retries native movement before querying Herdr edges'] = function()
local result = child.lua_func(function()
local calls = {}
local start_win = vim.api.nvim_get_current_win()
package.loaded['herdr-splits.herdr'] = {
is_in_session = function() return true end,
unzoom_enabled = function() return true end,
current_pane_is_zoomed = function()
calls[#calls + 1] = 'zoomed'
return true
end,
unzoom = function()
calls[#calls + 1] = 'unzoom'
local old_splitright = vim.o.splitright
vim.o.splitright = true
vim.cmd('vsplit')
vim.api.nvim_set_current_win(start_win)
vim.o.splitright = old_splitright
return true
end,
current_pane_at_edge = function()
calls[#calls + 1] = 'edge'
return false
end,
focus_pane = function()
calls[#calls + 1] = 'focus'
return true
end,
}
package.loaded['herdr-splits.nav'] = nil
require('herdr-splits.nav').move_cursor('right')
return {
moved = vim.api.nvim_get_current_win() ~= start_win,
windows = #vim.api.nvim_tabpage_list_wins(0),
calls = calls,
}
end)
expect.equality(result, { moved = true, windows = 2, calls = { 'zoomed', 'unzoom' } })
end
T['wraps through the reverse Herdr neighbor unless nav_at_edge stops it'] = function()
local result = child.lua_func(function()
local mode = 'wrap'
local calls = {}
package.loaded['herdr-splits.herdr'] = {
is_in_session = function() return true end,
unzoom_enabled = function() return false end,
current_pane_at_edge = function(direction)
calls[#calls + 1] = 'edge:' .. direction
return direction == 'left'
end,
nav_at_edge = function() return mode end,
focus_pane = function(direction)
calls[#calls + 1] = 'focus:' .. direction
return true
end,
}
package.loaded['herdr-splits.nav'] = nil
local nav = require('herdr-splits.nav')
local function at_left()
vim.cmd('only')
vim.o.splitright = true
vim.cmd('vsplit')
local wins = vim.api.nvim_tabpage_list_wins(0)
table.sort(wins, function(a, b)
return vim.api.nvim_win_get_position(a)[2] < vim.api.nvim_win_get_position(b)[2]
end)
vim.api.nvim_set_current_win(wins[1])
return wins
end
local herdr_wrap_wins = at_left()
nav.move_cursor('left')
local herdr_wrap_stays = vim.api.nvim_get_current_win() == herdr_wrap_wins[1]
mode = 'stop'
local local_wrap_wins = at_left()
nav.move_cursor('left')
local local_wraps = vim.api.nvim_get_current_win() == local_wrap_wins[2]
return { herdr_wrap_stays = herdr_wrap_stays, local_wraps = local_wraps, calls = calls }
end)
expect.equality(result, {
herdr_wrap_stays = true,
local_wraps = true,
calls = { 'edge:left', 'edge:right', 'focus:right', 'edge:left' },
})
end
T['command-line window delegates to Herdr at the screen edge and never wincmds'] = function()
local result = child.lua_func(function()
local calls = {}
local edge = false
local nav_mode = 'wrap'
package.loaded['herdr-splits.win'] = {
is_command_line_window = function() return true end,
is_floating = function() return false end,
is_embedded_floating_window = function() return false end,
is_ignored_or_preview = function() return false end,
dir_keys = { left = 'h', right = 'l', up = 'k', down = 'j' },
dir_keys_reverse = { left = 'l', right = 'h', up = 'j', down = 'k' },
reverse_direction = { left = 'right', right = 'left', up = 'down', down = 'up' },
}
package.loaded['herdr-splits.herdr'] = {
is_in_session = function() return true end,
current_pane_at_edge = function(direction)
calls[#calls + 1] = 'edge:' .. direction
return edge
end,
nav_at_edge = function() return nav_mode end,
focus_pane = function(direction)
calls[#calls + 1] = 'focus:' .. direction
return true
end,
}
package.loaded['herdr-splits.nav'] = nil
local nav = require('herdr-splits.nav')
-- single window: will_wrap is true in every direction, mirroring the
-- full-width/bottom command-line window geometry.
vim.cmd('only')
edge = false
nav.move_cursor('left')
local c1 = vim.deepcopy(calls); calls = {}
edge = true
nav_mode = 'wrap'
nav.move_cursor('right')
local c2 = vim.deepcopy(calls); calls = {}
nav_mode = 'stop'
nav.move_cursor('right')
local c3 = vim.deepcopy(calls)
return { neighbor = c1, wrap_reverse = c2, stop_noop = c3 }
end)
expect.equality(result, {
neighbor = { 'edge:left', 'focus:left' },
wrap_reverse = { 'edge:right', 'focus:left' },
stop_noop = { 'edge:right' },
})
end
T['command-line window no-ops silently when not at a screen edge'] = function()
local result = child.lua_func(function()
package.loaded['herdr-splits.win'] = {
is_command_line_window = function() return true end,
is_floating = function() return false end,
is_embedded_floating_window = function() return false end,
is_ignored_or_preview = function() return false end,
dir_keys = { left = 'h', right = 'l', up = 'k', down = 'j' },
dir_keys_reverse = { left = 'l', right = 'h', up = 'j', down = 'k' },
reverse_direction = { left = 'right', right = 'left', up = 'down', down = 'up' },
}
local focused = {}
package.loaded['herdr-splits.herdr'] = {
is_in_session = function() return true end,
current_pane_at_edge = function() return false end,
nav_at_edge = function() return 'wrap' end,
focus_pane = function(d) focused[#focused + 1] = d; return true end,
}
package.loaded['herdr-splits.nav'] = nil
local nav = require('herdr-splits.nav')
vim.o.splitright = true
vim.cmd('vsplit')
local wins = vim.api.nvim_tabpage_list_wins(0)
table.sort(wins, function(a, b)
return vim.api.nvim_win_get_position(a)[2] < vim.api.nvim_win_get_position(b)[2]
end)
-- left window: 'right' has a Neovim neighbor -> will_wrap == false
vim.api.nvim_set_current_win(wins[1])
nav.move_cursor('right')
return { stayed = vim.api.nvim_get_current_win() == wins[1], focused = focused }
end)
-- Without the guard, wincmd l would move to wins[2]; with it, silent no-op.
expect.equality(result, { stayed = true, focused = {} })
end
return T

Some files were not shown because too many files have changed in this diff Show More