From 37799fb861f03bb1de66347f40744589b6958ec9 Mon Sep 17 00:00:00 2001 From: kilyabin <65072190+kilyabin@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:32:37 +0400 Subject: [PATCH] feat: disable native decorations on Linux, hide custom TitleBar for tiling WMs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On tiling window managers (Hyprland, Sway, i3, bspwm, AwesomeWM, Openbox, etc.) the window has no title bar at all — neither native nor custom. On stacking DEs (GNOME, KDE, XFCE) the custom TitleBar can be toggled in settings as before. Native decorations are disabled on all Linux at startup. The frontend checks is_tiling_wm() to decide whether to render the custom TitleBar: - DE (GNOME/KDE): custom TitleBar shown (user-toggleable in settings) - Tiling WM: no TitleBar at all, setting hidden from Settings page Detection is based on environment variables: - HYPRLAND_INSTANCE_SIGNATURE, SWAYSOCK, I3SOCK (direct compositor signatures) - XDG_CURRENT_DESKTOP check for known tiling WMs Changes: - Added is_tiling_wm_cmd() Tauri command for frontend detection - Native decorations disabled on all Linux at startup - TitleBar not rendered on tiling WMs - Custom TitleBar setting hidden in Settings for tiling WMs --- src-tauri/src/lib.rs | 53 ++++++++++++++++++++++++++++++++++++++++-- src/App.tsx | 16 +++++++++---- src/pages/Settings.tsx | 9 ++++++- 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 24a78387..07828986 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -816,6 +816,53 @@ fn stop_audio_engine(app: &tauri::AppHandle) { if let Some(sink) = cur.sink.take() { sink.stop(); } } +/// Returns `true` if running under a tiling window manager (Hyprland, Sway, i3, +/// bspwm, AwesomeWM, Openbox, etc.). Detection is based on environment variables +/// set by the compositor / DE. +#[cfg(target_os = "linux")] +fn is_tiling_wm() -> bool { + // Direct compositor signatures (most reliable). + let direct = [ + "HYPRLAND_INSTANCE_SIGNATURE", // Hyprland + "SWAYSOCK", // Sway + "I3SOCK", // i3 + ] + .iter() + .any(|&var| std::env::var_os(var).is_some()); + + if direct { + return true; + } + + // Check XDG_CURRENT_DESKTOP for known tiling WMs. + if let Ok(desktop) = std::env::var("XDG_CURRENT_DESKTOP") { + let desktop = desktop.to_lowercase(); + let tiling_wms = [ + "hyprland", "sway", "i3", "bspwm", "awesome", "openbox", + "xmonad", "dwm", "qtile", "herbstluftwm", "leftwm", + ]; + if tiling_wms.iter().any(|&wm| desktop.contains(wm)) { + return true; + } + } + + false +} + +/// Tauri command: lets the frontend know whether we're running under a tiling +/// WM so it can decide whether to render the custom TitleBar component. +#[tauri::command] +fn is_tiling_wm_cmd() -> bool { + #[cfg(target_os = "linux")] + { + is_tiling_wm() + } + #[cfg(not(target_os = "linux"))] + { + false + } +} + pub fn run() { let (audio_engine, _audio_thread) = audio::create_engine(); @@ -840,8 +887,9 @@ pub fn run() { .setup(|app| { // ── Custom title bar on Linux ───────────────────────────────── - // Remove OS window decorations so the React TitleBar component - // takes over. macOS and Windows keep their native decorations. + // Remove OS window decorations on all Linux so the React TitleBar + // can take over. The frontend checks is_tiling_wm() to decide + // whether to actually render the TitleBar (hidden on tiling WMs). #[cfg(target_os = "linux")] { use tauri::Manager; @@ -976,6 +1024,7 @@ pub fn run() { greet, exit_app, set_window_decorations, + is_tiling_wm_cmd, register_global_shortcut, unregister_global_shortcut, mpris_set_metadata, diff --git a/src/App.tsx b/src/App.tsx index c2a01e6f..9464d241 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -78,6 +78,12 @@ function AppShell() { const { t } = useTranslation(); const isMobile = useIsMobile(); const [isWindowFullscreen, setIsWindowFullscreen] = useState(false); + const [isTilingWm, setIsTilingWm] = useState(false); + + useEffect(() => { + if (!IS_LINUX) return; + invoke('is_tiling_wm_cmd').then(setIsTilingWm).catch(() => {}); + }, []); useEffect(() => { if (!IS_LINUX) return; @@ -108,10 +114,12 @@ function AppShell() { const hasOfflineContent = Object.values(offlineAlbums).some(a => a.serverId === serverId); // Sync custom titlebar preference with native decorations on Linux + // On tiling WMs decorations are always off (no native title bar to replace). useEffect(() => { if (!IS_LINUX) return; - invoke('set_window_decorations', { enabled: !useCustomTitlebar }).catch(() => {}); - }, [useCustomTitlebar]); + const enabled = isTilingWm ? false : !useCustomTitlebar; + invoke('set_window_decorations', { enabled }).catch(() => {}); + }, [useCustomTitlebar, isTilingWm]); useEffect(() => { if (!isLoggedIn || !activeServerId) return; @@ -288,14 +296,14 @@ function AppShell() { className="app-shell" data-mobile={isMobile || undefined} data-mobile-player={isMobilePlayer || undefined} - data-titlebar={(IS_LINUX && useCustomTitlebar && !isWindowFullscreen) || undefined} + data-titlebar={(IS_LINUX && useCustomTitlebar && !isWindowFullscreen && !isTilingWm) || undefined} style={{ '--sidebar-width': isMobile ? '0px' : (isSidebarCollapsed ? '72px' : 'clamp(200px, 15vw, 220px)'), '--queue-width': isMobile ? '0px' : (isQueueVisible ? `${queueWidth}px` : '0px') } as React.CSSProperties} onContextMenu={e => e.preventDefault()} > - {IS_LINUX && useCustomTitlebar && !isWindowFullscreen && } + {IS_LINUX && useCustomTitlebar && !isWindowFullscreen && !isTilingWm && } {!isMobile && ( s.clearAll); const clearHotCacheDisk = useHotCacheStore(s => s.clearAllDisk); const hotCacheEntries = useHotCacheStore(s => s.entries); + const [isTilingWm, setIsTilingWm] = useState(false); + + useEffect(() => { + if (!IS_LINUX) return; + invoke('is_tiling_wm_cmd').then(setIsTilingWm).catch(() => {}); + }, []); + const hotCacheTrackCount = useMemo(() => { if (!serverId) return 0; const prefix = `${serverId}:`; @@ -618,7 +625,7 @@ export default function Settings() { - {IS_LINUX && ( + {IS_LINUX && !isTilingWm && ( <>