From a32e010f8917844bed2f336b281fbc399f9fa69e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 6 Apr 2018 10:38:26 +0530 Subject: [PATCH] Move the remote commands into their own module Separates the commands from the UI for running them. --- kitty/cmds.py | 540 ++++++++++++++++++++++++++++++++++++++++ kitty/remote_control.py | 533 +-------------------------------------- 2 files changed, 542 insertions(+), 531 deletions(-) create mode 100644 kitty/cmds.py diff --git a/kitty/cmds.py b/kitty/cmds.py new file mode 100644 index 000000000..8f49cf4fb --- /dev/null +++ b/kitty/cmds.py @@ -0,0 +1,540 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2018, Kovid Goyal + +import json +import os +import sys + +from .cli import parse_args +from .config import parse_config, parse_send_text_bytes +from .constants import appname +from .tabs import SpecialWindow +from .utils import non_blocking_read + + +class MatchError(ValueError): + + hide_traceback = True + + def __init__(self, expression, target='windows'): + ValueError.__init__(self, 'No matching {} for expression: {}'.format(target, expression)) + + +def cmd(short_desc, desc=None, options_spec=None, no_response=False, argspec='...'): + + def w(func): + func.short_desc = short_desc + func.argspec = argspec + func.desc = desc or short_desc + func.name = func.__name__[4:].replace('_', '-') + func.options_spec = options_spec + func.is_cmd = True + func.impl = lambda: globals()[func.__name__[4:]] + func.no_response = no_response + return func + return w + + +MATCH_WINDOW_OPTION = '''\ +--match -m +The window to match. Match specifications are of the form: +|_ field:regexp|. Where field can be one of: id, title, pid, cwd, cmdline. +You can use the |_ ls| command to get a list of windows. Note that for +numeric fields such as id and pid the expression is interpreted as a number, +not a regular expression. +''' +MATCH_TAB_OPTION = '''\ +--match -m +The tab to match. Match specifications are of the form: +|_ field:regexp|. Where field can be one of: id, title, pid, cwd, cmdline. +You can use the |_ ls| command to get a list of tabs. Note that for +numeric fields such as id and pid the expression is interpreted as a number, +not a regular expression. When using title or id, first a matching tab is +looked for and if not found a matching window is looked for, and the tab +for that window is used. +''' + + +# ls {{{ +@cmd( + 'List all tabs/windows', + 'List all windows. The list is returned as JSON tree. The top-level is a list of' + ' operating system {appname} windows. Each OS window has an |_ id| and a list' + ' of |_ tabs|. Each tab has its own |_ id|, a |_ title| and a list of |_ windows|.' + ' Each window has an |_ id|, |_ title|, |_ current working directory|, |_ process id (PID)| and' + ' |_ command-line| of the process running in the window.\n\n' + 'You can use these criteria to select windows/tabs for the other commands.'.format(appname=appname), + argspec='' +) +def cmd_ls(global_opts, opts, args): + pass + + +def ls(boss, window): + data = list(boss.list_os_windows()) + data = json.dumps(data, indent=2, sort_keys=True) + return data +# }}} + + +# set_font_size {{{ +@cmd( + 'Set the font size in all windows', + 'Sets the font size to the specified size, in pts.', + argspec='FONT_SIZE' +) +def cmd_set_font_size(global_opts, opts, args): + try: + return {'size': float(args[0])} + except IndexError: + raise SystemExit('No font size specified') + + +def set_font_size(boss, window, payload): + boss.set_font_size(payload['size']) +# }}} + + +# send_text {{{ +@cmd( + 'Send arbitrary text to specified windows', + 'Send arbitrary text to specified windows. The text follows Python' + ' escaping rules. So you can use escapes like |_ \\x1b| to send control codes' + ' and |_ \\u21fa| to send unicode characters. If you use the |_ --match| option' + ' the text will be sent to all matched windows. By default, text is sent to' + ' only the currently active window.', + options_spec=MATCH_WINDOW_OPTION + '''\n +--stdin +type=bool-set +Read the text to be sent from |_ stdin|. Note that in this case the text is sent as is, +not interpreted for escapes. If stdin is a terminal, you can press Ctrl-D to end reading. + + +--from-file +Path to a file whose contents you wish to send. Note that in this case the file contents +are sent as is, not interpreted for escapes. +''', + no_response=True, + argspec='[TEXT TO SEND]' +) +def cmd_send_text(global_opts, opts, args): + limit = 1024 + ret = {'match': opts.match, 'is_binary': False} + + def pipe(src=sys.stdin): + ret['is_binary'] = True + import select + with non_blocking_read() as fd: + keep_going = True + while keep_going: + rd = select.select([fd], [], []) + if rd: + data = sys.stdin.buffer.read() + if not data: + break + data = data.decode('utf-8') + if '\x04' in data: + data = data[:data.index('\x04')] + keep_going = False + while data: + ret['text'] = data[:limit] + yield ret + data = data[limit:] + else: + break + + def chunks(text): + ret['is_binary'] = False + while text: + ret['text'] = text[:limit] + yield ret + text = text[limit:] + + def file_pipe(path): + ret['is_binary'] = True + with open(path, encoding='utf-8') as f: + while True: + data = f.read(limit) + if not data: + break + ret['text'] = data + yield ret + + sources = [] + if opts.stdin: + sources.append(pipe()) + + if opts.from_file: + sources.append(file_pipe(opts.from_file)) + + text = ' '.join(args) + sources.append(chunks(text)) + + def chain(): + for src in sources: + yield from src + return chain() + + +def send_text(boss, window, payload): + windows = [boss.active_window] + match = payload['match'] + if match: + windows = tuple(boss.match_windows(match)) + data = payload['text'].encode('utf-8') if payload['is_binary'] else parse_send_text_bytes(payload['text']) + for window in windows: + if window is not None: + window.write_to_child(data) +# }}} + + +# set_window_title {{{ +@cmd( + 'Set the window title', + 'Set the title for the specified window(s). If you use the |_ --match| option' + ' the title will be set for all matched windows. By default, only the window' + ' in which the command is run is affected. If you do not specify a title, the' + ' last title set by the child process running in the window will be used.', + options_spec=MATCH_WINDOW_OPTION, + argspec='TITLE ...' +) +def cmd_set_window_title(global_opts, opts, args): + return {'title': ' '.join(args), 'match': opts.match} + + +def set_window_title(boss, window, payload): + windows = [window or boss.active_window] + match = payload['match'] + if match: + windows = tuple(boss.match_windows(match)) + if not windows: + raise MatchError(match) + for window in windows: + if window: + window.set_title(payload['title']) +# }}} + + +# set_tab_title {{{ +@cmd( + 'Set the tab title', + 'Set the title for the specified tab(s). If you use the |_ --match| option' + ' the title will be set for all matched tabs. By default, only the tab' + ' in which the command is run is affected. If you do not specify a title, the' + ' title of the currently active window in the tab is used.', + options_spec=MATCH_TAB_OPTION, + argspec='TITLE ...' +) +def cmd_set_tab_title(global_opts, opts, args): + return {'title': ' '.join(args), 'match': opts.match} + + +def set_tab_title(boss, window, payload): + match = payload['match'] + if match: + tabs = tuple(boss.match_tabs(match)) + if not tabs: + raise MatchError(match, 'tabs') + else: + tabs = [boss.tab_for_window(window) if window else boss.active_tab] + for tab in tabs: + if tab: + tab.set_title(payload['title']) +# }}} + + +# close_window {{{ +@cmd( + 'Close the specified window(s)', + options_spec=MATCH_WINDOW_OPTION + '''\n +--self +type=bool-set +If specified close the window this command is run in, rather than the active window. +''', + argspec='' +) +def cmd_close_window(global_opts, opts, args): + return {'match': opts.match, 'self': opts.self} + + +def close_window(boss, window, payload): + match = payload['match'] + if match: + windows = tuple(boss.match_windows(match)) + if not windows: + raise MatchError(match) + else: + windows = [window if window and payload['self'] else boss.active_window] + for window in windows: + if window: + boss.close_window(window) +# }}} + + +# close_tab {{{ +@cmd( + 'Close the specified tab(s)', + options_spec=MATCH_TAB_OPTION + '''\n +--self +type=bool-set +If specified close the tab this command is run in, rather than the active tab. +''', + argspec='' +) +def cmd_close_tab(global_opts, opts, args): + return {'match': opts.match, 'self': opts.self} + + +def close_tab(boss, window, payload): + match = payload['match'] + if match: + tabs = tuple(boss.match_tabs(match)) + if not tabs: + raise MatchError(match, 'tabs') + else: + tabs = [boss.tab_for_window(window) if window and payload['self'] else boss.active_tab] + for tab in tabs: + if window: + if tab: + boss.close_tab(tab) +# }}} + + +# new_window {{{ +@cmd( + 'Open new window', + 'Open a new window in the specified tab. If you use the |_ --match| option' + ' the first matching tab is used. Otherwise the currently active tab is used.' + ' Prints out the id of the newly opened window. Any command line arguments' + ' are assumed to be the command line used to run in the new window, if none' + ' are provided, the default shell is run. For example:\n' + '|_ kitty @ new-window --title Email mutt|', + options_spec=MATCH_TAB_OPTION + '''\n +--title +The title for the new window. By default it will use the title set by the +program running in it. + + +--cwd +The initial working directory for the new window. + + +--keep-focus +type=bool-set +Keep the current window focused instead of switching to the newly opened window + + +--new-tab +type=bool-set +Open a new tab + + +--tab-title +When using --new-tab set the title of the tab. +''', + argspec='[CMD ...]' +) +def cmd_new_window(global_opts, opts, args): + return {'match': opts.match, 'title': opts.title, 'cwd': opts.cwd, + 'new_tab': opts.new_tab, 'tab_title': opts.tab_title, + 'keep_focus': opts.keep_focus, 'args': args or []} + + +def new_window(boss, window, payload): + w = SpecialWindow(cmd=payload['args'] or None, override_title=payload['title'], cwd=payload['cwd']) + old_window = boss.active_window + if payload['new_tab']: + boss._new_tab(w) + tab = boss.active_tab + if payload['tab_title']: + tab.set_title(payload['tab_title']) + wid = boss.active_window.id + if payload['keep_focus'] and old_window: + boss.set_active_window(old_window) + return str(wid) + + match = payload['match'] + if match: + tabs = tuple(boss.match_tabs(match)) + if not tabs: + raise MatchError(match, 'tabs') + else: + tabs = [boss.active_tab] + tab = tabs[0] + w = tab.new_special_window(w) + if payload['keep_focus'] and old_window: + boss.set_active_window(old_window) + return str(w.id) +# }}} + + +# focus_window {{{ +@cmd( + 'Focus the specified window', + options_spec=MATCH_WINDOW_OPTION, + argspec='', +) +def cmd_focus_window(global_opts, opts, args): + return {'match': opts.match} + + +def focus_window(boss, window, payload): + windows = [window or boss.active_window] + match = payload['match'] + if match: + windows = tuple(boss.match_windows(match)) + if not windows: + raise MatchError(match) + for window in windows: + if window: + boss.set_active_window(window) + break +# }}} + + +# focus_tab {{{ +@cmd( + 'Focus the specified tab', + 'The active window in the specified tab will be focused.', + options_spec=MATCH_TAB_OPTION, + argspec='', +) +def cmd_focus_tab(global_opts, opts, args): + return {'match': opts.match} + + +def focus_tab(boss, window, payload): + match = payload['match'] + tabs = tuple(boss.match_tabs(match)) + if not tabs: + raise MatchError(match, 'tabs') + tab = tabs[0] + boss.set_active_tab(tab) +# }}} + + +# get_text {{{ +@cmd( + 'Get text from the specified window', + options_spec=MATCH_WINDOW_OPTION + '''\n +--extent +default=screen +choices=screen, all, selection +What text to get. The default of screen means all text currently on the screen. all means +all the screen+scrollback and selection means currently selected text. + + +--ansi +type=bool-set +By default, only plain text is returned. If you specify this flag, the text will +include the formatting escape codes for colors/bold/italic/etc. Note that when +getting the current selection, the result is always plain text. + + +--self +type=bool-set +If specified get text from the window this command is run in, rather than the active window. +''', + argspec='' +) +def cmd_get_text(global_opts, opts, args): + return {'match': opts.match, 'extent': opts.extent, 'ansi': opts.ansi, 'self': opts.self} + + +def get_text(boss, window, payload): + match = payload['match'] + if match: + windows = tuple(boss.match_windows(match)) + if not windows: + raise MatchError(match) + else: + windows = [window if window and payload['self'] else boss.active_window] + window = windows[0] + if payload['extent'] == 'selection': + ans = window.text_for_selection() + else: + ans = window.as_text(as_ansi=bool(payload['ansi']), add_history=True) + return ans +# }}} + + +# set_colors {{{ +@cmd( + 'Set terminal colors', + 'Set the terminal colors for the specified windows/tabs (defaults to active window). You can either specify the path to a conf file' + ' (in the same format as kitty.conf) to read the colors from or you can specify individual colors,' + ' for example: kitty @ set-colors foreground=red background=white', + options_spec='''\ +--all -a +type=bool-set +By default, colors are only changed for the currently active window. This option will +cause colors to be changed in all windows. + + +--configured -c +type=bool-set +Also change the configured colors (i.e. the colors kitty will use for new +windows or after a reset). + + +--reset +type=bool-set +Restore all colors to the values they had at kitty startup. Note that if you specify +this option, any color arguments are ignored and --configured and --all are implied. +''' + '\n\n' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t'), + argspec='COLOR_OR_FILE ...' +) +def cmd_set_colors(global_opts, opts, args): + from .rgb import color_as_int, Color + colors = {} + if not opts.reset: + for spec in args: + if '=' in spec: + colors.update(parse_config((spec.replace('=', ' '),))) + else: + with open(os.path.expanduser(spec), encoding='utf-8', errors='replace') as f: + colors.update(parse_config(f)) + colors = {k: color_as_int(v) for k, v in colors.items() if isinstance(v, Color)} + return { + 'title': ' '.join(args), 'match_window': opts.match, 'match_tab': opts.match_tab, + 'all': opts.all or opts.reset, 'configured': opts.configured or opts.reset, 'colors': colors, 'reset': opts.reset + } + + +def set_colors(boss, window, payload): + from .rgb import color_as_int + if payload['all']: + windows = tuple(boss.all_windows) + else: + windows = (window or boss.active_window,) + if payload['match_window']: + windows = tuple(boss.match_windows(payload['match_window'])) + if not windows: + raise MatchError(payload['match_window']) + if payload['match_tab']: + tabs = tuple(boss.match_tabs(payload['match_tab'])) + if not tabs: + raise MatchError(payload['match_tab'], 'tabs') + for tab in tabs: + windows += tuple(tab) + if payload['reset']: + payload['colors'] = {k: color_as_int(v) for k, v in boss.startup_colors.items()} + profiles = tuple(w.screen.color_profile for w in windows) + from .fast_data_types import patch_color_profiles + patch_color_profiles(payload['colors'], profiles, payload['configured']) + boss.patch_colors(payload['colors'], payload['configured']) + default_bg_changed = 'background' in payload['colors'] + for w in windows: + if default_bg_changed: + boss.default_bg_changed_for(w.id) + w.refresh() +# }}} + + +cmap = {v.name: v for v in globals().values() if hasattr(v, 'is_cmd')} + + +def parse_subcommand_cli(func, args): + opts, items = parse_args(args[1:], (func.options_spec or '\n').format, func.argspec, func.desc, '{} @ {}'.format(appname, func.name)) + return opts, items diff --git a/kitty/remote_control.py b/kitty/remote_control.py index e8a7c0acf..d5837d491 100644 --- a/kitty/remote_control.py +++ b/kitty/remote_control.py @@ -3,7 +3,6 @@ # License: GPL v3 Copyright: 2018, Kovid Goyal import json -import os import re import socket import sys @@ -11,537 +10,9 @@ import types from functools import partial from .cli import emph, parse_args -from .config import parse_config, parse_send_text_bytes from .constants import appname, version -from .tabs import SpecialWindow -from .utils import non_blocking_read, parse_address_spec, read_with_timeout - - -class MatchError(ValueError): - - hide_traceback = True - - def __init__(self, expression, target='windows'): - ValueError.__init__(self, 'No matching {} for expression: {}'.format(target, expression)) - - -def cmd(short_desc, desc=None, options_spec=None, no_response=False, argspec='...'): - - def w(func): - func.short_desc = short_desc - func.argspec = argspec - func.desc = desc or short_desc - func.name = func.__name__[4:].replace('_', '-') - func.options_spec = options_spec - func.is_cmd = True - func.impl = lambda: globals()[func.__name__[4:]] - func.no_response = no_response - return func - return w - - -def parse_subcommand_cli(func, args): - opts, items = parse_args(args[1:], (func.options_spec or '\n').format, func.argspec, func.desc, '{} @ {}'.format(appname, func.name)) - return opts, items - - -MATCH_WINDOW_OPTION = '''\ ---match -m -The window to match. Match specifications are of the form: -|_ field:regexp|. Where field can be one of: id, title, pid, cwd, cmdline. -You can use the |_ ls| command to get a list of windows. Note that for -numeric fields such as id and pid the expression is interpreted as a number, -not a regular expression. -''' -MATCH_TAB_OPTION = '''\ ---match -m -The tab to match. Match specifications are of the form: -|_ field:regexp|. Where field can be one of: id, title, pid, cwd, cmdline. -You can use the |_ ls| command to get a list of tabs. Note that for -numeric fields such as id and pid the expression is interpreted as a number, -not a regular expression. When using title or id, first a matching tab is -looked for and if not found a matching window is looked for, and the tab -for that window is used. -''' - - -# ls {{{ -@cmd( - 'List all tabs/windows', - 'List all windows. The list is returned as JSON tree. The top-level is a list of' - ' operating system {appname} windows. Each OS window has an |_ id| and a list' - ' of |_ tabs|. Each tab has its own |_ id|, a |_ title| and a list of |_ windows|.' - ' Each window has an |_ id|, |_ title|, |_ current working directory|, |_ process id (PID)| and' - ' |_ command-line| of the process running in the window.\n\n' - 'You can use these criteria to select windows/tabs for the other commands.'.format(appname=appname), - argspec='' -) -def cmd_ls(global_opts, opts, args): - pass - - -def ls(boss, window): - data = list(boss.list_os_windows()) - data = json.dumps(data, indent=2, sort_keys=True) - return data -# }}} - - -# set_font_size {{{ -@cmd( - 'Set the font size in all windows', - 'Sets the font size to the specified size, in pts.', - argspec='FONT_SIZE' -) -def cmd_set_font_size(global_opts, opts, args): - try: - return {'size': float(args[0])} - except IndexError: - raise SystemExit('No font size specified') - - -def set_font_size(boss, window, payload): - boss.set_font_size(payload['size']) -# }}} - - -# send_text {{{ -@cmd( - 'Send arbitrary text to specified windows', - 'Send arbitrary text to specified windows. The text follows Python' - ' escaping rules. So you can use escapes like |_ \\x1b| to send control codes' - ' and |_ \\u21fa| to send unicode characters. If you use the |_ --match| option' - ' the text will be sent to all matched windows. By default, text is sent to' - ' only the currently active window.', - options_spec=MATCH_WINDOW_OPTION + '''\n ---stdin -type=bool-set -Read the text to be sent from |_ stdin|. Note that in this case the text is sent as is, -not interpreted for escapes. If stdin is a terminal, you can press Ctrl-D to end reading. - - ---from-file -Path to a file whose contents you wish to send. Note that in this case the file contents -are sent as is, not interpreted for escapes. -''', - no_response=True, - argspec='[TEXT TO SEND]' -) -def cmd_send_text(global_opts, opts, args): - limit = 1024 - ret = {'match': opts.match, 'is_binary': False} - - def pipe(src=sys.stdin): - ret['is_binary'] = True - import select - with non_blocking_read() as fd: - keep_going = True - while keep_going: - rd = select.select([fd], [], []) - if rd: - data = sys.stdin.buffer.read() - if not data: - break - data = data.decode('utf-8') - if '\x04' in data: - data = data[:data.index('\x04')] - keep_going = False - while data: - ret['text'] = data[:limit] - yield ret - data = data[limit:] - else: - break - - def chunks(text): - ret['is_binary'] = False - while text: - ret['text'] = text[:limit] - yield ret - text = text[limit:] - - def file_pipe(path): - ret['is_binary'] = True - with open(path, encoding='utf-8') as f: - while True: - data = f.read(limit) - if not data: - break - ret['text'] = data - yield ret - - sources = [] - if opts.stdin: - sources.append(pipe()) - - if opts.from_file: - sources.append(file_pipe(opts.from_file)) - - text = ' '.join(args) - sources.append(chunks(text)) - - def chain(): - for src in sources: - yield from src - return chain() - - -def send_text(boss, window, payload): - windows = [boss.active_window] - match = payload['match'] - if match: - windows = tuple(boss.match_windows(match)) - data = payload['text'].encode('utf-8') if payload['is_binary'] else parse_send_text_bytes(payload['text']) - for window in windows: - if window is not None: - window.write_to_child(data) -# }}} - - -# set_window_title {{{ -@cmd( - 'Set the window title', - 'Set the title for the specified window(s). If you use the |_ --match| option' - ' the title will be set for all matched windows. By default, only the window' - ' in which the command is run is affected. If you do not specify a title, the' - ' last title set by the child process running in the window will be used.', - options_spec=MATCH_WINDOW_OPTION, - argspec='TITLE ...' -) -def cmd_set_window_title(global_opts, opts, args): - return {'title': ' '.join(args), 'match': opts.match} - - -def set_window_title(boss, window, payload): - windows = [window or boss.active_window] - match = payload['match'] - if match: - windows = tuple(boss.match_windows(match)) - if not windows: - raise MatchError(match) - for window in windows: - if window: - window.set_title(payload['title']) -# }}} - - -# set_tab_title {{{ -@cmd( - 'Set the tab title', - 'Set the title for the specified tab(s). If you use the |_ --match| option' - ' the title will be set for all matched tabs. By default, only the tab' - ' in which the command is run is affected. If you do not specify a title, the' - ' title of the currently active window in the tab is used.', - options_spec=MATCH_TAB_OPTION, - argspec='TITLE ...' -) -def cmd_set_tab_title(global_opts, opts, args): - return {'title': ' '.join(args), 'match': opts.match} - - -def set_tab_title(boss, window, payload): - match = payload['match'] - if match: - tabs = tuple(boss.match_tabs(match)) - if not tabs: - raise MatchError(match, 'tabs') - else: - tabs = [boss.tab_for_window(window) if window else boss.active_tab] - for tab in tabs: - if tab: - tab.set_title(payload['title']) -# }}} - - -# close_window {{{ -@cmd( - 'Close the specified window(s)', - options_spec=MATCH_WINDOW_OPTION + '''\n ---self -type=bool-set -If specified close the window this command is run in, rather than the active window. -''', - argspec='' -) -def cmd_close_window(global_opts, opts, args): - return {'match': opts.match, 'self': opts.self} - - -def close_window(boss, window, payload): - match = payload['match'] - if match: - windows = tuple(boss.match_windows(match)) - if not windows: - raise MatchError(match) - else: - windows = [window if window and payload['self'] else boss.active_window] - for window in windows: - if window: - boss.close_window(window) -# }}} - - -# close_tab {{{ -@cmd( - 'Close the specified tab(s)', - options_spec=MATCH_TAB_OPTION + '''\n ---self -type=bool-set -If specified close the tab this command is run in, rather than the active tab. -''', - argspec='' -) -def cmd_close_tab(global_opts, opts, args): - return {'match': opts.match, 'self': opts.self} - - -def close_tab(boss, window, payload): - match = payload['match'] - if match: - tabs = tuple(boss.match_tabs(match)) - if not tabs: - raise MatchError(match, 'tabs') - else: - tabs = [boss.tab_for_window(window) if window and payload['self'] else boss.active_tab] - for tab in tabs: - if window: - if tab: - boss.close_tab(tab) -# }}} - - -# new_window {{{ -@cmd( - 'Open new window', - 'Open a new window in the specified tab. If you use the |_ --match| option' - ' the first matching tab is used. Otherwise the currently active tab is used.' - ' Prints out the id of the newly opened window. Any command line arguments' - ' are assumed to be the command line used to run in the new window, if none' - ' are provided, the default shell is run. For example:\n' - '|_ kitty @ new-window --title Email mutt|', - options_spec=MATCH_TAB_OPTION + '''\n ---title -The title for the new window. By default it will use the title set by the -program running in it. - - ---cwd -The initial working directory for the new window. - - ---keep-focus -type=bool-set -Keep the current window focused instead of switching to the newly opened window - - ---new-tab -type=bool-set -Open a new tab - - ---tab-title -When using --new-tab set the title of the tab. -''', - argspec='[CMD ...]' -) -def cmd_new_window(global_opts, opts, args): - return {'match': opts.match, 'title': opts.title, 'cwd': opts.cwd, - 'new_tab': opts.new_tab, 'tab_title': opts.tab_title, - 'keep_focus': opts.keep_focus, 'args': args or []} - - -def new_window(boss, window, payload): - w = SpecialWindow(cmd=payload['args'] or None, override_title=payload['title'], cwd=payload['cwd']) - old_window = boss.active_window - if payload['new_tab']: - boss._new_tab(w) - tab = boss.active_tab - if payload['tab_title']: - tab.set_title(payload['tab_title']) - wid = boss.active_window.id - if payload['keep_focus'] and old_window: - boss.set_active_window(old_window) - return str(wid) - - match = payload['match'] - if match: - tabs = tuple(boss.match_tabs(match)) - if not tabs: - raise MatchError(match, 'tabs') - else: - tabs = [boss.active_tab] - tab = tabs[0] - w = tab.new_special_window(w) - if payload['keep_focus'] and old_window: - boss.set_active_window(old_window) - return str(w.id) -# }}} - - -# focus_window {{{ -@cmd( - 'Focus the specified window', - options_spec=MATCH_WINDOW_OPTION, - argspec='', -) -def cmd_focus_window(global_opts, opts, args): - return {'match': opts.match} - - -def focus_window(boss, window, payload): - windows = [window or boss.active_window] - match = payload['match'] - if match: - windows = tuple(boss.match_windows(match)) - if not windows: - raise MatchError(match) - for window in windows: - if window: - boss.set_active_window(window) - break -# }}} - - -# focus_tab {{{ -@cmd( - 'Focus the specified tab', - 'The active window in the specified tab will be focused.', - options_spec=MATCH_TAB_OPTION, - argspec='', -) -def cmd_focus_tab(global_opts, opts, args): - return {'match': opts.match} - - -def focus_tab(boss, window, payload): - match = payload['match'] - tabs = tuple(boss.match_tabs(match)) - if not tabs: - raise MatchError(match, 'tabs') - tab = tabs[0] - boss.set_active_tab(tab) -# }}} - - -# get_text {{{ -@cmd( - 'Get text from the specified window', - options_spec=MATCH_WINDOW_OPTION + '''\n ---extent -default=screen -choices=screen, all, selection -What text to get. The default of screen means all text currently on the screen. all means -all the screen+scrollback and selection means currently selected text. - - ---ansi -type=bool-set -By default, only plain text is returned. If you specify this flag, the text will -include the formatting escape codes for colors/bold/italic/etc. Note that when -getting the current selection, the result is always plain text. - - ---self -type=bool-set -If specified get text from the window this command is run in, rather than the active window. -''', - argspec='' -) -def cmd_get_text(global_opts, opts, args): - return {'match': opts.match, 'extent': opts.extent, 'ansi': opts.ansi, 'self': opts.self} - - -def get_text(boss, window, payload): - match = payload['match'] - if match: - windows = tuple(boss.match_windows(match)) - if not windows: - raise MatchError(match) - else: - windows = [window if window and payload['self'] else boss.active_window] - window = windows[0] - if payload['extent'] == 'selection': - ans = window.text_for_selection() - else: - ans = window.as_text(as_ansi=bool(payload['ansi']), add_history=True) - return ans -# }}} - - -# set_colors {{{ -@cmd( - 'Set terminal colors', - 'Set the terminal colors for the specified windows/tabs (defaults to active window). You can either specify the path to a conf file' - ' (in the same format as kitty.conf) to read the colors from or you can specify individual colors,' - ' for example: kitty @ set-colors foreground=red background=white', - options_spec='''\ ---all -a -type=bool-set -By default, colors are only changed for the currently active window. This option will -cause colors to be changed in all windows. - - ---configured -c -type=bool-set -Also change the configured colors (i.e. the colors kitty will use for new -windows or after a reset). - - ---reset -type=bool-set -Restore all colors to the values they had at kitty startup. Note that if you specify -this option, any color arguments are ignored and --configured and --all are implied. -''' + '\n\n' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t'), - argspec='COLOR_OR_FILE ...' -) -def cmd_set_colors(global_opts, opts, args): - from .rgb import color_as_int, Color - colors = {} - if not opts.reset: - for spec in args: - if '=' in spec: - colors.update(parse_config((spec.replace('=', ' '),))) - else: - with open(os.path.expanduser(spec), encoding='utf-8', errors='replace') as f: - colors.update(parse_config(f)) - colors = {k: color_as_int(v) for k, v in colors.items() if isinstance(v, Color)} - return { - 'title': ' '.join(args), 'match_window': opts.match, 'match_tab': opts.match_tab, - 'all': opts.all or opts.reset, 'configured': opts.configured or opts.reset, 'colors': colors, 'reset': opts.reset - } - - -def set_colors(boss, window, payload): - from .rgb import color_as_int - if payload['all']: - windows = tuple(boss.all_windows) - else: - windows = (window or boss.active_window,) - if payload['match_window']: - windows = tuple(boss.match_windows(payload['match_window'])) - if not windows: - raise MatchError(payload['match_window']) - if payload['match_tab']: - tabs = tuple(boss.match_tabs(payload['match_tab'])) - if not tabs: - raise MatchError(payload['match_tab'], 'tabs') - for tab in tabs: - windows += tuple(tab) - if payload['reset']: - payload['colors'] = {k: color_as_int(v) for k, v in boss.startup_colors.items()} - profiles = tuple(w.screen.color_profile for w in windows) - from .fast_data_types import patch_color_profiles - patch_color_profiles(payload['colors'], profiles, payload['configured']) - boss.patch_colors(payload['colors'], payload['configured']) - default_bg_changed = 'background' in payload['colors'] - for w in windows: - if default_bg_changed: - boss.default_bg_changed_for(w.id) - w.refresh() -# }}} - - -cmap = {v.name: v for v in globals().values() if hasattr(v, 'is_cmd')} +from .utils import parse_address_spec, read_with_timeout +from .cmds import cmap, parse_subcommand_cli def handle_cmd(boss, window, cmd):