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