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.
- {commandertui-1.4.0 → commandertui-1.4.1}/PKG-INFO +2 -1
- {commandertui-1.4.0 → commandertui-1.4.1}/README.md +1 -0
- commandertui-1.4.1/commandertui/__init__.py +1 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/main_screen.py +43 -4
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/netlocations.py +41 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui.egg-info/PKG-INFO +2 -1
- {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_app_smoke.py +63 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_netlocations.py +42 -0
- commandertui-1.4.0/commandertui/__init__.py +0 -1
- {commandertui-1.4.0 → commandertui-1.4.1}/LICENSE +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/app.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/ascii_font.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/bookmarks.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/boot.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/cli.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/compare_screen.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/diff.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/editor.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/executor.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/modals.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/models.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/plan.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/preview.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/progress_screen.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/run_queue.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/scanner.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/settings.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/sync.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/theme.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/trash.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/updater.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui/widgets.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui.egg-info/SOURCES.txt +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui.egg-info/dependency_links.txt +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui.egg-info/entry_points.txt +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui.egg-info/requires.txt +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/commandertui.egg-info/top_level.txt +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/pyproject.toml +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/setup.cfg +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_bookmarks.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_diff.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_executor.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_plan.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_preview.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_scanner.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_sync.py +0 -0
- {commandertui-1.4.0 → commandertui-1.4.1}/tests/test_updater.py +0 -0
- {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.
|
|
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",
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|