Mullion FileMan: A Dual-Pane File Manager and a Set of Folder Tools
Every few months I end up with two copies of the same photo library on two different drives, and I don’t know which files actually differ between them. File Explorer won’t answer that question, and I usually need the answer before I delete one of the copies.
I wrote Mullion FileMan to solve
this problem. It’s a C++17 project with two parts: about twenty standalone
folder_* command line tools that do the actual work, and a Norton
Commander-style dual-pane terminal UI built on top of them. There’s also an
MCP server, so an AI agent can call the same tools. In this post, I will go
through building the project, the tools I use the most, the dual-pane UI, and
the MCP server.
Build it¶
There’s no installer. The binaries are placed in the project root and run from there. On Windows, for the CLI tools:
.\build.ps1
On macOS or Linux:
./build.sh
Either script auto-detects your compiler. Add -Test (or --test on Unix) to
run the unit tests as part of the build. The only hard requirement is a C++17
compiler with a working std::filesystem. The CLI tools have no external
dependencies.
The TUI needs FTXUI, which CMake fetches for you, so it goes through CMake on any platform:
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
On Windows there’s a batch script that builds the TUI with the CLI tools bundled alongside it:
.\build-mullion-fileman.bat
Compare two folders¶
folder_parity is the tool I wrote first, and I still use it the most. It
walks two directory trees and reports what doesn’t match:
folder_parity ./backup ./live -o report.csv
Every row in the CSV is a file that differs, tagged MISSING_IN_1,
MISSING_IN_2, or DIFFERENT, with sizes and modification dates. Files that
match aren’t included in the report. The CSV is written as UTF-8 with a BOM
so Excel opens non-ASCII filenames correctly.
By default it compares files by size. That’s fast, but it can miss an edit
that keeps the file size the same. When I need to be sure, I pass --hash so
it reads the actual bytes:
folder_parity "J:\Photos" "I:\Photos" --hash --jobs -1 --ignore-system -o report.csv
--jobs -1 uses every core, and --ignore-system skips .DS_Store,
Thumbs.db and similar files that would otherwise fill the report. Hashing a
photo library is I/O-bound, but on an SSD the parallel version is noticeably
faster.
Once you’ve read the report, the same tool can reconcile the two trees:
folder_parity ./backup ./live --sync --sync-to 1 # one-way mirror
folder_parity ./a ./b --sync --prefer newer # two-way, newest wins
--prefer takes 1, 2, newer, or larger, and decides which side wins
when both have a file and the contents differ. The exit codes are scriptable:
0 means the trees are identical, 2 means differences were found, and 1
is an error.
Sort files into a tree¶
The other tool I use constantly is folder_sort. It takes a flat folder of
files and moves them into a directory hierarchy. By default it only shows a
preview; nothing changes until you add --apply:
folder_sort ~/Downloads ~/Sorted --by type # preview
folder_sort ~/Downloads ~/Sorted --by type --apply # do it
--by picks one of five built-in layouts:
| Layout | Result |
|---|---|
date |
{year}/{month}/{name} |
month |
{year}-{month}/{name} |
type |
{category}/{name} — images, video, audio, documents, archives, code, other |
ext |
{ext}/{name} |
letter |
{firstletter}/{name} |
If none of those is the shape you want, you can write the path yourself with
--template. It takes {name}, {stem}, {ext}, {category},
{firstletter}, {year}, {month}, {day}, {date:FMT}, {size}, and
{idx} / {idx:W} for zero-padded numbering:
folder_sort ./inbox ./archive --template "{year}/{category}/{stem}{ext}" --move --apply
It copies by default; --move relocates the files instead. Name clashes get
a " (2)" suffix unless you pass --skip. Every run writes a CSV index of
what went where (sort-index.csv by default, -o to change it). If you ran
a big --move with the wrong template, this index is what lets you undo it.
One thing to know: folder_sort reads metadata from the filesystem only —
mtime, extension, size. That keeps it dependency-free, but it means --by
date uses the modified date, not the capture date. For photos those two
often differ by years, so use folder_photos instead; it reads EXIF.
The rest of the tools¶
The rest of the tools follow the same rules. They create subfolders on their
own, they use multiple cores where that helps, and anything that can lose
data shows you what it’s about to do and waits for --apply. Each tool is a
single executable with nothing shared behind it, so you can copy one binary
to a machine and run it.
Files and folders
| Tool | What it does |
|---|---|
folder_parity |
Compare two trees, report differences, optionally sync |
folder_clean |
Remove OS junk (.DS_Store, Thumbs.db, and friends) |
folder_copy / folder_move / folder_delete |
Multi-threaded recursive file operations |
folder_find |
Search by name, content, size, or age |
folder_view |
View a file as text or hex |
Organizing
| Tool | What it does |
|---|---|
folder_sort |
File a flat dump into a templated tree |
folder_rename |
Bulk rename: literal, regex, case, prepend/append, {n:03} numbering |
folder_attr |
Timestamps, Windows attributes, POSIX chmod/owner/group |
Maintenance
| Tool | What it does |
|---|---|
folder_dupes |
Find duplicates by size then hash; report reclaimable space |
folder_du |
Disk usage with bars, sorted largest-first |
folder_checksum |
Write a checksum manifest, and --verify it later |
folder_shred |
Overwrite before deleting (--passes N) |
folder_scan |
Grep for leaked secrets — AWS keys, private keys, JWTs, tokens |
Media
| Tool | What it does |
|---|---|
folder_photos |
Sort images by EXIF capture date into a templated tree |
folder_convert |
Batch transcode through ffmpeg, codec inferred from extension |
folder_archive |
Create, extract, or list tar archives |
folder_watch |
Watch a directory and run a command when matching files change |
System
| Tool | What it does |
|---|---|
folder_sysmon |
CPU, memory and GPU usage; --watch, --json, --stream |
folder_proc |
List, filter, kill and re-prioritize processes |
bookmark_sync |
Bidirectional bookmark sync across Chrome, Firefox, and Safari |
Some of these combine well. folder_watch takes a --command with {}
standing in for the changed file, so you can point it at a folder and turn
any of the other tools into a pipeline:
folder_watch ./inbox --pattern "*.png" --command "folder_convert {} --to jpg --apply"
folder_scan is also useful in a pre-commit hook. It reports matches as
file:line with the secret redacted.
The dual-pane UI¶
mullion-fileman is the interactive frontend. It takes the two directories
to open as arguments:
mullion-fileman ./backup ./live

If you’ve used Norton Commander or Midnight Commander, you already know how
it works: two panes side by side, Tab to switch, Enter to descend, Backspace
to go up. Vim-style j/k also work. Each pane shows name, date and size,
with a status line carrying the path, the item count and the current sort.
The features I use the most:
- Tagging. Space tags the current row,
+and-tag by glob,*inverts the selection. Tagged rows turn yellow with an asterisk, and copy/move/delete act on everything tagged. - Quick filter.
/filters the pane as you type;Escclears it,Enterkeeps the filtered view. - F-keys. F3 views a file as text or hex, F5–F8 are copy, move, mkdir and delete. F8 shows a dry run and asks before anything is removed.
topens the tree view,sthe sort menu,xthe Tools menu that drives the bulk operations, and:runs a shell command in the active pane’s directory.
There’s a menu bar and full mouse support if you’d rather click:
double-click opens a folder, right-click opens a context menu. m opens a
system monitor with live CPU, memory and GPU use. I added it as a debugging
aid and kept it because it’s useful.
The TUI can also be built with a local AI agent (a) that inspects folders
read-only, and voice input (V) for talking to it. Both are opt-in at build
time — -DBUILD_MULLION_FILEMAN_AI=ON, plus -DMULLION_FILEMAN_AI_CUDA=ON
if you want it on the GPU. Both are disabled by default so the normal build
stays small.
Let an agent drive it¶
There’s also an MCP server. I didn’t expect to use it much, but I do. It’s a
thin Python wrapper that shells out to the same folder_* binaries over
stdio, so the C++ side stays dependency-free:
cd mcp
pip install -r requirements.txt
python server.py
It exposes eight tools — compare, sync, find, view, clean, copy,
move, delete — and finds the executables through $FOLDER_TOOLS_DIR,
falling back to the repository root. For Claude Desktop, add an entry to
claude_desktop_config.json:
{
"mcpServers": {
"folder-tools": {
"command": "python",
"args": ["path/to/server.py"],
"env": { "FOLDER_TOOLS_DIR": "path/to/repo" }
}
}
}
After that, you can ask the agent to compare J:\Photos and I:\Photos by
hash, or to find *.log files over 1 MB under D:\logs. The agent gets the
same dry-run defaults as the CLI.
One warning¶
folder_parity --sync never deletes anything. It only adds files and
overwrites them. That’s deliberate: a sync can’t destroy a file you forgot
about. But it also means “sync” is not “mirror” — files that exist only on
the destination stay there. Read the CSV before you sync anything you care
about.
Links¶
- Repo: github.com/sina5/mullion-fileman
- Docs: sinafathi.com/mullion-fileman
- Building · The TUI · MCP server
- Tool reference: folder_parity · organize · maintenance · media · system
It runs on Windows, macOS and Linux. Issues and PRs welcome.