620 lines
22 KiB
Python
620 lines
22 KiB
Python
#!/usr/bin/env python
|
|
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
import traceback
|
|
from enum import Enum, auto
|
|
from gettext import gettext as _
|
|
from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Union
|
|
|
|
from kitty.cli import CompletionSpec, create_default_opts, parse_args
|
|
from kitty.cli_stub import ThemesCLIOptions
|
|
from kitty.config import cached_values_for
|
|
from kitty.constants import config_dir
|
|
from kitty.fast_data_types import truncate_point_for_length, wcswidth
|
|
from kitty.options.types import Options as KittyOptions
|
|
from kitty.rgb import color_as_sharp, color_from_int
|
|
from kitty.typing import KeyEventType
|
|
from kitty.utils import ScreenSize
|
|
|
|
from ..tui.handler import Handler
|
|
from ..tui.line_edit import LineEdit
|
|
from ..tui.loop import Loop
|
|
from ..tui.operations import color_code, set_default_colors, styled
|
|
from .collection import MARK_AFTER, NoCacheFound, Theme, Themes, load_themes
|
|
|
|
separator = '║'
|
|
|
|
|
|
def format_traceback(msg: str) -> str:
|
|
return traceback.format_exc() + '\n\n' + styled(msg, fg='red')
|
|
|
|
|
|
def limit_length(text: str, limit: int = 32) -> str:
|
|
x = truncate_point_for_length(text, limit - 1)
|
|
if x >= len(text):
|
|
return text
|
|
return f'{text[:x]}…'
|
|
|
|
|
|
class State(Enum):
|
|
fetching = auto()
|
|
browsing = auto()
|
|
searching = auto()
|
|
accepting = auto()
|
|
|
|
|
|
def dark_filter(q: Theme) -> bool:
|
|
return q.is_dark
|
|
|
|
|
|
def light_filter(q: Theme) -> bool:
|
|
return not q.is_dark
|
|
|
|
|
|
def all_filter(q: Theme) -> bool:
|
|
return True
|
|
|
|
|
|
def create_recent_filter(names: Iterable[str]) -> Callable[[Theme], bool]:
|
|
allowed = frozenset(names)
|
|
|
|
def recent_filter(q: Theme) -> bool:
|
|
return q.name in allowed
|
|
|
|
return recent_filter
|
|
|
|
|
|
def user_filter(q: Theme) -> bool:
|
|
return q.is_user_defined
|
|
|
|
|
|
def mark_shortcut(text: str, acc: str) -> str:
|
|
acc_idx = text.lower().index(acc.lower())
|
|
return text[:acc_idx] + styled(text[acc_idx], underline='straight', bold=True, fg_intense=True) + text[acc_idx+1:]
|
|
|
|
|
|
class ThemesList:
|
|
|
|
def __init__(self) -> None:
|
|
self.themes = Themes()
|
|
self.current_search: str = ''
|
|
self.display_strings: Tuple[str, ...] = ()
|
|
self.widths: Tuple[int, ...] = ()
|
|
self.max_width = 0
|
|
self.current_idx = 0
|
|
|
|
def __bool__(self) -> bool:
|
|
return bool(self.display_strings)
|
|
|
|
def __len__(self) -> int:
|
|
return len(self.themes)
|
|
|
|
def next(self, delta: int = 1, allow_wrapping: bool = True) -> bool:
|
|
if not self:
|
|
return False
|
|
idx = self.current_idx + delta
|
|
if not allow_wrapping and (idx < 0 or idx >= len(self)):
|
|
return False
|
|
while idx < 0:
|
|
idx += len(self)
|
|
self.current_idx = idx % len(self)
|
|
return True
|
|
|
|
def update_themes(self, themes: Themes) -> None:
|
|
self.themes = self.all_themes = themes
|
|
if self.current_search:
|
|
self.themes = self.all_themes.copy()
|
|
self.display_strings = tuple(map(limit_length, self.themes.apply_search(self.current_search)))
|
|
else:
|
|
self.display_strings = tuple(map(limit_length, (t.name for t in self.themes)))
|
|
self.widths = tuple(map(wcswidth, self.display_strings))
|
|
self.max_width = max(self.widths) if self.widths else 0
|
|
self.current_idx = 0
|
|
|
|
def update_search(self, search: str = '') -> bool:
|
|
if search == self.current_search:
|
|
return False
|
|
self.current_search = search
|
|
self.update_themes(self.all_themes)
|
|
return True
|
|
|
|
def lines(self, num_rows: int) -> Iterator[Tuple[str, int, bool]]:
|
|
if num_rows < 1:
|
|
return
|
|
before_num = min(self.current_idx, num_rows - 1)
|
|
start = self.current_idx - before_num
|
|
for i in range(start, min(start + num_rows, len(self.display_strings))):
|
|
line = self.display_strings[i]
|
|
yield line, self.widths[i], i == self.current_idx
|
|
|
|
@property
|
|
def current_theme(self) -> Theme:
|
|
return self.themes[self.current_idx]
|
|
|
|
|
|
def colors_as_escape_codes(o: KittyOptions) -> str:
|
|
ans = set_default_colors(
|
|
fg=o.foreground, bg=o.background, cursor=o.cursor, select_bg=o.selection_background, select_fg=o.selection_foreground
|
|
)
|
|
cmds = []
|
|
for i in range(256):
|
|
col = color_as_sharp(color_from_int(o.color_table[i]))
|
|
cmds.append(f'{i};{col}')
|
|
return ans + '\033]4;' + ';'.join(cmds) + '\033\\'
|
|
|
|
|
|
class ThemesHandler(Handler):
|
|
|
|
def __init__(self, cached_values: Dict[str, Any], cli_opts: ThemesCLIOptions) -> None:
|
|
self.cached_values = cached_values
|
|
self.cli_opts = cli_opts
|
|
self.state = State.fetching
|
|
self.report_traceback_on_exit: Optional[str] = None
|
|
self.filter_map: Dict[str, Callable[[Theme], bool]] = {
|
|
'dark': dark_filter, 'light': light_filter, 'all': all_filter,
|
|
'recent': create_recent_filter(self.cached_values.get('recent', ())),
|
|
'user': user_filter
|
|
}
|
|
self.themes_list = ThemesList()
|
|
self.colors_set_once = False
|
|
self.line_edit = LineEdit()
|
|
self.tabs = tuple('all dark light recent user'.split())
|
|
self.quit_on_next_key_release = -1
|
|
|
|
def update_recent(self) -> None:
|
|
r = list(self.cached_values.get('recent', ()))
|
|
if self.themes_list:
|
|
name = self.themes_list.current_theme.name
|
|
r = [name] + [x for x in r if x != name]
|
|
self.cached_values['recent'] = r[:20]
|
|
|
|
def enforce_cursor_state(self) -> None:
|
|
self.cmd.set_cursor_visible(self.state == State.fetching)
|
|
|
|
def init_terminal_state(self) -> None:
|
|
self.cmd.save_colors()
|
|
self.cmd.set_line_wrapping(False)
|
|
self.cmd.set_window_title('Choose a theme for kitty')
|
|
self.cmd.set_cursor_shape('beam')
|
|
|
|
def initialize(self) -> None:
|
|
self.init_terminal_state()
|
|
self.draw_screen()
|
|
self.fetch_themes()
|
|
|
|
def finalize(self) -> None:
|
|
self.cmd.restore_colors()
|
|
self.cmd.set_cursor_visible(True)
|
|
|
|
@property
|
|
def current_category(self) -> str:
|
|
cat: str = self.cached_values.get('category', 'all')
|
|
if cat not in self.filter_map:
|
|
cat = 'all'
|
|
return cat
|
|
|
|
@current_category.setter
|
|
def current_category(self, cat: str) -> None:
|
|
if cat not in self.filter_map:
|
|
cat = 'all'
|
|
self.cached_values['category'] = cat
|
|
|
|
def set_colors_to_current_theme(self) -> bool:
|
|
if not self.themes_list and self.colors_set_once:
|
|
return False
|
|
self.colors_set_once = True
|
|
if self.themes_list:
|
|
o = self.themes_list.current_theme.kitty_opts
|
|
else:
|
|
o = create_default_opts()
|
|
self.current_opts = o
|
|
cmd = colors_as_escape_codes(o)
|
|
self.write(cmd)
|
|
return True
|
|
|
|
def redraw_after_category_change(self) -> None:
|
|
self.themes_list.update_themes(self.all_themes.filtered(self.filter_map[self.current_category]))
|
|
self.set_colors_to_current_theme()
|
|
self.draw_screen()
|
|
|
|
# Theme fetching {{{
|
|
def fetch_themes(self) -> None:
|
|
|
|
def fetching_done(themes_or_exception: Union[Themes, str]) -> None:
|
|
if isinstance(themes_or_exception, str):
|
|
self.report_traceback_on_exit = themes_or_exception
|
|
self.quit_loop(1)
|
|
return
|
|
self.all_themes: Themes = themes_or_exception
|
|
self.state = State.browsing
|
|
self.redraw_after_category_change()
|
|
|
|
def fetch() -> None:
|
|
try:
|
|
themes: Union[Themes, str] = load_themes(self.cli_opts.cache_age)
|
|
except Exception:
|
|
themes = format_traceback('Failed to download themes')
|
|
self.asyncio_loop.call_soon_threadsafe(fetching_done, themes)
|
|
|
|
self.asyncio_loop.run_in_executor(None, fetch)
|
|
self.draw_screen()
|
|
|
|
def draw_fetching_screen(self) -> None:
|
|
self.print('Downloading themes from repository, please wait...')
|
|
|
|
def on_fetching_key_event(self, key_event: KeyEventType, in_bracketed_paste: bool = False) -> None:
|
|
if key_event.matches('esc'):
|
|
self.quit_on_next_key_release = 0
|
|
return
|
|
|
|
# }}}
|
|
|
|
# Theme browsing {{{
|
|
def draw_tab_bar(self) -> None:
|
|
self.print(styled(' ' * self.screen_size.cols, reverse=True), end='\r')
|
|
|
|
def draw_tab(text: str, name: str, acc: str) -> None:
|
|
is_active = name == self.current_category
|
|
if is_active:
|
|
text = styled(f'{text} #{len(self.themes_list)}', italic=True)
|
|
else:
|
|
text = mark_shortcut(text, acc)
|
|
|
|
self.cmd.styled(f' {text} ', reverse=not is_active)
|
|
|
|
for t in self.tabs:
|
|
draw_tab(t.capitalize(), t, t[0])
|
|
self.cmd.sgr('0')
|
|
self.print()
|
|
|
|
def draw_bottom_bar(self) -> None:
|
|
self.cmd.set_cursor_position(0, self.screen_size.rows)
|
|
self.print(styled(' ' * self.screen_size.cols, reverse=True), end='\r')
|
|
for (t, sc) in (('search (/)', 's'), ('accept (⏎)', 'c')):
|
|
text = mark_shortcut(t.capitalize(), sc)
|
|
self.cmd.styled(f' {text} ', reverse=True)
|
|
self.cmd.sgr('0')
|
|
|
|
def draw_search_bar(self) -> None:
|
|
self.cmd.set_cursor_position(0, self.screen_size.rows)
|
|
self.cmd.clear_to_eol()
|
|
self.line_edit.write(self.write)
|
|
|
|
def draw_theme_demo(self) -> None:
|
|
theme = self.themes_list.current_theme
|
|
xstart = self.themes_list.max_width + 3
|
|
sz = self.screen_size.cols - xstart
|
|
if sz < 20:
|
|
return
|
|
sz -= 1
|
|
y = 0
|
|
colors = 'black red green yellow blue magenta cyan white'.split()
|
|
trunc = sz // 8 - 1
|
|
|
|
def next_line() -> None:
|
|
nonlocal y
|
|
self.write('\r')
|
|
y += 1
|
|
self.cmd.set_cursor_position(xstart - 1, y)
|
|
self.write(separator + ' ')
|
|
|
|
def write_para(text: str) -> None:
|
|
text = re.sub(r'\s+', ' ', text)
|
|
while text:
|
|
sp = truncate_point_for_length(text, sz)
|
|
self.write(text[:sp])
|
|
next_line()
|
|
text = text[sp:]
|
|
|
|
def write_colors(bg: Optional[str] = None) -> None:
|
|
for intense in (False, True):
|
|
buf = []
|
|
for c in colors:
|
|
buf.append(styled(c[:trunc], fg=c, fg_intense=intense))
|
|
self.cmd.styled(' '.join(buf), bg=bg, bg_intense=intense)
|
|
next_line()
|
|
next_line()
|
|
|
|
self.cmd.set_cursor_position()
|
|
next_line()
|
|
self.cmd.styled(theme.name.center(sz), bold=True, fg='green')
|
|
next_line()
|
|
if theme.author:
|
|
self.cmd.styled(theme.author.center(sz), italic=True)
|
|
next_line()
|
|
if theme.blurb:
|
|
next_line()
|
|
write_para(theme.blurb)
|
|
next_line()
|
|
write_colors()
|
|
|
|
for bg in colors:
|
|
write_colors(bg)
|
|
|
|
def draw_browsing_screen(self) -> None:
|
|
self.draw_tab_bar()
|
|
num_rows = self.screen_size.rows - 2
|
|
mw = self.themes_list.max_width + 1
|
|
for line, width, is_current in self.themes_list.lines(num_rows):
|
|
num_rows -= 1
|
|
if is_current:
|
|
line = line.replace(MARK_AFTER, f'\033[{color_code("green")}m')
|
|
self.cmd.styled('>' if is_current else ' ', fg='green')
|
|
self.cmd.styled(line, bold=is_current, fg='green' if is_current else None)
|
|
self.cmd.move_cursor_by(mw - width, 'right')
|
|
self.print(separator)
|
|
if self.themes_list:
|
|
self.draw_theme_demo()
|
|
self.draw_bottom_bar() if self.state is State.browsing else self.draw_search_bar()
|
|
|
|
def on_searching_key_event(self, key_event: KeyEventType, in_bracketed_paste: bool = False) -> None:
|
|
if key_event.matches('enter'):
|
|
self.state = State.browsing
|
|
self.draw_bottom_bar()
|
|
return
|
|
if key_event.matches('esc'):
|
|
self.state = State.browsing
|
|
self.themes_list.update_search('')
|
|
self.set_colors_to_current_theme()
|
|
return self.draw_screen()
|
|
if key_event.text:
|
|
self.line_edit.on_text(key_event.text, in_bracketed_paste)
|
|
else:
|
|
if not self.line_edit.on_key(key_event):
|
|
if key_event.matches('left') or key_event.matches('shift+tab'):
|
|
return self.next_category(-1)
|
|
if key_event.matches('right') or key_event.matches('tab'):
|
|
return self.next_category(1)
|
|
if key_event.matches('down'):
|
|
return self.next(delta=1)
|
|
if key_event.matches('up'):
|
|
return self.next(delta=-1)
|
|
if key_event.matches('page_down'):
|
|
return self.next(delta=self.screen_size.rows - 3, allow_wrapping=False)
|
|
if key_event.matches('page_up'):
|
|
return self.next(delta=3 - self.screen_size.rows, allow_wrapping=False)
|
|
return
|
|
if self.line_edit.current_input:
|
|
q = self.line_edit.current_input[1:]
|
|
if self.themes_list.update_search(q):
|
|
self.set_colors_to_current_theme()
|
|
self.draw_screen()
|
|
else:
|
|
self.draw_search_bar()
|
|
else:
|
|
self.state = State.browsing
|
|
self.draw_bottom_bar()
|
|
|
|
def on_browsing_key_event(self, key_event: KeyEventType, in_bracketed_paste: bool = False) -> None:
|
|
if key_event.matches('esc') or key_event.matches_text('q'):
|
|
self.quit_on_next_key_release = 0
|
|
return
|
|
for cat in self.tabs:
|
|
if key_event.matches_text(cat[0]) or key_event.matches(f'alt+{cat[0]}'):
|
|
if cat != self.current_category:
|
|
self.current_category = cat
|
|
self.redraw_after_category_change()
|
|
return
|
|
if key_event.matches('left') or key_event.matches('shift+tab'):
|
|
return self.next_category(-1)
|
|
if key_event.matches('right') or key_event.matches('tab'):
|
|
return self.next_category(1)
|
|
if key_event.matches_text('j') or key_event.matches('down'):
|
|
return self.next(delta=1)
|
|
if key_event.matches_text('k') or key_event.matches('up'):
|
|
return self.next(delta=-1)
|
|
if key_event.matches('page_down'):
|
|
return self.next(delta=self.screen_size.rows - 3, allow_wrapping=False)
|
|
if key_event.matches('page_up'):
|
|
return self.next(delta=3 - self.screen_size.rows, allow_wrapping=False)
|
|
if key_event.matches_text('s') or key_event.matches('/'):
|
|
return self.start_search()
|
|
if key_event.matches_text('c') or key_event.matches('enter'):
|
|
if not self.themes_list:
|
|
self.cmd.beep()
|
|
return
|
|
self.state = State.accepting
|
|
return self.draw_screen()
|
|
|
|
def start_search(self) -> None:
|
|
self.line_edit.clear()
|
|
self.line_edit.add_text('/' + self.themes_list.current_search)
|
|
self.state = State.searching
|
|
self.draw_screen()
|
|
|
|
def next_category(self, delta: int = 1) -> None:
|
|
idx = self.tabs.index(self.current_category) + delta + len(self.tabs)
|
|
self.current_category = self.tabs[idx % len(self.tabs)]
|
|
self.redraw_after_category_change()
|
|
|
|
def next(self, delta: int = 1, allow_wrapping: bool = True) -> None:
|
|
if self.themes_list.next(delta, allow_wrapping):
|
|
self.set_colors_to_current_theme()
|
|
self.draw_screen()
|
|
else:
|
|
self.cmd.bell()
|
|
# }}}
|
|
|
|
# Accepting {{{
|
|
def draw_accepting_screen(self) -> None:
|
|
name = self.themes_list.current_theme.name
|
|
name = styled(name, bold=True, fg="green")
|
|
kc = styled(self.cli_opts.config_file_name, italic=True)
|
|
|
|
def ac(x: str) -> str:
|
|
return styled(x, fg='red')
|
|
|
|
self.cmd.set_line_wrapping(True)
|
|
self.print(f'You have chosen the {name} theme')
|
|
self.print()
|
|
self.print('What would you like to do?')
|
|
self.print()
|
|
self.print(' ', f'{ac("M")}odify {kc} to load', styled(name, bold=True, fg="green"))
|
|
self.print()
|
|
self.print(' ', f'{ac("P")}lace the theme file in {config_dir} but do not modify {kc}')
|
|
self.print()
|
|
self.print(' ', f'{ac("A")}bort and return to list of themes')
|
|
self.print()
|
|
self.print(' ', f'{ac("Q")}uit')
|
|
|
|
def on_accepting_key_event(self, key_event: KeyEventType, in_bracketed_paste: bool = False) -> None:
|
|
if key_event.matches_text('q') or key_event.matches('esc'):
|
|
self.quit_on_next_key_release = 0
|
|
return
|
|
if key_event.matches_text('a'):
|
|
self.state = State.browsing
|
|
self.draw_screen()
|
|
return
|
|
if key_event.matches_text('p'):
|
|
self.themes_list.current_theme.save_in_dir(config_dir)
|
|
self.update_recent()
|
|
self.quit_on_next_key_release = 0
|
|
return
|
|
if key_event.matches_text('m'):
|
|
self.themes_list.current_theme.save_in_conf(config_dir, self.cli_opts.reload_in, self.cli_opts.config_file_name)
|
|
self.update_recent()
|
|
self.quit_on_next_key_release = 0
|
|
return
|
|
# }}}
|
|
|
|
def on_key_event(self, key_event: KeyEventType, in_bracketed_paste: bool = False) -> None:
|
|
if self.quit_on_next_key_release > -1 and key_event.is_release:
|
|
self.quit_loop(self.quit_on_next_key_release)
|
|
return
|
|
if self.state is State.fetching:
|
|
self.on_fetching_key_event(key_event, in_bracketed_paste)
|
|
elif self.state is State.browsing:
|
|
self.on_browsing_key_event(key_event, in_bracketed_paste)
|
|
elif self.state is State.searching:
|
|
self.on_searching_key_event(key_event, in_bracketed_paste)
|
|
elif self.state is State.accepting:
|
|
self.on_accepting_key_event(key_event, in_bracketed_paste)
|
|
|
|
@Handler.atomic_update
|
|
def draw_screen(self) -> None:
|
|
self.cmd.clear_screen()
|
|
self.enforce_cursor_state()
|
|
self.cmd.set_line_wrapping(False)
|
|
if self.state is State.fetching:
|
|
self.draw_fetching_screen()
|
|
elif self.state in (State.browsing, State.searching):
|
|
self.draw_browsing_screen()
|
|
elif self.state is State.accepting:
|
|
self.draw_accepting_screen()
|
|
|
|
def on_resize(self, screen_size: ScreenSize) -> None:
|
|
self.screen_size = screen_size
|
|
self.draw_screen()
|
|
|
|
def on_interrupt(self) -> None:
|
|
self.quit_loop(1)
|
|
|
|
def on_eot(self) -> None:
|
|
self.quit_loop(1)
|
|
|
|
|
|
help_text = (
|
|
'Change the kitty theme. If no theme name is supplied, run interactively, otherwise'
|
|
' change the current theme to the specified theme name.'
|
|
)
|
|
usage = '[theme name to switch to]'
|
|
OPTIONS = '''
|
|
--cache-age
|
|
type=float
|
|
default=1
|
|
Check for new themes only after the specified number of days. A value of
|
|
zero will always check for new themes. A negative value will never check
|
|
for new themes, instead raising an error if a local copy of the themes
|
|
is not available.
|
|
|
|
|
|
--reload-in
|
|
default=parent
|
|
choices=none,parent,all
|
|
By default, this kitten will signal only the parent kitty instance it is
|
|
running in to reload its config, after making changes. Use this option
|
|
to instead either not reload the config at all or in all running
|
|
kitty instances.
|
|
|
|
|
|
--dump-theme
|
|
type=bool-set
|
|
default=false
|
|
When running non-interactively, dump the specified theme to STDOUT
|
|
instead of changing kitty.conf.
|
|
|
|
|
|
--config-file-name
|
|
default=kitty.conf
|
|
The name or path to the config file to edit. Relative paths are interpreted
|
|
with respect to the kitty config directory. By default the kitty config file,
|
|
kitty.conf is edited. This is most useful if you add :code:`include themes.conf`
|
|
to your kitty.conf and then have the kitten operate only on :file:`themes.conf`,
|
|
allowing :code:`kitty.conf` to remain unchanged.
|
|
'''.format
|
|
|
|
|
|
def parse_themes_args(args: List[str]) -> Tuple[ThemesCLIOptions, List[str]]:
|
|
return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten themes', result_class=ThemesCLIOptions)
|
|
|
|
|
|
def non_interactive(cli_opts: ThemesCLIOptions, theme_name: str) -> None:
|
|
try:
|
|
themes = load_themes(cli_opts.cache_age)
|
|
except NoCacheFound as e:
|
|
raise SystemExit(str(e))
|
|
try:
|
|
theme = themes[theme_name]
|
|
except KeyError:
|
|
theme_name = theme_name.replace('\\', '')
|
|
try:
|
|
theme = themes[theme_name]
|
|
except KeyError:
|
|
raise SystemExit(f'No theme named: {theme_name}')
|
|
if cli_opts.dump_theme:
|
|
print(theme.raw)
|
|
return
|
|
theme.save_in_conf(config_dir, cli_opts.reload_in, cli_opts.config_file_name)
|
|
|
|
|
|
def main(args: List[str]) -> None:
|
|
try:
|
|
cli_opts, items = parse_themes_args(args[1:])
|
|
except SystemExit as e:
|
|
if e.code != 0:
|
|
print(e.args[0], file=sys.stderr)
|
|
input(_('Press Enter to quit'))
|
|
return None
|
|
if len(items) > 1:
|
|
items = [' '.join(items)]
|
|
if len(items) == 1:
|
|
return non_interactive(cli_opts, items[0])
|
|
|
|
loop = Loop()
|
|
with cached_values_for('themes-kitten') as cached_values:
|
|
handler = ThemesHandler(cached_values, cli_opts)
|
|
loop.loop(handler)
|
|
if loop.return_code != 0:
|
|
if handler.report_traceback_on_exit:
|
|
print(handler.report_traceback_on_exit, file=sys.stderr)
|
|
input('Press Enter to quit.')
|
|
if handler.state is State.fetching:
|
|
# asyncio uses non-daemonic threads in its ThreadPoolExecutor
|
|
# so we will hang here till the download completes without
|
|
# os._exit
|
|
os._exit(loop.return_code)
|
|
raise SystemExit(loop.return_code)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main(sys.argv)
|
|
elif __name__ == '__doc__':
|
|
cd = sys.cli_docs # type: ignore
|
|
cd['usage'] = usage
|
|
cd['options'] = OPTIONS
|
|
cd['help_text'] = help_text
|
|
cd['args_completion'] = CompletionSpec.from_string('type:special group:complete_themes')
|