Remote control: Allow modifying desktop panels and showing/hiding OS Windows using the kitten @ resize-os-window command

Also move the visibility toggle debounce into C code with a per OS
Window timer.

Fixes #8550
This commit is contained in:
Kovid Goyal 2025-04-22 13:34:09 +05:30
parent c1a9873530
commit fc5fc7c9c4
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
8 changed files with 108 additions and 50 deletions

View file

@ -106,6 +106,9 @@ Detailed list of changes
- launch: Allow creating desktop panels such as those created by the :doc:`panel kitten </kittens/panel>` (:iss:`8549`)
- Remote control: Allow modifying desktop panels and showing/hiding OS Windows
using the `kitten @ resize-os-window` command (:iss:`8550`)
- Allow configuring the mouse unhide behavior when using :opt:`mouse_hide_wait` (:pull:`8508`)
- diff kitten: Add half page and full page scroll vim-like bindings (:pull:`8514`)

6
glfw/wl_window.c vendored
View file

@ -1683,7 +1683,7 @@ void _glfwPlatformShowWindow(_GLFWwindow* window)
if (!window->wl.visible) {
if (!is_layer_shell(window)) create_window_desktop_surface(window);
window->wl.visible = true;
wl_surface_commit(window->wl.surface);
commit_window_surface(window);
if (is_layer_shell(window)) debug("Layer shell surface mapped waiting for configure event from compositor\n");
}
}
@ -1704,12 +1704,14 @@ void _glfwPlatformHideWindow(_GLFWwindow* window)
window->wl.once.surface_configured = false;
window->swaps_disallowed = true;
window->wl.visible = false;
wl_surface_commit(window->wl.surface);
commit_window_surface(window);
}
bool _glfwPlatformSetLayerShellConfig(_GLFWwindow* window, const GLFWLayerShellConfig *value) {
if (!is_layer_shell(window)) return false;
window->wl.layer_shell.config = *value;
layer_set_properties(window);
commit_window_surface(window);
return true;
}

View file

@ -24,11 +24,8 @@ from kitty.fast_data_types import (
GLFW_LAYER_SHELL_OVERLAY,
GLFW_LAYER_SHELL_PANEL,
GLFW_LAYER_SHELL_TOP,
add_timer,
get_boss,
glfw_primary_monitor_size,
make_x11_window_a_dock_window,
monotonic,
toggle_os_window_visibility,
)
from kitty.os_window_size import WindowSizeData, edge_spacing
@ -304,30 +301,6 @@ def layer_shell_config(opts: PanelCLIOptions) -> LayerShellConfig:
output_name=opts.output_name or '')
last_toggled_at = 0.
num_of_pending_toggles = 0
def do_visibility_toggle(timer_id: int | None = None) -> None:
global last_toggled_at, num_of_pending_toggles
if num_of_pending_toggles & 1:
for os_window_id in get_boss().os_window_map:
toggle_os_window_visibility(os_window_id)
last_toggled_at = monotonic()
num_of_pending_toggles = 0
def schedule_visibility_toggle(debounce_interval: float = 0.2) -> None:
# Debouncing of toggle requests is needed because of buggy Wayland
# compositors: https://github.com/kovidgoyal/kitty/issues/8557
global num_of_pending_toggles, last_toggled_at
num_of_pending_toggles += 1
if (delta := monotonic() - last_toggled_at) >= debounce_interval:
do_visibility_toggle()
elif num_of_pending_toggles == 1:
add_timer(do_visibility_toggle, debounce_interval - delta, False)
def handle_single_instance_command(boss: BossType, sys_args: Sequence[str], environ: Mapping[str, str], notify_on_os_window_death: str | None = '') -> None:
from kitty.tabs import SpecialWindow
try:
@ -336,7 +309,8 @@ def handle_single_instance_command(boss: BossType, sys_args: Sequence[str], envi
log_error(f'Invalid arguments received over single instance socket: {sys_args} with error: {e}')
return
if args.toggle_visibility and boss.os_window_map:
schedule_visibility_toggle()
for os_window_id in boss.os_window_map:
toggle_os_window_visibility(os_window_id)
return
items = items or [kitten_exe(), 'run-shell']
lsc = layer_shell_config(args)

View file

@ -1703,7 +1703,7 @@ def set_clipboard_data_types(ct: int, mime_types: Tuple[str, ...]) -> None: ...
def get_clipboard_mime(ct: int, mime: Optional[str], callback: Callable[[bytes], None]) -> None: ...
def run_with_activation_token(func: Callable[[str], None]) -> bool: ...
def make_x11_window_a_dock_window(x11_window_id: int, strut: Tuple[int, int, int, int, int, int, int, int, int, int, int, int]) -> None: ...
def toggle_os_window_visibility(os_window_id: int) -> bool: ...
def toggle_os_window_visibility(os_window_id: int, visible: Literal[True, False] = ...) -> bool: ...
def layer_shell_config_for_os_window(os_window_id: int) -> dict[str, Any] | None: ...
def set_layer_shell_config(os_window_id: int, cfg: LayerShellConfig) -> bool: ...
def wrapped_kitten_names() -> List[str]: ...

View file

@ -2447,14 +2447,42 @@ is_layer_shell_supported(PyObject *self UNUSED, PyObject *args UNUSED) {
#endif
}
static void
do_os_visibility_change(id_type timer_id, void *d) {
id_type wid = (uintptr_t)d;
OSWindow *w = os_window_for_id(wid);
if (w && w->handle && w->debounce_visibility_changes.timer_id == timer_id) {
w->debounce_visibility_changes.timer_id = 0;
if (w->debounce_visibility_changes.set_visible) {
glfwShowWindow(w->handle);
w->needs_render = true;
request_tick_callback();
} else glfwHideWindow(w->handle);
w->debounce_visibility_changes.last_change_at = monotonic();
}
}
static PyObject*
toggle_os_window_visibility(PyObject *self UNUSED, PyObject *wid) {
if (!PyLong_Check(wid)) { PyErr_SetString(PyExc_TypeError, "os_window_id must be a int"); return NULL; }
id_type id = PyLong_AsUnsignedLongLong(wid);
OSWindow *w = os_window_for_id(id);
toggle_os_window_visibility(PyObject *self UNUSED, PyObject *args) {
unsigned long long wid;
int set_visible = -1;
if (!PyArg_ParseTuple(args, "K|p", &wid, &set_visible)) return NULL;
OSWindow *w = os_window_for_id(wid);
if (!w || !w->handle) Py_RETURN_FALSE;
if (glfwGetWindowAttrib(w->handle, GLFW_VISIBLE)) glfwHideWindow(w->handle);
else glfwShowWindow(w->handle);
bool is_visible = glfwGetWindowAttrib(w->handle, GLFW_VISIBLE) != 0;
if (set_visible == -1) set_visible = !is_visible;
else if (set_visible == is_visible) Py_RETURN_FALSE;
// Debouncing of toggle requests is needed because of buggy Wayland
// compositors: https://github.com/kovidgoyal/kitty/issues/8557
monotonic_t debounce_interval = ms_to_monotonic_t(250);
monotonic_t now = monotonic();
w->debounce_visibility_changes.set_visible = set_visible;
if (now - w->debounce_visibility_changes.last_change_at >= debounce_interval) {
do_os_visibility_change(0, (void*)(uintptr_t)w->id);
} else if (w->debounce_visibility_changes.timer_id == 0) {
w->debounce_visibility_changes.timer_id = add_main_loop_timer(
debounce_interval - (now - w->debounce_visibility_changes.last_change_at), false, do_os_visibility_change, (void*)(uintptr_t)w->id, NULL);
}
Py_RETURN_TRUE;
}
@ -2491,7 +2519,7 @@ set_layer_shell_config(PyObject *self UNUSED, PyObject *args) {
static PyMethodDef module_methods[] = {
METHODB(set_custom_cursor, METH_VARARGS),
METHODB(is_css_pointer_name_valid, METH_O),
METHODB(toggle_os_window_visibility, 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),
METHODB(pointer_name_to_css_name, METH_O),

View file

@ -419,7 +419,10 @@ def get_env(opts: LaunchCLIOptions, active_child: Child | None = None, base_env:
def layer_shell_config_from_panel_opts(panel_opts: Iterable[str]) -> LayerShellConfig:
from kittens.panel.main import layer_shell_config, parse_panel_args
args = [('' if x.startswith('--') else '--') + x for x in panel_opts]
opts, _ = parse_panel_args(args)
try:
opts, _ = parse_panel_args(args)
except SystemExit as e:
raise ValueError(str(e))
return layer_shell_config(opts)

View file

@ -3,7 +3,7 @@
from typing import TYPE_CHECKING
from kitty.fast_data_types import get_os_window_size
from kitty.fast_data_types import get_os_window_size, layer_shell_config_for_os_window, set_layer_shell_config, toggle_os_window_visibility
from .base import (
MATCH_WINDOW_OPTION,
@ -27,22 +27,28 @@ class ResizeOSWindow(RemoteCommand):
match/str: Which window to resize
self/bool: Boolean indicating whether to close the window the command is run in
incremental/bool: Boolean indicating whether to adjust the size incrementally
action/choices.resize.toggle-fullscreen.toggle-maximized: One of :code:`resize, toggle-fullscreen` or :code:`toggle-maximized`
action/choices.resize.toggle-fullscreen.toggle-maximized.toggle-visibility.hide.show.os-panel: The action to perform
unit/choices.cells.pixels: One of :code:`cells` or :code:`pixels`
width/int: Integer indicating desired window width
height/int: Integer indicating desired window height
os_panel/list.str: Settings for modifying the OS Panel
'''
short_desc = 'Resize the specified OS Windows'
short_desc = 'Resize/show/hide/etc. the specified OS Windows'
desc = (
'Resize the specified OS Windows.'
'Resize (or other operations) on the specified OS Windows.'
' Note that some window managers/environments do not allow applications to resize'
' their windows, for example, tiling window managers.'
' their windows, for example, tiling window managers.\n\nTo modify OS Panels created with the'
' panel kitten, use :option:`--action`=:code:`os-panel`. Specify the modifications in the same syntax as used'
' by the panel kitten, without the leading dashes. Use the :option:`--incremental` option to only change'
' the specified panel settings. For example, move the panel to bottom edge and make it two lines tall:'
' :code:`--action=os-panel --incremental lines=2 edge=bottom`'
)
args = RemoteCommand.Args(spec='[OS Panel settings ...]', json_field='os_panel', special_parse='escape_list_of_strings(args), nil')
options_spec = MATCH_WINDOW_OPTION + '''\n
--action
default=resize
choices=resize,toggle-fullscreen,toggle-maximized
choices=resize,toggle-fullscreen,toggle-maximized,toggle-visibility,hide,show,os-panel
The action to perform.
@ -67,7 +73,9 @@ Change the height of the window. Zero leaves the height unchanged.
--incremental
type=bool-set
Treat the specified sizes as increments on the existing window size
instead of absolute sizes.
instead of absolute sizes. When using :option:`--action`=:code:`os-panel`,
only the specified settings are changed, otherwise non-specified settings
are reset to default.
--self
@ -86,7 +94,7 @@ using this option means that you will not be notified of failures.
return {
'match': opts.match, 'action': opts.action, 'unit': opts.unit,
'width': opts.width, 'height': opts.height, 'self': opts.self,
'incremental': opts.incremental
'incremental': opts.incremental, 'os_panel': args,
}
def response_from_kitty(self, boss: Boss, window: Window | None, payload_get: PayloadGetType) -> ResponseType:
@ -97,9 +105,44 @@ using this option means that you will not be notified of failures.
metrics = get_os_window_size(os_window_id)
if metrics is None:
raise RemoteControlErrorWithoutTraceback(f'The OS Window {os_window_id} does not exist')
if metrics['is_layer_shell']:
raise RemoteControlErrorWithoutTraceback(f'The OS Window {os_window_id} is a panel and cannot be resized')
if ac == 'resize':
panels = payload_get('os_panel')
is_panel = metrics['is_layer_shell']
if ac == 'os-panel':
if not is_panel:
raise RemoteControlErrorWithoutTraceback(
f'The OS Window {os_window_id} is not a panel you should not use the --action=resize option to resize it')
if not panels:
raise RemoteControlErrorWithoutTraceback('Must specify at least one panel setting')
from kitty.launch import layer_shell_config_from_panel_opts
try:
lsc = layer_shell_config_from_panel_opts(panels)
except Exception as e:
raise RemoteControlErrorWithoutTraceback(
f'Invalid panel options specified: {e}')
if payload_get('incremental'):
defaults = layer_shell_config_from_panel_opts(())
changed_fields = {f for f in lsc._fields if getattr(lsc, f) != getattr(defaults, f)}
existing = layer_shell_config_for_os_window(os_window_id)
if existing is None:
raise RemoteControlErrorWithoutTraceback(
f'The OS Window {os_window_id} has no panel configuration')
replacements = {}
for x in lsc._fields:
if x not in changed_fields:
replacements[x] = existing[x]
lsc = lsc._replace(**replacements)
if not set_layer_shell_config(os_window_id, lsc):
raise RemoteControlErrorWithoutTraceback(f'Failed to change panel configuration for OS Window {os_window_id}')
elif ac == 'toggle-visibility':
toggle_os_window_visibility(os_window_id)
elif ac == 'hide':
toggle_os_window_visibility(os_window_id, False)
elif ac == 'show':
toggle_os_window_visibility(os_window_id, True)
elif is_panel:
raise RemoteControlErrorWithoutTraceback(
f'The OS Window {os_window_id} is a desktop panel, no actions other than resizing are supported for it')
elif ac == 'resize':
boss.resize_os_window(
os_window_id, width=payload_get('width'), height=payload_get('height'),
unit=payload_get('unit'), incremental=payload_get('incremental'), metrics=metrics,

View file

@ -313,6 +313,11 @@ typedef struct {
id_type last_focused_counter;
CloseRequest close_request;
bool is_layer_shell;
struct {
monotonic_t last_change_at;
bool set_visible;
id_type timer_id;
} debounce_visibility_changes;
} OSWindow;