; 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() } }