214 lines
6.0 KiB
Python
214 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import os, zipfile, argparse, logging, io
|
|
from typing import Optional
|
|
from pathlib import Path
|
|
from dataclasses import dataclass
|
|
from urllib.request import urlopen, Request
|
|
from urllib.parse import urlparse
|
|
|
|
logger = logging.getLogger("catppuccin-gtk")
|
|
logger.setLevel(logging.DEBUG)
|
|
ch = logging.StreamHandler()
|
|
formatter = logging.Formatter("[%(name)s] [%(levelname)s] - %(message)s")
|
|
ch.setFormatter(formatter)
|
|
logger.addHandler(ch)
|
|
|
|
|
|
@dataclass
|
|
class InstallContext:
|
|
flavor: str
|
|
accent: str
|
|
dest: Path
|
|
link: bool
|
|
|
|
def build_info(self, include_url=True) -> str:
|
|
url = build_release_url(self)
|
|
info = f"""Installation info:
|
|
flavor: {self.flavor}
|
|
accent: {self.accent}
|
|
dest: {self.dest.absolute()}
|
|
link: {self.link}"""
|
|
if include_url:
|
|
info += f"\nremote_url: {url}"
|
|
return info
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
"flavor",
|
|
type=str,
|
|
choices=["mocha", "frappe", "macchiato", "latte"],
|
|
help="Flavor of the theme to apply.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"accent",
|
|
type=str,
|
|
default="mauve",
|
|
choices=[
|
|
"rosewater",
|
|
"flamingo",
|
|
"pink",
|
|
"mauve",
|
|
"red",
|
|
"maroon",
|
|
"peach",
|
|
"yellow",
|
|
"green",
|
|
"teal",
|
|
"sky",
|
|
"sapphire",
|
|
"blue",
|
|
"lavender",
|
|
],
|
|
help="Accent of the theme.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--from-artifact",
|
|
type=Path,
|
|
dest="from_artifact",
|
|
help="Install from an artifact instead of a mainline release, pass the artifact path (e.g 7bff2448a81e36bf3b0e03bfbd649bebe6973ec7-artifacts.zip)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--dest",
|
|
"-d",
|
|
type=str,
|
|
dest="dest",
|
|
help="Destination of the files.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--link",
|
|
help="Whether to add symlinks for libadwaita",
|
|
type=bool,
|
|
default=False,
|
|
action=argparse.BooleanOptionalAction,
|
|
)
|
|
|
|
return parser.parse_args()
|
|
|
|
|
|
def build_release_url(ctx: InstallContext) -> str:
|
|
repo_root = "https://github.com/catppuccin/gtk/releases/download"
|
|
release = "v1.0.0-rc3"
|
|
zip_name = f"catppuccin-{ctx.flavor}-{ctx.accent}-standard+default.zip"
|
|
|
|
return f"{repo_root}/{release}/{zip_name}"
|
|
|
|
|
|
def fetch_zip(url: str) -> Optional[zipfile.ZipFile]:
|
|
req = Request(url)
|
|
|
|
zip_file = None
|
|
logger.info("Starting download...")
|
|
with urlopen(req) as response:
|
|
logger.info(f"Response status: {response.status}")
|
|
zip_file = zipfile.ZipFile(io.BytesIO(response.read()))
|
|
logger.info("Download finished, zip is valid")
|
|
|
|
logger.info("Verifying download..")
|
|
first_bad_file = zip_file.testzip()
|
|
if first_bad_file is not None:
|
|
logger.error(f'Zip appears to be corrupt, first bad file is "{first_bad_file}"')
|
|
return None
|
|
logger.info("Download verified")
|
|
return zip_file
|
|
|
|
|
|
def add_libadwaita_links(ctx: InstallContext, rewrite: True):
|
|
dir_name = (
|
|
ctx.dest / f"catppuccin-{ctx.flavor}-{ctx.accent}-standard+default" / "gtk-4.0"
|
|
).absolute()
|
|
gtk4_dir = (Path(os.path.expanduser("~")) / ".config" / "gtk-4.0").absolute()
|
|
os.makedirs(gtk4_dir, exist_ok=True)
|
|
|
|
logger.info("Adding symlinks for libadwaita")
|
|
logger.info(f"Root: {dir_name}")
|
|
logger.info(f"Target: {gtk4_dir}")
|
|
try:
|
|
if rewrite:
|
|
os.remove(dir_name / "assets", gtk4_dir / "assets")
|
|
os.remove(dir_name / "gtk.css", gtk4_dir / "gtk.css")
|
|
os.remove(dir_name / "gtk-dark.css", gtk4_dir / "gtk-dark.css")
|
|
except FileNotFoundError:
|
|
logger.debug("Ignoring FileNotFound in symlink rewrite")
|
|
|
|
os.symlink(dir_name / "assets", gtk4_dir / "assets")
|
|
os.symlink(dir_name / "gtk.css", gtk4_dir / "gtk.css")
|
|
os.symlink(dir_name / "gtk-dark.css", gtk4_dir / "gtk-dark.css")
|
|
|
|
|
|
def install(ctx: InstallContext):
|
|
url = build_release_url(ctx)
|
|
logger.info(ctx.build_info())
|
|
|
|
zip_file = fetch_zip(url)
|
|
if zip_file is None:
|
|
return
|
|
|
|
logger.info("Extracting...")
|
|
zip_file.extractall(ctx.dest)
|
|
logger.info("Extraction complete")
|
|
|
|
if ctx.link:
|
|
add_libadwaita_links(ctx)
|
|
|
|
def install_from_artifact(ctx: InstallContext, artifact_path: Path):
|
|
# Working from a pull request, special case it
|
|
logger.info(f"Extracting artifact from '{artifact_path}'")
|
|
artifacts = zipfile.ZipFile(artifact_path)
|
|
|
|
logger.info("Verifying artifact...")
|
|
first_bad_file = artifacts.testzip()
|
|
if first_bad_file is not None:
|
|
logger.error(f'Zip appears to be corrupt, first bad file is "{first_bad_file}"')
|
|
return None
|
|
logger.info("Artifact verified")
|
|
|
|
logger.info(ctx.build_info(False))
|
|
|
|
# The zip, inside the artifacts, that we want to pull out
|
|
zip_name = f"catppuccin-{ctx.flavor}-{ctx.accent}-standard+default.zip"
|
|
logger.info(f"Pulling '{zip_name}' from the artifacts")
|
|
info = artifacts.getinfo(zip_name)
|
|
|
|
logger.info("Extracting the artifact...")
|
|
artifact = zipfile.ZipFile(io.BytesIO(artifacts.read(info)))
|
|
artifact.extractall(ctx.dest)
|
|
logger.info("Extraction complete")
|
|
|
|
if ctx.link:
|
|
logger.info("Adding links (with rewrite)")
|
|
add_libadwaita_links(ctx, True)
|
|
logger.info("Links added")
|
|
|
|
def main():
|
|
args = parse_args()
|
|
|
|
dest = Path(os.path.expanduser("~")) / ".local" / "share" / "themes"
|
|
os.makedirs(dest, exist_ok=True)
|
|
|
|
if args.dest:
|
|
dest = Path(args.dest)
|
|
|
|
ctx = InstallContext(
|
|
flavor=args.flavor, accent=args.accent, dest=dest, link=args.link
|
|
)
|
|
|
|
if args.from_artifact:
|
|
install_from_artifact(ctx, args.from_artifact)
|
|
return
|
|
|
|
install(ctx)
|
|
logger.info("Theme installation complete!")
|
|
|
|
|
|
try:
|
|
main()
|
|
except Exception as e:
|
|
logger.error("Something went wrong when installing the theme:", exc_info=e)
|