From 6193527bae96ccdd70ce2dfb2db49c9e1bbe06dd Mon Sep 17 00:00:00 2001 From: KaniPan <24561326+bluekani@users.noreply.github.com> Date: Fri, 25 Sep 2026 04:10:31 +0900 Subject: [PATCH] feat: Add Windows 11 Acrylic window support (#11481) Co-authored-by: Eugene --- app/lib/app.ts | 4 +- app/lib/index.ts | 2 +- app/lib/pty.ts | 4 +- app/lib/window.ts | 121 ++++++++++++++++++++++-------------- app/lib/windows.ts | 10 +++ app/package.json | 1 - app/src/plugins.ts | 13 ++-- app/tsconfig.json | 1 + app/webpack.config.main.mjs | 1 - app/yarn.lock | 12 ---- 10 files changed, 100 insertions(+), 69 deletions(-) create mode 100644 app/lib/windows.ts diff --git a/app/lib/app.ts b/app/lib/app.ts index 381f5fb0b..d0be20ae3 100644 --- a/app/lib/app.ts +++ b/app/lib/app.ts @@ -219,7 +219,7 @@ export class Application { return } this.tray?.destroy() - this.tray = null + this.tray = undefined } hasWindows (): boolean { @@ -374,7 +374,7 @@ export class Application { ] if (process.env.TABBY_DEV) { - template[2].submenu['unshift']({ role: 'reload' }) + template[2].submenu!['unshift']({ role: 'reload' }) } Menu.setApplicationMenu(Menu.buildFromTemplate(template)) diff --git a/app/lib/index.ts b/app/lib/index.ts index 7c8f1e150..fcef672e2 100644 --- a/app/lib/index.ts +++ b/app/lib/index.ts @@ -91,7 +91,7 @@ if (!app.requestSingleInstanceLock()) { app.on('ready', async () => { if (process.platform === 'darwin') { - app.dock.setMenu(Menu.buildFromTemplate([ + app.dock?.setMenu(Menu.buildFromTemplate([ { label: 'New window', click () { diff --git a/app/lib/pty.ts b/app/lib/pty.ts index cc002cf0a..31040ca76 100644 --- a/app/lib/pty.ts +++ b/app/lib/pty.ts @@ -44,11 +44,11 @@ class PTYDataQueue { return } - const buffersToSend = [] + const buffersToSend: Buffer[] = [] let totalLength = 0 while (totalLength < this.maxChunk && this.buffers.length) { totalLength += this.buffers[0].length - buffersToSend.push(this.buffers.shift()) + buffersToSend.push(this.buffers.shift()!) } if (buffersToSend.length === 0) { diff --git a/app/lib/window.ts b/app/lib/window.ts index 8715ffd19..60887675b 100644 --- a/app/lib/window.ts +++ b/app/lib/window.ts @@ -4,7 +4,6 @@ import { Subject, Observable, debounceTime } from 'rxjs' import { BrowserWindow, app, ipcMain, Rectangle, Menu, screen, BrowserWindowConstructorOptions, TouchBar, nativeImage, WebContents, nativeTheme } from 'electron' import ElectronConfig = require('electron-config') import { enable as enableRemote } from '@electron/remote/main' -import * as os from 'os' import * as path from 'path' import macOSRelease from 'macos-release' import { compare as compareVersions } from 'compare-versions' @@ -12,21 +11,20 @@ import { compare as compareVersions } from 'compare-versions' import type { Application } from './app' import { parseArgs } from './cli' import { parseTabbyURL, isTabbyURL } from './urlHandler' +import { isWindowsBuild } from './windows' -let DwmEnableBlurBehindWindow: any = null -if (process.platform === 'win32') { - DwmEnableBlurBehindWindow = require('@tabby-gang/windows-blurbehind').DwmEnableBlurBehindWindow -} export interface WindowOptions { hidden?: boolean } -abstract class GlasstronWindow extends BrowserWindow { - blurType: string - abstract setBlur (_: boolean) +type GlasstronWindow = BrowserWindow & { + blurType?: string | null + setBlur?: (_: boolean) => void } +const isWindows11 = isWindowsBuild(22621) + const macOSVibrancyType: any = process.platform === 'darwin' ? compareVersions(macOSRelease().version || '0.0', '10.14', '>=') ? 'fullscreen-ui' : 'dark' : null const activityIcon = nativeImage.createFromPath(`${app.getAppPath()}/assets/activity.png`) @@ -51,8 +49,8 @@ export class Window { get closed$ (): Observable { return this.closed } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - constructor (private application: Application, private configStore: any, options?: WindowOptions) { - options = options ?? {} + constructor (private application: Application, private configStore: any, _options?: WindowOptions) { + const options = _options ?? {} this.windowConfig = new ElectronConfig({ name: 'window' }) this.windowBounds = this.windowConfig.get('windowBoundaries') @@ -85,8 +83,8 @@ export class Window { const [left2, top2, right2, bottom2] = [closestDisplay.bounds.x, closestDisplay.bounds.y, closestDisplay.bounds.x + closestDisplay.bounds.width, closestDisplay.bounds.y + closestDisplay.bounds.height] if ((left2 > right1 || right2 < left1 || top2 > bottom1 || bottom2 < top1) && !maximized) { - bwOptions.x = closestDisplay.bounds.width / 2 - bwOptions.width / 2 - bwOptions.y = closestDisplay.bounds.height / 2 - bwOptions.height / 2 + bwOptions.x = closestDisplay.bounds.width / 2 - bwOptions.width! / 2 + bwOptions.y = closestDisplay.bounds.height / 2 - bwOptions.height! / 2 } } @@ -105,17 +103,17 @@ export class Window { bwOptions.visualEffectState = 'active' } - if (process.platform === 'darwin') { + if (process.platform === 'darwin' || isWindows11) { this.window = new BrowserWindow(bwOptions) as GlasstronWindow } else { - this.window = new glasstron.BrowserWindow(bwOptions) + this.window = new glasstron.BrowserWindow(bwOptions) as GlasstronWindow } this.webContents = this.window.webContents this.window.webContents.once('did-finish-load', () => { if (process.platform === 'darwin') { - this.window.setVibrancy(macOSVibrancyType) + this.window!.setVibrancy(macOSVibrancyType) } else if (process.platform === 'win32' && this.configStore.appearance?.vibrancy) { this.setVibrancy(true) } @@ -124,12 +122,12 @@ export class Window { if (!options.hidden) { if (maximized) { - this.window.maximize() + this.window!.maximize() } else { - this.window.show() + this.window!.show() } - this.window.focus() - this.window.moveTop() + this.window!.focus() + this.window!.moveTop() application.focus() } }) @@ -139,7 +137,7 @@ export class Window { (this.configStore.appearance?.dock ?? 'off') !== 'off' && this.configStore.appearance?.dockHideOnBlur && !BrowserWindow.getFocusedWindow() && - this.window.isEnabled() + this.window?.isEnabled() ) { this.hide() } @@ -172,7 +170,7 @@ export class Window { this.ready = new Promise(resolve => { const listener = event => { - if (event.sender === this.window.webContents) { + if (event.sender === this.window?.webContents) { ipcMain.removeListener('app:ready', listener as any) resolve() } @@ -183,28 +181,36 @@ export class Window { makeMain (): void { this.isMainWindow = true - this.window.webContents.send('host:became-main-window') + this.window?.webContents.send('host:became-main-window') } - setVibrancy (enabled: boolean, type?: string, userRequested?: boolean): void { + setVibrancy(enabled: boolean, type?: string, userRequested?: boolean): void { + if (!this.window) { + return + } if (userRequested ?? true) { this.lastVibrancy = { enabled, type } } if (process.platform === 'win32') { - if (parseFloat(os.release()) >= 10) { - this.window.blurType = enabled ? type === 'fluent' ? 'acrylic' : 'blurbehind' : null + if (isWindows11) { try { - this.window.setBlur(enabled) + this.window.setBackgroundMaterial(enabled ? 'acrylic' : 'none') + this.isFluentVibrancy = enabled + } catch (error) { + console.error('Failed to set window acrylic', error) + } + } else { + try { + this.window.blurType = enabled ? type === 'fluent' ? 'acrylic' : 'blurbehind' : null + this.window.setBlur?.(enabled) this.isFluentVibrancy = enabled && type === 'fluent' } catch (error) { console.error('Failed to set window blur', error) } - } else { - DwmEnableBlurBehindWindow(this.window.getNativeWindowHandle(), enabled) } } else if (process.platform === 'linux') { this.window.setBackgroundColor(enabled ? '#00000000' : '#131d27') - this.window.setBlur(enabled) + this.window.setBlur?.(enabled) } else { this.window.setVibrancy(enabled ? macOSVibrancyType : null) } @@ -223,7 +229,7 @@ export class Window { } focus (): void { - this.window.focus() + this.window?.focus() } send (event: string, ...args: any[]): void { @@ -242,11 +248,11 @@ export class Window { } isFocused (): boolean { - return this.window.isFocused() + return this.window!.isFocused() } isVisible (): boolean { - return this.window.isVisible() + return this.window!.isVisible() } isDockedOnTop (): boolean { @@ -254,6 +260,9 @@ export class Window { } async hide (): Promise { + if (!this.window) { + return + } if (process.platform === 'darwin') { // Lose focus Menu.sendActionToFirstResponder('hide:') @@ -270,12 +279,17 @@ export class Window { } async show (): Promise { - await this.enableDockedWindowStyles(this.isDockedOnTop()) + if (!this.window) { + return + } await this.enableDockedWindowStyles(this.isDockedOnTop()) this.window.show() this.window.focus() } async present (): Promise { + if (!this.window) { + return + } await this.show() this.window.moveTop() } @@ -290,10 +304,13 @@ export class Window { } private async enableDockedWindowStyles (enabled: boolean) { + if (!this.window) { + return + } if (process.platform === 'darwin') { if (enabled) { if (!this.dockHidden) { - app.dock.hide() + app.dock?.hide() this.dockHidden = true } this.window.setAlwaysOnTop(true, 'screen-saver', 1) @@ -305,7 +322,7 @@ export class Window { } } else { if (this.dockHidden) { - await app.dock.show() + await app.dock?.show() this.dockHidden = false } if (this.window.isAlwaysOnTop()) { @@ -322,6 +339,9 @@ export class Window { } private setupWindowManagement () { + if (!this.window) { + return + } this.window.on('show', () => { this.visible.next(true) this.send('host:window-shown') @@ -332,7 +352,7 @@ export class Window { }) const moveSubscription = new Observable(observer => { - this.window.on('move', () => observer.next()) + this.window?.on('move', () => observer.next()) }).pipe(debounceTime(250)).subscribe(() => { this.send('host:window-moved') }) @@ -354,7 +374,7 @@ export class Window { return } this.windowConfig.set('windowBoundaries', this.windowBounds) - this.windowConfig.set('maximized', this.window.isMaximized()) + this.windowConfig.set('maximized', this.window!.isMaximized()) }) this.window.on('closed', () => { @@ -362,20 +382,31 @@ export class Window { }) this.window.on('resize', () => { - if (!this.window.isMaximized()) { - this.windowBounds = this.window.getBounds() + if (!this.window?.isMaximized()) { + this.windowBounds = this.window?.getBounds() } }) this.window.on('move', () => { - if (!this.window.isMaximized()) { - this.windowBounds = this.window.getBounds() + if (!this.window?.isMaximized()) { + this.windowBounds = this.window?.getBounds() } }) this.window.on('focus', () => { - this.window.flashFrame(false) + this.window?.flashFrame(false) this.send('host:window-focused') + // Re-apply acrylic on Win11 when window gains focus + if (isWindows11 && this.lastVibrancy?.enabled) { + this.window?.setBackgroundMaterial('acrylic') + } + }) + + this.window.on('blur', () => { + // Re-apply acrylic on Win11 when window loses focus + if (isWindows11 && this.lastVibrancy?.enabled) { + this.window?.setBackgroundMaterial('acrylic') + } }) this.on('ready', () => { @@ -433,7 +464,7 @@ export class Window { this.on('window-close', () => { this.closing = true - this.window.close() + this.window?.close() }) this.on('window-set-touch-bar', (_, segments, selectedIndex) => { @@ -462,7 +493,7 @@ export class Window { clearTimeout(moveEndedTimeout) } moveEndedTimeout = setTimeout(() => { - this.setVibrancy(this.lastVibrancy.enabled, this.lastVibrancy.type) + this.setVibrancy(this.lastVibrancy!.enabled, this.lastVibrancy!.type) }, 50) } this.window.on('move', onBoundsChange) @@ -526,7 +557,7 @@ export class Window { } private destroy () { - this.window = null + this.window = undefined this.closed.next() this.visible.complete() this.closed.complete() diff --git a/app/lib/windows.ts b/app/lib/windows.ts new file mode 100644 index 000000000..3f78ce906 --- /dev/null +++ b/app/lib/windows.ts @@ -0,0 +1,10 @@ +import * as os from 'node:os' + +export function getWindows10Build(): number | undefined { + return process.platform === 'win32' && parseFloat(os.release()) >= 10 ? parseInt(os.release().split('.')[2], 10) : undefined +} + +export function isWindowsBuild (build: number): boolean { + const b = getWindows10Build() + return b !== undefined && b >= build +} diff --git a/app/package.json b/app/package.json index acb684c04..8df87186b 100644 --- a/app/package.json +++ b/app/package.json @@ -37,7 +37,6 @@ "yargs": "^18" }, "optionalDependencies": { - "@tabby-gang/windows-blurbehind": "^3", "@tabby-gang/windows-process-tree": "^0.6.1", "macos-native-processlist": "^2", "patch-package": "^8", diff --git a/app/src/plugins.ts b/app/src/plugins.ts index 2fe8cf86a..3a082418b 100644 --- a/app/src/plugins.ts +++ b/app/src/plugins.ts @@ -69,7 +69,7 @@ export type ProgressCallback = (current: number, total: number) => void export function initModuleLookup (userPluginsPath: string): void { global['module'].paths.map((x: string) => nodeModule.globalPaths.push(normalizePath(x))) - const paths = [] + const paths: string[] = [] paths.unshift(path.join(userPluginsPath, 'node_modules')) paths.unshift(path.join(remote.app.getAppPath(), 'node_modules')) @@ -108,7 +108,7 @@ async function getCandidateLocationsInPluginDir (pluginDir: any): Promise<{ plug }) } - const promises = [] + const promises: Promise[] = [] for (const packageName of pluginNames) { if ((packageName.startsWith(PLUGIN_PREFIX) || packageName.startsWith(LEGACY_PLUGIN_PREFIX)) && !PLUGIN_BLACKLIST.includes(packageName)) { @@ -128,10 +128,10 @@ async function getCandidateLocationsInPluginDir (pluginDir: any): Promise<{ plug return candidateLocations } -async function getPluginCandidateLocation (paths: any): Promise<{ pluginDir: string, packageName: string }[]> { +async function getPluginCandidateLocation (paths: string[]): Promise<{ pluginDir: string, packageName: string }[]> { const candidateLocationsPromises: Promise<{ pluginDir: string, packageName: string }[]>[] = [] - const processedPaths = [] + const processedPaths: string[] = [] for (let pluginDir of paths) { if (processedPaths.includes(pluginDir)) { @@ -228,7 +228,7 @@ export async function findPlugins (): Promise { export async function loadPlugins (foundPlugins: PluginInfo[], progress: ProgressCallback): Promise { const plugins: any[] = [] - const pluginsPromises: Promise[] = [] + const pluginsPromises: Promise[] = [] let index = 0 const setProgress = function () { @@ -239,6 +239,9 @@ export async function loadPlugins (foundPlugins: PluginInfo[], progress: Progres progress(0, 1) for (const foundPlugin of foundPlugins) { pluginsPromises.push(new Promise(x => { + if (!foundPlugin.path) { + return + } console.info(`Loading ${foundPlugin.name}: ${nodeRequire.resolve(foundPlugin.path)}`) try { const packageModule = nodeRequire(foundPlugin.path) diff --git a/app/tsconfig.json b/app/tsconfig.json index 2e66d9250..be4bfac20 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -15,6 +15,7 @@ "noFallthroughCasesInSwitch": true, "noUnusedParameters": true, "noUnusedLocals": true, + "strictNullChecks": true, "lib": [ "dom", "es2015", diff --git a/app/webpack.config.main.mjs b/app/webpack.config.main.mjs index 79975266a..5526521a6 100644 --- a/app/webpack.config.main.mjs +++ b/app/webpack.config.main.mjs @@ -52,7 +52,6 @@ const config = { util: 'commonjs util', 'windows-swca': 'commonjs windows-swca', 'windows-native-registry': 'commonjs windows-native-registry', - '@tabby-gang/windows-blurbehind': 'commonjs @tabby-gang/windows-blurbehind', 'yargs/yargs': 'commonjs yargs/yargs', }, plugins: [ diff --git a/app/yarn.lock b/app/yarn.lock index d9f5c1fd1..cb5138684 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -346,13 +346,6 @@ "@sigstore/core" "^3.2.1" "@sigstore/protobuf-specs" "^0.5.0" -"@tabby-gang/windows-blurbehind@^3": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@tabby-gang/windows-blurbehind/-/windows-blurbehind-3.1.0.tgz#bd1462f2a970d195e277d48cceb8b2c0a20f09bd" - integrity sha512-wTvyNrBDNxD4yq1bXv7lvXRQujJYRQ2Ke3LJyE9yzY9e/L9/fHVIuprIgAMiLsnW+BKPERq0k27iC38WnxUBZQ== - dependencies: - "@types/node" "^10.12.18" - "@tabby-gang/windows-process-tree@^0.6.1": version "0.6.1" resolved "https://registry.yarnpkg.com/@tabby-gang/windows-process-tree/-/windows-process-tree-0.6.1.tgz#2a5a5cbbee16f611fc61d53cbadf930c8d3d20a8" @@ -387,11 +380,6 @@ dependencies: undici-types "~6.21.0" -"@types/node@^10.12.18": - version "10.17.60" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" - integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== - "@types/semver@^7.3.6": version "7.8.0" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.8.0.tgz#0bfe3ec51f5e9615bc317174cd5b88ea08b7fc2f"