terminal: add visibility reports

Allow terminal applications to detect when their view is not observable so they can avoid unnecessary rendering work.

Implement mode 2033 and the visibility status query, and report changes from tab, pane, window, and platform occlusion state.
This commit is contained in:
Tim Culverhouse 2026-07-28 14:22:30 -05:00
parent c04ce11a74
commit 365f1a6b37
No known key found for this signature in database
9 changed files with 125 additions and 3 deletions

View file

@ -193,6 +193,8 @@ Detailed list of changes
- Desktop file chooser integration: when the specified directory does not exist, open file chooser at home directory (:iss:`10292`)
- Add support for `terminal visibility reports <https://rockorager.dev/misc/visibility-reports/>`__
0.48.1 [2026-07-24]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View file

@ -334,6 +334,7 @@ window_occlusion_callback(GLFWwindow *window, bool occluded) {
if (!set_callback_window(window)) return;
debug("OSWindow %llu occlusion state changed, occluded: %d\n", global_state.callback_os_window->id, occluded);
if (!occluded) global_state.check_for_active_animated_images = true;
update_os_window_visibility_reports(global_state.callback_os_window);
request_tick_callback();
global_state.callback_os_window = NULL;
}
@ -342,6 +343,7 @@ static void
window_iconify_callback(GLFWwindow *window, int iconified) {
if (!set_callback_window(window)) return;
if (!iconified) global_state.check_for_active_animated_images = true;
update_os_window_visibility_reports(global_state.callback_os_window);
request_tick_callback();
global_state.callback_os_window = NULL;
}
@ -633,6 +635,7 @@ set_os_window_visibility(OSWindow *w, int set_visible, bool move_to_active_scree
w->keep_rendering_till_swap = 256; // try this many times
request_tick_callback();
} else glfwHideWindow(w->handle);
update_os_window_visibility_reports(w);
}
static void
@ -1456,6 +1459,7 @@ change_state_for_os_window(OSWindow *w, int state) {
case WINDOW_HIDDEN:
glfwHideWindow(w->handle); break;
}
update_os_window_visibility_reports(w);
}
#ifdef __APPLE__
@ -2562,15 +2566,19 @@ wakeup_main_loop(void) {
}
bool
should_os_window_be_rendered(OSWindow* w) {
is_os_window_potentially_visible(OSWindow* w) {
return (
glfwGetWindowAttrib(w->handle, GLFW_ICONIFIED)
|| !glfwGetWindowAttrib(w->handle, GLFW_VISIBLE)
|| glfwGetWindowAttrib(w->handle, GLFW_OCCLUDED)
|| !glfwAreSwapsAllowed(w->handle)
) ? false : true;
}
bool
should_os_window_be_rendered(OSWindow* w) {
return is_os_window_potentially_visible(w) && glfwAreSwapsAllowed(w->handle);
}
static PyObject*
primary_monitor_size(PYNOARG) {
GLFWmonitor* monitor = glfwGetPrimaryMonitor();

View file

@ -88,6 +88,9 @@
// Notification of color preference change
#define COLOR_PREFERENCE_NOTIFICATION (2031 << 5)
// Terminal visibility reports
#define VISIBILITY_REPORTS (2033 << 5)
// In-band resize notification mode
#define INBAND_RESIZE_NOTIFICATION (2048 << 5)

View file

@ -1524,6 +1524,27 @@ write_escape_code_to_child(Screen *self, unsigned char which, const char *data)
return written;
}
static void
send_visibility_report(Screen *self, bool potentially_visible) {
write_escape_code_to_child(self, ESC_CSI, potentially_visible ? "?999;1n" : "?999;2n");
}
void
screen_visibility_changed(Screen *self, bool potentially_visible) {
const uint8_t state = potentially_visible ? 1 : 2;
if (self->visibility_state != state) {
self->visibility_state = state;
if (self->modes.mVISIBILITY_REPORTS) send_visibility_report(self, potentially_visible);
}
}
static void
report_current_visibility(Screen *self) {
const bool potentially_visible = is_kitty_window_visible(self->window_id);
self->visibility_state = potentially_visible ? 1 : 2;
send_visibility_report(self, potentially_visible);
}
static bool
write_escape_code_to_child_python(Screen *self, unsigned char which, PyObject *data) {
bool written = false;
@ -1799,6 +1820,10 @@ set_mode_from_const(Screen *self, unsigned int mode, bool val) {
self->modes.mINBAND_RESIZE_NOTIFICATION = val;
if (val) CALLBACK("notify_child_of_resize", NULL);
break;
case VISIBILITY_REPORTS:
self->modes.mVISIBILITY_REPORTS = val;
if (val) report_current_visibility(self);
break;
default:
private = mode >= 1 << 5;
if (private) mode >>= 5;
@ -2341,6 +2366,7 @@ copy_specific_mode(Screen *self, unsigned int mode, const ScreenModes *src, Scre
SIMPLE_MODE(COLOR_PREFERENCE_NOTIFICATION)
SIMPLE_MODE(PASTE_EVENTS)
SIMPLE_MODE(INBAND_RESIZE_NOTIFICATION)
SIMPLE_MODE(VISIBILITY_REPORTS)
SIMPLE_MODE(DECCKM)
SIMPLE_MODE(DECTCEM)
SIMPLE_MODE(DECAWM)
@ -2383,6 +2409,7 @@ copy_specific_modes(Screen *self, const ScreenModes *src, ScreenModes *dest) {
copy_specific_mode(self, FOCUS_TRACKING, src, dest);
copy_specific_mode(self, COLOR_PREFERENCE_NOTIFICATION, src, dest);
copy_specific_mode(self, INBAND_RESIZE_NOTIFICATION, src, dest);
copy_specific_mode(self, VISIBILITY_REPORTS, src, dest);
copy_specific_mode(self, PASTE_EVENTS, src, dest);
copy_specific_mode(self, DECCKM, src, dest);
copy_specific_mode(self, DECTCEM, src, dest);
@ -2937,6 +2964,9 @@ report_device_status(Screen *self, unsigned int which, bool private) {
if (private) {
CALLBACK("report_color_scheme_preference", NULL);
} break;
case 998: // https://rockorager.dev/misc/visibility-reports/
if (private) report_current_visibility(self);
break;
}
}
@ -2961,6 +2991,7 @@ report_mode_status(Screen *self, unsigned int which, bool private) {
KNOWN_MODE(BRACKETED_PASTE);
KNOWN_MODE(FOCUS_TRACKING);
KNOWN_MODE(COLOR_PREFERENCE_NOTIFICATION);
KNOWN_MODE(VISIBILITY_REPORTS);
KNOWN_MODE(INBAND_RESIZE_NOTIFICATION);
KNOWN_MODE(PASTE_EVENTS);
#undef KNOWN_MODE

View file

@ -26,7 +26,7 @@ typedef struct DnDCommand {
typedef struct {
bool mLNM, mIRM, mDECTCEM, mDECSCNM, mDECOM, mDECAWM, mDECCOLM, mDECARM, mDECCKM, mCOLOR_PREFERENCE_NOTIFICATION,
mBRACKETED_PASTE, mFOCUS_TRACKING, mDECSACE, mHANDLE_TERMIOS_SIGNALS, mINBAND_RESIZE_NOTIFICATION,
mPASTE_EVENTS;
mPASTE_EVENTS, mVISIBILITY_REPORTS;
MouseTrackingMode mouse_tracking_mode;
MouseTrackingProtocol mouse_tracking_protocol;
} ScreenModes;
@ -150,6 +150,7 @@ typedef struct {
DisableLigature disable_ligatures;
PyObject *marker;
bool has_focus;
uint8_t visibility_state; // 0 = unknown, 1 = potentially visible, 2 = not visible
bool has_activity_since_last_focus;
hyperlink_id_type active_hyperlink_id;
HYPERLINK_POOL_HANDLE hyperlink_pool;
@ -225,6 +226,7 @@ void screen_restore_mode(Screen *, unsigned int);
void screen_save_modes(Screen *);
void screen_save_mode(Screen *, unsigned int);
bool write_escape_code_to_child(Screen *self, unsigned char which, const char *data);
void screen_visibility_changed(Screen *self, bool potentially_visible);
void screen_cursor_position(Screen*, unsigned int, unsigned int);
void screen_cursor_move(Screen *self, unsigned int count/*=1*/, int move_direction/*=-1*/, bool allow_move_to_previous_line);
void screen_erase_in_line(Screen *, unsigned int, bool);

View file

@ -165,6 +165,39 @@ window_for_window_id(id_type kitty_window_id) {
return NULL;
}
bool
is_kitty_window_visible(id_type kitty_window_id) {
for (size_t i = 0; i < global_state.num_os_windows; i++) {
OSWindow *os_window = global_state.os_windows + i;
for (size_t t = 0; t < os_window->num_tabs; t++) {
Tab *tab = os_window->tabs + t;
for (size_t w = 0; w < tab->num_windows; w++) {
Window *window = tab->windows + w;
if (window->id == kitty_window_id) {
return is_os_window_potentially_visible(os_window) && t == os_window->active_tab && window->visible;
}
}
}
}
// If the view cannot be found, its visibility is unknown and must be
// treated conservatively as potentially visible.
return true;
}
void
update_os_window_visibility_reports(OSWindow *os_window) {
const bool os_window_is_visible = is_os_window_potentially_visible(os_window);
for (size_t t = 0; t < os_window->num_tabs; t++) {
Tab *tab = os_window->tabs + t;
for (size_t w = 0; w < tab->num_windows; w++) {
Window *window = tab->windows + w;
if (window->render_data.screen) {
screen_visibility_changed(window->render_data.screen, os_window_is_visible && t == os_window->active_tab && window->visible);
}
}
}
}
static void
free_bgimage_bitmap(BackgroundImage *bgimage) {
if (!bgimage->bitmap) return;
@ -502,6 +535,10 @@ attach_window(id_type os_window_id, id_type tab_id, id_type id) {
) resize_screen(osw, w->render_data.screen, true);
else screen_dirty_sprite_positions(w->render_data.screen);
w->render_data.screen->reload_all_gpu_data = true;
screen_visibility_changed(
w->render_data.screen,
is_os_window_potentially_visible(osw) && t == osw->active_tab && w->visible
);
break;
}
}
@ -585,6 +622,7 @@ set_active_tab(id_type os_window_id, unsigned int idx) {
WITH_OS_WINDOW(os_window_id)
os_window->active_tab = idx;
os_window->needs_render = true;
update_os_window_visibility_reports(os_window);
END_WITH_OS_WINDOW
}
@ -1203,6 +1241,12 @@ PYWRAP1(update_window_visibility) {
bool was_visible = window->visible & 1;
window->visible = visible & 1;
if (!was_visible && window->visible) global_state.check_for_active_animated_images = true;
if (window->render_data.screen) {
screen_visibility_changed(
window->render_data.screen,
is_os_window_potentially_visible(osw) && t == osw->active_tab && window->visible
);
}
END_WITH_WINDOW;
Py_RETURN_NONE;
}

View file

@ -566,6 +566,9 @@ void update_os_window_references(void);
void mark_os_window_for_close(OSWindow* w, CloseRequest cr);
void update_os_window_viewport(OSWindow *window, bool notify_boss);
bool should_os_window_be_rendered(OSWindow* w);
bool is_os_window_potentially_visible(OSWindow* w);
bool is_kitty_window_visible(id_type kitty_window_id);
void update_os_window_visibility_reports(OSWindow *w);
void wakeup_main_loop(void);
bool make_window_context_current(id_type);
void hide_mouse(OSWindow *w);

View file

@ -799,6 +799,17 @@ class TestParser(BaseTest):
pb('\033[?2026$p', ('report_mode_status', 2026, 1))
self.ae(c.wtcbuf, b'\x1b[?2026;2$y')
c.clear()
pb('\033[?998n', ('report_device_status', 998, 1))
self.ae(c.wtcbuf, b'\x1b[?999;1n')
c.clear()
pb('\033[?2033h', ('screen_set_mode', 2033, 1))
self.ae(c.wtcbuf, b'\x1b[?999;1n')
c.clear()
pb('\033[?2033$p', ('report_mode_status', 2033, 1))
self.ae(c.wtcbuf, b'\x1b[?2033;1$y')
pb('\033[?2033l', ('screen_reset_mode', 2033, 1))
def test_csi_code_rep(self):
s = self.create_screen(8)
pb = partial(self.parse_bytes_dump, s)

View file

@ -404,6 +404,24 @@ class TestScreen(BaseTest):
parse_bytes(s, b'\x1b[?2048h') # ]
self.ae(c.num_of_resize_events, 2)
def test_visibility_reports(self):
s = self.create_screen()
c = s.callbacks
parse_bytes(s, b'\x1b[?2033$p') # ]
self.ae(c.wtcbuf, b'\x1b[?2033;2$y') # ]
c.clear()
parse_bytes(s, b'\x1b[?998n') # ]
self.ae(c.wtcbuf, b'\x1b[?999;1n') # ]
c.clear()
parse_bytes(s, b'\x1b[?2033h\x1b[?2033$p') # ]]
self.ae(c.wtcbuf, b'\x1b[?999;1n\x1b[?2033;1$y') # ]]
c.clear()
parse_bytes(s, b'\x1b[?2033h') # ]
self.ae(c.wtcbuf, b'\x1b[?999;1n') # ]
c.clear()
parse_bytes(s, b'\x1b[?2033l\x1b[?2033$p') # ]]
self.ae(c.wtcbuf, b'\x1b[?2033;2$y') # ]
def test_da1(self):
s = self.create_screen()
parse_bytes(s, b'\x1b[c\x1b[0c') # ]]