diff --git a/kittens/themes/collection.py b/kittens/themes/collection.py index c3cf6c0bd..cd0f6343c 100644 --- a/kittens/themes/collection.py +++ b/kittens/themes/collection.py @@ -11,12 +11,13 @@ import shutil import tempfile import zipfile from contextlib import suppress -from typing import Any, Callable, Dict, Iterator, Match, Optional +from typing import Any, Callable, Dict, Iterator, Match, Optional, Tuple, Union from urllib.error import HTTPError from urllib.request import Request, urlopen from kitty.config import parse_config from kitty.constants import cache_dir, config_dir +from kitty.options.types import Options as KittyOptions from kitty.rgb import Color from ..choose.main import match @@ -171,6 +172,7 @@ class Theme: def __init__(self, loader: Callable[[], str]): self._loader = loader self._raw: Optional[str] = None + self._opts: Optional[KittyOptions] = None @property def raw(self) -> str: @@ -178,11 +180,18 @@ class Theme: self._raw = self._loader() return self._raw + @property + def kitty_opts(self) -> KittyOptions: + if self._opts is None: + self._opts = KittyOptions(options_dict=parse_config(self.raw.splitlines())) + return self._opts + class Themes: def __init__(self) -> None: self.themes: Dict[str, Theme] = {} + self.index_map: Tuple[str, ...] = () def __len__(self) -> int: return len(self.themes) @@ -190,6 +199,13 @@ class 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(): @@ -225,7 +241,18 @@ class Themes: def filtered(self, is_ok: Callable[[Theme], bool]) -> 'Themes': ans = Themes() - ans.themes = {k: v for k, v in self.themes.items() if is_ok(v)} + + 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 = '\033[32m', mark_after: str = '\033[39m') -> Iterator[str]: @@ -235,10 +262,12 @@ class Themes: yield result[0] else: del self.themes[k] + self.index_map = tuple(x for x in self.index_map if x != k) def load_themes() -> Themes: ans = Themes() ans.load_from_zip(fetch_themes()) 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 1d1833725..e54403b42 100644 --- a/kittens/themes/main.py +++ b/kittens/themes/main.py @@ -10,14 +10,16 @@ from typing import ( Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Union ) +from kitty.cli import create_default_opts from kitty.config import cached_values_for from kitty.fast_data_types import wcswidth +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.loop import Loop -from ..tui.operations import styled +from ..tui.operations import styled, color_code from .collection import Theme, Themes, load_themes @@ -61,20 +63,28 @@ class ThemesList: 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 update_themes(self, themes: Themes) -> None: - self.themes = themes + self.themes = self.all_themes = themes if self.current_search: + self.themes = self.all_themes.copy() self.display_strings = tuple(self.themes.apply_search(self.current_search)) else: self.display_strings = tuple(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 = '') -> None: if search == self.current_search: return self.current_search = search - self.update_themes(self.themes) + self.update_themes(self.all_themes) def lines(self, num_rows: int) -> Iterator[str]: if num_rows < 1: @@ -87,6 +97,10 @@ class ThemesList: line = styled(line, reverse=True) yield line + @property + def current_theme(self) -> Theme: + return self.themes[self.current_idx] + class ThemesHandler(Handler): @@ -99,6 +113,7 @@ class ThemesHandler(Handler): 'recent': create_recent_filter(self.cached_values.get('recent', ())) } self.themes_list = ThemesList() + self.colors_set_once = False def enforce_cursor_state(self) -> None: self.cmd.set_cursor_visible(self.state == State.fetching) @@ -131,8 +146,27 @@ class ThemesHandler(Handler): 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.cmd.set_default_colors( + fg=o.foreground, bg=o.background, cursor=o.cursor, select_bg=o.selection_background, select_fg=o.selection_foreground + ) + self.current_opts = o + cmds = [] + for i in range(256): + col = color_as_sharp(color_from_int(o.color_table[i])) + cmds.append(f'{i};{col}') + self.print(end='\033]4;' + ';'.join(cmds) + '\033\\') + 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 {{{ @@ -168,7 +202,32 @@ class ThemesHandler(Handler): # Theme browsing {{{ def draw_tab_bar(self) -> None: - pass + bar_bg = self.current_opts.tab_bar_background or self.current_opts.background + self.cmd.sgr(color_code(bar_bg, base=40)) + self.print(' ' * self.screen_size.cols, end='\r') + + def draw_tab(text: str, name: str, sc: str) -> None: + is_active = name == self.current_category + text = f'{sc}: {text}' + if is_active: + text = f'{text} ({len(self.themes_list)})' + fg, bg = self.current_opts.active_tab_foreground, self.current_opts.active_tab_background + else: + fg, bg = self.current_opts.inactive_tab_foreground, self.current_opts.inactive_tab_background + + def draw_sep(which: str) -> None: + self.write(styled(which, fg=bar_bg, bg=bg)) + self.cmd.sgr(color_code(fg), color_code(bg, base=40)) + + draw_sep('') + self.write(f' {text} ') + draw_sep('') + + draw_tab('All', 'all', 'F1') + draw_tab('Dark', 'dark', 'F2') + draw_tab('Light', 'light', 'F3') + draw_tab('Recent', 'recent', 'F4') + self.cmd.sgr('39', '49') # reset fg/bg def draw_browsing_screen(self) -> None: self.draw_tab_bar() diff --git a/kitty/rgb.py b/kitty/rgb.py index 53733878e..72094a2ab 100644 --- a/kitty/rgb.py +++ b/kitty/rgb.py @@ -4,7 +4,7 @@ import re from contextlib import suppress -from typing import Optional, NamedTuple +from typing import NamedTuple, Optional, Tuple class Color(NamedTuple): @@ -12,6 +12,12 @@ class Color(NamedTuple): green: int = 0 blue: int = 0 + def __truediv__(self, denom: float) -> Tuple[float, float, float]: + return self.red / denom, self.green / denom, self.blue / denom + + def as_sgr(self) -> str: + return ':2:{}:{}:{}'.format(*self) + def luminance(self) -> float: return 0.299 * self.red + 0.587 * self.green + 0.114 * self.blue @@ -77,7 +83,7 @@ def color_as_sharp(x: Color) -> str: def color_as_sgr(x: Color) -> str: - return ':2:{}:{}:{}'.format(*x) + return x.as_sgr() def to_color(raw: str, validate: bool = False) -> Optional[Color]: @@ -856,8 +862,8 @@ color_names = { if __name__ == '__main__': # Read RGB color table from specified rgb.txt file - import sys import pprint + import sys data = {} with open(sys.argv[-1]) as f: for line in f: