commandertui 1.4.0__tar.gz → 1.4.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.
Files changed (48) hide show
  1. {commandertui-1.4.0 → commandertui-1.4.1}/PKG-INFO +2 -1
  2. {commandertui-1.4.0 → commandertui-1.4.1}/README.md +1 -0
  3. commandertui-1.4.1/commandertui/__init__.py +1 -0
  4. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/main_screen.py +43 -4
  5. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/netlocations.py +41 -0
  6. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui.egg-info/PKG-INFO +2 -1
  7. {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_app_smoke.py +63 -0
  8. {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_netlocations.py +42 -0
  9. commandertui-1.4.0/commandertui/__init__.py +0 -1
  10. {commandertui-1.4.0 → commandertui-1.4.1}/LICENSE +0 -0
  11. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/app.py +0 -0
  12. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/ascii_font.py +0 -0
  13. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/bookmarks.py +0 -0
  14. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/boot.py +0 -0
  15. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/cli.py +0 -0
  16. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/compare_screen.py +0 -0
  17. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/diff.py +0 -0
  18. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/editor.py +0 -0
  19. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/executor.py +0 -0
  20. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/modals.py +0 -0
  21. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/models.py +0 -0
  22. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/plan.py +0 -0
  23. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/preview.py +0 -0
  24. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/progress_screen.py +0 -0
  25. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/run_queue.py +0 -0
  26. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/scanner.py +0 -0
  27. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/settings.py +0 -0
  28. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/sync.py +0 -0
  29. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/theme.py +0 -0
  30. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/trash.py +0 -0
  31. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/updater.py +0 -0
  32. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/widgets.py +0 -0
  33. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui.egg-info/SOURCES.txt +0 -0
  34. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui.egg-info/dependency_links.txt +0 -0
  35. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui.egg-info/entry_points.txt +0 -0
  36. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui.egg-info/requires.txt +0 -0
  37. {commandertui-1.4.0 → commandertui-1.4.1}/commandertui.egg-info/top_level.txt +0 -0
  38. {commandertui-1.4.0 → commandertui-1.4.1}/pyproject.toml +0 -0
  39. {commandertui-1.4.0 → commandertui-1.4.1}/setup.cfg +0 -0
  40. {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_bookmarks.py +0 -0
  41. {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_diff.py +0 -0
  42. {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_executor.py +0 -0
  43. {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_plan.py +0 -0
  44. {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_preview.py +0 -0
  45. {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_scanner.py +0 -0
  46. {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_sync.py +0 -0
  47. {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_updater.py +0 -0
  48. {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_widgets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commandertui
3
- Version: 1.4.0
3
+ Version: 1.4.1
4
4
  Summary: Dual-pane retro terminal file manager: compare, sync, copy and move folders
5
5
  Author: Humberto Barchini
6
6
  License-Expression: MIT
@@ -162,6 +162,7 @@ location is saved in `~/.config/commandertui/network.json` (protocol, host,
162
162
  share and user only -- never a password) and reappears in Places, mounting itself
163
163
  when picked. If the server asks for a login, `gio` asks for it in the
164
164
  terminal and can remember it in the system keyring, exactly like Nautilus (on macOS, the system login dialog asks and saves it in Keychain).
165
+ If the GVFS backend for the chosen protocol is not installed (a bare Ubuntu server, for example, has none), the app says so and offers to install it for you (`apt-get install gvfs-backends`, `pacman -S gvfs gvfs-smb` or `dnf install gvfs-smb`); it asks for your sudo password in the terminal and only runs if you answer *Install now*.
165
166
 
166
167
  Every key also has a clickable box on the bottom bar: one row on wide windows
167
168
  (168+ columns), two rows when narrower (120 columns is the practical minimum).
@@ -143,6 +143,7 @@ location is saved in `~/.config/commandertui/network.json` (protocol, host,
143
143
  share and user only -- never a password) and reappears in Places, mounting itself
144
144
  when picked. If the server asks for a login, `gio` asks for it in the
145
145
  terminal and can remember it in the system keyring, exactly like Nautilus (on macOS, the system login dialog asks and saves it in Keychain).
146
+ If the GVFS backend for the chosen protocol is not installed (a bare Ubuntu server, for example, has none), the app says so and offers to install it for you (`apt-get install gvfs-backends`, `pacman -S gvfs gvfs-smb` or `dnf install gvfs-smb`); it asks for your sudo password in the terminal and only runs if you answer *Install now*.
146
147
 
147
148
  Every key also has a clickable box on the bottom bar: one row on wide windows
148
149
  (168+ columns), two rows when narrower (120 columns is the practical minimum).
@@ -0,0 +1 @@
1
+ __version__ = "1.4.1"
@@ -365,7 +365,10 @@ class MainScreen(Screen):
365
365
 
366
366
  self.app.push_screen(
367
367
  ChoiceScreen(
368
- "Color theme", labels, initial=initial, on_highlight=lambda i: self.app.preview_theme(names[i])
368
+ "Color theme",
369
+ labels,
370
+ initial=initial,
371
+ on_highlight=lambda i: self.app.preview_theme(names[i]),
369
372
  ),
370
373
  on_choice,
371
374
  )
@@ -421,9 +424,7 @@ class MainScreen(Screen):
421
424
  return
422
425
  command = editor.editor_command()
423
426
  if command is None:
424
- self.app.push_screen(
425
- MessageScreen("No text editor found.\nSet $EDITOR or install nano.")
426
- )
427
+ self.app.push_screen(MessageScreen("No text editor found.\nSet $EDITOR or install nano."))
427
428
  return
428
429
  with self.app.suspend():
429
430
  try:
@@ -561,12 +562,50 @@ class MainScreen(Screen):
561
562
  if netlocations.is_macos() and scheme != "smb":
562
563
  self.app.push_screen(MessageScreen(netlocations.MACOS_UNSUPPORTED_MSG))
563
564
  return
565
+ if netlocations.backend_missing(scheme):
566
+ self._offer_backend_install(scheme, choice)
567
+ return
564
568
  self.app.push_screen(
565
569
  InputScreen(f"{choice} server (IP or name):"), lambda host: on_host(scheme, host)
566
570
  )
567
571
 
568
572
  self.app.push_screen(ChoiceScreen("Protocol", ["SMB", "SFTP", "FTP"]), on_scheme)
569
573
 
574
+ def _offer_backend_install(self, scheme: str, label: str) -> None:
575
+ command = netlocations.backend_install_command()
576
+ if command is None:
577
+ self.app.push_screen(
578
+ MessageScreen(
579
+ f"{label} needs the GVFS {scheme} backend, which is not installed.\nInstall your system's GVFS backends package and try again."
580
+ )
581
+ )
582
+ return
583
+ text = " ".join(command)
584
+
585
+ def on_choice(choice: str | None) -> None:
586
+ if choice != "Install now":
587
+ return
588
+ with self.app.suspend():
589
+ ok = netlocations.install_backend()
590
+ if ok:
591
+ self.app.push_screen(
592
+ MessageScreen(
593
+ "GVFS backends installed.\nQuit and reopen CommanderTUI, then add the location again."
594
+ )
595
+ )
596
+ else:
597
+ self.app.push_screen(
598
+ MessageScreen("The install did not finish. Check the terminal output, then try again.")
599
+ )
600
+
601
+ self.app.push_screen(
602
+ ChoiceScreen(
603
+ f"{label} needs the GVFS backend, which is missing. Install it now? ({text}; asks for your sudo password)",
604
+ ["Install now", "Cancel"],
605
+ ),
606
+ on_choice,
607
+ )
608
+
570
609
  def _pick_share(self, host: str, retried: bool = False, user: str | None = None) -> None:
571
610
  self.notify(f"Listing shares on {host}...")
572
611
 
@@ -48,6 +48,47 @@ def network_available() -> bool:
48
48
  return gio_available()
49
49
 
50
50
 
51
+ def _gvfs_mount_dirs() -> list[str]:
52
+ data_dirs = os.environ.get("XDG_DATA_DIRS") or "/usr/local/share:/usr/share"
53
+ return [
54
+ os.path.join(d, "gvfs", "mounts")
55
+ for d in ["/usr/share", "/usr/local/share", *data_dirs.split(":")]
56
+ if d
57
+ ]
58
+
59
+
60
+ def backend_missing(scheme: str) -> bool:
61
+ """True on Linux when GVFS has no backend for `scheme` (gio then answers
62
+ "Operation not supported"). Always False on macOS and when `gio` is absent."""
63
+ if is_macos() or not gio_available():
64
+ return False
65
+ return not any(os.path.isfile(os.path.join(d, f"{scheme}.mount")) for d in _gvfs_mount_dirs())
66
+
67
+
68
+ # Package that ships the GVFS backends, per package manager.
69
+ _BACKEND_INSTALL = {
70
+ "apt-get": ["sudo", "apt-get", "install", "gvfs-backends"],
71
+ "pacman": ["sudo", "pacman", "-S", "gvfs", "gvfs-smb"],
72
+ "dnf": ["sudo", "dnf", "install", "gvfs-smb"],
73
+ }
74
+
75
+
76
+ def backend_install_command() -> list[str] | None:
77
+ """The command that installs the GVFS backends here, or None if the package manager is unknown."""
78
+ for manager, command in _BACKEND_INSTALL.items():
79
+ if shutil.which(manager):
80
+ return list(command)
81
+ return None
82
+
83
+
84
+ def install_backend() -> bool:
85
+ """Run the install attached to the real terminal (sudo asks for the password). True if it exited 0."""
86
+ command = backend_install_command()
87
+ if command is None:
88
+ return False
89
+ return subprocess.run(command).returncode == 0
90
+
91
+
51
92
  def build_uri(scheme: str, host: str, share: str | None = None, user: str | None = None) -> str:
52
93
  host = host.strip().strip("/")
53
94
  user_clean = user.strip() if user else ""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commandertui
3
- Version: 1.4.0
3
+ Version: 1.4.1
4
4
  Summary: Dual-pane retro terminal file manager: compare, sync, copy and move folders
5
5
  Author: Humberto Barchini
6
6
  License-Expression: MIT
@@ -162,6 +162,7 @@ location is saved in `~/.config/commandertui/network.json` (protocol, host,
162
162
  share and user only -- never a password) and reappears in Places, mounting itself
163
163
  when picked. If the server asks for a login, `gio` asks for it in the
164
164
  terminal and can remember it in the system keyring, exactly like Nautilus (on macOS, the system login dialog asks and saves it in Keychain).
165
+ If the GVFS backend for the chosen protocol is not installed (a bare Ubuntu server, for example, has none), the app says so and offers to install it for you (`apt-get install gvfs-backends`, `pacman -S gvfs gvfs-smb` or `dnf install gvfs-smb`); it asks for your sudo password in the terminal and only runs if you answer *Install now*.
165
166
 
166
167
  Every key also has a clickable box on the bottom bar: one row on wide windows
167
168
  (168+ columns), two rows when narrower (120 columns is the practical minimum).
@@ -1159,3 +1159,66 @@ async def test_places_unmount_share_checks_type_evicts_panel_and_reports_busy(tm
1159
1159
  await pilot.pause()
1160
1160
  assert isinstance(app.screen, MessageScreen)
1161
1161
  assert "failed to unmount" in app.screen.text
1162
+
1163
+
1164
+ @pytest.mark.asyncio
1165
+ async def test_missing_gvfs_backend_offers_install_and_runs_it(tmp_path, monkeypatch):
1166
+ import contextlib
1167
+
1168
+ from commandertui import netlocations
1169
+ from commandertui.modals import ChoiceScreen, InputScreen
1170
+
1171
+ left = tmp_path / "left"
1172
+ right = tmp_path / "right"
1173
+ left.mkdir()
1174
+ right.mkdir()
1175
+ ran = []
1176
+ monkeypatch.setattr(netlocations, "gio_available", lambda: True)
1177
+ monkeypatch.setattr(netlocations, "backend_missing", lambda scheme: True)
1178
+ monkeypatch.setattr(
1179
+ netlocations, "backend_install_command", lambda: ["sudo", "apt-get", "install", "gvfs-backends"]
1180
+ )
1181
+ monkeypatch.setattr(netlocations, "install_backend", lambda: ran.append(True) or True)
1182
+
1183
+ app = CommanderApp(left_path=str(left), right_path=str(right))
1184
+ async with app.run_test() as pilot:
1185
+ await _skip_boot(pilot)
1186
+ app.suspend = contextlib.nullcontext # headless: no real terminal to hand over
1187
+ app.screen._add_network()
1188
+ await pilot.pause()
1189
+ await pilot.press("enter") # protocol: SMB
1190
+ await pilot.pause()
1191
+ # no server prompt: the app asks to install first
1192
+ assert isinstance(app.screen, ChoiceScreen) and not isinstance(app.screen, InputScreen)
1193
+ assert "gvfs-backends" in app.screen.prompt
1194
+ await pilot.press("enter") # "Install now"
1195
+ await pilot.pause()
1196
+ assert ran == [True]
1197
+
1198
+
1199
+ @pytest.mark.asyncio
1200
+ async def test_missing_gvfs_backend_cancel_installs_nothing(tmp_path, monkeypatch):
1201
+ from commandertui import netlocations
1202
+
1203
+ left = tmp_path / "left"
1204
+ right = tmp_path / "right"
1205
+ left.mkdir()
1206
+ right.mkdir()
1207
+ ran = []
1208
+ monkeypatch.setattr(netlocations, "gio_available", lambda: True)
1209
+ monkeypatch.setattr(netlocations, "backend_missing", lambda scheme: True)
1210
+ monkeypatch.setattr(
1211
+ netlocations, "backend_install_command", lambda: ["sudo", "apt-get", "install", "gvfs-backends"]
1212
+ )
1213
+ monkeypatch.setattr(netlocations, "install_backend", lambda: ran.append(True) or True)
1214
+
1215
+ app = CommanderApp(left_path=str(left), right_path=str(right))
1216
+ async with app.run_test() as pilot:
1217
+ await _skip_boot(pilot)
1218
+ app.screen._add_network()
1219
+ await pilot.pause()
1220
+ await pilot.press("enter")
1221
+ await pilot.pause()
1222
+ await pilot.press("down", "enter") # "Cancel"
1223
+ await pilot.pause()
1224
+ assert ran == []
@@ -244,3 +244,45 @@ def test_trash_fallback_uses_macos_trash_on_darwin(tmp_path, monkeypatch):
244
244
  assert trash.send_to_trash(str(f)) is True
245
245
  assert not f.exists()
246
246
  assert (fake_home / ".Trash" / "doc.txt").read_text() == "trash me"
247
+
248
+
249
+ def test_backend_missing_checks_gvfs_mount_files(monkeypatch, tmp_path):
250
+ monkeypatch.setattr(netlocations.sys, "platform", "linux")
251
+ monkeypatch.setattr(netlocations, "gio_available", lambda: True)
252
+ mounts = tmp_path / "gvfs" / "mounts"
253
+ mounts.mkdir(parents=True)
254
+ monkeypatch.setattr(netlocations, "_gvfs_mount_dirs", lambda: [str(mounts)])
255
+ assert netlocations.backend_missing("smb") is True
256
+ (mounts / "smb.mount").write_text("")
257
+ assert netlocations.backend_missing("smb") is False
258
+ assert netlocations.backend_missing("sftp") is True
259
+
260
+
261
+ def test_backend_missing_is_false_on_macos_or_without_gio(monkeypatch):
262
+ monkeypatch.setattr(netlocations.sys, "platform", "darwin")
263
+ assert netlocations.backend_missing("smb") is False
264
+ monkeypatch.setattr(netlocations.sys, "platform", "linux")
265
+ monkeypatch.setattr(netlocations, "gio_available", lambda: False)
266
+ assert netlocations.backend_missing("smb") is False
267
+
268
+
269
+ def test_backend_install_command_per_package_manager(monkeypatch):
270
+ monkeypatch.setattr(
271
+ netlocations.shutil, "which", lambda name: "/usr/bin/" + name if name == "apt-get" else None
272
+ )
273
+ assert netlocations.backend_install_command() == ["sudo", "apt-get", "install", "gvfs-backends"]
274
+ monkeypatch.setattr(
275
+ netlocations.shutil, "which", lambda name: "/usr/bin/pacman" if name == "pacman" else None
276
+ )
277
+ assert netlocations.backend_install_command() == ["sudo", "pacman", "-S", "gvfs", "gvfs-smb"]
278
+ monkeypatch.setattr(netlocations.shutil, "which", lambda name: None)
279
+ assert netlocations.backend_install_command() is None
280
+ assert netlocations.install_backend() is False
281
+
282
+
283
+ def test_install_backend_reports_exit_code(monkeypatch):
284
+ monkeypatch.setattr(netlocations, "backend_install_command", lambda: ["sudo", "true"])
285
+ monkeypatch.setattr(netlocations.subprocess, "run", lambda cmd: subprocess.CompletedProcess(cmd, 0))
286
+ assert netlocations.install_backend() is True
287
+ monkeypatch.setattr(netlocations.subprocess, "run", lambda cmd: subprocess.CompletedProcess(cmd, 1))
288
+ assert netlocations.install_backend() is False
@@ -1 +0,0 @@
1
- __version__ = "1.4.0"
File without changes
File without changes