feat(terminal): support theme update notifications (DEC mode 2031) (#31999)

This commit is contained in:
Gregory Anders 2025-01-14 08:18:59 -06:00 committed by GitHub
parent 7eabc8899a
commit f1c45fc7a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 104 additions and 0 deletions

View File

@ -337,6 +337,8 @@ TERMINAL
unfocused terminal window will have no cursor at all (so there is nothing to unfocused terminal window will have no cursor at all (so there is nothing to
highlight). highlight).
• |jobstart()| gained the "term" flag. • |jobstart()| gained the "term" flag.
• The |terminal| will send theme update notifications when 'background' is
changed and DEC mode 2031 is enabled.
TREESITTER TREESITTER

View File

@ -44,6 +44,7 @@
#include "nvim/spellfile.h" #include "nvim/spellfile.h"
#include "nvim/spellsuggest.h" #include "nvim/spellsuggest.h"
#include "nvim/strings.h" #include "nvim/strings.h"
#include "nvim/terminal.h"
#include "nvim/types_defs.h" #include "nvim/types_defs.h"
#include "nvim/vim_defs.h" #include "nvim/vim_defs.h"
#include "nvim/window.h" #include "nvim/window.h"
@ -532,6 +533,15 @@ const char *did_set_background(optset_T *args)
check_string_option(&p_bg); check_string_option(&p_bg);
init_highlight(false, false); init_highlight(false, false);
} }
// Notify all terminal buffers that the background color changed so they can
// send a theme update notification
FOR_ALL_BUFFERS(buf) {
if (buf->terminal) {
terminal_notify_theme(buf->terminal, dark);
}
}
return NULL; return NULL;
} }

View File

@ -179,6 +179,8 @@ struct terminal {
StringBuilder *send; ///< When there is a pending TermRequest autocommand, block and store input. StringBuilder *send; ///< When there is a pending TermRequest autocommand, block and store input.
} pending; } pending;
bool theme_updates; ///< Send a theme update notification when 'bg' changes
bool color_set[16]; bool color_set[16];
char *selection_buffer; /// libvterm selection buffer char *selection_buffer; /// libvterm selection buffer
@ -193,6 +195,7 @@ static VTermScreenCallbacks vterm_screen_callbacks = {
.movecursor = term_movecursor, .movecursor = term_movecursor,
.settermprop = term_settermprop, .settermprop = term_settermprop,
.bell = term_bell, .bell = term_bell,
.theme = term_theme,
.sb_pushline = term_sb_push, // Called before a line goes offscreen. .sb_pushline = term_sb_push, // Called before a line goes offscreen.
.sb_popline = term_sb_pop, .sb_popline = term_sb_pop,
}; };
@ -1141,6 +1144,20 @@ bool terminal_running(const Terminal *term)
return !term->closed; return !term->closed;
} }
void terminal_notify_theme(Terminal *term, bool dark)
FUNC_ATTR_NONNULL_ALL
{
if (!term->theme_updates) {
return;
}
char buf[10];
ssize_t ret = snprintf(buf, sizeof(buf), "\x1b[997;%cn", dark ? '1' : '2');
assert(ret > 0);
assert((size_t)ret <= sizeof(buf));
terminal_send(term, buf, (size_t)ret);
}
static void terminal_focus(const Terminal *term, bool focus) static void terminal_focus(const Terminal *term, bool focus)
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_ALL
{ {
@ -1259,6 +1276,10 @@ static int term_settermprop(VTermProp prop, VTermValue *val, void *data)
invalidate_terminal(term, -1, -1); invalidate_terminal(term, -1, -1);
break; break;
case VTERM_PROP_THEMEUPDATES:
term->theme_updates = val->boolean;
break;
default: default:
return 0; return 0;
} }
@ -1273,6 +1294,14 @@ static int term_bell(void *data)
return 1; return 1;
} }
/// Called when the terminal wants to query the system theme.
static int term_theme(bool *dark, void *data)
FUNC_ATTR_NONNULL_ALL
{
*dark = (*p_bg == 'd');
return 1;
}
/// Scrollback push handler: called just before a line goes offscreen (and libvterm will forget it), /// Scrollback push handler: called just before a line goes offscreen (and libvterm will forget it),
/// giving us a chance to store it. /// giving us a chance to store it.
/// ///

View File

@ -784,6 +784,17 @@ static int resize(int new_rows, int new_cols, VTermStateFields *fields, void *us
return 1; return 1;
} }
static int theme(bool *dark, void *user)
{
VTermScreen *screen = user;
if (screen->callbacks && screen->callbacks->theme) {
return (*screen->callbacks->theme)(dark, screen->cbdata);
}
return 1;
}
static int setlineinfo(int row, const VTermLineInfo *newinfo, const VTermLineInfo *oldinfo, static int setlineinfo(int row, const VTermLineInfo *newinfo, const VTermLineInfo *oldinfo,
void *user) void *user)
{ {
@ -838,6 +849,7 @@ static VTermStateCallbacks state_cbs = {
.settermprop = &settermprop, .settermprop = &settermprop,
.bell = &bell, .bell = &bell,
.resize = &resize, .resize = &resize,
.theme = &theme,
.setlineinfo = &setlineinfo, .setlineinfo = &setlineinfo,
.sb_clear = &sb_clear, .sb_clear = &sb_clear,
}; };

View File

@ -819,6 +819,10 @@ static void set_dec_mode(VTermState *state, int num, int val)
state->mode.bracketpaste = (unsigned)val; state->mode.bracketpaste = (unsigned)val;
break; break;
case 2031:
settermprop_bool(state, VTERM_PROP_THEMEUPDATES, val);
break;
default: default:
DEBUG_LOG("libvterm: Unknown DEC mode %d\n", num); DEBUG_LOG("libvterm: Unknown DEC mode %d\n", num);
return; return;
@ -894,6 +898,10 @@ static void request_dec_mode(VTermState *state, int num)
reply = state->mode.bracketpaste; reply = state->mode.bracketpaste;
break; break;
case 2031:
reply = state->mode.theme_updates;
break;
default: default:
vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?%d;%d$y", num, 0); vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?%d;%d$y", num, 0);
return; return;
@ -1387,6 +1395,7 @@ static int on_csi(const char *leader, const long args[], int argcount, const cha
{ {
char *qmark = (leader_byte == '?') ? "?" : ""; char *qmark = (leader_byte == '?') ? "?" : "";
bool dark = false;
switch (val) { switch (val) {
case 0: case 0:
@ -1403,6 +1412,13 @@ static int on_csi(const char *leader, const long args[], int argcount, const cha
vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "%s%d;%dR", qmark, state->pos.row + 1, vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "%s%d;%dR", qmark, state->pos.row + 1,
state->pos.col + 1); state->pos.col + 1);
break; break;
case 996:
if (state->callbacks && state->callbacks->theme) {
if (state->callbacks->theme(&dark, state->cbdata)) {
vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?997;%cn", dark ? '1' : '2');
}
}
break;
} }
} }
break; break;
@ -2268,6 +2284,9 @@ int vterm_state_set_termprop(VTermState *state, VTermProp prop, VTermValue *val)
case VTERM_PROP_FOCUSREPORT: case VTERM_PROP_FOCUSREPORT:
state->mode.report_focus = (unsigned)val->boolean; state->mode.report_focus = (unsigned)val->boolean;
return 1; return 1;
case VTERM_PROP_THEMEUPDATES:
state->mode.theme_updates = (unsigned)val->boolean;
return 1;
case VTERM_N_PROPS: case VTERM_N_PROPS:
return 0; return 0;

View File

@ -86,6 +86,7 @@ typedef enum {
VTERM_PROP_CURSORSHAPE, // number VTERM_PROP_CURSORSHAPE, // number
VTERM_PROP_MOUSE, // number VTERM_PROP_MOUSE, // number
VTERM_PROP_FOCUSREPORT, // bool VTERM_PROP_FOCUSREPORT, // bool
VTERM_PROP_THEMEUPDATES, // bool
VTERM_N_PROPS, VTERM_N_PROPS,
} VTermProp; } VTermProp;
@ -111,6 +112,7 @@ typedef struct {
int (*settermprop)(VTermProp prop, VTermValue *val, void *user); int (*settermprop)(VTermProp prop, VTermValue *val, void *user);
int (*bell)(void *user); int (*bell)(void *user);
int (*resize)(int rows, int cols, void *user); int (*resize)(int rows, int cols, void *user);
int (*theme)(bool *dark, void *user);
int (*sb_pushline)(int cols, const VTermScreenCell *cells, void *user); int (*sb_pushline)(int cols, const VTermScreenCell *cells, void *user);
int (*sb_popline)(int cols, VTermScreenCell *cells, void *user); int (*sb_popline)(int cols, VTermScreenCell *cells, void *user);
int (*sb_clear)(void *user); int (*sb_clear)(void *user);
@ -263,6 +265,7 @@ typedef struct {
int (*settermprop)(VTermProp prop, VTermValue *val, void *user); int (*settermprop)(VTermProp prop, VTermValue *val, void *user);
int (*bell)(void *user); int (*bell)(void *user);
int (*resize)(int rows, int cols, VTermStateFields *fields, void *user); int (*resize)(int rows, int cols, VTermStateFields *fields, void *user);
int (*theme)(bool *dark, void *user);
int (*setlineinfo)(int row, const VTermLineInfo *newinfo, const VTermLineInfo *oldinfo, int (*setlineinfo)(int row, const VTermLineInfo *newinfo, const VTermLineInfo *oldinfo,
void *user); void *user);
int (*sb_clear)(void *user); int (*sb_clear)(void *user);

View File

@ -119,6 +119,7 @@ struct VTermState {
unsigned leftrightmargin:1; unsigned leftrightmargin:1;
unsigned bracketpaste:1; unsigned bracketpaste:1;
unsigned report_focus:1; unsigned report_focus:1;
unsigned theme_updates:1;
} mode; } mode;
VTermEncodingInstance encoding[4], encoding_utf8; VTermEncodingInstance encoding[4], encoding_utf8;

View File

@ -3309,6 +3309,32 @@ describe('TUI bg color', function()
{3:-- TERMINAL --} | {3:-- TERMINAL --} |
]]) ]])
end) end)
it('sends theme update notifications when background changes #31652', function()
command('set background=dark') -- set outer Nvim background
local child_server = new_pipename()
local screen = tt.setup_child_nvim({
'--listen',
child_server,
'-u',
'NONE',
'-i',
'NONE',
'--cmd',
'colorscheme vim',
'--cmd',
'set noswapfile',
})
screen:expect({ any = '%[No Name%]' })
local child_session = n.connect(child_server)
retry(nil, nil, function()
eq({ true, 'dark' }, { child_session:request('nvim_eval', '&background') })
end)
command('set background=light') -- set outer Nvim background
retry(nil, nil, function()
eq({ true, 'light' }, { child_session:request('nvim_eval', '&background') })
end)
end)
end) end)
-- These tests require `tt` because --headless/--embed -- These tests require `tt` because --headless/--embed

View File

@ -345,6 +345,8 @@ static VTermValueType vterm_get_prop_type(VTermProp prop)
return VTERM_VALUETYPE_INT; return VTERM_VALUETYPE_INT;
case VTERM_PROP_FOCUSREPORT: case VTERM_PROP_FOCUSREPORT:
return VTERM_VALUETYPE_BOOL; return VTERM_VALUETYPE_BOOL;
case VTERM_PROP_THEMEUPDATES:
return VTERM_VALUETYPE_BOOL;
case VTERM_N_PROPS: case VTERM_N_PROPS:
return 0; return 0;