Panel kitten: Add option to hide panel on focus loss

This commit is contained in:
Kovid Goyal
2025-04-30 07:46:41 +05:30
parent df052edae8
commit edcfd17ca9
10 changed files with 46 additions and 21 deletions

2
glfw/glfw3.h vendored
View File

@@ -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;

View File

@@ -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 '')

View File

@@ -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`)
}

View File

@@ -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:

View File

@@ -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']

2
kitty/glfw-wrapper.h generated
View File

@@ -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;

View File

@@ -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),

View File

@@ -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}

View File

@@ -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;

View File

@@ -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]: