cli-tools-kit 1.0.0__tar.gz → 1.1.1__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-1.0.0 → cli_tools_kit-1.1.1}/PKG-INFO +35 -6
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/README.md +34 -5
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/__init__.py +1 -1
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/cli.py +29 -1
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/gui_installer.py +221 -44
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/host.py +16 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/sources.py +37 -10
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/state.py +9 -1
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/tui_installer.py +48 -2
- cli_tools_kit-1.1.1/cli_tools_kit/upgrade.py +323 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit.egg-info/PKG-INFO +35 -6
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit.egg-info/SOURCES.txt +3 -1
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/tests/test_public_api.py +2 -2
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/tests/test_sources.py +14 -0
- cli_tools_kit-1.1.1/tests/test_upgrade.py +224 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/LICENSE +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/__main__.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/advertise.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/autostart.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/autostart_gate.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/cron_installer.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/discovery.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/icons.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/identity.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/install.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/onboarding.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/settings.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/skills.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/sweep.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/taxonomy/__init__.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/taxonomy/build.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/taxonomy/capability.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/taxonomy/cluster.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/taxonomy/corpus.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/taxonomy/embedder.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/taxonomy/groups.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/taxonomy/llm_groups.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/testing.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit/tool_installer.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit.egg-info/dependency_links.txt +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit.egg-info/entry_points.txt +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit.egg-info/requires.txt +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/cli_tools_kit.egg-info/top_level.txt +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/pyproject.toml +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/setup.cfg +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/tests/test_autostart_gate.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/tests/test_capability_groups.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/tests/test_cron_installer.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/tests/test_e2e.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/tests/test_engine.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/tests/test_gui_smoke.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/tests/test_hooks.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/tests/test_host.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/tests/test_identity.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/tests/test_llm_groups.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/tests/test_skills.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/tests/test_taxonomy_cluster.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/tests/test_testing.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/tests/test_tool_installer.py +0 -0
- {cli_tools_kit-1.0.0 → cli_tools_kit-1.1.1}/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: 1.
|
|
3
|
+
Version: 1.1.1
|
|
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
|
|
@@ -305,23 +305,52 @@ Keyword-only; every argument defaults to `None`, meaning "leave the default".
|
|
|
305
305
|
| `notify_app` | identity's | `notify-send` application label on the `--check` path. |
|
|
306
306
|
| `autostart_check_desktop_name`, `check_log_name`, `check_state_name` | identity's | Login-check artifact filenames. |
|
|
307
307
|
| `hooks` | `None` | `InstallHooks(install_tool=…, remove_tool=…, install_skill=…, uninstall_skill=…)`: replace how one tool is installed, for a wrapper that builds a venv per tool. A field left `None` keeps the kit's own. |
|
|
308
|
+
| `upgrade_repos` | `[]` | `(name, path)` of the tool repos an upgrade may pull. `sources.run_installer` fills it with the repos it cloned. See "Upgrades" below. |
|
|
308
309
|
|
|
309
310
|
The identity is applied first and these individual names override it, so you can
|
|
310
311
|
take the whole namespace from a slug and still change one thing.
|
|
311
312
|
|
|
312
313
|
`run()` owns its own `argparse` and consumes `sys.argv`: `--list`, `--check`,
|
|
313
|
-
`--enable-autostart-check`, `--install`, `--update-all`, `--
|
|
314
|
-
`--gui`, and a screen when given none of them. A wrapper that needs its own
|
|
314
|
+
`--enable-autostart-check`, `--install`, `--update-all`, `--upgrade`, `--cleanup`,
|
|
315
|
+
`--tui`, `--gui`, and a screen when given none of them. A wrapper that needs its own
|
|
315
316
|
subcommands should skip `run()` and call the primitives (`discover_tools`,
|
|
316
317
|
`install_tool`, `remove_tool`, `cli_check`) after applying an identity with
|
|
317
318
|
`_apply_identity`.
|
|
318
319
|
|
|
320
|
+
### Upgrades
|
|
321
|
+
|
|
322
|
+
When the window or the text screen opens, the installer checks in the
|
|
323
|
+
background whether anything it runs is out of date:
|
|
324
|
+
|
|
325
|
+
- the installer's own checkout (the git repo `entry_script` is in) is behind
|
|
326
|
+
its upstream;
|
|
327
|
+
- a tool repo in `upgrade_repos` is behind its upstream;
|
|
328
|
+
- pip would install a newer cli-tools-kit within the installer's pin. The pin is
|
|
329
|
+
the `requirements.txt` next to `entry_script` when it names the kit, else
|
|
330
|
+
anything below the next major version.
|
|
331
|
+
|
|
332
|
+
If something is, a strip above the table lists it with an Upgrade button (on
|
|
333
|
+
the text screen, a log line and the `u` key). Upgrade pulls the repos with `git
|
|
334
|
+
pull --ff-only`, runs `pip install --upgrade` for the kit, reinstalls the
|
|
335
|
+
installed tools of each pulled repo, and starts the installer again with its
|
|
336
|
+
original command line so the new code is loaded. `--upgrade` does the same
|
|
337
|
+
from a script, checking at once and without the restart.
|
|
338
|
+
|
|
339
|
+
The network is used at most once a day: one `git fetch` per repo and one `pip
|
|
340
|
+
install --dry-run`, cached under the identity's cache directory. The
|
|
341
|
+
comparison with what is on disk runs on every start, so a repo pulled by hand
|
|
342
|
+
stops showing at once. Git never asks for a password here; a private repo
|
|
343
|
+
without stored credentials is skipped. The kit is left alone when it runs from
|
|
344
|
+
a development checkout or outside a virtual environment. `--check` never runs
|
|
345
|
+
any of this, so the login check stays network-free.
|
|
346
|
+
|
|
319
347
|
### The text screen
|
|
320
348
|
|
|
321
349
|
Without a display (`DISPLAY`/`WAYLAND_DISPLAY` unset: SSH, WSL, a server) or
|
|
322
350
|
without `python3-tk`, `run()` opens a curses screen instead of the tkinter
|
|
323
351
|
window; `--tui` and `--gui` force either. Same rows, same Apply: `Space` ticks
|
|
324
|
-
Install, `s` ticks Skill, `a`/`n` tick all or none, `Enter` applies, `
|
|
352
|
+
Install, `s` ticks Skill, `a`/`n` tick all or none, `Enter` applies, `u`
|
|
353
|
+
upgrades when an upgrade is offered, `q` quits.
|
|
325
354
|
On a host where none of the tools is installed yet every row starts ticked.
|
|
326
355
|
|
|
327
356
|
A skill can go to more than one place. The default target writes
|
|
@@ -502,8 +531,8 @@ full rather than shallow (a tool that stamps its output with its commit needs th
|
|
|
502
531
|
history), and nothing is cloned into a root that does not exist or cannot be
|
|
503
532
|
written to. A clone that fails prints one line and that source is dropped, so a
|
|
504
533
|
colleague without access to a private repo still gets everybody else's tools.
|
|
505
|
-
`--refresh`
|
|
506
|
-
given by `path` is never pulled. Cloning happens in the engine's `pre_discovery`
|
|
534
|
+
`--refresh` and the upgrade (see "Upgrades") bring the clones up to date with
|
|
535
|
+
`git pull --ff-only`; a checkout given by `path` is never pulled. Cloning happens in the engine's `pre_discovery`
|
|
507
536
|
hook, which `--check` skips, so the login check stays network-free.
|
|
508
537
|
|
|
509
538
|
An `org` entry is the only thing in this module that reaches anything but git,
|
|
@@ -245,23 +245,52 @@ Keyword-only; every argument defaults to `None`, meaning "leave the default".
|
|
|
245
245
|
| `notify_app` | identity's | `notify-send` application label on the `--check` path. |
|
|
246
246
|
| `autostart_check_desktop_name`, `check_log_name`, `check_state_name` | identity's | Login-check artifact filenames. |
|
|
247
247
|
| `hooks` | `None` | `InstallHooks(install_tool=…, remove_tool=…, install_skill=…, uninstall_skill=…)`: replace how one tool is installed, for a wrapper that builds a venv per tool. A field left `None` keeps the kit's own. |
|
|
248
|
+
| `upgrade_repos` | `[]` | `(name, path)` of the tool repos an upgrade may pull. `sources.run_installer` fills it with the repos it cloned. See "Upgrades" below. |
|
|
248
249
|
|
|
249
250
|
The identity is applied first and these individual names override it, so you can
|
|
250
251
|
take the whole namespace from a slug and still change one thing.
|
|
251
252
|
|
|
252
253
|
`run()` owns its own `argparse` and consumes `sys.argv`: `--list`, `--check`,
|
|
253
|
-
`--enable-autostart-check`, `--install`, `--update-all`, `--
|
|
254
|
-
`--gui`, and a screen when given none of them. A wrapper that needs its own
|
|
254
|
+
`--enable-autostart-check`, `--install`, `--update-all`, `--upgrade`, `--cleanup`,
|
|
255
|
+
`--tui`, `--gui`, and a screen when given none of them. A wrapper that needs its own
|
|
255
256
|
subcommands should skip `run()` and call the primitives (`discover_tools`,
|
|
256
257
|
`install_tool`, `remove_tool`, `cli_check`) after applying an identity with
|
|
257
258
|
`_apply_identity`.
|
|
258
259
|
|
|
260
|
+
### Upgrades
|
|
261
|
+
|
|
262
|
+
When the window or the text screen opens, the installer checks in the
|
|
263
|
+
background whether anything it runs is out of date:
|
|
264
|
+
|
|
265
|
+
- the installer's own checkout (the git repo `entry_script` is in) is behind
|
|
266
|
+
its upstream;
|
|
267
|
+
- a tool repo in `upgrade_repos` is behind its upstream;
|
|
268
|
+
- pip would install a newer cli-tools-kit within the installer's pin. The pin is
|
|
269
|
+
the `requirements.txt` next to `entry_script` when it names the kit, else
|
|
270
|
+
anything below the next major version.
|
|
271
|
+
|
|
272
|
+
If something is, a strip above the table lists it with an Upgrade button (on
|
|
273
|
+
the text screen, a log line and the `u` key). Upgrade pulls the repos with `git
|
|
274
|
+
pull --ff-only`, runs `pip install --upgrade` for the kit, reinstalls the
|
|
275
|
+
installed tools of each pulled repo, and starts the installer again with its
|
|
276
|
+
original command line so the new code is loaded. `--upgrade` does the same
|
|
277
|
+
from a script, checking at once and without the restart.
|
|
278
|
+
|
|
279
|
+
The network is used at most once a day: one `git fetch` per repo and one `pip
|
|
280
|
+
install --dry-run`, cached under the identity's cache directory. The
|
|
281
|
+
comparison with what is on disk runs on every start, so a repo pulled by hand
|
|
282
|
+
stops showing at once. Git never asks for a password here; a private repo
|
|
283
|
+
without stored credentials is skipped. The kit is left alone when it runs from
|
|
284
|
+
a development checkout or outside a virtual environment. `--check` never runs
|
|
285
|
+
any of this, so the login check stays network-free.
|
|
286
|
+
|
|
259
287
|
### The text screen
|
|
260
288
|
|
|
261
289
|
Without a display (`DISPLAY`/`WAYLAND_DISPLAY` unset: SSH, WSL, a server) or
|
|
262
290
|
without `python3-tk`, `run()` opens a curses screen instead of the tkinter
|
|
263
291
|
window; `--tui` and `--gui` force either. Same rows, same Apply: `Space` ticks
|
|
264
|
-
Install, `s` ticks Skill, `a`/`n` tick all or none, `Enter` applies, `
|
|
292
|
+
Install, `s` ticks Skill, `a`/`n` tick all or none, `Enter` applies, `u`
|
|
293
|
+
upgrades when an upgrade is offered, `q` quits.
|
|
265
294
|
On a host where none of the tools is installed yet every row starts ticked.
|
|
266
295
|
|
|
267
296
|
A skill can go to more than one place. The default target writes
|
|
@@ -442,8 +471,8 @@ full rather than shallow (a tool that stamps its output with its commit needs th
|
|
|
442
471
|
history), and nothing is cloned into a root that does not exist or cannot be
|
|
443
472
|
written to. A clone that fails prints one line and that source is dropped, so a
|
|
444
473
|
colleague without access to a private repo still gets everybody else's tools.
|
|
445
|
-
`--refresh`
|
|
446
|
-
given by `path` is never pulled. Cloning happens in the engine's `pre_discovery`
|
|
474
|
+
`--refresh` and the upgrade (see "Upgrades") bring the clones up to date with
|
|
475
|
+
`git pull --ff-only`; a checkout given by `path` is never pulled. Cloning happens in the engine's `pre_discovery`
|
|
447
476
|
hook, which `--check` skips, so the login check stays network-free.
|
|
448
477
|
|
|
449
478
|
An `org` entry is the only thing in this module that reaches anything but git,
|
|
@@ -226,6 +226,22 @@ def cli_cleanup(dry_run: bool = False) -> int:
|
|
|
226
226
|
return errors
|
|
227
227
|
|
|
228
228
|
|
|
229
|
+
def cli_upgrade(tools: List[discovery.ToolEntry]) -> int:
|
|
230
|
+
"""Check for newer versions now and upgrade what is behind. Returns 0 or 1."""
|
|
231
|
+
from . import upgrade # noqa: PLC0415 — imports sources, only this path needs it
|
|
232
|
+
print("\nChecking for upgrades...")
|
|
233
|
+
items = upgrade.check(force=True)
|
|
234
|
+
if not items:
|
|
235
|
+
print("Everything is up to date.\n")
|
|
236
|
+
return 0
|
|
237
|
+
for item in items:
|
|
238
|
+
print(f" {item.label()}")
|
|
239
|
+
print()
|
|
240
|
+
result = upgrade.upgrade(items, tools, lambda msg, tag="info": print(msg))
|
|
241
|
+
print(f"\nDone{', with errors' if result['errors'] else ''}.\n")
|
|
242
|
+
return 1 if result["errors"] else 0
|
|
243
|
+
|
|
244
|
+
|
|
229
245
|
def cli_update_all(tools: List[discovery.ToolEntry]) -> int:
|
|
230
246
|
"""Sync: clean up orphans, then reinstall manager and all installed tool shortcuts.
|
|
231
247
|
|
|
@@ -439,6 +455,9 @@ def main():
|
|
|
439
455
|
help="Install the login update-check autostart entry")
|
|
440
456
|
parser.add_argument("--disable-autostart-check", action="store_true",
|
|
441
457
|
help="Remove the login update-check autostart entry")
|
|
458
|
+
parser.add_argument("--upgrade", action="store_true",
|
|
459
|
+
help="Check the installer's checkout, the tool repos it cloned and "
|
|
460
|
+
"cli-tools-kit for newer versions, and upgrade them (network).")
|
|
442
461
|
parser.add_argument("--refresh", action="store_true",
|
|
443
462
|
help="Before discovery, run the configured PRE_DISCOVERY hook in refresh "
|
|
444
463
|
"mode (e.g. ff-only pull every known repo checkout). No-op without a hook.")
|
|
@@ -494,6 +513,9 @@ def main():
|
|
|
494
513
|
if args.update_all:
|
|
495
514
|
sys.exit(1 if cli_update_all(tools) else 0)
|
|
496
515
|
|
|
516
|
+
if args.upgrade:
|
|
517
|
+
sys.exit(cli_upgrade(tools))
|
|
518
|
+
|
|
497
519
|
if args.apply:
|
|
498
520
|
from . import tui_installer
|
|
499
521
|
sys.exit(tui_installer.apply_headless(tools, ",".join(args.apply), args.skill_target,
|
|
@@ -549,7 +571,8 @@ def run(*, identity: Optional[InstallerIdentity] = None,
|
|
|
549
571
|
self_desktop_file: Optional[str] = None, self_desktop_name: Optional[str] = None,
|
|
550
572
|
self_desktop_icon: Optional[str] = None,
|
|
551
573
|
wm_class: Optional[str] = None, notify_app: Optional[str] = None,
|
|
552
|
-
hooks: Optional[install.InstallHooks] = None
|
|
574
|
+
hooks: Optional[install.InstallHooks] = None,
|
|
575
|
+
upgrade_repos: Optional[List] = None) -> None:
|
|
553
576
|
"""Configure the engine from a thin wrapper and dispatch the standard CLI/GUI.
|
|
554
577
|
|
|
555
578
|
Every argument maps to a module-level config global; ``None`` leaves the
|
|
@@ -567,6 +590,9 @@ def run(*, identity: Optional[InstallerIdentity] = None,
|
|
|
567
590
|
``hooks`` replaces how one tool is installed, removed or given its skill,
|
|
568
591
|
for a wrapper that builds a venv per tool itself. See :class:`InstallHooks`.
|
|
569
592
|
|
|
593
|
+
``upgrade_repos`` lists ``(name, path)`` of the tool repos the upgrade may
|
|
594
|
+
pull; ``sources.run_installer`` fills it with the repos it cloned.
|
|
595
|
+
|
|
570
596
|
``prune`` adds directory names the default wider walk never enters, on top
|
|
571
597
|
of ``DISCOVERY_PRUNE``. It does nothing when a wrapper passes its own
|
|
572
598
|
``discoverer``.
|
|
@@ -629,6 +655,8 @@ def run(*, identity: Optional[InstallerIdentity] = None,
|
|
|
629
655
|
state.NOTIFY_APP = notify_app
|
|
630
656
|
if hooks is not None:
|
|
631
657
|
install._apply_hooks(hooks)
|
|
658
|
+
if upgrade_repos is not None:
|
|
659
|
+
state.UPGRADE_REPOS = upgrade_repos
|
|
632
660
|
|
|
633
661
|
state._load_env() # re-read ROOT_DIR/.env now that ROOT_DIR is final
|
|
634
662
|
state._recompute_check_paths() # re-derive login-check artifact paths from the names
|
|
@@ -62,6 +62,7 @@ except ImportError: # pragma: no cover - exercised only where python3-tk is mis
|
|
|
62
62
|
from typing import Dict, List, Optional
|
|
63
63
|
|
|
64
64
|
from . import host, state, settings, discovery, install, sweep, autostart, icons, cli
|
|
65
|
+
from . import upgrade
|
|
65
66
|
from .autostart_gate import (
|
|
66
67
|
_parse_hhmm,
|
|
67
68
|
current_ssids,
|
|
@@ -98,6 +99,18 @@ RIGHT_COLS = (
|
|
|
98
99
|
("autostart", 85),
|
|
99
100
|
)
|
|
100
101
|
|
|
102
|
+
# The texts each right-hand column shows. A column is made wide enough for its
|
|
103
|
+
# longest text in the current font, so "✗ Not installed" is not cut off when
|
|
104
|
+
# the display scaling makes the font larger than the widths above assume.
|
|
105
|
+
_COLUMN_TEXTS = {
|
|
106
|
+
"uses": ("Uses", "9999"),
|
|
107
|
+
"status": ("Status", "✓ Installed", "✗ Not installed", "⟳ Needs Update",
|
|
108
|
+
"⟳ Skill update", "⟳ Update + skill"),
|
|
109
|
+
"skill": ("Skill",),
|
|
110
|
+
"icon": ("Icon",),
|
|
111
|
+
"autostart": ("Auto-Start",),
|
|
112
|
+
}
|
|
113
|
+
|
|
101
114
|
_DEBUG_CELL_COLORS = {
|
|
102
115
|
"uses": "#00ffff",
|
|
103
116
|
"status": "#0000ff",
|
|
@@ -258,6 +271,10 @@ class InstallerApp:
|
|
|
258
271
|
self.tk_widgets: List[tk.Widget] = [] # Other tk widgets that need bg updates
|
|
259
272
|
self.usage_counts: Dict[str, int] = get_all_usage_counts() # Tool usage statistics
|
|
260
273
|
|
|
274
|
+
# What upgrade.check() found; the strip above the table shows it and
|
|
275
|
+
# survives a theme rebuild.
|
|
276
|
+
self._upgrade_items: List[upgrade.Item] = []
|
|
277
|
+
|
|
261
278
|
# Search/filter tracking
|
|
262
279
|
self.search_var = tk.StringVar()
|
|
263
280
|
self.category_widgets: Dict[str, tk.Widget] = {} # category_name -> frame
|
|
@@ -272,6 +289,8 @@ class InstallerApp:
|
|
|
272
289
|
self.orphan_desktops = sweep.find_orphan_desktop_files()
|
|
273
290
|
self.orphan_aliases = sweep.find_orphan_aliases()
|
|
274
291
|
|
|
292
|
+
self._col_widths = self._column_widths()
|
|
293
|
+
self._wrap_held = False # see _position_window
|
|
275
294
|
self._setup_ui()
|
|
276
295
|
self._position_window()
|
|
277
296
|
self.root.deiconify() # Show now that it's properly sized
|
|
@@ -285,6 +304,10 @@ class InstallerApp:
|
|
|
285
304
|
# warning so the two don't fight over the log/dialog at the same tick.
|
|
286
305
|
self.root.after(200, self._maybe_auto_update_on_startup)
|
|
287
306
|
|
|
307
|
+
# Newer installer, tool repos or kit: asks git and pip off the main
|
|
308
|
+
# thread, over the network at most once a day (see upgrade.check).
|
|
309
|
+
self.root.after(300, self._start_upgrade_check)
|
|
310
|
+
|
|
288
311
|
# Standard freedesktop icon sizes. wm iconphoto silently fails to set
|
|
289
312
|
# _NET_WM_ICON at all when handed only a source-resolution (e.g. 512x512)
|
|
290
313
|
# image on some Tk/X11 combinations, so multiple smaller sizes are passed
|
|
@@ -314,6 +337,22 @@ class InstallerApp:
|
|
|
314
337
|
except Exception:
|
|
315
338
|
pass # cosmetic only; never block startup over a bad/unsupported icon
|
|
316
339
|
|
|
340
|
+
def _column_widths(self) -> Dict[str, int]:
|
|
341
|
+
"""RIGHT_COLS widths, scaled with the display and fitted to their texts.
|
|
342
|
+
|
|
343
|
+
The pixel widths in RIGHT_COLS were chosen at Tk's 96 dpi scaling.
|
|
344
|
+
They grow with the scaling and never drop below what the column's
|
|
345
|
+
longest text needs in the default font.
|
|
346
|
+
"""
|
|
347
|
+
from tkinter import font as tkfont # noqa: PLC0415 — tkinter is optional at import
|
|
348
|
+
font = tkfont.nametofont("TkDefaultFont")
|
|
349
|
+
scale = max(1.0, float(self.root.tk.call("tk", "scaling")) / (96 / 72))
|
|
350
|
+
widths = {}
|
|
351
|
+
for key, base in RIGHT_COLS:
|
|
352
|
+
text = max((font.measure(t) for t in _COLUMN_TEXTS.get(key, ())), default=0)
|
|
353
|
+
widths[key] = max(round(base * scale), text + 16)
|
|
354
|
+
return widths
|
|
355
|
+
|
|
317
356
|
def _get_primary_monitor_geometry(self) -> tuple[int, int, int, int]:
|
|
318
357
|
"""Get primary monitor geometry (x, y, width, height). Falls back to tkinter defaults."""
|
|
319
358
|
try:
|
|
@@ -338,41 +377,54 @@ class InstallerApp:
|
|
|
338
377
|
return 0, 0, self.root.winfo_screenwidth(), self.root.winfo_screenheight()
|
|
339
378
|
|
|
340
379
|
def _position_window(self):
|
|
341
|
-
"""
|
|
380
|
+
"""Size the window to what every row asks for, within the monitor.
|
|
381
|
+
|
|
382
|
+
The width is the widest of the title row, the footer and the tool
|
|
383
|
+
table. The height is the fixed rows plus the table, measured after the
|
|
384
|
+
width is set, because tool descriptions wrap to the width and a wider
|
|
385
|
+
table is shorter. Both stay within 90% of the monitor, which leaves
|
|
386
|
+
room for panels and the title bar; beyond that the table scrolls. The
|
|
387
|
+
minimum size keeps the footer's buttons from being cut off.
|
|
388
|
+
"""
|
|
342
389
|
self.root.update_idletasks()
|
|
343
|
-
|
|
344
390
|
mon_x, mon_y, mon_width, mon_height = self._get_primary_monitor_geometry()
|
|
391
|
+
avail_w, avail_h = int(mon_width * 0.9), int(mon_height * 0.9)
|
|
392
|
+
|
|
393
|
+
# The root's request covers the title, search and footer rows; the table
|
|
394
|
+
# sits in a canvas and is not part of it. The table's own request is no
|
|
395
|
+
# use either: until the window is drawn every description asks for one
|
|
396
|
+
# long line. So the table counts with its fixed columns plus room for
|
|
397
|
+
# the tool names.
|
|
398
|
+
scale = max(1.0, float(self.root.tk.call("tk", "scaling")) / (96 / 72))
|
|
399
|
+
scrollbar_w = self.scrollbar.winfo_reqwidth()
|
|
400
|
+
fixed_cols = sum(self._col_widths.values()) + len(RIGHT_COLS)
|
|
401
|
+
overhead = 40 + scrollbar_w + sum(TABLE_PADX) + fixed_cols + 70 # padding, checkbox
|
|
402
|
+
rows_w = self.root.winfo_reqwidth()
|
|
403
|
+
window_width = min(max(rows_w, overhead + round(300 * scale)), avail_w)
|
|
404
|
+
|
|
405
|
+
# Wrap the descriptions for that width before measuring the height.
|
|
406
|
+
# Their <Configure> handlers are held meanwhile: while the window is
|
|
407
|
+
# hidden they see the table's narrow first layout and wrap at 100 px.
|
|
408
|
+
# Once it is shown they set the exact wrap.
|
|
409
|
+
self._wrap_held = True
|
|
410
|
+
for label in self._desc_labels:
|
|
411
|
+
label.configure(wraplength=max(100, window_width - overhead))
|
|
412
|
+
self.root.update_idletasks()
|
|
345
413
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
non_scrollable_height = 230
|
|
351
|
-
content_height = scrollable_height + non_scrollable_height
|
|
352
|
-
|
|
353
|
-
# Clamp width to reasonable bounds (max 800 for usability)
|
|
354
|
-
window_width = max(500, min(content_width, 800, mon_width - 100))
|
|
355
|
-
|
|
356
|
-
# Use content height if it fits, otherwise use full monitor height
|
|
357
|
-
if content_height <= mon_height:
|
|
358
|
-
window_height = content_height
|
|
359
|
-
# Center vertically on primary monitor
|
|
360
|
-
y = mon_y + (mon_height - window_height) // 2
|
|
361
|
-
# Hide scrollbar initially - content fits
|
|
362
|
-
self.scrollbar.grid_forget()
|
|
363
|
-
else:
|
|
364
|
-
# Use full monitor height, align to top
|
|
365
|
-
window_height = mon_height
|
|
366
|
-
y = mon_y
|
|
367
|
-
# Show scrollbar initially - content exceeds screen
|
|
414
|
+
fixed_h = self.root.winfo_reqheight() - self.outer_frame.winfo_reqheight()
|
|
415
|
+
content_h = fixed_h + self.scrollable_frame.winfo_reqheight()
|
|
416
|
+
window_height = min(content_h, avail_h)
|
|
417
|
+
if content_h > avail_h:
|
|
368
418
|
self.scrollbar.grid(row=0, column=1, sticky="ns")
|
|
419
|
+
else:
|
|
420
|
+
self.scrollbar.grid_forget()
|
|
369
421
|
|
|
370
|
-
# Center horizontally on primary monitor
|
|
371
422
|
x = mon_x + (mon_width - window_width) // 2
|
|
372
|
-
|
|
423
|
+
y = mon_y + (mon_height - window_height) // 2
|
|
373
424
|
self.root.geometry(f"{window_width}x{window_height}+{x}+{y}")
|
|
374
|
-
|
|
425
|
+
self.root.minsize(min(rows_w, avail_w), min(fixed_h + 150, window_height))
|
|
375
426
|
self.root.resizable(True, True)
|
|
427
|
+
self._wrap_held = False
|
|
376
428
|
|
|
377
429
|
# Always enable mouse wheel scrolling
|
|
378
430
|
self._bind_mousewheel()
|
|
@@ -720,7 +772,7 @@ class InstallerApp:
|
|
|
720
772
|
main_container = ttk.Frame(self.root, padding="20")
|
|
721
773
|
main_container.grid(row=0, column=0, sticky="nsew")
|
|
722
774
|
main_container.columnconfigure(0, weight=1)
|
|
723
|
-
main_container.rowconfigure(
|
|
775
|
+
main_container.rowconfigure(3, weight=1)
|
|
724
776
|
|
|
725
777
|
# Title
|
|
726
778
|
title_frame = ttk.Frame(main_container)
|
|
@@ -771,9 +823,14 @@ class InstallerApp:
|
|
|
771
823
|
"Re-tint this theme from a base color "
|
|
772
824
|
"(re-click the theme emoji to reset)")
|
|
773
825
|
|
|
826
|
+
# Upgrade strip — only gridded while upgrade.check() has found something.
|
|
827
|
+
self.upgrade_bar = tk.Frame(main_container, bg=t["panel"], highlightthickness=1,
|
|
828
|
+
highlightbackground=t["accent"])
|
|
829
|
+
self._render_upgrade_bar()
|
|
830
|
+
|
|
774
831
|
# Search bar
|
|
775
832
|
search_frame = ttk.Frame(main_container)
|
|
776
|
-
search_frame.grid(row=
|
|
833
|
+
search_frame.grid(row=2, column=0, pady=(0, 10), sticky="ew")
|
|
777
834
|
|
|
778
835
|
self.search_entry = tk.Entry(
|
|
779
836
|
search_frame, textvariable=self.search_var,
|
|
@@ -832,7 +889,7 @@ class InstallerApp:
|
|
|
832
889
|
|
|
833
890
|
# Scrollable Area
|
|
834
891
|
self.outer_frame = ttk.Frame(main_container, relief="flat")
|
|
835
|
-
self.outer_frame.grid(row=
|
|
892
|
+
self.outer_frame.grid(row=3, column=0, sticky="nsew")
|
|
836
893
|
self.outer_frame.columnconfigure(0, weight=1)
|
|
837
894
|
self.outer_frame.rowconfigure(0, weight=1)
|
|
838
895
|
|
|
@@ -868,6 +925,7 @@ class InstallerApp:
|
|
|
868
925
|
for group_label in sorted(tools_by_group):
|
|
869
926
|
categories[group_label] = discovery.group_tools(tools_by_group[group_label])
|
|
870
927
|
|
|
928
|
+
self._desc_labels: List[ttk.Label] = [] # wrapped to the width in _position_window
|
|
871
929
|
self.expand_vars: Dict[str, tk.BooleanVar] = {} # Track expanded state
|
|
872
930
|
self.children_frames: Dict[str, ttk.Frame] = {} # Track child frames for show/hide
|
|
873
931
|
|
|
@@ -925,7 +983,15 @@ class InstallerApp:
|
|
|
925
983
|
|
|
926
984
|
# Footer / Buttons
|
|
927
985
|
footer = ttk.Frame(main_container, padding=(0, 20, 0, 0))
|
|
928
|
-
footer.grid(row=
|
|
986
|
+
footer.grid(row=4, column=0, sticky="ew")
|
|
987
|
+
|
|
988
|
+
# Packed first: when the window is narrower than the footer, pack takes
|
|
989
|
+
# the space from the widgets packed last, so Apply stays visible.
|
|
990
|
+
self._apply_btn = ttk.Button(footer, text="Apply Changes", style="Accent.TButton", command=self._apply_changes)
|
|
991
|
+
self._apply_btn.pack(side="right", padx=5)
|
|
992
|
+
self._apply_highlighted = False
|
|
993
|
+
self._op_in_progress = False # guards against overlapping bulk operations
|
|
994
|
+
ttk.Button(footer, text="Refresh Status", command=self._update_status_labels).pack(side="right", padx=5)
|
|
929
995
|
|
|
930
996
|
ttk.Button(footer, text="Select All", command=self._select_all).pack(side="left", padx=5)
|
|
931
997
|
ttk.Button(footer, text="Select None", command=self._select_none).pack(side="left", padx=5)
|
|
@@ -993,17 +1059,11 @@ class InstallerApp:
|
|
|
993
1059
|
|
|
994
1060
|
ttk.Separator(footer, orient="vertical").pack(side="left", fill="y", padx=15)
|
|
995
1061
|
|
|
996
|
-
self._apply_btn = ttk.Button(footer, text="Apply Changes", style="Accent.TButton", command=self._apply_changes)
|
|
997
|
-
self._apply_btn.pack(side="right", padx=5)
|
|
998
|
-
self._apply_highlighted = False
|
|
999
|
-
self._op_in_progress = False # guards against overlapping bulk operations
|
|
1000
|
-
ttk.Button(footer, text="Refresh Status", command=self._update_status_labels).pack(side="right", padx=5)
|
|
1001
|
-
|
|
1002
1062
|
# Collapsible Log Area
|
|
1003
1063
|
self.log_expanded = tk.BooleanVar(value=False)
|
|
1004
1064
|
|
|
1005
1065
|
log_container = ttk.Frame(main_container)
|
|
1006
|
-
log_container.grid(row=
|
|
1066
|
+
log_container.grid(row=5, column=0, sticky="nsew", pady=(15, 0))
|
|
1007
1067
|
log_container.columnconfigure(0, weight=1)
|
|
1008
1068
|
|
|
1009
1069
|
# Header row (clickable to expand/collapse)
|
|
@@ -1060,7 +1120,7 @@ class InstallerApp:
|
|
|
1060
1120
|
self.log_text.see("end")
|
|
1061
1121
|
|
|
1062
1122
|
indicator_frame = ttk.Frame(main_container)
|
|
1063
|
-
indicator_frame.grid(row=
|
|
1123
|
+
indicator_frame.grid(row=6, column=0, sticky="ew", pady=(10, 0))
|
|
1064
1124
|
|
|
1065
1125
|
self.status_bar = ttk.Label(indicator_frame, text="Ready", foreground=t["muted"], font=("", 9, "italic"))
|
|
1066
1126
|
self.status_bar.pack(side="left")
|
|
@@ -2700,7 +2760,8 @@ class InstallerApp:
|
|
|
2700
2760
|
height=24 is only a floor for the header row, whose labels are short).
|
|
2701
2761
|
"""
|
|
2702
2762
|
cells: Dict[str, tk.Frame] = {}
|
|
2703
|
-
for col_key,
|
|
2763
|
+
for col_key, _ in RIGHT_COLS:
|
|
2764
|
+
width = self._col_widths[col_key]
|
|
2704
2765
|
sep = tk.Frame(container, width=1, bg=self.theme["border"],
|
|
2705
2766
|
highlightthickness=0)
|
|
2706
2767
|
sep.pack(side="left", fill="y")
|
|
@@ -2840,9 +2901,11 @@ class InstallerApp:
|
|
|
2840
2901
|
desc_text = self._format_desc_with_alias(tool)
|
|
2841
2902
|
desc_label = ttk.Label(info_frame, text=desc_text, style="CardMuted.TLabel", font=("", 9))
|
|
2842
2903
|
desc_label.pack(anchor="w", fill="x")
|
|
2904
|
+
self._desc_labels.append(desc_label)
|
|
2843
2905
|
# Dynamic wraplength based on available width
|
|
2844
2906
|
def update_wrap(event, lbl=desc_label):
|
|
2845
|
-
|
|
2907
|
+
if not self._wrap_held:
|
|
2908
|
+
lbl.configure(wraplength=max(100, event.width - 10))
|
|
2846
2909
|
info_frame.bind("<Configure>", update_wrap)
|
|
2847
2910
|
|
|
2848
2911
|
def _render_tool_group(self, group: discovery.ToolGroup, current_row: int) -> int:
|
|
@@ -2985,9 +3048,11 @@ class InstallerApp:
|
|
|
2985
3048
|
desc_text = self._format_desc_with_alias(parent)
|
|
2986
3049
|
desc_label = ttk.Label(info_frame, text=desc_text, style="CardMuted.TLabel", font=("", 9))
|
|
2987
3050
|
desc_label.pack(anchor="w", fill="x")
|
|
3051
|
+
self._desc_labels.append(desc_label)
|
|
2988
3052
|
# Dynamic wraplength based on available width
|
|
2989
3053
|
def update_wrap(event, lbl=desc_label):
|
|
2990
|
-
|
|
3054
|
+
if not self._wrap_held:
|
|
3055
|
+
lbl.configure(wraplength=max(100, event.width - 10))
|
|
2991
3056
|
info_frame.bind("<Configure>", update_wrap)
|
|
2992
3057
|
|
|
2993
3058
|
current_row += 1
|
|
@@ -3888,6 +3953,117 @@ class InstallerApp:
|
|
|
3888
3953
|
else:
|
|
3889
3954
|
self._log("Already up to date — nothing to do.", "success")
|
|
3890
3955
|
|
|
3956
|
+
# --- upgrades -----------------------------------------------------------------
|
|
3957
|
+
|
|
3958
|
+
def _start_upgrade_check(self):
|
|
3959
|
+
"""Run upgrade.check() on a thread and show what it finds in the strip."""
|
|
3960
|
+
results: queue.Queue = queue.Queue()
|
|
3961
|
+
|
|
3962
|
+
def work():
|
|
3963
|
+
try:
|
|
3964
|
+
results.put(upgrade.check())
|
|
3965
|
+
except Exception as exc: # a failed check must not take the window down
|
|
3966
|
+
results.put(exc)
|
|
3967
|
+
|
|
3968
|
+
threading.Thread(target=work, daemon=True).start()
|
|
3969
|
+
self.root.after(200, lambda: self._poll_upgrade_check(results))
|
|
3970
|
+
|
|
3971
|
+
def _poll_upgrade_check(self, results):
|
|
3972
|
+
try:
|
|
3973
|
+
found = results.get_nowait()
|
|
3974
|
+
except queue.Empty:
|
|
3975
|
+
self.root.after(200, lambda: self._poll_upgrade_check(results))
|
|
3976
|
+
return
|
|
3977
|
+
if isinstance(found, Exception):
|
|
3978
|
+
self._log(f"Upgrade check failed: {found}", "error")
|
|
3979
|
+
return
|
|
3980
|
+
was_shown = bool(self._upgrade_items)
|
|
3981
|
+
self._upgrade_items = list(found)
|
|
3982
|
+
self._render_upgrade_bar()
|
|
3983
|
+
if self._upgrade_items and not was_shown:
|
|
3984
|
+
# The window was sized without the strip; make room for it rather
|
|
3985
|
+
# than take its height from the table.
|
|
3986
|
+
self.root.update_idletasks()
|
|
3987
|
+
_, _, _, mon_height = self._get_primary_monitor_geometry()
|
|
3988
|
+
height = min(self.root.winfo_height() + self.upgrade_bar.winfo_reqheight() + 10,
|
|
3989
|
+
int(mon_height * 0.9))
|
|
3990
|
+
self.root.geometry(f"{self.root.winfo_width()}x{height}")
|
|
3991
|
+
|
|
3992
|
+
def _render_upgrade_bar(self):
|
|
3993
|
+
"""Show the upgrade strip for what the last check found, or hide it."""
|
|
3994
|
+
bar = self.upgrade_bar
|
|
3995
|
+
for child in bar.winfo_children():
|
|
3996
|
+
child.destroy()
|
|
3997
|
+
self._upgrade_btn = None
|
|
3998
|
+
if not self._upgrade_items:
|
|
3999
|
+
bar.grid_remove()
|
|
4000
|
+
return
|
|
4001
|
+
t = self.theme
|
|
4002
|
+
# The button is packed first so a narrow window squeezes the text, not it.
|
|
4003
|
+
self._upgrade_btn = ttk.Button(bar, text="Upgrade", style="Accent.TButton",
|
|
4004
|
+
command=self._upgrade_clicked)
|
|
4005
|
+
self._upgrade_btn.pack(side="right", padx=8, pady=6)
|
|
4006
|
+
if getattr(self, "_op_in_progress", False):
|
|
4007
|
+
self._upgrade_btn.configure(state="disabled")
|
|
4008
|
+
tk.Label(bar, text="Upgrade available:", bg=t["panel"], fg=t["accent"],
|
|
4009
|
+
font=("", 10, "bold")).pack(side="left", padx=(10, 6), pady=6)
|
|
4010
|
+
detail = tk.Label(bar, text=" · ".join(item.label() for item in self._upgrade_items),
|
|
4011
|
+
bg=t["panel"], fg=t["fg"], font=("", 10), anchor="w",
|
|
4012
|
+
justify="left", wraplength=400)
|
|
4013
|
+
detail.pack(side="left", fill="x", expand=True, pady=6)
|
|
4014
|
+
detail.bind("<Configure>",
|
|
4015
|
+
lambda e: detail.configure(wraplength=max(150, e.width - 10)))
|
|
4016
|
+
bar.grid(row=1, column=0, pady=(0, 10), sticky="ew")
|
|
4017
|
+
|
|
4018
|
+
def _upgrade_clicked(self):
|
|
4019
|
+
if self._op_in_progress or not self._upgrade_items:
|
|
4020
|
+
return
|
|
4021
|
+
items = list(self._upgrade_items)
|
|
4022
|
+
lines = "\n".join(f" • {item.label()}" for item in items)
|
|
4023
|
+
if not messagebox.askyesno(
|
|
4024
|
+
"Upgrade?",
|
|
4025
|
+
f"This brings these up to date:\n\n{lines}\n\n"
|
|
4026
|
+
"Repos are pulled, cli-tools-kit is upgraded with pip, the installed "
|
|
4027
|
+
"tools of a pulled repo are reinstalled, and the installer restarts.\n\n"
|
|
4028
|
+
"This uses the network. Continue?",
|
|
4029
|
+
):
|
|
4030
|
+
return
|
|
4031
|
+
self._op_in_progress = True
|
|
4032
|
+
self._set_action_buttons_state("disabled")
|
|
4033
|
+
self._clear_hint()
|
|
4034
|
+
self._toggle_log(force_expand=True)
|
|
4035
|
+
events: queue.Queue = queue.Queue()
|
|
4036
|
+
|
|
4037
|
+
def work():
|
|
4038
|
+
def log(msg, tag="info"):
|
|
4039
|
+
events.put(("log", msg, tag))
|
|
4040
|
+
try:
|
|
4041
|
+
result = upgrade.upgrade(items, self.tools, log)
|
|
4042
|
+
except Exception as exc: # the UI must come back whatever went wrong
|
|
4043
|
+
log(f"Upgrade failed: {exc}", "error")
|
|
4044
|
+
result = {"changed": False, "errors": 1}
|
|
4045
|
+
events.put(("done", result))
|
|
4046
|
+
|
|
4047
|
+
threading.Thread(target=work, daemon=True).start()
|
|
4048
|
+
self.root.after(50, lambda: self._drain_op_events(events, self._finish_upgrade))
|
|
4049
|
+
|
|
4050
|
+
def _finish_upgrade(self, result):
|
|
4051
|
+
self._op_in_progress = False
|
|
4052
|
+
self._set_action_buttons_state("normal")
|
|
4053
|
+
if result["changed"] and not result["errors"]:
|
|
4054
|
+
self._log("Restarting the installer to load the new version...", "header")
|
|
4055
|
+
self.root.after(1500, self._restart_installer)
|
|
4056
|
+
return
|
|
4057
|
+
if result["changed"]:
|
|
4058
|
+
self._log("Some steps failed (see above). Close and reopen the installer "
|
|
4059
|
+
"to load what was upgraded.", "error")
|
|
4060
|
+
self._start_upgrade_check() # the strip then shows only what is still behind
|
|
4061
|
+
|
|
4062
|
+
def _restart_installer(self):
|
|
4063
|
+
argv = upgrade.restart_argv()
|
|
4064
|
+
self.root.destroy()
|
|
4065
|
+
host.restart(argv)
|
|
4066
|
+
|
|
3891
4067
|
def _toggle_autostart_check(self):
|
|
3892
4068
|
"""Enable/disable the login update-check autostart entry."""
|
|
3893
4069
|
if self._autostart_check_var.get():
|
|
@@ -4087,7 +4263,7 @@ class InstallerApp:
|
|
|
4087
4263
|
|
|
4088
4264
|
Greys the ttk action buttons and the custom update badge together; the badge
|
|
4089
4265
|
also ignores clicks while disabled (see _set_update_enabled)."""
|
|
4090
|
-
for attr in ("_reinstall_btn", "_apply_btn"):
|
|
4266
|
+
for attr in ("_reinstall_btn", "_apply_btn", "_upgrade_btn"):
|
|
4091
4267
|
btn = getattr(self, attr, None)
|
|
4092
4268
|
if btn is not None:
|
|
4093
4269
|
btn.configure(state=state)
|
|
@@ -4098,7 +4274,8 @@ class InstallerApp:
|
|
|
4098
4274
|
# globals (gi.IDENTITY, gi.install_tool = ...). Those names now live in the
|
|
4099
4275
|
# modules below. Reading, assigning or deleting one through gui_installer goes
|
|
4100
4276
|
# to the module that holds it, where every screen looks it up at call time.
|
|
4101
|
-
_ENGINE_MODULES = (state, settings, discovery, install, sweep, autostart, icons, cli
|
|
4277
|
+
_ENGINE_MODULES = (state, settings, discovery, install, sweep, autostart, icons, cli,
|
|
4278
|
+
upgrade)
|
|
4102
4279
|
|
|
4103
4280
|
|
|
4104
4281
|
def _home_of(name: str):
|
|
@@ -68,6 +68,22 @@ def child_env(env: Optional[dict] = None) -> dict:
|
|
|
68
68
|
return env
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
# --- restarting -------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
def restart(argv: Iterable[str]) -> None:
|
|
74
|
+
"""Replace this process with a new one running ``argv``; does not return.
|
|
75
|
+
|
|
76
|
+
After an upgrade the old code is still loaded, so the installer starts
|
|
77
|
+
again. Windows has no real exec (``os.execv`` there spawns a child and
|
|
78
|
+
leaves the console behind), so it starts the new process and exits.
|
|
79
|
+
"""
|
|
80
|
+
argv = list(argv)
|
|
81
|
+
if IS_WINDOWS:
|
|
82
|
+
subprocess.Popen(argv)
|
|
83
|
+
os._exit(0)
|
|
84
|
+
os.execv(argv[0], argv)
|
|
85
|
+
|
|
86
|
+
|
|
71
87
|
# --- where the shims live -----------------------------------------------------
|
|
72
88
|
|
|
73
89
|
def shim_dir(identity) -> str:
|