Move theme parser into kitty code

This commit is contained in:
Kovid Goyal 2021-08-02 18:54:48 +05:30
parent 050eb5660d
commit 4a1ca8d582
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C

View File

@ -6,13 +6,17 @@ import datetime
import http
import json
import os
import re
import shutil
import zipfile
from contextlib import suppress
from typing import Any, Callable, Dict, Match
from urllib.error import HTTPError
from urllib.request import Request, urlopen
from kitty.config import parse_config
from kitty.constants import cache_dir
from kitty.rgb import Color
def fetch_themes(
@ -49,3 +53,130 @@ def fetch_themes(
with zipfile.ZipFile(dest_path, 'a') as zf:
zf.comment = json.dumps({'etag': m.etag, 'timestamp': m.timestamp.isoformat()}).encode('utf-8')
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, 'rb') 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:
return str(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) -> Dict[str, Any]:
lines = raw.splitlines()
conf = parse_config(lines)
bg = conf.get('background', Color())
is_dark = max(bg) < 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 SystemExit(
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:
raise SystemExit(f'The theme {fname} has no settings')
return ans
class Theme:
name: str = ''
author: str = ''
license: str = ''
is_dark: bool = False
blurb: str = ''
num_settings: int = 0
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'):
a = d.get(x)
if isinstance(a, int):
setattr(self, x, a)
def __init__(self, loader: Callable[[], str]):
self.loader = loader
class Themes:
def __init__(self) -> None:
self.themes: Dict[str, Theme] = {}
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, 'rb') 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