mirror of
https://github.com/neovim/neovim.git
synced 2026-03-31 04:42:03 +00:00
feat(terminal): detect suspended PTY process (#37845)
Problem: Terminal doesn't detect if the PTY process is suspended or
offer a convenient way for the user to resume the process.
Solution: Detect suspended PTY process on SIGCHLD and show virtual text
"[Process suspended]" at the bottom-left. Resume the process
when the user presses a key.
This commit is contained in:
@@ -382,6 +382,8 @@ TERMINAL
|
||||
• |nvim_open_term()| can be called with a non-empty buffer. The buffer
|
||||
contents are piped to the PTY and displayed as terminal output.
|
||||
• CSI 3 J (the sequence to clear terminal scrollback) is now supported.
|
||||
• A suspended PTY process is now indicated by "[Process suspended]" at the
|
||||
bottom-left of the buffer and can be resumed by pressing a key.
|
||||
|
||||
TREESITTER
|
||||
|
||||
|
||||
@@ -1144,6 +1144,7 @@ Integer nvim_open_term(Buffer buffer, Dict(open_term) *opts, Error *err)
|
||||
.height = (uint16_t)curwin->w_view_height,
|
||||
.write_cb = term_write,
|
||||
.resize_cb = term_resize,
|
||||
.resume_cb = term_resume,
|
||||
.close_cb = term_close,
|
||||
.force_crlf = GET_BOOL_OR_TRUE(opts, open_term, force_crlf),
|
||||
};
|
||||
@@ -1194,6 +1195,10 @@ static void term_resize(uint16_t width, uint16_t height, void *data)
|
||||
// TODO(bfredl): Lua callback
|
||||
}
|
||||
|
||||
static void term_resume(void *data)
|
||||
{
|
||||
}
|
||||
|
||||
static void term_close(void *data)
|
||||
{
|
||||
Channel *chan = data;
|
||||
|
||||
@@ -393,6 +393,7 @@ Channel *channel_job_start(char **argv, const char *exepath, CallbackReader on_s
|
||||
proc->argv = argv;
|
||||
proc->exepath = exepath;
|
||||
proc->cb = channel_proc_exit_cb;
|
||||
proc->state_cb = channel_proc_state_cb;
|
||||
proc->events = chan->events;
|
||||
proc->detach = detach;
|
||||
proc->cwd = cwd;
|
||||
@@ -762,6 +763,14 @@ static void channel_proc_exit_cb(Proc *proc, int status, void *data)
|
||||
channel_decref(chan);
|
||||
}
|
||||
|
||||
static void channel_proc_state_cb(Proc *proc, bool suspended, void *data)
|
||||
{
|
||||
Channel *chan = data;
|
||||
if (chan->term) {
|
||||
terminal_set_state(chan->term, suspended);
|
||||
}
|
||||
}
|
||||
|
||||
static void channel_callback_call(Channel *chan, CallbackReader *reader)
|
||||
{
|
||||
Callback *cb;
|
||||
@@ -812,6 +821,7 @@ void channel_terminal_alloc(buf_T *buf, Channel *chan)
|
||||
.height = chan->stream.pty.height,
|
||||
.write_cb = term_write,
|
||||
.resize_cb = term_resize,
|
||||
.resume_cb = term_resume,
|
||||
.close_cb = term_close,
|
||||
.force_crlf = false,
|
||||
};
|
||||
@@ -839,6 +849,14 @@ static void term_resize(uint16_t width, uint16_t height, void *data)
|
||||
pty_proc_resize(&chan->stream.pty, width, height);
|
||||
}
|
||||
|
||||
static void term_resume(void *data)
|
||||
{
|
||||
#ifdef UNIX
|
||||
Channel *chan = data;
|
||||
pty_proc_resume(&chan->stream.pty);
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline void term_delayed_free(void **argv)
|
||||
{
|
||||
Channel *chan = argv[0];
|
||||
|
||||
@@ -1441,6 +1441,17 @@ static void win_update(win_T *wp)
|
||||
|
||||
decor_providers_invoke_win(wp);
|
||||
|
||||
if (buf->terminal && terminal_suspended(buf->terminal)) {
|
||||
static VirtTextChunk chunk = { .text = "[Process suspended]", .hl_id = -1 };
|
||||
static DecorVirtText virt_text = {
|
||||
.priority = DECOR_PRIORITY_BASE,
|
||||
.pos = kVPosWinCol,
|
||||
.data.virt_text = { .items = &chunk, .size = 1 },
|
||||
};
|
||||
decor_range_add_virt(&decor_state, buf->b_ml.ml_line_count - 1, 0,
|
||||
buf->b_ml.ml_line_count - 1, 0, &virt_text, false);
|
||||
}
|
||||
|
||||
FOR_ALL_WINDOWS_IN_TAB(win, curtab) {
|
||||
if (win->w_buffer == wp->w_buffer && win_redraw_signcols(win)) {
|
||||
changed_line_abv_curs_win(win);
|
||||
|
||||
@@ -152,6 +152,7 @@ typedef enum {
|
||||
/// OS process
|
||||
typedef struct proc Proc;
|
||||
typedef void (*proc_exit_cb)(Proc *proc, int status, void *data);
|
||||
typedef void (*proc_state_cb)(Proc *proc, bool suspended, void *data);
|
||||
typedef void (*internal_proc_cb)(Proc *proc);
|
||||
|
||||
struct proc {
|
||||
@@ -169,6 +170,7 @@ struct proc {
|
||||
RStream out, err;
|
||||
/// Exit handler. If set, user must call proc_free().
|
||||
proc_exit_cb cb;
|
||||
proc_state_cb state_cb;
|
||||
internal_proc_cb internal_exit_cb, internal_close_cb;
|
||||
bool closed, detach, overlapped, fwd_err;
|
||||
MultiQueue *events;
|
||||
|
||||
@@ -25,6 +25,7 @@ static inline Proc proc_init(Loop *loop, ProcType type, void *data)
|
||||
.out = { .s.closed = false, .s.fd = STDOUT_FILENO },
|
||||
.err = { .s.closed = false, .s.fd = STDERR_FILENO },
|
||||
.cb = NULL,
|
||||
.state_cb = NULL,
|
||||
.closed = false,
|
||||
.internal_close_cb = NULL,
|
||||
.internal_exit_cb = NULL,
|
||||
|
||||
@@ -239,6 +239,11 @@ void pty_proc_resize(PtyProc *ptyproc, uint16_t width, uint16_t height)
|
||||
ioctl(ptyproc->tty_fd, TIOCSWINSZ, &ptyproc->winsize);
|
||||
}
|
||||
|
||||
void pty_proc_resume(PtyProc *ptyproc)
|
||||
{
|
||||
kill(((Proc *)ptyproc)->pid, SIGCONT);
|
||||
}
|
||||
|
||||
void pty_proc_close(PtyProc *ptyproc)
|
||||
FUNC_ATTR_NONNULL_ALL
|
||||
{
|
||||
@@ -397,13 +402,22 @@ static void chld_handler(uv_signal_t *handle, int signum)
|
||||
for (size_t i = 0; i < kv_size(loop->children); i++) {
|
||||
Proc *proc = kv_A(loop->children, i);
|
||||
do {
|
||||
pid = waitpid(proc->pid, &stat, WNOHANG);
|
||||
pid = waitpid(proc->pid, &stat, WNOHANG|WUNTRACED|WCONTINUED);
|
||||
} while (pid < 0 && errno == EINTR);
|
||||
|
||||
if (pid <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (WIFSTOPPED(stat)) {
|
||||
proc->state_cb(proc, true, proc->data);
|
||||
continue;
|
||||
}
|
||||
if (WIFCONTINUED(stat)) {
|
||||
proc->state_cb(proc, false, proc->data);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (WIFEXITED(stat)) {
|
||||
proc->status = WEXITSTATUS(stat);
|
||||
} else if (WIFSIGNALED(stat)) {
|
||||
|
||||
@@ -174,6 +174,8 @@ struct terminal {
|
||||
bool in_altscreen;
|
||||
// program exited
|
||||
bool closed;
|
||||
// program suspended
|
||||
bool suspended;
|
||||
// when true, the terminal's destruction is already enqueued.
|
||||
bool destroy;
|
||||
|
||||
@@ -672,7 +674,6 @@ void terminal_close(Terminal **termpp, int status)
|
||||
// only need to call the close callback to clean up the terminal object.
|
||||
only_destroy = true;
|
||||
} else {
|
||||
term->forward_mouse = false;
|
||||
// flush any pending changes to the buffer
|
||||
if (!exiting) {
|
||||
block_autocmds();
|
||||
@@ -726,7 +727,33 @@ void terminal_close(Terminal **termpp, int status)
|
||||
}
|
||||
}
|
||||
|
||||
static void terminal_state_change_event(void **argv)
|
||||
{
|
||||
handle_T buf_handle = (handle_T)(intptr_t)argv[0];
|
||||
buf_T *buf = handle_get_buffer(buf_handle);
|
||||
if (buf && buf->terminal) {
|
||||
// Don't change the actual terminal content to indicate the suspended state here,
|
||||
// as unlike the process exit case the change needs to be reversed on resume.
|
||||
// Instead, the code in win_update() will add a "[Process suspended]" virtual text
|
||||
// at the botton-left of the buffer.
|
||||
redraw_buf_line_later(buf, buf->b_ml.ml_line_count, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the suspended state of the terminal program.
|
||||
void terminal_set_state(Terminal *term, bool suspended)
|
||||
FUNC_ATTR_NONNULL_ALL
|
||||
{
|
||||
if (term->suspended != suspended) {
|
||||
// Trigger a main loop iteration to redraw the buffer.
|
||||
multiqueue_put(main_loop.events, terminal_state_change_event,
|
||||
(void *)(intptr_t)term->buf_handle);
|
||||
}
|
||||
term->suspended = suspended;
|
||||
}
|
||||
|
||||
void terminal_check_size(Terminal *term)
|
||||
FUNC_ATTR_NONNULL_ALL
|
||||
{
|
||||
if (term->closed) {
|
||||
return;
|
||||
@@ -951,9 +978,14 @@ static void terminal_check_cursor(void)
|
||||
if (topline != curwin->w_topline) {
|
||||
set_topline(curwin, topline);
|
||||
}
|
||||
// Nudge cursor when returning to normal-mode.
|
||||
int off = is_focused(term) ? 0 : (curwin->w_p_rl ? 1 : -1);
|
||||
coladvance(curwin, MAX(0, term->cursor.col + off));
|
||||
if (term->suspended) {
|
||||
// If the terminal process is suspended, keep cursor at the bottom-left corner.
|
||||
curwin->w_cursor = (pos_T){ .lnum = curbuf->b_ml.ml_line_count };
|
||||
} else {
|
||||
// Nudge cursor when returning to normal-mode.
|
||||
int off = is_focused(term) ? 0 : (curwin->w_p_rl ? 1 : -1);
|
||||
coladvance(curwin, MAX(0, term->cursor.col + off));
|
||||
}
|
||||
}
|
||||
|
||||
static bool terminal_check_focus(TerminalState *const s)
|
||||
@@ -1129,6 +1161,13 @@ static int terminal_execute(VimState *state, int key)
|
||||
s->got_bsl = true;
|
||||
break;
|
||||
}
|
||||
if (s->term->suspended) {
|
||||
s->term->opts.resume_cb(s->term->opts.data);
|
||||
// XXX: detecting continued process via waitpid() on SIGCHLD doesn't always work
|
||||
// (e.g. on macOS), so also consider it continued after sending SIGCONT.
|
||||
terminal_set_state(s->term, false);
|
||||
break;
|
||||
}
|
||||
if (s->term->closed) {
|
||||
s->close = true;
|
||||
return 0;
|
||||
@@ -1393,15 +1432,23 @@ void terminal_get_line_attributes(Terminal *term, win_T *wp, int linenr, int *te
|
||||
}
|
||||
|
||||
Buffer terminal_buf(const Terminal *term)
|
||||
FUNC_ATTR_NONNULL_ALL
|
||||
{
|
||||
return term->buf_handle;
|
||||
}
|
||||
|
||||
bool terminal_running(const Terminal *term)
|
||||
FUNC_ATTR_NONNULL_ALL
|
||||
{
|
||||
return !term->closed;
|
||||
}
|
||||
|
||||
bool terminal_suspended(const Terminal *term)
|
||||
FUNC_ATTR_NONNULL_ALL
|
||||
{
|
||||
return term->suspended;
|
||||
}
|
||||
|
||||
void terminal_notify_theme(Terminal *term, bool dark)
|
||||
FUNC_ATTR_NONNULL_ALL
|
||||
{
|
||||
@@ -2063,7 +2110,8 @@ static bool send_mouse_event(Terminal *term, int c)
|
||||
}
|
||||
|
||||
int offset;
|
||||
if (term->forward_mouse && mouse_win->w_buffer->terminal == term && row >= 0
|
||||
if (!term->suspended && !term->closed
|
||||
&& term->forward_mouse && mouse_win->w_buffer->terminal == term && row >= 0
|
||||
&& (grid > 1 || row + mouse_win->w_winbar_height < mouse_win->w_height)
|
||||
&& col >= (offset = win_col_off(mouse_win))
|
||||
&& (grid > 1 || col < mouse_win->w_width)) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
typedef void (*terminal_write_cb)(const char *buffer, size_t size, void *data);
|
||||
typedef void (*terminal_resize_cb)(uint16_t width, uint16_t height, void *data);
|
||||
typedef void (*terminal_resume_cb)(void *data);
|
||||
typedef void (*terminal_close_cb)(void *data);
|
||||
|
||||
typedef struct {
|
||||
@@ -16,6 +17,7 @@ typedef struct {
|
||||
uint16_t width, height;
|
||||
terminal_write_cb write_cb;
|
||||
terminal_resize_cb resize_cb;
|
||||
terminal_resume_cb resume_cb;
|
||||
terminal_close_cb close_cb;
|
||||
bool force_crlf;
|
||||
} TerminalOptions;
|
||||
|
||||
@@ -450,6 +450,105 @@ describe(':terminal buffer', function()
|
||||
]])
|
||||
assert_alive()
|
||||
end)
|
||||
|
||||
describe('handles suspended PTY process', function()
|
||||
if skip(is_os('win'), 'N/A for Windows') then
|
||||
return
|
||||
end
|
||||
|
||||
--- @param external_resume boolean
|
||||
local function test_term_process_suspend_resume(external_resume)
|
||||
command('set mousemodel=extend')
|
||||
local pid = eval('jobpid(&channel)')
|
||||
vim.uv.kill(pid, 'sigstop')
|
||||
screen:expect([[
|
||||
tty ready |
|
||||
|*4
|
||||
^[Process suspended] |
|
||||
{5:-- TERMINAL --} |
|
||||
]])
|
||||
command('set laststatus=0 | botright vsplit')
|
||||
screen:expect([[
|
||||
tty ready │tty ready |
|
||||
│ |*4
|
||||
[Process suspended] │^[Process suspended] |
|
||||
{5:-- TERMINAL --} |
|
||||
]])
|
||||
-- Resize is detected by the process on resume.
|
||||
if external_resume then
|
||||
vim.uv.kill(pid, 'sigcont')
|
||||
else
|
||||
feed('a')
|
||||
end
|
||||
screen:expect([[
|
||||
tty ready │tty ready |
|
||||
rows: 6, cols: 25 │rows: 6, cols: 25 |
|
||||
│^ |
|
||||
│ |*3
|
||||
{5:-- TERMINAL --} |
|
||||
]])
|
||||
tt.enable_mouse()
|
||||
tt.feed_data('mouse enabled\n\n\n\n')
|
||||
screen:expect([[
|
||||
rows: 6, cols: 25 │rows: 6, cols: 25 |
|
||||
mouse enabled │mouse enabled |
|
||||
│ |*3
|
||||
│^ |
|
||||
{5:-- TERMINAL --} |
|
||||
]])
|
||||
api.nvim_input_mouse('right', 'press', '', 0, 0, 25)
|
||||
screen:expect({ any = vim.pesc('"!!^') })
|
||||
api.nvim_input_mouse('right', 'release', '', 0, 0, 25)
|
||||
screen:expect({ any = vim.pesc('#!!^') })
|
||||
vim.uv.kill(pid, 'sigstop')
|
||||
local s1 = [[
|
||||
rows: 6, cols: 25 │rows: 6, cols: 25 |
|
||||
mouse enabled │mouse enabled |
|
||||
│ |*3
|
||||
[Process suspended] │^[Process suspended] |
|
||||
{5:-- TERMINAL --} |
|
||||
]]
|
||||
screen:expect(s1)
|
||||
-- Mouse isn't forwarded when process is suspended.
|
||||
api.nvim_input_mouse('right', 'press', '', 0, 1, 27)
|
||||
api.nvim_input_mouse('right', 'release', '', 0, 1, 27)
|
||||
screen:expect([[
|
||||
rows: 6, cols: 25 │rows: 6, cols: 25 |
|
||||
mo{108:use enabled} │mo^u{108:se enabled} |
|
||||
{108: } │{108: } |*3
|
||||
[Process suspended] │[Process suspended] |
|
||||
{5:-- VISUAL --} |
|
||||
]])
|
||||
feed('<Esc>i')
|
||||
screen:expect(s1)
|
||||
if external_resume then
|
||||
vim.uv.kill(pid, 'sigcont')
|
||||
else
|
||||
feed('a')
|
||||
end
|
||||
screen:expect([[
|
||||
rows: 6, cols: 25 │rows: 6, cols: 25 |
|
||||
mouse enabled │mouse enabled |
|
||||
│ |*3
|
||||
#!! │ #!!^ |
|
||||
{5:-- TERMINAL --} |
|
||||
]])
|
||||
-- Mouse is forwarded after process is resumed.
|
||||
api.nvim_input_mouse('right', 'press', '', 0, 0, 28)
|
||||
screen:expect({ any = vim.pesc('"$!^') })
|
||||
api.nvim_input_mouse('right', 'release', '', 0, 0, 28)
|
||||
screen:expect({ any = vim.pesc('#$!^') })
|
||||
end
|
||||
|
||||
it('resumed by an external signal', function()
|
||||
skip(is_os('mac'), 'FIXME: does not work on macOS')
|
||||
test_term_process_suspend_resume(true)
|
||||
end)
|
||||
|
||||
it('resumed by pressing a key', function()
|
||||
test_term_process_suspend_resume(false)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe(':terminal buffer', function()
|
||||
|
||||
@@ -100,11 +100,11 @@ describe('TUI', function()
|
||||
]])
|
||||
else -- resuming works on other platforms
|
||||
screen:expect([[
|
||||
^ |
|
||||
|*5
|
||||
^[Process suspended] |
|
||||
{5:-- TERMINAL --} |
|
||||
]])
|
||||
exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigcont')]])
|
||||
n.feed('<Space>')
|
||||
screen:expect(s0)
|
||||
end
|
||||
feed_data(':')
|
||||
@@ -4409,7 +4409,6 @@ describe('TUI client', function()
|
||||
it('suspend/resume works with multiple clients', function()
|
||||
t.skip(is_os('win'), 'N/A for Windows')
|
||||
local server_super, screen_server, screen_client = start_tui_and_remote_client()
|
||||
local server_super_exec_lua = tt.make_lua_executor(server_super)
|
||||
|
||||
local screen_normal = [[
|
||||
Hello, Worl^d |
|
||||
@@ -4419,8 +4418,8 @@ describe('TUI client', function()
|
||||
{5:-- TERMINAL --} |
|
||||
]]
|
||||
local screen_suspended = [[
|
||||
^ |
|
||||
|*5
|
||||
^[Process suspended] |
|
||||
{5:-- TERMINAL --} |
|
||||
]]
|
||||
|
||||
@@ -4433,12 +4432,12 @@ describe('TUI client', function()
|
||||
screen_server:expect({ grid = screen_suspended })
|
||||
|
||||
-- Resume the remote client.
|
||||
exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigcont')]])
|
||||
n.feed('<Space>')
|
||||
screen_client:expect({ grid = screen_normal })
|
||||
screen_server:expect({ grid = screen_suspended, unchanged = true })
|
||||
|
||||
-- Resume the embedding client.
|
||||
server_super_exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigcont')]])
|
||||
server_super:request('nvim_input', '<Space>')
|
||||
screen_server:expect({ grid = screen_normal })
|
||||
screen_client:expect({ grid = screen_normal, unchanged = true })
|
||||
|
||||
@@ -4448,7 +4447,7 @@ describe('TUI client', function()
|
||||
screen_server:expect({ grid = screen_suspended })
|
||||
|
||||
-- Resume the remote client.
|
||||
exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigcont')]])
|
||||
n.feed('<Space>')
|
||||
screen_client:expect({ grid = screen_normal })
|
||||
screen_server:expect({ grid = screen_suspended, unchanged = true })
|
||||
|
||||
@@ -4458,12 +4457,12 @@ describe('TUI client', function()
|
||||
screen_server:expect({ grid = screen_suspended, unchanged = true })
|
||||
|
||||
-- Resume the embedding client.
|
||||
server_super_exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigcont')]])
|
||||
server_super:request('nvim_input', '<Space>')
|
||||
screen_server:expect({ grid = screen_normal })
|
||||
screen_client:expect({ grid = screen_suspended, unchanged = true })
|
||||
|
||||
-- Resume the remote client.
|
||||
exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigcont')]])
|
||||
n.feed('<Space>')
|
||||
screen_client:expect({ grid = screen_normal })
|
||||
screen_server:expect({ grid = screen_normal, unchanged = true })
|
||||
|
||||
|
||||
Reference in New Issue
Block a user