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',