From 9443b0e36179e73e41eafcd2c2428b22ee535fb1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Mar 2023 20:28:45 +0530 Subject: [PATCH] Remove themes python code --- kittens/themes/collection.py | 658 ----------------------------------- kittens/themes/main.py | 569 +----------------------------- 2 files changed, 3 insertions(+), 1224 deletions(-) delete mode 100644 kittens/themes/collection.py diff --git a/kittens/themes/collection.py b/kittens/themes/collection.py deleted file mode 100644 index 7b0f2b4fa..000000000 --- a/kittens/themes/collection.py +++ /dev/null @@ -1,658 +0,0 @@ -#!/usr/bin/env python -# License: GPLv3 Copyright: 2021, Kovid Goyal - -import datetime -import http -import json -import os -import re -import shutil -import signal -import sys -import tempfile -import zipfile -from contextlib import suppress -from typing import Any, Callable, Dict, Iterator, Match, Optional, Tuple, Type, Union -from urllib.error import HTTPError -from urllib.request import Request, urlopen - -from kitty.config import atomic_save, parse_config -from kitty.constants import cache_dir, config_dir -from kitty.fast_data_types import Color -from kitty.options.types import Options as KittyOptions -from kitty.utils import reload_conf_in_all_kitties - -MARK_BEFORE = '\033[33m' -MARK_AFTER = '\033[39m' - - -def patch_conf(raw: str, theme_name: str) -> str: - addition = f'# BEGIN_KITTY_THEME\n# {theme_name}\ninclude current-theme.conf\n# END_KITTY_THEME' - nraw, num = re.subn(r'^# BEGIN_KITTY_THEME.+?# END_KITTY_THEME', addition, raw, flags=re.MULTILINE | re.DOTALL) - if not num: - if raw: - raw += '\n\n' - nraw = raw + addition - # comment out all existing color definitions - color_conf_items = ( # {{{ - # generated by gen-config.py DO NOT edit - # ALL_COLORS_START - 'active_border_color', - 'active_tab_background', - 'active_tab_foreground', - 'background', - 'bell_border_color', - 'color0', - 'color1', - 'color10', - 'color100', - 'color101', - 'color102', - 'color103', - 'color104', - 'color105', - 'color106', - 'color107', - 'color108', - 'color109', - 'color11', - 'color110', - 'color111', - 'color112', - 'color113', - 'color114', - 'color115', - 'color116', - 'color117', - 'color118', - 'color119', - 'color12', - 'color120', - 'color121', - 'color122', - 'color123', - 'color124', - 'color125', - 'color126', - 'color127', - 'color128', - 'color129', - 'color13', - 'color130', - 'color131', - 'color132', - 'color133', - 'color134', - 'color135', - 'color136', - 'color137', - 'color138', - 'color139', - 'color14', - 'color140', - 'color141', - 'color142', - 'color143', - 'color144', - 'color145', - 'color146', - 'color147', - 'color148', - 'color149', - 'color15', - 'color150', - 'color151', - 'color152', - 'color153', - 'color154', - 'color155', - 'color156', - 'color157', - 'color158', - 'color159', - 'color16', - 'color160', - 'color161', - 'color162', - 'color163', - 'color164', - 'color165', - 'color166', - 'color167', - 'color168', - 'color169', - 'color17', - 'color170', - 'color171', - 'color172', - 'color173', - 'color174', - 'color175', - 'color176', - 'color177', - 'color178', - 'color179', - 'color18', - 'color180', - 'color181', - 'color182', - 'color183', - 'color184', - 'color185', - 'color186', - 'color187', - 'color188', - 'color189', - 'color19', - 'color190', - 'color191', - 'color192', - 'color193', - 'color194', - 'color195', - 'color196', - 'color197', - 'color198', - 'color199', - 'color2', - 'color20', - 'color200', - 'color201', - 'color202', - 'color203', - 'color204', - 'color205', - 'color206', - 'color207', - 'color208', - 'color209', - 'color21', - 'color210', - 'color211', - 'color212', - 'color213', - 'color214', - 'color215', - 'color216', - 'color217', - 'color218', - 'color219', - 'color22', - 'color220', - 'color221', - 'color222', - 'color223', - 'color224', - 'color225', - 'color226', - 'color227', - 'color228', - 'color229', - 'color23', - 'color230', - 'color231', - 'color232', - 'color233', - 'color234', - 'color235', - 'color236', - 'color237', - 'color238', - 'color239', - 'color24', - 'color240', - 'color241', - 'color242', - 'color243', - 'color244', - 'color245', - 'color246', - 'color247', - 'color248', - 'color249', - 'color25', - 'color250', - 'color251', - 'color252', - 'color253', - 'color254', - 'color255', - 'color26', - 'color27', - 'color28', - 'color29', - 'color3', - 'color30', - 'color31', - 'color32', - 'color33', - 'color34', - 'color35', - 'color36', - 'color37', - 'color38', - 'color39', - 'color4', - 'color40', - 'color41', - 'color42', - 'color43', - 'color44', - 'color45', - 'color46', - 'color47', - 'color48', - 'color49', - 'color5', - 'color50', - 'color51', - 'color52', - 'color53', - 'color54', - 'color55', - 'color56', - 'color57', - 'color58', - 'color59', - 'color6', - 'color60', - 'color61', - 'color62', - 'color63', - 'color64', - 'color65', - 'color66', - 'color67', - 'color68', - 'color69', - 'color7', - 'color70', - 'color71', - 'color72', - 'color73', - 'color74', - 'color75', - 'color76', - 'color77', - 'color78', - 'color79', - 'color8', - 'color80', - 'color81', - 'color82', - 'color83', - 'color84', - 'color85', - 'color86', - 'color87', - 'color88', - 'color89', - 'color9', - 'color90', - 'color91', - 'color92', - 'color93', - 'color94', - 'color95', - 'color96', - 'color97', - 'color98', - 'color99', - 'cursor', - 'cursor_text_color', - 'foreground', - 'inactive_border_color', - 'inactive_tab_background', - 'inactive_tab_foreground', - 'macos_titlebar_color', - 'mark1_background', - 'mark1_foreground', - 'mark2_background', - 'mark2_foreground', - 'mark3_background', - 'mark3_foreground', - 'selection_background', - 'selection_foreground', - 'tab_bar_background', - 'tab_bar_margin_color', - 'url_color', - 'visual_bell_color', - 'wayland_titlebar_color', - # ALL_COLORS_END - ) # }}} - pat = fr'^\s*({"|".join(color_conf_items)})\b' - return re.sub(pat, r'# \1', nraw, flags=re.MULTILINE) - - -def set_comment_in_zip_file(path: str, data: str) -> None: - with zipfile.ZipFile(path, 'a') as zf: - zf.comment = data.encode('utf-8') - - -class NoCacheFound(ValueError): - pass - - -def fetch_themes( - name: str = 'kitty-themes', - url: str = 'https://codeload.github.com/kovidgoyal/kitty-themes/zip/master', - cache_age: float = 1, -) -> str: - now = datetime.datetime.now(datetime.timezone.utc) - cache_age_delta = datetime.timedelta(days=cache_age) - - class Metadata: - def __init__(self) -> None: - self.etag = '' - self.timestamp = now - - def __str__(self) -> str: - return json.dumps({'etag': self.etag, 'timestamp': self.timestamp.isoformat()}) - - dest_path = os.path.join(cache_dir(), f'{name}.zip') - m = Metadata() - with suppress(Exception), zipfile.ZipFile(dest_path, 'r') as zf: - q = json.loads(zf.comment) - m.etag = str(q.get('etag') or '') - m.timestamp = datetime.datetime.fromisoformat(q['timestamp']) - if cache_age < 0 or (now - m.timestamp) < cache_age_delta: - return dest_path - if cache_age < 0: - raise NoCacheFound('No local themes cache found and negative cache age specified, aborting') - - rq = Request(url) - m.timestamp = now - if m.etag: - rq.add_header('If-None-Match', m.etag) - try: - res = urlopen(rq, timeout=30) - except HTTPError as e: - if m.etag and e.code == http.HTTPStatus.NOT_MODIFIED: - set_comment_in_zip_file(dest_path, str(m)) - return dest_path - raise - m.etag = res.headers.get('etag') or '' - - needs_delete = False - try: - with tempfile.NamedTemporaryFile(suffix=f'-{os.path.basename(dest_path)}', dir=os.path.dirname(dest_path), delete=False) as f: - needs_delete = True - shutil.copyfileobj(res, f) - f.flush() - set_comment_in_zip_file(f.name, str(m)) - os.replace(f.name, dest_path) - needs_delete = False - finally: - if needs_delete: - os.unlink(f.name) - return dest_path - - -def zip_file_loader(path_to_zip: str, theme_file_name: str, file_name: str) -> Callable[[], str]: - - name = os.path.join(os.path.dirname(theme_file_name), file_name) - - def zip_loader() -> str: - with zipfile.ZipFile(path_to_zip, 'r') as zf, zf.open(name) as f: - return f.read().decode('utf-8') - - return zip_loader - - -def theme_name_from_file_name(fname: str) -> str: - ans = fname.rsplit('.', 1)[0] - ans = ans.replace('_', ' ') - - def camel_case(m: 'Match[str]') -> str: - return f'{m.group(1)} {m.group(2)}' - - ans = re.sub(r'([a-z])([A-Z])', camel_case, ans) - return ' '.join(x.capitalize() for x in filter(None, ans.split())) - - -class LineParser: - - def __init__(self) -> None: - self.in_metadata = False - self.in_blurb = False - self.keep_going = True - - def __call__(self, line: str, ans: Dict[str, Any]) -> None: - is_block = line.startswith('## ') - if self.in_metadata and not is_block: - self.keep_going = False - return - if not self.in_metadata and is_block: - self.in_metadata = True - if not self.in_metadata: - return - line = line[3:] - if self.in_blurb: - ans['blurb'] += ' ' + line - return - try: - key, val = line.split(':', 1) - except Exception: - self.keep_going = False - return - key = key.strip().lower() - val = val.strip() - if val: - ans[key] = val - if key == 'blurb': - self.in_blurb = True - - -def parse_theme(fname: str, raw: str, exc_class: Type[BaseException] = SystemExit) -> Dict[str, Any]: - lines = raw.splitlines() - conf = parse_config(lines) - bg: Color = conf.get('background', Color()) - is_dark = max((bg.red, bg.green, bg.blue)) < 115 - ans: Dict[str, Any] = {'name': theme_name_from_file_name(fname)} - parser = LineParser() - for i, line in enumerate(raw.splitlines()): - line = line.strip() - if not line: - continue - try: - parser(line, ans) - except Exception as e: - raise exc_class( - f'Failed to parse {fname} line {i+1} with error: {e}') - if not parser.keep_going: - break - if is_dark: - ans['is_dark'] = True - ans['num_settings'] = len(conf) - len(parse_config(())) - if ans['num_settings'] < 1 and fname != 'default.conf': - raise exc_class(f'The theme {fname} has no settings') - return ans - - -def update_theme_file(path: str) -> bool: - with open(path) as f: - raw = f.read() - td = parse_theme(os.path.basename(path), raw, exc_class=ValueError) - if 'upstream' not in td: - return False - nraw = urlopen(td['upstream']).read().decode('utf-8') - if raw == nraw: - return False - atomic_save(nraw.encode('utf-8'), path) - return True - - -def text_as_opts(text: str) -> KittyOptions: - return KittyOptions(options_dict=parse_config(text.splitlines())) - - -class Theme: - name: str = '' - author: str = '' - license: str = '' - is_dark: bool = False - blurb: str = '' - num_settings: int = 0 - is_user_defined: bool = False - - def apply_dict(self, d: Dict[str, Any]) -> None: - self.name = str(d['name']) - for x in ('author', 'license', 'blurb'): - a = d.get(x) - if isinstance(a, str): - setattr(self, x, a) - for x in ('is_dark', 'num_settings', 'is_user_defined'): - a = d.get(x) - if isinstance(a, int): - setattr(self, x, a) - - def __init__(self, loader: Callable[[], str]): - self._loader = loader - self._raw: Optional[str] = None - self._opts: Optional[KittyOptions] = None - - @property - def raw(self) -> str: - if self._raw is None: - self._raw = self._loader() - return self._raw - - @property - def kitty_opts(self) -> KittyOptions: - if self._opts is None: - self._opts = text_as_opts(self.raw) - return self._opts - - def save_in_dir(self, dirpath: str) -> None: - atomic_save(self.raw.encode('utf-8'), os.path.join(dirpath, f'{self.name}.conf')) - - def save_in_conf(self, confdir: str, reload_in: str, config_file_name: str = 'kitty.conf') -> None: - os.makedirs(confdir, exist_ok=True) - atomic_save(self.raw.encode('utf-8'), os.path.join(confdir, 'current-theme.conf')) - confpath = os.path.realpath(os.path.join(confdir, config_file_name)) - try: - with open(confpath) as f: - raw = f.read() - except FileNotFoundError: - raw = '' - nraw = patch_conf(raw, self.name) - if raw: - with open(f'{confpath}.bak', 'w') as f: - f.write(raw) - atomic_save(nraw.encode('utf-8'), confpath) - if reload_in == 'parent': - if 'KITTY_PID' in os.environ: - os.kill(int(os.environ['KITTY_PID']), signal.SIGUSR1) - elif reload_in == 'all': - reload_conf_in_all_kitties() - - -class Themes: - - def __init__(self) -> None: - self.themes: Dict[str, Theme] = {} - self.index_map: Tuple[str, ...] = () - - def __len__(self) -> int: - return len(self.themes) - - def __iter__(self) -> Iterator[Theme]: - return iter(self.themes.values()) - - def __getitem__(self, key: Union[int, str]) -> Theme: - if isinstance(key, str): - return self.themes[key] - if key < 0: - key += len(self.index_map) - return self.themes[self.index_map[key]] - - def load_from_zip(self, path_to_zip: str) -> None: - with zipfile.ZipFile(path_to_zip, 'r') as zf: - for name in zf.namelist(): - if os.path.basename(name) == 'themes.json': - theme_file_name = name - with zf.open(theme_file_name) as f: - items = json.loads(f.read()) - break - else: - raise ValueError(f'No themes.json found in {path_to_zip}') - - for item in items: - t = Theme(zip_file_loader(path_to_zip, theme_file_name, item['file'])) - t.apply_dict(item) - if t.name: - self.themes[t.name] = t - - def load_from_dir(self, path: str) -> None: - if not os.path.isdir(path): - return - for name in os.listdir(path): - if name.endswith('.conf'): - with open(os.path.join(path, name), 'rb') as f: - raw = f.read().decode() - try: - d = parse_theme(name, raw) - except (Exception, SystemExit): - continue - d['is_user_defined'] = True - t = Theme(raw.__str__) - t.apply_dict(d) - if t.name: - self.themes[t.name] = t - - def filtered(self, is_ok: Callable[[Theme], bool]) -> 'Themes': - ans = Themes() - - def sort_key(k: Tuple[str, Theme]) -> str: - return k[1].name.lower() - - ans.themes = {k: v for k, v in sorted(self.themes.items(), key=sort_key) if is_ok(v)} - ans.index_map = tuple(ans.themes) - return ans - - def copy(self) -> 'Themes': - ans = Themes() - ans.themes = self.themes.copy() - ans.index_map = self.index_map - return ans - - def apply_search( - self, expression: str, mark_before: str = MARK_BEFORE, mark_after: str = MARK_AFTER - ) -> Iterator[str]: - raw = '\n'.join(self.themes) - results = match(raw, expression, positions=True, level1=' ') - themes: Dict[str, Theme] = {} - for r in results: - pos, k = r.split(':', 1) - positions = tuple(map(int, pos.split(','))) - text = k - for p in reversed(positions): - text = text[:p] + mark_before + text[p] + mark_after + text[p+1:] - themes[k] = self.themes[k] - yield text - self.themes = themes - self.index_map = tuple(self.themes) - - -def load_themes(cache_age: float = 1., ignore_no_cache: bool = False) -> Themes: - ans = Themes() - try: - fetched = fetch_themes(cache_age=cache_age) - except NoCacheFound: - if not ignore_no_cache: - raise - else: - ans.load_from_zip(fetched) - ans.load_from_dir(os.path.join(config_dir, 'themes')) - ans.index_map = tuple(ans.themes) - return ans - - -def print_theme_names() -> None: - found = False - for theme in load_themes(cache_age=-1, ignore_no_cache=True): - print(theme.name) - found = True - if not found: - print('Default') - sys.stdout.flush() diff --git a/kittens/themes/main.py b/kittens/themes/main.py index b440c6cc4..a47296b8f 100644 --- a/kittens/themes/main.py +++ b/kittens/themes/main.py @@ -1,523 +1,10 @@ #!/usr/bin/env python # License: GPLv3 Copyright: 2021, Kovid Goyal -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: - from urllib.error import URLError - try: - themes: Union[Themes, str] = load_themes(self.cli_opts.cache_age) - except URLError as e: - themes = f'Could not download themes, check your internet connection. Error: {e}' - 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) +from typing import List +from kitty.cli import CompletionSpec help_text = ( 'Change the kitty theme. If no theme name is supplied, run interactively, otherwise' @@ -559,58 +46,8 @@ 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) - + raise SystemExit('This must be run as kitten themes') if __name__ == '__main__': main(sys.argv)