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
highlight).
• |jobstart()| gained the "term" flag.
• The |terminal| will send theme update notifications when 'background' is
changed and DEC mode 2031 is enabled.
TREESITTER

View File

@ -44,6 +44,7 @@
#include "nvim/spellfile.h"
#include "nvim/spellsuggest.h"
#include "nvim/strings.h"
#include "nvim/terminal.h"
#include "nvim/types_defs.h"
#include "nvim/vim_defs.h"
#include "nvim/window.h"
@ -532,6 +533,15 @@ const char *did_set_background(optset_T *args)
check_string_option(&p_bg);
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;
}

View File

@ -179,6 +179,8 @@ struct terminal {
StringBuilder *send; ///< When there is a pending TermRequest autocommand, block and store input.
} pending;
bool theme_updates; ///< Send a theme update notification when 'bg' changes
bool color_set[16];
char *selection_buffer; /// libvterm selection buffer
@ -193,6 +195,7 @@ static VTermScreenCallbacks vterm_screen_callbacks = {
.movecursor = term_movecursor,
.settermprop = term_settermprop,
.bell = term_bell,
.theme = term_theme,
.sb_pushline = term_sb_push, // Called before a line goes offscreen.
.sb_popline = term_sb_pop,
};
@ -1141,6 +1144,20 @@ bool terminal_running(const Terminal *term)
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)
FUNC_ATTR_NONNULL_ALL
{
@ -1259,6 +1276,10 @@ static int term_settermprop(VTermProp prop, VTermValue *val, void *data)
invalidate_terminal(term, -1, -1);
break;
case VTERM_PROP_THEMEUPDATES:
term->theme_updates = val->boolean;
break;
default:
return 0;
}
@ -1273,6 +1294,14 @@ static int term_bell(void *data)
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),
/// 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;
}
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,
void *user)
{
@ -838,6 +849,7 @@ static VTermStateCallbacks state_cbs = {
.settermprop = &settermprop,
.bell = &bell,
.resize = &resize,
.theme = &theme,
.setlineinfo = &setlineinfo,
.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;
break;
case 2031:
settermprop_bool(state, VTERM_PROP_THEMEUPDATES, val);
break;
default:
DEBUG_LOG("libvterm: Unknown DEC mode %d\n", num);
return;
@ -894,6 +898,10 @@ static void request_dec_mode(VTermState *state, int num)
reply = state->mode.bracketpaste;
break;
case 2031:
reply = state->mode.theme_updates;
break;
default:
vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?%d;%d$y", num, 0);
return;
@ -1387,6 +1395,7 @@ static int on_csi(const char *leader, const long args[], int argcount, const cha
{
char *qmark = (leader_byte == '?') ? "?" : "";
bool dark = false;
switch (val) {
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,
state->pos.col + 1);
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;
@ -2268,6 +2284,9 @@ int vterm_state_set_termprop(VTermState *state, VTermProp prop, VTermValue *val)
case VTERM_PROP_FOCUSREPORT:
state->mode.report_focus = (unsigned)val->boolean;
return 1;
case VTERM_PROP_THEMEUPDATES:
state->mode.theme_updates = (unsigned)val->boolean;
return 1;
case VTERM_N_PROPS:
return 0;

View File

@ -86,6 +86,7 @@ typedef enum {
VTERM_PROP_CURSORSHAPE, // number
VTERM_PROP_MOUSE, // number
VTERM_PROP_FOCUSREPORT, // bool
VTERM_PROP_THEMEUPDATES, // bool
VTERM_N_PROPS,
} VTermProp;
@ -111,6 +112,7 @@ typedef struct {
int (*settermprop)(VTermProp prop, VTermValue *val, void *user);
int (*bell)(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_popline)(int cols, VTermScreenCell *cells, void *user);
int (*sb_clear)(void *user);
@ -263,6 +265,7 @@ typedef struct {
int (*settermprop)(VTermProp prop, VTermValue *val, void *user);
int (*bell)(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,
void *user);
int (*sb_clear)(void *user);

View File

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

View File

@ -3309,6 +3309,32 @@ describe('TUI bg color', function()
{3:-- TERMINAL --} |
]])
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)
-- 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;
case VTERM_PROP_FOCUSREPORT:
return VTERM_VALUETYPE_BOOL;
case VTERM_PROP_THEMEUPDATES:
return VTERM_VALUETYPE_BOOL;
case VTERM_N_PROPS:
return 0;