From edcfd17ca98cac90add3277cc7b9b44e73b9492a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 30 Apr 2025 07:46:41 +0530 Subject: [PATCH] Panel kitten: Add option to hide panel on focus loss --- glfw/glfw3.h | 2 +- kittens/panel/main.py | 3 +++ kittens/quick_access_terminal/main.go | 3 +++ kittens/quick_access_terminal/main.py | 15 ++++++++++++-- kitty/fast_data_types.pyi | 1 - kitty/glfw-wrapper.h | 2 +- kitty/glfw.c | 29 ++++++++++++++------------- kitty/simple_cli_definitions.py | 8 ++++++++ kitty/state.h | 3 +-- kitty/types.py | 1 + 10 files changed, 46 insertions(+), 21 deletions(-) diff --git a/glfw/glfw3.h b/glfw/glfw3.h index feb8774bd..e3ac36d4f 100644 --- a/glfw/glfw3.h +++ b/glfw/glfw3.h @@ -1314,7 +1314,7 @@ typedef struct GLFWLayerShellConfig { unsigned x_size_in_cells, x_size_in_pixels; unsigned y_size_in_cells, y_size_in_pixels; int requested_top_margin, requested_left_margin, requested_bottom_margin, requested_right_margin; - int requested_exclusive_zone; + int requested_exclusive_zone, hide_on_focus_loss; unsigned override_exclusive_zone; void (*size_callback)(GLFWwindow *window, float xscale, float yscale, unsigned *cell_width, unsigned *cell_height, double *left_edge_spacing, double *top_edge_spacing, double *right_edge_spacing, double *bottom_edge_spacing); struct { float xscale, yscale; } expected; diff --git a/kittens/panel/main.py b/kittens/panel/main.py index 35bc7c4a9..8e4f73cc4 100644 --- a/kittens/panel/main.py +++ b/kittens/panel/main.py @@ -152,6 +152,8 @@ def layer_shell_config(opts: PanelCLIOptions) -> LayerShellConfig: focus_policy = { 'not-allowed': GLFW_FOCUS_NOT_ALLOWED, 'exclusive': GLFW_FOCUS_EXCLUSIVE, 'on-demand': GLFW_FOCUS_ON_DEMAND }.get(opts.focus_policy, GLFW_FOCUS_NOT_ALLOWED) + if opts.hide_on_focus_loss: + focus_policy = GLFW_FOCUS_ON_DEMAND x, y = dual_distance(opts.columns, min_cell_value_if_no_pixels=1), dual_distance(opts.lines, min_cell_value_if_no_pixels=1) return LayerShellConfig(type=ltype, edge=edge, @@ -164,6 +166,7 @@ def layer_shell_config(opts: PanelCLIOptions) -> LayerShellConfig: focus_policy=focus_policy, requested_exclusive_zone=opts.exclusive_zone, override_exclusive_zone=opts.override_exclusive_zone, + hide_on_focus_loss=opts.hide_on_focus_loss, output_name=opts.output_name or '') diff --git a/kittens/quick_access_terminal/main.go b/kittens/quick_access_terminal/main.go index dc65350a3..0927aec6a 100644 --- a/kittens/quick_access_terminal/main.go +++ b/kittens/quick_access_terminal/main.go @@ -71,6 +71,9 @@ func main(cmd *cli.Command, opts *Options, args []string) (rc int, err error) { if conf.Start_as_hidden { argv = append(argv, `--start-as-hidden`) } + if conf.Hide_on_focus_loss { + argv = append(argv, `--hide-on-focus-loss`) + } if opts.Detach { argv = append(argv, `--detach`) } diff --git a/kittens/quick_access_terminal/main.py b/kittens/quick_access_terminal/main.py index 261850b9b..2020fd579 100644 --- a/kittens/quick_access_terminal/main.py +++ b/kittens/quick_access_terminal/main.py @@ -40,6 +40,11 @@ option of the same name, it is present here as it has a different default value for the quick access terminal. ''') +opt('hide_on_focus_loss', 'no', option_type='to_bool', long_text=''' +Hide the window when it loses keyboard focus automatically. Using this option +will force :opt:`focus_policy` to :code:`on-demand`. +''') + opt('margin_left', '0', option_type='int', long_text='Set the left margin for the window, in pixels. Has no effect for windows on the right edge of the screen.') @@ -67,8 +72,14 @@ opt('app_id', f'{appname}-quick-access', opt('start_as_hidden', 'no', option_type='to_bool', long_text='Whether to start the quick access terminal hidden. Useful if you are starting it as part of system startup.') -opt('focus_policy', 'exclusive', choices=('exclusive', 'on-demand'), - long_text='How to manage window focus.') +opt('focus_policy', 'exclusive', choices=('exclusive', 'on-demand'), long_text=''' +How to manage window focus. A value of :code:`exclusive` means prevent other windows from getting focus. +However, whether this works is entirely dependent on the compositor/desktop environment. +It does not have any effect on macOS and KDE, for example. Note that on sway using :code:`on-demand` means +the compositor will not focus the window when it appears until you click on it, which is why the default is set +to :code:`exclusive`. +''') + def options_spec() -> str: diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index cc6088f8d..9f2505689 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1740,7 +1740,6 @@ def set_redirect_keys_to_overlay(os_window_id: int, tab_id: int, window_id: int, def buffer_keys_in_window(os_window_id: int, tab_id: int, window_id: int, enabled: bool = True) -> bool: ... def sprite_idx_to_pos(idx: int, xnum: int, ynum: int) -> tuple[int, int, int]: ... def render_box_char(ch: int, width: int, height: int, scale: float = 1.0, dpi_x: float = 96.0, dpi_y: float = 96.0) -> bytes: ... -def set_os_window_hide_on_focus_lost(os_window_id: int, hide: bool = True) -> bool: ... def run_at_exit_cleanup_functions() -> None: ... DecorationTypes = Literal[ 'curl', 'dashed', 'dotted', 'double', 'straight', 'strikethrough', 'beam_cursor', 'underline_cursor', 'hollow_cursor', 'missing'] diff --git a/kitty/glfw-wrapper.h b/kitty/glfw-wrapper.h index d094b1e94..fc87512b5 100644 --- a/kitty/glfw-wrapper.h +++ b/kitty/glfw-wrapper.h @@ -1052,7 +1052,7 @@ typedef struct GLFWLayerShellConfig { unsigned x_size_in_cells, x_size_in_pixels; unsigned y_size_in_cells, y_size_in_pixels; int requested_top_margin, requested_left_margin, requested_bottom_margin, requested_right_margin; - int requested_exclusive_zone; + int requested_exclusive_zone, hide_on_focus_loss; unsigned override_exclusive_zone; void (*size_callback)(GLFWwindow *window, float xscale, float yscale, unsigned *cell_width, unsigned *cell_height, double *left_edge_spacing, double *top_edge_spacing, double *right_edge_spacing, double *bottom_edge_spacing); struct { float xscale, yscale; } expected; diff --git a/kitty/glfw.c b/kitty/glfw.c index cbab2dc5b..761def363 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -135,6 +135,7 @@ set_layer_shell_config_for(OSWindow *w, GLFWLayerShellConfig *lsc) { lsc->related.background_opacity = w->background_opacity; lsc->related.background_blur = OPT(background_blur); lsc->related.color_space = OPT(macos_colorspace); + w->hide_on_focus_loss = lsc->hide_on_focus_loss; } return glfwSetLayerShellConfig(w->handle, lsc); } @@ -561,11 +562,18 @@ set_os_window_visibility(OSWindow *w, int set_visible) { } else glfwHideWindow(w->handle); } +static void +update_os_window_visibility_based_on_focus(id_type timer_id UNUSED, void*d) { + OSWindow * osw = os_window_for_id((uintptr_t)d); + if (osw && osw->hide_on_focus_loss && !osw->is_focused) set_os_window_visibility(osw, 0); +} + static void window_focus_callback(GLFWwindow *w, int focused) { if (!set_callback_window(w)) return; #define osw global_state.callback_os_window debug_input("\x1b[35mon_focus_change\x1b[m: window id: 0x%llu focused: %d\n", osw->id, focused); + bool focus_changed = osw->is_focused != focused; osw->is_focused = focused ? true : false; monotonic_t now = monotonic(); id_type wid = osw->id; @@ -591,8 +599,8 @@ window_focus_callback(GLFWwindow *w, int focused) { } } request_tick_callback(); - if (osw && osw->hide_on_focus_lost && osw->handle) { - if (glfwGetWindowAttrib(osw->handle, GLFW_VISIBLE)) set_os_window_visibility(osw, 0); + if (osw && osw->handle && !focused && focus_changed && osw->hide_on_focus_loss && glfwGetWindowAttrib(osw->handle, GLFW_VISIBLE)) { + add_main_loop_timer(0, false, update_os_window_visibility_based_on_focus, (void*)(uintptr_t)osw->id, NULL); } osw = NULL; #undef osw @@ -1216,6 +1224,7 @@ layer_shell_config_from_python(PyObject *p, GLFWLayerShellConfig *ans) { A(requested_right_margin, PyLong_Check, PyLong_AsLong); A(requested_exclusive_zone, PyLong_Check, PyLong_AsLong); A(override_exclusive_zone, PyBool_Check, PyLong_AsLong); + A(hide_on_focus_loss, PyBool_Check, PyLong_AsLong); #undef A #define A(attr) { \ RAII_PyObject(attr, PyObject_GetAttrString(p, #attr)); if (attr == NULL) return false; \ @@ -1413,7 +1422,10 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) { OSWindow *w = add_os_window(); w->handle = glfw_window; w->disallow_title_changes = disallow_override_title; - w->is_layer_shell = lsc != NULL; + if (lsc != NULL) { + w->is_layer_shell = true; + w->hide_on_focus_loss = lsc->hide_on_focus_loss; + } update_os_window_references(); if (!w->is_layer_shell || (global_state.is_apple && w->is_layer_shell && lsc->focus_policy == GLFW_FOCUS_EXCLUSIVE)) { for (size_t i = 0; i < global_state.num_os_windows; i++) { @@ -2513,23 +2525,12 @@ set_layer_shell_config(PyObject *self UNUSED, PyObject *args) { return Py_NewRef(set_layer_shell_config_for(window, &lsc) ? Py_True : Py_False); } -static PyObject* -set_os_window_hide_on_focus_lost(PyObject *self UNUSED, PyObject *args) { - unsigned long long wid; int val = 1; - if (!PyArg_ParseTuple(args, "K|p", &wid, &val)) return NULL; - OSWindow *window = os_window_for_id(wid); - if (!window) Py_RETURN_FALSE; - window->hide_on_focus_lost = val; - Py_RETURN_TRUE; -} - // Boilerplate {{{ static PyMethodDef module_methods[] = { METHODB(set_custom_cursor, METH_VARARGS), METHODB(is_css_pointer_name_valid, METH_O), - METHODB(set_os_window_hide_on_focus_lost, METH_O), METHODB(toggle_os_window_visibility, METH_VARARGS), METHODB(layer_shell_config_for_os_window, METH_O), METHODB(set_layer_shell_config, METH_VARARGS), diff --git a/kitty/simple_cli_definitions.py b/kitty/simple_cli_definitions.py index 126e2fb9c..8baa05039 100644 --- a/kitty/simple_cli_definitions.py +++ b/kitty/simple_cli_definitions.py @@ -654,9 +654,17 @@ choices=not-allowed,exclusive,on-demand default={focus_policy} On a Wayland compositor that supports the wlr layer shell protocol, specify the focus policy for keyboard interactivity with the panel. Please refer to the wlr layer shell protocol documentation for more details. +Note that different Wayland compositors behave very differently with :code:`exclusive`, your mileage may vary. On macOS, :code:`exclusive` and :code:`on-demand` are currently the same. Ignored on X11. +--hide-on-focus-loss +type=bool-set +Automatically hide the panel window when it loses focus. Using this option will force :option:`--focus-policy` +to :code:`on-demand`. Note that on Wayland, depending on the compositor, this can result in the window never +becoming visible. + + --exclusive-zone type=int default={exclusive_zone} diff --git a/kitty/state.h b/kitty/state.h index e8af9f845..2f7cd25a1 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -313,8 +313,7 @@ typedef struct { uint64_t render_calls; id_type last_focused_counter; CloseRequest close_request; - bool is_layer_shell; - bool hide_on_focus_lost; + bool is_layer_shell, hide_on_focus_loss; } OSWindow; diff --git a/kitty/types.py b/kitty/types.py index 8a2bc3382..68bf40a65 100644 --- a/kitty/types.py +++ b/kitty/types.py @@ -82,6 +82,7 @@ class LayerShellConfig(NamedTuple): requested_right_margin: int = 0 requested_exclusive_zone: int = -1 override_exclusive_zone: bool = False + hide_on_focus_loss: bool = False def mod_to_names(mods: int, has_kitty_mod: bool = False, kitty_mod: int = 0) -> Iterator[str]: