Merge pull request 'Add ahead-of-time compilation for cli and optimize thumbnail caching' (#136) from feat/aot-compilation-cli into main
Reviewed-on: #136 Reviewed-by: AramJonghu <aramjonghu@tutamail.com>
This commit was merged in pull request #136.
This commit is contained in:
@@ -51,7 +51,9 @@ add_compile_options(
|
|||||||
-Wunreachable-code
|
-Wunreachable-code
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if("shell" IN_LIST ENABLE_MODULES)
|
if("shell" IN_LIST ENABLE_MODULES)
|
||||||
|
# Build settings index
|
||||||
find_package(Python3 COMPONENTS Interpreter REQUIRED)
|
find_package(Python3 COMPONENTS Interpreter REQUIRED)
|
||||||
set(SETTINGS_INDEX_JSON "${CMAKE_BINARY_DIR}/settings-index.json")
|
set(SETTINGS_INDEX_JSON "${CMAKE_BINARY_DIR}/settings-index.json")
|
||||||
execute_process(
|
execute_process(
|
||||||
@@ -64,6 +66,49 @@ if("shell" IN_LIST ENABLE_MODULES)
|
|||||||
if(NOT SETTINGS_INDEX_RESULT EQUAL 0)
|
if(NOT SETTINGS_INDEX_RESULT EQUAL 0)
|
||||||
message(FATAL_ERROR "Failed to build settings search index")
|
message(FATAL_ERROR "Failed to build settings search index")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Nuitka compilation
|
||||||
|
set(ZSHELL_CLI_BUILD_DIR "${CMAKE_BINARY_DIR}/zshell-cli")
|
||||||
|
set(ZSHELL_CLI_DIST "${ZSHELL_CLI_BUILD_DIR}/zshell.dist")
|
||||||
|
set(ZSHELL_CLI_SRC "${CMAKE_SOURCE_DIR}/cli/src/zshell")
|
||||||
|
|
||||||
|
find_program(NUITKA_EXECUTABLE nuitka REQUIRED)
|
||||||
|
|
||||||
|
file(GLOB_RECURSE ZSHELL_CLI_SOURCES CONFIGURE_DEPENDS
|
||||||
|
"${ZSHELL_CLI_SRC}/*.py"
|
||||||
|
)
|
||||||
|
file(GLOB_RECURSE ZSHELL_CLI_ASSETS CONFIGURE_DEPENDS
|
||||||
|
"${ZSHELL_CLI_SRC}/assets/*"
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT "${ZSHELL_CLI_DIST}/zshell-cli"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E make_directory "${ZSHELL_CLI_BUILD_DIR}"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E rm -rf "${ZSHELL_CLI_DIST}"
|
||||||
|
|
||||||
|
COMMAND
|
||||||
|
${NUITKA_EXECUTABLE}
|
||||||
|
--standalone
|
||||||
|
--include-data-dir=${CMAKE_SOURCE_DIR}/cli/src/zshell/assets=zshell/assets
|
||||||
|
--output-dir=${ZSHELL_CLI_BUILD_DIR}
|
||||||
|
--output-filename=zshell-cli
|
||||||
|
${CMAKE_SOURCE_DIR}/cli/src/zshell/
|
||||||
|
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/cli
|
||||||
|
DEPENDS ${ZSHELL_CLI_SOURCES} ${ZSHELL_CLI_ASSETS}
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_target(zshell-cli ALL DEPENDS "${ZSHELL_CLI_DIST}/zshell-cli")
|
||||||
|
|
||||||
|
install(PROGRAMS "${ZSHELL_CLI_DIST}/zshell-cli" DESTINATION "${INSTALL_LIBDIR}/zshell-cli")
|
||||||
|
install(DIRECTORY "${ZSHELL_CLI_DIST}/" DESTINATION "${INSTALL_LIBDIR}/zshell-cli" PATTERN "zshell-cli" EXCLUDE)
|
||||||
|
|
||||||
|
configure_file(
|
||||||
|
"${CMAKE_SOURCE_DIR}/Plugins/cmake/zshell-cli.cmake.in"
|
||||||
|
"${CMAKE_BINARY_DIR}/Plugins/cmake/zshell-cli.cmake"
|
||||||
|
@ONLY
|
||||||
|
)
|
||||||
|
install(SCRIPT "${CMAKE_BINARY_DIR}/Plugins/cmake/zshell-cli.cmake")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if("plugin" IN_LIST ENABLE_MODULES)
|
if("plugin" IN_LIST ENABLE_MODULES)
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
set(ZSHELL_CLI_BIN_DIR "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/usr/bin")
|
||||||
|
file(MAKE_DIRECTORY "${ZSHELL_CLI_BIN_DIR}")
|
||||||
|
|
||||||
|
file(RELATIVE_PATH ZSHELL_CLI_TARGET
|
||||||
|
"${ZSHELL_CLI_BIN_DIR}"
|
||||||
|
"${CMAKE_INSTALL_PREFIX}/@INSTALL_LIBDIR@/zshell-cli/zshell-cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
file(CREATE_LINK
|
||||||
|
"${ZSHELL_CLI_TARGET}"
|
||||||
|
"${ZSHELL_CLI_BIN_DIR}/zshell-cli" SYMBOLIC
|
||||||
|
)
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
cd "$(dirname $0)/../src" || exit
|
|
||||||
|
|
||||||
python3 -m zshell "$@"
|
|
||||||
@@ -19,6 +19,11 @@ zshell-cli = "zshell:main"
|
|||||||
[tool.hatch.version]
|
[tool.hatch.version]
|
||||||
source = "vcs"
|
source = "vcs"
|
||||||
|
|
||||||
|
[tool.hatch.build]
|
||||||
|
include = [
|
||||||
|
"src/zshell/assets/**",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.hatch.build.targets.sdist]
|
[tool.hatch.build.targets.sdist]
|
||||||
only-include = [
|
only-include = [
|
||||||
"src",
|
"src",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from . import main
|
from zshell import main
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ def _complete_accent(ctx, incomplete):
|
|||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def list_presets(
|
def list_presets(
|
||||||
json_format: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
json_format: bool = typer.Option(
|
||||||
|
False, "--json", help="Output in JSON format"),
|
||||||
):
|
):
|
||||||
schemes = list_schemes()
|
schemes = list_schemes()
|
||||||
if json_format:
|
if json_format:
|
||||||
@@ -139,7 +140,7 @@ def generate(
|
|||||||
HOME = str(os.getenv("HOME"))
|
HOME = str(os.getenv("HOME"))
|
||||||
OUTPUT = Path(HOME + "/.local/state/zshell/scheme.json")
|
OUTPUT = Path(HOME + "/.local/state/zshell/scheme.json")
|
||||||
SEQ_STATE = Path(HOME + "/.local/state/zshell/sequences.txt")
|
SEQ_STATE = Path(HOME + "/.local/state/zshell/sequences.txt")
|
||||||
THUMB_PATH = Path(HOME + "/.cache/zshell/imagecache/thumbnail.jpg")
|
THUMB_DIR = Path(HOME + "/.cache/zshell/imagecache/thumbnails")
|
||||||
WALL_DIR_PATH = Path(HOME + "/.local/state/zshell/wallpaper_path.json")
|
WALL_DIR_PATH = Path(HOME + "/.local/state/zshell/wallpaper_path.json")
|
||||||
|
|
||||||
TEMPLATE_DIR = Path(HOME + "/.config/zshell/templates")
|
TEMPLATE_DIR = Path(HOME + "/.config/zshell/templates")
|
||||||
@@ -147,7 +148,8 @@ def generate(
|
|||||||
CONFIG = Path(HOME + "/.config/zshell/config.json")
|
CONFIG = Path(HOME + "/.config/zshell/config.json")
|
||||||
|
|
||||||
if preset is not None and image_path is not None:
|
if preset is not None and image_path is not None:
|
||||||
raise typer.BadParameter("Use either --image-path or --preset, not both.")
|
raise typer.BadParameter(
|
||||||
|
"Use either --image-path or --preset, not both.")
|
||||||
|
|
||||||
def get_scheme_class(scheme_name: str):
|
def get_scheme_class(scheme_name: str):
|
||||||
match scheme_name:
|
match scheme_name:
|
||||||
@@ -273,7 +275,8 @@ def generate(
|
|||||||
diff = difference_degrees(from_hct.hue, to_hct.hue)
|
diff = difference_degrees(from_hct.hue, to_hct.hue)
|
||||||
rotation = min(diff * 0.8, 100)
|
rotation = min(diff * 0.8, 100)
|
||||||
output_hue = sanitize_degrees_double(
|
output_hue = sanitize_degrees_double(
|
||||||
from_hct.hue + rotation * rotation_direction(from_hct.hue, to_hct.hue)
|
from_hct.hue + rotation *
|
||||||
|
rotation_direction(from_hct.hue, to_hct.hue)
|
||||||
)
|
)
|
||||||
tone = max(0.0, min(100.0, from_hct.tone * (1 + tone_boost)))
|
tone = max(0.0, min(100.0, from_hct.tone * (1 + tone_boost)))
|
||||||
return Hct.from_hct(output_hue, from_hct.chroma, tone)
|
return Hct.from_hct(output_hue, from_hct.chroma, tone)
|
||||||
@@ -307,15 +310,26 @@ def generate(
|
|||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def generate_thumbnail(image_path, thumbnail_path, size=(128, 128)):
|
def thumbnail_cache_path(image_path: Path, thumb_dir: Path) -> Path:
|
||||||
thumbnail_file = Path(thumbnail_path)
|
stat = image_path.stat()
|
||||||
|
key = f"{image_path.stem}_{stat.st_size}_{int(stat.st_mtime)}"
|
||||||
|
safe_key = re.sub(r"[^A-Za-z0-9._-]", "_", key)
|
||||||
|
return thumb_dir / f"{safe_key}_thumbnail.jpg"
|
||||||
|
|
||||||
|
def generate_thumbnail(image_path: Path, thumb_dir: Path, size=(128, 128)) -> Path:
|
||||||
|
thumb_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
cache_path = thumbnail_cache_path(image_path, thumb_dir)
|
||||||
|
|
||||||
|
if cache_path.exists():
|
||||||
|
return cache_path
|
||||||
|
|
||||||
image = Image.open(image_path)
|
image = Image.open(image_path)
|
||||||
|
image.draft("RGB", size)
|
||||||
image = image.convert("RGB")
|
image = image.convert("RGB")
|
||||||
image.thumbnail(size, Image.Resampling.NEAREST)
|
image.thumbnail(size, Image.Resampling.NEAREST)
|
||||||
|
image.save(cache_path, "JPEG")
|
||||||
|
|
||||||
thumbnail_file.parent.mkdir(parents=True, exist_ok=True)
|
return cache_path
|
||||||
image.save(thumbnail_path, "JPEG")
|
|
||||||
|
|
||||||
def apply_terms(sequences: str, sequences_tmux: str, state_path: Path) -> None:
|
def apply_terms(sequences: str, sequences_tmux: str, state_path: Path) -> None:
|
||||||
state_path.parent.mkdir(parents=True, exist_ok=True)
|
state_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -523,7 +537,8 @@ def generate(
|
|||||||
template = env.from_string(body)
|
template = env.from_string(body)
|
||||||
text = template.render(**context)
|
text = template.render(**context)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(f"Template render failed for '{rel}': {e}") from e
|
raise RuntimeError(
|
||||||
|
f"Template render failed for '{rel}': {e}") from e
|
||||||
|
|
||||||
out_path.write_text(text, encoding="utf-8")
|
out_path.write_text(text, encoding="utf-8")
|
||||||
|
|
||||||
@@ -586,7 +601,8 @@ def generate(
|
|||||||
(v.accents for v in meta.variants if v.id == p_variant), ()
|
(v.accents for v in meta.variants if v.id == p_variant), ()
|
||||||
)
|
)
|
||||||
if accent not in var_accents:
|
if accent not in var_accents:
|
||||||
available = ", ".join(var_accents) if var_accents else "none"
|
available = ", ".join(
|
||||||
|
var_accents) if var_accents else "none"
|
||||||
raise typer.BadParameter(
|
raise typer.BadParameter(
|
||||||
f"Accent '{accent}' not available for '{p_scheme}:{p_variant}'. Available accents: {available}"
|
f"Accent '{accent}' not available for '{p_scheme}:{p_variant}'. Available accents: {available}"
|
||||||
)
|
)
|
||||||
@@ -623,13 +639,13 @@ def generate(
|
|||||||
seed = hex_to_hct(colors.get("primary", "#000000").lstrip("#"))
|
seed = hex_to_hct(colors.get("primary", "#000000").lstrip("#"))
|
||||||
else:
|
else:
|
||||||
image_path = image_path or Path(WALL_PATH)
|
image_path = image_path or Path(WALL_PATH)
|
||||||
generate_thumbnail(image_path, str(THUMB_PATH))
|
thumb_path = generate_thumbnail(image_path, THUMB_DIR)
|
||||||
seed = seed_from_image(THUMB_PATH)
|
seed = seed_from_image(thumb_path)
|
||||||
name = "dynamic"
|
name = "dynamic"
|
||||||
flavor = "default"
|
flavor = "default"
|
||||||
|
|
||||||
if smart:
|
if smart:
|
||||||
effective_mode = smart_mode(THUMB_PATH)
|
effective_mode = smart_mode(thumb_path)
|
||||||
elif mode is not None:
|
elif mode is not None:
|
||||||
effective_mode = mode
|
effective_mode = mode
|
||||||
else:
|
else:
|
||||||
@@ -675,7 +691,9 @@ def generate(
|
|||||||
print(f"rendered: {p}")
|
print(f"rendered: {p}")
|
||||||
|
|
||||||
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with open(OUTPUT, "w") as f:
|
tmp_output = OUTPUT.with_suffix(".json.tmp")
|
||||||
|
with open(tmp_output, "w") as f:
|
||||||
json.dump(output_dict, f, indent=4)
|
json.dump(output_dict, f, indent=4)
|
||||||
|
os.replace(tmp_output, OUTPUT)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from importlib.resources import files
|
||||||
|
from importlib.resources.abc import Traversable
|
||||||
|
from pathlib import PurePosixPath
|
||||||
|
|
||||||
ASSETS = Path(__file__).resolve().parent.parent / "assets" / "schemes"
|
ASSETS: Traversable = files("zshell") / "assets" / "schemes"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -30,7 +32,7 @@ class Palette:
|
|||||||
accent: str | None = None
|
accent: str | None = None
|
||||||
|
|
||||||
|
|
||||||
def _parse_txt(path: Path) -> dict[str, str]:
|
def _parse_txt(path: Traversable) -> dict[str, str]:
|
||||||
colors: dict[str, str] = {}
|
colors: dict[str, str] = {}
|
||||||
for line in path.read_text().splitlines():
|
for line in path.read_text().splitlines():
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
@@ -46,7 +48,7 @@ def _parse_txt(path: Path) -> dict[str, str]:
|
|||||||
def _discover_schemes() -> dict[str, SchemeMeta]:
|
def _discover_schemes() -> dict[str, SchemeMeta]:
|
||||||
schemes: dict[str, SchemeMeta] = {}
|
schemes: dict[str, SchemeMeta] = {}
|
||||||
|
|
||||||
for scheme_dir in sorted(ASSETS.iterdir()):
|
for scheme_dir in sorted(ASSETS.iterdir(), key=lambda p: p.name):
|
||||||
if not scheme_dir.is_dir() or scheme_dir.name.startswith("."):
|
if not scheme_dir.is_dir() or scheme_dir.name.startswith("."):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -54,7 +56,7 @@ def _discover_schemes() -> dict[str, SchemeMeta]:
|
|||||||
display_name = sid.capitalize()
|
display_name = sid.capitalize()
|
||||||
|
|
||||||
variants: list[SchemeVariant] = []
|
variants: list[SchemeVariant] = []
|
||||||
for var_dir in sorted(scheme_dir.iterdir()):
|
for var_dir in sorted(scheme_dir.iterdir(), key=lambda p: p.name):
|
||||||
if not var_dir.is_dir() or var_dir.name.startswith("."):
|
if not var_dir.is_dir() or var_dir.name.startswith("."):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -62,9 +64,10 @@ def _discover_schemes() -> dict[str, SchemeMeta]:
|
|||||||
accents: set[str] = set()
|
accents: set[str] = set()
|
||||||
|
|
||||||
for f in var_dir.iterdir():
|
for f in var_dir.iterdir():
|
||||||
if f.suffix != ".txt":
|
name = PurePosixPath(f.name)
|
||||||
|
if name.suffix != ".txt":
|
||||||
continue
|
continue
|
||||||
stem = f.stem
|
stem = name.stem
|
||||||
if "-" in stem:
|
if "-" in stem:
|
||||||
maybe_accent, maybe_mode = stem.rsplit("-", 1)
|
maybe_accent, maybe_mode = stem.rsplit("-", 1)
|
||||||
if maybe_mode in ("dark", "light"):
|
if maybe_mode in ("dark", "light"):
|
||||||
@@ -101,12 +104,14 @@ SCHEMES: dict[str, SchemeMeta] = _discover_schemes()
|
|||||||
|
|
||||||
def get_palette(scheme: str, variant: str, mode: str, accent: str | None = None) -> Palette:
|
def get_palette(scheme: str, variant: str, mode: str, accent: str | None = None) -> Palette:
|
||||||
if scheme not in SCHEMES:
|
if scheme not in SCHEMES:
|
||||||
raise KeyError(f"Unknown scheme '{scheme}'. Available: {', '.join(SCHEMES)}")
|
raise KeyError(
|
||||||
|
f"Unknown scheme '{scheme}'. Available: {', '.join(SCHEMES)}")
|
||||||
|
|
||||||
meta = SCHEMES[scheme]
|
meta = SCHEMES[scheme]
|
||||||
var_ids = {v.id for v in meta.variants}
|
var_ids = {v.id for v in meta.variants}
|
||||||
if variant not in var_ids:
|
if variant not in var_ids:
|
||||||
raise KeyError(f"Unknown variant '{variant}' for scheme '{scheme}'. Available: {', '.join(sorted(var_ids))}")
|
raise KeyError(
|
||||||
|
f"Unknown variant '{variant}' for scheme '{scheme}'. Available: {', '.join(sorted(var_ids))}")
|
||||||
|
|
||||||
if accent:
|
if accent:
|
||||||
filename = f"{accent}-{mode}.txt"
|
filename = f"{accent}-{mode}.txt"
|
||||||
@@ -114,10 +119,10 @@ def get_palette(scheme: str, variant: str, mode: str, accent: str | None = None)
|
|||||||
filename = f"{mode}.txt"
|
filename = f"{mode}.txt"
|
||||||
|
|
||||||
txt_path = ASSETS / scheme / variant / filename
|
txt_path = ASSETS / scheme / variant / filename
|
||||||
if not txt_path.exists():
|
if not txt_path.is_file():
|
||||||
txt_path = ASSETS / scheme / variant / f"{mode}.txt"
|
txt_path = ASSETS / scheme / variant / f"{mode}.txt"
|
||||||
|
|
||||||
if not txt_path.exists():
|
if not txt_path.is_file():
|
||||||
var_info = next(v for v in meta.variants if v.id == variant)
|
var_info = next(v for v in meta.variants if v.id == variant)
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
f"No {mode} palette for '{scheme}:{variant}'. Available modes: {sorted(var_info.modes)}"
|
f"No {mode} palette for '{scheme}:{variant}'. Available modes: {sorted(var_info.modes)}"
|
||||||
|
|||||||
Reference in New Issue
Block a user