Skip to content

Commit ce33cb2

Browse files
Merge pull request #202 from athasdev/themes
Fix toml themes not loading
2 parents a920048 + 8e03f6c commit ce33cb2

16 files changed

Lines changed: 458 additions & 203 deletions

File tree

src-tauri/src/commands/theme.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,35 @@ pub async fn cache_themes(
198198
}
199199
Ok(())
200200
}
201+
202+
#[tauri::command]
203+
pub async fn get_temp_dir() -> Result<String, String> {
204+
let temp_dir = std::env::temp_dir();
205+
temp_dir.to_str()
206+
.map(|s| s.to_string())
207+
.ok_or_else(|| "Failed to convert temp directory path to string".to_string())
208+
}
209+
210+
#[tauri::command]
211+
pub async fn write_temp_file(file_name: String, content: String) -> Result<(), String> {
212+
let temp_dir = std::env::temp_dir();
213+
let file_path = temp_dir.join(&file_name);
214+
215+
fs::write(&file_path, content)
216+
.map_err(|e| format!("Failed to write temp file {}: {}", file_name, e))?;
217+
218+
Ok(())
219+
}
220+
221+
#[tauri::command]
222+
pub async fn delete_temp_file(file_name: String) -> Result<(), String> {
223+
let temp_dir = std::env::temp_dir();
224+
let file_path = temp_dir.join(&file_name);
225+
226+
if file_path.exists() {
227+
fs::remove_file(&file_path)
228+
.map_err(|e| format!("Failed to delete temp file {}: {}", file_name, e))?;
229+
}
230+
231+
Ok(())
232+
}

src-tauri/src/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,9 @@ fn main() {
249249
load_single_toml_theme,
250250
get_cached_themes,
251251
cache_themes,
252+
get_temp_dir,
253+
write_temp_file,
254+
delete_temp_file,
252255
// Font commands
253256
get_system_fonts,
254257
get_monospace_fonts,

src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { MainLayout } from "./components/layout/main-layout";
66
import { ToastContainer } from "./components/ui/toast";
77
import WelcomeScreen from "./components/window/welcome-screen";
88
import { ZoomIndicator } from "./components/zoom-indicator";
9+
import { initializeThemeSystem } from "./extensions/themes/theme-initializer";
910
import { useScroll } from "./hooks/use-scroll";
1011
import { useAppStore } from "./stores/app-store";
1112
import { useFileSystemStore } from "./stores/file-system/store";
@@ -19,6 +20,10 @@ import { useZoomStore } from "./stores/zoom-store";
1920
import { cn } from "./utils/cn";
2021
import { isMac } from "./utils/platform";
2122

23+
// Initialize theme system immediately when the module loads
24+
// This ensures themes are loaded before the settings store tries to apply them
25+
initializeThemeSystem().catch(console.error);
26+
2227
function App() {
2328
enableMapSet();
2429

src/components/command/command-bar.tsx renamed to src/components/command/components/command-bar.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import { ClockIcon, File } from "lucide-react";
22
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3-
import { IGNORED_PATTERNS } from "@/constants/ignored-patterns";
4-
import { useBufferStore } from "../../stores/buffer-store";
5-
import { useFileSystemStore } from "../../stores/file-system/store";
6-
import { useRecentFilesStore } from "../../stores/recent-files-store";
7-
import { useUIState } from "../../stores/ui-state-store";
3+
import { IGNORED_PATTERNS } from "@/components/command/constants/ignored-patterns";
4+
import { useBufferStore } from "../../../stores/buffer-store";
5+
import { useFileSystemStore } from "../../../stores/file-system/store";
6+
import { useRecentFilesStore } from "../../../stores/recent-files-store";
7+
import { useUIState } from "../../../stores/ui-state-store";
88
import Command, {
99
CommandEmpty,
1010
CommandHeader,
1111
CommandInput,
1212
CommandItem,
1313
CommandList,
14-
} from "../ui/command";
14+
} from "../../ui/command";
1515

1616
// Function to check if a file should be ignored
1717
const shouldIgnoreFile = (filePath: string): boolean => {

src/components/command/command-palette.tsx renamed to src/components/command/components/command-palette.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { Palette, Settings, Sparkles } from "lucide-react";
22
import type React from "react";
33
import { useEffect, useRef, useState } from "react";
4-
import { useAppStore } from "../../stores/app-store";
5-
import { useUIState } from "../../stores/ui-state-store";
4+
import { useAppStore } from "../../../stores/app-store";
5+
import { useUIState } from "../../../stores/ui-state-store";
66
import Command, {
77
CommandEmpty,
88
CommandHeader,
99
CommandInput,
1010
CommandItem,
1111
CommandList,
12-
} from "../ui/command";
13-
import KeybindingBadge from "../ui/keybinding-badge";
12+
} from "../../ui/command";
13+
import KeybindingBadge from "../../ui/keybinding-badge";
1414

1515
interface Action {
1616
id: string;

src/components/command/theme-selector.tsx renamed to src/components/command/components/theme-selector.tsx

Lines changed: 100 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
import { Monitor, Moon, Sun } from "lucide-react";
1+
import { Monitor, Moon, Palette, Sun, Upload } from "lucide-react";
22
import type React from "react";
33
import { useCallback, useEffect, useRef, useState } from "react";
4+
import { themeRegistry } from "../../../extensions/themes/theme-registry";
5+
import type { ThemeDefinition } from "../../../extensions/themes/types";
6+
import Button from "../../ui/button";
47
import Command, {
58
CommandEmpty,
69
CommandHeader,
710
CommandInput,
811
CommandItem,
912
CommandList,
10-
} from "../ui/command";
11-
12-
type Theme = "auto" | "athas-light" | "athas-dark";
13+
} from "../../ui/command";
1314

1415
interface ThemeInfo {
15-
id: Theme;
16+
id: string;
1617
name: string;
1718
description: string;
1819
category: "System" | "Light" | "Dark" | "Colorful";
@@ -22,50 +23,68 @@ interface ThemeInfo {
2223
interface ThemeSelectorProps {
2324
isVisible: boolean;
2425
onClose: () => void;
25-
onThemeChange: (theme: Theme) => void;
26-
currentTheme?: Theme;
26+
onThemeChange: (theme: string) => void;
27+
currentTheme?: string;
2728
}
2829

29-
// Dynamic theme definitions based on existing theme system
30-
const THEME_DEFINITIONS: ThemeInfo[] = [
31-
// System
32-
{
33-
id: "auto",
34-
name: "Auto",
35-
description: "Follow system preference",
36-
category: "System",
37-
icon: <Monitor size={14} />,
38-
},
39-
// Light theme
40-
{
41-
id: "athas-light",
42-
name: "Athas Light",
43-
description: "Clean and bright theme",
44-
category: "Light",
45-
icon: <Sun size={14} />,
46-
},
47-
// Dark theme
48-
{
49-
id: "athas-dark",
50-
name: "Athas Dark",
51-
description: "Modern dark theme",
52-
category: "Dark",
53-
icon: <Moon size={14} />,
54-
},
55-
];
30+
const getThemeIcon = (category: string, _isDark?: boolean): React.ReactNode => {
31+
switch (category) {
32+
case "System":
33+
return <Monitor size={14} />;
34+
case "Light":
35+
return <Sun size={14} />;
36+
case "Dark":
37+
return <Moon size={14} />;
38+
default:
39+
return <Palette size={14} />;
40+
}
41+
};
5642

5743
const ThemeSelector = ({ isVisible, onClose, onThemeChange, currentTheme }: ThemeSelectorProps) => {
5844
const [query, setQuery] = useState("");
5945
const [selectedIndex, setSelectedIndex] = useState(0);
6046
const [initialTheme, setInitialTheme] = useState(currentTheme);
61-
const [previewTheme, setPreviewTheme] = useState<Theme | null>(null);
47+
const [previewTheme, setPreviewTheme] = useState<string | null>(null);
48+
const [themes, setThemes] = useState<ThemeInfo[]>([]);
6249
const inputRef = useRef<HTMLInputElement>(null);
6350
const resultsRef = useRef<HTMLDivElement>(null);
6451

65-
// Focus is handled internally when the selector becomes visible
52+
// Load themes from theme registry
53+
useEffect(() => {
54+
const loadThemes = () => {
55+
const registryThemes = themeRegistry.getAllThemes();
56+
const themeInfos: ThemeInfo[] = [
57+
// System theme always first
58+
{
59+
id: "auto",
60+
name: "Auto",
61+
description: "Follow system preference",
62+
category: "System",
63+
icon: <Monitor size={14} />,
64+
},
65+
// Convert registry themes to ThemeInfo
66+
...registryThemes.map(
67+
(theme: ThemeDefinition): ThemeInfo => ({
68+
id: theme.id,
69+
name: theme.name,
70+
description: theme.description,
71+
category: theme.category,
72+
icon: getThemeIcon(theme.category, theme.isDark),
73+
}),
74+
),
75+
];
76+
setThemes(themeInfos);
77+
};
78+
79+
loadThemes();
80+
81+
// Listen for theme registry changes
82+
const unsubscribe = themeRegistry.onRegistryChange(loadThemes);
83+
return unsubscribe;
84+
}, []);
6685

6786
// Filter themes based on query
68-
const filteredThemes = THEME_DEFINITIONS.filter(
87+
const filteredThemes = themes.filter(
6988
(theme) =>
7089
theme.name.toLowerCase().includes(query.toLowerCase()) ||
7190
theme.description?.toLowerCase().includes(query.toLowerCase()) ||
@@ -79,12 +98,12 @@ const ThemeSelector = ({ isVisible, onClose, onThemeChange, currentTheme }: Them
7998
setQuery("");
8099
setPreviewTheme(null);
81100

82-
const initialIndex = THEME_DEFINITIONS.findIndex((t) => t.id === currentTheme);
101+
const initialIndex = themes.findIndex((t) => t.id === currentTheme);
83102
setSelectedIndex(initialIndex >= 0 ? initialIndex : 0);
84103

85104
requestAnimationFrame(() => inputRef.current?.focus());
86105
}
87-
}, [isVisible]);
106+
}, [isVisible, themes, currentTheme]);
88107

89108
const handleKeyDown = useCallback(
90109
(e: KeyboardEvent) => {
@@ -143,6 +162,31 @@ const ThemeSelector = ({ isVisible, onClose, onThemeChange, currentTheme }: Them
143162
selectedElement?.scrollIntoView({ block: "nearest", behavior: "smooth" });
144163
}, [selectedIndex]);
145164

165+
const handleUploadTheme = async () => {
166+
// Create file input element
167+
const input = document.createElement("input");
168+
input.type = "file";
169+
input.accept = ".toml";
170+
input.onchange = async (e) => {
171+
const file = (e.target as HTMLInputElement).files?.[0];
172+
if (file) {
173+
const { uploadTheme } = await import("../../../utils/theme-upload");
174+
const result = await uploadTheme(file);
175+
if (result.success) {
176+
console.log("Theme uploaded successfully:", result.theme?.name);
177+
// Optionally switch to the newly uploaded theme
178+
if (result.theme) {
179+
onThemeChange(result.theme.id);
180+
onClose();
181+
}
182+
} else {
183+
console.error("Theme upload failed:", result.error);
184+
}
185+
}
186+
};
187+
input.click();
188+
};
189+
146190
if (!isVisible) return null;
147191

148192
const handleClose = () => {
@@ -155,12 +199,23 @@ const ThemeSelector = ({ isVisible, onClose, onThemeChange, currentTheme }: Them
155199
return (
156200
<Command isVisible={isVisible}>
157201
<CommandHeader onClose={handleClose}>
158-
<CommandInput
159-
ref={inputRef}
160-
value={query}
161-
onChange={setQuery}
162-
placeholder="Search themes..."
163-
/>
202+
<div className="flex w-full items-center gap-2">
203+
<CommandInput
204+
ref={inputRef}
205+
value={query}
206+
onChange={setQuery}
207+
placeholder="Search themes..."
208+
className="flex-1"
209+
/>
210+
<Button
211+
onClick={handleUploadTheme}
212+
variant="ghost"
213+
size="xs"
214+
className="flex-shrink-0 gap-1 px-2"
215+
>
216+
<Upload size={12} />
217+
</Button>
218+
</div>
164219
</CommandHeader>
165220

166221
<CommandList ref={resultsRef}>
File renamed without changes.

src/components/editor/core/text-editor.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useCallback, useEffect, useRef, useState } from "react";
2-
import { EDITOR_CONSTANTS } from "../../../constants/editor-constants";
32
import { basicEditingExtension } from "../../../extensions/basic-editing-extension";
43
import { editorAPI } from "../../../extensions/editor-api";
54
import { extensionManager } from "../../../extensions/extension-manager";

src/components/extensions-view.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ interface Extension {
1616
interface ExtensionsViewProps {
1717
onServerInstall: (server: Extension) => void;
1818
onServerUninstall: (serverId: string) => void;
19-
onThemeChange: (theme: "auto" | "athas-light" | "athas-dark") => void;
20-
currentTheme: "auto" | "athas-light" | "athas-dark";
19+
onThemeChange: (theme: string) => void;
20+
currentTheme: string;
2121
}
2222

2323
const AVAILABLE_EXTENSIONS: Extension[] = [

src/components/layout/main-layout.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import { useUIState } from "../../stores/ui-state-store";
1212
import { type GitHunk, stageHunk, unstageHunk } from "../../utils/git";
1313
import AIChat from "../ai-chat/ai-chat";
1414
import BottomPane from "../bottom-pane";
15-
import CommandBar from "../command/command-bar";
16-
import CommandPalette from "../command/command-palette";
17-
import ThemeSelector from "../command/theme-selector";
15+
import CommandBar from "../command/components/command-bar";
16+
import CommandPalette from "../command/components/command-palette";
17+
import ThemeSelector from "../command/components/theme-selector";
1818
import type { Diagnostic } from "../diagnostics/diagnostics-pane";
1919
import DiffViewer from "../diff-viewer/diff-viewer";
2020
import CodeEditor from "../editor/code-editor";
@@ -49,7 +49,7 @@ export function MainLayout() {
4949
const sidebarPosition = "left" as "left" | "right";
5050

5151
// Handle theme change
52-
const handleThemeChange = (theme: "auto" | "athas-light" | "athas-dark") => {
52+
const handleThemeChange = (theme: string) => {
5353
updateTheme(theme);
5454
};
5555

0 commit comments

Comments
 (0)