Sina's Blog

CS and AI

7 min readProgrammingAI

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

The dual-pane UI, with the menu bar on top and the F-key row along the bottom.

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; Esc clears it, Enter keeps 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.
  • t opens the tree view, s the sort menu, x the 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.

It runs on Windows, macOS and Linux. Issues and PRs welcome.