Skip to main content

max / makeover

shared: theme-common delete-by-id + preview palette, updater progress + markdown hook theme-common: add delete_theme(custom_dir, id) with id validation and custom-dir scoping; add ThemePreview struct + load_theme_preview() for UI thumbnails that don't need the full color HashMap. 6 new tests, 34 total. tauri-updater-ui: wire pendingUpdate.downloadAndInstall(onEvent) with Started/Progress/Finished handlers into a thin progress bar + percent status line; add opts.renderMarkdown(body) hook so callers can render full release notes as sanitized HTML in a scrollable max-height block. Falls back to the 120-char text truncation when no hook is supplied. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-25 19:56 UTC
Commit: dd78c60705e5b7f7c9cc0818b64c18c69e0f925f
Parent: 253ce26
1 file changed, +140 insertions, -0 deletions
M src/lib.rs +140
@@ -218,6 +218,70 @@ pub fn import_theme(source_path: &Path, custom_dir: &Path) -> Result<ThemeMeta,
218 218 Ok(parse_meta(&id, &table, true))
219 219 }
220 220
221 + /// Delete a custom theme by ID.
222 + ///
223 + /// Only operates on `custom_dir` — bundled themes are not deletable through
224 + /// this entry point. Returns `Err` if the ID is invalid, the file does not
225 + /// exist, or the underlying remove fails.
226 + pub fn delete_theme(custom_dir: &Path, id: &str) -> Result<(), String> {
227 + validate_theme_id(id)?;
228 +
229 + let path = custom_dir.join(format!("{}.toml", id));
230 + if !path.is_file() {
231 + return Err(format!("Custom theme '{}' not found", id));
232 + }
233 +
234 + std::fs::remove_file(&path)
235 + .map_err(|e| format!("Failed to delete {}: {}", path.display(), e))
236 + }
237 +
238 + /// A four-color palette for preview chips, swatches, etc.
239 + ///
240 + /// Smaller than `ThemeColors`: only the `primary` value from each of the four
241 + /// canonical sections, so callers rendering a list of theme thumbnails don't
242 + /// need to allocate a full `HashMap` per row.
243 + #[derive(Debug, Clone, Serialize)]
244 + #[serde(rename_all = "camelCase")]
245 + pub struct ThemePreview {
246 + pub meta: ThemeMeta,
247 + pub background: Option<String>,
248 + pub foreground: Option<String>,
249 + pub accent: Option<String>,
250 + pub border: Option<String>,
251 + }
252 +
253 + fn primary_color(table: &toml::Table, section: &str) -> Option<String> {
254 + table
255 + .get(section)
256 + .and_then(|s| s.as_table())
257 + .and_then(|s| s.get("primary"))
258 + .and_then(|v| v.as_str())
259 + .map(|s| s.to_string())
260 + }
261 +
262 + /// Load just the primary colors for a theme — for UI previews / thumbnails.
263 + pub fn load_theme_preview(dirs: &[(PathBuf, bool)], id: &str) -> Result<ThemePreview, String> {
264 + validate_theme_id(id)?;
265 +
266 + let (path, is_custom) =
267 + find_theme_path(dirs, id).ok_or_else(|| format!("Theme '{}' not found", id))?;
268 +
269 + let content = std::fs::read_to_string(&path)
270 + .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
271 +
272 + let table: toml::Table = content
273 + .parse()
274 + .map_err(|e| format!("Failed to parse {}: {}", path.display(), e))?;
275 +
276 + Ok(ThemePreview {
277 + meta: parse_meta(id, &table, is_custom),
278 + background: primary_color(&table, "background"),
279 + foreground: primary_color(&table, "foreground"),
280 + accent: primary_color(&table, "accent"),
281 + border: primary_color(&table, "border"),
282 + })
283 + }
284 +
221 285 /// Export a theme to a user-chosen path.
222 286 ///
223 287 /// Finds the theme by ID in the given directories and copies the TOML file
@@ -604,6 +668,82 @@ primary = "#c0caf5"
604 668 }
605 669
606 670 #[test]
671 + fn delete_theme_removes_file() {
672 + let custom = tempfile::tempdir().unwrap();
673 + let path = custom.path().join("doomed.toml");
674 + fs::write(&path, "[background]\nprimary = \"#000\"\n").unwrap();
675 + assert!(path.exists());
676 +
677 + delete_theme(custom.path(), "doomed").unwrap();
678 + assert!(!path.exists());
679 + }
680 +
681 + #[test]
682 + fn delete_theme_rejects_invalid_id() {
683 + let custom = tempfile::tempdir().unwrap();
684 + assert!(delete_theme(custom.path(), "../etc/passwd").is_err());
685 + }
686 +
687 + #[test]
688 + fn delete_theme_not_found() {
689 + let custom = tempfile::tempdir().unwrap();
690 + assert!(delete_theme(custom.path(), "ghost").is_err());
691 + }
692 +
693 + #[test]
694 + fn load_theme_preview_returns_primaries() {
695 + let dir = tempfile::tempdir().unwrap();
696 + let content = r##"
697 + [meta]
698 + name = "Preview Me"
699 + variant = "dark"
700 +
701 + [background]
702 + primary = "#111"
703 + secondary = "#222"
704 +
705 + [foreground]
706 + primary = "#eee"
707 +
708 + [accent]
709 + primary = "#f0a"
710 +
711 + [border]
712 + primary = "#444"
713 + "##;
714 + fs::write(dir.path().join("preview.toml"), content).unwrap();
715 +
716 + let dirs = vec![(dir.path().to_path_buf(), false)];
717 + let p = load_theme_preview(&dirs, "preview").unwrap();
718 + assert_eq!(p.meta.name, "Preview Me");
719 + assert_eq!(p.background.as_deref(), Some("#111"));
720 + assert_eq!(p.foreground.as_deref(), Some("#eee"));
721 + assert_eq!(p.accent.as_deref(), Some("#f0a"));
722 + assert_eq!(p.border.as_deref(), Some("#444"));
723 + }
724 +
725 + #[test]
726 + fn load_theme_preview_missing_sections_are_none() {
727 + let dir = tempfile::tempdir().unwrap();
728 + let content = "[background]\nprimary = \"#000\"\n";
729 + fs::write(dir.path().join("sparse.toml"), content).unwrap();
730 +
731 + let dirs = vec![(dir.path().to_path_buf(), false)];
732 + let p = load_theme_preview(&dirs, "sparse").unwrap();
733 + assert_eq!(p.background.as_deref(), Some("#000"));
734 + assert!(p.foreground.is_none());
735 + assert!(p.accent.is_none());
736 + assert!(p.border.is_none());
737 + }
738 +
739 + #[test]
740 + fn load_theme_preview_not_found() {
741 + let dir = tempfile::tempdir().unwrap();
742 + let dirs = vec![(dir.path().to_path_buf(), false)];
743 + assert!(load_theme_preview(&dirs, "missing").is_err());
744 + }
745 +
746 + #[test]
607 747 fn export_theme_not_found() {
608 748 let dir = tempfile::tempdir().unwrap();
609 749 let dirs = vec![(dir.path().to_path_buf(), false)];