#!/usr/bin/env python # License: GPL v3 Copyright: 2018, Kovid Goyal import os import sys from collections.abc import Iterable, Mapping, Sequence from contextlib import suppress from functools import partial from types import MappingProxyType from typing import Any from kitty.cli import parse_args from kitty.cli_stub import PanelCLIOptions from kitty.constants import is_macos, kitten_exe from kitty.fast_data_types import ( GLFW_EDGE_BOTTOM, GLFW_EDGE_CENTER, GLFW_EDGE_CENTER_SIZED, GLFW_EDGE_LEFT, GLFW_EDGE_NONE, GLFW_EDGE_RIGHT, GLFW_EDGE_TOP, GLFW_FOCUS_EXCLUSIVE, GLFW_FOCUS_NOT_ALLOWED, GLFW_FOCUS_ON_DEMAND, GLFW_LAYER_SHELL_BACKGROUND, GLFW_LAYER_SHELL_OVERLAY, GLFW_LAYER_SHELL_PANEL, GLFW_LAYER_SHELL_TOP, layer_shell_config_for_os_window, set_layer_shell_config, toggle_os_window_visibility, ) from kitty.simple_cli_definitions import panel_options_spec from kitty.types import LayerShellConfig, run_once from kitty.typing_compat import BossType from kitty.utils import log_error args = PanelCLIOptions() help_text = 'Use a command line program to draw a GPU accelerated panel on your desktop' usage = '[cmdline-to-run ...]' def panel_kitten_options_spec() -> str: if not hasattr(panel_kitten_options_spec, 'ans'): setattr(panel_kitten_options_spec, 'ans', panel_options_spec()) ans: str = getattr(panel_kitten_options_spec, 'ans') return ans def parse_panel_args(args: list[str], track_seen_options: dict[str, Any] | None = None) -> tuple[PanelCLIOptions, list[str]]: return parse_args( args, panel_kitten_options_spec, usage, help_text, 'kitty +kitten panel', result_class=PanelCLIOptions, track_seen_options=track_seen_options) def dual_distance(spec: str, min_cell_value_if_no_pixels: int = 0) -> tuple[int, int]: with suppress(Exception): return int(spec), 0 if spec.endswith('px'): return min_cell_value_if_no_pixels, int(spec[:-2]) if spec.endswith('c'): return int(spec[:-1]), 0 return min_cell_value_if_no_pixels, 0 def layer_shell_config(opts: PanelCLIOptions) -> LayerShellConfig: ltype = { 'background': GLFW_LAYER_SHELL_BACKGROUND, 'bottom': GLFW_LAYER_SHELL_PANEL, 'top': GLFW_LAYER_SHELL_TOP, 'overlay': GLFW_LAYER_SHELL_OVERLAY }.get(opts.layer, GLFW_LAYER_SHELL_PANEL) ltype = GLFW_LAYER_SHELL_BACKGROUND if opts.edge == 'background' else ltype edge = { 'top': GLFW_EDGE_TOP, 'bottom': GLFW_EDGE_BOTTOM, 'left': GLFW_EDGE_LEFT, 'right': GLFW_EDGE_RIGHT, 'center': GLFW_EDGE_CENTER, 'none': GLFW_EDGE_NONE, 'center-sized': GLFW_EDGE_CENTER_SIZED, }.get(opts.edge, GLFW_EDGE_TOP) focus_policy = { 'not-allowed': GLFW_FOCUS_NOT_ALLOWED, 'exclusive': GLFW_FOCUS_EXCLUSIVE, 'on-demand': GLFW_FOCUS_ON_DEMAND }.get(opts.focus_policy, GLFW_FOCUS_NOT_ALLOWED) if opts.hide_on_focus_loss: focus_policy = GLFW_FOCUS_ON_DEMAND x, y = dual_distance(opts.columns, min_cell_value_if_no_pixels=1), dual_distance(opts.lines, min_cell_value_if_no_pixels=1) return LayerShellConfig(type=ltype, edge=edge, x_size_in_cells=x[0], x_size_in_pixels=x[1], y_size_in_cells=y[0], y_size_in_pixels=y[1], requested_top_margin=max(0, opts.margin_top), requested_left_margin=max(0, opts.margin_left), requested_bottom_margin=max(0, opts.margin_bottom), requested_right_margin=max(0, opts.margin_right), focus_policy=focus_policy, requested_exclusive_zone=opts.exclusive_zone, override_exclusive_zone=opts.override_exclusive_zone, hide_on_focus_loss=opts.hide_on_focus_loss, output_name=opts.output_name or '') @run_once def cli_option_to_lsc_configs_map() -> MappingProxyType[str, tuple[str, ...]]: return MappingProxyType({ 'lines': ('y_size_in_cells', 'y_size_in_pixels'), 'columns': ('x_size_in_cells', 'x_size_in_pixels'), 'margin_top': ('requested_top_margin',), 'margin_left': ('requested_left_margin',), 'margin_bottom': ('requested_bottom_margin',), 'margin_right': ('requested_right_margin',), 'edge': ('edge',), 'layer': ('type',), 'output_name': ('output_name',), 'focus_policy': ('focus_policy',), 'exclusive_zone': ('requested_exclusive_zone',), 'override_exclusive_zone': ('override_exclusive_zone',), 'hide_on_focus_loss': ('hide_on_focus_loss',) }) def incrementally_update_layer_shell_config(existing: dict[str, Any], cli_options: Iterable[str]) -> LayerShellConfig: seen_options: dict[str, Any] = {} cli_options = [('' if x.startswith('--') else '--') + x for x in cli_options] try: try: opts, _ = parse_panel_args(cli_options, track_seen_options=seen_options) except SystemExit as e: raise ValueError(str(e)) lsc = layer_shell_config(opts) except Exception as e: raise ValueError(f'Invalid panel options specified: {e}') lsc_cli_map = cli_option_to_lsc_configs_map() for option in seen_options: for config in lsc_cli_map.get(option, ()): existing[config] = getattr(lsc, config) if seen_options.get('edge') == 'background': existing['type'] = GLFW_LAYER_SHELL_BACKGROUND if existing['hide_on_focus_loss']: existing['focus_policy'] = GLFW_FOCUS_ON_DEMAND return LayerShellConfig(**existing) mtime_map: dict[str, float] = {} def have_config_files_been_updated(config_files: Iterable[str]) -> bool: ans = False for cf in config_files: try: mtime = os.path.getmtime(cf) except OSError: mtime = 0 if mtime_map.get(cf, 0) != mtime: ans = True mtime_map[cf] = mtime return ans def handle_single_instance_command(boss: BossType, sys_args: Sequence[str], environ: Mapping[str, str], notify_on_os_window_death: str | None = '') -> None: global args from kitty.cli import parse_override from kitty.tabs import SpecialWindow try: new_args, items = parse_panel_args(list(sys_args[1:])) except BaseException as e: log_error(f'Invalid arguments received over single instance socket: {sys_args} with error: {e}') return lsc = layer_shell_config(new_args) config_changed = have_config_files_been_updated(new_args.config) or args.config != new_args.config or args.override != new_args.override args = new_args if config_changed: boss.load_config_file(*args.config, overrides=tuple(map(parse_override, new_args.override))) if args.toggle_visibility and boss.os_window_map: for os_window_id in boss.os_window_map: existing = layer_shell_config_for_os_window(os_window_id) layer_shell_config_changed = not existing or any(f for f in lsc._fields if getattr(lsc, f) != existing.get(f)) toggle_os_window_visibility(os_window_id, move_to_active_screen=args.move_to_active_monitor) if layer_shell_config_changed: set_layer_shell_config(os_window_id, lsc) return items = items or [kitten_exe(), 'run-shell'] os_window_id = boss.add_os_panel(lsc, args.cls, args.name) if notify_on_os_window_death: boss.os_window_death_actions[os_window_id] = partial(boss.notify_on_os_window_death, notify_on_os_window_death) tm = boss.os_window_map[os_window_id] tm.new_tab(SpecialWindow(cmd=items, env=dict(environ))) def main(sys_args: list[str]) -> None: # run_kitten runs using runpy.run_module which does not import into # sys.modules, which means the module will be re-imported later, causing # global variables to be duplicated, so do it now. from kittens.panel.main import actual_main actual_main(sys_args) return def actual_main(sys_args: list[str]) -> None: global args args, items = parse_panel_args(sys_args[1:]) have_config_files_been_updated(args.config) sys.argv = ['kitty'] if args.debug_rendering: sys.argv.append('--debug-rendering') if args.debug_input: sys.argv.append('--debug-input') for config in args.config: sys.argv.extend(('--config', config)) if not is_macos: sys.argv.extend(('--class', args.cls)) if args.name: sys.argv.extend(('--name', args.name)) if args.start_as_hidden: sys.argv.append('--start-as=hidden') if args.grab_keyboard: sys.argv.append('--grab-keyboard') for override in args.override: sys.argv.extend(('--override', override)) sys.argv.append('--override=linux_display_server=auto') sys.argv.append('--override=macos_quit_when_last_window_closed=yes') sys.argv.append('--override=macos_hide_from_tasks=yes') sys.argv.append('--override=macos_window_resizable=no') if args.single_instance: sys.argv.append('--single-instance') if args.instance_group: sys.argv.append(f'--instance-group={args.instance_group}') if args.listen_on: sys.argv.append(f'--listen-on={args.listen_on}') 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) real_main(called_from_panel=True) if __name__ == '__main__': main(sys.argv) elif __name__ == '__doc__': cd: dict = sys.cli_docs # type: ignore cd['usage'] = usage cd['options'] = panel_kitten_options_spec cd['help_text'] = help_text cd['short_desc'] = help_text