diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs
index c03a0e7..bc9ebb5 100644
--- a/src-tauri/src/ipc.rs
+++ b/src-tauri/src/ipc.rs
@@ -214,6 +214,23 @@ pub enum Theme {
Nord,
}
+#[derive(Clone, Debug, Serialize, Deserialize, TS)]
+#[serde(rename_all = "lowercase")]
+#[ts(export, export_to = "../../src/bindings/")]
+pub enum WindowControlStyle {
+ Auto,
+ Macos,
+ Windows,
+ Gnome,
+ Minimal,
+}
+
+impl Default for WindowControlStyle {
+ fn default() -> Self {
+ Self::Auto
+ }
+}
+
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
#[serde(rename_all = "lowercase")]
#[ts(export, export_to = "../../src/bindings/")]
@@ -295,6 +312,8 @@ pub struct SchedulerSettings {
pub struct PersistedSettings {
pub theme: Theme,
#[serde(default)]
+ pub window_control_style: WindowControlStyle,
+ #[serde(default)]
pub calendar_preference: CalendarPreference,
#[serde(default = "default_language_preference")]
pub language: String,
diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs
index f83d4d9..a9630fd 100644
--- a/src-tauri/src/settings.rs
+++ b/src-tauri/src/settings.rs
@@ -1,6 +1,6 @@
use crate::ipc::{
AppFontSize, CalendarPreference, ListRowDensity, MediaCookieSource, PersistedSettings,
- PostQueueAction, ProxyMode, SchedulerSettings, SettingsTab, Theme,
+ PostQueueAction, ProxyMode, SchedulerSettings, SettingsTab, Theme, WindowControlStyle,
};
use serde_json::{Map, Value};
use std::collections::HashMap;
@@ -192,6 +192,11 @@ fn sanitize_persisted_setting_values(state: &mut Value) {
"theme",
&["system", "light", "dark", "dracula", "nord"],
);
+ sanitize_allowed_string(
+ state,
+ "windowControlStyle",
+ &["auto", "macos", "windows", "gnome", "minimal"],
+ );
sanitize_allowed_string(
state,
"language",
@@ -417,6 +422,7 @@ fn derived_location_path(base: &str, subfolder: &str) -> String {
fn default_settings() -> PersistedSettings {
PersistedSettings {
theme: Theme::System,
+ window_control_style: WindowControlStyle::Auto,
calendar_preference: CalendarPreference::Gregorian,
language: "system".to_string(),
base_download_folder: "~/Downloads".to_string(),
@@ -472,6 +478,7 @@ fn default_settings() -> PersistedSettings {
#[cfg(test)]
mod tests {
+ use crate::ipc::WindowControlStyle;
use super::{
decode_stored_settings, default_settings, preserve_portable_pairing_token,
preserve_scheduler_runtime_keys,
@@ -678,6 +685,21 @@ mod tests {
assert_eq!(settings.sidebar_position, "auto");
}
+ #[test]
+ fn invalid_window_control_style_uses_automatic_layout() {
+ let stored = json!({
+ "state": {"windowControlStyle": "neon"},
+ "version": 5
+ });
+
+ let settings = decode_stored_settings(&Value::String(stored.to_string())).unwrap();
+
+ assert!(matches!(
+ settings.window_control_style,
+ WindowControlStyle::Auto
+ ));
+ }
+
#[test]
fn clamps_out_of_range_download_settings() {
let stored = json!({
diff --git a/src/App.tsx b/src/App.tsx
index 17c5567..d11a419 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -20,6 +20,7 @@ import { setLogStreamActive } from './utils/logger';
import { updateDockBadge } from './utils/dockBadge';
import { openUrl } from '@tauri-apps/plugin-opener';
import { getPlatformInfo, shouldUseCustomWindowControls, usePlatformInfo } from './utils/platform';
+import { resolveWindowControlStyle } from './utils/windowControlStyle';
import {
getKeychainAccessReady,
getKeychainConsentVersion,
@@ -187,6 +188,7 @@ function App() {
});
const theme = useSettingsStore(state => state.theme);
+ const windowControlStylePreference = useSettingsStore(state => state.windowControlStyle);
const languagePreference = useSettingsStore(state => state.language);
const isSidebarVisible = useSettingsStore(state => state.isSidebarVisible);
const sidebarPosition = useSettingsStore(state => state.sidebarPosition);
@@ -246,6 +248,7 @@ function App() {
const { addToast, removeToast } = useToast();
const isMacUserAgent = navigator.userAgent.includes('Mac');
const usesCustomWindowControls = shouldUseCustomWindowControls(platform.os, navigator.userAgent);
+ const windowControlStyle = resolveWindowControlStyle(windowControlStylePreference, platform.os, navigator.userAgent);
const isRtl = localeDirection(resolveAppLocale(i18n.language)) === 'rtl';
const isSidebarOnRight = sidebarPosition === 'right' || (sidebarPosition === 'auto' && isRtl);
// Keep dialogs out of the titlebar area while platform detection is still
@@ -1021,7 +1024,10 @@ function App() {
hasWindowChrome ? 'app-shell--window-chrome' : ''
}`}>
{usesCustomWindowControls && (
-
+
)}
, maxConcurrentDownloads: number, globalSpeedLimit: string, speedLimitPresetValues: Array, logsEnabled: boolean, isSidebarVisible: boolean, sidebarPosition: string, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, lastCustomSpeedLimitUnit: string, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, autoAddClipboardLinks: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, customUserAgent: string, askWhereToSaveEachFile: boolean, rememberLastUsedDownloadDirectory: boolean, preventsSleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array, autoCheckUpdates: boolean, keychainAccessGranted: boolean, };
+export type PersistedSettings = { theme: Theme, windowControlStyle: WindowControlStyle, calendarPreference: CalendarPreference, language: string, baseDownloadFolder: string, categorySubfoldersEnabled: boolean, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array, maxConcurrentDownloads: number, globalSpeedLimit: string, speedLimitPresetValues: Array, logsEnabled: boolean, isSidebarVisible: boolean, sidebarPosition: string, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, lastCustomSpeedLimitUnit: string, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, autoAddClipboardLinks: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, customUserAgent: string, askWhereToSaveEachFile: boolean, rememberLastUsedDownloadDirectory: boolean, preventsSleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array, autoCheckUpdates: boolean, keychainAccessGranted: boolean, };
diff --git a/src/bindings/WindowControlStyle.ts b/src/bindings/WindowControlStyle.ts
new file mode 100644
index 0000000..8fd7bb9
--- /dev/null
+++ b/src/bindings/WindowControlStyle.ts
@@ -0,0 +1,3 @@
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
+export type WindowControlStyle = "auto" | "macos" | "windows" | "gnome" | "minimal";
diff --git a/src/components/SettingsView.tsx b/src/components/SettingsView.tsx
index 74902d6..ce719bb 100644
--- a/src/components/SettingsView.tsx
+++ b/src/components/SettingsView.tsx
@@ -3,6 +3,7 @@ import {
type AppFontSize,
type ListRowDensity,
type SidebarPosition,
+ type WindowControlStyle,
type SettingsState,
SettingsTab,
runSettingsPersistenceTransaction,
@@ -698,6 +699,13 @@ runEngineChecks(false);
{ value: 'persian', label: t($ => $.settings.lookAndFeel.calendarPersian) },
{ value: 'hebrew', label: t($ => $.settings.lookAndFeel.calendarHebrew) },
] as const;
+ const windowControlStyleOptions: Array<{ value: WindowControlStyle; label: string }> = [
+ { value: 'auto', label: t($ => $.settings.lookAndFeel.windowControlStyleAutomatic) },
+ { value: 'macos', label: t($ => $.settings.lookAndFeel.windowControlStyleMacos) },
+ { value: 'windows', label: t($ => $.settings.lookAndFeel.windowControlStyleWindows) },
+ { value: 'gnome', label: t($ => $.settings.lookAndFeel.windowControlStyleGnome) },
+ { value: 'minimal', label: t($ => $.settings.lookAndFeel.windowControlStyleMinimal) },
+ ];
const TabButton = ({ type, icon: Icon, label }: { type: SettingsTab; icon: typeof Download; label: string }) => {
const active = activeTab === type;
@@ -907,6 +915,25 @@ runEngineChecks(false);
+ {t($ => $.settings.lookAndFeel.windowControls)}
+
+
+
+ {t($ => $.settings.lookAndFeel.windowControlStyle)}
+ {t($ => $.settings.lookAndFeel.windowControlsDescription)}
+
+
+
+
+
{t($ => $.settings.lookAndFeel.appTheme)}
diff --git a/src/components/WindowControls.tsx b/src/components/WindowControls.tsx
index 5e25fdb..6a0dda4 100644
--- a/src/components/WindowControls.tsx
+++ b/src/components/WindowControls.tsx
@@ -2,6 +2,7 @@ import { getCurrentWindow } from '@tauri-apps/api/window';
import { Maximize2, Minus, X } from 'lucide-react';
import type { PointerEvent } from 'react';
import { useTranslation } from 'react-i18next';
+import type { ResolvedWindowControlStyle } from '../utils/windowControlStyle';
const appWindow = getCurrentWindow();
@@ -11,14 +12,15 @@ const stopTitlebarDrag = (event: PointerEvent
) => {
interface WindowControlsProps {
side: 'left' | 'right';
+ controlStyle: ResolvedWindowControlStyle;
}
-export function WindowControls({ side }: WindowControlsProps) {
+export function WindowControls({ side, controlStyle }: WindowControlsProps) {
const { t } = useTranslation();
return (
$.window.controls)}
>