1#!/usr/local/bin/python3.8
2# vim:fileencoding=utf-8
3# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
4
5
6import importlib
7import os
8import sys
9from contextlib import contextmanager
10from functools import partial
11from typing import TYPE_CHECKING, Any, Dict, FrozenSet, Generator, List, cast
12
13from kitty.types import run_once
14
15aliases = {'url_hints': 'hints'}
16if TYPE_CHECKING:
17    from kitty.conf.types import Definition
18else:
19    Definition = object
20
21
22def resolved_kitten(k: str) -> str:
23    ans = aliases.get(k, k)
24    head, tail = os.path.split(ans)
25    tail = tail.replace('-', '_')
26    return os.path.join(head, tail)
27
28
29def path_to_custom_kitten(config_dir: str, kitten: str) -> str:
30    path = os.path.expanduser(kitten)
31    if not os.path.isabs(path):
32        path = os.path.join(config_dir, path)
33    path = os.path.abspath(path)
34    return path
35
36
37@contextmanager
38def preserve_sys_path() -> Generator[None, None, None]:
39    orig = sys.path[:]
40    try:
41        yield
42    finally:
43        if sys.path != orig:
44            del sys.path[:]
45            sys.path.extend(orig)
46
47
48def import_kitten_main_module(config_dir: str, kitten: str) -> Dict[str, Any]:
49    if kitten.endswith('.py'):
50        with preserve_sys_path():
51            path = path_to_custom_kitten(config_dir, kitten)
52            if os.path.dirname(path):
53                sys.path.insert(0, os.path.dirname(path))
54            with open(path) as f:
55                src = f.read()
56            code = compile(src, path, 'exec')
57            g = {'__name__': 'kitten'}
58            exec(code, g)
59            hr = g.get('handle_result', lambda *a, **kw: None)
60        return {'start': g['main'], 'end': hr}
61
62    kitten = resolved_kitten(kitten)
63    m = importlib.import_module('kittens.{}.main'.format(kitten))
64    return {'start': getattr(m, 'main'), 'end': getattr(m, 'handle_result', lambda *a, **k: None)}
65
66
67def create_kitten_handler(kitten: str, orig_args: List[str]) -> Any:
68    from kitty.constants import config_dir
69    kitten = resolved_kitten(kitten)
70    m = import_kitten_main_module(config_dir, kitten)
71    ans = partial(m['end'], [kitten] + orig_args)
72    setattr(ans, 'type_of_input', getattr(m['end'], 'type_of_input', None))
73    setattr(ans, 'no_ui', getattr(m['end'], 'no_ui', False))
74    return ans
75
76
77def set_debug(kitten: str) -> None:
78    import builtins
79
80    from kittens.tui.loop import debug
81    setattr(builtins, 'debug', debug)
82
83
84def launch(args: List[str]) -> None:
85    config_dir, kitten = args[:2]
86    kitten = resolved_kitten(kitten)
87    del args[:2]
88    args = [kitten] + args
89    os.environ['KITTY_CONFIG_DIRECTORY'] = config_dir
90    from kittens.tui.operations import clear_screen, reset_mode, Mode
91    set_debug(kitten)
92    m = import_kitten_main_module(config_dir, kitten)
93    try:
94        result = m['start'](args)
95    finally:
96        sys.stdin = sys.__stdin__
97    print(reset_mode(Mode.ALTERNATE_SCREEN) + clear_screen(), end='')
98    if result is not None:
99        import json
100        data = json.dumps(result)
101        print('OK:', len(data), data)
102    sys.stderr.flush()
103    sys.stdout.flush()
104
105
106def deserialize(output: str) -> Any:
107    import json
108    if output.startswith('OK: '):
109        try:
110            prefix, sz, rest = output.split(' ', 2)
111            return json.loads(rest[:int(sz)])
112        except Exception:
113            raise ValueError('Failed to parse kitten output: {!r}'.format(output))
114
115
116def run_kitten(kitten: str, run_name: str = '__main__') -> None:
117    import runpy
118    original_kitten_name = kitten
119    kitten = resolved_kitten(kitten)
120    set_debug(kitten)
121    try:
122        runpy.run_module('kittens.{}.main'.format(kitten), run_name=run_name)
123        return
124    except ImportError:
125        pass
126    # Look for a custom kitten
127    if not kitten.endswith('.py'):
128        kitten += '.py'
129    from kitty.constants import config_dir
130    path = path_to_custom_kitten(config_dir, kitten)
131    if not os.path.exists(path):
132        print('Available builtin kittens:', file=sys.stderr)
133        for kitten in all_kitten_names():
134            print(kitten, file=sys.stderr)
135        raise SystemExit('No kitten named {}'.format(original_kitten_name))
136    m = runpy.run_path(path, init_globals={'sys': sys, 'os': os}, run_name='__run_kitten__')
137    m['main'](sys.argv)
138
139
140@run_once
141def all_kitten_names() -> FrozenSet[str]:
142    try:
143        from importlib.resources import contents
144    except ImportError:
145        from importlib_resources import contents  # type: ignore
146    ans = []
147    for name in contents('kittens'):
148        if '__' not in name and '.' not in name and name != 'tui':
149            ans.append(name)
150    return frozenset(ans)
151
152
153def list_kittens() -> None:
154    print('You must specify the name of a kitten to run')
155    print('Choose from:')
156    print()
157    for kitten in all_kitten_names():
158        print(kitten)
159
160
161def get_kitten_cli_docs(kitten: str) -> Any:
162    setattr(sys, 'cli_docs', {})
163    run_kitten(kitten, run_name='__doc__')
164    ans = getattr(sys, 'cli_docs')
165    delattr(sys, 'cli_docs')
166    if 'help_text' in ans and 'usage' in ans and 'options' in ans:
167        return ans
168
169
170def get_kitten_completer(kitten: str) -> Any:
171    run_kitten(kitten, run_name='__completer__')
172    ans = getattr(sys, 'kitten_completer', None)
173    if ans is not None:
174        delattr(sys, 'kitten_completer')
175    return ans
176
177
178def get_kitten_conf_docs(kitten: str) -> Definition:
179    setattr(sys, 'options_definition', None)
180    run_kitten(kitten, run_name='__conf__')
181    ans = getattr(sys, 'options_definition')
182    delattr(sys, 'options_definition')
183    return cast(Definition, ans)
184
185
186def main() -> None:
187    try:
188        args = sys.argv[1:]
189        launch(args)
190    except Exception:
191        print('Unhandled exception running kitten:')
192        import traceback
193        traceback.print_exc()
194        input('Press Enter to quit...')
195