Skip to content

Repository files navigation

xc

A two-panel console file manager inspired by Midnight Commander, written in Python.

xc

Intro

The Python version of xc is a single self-contained script (xc.py) that runs via uv. This means:

  • Zero setup -- no virtualenv, no pip install, no requirements.txt. Just run uvx xcfm.
  • Inline dependencies -- the script header declares its own dependencies (boto3, google-cloud-storage, oci, google-api-python-client), and uv resolves and caches them automatically on the first run.
  • Reproducible -- uv pins the Python version (>=3.11) and handles isolation, so the script works the same way on any machine.
  • Single file to deploy -- copy xc.py to a server, a dotfiles repo, or a USB stick. There is nothing else to carry.

Installing uv

# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Homebrew
brew install uv
# Windows
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

After installing, run the file manager with:

uvx xcfm

Or download the script and make it executable as "xc":

curl -fsSL https://raw.githubusercontent.com/begoon/xc/main/install.sh | sh

Or download the script directly:

curl -LO https://raw.githubusercontent.com/begoon/xc/main/xc.py
chmod +x xc.py
uv run xc.py

Or directly from a cloned repo:

uv run xc.py

The script has a shebang line, so you can rename it, make it executable, and put it on your PATH:

cp xc.py ~/.local/bin/xc
chmod +x ~/.local/bin/xc
xc

Command line

xc [folder1 [folder2]]
ArgumentMeaning
folder1Directory to open in the active panel
folder2Directory to open in the other (inactive) panel
-uSelf-update from GitHub and exit

Paths may be relative and may use ~. With no arguments, the active panel opens in the current working directory and the inactive panel restores the path from the previous session.

Development

The pyproject.toml in the repository is only used for local development tooling (e.g. black formatter settings). It is not needed to run xc -- xc.py is fully self-contained with its own inline dependency declarations.

Self-update

To update xc to the latest version from GitHub:

xc -u

This fetches the latest xc.py from the repository, compares versions, and replaces the current binary if a newer version is available. The previous version is saved as xc.prev next to the executable.

User manual

Dual-panel concept

xc shows two file panels side by side. One panel is active (highlighted border), the other is inactive. You navigate files in the active panel and use the inactive panel as a target for file operations like copy and move. Press Tab to switch the active panel. Press h / l to activate the left / right panel directly.

On startup the active panel opens in the current working directory (or folder1 if given on the command line). The inactive panel restores the path from the previous session (or opens folder2).

Navigation

KeyAction
Up / Ctrl-PMove cursor up
Down / jMove cursor down
EnterEnter directory or open VFS
BackspaceGo to parent directory (or exit VFS)
Left / RightPage up / page down
PgUp / PgDnPage up / page down
Home / ^Jump to first file
End / GJump to last file
Ctrl-D / Ctrl-UHalf-page down / up
Ctrl-LReload current directory
TabSwitch active panel
h / lActivate left / right panel
qQuit

File operations (x)

Press x to open the command menu:

KeyAction
cCopy -- copy file or tagged files from active to inactive panel
mMove -- move (rename) file or tagged files
dDelete -- delete file or tagged files
kMkdir -- create a new directory
tTouch -- create an empty file
pChmod -- change file permissions
rRename -- rename the selected file
gChdir -- type a path to navigate to

Copy and move operations use the inactive panel's current path as the default destination. A prompt lets you edit the destination before confirming.

Overwrite confirmation

If a copy, move, or rename target already exists, a confirmation dialog appears showing the size and modification time of both the new and the existing file (directories are shown as <DIR>):

ButtonAction
OverwriteReplace the existing file
SkipLeave the existing file and continue with the remaining ones
AllOverwrite this and all further conflicts without asking
CancelStop the remaining operation (Esc does the same)

Buttons respond to their first letter (o, s, a, c), Tab/arrow keys, and Enter. Rename shows only Overwrite / Cancel.

On move, a source file is deleted only after it has been fully copied -- skipped, cancelled, or failed files always keep their originals. Existence checks work on remote VFS too (the target directory listing is fetched once per operation).

Tagging and group operations

Press Space on a file to tag it (marked with +). Tagged files are used as the source for copy, move, delete, and chmod. If nothing is tagged, the operation applies to the file under the cursor.

KeyAction
SpaceToggle tag on current file and move down
+Tag all files in current directory
_Untag all
iCalculate sizes of tagged (or selected) directories

Bookmarks (b)

Press b to open the bookmark menu for quick jumps to common directories (home, desktop, downloads, etc.).

Remotes (r)

Press r to open the remote menu. This scans ~/.xc/remotes/ for VFS config files (.s3, .gcs, .oci, .gdrive, .ssh) and presents them as a selector. Choosing a remote opens it on the active panel, just like pressing Enter on a VFS config file.

This lets you keep all your remote connections in one place and access them from any directory without navigating to where the config files live.

Example setup:

~/.xc/remotes/
production.s3
analytics.gcs
storage.oci
shared-drive.gdrive
webserver.ssh

Editor (e) and view (v)

Press e to open a file in an editor, or v to view it. These menus launch external commands with the current file path substituted via macros (see below). On remote VFS (SSH, S3, GCS, OCI, GDrive), the file is automatically downloaded to a temp location, opened locally, and uploaded back if modified.

Running shell commands

There are two command-line modes:

KeyModeBehavior
;DirectRun a command interactively in the terminal
:PipedRun a command; output is piped through less

Both modes support macro expansion. The command runs in the active panel's current directory. If the command exits with a non-zero code, the error is shown in the bottom line.

Alternate screen and command output

xc runs on the terminal's alternate screen -- the panels never mix with your shell's scroll buffer. When you run a shell command (; or :), xc temporarily switches back to the main screen so the command's output is preserved in the normal scroll buffer.

To review previous command output without running anything:

KeyAction
EscEscSwitch to main screen
Ctrl-OSwitch to main screen
Esc or Ctrl-OReturn to panels (main screen)

Once on the main screen you can scroll through your terminal's history as usual. Press Esc or Ctrl-O to return to the file panels.

Search (/)

Press / to start an incremental search. Type characters to filter -- the cursor jumps to the first matching file. Press Enter to accept or Esc to cancel.

Search (s / S)

Press s to search by file name pattern, or S to search file contents (grep).

  • s -- File search. A search prompt appears. Enter a glob pattern (e.g. *.py) to find matching files recursively.
  • S -- Grep search. A two-step prompt:
    1. File pattern -- search prompt appears. Enter a glob pattern to filter by filename (default *.*).
    2. Search string -- search *.py grep for prompt appears. Enter the text to find in file contents.

The file pattern uses Unix shell-style wildcards (matched against the filename only, not the path):

PatternMeaningExample
*Matches everything*.py -- all Python
?Matches any single character?.txt -- a.txt
[seq]Matches any character in seq[abc].py
[!seq]Matches any character not in seq[!.]cfg

Patterns do not support ** (recursive globs) or path separators -- they match the base filename only.

Search is implemented in pure Python -- no external tools required. Binary files are automatically skipped. Hidden directories (starting with .) are excluded. A spinner shows progress during search; press ESC to cancel.

Results are displayed as a virtual filesystem tree (GREP) on the current panel. Navigate the results normally -- directories expand, .. exits back to the real filesystem.

Search works only on local filesystems.

Processes (p)

Press p to open a modal showing all running processes with PID, user, full command line (middle-truncated with ... to fit), and any listening TCP/UDP ports.

The layout is:

  • Filter input at the top — space-separated words. All words must appear in the command line, ports, pid, or user. A word prefixed with - excludes matches containing that word.
  • Process list with the filtered results.
  • Full command line of the selected process, below the list.
  • Environment variables of the selected process at the bottom (read from /proc/<pid>/environ on Linux, or ps eww on macOS).
KeyAction
TabCycle focus: filter → list → env
Up / DownNavigate one entry up / down
PgUp / PgDn / Left / RightNavigate one page up / down
Home / EndJump to first / last entry
letters / BackspaceEdit the filter
k (list focus) / Ctrl-KSend SIGKILL (kill -9) with y/N confirmation
Ctrl-RRefresh the process list
EscClose the modal

Listening ports are gathered via lsof -iTCP -sTCP:LISTEN -iUDP. On machines where lsof is restricted or not installed, the ports column will simply be empty.

PATH viewer (o)

Press o to open a modal listing every directory in $PATH with the number of executable files it contains. Missing directories are flagged as missing (dimmed and coloured as errors).

Press Enter on an existing entry to open a second modal showing a scrollable list of the executables in that directory.

KeyAction
Up / DownNavigate one entry up / down
PgUp / PgDn / Left / RightNavigate one page up / down
Home / EndJump to first / last entry
EnterShow executables in selected path
Ctrl-RRescan $PATH
EscClose (inner modal first)

Environment variables (k)

Press k to open a modal listing every variable from the current process environment, sorted by key. Each row is KEY=value; long values are middle-truncated with ... in the list. The full value of the selected variable is displayed below the list.

KeyAction
Up / DownNavigate one entry up / down
PgUp / PgDn / Left / RightNavigate one page up / down
Home / EndJump to first / last entry
Ctrl-RRefresh the env list
EscClose the modal

Command history (Esch)

In command-line mode (; or :), press Esc then h to open a history selector showing previously executed commands. Use Up / Down to browse, Enter to accept, Esc to cancel. History is persisted across sessions (up to 100 entries).

Customizing menus

xc is a single Python script. Menus and keymaps are defined at the bottom of the file as plain data -- just edit them to add your own editors, bookmarks, or commands:

app.add_menu("editor", [
MenuItem("v", "vi", lambda: app.action_run("vi %F")),
MenuItem("c", "code", lambda: app.action_run("code %F")),
])

There is no config file on purpose. The script is the config.

Virtual filesystems

Entering certain files opens them as virtual directories:

ExtensionVFSDescription
.tar, .tar.gz, .tgz, .tar.bz2TARBrowse tar archives
.zipZIPBrowse zip archives
.gz, .bz2, .xz, .lzma-View compressed single files
.s3S3Browse Amazon S3 buckets
.gcsGCSBrowse Google Cloud Storage buckets
.ociOCIBrowse Oracle Cloud Object Storage
.gdriveGDriveBrowse Google Drive folders
.sshSSHBrowse remote servers over SSH

VFS config files (.s3, .gcs, .oci, .gdrive, .ssh) are simple key=value text files. The path header shows the VFS type, e.g. ~/servers/prod.ssh (SSH).

S3 example (production.s3):

type=s3
bucket=my-data-bucket
AWS_PROFILE=production
AWS_REGION=eu-west-1

AWS_PROFILE selects a named profile from ~/.aws/credentials. You can also specify AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY inline, but using a profile is recommended. If all credentials are omitted, the default AWS credential chain is used.

GCS example (analytics.gcs):

type=gcs
bucket=my-analytics-bucket
key=service-account.json

The key path can be relative (resolved from the active panel's current directory), absolute, or use ~, $HOME, or $(HOME) to refer to the home directory. If omitted, application default credentials are used.

OCI example (storage.oci):

type=oci
bucket=my-bucket
OCI_BUCKET_NAMESPACE=mynamespace
OCI_USER=ocid1.user.oc1..aaa...
OCI_FINGERPRINT=aa:bb:cc:...
OCI_TENANCY=ocid1.tenancy.oc1..aaa...
OCI_REGION=uk-london-1
OCI_KEY_FILE=my-key.pem

OCI_KEY_FILE is a path to a PEM private key file. Alternatively, use OCI_KEY_BASE64 to embed the key inline as base64. If neither is provided, the default OCI config (~/.oci/config) is used.

Google Drive example (shared.gdrive):

type=gdrive
folder=1Z_NJ0-LAPzaO7eursL92DWPmFKQT3lpK
key=service-account.json

The folder is the ID from the Google Drive folder URL. The key path points to a service account JSON credentials file (relative paths are resolved from the config file's directory). If omitted, application default credentials are used. Shared drives and folders shared from other accounts are supported.

SSH example (prod.ssh):

kind=ssh
host=prod-server
user=deploy
identity=~/.ssh/id_ed25519
port=22

Only host is required. All other fields are optional -- SSH will pick them up from ~/.ssh/config. The host value can be an SSH config alias. The identity path supports ~, $HOME, and $(HOME) prefixes.

Remote file editing

The %F macro works transparently on remote VFS (SSH, S3, GCS, OCI, GDrive). When you run a command like vi %F on a remote file, xc automatically:

  1. Downloads the file to a local temp location
  2. Runs the command against the local copy
  3. If the command exits successfully and the file was modified, uploads it back

This means editors, viewers, and any shell command work on remote files the same way as local ones.

Macros

Editor, view, and shell command modes (; and :) all support macro expansion. Macros are prefixed with % and let you refer to the current file, directory, or tagged selection in your commands.

MacroDescription
%fCurrent file name
%FCurrent file full path
%xCurrent file name without extension
%XCurrent file full path without extension
%dCurrent directory name
%DCurrent directory full path
%mTagged file names (space-separated, shell-quoted)
%MTagged file full paths (space-separated, shell-quoted)
%&Run command in the background (no terminal output)

Quoting

All macros are automatically shell-quoted. To disable quoting, prefix the macro letter with ~:

MacroDescription
%~fFile name, unquoted
%~FFile path, unquoted
%~mTagged file names, unquoted
%~MTagged file paths, unquoted

Examples

vi %F → vi '/home/user/hello world.txt'
less %F → less '/home/user/notes.md'
tar czf %x.tar.gz %~f → tar czf 'mydir.tar.gz' mydir
echo %m → echo 'file1.txt' 'file2.txt'
cp %M /tmp %& → cp '/home/user/a.txt' '/home/user/b.txt' /tmp (background)

About

Hackable two-panel console file manager powered by Python and uv

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages