diff --git a/docs/kittens/themes.rst b/docs/kittens/themes.rst index 7dd9be3ea..a1a8e9b90 100644 --- a/docs/kittens/themes.rst +++ b/docs/kittens/themes.rst @@ -47,3 +47,5 @@ go to `kitty-themes `_ and open a pu asking to add your contributions to the repository. Use the file `template.conf `_ as a template when creating your theme. + +.. include:: ../generated/cli-kitten-themes.rst diff --git a/kittens/themes/collection.py b/kittens/themes/collection.py index 46cbb4218..bb2c84f13 100644 --- a/kittens/themes/collection.py +++ b/kittens/themes/collection.py @@ -75,11 +75,17 @@ def set_comment_in_zip_file(path: str, data: str) -> None: 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' + 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: @@ -95,8 +101,11 @@ def fetch_themes( q = json.loads(zf.comment) m.etag = str(q.get('etag') or '') m.timestamp = datetime.datetime.fromisoformat(q['timestamp']) - if (now - m.timestamp).days < 1: + 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: @@ -348,9 +357,9 @@ class Themes: self.index_map = tuple(self.themes) -def load_themes() -> Themes: +def load_themes(cache_age: float = 1.) -> Themes: ans = Themes() - ans.load_from_zip(fetch_themes()) + ans.load_from_zip(fetch_themes(cache_age=cache_age)) ans.load_from_dir(os.path.join(config_dir, 'themes')) ans.index_map = tuple(ans.themes) return ans diff --git a/kittens/themes/main.py b/kittens/themes/main.py index c3d8d8199..20a7183f2 100644 --- a/kittens/themes/main.py +++ b/kittens/themes/main.py @@ -7,11 +7,13 @@ 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 create_default_opts +from kitty.cli import 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 @@ -23,7 +25,7 @@ from ..tui.handler import Handler from ..tui.line_edit import LineEdit from ..tui.loop import Loop from ..tui.operations import color_code, styled -from .collection import MARK_AFTER, Theme, Themes, load_themes +from .collection import MARK_AFTER, NoCacheFound, Theme, Themes, load_themes separator = '║' @@ -133,8 +135,9 @@ class ThemesList: class ThemesHandler(Handler): - def __init__(self, cached_values: Dict[str, Any]) -> None: + 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]] = { @@ -221,7 +224,7 @@ class ThemesHandler(Handler): def fetch() -> None: try: - themes: Union[Themes, str] = load_themes() + 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) @@ -502,10 +505,56 @@ class ThemesHandler(Handler): 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. + + +'''.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: + raise SystemExit(f'No theme named: {theme_name}') + theme.save_in_conf(config_dir) + + 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: + raise SystemExit('At most one theme name must be specified') + 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) + handler = ThemesHandler(cached_values, cli_opts) loop.loop(handler) if loop.return_code != 0: if handler.report_traceback_on_exit: @@ -521,3 +570,8 @@ def main(args: List[str]) -> None: 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 diff --git a/kitty/cli_stub.py b/kitty/cli_stub.py index 8e2ace661..fbccbbf80 100644 --- a/kitty/cli_stub.py +++ b/kitty/cli_stub.py @@ -14,6 +14,7 @@ LaunchCLIOptions = AskCLIOptions = ClipboardCLIOptions = DiffCLIOptions = CLIOpt HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions ErrorCLIOptions = UnicodeCLIOptions = RCOptions = RemoteFileCLIOptions = CLIOptions QueryTerminalCLIOptions = BroadcastCLIOptions = ShowKeyCLIOptions = CLIOptions +ThemesCLIOptions = CLIOptions def generate_stub() -> None: @@ -72,6 +73,9 @@ def generate_stub() -> None: from kittens.unicode_input.main import OPTIONS do(OPTIONS(), 'UnicodeCLIOptions') + from kittens.themes.main import OPTIONS + do(OPTIONS(), 'ThemesCLIOptions') + from kitty.rc.base import all_command_names, command_for_name for cmd_name in all_command_names(): cmd = command_for_name(cmd_name)