From e536f48d0eeaf6f942220f5676de112715b8d7ae Mon Sep 17 00:00:00 2001 From: Jonathan Slenders <jonathan@slenders.be> Date: Wed, 11 Dec 2024 21:56:28 +0000 Subject: [PATCH] Add mode 2031 support (automatic dark/light mode). Co-Author: Nicholas Marriott <nicholas.marriott@gmail.com> --- colour.c | 32 ++++++++++ format.c | 20 +++++++ input-keys.c | 6 ++ input.c | 135 ++++++++++++++---------------------------- options-table.c | 2 + screen-redraw.c | 3 + server-client.c | 31 +++++++++- tmux.h | 20 ++++++- tty-keys.c | 20 +++++++ tty.c | 8 +++ window.c | 154 ++++++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 338 insertions(+), 93 deletions(-) diff --git a/colour.c b/colour.c index 5fd91c9c..9f7290f9 100644 --- a/colour.c +++ b/colour.c @@ -182,6 +182,38 @@ colour_tostring(int c) return ("invalid"); } +/* Convert background colour to theme. */ +enum client_theme +colour_totheme(int c) +{ + int r, g, b, brightness; + + if (c == -1) + return (THEME_UNKNOWN); + + if (c & COLOUR_FLAG_RGB) { + r = (c >> 16) & 0xff; + g = (c >> 8) & 0xff; + b = (c >> 0) & 0xff; + + brightness = r + g + b; + if (brightness > 382) + return (THEME_LIGHT); + return (THEME_DARK); + } + + if (c & COLOUR_FLAG_256) + return (colour_totheme(colour_256toRGB(c))); + + switch (c) { + case 0: + return (THEME_DARK); + case 7: + return (THEME_LIGHT); + } + return (THEME_UNKNOWN); +} + /* Convert colour from string. */ int colour_fromstring(const char *s) diff --git a/format.c b/format.c index 221550a1..9ce56104 100644 --- a/format.c +++ b/format.c @@ -1544,6 +1544,23 @@ format_cb_client_written(struct format_tree *ft) return (NULL); } +/* Callback for client_theme. */ +static void * +format_cb_client_theme(struct format_tree *ft) +{ + if (ft->c != NULL) { + switch (ft->c->theme) { + case THEME_DARK: + return (xstrdup("dark")); + case THEME_LIGHT: + return (xstrdup("light")); + case THEME_UNKNOWN: + return (NULL); + } + } + return (NULL); +} + /* Callback for config_files. */ static void * format_cb_config_files(__unused struct format_tree *ft) @@ -2881,6 +2898,9 @@ static const struct format_table_entry format_table[] = { { "client_termtype", FORMAT_TABLE_STRING, format_cb_client_termtype }, + { "client_theme", FORMAT_TABLE_STRING, + format_cb_client_theme + }, { "client_tty", FORMAT_TABLE_STRING, format_cb_client_tty }, diff --git a/input-keys.c b/input-keys.c index 097feeb2..dee438e3 100644 --- a/input-keys.c +++ b/input-keys.c @@ -307,6 +307,12 @@ static struct input_key_entry input_key_defaults[] = { { .key = KEYC_DC|KEYC_BUILD_MODIFIERS, .data = "\033[3;_~" }, + { .key = KEYC_REPORT_DARK_THEME, + .data = "\033[?997;1n" + }, + { .key = KEYC_REPORT_LIGHT_THEME, + .data = "\033[?997;2n" + }, }; static const key_code input_key_modifiers[] = { 0, diff --git a/input.c b/input.c index 6216cfb8..0a24234f 100644 --- a/input.c +++ b/input.c @@ -133,7 +133,7 @@ static void printflike(2, 3) input_reply(struct input_ctx *, const char *, ...); static void input_set_state(struct input_ctx *, const struct input_transition *); static void input_reset_cell(struct input_ctx *); - +static void input_report_current_theme(struct input_ctx *); static void input_osc_4(struct input_ctx *, const char *); static void input_osc_8(struct input_ctx *, const char *); static void input_osc_10(struct input_ctx *, const char *); @@ -243,6 +243,7 @@ enum input_csi_type { INPUT_CSI_DECSTBM, INPUT_CSI_DL, INPUT_CSI_DSR, + INPUT_CSI_DSR_PRIVATE, INPUT_CSI_ECH, INPUT_CSI_ED, INPUT_CSI_EL, @@ -251,6 +252,7 @@ enum input_csi_type { INPUT_CSI_IL, INPUT_CSI_MODOFF, INPUT_CSI_MODSET, + INPUT_CSI_QUERY_PRIVATE, INPUT_CSI_RCP, INPUT_CSI_REP, INPUT_CSI_RM, @@ -259,8 +261,8 @@ enum input_csi_type { INPUT_CSI_SD, INPUT_CSI_SGR, INPUT_CSI_SM, - INPUT_CSI_SM_PRIVATE, INPUT_CSI_SM_GRAPHICS, + INPUT_CSI_SM_PRIVATE, INPUT_CSI_SU, INPUT_CSI_TBC, INPUT_CSI_VPA, @@ -304,6 +306,8 @@ static const struct input_table_entry input_csi_table[] = { { 'm', ">", INPUT_CSI_MODSET }, { 'n', "", INPUT_CSI_DSR }, { 'n', ">", INPUT_CSI_MODOFF }, + { 'n', "?", INPUT_CSI_DSR_PRIVATE }, + { 'p', "?$", INPUT_CSI_QUERY_PRIVATE }, { 'q', " ", INPUT_CSI_DECSCUSR }, { 'q', ">", INPUT_CSI_XDA }, { 'r', "", INPUT_CSI_DECSTBM }, @@ -1531,6 +1535,20 @@ input_csi_dispatch(struct input_ctx *ictx) if (n != -1) screen_write_deleteline(sctx, n, bg); break; + case INPUT_CSI_DSR_PRIVATE: + switch (input_get(ictx, 0, 0, 0)) { + case 996: + input_report_current_theme(ictx); + break; + } + break; + case INPUT_CSI_QUERY_PRIVATE: + switch (input_get(ictx, 0, 0, 0)) { + case 2031: + input_reply(ictx, "\033[?2031;2$y"); + break; + } + break; case INPUT_CSI_DSR: switch (input_get(ictx, 0, 0, 0)) { case -1: @@ -1781,6 +1799,9 @@ input_csi_dispatch_rm_private(struct input_ctx *ictx) case 2004: screen_write_mode_clear(sctx, MODE_BRACKETPASTE); break; + case 2031: + screen_write_mode_clear(sctx, MODE_THEME_UPDATES); + break; default: log_debug("%s: unknown '%c'", __func__, ictx->ch); break; @@ -1876,6 +1897,9 @@ input_csi_dispatch_sm_private(struct input_ctx *ictx) case 2004: screen_write_mode_set(sctx, MODE_BRACKETPASTE); break; + case 2031: + screen_write_mode_set(sctx, MODE_THEME_UPDATES); + break; default: log_debug("%s: unknown '%c'", __func__, ictx->ch); break; @@ -2697,84 +2721,6 @@ bad: free(id); } -/* - * Get a client with a foreground for the pane. There isn't much to choose - * between them so just use the first. - */ -static int -input_get_fg_client(struct window_pane *wp) -{ - struct window *w = wp->window; - struct client *loop; - - TAILQ_FOREACH(loop, &clients, entry) { - if (loop->flags & CLIENT_UNATTACHEDFLAGS) - continue; - if (loop->session == NULL || !session_has(loop->session, w)) - continue; - if (loop->tty.fg == -1) - continue; - return (loop->tty.fg); - } - return (-1); -} - -/* Get a client with a background for the pane. */ -static int -input_get_bg_client(struct window_pane *wp) -{ - struct window *w = wp->window; - struct client *loop; - - TAILQ_FOREACH(loop, &clients, entry) { - if (loop->flags & CLIENT_UNATTACHEDFLAGS) - continue; - if (loop->session == NULL || !session_has(loop->session, w)) - continue; - if (loop->tty.bg == -1) - continue; - return (loop->tty.bg); - } - return (-1); -} - -/* - * If any control mode client exists that has provided a bg color, return it. - * Otherwise, return -1. - */ -static int -input_get_bg_control_client(struct window_pane *wp) -{ - struct client *c; - - if (wp->control_bg == -1) - return (-1); - - TAILQ_FOREACH(c, &clients, entry) { - if (c->flags & CLIENT_CONTROL) - return (wp->control_bg); - } - return (-1); -} - -/* - * If any control mode client exists that has provided a fg color, return it. - * Otherwise, return -1. - */ -static int -input_get_fg_control_client(struct window_pane *wp) -{ - struct client *c; - - if (wp->control_fg == -1) - return (-1); - - TAILQ_FOREACH(c, &clients, entry) { - if (c->flags & CLIENT_CONTROL) - return (wp->control_fg); - } - return (-1); -} /* Handle the OSC 10 sequence for setting and querying foreground colour. */ static void @@ -2787,11 +2733,11 @@ input_osc_10(struct input_ctx *ictx, const char *p) if (strcmp(p, "?") == 0) { if (wp == NULL) return; - c = input_get_fg_control_client(wp); + c = window_pane_get_fg_control_client(wp); if (c == -1) { tty_default_colours(&defaults, wp); if (COLOUR_DEFAULT(defaults.fg)) - c = input_get_fg_client(wp); + c = window_pane_get_fg(wp); else c = defaults.fg; } @@ -2832,20 +2778,12 @@ static void input_osc_11(struct input_ctx *ictx, const char *p) { struct window_pane *wp = ictx->wp; - struct grid_cell defaults; int c; if (strcmp(p, "?") == 0) { if (wp == NULL) return; - c = input_get_bg_control_client(wp); - if (c == -1) { - tty_default_colours(&defaults, wp); - if (COLOUR_DEFAULT(defaults.bg)) - c = input_get_bg_client(wp); - else - c = defaults.bg; - } + c = window_pane_get_bg(wp); input_osc_colour_reply(ictx, 11, c); return; } @@ -3064,3 +3002,18 @@ input_set_buffer_size(size_t buffer_size) log_debug("%s: %lu -> %lu", __func__, input_buffer_size, buffer_size); input_buffer_size = buffer_size; } + +static void +input_report_current_theme(struct input_ctx *ictx) +{ + switch (window_pane_get_theme(ictx->wp)) { + case THEME_DARK: + input_reply(ictx, "\033[?997;1n"); + break; + case THEME_LIGHT: + input_reply(ictx, "\033[?997;2n"); + break; + case THEME_UNKNOWN: + break; + } +} diff --git a/options-table.c b/options-table.c index 39215350..a461b3ea 100644 --- a/options-table.c +++ b/options-table.c @@ -1454,6 +1454,8 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_HOOK("client-focus-out", ""), OPTIONS_TABLE_HOOK("client-resized", ""), OPTIONS_TABLE_HOOK("client-session-changed", ""), + OPTIONS_TABLE_HOOK("client-light-theme", ""), + OPTIONS_TABLE_HOOK("client-dark-theme", ""), OPTIONS_TABLE_HOOK("command-error", ""), OPTIONS_TABLE_PANE_HOOK("pane-died", ""), OPTIONS_TABLE_PANE_HOOK("pane-exited", ""), diff --git a/screen-redraw.c b/screen-redraw.c index 34fd0069..d8281784 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -878,6 +878,9 @@ screen_redraw_draw_pane(struct screen_redraw_ctx *ctx, struct window_pane *wp) log_debug("%s: %s @%u %%%u", __func__, c->name, w->id, wp->id); + if (wp->flags & PANE_STYLECHANGED) + window_pane_send_theme_update(wp); + if (wp->xoff + wp->sx <= ctx->ox || wp->xoff >= ctx->ox + ctx->sx) return; if (ctx->statustop) diff --git a/server-client.c b/server-client.c index 0f8ba245..83cac567 100644 --- a/server-client.c +++ b/server-client.c @@ -56,11 +56,11 @@ static void server_client_set_title(struct client *); static void server_client_set_path(struct client *); static void server_client_reset_state(struct client *); static void server_client_update_latest(struct client *); - static void server_client_dispatch(struct imsg *, void *); static void server_client_dispatch_command(struct client *, struct imsg *); static void server_client_dispatch_identify(struct client *, struct imsg *); static void server_client_dispatch_shell(struct client *); +static void server_client_report_theme(struct client *, enum client_theme); /* Compare client windows. */ static int @@ -299,6 +299,7 @@ server_client_create(int fd) c->tty.sx = 80; c->tty.sy = 24; + c->theme = THEME_UNKNOWN; status_init(c); c->flags |= CLIENT_FOCUSED; @@ -2383,6 +2384,16 @@ server_client_key_callback(struct cmdq_item *item, void *data) event->key = key; } + /* Handle theme reporting keys. */ + if (key == KEYC_REPORT_LIGHT_THEME) { + server_client_report_theme(c, THEME_LIGHT); + goto out; + } + if (key == KEYC_REPORT_DARK_THEME) { + server_client_report_theme(c, THEME_DARK); + goto out; + } + /* Find affected pane. */ if (!KEYC_IS_MOUSE(key) || cmd_find_from_mouse(&fs, m, 0) != 0) cmd_find_from_client(&fs, c, 0); @@ -3909,3 +3920,21 @@ out: if (!parse) free(msg); } + +static void +server_client_report_theme(struct client *c, enum client_theme theme) +{ + if (theme == THEME_LIGHT) { + c->theme = THEME_LIGHT; + notify_client("client-light-theme", c); + } else { + c->theme = THEME_DARK; + notify_client("client-dark-theme", c); + } + + /* + * Request foregroundand background colour again. Don't forward 2031 to + * panes until a response is received. + */ + tty_puts(&c->tty, "\033]10;?\033\\\033]11;?\033\\"); +} diff --git a/tmux.h b/tmux.h index d448faa3..603fb1b6 100644 --- a/tmux.h +++ b/tmux.h @@ -377,6 +377,10 @@ enum { KEYC_KP_ZERO, KEYC_KP_PERIOD, + /* Theme reporting. */ + KEYC_REPORT_DARK_THEME, + KEYC_REPORT_LIGHT_THEME, + /* End of special keys. */ KEYC_BASE_END }; @@ -644,6 +648,7 @@ enum tty_code_code { #define MODE_CURSOR_VERY_VISIBLE 0x10000 #define MODE_CURSOR_BLINKING_SET 0x20000 #define MODE_KEYS_EXTENDED_2 0x40000 +#define MODE_THEME_UPDATES 0x80000 #define ALL_MODES 0xffffff #define ALL_MOUSE_MODES (MODE_MOUSE_STANDARD|MODE_MOUSE_BUTTON|MODE_MOUSE_ALL) @@ -1863,6 +1868,12 @@ struct overlay_ranges { u_int nx[OVERLAY_MAX_RANGES]; }; +enum client_theme { + THEME_UNKNOWN, + THEME_LIGHT, + THEME_DARK +}; + /* Client connection. */ typedef int (*prompt_input_cb)(struct client *, void *, const char *, int); typedef void (*prompt_free_cb)(void *); @@ -1922,6 +1933,7 @@ struct client { struct mouse_event click_event; struct status_line status; + enum client_theme theme; #define CLIENT_TERMINAL 0x1 #define CLIENT_LOGIN 0x2 @@ -2929,6 +2941,8 @@ void input_parse_screen(struct input_ctx *, struct screen *, void input_reply_clipboard(struct bufferevent *, const char *, size_t, const char *); void input_set_buffer_size(size_t); +int input_get_bg_client(struct window_pane *); +int input_get_bg_control_client(struct window_pane *); /* input-key.c */ void input_key_build(void); @@ -2943,7 +2957,8 @@ int colour_join_rgb(u_char, u_char, u_char); void colour_split_rgb(int, u_char *, u_char *, u_char *); int colour_force_rgb(int); const char *colour_tostring(int); -int colour_fromstring(const char *s); +enum client_theme colour_totheme(int); +int colour_fromstring(const char *); int colour_256toRGB(int); int colour_256to16(int); int colour_byname(const char *); @@ -3244,6 +3259,9 @@ void window_set_fill_character(struct window *); void window_pane_default_cursor(struct window_pane *); int window_pane_mode(struct window_pane *); int window_pane_show_scrollbar(struct window_pane *, int); +int window_pane_get_bg(struct window_pane *); +enum client_theme window_pane_get_theme(struct window_pane *); +void window_pane_send_theme_update(struct window_pane *); /* layout.c */ u_int layout_count_cells(struct layout_cell *); diff --git a/tty-keys.c b/tty-keys.c index 0de31c5d..262928ae 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -59,6 +59,7 @@ static int tty_keys_device_attributes2(struct tty *, const char *, size_t, size_t *); static int tty_keys_extended_device_attributes(struct tty *, const char *, size_t, size_t *); +static void tty_keys_send_theme_updates(struct tty *); /* A key tree entry. */ struct tty_key { @@ -213,6 +214,10 @@ static const struct tty_default_key_raw tty_default_raw_keys[] = { /* Extended keys. */ { "\033[1;5Z", '\011'|KEYC_CTRL|KEYC_SHIFT }, + + /* Theme reporting. */ + { "\033[?997;1n", KEYC_REPORT_DARK_THEME }, + { "\033[?997;2n", KEYC_REPORT_LIGHT_THEME }, }; /* Default xterm keys. */ @@ -791,10 +796,12 @@ tty_keys_next(struct tty *tty) switch (tty_keys_colours(tty, buf, len, &size, &tty->fg, &tty->bg)) { case 0: /* yes */ key = KEYC_UNKNOWN; + tty_keys_send_theme_updates(tty); goto complete_key; case -1: /* no, or not valid */ break; case 1: /* partial */ + tty_keys_send_theme_updates(tty); goto partial_key; } @@ -1679,3 +1686,16 @@ tty_keys_colours(struct tty *tty, const char *buf, size_t len, size_t *size, return (0); } + +/* Update theme in every pane for client's session. */ +static void +tty_keys_send_theme_updates(struct tty *tty) +{ + struct window_pane *wp; + struct winlink *wl; + + RB_FOREACH(wl, winlinks, &tty->client->session->windows) { + TAILQ_FOREACH(wp, &wl->window->panes, entry) + window_pane_send_theme_update(wp); + } +} diff --git a/tty.c b/tty.c index ecca6cd9..4290f66b 100644 --- a/tty.c +++ b/tty.c @@ -356,6 +356,11 @@ tty_start_tty(struct tty *tty) if (tty_term_has(tty->term, TTYC_ENBP)) tty_putcode(tty, TTYC_ENBP); + if (tty->term->flags & TERM_VT100LIKE) { + tty_puts(tty, "\033[?2031h"); /* Subscribe for theme updates. */ + tty_puts(tty, "\033[?996n"); /* Request theme right away. */ + } + evtimer_set(&tty->start_timer, tty_start_timer_callback, tty); evtimer_add(&tty->start_timer, &tv); @@ -468,6 +473,9 @@ tty_stop_tty(struct tty *tty) tty_raw(tty, tty_term_string(tty->term, TTYC_DSMG)); tty_raw(tty, tty_term_string(tty->term, TTYC_RMCUP)); + if (tty->term->flags & TERM_VT100LIKE) + tty_raw(tty, "\033[?2031l"); + setblocking(c->fd, 1); } diff --git a/window.c b/window.c index 11a00172..6e6990e2 100644 --- a/window.c +++ b/window.c @@ -1756,3 +1756,157 @@ window_pane_show_scrollbar(struct window_pane *wp, int sb_option) return (1); return (0); } + +int +window_pane_get_bg(struct window_pane *wp) +{ + int c; + struct grid_cell defaults; + + c = window_pane_get_bg_control_client(wp); + if (c == -1) { + tty_default_colours(&defaults, wp); + if (COLOUR_DEFAULT(defaults.bg)) + c = window_get_bg_client(wp); + else + c = defaults.bg; + } + return (c); +} + +/* Get a client with a background for the pane. */ +int +window_get_bg_client(struct window_pane *wp) +{ + struct window *w = wp->window; + struct client *loop; + + TAILQ_FOREACH(loop, &clients, entry) { + if (loop->flags & CLIENT_UNATTACHEDFLAGS) + continue; + if (loop->session == NULL || !session_has(loop->session, w)) + continue; + if (loop->tty.bg == -1) + continue; + return (loop->tty.bg); + } + return (-1); +} + +/* + * If any control mode client exists that has provided a bg color, return it. + * Otherwise, return -1. + */ +int +window_pane_get_bg_control_client(struct window_pane *wp) +{ + struct client *c; + + if (wp->control_bg == -1) + return (-1); + + TAILQ_FOREACH(c, &clients, entry) { + if (c->flags & CLIENT_CONTROL) + return (wp->control_bg); + } + return (-1); +} + +/* + * Get a client with a foreground for the pane. There isn't much to choose + * between them so just use the first. + */ +int +window_pane_get_fg(struct window_pane *wp) +{ + struct window *w = wp->window; + struct client *loop; + + TAILQ_FOREACH(loop, &clients, entry) { + if (loop->flags & CLIENT_UNATTACHEDFLAGS) + continue; + if (loop->session == NULL || !session_has(loop->session, w)) + continue; + if (loop->tty.fg == -1) + continue; + return (loop->tty.fg); + } + return (-1); +} + +/* + * If any control mode client exists that has provided a fg color, return it. + * Otherwise, return -1. + */ +int +window_pane_get_fg_control_client(struct window_pane *wp) +{ + struct client *c; + + if (wp->control_fg == -1) + return (-1); + + TAILQ_FOREACH(c, &clients, entry) { + if (c->flags & CLIENT_CONTROL) + return (wp->control_fg); + } + return (-1); +} + +enum client_theme +window_pane_get_theme(struct window_pane *wp) +{ + struct window *w = wp->window; + struct client *loop; + enum client_theme theme; + int found_light = 0, found_dark = 0; + + /* + * Derive theme from pane background color, if it's not the default + * colour. + */ + theme = colour_totheme(window_pane_get_bg(wp)); + if (theme != THEME_UNKNOWN) + return (theme); + + /* Try to find a client that has a theme. */ + TAILQ_FOREACH(loop, &clients, entry) { + if (loop->flags & CLIENT_UNATTACHEDFLAGS) + continue; + if (loop->session == NULL || !session_has(loop->session, w)) + continue; + switch (loop->theme) { + case THEME_LIGHT: + found_light = 1; + break; + case THEME_DARK: + found_dark = 1; + break; + case THEME_UNKNOWN: + break; + } + } + + if (found_dark && !found_light) + return (THEME_DARK); + if (found_light && !found_dark) + return (THEME_LIGHT); + return (THEME_UNKNOWN); +} + +void +window_pane_send_theme_update(struct window_pane *wp) +{ + if (~wp->screen->mode & MODE_THEME_UPDATES) + return; + switch (window_pane_get_theme(wp)) { + case THEME_LIGHT: + input_key_pane(wp, KEYC_REPORT_LIGHT_THEME, NULL); + break; + case THEME_DARK: + input_key_pane(wp, KEYC_REPORT_LIGHT_THEME, NULL); + break; + case THEME_UNKNOWN: + break; + } +}