Files
promptkey/lib/VariableEngine.ahk
Spark b102a4a206 feat(core): moteur PromptKey — bibliothèque .md, variables, injection, raccourcis
- PromptStore/Frontmatter : un prompt = un fichier Markdown, en-tête au
  sous-ensemble strict du document d'architecture (§7), écriture UTF-8
- VariableEngine : variables typées + automatiques (selection, clipboard,
  date, app, cursor), résolution et profils de destination
- Injector/ClipboardGuard : focus capturé/rendu, presse-papiers sauvegardé
  puis restauré conditionnellement, aucun envoi automatique par défaut
- Search : classement flou insensible aux accents, pondéré par l'usage
- UsageStats (usage.json), HotkeyManager (conflits signalés, jamais muets)
- Settings : config.ini + profils par IA (claude, chatgpt, qwen, terminal…)
- PromptKey.ahk : tray, instance unique, capture du contexte, surveillance
  de la bibliothèque, installation du pack au premier lancement
2026-10-11 10:17:30 -04:00

126 lines
4.7 KiB
AutoHotkey

; VariableEngine.ahk — Variables des prompts : automatiques et manuelles (architecture §10).
class VariableEngine {
static AutoNames := ["selection", "clipboard", "date", "datetime", "time", "app", "cursor"]
static IsAuto(name) {
return HasInArray(VariableEngine.AutoNames, StrLower(name))
}
; Toutes les variables {{...}} d'un corps, dans l'ordre d'apparition, dédupliquées.
static FindVars(body) {
found := []
seen := Map()
pos := 1
while (m := RegExMatch(body, "\{\{\s*([A-Za-z0-9_]+)\s*\}\}", &match, pos)) {
name := StrLower(match[1])
if !seen.Has(name) {
seen[name] := true
found.Push(name)
}
pos := m + match.Len[0]
}
return found
}
; Variables manuelles requises : celles du corps qui ne sont pas automatiques.
; Retourne [ Map {name, label, type, choices, default, required, multiline, declared} ].
static ManualVars(prompt) {
declared := Map()
for v in prompt.Variables() {
if v.Has("name")
declared[StrLower(String(v["name"]))] := v
}
out := []
for name in VariableEngine.FindVars(prompt.Body) {
if VariableEngine.IsAuto(name)
continue
if declared.Has(name) {
d := declared[name]
out.Push(Map(
"name", name,
"label", d.Has("label") && d["label"] != "" ? String(d["label"]) : name,
"type", d.Has("type") ? String(d["type"]) : "text",
"choices", d.Has("choices") && (d["choices"] is Array) ? d["choices"] : [],
"default", d.Has("default") ? String(d["default"]) : "",
"required", d.Has("required") ? Truthy(d["required"]) : 0,
"multiline", d.Has("multiline") ? Truthy(d["multiline"]) : 0,
"declared", 1))
} else {
out.Push(Map("name", name, "label", name, "type", "text", "choices", [], "default", "", "required", 0, "multiline", 0, "declared", 0))
}
}
return out
}
; Valeurs automatiques à partir du contexte d'invocation (figées à l'invocation, §10.3).
static AutoValues(ctx) {
vals := Map()
vals["selection"] := ctx.Has("selection") ? ctx["selection"] : ""
vals["clipboard"] := ctx.Has("clipboardText") ? ctx["clipboardText"] : ""
vals["date"] := FormatTime(, "yyyy-MM-dd")
vals["datetime"] := FormatTime(, "yyyy-MM-dd HH:mm")
vals["time"] := FormatTime(, "HH:mm")
vals["app"] := ctx.Has("process") ? ctx["process"] : ""
return vals
}
; Résolution (§10.5). Retourne Map { text, unresolved: [], cursorFromEnd: Integer }.
static Resolve(body, manualValues, ctx) {
auto := VariableEngine.AutoValues(ctx)
text := body
unresolved := []
; Remplacement variable par variable (toutes occurrences).
for name in VariableEngine.FindVars(body) {
token := ""
if (name = "cursor")
continue
if auto.Has(name)
value := auto[name]
else if manualValues.Has(name)
value := String(manualValues[name])
else {
unresolved.Push(name)
continue
}
text := RegExReplace(text, "\{\{\s*" name "\s*\}\}", VariableEngine.EscapeReplacement(value))
}
; Marqueur de curseur : on retient la distance à la fin, puis on retire le marqueur.
cursorFromEnd := 0
if RegExMatch(text, "\{\{\s*cursor\s*\}\}", &cm) {
cursorFromEnd := StrLen(text) - (cm.Pos[0] + cm.Len[0] - 1)
text := StrReplace(text, cm[0], "")
}
return Map("text", text, "unresolved", unresolved, "cursorFromEnd", cursorFromEnd)
}
; Dans RegExReplace, $ a un sens spécial : on le neutralise pour insérer le texte tel quel.
static EscapeReplacement(value) {
return StrReplace(value, "$", "$$")
}
; Enveloppe du profil (§12.2) : préambule + corps + postambule.
static ApplyProfile(text, profile) {
out := text
pre := profile.Has("preambule") ? profile["preambule"] : ""
post := profile.Has("postambule") ? profile["postambule"] : ""
if (pre != "")
out := pre "`n`n" out
if (post != "")
out := out "`n`n" post
return out
}
}
; Vrai pour 1, "1", "true" (insensible à la casse) — valeurs lues depuis l'en-tête.
Truthy(v) {
if (v = 1)
return 1
if (v is String) {
lv := StrLower(v)
if (lv = "true" || lv = "1" || lv = "oui")
return 1
}
return 0
}