Skip to main content

Two-pane terminal file manager (Midnight Commander style): local + SFTP/SSH/FTP panes, cross-location copy/move, bidirectional sync, in-pane terminal, viewer, editor and plug-ins.

Project description

Meridian Commander

PyPI Python versions License: GPL v3 CI

The meridian is noon — the other end of the clock from midnight.

A two-pane terminal file manager in the spirit of Midnight Commander, written in pure Python. It browses local and networked locations, copies and moves files between the two panes regardless of where each side lives, synchronizes directories so both panes hold the newest version of every file, and ships with a built-in file viewer and editor.

+- local:/home/user ---------------+- sftp://me@server:/srv/www ------+
| Name                Size  Modify | Name                Size  Modify |
| ..                               | ..                               |
| projects/          <DIR>  Jul 20 | assets/            <DIR>  Jul 19 |
|*report.pdf          1.2M  Jul 21 | index.html          4.3K  Jul 18 |
| notes.txt           842   Jul 22 | style.css           1.1K  Jul 18 |
+----------------------------------+----------------------------------+
 F1 Help  F5 Copy  F6 Move  F9 Sync  F10 Quit

Features

  • Two independent panes — browse two locations side by side, Tab between them, and swap them with Ctrl-U.
  • Local and networked locations — each pane can point at the local disk, an SFTP server, an SSH (shell) host, or an FTP server. Press F2 to open a location. The SSH (shell) mode lists and transfers files by running ordinary commands (ls, cat, …) over the SSH channel, so it works even on servers that permit SSH login but have the SFTP subsystem disabled.
  • Copy & move across any pair of panes — local→remote, remote→local, remote→remote and local→local all work through one streaming engine, with a cancellable progress bar (F5 copy, F6 move).
  • Bidirectional directory sync (F9) — compares the two panes and copies the newest version of each file in whichever direction is needed, so both sides end up holding the latest of everything. Nothing is deleted; you get a preview and confirmation before anything is written.
  • File viewer (F3) — scrollable, with search (/, smart case, highlighted matches, n/N next/previous with wrap-around), toggleable line numbers (l) and horizontal scrolling; works on remote files too.
  • File editor (F4) — a real in-place editor (insert/delete, Enter/Backspace line handling, save with Ctrl-S), also with toggleable line numbers.
  • Tag multiple files (Insert/Space, + all, - none) for batch copy/move/delete.
  • Find files (f) — search the pane's tree by substring or glob (remote panes included, cancellable) and get a browsable result list: view or edit a hit right from the list, or press Enter to jump the pane to the containing directory with the cursor on the file.
  • Per-pane hidden-file toggle (.) — show or hide dotfiles independently in each pane.
  • Terminal inside the pane (t) — the pane itself becomes a pseudo-terminal running a shell in the pane's directory, while the other pane keeps working normally. Works for local panes (a real pty) and for SFTP/SSH panes (an interactive shell on the pane's existing SSH connection). Ctrl-] switches to the other pane while the shell keeps running (Tab back to return); F10 or exiting the shell closes it. For full-screen programs (vim, htop) use !, which suspends the UI into a real terminal instead.
  • Mouse support — click to select, double-click to open, wheel to scroll, and right-click for a context menu of actions (view, edit, copy, move, rename, delete, tag, mkdir, terminal).
  • Works even when F-keys are hijacked — every function key has a digit alias (10F1F10) and the common actions have mnemonic letters.
  • Pane plug-ins (p) — put a pane into plug-in mode: pick from discovered plug-ins and it takes over the pane, with access to the opposite pane's contents. Writing one takes a dozen lines (see Plug-ins below); built-ins include remote JSON push and run-remote-script over SSH.
  • In-app configuration (C) — edit config.ini and plug-in files in the built-in editor without leaving the app.
  • No required dependencies for local + FTP use — it runs on the Python standard library. SFTP uses the optional paramiko package.

Install

pip install meridian-commander            # once published to PyPI

# with SFTP/SSH support (remote panes, in-pane remote terminal, SSH plug-ins)
pip install "meridian-commander[ssh]"

# or from a checkout
pip install ".[ssh]"

This installs the meridian-commander command and its short alias meridian.

For development use an editable install from the repo root, so your code changes take effect without reinstalling:

python -m pip install --upgrade pip   # editable installs need pip >= 21.3
pip install -e ".[ssh]"

(Older pip versions answer -e with file 'setup.py' not found; upgrading pip inside the virtualenv fixes it.)

You can also run it straight from the source tree without installing:

python -m meridian_commander

Usage

meridian-commander                 # both panes start in your home directory
meridian-commander /etc /var/log   # left pane in /etc, right pane in /var/log

Connecting to a remote location

Press F2 in the pane you want to change and choose SFTP, SSH (shell) or FTP. You will be asked for host, username, port and credentials:

Meridian reads your ~/.ssh/config, so you can enter a host alias (with its HostName, User, Port, IdentityFile, ProxyJump/ProxyCommand all applied) or a user@host string, and leave username, port, key file and password blank — it authenticates through your SSH agent and default keys (~/.ssh/id_*) just like the ssh command. In other words, if ssh mybox works in your shell, typing mybox here works too.

ProxyJump is native and alias-aware: given

Host A
    HostName a.example.com
    User usera
Host B
    HostName b.internal
    ProxyJump A

connecting to B first connects to A (with A's user, port and keys), opens a tunnel through it, and reaches B — entirely inside the app, no external ssh process. Chains (ProxyJump A,B) and user@host:port hop specs work; jump hops authenticate via your agent/keys (a hop's password cannot be prompted mid-connection).

  • SFTP authenticates with your SSH agent / default keys / a per-host IdentityFile (or an explicit key file or password if you supply one), and browses through the SFTP subsystem.
  • SSH (shell) authenticates the same way but does not use SFTP at all — it drives ls/mkdir/rm/mv over the SSH channel. Use it when a server allows SSH login but has SFTP disabled. File contents are transferred with a fallback chain — cat, then dd, then the raw scp protocol — so viewing and editing work even on restricted appliance shells that answer Command 'cat' not supported (most embedded SSH servers still implement scp). The method that works is remembered for the rest of the session.
  • FTP prefers the modern MLSD listing command and automatically falls back to parsing classic LIST output on older servers that don't support it (which otherwise answer 500 Unknown command). Log in anonymously by leaving the defaults, or supply a username and password.

Once connected, that pane behaves exactly like a local one — navigate, view, edit, and copy/move/sync to and from it.

Key bindings

Key Action Key Action
Tab switch active pane F1 help
/ j/k move cursor F2 open / connect location
PgUp/PgDn page F3 view file
Home/End first / last F4 edit file
Enter / enter dir / view file F5 copy to other pane
Backspace / parent directory F6 move to other pane
Insert / Space tag file F7 make directory
+ / - tag all / untag all F8 delete
Ctrl-U swap panes F9 synchronize panes
Ctrl-R reload panes F10 quit
Ctrl-G go to path Ctrl-T change sort order
. show/hide hidden files t terminal inside this pane
p / F11 plug-in mode (this pane) ! full-screen shell
f find files (browsable results)
C configuration menu Ctrl-] terminal: switch to other pane

F-key aliases (for terminals that swallow function keys): press the digit 10 for F1F10, or the mnemonic letter — ?/1 help, o open/connect, v view, e edit, c copy, m move, d delete, s sync, q quit.

Mouse: click to select and focus a pane, double-click to open a file/directory, scroll wheel to move through the listing, and right-click for a context menu of actions.

In the viewer: / (or F7) searches — smart case (a lowercase pattern is case-insensitive), matches highlighted, n/N next/previous with wrap-around; l toggles line numbers, W toggles wrap, arrows/PgUp/PgDn scroll, Q quits. In the editor: F2 / Ctrl-S / Ctrl-O save, F10 / Ctrl-Q quit, Ctrl-Y / Ctrl-K delete a line, Ctrl-L toggles line numbers. Esc does not quit — only q-style keys and F10 leave the app, so a stray Esc never throws you out.

Running inside VS Code's integrated terminal

VS Code intercepts some control keys before they reach terminal programs: Ctrl-K is a chord prefix (terminal.integrated.allowChords), and keys bound to workbench commands in terminal.integrated.commandsToSkipShell (on some platforms Ctrl-Q) never arrive. Every editor command therefore has a VS Code-safe alias — use F2 to save, F10 to quit, Ctrl-Y to delete a line and you'll never notice the difference. If you prefer the control-key bindings, add this to your VS Code settings.json:

{
  "terminal.integrated.allowChords": false,
  "terminal.integrated.commandsToSkipShell": ["-workbench.action.quit"]
}

Plug-ins

Press p (or F11) to put the active pane into plug-in mode: a menu lists the discovered plug-ins and the chosen one takes over that pane. The plug-in can see the opposite pane — its filesystem (local or remote), its directory and entries — so it can do work on whatever you have open next to it. Esc closes the plug-in and returns the pane to its file listing; Tab still switches panes while a plug-in is open.

Built-in plug-ins:

  • Terminal — the in-pane pseudo-terminal (also on the t key); a shell in the pane's directory, local or over the pane's SSH connection.
  • Find in other pane — recursively search the other pane's directory by glob pattern (works on remote panes too).
  • JSON push — the user enters input in the bottom line; the plug-in logs into a remote server over SSH, delivers the input as JSON to a TCP listener on that server (via an SSH channel, so the listener can stay on loopback), waits for the reply and shows it in the output area.
  • Run remote script — on each input, logs into an SSH server, copies a configured local script into a configured remote directory, runs it with the input as arguments, and shows its output.

Writing a plug-in

Drop a .py file into ~/.config/meridian-commander/plugins/ (or into meridian_commander/plugins/ inside the framework — both are scanned, plus any extra directories listed in the config file). A complete plug-in is:

from meridian_commander.plugin_api import InputOutputPlugin

class Shout(InputOutputPlugin):
    name = "Shout"
    description = "Uppercase whatever you type"
    prompt = "say> "

    def process(self, line):
        return [line.upper()]

InputOutputPlugin provides the classic two-part layout: a scrolling output area on top and an input line at the bottom; process() is called on Enter, and self.print(...) emits output at any time. The plug-in context is at self.ctxctx.other_fs, ctx.other_path, ctx.other_entries(), ctx.refresh_other() give access to the opposite pane. For full control of drawing and keys, subclass PanePlugin instead.

Configuration

Press C for the configuration menu:

  • Edit configuration opens ~/.config/meridian-commander/config.ini in the built-in editor (created with commented defaults on first use). Plug-ins read their settings from [plugin:<name>] sections; [plugins] dirs adds extra plug-in directories.
  • Edit a plug-in file lists every discovered plug-in file (built-in and user) and opens the chosen one in the editor.
  • Open user plug-in folder in this pane jumps the pane to ~/.config/meridian-commander/plugins/ so you can manage plug-ins like any other files.

How synchronization works

F9 builds a plan by walking both panes' directory trees:

  • a file present on only one side is copied to the other;
  • a file present on both sides is compared by modification time, and the newer copy overwrites the older one (times within 2 seconds are treated as equal to avoid needless copies);
  • the copied file is stamped with the source file's modification time, so both sides stay identical in age — a second sync finds nothing to do instead of copying the file back the other way;
  • nothing is ever deleted.

You see the full list of planned copies and the total byte count before confirming, and the operation can be cancelled mid-way.

Architecture

Every location — local, SFTP, SSH shell, FTP — implements one small FileSystem interface (listdir, stat, streaming open_read/open_write, and the mutating operations). Because the interface is uniform, the copy/move engine and the sync engine are written once and work across any pair of backends.

Module Responsibility
filesystems.py FileSystem interface + Local / SFTP / SSH / FTP backends
operations.py streaming copy, recursive copy, move
sync.py bidirectional sync plan + execution
plugin_api.py pane plug-in API (PanePlugin, InputOutputPlugin, context)
plugins/ plug-in discovery + built-in plug-ins
config.py config.ini handling (per-plug-in sections, plug-in dirs)
panel.py one pane's listing, cursor, selection, sorting
viewer.py / editor.py file viewer and editor
dialogs.py prompts, menus, confirmations, progress bars
app.py curses UI, key bindings, orchestration

Utility scripts

scripts/merge.sh bundles a directory tree into a single text file and scripts/split.sh expands it again:

scripts/merge.sh bundle.txt some/dir     # bundle a tree (default: current dir)
scripts/split.sh bundle.txt restored/    # expand it (default: current dir)

The bundle format inlines text files verbatim and base64-encodes binaries (and any text file whose content would collide with the section markers). Permissions, symlinks, empty directories and missing trailing newlines are preserved; every file carries a sha256 that split.sh verifies on expansion. split.sh refuses bundles containing absolute or .. paths and never passes bundle-controlled strings to a shell.

Development

pip install ".[dev]"
pytest

The test suite covers the filesystem-agnostic core (copy, move, sync, panel logic) using the local backend and temporary directories.

License

GNU General Public License v3.0 — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

meridian_commander-1.1.0.tar.gz (96.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

meridian_commander-1.1.0-py3-none-any.whl (84.1 kB view details)

Uploaded Python 3

File details

Details for the file meridian_commander-1.1.0.tar.gz.

File metadata

  • Download URL: meridian_commander-1.1.0.tar.gz
  • Upload date:
  • Size: 96.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for meridian_commander-1.1.0.tar.gz
Algorithm Hash digest
SHA256 af135cbc4f67c9fa7b8367bd20101f69c6490379c35751262824212a5609dfd5
MD5 d6f14b8c713367ca98cfce2944134845
BLAKE2b-256 5331196479f9c14b1739094cb722cca539bd8e921caf2503f950b4451b471eb1

See more details on using hashes here.

Provenance

The following attestation bundles were made for meridian_commander-1.1.0.tar.gz:

Publisher: publish.yml on MartinGallagher-code/meridian_commander

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file meridian_commander-1.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for meridian_commander-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 43dedb35b2bdcbabde8f64829d43c6541072f9dedd1ae4a24a999aa6bbfbbac5
MD5 a6edcdbc8ef0916aefbddf00910e1b63
BLAKE2b-256 3704ace8cbb9fb8b1b2bb5df8cd36aa7d9d71ac62149cf605ea78d9a8faf911d

See more details on using hashes here.

Provenance

The following attestation bundles were made for meridian_commander-1.1.0-py3-none-any.whl:

Publisher: publish.yml on MartinGallagher-code/meridian_commander

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page