mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-05-13 08:26:56 +00:00
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:
parent
c1a9873530
commit
fc5fc7c9c4
8 changed files with 108 additions and 50 deletions
|
|
@ -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
6
glfw/wl_window.c
vendored
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]: ...
|
||||
|
|
|
|||
42
kitty/glfw.c
42
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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue