mod color_scheme; mod composite_icon; mod config; mod icon_index; mod ui; use gtk::prelude::*; use gtk::{glib, Application, ApplicationWindow, Stack}; use std::path::{Path, PathBuf}; use crate::color_scheme::ColorScheme; use crate::composite_icon::CompositeIcon; use std::cell::RefCell; use std::rc::Rc; const APP_ID: &str = "dev.rootiest.IconColorTool"; fn main() -> glib::ExitCode { let app = Application::builder().application_id(APP_ID).build(); app.connect_activate(|app| { let breeze_path = match config::load_breeze_path() { Ok(p) => p, Err(e) => { eprintln!("Failed to resolve breeze-icons path: {e:#}"); std::process::exit(1); } }; let initial_arg = std::env::args().nth(1); let initial_icon = initial_arg .as_deref() .map(|a| resolve_icon_arg(a, &breeze_path)); if let Some(Err(e)) = &initial_icon { eprintln!("Error: {e}"); std::process::exit(1); } let window = ApplicationWindow::builder() .application(app) .title("Icon Color Tool") .default_width(900) .default_height(800) .build(); let stack = Stack::new(); window.set_child(Some(&stack)); let current_composite: Rc>>>> = Rc::new(RefCell::new(None)); { let current_composite = Rc::clone(¤t_composite); let window_clone = window.clone(); window.connect_close_request(move |_| { if let Some(comp) = current_composite.borrow().as_ref() { if comp.borrow().is_modified { let dialog = gtk::AlertDialog::builder() .message("You have unsaved changes. Are you sure you want to close?") .detail("All unsaved changes will be lost.") .buttons(["Cancel", "Discard"]) .cancel_button(0) .default_button(1) .build(); let window_clone2 = window_clone.clone(); let comp_ref = Rc::clone(¤t_composite); dialog.choose(Some(&window_clone), gtk::gio::Cancellable::NONE, move |res| { if let Ok(1) = res { if let Some(c) = comp_ref.borrow().as_ref() { c.borrow_mut().is_modified = false; } window_clone2.close(); } }); return glib::Propagation::Stop; } } glib::Propagation::Proceed }); } build_picker(&stack, &window, breeze_path.clone(), Rc::clone(¤t_composite)); if let Some(Ok(path)) = initial_icon { build_editor(&stack, &window, path, breeze_path, Rc::clone(¤t_composite)); } window.present(); }); app.run() } fn resolve_icon_arg(arg: &str, breeze_path: &Path) -> anyhow::Result { let as_given = PathBuf::from(arg); if as_given.is_absolute() { anyhow::ensure!( as_given.exists(), "Icon not found at {}", as_given.display() ); return Ok(as_given); } if as_given.exists() { return Ok(as_given.canonicalize()?); } let under_breeze = breeze_path.join(arg); anyhow::ensure!( under_breeze.exists(), "Icon not found at {} or {}", as_given.display(), under_breeze.display() ); Ok(under_breeze.canonicalize()?) } fn build_picker(stack: &Stack, window: &ApplicationWindow, breeze_path: PathBuf, current_composite: Rc>>>>) { let stack_for_callback = stack.clone(); let window_for_callback = window.clone(); let breeze_for_editor = breeze_path.clone(); let comp_callback = Rc::clone(¤t_composite); let widget = ui::picker::build_picker_page(breeze_path, move |icon_path| { build_editor(&stack_for_callback, &window_for_callback, icon_path, breeze_for_editor.clone(), Rc::clone(&comp_callback)); }); stack.add_named(&widget, Some("picker")); stack.set_visible_child_name("picker"); } fn build_editor(stack: &Stack, window: &ApplicationWindow, icon_path: PathBuf, breeze_path: PathBuf, current_composite: Rc>>>>) { let mut scheme = match ColorScheme::load(&icon_path) { Ok(s) => s, Err(e) => { eprintln!("Failed to open {}: {e:#}", icon_path.display()); return; } }; scheme.auto_save = false; let composite = Rc::new(RefCell::new(CompositeIcon::new(scheme))); *current_composite.borrow_mut() = Some(Rc::clone(&composite)); let editor_container = gtk::Box::new(gtk::Orientation::Vertical, 0); let rebuild_editor = { let composite = Rc::clone(&composite); let stack = stack.clone(); let window = window.clone(); let editor_container = editor_container.clone(); let current_comp = Rc::clone(¤t_composite); Rc::new(move || { while let Some(child) = editor_container.first_child() { editor_container.remove(&child); } let stack_clone = stack.clone(); let stack_emblem = stack.clone(); let window_clone = window.clone(); let comp = Rc::clone(&composite); let comp_for_back = Rc::clone(&comp); let current_comp_for_back = Rc::clone(¤t_comp); let page = ui::editor::build_editor_page( Rc::clone(&composite), move || { if comp_for_back.borrow().is_modified { let dialog = gtk::AlertDialog::builder() .message("You have unsaved changes. Are you sure you want to discard them?") .detail("All unsaved changes will be lost.") .buttons(["Cancel", "Discard"]) .cancel_button(0) .default_button(1) .build(); let stack_clone = stack_clone.clone(); let comp_ref = Rc::clone(¤t_comp_for_back); dialog.choose(Some(&window_clone), gtk::gio::Cancellable::NONE, move |res| { if let Ok(1) = res { *comp_ref.borrow_mut() = None; stack_clone.set_visible_child_name("picker"); } }); } else { *current_comp_for_back.borrow_mut() = None; stack_clone.set_visible_child_name("picker"); } }, move || { stack_emblem.set_visible_child_name("emblem_picker"); }, { let window_clone = window.clone(); let comp = Rc::clone(&comp); move || { let filter = gtk::FileFilter::new(); filter.set_name(Some("SVG Images")); filter.add_mime_type("image/svg+xml"); filter.add_pattern("*.svg"); let filters = gtk::gio::ListStore::new::(); filters.append(&filter); let dialog = gtk::FileDialog::builder() .title("Save Composite Icon As...") .filters(&filters) .default_filter(&filter) .build(); let c = Rc::clone(&comp); dialog.save(Some(&window_clone), gtk::gio::Cancellable::NONE, move |res| { if let Ok(file) = res { if let Some(path) = file.path() { let rendered = c.borrow().render(); if let Err(e) = std::fs::write(&path, rendered) { eprintln!("Failed to save composite to {}: {e}", path.display()); } else { c.borrow_mut().is_modified = false; } } } }); } } ); editor_container.append(&page); }) }; rebuild_editor(); if let Some(existing) = stack.child_by_name("editor") { stack.remove(&existing); } stack.add_named(&editor_container, Some("editor")); let rebuild_editor_clone = Rc::clone(&rebuild_editor); let stack_emblem_callback = stack.clone(); let comp_for_picker = Rc::clone(&composite); let emblem_picker = ui::picker::build_picker_page(breeze_path, move |emblem_path| { if let Ok(mut emblem_scheme) = ColorScheme::load_with_namespace(&emblem_path, "Emblem") { emblem_scheme.auto_save = false; let mut c = comp_for_picker.borrow_mut(); c.set_emblem(emblem_scheme); c.is_modified = true; drop(c); rebuild_editor_clone(); stack_emblem_callback.set_visible_child_name("editor"); } else { eprintln!("Failed to load emblem: {}", emblem_path.display()); } }); if let Some(existing) = stack.child_by_name("emblem_picker") { stack.remove(&existing); } stack.add_named(&emblem_picker, Some("emblem_picker")); stack.set_visible_child_name("editor"); }