aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/news.txt3
-rw-r--r--runtime/doc/options.txt8
-rw-r--r--runtime/lua/vim/_meta/options.lua9
-rw-r--r--src/nvim/option_vars.h1
-rw-r--r--src/nvim/options.lua15
-rw-r--r--src/nvim/tui/input.c15
-rw-r--r--src/nvim/tui/tui.c68
-rw-r--r--src/nvim/tui/tui.h12
-rw-r--r--test/functional/ui/options_spec.lua1
9 files changed, 130 insertions, 2 deletions
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index ee48bddc4d..ae97772b66 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -211,6 +211,9 @@ The following new APIs and features were added.
• A clipboard provider which uses OSC 52 to copy the selection to the system
clipboard is now bundled by default. |clipboard-osc52|
+• The 'termsync' option asks the terminal emulator to buffer screen updates
+ until the redraw cycle is complete. Requires support from the terminal.
+
==============================================================================
CHANGED FEATURES *news-changed*
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index 603b777f55..07326c8c13 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -6523,6 +6523,14 @@ A jump table for the options with a short description can be found at |Q_op|.
C1 Control characters 0x80...0x9F
+ *'termsync'* *'notermsync'*
+'termsync' boolean (default on)
+ global
+ If the host terminal supports it, buffer all screen updates
+ made during a redraw cycle so that each screen is displayed in
+ the terminal all at once. This can prevent tearing or flickering
+ when the terminal updates faster than Nvim can redraw.
+
*'textwidth'* *'tw'*
'textwidth' 'tw' number (default 0)
local to buffer
diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua
index 0ef0fece90..19ae786177 100644
--- a/runtime/lua/vim/_meta/options.lua
+++ b/runtime/lua/vim/_meta/options.lua
@@ -6978,6 +6978,15 @@ vim.o.tpf = vim.o.termpastefilter
vim.go.termpastefilter = vim.o.termpastefilter
vim.go.tpf = vim.go.termpastefilter
+--- If the host terminal supports it, buffer all screen updates
+--- made during a redraw cycle so that each screen is displayed in
+--- the terminal all at once. This can prevent tearing or flickering
+--- when the terminal updates faster than Nvim can redraw.
+---
+--- @type boolean
+vim.o.termsync = true
+vim.go.termsync = vim.o.termsync
+
--- Maximum width of text that is being inserted. A longer line will be
--- broken after white space to get this width. A zero value disables
--- this.
diff --git a/src/nvim/option_vars.h b/src/nvim/option_vars.h
index f8f6fa5670..f0c752a2b1 100644
--- a/src/nvim/option_vars.h
+++ b/src/nvim/option_vars.h
@@ -737,6 +737,7 @@ EXTERN OptInt p_uc; ///< 'updatecount'
EXTERN OptInt p_ut; ///< 'updatetime'
EXTERN char *p_shada; ///< 'shada'
EXTERN char *p_shadafile; ///< 'shadafile'
+EXTERN int p_termsync; ///< 'termsync'
EXTERN char *p_vsts; ///< 'varsofttabstop'
EXTERN char *p_vts; ///< 'vartabstop'
EXTERN char *p_vdir; ///< 'viewdir'
diff --git a/src/nvim/options.lua b/src/nvim/options.lua
index 373dc3c460..9ca1396753 100644
--- a/src/nvim/options.lua
+++ b/src/nvim/options.lua
@@ -8810,6 +8810,21 @@ return {
varname = 'p_tpf',
},
{
+ defaults = { if_true = true },
+ desc = [=[
+ If the host terminal supports it, buffer all screen updates
+ made during a redraw cycle so that each screen is displayed in
+ the terminal all at once. This can prevent tearing or flickering
+ when the terminal updates faster than Nvim can redraw.
+ ]=],
+ full_name = 'termsync',
+ redraw = { 'ui_option' },
+ scope = { 'global' },
+ short_desc = N_('synchronize redraw output with the host terminal'),
+ type = 'bool',
+ varname = 'p_termsync',
+ },
+ {
defaults = { if_true = false },
full_name = 'terse',
scope = { 'global' },
diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c
index 7dce8473d5..db1281a0b5 100644
--- a/src/nvim/tui/input.c
+++ b/src/nvim/tui/input.c
@@ -480,6 +480,8 @@ static void tk_getkeys(TermInput *input, bool force)
}
} else if (key.type == TERMKEY_TYPE_OSC) {
handle_osc_event(input, &key);
+ } else if (key.type == TERMKEY_TYPE_MODEREPORT) {
+ handle_modereport(input, &key);
}
}
@@ -579,9 +581,8 @@ static HandleState handle_bracketed_paste(TermInput *input)
}
static void handle_osc_event(TermInput *input, const TermKeyKey *key)
+ FUNC_ATTR_NONNULL_ALL
{
- assert(input);
-
const char *str = NULL;
if (termkey_interpret_string(input->tk, key, &str) == TERMKEY_RES_KEY) {
assert(str != NULL);
@@ -601,6 +602,16 @@ static void handle_osc_event(TermInput *input, const TermKeyKey *key)
}
}
+static void handle_modereport(TermInput *input, const TermKeyKey *key)
+ FUNC_ATTR_NONNULL_ALL
+{
+ // termkey_interpret_modereport incorrectly sign extends the mode so we parse the response
+ // ourselves
+ int mode = (uint8_t)key->code.mouse[1] << 8 | (uint8_t)key->code.mouse[2];
+ TerminalModeState value = (uint8_t)key->code.mouse[3];
+ tui_dec_report_mode(input->tui_data, (TerminalDecMode)mode, value);
+}
+
static void handle_raw_buffer(TermInput *input, bool force)
{
HandleState is_paste = kNotApplicable;
diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c
index 8aab4d836c..aff7b100d8 100644
--- a/src/nvim/tui/tui.c
+++ b/src/nvim/tui/tui.c
@@ -104,6 +104,7 @@ struct TUIData {
bool mouse_enabled;
bool mouse_move_enabled;
bool title_enabled;
+ bool sync_output;
bool busy, is_invisible, want_invisible;
bool cork, overflow;
bool set_cursor_color_as_str;
@@ -137,6 +138,7 @@ struct TUIData {
int set_underline_color;
int enable_extended_keys, disable_extended_keys;
int get_extkeys;
+ int sync;
} unibi_ext;
char *space_buf;
bool stopped;
@@ -217,6 +219,41 @@ static size_t unibi_pre_fmt_str(TUIData *tui, unsigned unibi_index, char *buf, s
return unibi_run(str, tui->params, buf, len);
}
+/// Request the terminal's DEC mode (DECRQM).
+///
+/// @see handle_modereport
+static void tui_dec_request_mode(TUIData *tui, TerminalDecMode mode)
+{
+ // 5 bytes for \x1b[?$p, 1 byte for null terminator, 6 bytes for mode digits (more than enough)
+ char buf[12];
+ int len = snprintf(buf, sizeof(buf), "\x1b[?%d$p", (int)mode);
+ assert((len > 0) && (len < (int)sizeof(buf)));
+ out(tui, buf, (size_t)len);
+}
+
+/// Handle a DECRPM response from the terminal.
+void tui_dec_report_mode(TUIData *tui, TerminalDecMode mode, TerminalModeState state)
+{
+ assert(tui);
+ switch (state) {
+ case kTerminalModeNotRecognized:
+ case kTerminalModePermanentlySet:
+ case kTerminalModePermanentlyReset:
+ // If the mode is not recognized, or if the terminal emulator does not allow it to be changed,
+ // then there is nothing to do
+ break;
+ case kTerminalModeSet:
+ case kTerminalModeReset:
+ // The terminal supports changing the given mode
+ switch (mode) {
+ case kDecModeSynchronizedOutput:
+ // Ref: https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036
+ tui->unibi_ext.sync = (int)unibi_add_ext_str(tui->ut, "Sync",
+ "\x1b[?2026%?%p1%{1}%-%tl%eh%;");
+ }
+ }
+}
+
static void terminfo_start(TUIData *tui)
{
tui->scroll_region_is_full_screen = true;
@@ -253,6 +290,7 @@ static void terminfo_start(TUIData *tui)
tui->unibi_ext.enable_extended_keys = -1;
tui->unibi_ext.disable_extended_keys = -1;
tui->unibi_ext.get_extkeys = -1;
+ tui->unibi_ext.sync = -1;
tui->out_fd = STDOUT_FILENO;
tui->out_isatty = os_isatty(tui->out_fd);
tui->input.tui_data = tui;
@@ -329,6 +367,11 @@ static void terminfo_start(TUIData *tui)
// Enable bracketed paste
unibi_out_ext(tui, tui->unibi_ext.enable_bracketed_paste);
+ // Query support for mode 2026 (Synchronized Output). Some terminals also
+ // support an older DCS sequence for synchronized output, but we will only use
+ // mode 2026
+ tui_dec_request_mode(tui, kDecModeSynchronizedOutput);
+
// Query the terminal to see if it supports CSI u
tui->input.waiting_for_csiu_response = 5;
unibi_out_ext(tui, tui->unibi_ext.get_extkeys);
@@ -395,6 +438,11 @@ static void terminfo_stop(TUIData *tui)
unibi_out_ext(tui, tui->unibi_ext.disable_bracketed_paste);
// Disable focus reporting
unibi_out_ext(tui, tui->unibi_ext.disable_focus_reporting);
+
+ // Disable synchronized output
+ UNIBI_SET_NUM_VAR(tui->params[0], 0);
+ unibi_out_ext(tui, tui->unibi_ext.sync);
+
flush_buf(tui);
uv_tty_reset_mode();
uv_close((uv_handle_t *)&tui->output_handle, NULL);
@@ -1257,6 +1305,20 @@ void tui_default_colors_set(TUIData *tui, Integer rgb_fg, Integer rgb_bg, Intege
invalidate(tui, 0, tui->grid.height, 0, tui->grid.width);
}
+/// Enable synchronized output. When enabled, the terminal emulator will preserve the last rendered
+/// state on subsequent re-renders. It will continue to process incoming events. When synchronized
+/// mode is disabled again the emulator renders using the most recent state. This avoids tearing
+/// when the terminal updates the screen faster than Nvim can redraw it.
+static void tui_sync_output(TUIData *tui, bool enable)
+{
+ if (!tui->sync_output) {
+ return;
+ }
+
+ UNIBI_SET_NUM_VAR(tui->params[0], enable ? 1 : 0);
+ unibi_out_ext(tui, tui->unibi_ext.sync);
+}
+
void tui_flush(TUIData *tui)
{
UGrid *grid = &tui->grid;
@@ -1273,6 +1335,8 @@ void tui_flush(TUIData *tui)
tui_busy_stop(tui); // avoid hidden cursor
}
+ tui_sync_output(tui, true);
+
while (kv_size(tui->invalid_regions)) {
Rect r = kv_pop(tui->invalid_regions);
assert(r.bot <= grid->height && r.right <= grid->width);
@@ -1300,6 +1364,8 @@ void tui_flush(TUIData *tui)
cursor_goto(tui, tui->row, tui->col);
+ tui_sync_output(tui, false);
+
flush_buf(tui);
}
@@ -1449,6 +1515,8 @@ void tui_option_set(TUIData *tui, String name, Object value)
tui->input.ttimeoutlen = (OptInt)value.data.integer;
} else if (strequal(name.data, "verbose")) {
tui->verbose = value.data.integer;
+ } else if (strequal(name.data, "termsync")) {
+ tui->sync_output = value.data.boolean;
}
}
diff --git a/src/nvim/tui/tui.h b/src/nvim/tui/tui.h
index c89053d053..29afdef4de 100644
--- a/src/nvim/tui/tui.h
+++ b/src/nvim/tui/tui.h
@@ -5,6 +5,18 @@
typedef struct TUIData TUIData;
+typedef enum {
+ kDecModeSynchronizedOutput = 2026,
+} TerminalDecMode;
+
+typedef enum {
+ kTerminalModeNotRecognized = 0,
+ kTerminalModeSet = 1,
+ kTerminalModeReset = 2,
+ kTerminalModePermanentlySet = 3,
+ kTerminalModePermanentlyReset = 4,
+} TerminalModeState;
+
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "tui/tui.h.generated.h"
#endif
diff --git a/test/functional/ui/options_spec.lua b/test/functional/ui/options_spec.lua
index 6af1820430..2c649709c6 100644
--- a/test/functional/ui/options_spec.lua
+++ b/test/functional/ui/options_spec.lua
@@ -23,6 +23,7 @@ describe('UI receives option updates', function()
mousemoveevent=false,
showtabline=1,
termguicolors=false,
+ termsync=true,
ttimeout=true,
ttimeoutlen=50,
verbose=0,