Refactor the kittens framework

Make it possible to perform arbitrary actions with the kittens output
and also allow running kittens from standalone python files.
This commit is contained in:
Kovid Goyal 2018-04-11 13:03:40 +05:30
parent 5755ba72b1
commit 2cf8c6aea7
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 98 additions and 76 deletions

59
kittens/runner.py Normal file
View File

@ -0,0 +1,59 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import importlib
import os
import sys
from functools import partial
def import_kitten_main_module(config_dir, kitten):
if kitten.endswith('.py'):
path = os.path.expanduser(kitten)
if not os.path.isabs(path):
path = os.path.join(config_dir, path)
path = os.path.abspath(path)
if os.path.dirname(path):
sys.path.insert(0, os.path.dirname(path))
with open(path) as f:
src = f.read()
code = compile(src, path, 'exec')
g = {'__name__': 'kitten'}
exec(code, g)
return {'start': g['main'], 'end': g['handle_result']}
else:
m = importlib.import_module('kittens.{}.main'.format(kitten))
return {'start': m.main, 'end': m.handle_result}
def create_kitten_handler(kitten, orig_args):
from kitty.constants import config_dir
m = import_kitten_main_module(config_dir, kitten)
return partial(m['end'], [kitten] + orig_args)
def launch(args):
config_dir, kitten = args[:2]
del args[:2]
args = [kitten] + args
os.environ['KITTY_CONFIG_DIRECTORY'] = config_dir
from kittens.tui.operations import clear_screen, reset_mode
m = import_kitten_main_module(config_dir, kitten)
result = m['start'](args)
print(reset_mode('ALTERNATE_SCREEN') + clear_screen(), end='')
if result is not None:
import json
print('OK:', json.dumps(result))
def main():
try:
args = sys.argv[1:]
launch(args)
except Exception:
print('Unhandled exception running kitten:')
import traceback
traceback.print_exc()
input('Press Enter to quit...')

View File

@ -5,7 +5,6 @@
import os import os
import string import string
import subprocess import subprocess
import sys
from functools import lru_cache from functools import lru_cache
from gettext import gettext as _ from gettext import gettext as _
@ -454,26 +453,24 @@ class UnicodeInput(Handler):
self.refresh() self.refresh()
def run_loop(args): def main(args):
loop = Loop() loop = Loop()
with cached_values_for('unicode-input') as cached_values: with cached_values_for('unicode-input') as cached_values:
handler = UnicodeInput(cached_values) handler = UnicodeInput(cached_values)
loop.loop(handler) loop.loop(handler)
if handler.current_char and loop.return_code == 0: if handler.current_char and loop.return_code == 0:
print('OK:', hex(ord(handler.current_char))[2:])
try: try:
handler.recent.remove(ord(handler.current_char)) handler.recent.remove(ord(handler.current_char))
except Exception: except Exception:
pass pass
recent = [ord(handler.current_char)] + handler.recent recent = [ord(handler.current_char)] + handler.recent
cached_values['recent'] = recent[:len(DEFAULT_SET)] cached_values['recent'] = recent[:len(DEFAULT_SET)]
return loop.return_code return handler.current_char
if loop.return_code != 0:
raise SystemExit(loop.return_code)
def main(args=sys.argv): def handle_result(args, current_char, target_window_id, boss):
try: w = boss.window_id_map.get(target_window_id)
raise SystemExit(run_loop(args)) if w is not None:
except Exception: w.paste(current_char)
import traceback
traceback.print_exc()
input(_('Press Enter to quit.'))

View File

@ -4,7 +4,6 @@
import re import re
import string import string
import subprocess
import sys import sys
from collections import namedtuple from collections import namedtuple
from functools import lru_cache, partial from functools import lru_cache, partial
@ -12,7 +11,6 @@ from gettext import gettext as _
from kitty.cli import parse_args from kitty.cli import parse_args
from kitty.key_encoding import ESCAPE, backspace_key, enter_key from kitty.key_encoding import ESCAPE, backspace_key, enter_key
from kitty.utils import command_for_open
from ..tui.handler import Handler from ..tui.handler import Handler
from ..tui.loop import Loop from ..tui.loop import Loop
@ -178,16 +176,7 @@ def run_loop(args, lines, index_map):
handler = URLHints(lines, index_map) handler = URLHints(lines, index_map)
loop.loop(handler) loop.loop(handler)
if handler.chosen and loop.return_code == 0: if handler.chosen and loop.return_code == 0:
if args.in_kitty: return {'url': handler.chosen, 'program': args.program}
import json
print('OK:', json.dumps({'url': handler.chosen, 'program': args.program, 'action': 'open_with'}))
else:
cmd = command_for_open(args.program)
ret = subprocess.Popen(cmd + [handler.chosen]).wait()
if ret != 0:
print('URL handler "{}" failed with return code: {}'.format(' '.join(cmd), ret), file=sys.stderr)
input('Press Enter to quit')
loop.return_code = ret
raise SystemExit(loop.return_code) raise SystemExit(loop.return_code)
@ -215,7 +204,7 @@ def run(args, source_file=None):
input(_('No URLs found, press Enter to abort.')) input(_('No URLs found, press Enter to abort.'))
return return
run_loop(args, lines, index_map) return run_loop(args, lines, index_map)
OPTIONS = partial('''\ OPTIONS = partial('''\
@ -234,16 +223,10 @@ expression instead.
default={0} default={0}
Comma separated list of recognized URL prefixes. Defaults to: Comma separated list of recognized URL prefixes. Defaults to:
{0} {0}
--in-kitty
type=bool-set
Output the URL instead of opening it. Intended for use from within
kitty.
'''.format, ','.join(sorted(URL_PREFIXES))) '''.format, ','.join(sorted(URL_PREFIXES)))
def main(args=sys.argv): def main(args):
msg = 'Highlight URLs inside the specified text' msg = 'Highlight URLs inside the specified text'
try: try:
args, items = parse_args(args[1:], OPTIONS, '[path to file or omit to use stdin]', msg, 'url_hints') args, items = parse_args(args[1:], OPTIONS, '[path to file or omit to use stdin]', msg, 'url_hints')
@ -251,9 +234,9 @@ def main(args=sys.argv):
print(e.args[0], file=sys.stderr) print(e.args[0], file=sys.stderr)
input(_('Press Enter to quit')) input(_('Press Enter to quit'))
return 1 return 1
try: return run(args, (items or [None])[0])
run(args, (items or [None])[0])
except Exception:
import traceback def handle_result(args, data, target_window_id, boss):
traceback.print_exc() program = data['program']
input(_('Press Enter to quit')) boss.open_url(data['url'], None if program == 'default' else program)

View File

@ -15,7 +15,7 @@ from .cli import create_opts, parse_args
from .config import ( from .config import (
MINIMUM_FONT_SIZE, initial_window_size, prepare_config_file_for_editing MINIMUM_FONT_SIZE, initial_window_size, prepare_config_file_for_editing
) )
from .constants import appname, editor, set_boss from .constants import appname, editor, set_boss, config_dir
from .fast_data_types import ( from .fast_data_types import (
ChildMonitor, create_os_window, current_os_window, destroy_global_data, ChildMonitor, create_os_window, current_os_window, destroy_global_data,
destroy_sprite_map, get_clipboard_string, glfw_post_empty_event, destroy_sprite_map, get_clipboard_string, glfw_post_empty_event,
@ -471,14 +471,7 @@ class Boss:
self.new_os_window(*cmd) self.new_os_window(*cmd)
def input_unicode_character(self): def input_unicode_character(self):
w = self.active_window self.run_kitten('none', 'unicode_input')
tab = self.active_tab
if w is not None and tab is not None and w.overlay_for is None:
overlay_window = tab.new_special_window(
SpecialWindow(
['kitty', '+runpy', 'from kittens.unicode_input.main import main; main()'],
overlay_for=w.id))
overlay_window.action_on_close = partial(self.send_unicode_character, w.id)
def get_output(self, source_window, num_lines=1): def get_output(self, source_window, num_lines=1):
output = '' output = ''
@ -489,19 +482,6 @@ class Boss:
output += str(s.linebuf.line(i)) output += str(s.linebuf.line(i))
return output return output
def send_unicode_character(self, target_window_id, source_window):
w = self.window_id_map.get(target_window_id)
if w is not None:
output = self.get_output(source_window)
if output.startswith('OK: '):
try:
text = chr(int(output.partition(' ')[2], 16))
except Exception:
import traceback
traceback.print_exc()
else:
w.paste(text)
def set_tab_title(self): def set_tab_title(self):
w = self.active_window w = self.active_window
tab = self.active_tab tab = self.active_tab
@ -524,34 +504,35 @@ class Boss:
tab.set_title(title) tab.set_title(title)
break break
def run_simple_kitten(self, type_of_input, kitten, *args): def run_kitten(self, type_of_input, kitten, *args):
import shlex import shlex
w = self.active_window w = self.active_window
tab = self.active_tab tab = self.active_tab
if w is not None and tab is not None and w.overlay_for is None: if w is not None and tab is not None and w.overlay_for is None:
cmdline = args[0] if args else '' cmdline = args[0] if args else ''
args = shlex.split(cmdline) if cmdline else [] args = shlex.split(cmdline) if cmdline else []
if kitten == 'url_hints': orig_args = args[:]
args[0:0] = ['--in-kitty', '--program', self.opts.open_url_with] args[0:0] = [config_dir, kitten]
if type_of_input in ('text', 'history', 'ansi', 'ansi-history'): if type_of_input in ('text', 'history', 'ansi', 'ansi-history'):
data = w.as_text(as_ansi='ansi' in type_of_input, add_history='history' in type_of_input).encode('utf-8') data = w.as_text(as_ansi='ansi' in type_of_input, add_history='history' in type_of_input).encode('utf-8')
elif type_of_input == 'none': elif type_of_input == 'none':
data = None data = None
else: else:
raise ValueError('Unknown type_of_input: {}'.format(type_of_input)) raise ValueError('Unknown type_of_input: {}'.format(type_of_input))
from kittens.runner import create_kitten_handler
end_kitten = create_kitten_handler(kitten, orig_args)
overlay_window = tab.new_special_window( overlay_window = tab.new_special_window(
SpecialWindow( SpecialWindow(
['kitty', '+runpy', 'from kittens.{}.main import main; main()'.format(kitten)] + args, ['kitty', '+runpy', 'from kittens.runner import main; main()'] + args,
stdin=data, stdin=data,
overlay_for=w.id)) overlay_for=w.id))
if kitten == 'url_hints': overlay_window.action_on_close = partial(self.on_kitten_finish, w.id, end_kitten)
overlay_window.action_on_close = self.open_hinted_url
def open_hinted_url(self, source_window): def on_kitten_finish(self, target_window_id, end_kitten, source_window):
output = self.get_output(source_window, num_lines=None) output = self.get_output(source_window, num_lines=None)
if output.startswith('OK: '): if output.startswith('OK: '):
cmd = json.loads(output.partition(' ')[2].strip()) data = json.loads(output.partition(' ')[2].strip())
open_url(cmd['url'], cmd['program']) end_kitten(data, target_window_id, self)
def kitty_shell(self, window_type): def kitty_shell(self, window_type):
cmd = ['kitty', '@'] cmd = ['kitty', '@']
@ -580,12 +561,12 @@ class Boss:
old_focus.focus_changed(False) old_focus.focus_changed(False)
tab.active_window.focus_changed(True) tab.active_window.focus_changed(True)
def open_url(self, url): def open_url(self, url, program=None):
if url: if url:
open_url(url, self.opts.open_url_with) open_url(url, program or self.opts.open_url_with)
def open_url_lines(self, lines): def open_url_lines(self, lines, program=None):
self.open_url(''.join(lines)) self.open_url(''.join(lines), program)
def destroy(self): def destroy(self):
self.shutting_down = True self.shutting_down = True

View File

@ -108,7 +108,9 @@ def parse_key_action(action):
args = tuple(map(parse_key_action, filter(None, parts))) args = tuple(map(parse_key_action, filter(None, parts)))
elif func == 'send_text': elif func == 'send_text':
args = rest.split(' ', 1) args = rest.split(' ', 1)
elif func == 'run_simple_kitten': elif func in ('run_kitten', 'run_simple_kitten'):
if func == 'run_simple_kitten':
func = 'run_kitten'
args = rest.split(' ', 2) args = rest.split(' ', 2)
elif func == 'goto_tab': elif func == 'goto_tab':
args = (max(0, int(rest)), ) args = (max(0, int(rest)), )
@ -389,7 +391,7 @@ def parse_defaults(lines, check_keys=False):
Options, defaults = init_config(default_config_path, parse_defaults) Options, defaults = init_config(default_config_path, parse_defaults)
actions = frozenset(all_key_actions) | frozenset( actions = frozenset(all_key_actions) | frozenset(
'combine send_text goto_tab goto_layout set_font_size new_tab_with_cwd new_window_with_cwd new_os_window_with_cwd'. 'run_simple_kitten combine send_text goto_tab goto_layout set_font_size new_tab_with_cwd new_window_with_cwd new_os_window_with_cwd'.
split() split()
) )
no_op_actions = frozenset({'noop', 'no-op', 'no_op'}) no_op_actions = frozenset({'noop', 'no-op', 'no_op'})

View File

@ -435,8 +435,8 @@ map ctrl+shift+f2 edit_config_file
# Open a currently visible URL using the keyboard. The program used to open the URL is specified in open_url_with. # Open a currently visible URL using the keyboard. The program used to open the URL is specified in open_url_with.
# You can customize how the URLs are detected and opened by specifying command line options to # You can customize how the URLs are detected and opened by specifying command line options to
# url_hints. For example: # url_hints. For example:
# map ctrl+shift+e run_simple_kitten text url_hints --program firefox --regex "http://[^ ]+" # map ctrl+shift+e run_kitten text url_hints --program firefox --regex "http://[^ ]+"
map ctrl+shift+e run_simple_kitten text url_hints map ctrl+shift+e run_kitten text url_hints
# Open the kitty shell in a new window/tab/overlay/os_window to control kitty using commands. # Open the kitty shell in a new window/tab/overlay/os_window to control kitty using commands.
map ctrl+shift+escape kitty_shell window map ctrl+shift+escape kitty_shell window