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.
Files changed (41) hide show
  1. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/PKG-INFO +1 -1
  2. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/gui_installer.py +49 -7
  3. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/sources.py +9 -1
  4. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit.egg-info/PKG-INFO +1 -1
  5. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/pyproject.toml +1 -1
  6. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/tests/test_sources.py +15 -0
  7. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/LICENSE +0 -0
  8. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/README.md +0 -0
  9. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/__init__.py +0 -0
  10. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/__main__.py +0 -0
  11. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/advertise.py +0 -0
  12. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/cron_installer.py +0 -0
  13. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/host.py +0 -0
  14. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/identity.py +0 -0
  15. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/onboarding.py +0 -0
  16. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/skills.py +0 -0
  17. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/taxonomy/__init__.py +0 -0
  18. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/taxonomy/build.py +0 -0
  19. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/taxonomy/capability.py +0 -0
  20. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/taxonomy/cluster.py +0 -0
  21. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/taxonomy/corpus.py +0 -0
  22. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/taxonomy/embedder.py +0 -0
  23. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/taxonomy/groups.py +0 -0
  24. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/taxonomy/llm_groups.py +0 -0
  25. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/tool_installer.py +0 -0
  26. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit/tui_installer.py +0 -0
  27. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit.egg-info/SOURCES.txt +0 -0
  28. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit.egg-info/dependency_links.txt +0 -0
  29. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit.egg-info/entry_points.txt +0 -0
  30. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit.egg-info/requires.txt +0 -0
  31. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/cli_tools_kit.egg-info/top_level.txt +0 -0
  32. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/setup.cfg +0 -0
  33. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/tests/test_capability_groups.py +0 -0
  34. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/tests/test_cron_installer.py +0 -0
  35. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/tests/test_host.py +0 -0
  36. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/tests/test_identity.py +0 -0
  37. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/tests/test_llm_groups.py +0 -0
  38. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/tests/test_skills.py +0 -0
  39. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/tests/test_taxonomy_cluster.py +0 -0
  40. {cli_tools_kit-0.6.2 → cli_tools_kit-0.6.4}/tests/test_tool_installer.py +0 -0
  41. {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.2
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
- image = ImageTk.PhotoImage(Image.open(icon_path))
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
- image = tk.PhotoImage(file=icon_path)
1855
- self.root.iconphoto(True, image)
1856
- self._icon_photo = image # keep a reference; Tk drops the icon otherwise
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
- return os.path.join(APPS_DIR,
5432
- os.path.splitext(SELF_DESKTOP_FILE)[0] + ".lnk")
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
- line = f'root = "{root}"\n'
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.2
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.2"
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