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.
- {commandertui-1.0.0 → commandertui-1.1.0}/PKG-INFO +18 -2
- {commandertui-1.0.0 → commandertui-1.1.0}/README.md +17 -1
- commandertui-1.1.0/commandertui/__init__.py +1 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/bookmarks.py +62 -6
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui.egg-info/PKG-INFO +18 -2
- {commandertui-1.0.0 → commandertui-1.1.0}/tests/test_bookmarks.py +58 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/tests/test_executor.py +7 -1
- commandertui-1.0.0/commandertui/__init__.py +0 -1
- {commandertui-1.0.0 → commandertui-1.1.0}/LICENSE +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/app.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/ascii_font.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/boot.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/cli.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/compare_screen.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/diff.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/executor.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/main_screen.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/modals.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/models.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/plan.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/progress_screen.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/run_queue.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/scanner.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/sync.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/theme.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/trash.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui/widgets.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui.egg-info/SOURCES.txt +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui.egg-info/dependency_links.txt +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui.egg-info/entry_points.txt +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui.egg-info/requires.txt +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/commandertui.egg-info/top_level.txt +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/pyproject.toml +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/setup.cfg +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/tests/test_app_smoke.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/tests/test_diff.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/tests/test_plan.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/tests/test_scanner.py +0 -0
- {commandertui-1.0.0 → commandertui-1.1.0}/tests/test_sync.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
|
11
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|