More work on the themes kitten

This commit is contained in:
Kovid Goyal 2021-08-04 18:43:26 +05:30
parent e50c26d1b9
commit 92a9b71f21
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 103 additions and 9 deletions

View File

@ -11,12 +11,13 @@ import shutil
import tempfile import tempfile
import zipfile import zipfile
from contextlib import suppress 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.error import HTTPError
from urllib.request import Request, urlopen from urllib.request import Request, urlopen
from kitty.config import parse_config from kitty.config import parse_config
from kitty.constants import cache_dir, config_dir from kitty.constants import cache_dir, config_dir
from kitty.options.types import Options as KittyOptions
from kitty.rgb import Color from kitty.rgb import Color
from ..choose.main import match from ..choose.main import match
@ -171,6 +172,7 @@ class Theme:
def __init__(self, loader: Callable[[], str]): def __init__(self, loader: Callable[[], str]):
self._loader = loader self._loader = loader
self._raw: Optional[str] = None self._raw: Optional[str] = None
self._opts: Optional[KittyOptions] = None
@property @property
def raw(self) -> str: def raw(self) -> str:
@ -178,11 +180,18 @@ class Theme:
self._raw = self._loader() self._raw = self._loader()
return self._raw 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: class Themes:
def __init__(self) -> None: def __init__(self) -> None:
self.themes: Dict[str, Theme] = {} self.themes: Dict[str, Theme] = {}
self.index_map: Tuple[str, ...] = ()
def __len__(self) -> int: def __len__(self) -> int:
return len(self.themes) return len(self.themes)
@ -190,6 +199,13 @@ class Themes:
def __iter__(self) -> Iterator[Theme]: def __iter__(self) -> Iterator[Theme]:
return iter(self.themes.values()) 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: def load_from_zip(self, path_to_zip: str) -> None:
with zipfile.ZipFile(path_to_zip, 'r') as zf: with zipfile.ZipFile(path_to_zip, 'r') as zf:
for name in zf.namelist(): for name in zf.namelist():
@ -225,7 +241,18 @@ class Themes:
def filtered(self, is_ok: Callable[[Theme], bool]) -> 'Themes': def filtered(self, is_ok: Callable[[Theme], bool]) -> 'Themes':
ans = 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 return ans
def apply_search(self, expression: str, mark_before: str = '\033[32m', mark_after: str = '\033[39m') -> Iterator[str]: 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] yield result[0]
else: else:
del self.themes[k] del self.themes[k]
self.index_map = tuple(x for x in self.index_map if x != k)
def load_themes() -> Themes: def load_themes() -> Themes:
ans = Themes() ans = Themes()
ans.load_from_zip(fetch_themes()) ans.load_from_zip(fetch_themes())
ans.load_from_dir(os.path.join(config_dir, 'themes')) ans.load_from_dir(os.path.join(config_dir, 'themes'))
ans.index_map = tuple(ans.themes)
return ans return ans

View File

@ -10,14 +10,16 @@ from typing import (
Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Union 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.config import cached_values_for
from kitty.fast_data_types import wcswidth from kitty.fast_data_types import wcswidth
from kitty.rgb import color_as_sharp, color_from_int
from kitty.typing import KeyEventType from kitty.typing import KeyEventType
from kitty.utils import ScreenSize from kitty.utils import ScreenSize
from ..tui.handler import Handler from ..tui.handler import Handler
from ..tui.loop import Loop from ..tui.loop import Loop
from ..tui.operations import styled from ..tui.operations import styled, color_code
from .collection import Theme, Themes, load_themes from .collection import Theme, Themes, load_themes
@ -61,20 +63,28 @@ class ThemesList:
self.max_width = 0 self.max_width = 0
self.current_idx = 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: def update_themes(self, themes: Themes) -> None:
self.themes = themes self.themes = self.all_themes = themes
if self.current_search: if self.current_search:
self.themes = self.all_themes.copy()
self.display_strings = tuple(self.themes.apply_search(self.current_search)) self.display_strings = tuple(self.themes.apply_search(self.current_search))
else: else:
self.display_strings = tuple(t.name for t in self.themes) self.display_strings = tuple(t.name for t in self.themes)
self.widths = tuple(map(wcswidth, self.display_strings)) self.widths = tuple(map(wcswidth, self.display_strings))
self.max_width = max(self.widths) if self.widths else 0 self.max_width = max(self.widths) if self.widths else 0
self.current_idx = 0
def update_search(self, search: str = '') -> None: def update_search(self, search: str = '') -> None:
if search == self.current_search: if search == self.current_search:
return return
self.current_search = search self.current_search = search
self.update_themes(self.themes) self.update_themes(self.all_themes)
def lines(self, num_rows: int) -> Iterator[str]: def lines(self, num_rows: int) -> Iterator[str]:
if num_rows < 1: if num_rows < 1:
@ -87,6 +97,10 @@ class ThemesList:
line = styled(line, reverse=True) line = styled(line, reverse=True)
yield line yield line
@property
def current_theme(self) -> Theme:
return self.themes[self.current_idx]
class ThemesHandler(Handler): class ThemesHandler(Handler):
@ -99,6 +113,7 @@ class ThemesHandler(Handler):
'recent': create_recent_filter(self.cached_values.get('recent', ())) 'recent': create_recent_filter(self.cached_values.get('recent', ()))
} }
self.themes_list = ThemesList() self.themes_list = ThemesList()
self.colors_set_once = False
def enforce_cursor_state(self) -> None: def enforce_cursor_state(self) -> None:
self.cmd.set_cursor_visible(self.state == State.fetching) self.cmd.set_cursor_visible(self.state == State.fetching)
@ -131,8 +146,27 @@ class ThemesHandler(Handler):
cat = 'all' cat = 'all'
self.cached_values['category'] = cat 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: def redraw_after_category_change(self) -> None:
self.themes_list.update_themes(self.all_themes.filtered(self.filter_map[self.current_category])) self.themes_list.update_themes(self.all_themes.filtered(self.filter_map[self.current_category]))
self.set_colors_to_current_theme()
self.draw_screen() self.draw_screen()
# Theme fetching {{{ # Theme fetching {{{
@ -168,7 +202,32 @@ class ThemesHandler(Handler):
# Theme browsing {{{ # Theme browsing {{{
def draw_tab_bar(self) -> None: 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: def draw_browsing_screen(self) -> None:
self.draw_tab_bar() self.draw_tab_bar()

12
kitty/rgb.py generated
View File

@ -4,7 +4,7 @@
import re import re
from contextlib import suppress from contextlib import suppress
from typing import Optional, NamedTuple from typing import NamedTuple, Optional, Tuple
class Color(NamedTuple): class Color(NamedTuple):
@ -12,6 +12,12 @@ class Color(NamedTuple):
green: int = 0 green: int = 0
blue: 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: def luminance(self) -> float:
return 0.299 * self.red + 0.587 * self.green + 0.114 * self.blue 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: 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]: def to_color(raw: str, validate: bool = False) -> Optional[Color]:
@ -856,8 +862,8 @@ color_names = {
if __name__ == '__main__': if __name__ == '__main__':
# Read RGB color table from specified rgb.txt file # Read RGB color table from specified rgb.txt file
import sys
import pprint import pprint
import sys
data = {} data = {}
with open(sys.argv[-1]) as f: with open(sys.argv[-1]) as f:
for line in f: for line in f: