#!/usr/bin/env python # License: GPLv3 Copyright: 2019, Kovid Goyal from typing import Any, Dict, Iterable, List, NamedTuple, Optional, Sequence from .boss import Boss from .child import Child from .cli import parse_args from .cli_stub import LaunchCLIOptions from .constants import resolve_custom_file from .fast_data_types import ( get_options, patch_color_profiles, set_clipboard_string ) from .options.utils import env as parse_env from .tabs import Tab from .types import run_once from .utils import find_exe, read_shell_environment, set_primary_selection, log_error from .window import Watchers, Window try: from typing import TypedDict except ImportError: TypedDict = dict class LaunchSpec(NamedTuple): opts: LaunchCLIOptions args: List[str] @run_once def options_spec() -> str: return ''' --window-title --title The title to set for the new window. By default, title is controlled by the child process. --tab-title The title for the new tab if launching in a new tab. By default, the title of the active window in the tab is used as the tab title. --type type=choices default=window choices=window,tab,os-window,overlay,background,clipboard,primary Where to launch the child process, in a new kitty window in the current tab, a new tab, or a new OS window or an overlay over the current window. Note that if the current window already has an overlay, then it will open a new window. The value of background means the process will be run in the background. The values clipboard and primary are meant to work with :option:`launch --stdin-source` to copy data to the system clipboard or primary selection. --keep-focus --dont-take-focus type=bool-set Keep the focus on the currently active window instead of switching to the newly opened window. --cwd The working directory for the newly launched child. Use the special value :code:`current` to use the working directory of the currently active window. --env type=list Environment variables to set in the child process. Can be specified multiple times to set different environment variables. Syntax: :code:`name=value`. Using :code:`name=` will set to empty string and just :code:`name` will remove the environment variable. --copy-colors type=bool-set Set the colors of the newly created window to be the same as the colors in the currently active window. --copy-cmdline type=bool-set Ignore any specified command line and instead use the command line from the currently active window. --copy-env type=bool-set Copy the environment variables from the currently active window into the newly launched child process. Note that most shells only set environment variables for child processes, so this will only copy the environment variables that the shell process itself has not the environment variables child processes inside the shell see. To copy that enviroment, use the kitty remote control feature with :code:`kitty @launch --copy-env`. --location type=choices default=default choices=first,after,before,neighbor,last,vsplit,hsplit,default Where to place the newly created window when it is added to a tab which already has existing windows in it. :code:`after` and :code:`before` place the new window before or after the active window. :code:`neighbor` is a synonym for :code:`after`. Also applies to creating a new tab, where the value of :code:`after` will cause the new tab to be placed next to the current tab instead of at the end. The values of :code:`vsplit` and :code:`hsplit` are only used by the :code:`splits` layout and control if the new window is placed in a vertical or horizontal split with the currently active window. The default is to place the window in a layout dependent manner, typically, after the currently active window. --allow-remote-control type=bool-set Programs running in this window can control kitty (if remote control is enabled). Note that any program with the right level of permissions can still write to the pipes of any other program on the same computer and therefore can control kitty. It can, however, be useful to block programs running on other computers (for example, over ssh) or as other users. --stdin-source type=choices default=none choices=none,@selection,@screen,@screen_scrollback,@alternate,@alternate_scrollback,@last_cmd_output Pass the screen contents as :code:`STDIN` to the child process. :code:`@selection` is the currently selected text. :code:`@screen` is the contents of the currently active window. :code:`@screen_scrollback` is the same as :code:`@screen`, but includes the scrollback buffer as well. :code:`@alternate` is the secondary screen of the current active window. For example if you run a full screen terminal application, the secondary screen will be the screen you return to when quitting the application. :code:`@last_cmd_output` is the output from the last command run in the shell, this needs :ref:`shell_integration` to work. --stdin-add-formatting type=bool-set When using :option:`launch --stdin-source` add formatting escape codes, without this only plain text will be sent. --stdin-add-line-wrap-markers type=bool-set When using :option:`launch --stdin-source` add a carriage return at every line wrap location (where long lines are wrapped at screen edges). This is useful if you want to pipe to program that wants to duplicate the screen layout of the screen. --marker Create a marker that highlights text in the newly created window. The syntax is the same as for the :code:`toggle_marker` map action (see :doc:`/marks`). --os-window-class Set the WM_CLASS property on X11 and the application id property on Wayland for the newly created OS Window when using :option:`launch --type`=os-window. Defaults to whatever is used by the parent kitty process, which in turn defaults to :code:`kitty`. --os-window-name Set the WM_NAME property on X11 for the newly created OS Window when using :option:`launch --type`=os-window. Defaults to :option:`launch --os-window-class`. --os-window-title Set the title for the newly created OS window. This title will override any titles set by programs running in kitty. --color type=list Change colors in the newly launched window. You can either specify a path to a .conf file with the same syntax as kitty.conf to read the colors from, or specify them individually, for example: ``--color background=white`` ``--color foreground=red`` --watcher -w type=list Path to a python file. Appropriately named functions in this file will be called for various events, such as when the window is resized, focused or closed. See the section on watchers in the launch command documentation: :ref:`watchers`. Relative paths are resolved relative to the kitty config directory. Global watchers for all windows can be specified with :opt:`watcher`. ''' def parse_launch_args(args: Optional[Sequence[str]] = None) -> LaunchSpec: args = list(args or ()) try: opts, args = parse_args(result_class=LaunchCLIOptions, args=args, ospec=options_spec) except SystemExit as e: raise ValueError from e return LaunchSpec(opts, args) def get_env(opts: LaunchCLIOptions, active_child: Child) -> Dict[str, str]: env: Dict[str, str] = {} if opts.copy_env and active_child: env.update(active_child.foreground_environ) for x in opts.env: for k, v in parse_env(x, env): env[k] = v return env def tab_for_window(boss: Boss, opts: LaunchCLIOptions, target_tab: Optional[Tab] = None) -> Optional[Tab]: if opts.type == 'tab': if target_tab is not None: tm = target_tab.tab_manager_ref() or boss.active_tab_manager else: tm = boss.active_tab_manager if tm: tab: Optional[Tab] = tm.new_tab(empty_tab=True, location=opts.location) if opts.tab_title and tab is not None: tab.set_title(opts.tab_title) else: tab = None elif opts.type == 'os-window': oswid = boss.add_os_window(wclass=opts.os_window_class, wname=opts.os_window_name, override_title=opts.os_window_title or None) tm = boss.os_window_map[oswid] tab = tm.new_tab(empty_tab=True) if opts.tab_title and tab is not None: tab.set_title(opts.tab_title) else: tab = target_tab or boss.active_tab return tab watcher_modules: Dict[str, Any] = {} def load_watch_modules(watchers: Iterable[str]) -> Optional[Watchers]: if not watchers: return None import runpy ans = Watchers() for path in watchers: path = resolve_custom_file(path) m = watcher_modules.get(path, None) if m is None: try: m = runpy.run_path(path, run_name='__kitty_watcher__') except Exception as err: import traceback log_error(traceback.format_exc()) log_error(f'Failed to load watcher from {path} with error: {err}') watcher_modules[path] = False continue watcher_modules[path] = m if m is False: continue w = m.get('on_close') if callable(w): ans.on_close.append(w) w = m.get('on_resize') if callable(w): ans.on_resize.append(w) w = m.get('on_focus_change') if callable(w): ans.on_focus_change.append(w) return ans class LaunchKwds(TypedDict): allow_remote_control: bool cwd_from: Optional[int] cwd: Optional[str] location: Optional[str] override_title: Optional[str] copy_colors_from: Optional[Window] marker: Optional[str] cmd: Optional[List[str]] overlay_for: Optional[int] stdin: Optional[bytes] def apply_colors(window: Window, spec: Sequence[str]) -> None: from kitty.rc.set_colors import parse_colors colors, cursor_text_color = parse_colors(spec) profiles = window.screen.color_profile, patch_color_profiles(colors, cursor_text_color, profiles, True) def launch( boss: Boss, opts: LaunchCLIOptions, args: List[str], target_tab: Optional[Tab] = None, force_target_tab: bool = False ) -> Optional[Window]: active = boss.active_window_for_cwd active_child = getattr(active, 'child', None) env = get_env(opts, active_child) kw: LaunchKwds = { 'allow_remote_control': opts.allow_remote_control, 'cwd_from': None, 'cwd': None, 'location': None, 'override_title': opts.window_title or None, 'copy_colors_from': None, 'marker': opts.marker or None, 'cmd': None, 'overlay_for': None, 'stdin': None } if opts.cwd: if opts.cwd == 'current': if active_child: kw['cwd_from'] = active_child.pid_for_cwd else: kw['cwd'] = opts.cwd if opts.location != 'default': kw['location'] = opts.location if opts.copy_colors and active: kw['copy_colors_from'] = active pipe_data: Dict[str, Any] = {} if opts.stdin_source != 'none': q = str(opts.stdin_source) if opts.stdin_add_formatting: if q in ('@screen', '@screen_scrollback', '@alternate', '@alternate_scrollback'): q = '@ansi_' + q[1:] if opts.stdin_add_line_wrap_markers: q += '_wrap' penv, stdin = boss.process_stdin_source(window=active, stdin=q, copy_pipe_data=pipe_data) if stdin: kw['stdin'] = stdin if penv: env.update(penv) cmd = args or None if opts.copy_cmdline and active_child: cmd = active_child.foreground_cmdline if cmd: final_cmd: List[str] = [] for x in cmd: if active and not opts.copy_cmdline: if x == '@selection': s = boss.data_for_at(which=x, window=active) if s: x = s elif x == '@active-kitty-window-id': x = str(active.id) elif x == '@input-line-number': if 'input_line_number' in pipe_data: x = str(pipe_data['input_line_number']) elif x == '@line-count': if 'lines' in pipe_data: x = str(pipe_data['lines']) elif x in ('@cursor-x', '@cursor-y', '@scrolled-by'): if active is not None: screen = active.screen if x == '@scrolled-by': x = str(screen.scrolled_by) elif x == '@cursor-x': x = str(screen.cursor.x + 1) elif x == '@cursor-y': x = str(screen.cursor.y + 1) final_cmd.append(x) exe = find_exe(final_cmd[0]) if not exe: xenv = read_shell_environment(get_options()) if 'PATH' in xenv: import shutil exe = shutil.which(final_cmd[0], path=xenv['PATH']) if exe: final_cmd[0] = exe kw['cmd'] = final_cmd if opts.type == 'overlay' and active: kw['overlay_for'] = active.id if opts.type == 'background': cmd = kw['cmd'] if not cmd: raise ValueError('The cmd to run must be specified when running a background process') boss.run_background_process(cmd, cwd=kw['cwd'], cwd_from=kw['cwd_from'], env=env or None, stdin=kw['stdin']) elif opts.type in ('clipboard', 'primary'): stdin = kw.get('stdin') if stdin is not None: if opts.type == 'clipboard': set_clipboard_string(stdin) else: set_primary_selection(stdin) else: if force_target_tab: tab = target_tab else: tab = tab_for_window(boss, opts, target_tab) if tab is not None: watchers = load_watch_modules(opts.watcher) new_window: Window = tab.new_window(env=env or None, watchers=watchers or None, **kw) if opts.color: apply_colors(new_window, opts.color) if opts.keep_focus and active: boss.set_active_window(active, switch_os_window_if_needed=True) return new_window return None