diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 2dac62382a..236f60e5ee 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -101,6 +101,16 @@ Running an AppImage normally requires FUSE and, on some distributions, the `libfuse2` compatibility package. If FUSE is unavailable, launch it with `--appimage-extract-and-run` instead. +Linux users whose window manager supplies all window controls can remove the +native Electron title bar with `--no-window-frame`: + +```bash +./bb-x86_64.AppImage --no-window-frame +``` + +The native frame remains the default. Changing this startup option requires a +full desktop app restart. + CI builds Linux artifacts on the pinned `ubuntu-22.04` runner. The AppImage links against the build machine's glibc, so that pin sets the oldest distribution that can run a published build. Raise it deliberately. diff --git a/apps/desktop/src/desktop-window-factory.ts b/apps/desktop/src/desktop-window-factory.ts index 7eb7e83ee1..c9ff7b879f 100644 --- a/apps/desktop/src/desktop-window-factory.ts +++ b/apps/desktop/src/desktop-window-factory.ts @@ -88,6 +88,7 @@ interface CreateDesktopWindowFactoryArgs { displayWorkAreas: DisplayWorkArea[] | null; icon: DesktopWindowIcon; isMac: boolean; + isLinuxFrameless: boolean; isQuitting(): boolean; openExternalUrl(args: OpenExternalUrlArgs): void; preloadPath: string; @@ -138,6 +139,7 @@ interface CreateWindowOptionsArgs { bounds: WindowBounds; icon: DesktopWindowIcon; isMac: boolean; + isLinuxFrameless: boolean; preloadPath: string; } @@ -168,6 +170,7 @@ function createWindowOptions( args: CreateWindowOptionsArgs, ): BrowserWindowConstructorOptions { return { + ...(args.isLinuxFrameless ? { frame: false } : {}), ...(args.isMac ? { frame: false, @@ -238,6 +241,7 @@ export function createDesktopWindowFactory( bounds: restoredState.bounds, icon: args.icon, isMac: args.isMac, + isLinuxFrameless: args.isLinuxFrameless, preloadPath: args.preloadPath, }), ); diff --git a/apps/desktop/src/desktop-window-frame.ts b/apps/desktop/src/desktop-window-frame.ts new file mode 100644 index 0000000000..802361f3b4 --- /dev/null +++ b/apps/desktop/src/desktop-window-frame.ts @@ -0,0 +1,15 @@ +export const LINUX_FRAMELESS_WINDOW_ARGUMENT = "--no-window-frame"; + +interface ShouldUseLinuxFramelessWindowArgs { + argv: readonly string[]; + platform: NodeJS.Platform; +} + +export function shouldUseLinuxFramelessWindow( + args: ShouldUseLinuxFramelessWindowArgs, +): boolean { + return ( + args.platform === "linux" && + args.argv.includes(LINUX_FRAMELESS_WINDOW_ARGUMENT) + ); +} diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 6c65429b83..13eb105c45 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -109,6 +109,7 @@ import { type DesktopBrowserWindowCreator, type DesktopWindowFactory, } from "./desktop-window-factory.js"; +import { shouldUseLinuxFramelessWindow } from "./desktop-window-frame.js"; import { createDesktopAboutDialogOptions, createDesktopAboutPanelOptions, @@ -2371,6 +2372,10 @@ async function runDesktopApp(): Promise { displayWorkAreas: null, icon: nativeImage.createFromPath(iconPath), isMac: process.platform === "darwin", + isLinuxFrameless: shouldUseLinuxFramelessWindow({ + argv: process.argv, + platform: process.platform, + }), isQuitting() { return quitting; }, diff --git a/apps/desktop/test/desktop-window-factory.test.ts b/apps/desktop/test/desktop-window-factory.test.ts index f8368ae666..f721d8ac28 100644 --- a/apps/desktop/test/desktop-window-factory.test.ts +++ b/apps/desktop/test/desktop-window-factory.test.ts @@ -240,6 +240,7 @@ describe("desktop window factory", () => { ], icon: undefined, isMac: true, + isLinuxFrameless: false, isQuitting() { return false; }, @@ -335,6 +336,7 @@ describe("desktop window factory", () => { ], icon: undefined, isMac: true, + isLinuxFrameless: false, isQuitting() { return false; }, @@ -393,6 +395,7 @@ describe("desktop window factory", () => { ], icon: undefined, isMac: true, + isLinuxFrameless: false, isQuitting() { return false; }, @@ -448,6 +451,7 @@ describe("desktop window factory", () => { ], icon: undefined, isMac: true, + isLinuxFrameless: false, isQuitting() { return false; }, @@ -505,6 +509,7 @@ describe("desktop window factory", () => { ], icon: undefined, isMac: true, + isLinuxFrameless: false, isQuitting() { return false; }, @@ -565,6 +570,7 @@ describe("desktop window factory", () => { ], icon: undefined, isMac: true, + isLinuxFrameless: false, isQuitting() { return false; }, @@ -623,6 +629,7 @@ describe("desktop window factory", () => { ], icon: undefined, isMac: false, + isLinuxFrameless: false, isQuitting() { return false; }, @@ -639,4 +646,47 @@ describe("desktop window factory", () => { "trafficLightPosition", ); }); + + it("removes the native window frame when requested on Linux", async () => { + const tempDir = await createTempDir(); + const createdWindows: FakeDesktopWindow[] = []; + const browserWindowCreator: DesktopBrowserWindowCreator = { + create(options) { + const browserWindow = new FakeDesktopWindow({ options }); + createdWindows.push(browserWindow); + return browserWindow; + }, + }; + const factory = createDesktopWindowFactory({ + browserWindowCreator, + createWindowStateKey() { + return "frameless-linux-window"; + }, + displayWorkAreas: [ + { + height: 900, + width: 1440, + x: 0, + y: 0, + }, + ], + icon: undefined, + isMac: false, + isLinuxFrameless: true, + isQuitting() { + return false; + }, + openExternalUrl() {}, + preloadPath: "/tmp/preload.cjs", + userDataPath: tempDir.path, + }); + + await factory.createWindow({ initialUrl: null, stateKey: null }); + + expect(createdWindows[0]?.options.frame).toBe(false); + expect(createdWindows[0]?.options).not.toHaveProperty("titleBarStyle"); + expect(createdWindows[0]?.options).not.toHaveProperty( + "trafficLightPosition", + ); + }); }); diff --git a/apps/desktop/test/desktop-window-frame.test.ts b/apps/desktop/test/desktop-window-frame.test.ts new file mode 100644 index 0000000000..9f282c2f78 --- /dev/null +++ b/apps/desktop/test/desktop-window-frame.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; +import { + LINUX_FRAMELESS_WINDOW_ARGUMENT, + shouldUseLinuxFramelessWindow, +} from "../src/desktop-window-frame.js"; + +describe("desktop window frame", () => { + it("enables frameless windows on Linux when requested", () => { + expect( + shouldUseLinuxFramelessWindow({ + argv: ["bb-nightly", LINUX_FRAMELESS_WINDOW_ARGUMENT], + platform: "linux", + }), + ).toBe(true); + }); + + it("keeps the native Linux frame by default", () => { + expect( + shouldUseLinuxFramelessWindow({ + argv: ["bb-nightly"], + platform: "linux", + }), + ).toBe(false); + }); + + it("ignores the Linux-only option on macOS and Windows", () => { + for (const platform of ["darwin", "win32"] as const) { + expect( + shouldUseLinuxFramelessWindow({ + argv: ["bb", LINUX_FRAMELESS_WINDOW_ARGUMENT], + platform, + }), + ).toBe(false); + } + }); +});