commandertui 1.0.0__tar.gz → 1.2.0__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 (50) hide show
  1. {commandertui-1.0.0 → commandertui-1.2.0}/PKG-INFO +68 -6
  2. {commandertui-1.0.0 → commandertui-1.2.0}/README.md +67 -5
  3. commandertui-1.2.0/commandertui/__init__.py +1 -0
  4. commandertui-1.2.0/commandertui/app.py +70 -0
  5. {commandertui-1.0.0 → commandertui-1.2.0}/commandertui/bookmarks.py +66 -7
  6. {commandertui-1.0.0 → commandertui-1.2.0}/commandertui/cli.py +6 -1
  7. {commandertui-1.0.0 → commandertui-1.2.0}/commandertui/compare_screen.py +4 -4
  8. commandertui-1.2.0/commandertui/main_screen.py +443 -0
  9. commandertui-1.2.0/commandertui/modals.py +626 -0
  10. commandertui-1.2.0/commandertui/netlocations.py +118 -0
  11. {commandertui-1.0.0 → commandertui-1.2.0}/commandertui/progress_screen.py +38 -3
  12. {commandertui-1.0.0 → commandertui-1.2.0}/commandertui/sync.py +13 -2
  13. commandertui-1.2.0/commandertui/theme.py +255 -0
  14. commandertui-1.2.0/commandertui/updater.py +163 -0
  15. commandertui-1.2.0/commandertui/widgets.py +357 -0
  16. {commandertui-1.0.0 → commandertui-1.2.0}/commandertui.egg-info/PKG-INFO +68 -6
  17. {commandertui-1.0.0 → commandertui-1.2.0}/commandertui.egg-info/SOURCES.txt +4 -0
  18. commandertui-1.2.0/tests/test_app_smoke.py +761 -0
  19. {commandertui-1.0.0 → commandertui-1.2.0}/tests/test_bookmarks.py +58 -0
  20. {commandertui-1.0.0 → commandertui-1.2.0}/tests/test_executor.py +7 -1
  21. commandertui-1.2.0/tests/test_netlocations.py +51 -0
  22. {commandertui-1.0.0 → commandertui-1.2.0}/tests/test_sync.py +19 -0
  23. commandertui-1.2.0/tests/test_updater.py +109 -0
  24. {commandertui-1.0.0 → commandertui-1.2.0}/tests/test_widgets.py +13 -0
  25. commandertui-1.0.0/commandertui/__init__.py +0 -1
  26. commandertui-1.0.0/commandertui/app.py +0 -40
  27. commandertui-1.0.0/commandertui/main_screen.py +0 -211
  28. commandertui-1.0.0/commandertui/modals.py +0 -250
  29. commandertui-1.0.0/commandertui/theme.py +0 -94
  30. commandertui-1.0.0/commandertui/widgets.py +0 -134
  31. commandertui-1.0.0/tests/test_app_smoke.py +0 -129
  32. {commandertui-1.0.0 → commandertui-1.2.0}/LICENSE +0 -0
  33. {commandertui-1.0.0 → commandertui-1.2.0}/commandertui/ascii_font.py +0 -0
  34. {commandertui-1.0.0 → commandertui-1.2.0}/commandertui/boot.py +0 -0
  35. {commandertui-1.0.0 → commandertui-1.2.0}/commandertui/diff.py +0 -0
  36. {commandertui-1.0.0 → commandertui-1.2.0}/commandertui/executor.py +0 -0
  37. {commandertui-1.0.0 → commandertui-1.2.0}/commandertui/models.py +0 -0
  38. {commandertui-1.0.0 → commandertui-1.2.0}/commandertui/plan.py +0 -0
  39. {commandertui-1.0.0 → commandertui-1.2.0}/commandertui/run_queue.py +0 -0
  40. {commandertui-1.0.0 → commandertui-1.2.0}/commandertui/scanner.py +0 -0
  41. {commandertui-1.0.0 → commandertui-1.2.0}/commandertui/trash.py +0 -0
  42. {commandertui-1.0.0 → commandertui-1.2.0}/commandertui.egg-info/dependency_links.txt +0 -0
  43. {commandertui-1.0.0 → commandertui-1.2.0}/commandertui.egg-info/entry_points.txt +0 -0
  44. {commandertui-1.0.0 → commandertui-1.2.0}/commandertui.egg-info/requires.txt +0 -0
  45. {commandertui-1.0.0 → commandertui-1.2.0}/commandertui.egg-info/top_level.txt +0 -0
  46. {commandertui-1.0.0 → commandertui-1.2.0}/pyproject.toml +0 -0
  47. {commandertui-1.0.0 → commandertui-1.2.0}/setup.cfg +0 -0
  48. {commandertui-1.0.0 → commandertui-1.2.0}/tests/test_diff.py +0 -0
  49. {commandertui-1.0.0 → commandertui-1.2.0}/tests/test_plan.py +0 -0
  50. {commandertui-1.0.0 → commandertui-1.2.0}/tests/test_scanner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commandertui
3
- Version: 1.0.0
3
+ Version: 1.2.0
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
@@ -22,7 +22,7 @@ Dual-pane terminal file manager: compare two folders, sync them, or move
22
22
  files/folders between them (and to USB/SMB/any mounted path) — with a
23
23
  phosphor-green 80s-terminal look and a short modem-style boot animation.
24
24
 
25
- Status: **v1.0.0** — 40/40 tests passing. MIT licensed.
25
+ Status: **v1.2.0** — 108/108 tests passing. MIT licensed.
26
26
 
27
27
  ---
28
28
 
@@ -32,7 +32,7 @@ A safe, simple alternative to heavier dual-pane file managers. It was
32
32
  written after an earlier attempt of the same idea had to be scrapped for
33
33
  running an unauthenticated local HTTP server. This project deliberately:
34
34
 
35
- - **Never opens a network port.** Pure terminal UI (Textual), no HTTP server,
35
+ - **Never opens a network port.** Pure terminal UI (Textual), no HTTP server (the only outgoing request is the daily update check, which you can turn off),
36
36
  no webview.
37
37
  - **Never does a raw `rm -rf`.** Deletes always go through the desktop trash
38
38
  (`send2trash` → `gio trash` → manual move into `~/.local/share/Trash`, in
@@ -58,12 +58,34 @@ makepkg -si
58
58
 
59
59
  Remove it with `sudo pacman -R commandertui`.
60
60
 
61
- **Other distros.** It is a regular Python package (Python 3.11+):
61
+ **PyPI (Linux / macOS / WSL).** It is a standard package on [PyPI](https://pypi.org/project/commandertui/) (Python 3.11+). Recommended via `pipx`:
62
62
 
63
63
  ```bash
64
- pipx install git+https://github.com/juniorbarchini-oss/commandertui.git
64
+ pipx install commandertui
65
65
  ```
66
66
 
67
+ Or inside an activated virtual environment:
68
+
69
+ ```bash
70
+ pip install commandertui
71
+ ```
72
+
73
+ **macOS.** Same Python package (`pipx install commandertui`), plus
74
+ [kitty](https://sw.kovidgoyal.net/kitty/) for the standalone window. Places
75
+ lists the volumes under `/Volumes` (system volumes such as Recovery and
76
+ Preboot are hidden) and FUSE mounts inside your home folder. To get a
77
+ double-clickable app with the CommanderTUI icon:
78
+
79
+ ```bash
80
+ packaging/macos/build_app.sh # builds packaging/macos/CommanderTUI.app
81
+ packaging/macos/build_app.sh --install # also copies it to /Applications
82
+ ```
83
+
84
+ The app opens kitty at 1174x637 px and runs `commandertui`. It looks for the
85
+ command in `~/.local/bin` (pipx), Homebrew, `/usr/local/bin`, your `PATH`,
86
+ and a source checkout's `.venv`. Tested by the author on macOS with a Time
87
+ Machine volume, Google Drive and OneDrive mounts.
88
+
67
89
  **From a source checkout** (development):
68
90
 
69
91
  ```bash
@@ -104,13 +126,17 @@ o.window("^commandertui$", { float = true, center = true, size = { 1174, 637 } }
104
126
  | `↑`/`↓`, `Enter` | Navigate, open directory |
105
127
  | `Backspace` | Go to parent directory |
106
128
  | `Space` | Mark/unmark item under cursor |
129
+ | `F2` | Rename the item under the cursor (never overwrites an existing name) |
107
130
  | `F5` | Copy marked (or item under cursor) to the other pane |
108
131
  | `F6` | Move marked to the other pane |
109
132
  | `F7` | Create a folder in the active pane |
110
133
  | `F8` / `Delete` | Delete marked (to trash) |
134
+ | `/` | Filter the active pane by name (contains, ignores case) as you type; `Enter` keeps it, `Esc` clears it |
135
+ | `+` / `-` / `*` | Mark / unmark by pattern (`*.iso`, or plain text = contains) / invert marks, over the rows shown |
136
+ | `u` | Update: shows the pending new version (see *Updates*) |
111
137
  | `i` | Size: file/folder count and total bytes of marked items (background, `Esc` stops it) |
112
138
  | `h` | Show/hide hidden (dot) files in both panes |
113
- | `p` | Places: jump to a mounted drive (USB/SMB/rclone) or saved bookmark |
139
+ | `p` | Places: jump to a mounted drive (USB/SMB/rclone), saved bookmark or saved network location. Inside: `a` add a network location, `s` save the active pane's folder as a bookmark, `d` delete a saved one |
114
140
  | `c` | Compare: full recursive diff between the two panes' current folders |
115
141
  | `r` | Refresh both panes |
116
142
  | `q` | Quit |
@@ -125,6 +151,41 @@ half-written), and a move never removes a source it didn't fully copy.
125
151
  on-demand mount point doesn't show), GVFS/Nautilus network shares, FUSE
126
152
  mounts inside `$HOME` (e.g. `rclone mount`), and saved bookmarks.
127
153
 
154
+ **Network locations (Linux, needs `gio`/GVFS):** in Places press `a`, pick
155
+ SMB, SFTP or FTP and type the server's IP or name. For SMB the app lists the
156
+ shares on the server and you pick one; SFTP/FTP connect straight in. The
157
+ location is saved in `~/.config/commandertui/network.json` (protocol, host
158
+ and share only -- never a password) and reappears in Places, mounting itself
159
+ when picked. If the server asks for a login, `gio` asks for it in the
160
+ terminal and can remember it in the system keyring, exactly like Nautilus.
161
+
162
+ Every key also has a clickable box on the bottom bar: one row on wide windows
163
+ (168+ columns), two rows when narrower (110 columns is the practical minimum).
164
+ The active pane has the thick border and the inverted path bar; each path bar
165
+ shows the free space of that disk, and the marked count and total size.
166
+
167
+ ## Updates
168
+
169
+ Once a day, in the background and only after the start-up animation, the app
170
+ asks PyPI for the latest version (a plain GET, nothing about you is sent). If
171
+ a newer one exists you get a dialog -- **Update now / Later / Skip this
172
+ version** -- and the header keeps saying `update X available (press u)`.
173
+ It never installs on its own. **Update now** hands the terminal over and runs
174
+ the command that matches how it was installed:
175
+
176
+ | Installed as | Update runs |
177
+ |---|---|
178
+ | Arch package (`makepkg`) | rebuilds from the release's PKGBUILD with `makepkg -si` (your sudo password is typed in the terminal, never read by the app) |
179
+ | pipx | `pipx upgrade commandertui` |
180
+ | pip | `pip install --upgrade commandertui` |
181
+ | git checkout | no button: the dialog tells you to `git pull` |
182
+
183
+ Turn the check off with `commandertui --no-update-check` or by setting
184
+ `COMMANDERTUI_NO_UPDATE_CHECK=1`. `COMMANDERTUI_FAKE_LATEST=9.9.9` shows the
185
+ dialog without a real release, to see how it looks. The logic lives in one
186
+ self-contained file, `commandertui/updater.py`, written to be copied into
187
+ other apps.
188
+
128
189
  Inside **Compare**:
129
190
 
130
191
  | Key | Action |
@@ -151,6 +212,7 @@ commandertui/
151
212
  ├── trash.py # 3-layer safe delete (send2trash / gio trash / manual)
152
213
  ├── executor.py # runs an OperationQueue against the filesystem
153
214
  ├── bookmarks.py # detected mounts (USB/SMB/rclone) + saved places
215
+ ├── netlocations.py # SMB/SFTP/FTP locations via gio (list shares, mount, save)
154
216
  ├── theme.py # green/amber phosphor palettes
155
217
  ├── boot.py # startup "modem" reveal animation
156
218
  ├── widgets.py # FilePanel: one pane's directory listing
@@ -4,7 +4,7 @@ Dual-pane terminal file manager: compare two folders, sync them, or move
4
4
  files/folders between them (and to USB/SMB/any mounted path) — with a
5
5
  phosphor-green 80s-terminal look and a short modem-style boot animation.
6
6
 
7
- Status: **v1.0.0** — 40/40 tests passing. MIT licensed.
7
+ Status: **v1.2.0** — 108/108 tests passing. MIT licensed.
8
8
 
9
9
  ---
10
10
 
@@ -14,7 +14,7 @@ A safe, simple alternative to heavier dual-pane file managers. It was
14
14
  written after an earlier attempt of the same idea had to be scrapped for
15
15
  running an unauthenticated local HTTP server. This project deliberately:
16
16
 
17
- - **Never opens a network port.** Pure terminal UI (Textual), no HTTP server,
17
+ - **Never opens a network port.** Pure terminal UI (Textual), no HTTP server (the only outgoing request is the daily update check, which you can turn off),
18
18
  no webview.
19
19
  - **Never does a raw `rm -rf`.** Deletes always go through the desktop trash
20
20
  (`send2trash` → `gio trash` → manual move into `~/.local/share/Trash`, in
@@ -40,12 +40,34 @@ makepkg -si
40
40
 
41
41
  Remove it with `sudo pacman -R commandertui`.
42
42
 
43
- **Other distros.** It is a regular Python package (Python 3.11+):
43
+ **PyPI (Linux / macOS / WSL).** It is a standard package on [PyPI](https://pypi.org/project/commandertui/) (Python 3.11+). Recommended via `pipx`:
44
44
 
45
45
  ```bash
46
- pipx install git+https://github.com/juniorbarchini-oss/commandertui.git
46
+ pipx install commandertui
47
47
  ```
48
48
 
49
+ Or inside an activated virtual environment:
50
+
51
+ ```bash
52
+ pip install commandertui
53
+ ```
54
+
55
+ **macOS.** Same Python package (`pipx install commandertui`), plus
56
+ [kitty](https://sw.kovidgoyal.net/kitty/) for the standalone window. Places
57
+ lists the volumes under `/Volumes` (system volumes such as Recovery and
58
+ Preboot are hidden) and FUSE mounts inside your home folder. To get a
59
+ double-clickable app with the CommanderTUI icon:
60
+
61
+ ```bash
62
+ packaging/macos/build_app.sh # builds packaging/macos/CommanderTUI.app
63
+ packaging/macos/build_app.sh --install # also copies it to /Applications
64
+ ```
65
+
66
+ The app opens kitty at 1174x637 px and runs `commandertui`. It looks for the
67
+ command in `~/.local/bin` (pipx), Homebrew, `/usr/local/bin`, your `PATH`,
68
+ and a source checkout's `.venv`. Tested by the author on macOS with a Time
69
+ Machine volume, Google Drive and OneDrive mounts.
70
+
49
71
  **From a source checkout** (development):
50
72
 
51
73
  ```bash
@@ -86,13 +108,17 @@ o.window("^commandertui$", { float = true, center = true, size = { 1174, 637 } }
86
108
  | `↑`/`↓`, `Enter` | Navigate, open directory |
87
109
  | `Backspace` | Go to parent directory |
88
110
  | `Space` | Mark/unmark item under cursor |
111
+ | `F2` | Rename the item under the cursor (never overwrites an existing name) |
89
112
  | `F5` | Copy marked (or item under cursor) to the other pane |
90
113
  | `F6` | Move marked to the other pane |
91
114
  | `F7` | Create a folder in the active pane |
92
115
  | `F8` / `Delete` | Delete marked (to trash) |
116
+ | `/` | Filter the active pane by name (contains, ignores case) as you type; `Enter` keeps it, `Esc` clears it |
117
+ | `+` / `-` / `*` | Mark / unmark by pattern (`*.iso`, or plain text = contains) / invert marks, over the rows shown |
118
+ | `u` | Update: shows the pending new version (see *Updates*) |
93
119
  | `i` | Size: file/folder count and total bytes of marked items (background, `Esc` stops it) |
94
120
  | `h` | Show/hide hidden (dot) files in both panes |
95
- | `p` | Places: jump to a mounted drive (USB/SMB/rclone) or saved bookmark |
121
+ | `p` | Places: jump to a mounted drive (USB/SMB/rclone), saved bookmark or saved network location. Inside: `a` add a network location, `s` save the active pane's folder as a bookmark, `d` delete a saved one |
96
122
  | `c` | Compare: full recursive diff between the two panes' current folders |
97
123
  | `r` | Refresh both panes |
98
124
  | `q` | Quit |
@@ -107,6 +133,41 @@ half-written), and a move never removes a source it didn't fully copy.
107
133
  on-demand mount point doesn't show), GVFS/Nautilus network shares, FUSE
108
134
  mounts inside `$HOME` (e.g. `rclone mount`), and saved bookmarks.
109
135
 
136
+ **Network locations (Linux, needs `gio`/GVFS):** in Places press `a`, pick
137
+ SMB, SFTP or FTP and type the server's IP or name. For SMB the app lists the
138
+ shares on the server and you pick one; SFTP/FTP connect straight in. The
139
+ location is saved in `~/.config/commandertui/network.json` (protocol, host
140
+ and share only -- never a password) and reappears in Places, mounting itself
141
+ when picked. If the server asks for a login, `gio` asks for it in the
142
+ terminal and can remember it in the system keyring, exactly like Nautilus.
143
+
144
+ Every key also has a clickable box on the bottom bar: one row on wide windows
145
+ (168+ columns), two rows when narrower (110 columns is the practical minimum).
146
+ The active pane has the thick border and the inverted path bar; each path bar
147
+ shows the free space of that disk, and the marked count and total size.
148
+
149
+ ## Updates
150
+
151
+ Once a day, in the background and only after the start-up animation, the app
152
+ asks PyPI for the latest version (a plain GET, nothing about you is sent). If
153
+ a newer one exists you get a dialog -- **Update now / Later / Skip this
154
+ version** -- and the header keeps saying `update X available (press u)`.
155
+ It never installs on its own. **Update now** hands the terminal over and runs
156
+ the command that matches how it was installed:
157
+
158
+ | Installed as | Update runs |
159
+ |---|---|
160
+ | Arch package (`makepkg`) | rebuilds from the release's PKGBUILD with `makepkg -si` (your sudo password is typed in the terminal, never read by the app) |
161
+ | pipx | `pipx upgrade commandertui` |
162
+ | pip | `pip install --upgrade commandertui` |
163
+ | git checkout | no button: the dialog tells you to `git pull` |
164
+
165
+ Turn the check off with `commandertui --no-update-check` or by setting
166
+ `COMMANDERTUI_NO_UPDATE_CHECK=1`. `COMMANDERTUI_FAKE_LATEST=9.9.9` shows the
167
+ dialog without a real release, to see how it looks. The logic lives in one
168
+ self-contained file, `commandertui/updater.py`, written to be copied into
169
+ other apps.
170
+
110
171
  Inside **Compare**:
111
172
 
112
173
  | Key | Action |
@@ -133,6 +194,7 @@ commandertui/
133
194
  ├── trash.py # 3-layer safe delete (send2trash / gio trash / manual)
134
195
  ├── executor.py # runs an OperationQueue against the filesystem
135
196
  ├── bookmarks.py # detected mounts (USB/SMB/rclone) + saved places
197
+ ├── netlocations.py # SMB/SFTP/FTP locations via gio (list shares, mount, save)
136
198
  ├── theme.py # green/amber phosphor palettes
137
199
  ├── boot.py # startup "modem" reveal animation
138
200
  ├── widgets.py # FilePanel: one pane's directory listing
@@ -0,0 +1 @@
1
+ __version__ = "1.2.0"
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from textual.app import App
6
+
7
+ from . import updater
8
+ from .boot import BootScreen
9
+ from .main_screen import MainScreen
10
+ from .theme import css_for
11
+
12
+
13
+ class CommanderApp(App):
14
+ """Owns only the screen stack. All pane behavior lives in MainScreen --
15
+ see its docstring for why that split matters for key bindings.
16
+ """
17
+
18
+ CSS = css_for("green")
19
+
20
+ def __init__(
21
+ self, left_path: str | None = None, right_path: str | None = None, check_updates: bool = True
22
+ ) -> None:
23
+ super().__init__()
24
+ self.check_updates = check_updates and not updater.updates_disabled()
25
+ self.update_info: updater.UpdateInfo | None = None
26
+ self.update_prompted = False
27
+ self.left_path = os.path.abspath(left_path or os.path.expanduser("~"))
28
+ self.right_path = os.path.abspath(right_path or os.path.expanduser("~"))
29
+
30
+ def on_mount(self) -> None:
31
+ self.push_screen(MainScreen(self.left_path, self.right_path))
32
+ self.push_screen(BootScreen(), self._after_boot)
33
+
34
+ def _after_boot(self, _result=None) -> None:
35
+ # Looked up only once the boot animation is over, so a quick answer
36
+ # can never pop a dialog over it.
37
+ if self.check_updates:
38
+ self.run_worker(self._look_for_update, thread=True)
39
+
40
+ def _look_for_update(self) -> None:
41
+ info = updater.check()
42
+ if info:
43
+ self.call_from_thread(self.update_found, info)
44
+
45
+ def update_found(self, info: updater.UpdateInfo) -> None:
46
+ """Remember the newer version and say so in the header; the dialog
47
+ itself waits until the main screen is actually showing (not the boot
48
+ animation)."""
49
+ self.update_info = info
50
+ self.sub_title = f"update {info.latest} available (press u)"
51
+ if isinstance(self.screen, MainScreen):
52
+ self.screen.prompt_update()
53
+
54
+ def clear_update(self) -> None:
55
+ self.update_info = None
56
+ self.sub_title = ""
57
+
58
+ # Convenience passthroughs so existing tests/tools that poke at the app
59
+ # directly (e.g. app._panel("left")) keep working without knowing about
60
+ # the MainScreen split.
61
+ def _panel(self, side: str):
62
+ return self.main_screen._panel(side)
63
+
64
+ @property
65
+ def main_screen(self) -> MainScreen:
66
+ return next(s for s in self.screen_stack if isinstance(s, MainScreen))
67
+
68
+ @property
69
+ def active_side(self) -> str:
70
+ return self.main_screen.active_side
@@ -2,13 +2,30 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import os
5
+ import subprocess
6
+ import sys
5
7
  from dataclasses import dataclass
6
8
 
7
9
  CONFIG_DIR = os.path.expanduser("~/.config/commandertui")
8
10
  BOOKMARKS_FILE = os.path.join(CONFIG_DIR, "bookmarks.json")
9
11
 
10
- # Where Linux desktops mount removable/network drives.
11
- MOUNT_ROOTS = ("/run/media", "/media", "/mnt")
12
+ # Where OS mounts removable/network drives.
13
+ # Linux uses /run/media, /media, /mnt; macOS uses /Volumes.
14
+ if sys.platform == "darwin":
15
+ MOUNT_ROOTS = ("/Volumes",)
16
+ else:
17
+ MOUNT_ROOTS = ("/run/media", "/media", "/mnt")
18
+
19
+ # macOS system volumes to exclude from Places (macOS only: on Linux a drive
20
+ # labelled "VM" or "Update" is a perfectly normal drive and must show up).
21
+ MACOS_EXCLUDED_VOLUMES = {"Recovery", "VM", "Preboot", "Update"}
22
+
23
+
24
+ def _hidden_volume(name: str) -> bool:
25
+ if sys.platform != "darwin":
26
+ return False
27
+ return name.startswith((".", "com.apple.")) or name in MACOS_EXCLUDED_VOLUMES
28
+
12
29
 
13
30
  # Nautilus (and GNOME apps generally) mount network shares -- SMB, FTP,
14
31
  # SFTP -- through GVFS rather than udisks2, so they never show up under
@@ -30,11 +47,15 @@ def detect_mounted() -> list[Place]:
30
47
  continue
31
48
  try:
32
49
  for user_dir in os.scandir(root):
50
+ if _hidden_volume(user_dir.name):
51
+ continue
33
52
  if not user_dir.is_dir():
34
53
  continue
35
- # /run/media/<user>/<Label> vs /mnt/<Label> (no user subdir)
54
+ # /run/media/<user>/<Label> vs /mnt/<Label> or /Volumes/<Label> (no user subdir)
36
55
  candidates = list(os.scandir(user_dir.path)) if root == "/run/media" else [user_dir]
37
56
  for c in candidates:
57
+ if _hidden_volume(c.name):
58
+ continue
38
59
  # An on-demand mount point (e.g. /mnt/i7server) exists as
39
60
  # an empty directory even when nothing is mounted there --
40
61
  # os.path.ismount() is what actually tells them apart.
@@ -81,9 +102,7 @@ def _unescape_mount_path(path: str) -> str:
81
102
  return path.replace("\\040", " ").replace("\\011", "\t").replace("\\012", "\n").replace("\\134", "\\")
82
103
 
83
104
 
84
- def detect_home_mounts() -> list[Place]:
85
- """FUSE mounts inside $HOME, e.g. rclone's ~/365 and ~/GoogleDrive.
86
- Read from /proc/mounts so only what is live right now shows up."""
105
+ def _detect_home_mounts_proc() -> list[Place]:
87
106
  home = os.path.expanduser("~")
88
107
  try:
89
108
  with open(PROC_MOUNTS) as f:
@@ -101,6 +120,43 @@ def detect_home_mounts() -> list[Place]:
101
120
  return places
102
121
 
103
122
 
123
+ def _detect_home_mounts_darwin() -> list[Place]:
124
+ home = os.path.expanduser("~")
125
+ places: list[Place] = []
126
+ try:
127
+ res = subprocess.run(["mount"], capture_output=True, text=True, timeout=5)
128
+ if res.returncode != 0:
129
+ return []
130
+ for line in res.stdout.splitlines():
131
+ # e.g.: remote: on /Users/user/365 (macfuse, nodev, ...)
132
+ parts = line.split(" on ")
133
+ if len(parts) < 2:
134
+ continue
135
+ rest = parts[1]
136
+ path, _, fstype_part = rest.rpartition(" (")
137
+ if not fstype_part:
138
+ continue
139
+ fstype = fstype_part.rstrip(")").split(",")[0].strip()
140
+ path = path.strip()
141
+ if path.startswith(home + os.sep) and any(
142
+ f in fstype.lower() for f in ("fuse", "osxfuse", "macfuse", "rclone")
143
+ ):
144
+ places.append(Place(label=os.path.basename(path), path=path, kind=fstype))
145
+ except Exception:
146
+ pass
147
+ return places
148
+
149
+
150
+ def detect_home_mounts() -> list[Place]:
151
+ """FUSE mounts inside $HOME, e.g. rclone's ~/365 and ~/GoogleDrive.
152
+ Read from /proc/mounts on Linux (or if PROC_MOUNTS exists) or mount on macOS."""
153
+ if os.path.isfile(PROC_MOUNTS):
154
+ return _detect_home_mounts_proc()
155
+ if sys.platform == "darwin":
156
+ return _detect_home_mounts_darwin()
157
+ return []
158
+
159
+
104
160
  def load_saved() -> list[Place]:
105
161
  if not os.path.isfile(BOOKMARKS_FILE):
106
162
  return []
@@ -130,4 +186,7 @@ def remove_bookmark(path: str) -> None:
130
186
 
131
187
  def all_places() -> list[Place]:
132
188
  home = Place(label="Home", path=os.path.expanduser("~"), kind="home")
133
- return [home] + detect_mounted() + detect_network_mounts() + detect_home_mounts() + load_saved()
189
+ from .netlocations import load_locations # lazy: netlocations imports CONFIG_DIR from here
190
+
191
+ saved_net = [Place(label=loc.label, path=loc.uri, kind="net-saved") for loc in load_locations()]
192
+ return [home] + detect_mounted() + detect_network_mounts() + detect_home_mounts() + load_saved() + saved_net
@@ -11,10 +11,15 @@ def main() -> None:
11
11
  parser = argparse.ArgumentParser(prog="commandertui", description="Dual-pane TUI file manager")
12
12
  parser.add_argument("left", nargs="?", default=os.path.expanduser("~"))
13
13
  parser.add_argument("right", nargs="?", default=os.path.expanduser("~"))
14
+ parser.add_argument(
15
+ "--no-update-check",
16
+ action="store_true",
17
+ help="never contact PyPI to look for a newer version",
18
+ )
14
19
  parser.add_argument("--version", action="version", version=f"commandertui {__version__}")
15
20
  args = parser.parse_args()
16
21
 
17
- app = CommanderApp(left_path=args.left, right_path=args.right)
22
+ app = CommanderApp(left_path=args.left, right_path=args.right, check_updates=not args.no_update_check)
18
23
  app.run()
19
24
 
20
25
 
@@ -4,13 +4,14 @@ from textual.app import ComposeResult
4
4
  from textual.binding import Binding
5
5
  from textual.containers import Vertical
6
6
  from textual.screen import Screen
7
- from textual.widgets import DataTable, Footer, Static
7
+ from textual.widgets import DataTable, Static
8
8
 
9
9
  from .diff import compare
10
10
  from .modals import MessageScreen
11
11
  from .models import DiffStatus, OpKind, OpSide, OperationQueue, QueuedOp
12
12
  from .plan import plan_mirror, plan_two_way, plan_update
13
13
  from .run_queue import run_queue_with_progress
14
+ from .widgets import CompareBar
14
15
 
15
16
  STATUS_LABEL = {
16
17
  DiffStatus.LEFT_ONLY: "< only left",
@@ -45,7 +46,7 @@ class CompareScreen(Screen):
45
46
  def compose(self) -> ComposeResult:
46
47
  yield Static("", id="compare-summary")
47
48
  yield DataTable(id="compare-table", cursor_type="row")
48
- yield Footer()
49
+ yield CompareBar()
49
50
 
50
51
  def on_mount(self) -> None:
51
52
  self.reload()
@@ -65,8 +66,7 @@ class CompareScreen(Screen):
65
66
  summary = self.query_one("#compare-summary", Static)
66
67
  summary.update(
67
68
  f"{self.left_root} <-> {self.right_root}\n"
68
- f"{non_identical} differences "
69
- "[m]irror [u]pdate [b]idirectional [space] mark [c]opy marked [esc] back"
69
+ f"{non_identical} differences"
70
70
  )
71
71
 
72
72
  def action_back(self) -> None: