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
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
; PromptStore.ahk — Bibliothèque de prompts : un fichier .md par prompt (architecture §7).
|
||||
; La catégorie est le dossier de premier niveau. L'id (en-tête) est l'identité stable.
|
||||
|
||||
class PromptItem {
|
||||
Id := ""
|
||||
Title := ""
|
||||
Category := ""
|
||||
Tags := []
|
||||
Targets := []
|
||||
Favorite := 0
|
||||
Pinned := 0
|
||||
Hotkey := ""
|
||||
Hotstring := ""
|
||||
Profile := ""
|
||||
SendAfter := "" ; "" = hérite du profil ; 0/1 = surcharge
|
||||
Sensitive := 0
|
||||
Created := ""
|
||||
Updated := ""
|
||||
Body := ""
|
||||
Meta := Map()
|
||||
Path := ""
|
||||
ParseError := ""
|
||||
|
||||
__New(path, category, parsed) {
|
||||
this.Path := path
|
||||
this.Category := category
|
||||
meta := parsed["meta"]
|
||||
this.Meta := meta
|
||||
this.Body := parsed["body"]
|
||||
if (parsed["errors"].Length > 0)
|
||||
this.ParseError := parsed["errors"][1]
|
||||
SplitPath(path, , , , &nameNoExt)
|
||||
this.Id := meta.Has("id") ? String(meta["id"]) : ""
|
||||
this.Title := meta.Has("title") && meta["title"] != "" ? String(meta["title"]) : nameNoExt
|
||||
this.Tags := meta.Has("tags") && (meta["tags"] is Array) ? meta["tags"] : []
|
||||
this.Targets := meta.Has("targets") && (meta["targets"] is Array) ? meta["targets"] : []
|
||||
this.Favorite := meta.Has("favorite") ? Truthy(meta["favorite"]) : 0
|
||||
this.Pinned := meta.Has("pinned") ? Integer(meta["pinned"]) : 0
|
||||
this.Hotkey := meta.Has("hotkey") ? String(meta["hotkey"]) : ""
|
||||
this.Hotstring := meta.Has("hotstring") ? String(meta["hotstring"]) : ""
|
||||
this.Profile := meta.Has("profile") ? String(meta["profile"]) : ""
|
||||
if meta.Has("send_after")
|
||||
this.SendAfter := Truthy(meta["send_after"])
|
||||
this.Sensitive := meta.Has("sensitive") ? Truthy(meta["sensitive"]) : 0
|
||||
this.Created := meta.Has("created") ? String(meta["created"]) : ""
|
||||
this.Updated := meta.Has("updated") ? String(meta["updated"]) : ""
|
||||
}
|
||||
|
||||
Variables() {
|
||||
if (this.Meta.Has("variables") && (this.Meta["variables"] is Array))
|
||||
return this.Meta["variables"]
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
class PromptStore {
|
||||
LibraryPath := ""
|
||||
Prompts := [] ; [PromptItem]
|
||||
ErrorFiles := [] ; [PromptItem avec ParseError]
|
||||
Signature := ""
|
||||
|
||||
__New(libraryPath) {
|
||||
this.LibraryPath := libraryPath
|
||||
}
|
||||
|
||||
; Charge (ou recharge) toute la bibliothèque depuis le disque (encodage UTF-8 explicite, §19.4).
|
||||
Load() {
|
||||
this.Prompts := []
|
||||
this.ErrorFiles := []
|
||||
root := this.LibraryPath
|
||||
if !DirExist(root)
|
||||
return
|
||||
; Fichiers à la racine = catégorie « Sans catégorie ».
|
||||
this.LoadFolder(root, "Sans catégorie", false)
|
||||
; Dossiers de premier niveau = catégories (sauf _packs).
|
||||
Loop Files root "\*", "D" {
|
||||
if (SubStr(A_LoopFileName, 1, 1) = "_")
|
||||
continue
|
||||
this.LoadFolder(A_LoopFilePath, A_LoopFileName, false)
|
||||
}
|
||||
this.Signature := this.ComputeSignature()
|
||||
}
|
||||
|
||||
LoadFolder(folder, category, recursive) {
|
||||
Loop Files folder "\*.md", recursive ? "FR" : "F" {
|
||||
try {
|
||||
content := FileRead(A_LoopFilePath, "UTF-8")
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
parsed := Frontmatter.Parse(content)
|
||||
item := PromptItem(A_LoopFilePath, category, parsed)
|
||||
; Générer un id manquant et le persister (avec sauvegarde .bak unique, §7.4).
|
||||
if (item.Id = "") {
|
||||
item.Id := Util.NewGuid()
|
||||
item.Meta["id"] := item.Id
|
||||
try {
|
||||
if !FileExist(A_LoopFilePath ".bak")
|
||||
FileCopy(A_LoopFilePath, A_LoopFilePath ".bak")
|
||||
this.WriteFile(item)
|
||||
}
|
||||
}
|
||||
if (item.ParseError != "")
|
||||
this.ErrorFiles.Push(item)
|
||||
else
|
||||
this.Prompts.Push(item)
|
||||
}
|
||||
}
|
||||
|
||||
ComputeSignature() {
|
||||
sig := 0
|
||||
count := 0
|
||||
Loop Files this.LibraryPath "\*.md", "FR" {
|
||||
count++
|
||||
sig += A_LoopFileSize + Integer(SubStr(A_LoopFileTimeModified, 9))
|
||||
}
|
||||
return count ":" sig
|
||||
}
|
||||
|
||||
ChangedOnDisk() {
|
||||
return this.ComputeSignature() != this.Signature
|
||||
}
|
||||
|
||||
ById(id) {
|
||||
for p in this.Prompts {
|
||||
if (p.Id = id)
|
||||
return p
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
Categories() {
|
||||
seen := Map()
|
||||
cats := []
|
||||
for p in this.Prompts {
|
||||
if !seen.Has(p.Category) {
|
||||
seen[p.Category] := true
|
||||
cats.Push(p.Category)
|
||||
}
|
||||
}
|
||||
return cats
|
||||
}
|
||||
|
||||
; Crée un nouveau prompt (fichier) et retourne le PromptItem.
|
||||
CreateNew(title, category, body) {
|
||||
if (Trim(category) = "")
|
||||
category := "Sans catégorie"
|
||||
folder := category = "Sans catégorie" ? this.LibraryPath : this.LibraryPath "\" category
|
||||
DirCreate(folder)
|
||||
path := folder "\" Util.Slugify(title) ".md"
|
||||
n := 2
|
||||
while FileExist(path) {
|
||||
path := folder "\" Util.Slugify(title) "-" n ".md"
|
||||
n++
|
||||
}
|
||||
item := PromptItem(path, category, Map("meta", Map(), "body", body, "errors", [], "hasHeader", false))
|
||||
item.Id := Util.NewGuid()
|
||||
item.Title := title
|
||||
item.Created := Util.Today()
|
||||
item.Updated := Util.Today()
|
||||
item.Body := body
|
||||
this.SyncMeta(item)
|
||||
this.WriteFile(item)
|
||||
this.Prompts.Push(item)
|
||||
this.Signature := this.ComputeSignature()
|
||||
return item
|
||||
}
|
||||
|
||||
; Recopie les champs de l'objet vers la Map Meta (avant sérialisation).
|
||||
SyncMeta(item) {
|
||||
m := item.Meta
|
||||
m["id"] := item.Id
|
||||
m["title"] := item.Title
|
||||
m["tags"] := item.Tags
|
||||
m["targets"] := item.Targets
|
||||
m["favorite"] := item.Favorite
|
||||
m["pinned"] := item.Pinned
|
||||
m["hotkey"] := item.Hotkey
|
||||
m["hotstring"] := item.Hotstring
|
||||
m["profile"] := item.Profile
|
||||
if (item.SendAfter != "")
|
||||
m["send_after"] := item.SendAfter
|
||||
m["sensitive"] := item.Sensitive
|
||||
if (item.Created != "")
|
||||
m["created"] := item.Created
|
||||
m["updated"] := Util.Today()
|
||||
item.Body := item.Body
|
||||
}
|
||||
|
||||
WriteFile(item) {
|
||||
this.SyncMeta(item)
|
||||
text := Frontmatter.Serialize(item.Meta, item.Body)
|
||||
; Écriture UTF-8 explicite : FileOpen avec encodage nommé (jamais ANSI par défaut, §19.4).
|
||||
f := FileOpen(item.Path, "w", "UTF-8")
|
||||
normalized := StrReplace(text, "`r`n", "`n") ; d'abord LF partout (le corps peut venir de l'éditeur en CRLF)
|
||||
f.Write(StrReplace(normalized, "`n", "`r`n"))
|
||||
f.Close()
|
||||
}
|
||||
|
||||
Save(item) {
|
||||
this.WriteFile(item)
|
||||
this.Signature := this.ComputeSignature()
|
||||
}
|
||||
|
||||
Duplicate(item) {
|
||||
copy := this.CreateNew(item.Title " (copie)", item.Category, item.Body)
|
||||
copy.Tags := item.Tags.Clone()
|
||||
copy.Targets := item.Targets.Clone()
|
||||
copy.Meta["variables"] := item.Meta.Has("variables") ? item.Meta["variables"] : []
|
||||
if (copy.Meta["variables"].Length = 0)
|
||||
copy.Meta.Delete("variables")
|
||||
this.Save(copy)
|
||||
return copy
|
||||
}
|
||||
|
||||
; Suppression = corbeille Windows (jamais définitive, §9.1).
|
||||
Delete(item) {
|
||||
FileRecycle(item.Path)
|
||||
this.Load()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user