Get rid of the horrible argparse
It's slow, bloated and has no support for decent output formatting
This commit is contained in:
parent
fb41ecb2e5
commit
0cac74d39a
@ -5,7 +5,7 @@
|
||||
from gettext import gettext as _
|
||||
from weakref import WeakValueDictionary
|
||||
|
||||
from .cli import create_opts, option_parser
|
||||
from .cli import create_opts, parse_args
|
||||
from .config import MINIMUM_FONT_SIZE, cached_values, initial_window_size
|
||||
from .constants import set_boss, wakeup
|
||||
from .fast_data_types import (
|
||||
@ -102,7 +102,8 @@ class Boss:
|
||||
msg = json.loads(msg.decode('utf-8'))
|
||||
if isinstance(msg, dict) and msg.get('cmd') == 'new_instance':
|
||||
startup_id = msg.get('startup_id')
|
||||
args = option_parser().parse_args(msg['args'][1:])
|
||||
args, rest = parse_args(msg['args'][1:])
|
||||
args.args = rest
|
||||
opts = create_opts(args)
|
||||
session = create_session(opts, args)
|
||||
os_window_id = self.add_os_window(session, wclass=args.cls, wname=args.name, size=initial_window_size(opts), visible=False)
|
||||
|
||||
485
kitty/cli.py
485
kitty/cli.py
@ -2,142 +2,367 @@
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import argparse
|
||||
from gettext import gettext as _
|
||||
import re
|
||||
import sys
|
||||
from collections import deque
|
||||
|
||||
from .config import load_config
|
||||
from .constants import appname, str_version, is_macos, defconf
|
||||
from .constants import appname, defconf, is_macos, str_version
|
||||
from .layout import all_layouts
|
||||
|
||||
|
||||
def option_parser():
|
||||
parser = argparse.ArgumentParser(
|
||||
prog=appname,
|
||||
description=_('The {} terminal emulator').format(appname)
|
||||
)
|
||||
a = parser.add_argument
|
||||
a(
|
||||
'--class',
|
||||
default=appname,
|
||||
dest='cls',
|
||||
help=_('Set the class part of the WM_CLASS property')
|
||||
)
|
||||
a(
|
||||
'--name',
|
||||
default=None,
|
||||
dest='name',
|
||||
help=_('Set the name part of the WM_CLASS property (defaults to using the value from {})').format('--class')
|
||||
)
|
||||
a(
|
||||
'--config',
|
||||
action='append',
|
||||
help=_(
|
||||
'Specify a path to the config file(s) to use.'
|
||||
' Can be specified multiple times to read multiple'
|
||||
' config files in sequence, which are merged. Default: {}'
|
||||
).format(defconf)
|
||||
)
|
||||
a(
|
||||
'--override',
|
||||
'-o',
|
||||
action='append',
|
||||
help=_(
|
||||
'Override individual configuration options, can be specified'
|
||||
' multiple times. Syntax: name=value. For example: {}'
|
||||
).format('-o font_size=20')
|
||||
)
|
||||
a(
|
||||
'--cmd',
|
||||
'-c',
|
||||
default=None,
|
||||
help=_('Run python code in the kitty context')
|
||||
)
|
||||
a(
|
||||
'-d',
|
||||
'--directory',
|
||||
default='.',
|
||||
help=_('Change to the specified directory when launching')
|
||||
)
|
||||
a(
|
||||
'--version',
|
||||
'-v',
|
||||
action='version',
|
||||
version='{} {} by Kovid Goyal'.format(appname, str_version)
|
||||
)
|
||||
a(
|
||||
'--dump-commands',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help=_('Output commands received from child process to stdout')
|
||||
)
|
||||
if not is_macos:
|
||||
a(
|
||||
'--detach',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help=_('Detach from the controlling terminal, if any')
|
||||
is_macos
|
||||
OPTIONS = '''
|
||||
--class
|
||||
dest=cls
|
||||
default={appname}
|
||||
condition=not is_macos
|
||||
Set the class part of the |_ WM_CLASS| window property
|
||||
|
||||
|
||||
--name
|
||||
condition=not is_macos
|
||||
Set the name part of the |_ WM_CLASS| property (defaults to using the value from |_ --class|)
|
||||
|
||||
|
||||
--config
|
||||
type=list
|
||||
default={config_path}
|
||||
Specify a path to the configuration file(s) to use.
|
||||
Can be specified multiple times to read multiple configuration files in sequence, which are merged.
|
||||
Default: |_ %default|
|
||||
|
||||
|
||||
--override -o
|
||||
type=list
|
||||
Override individual configuration options, can be specified multiple times.
|
||||
Syntax: |_ name=value|. For example: |_ -o font_size=20|
|
||||
|
||||
|
||||
--cmd -c
|
||||
Run python code in the kitty context
|
||||
|
||||
|
||||
--directory -d
|
||||
default=.
|
||||
Change to the specified directory when launching
|
||||
|
||||
|
||||
--detach
|
||||
type=bool-set
|
||||
condition=not is_macos
|
||||
Detach from the controlling terminal, if any
|
||||
|
||||
|
||||
--window-layout
|
||||
type=choices
|
||||
choices={window_layout_choices}
|
||||
The window layout to use on startup
|
||||
|
||||
|
||||
--session
|
||||
Path to a file containing the startup |_ session| (tabs, windows, layout, programs)
|
||||
|
||||
|
||||
--single-instance -1
|
||||
type=bool-set
|
||||
If specified only a single instance of {appname} will run. New invocations will
|
||||
instead create a new top-level window in the existing {appname} instance. This
|
||||
allows {appname} to share a single sprite cache on the GPU and also reduces
|
||||
startup time. You can also have separate groups of {appname} instances by using the
|
||||
|_ --instance-group| option
|
||||
|
||||
|
||||
--instance-group
|
||||
Used in combination with the |_ --single-instance| option. All {appname} invocations
|
||||
with the same |_ --instance-group| will result in new windows being created
|
||||
in the first {appname} instance with that group
|
||||
|
||||
|
||||
# Debugging options
|
||||
|
||||
--version -v
|
||||
The current {appname} version
|
||||
|
||||
|
||||
--dump-commands
|
||||
type=bool-set
|
||||
Output commands received from child process to stdout
|
||||
|
||||
|
||||
--replay-commands
|
||||
type=bool-set
|
||||
Replay previously dumped commands
|
||||
|
||||
|
||||
--dump-bytes
|
||||
Path to file in which to store the raw bytes received from the child process
|
||||
|
||||
|
||||
--debug-gl
|
||||
type=bool-set
|
||||
Debug OpenGL commands. This will cause all OpenGL calls to check for errors instead of ignoring them. Useful when debugging rendering problems
|
||||
'''
|
||||
|
||||
|
||||
def surround(x, start, end):
|
||||
if sys.stdout.isatty():
|
||||
x = '\033[{}m{}\033[{}m'.format(start, x, end)
|
||||
return x
|
||||
|
||||
|
||||
def emph(x):
|
||||
return surround(x, 91, 39)
|
||||
|
||||
|
||||
def cyan(x):
|
||||
return surround(x, 96, 39)
|
||||
|
||||
|
||||
def green(x):
|
||||
return surround(x, 32, 39)
|
||||
|
||||
|
||||
def blue(x):
|
||||
return surround(x, 34, 39)
|
||||
|
||||
|
||||
def yellow(x):
|
||||
return surround(x, 93, 39)
|
||||
|
||||
|
||||
def italic(x):
|
||||
return surround(x, 3, 23)
|
||||
|
||||
|
||||
def bold(x):
|
||||
return surround(x, 1, 22)
|
||||
|
||||
|
||||
def title(x):
|
||||
return blue(bold(x))
|
||||
|
||||
|
||||
def parse_option_spec(spec=OPTIONS):
|
||||
NORMAL, METADATA, HELP = 'NORMAL', 'METADATA', 'HELP'
|
||||
state = NORMAL
|
||||
lines = spec.splitlines()
|
||||
prev_line = ''
|
||||
seq = []
|
||||
disabled = []
|
||||
mpat = re.compile('([a-z]+)=(.+)')
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if state is NORMAL:
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith('# '):
|
||||
seq.append(line[2:])
|
||||
continue
|
||||
if line.startswith('--'):
|
||||
parts = line.split(' ')
|
||||
current_cmd = {'dest': parts[0][2:].replace('-', '_'), 'aliases': frozenset(parts), 'help': ''}
|
||||
state = METADATA
|
||||
continue
|
||||
raise ValueError('Invalid option spec, unexpected line: {}'.format(line))
|
||||
elif state is METADATA:
|
||||
m = mpat.match(line)
|
||||
if m is None:
|
||||
state = HELP
|
||||
current_cmd['help'] += line
|
||||
else:
|
||||
k, v = m.group(1), m.group(2)
|
||||
if k == 'condition':
|
||||
v = eval(v)
|
||||
current_cmd[k] = v
|
||||
if k == 'choices':
|
||||
current_cmd['choices'] = {x.strip() for x in current_cmd['choices'].split(',')}
|
||||
elif state is HELP:
|
||||
if line:
|
||||
current_cmd['help'] += ' ' + line
|
||||
else:
|
||||
if prev_line:
|
||||
current_cmd['help'] += '\n'
|
||||
else:
|
||||
state = NORMAL
|
||||
(seq if current_cmd.get('condition', True) else disabled).append(current_cmd)
|
||||
current_cmd = None
|
||||
prev_line = line
|
||||
if current_cmd is not None:
|
||||
(seq if current_cmd.get('condition', True) else disabled).append(current_cmd)
|
||||
|
||||
return seq, disabled
|
||||
|
||||
|
||||
def prettify(text):
|
||||
|
||||
def sub(m):
|
||||
t = m.group(2)
|
||||
for ch in m.group(1):
|
||||
t = {'C': cyan, '_': italic, '*': bold}[ch](t)
|
||||
return t
|
||||
|
||||
text = re.sub(r'[|]([a-zA-Z_*]+?) (.+?)[|]', sub, text)
|
||||
return text
|
||||
|
||||
|
||||
def version():
|
||||
return '{} {} created by {}'.format(italic(appname), green(str_version), title('Kovid Goyal'))
|
||||
|
||||
|
||||
def print_help_for_seq(seq):
|
||||
blocks = []
|
||||
a = blocks.append
|
||||
a('{}: {} [options] [program-to-run ...]'.format(title('Usage'), bold(yellow(appname))))
|
||||
a('')
|
||||
a('Run the {appname} terminal emulator. You can also specify the {program} to run inside {appname} as normal'
|
||||
' arguments following the {options}. For example: {appname} /bin/sh'.format(
|
||||
appname=italic(appname), options=italic('options'), program=italic('program')))
|
||||
a('')
|
||||
a('{}:'.format(title('Options')))
|
||||
for opt in seq:
|
||||
if isinstance(opt, str):
|
||||
a('{}:'.format(title(opt)))
|
||||
continue
|
||||
a(' ' + ', '.join(map(green, sorted(opt['aliases']))))
|
||||
if opt.get('help'):
|
||||
t = opt['help'].replace('%default', str(opt.get('default')))
|
||||
a(' ' * 4 + prettify(t))
|
||||
|
||||
print('\n'.join(blocks))
|
||||
print('\n' + version())
|
||||
|
||||
|
||||
def defval_for_opt(opt):
|
||||
dv = opt.get('default')
|
||||
typ = opt.get('type', '')
|
||||
if typ.startswith('bool-'):
|
||||
if dv is None:
|
||||
dv = False if typ == 'bool-set' else True
|
||||
else:
|
||||
dv = dv.lower() in ('true', 'yes', 'y')
|
||||
elif typ == 'list':
|
||||
dv = []
|
||||
return dv
|
||||
|
||||
|
||||
class Options:
|
||||
|
||||
def __init__(self, seq):
|
||||
self.alias_map = {}
|
||||
self.seq = seq
|
||||
self.names_map = {}
|
||||
self.values_map = {}
|
||||
for opt in seq:
|
||||
if isinstance(opt, str):
|
||||
continue
|
||||
for alias in opt['aliases']:
|
||||
self.alias_map[alias] = opt
|
||||
name = opt['dest']
|
||||
self.names_map[name] = opt
|
||||
self.values_map[name] = defval_for_opt(opt)
|
||||
|
||||
def opt_for_alias(self, alias):
|
||||
opt = self.alias_map.get(alias)
|
||||
if opt is None:
|
||||
raise SystemExit('Unknown option: {}'.format(emph(alias)))
|
||||
return opt
|
||||
|
||||
def needs_arg(self, alias):
|
||||
if alias in ('-h', '--help'):
|
||||
print_help_for_seq(self.seq)
|
||||
raise SystemExit(0)
|
||||
opt = self.opt_for_alias(alias)
|
||||
if opt['dest'] == 'version':
|
||||
print(version())
|
||||
raise SystemExit(0)
|
||||
typ = opt.get('type', '')
|
||||
return not typ.startswith('bool-')
|
||||
|
||||
def process_arg(self, alias, val=None):
|
||||
opt = self.opt_for_alias(alias)
|
||||
typ = opt.get('type', '')
|
||||
name = opt['dest']
|
||||
if typ == 'bool-set':
|
||||
self.values_map[name] = True
|
||||
elif typ == 'bool-reset':
|
||||
self.values_map[name] = False
|
||||
elif typ == 'list':
|
||||
self.values_map.setdefault(name, [])
|
||||
self.values_map[name].append(val)
|
||||
elif typ == 'choices':
|
||||
choices = opt['choices']
|
||||
if val not in choices:
|
||||
raise SystemExit('{} is not a valid value for the {} option. Valid values are: {}'.format(
|
||||
val, emph(alias), ', '.join(choices)))
|
||||
self.values_map[name] = val
|
||||
else:
|
||||
self.values_map[name] = val
|
||||
|
||||
|
||||
class Namespace:
|
||||
|
||||
def __init__(self, kwargs):
|
||||
for name in kwargs:
|
||||
setattr(self, name, kwargs[name])
|
||||
|
||||
|
||||
def parse_cmdline(options, args=None):
|
||||
NORMAL, EXPECTING_ARG = 'NORMAL', 'EXPECTING_ARG'
|
||||
state = NORMAL
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
args = deque(args)
|
||||
seq, disabled = options
|
||||
oc = Options(seq)
|
||||
current_option = None
|
||||
|
||||
while args:
|
||||
arg = args.popleft()
|
||||
if state is NORMAL:
|
||||
if arg.startswith('-'):
|
||||
if arg == '--':
|
||||
break
|
||||
parts = arg.split('=', 1)
|
||||
needs_arg = oc.needs_arg(parts[0])
|
||||
if not needs_arg:
|
||||
if len(parts) != 1:
|
||||
raise SystemExit('The {} option does not accept arguments'.format(emph(parts[0])))
|
||||
oc.process_arg(parts[0])
|
||||
continue
|
||||
if len(parts) == 1:
|
||||
current_option = parts[0]
|
||||
state = EXPECTING_ARG
|
||||
continue
|
||||
oc.process_arg(parts[0], parts[1])
|
||||
else:
|
||||
args = [arg] + list(args)
|
||||
break
|
||||
else:
|
||||
oc.process_arg(current_option, arg)
|
||||
current_option, state = None, NORMAL
|
||||
if state is EXPECTING_ARG:
|
||||
raise SystemExit('An argument is required for the option: {}'.format(emph(arg)))
|
||||
|
||||
ans = Namespace(oc.values_map)
|
||||
for opt in disabled:
|
||||
setattr(ans, opt['dest'], defval_for_opt(opt))
|
||||
return ans, list(args)
|
||||
|
||||
|
||||
def options_spec():
|
||||
if not hasattr(options_spec, 'ans'):
|
||||
options_spec.ans = OPTIONS.format(
|
||||
appname=appname, config_path=defconf,
|
||||
window_layout_choices=','.join(all_layouts)
|
||||
)
|
||||
a(
|
||||
'--replay-commands',
|
||||
default=None,
|
||||
help=_('Replay previously dumped commands')
|
||||
)
|
||||
a(
|
||||
'--dump-bytes',
|
||||
help=_('Path to file in which to store the raw bytes received from the'
|
||||
' child process. Useful for debugging.')
|
||||
)
|
||||
a(
|
||||
'--debug-gl',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help=_('Debug OpenGL commands. This will cause all OpenGL calls'
|
||||
' to check for errors instead of ignoring them. Useful'
|
||||
' when debugging rendering problems.')
|
||||
)
|
||||
a(
|
||||
'--window-layout',
|
||||
default=None,
|
||||
choices=frozenset(all_layouts.keys()),
|
||||
help=_('The window layout to use on startup')
|
||||
)
|
||||
a(
|
||||
'--session',
|
||||
default=None,
|
||||
help=_(
|
||||
'Path to a file containing the startup session (tabs, windows, layout, programs)'
|
||||
)
|
||||
)
|
||||
a(
|
||||
'-1', '--single-instance',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help=_(
|
||||
'If specified only a single instance of {0} will run. New invocations will'
|
||||
' instead create a new top-level window in the existing {0} instance. This'
|
||||
' allows {0} to share a single sprite cache on the GPU and also reduces'
|
||||
' startup time. You can also have groups of {0} instances by using the'
|
||||
' {1} option.'
|
||||
).format(appname, '--instance-group')
|
||||
)
|
||||
a(
|
||||
'--instance-group',
|
||||
default=None,
|
||||
help=_(
|
||||
'Used in combination with the --single-instance option. All {0} invocations'
|
||||
' with the same --instance-group will result in new windows being created'
|
||||
' in the first {0} instance with that group.'
|
||||
).format(appname)
|
||||
)
|
||||
a(
|
||||
'args',
|
||||
nargs=argparse.REMAINDER,
|
||||
help=_(
|
||||
'The remaining arguments are used to launch a program other than the default shell. Any further options are passed'
|
||||
' directly to the program being invoked.'
|
||||
)
|
||||
)
|
||||
return parser
|
||||
return options_spec.ans
|
||||
|
||||
|
||||
def parse_args(args=None):
|
||||
options = parse_option_spec(options_spec())
|
||||
return parse_cmdline(options, args=args)
|
||||
|
||||
|
||||
def create_opts(args):
|
||||
|
||||
@ -10,7 +10,7 @@ from contextlib import contextmanager
|
||||
|
||||
from .borders import load_borders_program
|
||||
from .boss import Boss
|
||||
from .cli import create_opts, option_parser
|
||||
from .cli import create_opts, parse_args
|
||||
from .config import initial_window_size, load_cached_values, save_cached_values
|
||||
from .constants import glfw_path, is_macos, is_wayland, logo_data_file
|
||||
from .fast_data_types import (
|
||||
@ -116,7 +116,8 @@ def main():
|
||||
os.chdir(os.path.expanduser('~'))
|
||||
if not os.path.isdir(os.getcwd()):
|
||||
os.chdir(os.path.expanduser('~'))
|
||||
args = option_parser().parse_args()
|
||||
args, rest = parse_args()
|
||||
args.args = rest
|
||||
if getattr(args, 'detach', False):
|
||||
detach()
|
||||
if args.cmd:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user