1#!/usr/local/bin/python3.8 2# vim:fileencoding=utf-8 3# License: GPLv3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net> 4 5 6from typing import Any, Dict, List, NamedTuple, Optional, Sequence 7 8from .boss import Boss 9from .child import Child 10from .cli import WATCHER_DEFINITION, parse_args 11from .cli_stub import LaunchCLIOptions 12from .constants import resolve_custom_file 13from .fast_data_types import ( 14 get_options, patch_color_profiles, set_clipboard_string 15) 16from .tabs import Tab 17from .types import run_once 18from .utils import find_exe, read_shell_environment, set_primary_selection 19from .window import Watchers, Window 20 21try: 22 from typing import TypedDict 23except ImportError: 24 TypedDict = dict 25 26 27class LaunchSpec(NamedTuple): 28 opts: LaunchCLIOptions 29 args: List[str] 30 31 32@run_once 33def options_spec() -> str: 34 return ''' 35--window-title --title 36The title to set for the new window. By default, title is controlled by the 37child process. 38 39 40--tab-title 41The title for the new tab if launching in a new tab. By default, the title 42of the active window in the tab is used as the tab title. 43 44 45--type 46type=choices 47default=window 48choices=window,tab,os-window,overlay,background,clipboard,primary 49Where to launch the child process, in a new kitty window in the current tab, 50a new tab, or a new OS window or an overlay over the current window. 51Note that if the current window already has an overlay, then it will 52open a new window. The value of background means the process will be 53run in the background. The values clipboard and primary are meant 54to work with :option:`launch --stdin-source` to copy data to the system 55clipboard or primary selection. 56 57 58--keep-focus 59type=bool-set 60Keep the focus on the currently active window instead of switching 61to the newly opened window. 62 63 64--cwd 65The working directory for the newly launched child. Use the special value 66:code:`current` to use the working directory of the currently active window. 67 68 69--env 70type=list 71Environment variables to set in the child process. Can be specified multiple 72times to set different environment variables. 73Syntax: :italic:`name=value`. 74 75 76--copy-colors 77type=bool-set 78Set the colors of the newly created window to be the same as the colors in the 79currently active window. 80 81 82--copy-cmdline 83type=bool-set 84Ignore any specified command line and instead use the command line from the 85currently active window. 86 87 88--copy-env 89type=bool-set 90Copy the environment variables from the currently active window into the 91newly launched child process. Note that most shells only set environment 92variables for child processes, so this will only copy the environment 93variables that the shell process itself has not the environment variables 94child processes inside the shell see. To copy that enviroment, use the 95kitty remote control feature with :code:`kitty @launch --copy-env`. 96 97 98--location 99type=choices 100default=default 101choices=first,after,before,neighbor,last,vsplit,hsplit,default 102Where to place the newly created window when it is added to a tab which 103already has existing windows in it. :code:`after` and :code:`before` place the new 104window before or after the active window. :code:`neighbor` is a synonym for :code:`after`. 105Also applies to creating a new tab, where the value of :code:`after` 106will cause the new tab to be placed next to the current tab instead of at the end. 107The values of :code:`vsplit` and :code:`hsplit` are only used by the :code:`splits` 108layout and control if the new window is placed in a vertical or horizontal split 109with the currently active window. The default is to place the window in a 110layout dependent manner, typically, after the currently active window. 111 112 113--allow-remote-control 114type=bool-set 115Programs running in this window can control kitty (if remote control is 116enabled). Note that any program with the right level of permissions can still 117write to the pipes of any other program on the same computer and therefore can 118control kitty. It can, however, be useful to block programs running on other 119computers (for example, over ssh) or as other users. 120 121 122--stdin-source 123type=choices 124default=none 125choices=none,@selection,@screen,@screen_scrollback,@alternate,@alternate_scrollback 126Pass the screen contents as :code:`STDIN` to the child process. :code:`@selection` is 127the currently selected text. :code:`@screen` is the contents of the currently active 128window. :code:`@screen_scrollback` is the same as :code:`@screen`, but includes the 129scrollback buffer as well. :code:`@alternate` is the secondary screen of the current 130active window. For example if you run a full screen terminal application, the 131secondary screen will be the screen you return to when quitting the 132application. 133 134 135--stdin-add-formatting 136type=bool-set 137When using :option:`launch --stdin-source` add formatting escape codes, without this 138only plain text will be sent. 139 140 141--stdin-add-line-wrap-markers 142type=bool-set 143When using :option:`launch --stdin-source` add a carriage return at every line wrap 144location (where long lines are wrapped at screen edges). This is useful if you 145want to pipe to program that wants to duplicate the screen layout of the 146screen. 147 148 149--marker 150Create a marker that highlights text in the newly created window. The syntax is 151the same as for the :code:`toggle_marker` map action (see :doc:`/marks`). 152 153 154--os-window-class 155Set the WM_CLASS property on X11 and the application id property on Wayland for 156the newly created OS Window when using :option:`launch --type`=os-window. 157Defaults to whatever is used by the parent kitty process, which in turn 158defaults to :code:`kitty`. 159 160 161--os-window-name 162Set the WM_NAME property on X11 for the newly created OS Window when using 163:option:`launch --type`=os-window. Defaults to :option:`launch --os-window-class`. 164 165 166--color 167type=list 168Change colors in the newly launched window. You can either specify a path to a .conf 169file with the same syntax as kitty.conf to read the colors from, or specify them 170individually, for example: ``--color background=white`` ``--color foreground=red`` 171 172 173''' + WATCHER_DEFINITION 174 175 176def parse_launch_args(args: Optional[Sequence[str]] = None) -> LaunchSpec: 177 args = list(args or ()) 178 try: 179 opts, args = parse_args(result_class=LaunchCLIOptions, args=args, ospec=options_spec) 180 except SystemExit as e: 181 raise ValueError from e 182 return LaunchSpec(opts, args) 183 184 185def get_env(opts: LaunchCLIOptions, active_child: Child) -> Dict[str, str]: 186 env: Dict[str, str] = {} 187 if opts.copy_env and active_child: 188 env.update(active_child.foreground_environ) 189 for x in opts.env: 190 parts = x.split('=', 1) 191 if len(parts) == 2: 192 env[parts[0]] = parts[1] 193 return env 194 195 196def tab_for_window(boss: Boss, opts: LaunchCLIOptions, target_tab: Optional[Tab] = None) -> Optional[Tab]: 197 if opts.type == 'tab': 198 tm = boss.active_tab_manager 199 if tm: 200 tab: Optional[Tab] = tm.new_tab(empty_tab=True, location=opts.location) 201 if opts.tab_title and tab is not None: 202 tab.set_title(opts.tab_title) 203 else: 204 tab = None 205 elif opts.type == 'os-window': 206 oswid = boss.add_os_window(wclass=opts.os_window_class, wname=opts.os_window_name) 207 tm = boss.os_window_map[oswid] 208 tab = tm.new_tab(empty_tab=True) 209 if opts.tab_title and tab is not None: 210 tab.set_title(opts.tab_title) 211 else: 212 tab = target_tab or boss.active_tab 213 214 return tab 215 216 217def load_watch_modules(watchers: Sequence[str]) -> Optional[Watchers]: 218 if not watchers: 219 return None 220 import runpy 221 ans = Watchers() 222 for path in watchers: 223 path = resolve_custom_file(path) 224 m = runpy.run_path(path, run_name='__kitty_watcher__') 225 w = m.get('on_close') 226 if callable(w): 227 ans.on_close.append(w) 228 w = m.get('on_resize') 229 if callable(w): 230 ans.on_resize.append(w) 231 w = m.get('on_focus_change') 232 if callable(w): 233 ans.on_focus_change.append(w) 234 return ans 235 236 237class LaunchKwds(TypedDict): 238 239 allow_remote_control: bool 240 cwd_from: Optional[int] 241 cwd: Optional[str] 242 location: Optional[str] 243 override_title: Optional[str] 244 copy_colors_from: Optional[Window] 245 marker: Optional[str] 246 cmd: Optional[List[str]] 247 overlay_for: Optional[int] 248 stdin: Optional[bytes] 249 250 251def apply_colors(window: Window, spec: Sequence[str]) -> None: 252 from kitty.rc.set_colors import parse_colors 253 colors, cursor_text_color = parse_colors(spec) 254 profiles = window.screen.color_profile, 255 patch_color_profiles(colors, cursor_text_color, profiles, True) 256 257 258def launch( 259 boss: Boss, 260 opts: LaunchCLIOptions, 261 args: List[str], 262 target_tab: Optional[Tab] = None, 263 force_target_tab: bool = False 264) -> Optional[Window]: 265 active = boss.active_window_for_cwd 266 active_child = getattr(active, 'child', None) 267 env = get_env(opts, active_child) 268 kw: LaunchKwds = { 269 'allow_remote_control': opts.allow_remote_control, 270 'cwd_from': None, 271 'cwd': None, 272 'location': None, 273 'override_title': opts.window_title or None, 274 'copy_colors_from': None, 275 'marker': opts.marker or None, 276 'cmd': None, 277 'overlay_for': None, 278 'stdin': None 279 } 280 if opts.cwd: 281 if opts.cwd == 'current': 282 if active_child: 283 kw['cwd_from'] = active_child.pid_for_cwd 284 else: 285 kw['cwd'] = opts.cwd 286 if opts.location != 'default': 287 kw['location'] = opts.location 288 if opts.copy_colors and active: 289 kw['copy_colors_from'] = active 290 pipe_data: Dict[str, Any] = {} 291 if opts.stdin_source != 'none': 292 q = str(opts.stdin_source) 293 if opts.stdin_add_formatting: 294 if q in ('@screen', '@screen_scrollback', '@alternate', '@alternate_scrollback'): 295 q = '@ansi_' + q[1:] 296 if opts.stdin_add_line_wrap_markers: 297 q += '_wrap' 298 penv, stdin = boss.process_stdin_source(window=active, stdin=q, copy_pipe_data=pipe_data) 299 if stdin: 300 kw['stdin'] = stdin 301 if penv: 302 env.update(penv) 303 304 cmd = args or None 305 if opts.copy_cmdline and active_child: 306 cmd = active_child.foreground_cmdline 307 if cmd: 308 final_cmd: List[str] = [] 309 for x in cmd: 310 if active and not opts.copy_cmdline: 311 if x == '@selection': 312 s = boss.data_for_at(which=x, window=active) 313 if s: 314 x = s 315 elif x == '@active-kitty-window-id': 316 x = str(active.id) 317 elif x == '@input-line-number': 318 if 'input_line_number' in pipe_data: 319 x = str(pipe_data['input_line_number']) 320 elif x == '@line-count': 321 if 'lines' in pipe_data: 322 x = str(pipe_data['lines']) 323 elif x in ('@cursor-x', '@cursor-y', '@scrolled-by'): 324 if active is not None: 325 screen = active.screen 326 if x == '@scrolled-by': 327 x = str(screen.scrolled_by) 328 elif x == '@cursor-x': 329 x = str(screen.cursor.x + 1) 330 elif x == '@cursor-y': 331 x = str(screen.cursor.y + 1) 332 final_cmd.append(x) 333 exe = find_exe(final_cmd[0]) 334 if not exe: 335 env = read_shell_environment(get_options()) 336 if 'PATH' in env: 337 import shutil 338 exe = shutil.which(final_cmd[0], path=env['PATH']) 339 if exe: 340 final_cmd[0] = exe 341 kw['cmd'] = final_cmd 342 if opts.type == 'overlay' and active: 343 kw['overlay_for'] = active.id 344 if opts.type == 'background': 345 cmd = kw['cmd'] 346 if not cmd: 347 raise ValueError('The cmd to run must be specified when running a background process') 348 boss.run_background_process(cmd, cwd=kw['cwd'], cwd_from=kw['cwd_from'], env=env or None, stdin=kw['stdin']) 349 elif opts.type in ('clipboard', 'primary'): 350 stdin = kw.get('stdin') 351 if stdin is not None: 352 if opts.type == 'clipboard': 353 set_clipboard_string(stdin) 354 else: 355 set_primary_selection(stdin) 356 else: 357 if force_target_tab: 358 tab = target_tab 359 else: 360 tab = tab_for_window(boss, opts, target_tab) 361 if tab is not None: 362 watchers = load_watch_modules(opts.watcher) 363 new_window: Window = tab.new_window(env=env or None, watchers=watchers or None, **kw) 364 if opts.color: 365 apply_colors(new_window, opts.color) 366 if opts.keep_focus and active: 367 boss.set_active_window(active, switch_os_window_if_needed=True) 368 return new_window 369 return None 370