commandertui 1.0.0__tar.gz → 1.1.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 (40) hide show
  1. {commandertui-1.0.0 → commandertui-1.1.0}/PKG-INFO +18 -2
  2. {commandertui-1.0.0 → commandertui-1.1.0}/README.md +17 -1
  3. commandertui-1.1.0/commandertui/__init__.py +1 -0
  4. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/bookmarks.py +62 -6
  5. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui.egg-info/PKG-INFO +18 -2
  6. {commandertui-1.0.0 → commandertui-1.1.0}/tests/test_bookmarks.py +58 -0
  7. {commandertui-1.0.0 → commandertui-1.1.0}/tests/test_executor.py +7 -1
  8. commandertui-1.0.0/commandertui/__init__.py +0 -1
  9. {commandertui-1.0.0 → commandertui-1.1.0}/LICENSE +0 -0
  10. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/app.py +0 -0
  11. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/ascii_font.py +0 -0
  12. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/boot.py +0 -0
  13. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/cli.py +0 -0
  14. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/compare_screen.py +0 -0
  15. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/diff.py +0 -0
  16. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/executor.py +0 -0
  17. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/main_screen.py +0 -0
  18. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/modals.py +0 -0
  19. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/models.py +0 -0
  20. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/plan.py +0 -0
  21. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/progress_screen.py +0 -0
  22. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/run_queue.py +0 -0
  23. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/scanner.py +0 -0
  24. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/sync.py +0 -0
  25. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/theme.py +0 -0
  26. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/trash.py +0 -0
  27. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/widgets.py +0 -0
  28. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui.egg-info/SOURCES.txt +0 -0
  29. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui.egg-info/dependency_links.txt +0 -0
  30. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui.egg-info/entry_points.txt +0 -0
  31. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui.egg-info/requires.txt +0 -0
  32. {commandertui-1.0.0 → commandertui-1.1.0}/commandertui.egg-info/top_level.txt +0 -0
  33. {commandertui-1.0.0 → commandertui-1.1.0}/pyproject.toml +0 -0
  34. {commandertui-1.0.0 → commandertui-1.1.0}/setup.cfg +0 -0
  35. {commandertui-1.0.0 → commandertui-1.1.0}/tests/test_app_smoke.py +0 -0
  36. {commandertui-1.0.0 → commandertui-1.1.0}/tests/test_diff.py +0 -0
  37. {commandertui-1.0.0 → commandertui-1.1.0}/tests/test_plan.py +0 -0
  38. {commandertui-1.0.0 → commandertui-1.1.0}/tests/test_scanner.py +0 -0
  39. {commandertui-1.0.0 → commandertui-1.1.0}/tests/test_sync.py +0 -0
  40. {commandertui-1.0.0 → commandertui-1.1.0}/tests/test_widgets.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.1.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.1.0** — tests passing. MIT licensed.
26
26
 
27
27
  ---
28
28
 
@@ -64,6 +64,22 @@ Remove it with `sudo pacman -R commandertui`.
64
64
  pipx install git+https://github.com/juniorbarchini-oss/commandertui.git
65
65
  ```
66
66
 
67
+ **macOS.** Same Python package (`pipx install ...` as above), plus
68
+ [kitty](https://sw.kovidgoyal.net/kitty/) for the standalone window. Places
69
+ lists the volumes under `/Volumes` (system volumes such as Recovery and
70
+ Preboot are hidden) and FUSE mounts inside your home folder. To get a
71
+ double-clickable app with the CommanderTUI icon:
72
+
73
+ ```bash
74
+ packaging/macos/build_app.sh # builds packaging/macos/CommanderTUI.app
75
+ packaging/macos/build_app.sh --install # also copies it to /Applications
76
+ ```
77
+
78
+ The app opens kitty at 1174x637 px and runs `commandertui`. It looks for the
79
+ command in `~/.local/bin` (pipx), Homebrew, `/usr/local/bin`, your `PATH`,
80
+ and a source checkout's `.venv`. Tested by the author on macOS with a Time
81
+ Machine volume, Google Drive and OneDrive mounts.
82
+
67
83
  **From a source checkout** (development):
68
84
 
69
85
  ```bash
@@ -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.1.0** — tests passing. MIT licensed.
8
8
 
9
9
  ---
10
10
 
@@ -46,6 +46,22 @@ Remove it with `sudo pacman -R commandertui`.
46
46
  pipx install git+https://github.com/juniorbarchini-oss/commandertui.git
47
47
  ```
48
48
 
49
+ **macOS.** Same Python package (`pipx install ...` as above), plus
50
+ [kitty](https://sw.kovidgoyal.net/kitty/) for the standalone window. Places
51
+ lists the volumes under `/Volumes` (system volumes such as Recovery and
52
+ Preboot are hidden) and FUSE mounts inside your home folder. To get a
53
+ double-clickable app with the CommanderTUI icon:
54
+
55
+ ```bash
56
+ packaging/macos/build_app.sh # builds packaging/macos/CommanderTUI.app
57
+ packaging/macos/build_app.sh --install # also copies it to /Applications
58
+ ```
59
+
60
+ The app opens kitty at 1174x637 px and runs `commandertui`. It looks for the
61
+ command in `~/.local/bin` (pipx), Homebrew, `/usr/local/bin`, your `PATH`,
62
+ and a source checkout's `.venv`. Tested by the author on macOS with a Time
63
+ Machine volume, Google Drive and OneDrive mounts.
64
+
49
65
  **From a source checkout** (development):
50
66
 
51
67
  ```bash
@@ -0,0 +1 @@
1
+ __version__ = "1.1.0"
@@ -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 []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commandertui
3
- Version: 1.0.0
3
+ Version: 1.1.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.1.0** — tests passing. MIT licensed.
26
26
 
27
27
  ---
28
28
 
@@ -64,6 +64,22 @@ Remove it with `sudo pacman -R commandertui`.
64
64
  pipx install git+https://github.com/juniorbarchini-oss/commandertui.git
65
65
  ```
66
66
 
67
+ **macOS.** Same Python package (`pipx install ...` as above), plus
68
+ [kitty](https://sw.kovidgoyal.net/kitty/) for the standalone window. Places
69
+ lists the volumes under `/Volumes` (system volumes such as Recovery and
70
+ Preboot are hidden) and FUSE mounts inside your home folder. To get a
71
+ double-clickable app with the CommanderTUI icon:
72
+
73
+ ```bash
74
+ packaging/macos/build_app.sh # builds packaging/macos/CommanderTUI.app
75
+ packaging/macos/build_app.sh --install # also copies it to /Applications
76
+ ```
77
+
78
+ The app opens kitty at 1174x637 px and runs `commandertui`. It looks for the
79
+ command in `~/.local/bin` (pipx), Homebrew, `/usr/local/bin`, your `PATH`,
80
+ and a source checkout's `.venv`. Tested by the author on macOS with a Time
81
+ Machine volume, Google Drive and OneDrive mounts.
82
+
67
83
  **From a source checkout** (development):
68
84
 
69
85
  ```bash
@@ -71,3 +71,61 @@ def test_home_fuse_mounts_detected_from_proc_mounts(tmp_path, monkeypatch):
71
71
  ("365", "/home/tester/365", "rclone"),
72
72
  ("Google Drive", "/home/tester/Google Drive", "rclone"),
73
73
  ]
74
+
75
+
76
+ def test_macos_volumes_detects_mounted_and_skips_system_and_hidden(tmp_path, monkeypatch):
77
+ volumes = tmp_path / "Volumes"
78
+ volumes.mkdir()
79
+ (volumes / ".timemachine").mkdir()
80
+ (volumes / "Recovery").mkdir()
81
+ (volumes / "USB-Drive").mkdir()
82
+ (volumes / "Backup Disk").mkdir()
83
+
84
+ monkeypatch.setattr(bookmarks, "MOUNT_ROOTS", (str(volumes),))
85
+ monkeypatch.setattr(bookmarks.os.path, "ismount", lambda p: True)
86
+ monkeypatch.setattr(bookmarks.sys, "platform", "darwin")
87
+
88
+ places = bookmarks.detect_mounted()
89
+ labels = {p.label for p in places}
90
+
91
+ assert "USB-Drive" in labels
92
+ assert "Backup Disk" in labels
93
+ assert "Recovery" not in labels
94
+ assert ".timemachine" not in labels
95
+
96
+
97
+ def test_linux_does_not_hide_drives_named_like_macos_system_volumes(tmp_path, monkeypatch):
98
+ root = tmp_path / "mnt"
99
+ root.mkdir()
100
+ for name in ("VM", "Update", "Recovery", "MyUSB"):
101
+ (root / name).mkdir()
102
+
103
+ monkeypatch.setattr(bookmarks, "MOUNT_ROOTS", (str(root),))
104
+ monkeypatch.setattr(bookmarks.os.path, "ismount", lambda p: True)
105
+ monkeypatch.setattr(bookmarks.sys, "platform", "linux")
106
+
107
+ labels = {p.label for p in bookmarks.detect_mounted()}
108
+
109
+ assert labels == {"VM", "Update", "Recovery", "MyUSB"}
110
+
111
+
112
+ def test_home_fuse_mounts_darwin(monkeypatch):
113
+ sample_mount_output = (
114
+ "/dev/disk3s1s1 on / (apfs, sealed, local, read-only)\n"
115
+ "//user@nas/Media on /Volumes/Media (smbfs, nodev)\n"
116
+ "rclone: on /Users/tester/365 (macfuse, nodev, nosuid)\n"
117
+ "google: on /Users/tester/Drive (fuse.rclone, nodev)\n"
118
+ )
119
+
120
+ class DummyResult:
121
+ returncode = 0
122
+ stdout = sample_mount_output
123
+
124
+ monkeypatch.setattr(bookmarks.subprocess, "run", lambda *a, **kw: DummyResult())
125
+ monkeypatch.setattr(bookmarks.os.path, "expanduser", lambda p: "/Users/tester")
126
+
127
+ places = bookmarks._detect_home_mounts_darwin()
128
+ assert [(p.label, p.path, p.kind) for p in places] == [
129
+ ("365", "/Users/tester/365", "macfuse"),
130
+ ("Drive", "/Users/tester/Drive", "fuse.rclone"),
131
+ ]
@@ -1,9 +1,15 @@
1
+ import os
2
+
1
3
  from commandertui.diff import compare
2
4
  from commandertui.executor import execute
3
5
  from commandertui.plan import plan_mirror
4
6
 
5
7
 
6
- def test_execute_mirror_makes_right_match_left(tmp_path):
8
+ def test_execute_mirror_makes_right_match_left(tmp_path, monkeypatch):
9
+ import commandertui.sync as sync_mod
10
+
11
+ monkeypatch.setattr(sync_mod, "send_to_trash", lambda p: os.remove(p) or True)
12
+
7
13
  left = tmp_path / "left"
8
14
  right = tmp_path / "right"
9
15
  left.mkdir()
@@ -1 +0,0 @@
1
- __version__ = "1.0.0"
File without changes
File without changes