diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a558bdf
--- /dev/null
+++ b/README.md
@@ -0,0 +1,46 @@
+# icon-color-tool
+
+A small GTK4 desktop app for editing the `ColorScheme-*` CSS colors
+embedded in [Breeze icon](https://github.com/KDE/breeze-icons) SVGs.
+
+## Usage
+
+```sh
+icon-color-tool # opens a searchable icon picker
+icon-color-tool path/to/icon.svg # opens directly in the color editor
+```
+
+The picker's search box does a fuzzy (subsequence) match against each
+icon's path. Selecting an icon (or passing one directly) opens the color
+editor: click a swatch to pick a new color for that `ColorScheme-*` class;
+changes save to disk immediately, and Undo/Redo are available from the
+toolbar.
+
+## Configuration
+
+On first run, a config file is created at
+`~/.config/icon-color-tool/config.toml`:
+
+```toml
+[settings]
+breeze_path = "~/.local/share/breeze-icons"
+```
+
+If the configured `breeze_path` doesn't exist (or is empty), the
+[breeze-icons](https://github.com/KDE/breeze-icons) repository is cloned
+there automatically (shallow clone via `git`, which must be installed and
+on `PATH`).
+
+Set the `BREEZE_ICONS_PATH` environment variable to override the
+configured path entirely (also auto-clones if missing).
+
+## Building
+
+Requires Rust (stable) and the GTK4 + librsvg development libraries
+installed system-wide (e.g. `gtk4-devel`/`libgtk-4-dev` and
+`librsvg2-devel`/`librsvg2-dev`, depending on your distro).
+
+```sh
+cargo build --release
+./target/release/icon-color-tool
+```
diff --git a/config.py b/config.py
deleted file mode 100644
index 7234da6..0000000
--- a/config.py
+++ /dev/null
@@ -1,45 +0,0 @@
-import os
-import sys
-import subprocess
-from pathlib import Path
-import tomllib
-
-def get_config_dir():
- xdg_config = os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
- return Path(xdg_config) / 'svg-color-tool'
-
-def generate_default_config(config_path: Path):
- default_path = os.path.expanduser("~/.local/share/breeze-icons")
- config_content = f"""[settings]
-breeze_path = "{default_path}"
-"""
- config_path.parent.mkdir(parents=True, exist_ok=True)
- with open(config_path, "w") as f:
- f.write(config_content)
-
-def ensure_breeze_repo(breeze_path: str):
- path = Path(breeze_path)
- if not path.exists() or not any(path.iterdir()):
- print(f"Breeze icons not found at {path}. Cloning repository...")
- path.mkdir(parents=True, exist_ok=True)
- subprocess.run(["git", "clone", "--depth", "1", "https://github.com/KDE/breeze-icons.git", str(path)], check=True)
-
-def load_config():
- env_path = os.environ.get('BREEZE_ICONS_PATH')
- if env_path:
- ensure_breeze_repo(env_path)
- return env_path
-
- config_dir = get_config_dir()
- config_file = config_dir / 'config.toml'
-
- if not config_file.exists():
- generate_default_config(config_file)
-
- with open(config_file, "rb") as f:
- config_data = tomllib.load(f)
-
- breeze_path = config_data.get('settings', {}).get('breeze_path', os.path.expanduser("~/.local/share/breeze-icons"))
- breeze_path = os.path.expanduser(breeze_path)
- ensure_breeze_repo(breeze_path)
- return breeze_path
diff --git a/gui.py b/gui.py
deleted file mode 100644
index 2d79818..0000000
--- a/gui.py
+++ /dev/null
@@ -1,141 +0,0 @@
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout,
- QLabel, QColorDialog, QScrollArea, QFrame, QSplitter, QMainWindow,
- QMessageBox
-)
-from PyQt6.QtGui import QColor, QAction, QKeySequence
-from PyQt6.QtSvgWidgets import QSvgWidget
-from PyQt6.QtCore import Qt
-
-from svg_parser import SvgColorParser
-
-class ColorSwatch(QFrame):
- def __init__(self, tag, color_hex, on_click):
- super().__init__()
- self.tag = tag
- self.color_hex = color_hex
- self.on_click = on_click
-
- self.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Raised)
- self.setLineWidth(1)
-
- layout = QHBoxLayout()
- self.setLayout(layout)
-
- self.lbl_tag = QLabel(tag)
- self.lbl_color = QLabel(color_hex)
-
- self.swatch = QLabel()
- self.swatch.setFixedSize(30, 30)
- self.swatch.setAutoFillBackground(True)
- self.update_swatch_color(color_hex)
-
- layout.addWidget(self.swatch)
- layout.addWidget(self.lbl_tag)
- layout.addWidget(self.lbl_color)
- layout.addStretch()
-
- def update_swatch_color(self, hex_color):
- self.color_hex = hex_color
- self.lbl_color.setText(hex_color)
- self.swatch.setStyleSheet(f"background-color: {hex_color}; border: 1px solid black;")
-
- def mousePressEvent(self, event):
- if event.button() == Qt.MouseButton.LeftButton:
- self.on_click(self.tag, self.color_hex, self)
-
-class MainWindow(QMainWindow):
- def __init__(self, svg_path):
- super().__init__()
- self.setWindowTitle(f"SVG Color Editor - {svg_path}")
- self.resize(800, 600)
-
- self.parser = SvgColorParser(svg_path)
- self.svg_path = svg_path
-
- self.setup_ui()
- self.setup_menu()
-
- if not self.parser.colors:
- QMessageBox.warning(self, "No Colors Found", "No ColorScheme tags were found in the SVG file.")
-
- def setup_menu(self):
- menubar = self.menuBar()
- edit_menu = menubar.addMenu("Edit")
-
- self.undo_action = QAction("Undo", self)
- self.undo_action.setShortcut(QKeySequence.StandardKey.Undo)
- self.undo_action.triggered.connect(self.undo)
- edit_menu.addAction(self.undo_action)
-
- self.redo_action = QAction("Redo", self)
- self.redo_action.setShortcut(QKeySequence.StandardKey.Redo)
- self.redo_action.triggered.connect(self.redo)
- edit_menu.addAction(self.redo_action)
-
- def setup_ui(self):
- central_widget = QWidget()
- self.setCentralWidget(central_widget)
- main_layout = QVBoxLayout(central_widget)
-
- splitter = QSplitter(Qt.Orientation.Horizontal)
- main_layout.addWidget(splitter)
-
- # Left Panel - Colors
- scroll_area = QScrollArea()
- scroll_area.setWidgetResizable(True)
-
- self.colors_widget = QWidget()
- self.colors_layout = QVBoxLayout(self.colors_widget)
- scroll_area.setWidget(self.colors_widget)
-
- splitter.addWidget(scroll_area)
-
- # Right Panel - SVG Preview
- preview_container = QWidget()
- preview_layout = QVBoxLayout(preview_container)
-
- self.svg_widget = QSvgWidget(str(self.svg_path))
- self.svg_widget.renderer().setAspectRatioMode(Qt.AspectRatioMode.KeepAspectRatio)
- preview_layout.addWidget(self.svg_widget)
-
- splitter.addWidget(preview_container)
- splitter.setSizes([350, 450])
-
- self.populate_colors()
-
- def populate_colors(self):
- # Clear layout
- while self.colors_layout.count():
- item = self.colors_layout.takeAt(0)
- widget = item.widget()
- if widget:
- widget.deleteLater()
-
- for tag, color in self.parser.colors.items():
- swatch = ColorSwatch(tag, color, self.on_swatch_click)
- self.colors_layout.addWidget(swatch)
-
- self.colors_layout.addStretch()
-
- def on_swatch_click(self, tag, current_color, swatch_widget):
- color = QColorDialog.getColor(QColor(current_color), self, f"Select Color for {tag}")
- if color.isValid():
- new_hex = color.name() # Returns #RRGGBB
- self.parser.update_color(tag, new_hex)
- swatch_widget.update_swatch_color(new_hex)
- self.refresh_preview()
- self.populate_colors()
-
- def refresh_preview(self):
- self.svg_widget.load(self.parser.content.encode('utf-8'))
-
- def undo(self):
- if self.parser.undo():
- self.refresh_preview()
- self.populate_colors()
-
- def redo(self):
- if self.parser.redo():
- self.refresh_preview()
- self.populate_colors()
diff --git a/icon-color-tool b/icon-color-tool
deleted file mode 100755
index 603b4df..0000000
--- a/icon-color-tool
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/env bash
-
-python3 main.py
diff --git a/main.py b/main.py
deleted file mode 100644
index f57a70d..0000000
--- a/main.py
+++ /dev/null
@@ -1,89 +0,0 @@
-import sys
-import os
-import subprocess
-from pathlib import Path
-from config import load_config
-from gui import MainWindow
-from PyQt6.QtWidgets import QApplication
-
-
-def get_icon_with_fzf(breeze_path):
- try:
- preview_script = r'''
- FILE="$1"
- DIM="${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}"
- if command -v kitten >/dev/null; then
- kitten icat --clear --transfer-mode=memory --stdin=no --scale-up --place="${DIM}@${FZF_PREVIEW_LEFT:-0}x${FZF_PREVIEW_TOP:-0}" "$FILE" > /dev/tty
- elif command -v wezterm >/dev/null; then
- wezterm imgcat "$FILE"
- elif command -v chafa >/dev/null; then
- chafa -s "$DIM" "$FILE"
- else
- cat "$FILE"
- fi
- '''
- env = os.environ.copy()
- env['FZF_PREVIEW_SCRIPT'] = preview_script
-
- find_proc = subprocess.Popen(
- ["find", ".", "-type", "f", "-name", "*.svg"],
- cwd=breeze_path,
- stdout=subprocess.PIPE,
- )
- fzf_proc = subprocess.Popen(
- ["fzf", "--preview", "bash -c \"$FZF_PREVIEW_SCRIPT\" _ {}"],
- stdin=find_proc.stdout,
- stdout=subprocess.PIPE,
- text=True,
- cwd=breeze_path,
- env=env
- )
- find_proc.stdout.close()
- out, _ = fzf_proc.communicate()
- if fzf_proc.returncode == 0 and out.strip():
- return (Path(breeze_path) / out.strip()).resolve()
- return None
- except Exception as e:
- print(f"Error running fzf: {e}")
- return None
-
-
-def main():
- breeze_path = load_config()
-
- icon_path = None
- if len(sys.argv) > 1:
- arg_path = Path(sys.argv[1])
- if arg_path.is_absolute():
- icon_path = arg_path
- else:
- # First try relative to current directory
- if arg_path.exists():
- icon_path = arg_path.resolve()
- else:
- # Then try relative to breeze_path
- icon_path = (Path(breeze_path) / arg_path).resolve()
-
- if not icon_path.exists():
- print(f"Error: Icon not found at {icon_path}")
- sys.exit(1)
- else:
- print("Select an icon using fzf...")
- icon_path = get_icon_with_fzf(breeze_path)
- if not icon_path:
- print("No icon selected.")
- sys.exit(0)
-
- app = QApplication(sys.argv)
- window = MainWindow(icon_path)
- window.show()
- exit_code = app.exec()
-
- if window.parser.history:
- print(f"\nIcon colors updated: {icon_path.resolve()}")
-
- sys.exit(exit_code)
-
-
-if __name__ == "__main__":
- main()
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index ee397a4..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1 +0,0 @@
-PyQt6
diff --git a/svg_parser.py b/svg_parser.py
deleted file mode 100644
index ca3af6a..0000000
--- a/svg_parser.py
+++ /dev/null
@@ -1,56 +0,0 @@
-import re
-from pathlib import Path
-
-class SvgColorParser:
- def __init__(self, filepath):
- self.filepath = Path(filepath)
- self.content = self.filepath.read_text(encoding="utf-8")
- self.colors = {} # tag -> hex
- self.history = [] # list of contents for undo
- self.redo_history = []
- self._parse()
-
- def _parse(self):
- # Find the regardless of attribute order
- style_match = re.search(r'', self.content, re.DOTALL)
- if style_match:
- style_content = style_match.group(1)
- # Find classes e.g. .ColorScheme-Text { color:#232629; }
- class_matches = re.finditer(r'\.(ColorScheme-[a-zA-Z0-9]+)\s*\{\s*color:\s*(#[a-fA-F0-9]{3,6})', style_content)
- self.colors = {m.group(1): m.group(2) for m in class_matches}
-
- def update_color(self, tag, new_hex):
- if tag not in self.colors: return
- old_hex = self.colors[tag]
- self.history.append(self.content)
- self.redo_history.clear()
-
- # Globally replace the exact hex string, case insensitive
- # We need to compile a regex for the old hex.
- hex_pattern = re.compile(re.escape(old_hex), re.IGNORECASE)
- self.content = hex_pattern.sub(new_hex, self.content)
-
- self.colors[tag] = new_hex
- self.save()
- self._parse() # re-parse to ensure consistency
-
- def save(self):
- self.filepath.write_text(self.content, encoding="utf-8")
-
- def undo(self):
- if self.history:
- self.redo_history.append(self.content)
- self.content = self.history.pop()
- self.save()
- self._parse()
- return True
- return False
-
- def redo(self):
- if self.redo_history:
- self.history.append(self.content)
- self.content = self.redo_history.pop()
- self.save()
- self._parse()
- return True
- return False