diff --git a/docs/kittens/panel.rst b/docs/kittens/panel.rst index bd1b4fcc9..8fe342f4c 100644 --- a/docs/kittens/panel.rst +++ b/docs/kittens/panel.rst @@ -30,7 +30,9 @@ activity, CPU load, date/time, etc. .. note:: - This kitten currently only works on X11 desktops + This kitten currently only works on X11 desktops and Wayland compositors + that support the `wlr layer shell protocol + `__ Using this kitten is simple, for example:: diff --git a/glfw/glfw3.h b/glfw/glfw3.h index 88f72960d..660ceb3cf 100644 --- a/glfw/glfw3.h +++ b/glfw/glfw3.h @@ -1303,7 +1303,8 @@ typedef struct GLFWLayerShellConfig { GLFWEdge edge; const char *output_name; GLFWFocusPolicy focus_policy; - void (*size_callback)(GLFWwindow *window, const struct GLFWLayerShellConfig *config, float scale, unsigned monitor_width, unsigned monitor_height, uint32_t *width, uint32_t *height); + unsigned size_in_cells; + void (*size_callback)(GLFWwindow *window, const struct GLFWLayerShellConfig *config, unsigned monitor_width, unsigned monitor_height, uint32_t *width, uint32_t *height); } GLFWLayerShellConfig; /*! @brief The function pointer type for error callbacks. diff --git a/glfw/wl_window.c b/glfw/wl_window.c index 8acf92dc4..1f277b5f3 100644 --- a/glfw/wl_window.c +++ b/glfw/wl_window.c @@ -795,20 +795,17 @@ static void layer_surface_handle_configure(void* data, struct zwlr_layer_surface_v1* surface, uint32_t serial, uint32_t width, uint32_t height) { debug("Layer shell configure event: width: %u height: %u\n", width, height); _GLFWwindow* window = data; - unsigned monitor_width = 0, monitor_height = 0; - if (window->wl.monitorsCount) { - _GLFWmonitor *m = window->wl.monitors[0]; - monitor_width = m->currentMode.width; monitor_height = m->currentMode.height; - } + GLFWvidmode m = {0}; + if (window->wl.monitorsCount) _glfwPlatformGetVideoMode(window->wl.monitors[0], &m); window->wl.layer_shell.config.size_callback( - (GLFWwindow*)window, &window->wl.layer_shell.config, _glfwWaylandWindowScale(window), monitor_width, monitor_height, - &width, &height); + (GLFWwindow*)window, &window->wl.layer_shell.config, m.width, m.height, &width, &height); zwlr_layer_surface_v1_ack_configure(surface, serial); if ((int)width != window->wl.width || (int)height != window->wl.height) { debug("Layer shell size changed to %ux%u in layer_surface_handle_configure\n", width, height); _glfwInputWindowSize(window, width, height); window->wl.width = width; window->wl.height = height; resizeFramebuffer(window); + _glfwInputWindowDamage(window); } commit_window_surface_if_safe(window); } diff --git a/kittens/panel/main.py b/kittens/panel/main.py index 39bc8ec62..018d35417 100644 --- a/kittens/panel/main.py +++ b/kittens/panel/main.py @@ -8,27 +8,34 @@ from typing import Any, Callable, Dict, List, Tuple from kitty.cli import parse_args from kitty.cli_stub import PanelCLIOptions from kitty.constants import appname, is_macos, is_wayland -from kitty.fast_data_types import make_x11_window_a_dock_window -from kitty.os_window_size import WindowSizeData +from kitty.fast_data_types import ( + GLFW_EDGE_BOTTOM, + GLFW_EDGE_LEFT, + GLFW_EDGE_RIGHT, + GLFW_EDGE_TOP, + GLFW_LAYER_SHELL_BACKGROUND, + GLFW_LAYER_SHELL_PANEL, + glfw_primary_monitor_size, + make_x11_window_a_dock_window, +) +from kitty.os_window_size import WindowSizeData, edge_spacing +from kitty.types import LayerShellConfig +from kitty.typing import EdgeLiteral OPTIONS = r''' ---lines +--lines --columns type=int default=1 -The number of lines shown in the panel (the height of the panel). Applies to horizontal panels. - - ---columns -type=int -default=20 -The number of columns shown in the panel (the width of the panel). Applies to vertical panels. +The number of lines shown in the panel if horizontal otherwise the number of columns shown in the panel. Ignored for background panels. --edge -choices=top,bottom,left,right +choices=top,bottom,left,right,background default=top Which edge of the screen to place the panel on. Note that some window managers (such as i3) do not support placing docked windows on the left and right edges. +The value :code:`background` means make the panel the "desktop wallpaper". This +is only supported on Wayland, not X11. --config -c @@ -42,6 +49,12 @@ Override individual kitty configuration options, can be specified multiple times Syntax: :italic:`name=value`. For example: :option:`kitty +kitten panel -o` font_size=20 +--output-name +On Wayland, the panel can only be displayed on a single monitor (output) at a time. This allows +you to specify which output is used, by name. If not specified the compositor will choose an +output automatically, typically the last output the user interacted with or the primary monitor. + + --class dest=cls default={appname}-panel @@ -96,26 +109,17 @@ window_width = window_height = 0 def setup_x11_window(win_id: int) -> None: + if is_wayland(): + return func = globals()[f'create_{args.edge}_strut'] strut = func(win_id, window_width, window_height) make_x11_window_a_dock_window(win_id, strut) def initial_window_size_func(opts: WindowSizeData, cached_values: Dict[str, Any]) -> Callable[[int, int, float, float, float, float], Tuple[int, int]]: - from kitty.fast_data_types import glfw_primary_monitor_size - from kitty.typing import EdgeLiteral - def effective_margin(which: EdgeLiteral) -> float: - ans: float = getattr(opts.single_window_margin_width, which) - if ans < 0: - ans = getattr(opts.window_margin_width, which) - return ans - - def effective_padding(which: EdgeLiteral) -> float: - ans: float = getattr(opts.single_window_padding_width, which) - if ans < 0: - ans = getattr(opts.window_padding_width, which) - return ans + def es(which: EdgeLiteral) -> float: + return edge_spacing(which, opts) def initial_window_size(cell_width: int, cell_height: int, dpi_x: float, dpi_y: float, xscale: float, yscale: float) -> Tuple[int, int]: if not is_macos and not is_wayland(): @@ -125,20 +129,26 @@ def initial_window_size_func(opts: WindowSizeData, cached_values: Dict[str, Any] monitor_width, monitor_height = glfw_primary_monitor_size() if args.edge in {'top', 'bottom'}: - spacing = effective_margin('top') + effective_margin('bottom') - spacing += effective_padding('top') + effective_padding('bottom') + spacing = es('top') + es('bottom') window_height = int(cell_height * args.lines / yscale + (dpi_y / 72) * spacing + 1) window_width = monitor_width + elif args.edge == 'background': + window_width, window_height = monitor_width, monitor_height else: - spacing = effective_margin('left') + effective_margin('right') - spacing += effective_padding('left') + effective_padding('right') - window_width = int(cell_width * args.columns / xscale + (dpi_x / 72) * spacing + 1) + spacing = es('left') + es('right') + window_width = int(cell_width * args.lines / xscale + (dpi_x / 72) * spacing + 1) window_height = monitor_height return window_width, window_height return initial_window_size +def layer_shell_config(opts: PanelCLIOptions) -> LayerShellConfig: + ltype = GLFW_LAYER_SHELL_BACKGROUND if opts.edge == 'background' else GLFW_LAYER_SHELL_PANEL + edge = {'top': GLFW_EDGE_TOP, 'bottom': GLFW_EDGE_BOTTOM, 'left': GLFW_EDGE_LEFT, 'right': GLFW_EDGE_RIGHT}.get(opts.edge, GLFW_EDGE_TOP) + return LayerShellConfig(type=ltype, edge=edge, size_in_cells=max(1, opts.lines), output_name=opts.output_name or None) + + def main(sys_args: List[str]) -> None: global args if is_macos or not os.environ.get('DISPLAY'): @@ -154,10 +164,12 @@ def main(sys_args: List[str]) -> None: sys.argv.extend(('--name', args.name)) for override in args.override: sys.argv.extend(('--override', override)) + sys.argv.append('--override=linux_display_server=auto') sys.argv.extend(items) from kitty.main import main as real_main from kitty.main import run_app run_app.cached_values_name = 'panel' + run_app.layer_shell_config = layer_shell_config(args) run_app.first_window_callback = setup_x11_window run_app.initial_window_size_func = initial_window_size_func real_main() diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 6be573307..695c37b77 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -8,6 +8,7 @@ from kitty.fonts.render import FontObject from kitty.marks import MarkerFunc from kitty.options.types import Options from kitty.types import LayerShellConfig, SignalInfo +from kitty.typing import EdgeLiteral # Constants {{{ GLFW_LAYER_SHELL_NONE: int @@ -586,7 +587,7 @@ def glfw_terminate() -> None: pass -def glfw_init(path: str, debug_keyboard: bool = False, debug_rendering: bool = False) -> bool: +def glfw_init(path: str, edge_spacing_func: Callable[[EdgeLiteral], float], debug_keyboard: bool = False, debug_rendering: bool = False) -> bool: pass diff --git a/kitty/glfw-wrapper.h b/kitty/glfw-wrapper.h index 27e928428..5a10de78b 100644 --- a/kitty/glfw-wrapper.h +++ b/kitty/glfw-wrapper.h @@ -1041,7 +1041,8 @@ typedef struct GLFWLayerShellConfig { GLFWEdge edge; const char *output_name; GLFWFocusPolicy focus_policy; - void (*size_callback)(GLFWwindow *window, const struct GLFWLayerShellConfig *config, float scale, unsigned monitor_width, unsigned monitor_height, uint32_t *width, uint32_t *height); + unsigned size_in_cells; + void (*size_callback)(GLFWwindow *window, const struct GLFWLayerShellConfig *config, unsigned monitor_width, unsigned monitor_height, uint32_t *width, uint32_t *height); } GLFWLayerShellConfig; /*! @brief The function pointer type for error callbacks. diff --git a/kitty/glfw.c b/kitty/glfw.c index c577a5b23..48f6a3dba 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -1047,14 +1047,63 @@ native_window_handle(GLFWwindow *w) { return Py_None; } +static PyObject* edge_spacing_func = NULL; + +static double +edge_spacing(GLFWEdge which) { + const char* edge = "top"; + switch(which) { + case GLFW_EDGE_TOP: edge = "top"; break; + case GLFW_EDGE_BOTTOM: edge = "bottom"; break; + case GLFW_EDGE_LEFT: edge = "left"; break; + case GLFW_EDGE_RIGHT: edge = "right"; break; + } + if (!edge_spacing_func) { + log_error("Attempt to call edge_spacing() without first setting edge_spacing_func"); + return 100; + } + RAII_PyObject(ret, PyObject_CallFunction(edge_spacing_func, "s", edge)); + if (!ret) { PyErr_Print(); return 100; } + if (!PyFloat_Check(ret)) { log_error("edge_spacing_func() return something other than a float"); return 100; } + return PyFloat_AsDouble(ret); +} + +static void +calculate_layer_shell_window_size( + GLFWwindow *window UNUSED, const GLFWLayerShellConfig *config, unsigned monitor_width, unsigned monitor_height, uint32_t *width, uint32_t *height) { + if (config->type == GLFW_LAYER_SHELL_BACKGROUND) { + if (!*width) *width = monitor_width; + if (!*height) *height = monitor_height; + return; + } + OSWindow *os_window = os_window_for_glfw_window(window); + if (!os_window) return; + float xscale, yscale; + glfwGetWindowContentScale(window, &xscale, &yscale); + if (config->edge == GLFW_EDGE_LEFT || config->edge == GLFW_EDGE_RIGHT) { + if (!*height) *height = monitor_height; + double spacing = edge_spacing(GLFW_EDGE_LEFT) + edge_spacing(GLFW_EDGE_RIGHT); + spacing *= os_window->logical_dpi_x / 72.; + spacing += (os_window->fonts_data->cell_width * config->size_in_cells) / yscale; + *width = (uint32_t)(1. + spacing); + } else { + if (!*width) *width = monitor_width; + double spacing = edge_spacing(GLFW_EDGE_TOP) + edge_spacing(GLFW_EDGE_BOTTOM); + spacing *= os_window->logical_dpi_y / 72.; + spacing += 1. + (os_window->fonts_data->cell_height * config->size_in_cells) / xscale; + *height = (uint32_t)(1. + spacing); + } +} + static GLFWLayerShellConfig translate_layer_shell_config(PyObject *p) { - GLFWLayerShellConfig ans = {0}; + GLFWLayerShellConfig ans = {.size_callback=calculate_layer_shell_window_size}; #define A(attr, type_check, convert) RAII_PyObject(attr, PyObject_GetAttrString(p, #attr)); if (attr == NULL) return ans; if (!type_check(attr)) { PyErr_SetString(PyExc_TypeError, #attr " not of the correct type"); return ans; }; ans.attr = convert(attr); A(output_name, PyUnicode_Check, PyUnicode_AsUTF8); A(type, PyLong_Check, PyLong_AsLong); A(edge, PyLong_Check, PyLong_AsLong); A(focus_policy, PyLong_Check, PyLong_AsLong); + A(size_in_cells, PyLong_Check, PyLong_AsLong); #undef A return ans; } @@ -1370,7 +1419,10 @@ static PyObject* glfw_init(PyObject UNUSED *self, PyObject *args) { const char* path; int debug_keyboard = 0, debug_rendering = 0; - if (!PyArg_ParseTuple(args, "s|pp", &path, &debug_keyboard, &debug_rendering)) return NULL; + PyObject *edge_sf; + if (!PyArg_ParseTuple(args, "sO|pp", &path, &edge_sf, &debug_keyboard, &debug_rendering)) return NULL; + if (!PyCallable_Check(edge_sf)) { PyErr_SetString(PyExc_TypeError, "edge_spacing_func must be a callable"); return NULL; } + Py_CLEAR(edge_spacing_func); #ifdef __APPLE__ cocoa_set_uncaught_exception_handler(); #endif @@ -1400,6 +1452,7 @@ glfw_init(PyObject UNUSED *self, PyObject *args) { global_state.default_dpi.x = w.logical_dpi_x; global_state.default_dpi.y = w.logical_dpi_y; } + edge_spacing_func = edge_sf; Py_INCREF(edge_spacing_func); Py_INCREF(ans); return ans; } @@ -1413,6 +1466,7 @@ glfw_terminate(PYNOARG) { } } glfwTerminate(); + Py_CLEAR(edge_spacing_func); Py_RETURN_NONE; } @@ -2183,6 +2237,7 @@ static PyMethodDef module_methods[] = { void cleanup_glfw(void) { if (logo.pixels) free(logo.pixels); logo.pixels = NULL; + Py_CLEAR(edge_spacing_func); #ifndef __APPLE__ release_freetype_render_context(csd_title_render_ctx); #endif diff --git a/kitty/main.py b/kitty/main.py index 7a1c4fa95..c4237a38f 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -47,7 +47,7 @@ from .fonts.box_drawing import set_scale from .fonts.render import set_font_family from .options.types import Options from .options.utils import DELETE_ENV_VAR -from .os_window_size import initial_window_size_func +from .os_window_size import edge_spacing, initial_window_size_func from .session import create_sessions, get_os_window_sizing_data from .shaders import CompileError, load_shader_programs from .types import LayerShellConfig, SingleInstanceData @@ -135,7 +135,7 @@ def load_all_shaders(semi_transparent: bool = False) -> None: def init_glfw_module(glfw_module: str, debug_keyboard: bool = False, debug_rendering: bool = False) -> None: - if not glfw_init(glfw_path(glfw_module), debug_keyboard, debug_rendering): + if not glfw_init(glfw_path(glfw_module), edge_spacing, debug_keyboard, debug_rendering): raise SystemExit('GLFW initialization failed') @@ -539,15 +539,15 @@ def _main() -> None: with suppress(AttributeError): # python compiled without threading sys.setswitchinterval(1000.0) # we have only a single python thread + if cli_opts.watcher: + from .window import global_watchers + global_watchers.set_extra(cli_opts.watcher) + log_error('The --watcher command line option has been deprecated in favor of using the watcher option in kitty.conf') # mask the signals now as on some platforms the display backend starts # threads. These threads must not handle the masked signals, to ensure # kitty can handle them. See https://github.com/kovidgoyal/kitty/issues/4636 mask_kitty_signals_process_wide() init_glfw(opts, cli_opts.debug_keyboard, cli_opts.debug_rendering) - if cli_opts.watcher: - from .window import global_watchers - global_watchers.set_extra(cli_opts.watcher) - log_error('The --watcher command line option has been deprecated in favor of using the watcher option in kitty.conf') try: with setup_profiling(): # Avoid needing to launch threads to reap zombies diff --git a/kitty/os_window_size.py b/kitty/os_window_size.py index 61833db53..3d59433ef 100644 --- a/kitty/os_window_size.py +++ b/kitty/os_window_size.py @@ -1,9 +1,11 @@ #!/usr/bin/env python # License: GPLv3 Copyright: 2020, Kovid Goyal -from typing import Any, Callable, Dict, NamedTuple, Tuple +from typing import Any, Callable, Dict, NamedTuple, Optional, Tuple, Union from .constants import is_macos, is_wayland +from .fast_data_types import get_options +from .options.types import Options from .types import FloatEdges from .typing import EdgeLiteral from .utils import log_error @@ -35,6 +37,20 @@ def sanitize_window_size(x: Any) -> int: return max(20, min(ans, 50000)) +def edge_spacing(which: EdgeLiteral, opts:Optional[Union[WindowSizeData, Options]] = None) -> float: + if opts is None: + opts = get_options() + margin: float = getattr(opts.single_window_margin_width, which) + if margin < 0: + margin = getattr(opts.window_margin_width, which) + + padding: float = getattr(opts.single_window_padding_width, which) + if padding < 0: + padding = getattr(opts.window_padding_width, which) + return padding + margin + + + def initial_window_size_func(opts: WindowSizeData, cached_values: Dict[str, Any]) -> Callable[[int, int, float, float, float, float], Tuple[int, int]]: if 'window-size' in cached_values and opts.remember_window_size: diff --git a/kitty/types.py b/kitty/types.py index 7ba1e00f7..38aa0271d 100644 --- a/kitty/types.py +++ b/kitty/types.py @@ -71,6 +71,7 @@ class LayerShellConfig(NamedTuple): edge: int = 0 focus_policy: int = 0 output_name: Optional[str] = None + size_in_cells: int = 0 def mod_to_names(mods: int, has_kitty_mod: bool = False, kitty_mod: int = 0) -> Iterator[str]: