From c8693051a8b6544aefd6a644564b648ee724f43c Mon Sep 17 00:00:00 2001 From: Ayaan <138162656+def3r@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:32:50 +0530 Subject: [PATCH] feat(terminal): surface exit code via virttext + nvim_get_chan_info #37987 Problem: When a terminal process exits, "[Process Exited]" text is added to the buffer contents. Solution: - Return `exitcode` field from `nvim_get_chan_info`. - Show it in the default 'statusline'. - Show exitcode as virtual text in the terminal buffer. --- runtime/doc/api.txt | 1 + runtime/doc/news.txt | 4 +++ runtime/lua/vim/_core/util.lua | 18 ++++++++++ runtime/lua/vim/_meta/api.lua | 1 + runtime/lua/vim/_meta/options.lua | 2 +- src/nvim/api/vim.c | 1 + src/nvim/channel.c | 4 ++- src/nvim/options.lua | 1 + src/nvim/terminal.c | 33 ++++++++++++++++--- test/functional/api/vim_spec.lua | 3 ++ test/functional/core/job_spec.lua | 4 +-- test/functional/core/main_spec.lua | 4 +-- test/functional/core/startup_spec.lua | 3 +- .../swapfile_preserve_recover_spec.lua | 25 +++----------- test/functional/terminal/buffer_spec.lua | 17 ++++++---- test/functional/terminal/channel_spec.lua | 4 +-- test/functional/terminal/edit_spec.lua | 3 +- test/functional/terminal/scrollback_spec.lua | 9 +++-- test/functional/terminal/tui_spec.lua | 20 +++++------ test/functional/testterm.lua | 16 +++++++++ test/functional/ui/messages_spec.lua | 2 +- test/functional/ui/statusline_spec.lua | 14 ++++++++ 22 files changed, 132 insertions(+), 57 deletions(-) diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index d704a3a071..3d17864047 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -841,6 +841,7 @@ nvim_get_chan_info({chan}) *nvim_get_chan_info()* • "buffer" (optional) Buffer connected to |terminal| instance. • "client" (optional) Info about the peer (client on the other end of the channel), as set by |nvim_set_client_info()|. + • "exitcode" (optional) Exit code of the |terminal| process. nvim_get_color_by_name({name}) *nvim_get_color_by_name()* Returns the 24-bit RGB value of a |nvim_get_color_map()| color name or diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 0398086e4e..91902bbb56 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -165,6 +165,7 @@ API • Added experimental |nvim__exec_lua_fast()| to allow remote API clients to execute code while nvim is blocking for input. • |vim.secure.trust()| accepts `path` for the `allow` action. +• |nvim_get_chan_info()| includes `exitcode` field for terminal buffers BUILD @@ -192,6 +193,7 @@ DEFAULTS • |grt| in Normal mode maps to |vim.lsp.buf.type_definition()| • 'shada' default now excludes "/tmp/" and "/private/" paths to reduce clutter in |:oldfiles|. • Enabled treesitter highlighting for Markdown files +• 'statusline' shows exit code of finished terminal buffers DIAGNOSTICS @@ -472,6 +474,8 @@ These existing features changed their behavior. for input and output. This matters mostly for Linux where some command lines using "/dev/stdin" and similiar would break as these special files can be reopened when backed by pipes but not when backed by socket pairs. +• On terminal exit, "[Process exited]" is not added to the buffer contents, + instead it is shown as virtual text and exit code is shown in statusline. ============================================================================== REMOVED FEATURES *news-removed* diff --git a/runtime/lua/vim/_core/util.lua b/runtime/lua/vim/_core/util.lua index b9eb9a81c9..70eda8fdcd 100644 --- a/runtime/lua/vim/_core/util.lua +++ b/runtime/lua/vim/_core/util.lua @@ -81,4 +81,22 @@ function M.source_is_lua(bufnr, line1, line2) return lang_tree:lang() == 'lua' end +--- Returns the exit code string for the current buffer, given: +--- - Channel is attached to the current buffer +--- - Current buffer is a terminal buffer +--- +--- @return string +function M.term_exitcode() + local chan_id = vim.bo.channel + if chan_id == 0 or vim.bo.buftype ~= 'terminal' then + return '' + end + + local info = vim.api.nvim_get_chan_info(chan_id) + if info.exitcode and info.exitcode >= 0 then + return string.format('[Exit: %d]', info.exitcode) + end + return '' +end + return M diff --git a/runtime/lua/vim/_meta/api.lua b/runtime/lua/vim/_meta/api.lua index 0ff74f4a2b..9b7bec6678 100644 --- a/runtime/lua/vim/_meta/api.lua +++ b/runtime/lua/vim/_meta/api.lua @@ -1309,6 +1309,7 @@ function vim.api.nvim_get_autocmds(opts) end --- - "buffer" (optional) Buffer connected to |terminal| instance. --- - "client" (optional) Info about the peer (client on the other end of the channel), as set --- by |nvim_set_client_info()|. +--- - "exitcode" (optional) Exit code of the |terminal| process. --- function vim.api.nvim_get_chan_info(chan) end diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua index 99bd6ede5d..177c77b8eb 100644 --- a/runtime/lua/vim/_meta/options.lua +++ b/runtime/lua/vim/_meta/options.lua @@ -6964,7 +6964,7 @@ vim.wo.stc = vim.wo.statuscolumn --- --- --- @type string -vim.o.statusline = "%<%f %h%w%m%r %=%{% &showcmdloc == 'statusline' ? '%-10.S ' : '' %}%{% exists('b:keymap_name') ? '<'..b:keymap_name..'> ' : '' %}%{% &busy > 0 ? '◐ ' : '' %}%{% luaeval('(package.loaded[''vim.diagnostic''] and #vim.diagnostic.count() ~= 0 and vim.diagnostic.status() .. '' '') or '''' ') %}%{% &ruler ? ( &rulerformat == '' ? '%-14.(%l,%c%V%) %P' : &rulerformat ) : '' %}" +vim.o.statusline = "%<%f %h%w%m%r %{% v:lua.require('vim._core.util').term_exitcode() %}%=%{% &showcmdloc == 'statusline' ? '%-10.S ' : '' %}%{% exists('b:keymap_name') ? '<'..b:keymap_name..'> ' : '' %}%{% &busy > 0 ? '◐ ' : '' %}%{% luaeval('(package.loaded[''vim.diagnostic''] and #vim.diagnostic.count() ~= 0 and vim.diagnostic.status() .. '' '') or '''' ') %}%{% &ruler ? ( &rulerformat == '' ? '%-14.(%l,%c%V%) %P' : &rulerformat ) : '' %}" vim.o.stl = vim.o.statusline vim.wo.statusline = vim.o.statusline vim.wo.stl = vim.wo.statusline diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 26a1f4cda9..1c5ffb5c0e 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -1719,6 +1719,7 @@ void nvim_set_client_info(uint64_t channel_id, String name, Dict version, String /// - "buffer" (optional) Buffer connected to |terminal| instance. /// - "client" (optional) Info about the peer (client on the other end of the channel), as set /// by |nvim_set_client_info()|. +/// - "exitcode" (optional) Exit code of the |terminal| process. /// Dict nvim_get_chan_info(uint64_t channel_id, Integer chan, Arena *arena, Error *err) FUNC_API_SINCE(4) diff --git a/src/nvim/channel.c b/src/nvim/channel.c index 12e2c93739..0314a6f15e 100644 --- a/src/nvim/channel.c +++ b/src/nvim/channel.c @@ -182,6 +182,7 @@ bool channel_close(uint64_t id, ChannelPart part, const char **error) chan->stream.internal.cb = LUA_NOREF; chan->stream.internal.closed = true; terminal_close(&chan->term, 0); + chan->exit_status = 0; } else { channel_decref(chan); } @@ -784,8 +785,8 @@ static void channel_proc_exit_cb(Proc *proc, int status, void *data) bool exited = (status >= 0); if (exited && chan->on_exit.type != kCallbackNone) { schedule_channel_event(chan); - chan->exit_status = status; } + chan->exit_status = exited ? status : chan->exit_status; channel_decref(chan); } @@ -1001,6 +1002,7 @@ Dict channel_info(uint64_t id, Arena *arena) } else if (chan->term) { mode_desc = "terminal"; PUT_C(info, "buffer", BUFFER_OBJ(terminal_buf(chan->term))); + PUT_C(info, "exitcode", INTEGER_OBJ(chan->exit_status)); } else { mode_desc = "bytes"; } diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 24acc80202..e17b80817a 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -8774,6 +8774,7 @@ local options = { if_true = table.concat({ '%<', '%f %h%w%m%r ', + "%{% v:lua.require('vim._core.util').term_exitcode() %}", '%=', "%{% &showcmdloc == 'statusline' ? '%-10.S ' : '' %}", "%{% exists('b:keymap_name') ? '<'..b:keymap_name..'> ' : '' %}", diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index a250f2aaa2..fba8376503 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -64,6 +64,7 @@ #include "nvim/event/multiqueue.h" #include "nvim/event/time.h" #include "nvim/ex_docmd.h" +#include "nvim/extmark.h" #include "nvim/getchar.h" #include "nvim/globals.h" #include "nvim/grid.h" @@ -706,13 +707,37 @@ void terminal_close(Terminal **termpp, int status) } else if (!only_destroy) { // Associated channel has been closed and the editor is not exiting. // Do not call the close callback now. Wait for the user to press a key. - char msg[sizeof("\r\n[Process exited ]") + NUMBUFLEN]; + char msg[sizeof("[Process exited ]") + NUMBUFLEN]; if (((Channel *)term->opts.data)->streamtype == kChannelStreamInternal) { - snprintf(msg, sizeof msg, "\r\n[Terminal closed]"); + snprintf(msg, sizeof msg, "[Terminal closed]"); } else { - snprintf(msg, sizeof msg, "\r\n[Process exited %d]", status); + snprintf(msg, sizeof msg, "[Process exited %d]", status); + } + + // Show the msg as virtual text instead of adding it to buffer + VirtTextChunk *chunk = xmalloc(sizeof(VirtTextChunk)); + *chunk = (VirtTextChunk) { .text = xstrdup(msg), .hl_id = -1 }; + DecorVirtText *virt_text = xmalloc(sizeof(DecorVirtText)); + *virt_text = (DecorVirtText) { + .priority = DECOR_PRIORITY_BASE, + .pos = kVPosWinCol, + .data.virt_text = { .items = chunk, .size = 1 } + }; + DecorInline decor = { + .ext = true, .data.ext = { .sh_idx = DECOR_ID_INVALID, .vt = virt_text } + }; + + int pos = MIN(row_to_linenr(term, term->cursor.row), + buf->b_ml.ml_line_count - 1); + extmark_set(buf, (uint32_t)-1, NULL, pos, 0, -1, 0, + decor, 0, true, false, true, false, NULL); + + // Redraw statusline to show the exit code. + FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { + if (wp->w_buffer == buf) { + wp->w_redr_status = true; + } } - terminal_receive(term, msg, strlen(msg)); } if (only_destroy) { diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 7e7c00a0f0..6029da0fc4 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -2834,6 +2834,7 @@ describe('API', function() mode = 'terminal', buffer = 1, pty = '?', + exitcode = -1, } local event = api.nvim_get_var('opened_event') if not is_os('win') then @@ -2869,6 +2870,7 @@ describe('API', function() mode = 'terminal', buffer = 2, pty = '?', + exitcode = -1, } local actual2 = eval('nvim_get_chan_info(&channel)') expected2.pty = actual2.pty @@ -2878,6 +2880,7 @@ describe('API', function() eq(1, eval('jobstop(&channel)')) eval('jobwait([&channel], 1000)') -- Wait. expected2.pty = (is_os('win') and '?' or '') -- pty stream was closed. + expected2.exitcode = (is_os('win') and 143 or 129) eq(expected2, eval('nvim_get_chan_info(&channel)')) end) end) diff --git a/test/functional/core/job_spec.lua b/test/functional/core/job_spec.lua index da85155eb4..8a6de168b2 100644 --- a/test/functional/core/job_spec.lua +++ b/test/functional/core/job_spec.lua @@ -1370,8 +1370,8 @@ describe('jobs', function() feed(':q') screen:expect([[ - | - [Process exited 0]^ | + ^ | + [Process exited 0] | |*4 {5:-- TERMINAL --} | ]]) diff --git a/test/functional/core/main_spec.lua b/test/functional/core/main_spec.lua index 11ed0e69c6..2ff02901e6 100644 --- a/test/functional/core/main_spec.lua +++ b/test/functional/core/main_spec.lua @@ -117,8 +117,8 @@ describe('command-line option', function() ) feed('i:cq') screen:expect([[ - | - [Process exited 1]^ | + ^ | + [Process exited 1] | |*5 {5:-- TERMINAL --} | ]]) diff --git a/test/functional/core/startup_spec.lua b/test/functional/core/startup_spec.lua index 3b5ded0dd1..503fe7997d 100644 --- a/test/functional/core/startup_spec.lua +++ b/test/functional/core/startup_spec.lua @@ -60,7 +60,8 @@ describe('startup', function() ) screen:expect([[ ^Cannot attach UI of :terminal child to its parent. (Unset $NVIM to skip this check) | - |*2 + [Process exited 1] | + | ]]) end) diff --git a/test/functional/ex_cmds/swapfile_preserve_recover_spec.lua b/test/functional/ex_cmds/swapfile_preserve_recover_spec.lua index 8f295f0f95..96f43bb037 100644 --- a/test/functional/ex_cmds/swapfile_preserve_recover_spec.lua +++ b/test/functional/ex_cmds/swapfile_preserve_recover_spec.lua @@ -1,5 +1,6 @@ local t = require('test.testutil') local n = require('test.functional.testnvim')() +local tt = require('test.functional.testterm') local Screen = require('test.functional.ui.screen') local uv = vim.uv @@ -24,6 +25,7 @@ local poke_eventloop = n.poke_eventloop local api = n.api local retry = t.retry local write_file = t.write_file +local expect_exitcode = tt.expect_exitcode describe(':recover', function() before_each(clear) @@ -552,12 +554,7 @@ describe('quitting swapfile dialog on startup stops TUI properly', function() ) end) api.nvim_chan_send(chan, 'q') - retry(nil, nil, function() - eq( - { '', '[Process exited 1]', '' }, - eval("[1, 2, '$']->map({_, lnum -> getline(lnum)->trim(' ', 2)})") - ) - end) + expect_exitcode(1) end) it('(A)bort at second file argument with -p', function() @@ -585,12 +582,7 @@ describe('quitting swapfile dialog on startup stops TUI properly', function() ) end) api.nvim_chan_send(chan, 'a') - retry(nil, nil, function() - eq( - { '', '[Process exited 1]', '' }, - eval("[1, 2, '$']->map({_, lnum -> getline(lnum)->trim(' ', 2)})") - ) - end) + expect_exitcode(1) end) it('(Q)uit at file opened by -t', function() @@ -626,13 +618,6 @@ describe('quitting swapfile dialog on startup stops TUI properly', function() ) end) api.nvim_chan_send(chan, 'q') - retry(nil, nil, function() - eq( - { '[Process exited 1]' }, - eval( - "[1, 2, '$']->map({_, lnum -> getline(lnum)->trim(' ', 2)})->filter({_, s -> !empty(trim(s))})" - ) - ) - end) + expect_exitcode(1) end) end) diff --git a/test/functional/terminal/buffer_spec.lua b/test/functional/terminal/buffer_spec.lua index 54838516e3..003a63ec42 100644 --- a/test/functional/terminal/buffer_spec.lua +++ b/test/functional/terminal/buffer_spec.lua @@ -986,7 +986,7 @@ describe(':terminal buffer', function() 3: å̲│{1:~ }| │{1:~ }| [Pro│{1:~ }| - cess│{1:~ }| + │{1:~ }| | ]]) end) @@ -1005,10 +1005,10 @@ describe(':terminal buffer', function() %d: TEST{MATCH: +}| %d: TEST{MATCH: +}| %d: TEST{MATCH: +}| - | - [Process exited 0]^ | + %d: TEST{MATCH: +}| + ^[Process exited 0] | {5:-- TERMINAL --} | - ]]):format(count - 4, count - 3, count - 2, count - 1)) + ]]):format(count - 5, count - 4, count - 3, count - 2, count - 1)) local lines = api.nvim_buf_get_lines(0, 0, -1, true) for i = 1, count do eq(('%d: TEST'):format(i - 1), lines[i]) @@ -1313,6 +1313,11 @@ describe(':terminal buffer', function() [Process exited 0] | |*5 ]]) + api.nvim_buf_clear_namespace(0, -1, 0, -1) + env.screen:expect([[ + ^ready $ | + |*6 + ]]) env.buf = api.nvim_get_current_buf() api.nvim_set_option_value('modified', false, { buf = env.buf }) end) @@ -1329,10 +1334,10 @@ describe(':terminal buffer', function() local chan = api.nvim_open_term(0, {}) api.nvim_chan_send(chan, 'TEST') fn.chanclose(chan) + api.nvim_buf_clear_namespace(0, -1, 0, -1) env.screen:expect([[ ^TEST | - [Terminal closed] | - |*5 + |*6 ]]) env.buf = api.nvim_get_current_buf() api.nvim_set_option_value('modified', false, { buf = env.buf }) diff --git a/test/functional/terminal/channel_spec.lua b/test/functional/terminal/channel_spec.lua index 42d2880096..021bb58c17 100644 --- a/test/functional/terminal/channel_spec.lua +++ b/test/functional/terminal/channel_spec.lua @@ -185,9 +185,9 @@ local function test_autocmd_no_crash(event, extra_tests) ]]) feed('i') env.screen:expect([[ + 48: TEST | 49: TEST | - | - [Process exited 0]^ | + ^[Process exited 0] | {5:-- TERMINAL --} | ]]) feed('') diff --git a/test/functional/terminal/edit_spec.lua b/test/functional/terminal/edit_spec.lua index dcd566d04a..765e629f31 100644 --- a/test/functional/terminal/edit_spec.lua +++ b/test/functional/terminal/edit_spec.lua @@ -43,12 +43,11 @@ describe(':edit term://*', function() local bufcontents = {} local winheight = api.nvim_win_get_height(0) - local buf_cont_start = rep - sb - winheight + 2 + local buf_cont_start = rep - sb - winheight + 1 for i = buf_cont_start, (rep - 1) do bufcontents[#bufcontents + 1] = ('%d: foobar'):format(i) end bufcontents[#bufcontents + 1] = '' - bufcontents[#bufcontents + 1] = '[Process exited 0]' local exp_screen = '\n' for i = 1, (winheight - 1) do diff --git a/test/functional/terminal/scrollback_spec.lua b/test/functional/terminal/scrollback_spec.lua index ac44b784de..2a766040e3 100644 --- a/test/functional/terminal/scrollback_spec.lua +++ b/test/functional/terminal/scrollback_spec.lua @@ -861,12 +861,12 @@ describe(':terminal prints more lines than the screen height and exits', functio ("call jobstart(['%s', '10'], {'term':v:true}) | startinsert"):format(testprg('tty-test')) ) screen:expect([[ + line5 | line6 | line7 | line8 | line9 | - | - [Process exited 0]^ | + ^[Process exited 0] | {5:-- TERMINAL --} | ]]) feed('') @@ -1096,9 +1096,8 @@ describe('pending scrollback line handling', function() or { 'printf', ('hi\n'):rep(12) } ) screen:expect [[ - hi |*4 - | - [Process exited 0]^ | + hi |*5 + ^[Process exited 0] | {5:-- TERMINAL --} | ]] assert_alive() diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 2e18802a7a..f0da67c699 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -2519,8 +2519,8 @@ describe('TUI', function() exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigterm')]]) screen:expect(is_os('win') and { any = '%[Process exited 1%]' } or [[ Nvim: Caught deadly signal 'SIGTERM' | - | - [Process exited 1]^ | + ^ | + [Process exited 1] | |*3 {5:-- TERMINAL --} | ]]) @@ -2554,8 +2554,8 @@ describe('TUI', function() ]] child_session:notify('nvim_exec_lua', code, {}) screen:expect([[ - | - [Process exited 0]^ | + ^ | + [Process exited 0] | |*4 {5:-- TERMINAL --} | ]]) @@ -2970,8 +2970,8 @@ describe('TUI', function() :w testF | :q | abc | - | - [Process exited 0]^ | + ^ | + [Process exited 0] | | {5:-- TERMINAL --} | ]]) @@ -4237,8 +4237,8 @@ describe('TUI client', function() exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigterm')]]) screen_client:expect(is_os('win') and { any = '%[Process exited 1%]' } or [[ Nvim: Caught deadly signal 'SIGTERM' | - | - [Process exited 1]^ | + ^ | + [Process exited 1] | |*3 {5:-- TERMINAL --} | ]]) @@ -4362,8 +4362,8 @@ describe('TUI client', function() screen:expect([[ Remote ui failed to start: {MATCH:.*}| - | - [Process exited 1]^ | + ^ | + [Process exited 1] | |*3 {5:-- TERMINAL --} | ]]) diff --git a/test/functional/testterm.lua b/test/functional/testterm.lua index 92cab6e540..f7e7df6080 100644 --- a/test/functional/testterm.lua +++ b/test/functional/testterm.lua @@ -16,6 +16,8 @@ local testprg = n.testprg local exec_lua = n.exec_lua local api = n.api local nvim_prog = n.nvim_prog +local retry = t.retry +local eq = t.eq local M = {} @@ -221,4 +223,18 @@ function M.screen_expect(screen, s) screen:expect(s) end +--- Asserts that the exit code of chan eventually matches the expected exit code +--- +--- @param code integer expected exit code +--- @param chan? integer channel id, defaults to current buffer's channel +function M.expect_exitcode(code, chan) + chan = chan or api.nvim_get_option_value('channel', { buf = 0 }) or 0 + eq(true, chan > 0, 'Expected a valid channel ID, but got: ' .. chan) + + retry(nil, nil, function() + local info = api.nvim_get_chan_info(chan) + eq(code, info.exitcode) + end) +end + return M diff --git a/test/functional/ui/messages_spec.lua b/test/functional/ui/messages_spec.lua index e6041d0bcb..1b56bea313 100644 --- a/test/functional/ui/messages_spec.lua +++ b/test/functional/ui/messages_spec.lua @@ -2479,7 +2479,7 @@ describe('ui/msg_puts_printf', function() ^Exモードに入ります。ノー | マルモードに戻るには "vis| ual" と入力してください。| - : | + [Process exited 0] | | ]]) end) diff --git a/test/functional/ui/statusline_spec.lua b/test/functional/ui/statusline_spec.lua index 606ecf82f6..2621466d2d 100644 --- a/test/functional/ui/statusline_spec.lua +++ b/test/functional/ui/statusline_spec.lua @@ -1,5 +1,6 @@ local t = require('test.testutil') local n = require('test.functional.testnvim')() +local tt = require('test.functional.testterm') local Screen = require('test.functional.ui.screen') local assert_alive = n.assert_alive @@ -14,6 +15,8 @@ local exec_lua = n.exec_lua local eval = n.eval local sleep = vim.uv.sleep local pcall_err = t.pcall_err +local testprg = n.testprg +local expect_exitcode = tt.expect_exitcode local mousemodels = { 'extend', 'popup', 'popup_setpos' } @@ -959,6 +962,7 @@ describe('default statusline', function() local default_statusline = table.concat({ '%<', '%f %h%w%m%r ', + "%{% v:lua.require('vim._core.util').term_exitcode() %}", '%=', "%{% &showcmdloc == 'statusline' ? '%-10.S ' : '' %}", "%{% exists('b:keymap_name') ? '<'..b:keymap_name..'> ' : '' %}", @@ -1026,6 +1030,16 @@ describe('default statusline', function() | ]]) end) + + it('shows exit code when terminal exits #14986', function() + exec_lua("vim.o.statusline = ''") + api.nvim_set_option_value('shell', testprg('shell-test'), {}) + api.nvim_set_option_value('shellcmdflag', 'EXIT', {}) + api.nvim_set_option_value('shellxquote', '', {}) -- win: avoid extra quotes + command('terminal 9') + screen:expect({ any = '%[Exit: 9%]' }) + expect_exitcode(9) + end) end) describe("'statusline' in floatwin", function()