From 2360c5a71fa52fd486b063ab50dde6bf193cff5a Mon Sep 17 00:00:00 2001 From: Patrick Dewey Date: Sat, 13 Jun 2026 16:25:50 -0400 Subject: [PATCH] add optional native Linux title bar setting --- apps/desktop/src/main/index.ts | 28 +++++++++++++--- apps/desktop/src/main/vault.ts | 4 +++ apps/desktop/src/preload/index.ts | 4 +++ packages/app-core/src/App.tsx | 7 ++-- .../app-core/src/components/SettingsModal.tsx | 32 +++++++++++++++++++ packages/bridge-contract/src/bridge.ts | 2 ++ packages/bridge-contract/src/ipc.ts | 2 ++ 7 files changed, 72 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index f9d36958..63950646 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -209,6 +209,7 @@ const MAX_ZOOM_FACTOR = 3 const ZOOM_STEP = 0.1 const MAC_WINDOW_BACKGROUND_COLOR = '#1f1f1f' const MAIN_WINDOW_TABBING_IDENTIFIER = 'zennotes-vault-window' + const APP_WEBSITE_URL = 'https://zennotes.org' const APP_DISCORD_URL = 'https://discord.gg/W4fWzapKS6' const APP_REPOSITORY_URL = 'https://github.com/ZenNotes/zennotes' @@ -855,6 +856,7 @@ async function createWindow(options: CreateWindowOptions = {}): Promise => { + const cfg = await loadConfig() + return process.platform === 'linux' ? cfg.hideNativeTitleBar !== false : true + }) + + handle( + IPC.APP_SET_WINDOW_CHROME_SETTINGS, + async (_e, hide: boolean): Promise => { + const hideNativeTitleBar = hide !== false + await updateConfig((cfg) => ({ ...cfg, hideNativeTitleBar })) + return process.platform === 'linux' ? hideNativeTitleBar : true + } + ) + handle(IPC.WINDOW_TOGGLE_QUICK_CAPTURE, async () => { toggleQuickCaptureWindow() }) diff --git a/apps/desktop/src/main/vault.ts b/apps/desktop/src/main/vault.ts index 1fd2dc91..326ed672 100644 --- a/apps/desktop/src/main/vault.ts +++ b/apps/desktop/src/main/vault.ts @@ -212,6 +212,8 @@ export interface PersistedConfig { remoteWorkspaceProfiles: PersistedRemoteWorkspaceProfile[] windowState: PersistedWindowState | null zoomFactor: number + /** Hide the native Linux/GTK title bar and use ZenNotes' custom renderer chrome. */ + hideNativeTitleBar?: boolean /** Electron accelerator string for the system-wide quick capture hotkey. * Empty string disables the global shortcut. */ quickCaptureHotkey: string @@ -231,6 +233,7 @@ const DEFAULT_CONFIG: PersistedConfig = { remoteWorkspaceProfiles: [], windowState: null, zoomFactor: 1, + hideNativeTitleBar: true, quickCaptureHotkey: DEFAULT_QUICK_CAPTURE_HOTKEY, quickCapturePinned: false } @@ -367,6 +370,7 @@ function normalizePersistedConfig(value: unknown): PersistedConfig { remoteWorkspaceProfiles, windowState: normalizeWindowState(candidate.windowState), zoomFactor, + hideNativeTitleBar: candidate.hideNativeTitleBar !== false, quickCaptureHotkey, quickCapturePinned } diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index b27a0975..8a3d0fec 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -447,6 +447,10 @@ const api: ZenBridge = { ipcRenderer.invoke(IPC.APP_MOVE_EXTERNAL_FILE_TO_VAULT), openMarkdownFile: (absPath: string): Promise => ipcRenderer.invoke(IPC.APP_OPEN_MARKDOWN_FILE, absPath), + getHideNativeTitleBar: (): Promise => + ipcRenderer.invoke(IPC.APP_GET_WINDOW_CHROME_SETTINGS), + setHideNativeTitleBar: (hide: boolean): Promise => + ipcRenderer.invoke(IPC.APP_SET_WINDOW_CHROME_SETTINGS, hide), toggleQuickCapture: (): Promise => ipcRenderer.invoke(IPC.WINDOW_TOGGLE_QUICK_CAPTURE), getQuickCaptureHotkey: (): Promise => diff --git a/packages/app-core/src/App.tsx b/packages/app-core/src/App.tsx index 8a916bb4..c5701c28 100644 --- a/packages/app-core/src/App.tsx +++ b/packages/app-core/src/App.tsx @@ -25,6 +25,7 @@ const EDITOR_MODULE_WARMUP_GRACE_MS = 40 let searchPaletteModulePromise: Promise | null = null const SEARCH_PALETTE_MODULE_WARMUP_DELAY_MS = 40 const ASSET_UNDO_SHORTCUT_GRACE_MS = 30_000 +const USE_NATIVE_TITLE_BAR = new URLSearchParams(window.location.search).get('nativeTitleBar') === '1' function loadEditorModule(): Promise { editorModulePromise ??= import('./components/Editor') @@ -670,7 +671,7 @@ function App(): JSX.Element { if (!hasCompletedOnboarding) { return (
- {!zenMode && } + {!zenMode && !USE_NATIVE_TITLE_BAR && } }> @@ -685,7 +686,7 @@ function App(): JSX.Element { if (!vault) { return (
- {!zenMode && } + {!zenMode && !USE_NATIVE_TITLE_BAR && } }> @@ -699,7 +700,7 @@ function App(): JSX.Element { return (
- {!zenMode && } + {!zenMode && !USE_NATIVE_TITLE_BAR && }
{!zenMode && sidebarOpen && } {!zenMode && noteListOpen && !unifiedSidebar && } diff --git a/packages/app-core/src/components/SettingsModal.tsx b/packages/app-core/src/components/SettingsModal.tsx index 346064cc..a2228854 100644 --- a/packages/app-core/src/components/SettingsModal.tsx +++ b/packages/app-core/src/components/SettingsModal.tsx @@ -194,6 +194,7 @@ function formatReleaseNotesForDisplay(notes: string | null): string | null { export function SettingsModal(): JSX.Element { const zenBridge = getZenBridge() const appInfo = zenBridge.getAppInfo() + const isLinuxDesktop = appInfo.runtime === 'desktop' && zenBridge.platformSync() === 'linux' const supportsRemoteWorkspace = appInfo.runtime === 'desktop' && zenBridge.getCapabilities().supportsRemoteWorkspace const setSettingsOpen = useStore((s) => s.setSettingsOpen) @@ -351,6 +352,7 @@ export function SettingsModal(): JSX.Element { const setDarkSidebar = useStore((s) => s.setDarkSidebar) const showSidebarChevrons = useStore((s) => s.showSidebarChevrons) const setShowSidebarChevrons = useStore((s) => s.setShowSidebarChevrons) + const [hideNativeTitleBar, setHideNativeTitleBar] = useState(true) const appUpdateState = useAppUpdateState() const [editingRemoteProfile, setEditingRemoteProfile] = useState<{ mode: 'create' | 'edit' @@ -380,6 +382,20 @@ export function SettingsModal(): JSX.Element { } }, []) + useEffect(() => { + if (!isLinuxDesktop || !zenBridge.getHideNativeTitleBar) return + void zenBridge.getHideNativeTitleBar().then( + (hide) => setHideNativeTitleBar(hide), + () => setHideNativeTitleBar(true) + ) + }, [isLinuxDesktop, zenBridge]) + + const persistHideNativeTitleBar = (hideNativeTitleBar: boolean): void => { + setHideNativeTitleBar(hideNativeTitleBar) + if (!zenBridge.setHideNativeTitleBar) return + void zenBridge.setHideNativeTitleBar(hideNativeTitleBar).then(setHideNativeTitleBar) + } + useEffect(() => { let cancelled = false if (typeof window.zen.getVaultTextSearchCapabilities !== 'function') { @@ -749,6 +765,13 @@ export function SettingsModal(): JSX.Element { title: 'Sidebar arrows', description: 'Show disclosure arrows for collapsible folders and sidebar sections.', keywords: ['chevrons', 'disclosure'] + }, + { + id: 'native-linux-title-bar', + title: 'Native Linux title bar', + description: 'Use the GTK title bar instead of ZenNotes custom window chrome after restart.', + keywords: ['linux', 'gtk', 'title bar', 'header bar', 'native', 'window chrome'], + available: isLinuxDesktop } ], content: ( @@ -850,6 +873,15 @@ export function SettingsModal(): JSX.Element { settingId="sidebar-arrows" onChange={setShowSidebarChevrons} /> + {isLinuxDesktop && ( + persistHideNativeTitleBar(!useNative)} + /> + )}
) diff --git a/packages/bridge-contract/src/bridge.ts b/packages/bridge-contract/src/bridge.ts index 169d7476..e6970928 100644 --- a/packages/bridge-contract/src/bridge.ts +++ b/packages/bridge-contract/src/bridge.ts @@ -79,6 +79,8 @@ export interface ZenBridge { zoomInApp(): Promise zoomOutApp(): Promise resetAppZoom(): Promise + getHideNativeTitleBar?(): Promise + setHideNativeTitleBar?(hide: boolean): Promise getAppUpdateState(): Promise checkForAppUpdates(): Promise checkForAppUpdatesWithUi(): Promise diff --git a/packages/bridge-contract/src/ipc.ts b/packages/bridge-contract/src/ipc.ts index 6899638f..7d17254c 100644 --- a/packages/bridge-contract/src/ipc.ts +++ b/packages/bridge-contract/src/ipc.ts @@ -79,6 +79,8 @@ export const IPC = { APP_ZOOM_IN: 'app:zoom-in', APP_ZOOM_OUT: 'app:zoom-out', APP_ZOOM_RESET: 'app:zoom-reset', + APP_GET_WINDOW_CHROME_SETTINGS: 'app:get-window-chrome-settings', + APP_SET_WINDOW_CHROME_SETTINGS: 'app:set-window-chrome-settings', APP_UPDATER_GET_STATE: 'app-updater:get-state', APP_UPDATER_CHECK: 'app-updater:check', APP_UPDATER_CHECK_WITH_UI: 'app-updater:check-with-ui',