Skip to content

Right-click file operations, and an icon in the taskbar - #12

Merged
ralyodio merged 2 commits into
mainfrom
feat/file-ops-menu
Aug 30, 2026
Merged

Right-click file operations, and an icon in the taskbar#12
ralyodio merged 2 commits into
mainfrom
feat/file-ops-menu

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Right-click file operations

Refresh, New folder, New file, Rename and Delete, on a context menu, in the local pane and the remote one alike — the FileZilla operations the panes looked like they already had. F2 renames and Del deletes.

The remote half already had mkdir/rename/delete over SFTP and nothing called them; the local half had no mutations at all. Both now go through one bridge where the connection id selects local or remote, so a caller cannot act on the wrong pane by reaching for the wrong method name.

Not letting the renderer name a path

Every mutation takes a directory plus a bare entry name and joins them in the main process. EntryNameSchema rejects a separator, .., a NUL and surrounding space — so "New folder" in a listing of /home/you cannot write outside it, and on the remote side cannot write anywhere the SSH user can reach.

Delete re-stats its target and refuses when what is on disk disagrees with what the renderer claimed, so a mislabelled request cannot turn one unlink into a recursive delete. A symlink is unlinked, never followed into.

Two things SFTP does not hand you: creating a file (open with wx, which fails rather than truncating something already there) and removing a populated directory (rmdir only unlinks an empty one, so removeRecursive walks it depth first).

What the render caught

Verified in a headless render of the real export under the app's real CSP, both themes: the menu opens on a row with all five items live, the row becomes the selection, and the New folder dialog focuses its field with its footer inside the panel.

That render found the first draft's bug — the container's onContextMenu ran after the row's as the event bubbled and cleared the target, leaving Rename and Delete greyed out on every row. It reads correctly in the source; it is obvious in a screenshot.

An icon in the taskbar

The window was created with no icon, so it carried no _NET_WM_ICON and a Linux taskbar had nothing to draw. That is invisible when the app starts from its .desktop file, because the launcher supplies the icon — and it is exactly what you get starting it any other way, which is how anyone on the AppImage or a checkout starts it.

The window now takes resources/icon.png: process.resourcesPath when packaged, the repo copy in a checkout, and undefined rather than a path to nothing.

The other half is grouping. A taskbar ties a window to its launcher by matching WM_CLASS against the entry's StartupWMClass, and neither desktop entry had one. Chromium derives WM_CLASS from the executable name, which differs between the deb, the AppImage and a dev run, so the app pins it to DiskPush before the window exists and both entries declare that value. A test reads both files and fails if either drifts from the constant.

Honest limit: the WM_CLASS the window actually reports is not verified — this machine has no X server and no xprop, so the pinning is argued from Chromium's behaviour rather than measured. Its failure mode is the icon we now set being used without grouping, which is no worse than today.

Checks

  • pnpm test — 296 passed, 27 files (25 new)
  • pnpm typecheck — clean across every package and app
  • pnpm smoke:desktop — assets resolve, served over its own scheme, CSP admits all 7 inline scripts (its Electron-launch step skips locally for missing system libs; it runs in CI)

🤖 Generated with Claude Code

https://claude.ai/code/session_01GTQ3RzTAey9nT6r1kbGBCd

ralyodio and others added 2 commits August 30, 2026 07:26
Refresh, New folder, New file, Rename and Delete, on a context menu, in
the local pane and the remote one alike — the FileZilla operations the
panes looked like they already had. F2 renames and Del deletes, because a
file manager that answers only the mouse is half a file manager.

The remote half already had mkdir/rename/delete over SFTP and nothing
called them; the local half had no mutations at all. Both sides now go
through one bridge where the connection id is what selects local or
remote, so a caller cannot act on the wrong pane by picking the wrong
method name.

Every mutation takes a directory and a bare entry name and joins them in
the main process. EntryNameSchema rejects a separator, `..`, a NUL and
surrounding space, so "New folder" in a listing cannot write outside the
folder being listed — on the remote side, anywhere the SSH user can
reach. Delete re-stats its target and refuses when what is on disk
disagrees with what the renderer claimed, so a mislabelled request cannot
turn one unlink into a recursive delete; a symlink is unlinked, never
followed.

Two things SFTP does not give you: creating a file (open 'wx', which
fails rather than truncating an existing one) and removing a populated
directory (rmdir only unlinks an empty one, so removeRecursive walks it
depth first).

Verified in a headless render of the real export under the app's real
CSP, both themes: the menu opens on a row with all five items live, the
row it opened on becomes the selection, and the New folder dialog focuses
its field with its footer inside the panel. That render caught the first
draft's bug — the container's handler ran after the row's as the event
bubbled and cleared the target, leaving Rename and Delete greyed out on
every row.

20 new schema tests cover the traversal cases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTQ3RzTAey9nT6r1kbGBCd
The window was created with no `icon`, so it carried no _NET_WM_ICON and
a Linux taskbar had nothing to draw but a placeholder. That is invisible
when the app is started from its .desktop file, because the launcher
supplies the icon — and it is exactly what you see when the app is
started any other way, which is how anyone running the AppImage or a
checkout starts it.

The window now takes resources/icon.png: from `process.resourcesPath`
when packaged, from the repo when run out of a checkout, and undefined
rather than a path to nothing, since BrowserWindow given a missing icon
logs nothing and shows no icon.

The second half is grouping. A taskbar ties a window to its launcher by
matching WM_CLASS against the .desktop file's StartupWMClass, and neither
desktop entry had one. Chromium derives WM_CLASS from the executable
name, which differs between the deb, the AppImage and a dev run, so the
app now pins it to `DiskPush` before the window is created and both
entries — the installer's and electron-builder's — declare that same
value. A test reads both files and fails if either drifts from the
constant, because a rename that touched one of them would un-group the
window with nothing else failing.

Verified: the checkout icon path resolves against the real dist-electron
layout, and the 5 new tests cover candidate ordering, the dev fallback,
the missing-icon case and both desktop entries. Not verified here: the
WM_CLASS the window actually reports. This box has no X server and no
xprop, so the pinning is argued from Chromium's behaviour rather than
measured. Its failure mode is the icon we now set being used without
grouping, which is no worse than today.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTQ3RzTAey9nT6r1kbGBCd
@ralyodio
ralyodio merged commit cead732 into main Aug 30, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant