A terminal TUI for browsing, diffing, and syncing files with remote hosts — think PHPStorm's "Browse Remote Host" and "Sync with Deployed To", but in your terminal.
Supports SFTP/SSH, FTP, and FTPS targets. Runs on Linux and macOS.
Warning
Alpha: drift is in an early public stage. Expect rough edges, incomplete polish, and breaking changes between releases.
- Side-by-side local/remote file browser with multi-select (Space) and recursive directory marking
- Safe local/remote text preview in the opposite pane (
p), with line numbers and wrapping - Project-wide fuzzy file finder (
f) for marking files - Unified diff view with the file list on the left for local vs. remote files, unchanged
stretches folded away behind
@@hunk headers - Per-file sync direction control: upload ↑, download ↓, delete local ✗, delete remote ✗, or skip —
- Bulk sync direction toggle (A key cycles all files at once)
- Auto pre-selection of sync direction based on file modification time
- Sync current file (s) or all marked files (S) in one keystroke
- Esc hides a running connect or sync;
q/Ctrl+Ccancel it. Files already transferred stay; the rest is skipped - Per-host path mappings (like PHPStorm's Deployment Mappings tab)
- Host manager: create, edit, delete, and test connections
- Nothing is written into your project: global hosts in
~/.config/drift/config.toml, per-project hosts and mappings in~/.config/drift/projects/<slug>.toml - Skips
.git,node_modules,.idea, and other irrelevant directories automatically
git clone https://github.com/WariKoda/drift.git
cd drift
make installThis builds the binary and installs it to ~/.local/bin/drift.
go install github.com/WariKoda/drift@latestmake update# Start in the current directory, last project, or dashboard (see below)
drift
# Open the project dashboard explicitly
drift dash
# Open a registered project by name or slug
drift open kunde-a
# Manage projects
drift projects list
drift projects add "KUNDE A" ~/work/kunde-a
drift projects edit kunde-a --name "KUNDE A GmbH" --path ~/work/kunde-a
drift projects archive kunde-a
drift projects remove kunde-a
# Show version
drift versionNavigate to any file or directory, press Space to mark it, then s to open the sync target picker.
drift can keep a registry of your projects (one per customer, say) and show them in a
dashboard on startup. From the dashboard you select a project and drift re-roots into it:
it loads that project's config and opens the file browser in its directory — the same as
cd <path> && drift, but without leaving drift.
The dashboard appears automatically when you run drift outside any registered
project and nothing has been opened yet, as long as at least one
project is registered. If you have opened a project before, drift restores that
one instead (its path must still exist). Inside a project directory, drift
opens the browser as before. --dashboard (or drift dash) always opens the
list; --no-dashboard stays in the current directory.
From the file browser, P opens a filterable switcher and leaves the current
session alone. Esc goes back. Enter on another project re-roots. m (with an
empty filter) opens the full dashboard to add, edit, archive, or remove entries.
drift open accepts a display name or slug: exact slug, then exact name
(case-insensitive), then a unique prefix or substring of either.
~/.config/drift/projects.toml holds a slug, display name, local path and timestamps per
project. Its hosts and mappings live next to it in ~/.config/drift/projects/<slug>.toml.
The registry is also how drift knows which project a directory belongs to: the registered project whose path is the directory or a parent of it, longest match first. Registering is therefore what gives a directory hosts of its own.
drift offers to do it for you. Start it in a repository that no project covers and it asks,
suggesting the repository root rather than whatever subdirectory you were in — press y to
register, any other key to skip. Otherwise: drift projects add ., or n on the dashboard,
which prefills the repository root too.
- Run
driftin your project directory - Mark one or more files/directories with Space
- Press s and choose a host
- Review diffs and suggested sync directions
- Sync the current file with s or all files with S
| Key | Action |
|---|---|
j / k or ↑ / ↓ |
Navigate |
Enter |
Open project (re-root drift into it) |
1–9 |
Jump to / open the n-th project |
n |
New project |
e |
Edit project |
d |
Remove project (with confirmation) |
a |
Archive / unarchive project |
. |
Show / hide archived projects |
| Click / wheel | Move the cursor; double click opens |
Esc |
Back to the browser when opened from P then m; otherwise quit |
q |
Quit drift |
| Key | Action |
|---|---|
| type | Filter by name, slug, or path |
↑ / ↓ or Ctrl+n / Ctrl+p |
Navigate |
Enter |
Open the selected project |
m |
Open the dashboard to manage projects (empty filter only) |
Esc |
Back to the browser (session kept) |
| Key | Action |
|---|---|
j / k or ↑ / ↓ |
Navigate in the active pane |
h / ←, l / → / Enter |
Collapse / open directory |
g / G |
Jump to top / bottom |
Tab |
Switch local / remote pane |
p |
Toggle a text preview in the opposite pane |
c |
Copy the loaded preview content to the clipboard |
PgUp / PgDn, Home / End |
Scroll / jump within an active preview |
@ |
Choose or change the remote host |
Space |
Mark / unmark file or directory in the active pane |
V / * |
Mark the current level / invert the active pane's selection |
f |
Fuzzy-find files across the project and mark them |
s |
Sync marked local and remote files |
r / / / ? |
Refresh active pane / filter / help |
H |
Open host manager |
P |
Switch project (filterable picker; m opens the dashboard) |
Esc |
Clear filter and selections |
q / Ctrl+C |
Quit (cancels a running connect or sync first) |
While a connect or sync overlay is up, Esc hides it (the work keeps going, with [q] cancel on the status line) and q / Ctrl+C abort it. Already-transferred files are kept.
| Key | Action |
|---|---|
j / k or ↑ / ↓ |
Scroll diff content by line |
PgUp / PgDn |
Scroll diff content by page |
Ctrl+u / Ctrl+d |
Scroll diff content by half page |
Home / g, End / G |
Jump to start / end of diff content |
[ / ] |
Jump to the previous / next hunk |
Enter / l |
Expand or collapse the first fold in view |
h |
Collapse the fold around the top of the viewport |
c |
Expand or collapse every fold in the file |
Tab / Shift+Tab |
Select next / previous file |
Space |
Cycle sync direction for current file |
A |
Cycle sync direction for all files |
s |
Sync current file |
S |
Sync all files |
u / d |
Quick upload / download current file |
e |
Toggle the last bulk-sync error list (when errors occurred) |
r |
Refresh diffs |
q / Esc |
Back to browser |
| Key | Action |
|---|---|
n |
New host |
e / Enter |
Edit host |
d |
Delete host |
t |
Test connection |
q / Esc |
Back |
The mouse works in the file browser, the diff view and the host manager.
| Action | Effect |
|---|---|
| Wheel | Scroll the pane under the pointer |
| Click | Move the cursor there, and focus that pane |
| Click on a pane label | Focus that pane |
| Click on a fold marker | Expand or collapse that fold (diff view) |
| Double click | Expand a directory (local), open one (remote), cycle a file's sync direction (diff view), edit a host (host manager) |
| Drag while previewing | Select preview text with the terminal; mouse reporting resumes when the preview closes |
Shift+Click |
Select text with the terminal's own selection |
Mouse reporting pauses while a file preview is open, so preview text can be
selected without holding Shift. It resumes when the preview closes. Outside
the preview, it can be turned off entirely:
drift --no-mouse # this run only
DRIFT_NO_MOUSE=1 drift # via environment# ~/.config/drift/config.toml — permanently
[ui]
mouse = falseThe flag beats the environment variable, which beats the config file. Mouse support is on unless one of them turns it off.
Drift uses the terminal palette by default and auto-detects Omarchy themes from
$XDG_STATE_HOME/omarchy/current/theme/colors.toml (normally
~/.local/state/omarchy/current/theme/colors.toml). The legacy location under
$XDG_CONFIG_HOME is supported as a fallback. You can override this with:
DRIFT_THEME=auto|ansi|omarchy|default
DRIFT_THEME_FILE=/path/to/colors.tomlauto loads Omarchy colors first and falls back to ANSI terminal colors.
default uses Drift's built-in fallback palette.
Logging is off by default. When enabled, drift writes diagnostics (connection lifecycle and every connect/sync/diff error, with full paths) to a file — never to the terminal, which the TUI owns. Enable it per run with a flag or environment variable; the flag wins:
drift --debug # debug level → ~/.config/drift/drift.log
DRIFT_DEBUG=1 drift # same, via environment
drift --log /tmp/drift.log # info level → explicit path
DRIFT_LOG=/tmp/drift.log drift--debug (or DRIFT_DEBUG=1) raises the level to debug, adding a line per
synced file; without it the log stays at info level. The log file is appended to,
not rotated. This complements the in-app [e] error list in the diff view, which
only holds the most recent bulk sync.
[defaults]
user = "deploy"
[[hosts]]
name = "prod"
hostname = "example.com"
port = 22
user = "deploy"
root_path = "/var/www/html"
protocol = "sftp"
[hosts.auth]
type = "keyfile"
key_file = "~/.ssh/id_ed25519"drift writes this file; the slug comes from the registry. Nothing is stored in the project directory itself.
[defaults]
user = "deploy"
[[hosts]]
name = "staging"
hostname = "shopdev.example.com"
port = 21
user = "webuser"
root_path = "/var/www"
protocol = "ftp"
# For ftps with a self-signed / mismatched certificate (skips TLS verification):
# insecure_tls = true
[hosts.auth]
type = "password"
password = "$DEPLOY_PASSWORD"
[[hosts.mappings]]
local = "plugins/plugin1"
remote = "html/custom/plugins/plugin1"
[[mappings]]
local = "src"
remote = "html"The file holds credentials verbatim, so it is mode 600 in a 700 directory. Use
$ENV_VAR for a password or passphrase if you would rather keep the secret in your shell
environment or a password manager; drift expands it at connect time.
local paths are relative to the project root. remote paths are relative to the host's root_path.
When effective mappings are configured, only files that fall under a mapping rule can be synced. Files outside all mappings are excluded. Without mappings, all files sync relative to root_path.
| Type | Fields |
|---|---|
keyfile |
key_file, passphrase (optional) |
password |
password (supports $ENV_VAR) |
agent |
none — uses SSH agent |
For a project host, all of these live in ~/.config/drift/access.toml rather
than in the project. For a global host they stay in the global config.
drift keeps no file in your working tree. No .drift/ directory, no dotfile, nothing to
add to .gitignore and nothing to commit by accident:
| File | Holds |
|---|---|
~/.config/drift/config.toml |
global hosts, [ui] |
~/.config/drift/projects.toml |
the registry: slug, name, path, timestamps |
~/.config/drift/projects/<slug>.toml |
one project's hosts and mappings, mode 600 |
The trade-off is deliberate: mappings do not travel with a clone. Every developer enters them once per machine, and when someone moves a directory in the repo there is no commit that fixes the mapping for everyone.
Those versions kept a .drift/config.toml in the project, with credentials either in it or
in ~/.config/drift/secrets.toml. 0.1.7-alpha is the release that moves all of it into
the project store, and it is the only one that can: this version no longer reads those
files at all.
So if you are upgrading from 0.1.6-alpha or earlier, install 0.1.7-alpha first and start it once in each project — it migrates and reports what it moved — then upgrade to the current version. Skipping it leaves your hosts in files nothing reads, and drift will look like it has no hosts for those projects.
If such a config was ever committed, the password in it is in your repository's history, where deleting the file later does not reach it. Rotate it.
internal/
config/ config types, loader, writer
project/ project registry model + store (projects.toml)
diff/ diff engine, result types, renderer
ftp/ FTP/FTPS client (jlaffaye/ftp)
fs/ local file walker, directory reader
log/ optional file-based diagnostics
pathmap/ local ↔ remote path resolution with mapping rules
remote/ protocol-agnostic Client interface and connection factory
sftp/ SFTP client
ssh/ SSH auth and known_hosts verification
styles/ shared palettes and lipgloss styles
sync/ sync plan types and direction policy
tui/
app.go root Bubble Tea model, screen routing
browser/ file browser screen
dashboard/ project dashboard screen
projectform/ project create/edit form
projectselector/ project switcher modal
diffview/ diff + sync screen
hostform/ host create/edit form (incl. mapping manager)
hostmanager/ host list screen
hostselector/sync target picker
statusbar/ reusable one-line status renderer
textfield/ shared single-line text input widget
styles.go TUI-facing style aliases
go test ./...
go vet ./...
go build ./...make installmake release-build VERSION=vX.Y.ZIssues and pull requests are welcome.
For development notes and contribution workflow, see CONTRIBUTING.md.
MIT — see LICENSE.
