- 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
260 lines
9.8 KiB
AutoHotkey
260 lines
9.8 KiB
AutoHotkey
#Requires AutoHotkey v2.0
|
|
#SingleInstance Force
|
|
Persistent()
|
|
; PromptKey — Gestionnaire de prompts pour IA (Windows, AutoHotkey v2).
|
|
; Document d'architecture : architecture-prompt-manager-autohotkey.md (Spark, v1.0).
|
|
; Principe : insérer, ne pas envoyer (D5) ; zéro réseau (D7) ; un prompt = un fichier .md (D2).
|
|
|
|
#Include lib\Util.ahk
|
|
#Include lib\Json.ahk
|
|
#Include lib\Settings.ahk
|
|
#Include lib\Frontmatter.ahk
|
|
#Include lib\VariableEngine.ahk
|
|
#Include lib\PromptStore.ahk
|
|
#Include lib\Search.ahk
|
|
#Include lib\ClipboardGuard.ahk
|
|
#Include lib\Injector.ahk
|
|
#Include lib\UsageStats.ahk
|
|
#Include lib\HotkeyManager.ahk
|
|
#Include gui\VarForm.ahk
|
|
#Include gui\Picker.ahk
|
|
#Include gui\Editor.ahk
|
|
#Include gui\Manager.ahk
|
|
#Include gui\SettingsGui.ahk
|
|
|
|
class App {
|
|
static Settings := ""
|
|
static Store := ""
|
|
static Usage := ""
|
|
static InjectorObj := ""
|
|
static Hotkeys := HotkeyManager()
|
|
static Picker := PromptPicker()
|
|
static Manager := PromptManager()
|
|
static Editor := PromptEditor()
|
|
static PrefsGui := SettingsGui()
|
|
static VarForm := VarForm()
|
|
static LastCtx := ""
|
|
static LastPromptId := ""
|
|
static Started := false
|
|
|
|
static Init() {
|
|
Util.LogFile := A_ScriptDir "\promptkey.log"
|
|
App.Settings := Settings(A_ScriptDir "\config.ini")
|
|
App.Store := PromptStore(App.Settings.LibraryPath)
|
|
App.Store.Load()
|
|
App.InstallStarterPackIfEmpty()
|
|
App.Usage := UsageStats(App.Settings.LibraryPath "\usage.json")
|
|
App.InjectorObj := Injector(App.Settings)
|
|
App.Picker.Build((p, useDefaults) => App.PickerUse(p, useDefaults), (p) => App.Editor.Show(p))
|
|
App.Manager.Build((p) => App.UsePrompt(p, App.LastCtxOrCapture(), Map()), (p) => App.Editor.Show(p))
|
|
App.BuildTray()
|
|
App.RegisterHotkeys()
|
|
SetTimer(() => App.WatchLibrary(), 5000)
|
|
App.Started := true
|
|
Util.Log("PromptKey démarré — " App.Store.Prompts.Length " prompts, bibliothèque : " App.Settings.LibraryPath)
|
|
TrayTip("Prêt — " App.Store.Prompts.Length " prompts. Raccourci : " App.Settings.PickerHotkey, "PromptKey")
|
|
}
|
|
|
|
; Premier lancement : si la bibliothèque est vide et que le pack est présent, on le recopie.
|
|
static InstallStarterPackIfEmpty() {
|
|
if (App.Store.Prompts.Length > 0)
|
|
return
|
|
packRoot := A_ScriptDir "\packs\essentiels"
|
|
if !DirExist(packRoot)
|
|
return
|
|
copied := 0
|
|
Loop Files packRoot "\*.md", "FR" {
|
|
; Chemin relatif : <Catégorie>\<fichier>.md
|
|
rel := SubStr(A_LoopFilePath, StrLen(packRoot) + 2)
|
|
dest := App.Settings.LibraryPath "\" rel
|
|
SplitPath(dest, , &destDir)
|
|
DirCreate(destDir)
|
|
if !FileExist(dest) {
|
|
FileCopy(A_LoopFilePath, dest)
|
|
copied++
|
|
}
|
|
}
|
|
if (copied > 0) {
|
|
App.Store.Load()
|
|
Util.Log("Pack de démarrage installé : " copied " prompts")
|
|
}
|
|
}
|
|
|
|
static BuildTray() {
|
|
A_IconTip := "PromptKey — Gestionnaire de prompts"
|
|
tray := A_TrayMenu
|
|
tray.Delete()
|
|
tray.Add("Ouvrir le Picker", (*) => App.OpenPicker())
|
|
tray.Add("Gestionnaire", (*) => App.OpenManager())
|
|
tray.Add("Nouveau prompt", (*) => App.NewPromptInteractive())
|
|
tray.Add()
|
|
tray.Add("Recharger la bibliothèque", (*) => App.RefreshAfterLibraryChange(true))
|
|
tray.Add("Paramètres", (*) => App.PrefsGui.Show())
|
|
tray.Add("Ouvrir le dossier de la bibliothèque", (*) => Run(App.Settings.LibraryPath))
|
|
tray.Add()
|
|
tray.Add("Quitter", (*) => ExitApp())
|
|
tray.Default := "Ouvrir le Picker"
|
|
tray.ClickCount := 1
|
|
}
|
|
|
|
static RegisterHotkeys() {
|
|
callbacks := Map(
|
|
"picker", (*) => App.OpenPicker(),
|
|
"manager", (*) => App.OpenManager(),
|
|
"newPrompt", (*) => App.NewPromptInteractive(),
|
|
"replay", (*) => App.Replay(),
|
|
"usePrompt", (id, extra*) => App.UsePromptById(id)
|
|
)
|
|
App.Hotkeys.RegisterAll(App.Settings, App.Store.Prompts, callbacks)
|
|
}
|
|
|
|
; Après un changement de bibliothèque (édition, rechargement) : raccourcis à jour.
|
|
static RefreshAfterLibraryChange(announce := false) {
|
|
App.Store.Load()
|
|
App.RegisterHotkeys()
|
|
if announce
|
|
TrayTip("Bibliothèque rechargée : " App.Store.Prompts.Length " prompts.", "PromptKey")
|
|
}
|
|
|
|
static WatchLibrary() {
|
|
if (App.Started && App.Store.ChangedOnDisk()) {
|
|
Util.Log("Changement détecté dans la bibliothèque — rechargement.")
|
|
App.RefreshAfterLibraryChange()
|
|
}
|
|
}
|
|
|
|
; Capture du contexte d'invocation (§11.2) : fenêtre active, processus, sélection, presse-papiers.
|
|
; L'ordre compte : lire le presse-papiers AVANT la capture de sélection (qui le restaure ensuite).
|
|
static CaptureContext() {
|
|
ctx := Map("hwnd", 0, "title", "", "process", "", "selection", "", "clipboardText", "")
|
|
try ctx["hwnd"] := WinExist("A")
|
|
if (ctx["hwnd"]) {
|
|
try ctx["title"] := WinGetTitle("ahk_id " ctx["hwnd"])
|
|
try ctx["process"] := WinGetProcessName("ahk_id " ctx["hwnd"])
|
|
}
|
|
ctx["clipboardText"] := ClipboardGuard.ClipboardText()
|
|
ctx["selection"] := ClipboardGuard.CaptureSelection()
|
|
return ctx
|
|
}
|
|
|
|
static LastCtxOrCapture() {
|
|
return App.LastCtx != "" ? App.LastCtx : App.CaptureContext()
|
|
}
|
|
|
|
static OpenPicker() {
|
|
App.LastCtx := App.CaptureContext()
|
|
ctx := App.LastCtx
|
|
ctx["profileName"] := App.ResolveProfileName("", ctx)
|
|
App.Picker.Show(ctx)
|
|
}
|
|
|
|
static OpenManager() {
|
|
App.LastCtx := App.CaptureContext()
|
|
App.Manager.Show()
|
|
}
|
|
|
|
static NewPromptInteractive() {
|
|
ctx := App.CaptureContext()
|
|
App.LastCtx := ctx
|
|
ib := InputBox("Titre du nouveau prompt :", "PromptKey — Nouveau prompt")
|
|
if (ib.Result != "OK" || Trim(ib.Value) = "")
|
|
return
|
|
body := ctx["selection"] != "" ? ctx["selection"] : ""
|
|
item := App.Store.CreateNew(Trim(ib.Value), "Sans catégorie", body)
|
|
App.RefreshAfterLibraryChange()
|
|
App.Editor.Show(item)
|
|
}
|
|
|
|
static Replay() {
|
|
if (App.LastPromptId = "") {
|
|
TrayTip("Aucun prompt récent à rejouer.", "PromptKey")
|
|
return
|
|
}
|
|
App.LastCtx := App.CaptureContext()
|
|
App.UsePromptById(App.LastPromptId)
|
|
}
|
|
|
|
static PickerUse(prompt, useDefaults) {
|
|
opts := Map("useDefaults", useDefaults ? 1 : 0)
|
|
App.UsePrompt(prompt, App.LastCtxOrCapture(), opts)
|
|
}
|
|
|
|
static UsePromptById(id) {
|
|
prompt := App.Store.ById(id)
|
|
if (prompt = "")
|
|
return
|
|
App.UsePrompt(prompt, App.LastCtxOrCapture(), Map())
|
|
}
|
|
|
|
; Résolution du profil (§12.3) : prompt > targets > auto-détection > défaut.
|
|
static ResolveProfileName(prompt, ctx) {
|
|
if (prompt != "" && prompt.Profile != "")
|
|
return StrLower(prompt.Profile)
|
|
if (prompt != "" && prompt.Targets.Length > 0)
|
|
return StrLower(String(prompt.Targets[1]))
|
|
proc := ctx.Has("process") ? ctx["process"] : ""
|
|
title := ctx.Has("title") ? ctx["title"] : ""
|
|
return App.Settings.DetectProfile(proc, title)
|
|
}
|
|
|
|
static UsePrompt(prompt, ctx, opts) {
|
|
useDefaults := opts.Has("useDefaults") ? opts["useDefaults"] : 0
|
|
profileName := App.ResolveProfileName(prompt, ctx)
|
|
defs := VariableEngine.ManualVars(prompt)
|
|
lastVals := App.Usage.LastValues(prompt.Id)
|
|
defaults := Map()
|
|
for d in defs {
|
|
defaults[d["name"]] := lastVals.Has(d["name"]) ? lastVals[d["name"]] : d["default"]
|
|
}
|
|
if (defs.Length = 0 || useDefaults) {
|
|
values := Map()
|
|
for d in defs
|
|
values[d["name"]] := defaults[d["name"]]
|
|
App.FinishUse(prompt, ctx, profileName, values, defs)
|
|
return
|
|
}
|
|
App.VarForm.Show(prompt, defs, defaults, (values) => App.OnVarFormDone(prompt, ctx, profileName, values, defs))
|
|
}
|
|
|
|
static OnVarFormDone(prompt, ctx, profileName, values, defs) {
|
|
if (values = "") {
|
|
; Annulation : rendre le focus à l'application d'origine, sans rien injecter (§10.4).
|
|
if (ctx.Has("hwnd") && ctx["hwnd"] && WinExist("ahk_id " ctx["hwnd"]))
|
|
try WinActivate("ahk_id " ctx["hwnd"])
|
|
return
|
|
}
|
|
App.FinishUse(prompt, ctx, profileName, values, defs)
|
|
}
|
|
|
|
static FinishUse(prompt, ctx, profileName, values, defs) {
|
|
resolved := VariableEngine.Resolve(prompt.Body, values, ctx)
|
|
if (resolved["unresolved"].Length > 0)
|
|
TrayTip("Variables non résolues : " Join(resolved["unresolved"], ", "), "PromptKey")
|
|
profile := App.Settings.GetProfile(profileName)
|
|
finalText := VariableEngine.ApplyProfile(resolved["text"], profile)
|
|
sendAfter := prompt.SendAfter != "" ? prompt.SendAfter : profile["sendAfter"]
|
|
method := profile["methode"] != "" ? profile["methode"] : App.Settings.Method
|
|
; Statistiques : exclure les variables de type password (§15).
|
|
statsValues := Map()
|
|
pwNames := Map()
|
|
for d in defs {
|
|
if (d["type"] = "password")
|
|
pwNames[d["name"]] := true
|
|
}
|
|
for name, val in values {
|
|
if !pwNames.Has(name)
|
|
statsValues[name] := val
|
|
}
|
|
App.Usage.RecordUse(prompt.Id, statsValues)
|
|
App.LastPromptId := prompt.Id
|
|
App.InjectorObj.Deliver(finalText, Map(
|
|
"hwnd", ctx.Has("hwnd") ? ctx["hwnd"] : 0,
|
|
"method", method,
|
|
"sendAfter", sendAfter,
|
|
"sensitive", prompt.Sensitive,
|
|
"cursorFromEnd", resolved["cursorFromEnd"]))
|
|
}
|
|
}
|
|
|
|
App.Init()
|