diff --git a/docs/changelog.rst b/docs/changelog.rst index bf8d48f60..ee62c2633 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -106,6 +106,9 @@ Detailed list of changes - launch: Allow creating desktop panels such as those created by the :doc:`panel kitten ` (: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`) diff --git a/glfw/wl_window.c b/glfw/wl_window.c index 1c601d353..68c94237a 100644 --- a/glfw/wl_window.c +++ b/glfw/wl_window.c @@ -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; } diff --git a/kittens/panel/main.py b/kittens/panel/main.py index c971ff3aa..1633f0b58 100644 --- a/kittens/panel/main.py +++ b/kittens/panel/main.py @@ -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) diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 6d2ec4ee0..34c77ede0 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -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]: ... diff --git a/kitty/glfw.c b/kitty/glfw.c index 2ad860948..2a3c3d2f9 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -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), diff --git a/kitty/launch.py b/kitty/launch.py index c29b8bcd5..b826ef3cb 100644 --- a/kitty/launch.py +++ b/kitty/launch.py @@ -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) diff --git a/kitty/rc/resize_os_window.py b/kitty/rc/resize_os_window.py index d2573c43e..296994fc5 100644 --- a/kitty/rc/resize_os_window.py +++ b/kitty/rc/resize_os_window.py @@ -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, diff --git a/kitty/state.h b/kitty/state.h index 5c135219f..954b760e1 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -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;