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