From b176ca47fc190e27d17a81f71863350e67b02f3c Mon Sep 17 00:00:00 2001 From: nephalemsec Date: Fri, 12 Jun 2026 20:41:17 +1000 Subject: [PATCH 01/10] feat(linux): window decorations toggle with custom close button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `window_decorations` general-setting (default on). When toggled off on Linux the native titlebar is removed and a custom in-app close button (WindowControls, in the Settings titlebar) takes over — useful on tiling WMs. - config: window_decorations field (serde default true) - capability: core:window:allow-set-decorations (+ minimize/maximize); without it setDecorations() silently no-ops - lib.rs: apply the saved preference at startup (Linux) - Overview: a "Window Decorations" Switch beside "Show in Dock" (Linux only) - Settings: custom close button shown when decorations are off --- src-tauri/capabilities/default.json | 4 ++ src-tauri/src/config.rs | 6 +++ src-tauri/src/lib.rs | 11 ++++++ src/lib/components/OverviewPane.svelte | 29 ++++++++++++++ src/lib/components/WindowControls.svelte | 49 ++++++++++++++++++++++++ src/lib/stores/config.svelte.ts | 6 +++ src/lib/windows/Settings.svelte | 20 ++++++++++ 7 files changed, 125 insertions(+) create mode 100644 src/lib/components/WindowControls.svelte diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 82af4f9..7099050 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -16,6 +16,10 @@ "core:window:allow-center", "core:window:allow-set-position", "core:window:allow-set-size", + "core:window:allow-set-decorations", + "core:window:allow-minimize", + "core:window:allow-maximize", + "core:window:allow-toggle-maximize", "global-shortcut:default", "dialog:default", "fs:default", diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 944f310..99f964a 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -325,6 +325,10 @@ pub struct GeneralConfig { pub show_recording_indicator: bool, /// Visual style for the recording indicator pub indicator_style: IndicatorStyle, + /// Show native window decorations (Linux). When off, a custom in-app close + /// button is shown instead. macOS uses native traffic lights regardless. + #[serde(default = "default_true")] + pub window_decorations: bool, /// App version recorded on the most recent run. /// /// Used to detect that an update has been applied: when this differs from @@ -345,6 +349,7 @@ impl Default for GeneralConfig { check_for_updates: true, show_recording_indicator: true, indicator_style: IndicatorStyle::default(), + window_decorations: true, last_run_version: None, } } @@ -998,6 +1003,7 @@ mod tests { check_for_updates: true, show_recording_indicator: true, indicator_style: IndicatorStyle::CursorDot, + window_decorations: true, last_run_version: None, }, recorder: RecorderConfig { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 029ef73..4daf5d4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -278,6 +278,17 @@ pub fn run() { #[cfg(target_os = "linux")] text_insert::emit_linux_typing_advisory(&app_handle); + // Linux: apply the saved window-decoration preference at startup. + // With decorations off, the custom in-app close button (the + // WindowControls component) takes over. + #[cfg(target_os = "linux")] + if !cfg.general.window_decorations { + if let Some(window) = app.get_webview_window("main") { + let _ = window.set_decorations(false); + tracing::info!("Window decorations disabled per config"); + } + } + // Start the Local Control API if enabled in config. The API and // MCP server default on, so a fresh config arrives here enabled // but with no token — generate and persist one so it just works diff --git a/src/lib/components/OverviewPane.svelte b/src/lib/components/OverviewPane.svelte index 66fd004..079cfae 100644 --- a/src/lib/components/OverviewPane.svelte +++ b/src/lib/components/OverviewPane.svelte @@ -7,6 +7,7 @@ */ import { onMount, onDestroy } from 'svelte'; import { invoke } from '@tauri-apps/api/core'; + import { getCurrentWindow } from '@tauri-apps/api/window'; import { listen, type UnlistenFn } from '@tauri-apps/api/event'; import { getVersion } from '@tauri-apps/api/app'; import { enable, disable, isEnabled } from '@tauri-apps/plugin-autostart'; @@ -97,6 +98,10 @@ let showInDock = $state(false); let dockLoading = $state(false); + /** Window decorations (Linux): native titlebar vs. custom close button */ + const isLinux = /Linux/.test(navigator.userAgent); + let windowDecorations = $state(configStore.general.windowDecorations ?? true); + /** Permission states */ let microphonePermission = $state<'unknown' | 'granted' | 'denied' | 'not_determined'>('unknown'); let accessibilityPermission = $state<'unknown' | 'granted' | 'denied' | 'stale'>('unknown'); @@ -175,6 +180,18 @@ } } + async function handleDecorationToggle(checked: boolean) { + windowDecorations = checked; + try { + await getCurrentWindow().setDecorations(checked); + configStore.general.windowDecorations = checked; + await configStore.save(); + } catch (error) { + console.error('Failed to toggle window decorations:', error); + windowDecorations = !checked; // revert on failure + } + } + let permissionChangedUnlisten: UnlistenFn | null = null; async function checkPermissions() { @@ -763,6 +780,12 @@ Show in Dock + {#if isLinux} +
+ Window Decorations + +
+ {/if} {:else if stats} @@ -1029,6 +1052,12 @@ Show in Dock + {#if isLinux} +
+ Window Decorations + +
+ {/if} diff --git a/src/lib/components/WindowControls.svelte b/src/lib/components/WindowControls.svelte new file mode 100644 index 0000000..cfe4e5a --- /dev/null +++ b/src/lib/components/WindowControls.svelte @@ -0,0 +1,49 @@ + + +
+ +
+ + diff --git a/src/lib/stores/config.svelte.ts b/src/lib/stores/config.svelte.ts index ab7f1ac..943cc30 100644 --- a/src/lib/stores/config.svelte.ts +++ b/src/lib/stores/config.svelte.ts @@ -105,6 +105,8 @@ export interface GeneralConfig { showRecordingIndicator: boolean; /** Visual style for the recording indicator */ indicatorStyle: IndicatorStyle; + /** Show native window decorations (Linux); custom close button when off */ + windowDecorations: boolean; } /** Recorder window position options */ @@ -192,6 +194,7 @@ interface ConfigRaw { check_for_updates: boolean; show_recording_indicator: boolean; indicator_style: IndicatorStyle; + window_decorations: boolean; }; recorder: { position: RecorderPosition; @@ -251,6 +254,7 @@ function parseConfig(raw: ConfigRaw): Config { checkForUpdates: raw.general.check_for_updates, showRecordingIndicator: raw.general.show_recording_indicator, indicatorStyle: raw.general.indicator_style, + windowDecorations: raw.general.window_decorations ?? true, }, recorder: { position: raw.recorder.position, @@ -311,6 +315,7 @@ function serialiseConfig(config: Config): ConfigRaw { check_for_updates: config.general.checkForUpdates, show_recording_indicator: config.general.showRecordingIndicator, indicator_style: config.general.indicatorStyle, + window_decorations: config.general.windowDecorations, }, recorder: { position: config.recorder.position, @@ -371,6 +376,7 @@ function getDefaultConfig(): Config { checkForUpdates: true, showRecordingIndicator: true, indicatorStyle: 'cursor-dot', + windowDecorations: true, }, recorder: { position: 'top-right', diff --git a/src/lib/windows/Settings.svelte b/src/lib/windows/Settings.svelte index cba9f52..cc4eb75 100644 --- a/src/lib/windows/Settings.svelte +++ b/src/lib/windows/Settings.svelte @@ -39,6 +39,7 @@ import { soundStore } from '../stores/sound.svelte'; import { Button } from '$components/ui/button'; import { Switch } from '$components/ui/switch'; + import WindowControls from '../components/WindowControls.svelte'; /** Settings pane definition */ interface SettingsPane { @@ -75,6 +76,10 @@ /** About dialog visibility */ let showAbout = $state(false); + /** Linux only — show the custom close button when decorations are disabled. */ + const isLinux = /Linux/.test(navigator.userAgent); + const showWindowControls = $derived(isLinux && !configStore.general.windowDecorations); + /** Map of shortcut IDs to their pending (unsaved) accelerators */ const pendingChanges = $state>(new Map()); @@ -260,6 +265,11 @@ {:else if pipelineStore.isProcessing} Processing... {/if} + {#if showWindowControls} +
+ +
+ {/if}
@@ -719,6 +729,16 @@ user-select: none; } + /* Pinned to the right edge so the centred title stays centred. */ + .title-bar-controls { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + display: flex; + align-items: center; + } + .main-area { display: flex; flex: 1; From 12ece40b97989422dd69cfe70769e9bb7d77fcce Mon Sep 17 00:00:00 2001 From: nephalemsec Date: Fri, 12 Jun 2026 20:41:17 +1000 Subject: [PATCH 02/10] fix(dev): keep Tailwind out of Svelte component styles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under Vite 8 + @tailwindcss/vite 4.3 + vite-plugin-svelte 7, the Tailwind transform matches Svelte's `&lang.css` virtual modules and intermittently receives the raw `.svelte` source, throwing `Invalid declaration: \`invoke\`` 500s in the dev server. Exclude `.svelte?` modules from Tailwind's transform — no component style uses Tailwind directives (sole entry is src/app.css), so output is unchanged. --- vite.config.ts | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index b157e31..3d131fe 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,13 +1,42 @@ import { defineConfig } from 'vite'; import { sveltekit } from '@sveltejs/kit/vite'; import tailwindcss from '@tailwindcss/vite'; +import type { Plugin } from 'vite'; // @ts-expect-error process is a nodejs global const host = process.env.TAURI_DEV_HOST; +/** + * Keep Tailwind out of Svelte's scoped component styles. + * + * A Svelte `