Move settings into a standalone Settings window

The tab-bar popover had outgrown itself. Settings now open in their own
native window (a fourth window kind, built like the connection manager but
not modal): a sidebar with Appearance, Transfers, Editor, File list and
About, holding every popover row plus the visible-columns choice. Each
control is an ordinary settings save, so the other windows follow live
while it stays open.

The gear button in the tab bar, Ctrl+, and the macOS app menu's
"Settings..." (Cmd+,) open it. On macOS Cmd+W arrives as the main window's
close-tab menu action, so bun closes the focused Settings window first
instead of a main-window tab.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-10-05 09:55:15 +02:00
co-authored by Claude
parent f3bec7aeaf
commit 141d675bf7
28 changed files with 743 additions and 405 deletions
+56 -20
View File
@@ -31,6 +31,7 @@ export interface AppDeps {
openEditor: (ctx: EditorOpenContext) => void;
settingsChanged: (settings: AppSettings) => void;
openConnectionManager: () => void;
openSettings: () => void;
showAbout: () => void;
};
}
@@ -130,6 +131,28 @@ const pickKeyFileHandler = () =>
return { path };
});
/** External-editor picker, shared by the main and settings window rpcs. */
const pickEditorAppHandler = () =>
wrap(async () => {
// PORTABILITY: apps live in different places per OS
const { homedir } = await import("node:os");
const [startingFolder, allowedFileTypes] =
process.platform === "darwin"
? ["/Applications", "app"]
: process.platform === "win32"
? [process.env.ProgramFiles ?? "C:\\Program Files", "exe"]
: [homedir(), "*"];
const picked = await Utils.openFileDialog({
startingFolder,
allowedFileTypes,
canChooseFiles: true,
canChooseDirectory: false,
allowsMultipleSelection: false,
});
const path = picked.find((p) => p.trim().length > 0) ?? null;
return { path };
});
export function createAppRPC({
registry,
engine,
@@ -257,26 +280,7 @@ export function createAppRPC({
editExternal: ({ sessionId, path }) =>
wrap(() => edits.editExternal(sessionId, path)),
pickEditorApp: () =>
wrap(async () => {
// PORTABILITY: apps live in different places per OS
const { homedir } = await import("node:os");
const [startingFolder, allowedFileTypes] =
process.platform === "darwin"
? ["/Applications", "app"]
: process.platform === "win32"
? [process.env.ProgramFiles ?? "C:\\Program Files", "exe"]
: [homedir(), "*"];
const picked = await Utils.openFileDialog({
startingFolder,
allowedFileTypes,
canChooseFiles: true,
canChooseDirectory: false,
allowsMultipleSelection: false,
});
const path = picked.find((p) => p.trim().length > 0) ?? null;
return { path };
}),
pickEditorApp: pickEditorAppHandler,
pickKeyFile: pickKeyFileHandler,
@@ -318,6 +322,13 @@ export function createAppRPC({
return {};
}),
// ---- settings window ----
openSettings: () =>
wrap(async () => {
events.openSettings();
return {};
}),
showAbout: () =>
wrap(async () => {
events.showAbout();
@@ -449,3 +460,28 @@ export function createManagerRPC(
},
});
}
export interface SettingsRpcHooks {
/** Every edit here is a settings save — rebroadcast it to the other windows. */
onSettingsSaved: (next: AppSettings) => void;
showAbout: () => void;
}
/** RPC for the single Settings window (per-window instance). */
export function createSettingsRPC(deps: { settings: SettingsStore }, hooks: SettingsRpcHooks) {
return BrowserView.defineRPC<AppRPC>({
maxRequestTime: MAX_REQUEST_TIME_MS,
handlers: {
requests: {
getSettings: () => wrap(async () => ({ settings: await deps.settings.load() })),
saveSettings: saveSettingsHandler(deps.settings, hooks.onSettingsSaved),
pickEditorApp: pickEditorAppHandler,
showAbout: () =>
wrap(async () => {
hooks.showAbout();
return {};
}),
},
},
});
}