diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 2490b2c1..ae69b939 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -826,6 +826,17 @@ 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.
+ #[cfg(target_os = "linux")]
+ {
+ use tauri::Manager;
+ if let Some(win) = app.get_webview_window("main") {
+ let _ = win.set_decorations(false);
+ }
+ }
+
// ── System tray ───────────────────────────────────────────────
// Always build on startup; the frontend calls toggle_tray_icon(false)
// immediately after load if the user has disabled the tray icon.
diff --git a/src/App.tsx b/src/App.tsx
index c4b89f0e..26ddb851 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -51,6 +51,8 @@ import GenreDetail from './pages/GenreDetail';
import ExportPickerModal from './components/ExportPickerModal';
import ChangelogModal from './components/ChangelogModal';
import AppUpdater from './components/AppUpdater';
+import TitleBar from './components/TitleBar';
+import { IS_LINUX } from './utils/platform';
import { version } from '../package.json';
import { useConnectionStatus } from './hooks/useConnectionStatus';
import { useAuthStore } from './store/authStore';
@@ -248,16 +250,18 @@ function AppShell() {
const isMobilePlayer = isMobile && location.pathname === '/now-playing';
return (
-
e.preventDefault()}
>
+ {IS_LINUX &&
}
{!isMobile && (
s.currentTrack);
+ const isPlaying = usePlayerStore(s => s.isPlaying);
+
+ return (
+
+
Psysonic
+
+
+ {currentTrack && (
+ <>
+ {isPlaying ? '▶' : '⏸'}
+
+ {currentTrack.artist && `${currentTrack.artist} – `}{currentTrack.title}
+
+ >
+ )}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/styles/layout.css b/src/styles/layout.css
index 245137d4..180152e5 100644
--- a/src/styles/layout.css
+++ b/src/styles/layout.css
@@ -27,6 +27,98 @@
background: var(--bg-app);
}
+/* ─── Custom title bar (Linux only — decorations: false) ─── */
+:root {
+ --titlebar-height: 32px;
+}
+
+.app-shell[data-titlebar] {
+ grid-template-rows: var(--titlebar-height) 1fr var(--player-height);
+ grid-template-areas:
+ "titlebar titlebar titlebar"
+ "sidebar main queue"
+ "player player player";
+}
+
+.titlebar {
+ grid-area: titlebar;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: var(--bg-sidebar);
+ border-bottom: 1px solid var(--border-subtle);
+ padding: 0 6px 0 12px;
+ height: var(--titlebar-height);
+ user-select: none;
+}
+
+.titlebar-title {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text-muted);
+ letter-spacing: 0.02em;
+ pointer-events: none;
+ flex: 0 0 auto;
+}
+
+.titlebar-track {
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ max-width: 40%;
+ pointer-events: none;
+}
+
+.titlebar-track-state {
+ font-size: 9px;
+ color: var(--accent);
+ flex-shrink: 0;
+}
+
+.titlebar-track-text {
+ font-size: 12px;
+ color: var(--text-secondary);
+ max-width: 100%;
+}
+
+.titlebar-controls {
+ display: flex;
+ gap: 2px;
+ flex: 0 0 auto;
+}
+
+.titlebar-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 24px;
+ border: none;
+ background: transparent;
+ color: var(--text-muted);
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ transition: background var(--transition-fast), color var(--transition-fast);
+}
+
+.titlebar-btn:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+
+.titlebar-btn-close:hover {
+ background: var(--danger);
+ color: #fff;
+}
+
+/* Resizer handles must start below the titlebar */
+.app-shell[data-titlebar] .resizer {
+ top: var(--titlebar-height);
+}
+
/* ─── Resizer Handles ─── */
.resizer {
position: absolute;
diff --git a/src/utils/platform.ts b/src/utils/platform.ts
new file mode 100644
index 00000000..91e1c5e2
--- /dev/null
+++ b/src/utils/platform.ts
@@ -0,0 +1,2 @@
+/** True when running on Linux (WebKitGTK). Used to show the custom title bar. */
+export const IS_LINUX = navigator.platform.toLowerCase().includes('linux');