diff --git a/docs/changelog.rst b/docs/changelog.rst index 55278379e..1b4ee5e2d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -117,6 +117,9 @@ Detailed list of changes - Quick access terminal: Allow toggling the window to full screen and map using the standard kitty :sc:`toggle_fullscreen` shortcut (:iss:`8626`) +- A new setting :opt:`remember_window_position` to optionally use the position of the last closed kitty OS Window as the position of the first kitty OS Window when running a new kitty instance (:pull:`8601`) + + 0.42.0 [2025-05-11] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/boss.py b/kitty/boss.py index 0a773e634..fd2475d79 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -85,8 +85,6 @@ from .fast_data_types import ( get_boss, get_options, get_os_window_size, - get_os_window_pos, - glfw_primary_monitor_size, glfw_get_monitor_workarea, global_font_size, is_layer_shell_supported, @@ -1845,9 +1843,14 @@ class Boss: else: self.mark_os_window_for_close(os_window_id, NO_CLOSE_REQUESTED) - def on_os_window_closed(self, os_window_id: int, viewport_width: int, viewport_height: int) -> None: - self.cached_values['window-size'] = viewport_width, viewport_height + def on_os_window_closed(self, os_window_id: int, x: int, y: int, viewport_width: int, viewport_height: int, is_layer_shell: bool) -> None: tm = self.os_window_map.pop(os_window_id, None) + opts = get_options() + if not is_layer_shell: + if opts.remember_window_position and not is_wayland() and not self.os_window_map: + self.cached_values['window-pos'] = x, y + self.cached_values['monitor-workarea'] = glfw_get_monitor_workarea() + self.cached_values['window-size'] = viewport_width, viewport_height if tm is not None: tm.destroy() for window_id in tuple(w.id for w in self.window_id_map.values() if getattr(w, 'os_window_id', None) == os_window_id): @@ -1867,10 +1870,6 @@ class Boss: for qt in q: windows += list(qt) active_window = self.active_window - x, y = get_os_window_pos(active_window.id) - monitorGeometryX, monitorGeometryY = glfw_primary_monitor_size() - self.cached_values['window-pos'] = x, y - self.cached_values['monitor-workarea'] = glfw_get_monitor_workarea() msg, num_active_windows = self.close_windows_with_confirmation_msg(windows, active_window) x = get_options().confirm_os_window_close[0] num = num_active_windows if x < 0 else len(windows) diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index 02fd79a53..d5821d497 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -1114,8 +1114,11 @@ close_os_window(ChildMonitor *self, OSWindow *os_window) { if (os_window->before_fullscreen.is_set && is_os_window_fullscreen(os_window)) { w = os_window->before_fullscreen.w; h = os_window->before_fullscreen.h; } + int x = 0, y = 0; + if (os_window->handle && !global_state.is_wayland) glfwGetWindowPos(os_window->handle, &x, &y); + bool is_layer_shell = os_window->is_layer_shell; destroy_os_window(os_window); - call_boss(on_os_window_closed, "Kii", os_window->id, w, h); + call_boss(on_os_window_closed, "KiiiiO", os_window->id, x, y, w, h, is_layer_shell ? Py_True : Py_False); for (size_t t=0; t < os_window->num_tabs; t++) { Tab *tab = os_window->tabs + t; for (size_t w = 0; w < tab->num_windows; w++) mark_child_for_close(self, tab->windows[w].id); diff --git a/kitty/cli.py b/kitty/cli.py index 666e07287..68f8d6b8e 100644 --- a/kitty/cli.py +++ b/kitty/cli.py @@ -602,7 +602,6 @@ def apply_preparsed_cli_flags(preparsed_from_c: PreparsedCLIFlags, ans: Any, cre def parse_cmdline(oc: Options, disabled: OptionSpecSeq, ans: Any, args: list[str] | None = None) -> list[str]: - names_map = oc.names_map.copy() values_map = oc.values_map.copy() if 'help' not in names_map: diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 9ceb7f2f9..538366162 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -645,10 +645,10 @@ def get_options() -> Options: pass -def glfw_primary_monitor_size() -> Tuple[int, int]: +def glfw_primary_monitor_size() -> tuple[int, int]: pass -def glfw_get_monitor_workarea() -> [Tuple[int, int, int, int]]: +def glfw_get_monitor_workarea() -> tuple[tuple[int, int, int, int], ...]: pass diff --git a/kitty/glfw.c b/kitty/glfw.c index 7f5a8d96e..1059a54dd 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -2127,40 +2127,20 @@ primary_monitor_size(PYNOARG) { return Py_BuildValue("ii", mode->width, mode->height); } -static PyObject * -get_monitor_workarea(PYNOARG) -{ +static PyObject* +get_monitor_workarea(PYNOARG) { int count = 0; GLFWmonitor **monitors = glfwGetMonitors(&count); - if (count == 0) - { - PyErr_SetString(PyExc_ValueError, "Failed to retrieve monitors"); - } - - PyObject *result = PyList_New(count); - if (!result) - { - return NULL; - } - - for (int i = 0; i < count; i++) - { + if (count <= 0 || !monitors) return PyTuple_New(0); + RAII_PyObject(result, PyTuple_New(count)); if (!result) return NULL; + for (int i = 0; i < count; i++) { int xpos, ypos, width, height; - GLFWmonitor *monitor = monitors[i]; - - glfwGetMonitorWorkarea(monitor, &xpos, &ypos, &width, &height); - + glfwGetMonitorWorkarea(monitors[i], &xpos, &ypos, &width, &height); PyObject *monitor_workarea = Py_BuildValue("iiii", xpos, ypos, width, height); - if (!monitor_workarea) - { - Py_DECREF(monitor_workarea); - return NULL; - } - - PyList_SET_ITEM(result, i, monitor_workarea); + if (!monitor_workarea) return NULL; + PyTuple_SET_ITEM(result, i, monitor_workarea); } - - return result; + return Py_NewRef(result); } static PyObject* diff --git a/kitty/main.py b/kitty/main.py index a26657898..8f51c9a46 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -38,10 +38,9 @@ from .fast_data_types import ( SingleKey, create_os_window, free_font_data, + glfw_get_monitor_workarea, glfw_init, glfw_terminate, - glfw_primary_monitor_size, - glfw_get_monitor_workarea, is_layer_shell_supported, load_png_data, mask_kitty_signals_process_wide, @@ -234,8 +233,15 @@ def _run_app(opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = (), set_window_icon() if _is_panel_kitten and not is_layer_shell_supported(): raise SystemExit('Cannot create panels as the window manager/compositor does not support the neccessary protocols') - + pos_x, pos_y = None, None with cached_values_for(run_app.cached_values_name) as cached_values: + if not _is_panel_kitten and not is_wayland(): + if opts.remember_window_position: + cached_workarea = cached_values.get('monitor-workarea', ()) + if cached_workarea and glfw_get_monitor_workarea() == tuple(cached_workarea): + pos_x, pos_y = cached_values.get('window-pos', (None, None)) + if args.position: + pos_x, pos_y = map(int, args.position.lower().partition('x')[::2]) startup_sessions = tuple(create_sessions(opts, args, default_session=opts.startup_session)) wincls = (startup_sessions[0].os_window_class if startup_sessions else '') or args.cls or appname winname = (startup_sessions[0].os_window_name if startup_sessions else '') or args.name or wincls or appname @@ -243,19 +249,13 @@ def _run_app(opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = (), getattr(startup_sessions[0], 'os_window_state', None) if startup_sessions else None ) wstate = parse_os_window_state(window_state) if window_state is not None else None - posX, posY = None, None - if args.position: - monitor_workarea = glfw_get_monitor_workarea() - cached_workarea = cached_values.get('monitor-workarea', None) - if cached_workarea and len(monitor_workarea) == len(cached_workarea): - if all([tuple(cached_workarea[i]) == monitor_workarea[i] for i in range(len(cached_workarea))]): - posX, posY = cached_values.get('window-pos', (None, None)) + with startup_notification_handler(extra_callback=run_app.first_window_callback) as pre_show_callback: window_id = create_os_window( run_app.initial_window_size_func(get_os_window_sizing_data(opts, startup_sessions[0] if startup_sessions else None), cached_values), pre_show_callback, args.title or appname, winname, - wincls, wstate, load_all_shaders, disallow_override_title=bool(args.title), layer_shell_config=run_app.layer_shell_config, x=posX, y=posY) + wincls, wstate, load_all_shaders, disallow_override_title=bool(args.title), layer_shell_config=run_app.layer_shell_config, x=pos_x, y=pos_y) boss = Boss(opts, args, cached_values, global_shortcuts, talk_fd) boss.start(window_id, startup_sessions) if args.debug_font_fallback: diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 7ba13c0b8..86144248a 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -1079,6 +1079,18 @@ opt('initial_window_height', '400', option_type='window_size', ) +opt('remember_window_position', 'no', + option_type='to_bool', + long_text=''' +If enabled, the :term:`OS Window ` position will be remembered so that +new instances of kitty will have the same position as the previous instance. +If disabled, the :term:`OS Window ` will be placed by the window manager. +Note that remembering of position only works if the underlying desktop environment/window +manager supports it. It never works on Wayland. See also :option:`kitty --position` to +specify the position when launching kitty. +''' + ) + opt('enabled_layouts', '*', option_type='to_layout_names', long_text=''' diff --git a/kitty/options/parse.py b/kitty/options/parse.py index bc11eddc9..6ef063129 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -1171,6 +1171,9 @@ class Parser: choices_for_pointer_shape_when_grabbed = choices_for_default_pointer_shape + def remember_window_position(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['remember_window_position'] = to_bool(val) + def remember_window_size(self, val: str, ans: dict[str, typing.Any]) -> None: ans['remember_window_size'] = to_bool(val) diff --git a/kitty/options/to-c-generated.h b/kitty/options/to-c-generated.h index e2164ae2c..36622a48f 100644 --- a/kitty/options/to-c-generated.h +++ b/kitty/options/to-c-generated.h @@ -889,6 +889,19 @@ convert_from_opts_background_blur(PyObject *py_opts, Options *opts) { Py_DECREF(ret); } +static void +convert_from_python_dynamic_background_opacity(PyObject *val, Options *opts) { + opts->dynamic_background_opacity = PyObject_IsTrue(val); +} + +static void +convert_from_opts_dynamic_background_opacity(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "dynamic_background_opacity"); + if (ret == NULL) return; + convert_from_python_dynamic_background_opacity(ret, opts); + Py_DECREF(ret); +} + static void convert_from_python_background_image(PyObject *val, Options *opts) { background_image(val, opts); @@ -928,19 +941,6 @@ convert_from_opts_background_image_linear(PyObject *py_opts, Options *opts) { Py_DECREF(ret); } -static void -convert_from_python_dynamic_background_opacity(PyObject *val, Options *opts) { - opts->dynamic_background_opacity = PyObject_IsTrue(val); -} - -static void -convert_from_opts_dynamic_background_opacity(PyObject *py_opts, Options *opts) { - PyObject *ret = PyObject_GetAttrString(py_opts, "dynamic_background_opacity"); - if (ret == NULL) return; - convert_from_python_dynamic_background_opacity(ret, opts); - Py_DECREF(ret); -} - static void convert_from_python_background_tint(PyObject *val, Options *opts) { opts->background_tint = PyFloat_AsFloat(val); @@ -1313,14 +1313,14 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) { if (PyErr_Occurred()) return false; convert_from_opts_background_blur(py_opts, opts); if (PyErr_Occurred()) return false; + convert_from_opts_dynamic_background_opacity(py_opts, opts); + if (PyErr_Occurred()) return false; convert_from_opts_background_image(py_opts, opts); if (PyErr_Occurred()) return false; convert_from_opts_background_image_layout(py_opts, opts); if (PyErr_Occurred()) return false; convert_from_opts_background_image_linear(py_opts, opts); if (PyErr_Occurred()) return false; - convert_from_opts_dynamic_background_opacity(py_opts, opts); - if (PyErr_Occurred()) return false; convert_from_opts_background_tint(py_opts, opts); if (PyErr_Occurred()) return false; convert_from_opts_background_tint_gaps(py_opts, opts); diff --git a/kitty/options/types.py b/kitty/options/types.py index 8e6a75446..a960534da 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -403,6 +403,7 @@ option_names = ( 'placement_strategy', 'pointer_shape_when_dragging', 'pointer_shape_when_grabbed', + 'remember_window_position', 'remember_window_size', 'remote_control_password', 'repaint_delay', @@ -575,6 +576,7 @@ class Options: placement_strategy: choices_for_placement_strategy = 'center' pointer_shape_when_dragging: tuple[str, str] = ('beam', 'crosshair') pointer_shape_when_grabbed: choices_for_pointer_shape_when_grabbed = 'arrow' + remember_window_position: bool = False remember_window_size: bool = True repaint_delay: int = 10 resize_debounce_time: tuple[float, float] = (0.1, 0.5) diff --git a/kitty/simple_cli_definitions.py b/kitty/simple_cli_definitions.py index 108c3f5b0..b6051dc31 100644 --- a/kitty/simple_cli_definitions.py +++ b/kitty/simple_cli_definitions.py @@ -477,8 +477,11 @@ Control how the initial kitty window is created. --position -type=bool-set -Restores the last session's window position. Does not work on Wayland. +The position, for example 10x20, on screen at which to place the first kitty OS Window. +This may or may not work depending on the policies of the desktop +environment/window manager. It never works on Wayland. +See also :opt:`remember_window_position` to have kitty automatically try +to restore the previous window position. # Debugging options