Editor opens in native windows (one per file)
Real BrowserWindow per edited file with its own RPC instance, deduped by sessionId+path. The same Angular bundle boots an editor-only root via preload globals. Native close isn't cancellable in electrobun, so the editor mirrors its draft to bun; traffic-light closes get a native Save/Discard prompt after the fact, and failed saves park the draft in the edit-leftovers dir. In-window closes (Cmd+W, Close) use a styled prompt; the quit guard now also counts unsaved editors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+52
-10
@@ -1,8 +1,10 @@
|
||||
import Electrobun, { ApplicationMenu, BrowserWindow, Updater, Utils } from "electrobun/bun";
|
||||
import { BRANDING } from "../shared/branding";
|
||||
import { effectiveConcurrency } from "../shared/types";
|
||||
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";
|
||||
import { applyApplicationMenu } from "./menu";
|
||||
import { createAppRPC } from "./rpc-handlers";
|
||||
@@ -11,6 +13,7 @@ import { migrateLegacyDataDir } from "./store/paths";
|
||||
import { SettingsStore } from "./store/settings";
|
||||
import { AppTray } from "./tray";
|
||||
import { TransferEngine } from "./transfers/engine";
|
||||
import { buildPreload } from "./windows";
|
||||
|
||||
const PROD_URL = "views://app/index.html";
|
||||
|
||||
@@ -44,6 +47,8 @@ const contextMenu = new PanelContextMenu();
|
||||
const events = {
|
||||
sessionClosed: (_info: { sessionId: string; reason: string }) => {},
|
||||
connectionsChanged: () => {},
|
||||
openEditor: (_ctx: EditorOpenContext) => {},
|
||||
settingsChanged: (_settings: AppSettings) => {},
|
||||
};
|
||||
const rpc = createAppRPC({ registry, engine, store, settings, edits, contextMenu, events });
|
||||
const url = await getMainViewUrl();
|
||||
@@ -66,10 +71,23 @@ const win = new BrowserWindow({
|
||||
? { titleBarStyle: "hiddenInset" as const, trafficLightOffset: { x: 12, y: 13 } }
|
||||
: {}),
|
||||
// lets the UI pick the fused layout before first paint
|
||||
preload: `window.__APP_PLATFORM=${JSON.stringify(process.platform)};`,
|
||||
preload: buildPreload("main"),
|
||||
rpc,
|
||||
});
|
||||
|
||||
const editors = new EditorWindows({
|
||||
registry,
|
||||
settings,
|
||||
edits,
|
||||
url,
|
||||
mainFrame: () => win.getFrame(),
|
||||
notifyMain: { pathRefresh: (info) => rpc.send.pathRefresh(info) },
|
||||
});
|
||||
events.openEditor = (ctx) => editors.open(ctx);
|
||||
events.settingsChanged = (s) => editors.broadcastSettings(s);
|
||||
// clean editors close with the main window; dirty ones get to ask first
|
||||
win.on("close", () => editors.closeAllForAppExit());
|
||||
|
||||
new AppTray(win);
|
||||
contextMenu.onAction = (action) => rpc.send.menuAction({ action });
|
||||
|
||||
@@ -85,7 +103,11 @@ engine.onUpdate = (snapshot) => {
|
||||
const previous = notifiedStates.get(snapshot.id);
|
||||
if (previous !== snapshot.state) {
|
||||
notifiedStates.set(snapshot.id, snapshot.state);
|
||||
if (!windowFocused && (snapshot.state === "completed" || snapshot.state === "failed")) {
|
||||
if (
|
||||
!windowFocused &&
|
||||
!editors.anyFocused() &&
|
||||
(snapshot.state === "completed" || snapshot.state === "failed")
|
||||
) {
|
||||
Utils.showNotification({
|
||||
title: snapshot.state === "completed" ? "Transfer complete" : "Transfer failed",
|
||||
body:
|
||||
@@ -102,18 +124,24 @@ engine.onUpdate = (snapshot) => {
|
||||
}
|
||||
};
|
||||
|
||||
// don't quit out from under running transfers
|
||||
// don't quit out from under running transfers or unsaved editors
|
||||
let quitConfirmed = false;
|
||||
Electrobun.events.on("before-quit", (event) => {
|
||||
if (quitConfirmed || !engine.hasActiveJobs()) return;
|
||||
const transfers = engine.activeJobCount();
|
||||
const dirtyEditors = editors.dirtyCount();
|
||||
if (quitConfirmed || (transfers === 0 && dirtyEditors === 0)) return;
|
||||
(event as { response: { allow: boolean } }).response = { allow: false };
|
||||
void (async () => {
|
||||
const count = engine.activeJobCount();
|
||||
const parts = [
|
||||
transfers > 0 && `${transfers} transfer${transfers === 1 ? " is" : "s are"} still running`,
|
||||
dirtyEditors > 0 &&
|
||||
`${dirtyEditors} file${dirtyEditors === 1 ? " has" : "s have"} unsaved changes`,
|
||||
].filter(Boolean);
|
||||
const { response } = await Utils.showMessageBox({
|
||||
type: "warning",
|
||||
title: BRANDING.appName,
|
||||
message: `${count} transfer${count === 1 ? " is" : "s are"} still running.`,
|
||||
detail: "Quitting now will abort them.",
|
||||
message: `${parts.join(" and ")}.`,
|
||||
detail: "Quitting now will abort transfers and discard unsaved changes.",
|
||||
buttons: ["Quit Anyway", "Cancel"],
|
||||
defaultId: 1,
|
||||
cancelId: 1,
|
||||
@@ -124,14 +152,28 @@ Electrobun.events.on("before-quit", (event) => {
|
||||
}
|
||||
})();
|
||||
});
|
||||
events.sessionClosed = (info) => rpc.send.sessionClosed(info);
|
||||
events.sessionClosed = (info) => {
|
||||
rpc.send.sessionClosed(info);
|
||||
editors.notifySessionClosed(info);
|
||||
};
|
||||
events.connectionsChanged = () => void applyApplicationMenu(store);
|
||||
void settings.load().then((s) => engine.setConcurrency(effectiveConcurrency(s)));
|
||||
|
||||
void applyApplicationMenu(store);
|
||||
ApplicationMenu.on("application-menu-clicked", (event) => {
|
||||
const action = (event as { data?: { action?: string } }).data?.action;
|
||||
if (action) rpc.send.menuAction({ action });
|
||||
if (!action) return;
|
||||
// ⌘W while an editor window is focused closes that editor (with its dirty
|
||||
// prompt), not a main-window tab. Other menu actions deliberately still
|
||||
// target the main window.
|
||||
if (action === "close-tab") {
|
||||
const focusedEditor = editors.focusedKey();
|
||||
if (focusedEditor) {
|
||||
editors.requestClose(focusedEditor);
|
||||
return;
|
||||
}
|
||||
}
|
||||
rpc.send.menuAction({ action });
|
||||
});
|
||||
|
||||
console.log(`${BRANDING.appName} started (${url})`);
|
||||
|
||||
Reference in New Issue
Block a user