synapseForge 0.1.24.dev2__py3-none-any.whl
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.
- pipeline/__init__.py +1 -0
- pipeline/init/__init__.py +7 -0
- pipeline/init/__main__.py +10 -0
- pipeline/init/config_handler.py +35 -0
- pipeline/init/input_handler.py +124 -0
- pipeline/init/logo_handler.py +33 -0
- pipeline/init/main.py +151 -0
- pipeline/init/placeholder_handler.py +162 -0
- pipeline/init/template_handler.py +89 -0
- pipeline/init/venv_handler.py +68 -0
- pipeline/launch/__init__.py +1 -0
- pipeline/launch/forge.py +701 -0
- pipeline/launch/templates/launcher.py +79 -0
- pipeline/template.zip +0 -0
- synapseforge/__init__.py +3 -0
- synapseforge/__main__.py +6 -0
- synapseforge/cli/__init__.py +1 -0
- synapseforge/cli/main.py +390 -0
- synapseforge/tk/__init__.py +1 -0
- synapseforge/tk/colors_app.py +305 -0
- synapseforge/tk/init_app.py +447 -0
- synapseforge/tk/logo.ico +0 -0
- synapseforge/tk/logo.png +0 -0
- synapseforge-0.1.24.dev2.dist-info/METADATA +187 -0
- synapseforge-0.1.24.dev2.dist-info/RECORD +29 -0
- synapseforge-0.1.24.dev2.dist-info/WHEEL +5 -0
- synapseforge-0.1.24.dev2.dist-info/entry_points.txt +2 -0
- synapseforge-0.1.24.dev2.dist-info/licenses/LICENSE +201 -0
- synapseforge-0.1.24.dev2.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""tkinter GUI for synapseforge colors — replaces terminal color editor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
import tkinter as tk
|
|
10
|
+
from tkinter import colorchooser, messagebox, ttk
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import ctypes
|
|
14
|
+
|
|
15
|
+
_HERE = Path(__file__).resolve().parent
|
|
16
|
+
_ICO_PATH = _HERE / "logo.ico"
|
|
17
|
+
_LOGO_PNG_PATH = _HERE / "logo.png"
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from PIL import Image, ImageTk
|
|
21
|
+
except ImportError: # pragma: no cover
|
|
22
|
+
Image = None # type: ignore[assignment]
|
|
23
|
+
ImageTk = None
|
|
24
|
+
|
|
25
|
+
_HEX_RE = re.compile(r"^#[0-9a-fA-F]{6}$")
|
|
26
|
+
|
|
27
|
+
COLOR_FIELDS = [
|
|
28
|
+
("primary", "Color principal: botón de enviar, burbuja y avatar del asistente, barra de actividad, opción seleccionada del menú, enlaces de las respuestas y puntos de “escribiendo…”"),
|
|
29
|
+
("secondary", "Color de los detalles suaves: borde que se ilumina al hacer clic en un campo, anillo de la conversación seleccionada, bordes de las tarjetas y cursor de escritura"),
|
|
30
|
+
("primary_text", "Color del texto e íconos que van sobre el color principal: flecha de enviar, texto de botones, ícono del avatar y texto de la burbuja del asistente"),
|
|
31
|
+
("gradient_secondary", "Color final del degradé de los botones y del avatar del asistente (el inicio es el color principal)"),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ──────────────────────────────────────────────────────────────
|
|
36
|
+
# Fijar el AppUserModelID ANTES de crear cualquier ventana
|
|
37
|
+
# ──────────────────────────────────────────────────────────────
|
|
38
|
+
def _set_app_user_model_id() -> None:
|
|
39
|
+
if sys.platform != "win32":
|
|
40
|
+
return
|
|
41
|
+
try:
|
|
42
|
+
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
|
|
43
|
+
"synapseforge.colors.1"
|
|
44
|
+
)
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
_set_app_user_model_id()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ColorsApp:
|
|
52
|
+
"""GUI for editing runtime colors.json."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, colors_path: Path, current: Dict[str, str]) -> None:
|
|
55
|
+
self.colors_path = colors_path
|
|
56
|
+
self.current = current
|
|
57
|
+
self.result: Optional[Dict[str, str]] = None
|
|
58
|
+
|
|
59
|
+
self.root = tk.Tk()
|
|
60
|
+
self.root.title("synapseForge — Colors")
|
|
61
|
+
self.root.resizable(False, False)
|
|
62
|
+
|
|
63
|
+
# ── Configurar el ícono (con retraso para asegurar que la ventana esté lista) ──
|
|
64
|
+
self.root.after(100, self._set_icon)
|
|
65
|
+
|
|
66
|
+
# ── Logo header (desde archivo local) ────────────────────────
|
|
67
|
+
self._load_logo_local()
|
|
68
|
+
|
|
69
|
+
# ── Instruction ──────────────────────────────────────────────
|
|
70
|
+
ttk.Label(
|
|
71
|
+
self.root,
|
|
72
|
+
text="Colores del proyecto (dejá vacío para mantener el actual):",
|
|
73
|
+
font=("", 10, ""),
|
|
74
|
+
).pack(pady=(5, 10))
|
|
75
|
+
|
|
76
|
+
# ── Color fields ─────────────────────────────────────────────
|
|
77
|
+
frame = ttk.Frame(self.root)
|
|
78
|
+
frame.pack(fill="both", expand=True, padx=15, pady=(0, 10))
|
|
79
|
+
|
|
80
|
+
self._entries: Dict[str, tk.Entry] = {}
|
|
81
|
+
self._previews: Dict[str, tk.Canvas] = {}
|
|
82
|
+
|
|
83
|
+
for i, (key, desc) in enumerate(COLOR_FIELDS):
|
|
84
|
+
row = i * 2
|
|
85
|
+
|
|
86
|
+
# Explanation (wraps so it doesn't push the selector out of view)
|
|
87
|
+
ttk.Label(frame, text=desc, wraplength=430, justify="left").grid(
|
|
88
|
+
row=row, column=0, columnspan=4, sticky="w", pady=(8, 0), padx=(0, 10)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Preview square (18×18)
|
|
92
|
+
cv = tk.Canvas(frame, width=20, height=20, highlightthickness=1,
|
|
93
|
+
highlightbackground="#ccc")
|
|
94
|
+
cv.grid(row=row + 1, column=0, pady=(4, 8), padx=(0, 6))
|
|
95
|
+
self._previews[key] = cv
|
|
96
|
+
|
|
97
|
+
# Entry with current value
|
|
98
|
+
ent = ttk.Entry(frame, width=14)
|
|
99
|
+
ent.grid(row=row + 1, column=1, pady=(4, 8), padx=(0, 6))
|
|
100
|
+
cur_val = current.get(key, "")
|
|
101
|
+
if cur_val:
|
|
102
|
+
ent.insert(0, cur_val)
|
|
103
|
+
ent.bind("<KeyRelease>", lambda _e, k=key: self._update_preview(k))
|
|
104
|
+
self._entries[key] = ent
|
|
105
|
+
|
|
106
|
+
# Color picker button
|
|
107
|
+
ttk.Button(
|
|
108
|
+
frame, text="Seleccionar", command=lambda k=key: self._pick_color(k)
|
|
109
|
+
).grid(row=row + 1, column=2, pady=(4, 8), padx=(0, 0), sticky="w")
|
|
110
|
+
|
|
111
|
+
# Initial preview
|
|
112
|
+
self._update_preview(key)
|
|
113
|
+
|
|
114
|
+
# ── Gradient toggle ──────────────────────────────────────────
|
|
115
|
+
gradient_frame = ttk.Frame(self.root)
|
|
116
|
+
gradient_frame.pack(fill="x", padx=15, pady=(0, 5))
|
|
117
|
+
|
|
118
|
+
grad_default = current.get("usar_gradiente", True)
|
|
119
|
+
if isinstance(grad_default, str):
|
|
120
|
+
grad_default = grad_default.lower() in ("true", "1", "yes")
|
|
121
|
+
self._usar_gradiente_var = tk.BooleanVar(value=bool(grad_default))
|
|
122
|
+
ttk.Checkbutton(
|
|
123
|
+
gradient_frame,
|
|
124
|
+
text="Usar degradé en botones y avatar",
|
|
125
|
+
variable=self._usar_gradiente_var,
|
|
126
|
+
command=self._toggle_gradient_fields,
|
|
127
|
+
).pack(anchor="w")
|
|
128
|
+
|
|
129
|
+
self._toggle_gradient_fields()
|
|
130
|
+
|
|
131
|
+
# ── Bottom buttons ───────────────────────────────────────────
|
|
132
|
+
bottom = ttk.Frame(self.root)
|
|
133
|
+
bottom.pack(fill="x", padx=15, pady=(0, 12))
|
|
134
|
+
|
|
135
|
+
ttk.Button(bottom, text="Cancelar", command=self._on_cancel).pack(
|
|
136
|
+
side="right", padx=(5, 0)
|
|
137
|
+
)
|
|
138
|
+
ttk.Button(bottom, text="Guardar", command=self._on_save).pack(side="right")
|
|
139
|
+
|
|
140
|
+
# ── Center ───────────────────────────────────────────────────
|
|
141
|
+
self._center(660, 800)
|
|
142
|
+
|
|
143
|
+
# ──────────────────────────────────────────────────────────────────
|
|
144
|
+
# Configuración robusta del ícono usando ctypes
|
|
145
|
+
# ──────────────────────────────────────────────────────────────────
|
|
146
|
+
def _set_icon(self) -> None:
|
|
147
|
+
"""Asigna el ícono de la ventana y de la barra de tareas usando ctypes."""
|
|
148
|
+
if not _ICO_PATH.is_file():
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
# 1. iconbitmap (funciona para la ventana)
|
|
153
|
+
self.root.iconbitmap(str(_ICO_PATH.resolve()))
|
|
154
|
+
|
|
155
|
+
# 2. Forzar actualización de la barra de tareas con ctypes
|
|
156
|
+
hwnd = self.root.winfo_id()
|
|
157
|
+
user32 = ctypes.windll.user32
|
|
158
|
+
# Cargar el ícono desde el archivo
|
|
159
|
+
hicon = user32.LoadImageW(
|
|
160
|
+
0,
|
|
161
|
+
str(_ICO_PATH.resolve()),
|
|
162
|
+
1, # IMAGE_ICON
|
|
163
|
+
0, 0,
|
|
164
|
+
0x00000010 # LR_LOADFROMFILE
|
|
165
|
+
)
|
|
166
|
+
if hicon:
|
|
167
|
+
# GCL_HICON = -14, GCL_HICONSM = -34
|
|
168
|
+
user32.SetClassLongW(hwnd, -14, hicon)
|
|
169
|
+
user32.SetClassLongW(hwnd, -34, hicon)
|
|
170
|
+
# WM_SETICON = 0x0080, ICON_BIG = 0, ICON_SMALL = 1
|
|
171
|
+
user32.SendMessageW(hwnd, 0x0080, 0, hicon)
|
|
172
|
+
user32.SendMessageW(hwnd, 0x0080, 1, hicon)
|
|
173
|
+
|
|
174
|
+
except Exception:
|
|
175
|
+
pass # Silencioso si falla
|
|
176
|
+
|
|
177
|
+
# ------------------------------------------------------------------
|
|
178
|
+
# Logo (cargado desde archivo local)
|
|
179
|
+
# ------------------------------------------------------------------
|
|
180
|
+
def _load_logo_local(self) -> None:
|
|
181
|
+
"""Carga el logo desde logo_transparente.png (local) y lo muestra en la ventana."""
|
|
182
|
+
if Image is None or ImageTk is None:
|
|
183
|
+
return
|
|
184
|
+
if not _LOGO_PNG_PATH.is_file():
|
|
185
|
+
return
|
|
186
|
+
try:
|
|
187
|
+
pil = Image.open(_LOGO_PNG_PATH)
|
|
188
|
+
pil.thumbnail((150, 150), Image.LANCZOS)
|
|
189
|
+
self._logo_img = ImageTk.PhotoImage(pil)
|
|
190
|
+
lbl = tk.Label(self.root, image=self._logo_img)
|
|
191
|
+
lbl.pack(pady=(10, 2))
|
|
192
|
+
except Exception:
|
|
193
|
+
pass # silencioso si falla
|
|
194
|
+
|
|
195
|
+
# ------------------------------------------------------------------
|
|
196
|
+
# Color helpers
|
|
197
|
+
# ------------------------------------------------------------------
|
|
198
|
+
def _pick_color(self, key: str) -> None:
|
|
199
|
+
result = colorchooser.askcolor(
|
|
200
|
+
title=key,
|
|
201
|
+
color=self._entries[key].get() or None,
|
|
202
|
+
parent=self.root,
|
|
203
|
+
)
|
|
204
|
+
if result and result[1]:
|
|
205
|
+
self._entries[key].delete(0, tk.END)
|
|
206
|
+
self._entries[key].insert(0, result[1])
|
|
207
|
+
self._update_preview(key)
|
|
208
|
+
|
|
209
|
+
def _update_preview(self, key: str) -> None:
|
|
210
|
+
cv = self._previews[key]
|
|
211
|
+
raw = self._entries[key].get().strip()
|
|
212
|
+
cv.delete("all")
|
|
213
|
+
if raw and _HEX_RE.match(raw):
|
|
214
|
+
cv.config(bg=raw)
|
|
215
|
+
else:
|
|
216
|
+
cv.config(bg="#ffffff")
|
|
217
|
+
|
|
218
|
+
def _toggle_gradient_fields(self) -> None:
|
|
219
|
+
"""Enable/disable gradient_secondary picker based on checkbox."""
|
|
220
|
+
state = "normal" if self._usar_gradiente_var.get() else "disabled"
|
|
221
|
+
key = "gradient_secondary"
|
|
222
|
+
if key in self._entries:
|
|
223
|
+
self._entries[key].config(state=state)
|
|
224
|
+
if key in self._previews:
|
|
225
|
+
self._previews[key].config(highlightbackground="#ccc" if state == "normal" else "#eee")
|
|
226
|
+
|
|
227
|
+
# ------------------------------------------------------------------
|
|
228
|
+
# Save / Cancel
|
|
229
|
+
# ------------------------------------------------------------------
|
|
230
|
+
def _on_save(self) -> None:
|
|
231
|
+
updated: Dict[str, str] = {}
|
|
232
|
+
errors: list[str] = []
|
|
233
|
+
usar_gradiente = self._usar_gradiente_var.get()
|
|
234
|
+
updated["usar_gradiente"] = str(usar_gradiente).lower()
|
|
235
|
+
for key, _desc in COLOR_FIELDS:
|
|
236
|
+
raw = self._entries[key].get().strip()
|
|
237
|
+
if key == "gradient_secondary" and not usar_gradiente:
|
|
238
|
+
# If gradient off, set same as primary so gradient looks solid
|
|
239
|
+
updated[key] = updated.get("primary") or self.current.get("primary", "#000000")
|
|
240
|
+
continue
|
|
241
|
+
if not raw:
|
|
242
|
+
updated[key] = self.current.get(key, "")
|
|
243
|
+
elif _HEX_RE.match(raw):
|
|
244
|
+
updated[key] = raw
|
|
245
|
+
else:
|
|
246
|
+
errors.append(f"'{key}': '{raw}' no es un color hex válido (#RRGGBB).")
|
|
247
|
+
|
|
248
|
+
if errors:
|
|
249
|
+
messagebox.showerror("Errores de validación",
|
|
250
|
+
"\n".join(errors), parent=self.root)
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
# Write
|
|
254
|
+
try:
|
|
255
|
+
self.colors_path.write_text(
|
|
256
|
+
json.dumps(updated, indent=2, ensure_ascii=False), encoding="utf-8"
|
|
257
|
+
)
|
|
258
|
+
self.result = updated
|
|
259
|
+
self.root.destroy()
|
|
260
|
+
except Exception as exc:
|
|
261
|
+
messagebox.showerror("Error al guardar",
|
|
262
|
+
f"No se pudo escribir colors.json:\n{exc}",
|
|
263
|
+
parent=self.root)
|
|
264
|
+
|
|
265
|
+
def _on_cancel(self) -> None:
|
|
266
|
+
self.result = None
|
|
267
|
+
self.root.destroy()
|
|
268
|
+
|
|
269
|
+
# ------------------------------------------------------------------
|
|
270
|
+
# Window utils
|
|
271
|
+
# ------------------------------------------------------------------
|
|
272
|
+
def _center(self, w: int, h: int) -> None:
|
|
273
|
+
sw = self.root.winfo_screenwidth()
|
|
274
|
+
sh = self.root.winfo_screenheight()
|
|
275
|
+
x = (sw - w) // 2
|
|
276
|
+
y = (sh - h) // 2
|
|
277
|
+
self.root.geometry(f"{w}x{h}+{x}+{y}")
|
|
278
|
+
|
|
279
|
+
# ------------------------------------------------------------------
|
|
280
|
+
# Public entry point
|
|
281
|
+
# ------------------------------------------------------------------
|
|
282
|
+
@staticmethod
|
|
283
|
+
def launch(project_dir: str) -> Optional[Dict[str, str]]:
|
|
284
|
+
"""Open the colors GUI and return the updated dict (or None if cancelled).
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
project_dir: Path to project root containing frontend/public/colors.json.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Updated colors dict, or ``None`` if the user cancelled.
|
|
291
|
+
"""
|
|
292
|
+
colors_path = Path(project_dir).resolve() / "frontend" / "public" / "colors.json"
|
|
293
|
+
if not colors_path.is_file():
|
|
294
|
+
print(f"ERROR: No se encontró {colors_path}", file=sys.stderr)
|
|
295
|
+
sys.exit(1)
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
current = json.loads(colors_path.read_text(encoding="utf-8"))
|
|
299
|
+
except Exception as exc:
|
|
300
|
+
print(f"ERROR leyendo colors.json: {exc}", file=sys.stderr)
|
|
301
|
+
sys.exit(1)
|
|
302
|
+
|
|
303
|
+
app = ColorsApp(colors_path, current)
|
|
304
|
+
app.root.mainloop()
|
|
305
|
+
return app.result
|