diff --git a/README.md b/README.md index cc5cf53..80d2a64 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,29 @@ sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file libssl-dev curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` +**Linux** (Fedora/RPM): +```bash +sudo dnf install -y \ + webkit2gtk4.1-devel \ + openssl-devel \ + gtk3-devel \ + libayatana-appindicator-gtk3-devel \ + librsvg2-devel \ + curl wget file gcc make + +# Install Rust +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source ~/.cargo/env + +# Enable cron scheduling support +sudo dnf install -y cronie +sudo systemctl enable --now crond +``` + +> **Fedora note:** If `libayatana-appindicator-gtk3-devel` is not found, try `libappindicator-gtk3-devel`. If `webkit2gtk4.1-devel` is missing, also install `libsoup3-devel`. + +> **Wayland:** ATM supports Wayland natively via gnome-terminal or konsole. If you are running a pure Wayland session (no XWayland), ensure one of these is installed. + **Build for production**: @@ -221,7 +244,8 @@ pnpm tauri build |----------|--------|---------| | Windows | Full support | PowerShell | | macOS | Full support | bash (Terminal.app) | -| Linux | Supported | bash | +| Linux (Debian/Ubuntu) | Supported | gnome-terminal, xterm, or any installed emulator | +| Linux (Fedora/RPM) | Supported | konsole, gnome-terminal, alacritty, kitty, xterm | --- diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3ca68de..0232168 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -254,6 +254,42 @@ fn delete_scheduled_task(task_name: String) -> Result { } } +/// Finds an available terminal emulator, preferring Wayland-native ones when running +/// under a pure Wayland session (WAYLAND_DISPLAY set, DISPLAY unset). +#[cfg(target_os = "linux")] +fn find_terminal_emulator() -> Option<&'static str> { + let wayland = std::env::var("WAYLAND_DISPLAY").is_ok(); + let x11 = std::env::var("DISPLAY").is_ok(); + let pure_wayland = wayland && !x11; + + // X11-only terminals that won't work without XWayland + const X11_ONLY: &[&str] = &["xterm", "urxvt"]; + + // Ordered by preference: Debian compat first, then common DEs, then fallbacks + const CANDIDATES: &[&str] = &[ + "x-terminal-emulator", // Debian/Ubuntu alternatives system + "gnome-terminal", // GNOME (native Wayland) + "konsole", // KDE (native Wayland, default on Fedora KDE) + "xfce4-terminal", // XFCE + "alacritty", // Modern, widespread + "kitty", // Modern, widespread + "tilix", // GNOME alternative + "xterm", // X11 fallback + "urxvt", // X11 fallback + ]; + + CANDIDATES.iter().find(|&&term| { + if pure_wayland && X11_ONLY.contains(&term) { + return false; + } + StdCommand::new("which") + .arg(term) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + }).copied() +} + /// Opens a visible terminal window running the given script. #[tauri::command] fn open_terminal(script_path: String) -> Result<(), String> { @@ -287,21 +323,22 @@ fn open_terminal(script_path: String) -> Result<(), String> { #[cfg(target_os = "linux")] { - let terminals = [ - ("x-terminal-emulator", vec!["-e", &script_path]), - ("gnome-terminal", vec!["--", &script_path]), - ("xterm", vec!["-e", &script_path]), - ]; - let mut launched = false; - for (term, args) in &terminals { - if StdCommand::new(term).args(args).spawn().is_ok() { - launched = true; - break; - } - } - if !launched { - return Err("No terminal emulator found".into()); - } + let term = find_terminal_emulator().ok_or_else(|| { + "No terminal emulator found. Install gnome-terminal, konsole, xterm, or another terminal emulator.".to_string() + })?; + + // Terminals that use "--" as argument separator instead of "-e" + const DASH_DASH_TERMS: &[&str] = &["gnome-terminal", "konsole", "kitty"]; + let args: Vec<&str> = if DASH_DASH_TERMS.contains(&term) { + vec!["--", &script_path] + } else { + vec!["-e", &script_path] + }; + + StdCommand::new(term) + .args(&args) + .spawn() + .map_err(|e| format!("Failed to open terminal '{}': {}", term, e))?; } Ok(()) diff --git a/src/App.css b/src/App.css index b156c0d..a9dbe45 100644 --- a/src/App.css +++ b/src/App.css @@ -29,12 +29,84 @@ --radius-xl: 12px; } +/* Tinted background tokens — used for node cards and inline panels */ +:root { + --tint-blue: rgba(74, 158, 255, 0.06); + --tint-blue-hover: rgba(74, 158, 255, 0.14); + --tint-green: rgba(63, 185, 80, 0.06); + --tint-orange: rgba(240, 136, 62, 0.07); + --tint-purple: rgba(217, 70, 239, 0.06); + --tint-gold: rgba(210, 153, 34, 0.08); + --tint-subagent: rgba(165, 214, 255, 0.07); + --bg-toast: #1e1e3a; + --shadow-panel: rgba(0, 0, 0, 0.4); + --shadow-dropdown: rgba(0, 0, 0, 0.35); + /* White-alpha tokens used in inputs, pills, subtle dividers */ + --bg-input: rgba(255, 255, 255, 0.05); + --border-input: rgba(255, 255, 255, 0.10); + --border-input-hover:rgba(255, 255, 255, 0.25); + --border-subtle: rgba(255, 255, 255, 0.06); + --bg-hover-subtle: rgba(255, 255, 255, 0.06); +} + +/* Light theme overrides — applied when */ +[data-theme="light"] { + --bg-primary: #ffffff; + --bg-secondary: #f6f8fa; + --bg-surface: #ffffff; + --bg-elevated: #eaeef2; + --text-primary: #1f2328; + --text-secondary: #636c76; + --text-tertiary: #9198a1; + --border-color: #d1d9e0; + --border-hover: #8c959f; + /* Tint tokens need more opacity on light backgrounds to remain visible */ + --tint-blue: rgba(74, 158, 255, 0.13); + --tint-blue-hover: rgba(74, 158, 255, 0.24); + --tint-green: rgba(63, 185, 80, 0.13); + --tint-orange: rgba(240, 136, 62, 0.13); + --tint-purple: rgba(217, 70, 239, 0.11); + --tint-gold: rgba(210, 153, 34, 0.13); + --tint-subagent: rgba(74, 140, 210, 0.13); + --bg-toast: #eaeef2; + --shadow-panel: rgba(0, 0, 0, 0.12); + --shadow-dropdown: rgba(0, 0, 0, 0.10); + /* White-alpha tokens → solid equivalents on light background */ + --bg-input: var(--bg-primary); + --border-input: var(--border-color); + --border-input-hover:var(--border-hover); + --border-subtle: var(--border-color); + --bg-hover-subtle: var(--bg-elevated); +} + * { margin: 0; padding: 0; box-sizing: border-box; } +/* Native form elements — override OS/browser defaults so they follow the active theme */ +select, +input[type="text"], +input[type="password"], +input[type="number"], +input[type="email"], +textarea { + background-color: var(--bg-input); + color: var(--text-primary); + border-color: var(--border-input); + color-scheme: dark; +} + +[data-theme="light"] select, +[data-theme="light"] input[type="text"], +[data-theme="light"] input[type="password"], +[data-theme="light"] input[type="number"], +[data-theme="light"] input[type="email"], +[data-theme="light"] textarea { + color-scheme: light; +} + html, body, #root { height: 100%; width: 100%; diff --git a/src/App.tsx b/src/App.tsx index c9ff147..a87d253 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,6 +19,7 @@ import { useUiStore } from "./store/ui-store"; import { startWatching } from "./services/file-watcher"; function App() { + const theme = useUiStore((s) => s.theme); const inspectorOpen = useUiStore((s) => s.inspectorOpen); const createDialogOpen = useUiStore((s) => s.createDialogOpen); const closeCreateDialog = useUiStore((s) => s.closeCreateDialog); @@ -132,7 +133,13 @@ function App() { return () => document.removeEventListener("keydown", handleKeyDown); }, []); + // Apply theme to DOM whenever it changes + useEffect(() => { + document.documentElement.setAttribute("data-theme", theme); + }, [theme]); + // Load project on mount — use saved projectPath if available, else home directory + // Also restore saved theme preference. useEffect(() => { (async () => { const home = await homeDir(); @@ -148,6 +155,9 @@ function App() { targetPath = parsed.projectPath; } } + if (parsed.theme === "light" || parsed.theme === "dark") { + useUiStore.getState().setTheme(parsed.theme); + } } } catch { // Fall back to home directory diff --git a/src/components/common/Toast.tsx b/src/components/common/Toast.tsx index c865f48..ef0f5a2 100644 --- a/src/components/common/Toast.tsx +++ b/src/components/common/Toast.tsx @@ -59,13 +59,15 @@ function ToastItem({ entry }: { entry: ToastEntry }) { return (
@@ -502,7 +502,7 @@ function ImportDropdown({ background: "var(--bg-surface, #1c2333)", border: "1px solid var(--border-color)", borderRadius: 8, - boxShadow: "0 8px 24px rgba(0,0,0,0.5)", + boxShadow: "0 8px 24px var(--shadow-panel)", zIndex: 200, padding: 6, }} @@ -513,7 +513,7 @@ function ImportDropdown({ style={menuItemStyle} onClick={handleFileImport} disabled={importing} - onMouseEnter={(e) => { e.currentTarget.style.background = "rgba(255,255,255,0.06)"; }} + onMouseEnter={(e) => { e.currentTarget.style.background = "var(--bg-hover-subtle)"; }} onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; }} > From File... @@ -522,7 +522,7 @@ function ImportDropdown({ style={menuItemStyle} onClick={() => setMode("url")} disabled={importing} - onMouseEnter={(e) => { e.currentTarget.style.background = "rgba(255,255,255,0.06)"; }} + onMouseEnter={(e) => { e.currentTarget.style.background = "var(--bg-hover-subtle)"; }} onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; }} > From GitHub URL @@ -545,8 +545,8 @@ function ImportDropdown({ boxSizing: "border-box", padding: "7px 10px", fontSize: 12, - background: "rgba(255,255,255,0.05)", - border: "1px solid rgba(255,255,255,0.12)", + background: "var(--bg-input)", + border: "1px solid var(--border-subtle)", borderRadius: 6, color: "var(--text-primary)", outline: "none", @@ -591,12 +591,12 @@ function UtilityButton({ label, onClick }: { label: string; onClick: () => void onClick={onClick} style={{ padding: "4px 12px", fontSize: 11, fontWeight: 600, - background: "rgba(255,255,255,0.04)", border: "1px solid rgba(255,255,255,0.1)", + background: "var(--bg-input)", border: "1px solid var(--border-input)", color: "var(--text-secondary)", borderRadius: 6, cursor: "pointer", transition: "all 0.15s", }} - onMouseEnter={(e) => { e.currentTarget.style.borderColor = "rgba(255,255,255,0.25)"; e.currentTarget.style.color = "var(--text-primary)"; }} - onMouseLeave={(e) => { e.currentTarget.style.borderColor = "rgba(255,255,255,0.1)"; e.currentTarget.style.color = "var(--text-secondary)"; }} + onMouseEnter={(e) => { e.currentTarget.style.borderColor = "var(--border-input-hover)"; e.currentTarget.style.color = "var(--text-primary)"; }} + onMouseLeave={(e) => { e.currentTarget.style.borderColor = "var(--border-input)"; e.currentTarget.style.color = "var(--text-secondary)"; }} > {label} @@ -750,7 +750,7 @@ export function ContextHub() { position: "fixed", top: "var(--toolbar-height)", right: 0, bottom: 0, width: 480, background: "var(--bg-secondary)", borderLeft: "1px solid var(--border-color)", - boxShadow: "-4px 0 24px rgba(0,0,0,0.4)", + boxShadow: "-4px 0 24px var(--shadow-panel)", display: "flex", flexDirection: "column", zIndex: 100, animation: "slideInRight 0.2s ease", }} @@ -759,10 +759,8 @@ export function ContextHub() {
@@ -779,7 +777,7 @@ export function ContextHub() { borderRadius: 8, cursor: "pointer", transition: "border-color 0.15s", }} - onMouseEnter={(e) => { e.currentTarget.style.borderColor = "rgba(255,255,255,0.25)"; }} + onMouseEnter={(e) => { e.currentTarget.style.borderColor = "var(--border-input-hover)"; }} onMouseLeave={(e) => { e.currentTarget.style.borderColor = "var(--border-color)"; }} > Import @@ -842,14 +840,14 @@ export function ContextHub() { style={{ width: "100%", boxSizing: "border-box", padding: "8px 30px 8px 12px", - background: "rgba(255,255,255,0.05)", - border: "1px solid rgba(255,255,255,0.1)", + background: "var(--bg-input)", + border: "1px solid var(--border-input)", borderRadius: 8, color: "var(--text-primary)", fontSize: 13, outline: "none", transition: "border-color 0.2s", }} onFocus={(e) => { e.currentTarget.style.borderColor = "rgba(74,158,255,0.4)"; }} - onBlur={(e) => { e.currentTarget.style.borderColor = "rgba(255,255,255,0.1)"; }} + onBlur={(e) => { e.currentTarget.style.borderColor = "var(--border-input)"; }} /> {search && ( + ))} +
+
+