1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
|
#!/usr/bin/env python
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import os
import sys
from contextlib import suppress
from functools import partial
from types import MappingProxyType
from typing import Any, Iterable, Mapping, Sequence
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
|