cli-tools-kit 0.6.2__tar.gz → 0.6.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/PKG-INFO +1 -1
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/gui_installer.py +49 -7
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/sources.py +9 -1
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit.egg-info/PKG-INFO +1 -1
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/pyproject.toml +1 -1
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/tests/test_sources.py +15 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/LICENSE +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/README.md +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/__init__.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/__main__.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/advertise.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/cron_installer.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/host.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/identity.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/onboarding.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/skills.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/taxonomy/__init__.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/taxonomy/build.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/taxonomy/capability.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/taxonomy/cluster.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/taxonomy/corpus.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/taxonomy/embedder.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/taxonomy/groups.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/taxonomy/llm_groups.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/tool_installer.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/tui_installer.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit.egg-info/SOURCES.txt +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit.egg-info/dependency_links.txt +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit.egg-info/entry_points.txt +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit.egg-info/requires.txt +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit.egg-info/top_level.txt +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/setup.cfg +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/tests/test_capability_groups.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/tests/test_cron_installer.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/tests/test_host.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/tests/test_identity.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/tests/test_llm_groups.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/tests/test_skills.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/tests/test_taxonomy_cluster.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/tests/test_tool_installer.py +0 -0
- {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/tests/test_tui_installer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cli-tools-kit
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
4
4
|
Summary: Installer protocol + helpers for self-installing Python CLI/GUI tools (desktop shortcuts, bash aliases, cron entries), plus reusable tkinter and curses installer screens
|
|
5
5
|
Author: Steffen Probst
|
|
6
6
|
License: MIT License
|
|
@@ -1836,6 +1836,12 @@ class InstallerApp:
|
|
|
1836
1836
|
# warning so the two don't fight over the log/dialog at the same tick.
|
|
1837
1837
|
self.root.after(200, self._maybe_auto_update_on_startup)
|
|
1838
1838
|
|
|
1839
|
+
# Standard freedesktop icon sizes. wm iconphoto silently fails to set
|
|
1840
|
+
# _NET_WM_ICON at all when handed only a source-resolution (e.g. 512x512)
|
|
1841
|
+
# image on some Tk/X11 combinations, so multiple smaller sizes are passed
|
|
1842
|
+
# and the window manager picks the one it wants.
|
|
1843
|
+
_ICON_SIZES = (16, 24, 32, 48, 64, 128)
|
|
1844
|
+
|
|
1839
1845
|
def _set_window_icon(self) -> None:
|
|
1840
1846
|
"""Apply SELF_DESKTOP_ICON to the live window (taskbar/titlebar).
|
|
1841
1847
|
|
|
@@ -1849,11 +1855,13 @@ class InstallerApp:
|
|
|
1849
1855
|
return
|
|
1850
1856
|
try:
|
|
1851
1857
|
if _HAVE_PIL:
|
|
1852
|
-
|
|
1858
|
+
source = Image.open(icon_path).convert("RGBA")
|
|
1859
|
+
images = [ImageTk.PhotoImage(source.resize((size, size), Image.LANCZOS))
|
|
1860
|
+
for size in self._ICON_SIZES]
|
|
1853
1861
|
else:
|
|
1854
|
-
|
|
1855
|
-
self.root.iconphoto(True,
|
|
1856
|
-
self.
|
|
1862
|
+
images = [tk.PhotoImage(file=icon_path)]
|
|
1863
|
+
self.root.iconphoto(True, *images)
|
|
1864
|
+
self._icon_photos = images # keep references; Tk drops the icon otherwise
|
|
1857
1865
|
except Exception:
|
|
1858
1866
|
pass # cosmetic only; never block startup over a bad/unsupported icon
|
|
1859
1867
|
|
|
@@ -5426,13 +5434,37 @@ class InstallerApp:
|
|
|
5426
5434
|
# ================= CLI FUNCTIONS =================
|
|
5427
5435
|
|
|
5428
5436
|
def _self_shortcut_path() -> str:
|
|
5429
|
-
"""The manager's own shortcut: a .desktop file, or a .lnk on Windows.
|
|
5437
|
+
"""The manager's own shortcut: a .desktop file, or a .lnk on Windows.
|
|
5438
|
+
|
|
5439
|
+
Windows shows the file name in the Start Menu, where freedesktop reads the
|
|
5440
|
+
Name= line inside the file, so the .lnk is named after SELF_DESKTOP_NAME.
|
|
5441
|
+
Characters a file name cannot hold become spaces, and a name that is empty
|
|
5442
|
+
or all-punctuation falls back to the desktop file's stem.
|
|
5443
|
+
"""
|
|
5430
5444
|
if host.IS_WINDOWS:
|
|
5431
|
-
|
|
5432
|
-
|
|
5445
|
+
stem = "".join(" " if c in '<>:"/\\|?*' else c
|
|
5446
|
+
for c in SELF_DESKTOP_NAME).strip(" .")
|
|
5447
|
+
stem = " ".join(stem.split()) or os.path.splitext(SELF_DESKTOP_FILE)[0]
|
|
5448
|
+
return os.path.join(APPS_DIR, stem + ".lnk")
|
|
5433
5449
|
return os.path.join(APPS_DIR, SELF_DESKTOP_FILE)
|
|
5434
5450
|
|
|
5435
5451
|
|
|
5452
|
+
def _windows_icon(icon: str) -> Optional[str]:
|
|
5453
|
+
"""The .ico Windows can draw for `icon`, or None.
|
|
5454
|
+
|
|
5455
|
+
A .lnk renders only .ico/.exe/.dll; the shell stores a .png path without
|
|
5456
|
+
complaint and then draws nothing. Consumers configure one icon, normally a
|
|
5457
|
+
.png for freedesktop, so take a sibling .ico of the same stem when there is
|
|
5458
|
+
one and otherwise leave the shortcut on its default picture.
|
|
5459
|
+
"""
|
|
5460
|
+
if not icon:
|
|
5461
|
+
return None
|
|
5462
|
+
for candidate in (icon, os.path.splitext(icon)[0] + ".ico"):
|
|
5463
|
+
if candidate.lower().endswith(".ico") and os.path.isfile(candidate):
|
|
5464
|
+
return candidate
|
|
5465
|
+
return None
|
|
5466
|
+
|
|
5467
|
+
|
|
5436
5468
|
def cli_install_self(quiet: bool = False) -> tuple[bool, str]:
|
|
5437
5469
|
"""Install the manager's own desktop shortcut. Returns (success, message)."""
|
|
5438
5470
|
try:
|
|
@@ -5442,9 +5474,19 @@ def cli_install_self(quiet: bool = False) -> tuple[bool, str]:
|
|
|
5442
5474
|
if host.IS_WINDOWS:
|
|
5443
5475
|
lnk_path = _self_shortcut_path()
|
|
5444
5476
|
ok = host.write_shortcut(lnk_path, python_exec, f'"{script_path}"',
|
|
5477
|
+
icon=_windows_icon(SELF_DESKTOP_ICON),
|
|
5445
5478
|
workdir=ROOT_DIR)
|
|
5446
5479
|
if not ok:
|
|
5447
5480
|
return False, f"Could not write {lnk_path}"
|
|
5481
|
+
# Before 0.6.4 the .lnk was named after the desktop file's stem.
|
|
5482
|
+
# Drop that one, or the Start Menu keeps both.
|
|
5483
|
+
legacy = os.path.join(APPS_DIR,
|
|
5484
|
+
os.path.splitext(SELF_DESKTOP_FILE)[0] + ".lnk")
|
|
5485
|
+
if legacy != lnk_path and os.path.isfile(legacy):
|
|
5486
|
+
try:
|
|
5487
|
+
os.remove(legacy)
|
|
5488
|
+
except OSError:
|
|
5489
|
+
pass
|
|
5448
5490
|
if not quiet:
|
|
5449
5491
|
print(f"Installed: {lnk_path}")
|
|
5450
5492
|
return True, lnk_path
|
|
@@ -157,7 +157,15 @@ def save_local_root(config_path, root, local_path=None, log: Callable = print) -
|
|
|
157
157
|
except OSError as exc:
|
|
158
158
|
log(f"{local.name}: not updated ({exc})")
|
|
159
159
|
return False
|
|
160
|
-
|
|
160
|
+
# A TOML literal string, because a Windows root is full of backslashes and
|
|
161
|
+
# a basic string would read them as escapes: "C:\Users\..." dies on \U and
|
|
162
|
+
# takes the whole file with it, so the question would be asked again on
|
|
163
|
+
# every launch. A path holding a single quote falls back to a basic string
|
|
164
|
+
# with the two characters TOML needs escaped there.
|
|
165
|
+
if "'" in root:
|
|
166
|
+
line = 'root = "{}"\n'.format(root.replace("\\", "\\\\").replace('"', '\\"'))
|
|
167
|
+
else:
|
|
168
|
+
line = f"root = '{root}'\n"
|
|
161
169
|
try:
|
|
162
170
|
local.write_text(line + ("\n" + existing.lstrip("\n") if existing.strip() else ""),
|
|
163
171
|
encoding="utf-8")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cli-tools-kit
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
4
4
|
Summary: Installer protocol + helpers for self-installing Python CLI/GUI tools (desktop shortcuts, bash aliases, cron entries), plus reusable tkinter and curses installer screens
|
|
5
5
|
Author: Steffen Probst
|
|
6
6
|
License: MIT License
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cli-tools-kit"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.4"
|
|
8
8
|
description = "Installer protocol + helpers for self-installing Python CLI/GUI tools (desktop shortcuts, bash aliases, cron entries), plus reusable tkinter and curses installer screens"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -445,6 +445,21 @@ def test_an_existing_root_in_the_local_file_is_never_overwritten(tmp_path: Path)
|
|
|
445
445
|
assert sources.local_root(config) == str(tmp_path / "mine")
|
|
446
446
|
|
|
447
447
|
|
|
448
|
+
def test_a_windows_root_survives_the_round_trip(tmp_path: Path) -> None:
|
|
449
|
+
"""A backslash path must not be written as a TOML basic string.
|
|
450
|
+
|
|
451
|
+
``root = "C:\\Users\\..."`` reads the \\U as a Unicode escape and kills the
|
|
452
|
+
whole file, so the install location was asked for again on every launch.
|
|
453
|
+
"""
|
|
454
|
+
roots = [r"C:\Users\tester\WW3-tools", r"C:\temp\new\table\unicode",
|
|
455
|
+
r"C:\Users\o'brien\tools"]
|
|
456
|
+
for i, root in enumerate(roots):
|
|
457
|
+
config = _write(tmp_path / str(i) / "installer.toml", "")
|
|
458
|
+
assert sources.save_local_root(config, root, log=lambda *a: None)
|
|
459
|
+
local = config.with_name("installer.local.toml")
|
|
460
|
+
assert sources._read_toml(local).get("root") == root
|
|
461
|
+
|
|
462
|
+
|
|
448
463
|
def test_the_default_root_name_names_the_suggested_folder(tmp_path: Path, engine,
|
|
449
464
|
monkeypatch) -> None:
|
|
450
465
|
config = _tree(tmp_path)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|