fix(krita): address code review feedback for PR #119

- Remove duplicate cli-anything-plugin/.claude-plugin/marketplace.json
- Expand VALID_FILTERS to match all 23 filters advertised by filter list
- Add export_options parameter to krita_backend.export_file()
- Extract _locked_save_json to shared utils/io.py utility
- Replace deprecated datetime.utcnow() with datetime.now(timezone.utc)
- Add encoding="utf-8" to setup.py open() call
- Align setup.py with GIMP harness (url, classifiers, extras_require, etc.)
- Add Krita brand purple accent color to repl_skin.py

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AlexGabbia
2026-03-23 17:53:14 +01:00
parent 78b43faa0f
commit 172450110d
8 changed files with 88 additions and 110 deletions
@@ -1,30 +0,0 @@
{
"name": "cli-anything",
"id": "cli-anything",
"owner": {
"name": "HKUDS"
},
"metadata": {
"description": "Build powerful, stateful CLI interfaces for any GUI application using the cli-anything harness methodology.",
"version": "1.0.0"
},
"plugins": [
{
"name": "cli-anything",
"source": "./",
"description": "Build powerful, stateful CLI interfaces for any GUI application using the cli-anything harness methodology.",
"version": "1.0.0",
"author": {
"name": "cli-anything contributors"
},
"keywords": [
"cli",
"harness",
"gui-automation",
"cli-anything"
],
"category": "tools",
"strict": false
}
]
}
@@ -12,7 +12,7 @@ import tempfile
import xml.etree.ElementTree as ET
import zlib
import zipfile
from datetime import datetime
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
@@ -243,7 +243,7 @@ def _build_documentinfo_xml(project: dict) -> bytes:
creator_el = ET.SubElement(about, "creator")
creator_el.text = author
date_el = ET.SubElement(about, "date")
date_el.text = datetime.utcnow().isoformat()
date_el.text = datetime.now(timezone.utc).isoformat()
tree = ET.ElementTree(doc)
from io import BytesIO
@@ -10,6 +10,8 @@ import os
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
from cli_anything.krita.utils.io import locked_save_json
PROJECT_VERSION = "1.0.0"
@@ -24,58 +26,19 @@ VALID_LAYER_TYPES = (
)
VALID_FILTERS = (
"blur",
"sharpen",
"desaturate",
"levels",
"curves",
"brightness-contrast",
"hue-saturation",
"color-balance",
"unsharp-mask",
"posterize",
"threshold",
"blur", "gaussian-blur", "motion-blur", "lens-blur",
"sharpen", "unsharp-mask",
"brightness-contrast", "levels", "curves", "hue-saturation",
"color-balance", "desaturate", "invert", "posterize", "threshold",
"auto-contrast", "normalize",
"emboss", "edge-detection", "oil-paint", "pixelize",
"noise-reduction", "halftone",
)
VALID_COLORSPACES = ("RGBA", "RGB", "GRAYA", "GRAY", "CMYKA", "CMYK")
VALID_DEPTHS = ("U8", "U16", "F16", "F32")
# ---------------------------------------------------------------------------
# Atomic file locking helper
# ---------------------------------------------------------------------------
def _locked_save_json(path: str, data: dict, **dump_kwargs) -> None:
"""Atomically write JSON with exclusive file locking.
Uses fcntl on Unix; silently falls back to unlocked write on Windows
where fcntl is unavailable.
"""
path = str(path)
try:
f = open(path, "r+")
except FileNotFoundError:
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
f = open(path, "w")
with f:
_locked = False
try:
import fcntl
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
_locked = True
except (ImportError, OSError):
pass
try:
f.seek(0)
f.truncate()
json.dump(data, f, **dump_kwargs)
f.flush()
finally:
if _locked:
import fcntl # noqa: F811
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
@@ -232,7 +195,7 @@ def save_project(project: Dict[str, Any], path: Optional[str] = None) -> str:
path = os.path.join(os.getcwd(), f"{safe_name}.krita.json")
_touch_modified(project)
_locked_save_json(path, project, indent=2, default=str)
locked_save_json(path, project, indent=2, default=str)
return os.path.abspath(path)
@@ -11,30 +11,7 @@ import os
import time
from typing import Any, Dict, List, Optional, Tuple
def _locked_save_json(path: str, data: Any, **dump_kwargs: Any) -> None:
"""Persist JSON data to *path* using atomic file locking."""
try:
f = open(path, "r+")
except FileNotFoundError:
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
f = open(path, "w")
with f:
_locked = False
try:
import fcntl
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
_locked = True
except (ImportError, OSError):
pass
try:
f.seek(0)
f.truncate()
json.dump(data, f, **dump_kwargs)
f.flush()
finally:
if _locked:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
from cli_anything.krita.utils.io import locked_save_json
class Session:
@@ -119,7 +96,7 @@ class Session:
for ts, lbl, state in self._snapshots
],
}
_locked_save_json(path, data, indent=2, default=str)
locked_save_json(path, data, indent=2, default=str)
self._session_path = path
def load(self, path: str) -> None:
@@ -0,0 +1,36 @@
"""Shared I/O utilities for the Krita CLI harness."""
import json
import os
from typing import Any
def locked_save_json(path: str, data: Any, **dump_kwargs: Any) -> None:
"""Atomically write JSON with exclusive file locking.
Uses fcntl on Unix; silently falls back to unlocked write on Windows
where fcntl is unavailable.
"""
path = str(path)
try:
f = open(path, "r+")
except FileNotFoundError:
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
f = open(path, "w")
with f:
_locked = False
try:
import fcntl
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
_locked = True
except (ImportError, OSError):
pass
try:
f.seek(0)
f.truncate()
json.dump(data, f, **dump_kwargs)
f.flush()
finally:
if _locked:
import fcntl # noqa: F811
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
@@ -176,6 +176,7 @@ def export_file(
output_path: str | Path,
*,
format: Optional[str] = None,
export_options: Optional[Dict[str, Any]] = None,
timeout: int = 300,
) -> Dict[str, Any]:
"""Export *input_path* to *output_path* using Krita's CLI.
@@ -187,6 +188,8 @@ def export_file(
format: If provided, override the output format (e.g. ``"png"``).
The extension of *output_path* will still be respected for the
filename.
export_options: Optional dict of key-value pairs forwarded to Krita
via ``--export-option key=value`` flags (e.g. compression, quality).
timeout: Maximum seconds to wait for Krita.
Returns:
@@ -200,7 +203,11 @@ def export_file(
# Ensure output directory exists
os.makedirs(os.path.dirname(output_path), exist_ok=True)
args = [krita, "--export", "--export-filename", output_path, input_path]
args = [krita, "--export", "--export-filename", output_path]
if export_options:
for key, value in export_options.items():
args += ["--export-option", f"{key}={value}"]
args.append(input_path)
result = _run(args, timeout=timeout)
result["output_path"] = output_path
@@ -47,6 +47,7 @@ _ACCENT_COLORS = {
"obs_studio": "\033[38;5;55m", # purple
"kdenlive": "\033[38;5;69m", # slate blue
"shotcut": "\033[38;5;35m", # teal green
"krita": "\033[38;5;98m", # purple (Krita brand)
}
_DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue
@@ -515,6 +516,7 @@ _ANSI_256_TO_HEX = {
"\033[38;5;55m": "#5f00af", # obs purple
"\033[38;5;69m": "#5f87ff", # kdenlive slate blue
"\033[38;5;75m": "#5fafff", # default sky blue
"\033[38;5;98m": "#875fd7", # krita purple
"\033[38;5;80m": "#5fd7d7", # brand cyan
"\033[38;5;208m": "#ff8700", # blender deep orange
"\033[38;5;214m": "#ffaf00", # gimp warm orange
+28 -5
View File
@@ -1,25 +1,48 @@
from setuptools import setup, find_namespace_packages
with open("cli_anything/krita/README.md", encoding="utf-8") as fh:
long_description = fh.read()
setup(
name="cli-anything-krita",
version="1.0.0",
description="CLI harness for Krita digital painting application",
long_description=open("cli_anything/krita/README.md").read(),
long_description=long_description,
long_description_content_type="text/markdown",
author="cli-anything contributors",
url="https://github.com/HKUDS/CLI-Anything",
license="MIT",
packages=find_namespace_packages(include=["cli_anything.*"]),
package_data={
"cli_anything.krita": ["skills/*.md"],
},
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Multimedia :: Graphics :: Editors",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
],
python_requires=">=3.10",
install_requires=[
"click>=8.0.0",
"prompt-toolkit>=3.0.0",
],
extras_require={
"dev": [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
],
},
entry_points={
"console_scripts": [
"cli-anything-krita=cli_anything.krita.krita_cli:main",
],
},
python_requires=">=3.10",
package_data={
"cli_anything.krita": ["skills/*.md"],
},
include_package_data=True,
zip_safe=False,
)