@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
|
+
}
|