diff --git a/docs/changelog.rst b/docs/changelog.rst index 2f56a7752..735a68dc6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -26,6 +26,9 @@ To update |kitty|, :doc:`follow the instructions `. could cause incorrect parsing if either the pending buffer capacity or the pending timeout were exceeded (:iss:`3779`) +- A new remote control command to :program:`resize the OS Window ` + - Graphics protocol: Add support for composing rectangles from one animation frame onto another (:iss:`3809`) diff --git a/kitty/boss.py b/kitty/boss.py index f2779f3ee..26237eaa8 100755 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -32,11 +32,12 @@ background_opacity_of, change_background_opacity, change_os_window_state, cocoa_set_menubar_title, create_os_window, current_application_quit_request, current_os_window, destroy_global_data, - focus_os_window, get_clipboard_string, get_options, global_font_size, - mark_os_window_for_close, os_window_font_size, patch_global_colors, - safe_pipe, set_application_quit_request, set_background_image, set_boss, - set_clipboard_string, set_in_sequence_mode, set_options, thread_write, - toggle_fullscreen, toggle_maximized + focus_os_window, get_clipboard_string, get_options, get_os_window_size, + global_font_size, mark_os_window_for_close, os_window_font_size, + patch_global_colors, safe_pipe, set_application_quit_request, + set_background_image, set_boss, set_clipboard_string, set_in_sequence_mode, + set_options, set_os_window_size, thread_write, toggle_fullscreen, + toggle_maximized ) from .keys import get_shortcut, shortcut_matches from .layout.base import set_layout_options @@ -52,10 +53,11 @@ from .types import SingleKey from .typing import PopenType, TypedDict from .utils import ( - func_name, get_editor, get_primary_selection, is_path_in_temp_dir, - log_error, open_url, parse_address_spec, parse_uri_list, - platform_window_id, read_shell_environment, remove_socket_file, safe_print, - set_primary_selection, single_instance, startup_notification_handler + func_name, get_editor, get_new_os_window_size, get_primary_selection, + is_path_in_temp_dir, log_error, open_url, parse_address_spec, + parse_uri_list, platform_window_id, read_shell_environment, + remove_socket_file, safe_print, set_primary_selection, single_instance, + startup_notification_handler ) from .window import MatchPatternType, Window @@ -533,11 +535,11 @@ def close_tab_no_confirm(self, tab: Tab) -> None: self.close_window(window) def toggle_fullscreen(self, os_window_id: int = 0) -> None: - '@ac:win: Toggle the fullscreen status of the specified or the active OS Window' + '@ac:win: Toggle the fullscreen status of the active OS Window' toggle_fullscreen(os_window_id) def toggle_maximized(self, os_window_id: int = 0) -> None: - '@ac:win: Toggle the maximized status of the specified or the active OS Window' + '@ac:win: Toggle the maximized status of the active OS Window' toggle_maximized(os_window_id) def start(self, first_os_window_id: int) -> None: @@ -784,6 +786,16 @@ def resize_layout_window(self, window: Window, increment: float, is_horizontal: return None return tab.resize_window_by(window.id, increment, is_horizontal) + def resize_os_window(self, os_window_id: int, width: int, height: int, unit: str, incremental: bool = False) -> None: + if not incremental and (width < 0 or height < 0): + return + metrics = get_os_window_size(os_window_id) + if metrics is None: + return + has_window_scaling = is_macos or is_wayland() + w, h = get_new_os_window_size(metrics, width, height, unit, incremental, has_window_scaling) + set_os_window_size(os_window_id, w, h) + def default_bg_changed_for(self, window_id: int) -> None: w = self.window_id_map.get(window_id) if w is not None: diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 377f0f184..0516ed2bd 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1202,6 +1202,8 @@ class OSWindowSize(TypedDict): yscale: float xdpi: float ydpi: float + cell_width: int + cell_height: int def get_os_window_size(os_window_id: int) -> Optional[OSWindowSize]: diff --git a/kitty/rc/resize_os_window.py b/kitty/rc/resize_os_window.py new file mode 100644 index 000000000..30592928d --- /dev/null +++ b/kitty/rc/resize_os_window.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPLv3 Copyright: 2020, Kovid Goyal + +from typing import TYPE_CHECKING, Optional + +from .base import ( + MATCH_WINDOW_OPTION, ArgsType, Boss, PayloadGetType, + PayloadType, RCOptions, RemoteCommand, ResponseType, Window +) + +if TYPE_CHECKING: + from kitty.cli_stub import ResizeOSWindowRCOptions as CLIOptions + + +class ResizeOSWindow(RemoteCommand): + ''' + match: Which window to resize + self: Boolean indicating whether to close the window the command is run in + incremental: Boolean indicating whether to adjust the size incrementally + action: One of :code:`resize, toggle-fullscreen` or :code:`toggle-maximized` + unit: One of :code:`cells` or :code:`pixels` + width: Integer indicating desired window width + height: Integer indicating desired window height + ''' + + short_desc = 'Resize the specified OS Window' + desc = ( + 'Resize the specified OS Window.' + ' Note that some window managers/environments do not allow applications to resize' + ' their windows, for example, tiling window managers.' + ) + options_spec = MATCH_WINDOW_OPTION + '''\n +--action +default=resize +choices=resize,toggle-fullscreen,toggle-maximized +The action to perform. + + +--unit +default=cells +choices=cells,pixels +The unit in which to interpret specified sizes + + +--width +default=0 +type=int +Change the width of the window. Zero leaves the width unchanged. + + +--height +default=0 +type=int +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. + + +--self +type=bool-set +If specified resize the window this command is run in, rather than the active window. + + +--no-response +type=bool-set +default=false +Don't wait for a response indicating the success of the action. Note that +using this option means that you will not be notified of failures. +''' + argspec = '' + + def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType: + if opts.no_response: + global_opts.no_command_response = True + return { + 'match': opts.match, 'action': opts.action, 'unit': opts.unit, + 'width': opts.width, 'height': opts.height, 'self': opts.self, + 'incremental': opts.incremental + } + + def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get: PayloadGetType) -> ResponseType: + windows = self.windows_for_match_payload(boss, window, payload_get) + if windows: + ac = payload_get('action') + for os_window_id in {w.os_window_id for w in windows}: + if 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') + ) + elif ac == 'toggle-fullscreen': + boss.toggle_fullscreen(os_window_id) + elif ac == 'toggle-maximized': + boss.toggle_maximized(os_window_id) + + +resize_os_window = ResizeOSWindow() diff --git a/kitty/state.c b/kitty/state.c index 794ffd98f..e65e7b8c9 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -891,9 +891,11 @@ PYWRAP1(get_os_window_size) { int width, height, fw, fh; get_os_window_size(os_window, &width, &height, &fw, &fh); get_os_window_content_scale(os_window, &xdpi, &ydpi, &xscale, &yscale); - return Py_BuildValue("{si si si si sf sf sd sd}", + unsigned int cell_width = os_window->fonts_data->cell_width, cell_height = os_window->fonts_data->cell_height; + return Py_BuildValue("{si si si si sf sf sd sd sI sI}", "width", width, "height", height, "framebuffer_width", fw, "framebuffer_height", fh, - "xscale", xscale, "yscale", yscale, "xdpi", xdpi, "ydpi", ydpi); + "xscale", xscale, "yscale", yscale, "xdpi", xdpi, "ydpi", ydpi, + "cell_width", cell_width, "cell_height", cell_height); END_WITH_OS_WINDOW Py_RETURN_NONE; } diff --git a/kitty/utils.py b/kitty/utils.py index c4d81c2eb..505d26065 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -28,6 +28,7 @@ if TYPE_CHECKING: from .options.types import Options + from .fast_data_types import OSWindowSize else: Options = object @@ -692,3 +693,23 @@ class SSHConnectionData(NamedTuple): binary: str hostname: str port: Optional[int] = None + + +def get_new_os_window_size( + metrics: 'OSWindowSize', width: int, height: int, unit: str, incremental: bool = False, has_window_scaling: bool = True +) -> Tuple[int, int]: + if unit == 'cells': + cw = metrics['cell_width'] + ch = metrics['cell_height'] + if has_window_scaling: + cw = int(cw / metrics['xscale']) + ch = int(ch / metrics['yscale']) + width *= cw + height *= ch + if incremental: + w = metrics['width'] + width + h = metrics['height'] + height + else: + w = width or metrics['width'] + h = height or metrics['height'] + return w, h diff --git a/kitty_tests/glfw.py b/kitty_tests/glfw.py index 0c531f8ee..2c2a87f5c 100644 --- a/kitty_tests/glfw.py +++ b/kitty_tests/glfw.py @@ -12,6 +12,34 @@ class TestGLFW(BaseTest): + def test_os_window_size_calculation(self): + from kitty.utils import get_new_os_window_size + + def t(w, h, width=0, height=0, unit='cells', incremental=False): + self.ae((w, h), get_new_os_window_size(metrics, width, height, unit, incremental, has_window_scaling)) + + with self.subTest(has_window_scaling=False): + has_window_scaling = False + metrics = { + 'width': 200, 'height': 100, + 'framebuffer_width': 200, 'framebuffer_height': 100, + 'xscale': 2.0, 'yscale': 2.0, 'xdpi': 192.0, 'ydpi': 192.0, + 'cell_width': 8, 'cell_height': 16 + } + t(80 * metrics['cell_width'], 100, 80) + t(80 * metrics['cell_width'] + metrics['width'], 100, 80, incremental=True) + t(1217, 100, 1217, unit='pixels') + t(1217 + metrics['width'], 100, 1217, unit='pixels', incremental=True) + + with self.subTest(has_window_scaling=True): + has_window_scaling = True + metrics['framebuffer_width'] = metrics['width'] * 2 + metrics['framebuffer_height'] = metrics['height'] * 2 + t(80 * metrics['cell_width'] / metrics['xscale'], 100, 80) + t(80 * metrics['cell_width'] / metrics['xscale'] + metrics['width'], 100, 80, incremental=True) + t(1217, 100, 1217, unit='pixels') + t(1217 + metrics['width'], 100, 1217, unit='pixels', incremental=True) + @unittest.skipIf(is_macos, 'Skipping test on macOS because glfw-cocoa.so is not built with backend_utils') def test_utf_8_strndup(self): import ctypes