mirror of
https://github.com/eugeny/tabby
synced 2026-09-27 07:25:43 +00:00
feat: Add Windows 11 Acrylic window support (#11481)
Co-authored-by: Eugene <inbox@null.page>
This commit is contained in:
+2
-2
@@ -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
|
||||
}
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
|
||||
|
||||
+1
-1
@@ -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 () {
|
||||
|
||||
+2
-2
@@ -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) {
|
||||
|
||||
+76
-45
@@ -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<void> { 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<void> {
|
||||
if (!this.window) {
|
||||
return
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
// Lose focus
|
||||
Menu.sendActionToFirstResponder('hide:')
|
||||
@@ -270,12 +279,17 @@ export class Window {
|
||||
}
|
||||
|
||||
async show (): Promise<void> {
|
||||
await this.enableDockedWindowStyles(this.isDockedOnTop())
|
||||
if (!this.window) {
|
||||
return
|
||||
} await this.enableDockedWindowStyles(this.isDockedOnTop())
|
||||
this.window.show()
|
||||
this.window.focus()
|
||||
}
|
||||
|
||||
async present (): Promise<void> {
|
||||
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<void>(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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
+8
-5
@@ -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<void>[] = []
|
||||
|
||||
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<PluginInfo[]> {
|
||||
|
||||
export async function loadPlugins (foundPlugins: PluginInfo[], progress: ProgressCallback): Promise<any[]> {
|
||||
const plugins: any[] = []
|
||||
const pluginsPromises: Promise<any>[] = []
|
||||
const pluginsPromises: Promise<void>[] = []
|
||||
|
||||
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)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedParameters": true,
|
||||
"noUnusedLocals": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": [
|
||||
"dom",
|
||||
"es2015",
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user