Merge main into feat/7css-skin + give the HTML context menu an Aero skin
main retired electrobun's native context menu (a documented no-op on Linux) in favour of an HTML one on every platform. Merging that here needs the menu to look right under the Win7 skin as well. Conflicts, all resolved by keeping both sides: - src/bun/index.ts — createAppRPC keeps the branch's mainWindow getter, drops main's removed contextMenu dep. - ui/src/app/app.ts — imports both TitleBar and ContextMenu. - ui/src/app/platform.ts — kept the branch's version (its IS_MAC_CHROME accounts for the frameless win7 chrome, and MOD_LABEL correctly keys on the real platform rather than the chrome), plus exporting IS_MAC and adding IS_LINUX for the menu's per-OS shell wording. Aero menu (branch-only): 7.css has no menu component, so it is hand-authored across both layers — the row <button>s shed their push-button slab in `skin` (revert-layer only reaches `utilities` from there), and the square-cornered slab, hairline border and gradient selection band are stated in `skinfix`. Built strictly from existing --w7-* vars, never literal hex, so the hand-written dark Win7 palette recolours it for free. Hover and the keyboard cursor share one band (the component marks the cursor with `bg-accent`, which is overridden alongside :hover) as a real Win7 menu does. Also answers a question the merge raised: frameless windows DO work on Linux. The GTK wrapper honours titleBarStyle "hidden" — it calls gtk_window_set_decorated(false) and logs "GTK: Created window without decorations (custom titlebar)". Verified on GNOME/Wayland and documented in architecture.md. Verified: backend `tsc --noEmit` clean, `bun run build:ui` clean (492.52 kB initial, still under the 500 kB budget), app launches with the skin active, frameless, no console errors and no GBM failures. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,128 +0,0 @@
|
||||
import { ContextMenu, Utils, type ApplicationMenuItemConfig } from "electrobun/bun";
|
||||
import type { ColumnKey } from "../shared/types";
|
||||
import { t } from "./i18n";
|
||||
|
||||
/** Optional list columns, in display order (Name is fixed). */
|
||||
const COLUMN_KEYS: ColumnKey[] = ["size", "mtime", "kind", "rights", "owner", "group"];
|
||||
|
||||
// PORTABILITY: shell-integration wording per OS (resolved at show() for locale)
|
||||
const IS_WIN = process.platform === "win32";
|
||||
const revealLabel = (): string =>
|
||||
t(
|
||||
process.platform === "darwin"
|
||||
? "ctx.revealFinder"
|
||||
: IS_WIN
|
||||
? "ctx.revealExplorer"
|
||||
: "ctx.revealFileManager",
|
||||
);
|
||||
const trashLabel = (): string => t(IS_WIN ? "ctx.moveToRecycleBin" : "ctx.moveToTrash");
|
||||
|
||||
export interface RowMenuContext {
|
||||
path: string;
|
||||
name: string;
|
||||
type: "file" | "dir" | "symlink";
|
||||
isLocal: boolean;
|
||||
hasEntry: boolean; // false = right-click on empty panel space
|
||||
}
|
||||
|
||||
/**
|
||||
* Native right-click menus for the file panels. Local-only items run directly
|
||||
* here via Utils (reveal, open with default app, copy path); everything else
|
||||
* forwards to the webview through the menuAction channel — the UI kept its own
|
||||
* context (which panel/row was clicked) when it requested the menu.
|
||||
*/
|
||||
export class PanelContextMenu {
|
||||
private context: RowMenuContext | null = null;
|
||||
/** Forwards actions the UI must handle (ctx-open, ctx-delete, ...). */
|
||||
onAction: (action: string) => void = () => {};
|
||||
|
||||
constructor() {
|
||||
ContextMenu.on("context-menu-clicked", (event) => {
|
||||
const action = (event as { data?: { action?: string } }).data?.action;
|
||||
if (!action) return;
|
||||
// column toggles carry no row context
|
||||
if (action.startsWith("col:")) {
|
||||
this.onAction(action);
|
||||
return;
|
||||
}
|
||||
const ctx = this.context;
|
||||
if (!ctx) return;
|
||||
switch (action) {
|
||||
case "ctx-reveal":
|
||||
Utils.showItemInFolder(ctx.path);
|
||||
break;
|
||||
case "ctx-open-default":
|
||||
Utils.openPath(ctx.path);
|
||||
break;
|
||||
case "ctx-copy-path":
|
||||
Utils.clipboardWriteText(ctx.path);
|
||||
break;
|
||||
default:
|
||||
if (action.startsWith("ctx-")) this.onAction(action);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
show(context: RowMenuContext): void {
|
||||
this.context = context;
|
||||
ContextMenu.showContextMenu(this.build(context));
|
||||
}
|
||||
|
||||
/** Column picker for the list headers: checkable toggles per column. */
|
||||
showColumns(visible: ColumnKey[]): void {
|
||||
ContextMenu.showContextMenu(
|
||||
COLUMN_KEYS.map((key) => ({
|
||||
label: t(`columns.${key}`),
|
||||
action: `col:${key}`,
|
||||
checked: visible.includes(key),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
private build(ctx: RowMenuContext): ApplicationMenuItemConfig[] {
|
||||
const items: ApplicationMenuItemConfig[] = [];
|
||||
|
||||
if (!ctx.hasEntry || ctx.name === "..") {
|
||||
// empty space (or the parent row): folder-level actions
|
||||
items.push({ label: t("ctx.newFolder"), action: "ctx-new-folder" });
|
||||
items.push({ label: t("ctx.newFile"), action: "ctx-new-file" });
|
||||
items.push({ type: "separator" });
|
||||
items.push({ label: t("ctx.refresh"), action: "ctx-refresh" });
|
||||
if (ctx.isLocal) {
|
||||
items.push({ type: "separator" });
|
||||
items.push({ label: revealLabel(), action: "ctx-reveal" });
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
items.push({ label: t("ctx.copyToOther"), action: "ctx-copy" });
|
||||
if (ctx.type === "dir") {
|
||||
items.push({ label: t("ctx.open"), action: "ctx-open" });
|
||||
} else {
|
||||
if (ctx.isLocal) {
|
||||
items.push({ label: t("ctx.open"), action: "ctx-open-default", tooltip: t("ctx.openDefault") });
|
||||
}
|
||||
items.push({ label: t("ctx.edit"), action: "ctx-edit", accelerator: "e" });
|
||||
}
|
||||
items.push({ label: t("ctx.rename"), action: "ctx-rename" });
|
||||
// chmod on Windows local files only flips the read-only bit — hide the lie
|
||||
if (!(ctx.isLocal && IS_WIN)) {
|
||||
items.push({ label: t("ctx.permissions"), action: "ctx-chmod" });
|
||||
}
|
||||
items.push({ type: "separator" });
|
||||
items.push({ label: t("ctx.copyPath"), action: "ctx-copy-path" });
|
||||
if (ctx.isLocal) {
|
||||
items.push({ label: revealLabel(), action: "ctx-reveal" });
|
||||
}
|
||||
items.push({ type: "separator" });
|
||||
if (ctx.isLocal) {
|
||||
items.push({
|
||||
label: trashLabel(),
|
||||
action: "ctx-trash",
|
||||
...(process.platform === "darwin" ? { tooltip: t("ctx.trashTooltip") } : {}),
|
||||
});
|
||||
}
|
||||
items.push({ label: t("ctx.delete"), action: "ctx-delete" });
|
||||
return items;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import Electrobun, { ApplicationMenu, BrowserWindow, Updater, Utils } from "elec
|
||||
import { BRANDING } from "../shared/branding";
|
||||
import type { EditorOpenContext } from "../shared/rpc";
|
||||
import { effectiveConcurrency, type AppSettings } from "../shared/types";
|
||||
import { PanelContextMenu } from "./context-menu";
|
||||
import { EditManager } from "./edits";
|
||||
import { EditorWindows } from "./editor-windows";
|
||||
import { SessionRegistry } from "./fs/registry";
|
||||
@@ -46,7 +45,6 @@ const engine = new TransferEngine(registry);
|
||||
const store = new ConnectionStore();
|
||||
const settings = new SettingsStore();
|
||||
const edits = new EditManager(registry, engine, settings);
|
||||
const contextMenu = new PanelContextMenu();
|
||||
const watcher = new LocalDirWatcher();
|
||||
const events = {
|
||||
sessionClosed: (_info: { sessionId: string; reason: string }) => {},
|
||||
@@ -65,7 +63,6 @@ const rpc = createAppRPC({
|
||||
store,
|
||||
settings,
|
||||
edits,
|
||||
contextMenu,
|
||||
watcher,
|
||||
mainWindow: () => mainWin,
|
||||
events,
|
||||
@@ -158,7 +155,6 @@ win.on("close", () => {
|
||||
});
|
||||
|
||||
new AppTray(win);
|
||||
contextMenu.onAction = (action) => rpc.send.menuAction({ action });
|
||||
|
||||
// notify on finished transfers while the window is in the background
|
||||
let windowFocused = true;
|
||||
|
||||
+5
-12
@@ -1,7 +1,6 @@
|
||||
import { BrowserView, type BrowserWindow, Updater, Utils } from "electrobun/bun";
|
||||
import type { AppRPC, EditorOpenContext } from "../shared/rpc";
|
||||
import { effectiveConcurrency, type AppSettings, type Result } from "../shared/types";
|
||||
import type { PanelContextMenu } from "./context-menu";
|
||||
import { readFileText, writeFileText, type EditManager } from "./edits";
|
||||
import { listLocalDrives } from "./fs/local";
|
||||
import type { SessionRegistry } from "./fs/registry";
|
||||
@@ -24,7 +23,6 @@ export interface AppDeps {
|
||||
store: ConnectionStore;
|
||||
settings: SettingsStore;
|
||||
edits: EditManager;
|
||||
contextMenu: PanelContextMenu;
|
||||
watcher: LocalDirWatcher;
|
||||
/**
|
||||
* The main BrowserWindow, for the win7 skin's custom title-bar controls.
|
||||
@@ -175,7 +173,6 @@ export function createAppRPC({
|
||||
store,
|
||||
settings,
|
||||
edits,
|
||||
contextMenu,
|
||||
watcher,
|
||||
mainWindow,
|
||||
events,
|
||||
@@ -339,10 +336,12 @@ export function createAppRPC({
|
||||
return {};
|
||||
}),
|
||||
|
||||
// ---- context menu + local shell integration ----
|
||||
showRowMenu: (params) =>
|
||||
// ---- local shell integration (menu itself is HTML, see ui/) ----
|
||||
shellItemAction: ({ action, path }) =>
|
||||
wrap(async () => {
|
||||
contextMenu.show(params);
|
||||
if (action === "reveal") Utils.showItemInFolder(path);
|
||||
else if (action === "open") Utils.openPath(path);
|
||||
else Utils.clipboardWriteText(path);
|
||||
return {};
|
||||
}),
|
||||
|
||||
@@ -352,12 +351,6 @@ export function createAppRPC({
|
||||
return {};
|
||||
}),
|
||||
|
||||
showColumnsMenu: ({ visible }) =>
|
||||
wrap(async () => {
|
||||
contextMenu.showColumns(visible);
|
||||
return {};
|
||||
}),
|
||||
|
||||
// ---- editor windows ----
|
||||
openEditor: (ctx) =>
|
||||
wrap(async () => {
|
||||
|
||||
+9
-12
@@ -1,6 +1,5 @@
|
||||
import type {
|
||||
AppSettings,
|
||||
ColumnKey,
|
||||
ConnectionConfig,
|
||||
ConnectionsFile,
|
||||
EntryType,
|
||||
@@ -126,20 +125,18 @@ export type AppRPC = {
|
||||
response: Result<{ names: string[] }>;
|
||||
};
|
||||
clearEditLeftovers: { params: Record<string, never>; response: Result };
|
||||
// context menu + local shell integration
|
||||
showRowMenu: {
|
||||
params: {
|
||||
path: string;
|
||||
name: string;
|
||||
type: EntryType;
|
||||
isLocal: boolean;
|
||||
hasEntry: boolean;
|
||||
};
|
||||
// local shell integration
|
||||
/**
|
||||
* Shell actions the context menu triggers on a LOCAL path. The menu
|
||||
* itself is HTML (ui/src/app/context-menu.ts) — electrobun's native
|
||||
* context menu is a no-op on Linux — but these three still need the
|
||||
* bun side's Utils.
|
||||
*/
|
||||
shellItemAction: {
|
||||
params: { action: "reveal" | "open" | "copyPath"; path: string };
|
||||
response: Result;
|
||||
};
|
||||
moveToTrash: { params: { path: string }; response: Result };
|
||||
/** Native column-picker menu (right-click on the list headers). */
|
||||
showColumnsMenu: { params: { visible: ColumnKey[] }; response: Result };
|
||||
/** Native About box (name, Build n, commit, engine versions). */
|
||||
showAbout: { params: Record<string, never>; response: Result };
|
||||
/** Windows drive roots for the local panel ("C:\", …); empty elsewhere. */
|
||||
|
||||
Reference in New Issue
Block a user