feat: initial commit
This commit is contained in:
+15
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
# ──────────────── Added by agents-init ──────────────────
|
||||||
|
# agents-init --agents
|
||||||
|
AGENTS/
|
||||||
|
/AGENTS.md
|
||||||
|
/CLAUDE.md
|
||||||
|
# ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# ──────────────── Added by agents-init ──────────────────
|
||||||
|
# agents-init --plugins
|
||||||
|
docs/superpowers
|
||||||
|
docs/plans
|
||||||
|
docs/specs
|
||||||
|
docs/devlogs
|
||||||
|
# ────────────────────────────────────────────────────────
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Submodule
+1
Submodule breeze-icons added at f8d528544f
@@ -0,0 +1,45 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
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()
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
python3 main.py
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
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()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PyQt6
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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 <style id="current-color-scheme">...</style> regardless of attribute order
|
||||||
|
style_match = re.search(r'<style[^>]*?id="current-color-scheme"[^>]*>(.*?)</style>', 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
|
||||||
Reference in New Issue
Block a user