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 && ( <>