Skip to content

Instantly share code, notes, and snippets.

@YUKI2eN3e
Last active June 26, 2024 02:50
Show Gist options
  • Select an option

  • Save YUKI2eN3e/ee91e931a616507644fee3b240fa91c2 to your computer and use it in GitHub Desktop.

Select an option

Save YUKI2eN3e/ee91e931a616507644fee3b240fa91c2 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import re
import subprocess as sp
from dataclasses import dataclass
from os import listdir, path, readlink
from sys import argv, platform
from typing import Dict, List, TypedDict
WINDOWS = platform == "win32"
PYTHON_EXECUTABLE = "python.exe" if WINDOWS else "python3"
BIN_DIR = path.join(path.expanduser("~"), ".local", "bin")
PIPX_VENVS = path.join(path.expanduser("~"), ".local", "pipx", "venvs")
RESET_PATTERN = re.compile(pattern=r"(</[_a-zA-Z]*>)")
ARGC = len(argv)
class Colours:
"""ANSI color codes"""
RED = "\033[0;31m"
DARK_GRAY = "\033[1;30m"
LIGHT_GREEN = "\033[1;32m"
LIGHT_BLUE = "\033[1;34m"
LIGHT_WHITE = "\033[1;37m"
BOLD = "\033[1m" if WINDOWS else LIGHT_WHITE
RESET = "\033[0m"
ORANGE = "\033[38;2;255;165;0m"
# cancel SGR codes if we don't write to a terminal
if not __import__("sys").stdout.isatty():
for _ in dir():
if isinstance(_, str) and _[0] != "_":
locals()[_] = ""
else:
# set Windows console in VT mode
if WINDOWS:
from os import system # type: ignore[misc]
system("")
class ColourJSONEncoder(json.JSONEncoder):
def encode(self, obj: object) -> str:
indented = json.dumps(obj=obj, indent=2, default=lambda x: x.__dict__)
return self._apply_colours(content=indented)
@staticmethod
def _apply_colours(content: str) -> str:
lines = content.splitlines()
for (
i,
line,
) in enumerate(lines):
lines[i] = re.sub(
RESET_PATTERN,
Colours.RESET,
line.replace("<bold>", Colours.BOLD)
.replace("<light_blue>", Colours.LIGHT_BLUE)
.replace("<light_green>", Colours.LIGHT_GREEN)
.replace("<orange>", Colours.ORANGE),
)
return "\n".join(lines) + Colours.RESET
class File:
full_path: str
_file_name: str | None = None
@property
def file_name(self) -> str:
if self._file_name is None:
self._file_name = path.basename(self.full_path)
return self._file_name
def __init__(self, full_path: str) -> None:
self.full_path = full_path
class VEnvExecutable(File):
_venv: str | None = None
@property
def venv(self) -> str:
if self._venv is None:
self._venv = path.basename(path.dirname(path.dirname(self.full_path)))
return self._venv
_python_version: str | None = None
@property
def python_version(self) -> str:
if self._python_version is None:
result = sp.run(
[
path.join(path.dirname(self.full_path), PYTHON_EXECUTABLE),
"--version",
],
capture_output=True,
)
self._python_version = result.stdout.decode().strip()
return self._python_version
@dataclass
class Link:
link_file: File
src_file: VEnvExecutable
_MainPackageDict = TypedDict(
"_MainPackageDict", {"<light_blue>apps</light_blue>": List[str]}
)
_PipxProgramMetadataDict = TypedDict(
"_PipxProgramMetadataDict",
{
"<light_blue>main_package</light_blue>": _MainPackageDict,
"<light_blue>python_version</light_blue>": str,
},
)
PipxProgramDict = TypedDict(
"PipxProgramDict",
{
"<light_blue>metadata</light_blue>": _PipxProgramMetadataDict,
},
)
_SimplifiedMainPackageDict = TypedDict(
"_SimplifiedMainPackageDict", {"apps": List[str]}
)
_SimplifiedMetadataDict = TypedDict(
"_SimplifiedMetadataDict",
{"main_package": _SimplifiedMainPackageDict, "python_version": str},
)
class PipxProgram:
__dict__: PipxProgramDict # type: ignore[assignment]
@property
def apps(self) -> List[str]:
return self.__dict__["<light_blue>metadata</light_blue>"][
"<light_blue>main_package</light_blue>"
]["<light_blue>apps</light_blue>"]
@apps.setter
def apps(self, value: List[str]) -> None:
self.__dict__["<light_blue>metadata</light_blue>"][
"<light_blue>main_package</light_blue>"
]["<light_blue>apps</light_blue>"] = value
def __init__(self, metadata: _SimplifiedMetadataDict) -> None:
self.__dict__["<light_blue>metadata</light_blue>"] = {
"<light_blue>main_package</light_blue>": {
"<light_blue>apps</light_blue>": [
f"<light_green>{a}</light_green>"
for a in metadata["main_package"]["apps"]
],
},
"<light_blue>python_version</light_blue>": metadata["python_version"],
}
def get_symlinks() -> List[Link]:
links: List[Link] = []
for f in listdir(BIN_DIR):
file = path.join(BIN_DIR, f)
if path.islink(file):
src = readlink(path=file)
if PIPX_VENVS in src:
links.append(Link(link_file=File(file), src_file=VEnvExecutable(src)))
return links
def display(files: List[Link]) -> None:
print(f"{Colours.BOLD}venvs are in {Colours.LIGHT_BLUE}{PIPX_VENVS}{Colours.RESET}")
print(
f"{Colours.BOLD}apps are exposed at {Colours.LIGHT_BLUE}{BIN_DIR}{Colours.RESET}"
)
print(
f"{Colours.BOLD}package {Colours.LIGHT_BLUE}{files[0].src_file.venv}{Colours.RESET + Colours.BOLD}, installed with {files[0].src_file.python_version}{Colours.RESET}\n\t- {files[0].link_file.file_name.removesuffix('.exe')}"
)
last_venv = files[0].src_file.venv
for file in files[1:]:
if file.src_file.venv != last_venv:
print(
f"{Colours.BOLD}package {Colours.LIGHT_BLUE}{file.src_file.venv}{Colours.RESET + Colours.BOLD}, installed with {file.src_file.python_version}{Colours.RESET}\n\t- {file.link_file.file_name.removesuffix('.exe')}"
)
last_venv = file.src_file.venv
else:
print(f"\t- {file.link_file.file_name.removesuffix('.exe')}")
def display_as_json(files: List[Link]) -> None:
data: Dict[str, PipxProgram] = {
f"<light_blue>{files[0].src_file.venv}</light_blue>": PipxProgram(
metadata={
"main_package": {
"apps": [files[0].link_file.file_name.removesuffix(".exe")]
},
"python_version": files[0].src_file.python_version,
}
)
}
last_venv = files[0].src_file.venv
for file in files[1:]:
if file.src_file.venv != last_venv:
data[f"<light_blue>{file.src_file.venv}</light_blue>"] = PipxProgram(
metadata={
"main_package": {
"apps": [file.link_file.file_name.removesuffix(".exe")]
},
"python_version": file.src_file.python_version,
}
)
last_venv = file.src_file.venv
else:
data[f"<light_blue>{file.src_file.venv}</light_blue>"].apps.append(
f"<light_green>{file.link_file.file_name.removesuffix('.exe')}</light_green>"
)
print(ColourJSONEncoder().encode(data))
def display_help() -> None:
print(
f"{Colours.BOLD}{Colours.ORANGE}Usage: {Colours.RESET}{Colours.BOLD}{Colours.DARK_GRAY}{path.basename(__file__).removesuffix('.py')}{Colours.RESET} [{Colours.LIGHT_BLUE}-h{Colours.RESET}] [{Colours.LIGHT_BLUE}-j{Colours.RESET}]\n"
)
print(f"{Colours.BOLD}{Colours.ORANGE}Options: {Colours.RESET}")
print(
f" {Colours.LIGHT_BLUE}-h{Colours.RESET}, {Colours.LIGHT_BLUE}--help{Colours.RESET}\t\tshow this help message and exit"
)
print(
f" {Colours.LIGHT_BLUE}-j{Colours.RESET}, {Colours.LIGHT_BLUE}--json{Colours.RESET}\t\toutput pipx installed programs as json"
)
def main() -> None:
files = get_symlinks()
files.sort(key=lambda l: l.src_file.venv) # noqa: E741
if ARGC > 1:
if argv[1].lower() in ["-j", "--json"]:
display_as_json(files=files)
elif argv[1].lower() in ["-h", "--help"]:
display_help()
else:
print(
f"{Colours.BOLD}{Colours.RED}ERROR:{Colours.RESET}{Colours.BOLD} Unrecognized arguments: {argv[1]}{Colours.RESET}"
)
else:
display(files=files)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment