From 90ca235af1816ad3d1c507508d8ccd1c958af8db Mon Sep 17 00:00:00 2001 From: Moritz Wolf Date: Tue, 17 Mar 2026 21:06:26 +0100 Subject: [PATCH 1/7] feat: add Linux/Fedora terminal support with Wayland detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes terminal spawning on Fedora and other RPM-based distributions where `x-terminal-emulator` (a Debian/Ubuntu mechanism) does not exist. Changes: - Replace hardcoded terminal list in `open_terminal` with a new `find_terminal_emulator()` helper that probes installed terminals in order of preference: x-terminal-emulator → gnome-terminal → konsole → xfce4-terminal → alacritty → kitty → tilix → xterm → urxvt - Add Wayland awareness: under a pure Wayland session (WAYLAND_DISPLAY set, DISPLAY unset) X11-only terminals (xterm, urxvt) are skipped to avoid launch failures when XWayland is not running - Use correct argument separator per terminal ("--" for gnome-terminal, konsole, kitty; "-e" for everything else) - Update README with Fedora/DNF build dependencies, cronie setup, and a Wayland compatibility note Tested on Fedora 43 with GNOME (Wayland + XWayland), producing a working .rpm and .AppImage via `pnpm tauri build`. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 26 ++++++++++++++++- src-tauri/src/lib.rs | 67 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 77 insertions(+), 16 deletions(-) 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(()) From 8d98ab628cfdb6760d58efce8774da2a0eede1bf Mon Sep 17 00:00:00 2001 From: Moritz Wolf Date: Tue, 17 Mar 2026 21:21:41 +0100 Subject: [PATCH 2/7] feat: add dark/light theme toggle (closes #3) Adds a dark/light mode toggle to the Settings panel. The selected theme persists across restarts via ~/.aui/settings.json. Changes: - ui-store.ts: add `theme` state ("dark" | "light", default "dark") and `setTheme()` action - App.css: add `[data-theme="light"]` block with GitHub-light-inspired color variables overriding the dark defaults - App.tsx: read saved theme from ~/.aui/settings.json on startup and call setTheme(); apply `data-theme` attribute to reactively via useEffect so all CSS variables update instantly - SettingsPanel.tsx: add `theme` field to AppSettings, render a Dark / Light two-button toggle in the Preferences section, call setTheme() on load and on save, persist theme to ~/.aui/settings.json so the preference survives app restarts - FileViewer, MarkdownEditor, SettingsEditor, AgentEditor: switch Monaco editor theme between "vs-dark" and "vs" based on the active theme Co-Authored-By: Claude Sonnet 4.6 --- src/App.css | 13 ++++++ src/App.tsx | 10 +++++ src/components/context-hub/FileViewer.tsx | 4 +- src/components/inspector/AgentEditor.tsx | 4 +- src/components/inspector/MarkdownEditor.tsx | 4 +- src/components/inspector/SettingsEditor.tsx | 4 +- src/components/settings/SettingsPanel.tsx | 49 +++++++++++++++++++-- src/store/ui-store.ts | 7 +++ 8 files changed, 87 insertions(+), 8 deletions(-) diff --git a/src/App.css b/src/App.css index b156c0d..4a63e72 100644 --- a/src/App.css +++ b/src/App.css @@ -29,6 +29,19 @@ --radius-xl: 12px; } +/* 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; +} + * { margin: 0; padding: 0; 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/context-hub/FileViewer.tsx b/src/components/context-hub/FileViewer.tsx index e9046dc..4e0e480 100644 --- a/src/components/context-hub/FileViewer.tsx +++ b/src/components/context-hub/FileViewer.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from "react"; import Editor from "@monaco-editor/react"; import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs"; +import { useUiStore } from "@/store/ui-store"; function detectLanguage(filePath: string): string { if (filePath.endsWith(".md")) return "markdown"; @@ -18,6 +19,7 @@ interface FileViewerProps { } export function FileViewer({ filePath, language }: FileViewerProps) { + const theme = useUiStore((s) => s.theme); const [content, setContent] = useState(null); const [editedContent, setEditedContent] = useState(""); const [editing, setEditing] = useState(false); @@ -138,7 +140,7 @@ export function FileViewer({ filePath, language }: FileViewerProps) { setEditedContent(v ?? "") : undefined} options={{ diff --git a/src/components/inspector/AgentEditor.tsx b/src/components/inspector/AgentEditor.tsx index 7be46ed..4252d79 100644 --- a/src/components/inspector/AgentEditor.tsx +++ b/src/components/inspector/AgentEditor.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, type CSSProperties, type KeyboardEven import Editor from "@monaco-editor/react"; import { MarkdownEditor } from "./MarkdownEditor"; import { useTreeStore } from "@/store/tree-store"; +import { useUiStore } from "@/store/ui-store"; import { useAutosave } from "@/hooks/useAutosave"; import { getApiKey, generateText } from "@/services/claude-api"; import { toast } from "@/components/common/Toast"; @@ -168,6 +169,7 @@ interface AgentEditorProps { export function AgentEditor({ node }: AgentEditorProps) { const updateNode = useTreeStore((s) => s.updateNode); const saveNode = useTreeStore((s) => s.saveNode); + const theme = useUiStore((s) => s.theme); const cfg = (node.config as AgentConfig | null) ?? { name: node.name }; @@ -482,7 +484,7 @@ export function AgentEditor({ node }: AgentEditorProps) { s.theme); return ( onChange?.(v ?? "")} options={{ diff --git a/src/components/inspector/SettingsEditor.tsx b/src/components/inspector/SettingsEditor.tsx index 4983423..f6cd954 100644 --- a/src/components/inspector/SettingsEditor.tsx +++ b/src/components/inspector/SettingsEditor.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, type CSSProperties, type KeyboardEvent } from "react"; import Editor from "@monaco-editor/react"; import { useTreeStore } from "@/store/tree-store"; +import { useUiStore } from "@/store/ui-store"; import type { AuiNode } from "@/types/aui-node"; import type { SettingsConfig } from "@/types/settings"; @@ -213,6 +214,7 @@ interface SettingsEditorProps { export function SettingsEditor({ node }: SettingsEditorProps) { const updateNode = useTreeStore((s) => s.updateNode); const saveNode = useTreeStore((s) => s.saveNode); + const theme = useUiStore((s) => s.theme); const cfg = (node.config as SettingsConfig | null) ?? {}; @@ -328,7 +330,7 @@ export function SettingsEditor({ node }: SettingsEditorProps) { setRawJson(v ?? "")} options={{ diff --git a/src/components/settings/SettingsPanel.tsx b/src/components/settings/SettingsPanel.tsx index e39d501..0055db3 100644 --- a/src/components/settings/SettingsPanel.tsx +++ b/src/components/settings/SettingsPanel.tsx @@ -15,6 +15,7 @@ interface AppSettings { agentColor: string; accentColor: string; autoSave: boolean; + theme: "dark" | "light"; } const DEFAULT_SETTINGS: AppSettings = { @@ -23,6 +24,7 @@ const DEFAULT_SETTINGS: AppSettings = { agentColor: "#f0883e", accentColor: "#4a9eff", autoSave: true, + theme: "dark", }; const labelStyle: CSSProperties = { @@ -60,10 +62,10 @@ const sectionStyle: CSSProperties = { }; /** - * Persist the chosen project path to ~/.aui/settings.json so it survives restarts. - * Pass null to clear the saved path (reset to home). + * Persist the chosen project path (and optionally theme) to ~/.aui/settings.json + * so they survive restarts. Pass null for path to clear the saved path (reset to home). */ -async function saveProjectPath(path: string | null) { +async function saveProjectPath(path: string | null, theme?: "dark" | "light") { const home = await homeDir(); const auiDir = join(home, ".aui"); if (!(await exists(auiDir))) { @@ -83,12 +85,16 @@ async function saveProjectPath(path: string | null) { } else { delete current.projectPath; } + if (theme) { + current.theme = theme; + } await writeTextFile(settingsPath, JSON.stringify(current, null, 2)); } export function SettingsPanel() { const settingsOpen = useUiStore((s) => s.settingsOpen); const toggleSettings = useUiStore((s) => s.toggleSettings); + const setTheme = useUiStore((s) => s.setTheme); const projectPath = useTreeStore((s) => s.projectPath); const [settings, setSettings] = useState(DEFAULT_SETTINGS); @@ -113,7 +119,11 @@ export function SettingsPanel() { if (await exists(settingsPath)) { const raw = await readTextFile(settingsPath); const parsed = JSON.parse(raw); - if (!cancelled) setSettings({ ...DEFAULT_SETTINGS, ...parsed }); + if (!cancelled) { + const loaded = { ...DEFAULT_SETTINGS, ...parsed }; + setSettings(loaded); + setTheme(loaded.theme); + } } } catch { // Use defaults @@ -138,6 +148,10 @@ export function SettingsPanel() { const root = document.documentElement; if (settings.accentColor) root.style.setProperty("--accent-blue", settings.accentColor); + // Apply and persist theme + setTheme(settings.theme); + await saveProjectPath(null, settings.theme); + toast("Settings saved", "success"); } catch (err) { toast(err instanceof Error ? err.message : "Failed to save settings", "error"); @@ -389,6 +403,33 @@ export function SettingsPanel() { {/* Preferences */}
Preferences
+ {/* Theme toggle */} +
+ Theme +
+ {(["dark", "light"] as const).map((t) => ( + + ))} +
+
+