- 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
222 lines
7.3 KiB
AutoHotkey
222 lines
7.3 KiB
AutoHotkey
; 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()
|
|
}
|
|
}
|