Merge branch 'visibility-reports' of https://github.com/rockorager/kitty

This commit is contained in:
Kovid Goyal 2026-07-29 06:33:11 +05:30
commit 7e647774d1
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
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') # ]]