@agent-webui/ai-desk-harness-gimp 1.0.29-beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/manifest.json +22 -0
- package/package.json +11 -0
- package/python/agent-harness/GIMP.md +301 -0
- package/python/agent-harness/build/lib/cli_anything/gimp/__init__.py +1 -0
- package/python/agent-harness/build/lib/cli_anything/gimp/__main__.py +3 -0
- package/python/agent-harness/build/lib/cli_anything/gimp/core/__init__.py +1 -0
- package/python/agent-harness/build/lib/cli_anything/gimp/core/canvas.py +193 -0
- package/python/agent-harness/build/lib/cli_anything/gimp/core/export.py +479 -0
- package/python/agent-harness/build/lib/cli_anything/gimp/core/filters.py +382 -0
- package/python/agent-harness/build/lib/cli_anything/gimp/core/layers.py +249 -0
- package/python/agent-harness/build/lib/cli_anything/gimp/core/media.py +174 -0
- package/python/agent-harness/build/lib/cli_anything/gimp/core/project.py +131 -0
- package/python/agent-harness/build/lib/cli_anything/gimp/core/session.py +130 -0
- package/python/agent-harness/build/lib/cli_anything/gimp/gimp_cli.py +788 -0
- package/python/agent-harness/build/lib/cli_anything/gimp/tests/__init__.py +1 -0
- package/python/agent-harness/build/lib/cli_anything/gimp/tests/test_core.py +478 -0
- package/python/agent-harness/build/lib/cli_anything/gimp/tests/test_full_e2e.py +578 -0
- package/python/agent-harness/build/lib/cli_anything/gimp/utils/__init__.py +1 -0
- package/python/agent-harness/build/lib/cli_anything/gimp/utils/gimp_backend.py +208 -0
- package/python/agent-harness/build/lib/cli_anything/gimp/utils/repl_skin.py +498 -0
- package/python/agent-harness/cli_anything/gimp/README.md +202 -0
- package/python/agent-harness/cli_anything/gimp/__init__.py +1 -0
- package/python/agent-harness/cli_anything/gimp/__main__.py +3 -0
- package/python/agent-harness/cli_anything/gimp/core/__init__.py +1 -0
- package/python/agent-harness/cli_anything/gimp/core/canvas.py +193 -0
- package/python/agent-harness/cli_anything/gimp/core/export.py +479 -0
- package/python/agent-harness/cli_anything/gimp/core/filters.py +382 -0
- package/python/agent-harness/cli_anything/gimp/core/layers.py +249 -0
- package/python/agent-harness/cli_anything/gimp/core/media.py +174 -0
- package/python/agent-harness/cli_anything/gimp/core/project.py +131 -0
- package/python/agent-harness/cli_anything/gimp/core/session.py +130 -0
- package/python/agent-harness/cli_anything/gimp/gimp_cli.py +788 -0
- package/python/agent-harness/cli_anything/gimp/tests/TEST.md +137 -0
- package/python/agent-harness/cli_anything/gimp/tests/__init__.py +1 -0
- package/python/agent-harness/cli_anything/gimp/tests/test_core.py +478 -0
- package/python/agent-harness/cli_anything/gimp/tests/test_full_e2e.py +578 -0
- package/python/agent-harness/cli_anything/gimp/utils/__init__.py +1 -0
- package/python/agent-harness/cli_anything/gimp/utils/gimp_backend.py +208 -0
- package/python/agent-harness/cli_anything/gimp/utils/repl_skin.py +498 -0
- package/python/agent-harness/cli_anything_gimp.egg-info/PKG-INFO +236 -0
- package/python/agent-harness/cli_anything_gimp.egg-info/SOURCES.txt +25 -0
- package/python/agent-harness/cli_anything_gimp.egg-info/dependency_links.txt +1 -0
- package/python/agent-harness/cli_anything_gimp.egg-info/entry_points.txt +2 -0
- package/python/agent-harness/cli_anything_gimp.egg-info/not-zip-safe +1 -0
- package/python/agent-harness/cli_anything_gimp.egg-info/requires.txt +7 -0
- package/python/agent-harness/cli_anything_gimp.egg-info/top_level.txt +1 -0
- package/python/agent-harness/setup.py +54 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
"""cli-anything REPL Skin — Unified terminal interface for all CLI harnesses.
|
|
2
|
+
|
|
3
|
+
Copy this file into your CLI package at:
|
|
4
|
+
cli_anything/<software>/utils/repl_skin.py
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from cli_anything.<software>.utils.repl_skin import ReplSkin
|
|
8
|
+
|
|
9
|
+
skin = ReplSkin("shotcut", version="1.0.0")
|
|
10
|
+
skin.print_banner()
|
|
11
|
+
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
|
|
12
|
+
skin.success("Project saved")
|
|
13
|
+
skin.error("File not found")
|
|
14
|
+
skin.warning("Unsaved changes")
|
|
15
|
+
skin.info("Processing 24 clips...")
|
|
16
|
+
skin.status("Track 1", "3 clips, 00:02:30")
|
|
17
|
+
skin.table(headers, rows)
|
|
18
|
+
skin.print_goodbye()
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
# ── ANSI color codes (no external deps for core styling) ──────────────
|
|
25
|
+
|
|
26
|
+
_RESET = "\033[0m"
|
|
27
|
+
_BOLD = "\033[1m"
|
|
28
|
+
_DIM = "\033[2m"
|
|
29
|
+
_ITALIC = "\033[3m"
|
|
30
|
+
_UNDERLINE = "\033[4m"
|
|
31
|
+
|
|
32
|
+
# Brand colors
|
|
33
|
+
_CYAN = "\033[38;5;80m" # cli-anything brand cyan
|
|
34
|
+
_CYAN_BG = "\033[48;5;80m"
|
|
35
|
+
_WHITE = "\033[97m"
|
|
36
|
+
_GRAY = "\033[38;5;245m"
|
|
37
|
+
_DARK_GRAY = "\033[38;5;240m"
|
|
38
|
+
_LIGHT_GRAY = "\033[38;5;250m"
|
|
39
|
+
|
|
40
|
+
# Software accent colors — each software gets a unique accent
|
|
41
|
+
_ACCENT_COLORS = {
|
|
42
|
+
"gimp": "\033[38;5;214m", # warm orange
|
|
43
|
+
"blender": "\033[38;5;208m", # deep orange
|
|
44
|
+
"inkscape": "\033[38;5;39m", # bright blue
|
|
45
|
+
"audacity": "\033[38;5;33m", # navy blue
|
|
46
|
+
"libreoffice": "\033[38;5;40m", # green
|
|
47
|
+
"obs_studio": "\033[38;5;55m", # purple
|
|
48
|
+
"kdenlive": "\033[38;5;69m", # slate blue
|
|
49
|
+
"shotcut": "\033[38;5;35m", # teal green
|
|
50
|
+
}
|
|
51
|
+
_DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue
|
|
52
|
+
|
|
53
|
+
# Status colors
|
|
54
|
+
_GREEN = "\033[38;5;78m"
|
|
55
|
+
_YELLOW = "\033[38;5;220m"
|
|
56
|
+
_RED = "\033[38;5;196m"
|
|
57
|
+
_BLUE = "\033[38;5;75m"
|
|
58
|
+
_MAGENTA = "\033[38;5;176m"
|
|
59
|
+
|
|
60
|
+
# ── Brand icon ────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
# The cli-anything icon: a small colored diamond/chevron mark
|
|
63
|
+
_ICON = f"{_CYAN}{_BOLD}◆{_RESET}"
|
|
64
|
+
_ICON_SMALL = f"{_CYAN}▸{_RESET}"
|
|
65
|
+
|
|
66
|
+
# ── Box drawing characters ────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
_H_LINE = "─"
|
|
69
|
+
_V_LINE = "│"
|
|
70
|
+
_TL = "╭"
|
|
71
|
+
_TR = "╮"
|
|
72
|
+
_BL = "╰"
|
|
73
|
+
_BR = "╯"
|
|
74
|
+
_T_DOWN = "┬"
|
|
75
|
+
_T_UP = "┴"
|
|
76
|
+
_T_RIGHT = "├"
|
|
77
|
+
_T_LEFT = "┤"
|
|
78
|
+
_CROSS = "┼"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _strip_ansi(text: str) -> str:
|
|
82
|
+
"""Remove ANSI escape codes for length calculation."""
|
|
83
|
+
import re
|
|
84
|
+
return re.sub(r"\033\[[^m]*m", "", text)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _visible_len(text: str) -> int:
|
|
88
|
+
"""Get visible length of text (excluding ANSI codes)."""
|
|
89
|
+
return len(_strip_ansi(text))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ReplSkin:
|
|
93
|
+
"""Unified REPL skin for cli-anything CLIs.
|
|
94
|
+
|
|
95
|
+
Provides consistent branding, prompts, and message formatting
|
|
96
|
+
across all CLI harnesses built with the cli-anything methodology.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(self, software: str, version: str = "1.0.0",
|
|
100
|
+
history_file: str | None = None):
|
|
101
|
+
"""Initialize the REPL skin.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
software: Software name (e.g., "gimp", "shotcut", "blender").
|
|
105
|
+
version: CLI version string.
|
|
106
|
+
history_file: Path for persistent command history.
|
|
107
|
+
Defaults to ~/.cli-anything-<software>/history
|
|
108
|
+
"""
|
|
109
|
+
self.software = software.lower().replace("-", "_")
|
|
110
|
+
self.display_name = software.replace("_", " ").title()
|
|
111
|
+
self.version = version
|
|
112
|
+
self.accent = _ACCENT_COLORS.get(self.software, _DEFAULT_ACCENT)
|
|
113
|
+
|
|
114
|
+
# History file
|
|
115
|
+
if history_file is None:
|
|
116
|
+
from pathlib import Path
|
|
117
|
+
hist_dir = Path.home() / f".cli-anything-{self.software}"
|
|
118
|
+
hist_dir.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
self.history_file = str(hist_dir / "history")
|
|
120
|
+
else:
|
|
121
|
+
self.history_file = history_file
|
|
122
|
+
|
|
123
|
+
# Detect terminal capabilities
|
|
124
|
+
self._color = self._detect_color_support()
|
|
125
|
+
|
|
126
|
+
def _detect_color_support(self) -> bool:
|
|
127
|
+
"""Check if terminal supports color."""
|
|
128
|
+
if os.environ.get("NO_COLOR"):
|
|
129
|
+
return False
|
|
130
|
+
if os.environ.get("CLI_ANYTHING_NO_COLOR"):
|
|
131
|
+
return False
|
|
132
|
+
if not hasattr(sys.stdout, "isatty"):
|
|
133
|
+
return False
|
|
134
|
+
return sys.stdout.isatty()
|
|
135
|
+
|
|
136
|
+
def _c(self, code: str, text: str) -> str:
|
|
137
|
+
"""Apply color code if colors are supported."""
|
|
138
|
+
if not self._color:
|
|
139
|
+
return text
|
|
140
|
+
return f"{code}{text}{_RESET}"
|
|
141
|
+
|
|
142
|
+
# ── Banner ────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
def print_banner(self):
|
|
145
|
+
"""Print the startup banner with branding."""
|
|
146
|
+
inner = 54
|
|
147
|
+
|
|
148
|
+
def _box_line(content: str) -> str:
|
|
149
|
+
"""Wrap content in box drawing, padding to inner width."""
|
|
150
|
+
pad = inner - _visible_len(content)
|
|
151
|
+
vl = self._c(_DARK_GRAY, _V_LINE)
|
|
152
|
+
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
|
|
153
|
+
|
|
154
|
+
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
|
|
155
|
+
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
|
|
156
|
+
|
|
157
|
+
# Title: ◆ cli-anything · Shotcut
|
|
158
|
+
icon = self._c(_CYAN + _BOLD, "◆")
|
|
159
|
+
brand = self._c(_CYAN + _BOLD, "cli-anything")
|
|
160
|
+
dot = self._c(_DARK_GRAY, "·")
|
|
161
|
+
name = self._c(self.accent + _BOLD, self.display_name)
|
|
162
|
+
title = f" {icon} {brand} {dot} {name}"
|
|
163
|
+
|
|
164
|
+
ver = f" {self._c(_DARK_GRAY, f' v{self.version}')}"
|
|
165
|
+
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
|
|
166
|
+
empty = ""
|
|
167
|
+
|
|
168
|
+
print(top)
|
|
169
|
+
print(_box_line(title))
|
|
170
|
+
print(_box_line(ver))
|
|
171
|
+
print(_box_line(empty))
|
|
172
|
+
print(_box_line(tip))
|
|
173
|
+
print(bot)
|
|
174
|
+
print()
|
|
175
|
+
|
|
176
|
+
# ── Prompt ────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
def prompt(self, project_name: str = "", modified: bool = False,
|
|
179
|
+
context: str = "") -> str:
|
|
180
|
+
"""Build a styled prompt string for prompt_toolkit or input().
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
project_name: Current project name (empty if none open).
|
|
184
|
+
modified: Whether the project has unsaved changes.
|
|
185
|
+
context: Optional extra context to show in prompt.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Formatted prompt string.
|
|
189
|
+
"""
|
|
190
|
+
parts = []
|
|
191
|
+
|
|
192
|
+
# Icon
|
|
193
|
+
if self._color:
|
|
194
|
+
parts.append(f"{_CYAN}◆{_RESET} ")
|
|
195
|
+
else:
|
|
196
|
+
parts.append("> ")
|
|
197
|
+
|
|
198
|
+
# Software name
|
|
199
|
+
parts.append(self._c(self.accent + _BOLD, self.software))
|
|
200
|
+
|
|
201
|
+
# Project context
|
|
202
|
+
if project_name or context:
|
|
203
|
+
ctx = context or project_name
|
|
204
|
+
mod = "*" if modified else ""
|
|
205
|
+
parts.append(f" {self._c(_DARK_GRAY, '[')}")
|
|
206
|
+
parts.append(self._c(_LIGHT_GRAY, f"{ctx}{mod}"))
|
|
207
|
+
parts.append(self._c(_DARK_GRAY, ']'))
|
|
208
|
+
|
|
209
|
+
parts.append(self._c(_GRAY, " ❯ "))
|
|
210
|
+
|
|
211
|
+
return "".join(parts)
|
|
212
|
+
|
|
213
|
+
def prompt_tokens(self, project_name: str = "", modified: bool = False,
|
|
214
|
+
context: str = ""):
|
|
215
|
+
"""Build prompt_toolkit formatted text tokens for the prompt.
|
|
216
|
+
|
|
217
|
+
Use with prompt_toolkit's FormattedText for proper ANSI handling.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
list of (style, text) tuples for prompt_toolkit.
|
|
221
|
+
"""
|
|
222
|
+
accent_hex = _ANSI_256_TO_HEX.get(self.accent, "#5fafff")
|
|
223
|
+
tokens = []
|
|
224
|
+
|
|
225
|
+
tokens.append(("class:icon", "◆ "))
|
|
226
|
+
tokens.append(("class:software", self.software))
|
|
227
|
+
|
|
228
|
+
if project_name or context:
|
|
229
|
+
ctx = context or project_name
|
|
230
|
+
mod = "*" if modified else ""
|
|
231
|
+
tokens.append(("class:bracket", " ["))
|
|
232
|
+
tokens.append(("class:context", f"{ctx}{mod}"))
|
|
233
|
+
tokens.append(("class:bracket", "]"))
|
|
234
|
+
|
|
235
|
+
tokens.append(("class:arrow", " ❯ "))
|
|
236
|
+
|
|
237
|
+
return tokens
|
|
238
|
+
|
|
239
|
+
def get_prompt_style(self):
|
|
240
|
+
"""Get a prompt_toolkit Style object matching the skin.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
prompt_toolkit.styles.Style
|
|
244
|
+
"""
|
|
245
|
+
try:
|
|
246
|
+
from prompt_toolkit.styles import Style
|
|
247
|
+
except ImportError:
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
accent_hex = _ANSI_256_TO_HEX.get(self.accent, "#5fafff")
|
|
251
|
+
|
|
252
|
+
return Style.from_dict({
|
|
253
|
+
"icon": "#5fdfdf bold", # cyan brand color
|
|
254
|
+
"software": f"{accent_hex} bold",
|
|
255
|
+
"bracket": "#585858",
|
|
256
|
+
"context": "#bcbcbc",
|
|
257
|
+
"arrow": "#808080",
|
|
258
|
+
# Completion menu
|
|
259
|
+
"completion-menu.completion": "bg:#303030 #bcbcbc",
|
|
260
|
+
"completion-menu.completion.current": f"bg:{accent_hex} #000000",
|
|
261
|
+
"completion-menu.meta.completion": "bg:#303030 #808080",
|
|
262
|
+
"completion-menu.meta.completion.current": f"bg:{accent_hex} #000000",
|
|
263
|
+
# Auto-suggest
|
|
264
|
+
"auto-suggest": "#585858",
|
|
265
|
+
# Bottom toolbar
|
|
266
|
+
"bottom-toolbar": "bg:#1c1c1c #808080",
|
|
267
|
+
"bottom-toolbar.text": "#808080",
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
# ── Messages ──────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
def success(self, message: str):
|
|
273
|
+
"""Print a success message with green checkmark."""
|
|
274
|
+
icon = self._c(_GREEN + _BOLD, "✓")
|
|
275
|
+
print(f" {icon} {self._c(_GREEN, message)}")
|
|
276
|
+
|
|
277
|
+
def error(self, message: str):
|
|
278
|
+
"""Print an error message with red cross."""
|
|
279
|
+
icon = self._c(_RED + _BOLD, "✗")
|
|
280
|
+
print(f" {icon} {self._c(_RED, message)}", file=sys.stderr)
|
|
281
|
+
|
|
282
|
+
def warning(self, message: str):
|
|
283
|
+
"""Print a warning message with yellow triangle."""
|
|
284
|
+
icon = self._c(_YELLOW + _BOLD, "⚠")
|
|
285
|
+
print(f" {icon} {self._c(_YELLOW, message)}")
|
|
286
|
+
|
|
287
|
+
def info(self, message: str):
|
|
288
|
+
"""Print an info message with blue dot."""
|
|
289
|
+
icon = self._c(_BLUE, "●")
|
|
290
|
+
print(f" {icon} {self._c(_LIGHT_GRAY, message)}")
|
|
291
|
+
|
|
292
|
+
def hint(self, message: str):
|
|
293
|
+
"""Print a subtle hint message."""
|
|
294
|
+
print(f" {self._c(_DARK_GRAY, message)}")
|
|
295
|
+
|
|
296
|
+
def section(self, title: str):
|
|
297
|
+
"""Print a section header."""
|
|
298
|
+
print()
|
|
299
|
+
print(f" {self._c(self.accent + _BOLD, title)}")
|
|
300
|
+
print(f" {self._c(_DARK_GRAY, _H_LINE * len(title))}")
|
|
301
|
+
|
|
302
|
+
# ── Status display ────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
def status(self, label: str, value: str):
|
|
305
|
+
"""Print a key-value status line."""
|
|
306
|
+
lbl = self._c(_GRAY, f" {label}:")
|
|
307
|
+
val = self._c(_WHITE, f" {value}")
|
|
308
|
+
print(f"{lbl}{val}")
|
|
309
|
+
|
|
310
|
+
def status_block(self, items: dict[str, str], title: str = ""):
|
|
311
|
+
"""Print a block of status key-value pairs.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
items: Dict of label -> value pairs.
|
|
315
|
+
title: Optional title for the block.
|
|
316
|
+
"""
|
|
317
|
+
if title:
|
|
318
|
+
self.section(title)
|
|
319
|
+
|
|
320
|
+
max_key = max(len(k) for k in items) if items else 0
|
|
321
|
+
for label, value in items.items():
|
|
322
|
+
lbl = self._c(_GRAY, f" {label:<{max_key}}")
|
|
323
|
+
val = self._c(_WHITE, f" {value}")
|
|
324
|
+
print(f"{lbl}{val}")
|
|
325
|
+
|
|
326
|
+
def progress(self, current: int, total: int, label: str = ""):
|
|
327
|
+
"""Print a simple progress indicator.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
current: Current step number.
|
|
331
|
+
total: Total number of steps.
|
|
332
|
+
label: Optional label for the progress.
|
|
333
|
+
"""
|
|
334
|
+
pct = int(current / total * 100) if total > 0 else 0
|
|
335
|
+
bar_width = 20
|
|
336
|
+
filled = int(bar_width * current / total) if total > 0 else 0
|
|
337
|
+
bar = "█" * filled + "░" * (bar_width - filled)
|
|
338
|
+
text = f" {self._c(_CYAN, bar)} {self._c(_GRAY, f'{pct:3d}%')}"
|
|
339
|
+
if label:
|
|
340
|
+
text += f" {self._c(_LIGHT_GRAY, label)}"
|
|
341
|
+
print(text)
|
|
342
|
+
|
|
343
|
+
# ── Table display ─────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
def table(self, headers: list[str], rows: list[list[str]],
|
|
346
|
+
max_col_width: int = 40):
|
|
347
|
+
"""Print a formatted table with box-drawing characters.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
headers: Column header strings.
|
|
351
|
+
rows: List of rows, each a list of cell strings.
|
|
352
|
+
max_col_width: Maximum column width before truncation.
|
|
353
|
+
"""
|
|
354
|
+
if not headers:
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
# Calculate column widths
|
|
358
|
+
col_widths = [min(len(h), max_col_width) for h in headers]
|
|
359
|
+
for row in rows:
|
|
360
|
+
for i, cell in enumerate(row):
|
|
361
|
+
if i < len(col_widths):
|
|
362
|
+
col_widths[i] = min(
|
|
363
|
+
max(col_widths[i], len(str(cell))), max_col_width
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
def pad(text: str, width: int) -> str:
|
|
367
|
+
t = str(text)[:width]
|
|
368
|
+
return t + " " * (width - len(t))
|
|
369
|
+
|
|
370
|
+
# Header
|
|
371
|
+
header_cells = [
|
|
372
|
+
self._c(_CYAN + _BOLD, pad(h, col_widths[i]))
|
|
373
|
+
for i, h in enumerate(headers)
|
|
374
|
+
]
|
|
375
|
+
sep = self._c(_DARK_GRAY, f" {_V_LINE} ")
|
|
376
|
+
header_line = f" {sep.join(header_cells)}"
|
|
377
|
+
print(header_line)
|
|
378
|
+
|
|
379
|
+
# Separator
|
|
380
|
+
sep_parts = [self._c(_DARK_GRAY, _H_LINE * w) for w in col_widths]
|
|
381
|
+
sep_line = self._c(_DARK_GRAY, f" {'───'.join([_H_LINE * w for w in col_widths])}")
|
|
382
|
+
print(sep_line)
|
|
383
|
+
|
|
384
|
+
# Rows
|
|
385
|
+
for row in rows:
|
|
386
|
+
cells = []
|
|
387
|
+
for i, cell in enumerate(row):
|
|
388
|
+
if i < len(col_widths):
|
|
389
|
+
cells.append(self._c(_LIGHT_GRAY, pad(str(cell), col_widths[i])))
|
|
390
|
+
row_sep = self._c(_DARK_GRAY, f" {_V_LINE} ")
|
|
391
|
+
print(f" {row_sep.join(cells)}")
|
|
392
|
+
|
|
393
|
+
# ── Help display ──────────────────────────────────────────────────
|
|
394
|
+
|
|
395
|
+
def help(self, commands: dict[str, str]):
|
|
396
|
+
"""Print a formatted help listing.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
commands: Dict of command -> description pairs.
|
|
400
|
+
"""
|
|
401
|
+
self.section("Commands")
|
|
402
|
+
max_cmd = max(len(c) for c in commands) if commands else 0
|
|
403
|
+
for cmd, desc in commands.items():
|
|
404
|
+
cmd_styled = self._c(self.accent, f" {cmd:<{max_cmd}}")
|
|
405
|
+
desc_styled = self._c(_GRAY, f" {desc}")
|
|
406
|
+
print(f"{cmd_styled}{desc_styled}")
|
|
407
|
+
print()
|
|
408
|
+
|
|
409
|
+
# ── Goodbye ───────────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
def print_goodbye(self):
|
|
412
|
+
"""Print a styled goodbye message."""
|
|
413
|
+
print(f"\n {_ICON_SMALL} {self._c(_GRAY, 'Goodbye!')}\n")
|
|
414
|
+
|
|
415
|
+
# ── Prompt toolkit session factory ────────────────────────────────
|
|
416
|
+
|
|
417
|
+
def create_prompt_session(self):
|
|
418
|
+
"""Create a prompt_toolkit PromptSession with skin styling.
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
A configured PromptSession, or None if prompt_toolkit unavailable.
|
|
422
|
+
"""
|
|
423
|
+
try:
|
|
424
|
+
from prompt_toolkit import PromptSession
|
|
425
|
+
from prompt_toolkit.history import FileHistory
|
|
426
|
+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
427
|
+
from prompt_toolkit.formatted_text import FormattedText
|
|
428
|
+
|
|
429
|
+
style = self.get_prompt_style()
|
|
430
|
+
|
|
431
|
+
session = PromptSession(
|
|
432
|
+
history=FileHistory(self.history_file),
|
|
433
|
+
auto_suggest=AutoSuggestFromHistory(),
|
|
434
|
+
style=style,
|
|
435
|
+
enable_history_search=True,
|
|
436
|
+
)
|
|
437
|
+
return session
|
|
438
|
+
except ImportError:
|
|
439
|
+
return None
|
|
440
|
+
|
|
441
|
+
def get_input(self, pt_session, project_name: str = "",
|
|
442
|
+
modified: bool = False, context: str = "") -> str:
|
|
443
|
+
"""Get input from user using prompt_toolkit or fallback.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
pt_session: A prompt_toolkit PromptSession (or None).
|
|
447
|
+
project_name: Current project name.
|
|
448
|
+
modified: Whether project has unsaved changes.
|
|
449
|
+
context: Optional context string.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
User input string (stripped).
|
|
453
|
+
"""
|
|
454
|
+
if pt_session is not None:
|
|
455
|
+
from prompt_toolkit.formatted_text import FormattedText
|
|
456
|
+
tokens = self.prompt_tokens(project_name, modified, context)
|
|
457
|
+
return pt_session.prompt(FormattedText(tokens)).strip()
|
|
458
|
+
else:
|
|
459
|
+
raw_prompt = self.prompt(project_name, modified, context)
|
|
460
|
+
return input(raw_prompt).strip()
|
|
461
|
+
|
|
462
|
+
# ── Toolbar builder ───────────────────────────────────────────────
|
|
463
|
+
|
|
464
|
+
def bottom_toolbar(self, items: dict[str, str]):
|
|
465
|
+
"""Create a bottom toolbar callback for prompt_toolkit.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
items: Dict of label -> value pairs to show in toolbar.
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
A callable that returns FormattedText for the toolbar.
|
|
472
|
+
"""
|
|
473
|
+
def toolbar():
|
|
474
|
+
from prompt_toolkit.formatted_text import FormattedText
|
|
475
|
+
parts = []
|
|
476
|
+
for i, (k, v) in enumerate(items.items()):
|
|
477
|
+
if i > 0:
|
|
478
|
+
parts.append(("class:bottom-toolbar.text", " │ "))
|
|
479
|
+
parts.append(("class:bottom-toolbar.text", f" {k}: "))
|
|
480
|
+
parts.append(("class:bottom-toolbar", v))
|
|
481
|
+
return FormattedText(parts)
|
|
482
|
+
return toolbar
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
# ── ANSI 256-color to hex mapping (for prompt_toolkit styles) ─────────
|
|
486
|
+
|
|
487
|
+
_ANSI_256_TO_HEX = {
|
|
488
|
+
"\033[38;5;33m": "#0087ff", # audacity navy blue
|
|
489
|
+
"\033[38;5;35m": "#00af5f", # shotcut teal
|
|
490
|
+
"\033[38;5;39m": "#00afff", # inkscape bright blue
|
|
491
|
+
"\033[38;5;40m": "#00d700", # libreoffice green
|
|
492
|
+
"\033[38;5;55m": "#5f00af", # obs purple
|
|
493
|
+
"\033[38;5;69m": "#5f87ff", # kdenlive slate blue
|
|
494
|
+
"\033[38;5;75m": "#5fafff", # default sky blue
|
|
495
|
+
"\033[38;5;80m": "#5fd7d7", # brand cyan
|
|
496
|
+
"\033[38;5;208m": "#ff8700", # blender deep orange
|
|
497
|
+
"\033[38;5;214m": "#ffaf00", # gimp warm orange
|
|
498
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# GIMP CLI
|
|
2
|
+
|
|
3
|
+
A stateful command-line interface for image editing, built on Pillow.
|
|
4
|
+
Designed for AI agents and power users who need to create and manipulate
|
|
5
|
+
images without a GUI.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- Python 3.10+
|
|
10
|
+
- `Pillow` (image processing)
|
|
11
|
+
- `click` (CLI framework)
|
|
12
|
+
- `numpy` (blend modes, pixel analysis)
|
|
13
|
+
|
|
14
|
+
Optional (for interactive REPL):
|
|
15
|
+
- `prompt_toolkit`
|
|
16
|
+
|
|
17
|
+
## Install Dependencies
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install Pillow click numpy prompt_toolkit
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## How to Run
|
|
24
|
+
|
|
25
|
+
All commands are run from the `agent-harness/` directory.
|
|
26
|
+
|
|
27
|
+
### One-shot commands
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Show help
|
|
31
|
+
python3 -m cli.gimp_cli --help
|
|
32
|
+
|
|
33
|
+
# Create a new project
|
|
34
|
+
python3 -m cli.gimp_cli project new --width 1920 --height 1080 -o my_project.json
|
|
35
|
+
|
|
36
|
+
# Create with a profile
|
|
37
|
+
python3 -m cli.gimp_cli project new --profile hd720p -o project.json
|
|
38
|
+
|
|
39
|
+
# Open a project and show info
|
|
40
|
+
python3 -m cli.gimp_cli --project project.json project info
|
|
41
|
+
|
|
42
|
+
# JSON output (for agent consumption)
|
|
43
|
+
python3 -m cli.gimp_cli --json --project project.json project info
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Interactive REPL
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
python3 -m cli.gimp_cli repl
|
|
50
|
+
python3 -m cli.gimp_cli repl --project my_project.json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Inside the REPL, type `help` for all available commands.
|
|
54
|
+
|
|
55
|
+
## Command Reference
|
|
56
|
+
|
|
57
|
+
### Project
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
project new [--width W] [--height H] [--mode RGB|RGBA|L|LA] [--profile P] [-o path]
|
|
61
|
+
project open <path>
|
|
62
|
+
project save [path]
|
|
63
|
+
project info
|
|
64
|
+
project profiles
|
|
65
|
+
project json
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Available profiles: `hd1080p`, `hd720p`, `4k`, `square1080`, `a4_300dpi`, `a4_150dpi`,
|
|
69
|
+
`letter_300dpi`, `web_banner`, `instagram_post`, `instagram_story`, `twitter_header`,
|
|
70
|
+
`youtube_thumb`, `icon_256`, `icon_512`
|
|
71
|
+
|
|
72
|
+
### Layer
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
layer new [--name N] [--type image|text|solid] [--fill F] [--opacity O] [--mode M]
|
|
76
|
+
layer add-from-file <path> [--name N] [--position P] [--opacity O] [--mode M]
|
|
77
|
+
layer list
|
|
78
|
+
layer remove <index>
|
|
79
|
+
layer duplicate <index>
|
|
80
|
+
layer move <index> --to <position>
|
|
81
|
+
layer set <index> <property> <value>
|
|
82
|
+
layer flatten
|
|
83
|
+
layer merge-down <index>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Layer properties: `name`, `opacity` (0.0-1.0), `visible` (true/false),
|
|
87
|
+
`mode` (blend mode), `offset_x`, `offset_y`
|
|
88
|
+
|
|
89
|
+
### Canvas
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
canvas info
|
|
93
|
+
canvas resize --width W --height H [--anchor center|top-left|...]
|
|
94
|
+
canvas scale --width W --height H [--resample lanczos|bicubic|bilinear|nearest]
|
|
95
|
+
canvas crop --left L --top T --right R --bottom B
|
|
96
|
+
canvas mode <RGB|RGBA|L|LA|CMYK|P>
|
|
97
|
+
canvas dpi <value>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Filters
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
filter list-available [--category adjustment|blur|stylize|transform]
|
|
104
|
+
filter info <name>
|
|
105
|
+
filter add <name> [--layer L] [--param key=value ...]
|
|
106
|
+
filter remove <index> [--layer L]
|
|
107
|
+
filter set <index> <param> <value> [--layer L]
|
|
108
|
+
filter list [--layer L]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Available filters:
|
|
112
|
+
- **Adjustments**: brightness, contrast, saturation, sharpness, autocontrast,
|
|
113
|
+
equalize, invert, posterize, solarize, grayscale, sepia
|
|
114
|
+
- **Blur**: gaussian_blur, box_blur, unsharp_mask, smooth
|
|
115
|
+
- **Stylize**: find_edges, emboss, contour, detail
|
|
116
|
+
- **Transform**: rotate, flip_h, flip_v, resize, crop
|
|
117
|
+
|
|
118
|
+
### Media
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
media probe <file>
|
|
122
|
+
media list
|
|
123
|
+
media check
|
|
124
|
+
media histogram <file>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Export
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
export presets
|
|
131
|
+
export preset-info <name>
|
|
132
|
+
export render <output> [--preset name] [--overwrite] [--quality Q] [--format F]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Available presets: `png`, `png-max`, `jpeg-high`, `jpeg-medium`, `jpeg-low`,
|
|
136
|
+
`webp`, `webp-lossless`, `tiff`, `tiff-none`, `bmp`, `gif`, `pdf`, `ico`
|
|
137
|
+
|
|
138
|
+
### Draw
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
draw text --layer L --text "Hello" [--x X] [--y Y] [--font F] [--size S] [--color C]
|
|
142
|
+
draw rect --layer L --x1 X --y1 Y --x2 X --y2 Y [--fill C] [--outline C]
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Session
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
session status
|
|
149
|
+
session undo
|
|
150
|
+
session redo
|
|
151
|
+
session history
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## JSON Mode
|
|
155
|
+
|
|
156
|
+
Add `--json` before the subcommand for machine-readable output:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
python3 -m cli.gimp_cli --json --project p.json layer list
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Running Tests
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
cd agent-harness
|
|
166
|
+
python3 -m pytest cli/tests/test_core.py -v # Unit tests (no images needed)
|
|
167
|
+
python3 -m pytest cli/tests/test_full_e2e.py -v # E2E tests (creates test images)
|
|
168
|
+
python3 -m pytest cli/tests/ -v # All tests
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Example Workflow
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Create a project
|
|
175
|
+
python3 -m cli.gimp_cli project new --width 1920 --height 1080 --profile hd1080p -o edit.json
|
|
176
|
+
|
|
177
|
+
# Add an image layer
|
|
178
|
+
python3 -m cli.gimp_cli --project edit.json layer add-from-file photo.jpg --name "Background"
|
|
179
|
+
|
|
180
|
+
# Apply filters
|
|
181
|
+
python3 -m cli.gimp_cli --project edit.json filter add brightness --layer 0 --param factor=1.2
|
|
182
|
+
python3 -m cli.gimp_cli --project edit.json filter add contrast --layer 0 --param factor=1.1
|
|
183
|
+
python3 -m cli.gimp_cli --project edit.json filter add saturation --layer 0 --param factor=1.3
|
|
184
|
+
|
|
185
|
+
# Add a text overlay
|
|
186
|
+
python3 -m cli.gimp_cli --project edit.json layer new --type text --name "Title"
|
|
187
|
+
python3 -m cli.gimp_cli --project edit.json draw text --layer 0 --text "My Photo" --size 48 --color "#ffffff"
|
|
188
|
+
|
|
189
|
+
# View the layer stack
|
|
190
|
+
python3 -m cli.gimp_cli --project edit.json layer list
|
|
191
|
+
|
|
192
|
+
# Save and render
|
|
193
|
+
python3 -m cli.gimp_cli --project edit.json project save
|
|
194
|
+
python3 -m cli.gimp_cli --project edit.json export render output.jpg --preset jpeg-high --overwrite
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Blend Modes
|
|
198
|
+
|
|
199
|
+
Supported blend modes for layer compositing:
|
|
200
|
+
`normal`, `multiply`, `screen`, `overlay`, `soft_light`, `hard_light`,
|
|
201
|
+
`difference`, `darken`, `lighten`, `color_dodge`, `color_burn`,
|
|
202
|
+
`addition`, `subtract`, `grain_merge`, `grain_extract`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""GIMP CLI - A stateful CLI for image editing."""
|