Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4145029ed4
|
||
|
|
77399f938a | ||
|
|
2906bdad03 | ||
|
|
a4dfef304a
|
||
|
|
960c729aed | ||
|
|
5cd93328f5 |
+24
@@ -1,3 +1,5 @@
|
||||
#### Submodules
|
||||
breeze-icons/
|
||||
|
||||
# ──────────────── Added by agents-init ──────────────────
|
||||
# agents-init --agents
|
||||
@@ -14,3 +16,25 @@ docs/specs
|
||||
docs/devlogs
|
||||
# ────────────────────────────────────────────────────────
|
||||
/target
|
||||
|
||||
# id: gi-patterns-b05185fcb02652ad80fb49aa015b2ae8
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/rust
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=rust
|
||||
|
||||
### Rust ###
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/rust
|
||||
|
||||
@@ -7,6 +7,8 @@ embedded in [Breeze icon](https://github.com/KDE/breeze-icons) SVGs.
|
||||
|
||||
```sh
|
||||
icon-color-tool # opens a searchable icon picker
|
||||
icon-color-tool -h # print colorful help output
|
||||
icon-color-tool -d path/to/icons # use a custom icon directory
|
||||
icon-color-tool path/to/icon.svg # opens directly in the color editor
|
||||
```
|
||||
|
||||
|
||||
-1
Submodule breeze-icons deleted from f8d528544f
+9
-2
@@ -2,7 +2,7 @@
|
||||
set -e
|
||||
|
||||
REPO_URL="https://git.rootiest.dev/rootiest/icon-color-tool.git"
|
||||
DOWNLOAD_URL="https://git.rootiest.dev/rootiest/icon-color-tool/releases/latest/download/icon-color-tool"
|
||||
API_URL="https://git.rootiest.dev/api/v1/repos/rootiest/icon-color-tool/releases/latest"
|
||||
INSTALL_DIR="${XDG_BIN_HOME:-$HOME/.local/bin}"
|
||||
TMP_DIR=$(mktemp -d)
|
||||
|
||||
@@ -35,9 +35,16 @@ if [ "$BUILD" -eq 1 ]; then
|
||||
cd - > /dev/null
|
||||
else
|
||||
echo "Mode: Downloading pre-built binary"
|
||||
DOWNLOAD_URL=$(curl -sL "$API_URL" | grep -o '"browser_download_url":"[^"]*"' | cut -d'"' -f4 | head -n 1)
|
||||
|
||||
if [ -z "$DOWNLOAD_URL" ]; then
|
||||
echo "Error: Could not find latest release binary download URL."
|
||||
echo "If the release doesn't exist yet, you can use the --build flag to build from source."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! curl -sfL "$DOWNLOAD_URL" -o "$TMP_DIR/icon-color-tool"; then
|
||||
echo "Error: Failed to download binary from $DOWNLOAD_URL"
|
||||
echo "If the release doesn't exist yet, you can use the --build flag to build from source."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -63,7 +63,7 @@ impl CompositeIcon {
|
||||
if let Some(emblem) = &self.emblem {
|
||||
|
||||
fn parse_dims(svg: &str) -> (f64, f64) {
|
||||
if let Some(re) = Regex::new(r#"(?i)<svg[^>]*viewBox\s*=\s*"([^"]+)""#).ok() {
|
||||
if let Ok(re) = Regex::new(r#"(?i)<svg[^>]*viewBox\s*=\s*"([^"]+)""#) {
|
||||
if let Some(caps) = re.captures(svg) {
|
||||
let parts: Vec<&str> = caps.get(1).unwrap().as_str().split_whitespace().collect();
|
||||
if parts.len() == 4 {
|
||||
@@ -75,14 +75,14 @@ impl CompositeIcon {
|
||||
}
|
||||
let mut w = 64.0;
|
||||
let mut h = 64.0;
|
||||
if let Some(re_w) = Regex::new(r#"(?i)<svg[^>]*width\s*=\s*"([^"a-zA-Z]+)[a-zA-Z]*""#).ok() {
|
||||
if let Ok(re_w) = Regex::new(r#"(?i)<svg[^>]*width\s*=\s*"([^"a-zA-Z]+)[a-zA-Z]*""#) {
|
||||
if let Some(caps) = re_w.captures(svg) {
|
||||
if let Ok(val) = caps.get(1).unwrap().as_str().trim().parse::<f64>() {
|
||||
w = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(re_h) = Regex::new(r#"(?i)<svg[^>]*height\s*=\s*"([^"a-zA-Z]+)[a-zA-Z]*""#).ok() {
|
||||
if let Ok(re_h) = Regex::new(r#"(?i)<svg[^>]*height\s*=\s*"([^"a-zA-Z]+)[a-zA-Z]*""#) {
|
||||
if let Some(caps) = re_h.captures(svg) {
|
||||
if let Ok(val) = caps.get(1).unwrap().as_str().trim().parse::<f64>() {
|
||||
h = val;
|
||||
|
||||
+4
-4
@@ -37,11 +37,11 @@ pub fn list_svg_icons(root: &Path) -> Vec<PathBuf> {
|
||||
|
||||
let size_str = if let Some(c) = components.next() {
|
||||
c.as_os_str().to_string_lossy().into_owned()
|
||||
} else { continue; };
|
||||
} else { String::new() };
|
||||
|
||||
let category = if let Some(c) = components.next() {
|
||||
c.as_os_str().to_string_lossy().into_owned()
|
||||
} else { continue; };
|
||||
} else { String::new() };
|
||||
|
||||
let score = if size_str == "scalable" {
|
||||
10000
|
||||
@@ -75,8 +75,8 @@ pub fn fuzzy_match_score(query: &str, candidate: &str) -> Option<i32> {
|
||||
let mut q_char = q_chars.next().unwrap();
|
||||
let mut match_indices = Vec::new();
|
||||
|
||||
let mut chars = candidate_lower.char_indices();
|
||||
while let Some((i, c)) = chars.next() {
|
||||
let chars = candidate_lower.char_indices();
|
||||
for (i, c) in chars {
|
||||
if c == q_char {
|
||||
match_indices.push(i);
|
||||
if let Some(next_q) = q_chars.next() {
|
||||
|
||||
+46
-11
@@ -31,18 +31,52 @@ use std::rc::Rc;
|
||||
const APP_ID: &str = "dev.rootiest.IconColorTool";
|
||||
|
||||
fn main() -> glib::ExitCode {
|
||||
let mut args: Vec<String> = std::env::args().collect();
|
||||
if args.iter().any(|a| a == "-h" || a == "--help") {
|
||||
println!("\x1b[1;36micon-color-tool\x1b[0m - Edit the ColorScheme-* CSS colors in KDE Breeze icons\n");
|
||||
println!("\x1b[1;33mUSAGE:\x1b[0m");
|
||||
println!(" icon-color-tool [OPTIONS] [FILE]\n");
|
||||
println!("\x1b[1;33mARGS:\x1b[0m");
|
||||
println!(" \x1b[32m<FILE>\x1b[0m Open a specific SVG file directly in the color editor\n");
|
||||
println!("\x1b[1;33mOPTIONS:\x1b[0m");
|
||||
println!(" \x1b[32m-d, --icon-dir <DIR>\x1b[0m Specify a custom directory to source icons from");
|
||||
println!(" \x1b[32m-h, --help\x1b[0m Print help information");
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
let mut custom_icon_dir = None;
|
||||
let mut i = 1;
|
||||
while i < args.len() {
|
||||
if (args[i] == "--icon-dir" || args[i] == "-d") && i + 1 < args.len() {
|
||||
custom_icon_dir = Some(PathBuf::from(&args[i + 1]));
|
||||
args.remove(i);
|
||||
args.remove(i);
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let initial_arg = args.get(1).cloned();
|
||||
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);
|
||||
app.connect_activate(move |app| {
|
||||
let breeze_path = match &custom_icon_dir {
|
||||
Some(p) => {
|
||||
if !p.exists() {
|
||||
eprintln!("Error: Icon directory {} does not exist", p.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
p.clone()
|
||||
}
|
||||
None => 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));
|
||||
@@ -104,7 +138,7 @@ fn main() -> glib::ExitCode {
|
||||
window.present();
|
||||
});
|
||||
|
||||
app.run()
|
||||
app.run_with_args(&args)
|
||||
}
|
||||
|
||||
fn resolve_icon_arg(arg: &str, breeze_path: &Path) -> anyhow::Result<PathBuf> {
|
||||
@@ -136,8 +170,9 @@ fn build_picker(stack: &Stack, window: &ApplicationWindow, breeze_path: PathBuf,
|
||||
let breeze_for_editor = breeze_path.clone();
|
||||
let comp_callback = Rc::clone(¤t_composite);
|
||||
|
||||
let widget = ui::picker::build_picker_page(breeze_path, "folder.svg", move |icon_path| {
|
||||
build_editor(&stack_for_callback, &window_for_callback, icon_path, breeze_for_editor.clone(), Rc::clone(&comp_callback));
|
||||
let window_for_closure = window_for_callback.clone();
|
||||
let widget = ui::picker::build_picker_page(&window_for_callback, breeze_path, "folder.svg", move |icon_path| {
|
||||
build_editor(&stack_for_callback, &window_for_closure, icon_path, breeze_for_editor.clone(), Rc::clone(&comp_callback));
|
||||
});
|
||||
stack.add_named(&widget, Some("picker"));
|
||||
stack.set_visible_child_name("picker");
|
||||
@@ -253,7 +288,7 @@ fn build_editor(stack: &Stack, window: &ApplicationWindow, icon_path: PathBuf, b
|
||||
let stack_emblem_callback = stack.clone();
|
||||
let comp_for_picker = Rc::clone(&composite);
|
||||
|
||||
let emblem_picker = ui::picker::build_picker_page(breeze_path, "symbolic", move |emblem_path| {
|
||||
let emblem_picker = ui::picker::build_picker_page(window, breeze_path, "symbolic", 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();
|
||||
|
||||
+10
-6
@@ -240,9 +240,12 @@ pub fn build_editor_page(
|
||||
}
|
||||
|
||||
let popover = gtk::Popover::new();
|
||||
#[allow(deprecated)]
|
||||
let chooser = gtk::ColorChooserWidget::new();
|
||||
#[allow(deprecated)]
|
||||
chooser.set_use_alpha(false);
|
||||
chooser.set_rgba(&*current_rgba.borrow());
|
||||
#[allow(deprecated)]
|
||||
chooser.set_rgba(¤t_rgba.borrow());
|
||||
popover.set_child(Some(&chooser));
|
||||
|
||||
let button = gtk::MenuButton::builder()
|
||||
@@ -257,7 +260,9 @@ pub fn build_editor_page(
|
||||
let current_rgba = Rc::clone(¤t_rgba);
|
||||
let drawing_area = drawing_area.clone();
|
||||
|
||||
#[allow(deprecated)]
|
||||
chooser.connect_rgba_notify(move |c| {
|
||||
#[allow(deprecated)]
|
||||
let new_rgba = c.rgba();
|
||||
let new_hex = format!(
|
||||
"#{:02x}{:02x}{:02x}",
|
||||
@@ -267,12 +272,11 @@ pub fn build_editor_page(
|
||||
);
|
||||
let changed = {
|
||||
let mut comp = composite.borrow_mut();
|
||||
let res;
|
||||
if is_emblem {
|
||||
res = comp.emblem.as_mut().unwrap().update_color(&tag, &new_hex).unwrap_or(false);
|
||||
let res = if is_emblem {
|
||||
comp.emblem.as_mut().unwrap().update_color(&tag, &new_hex).unwrap_or(false)
|
||||
} else {
|
||||
res = comp.bg.update_color(&tag, &new_hex).unwrap_or(false);
|
||||
}
|
||||
comp.bg.update_color(&tag, &new_hex).unwrap_or(false)
|
||||
};
|
||||
if res {
|
||||
comp.is_modified = true;
|
||||
}
|
||||
|
||||
+41
-2
@@ -24,10 +24,13 @@ use std::rc::Rc;
|
||||
const RESULT_LIMIT: usize = 300;
|
||||
|
||||
pub fn build_picker_page(
|
||||
window: >k::ApplicationWindow,
|
||||
breeze_root: PathBuf,
|
||||
initial_query: &str,
|
||||
on_select: impl Fn(PathBuf) + 'static,
|
||||
) -> gtk::Widget {
|
||||
let on_select = Rc::new(on_select) as Rc<dyn Fn(PathBuf)>;
|
||||
|
||||
let root = GtkBox::new(Orientation::Vertical, 8);
|
||||
root.set_margin_top(8);
|
||||
root.set_margin_bottom(8);
|
||||
@@ -39,7 +42,42 @@ pub fn build_picker_page(
|
||||
search.set_text(initial_query);
|
||||
}
|
||||
search.set_placeholder_text(Some("Search icons…"));
|
||||
root.append(&search);
|
||||
search.set_hexpand(true);
|
||||
|
||||
let open_button = gtk::Button::with_label("Open SVG…");
|
||||
{
|
||||
let window_clone = window.clone();
|
||||
let on_select_clone = Rc::clone(&on_select);
|
||||
open_button.connect_clicked(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::<gtk::FileFilter>();
|
||||
filters.append(&filter);
|
||||
|
||||
let dialog = gtk::FileDialog::builder()
|
||||
.title("Open SVG")
|
||||
.filters(&filters)
|
||||
.default_filter(&filter)
|
||||
.build();
|
||||
|
||||
let on_select_clone2 = Rc::clone(&on_select_clone);
|
||||
dialog.open(Some(&window_clone), gtk::gio::Cancellable::NONE, move |res| {
|
||||
if let Ok(file) = res {
|
||||
if let Some(path) = file.path() {
|
||||
on_select_clone2(path);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let search_box = GtkBox::new(Orientation::Horizontal, 8);
|
||||
search_box.append(&search);
|
||||
search_box.append(&open_button);
|
||||
root.append(&search_box);
|
||||
|
||||
let flow_box = FlowBox::new();
|
||||
flow_box.set_valign(gtk::Align::Start);
|
||||
@@ -63,13 +101,14 @@ pub fn build_picker_page(
|
||||
|
||||
{
|
||||
let current_matches = Rc::clone(¤t_matches);
|
||||
let on_select_clone = Rc::clone(&on_select);
|
||||
flow_box.connect_child_activated(move |_, child| {
|
||||
let index = child.index();
|
||||
if index < 0 {
|
||||
return;
|
||||
}
|
||||
if let Some(path) = current_matches.borrow().get(index as usize) {
|
||||
on_select(path.clone());
|
||||
on_select_clone(path.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user