mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
#7097 force-disabled the custom title bar on all GNOME and hid the toggle, even though only GNOME+Wayland actually fails to render the Window-Controls-Overlay. This stranded GNOME-X11 users (where the feature worked) with no way to re-enable it. - Narrow the kill-switch from IS_GNOME_DESKTOP to a new IS_GNOME_WAYLAND (XDG_SESSION_TYPE/WAYLAND_DISPLAY, mirroring idle-time-handler). GNOME on X11 keeps the feature and the settings toggle. - Unify GNOME detection: preload imports it from common.const instead of re-implementing, so main and renderer can't drift. - Align global-theme.service with main-window: force native decorations on GNOME+Wayland in both, so a persisted/synced `true` no longer lays the custom header over native controls (doubled header). - Add unit tests for the detection matrix.
This commit is contained in:
parent
cbe3f1527f
commit
b2a68eddee
9 changed files with 134 additions and 43 deletions
62
electron/common-const.test.cjs
Normal file
62
electron/common-const.test.cjs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
|
||||
require('ts-node/register/transpile-only');
|
||||
|
||||
const {
|
||||
isGnomeDesktopEnv,
|
||||
isWaylandEnv,
|
||||
isGnomeWaylandEnv,
|
||||
} = require(path.resolve(__dirname, 'common.const.ts'));
|
||||
|
||||
test('isGnomeDesktopEnv: detects GNOME from desktop env vars', () => {
|
||||
assert.equal(isGnomeDesktopEnv('linux', { XDG_CURRENT_DESKTOP: 'GNOME' }), true);
|
||||
assert.equal(isGnomeDesktopEnv('linux', { XDG_CURRENT_DESKTOP: 'ubuntu:GNOME' }), true);
|
||||
assert.equal(isGnomeDesktopEnv('linux', { DESKTOP_SESSION: 'ubuntu' }), true);
|
||||
assert.equal(isGnomeDesktopEnv('linux', { XDG_SESSION_DESKTOP: 'gnome' }), true);
|
||||
});
|
||||
|
||||
test('isGnomeDesktopEnv: false for non-GNOME / non-linux', () => {
|
||||
assert.equal(isGnomeDesktopEnv('linux', { XDG_CURRENT_DESKTOP: 'KDE' }), false);
|
||||
assert.equal(isGnomeDesktopEnv('linux', {}), false);
|
||||
// env vars are present but platform is not linux
|
||||
assert.equal(isGnomeDesktopEnv('win32', { XDG_CURRENT_DESKTOP: 'GNOME' }), false);
|
||||
assert.equal(isGnomeDesktopEnv('darwin', { XDG_CURRENT_DESKTOP: 'GNOME' }), false);
|
||||
});
|
||||
|
||||
test('isWaylandEnv: detects Wayland via session type or display', () => {
|
||||
assert.equal(isWaylandEnv('linux', { XDG_SESSION_TYPE: 'wayland' }), true);
|
||||
// some sessions only set WAYLAND_DISPLAY
|
||||
assert.equal(isWaylandEnv('linux', { WAYLAND_DISPLAY: 'wayland-0' }), true);
|
||||
});
|
||||
|
||||
test('isWaylandEnv: false for X11 / non-linux', () => {
|
||||
assert.equal(isWaylandEnv('linux', { XDG_SESSION_TYPE: 'x11' }), false);
|
||||
assert.equal(isWaylandEnv('linux', {}), false);
|
||||
assert.equal(isWaylandEnv('win32', { WAYLAND_DISPLAY: 'wayland-0' }), false);
|
||||
});
|
||||
|
||||
test('isGnomeWaylandEnv: only true for GNOME AND Wayland together', () => {
|
||||
// GNOME + Wayland -> the one combination the kill-switch targets
|
||||
assert.equal(
|
||||
isGnomeWaylandEnv('linux', {
|
||||
XDG_CURRENT_DESKTOP: 'GNOME',
|
||||
XDG_SESSION_TYPE: 'wayland',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
// GNOME on X11 keeps the feature
|
||||
assert.equal(
|
||||
isGnomeWaylandEnv('linux', {
|
||||
XDG_CURRENT_DESKTOP: 'GNOME',
|
||||
XDG_SESSION_TYPE: 'x11',
|
||||
}),
|
||||
false,
|
||||
);
|
||||
// non-GNOME Wayland (e.g. KDE) is unaffected
|
||||
assert.equal(
|
||||
isGnomeWaylandEnv('linux', { XDG_CURRENT_DESKTOP: 'KDE', XDG_SESSION_TYPE: 'wayland' }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
|
@ -1,12 +1,35 @@
|
|||
export const IS_MAC = process.platform === 'darwin';
|
||||
export const IS_GNOME_DESKTOP =
|
||||
process.platform === 'linux' &&
|
||||
export const isGnomeDesktopEnv = (
|
||||
platform: NodeJS.Platform,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): boolean =>
|
||||
platform === 'linux' &&
|
||||
[
|
||||
process.env.XDG_CURRENT_DESKTOP,
|
||||
process.env.XDG_SESSION_DESKTOP,
|
||||
process.env.DESKTOP_SESSION,
|
||||
process.env.GNOME_SHELL_SESSION_MODE,
|
||||
env.XDG_CURRENT_DESKTOP,
|
||||
env.XDG_SESSION_DESKTOP,
|
||||
env.DESKTOP_SESSION,
|
||||
env.GNOME_SHELL_SESSION_MODE,
|
||||
]
|
||||
.filter((v): v is string => !!v)
|
||||
.map((v) => v.toLowerCase())
|
||||
.some((v) => v.includes('gnome') || v.includes('ubuntu'));
|
||||
|
||||
// Some sessions only set WAYLAND_DISPLAY and leave XDG_SESSION_TYPE unset, so
|
||||
// check both.
|
||||
export const isWaylandEnv = (
|
||||
platform: NodeJS.Platform,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): boolean =>
|
||||
platform === 'linux' && (env.XDG_SESSION_TYPE === 'wayland' || !!env.WAYLAND_DISPLAY);
|
||||
|
||||
// Only GNOME on Wayland reliably fails to render the Window-Controls-Overlay
|
||||
// when titleBarStyle is 'hidden', stranding the window with no min/max/close
|
||||
// controls. The custom-title-bar kill-switch is narrowed to this combination
|
||||
// so GNOME-on-X11 (where the feature works) keeps it.
|
||||
export const isGnomeWaylandEnv = (
|
||||
platform: NodeJS.Platform,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): boolean => isGnomeDesktopEnv(platform, env) && isWaylandEnv(platform, env);
|
||||
|
||||
export const IS_MAC = process.platform === 'darwin';
|
||||
export const IS_GNOME_DESKTOP = isGnomeDesktopEnv(process.platform, process.env);
|
||||
export const IS_GNOME_WAYLAND = isGnomeWaylandEnv(process.platform, process.env);
|
||||
|
|
|
|||
2
electron/electronAPI.d.ts
vendored
2
electron/electronAPI.d.ts
vendored
|
|
@ -136,6 +136,8 @@ export interface ElectronAPI {
|
|||
|
||||
isGnomeDesktop(): boolean;
|
||||
|
||||
isGnomeWayland(): boolean;
|
||||
|
||||
isMacOS(): boolean;
|
||||
|
||||
isAppleSilicon(): boolean;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { IPC } from './shared-with-frontend/ipc-events.const';
|
|||
import { isExternalUrlSchemeAllowed } from './shared-with-frontend/is-external-url-allowed';
|
||||
import { readFileSync, stat, writeFileSync } from 'fs';
|
||||
import { error, log } from 'electron-log/main';
|
||||
import { IS_MAC, IS_GNOME_DESKTOP } from './common.const';
|
||||
import { IS_MAC, IS_GNOME_WAYLAND } from './common.const';
|
||||
import {
|
||||
destroyTaskWidget,
|
||||
getIsTaskWidgetAlwaysShow,
|
||||
|
|
@ -148,10 +148,12 @@ export const createWindow = async ({
|
|||
const userPrefersCustomWindowTitleBar =
|
||||
persistedIsUseCustomWindowTitleBar ??
|
||||
legacyIsUseObsidianStyleHeader ??
|
||||
!IS_GNOME_DESKTOP;
|
||||
// GNOME + Wayland combinations can miss native controls when titleBarStyle is hidden.
|
||||
// Force native decorations on GNOME to keep window controls available.
|
||||
const isUseCustomWindowTitleBar = IS_GNOME_DESKTOP
|
||||
!IS_GNOME_WAYLAND;
|
||||
// GNOME + Wayland can't render the Window-Controls-Overlay when titleBarStyle
|
||||
// is 'hidden', leaving the window with no min/max/close controls. Force native
|
||||
// decorations only for that combination; GNOME-on-X11 and every other desktop
|
||||
// honor the user's preference. Keep in sync with global-theme.service.ts.
|
||||
const isUseCustomWindowTitleBar = IS_GNOME_WAYLAND
|
||||
? false
|
||||
: userPrefersCustomWindowTitleBar;
|
||||
// On macOS use 'hiddenInset' so AppKit positions the traffic lights at the
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
webUtils,
|
||||
} from 'electron';
|
||||
import { ElectronAPI } from './electronAPI.d';
|
||||
import { IS_GNOME_DESKTOP, IS_GNOME_WAYLAND } from './common.const';
|
||||
import { IPC, IPCEventValue } from './shared-with-frontend/ipc-events.const';
|
||||
import {
|
||||
getDistChannel,
|
||||
|
|
@ -30,25 +31,6 @@ const _invoke: (channel: IPCEventValue, ...args: unknown[]) => Promise<unknown>
|
|||
...args
|
||||
) => ipcRenderer.invoke(channel, ...args);
|
||||
|
||||
const isGnomeDesktop = (): boolean => {
|
||||
if (process.platform !== 'linux') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const desktopValues = [
|
||||
process.env.XDG_CURRENT_DESKTOP,
|
||||
process.env.XDG_SESSION_DESKTOP,
|
||||
process.env.DESKTOP_SESSION,
|
||||
process.env.GNOME_SHELL_SESSION_MODE,
|
||||
]
|
||||
.filter((value): value is string => !!value)
|
||||
.map((value) => value.toLowerCase());
|
||||
|
||||
return desktopValues.some(
|
||||
(value) => value.includes('gnome') || value.includes('ubuntu'),
|
||||
);
|
||||
};
|
||||
|
||||
const ea: ElectronAPI = {
|
||||
on: (
|
||||
channel: string,
|
||||
|
|
@ -108,7 +90,8 @@ const ea: ElectronAPI = {
|
|||
},
|
||||
getZoomFactor: () => webFrame.getZoomFactor(),
|
||||
isLinux: () => process.platform === 'linux',
|
||||
isGnomeDesktop,
|
||||
isGnomeDesktop: () => IS_GNOME_DESKTOP,
|
||||
isGnomeWayland: () => IS_GNOME_WAYLAND,
|
||||
isMacOS: () => process.platform === 'darwin',
|
||||
isAppleSilicon: () => process.platform === 'darwin' && process.arch === 'arm64',
|
||||
isSnap: () => process && process.env && !!process.env.SNAP,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ export const IS_ELECTRON_TOKEN = new InjectionToken<boolean>('IS_ELECTRON', {
|
|||
});
|
||||
// effectively IS_BROWSER
|
||||
export const IS_WEB_BROWSER = !IS_ELECTRON && !IS_ANDROID_WEB_VIEW;
|
||||
export const IS_GNOME_DESKTOP = IS_ELECTRON && window.ea.isGnomeDesktop();
|
||||
// Only GNOME+Wayland force-disables the custom title bar (the Window-Controls-
|
||||
// Overlay won't render there). Mirrors electron/common.const.ts so the renderer
|
||||
// and main process agree. See global-theme.service.ts / main-window.ts.
|
||||
// (window.ea.isGnomeDesktop() still exists on the bridge for plugin use.)
|
||||
export const IS_GNOME_WAYLAND = IS_ELECTRON && window.ea.isGnomeWayland();
|
||||
// True only inside the Electron build — preload exposes process.arch.
|
||||
// Web builds can't reliably distinguish Apple Silicon from Intel and stay false.
|
||||
export const IS_APPLE_SILICON = IS_ELECTRON && window.ea.isAppleSilicon();
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
untracked,
|
||||
} from '@angular/core';
|
||||
import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop';
|
||||
import { BodyClass, IS_ELECTRON, IS_GNOME_DESKTOP } from '../../app.constants';
|
||||
import { BodyClass, IS_ELECTRON, IS_GNOME_WAYLAND } from '../../app.constants';
|
||||
import { IS_MAC } from '../../util/is-mac';
|
||||
import { distinctUntilChanged, map, startWith, switchMap, take } from 'rxjs/operators';
|
||||
import { IS_TOUCH_ONLY } from '../../util/is-touch-only';
|
||||
|
|
@ -93,8 +93,22 @@ export class GlobalThemeService {
|
|||
private _iosViewportChangeRaf: number | null = null;
|
||||
|
||||
private _isCustomWindowTitleBarEnabled(): boolean {
|
||||
// The main process (main-window.ts) force-disables the custom title bar on
|
||||
// GNOME+Wayland because the Window-Controls-Overlay won't render there.
|
||||
// Mirror that here so we never lay the custom header on top of native
|
||||
// decorations, which would produce a doubled header.
|
||||
if (IS_GNOME_WAYLAND) {
|
||||
return false;
|
||||
}
|
||||
// Default ON to match main-window's `?? !IS_GNOME_WAYLAND` default.
|
||||
// KNOWN RESIDUAL: main-window also honors the legacy `isUseObsidianStyleHeader`
|
||||
// SimpleStore field, which is never mirrored into global config, so it isn't
|
||||
// visible here. A user who explicitly disabled the *old* header and never set
|
||||
// the new toggle gets native decorations + custom header (doubled). This is a
|
||||
// pre-existing divergence (already present for non-GNOME) and is self-healing:
|
||||
// the now-visible Misc toggle lets them turn the custom header off.
|
||||
const misc = this._globalConfigService.misc();
|
||||
return misc?.isUseCustomWindowTitleBar ?? !IS_GNOME_DESKTOP;
|
||||
return misc?.isUseCustomWindowTitleBar ?? true;
|
||||
}
|
||||
|
||||
darkMode = signal<DarkModeCfg>(
|
||||
|
|
|
|||
|
|
@ -63,8 +63,9 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfigState = {
|
|||
// NOTE: isUseCustomWindowTitleBar is intentionally NOT defaulted here. A
|
||||
// persisted default would be pushed to Electron on every launch and override
|
||||
// a legacy `isUseObsidianStyleHeader` choice. Its effective default is resolved
|
||||
// at read time (main-window.ts / global-theme.service.ts: `?? !IS_GNOME_DESKTOP`)
|
||||
// and the settings checkbox is seeded display-only in misc-settings-form (#7891).
|
||||
// at read time (main-window.ts / global-theme.service.ts: defaults on, forced
|
||||
// off only on GNOME+Wayland) and the settings checkbox is seeded display-only
|
||||
// in misc-settings-form (#7891).
|
||||
isShowProductivityTipLonger: false,
|
||||
customTheme: 'default',
|
||||
defaultStartPage: 0,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
MiscConfig,
|
||||
} from '../global-config.model';
|
||||
import { T } from '../../../t.const';
|
||||
import { IS_ELECTRON, IS_GNOME_DESKTOP } from '../../../app.constants';
|
||||
import { IS_ELECTRON, IS_GNOME_WAYLAND } from '../../../app.constants';
|
||||
import { isValidSplitTime } from '../../../util/is-valid-split-time';
|
||||
|
||||
export const MISC_SETTINGS_FORM_CFG: ConfigFormSection<MiscConfig> = {
|
||||
|
|
@ -102,21 +102,21 @@ export const MISC_SETTINGS_FORM_CFG: ConfigFormSection<MiscConfig> = {
|
|||
label: T.GCF.MISC.IS_TRAY_SHOW_CURRENT_COUNTDOWN,
|
||||
},
|
||||
},
|
||||
...((IS_ELECTRON && !IS_GNOME_DESKTOP
|
||||
...((IS_ELECTRON && !IS_GNOME_WAYLAND
|
||||
? [
|
||||
{
|
||||
key: 'isUseCustomWindowTitleBar',
|
||||
type: 'checkbox',
|
||||
// Display-only default: seed the checkbox so it reflects the actual
|
||||
// window state on a fresh install (the custom title bar is on by
|
||||
// default here -- this field is only shown on non-GNOME Electron, see
|
||||
// the enclosing guard). formly seeds the control without an initial
|
||||
// modelChange, so nothing is persisted on load.
|
||||
// default here -- this field is hidden only on GNOME+Wayland, where the
|
||||
// main process force-disables it, see the enclosing guard). formly seeds
|
||||
// the control without an initial modelChange, so nothing is persisted on load.
|
||||
// KNOWN RESIDUAL (#7891): saving any *other* Misc setting emits the
|
||||
// whole model and persists this seeded value too. We accept that over a
|
||||
// persisted DEFAULT_GLOBAL_CONFIG default, which would be pushed to
|
||||
// Electron on *every* launch and override a legacy `isUseObsidianStyleHeader`
|
||||
// choice. So a pre-2025-12 non-GNOME user who had disabled the old
|
||||
// choice. So a pre-2025-12 user who had disabled the old
|
||||
// header may see it re-enabled after editing Misc settings (reversible here).
|
||||
defaultValue: true,
|
||||
templateOptions: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue