Metadata-Version: 2.4
Name: commandertui
Version: 1.4.0
Summary: Dual-pane retro terminal file manager: compare, sync, copy and move folders
Author: Humberto Barchini
License-Expression: MIT
Project-URL: Homepage, https://github.com/juniorbarchini-oss/commandertui
Classifier: Environment :: Console
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: System :: Filesystems
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: textual>=8.0
Requires-Dist: send2trash>=1.8
Dynamic: license-file

# CommanderTUI

Dual-pane terminal file manager: compare two folders, sync them, or move
files/folders between them (and to USB/SMB/any mounted path) — with a
phosphor-green 80s-terminal look and a short modem-style boot animation.

Status: **v1.4.0** — 153/153 tests passing. MIT licensed.

---

## Why this exists

A safe, simple alternative to heavier dual-pane file managers. It was
written after an earlier attempt of the same idea had to be scrapped for
running an unauthenticated local HTTP server. This project deliberately:

- **Never opens a network port.** Pure terminal UI (Textual), no HTTP server (the only outgoing request is the update check on startup, which you can turn off),
  no webview.
- **Never does a raw `rm -rf`.** Deletes always go through the desktop trash
  (`send2trash` → `gio trash` → manual move into `~/.local/share/Trash`, in
  that order).
- **Never overwrites in place.** Copies stream into a hidden `.tmp` file next
  to the destination and are committed with `os.replace` — a crash or
  Ctrl+C never leaves a half-written file.
- **Never guesses on a conflict.** Two-way sync copies each side's exclusive
  files automatically, but a file changed on *both* sides is always shown to
  you, never auto-resolved.

## Install

**Arch Linux / Omarchy.** An AUR package is on the way (`yay -S commandertui`).
Until then, build it from the `PKGBUILD` in this repo. `makepkg` pulls in
everything it needs:

```bash
git clone https://github.com/juniorbarchini-oss/commandertui.git
cd commandertui/packaging/arch
makepkg -si
```

Remove it with `sudo pacman -R commandertui`.

**PyPI (Linux / macOS / WSL).** It is a standard package on [PyPI](https://pypi.org/project/commandertui/) (Python 3.11+). Recommended via `pipx`:

```bash
pipx install commandertui
```

Or inside an activated virtual environment:

```bash
pip install commandertui
```

**macOS.** Same Python package (`pipx install commandertui`), plus
[kitty](https://sw.kovidgoyal.net/kitty/) for the standalone window. Places
lists the volumes under `/Volumes` (system volumes such as Recovery and
Preboot are hidden) and FUSE mounts inside your home folder. To get a
double-clickable app with the CommanderTUI icon:

```bash
packaging/macos/build_app.sh            # builds packaging/macos/CommanderTUI.app
packaging/macos/build_app.sh --install  # also copies it to /Applications
```

The app opens kitty at 1174x637 px and runs `commandertui`. It looks for the
command in `~/.local/bin` (pipx), Homebrew, `/usr/local/bin`, your `PATH`,
and a source checkout's `.venv`. Tested by the author on macOS with a Time
Machine volume, Google Drive and OneDrive mounts.

**From a source checkout** (development):

```bash
python3 -m venv .venv && .venv/bin/pip install -e . -r requirements-dev.txt
bin/commandertui [left-dir] [right-dir]
```

## Run

| Command | What it does |
|---|---|
| `cmdrtui` | Opens it in its own terminal window. This is also what the **CommanderTUI** app-launcher entry runs |
| `commandertui` | Runs it inside the terminal you are in |
| `commandertui DIR1 DIR2` | Opens with those two folders side by side |

`cmdrtui` uses your desktop's default terminal (`xdg-terminal-exec`). If that
isn't available it tries foot, alacritty, kitty and ghostty. The window
always gets the app-id/class `commandertui`, so a window-manager rule can
float it. It needs about 1174x637 px to fit every key hint. On Hyprland:

```ini
# hyprland.conf
windowrulev2 = float, class:^(commandertui)$
windowrulev2 = size 1174 637, class:^(commandertui)$
windowrulev2 = center, class:^(commandertui)$
```

```lua
-- Omarchy Lua config (~/.config/hypr/hyprland.lua)
o.window("^commandertui$", { float = true, center = true, size = { 1174, 637 } })
```

## Keys

| Key | Action |
|---|---|
| `Tab` | Switch active pane |
| `↑`/`↓`, `Enter` | Navigate, open directory (or sort column when header is focused with `↑` beyond row 0) |
| `Backspace` | Go to parent directory |
| `Space` | Mark/unmark item under cursor |
| `F2` | Rename the item under the cursor (never overwrites an existing name) |
| `F4` | Edit the file under the cursor with `$VISUAL` / `$EDITOR` (falls back to nano, vim or vi); CommanderTUI pauses and comes back when you close the editor |
| `F5` | Copy marked (or item under cursor) to the other pane |
| `F6` | Move marked to the other pane |
| `F7` | Create a folder in the active pane |
| `F8` / `Delete` | Delete marked (to trash) |
| `/` | Filter the active pane by name (contains, ignores case) as you type; `Enter` keeps it, `Esc` clears it |
| `+` / `-` / `*` | Mark / unmark by pattern (`*.iso`, or plain text = contains) / invert marks, over the rows shown |
| `u` | Check for updates now (also the **Updates** button): asks PyPI right away and offers the new version if there is one |
| `v` | Quick Look file preview (macOS only): opens `qlmanage -p` for the file under cursor; press `v` again or close the window to dismiss |
| `i` | Size: file/folder count and total bytes of marked items (background, `Esc` stops it) |
| `h` | Show/hide hidden (dot) files in both panes |
| `t` | Color theme: Phosphor green (default), Phosphor amber, Nord, Gruvbox, Dracula, Tokyo Night, Catppuccin Mocha, Solarized Dark, Monokai, Textual Dark. Previews as you move, Enter keeps it (saved in `~/.config/commandertui/settings.json`), Esc goes back |
| `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, `e` unmount a mounted SMB share |
| `c` | Compare: full recursive diff between the two panes' current folders, with size and date of both sides (and which is newer when a file changed) |
| `r` | Refresh both panes |
| `q` | Quit |

While a copy/move runs, the progress window shows two bars (current file
and whole job, by bytes) plus speed in Mb/s and time left. `Esc` asks to
cancel: finished files stay, the file in progress is discarded (never left
half-written), and a move never removes a source it didn't fully copy.

**Places** lists only what is mounted right now: USB drives
(`/run/media`, `/media`, `/mnt` — checked with `ismount`, so an empty
on-demand mount point doesn't show), GVFS/Nautilus network shares, FUSE
mounts inside `$HOME` (e.g. `rclone mount`), and saved bookmarks.

**Network locations (Linux, needs `gio`/GVFS; macOS supports SMB via `smbutil`/`osascript` and `/Volumes`):** in Places press `a`, pick
SMB, SFTP or FTP and type the server's IP or name. For SMB the app lists the
shares on the server and you pick one; SFTP/FTP connect straight in (Linux only). The
location is saved in `~/.config/commandertui/network.json` (protocol, host,
share and user only -- never a password) and reappears in Places, mounting itself
when picked. If the server asks for a login, `gio` asks for it in the
terminal and can remember it in the system keyring, exactly like Nautilus (on macOS, the system login dialog asks and saves it in Keychain).

Every key also has a clickable box on the bottom bar: one row on wide windows
(168+ columns), two rows when narrower (120 columns is the practical minimum).
The active pane has the thick border and the inverted path bar; each path bar
shows the free space of that disk, and the marked count and total size.

## Updates

On every launch, in the background while the start-up animation plays (its
CHECKING UPDATES line shows the result), the app
asks PyPI for the latest version (a plain GET, nothing about you is sent). If
a newer one exists you get a dialog -- **Update now / Later / Skip this
version** -- and the header keeps saying `update X available (press u)`.
It never installs on its own. **Update now** hands the terminal over and runs
the command that matches how it was installed:

| Installed as | Update runs |
|---|---|
| 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) |
| pipx | `pipx upgrade commandertui` |
| pip | `pip install --upgrade commandertui` |
| git checkout | no button: the dialog tells you to `git pull` |

Turn the check off with `commandertui --no-update-check` or by setting
`COMMANDERTUI_NO_UPDATE_CHECK=1`. `COMMANDERTUI_FAKE_LATEST=9.9.9` shows the
dialog without a real release, to see how it looks. The logic lives in one
self-contained file, `commandertui/updater.py`, written to be copied into
other apps.

Inside **Compare**:

| Key | Action |
|---|---|
| `m` | Mirror: make right identical to left |
| `u` | Update: copy new/changed left → right, never deletes |
| `b` | Two-way: propagate each side's exclusive changes; conflicts (changed on both sides) are listed, never guessed |
| `Space` | Mark a row for manual handling |
| `c` | Copy marked rows in whichever direction they diverge |
| `Esc` | Back to the panes |

Every bulk or destructive action shows a plain-language summary (count of
operations, total bytes) and requires `y` to confirm.

## Architecture

```
commandertui/
├── models.py          # FileEntry, DiffEntry, OperationQueue, QueuedOp
├── scanner.py          # os.scandir recursive tree walk
├── diff.py             # pure comparison: left-only / right-only / modified / identical
├── plan.py              # diff -> OperationQueue (mirror / update / two-way)
├── sync.py               # atomic copy/move (streamed, tmp+replace), checksum verify
├── trash.py               # 3-layer safe delete (send2trash / gio trash / manual)
├── executor.py             # runs an OperationQueue against the filesystem
├── bookmarks.py             # detected mounts (USB/SMB/rclone) + saved places
├── netlocations.py           # SMB/SFTP/FTP locations via gio (list shares, mount, save)
├── theme.py                  # phosphor green/amber themes + the stylesheet (Textual theme variables)
├── settings.py               # saved settings (color theme)
├── boot.py                    # startup "modem" reveal animation
├── widgets.py                  # FilePanel: one pane's directory listing
├── modals.py                    # Confirm / Places / Input / Size / Message screens
├── progress_screen.py            # two-bar transfer progress + cancel
├── run_queue.py                   # confirm -> run queue in a worker thread
├── compare_screen.py              # full diff view + bulk/manual sync actions
├── app.py                          # CommanderApp: wires panes + keybindings
└── cli.py                           # argument parsing, entrypoint
```

## Tests

```bash
pip install -r requirements-dev.txt
pytest
black commandertui tests   # formatter, line length set in pyproject.toml
```

Engine (scanner/diff/plan/sync/executor) is fully unit tested against a real
filesystem (`tmp_path`). The UI has smoke tests via Textual's `Pilot`
(boot sequence dismisses, pane switching, mark→copy→confirm end to end).

## Not in scope

No file preview, no deduplication engine, no desktop GUI wrapper — those are
different problems.

## Credits

Designed and tested by Humberto Barchini. The code was written together
with [Claude](https://www.anthropic.com/claude) (Anthropic) through
[Claude Code](https://claude.com/claude-code): **Claude Sonnet 5** built the
first version, **Claude Opus 5.5** did most of v0.2 and the v1.0 packaging.

## License

MIT — see [LICENSE](LICENSE).
