class DownloadFinder(HTMLParser):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.attention = False
+ self.attention = 0
self.links = []
@classmethod
def handle_starttag(self, tag, attrs):
tag = tag.upper()
- if tag == "DETAILS" and "download" in self.get_classes(attrs):
- self.attention = True
- elif self.attention and tag == "A":
+ if tag == "DETAILS" and (
+ self.attention > 0 or "download" in self.get_classes(attrs)
+ ):
+ self.attention += 1
+ elif self.attention > 0 and tag == "A":
href = find_html_attr(attrs, "href")
if href and not "archive-link" in self.get_classes(attrs):
self.links.append(href)
def handle_endtag(self, tag):
- if self.attention and tag.upper() == "DETAILS":
- self.attention = False
+ if tag.upper() == "DETAILS" and self.attention > 0:
+ self.attention -= 1
class LibreWolfApp(AppBase):
ICON_URL = (
"https://aur.archlinux.org/cgit/aur.git/plain/default192x192.png?h=librewolf"
)
- BIN_PATH = AppBase.APPS_DIR / "librewolf" / "librewolf"
+ BIN_PATH = AppBase.APPS_DIR / NAME.lower() / "librewolf"
@classmethod
def get_latest_version(cls):
)
continue
elif not suffix.endswith("sum"):
- print(f"Ignoring {check_url[check_url.rfind('/') + 1:]}", file=sys.stderr)
+ print(
+ f"Ignoring {check_url[check_url.rfind('/') + 1:]}", file=sys.stderr
+ )
continue
m = getattr(hashlib, suffix[1:-3])(content)
assert m.hexdigest() == urlopen(check_url).read().decode().strip()
assert AppBase.APPS_DIR.is_dir() or not AppBase.APPS_DIR.exists()
- app_dir = (AppBase.APPS_DIR / cls.NAME.lower()).absolute()
+ app_dir = cls.BIN_PATH.parent
app_dir.mkdir(0o755, parents=True, exist_ok=True)
run(
["tar", "xJ", "--strip-components=1", "-C", str(app_dir)],
)
with (app_dir / "default192x192.png").open("wb") as fh:
fh.write(urlopen(cls.ICON_URL).read())
- with (cls.get_xdg_home() / "applications" / "librewolf.desktop").open("wt") as fh:
+ with (
+ cls.get_xdg_home() / "applications" / "librewolf.desktop"
+ ).open("wt") as fh:
fh.write(desktop_file)
@classmethod
def uninstall(cls):
- rmtree((AppBase.APPS_DIR / cls.NAME.lower()).absolute())
- (
- cls.get_xdg_home() / "applications" / "librewolf.desktop"
- ).unlink(missing_ok=True)
+ rmtree(cls.BIN_PATH.parent)
+ (cls.get_xdg_home() / "applications" / "librewolf.desktop").unlink(
+ missing_ok=True
+ )
class DownloadFinder(HTMLParser):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.attention = False
- self.spans = False
+ self.attention = 0
+ self.spans = 0
self.items = []
def handle_starttag(self, tag, attrs):
tag = tag.upper()
- if not self.attention:
- if tag == "UL":
- self.attention = True
+ if tag == "UL":
+ self.attention += 1
+ elif self.attention == 0:
+ return
elif tag == "A":
href = find_html_attr(attrs, "href")
if href:
}
)
elif tag == "SPAN":
- self.spans = True
+ self.spans += 1
def handle_data(self, data):
- if self.spans:
+ if self.spans > 0:
data = data.strip()
if data:
self.items[-1]["spans"].append(data)
def handle_endtag(self, tag):
if self.spans and tag.upper() == "SPAN":
- self.spans = False
+ self.spans -= 1
elif self.attention and tag.upper() == "UL":
- self.attention = False
+ self.spans = 0
+ self.attention -= 1
class Materialgram(AppBase):
NAME = "Materialgram"
RELEASE_URL = "https://github.com/kukuruzka165/materialgram/releases.atom"
- BIN_PATH = AppBase.APPS_DIR / "materialgram" / "usr" / "bin" / "materialgram"
+ BIN_PATH = AppBase.APPS_DIR / NAME.lower() / "usr" / "bin" / "materialgram"
NS = {"": "http://www.w3.org/2005/Atom"}
@classmethod
desktop_file_paths = [
path
for path in (
- (AppBase.APPS_DIR / cls.NAME.lower()).absolute()
+ cls.BIN_PATH.parents[2]
/ "usr"
/ "share"
/ "applications"
algo, hexdigest = item["spans"][1].split(":")
m = getattr(hashlib, algo)(content)
assert m.hexdigest() == hexdigest
- app_dir = (AppBase.APPS_DIR / cls.NAME.lower()).absolute()
+ app_dir = cls.BIN_PATH.parents[2]
app_dir.mkdir(0o755, parents=True, exist_ok=True)
run(["tar", "xz", "-C", str(app_dir)], input=content)
desktop_file_path = cls.get_desktop_file_path()
@classmethod
def uninstall(cls):
- (
- cls.get_xdg_home() / "applications" / cls.get_desktop_file_path().name
- ).unlink(missing_ok=True)
- rmtree((AppBase.APPS_DIR / cls.NAME.lower()).absolute())
+ (cls.get_xdg_home() / "applications" / cls.get_desktop_file_path().name).unlink(
+ missing_ok=True
+ )
+ rmtree(cls.BIN_PATH.parents[2])
class PyCharm(AppBase):
- NAME = "PyCharm"
+ NAME = "PyCharm-Community"
RELEASE_URL = (
"https://data.services.jetbrains.com/products/releases"
"?code=PCP&latest=true&type=release"
)
- BIN_PATH = AppBase.APPS_DIR / "pycharm" / "bin" / "pycharm"
+ BIN_PATH = AppBase.APPS_DIR / NAME.lower() / "bin" / "pycharm"
@classmethod
def get_latest_version(cls):
checksum = checksum[:pos]
m = getattr(hashlib, checksum_url[checksum_url.rfind(".") + 1:])(content)
assert m.hexdigest() == checksum
- app_dir = (AppBase.APPS_DIR / cls.NAME.lower()).absolute()
+ app_dir = cls.BIN_PATH.parents[1]
app_dir.mkdir(0o755, parents=True, exist_ok=True)
run(["tar", "xz", "--strip-components=1", "-C", str(app_dir)], input=content)
config_parser = DesktopConfigParser(interpolation=None)
@classmethod
def uninstall(cls):
- rmtree((AppBase.APPS_DIR / cls.NAME.lower()).absolute())
- (
- cls.get_xdg_home() / "applications" / "pycharm.desktop"
- ).unlink(missing_ok=True)
+ rmtree(cls.BIN_PATH.parents[1])
+ (cls.get_xdg_home() / "applications" / "pycharm.desktop").unlink(
+ missing_ok=True
+ )
--- /dev/null
+import hashlib
+import sysconfig
+from html.parser import HTMLParser
+from shutil import rmtree
+from subprocess import check_output, run
+from urllib.request import urlopen
+from xml.etree import ElementTree
+
+from localapps import AppBase, find_html_attr
+
+
+class DownloadFinder(HTMLParser):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.h2 = False
+ self.attention = False
+ self.data = []
+
+ def handle_starttag(self, tag, attrs):
+ tag = tag.upper()
+ if tag == "H2":
+ self.h2 = True
+ elif self.attention:
+ if tag == "A":
+ self.data[-1].append(find_html_attr(attrs, "href"))
+ elif tag == "LI":
+ self.data.append([])
+
+ def handle_data(self, data):
+ if self.h2 and data.strip().upper() == "DOWNLOADS":
+ self.attention = True
+ elif self.attention and self.data and data.strip():
+ self.data[-1].append(data)
+
+ def handle_endtag(self, tag):
+ tag = tag.upper()
+ if self.h2 and tag == "H2":
+ self.h2 = False
+
+
+class UngoogledChromium(AppBase):
+ NAME = "Ungoogled-Chromium"
+ RELEASE_URL = (
+ "https://ungoogled-software.github.io/ungoogled-chromium-binaries/feed.xml"
+ )
+ BIN_PATH = AppBase.APPS_DIR / NAME.lower() / "chrome"
+ NS = {"": "http://www.w3.org/2005/Atom"}
+ PLATFORMS = {"linux-x86_64": "Portable Linux 64-bit"}
+ DESKTOP_URL = (
+ "https://raw.githubusercontent.com/ungoogled-software"
+ "/ungoogled-chromium-portablelinux/refs/heads/master/package"
+ "/ungoogled-chromium.desktop"
+ )
+ ICON_URLS = (
+ (
+ "https://github.com/chromium/chromium/raw/refs/heads/main/chrome/app/theme"
+ "/chromium/product_logo_{size}.png"
+ ),
+ (
+ "https://github.com/chromium/chromium/raw/refs/heads/main/chrome/app/theme"
+ "/default_100_percent/chromium/product_logo_{size}.png"
+ ),
+ )
+
+ @classmethod
+ def get_latest_version(cls):
+ return dict(
+ entry.find("title", cls.NS).text.split(":", 1)
+ for entry in ElementTree.parse(
+ urlopen(cls.RELEASE_URL)
+ ).getroot().findall("entry", cls.NS)
+ )[cls.PLATFORMS[sysconfig.get_platform()]].strip()
+
+ @classmethod
+ def get_installed_version(cls):
+ if cls.has_executable():
+ output = check_output([cls.BIN_PATH, "--version"], text=True).strip()
+ return output.split(maxsplit=1)[1]
+ return None
+
+ @classmethod
+ def install(cls):
+ url = dict(
+ (key, url)
+ for key, url in (
+ (
+ entry.find("title", cls.NS).text.split(":", 1)[0],
+ entry.find("link", cls.NS).attrib["href"]
+ )
+ for entry in ElementTree.parse(
+ urlopen(cls.RELEASE_URL)
+ ).getroot().findall("entry", cls.NS)
+ )
+ )[cls.PLATFORMS[sysconfig.get_platform()]]
+ download_finder = DownloadFinder()
+ download_finder.feed(urlopen(url).read().decode())
+ content = cls.cached_download(download_finder.data[0][0])
+ for algo, digest in (
+ (a.strip().strip(":").lower(), b) for a, b in download_finder.data[1:]
+ ):
+ m = getattr(hashlib, algo)(content)
+ assert m.hexdigest() == digest
+ app_dir = cls.BIN_PATH.parent
+ app_dir.mkdir(0o755, parents=True, exist_ok=True)
+ run(
+ ["tar", "xJ", "--strip-components=1", "-C", str(app_dir)],
+ check=True,
+ input=content
+ )
+ for size in (16, 24, 32, 48, 64, 128, 256):
+ icon_path = (
+ cls.get_xdg_home()
+ / "icons"
+ / "hicolor"
+ / f"{size}x{size}"
+ / "apps"
+ / "chromium.png"
+ )
+ icon_path.parent.mkdir(0o755, parents = True, exist_ok=True)
+ with icon_path.open("wb") as fh:
+ fh.write(
+ urlopen(cls.ICON_URLS[size in (16, 32)].format(size=size)).read()
+ )
+ desktop_file = urlopen(cls.DESKTOP_URL).read().decode().replace(
+ "Exec=chromium", f"Exec={app_dir / 'chrome'}"
+ )
+ with (
+ cls.get_xdg_home() / "applications" / "ungoogled-chromium.desktop"
+ ).open("wt") as fh:
+ fh.write(desktop_file)
+
+ @classmethod
+ def uninstall(cls):
+ rmtree(cls.BIN_PATH.parent)
+ (
+ cls.get_xdg_home() / "applications" / "ungoogled-chromium.desktop"
+ ).unlink(missing_ok=True)
+ for size in (16, 24, 32, 48, 64, 128, 256):
+ (
+ cls.get_xdg_home()
+ / "icons"
+ / "hicolor"
+ / f"{size}x{size}"
+ / "apps"
+ / "chromium.png"
+ ).unlink(missing_ok=True)
registry: list[type[AppBase]] = []
def __init_subclass__(cls, *args, **kwargs):
+ assert cls.NAME.upper() not in (c.NAME.upper() for c in cls.registry)
cls.registry.append(cls)
assert cls.NAME is not None