Help us shape the future of Go! Take the 2026 Go Developer Survey

drift

command module
v0.1.8-alpha Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 12, 2026 License: MIT Imports: 1 Imported by: 0

README

drift

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.

drift screenshot

[!WARNING] Alpha: drift is in an early public stage. Expect rough edges, incomplete polish, and breaking changes between releases.


Features

  • 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+C cancel it. Files already transferred stay; the rest is skipped
  • Per-host path mappings (like PHPStorm's Deployment Mappings tab)
  • Host manager: create, duplicate, edit, delete, and test connections
  • Interactive FTPS certificate verification with session-only or persistent, fingerprint-bound trust
  • 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

Installation

From source (requires Go 1.25+)
git clone https://github.com/WariKoda/drift.git
cd drift
make install
Directly with Go
go install github.com/WariKoda/drift@latest

Both methods install to $GOBIN, or $GOPATH/bin when unset, usually ~/go/bin. Ensure that directory is in your $PATH. Git must also be available in $PATH; drift uses git check-ignore in one batched process per scan so nested rules, tracked files, worktrees, .git/info/exclude, and global excludes match Git itself. make install builds your local checkout; go install …@latest installs the latest published version.

Optionally set a persistent install directory for all Go programs:

go env -w GOBIN="$HOME/.local/bin"
Update after code changes
make update

Usage

# 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 version

Navigate to any file or directory, press Space to mark it, then s to open the sync target picker.

Project dashboard

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.

Typical workflow
  1. Run drift in your project directory
  2. Mark one or more files/directories with Space
  3. Press s and choose a host
  4. Review diffs and suggested sync directions
  5. Sync the current file with s or all files with S

Key Bindings

Project Dashboard
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
Project switcher (P in the file browser)
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)
File Browser
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
. / I Toggle hidden / gitignored entries
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.

Hidden and gitignored entries are hidden in the browser by default. The two visibility toggles are independent and do not change sync scope. A recursively selected directory includes hidden files such as .htaccess and .well-known, but skips gitignored paths. Drift applies the local project's ignore rules to both local and mapped remote files, so an ignored remote-only file does not become a local delete or download candidate. Selecting an ignored file directly includes that file. Selecting an ignored directory does not include its contents.

.git, .svn, .hg, node_modules, .idea, and .vscode directories remain excluded even when ignored files are included. Symlinks and paths outside the project or active mappings keep their existing safety checks.

The diff status shows the number of pairs, hidden files, skipped ignored paths, and fixed exclusions. Press i to rebuild that comparison with all gitignored paths included for this operation. Drift reloads the rules and comparison; the setting does not change the global config.

Diff View
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)
i Rebuild scope with / without gitignored paths
r Reload ignore rules, scope, and diffs
q / Esc Back to browser
Host Manager
Key Action
n New host
e / Enter Edit host
c Duplicate host into a pre-filled form with a free name
d Delete host
t Test connection
q / Esc Back
Mouse

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 = false
show_hidden = true   # initial browser visibility; default false
show_ignored = true  # initial browser visibility; default false

The flag beats the environment variable, which beats the config file. Mouse support is on unless one of them turns it off.


Theming

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.toml

auto loads Omarchy colors first and falls back to ANSI terminal colors. default uses Drift's built-in fallback palette.


Logging

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.


Configuration

Global config: ~/.config/drift/config.toml
[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"
Project config: ~/.config/drift/projects/<slug>.toml

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  = "ftps"

  [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.

Keep-alive

Remote connections send protocol keep-alive probes every 60 seconds by default. Set keep_alive_interval on a host in seconds, or edit the field in the host form:

keep_alive_interval = 60 # 0 disables keep-alive; omit for the default

TOML accepts values from 0 to 86400. In the host form, use Disable keep-alive to turn probes off. The interval field is hidden while disabled; otherwise it accepts 1 to 86400 seconds, with a blank field using the default. Toggling back on restores the interval entered during the current edit. Changes apply to new connections. This is a host setting, not a [defaults] field.

FTP and FTPS send NOOP only after an idle interval on the control connection, never between commands of a running transfer. SFTP sends SSH keep-alive requests, which can run alongside file transfers. A negative SSH reply still confirms that the peer is reachable. A probe has 15 seconds to receive a response.

A failed probe marks the connection as disconnected. In the remote browser, use r to reconnect; in the diff, return to the browser and compare again before syncing. Completed files retain their results. An interrupted operation may have an unknown server-side outcome. drift never reconnects or repeats transfers automatically because of a keep-alive failure.

Keep-alive cannot prevent every server idle limit or network failure. FTP probes are skipped during busy operations, and an SSH response does not guarantee that the SFTP subsystem is healthy. TCP keep-alive is separate from these probes.

FTPS certificate trust

FTPS certificates are checked against the operating system trust store, including the configured hostname and validity period. If that check fails, drift stops the connection and shows the certificate subject, issuer, validity, problem, and full SHA-256 fingerprint. Reject is selected by default.

After checking the fingerprint through a trusted channel, you can trust that exact certificate for the current drift process or permanently. Permanent exceptions are bound to the normalized hostname and port and stored in ~/.config/drift/trusted-certificates.toml with mode 600. A certificate change or a newly occurring validation problem asks again. Use r on an FTPS host in the host manager to reset both session and persistent trust for its endpoint.

The former insecure_tls host option is ignored and no longer written. It does not grant trust. Custom CA files and client certificates are not supported yet. FTPS continues to use explicit TLS on the FTP control port and TLS 1.2.

Path Mappings

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.

Auth types
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.

Nothing in your project

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
~/.config/drift/trusted-certificates.toml persistent FTPS certificate exceptions, 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.

Coming from 0.1.6-alpha or earlier

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.


Project Structure

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
  tlstrust/     FTPS certificate verification and trust policy
  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
    certtrust/  FTPS certificate trust modal
    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

Development

go test ./...
go vet ./...
go build ./...
Local install
make install
Versioned build
make release-build VERSION=vX.Y.Z

Contributing

Issues and pull requests are welcome.

For development notes and contribution workflow, see CONTRIBUTING.md.


License

MIT — see LICENSE.

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
internal
config
Package config defines drift's configuration types and loading logic.
Package config defines drift's configuration types and loading logic.
diff
Package diff compares local and remote files and produces structured diff results.
Package diff compares local and remote files and produces structured diff results.
fs
Package fs defines the shared FileEntry type used for both local and remote files.
Package fs defines the shared FileEntry type used for both local and remote files.
ftp
Package ftp provides an FTP client that implements remote.Client.
Package ftp provides an FTP client that implements remote.Client.
log
Package log provides drift's opt-in file logging.
Package log provides drift's opt-in file logging.
pathmap
Package pathmap resolves local filesystem paths to remote paths and vice versa using the Mapping rules from the project config.
Package pathmap resolves local filesystem paths to remote paths and vice versa using the Mapping rules from the project config.
project
Package project manages the global registry of drift projects.
Package project manages the global registry of drift projects.
remote
Package remote defines the protocol-agnostic interface for remote file operations and provides a factory that returns the right implementation (SFTP or FTP) based on host.Protocol.
Package remote defines the protocol-agnostic interface for remote file operations and provides a factory that returns the right implementation (SFTP or FTP) based on host.Protocol.
sftp
Package sftp provides an SFTP client that wraps SSH connection + SFTP session.
Package sftp provides an SFTP client that wraps SSH connection + SFTP session.
ssh
Package ssh provides SSH authentication helpers.
Package ssh provides SSH authentication helpers.
styles
Package styles defines all lipgloss colors and styles used across drift's TUI.
Package styles defines all lipgloss colors and styles used across drift's TUI.
sync
Package sync handles building and executing file sync plans.
Package sync handles building and executing file sync plans.
tlstrust
Package tlstrust verifies FTPS server certificates and represents narrowly scoped exceptions approved by the user.
Package tlstrust verifies FTPS server certificates and represents narrowly scoped exceptions approved by the user.
tui
Package tui re-exports styles for convenience within the tui package itself.
Package tui re-exports styles for convenience within the tui package itself.
tui/browser
Package browser implements the yazi-like local file browser TUI component.
Package browser implements the yazi-like local file browser TUI component.
tui/certtrust
Package certtrust implements the modal used to approve an FTPS certificate exception.
Package certtrust implements the modal used to approve an FTPS certificate exception.
tui/dashboard
Package dashboard implements the project dashboard — the optional landing screen that lists registered drift projects and lets the user open one.
Package dashboard implements the project dashboard — the optional landing screen that lists registered drift projects and lets the user open one.
tui/diffview
Package diffview implements the file-list + unified-diff screen.
Package diffview implements the file-list + unified-diff screen.
tui/hostform
Package hostform implements the create/edit host form screen.
Package hostform implements the create/edit host form screen.
tui/hostmanager
Package hostmanager implements the host list / CRUD screen.
Package hostmanager implements the host list / CRUD screen.
tui/hostselector
Package hostselector implements the host picker modal shown when the user presses [s].
Package hostselector implements the host picker modal shown when the user presses [s].
tui/loading
Package loading provides the global network activity indicator used by TUI screens.
Package loading provides the global network activity indicator used by TUI screens.
tui/mouse
Package mouse holds the mouse helpers shared by the TUI screens.
Package mouse holds the mouse helpers shared by the TUI screens.
tui/projectform
Package projectform implements the create/edit project form screen.
Package projectform implements the create/edit project form screen.
tui/projectselector
Package projectselector implements the project switcher modal shown when the user presses [P].
Package projectselector implements the project switcher modal shown when the user presses [P].
tui/statusbar
Package statusbar renders the persistent one-line status bar at the bottom.
Package statusbar renders the persistent one-line status bar at the bottom.
tui/textfield
Package textfield provides a single-line editable text input widget shared by drift's TUI forms (host form, project form).
Package textfield provides a single-line editable text input widget shared by drift's TUI forms (host form, project form).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL