Skip to content

Commit 94e8778

Browse files
authored
Merge pull request #12 from ttak0422/fix/pterm-open-snapshot-timing
fix: use pterm open to eliminate snapshot timing gap in :Pterm command
2 parents e223790 + b42e1cb commit 94e8778

File tree

2 files changed

+88
-124
lines changed

2 files changed

+88
-124
lines changed

lua/pterm/init.lua

Lines changed: 72 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ M.config = {
1111
rows = 24,
1212
-- Socket directory (nil = let daemon decide)
1313
socket_dir = nil,
14-
-- Max wait time for daemon socket creation after `pterm new`
15-
attach_wait_ms = 3000,
16-
-- Poll interval while waiting for socket
17-
attach_poll_ms = 50,
1814
}
1915

2016
--- Active connections: session_name -> { buf, job_id, session_name }
@@ -73,13 +69,6 @@ local function socket_path(session_name)
7369
return socket_dir() .. "/" .. session_name .. "/socket"
7470
end
7571

76-
local function wait_for_socket(session_name, timeout_ms, poll_ms)
77-
local sock = socket_path(session_name)
78-
return vim.wait(timeout_ms, function()
79-
return vim.uv.fs_stat(sock) ~= nil
80-
end, poll_ms)
81-
end
82-
8372
--- Recursively scan the socket directory for active sessions (pure Lua).
8473
--- Mirrors the Rust `find_sessions()` logic without spawning a subprocess,
8574
--- which avoids instability when called during command-line completion.
@@ -133,101 +122,9 @@ function M.kill(session_name)
133122
vim.notify("Killed session: " .. session_name, vim.log.levels.INFO)
134123
end
135124

136-
--- Open or attach to a session.
137-
--- If session exists, attach. Otherwise create new.
138-
function M.open(session_name, args)
139-
args = args or {}
140-
141-
-- Default session name
142-
if not session_name or session_name == "" then
143-
session_name = "main"
144-
end
145-
146-
-- Already connected?
147-
if M.connections[session_name] then
148-
-- Switch to existing buffer
149-
local conn = M.connections[session_name]
150-
if vim.api.nvim_buf_is_valid(conn.buf) then
151-
vim.api.nvim_set_current_buf(conn.buf)
152-
vim.cmd("startinsert")
153-
return
154-
else
155-
-- Buffer was closed, clean up
156-
M.connections[session_name] = nil
157-
end
158-
end
159-
160-
local sock = socket_path(session_name)
161-
local bin = find_binary()
162-
163-
-- Check if session exists
164-
if vim.uv.fs_stat(sock) == nil then
165-
-- Create new session
166-
local cmd_parts = {}
167-
-- Skip session name from args, collect rest as command
168-
local found_name = false
169-
for _, arg in ipairs(args) do
170-
if not found_name and arg == session_name then
171-
found_name = true
172-
elseif found_name then
173-
table.insert(cmd_parts, arg)
174-
end
175-
end
176-
177-
-- Do NOT pass --cols/--rows to `pterm new` here. The bridge
178-
-- (started by M.attach) will send an accurate RESIZE based on
179-
-- the actual PTY size after terminal-friendly window options
180-
-- (number=false, signcolumn=no, …) have been applied. Passing
181-
-- dimensions at this point would use the pre-option window size,
182-
-- which may differ and trigger an unnecessary resize + snapshot
183-
-- race on the daemon side.
184-
local create_cmd = {
185-
bin,
186-
"new",
187-
session_name,
188-
}
189-
190-
if #cmd_parts > 0 then
191-
table.insert(create_cmd, "--")
192-
for _, part in ipairs(cmd_parts) do
193-
table.insert(create_cmd, part)
194-
end
195-
end
196-
197-
vim.fn.system(create_cmd)
198-
if vim.v.shell_error ~= 0 then
199-
vim.notify(
200-
"Failed to create session '" .. session_name .. "' (pterm new exited " .. vim.v.shell_error .. ")",
201-
vim.log.levels.ERROR
202-
)
203-
return
204-
end
205-
206-
-- Wait for daemon socket before attach to avoid new->attach race.
207-
local ok = wait_for_socket(session_name, M.config.attach_wait_ms, M.config.attach_poll_ms)
208-
if not ok then
209-
vim.notify(
210-
"Session '" .. session_name .. "' was created but socket did not appear in time",
211-
vim.log.levels.ERROR
212-
)
213-
return
214-
end
215-
end
216-
217-
-- Attach to session
218-
M.attach(session_name)
219-
end
220-
221-
--- Attach to an existing session.
222-
function M.attach(session_name)
223-
local sock = socket_path(session_name)
224-
local bin = find_binary()
225-
226-
if vim.uv.fs_stat(sock) == nil then
227-
vim.notify("Session '" .. session_name .. "' not found", vim.log.levels.ERROR)
228-
return
229-
end
230-
125+
--- Internal: create a terminal buffer and start a pterm bridge process.
126+
--- `cmd` is the full argv for jobstart (e.g. {"pterm","open","main"}).
127+
local function start_terminal(session_name, cmd)
231128
-- Clean up any stale buffer with the same name from a previous connection
232129
local buf_name = "pterm://" .. session_name
233130
local existing = vim.fn.bufnr(buf_name)
@@ -252,11 +149,9 @@ function M.attach(session_name)
252149
-- Let the bridge read the actual PTY size via TIOCGWINSZ instead of
253150
-- passing --cols/--rows from Lua. jobstart({term=true}) creates a PTY
254151
-- sized to the current window, and the bridge's get_winsize(stdout)
255-
-- will return exactly that size. This avoids any mismatch between the
256-
-- Lua-reported dimensions and the real PTY geometry.
257-
local job_id = vim.fn.jobstart({
258-
bin, "attach", session_name,
259-
}, {
152+
-- will return exactly that size.
153+
local job_id
154+
job_id = vim.fn.jobstart(cmd, {
260155
term = true,
261156
on_exit = function(_, exit_code, _)
262157
vim.schedule(function()
@@ -270,7 +165,7 @@ function M.attach(session_name)
270165
})
271166

272167
if job_id <= 0 then
273-
vim.notify("Failed to start pterm attach for '" .. session_name .. "'", vim.log.levels.ERROR)
168+
vim.notify("Failed to start pterm for '" .. session_name .. "'", vim.log.levels.ERROR)
274169
if vim.api.nvim_buf_is_valid(buf) then
275170
pcall(vim.api.nvim_buf_delete, buf, { force = true })
276171
end
@@ -344,6 +239,71 @@ function M.attach(session_name)
344239
vim.cmd("startinsert")
345240
end
346241

242+
--- Open or attach to a session.
243+
--- If session exists, attach. Otherwise create new.
244+
--- Uses `pterm open` which handles both creation and attachment in a single
245+
--- process, eliminating the timing gap between daemon creation and bridge
246+
--- connection that caused wrong-size snapshot delivery.
247+
function M.open(session_name, args)
248+
args = args or {}
249+
250+
-- Default session name
251+
if not session_name or session_name == "" then
252+
session_name = "main"
253+
end
254+
255+
-- Already connected?
256+
if M.connections[session_name] then
257+
-- Switch to existing buffer
258+
local conn = M.connections[session_name]
259+
if vim.api.nvim_buf_is_valid(conn.buf) then
260+
vim.api.nvim_set_current_buf(conn.buf)
261+
vim.cmd("startinsert")
262+
return
263+
else
264+
-- Buffer was closed, clean up
265+
M.connections[session_name] = nil
266+
end
267+
end
268+
269+
local bin = find_binary()
270+
271+
-- Build `pterm open` command with optional child command arguments.
272+
local cmd = { bin, "open", session_name }
273+
274+
local cmd_parts = {}
275+
local found_name = false
276+
for _, arg in ipairs(args) do
277+
if not found_name and arg == session_name then
278+
found_name = true
279+
elseif found_name then
280+
table.insert(cmd_parts, arg)
281+
end
282+
end
283+
284+
if #cmd_parts > 0 then
285+
table.insert(cmd, "--")
286+
for _, part in ipairs(cmd_parts) do
287+
table.insert(cmd, part)
288+
end
289+
end
290+
291+
start_terminal(session_name, cmd)
292+
end
293+
294+
--- Attach to an existing session.
295+
function M.attach(session_name)
296+
local sock = socket_path(session_name)
297+
298+
if vim.uv.fs_stat(sock) == nil then
299+
vim.notify("Session '" .. session_name .. "' not found", vim.log.levels.ERROR)
300+
return
301+
end
302+
303+
local bin = find_binary()
304+
start_terminal(session_name, { bin, "attach", session_name })
305+
end
306+
347307
--- Detach from a session (does not kill the daemon).
348308
function M.detach(session_name)
349309
local conn = M.connections[session_name]

src/main.rs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Environment:
6262
);
6363
}
6464

65-
fn cmd_new(args: &[String]) -> io::Result<()> {
65+
fn cmd_new(args: &[String], quiet: bool) -> io::Result<()> {
6666
let mut session_name = String::new();
6767
let mut cols: u16 = 80;
6868
let mut rows: u16 = 24;
@@ -131,15 +131,19 @@ fn cmd_new(args: &[String]) -> io::Result<()> {
131131
// Daemonize: fork into background
132132
match unsafe { nix::unistd::fork() } {
133133
Ok(nix::unistd::ForkResult::Parent { child }) => {
134-
// Parent: print info and exit
135-
println!(
136-
"{}",
137-
serde_json::json!({
138-
"session": session_name,
139-
"pid": child.as_raw(),
140-
"socket": sock_path.to_string_lossy(),
141-
})
142-
);
134+
// Parent: print info and return.
135+
// Suppress output when called from cmd_open to avoid JSON
136+
// leaking into the Neovim terminal buffer.
137+
if !quiet {
138+
println!(
139+
"{}",
140+
serde_json::json!({
141+
"session": session_name,
142+
"pid": child.as_raw(),
143+
"socket": sock_path.to_string_lossy(),
144+
})
145+
);
146+
}
143147
return Ok(());
144148
}
145149
Ok(nix::unistd::ForkResult::Child) => {
@@ -371,7 +375,7 @@ fn cmd_open(args: &[String]) -> io::Result<()> {
371375

372376
let sock = session_socket_path(name);
373377
if !sock.exists() {
374-
cmd_new(args)?;
378+
cmd_new(args, true)?;
375379
let ok = wait_for_socket(
376380
&sock,
377381
Duration::from_millis(3000),
@@ -428,7 +432,7 @@ fn main() {
428432
}
429433

430434
let result = match args[1].as_str() {
431-
"new" => cmd_new(&args[2..]),
435+
"new" => cmd_new(&args[2..], false),
432436
"attach" => cmd_attach(&args[2..]),
433437
"open" => cmd_open(&args[2..]),
434438
"list" | "ls" => cmd_list(&args[2..]),

0 commit comments

Comments
 (0)