1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
|
---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
|