moleditpy-installer 3.0.2__tar.gz → 3.0.3__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 (21) hide show
  1. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/PKG-INFO +9 -9
  2. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/README.md +8 -8
  3. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/moleditpy_installer/main.py +229 -45
  4. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/moleditpy_installer/tui.py +6 -6
  5. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/moleditpy_installer.egg-info/PKG-INFO +9 -9
  6. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/pyproject.toml +1 -1
  7. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/LICENSE +0 -0
  8. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/moleditpy_installer/__init__.py +0 -0
  9. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/moleditpy_installer/__main__.py +0 -0
  10. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/moleditpy_installer/data/file_icon.icns +0 -0
  11. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/moleditpy_installer/data/file_icon.ico +0 -0
  12. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/moleditpy_installer/data/file_icon.png +0 -0
  13. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/moleditpy_installer/data/icon.icns +0 -0
  14. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/moleditpy_installer/data/icon.ico +0 -0
  15. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/moleditpy_installer/data/icon.png +0 -0
  16. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/moleditpy_installer.egg-info/SOURCES.txt +0 -0
  17. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/moleditpy_installer.egg-info/dependency_links.txt +0 -0
  18. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/moleditpy_installer.egg-info/entry_points.txt +0 -0
  19. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/moleditpy_installer.egg-info/requires.txt +0 -0
  20. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/moleditpy_installer.egg-info/top_level.txt +0 -0
  21. {moleditpy_installer-3.0.2 → moleditpy_installer-3.0.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moleditpy-installer
3
- Version: 3.0.2
3
+ Version: 3.0.3
4
4
  Summary: Installer and application bundler for MoleditPy
5
5
  Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -732,7 +732,7 @@ This package is a helper utility that automatically installs the correct version
732
732
  moleditpy-installer --no-tui # install with the defaults
733
733
  moleditpy-installer --desktop # also create a Desktop shortcut
734
734
  moleditpy-installer --no-file-assoc # skip the .pmeprj association
735
- sudo moleditpy-installer --system # system-wide (Linux/macOS)
735
+ sudo moleditpy-installer --system # system-wide (admin terminal on Windows)
736
736
  ```
737
737
 
738
738
  Any explicit option skips the TUI.
@@ -753,13 +753,13 @@ This package is a helper utility that automatically installs the correct version
753
753
 
754
754
  > **Tip:** Run the installer from the Python environment where `moleditpy` is installed (e.g. after `conda activate <env>`). The installer verifies the launch command and warns you if the pairing cannot start MoleditPy.
755
755
 
756
- 3. **Remove Shortcut**
757
- To remove the shortcuts, unregister file associations, and clean up extracted icon files, run (add `--system` for a system-wide install):
756
+ 3. **Uninstall**
757
+ To remove the shortcuts, unregister file associations, and clean up extracted icon files, run (add `--system` for a system-wide install; `--remove` remains as a deprecated alias):
758
758
 
759
759
  ```bash
760
- moleditpy-installer --remove
760
+ moleditpy-installer --uninstall
761
761
  # or
762
- python -m moleditpy_installer --remove
762
+ python -m moleditpy_installer --uninstall
763
763
  ```
764
764
 
765
765
  4. **Check Executable Location**
@@ -789,7 +789,7 @@ This package is a helper utility that automatically installs the correct version
789
789
  ## What the installer sets up
790
790
 
791
791
  ### Windows
792
- - Start Menu shortcut (icon included); Desktop shortcut when enabled (`--desktop` or the TUI checkbox).
792
+ - Start Menu shortcut (icon included); Desktop shortcut when enabled (`--desktop` or the TUI checkbox). With `--system` (admin) the shortcuts go to the all-users locations and the association to HKLM.
793
793
  - `.pmeprj` files open MoleditPy on double-click (per-user registry, no admin rights needed).
794
794
  - Icons are stored under `%LOCALAPPDATA%\MoleditPy\installer` so they survive temp-folder cleanup.
795
795
 
@@ -805,13 +805,13 @@ This package is a helper utility that automatically installs the correct version
805
805
  - `.pmeprj` files open MoleditPy on double-click: a per-user `application/x-moleditpy-project` MIME type is registered (freedesktop shared-mime-info, no root needed) and set as the default handler.
806
806
  - `.pmeprj` documents get their own file icon in file managers, matching Windows and macOS.
807
807
 
808
- `moleditpy-installer --remove` undoes all of the above for the current user.
808
+ `moleditpy-installer --uninstall` undoes all of the above for the current user (add `--system` for system-wide installs).
809
809
 
810
810
  ## Version 3.0 highlights
811
811
 
812
812
  - **New:** interactive Textual TUI — pick components and scope, watch the log, press Install/Remove.
813
813
  - **New:** component selection (`--desktop`, `--app-menu`, `--file-assoc`) with safer defaults (no Desktop shortcut unless requested).
814
- - **New:** system-wide scope on Linux (`/usr/share`, requires sudo) and macOS (`/Applications`); Windows stays per-user.
814
+ - **New:** system-wide scope on Linux (`/usr/share`), macOS (`/Applications`), and Windows (HKLM + all-users Start Menu; admin terminal) — all requiring sudo/admin.
815
815
  - Per-user Linux paths now honor `XDG_DATA_HOME`.
816
816
  - End-to-end smoke tests on all three OSes in CI, including the sudo/system path.
817
817
 
@@ -29,7 +29,7 @@ This package is a helper utility that automatically installs the correct version
29
29
  moleditpy-installer --no-tui # install with the defaults
30
30
  moleditpy-installer --desktop # also create a Desktop shortcut
31
31
  moleditpy-installer --no-file-assoc # skip the .pmeprj association
32
- sudo moleditpy-installer --system # system-wide (Linux/macOS)
32
+ sudo moleditpy-installer --system # system-wide (admin terminal on Windows)
33
33
  ```
34
34
 
35
35
  Any explicit option skips the TUI.
@@ -50,13 +50,13 @@ This package is a helper utility that automatically installs the correct version
50
50
 
51
51
  > **Tip:** Run the installer from the Python environment where `moleditpy` is installed (e.g. after `conda activate <env>`). The installer verifies the launch command and warns you if the pairing cannot start MoleditPy.
52
52
 
53
- 3. **Remove Shortcut**
54
- To remove the shortcuts, unregister file associations, and clean up extracted icon files, run (add `--system` for a system-wide install):
53
+ 3. **Uninstall**
54
+ To remove the shortcuts, unregister file associations, and clean up extracted icon files, run (add `--system` for a system-wide install; `--remove` remains as a deprecated alias):
55
55
 
56
56
  ```bash
57
- moleditpy-installer --remove
57
+ moleditpy-installer --uninstall
58
58
  # or
59
- python -m moleditpy_installer --remove
59
+ python -m moleditpy_installer --uninstall
60
60
  ```
61
61
 
62
62
  4. **Check Executable Location**
@@ -86,7 +86,7 @@ This package is a helper utility that automatically installs the correct version
86
86
  ## What the installer sets up
87
87
 
88
88
  ### Windows
89
- - Start Menu shortcut (icon included); Desktop shortcut when enabled (`--desktop` or the TUI checkbox).
89
+ - Start Menu shortcut (icon included); Desktop shortcut when enabled (`--desktop` or the TUI checkbox). With `--system` (admin) the shortcuts go to the all-users locations and the association to HKLM.
90
90
  - `.pmeprj` files open MoleditPy on double-click (per-user registry, no admin rights needed).
91
91
  - Icons are stored under `%LOCALAPPDATA%\MoleditPy\installer` so they survive temp-folder cleanup.
92
92
 
@@ -102,13 +102,13 @@ This package is a helper utility that automatically installs the correct version
102
102
  - `.pmeprj` files open MoleditPy on double-click: a per-user `application/x-moleditpy-project` MIME type is registered (freedesktop shared-mime-info, no root needed) and set as the default handler.
103
103
  - `.pmeprj` documents get their own file icon in file managers, matching Windows and macOS.
104
104
 
105
- `moleditpy-installer --remove` undoes all of the above for the current user.
105
+ `moleditpy-installer --uninstall` undoes all of the above for the current user (add `--system` for system-wide installs).
106
106
 
107
107
  ## Version 3.0 highlights
108
108
 
109
109
  - **New:** interactive Textual TUI — pick components and scope, watch the log, press Install/Remove.
110
110
  - **New:** component selection (`--desktop`, `--app-menu`, `--file-assoc`) with safer defaults (no Desktop shortcut unless requested).
111
- - **New:** system-wide scope on Linux (`/usr/share`, requires sudo) and macOS (`/Applications`); Windows stays per-user.
111
+ - **New:** system-wide scope on Linux (`/usr/share`), macOS (`/Applications`), and Windows (HKLM + all-users Start Menu; admin terminal) — all requiring sudo/admin.
112
112
  - Per-user Linux paths now honor `XDG_DATA_HOME`.
113
113
  - End-to-end smoke tests on all three OSes in CI, including the sudo/system path.
114
114
 
@@ -66,8 +66,43 @@ class InstallOptions:
66
66
  system: bool = False # system-wide (sudo/admin) instead of per-user
67
67
 
68
68
 
69
+ @contextlib.contextmanager
70
+ def _com_initialized():
71
+ """
72
+ Initialize COM for the current thread on Windows.
73
+
74
+ pyshortcuts creates .lnk files through COM, which must be initialized
75
+ per-thread. The TUI runs install() in a worker thread, where shortcut
76
+ creation otherwise fails with CO_E_NOTINITIALIZED
77
+ ("CoInitialize has not been called").
78
+ """
79
+ initialized = False
80
+ if platform.system() == "Windows":
81
+ try:
82
+ import pythoncom
83
+
84
+ pythoncom.CoInitialize()
85
+ initialized = True
86
+ except ImportError:
87
+ pass
88
+ try:
89
+ yield
90
+ finally:
91
+ if initialized:
92
+ import pythoncom
93
+
94
+ pythoncom.CoUninitialize()
95
+
96
+
69
97
  def is_root() -> bool:
70
- """True when running with root privileges (POSIX only)."""
98
+ """True when running with root (POSIX) or admin (Windows) privileges."""
99
+ if platform.system() == "Windows":
100
+ try:
101
+ import ctypes
102
+
103
+ return bool(ctypes.windll.shell32.IsUserAnAdmin())
104
+ except (OSError, AttributeError):
105
+ return False
71
106
  geteuid = getattr(os, "geteuid", None)
72
107
  return geteuid is not None and geteuid() == 0
73
108
 
@@ -80,7 +115,7 @@ def linux_data_home(system: bool = False) -> Path:
80
115
  return Path(xdg) if xdg else Path.home() / ".local" / "share"
81
116
 
82
117
 
83
- def get_persistent_data_dir() -> Path:
118
+ def get_persistent_data_dir(system: bool = False) -> Path:
84
119
  """
85
120
  Per-user directory for extracted installer assets (icons).
86
121
 
@@ -89,7 +124,13 @@ def get_persistent_data_dir() -> Path:
89
124
  (which is periodically cleaned, silently breaking the icons).
90
125
  """
91
126
  if platform.system() == "Windows":
92
- base = os.environ.get("LOCALAPPDATA") or str(Path.home() / "AppData" / "Local")
127
+ if system:
128
+ # all users must be able to read icons referenced from HKLM
129
+ base = os.environ.get("PROGRAMDATA") or "C:/ProgramData"
130
+ else:
131
+ base = os.environ.get("LOCALAPPDATA") or str(
132
+ Path.home() / "AppData" / "Local"
133
+ )
93
134
  data_dir = Path(base) / "MoleditPy" / "installer"
94
135
  else:
95
136
  data_dir = Path.home() / ".moleditpy" / "installer"
@@ -97,12 +138,12 @@ def get_persistent_data_dir() -> Path:
97
138
  return data_dir
98
139
 
99
140
 
100
- def _extract_data_file(file_name: str) -> Optional[str]:
141
+ def _extract_data_file(file_name: str, system: bool = False) -> Optional[str]:
101
142
  """Extract a packaged data file to the persistent data dir and return its path."""
102
143
  try:
103
144
  ref = importlib.resources.files("moleditpy_installer") / "data" / file_name
104
145
  content = ref.read_bytes()
105
- out_path = get_persistent_data_dir() / file_name
146
+ out_path = get_persistent_data_dir(system) / file_name
106
147
  out_path.write_bytes(content)
107
148
  return str(out_path)
108
149
  except (OSError, ModuleNotFoundError, ValueError) as e:
@@ -362,13 +403,36 @@ def find_executable(name: str) -> Optional[str]:
362
403
  return None
363
404
 
364
405
 
365
- def register_file_associations_windows(exe_path: str, icon_path: Optional[str]) -> bool:
406
+ def _notify_windows_assoc_changed() -> None:
407
+ """
408
+ Tell Explorer the file associations changed.
409
+
410
+ Without SHChangeNotify(SHCNE_ASSOCCHANGED) Explorer keeps showing the
411
+ old association/icon from its cache until the next logon.
412
+ """
413
+ try:
414
+ import ctypes
415
+
416
+ SHCNE_ASSOCCHANGED = 0x08000000
417
+ SHCNF_IDLIST = 0x0
418
+ ctypes.windll.shell32.SHChangeNotify(
419
+ SHCNE_ASSOCCHANGED, SHCNF_IDLIST, None, None
420
+ )
421
+ except (OSError, AttributeError):
422
+ pass # not on Windows / shell32 unavailable
423
+
424
+
425
+ def register_file_associations_windows(
426
+ exe_path: str, icon_path: Optional[str], system: bool = False
427
+ ) -> bool:
366
428
  """
367
429
  Register file associations for .pmeprj files on Windows.
368
430
 
369
431
  Args:
370
432
  exe_path (str): Path to the executable to associate.
371
433
  icon_path (Optional[str]): Path to the icon file for the association.
434
+ system (bool): Register machine-wide under HKLM (requires admin)
435
+ instead of per-user under HKCU.
372
436
 
373
437
  Returns:
374
438
  bool: True if successful, False otherwise.
@@ -376,6 +440,8 @@ def register_file_associations_windows(exe_path: str, icon_path: Optional[str])
376
440
  if platform.system() != "Windows":
377
441
  return False
378
442
 
443
+ root = winreg.HKEY_LOCAL_MACHINE if system else winreg.HKEY_CURRENT_USER
444
+
379
445
  try:
380
446
  extensions = [".pmeprj"]
381
447
  prog_id = "MoleditPy.File"
@@ -384,21 +450,19 @@ def register_file_associations_windows(exe_path: str, icon_path: Optional[str])
384
450
  print("Registering file associations...")
385
451
 
386
452
  # Create ProgID
387
- with winreg.CreateKey(
388
- winreg.HKEY_CURRENT_USER, f"Software\\Classes\\{prog_id}"
389
- ) as key:
453
+ with winreg.CreateKey(root, f"Software\\Classes\\{prog_id}") as key:
390
454
  winreg.SetValue(key, "", winreg.REG_SZ, f"{app_name} File")
391
455
 
392
456
  # Set default icon
393
457
  if icon_path and os.path.exists(icon_path):
394
458
  with winreg.CreateKey(
395
- winreg.HKEY_CURRENT_USER, f"Software\\Classes\\{prog_id}\\DefaultIcon"
459
+ root, f"Software\\Classes\\{prog_id}\\DefaultIcon"
396
460
  ) as key:
397
461
  winreg.SetValue(key, "", winreg.REG_SZ, icon_path)
398
462
 
399
463
  # Friendly context-menu label for the open verb
400
464
  with winreg.CreateKey(
401
- winreg.HKEY_CURRENT_USER, f"Software\\Classes\\{prog_id}\\shell\\open"
465
+ root, f"Software\\Classes\\{prog_id}\\shell\\open"
402
466
  ) as key:
403
467
  winreg.SetValue(key, "", winreg.REG_SZ, f"Open with {app_name}")
404
468
 
@@ -406,19 +470,18 @@ def register_file_associations_windows(exe_path: str, icon_path: Optional[str])
406
470
  # Quote paths to handle spaces
407
471
  command = f'"{exe_path}" "%1"'
408
472
  with winreg.CreateKey(
409
- winreg.HKEY_CURRENT_USER,
473
+ root,
410
474
  f"Software\\Classes\\{prog_id}\\shell\\open\\command",
411
475
  ) as key:
412
476
  winreg.SetValue(key, "", winreg.REG_SZ, command)
413
477
 
414
478
  # Associate extensions
415
479
  for ext in extensions:
416
- with winreg.CreateKey(
417
- winreg.HKEY_CURRENT_USER, f"Software\\Classes\\{ext}"
418
- ) as key:
480
+ with winreg.CreateKey(root, f"Software\\Classes\\{ext}") as key:
419
481
  winreg.SetValue(key, "", winreg.REG_SZ, prog_id)
420
482
  print(f" Associated {ext} with {app_name}")
421
483
 
484
+ _notify_windows_assoc_changed()
422
485
  print("File associations registered successfully.")
423
486
  return True
424
487
 
@@ -623,6 +686,27 @@ def register_file_associations_linux(system: bool = False) -> bool:
623
686
  return False
624
687
 
625
688
 
689
+ def _clean_linux_mimeapps() -> None:
690
+ """Drop MoleditPy's MIME entries from the per-user mimeapps.list files
691
+ (xdg-mime has no 'unset'; a dangling default otherwise remains)."""
692
+ config_home = Path(os.environ.get("XDG_CONFIG_HOME") or Path.home() / ".config")
693
+ candidates = [
694
+ config_home / "mimeapps.list",
695
+ linux_data_home() / "applications" / "mimeapps.list", # legacy location
696
+ ]
697
+ for path in candidates:
698
+ if not path.is_file():
699
+ continue
700
+ try:
701
+ lines = path.read_text(encoding="utf-8").splitlines()
702
+ kept = [line for line in lines if LINUX_MIME_TYPE not in line]
703
+ if len(kept) != len(lines):
704
+ path.write_text("\n".join(kept) + "\n", encoding="utf-8")
705
+ print(f" Cleaned {path}")
706
+ except OSError as e:
707
+ print(f" Failed to clean {path}: {e}")
708
+
709
+
626
710
  def unregister_file_associations_linux(system: bool = False) -> None:
627
711
  """Remove the .pmeprj MIME type, its icon, and refresh the databases."""
628
712
  if platform.system() != "Linux":
@@ -647,6 +731,9 @@ def unregister_file_associations_linux(system: bool = False) -> None:
647
731
  except OSError as e:
648
732
  print(f" Failed to remove {path}: {e}")
649
733
 
734
+ if not system:
735
+ _clean_linux_mimeapps()
736
+
650
737
  _run_quiet(["update-mime-database", str(mime_dir)])
651
738
  _run_quiet(["update-desktop-database", str(data_home / "applications")])
652
739
  _run_quiet(["gtk-update-icon-cache", str(data_home / "icons" / "hicolor")])
@@ -710,17 +797,20 @@ def delete_registry_tree(key, sub_key):
710
797
  return False
711
798
 
712
799
 
713
- def unregister_file_associations_windows() -> None:
800
+ def unregister_file_associations_windows(system: bool = False) -> None:
714
801
  """
715
802
  Unregister file associations for .pmeprj and .pmeraw files on Windows.
803
+ With ``system=True`` removes the machine-wide (HKLM) registration.
716
804
  """
717
805
  if platform.system() != "Windows":
718
806
  return
719
807
 
808
+ root = winreg.HKEY_LOCAL_MACHINE if system else winreg.HKEY_CURRENT_USER
809
+
720
810
  print("Unregistering file associations...")
721
811
  keys_to_remove = [
722
- (winreg.HKEY_CURRENT_USER, "Software\\Classes\\.pmeprj"),
723
- (winreg.HKEY_CURRENT_USER, "Software\\Classes\\.pmeraw"),
812
+ (root, "Software\\Classes\\.pmeprj"),
813
+ (root, "Software\\Classes\\.pmeraw"),
724
814
  ]
725
815
 
726
816
  # ProgID might have subkeys (shell, DefaultIcon), so we need recursive delete
@@ -738,9 +828,20 @@ def unregister_file_associations_windows() -> None:
738
828
  print(f" Failed to remove {key_path}: {e}")
739
829
 
740
830
  # Remove ProgID recursively
741
- if delete_registry_tree(winreg.HKEY_CURRENT_USER, prog_id_key):
831
+ if delete_registry_tree(root, prog_id_key):
742
832
  print(f" Removed registry tree: {prog_id_key}")
743
833
 
834
+ # Explorer pins its own per-user choice under FileExts; without
835
+ # removing it (and notifying), the association looks still present.
836
+ for ext in (".pmeprj",):
837
+ file_exts_key = (
838
+ "Software\\Microsoft\\Windows\\CurrentVersion\\"
839
+ f"Explorer\\FileExts\\{ext}"
840
+ )
841
+ if delete_registry_tree(winreg.HKEY_CURRENT_USER, file_exts_key):
842
+ print(f" Removed Explorer cache: {file_exts_key}")
843
+
844
+ _notify_windows_assoc_changed()
744
845
  print("File associations unregistered.")
745
846
  except OSError as e:
746
847
  print(f"Error during file association removal: {e}")
@@ -757,11 +858,17 @@ def remove_shortcut(system_scope: bool = False) -> None:
757
858
  shortcut_paths = []
758
859
  shortcut_name = "MoleditPy"
759
860
 
760
- if system_scope and system != "Windows" and not is_root():
761
- print(
762
- "Warning: removing a system-wide install requires root; "
763
- "system files will likely remain. Re-run with sudo."
764
- )
861
+ if system_scope and not is_root():
862
+ if system == "Windows":
863
+ print(
864
+ "Warning: removing a system-wide install requires an "
865
+ "administrator terminal; system entries will likely remain."
866
+ )
867
+ else:
868
+ print(
869
+ "Warning: removing a system-wide install requires root; "
870
+ "system files will likely remain. Re-run with sudo."
871
+ )
765
872
 
766
873
  if system == "Windows":
767
874
  # Usually in APPDATA/Microsoft/Windows/Start Menu/Programs
@@ -780,6 +887,20 @@ def remove_shortcut(system_scope: bool = False) -> None:
780
887
 
781
888
  unregister_file_associations_windows()
782
889
 
890
+ if system_scope:
891
+ program_data = os.environ.get("PROGRAMDATA") or "C:/ProgramData"
892
+ public = os.environ.get("PUBLIC") or "C:/Users/Public"
893
+ shortcut_paths.append(
894
+ Path(program_data)
895
+ / "Microsoft"
896
+ / "Windows"
897
+ / "Start Menu"
898
+ / "Programs"
899
+ / f"{shortcut_name}.lnk"
900
+ )
901
+ shortcut_paths.append(Path(public) / "Desktop" / f"{shortcut_name}.lnk")
902
+ unregister_file_associations_windows(system=True)
903
+
783
904
  elif system == "Linux":
784
905
  # Usually in ~/.local/share/applications/ plus a Desktop copy
785
906
  home = Path.home()
@@ -832,11 +953,13 @@ def remove_shortcut(system_scope: bool = False) -> None:
832
953
  # Clean up the icons extracted at install time
833
954
  try:
834
955
  shutil.rmtree(get_persistent_data_dir(), ignore_errors=True)
956
+ if system_scope:
957
+ shutil.rmtree(get_persistent_data_dir(system=True), ignore_errors=True)
835
958
  except OSError:
836
959
  pass
837
960
 
838
961
 
839
- def get_file_icon_path() -> Optional[str]:
962
+ def get_file_icon_path(system: bool = False) -> Optional[str]:
840
963
  """
841
964
  Gets the absolute path to the file icon (file_icon.ico) for Windows associations.
842
965
  """
@@ -845,7 +968,7 @@ def get_file_icon_path() -> Optional[str]:
845
968
 
846
969
  # Extract to a persistent location: as_file() paths may be temporary
847
970
  # (zip installs) and would be dead by the time the registry reads them.
848
- return _extract_data_file("file_icon.ico")
971
+ return _extract_data_file("file_icon.ico", system=system)
849
972
 
850
973
 
851
974
  def python_for_executable(exe_path: str) -> str:
@@ -969,6 +1092,56 @@ def refresh_launch_services(app_path: Path, unregister: bool = False) -> None:
969
1092
  return
970
1093
 
971
1094
 
1095
+ def _move_windows_shortcuts_to_all_users(desktop: bool, app_menu: bool) -> None:
1096
+ """
1097
+ Relocate freshly created per-user shortcuts to the all-users locations
1098
+ (ProgramData Start Menu / Public Desktop). pyshortcuts only writes
1099
+ per-user paths, so a system-wide install moves them afterwards.
1100
+ """
1101
+ name = "MoleditPy"
1102
+ moves = []
1103
+ appdata = os.environ.get("APPDATA")
1104
+ program_data = os.environ.get("PROGRAMDATA") or "C:/ProgramData"
1105
+ public = os.environ.get("PUBLIC") or "C:/Users/Public"
1106
+
1107
+ if app_menu and appdata:
1108
+ moves.append(
1109
+ (
1110
+ Path(appdata)
1111
+ / "Microsoft"
1112
+ / "Windows"
1113
+ / "Start Menu"
1114
+ / "Programs"
1115
+ / f"{name}.lnk",
1116
+ Path(program_data)
1117
+ / "Microsoft"
1118
+ / "Windows"
1119
+ / "Start Menu"
1120
+ / "Programs"
1121
+ / f"{name}.lnk",
1122
+ )
1123
+ )
1124
+ if desktop:
1125
+ moves.append(
1126
+ (
1127
+ Path.home() / "Desktop" / f"{name}.lnk",
1128
+ Path(public) / "Desktop" / f"{name}.lnk",
1129
+ )
1130
+ )
1131
+
1132
+ for src, dest in moves:
1133
+ if not src.is_file():
1134
+ continue
1135
+ try:
1136
+ dest.parent.mkdir(parents=True, exist_ok=True)
1137
+ if dest.exists():
1138
+ dest.unlink()
1139
+ shutil.move(str(src), str(dest))
1140
+ print(f"Moved shortcut to all-users location: {dest}")
1141
+ except OSError as e:
1142
+ print(f"Warning: could not move {src} to {dest}: {e}")
1143
+
1144
+
972
1145
  def install(options: Optional[InstallOptions] = None) -> int:
973
1146
  """
974
1147
  Creates shortcuts and file associations for the installed moleditpy
@@ -990,15 +1163,14 @@ def install(options: Optional[InstallOptions] = None) -> int:
990
1163
  print("Nothing to install: all components are disabled.")
991
1164
  return 1
992
1165
 
993
- if options.system:
1166
+ if options.system and not is_root():
994
1167
  if system == "Windows":
995
- print("Error: system-wide installation is not supported on Windows.")
996
- print("Run without --system for a per-user install (no admin needed).")
997
- return 1
998
- if not is_root():
1168
+ print("Error: system-wide installation requires an elevated terminal.")
1169
+ print("Re-run from an administrator terminal, or drop --system.")
1170
+ else:
999
1171
  print("Error: system-wide installation requires root privileges.")
1000
1172
  print("Re-run with sudo, or drop --system for a per-user install.")
1001
- return 1
1173
+ return 1
1002
1174
 
1003
1175
  command_name = "moleditpy"
1004
1176
 
@@ -1081,14 +1253,19 @@ def install(options: Optional[InstallOptions] = None) -> int:
1081
1253
  "skipped for a system-wide install."
1082
1254
  )
1083
1255
  else:
1084
- make_shortcut(
1085
- script=full_command,
1086
- name=shortcut_name,
1087
- icon=icon_path,
1088
- desktop=options.desktop,
1089
- startmenu=options.app_menu,
1090
- noexe=True,
1091
- )
1256
+ with _com_initialized():
1257
+ make_shortcut(
1258
+ script=full_command,
1259
+ name=shortcut_name,
1260
+ icon=icon_path,
1261
+ desktop=options.desktop,
1262
+ startmenu=options.app_menu,
1263
+ noexe=True,
1264
+ )
1265
+ if system == "Windows" and options.system:
1266
+ _move_windows_shortcuts_to_all_users(
1267
+ options.desktop, options.app_menu
1268
+ )
1092
1269
  created_in = [
1093
1270
  loc
1094
1271
  for enabled, loc in (
@@ -1283,11 +1460,13 @@ end open
1283
1460
  # pickle-based format and is intentionally NOT associated with
1284
1461
  # double-click for safety).
1285
1462
  if system == "Windows" and original_exe_path and options.file_assoc:
1286
- file_icon_path = get_file_icon_path()
1463
+ file_icon_path = get_file_icon_path(system=options.system)
1287
1464
  if not file_icon_path:
1288
1465
  file_icon_path = icon_path
1289
1466
 
1290
- register_file_associations_windows(str(original_exe_path), file_icon_path)
1467
+ register_file_associations_windows(
1468
+ str(original_exe_path), file_icon_path, system=options.system
1469
+ )
1291
1470
 
1292
1471
  print("\nYou can remove the shortcut and file associations by running:")
1293
1472
  print(" moleditpy-installer --remove")
@@ -1343,9 +1522,14 @@ def main() -> int:
1343
1522
  ),
1344
1523
  )
1345
1524
  parser.add_argument(
1525
+ "--uninstall",
1346
1526
  "--remove",
1527
+ dest="uninstall",
1347
1528
  action="store_true",
1348
- help="Remove the shortcuts and unregister file associations.",
1529
+ help=(
1530
+ "Uninstall: remove the shortcuts and unregister file "
1531
+ "associations (--remove is a deprecated alias)."
1532
+ ),
1349
1533
  )
1350
1534
  parser.add_argument(
1351
1535
  "--check",
@@ -1373,7 +1557,7 @@ def main() -> int:
1373
1557
  parser.add_argument(
1374
1558
  "--system",
1375
1559
  action="store_true",
1376
- help="Install system-wide instead of per-user (Linux/macOS; requires sudo).",
1560
+ help="Install system-wide instead of per-user (requires sudo/admin).",
1377
1561
  )
1378
1562
  parser.add_argument(
1379
1563
  "--no-tui",
@@ -1389,7 +1573,7 @@ def main() -> int:
1389
1573
 
1390
1574
  args = parser.parse_args()
1391
1575
 
1392
- if args.remove:
1576
+ if args.uninstall:
1393
1577
  remove_shortcut(system_scope=args.system)
1394
1578
  return 0
1395
1579
 
@@ -101,7 +101,7 @@ class InstallerApp(App):
101
101
 
102
102
  BINDINGS = [
103
103
  ("i", "install", "Install"),
104
- ("r", "remove", "Remove"),
104
+ ("u", "remove", "Uninstall"),
105
105
  ("q", "quit", "Quit"),
106
106
  ]
107
107
 
@@ -121,13 +121,12 @@ class InstallerApp(App):
121
121
  with RadioSet(id="scope"):
122
122
  yield RadioButton("User (recommended)", value=True, id="scope_user")
123
123
  yield RadioButton(
124
- "System-wide (requires sudo/root)",
124
+ "System-wide (requires sudo / admin terminal)",
125
125
  id="scope_system",
126
- disabled=platform.system() == "Windows",
127
126
  )
128
127
  with Horizontal(id="buttons"):
129
128
  yield Button("Install", variant="success", id="install")
130
- yield Button("Remove", variant="error", id="remove")
129
+ yield Button("Uninstall", variant="error", id="remove")
131
130
  yield Button("Quit", variant="default", id="quit")
132
131
  yield Static("Status: detecting moleditpy executable…", id="status")
133
132
  yield RichLog(id="log", wrap=True, markup=False)
@@ -136,7 +135,7 @@ class InstallerApp(App):
136
135
  def on_mount(self) -> None:
137
136
  self.log_line("Welcome! Pick components, then press Install.")
138
137
  if platform.system() == "Windows":
139
- self.log_line("Note: system-wide scope is not available on Windows.")
138
+ self.log_line("Note: system-wide scope needs an administrator terminal.")
140
139
  self.run_worker(self._detect_executable, thread=True)
141
140
 
142
141
  # ------------------------------------------------------------------ #
@@ -233,7 +232,8 @@ class InstallerApp(App):
233
232
  self._set_status("Removing…")
234
233
  self.run_worker(
235
234
  lambda: self._run_installer_action(
236
- lambda: installer.remove_shortcut(system_scope=system_scope), "Remove"
235
+ lambda: installer.remove_shortcut(system_scope=system_scope),
236
+ "Uninstall",
237
237
  ),
238
238
  thread=True,
239
239
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moleditpy-installer
3
- Version: 3.0.2
3
+ Version: 3.0.3
4
4
  Summary: Installer and application bundler for MoleditPy
5
5
  Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -732,7 +732,7 @@ This package is a helper utility that automatically installs the correct version
732
732
  moleditpy-installer --no-tui # install with the defaults
733
733
  moleditpy-installer --desktop # also create a Desktop shortcut
734
734
  moleditpy-installer --no-file-assoc # skip the .pmeprj association
735
- sudo moleditpy-installer --system # system-wide (Linux/macOS)
735
+ sudo moleditpy-installer --system # system-wide (admin terminal on Windows)
736
736
  ```
737
737
 
738
738
  Any explicit option skips the TUI.
@@ -753,13 +753,13 @@ This package is a helper utility that automatically installs the correct version
753
753
 
754
754
  > **Tip:** Run the installer from the Python environment where `moleditpy` is installed (e.g. after `conda activate <env>`). The installer verifies the launch command and warns you if the pairing cannot start MoleditPy.
755
755
 
756
- 3. **Remove Shortcut**
757
- To remove the shortcuts, unregister file associations, and clean up extracted icon files, run (add `--system` for a system-wide install):
756
+ 3. **Uninstall**
757
+ To remove the shortcuts, unregister file associations, and clean up extracted icon files, run (add `--system` for a system-wide install; `--remove` remains as a deprecated alias):
758
758
 
759
759
  ```bash
760
- moleditpy-installer --remove
760
+ moleditpy-installer --uninstall
761
761
  # or
762
- python -m moleditpy_installer --remove
762
+ python -m moleditpy_installer --uninstall
763
763
  ```
764
764
 
765
765
  4. **Check Executable Location**
@@ -789,7 +789,7 @@ This package is a helper utility that automatically installs the correct version
789
789
  ## What the installer sets up
790
790
 
791
791
  ### Windows
792
- - Start Menu shortcut (icon included); Desktop shortcut when enabled (`--desktop` or the TUI checkbox).
792
+ - Start Menu shortcut (icon included); Desktop shortcut when enabled (`--desktop` or the TUI checkbox). With `--system` (admin) the shortcuts go to the all-users locations and the association to HKLM.
793
793
  - `.pmeprj` files open MoleditPy on double-click (per-user registry, no admin rights needed).
794
794
  - Icons are stored under `%LOCALAPPDATA%\MoleditPy\installer` so they survive temp-folder cleanup.
795
795
 
@@ -805,13 +805,13 @@ This package is a helper utility that automatically installs the correct version
805
805
  - `.pmeprj` files open MoleditPy on double-click: a per-user `application/x-moleditpy-project` MIME type is registered (freedesktop shared-mime-info, no root needed) and set as the default handler.
806
806
  - `.pmeprj` documents get their own file icon in file managers, matching Windows and macOS.
807
807
 
808
- `moleditpy-installer --remove` undoes all of the above for the current user.
808
+ `moleditpy-installer --uninstall` undoes all of the above for the current user (add `--system` for system-wide installs).
809
809
 
810
810
  ## Version 3.0 highlights
811
811
 
812
812
  - **New:** interactive Textual TUI — pick components and scope, watch the log, press Install/Remove.
813
813
  - **New:** component selection (`--desktop`, `--app-menu`, `--file-assoc`) with safer defaults (no Desktop shortcut unless requested).
814
- - **New:** system-wide scope on Linux (`/usr/share`, requires sudo) and macOS (`/Applications`); Windows stays per-user.
814
+ - **New:** system-wide scope on Linux (`/usr/share`), macOS (`/Applications`), and Windows (HKLM + all-users Start Menu; admin terminal) — all requiring sudo/admin.
815
815
  - Per-user Linux paths now honor `XDG_DATA_HOME`.
816
816
  - End-to-end smoke tests on all three OSes in CI, including the sudo/system path.
817
817
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "moleditpy-installer"
7
- version = "3.0.2"
7
+ version = "3.0.3"
8
8
  authors = [
9
9
  { name="HiroYokoyama", email="titech.yoko.hiro@gmail.com" },
10
10
  ]