Audited every copy/move/delete path (local, remote same-host, cross-host)
and fixed:
- Shell command injection in same-host remote cp/mv (unescaped paths
interpolated into exec strings); added remote.ShellQuote.
- Missing guard against copying/moving a directory into its own
subdirectory, locally and on same-host remote transfers (unbounded
recursion / disk fill). New regression tests in ops_test.go.
- Remote-to-remote same-path file copy silently truncating the source
file via Create() before it finished being read.
- Overwrite warning ("N existing target(s)") never shown for remote
destinations, only local ones.
- Remote same-host server-side cp never advancing the file-done counter
(waited on -v output that was never requested).
- Removed a dead "plan-then-confirm" subsystem (5 functions, 2 message
types, 2 unreachable Update() cases) superseded by the current design.
- Remote delete (SFTP) now runs as a cancellable job with the same
progress modal as local delete, instead of a single blocking call.
Also add a CI workflow that runs build/vet/test on every push and PR,
since previously they only ran on release tags.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Replace ssh.InsecureIgnoreHostKey() with TOFU verification against
~/.ssh/known_hosts (accept-new for unknown hosts, hard reject on a
changed key) to close a MITM hole on every SFTP connection.
- Actually connect to a running ssh-agent (SSH_AUTH_SOCK) for auth
instead of only scanning default key files on disk; give a clear
error when an explicit IdentityFile is passphrase-protected.
- Fix SSHClient.walk()/DirectorySize: the internal filepathSkipDir
sentinel leaked out as a real error on stat/ReadDir failures instead
of being swallowed, aborting size calculation on the first
unreadable subdirectory instead of skipping it.
- Skip symlink-to-directory entries in remote directory copies instead
of failing the whole transfer trying to Open() them as regular files.
- Consolidate 5 duplicated sudo-aware home-dir lookups into
internal/homedir, adding a missing os.Geteuid()==0 check before
trusting SUDO_USER.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Delete: skip remoteDeletePlanCmd and trashPlanCmd, show dialog immediately
- Copy: skip copyPlanCmd and remoteCopyPlanCmd, show dialog immediately
- CopyStats: no lstat per file, count files via WalkDir only
- Copy: two-phase (count first, then copy with known total + progress bar)
- Progress: file-based ratio, remove Size/Speed display
- Stage: Counting files... → Coping files... (no empty stage)
- ExtractArchiveToDir now accepts context.Context for cancellation
- extractZipArchive/extractTarArchive check ctx.Done() in extraction loops
- startExtractJob creates context with cancel (C/c now works for extraction)
- Added startDeleteJob method with per-file progress reporting
- Local delete (trash/permanent) now shows progress dialog with B/b and C/c
- renderArchiveProgressModal handles 'delete' kind (file-based progress, no speed)
- archiveDoneMsg handles 'delete' completion (reload panes, clear marks)
- ExtractArchiveToDir now accepts progress callback for file-by-file reporting
- Added countArchiveEntries/countZipEntries/countTarEntries helpers
- Added startExtractJob method following startArchiveJob pattern
- archiveJobState now has kind field ('archive'/'extract')
- archiveDoneMsg handler reloads only passive pane for extraction
- renderArchiveProgressModal shows file-based progress for extraction (no size/speed)
- Removed old synchronous extractArchiveCmd() and opExtractArchive case in opMsg
- Delete mode toggle ('d') inside existing confirm dialog instead of separate modal
- F11/e shows confirm dialog before extracting archive to opposite pane
- deleteKind field toggles between 'permanent' and 'trash'
- Active pane reloads after extraction so files appear immediately
- Add Connected bool field to vfs.Entry and RemoteMount
- Track connection status in sshState.connectedHosts
- Show status icon (connected/disconnected) in pane header when browsing remote host
- Async SSH connection test with cancel support for Add Host dialog
- Colored labels and styled help text in SSH dialogs
- Confirmation dialog when deleting manually-added SSH hosts
- Reorder Category() to check known extensions (text, config, image,
pdf, audio, video, archive) before the executable bit check.
Fixes video/audio/image files with executable bit being opened in
editor instead of system default application.
- Remove 'executable' from isEditableEntry() - executables are now
launched via handleExecute() instead of opened in Neovim.
- Add handleExecute() method that runs executable files in the
terminal via tea.ExecProcess.
- Update handleOpenSelected() to route: text/config -> editor,
executable -> launch, everything else -> system default (xdg-open).
- Bump version to v0.2.1
Add rich preview support for three new file categories by leveraging
external CLI tools with graceful fallback when tools are missing.
- PDF: text extraction via pdftotext, page count via pdfinfo
- Audio: metadata via ffprobe (duration, bitrate, codec, sample rate, channels)
- Video: metadata via ffprobe (duration, bitrate, video/audio codec, resolution)
- New PreviewKind constants: PDF, Audio, Video
- New Metadata fields for extended preview data
- New extension maps and Category() entries for pdf/audio/video
- Icons: PDF (), audio (), video () in preview header
Closes#5
Root cause: renderPreviewPane() was stuffing directory entries into the
viewport model via SetContent(), then rendering through
renderPreviewContent() which wraps the viewport in an additional
border+padding box. This reduced the effective content width by 4
characters compared to the browser pane's innerWidth (width-2), causing
column misalignment where the date column would wrap to the next line.
Fix: For directory previews, directly render entries using the same
renderPaneRows() and renderColumnsHeader() functions as the browser
pane, at the same innerWidth. A temporary BrowserPane is created with
the directory entries, and renderPaneRows() is called with active=false,
producing pixel-identical output to the main file browser.
Removed renderDirectoryPreviewBody() which is no longer needed.
- Reset cursor to '..' (position 0) when entering an archive so it
doesn't stay at the previous directory's cursor position
- Fix archive exit landing in /tmp when pressing Enter on '..' inside
an archive by delegating to archive-aware goParent() in enterSelected()
- Fix goParent() to handle ALL navigation within archive mounts
explicitly instead of falling through to filepath.Dir which may
navigate outside the mount to /tmp
- Fix format cycling text color reset by preserving '(f to change)'
hint in modal note, ensuring renderModalNoteLine applies proper
key/action styling via the '(' separator match
Expand textExtensions with many missing common text file extensions
(.lua, .rb, .vue, .svelte, .dart, .tex, .scala, .lisp, and many more).
Add textFilenames list for well-known text files without a meaningful
extension (Makefile, Dockerfile, README, LICENSE, .gitignore, etc.)
so they open in the editor via handleEdit() instead of falling back
to xdg-open via handleOpenExternal().