Remove themes python code
This commit is contained in:
parent
0805330b77
commit
9443b0e361
@ -1,658 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
|
||||||
|
|
||||||
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()
|
|
||||||
@ -1,523 +1,10 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
from typing import List
|
||||||
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 kitty.cli import CompletionSpec
|
||||||
|
|
||||||
help_text = (
|
help_text = (
|
||||||
'Change the kitty theme. If no theme name is supplied, run interactively, otherwise'
|
'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.
|
allowing :code:`kitty.conf` to remain unchanged.
|
||||||
'''.format
|
'''.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:
|
def main(args: List[str]) -> None:
|
||||||
try:
|
raise SystemExit('This must be run as kitten themes')
|
||||||
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__':
|
if __name__ == '__main__':
|
||||||
main(sys.argv)
|
main(sys.argv)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user