summaryrefslogtreecommitdiff
path: root/internal/appconfig/config.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/appconfig/config.go')
-rw-r--r--internal/appconfig/config.go148
1 files changed, 137 insertions, 11 deletions
diff --git a/internal/appconfig/config.go b/internal/appconfig/config.go
index 2c4cee3..b912271 100644
--- a/internal/appconfig/config.go
+++ b/internal/appconfig/config.go
@@ -85,6 +85,22 @@ type App struct {
// CLI
PromptCLIDefaultSystem string `json:"-" toml:"-"`
PromptCLIExplainSystem string `json:"-" toml:"-"`
+
+ // Custom code actions and tmux integration
+ CustomActions []CustomAction `json:"-" toml:"-"`
+ TmuxCustomMenuHotkey string `json:"-" toml:"-"`
+}
+
+// CustomAction describes a user-defined code action.
+type CustomAction struct {
+ ID string
+ Title string
+ Kind string // optional; default "refactor"
+ Scope string // "selection" (default) | "diagnostics"
+ Hotkey string // optional, used by tmux submenu
+ Instruction string // optional; if set and User is empty, use global rewrite templates
+ System string // optional; used only when User is set
+ User string // optional; if set, render with available vars
}
// Constructor: defaults for App (kept first among functions)
@@ -181,6 +197,7 @@ type fileConfig struct {
Copilot sectionCopilot `toml:"copilot"`
Ollama sectionOllama `toml:"ollama"`
Prompts sectionPrompts `toml:"prompts"`
+ Tmux sectionTmux `toml:"tmux"`
}
type sectionGeneral struct {
@@ -260,16 +277,17 @@ type sectionPromptsChat struct {
}
type sectionPromptsCodeAction struct {
- RewriteSystem string `toml:"rewrite_system"`
- DiagnosticsSystem string `toml:"diagnostics_system"`
- DocumentSystem string `toml:"document_system"`
- RewriteUser string `toml:"rewrite_user"`
- DiagnosticsUser string `toml:"diagnostics_user"`
- DocumentUser string `toml:"document_user"`
- GoTestSystem string `toml:"go_test_system"`
- GoTestUser string `toml:"go_test_user"`
- SimplifySystem string `toml:"simplify_system"`
- SimplifyUser string `toml:"simplify_user"`
+ RewriteSystem string `toml:"rewrite_system"`
+ DiagnosticsSystem string `toml:"diagnostics_system"`
+ DocumentSystem string `toml:"document_system"`
+ RewriteUser string `toml:"rewrite_user"`
+ DiagnosticsUser string `toml:"diagnostics_user"`
+ DocumentUser string `toml:"document_user"`
+ GoTestSystem string `toml:"go_test_system"`
+ GoTestUser string `toml:"go_test_user"`
+ SimplifySystem string `toml:"simplify_system"`
+ SimplifyUser string `toml:"simplify_user"`
+ Custom []sectionCustomAction `toml:"custom"`
}
type sectionPromptsCLI struct {
@@ -281,6 +299,21 @@ type sectionPromptsProviderNative struct {
Completion string `toml:"completion"`
}
+type sectionCustomAction struct {
+ ID string `toml:"id"`
+ Title string `toml:"title"`
+ Kind string `toml:"kind"`
+ Scope string `toml:"scope"`
+ Hotkey string `toml:"hotkey"`
+ Instruction string `toml:"instruction"`
+ System string `toml:"system"`
+ User string `toml:"user"`
+}
+
+type sectionTmux struct {
+ CustomMenuHotkey string `toml:"custom_menu_hotkey"`
+}
+
func (fc *fileConfig) toApp() App {
out := App{}
@@ -393,7 +426,17 @@ func (fc *fileConfig) toApp() App {
out.PromptChatSystem = fc.Prompts.Chat.System
}
// code action
- if (fc.Prompts.CodeAction != sectionPromptsCodeAction{}) {
+ if strings.TrimSpace(fc.Prompts.CodeAction.RewriteSystem) != "" ||
+ strings.TrimSpace(fc.Prompts.CodeAction.DiagnosticsSystem) != "" ||
+ strings.TrimSpace(fc.Prompts.CodeAction.DocumentSystem) != "" ||
+ strings.TrimSpace(fc.Prompts.CodeAction.RewriteUser) != "" ||
+ strings.TrimSpace(fc.Prompts.CodeAction.DiagnosticsUser) != "" ||
+ strings.TrimSpace(fc.Prompts.CodeAction.DocumentUser) != "" ||
+ strings.TrimSpace(fc.Prompts.CodeAction.GoTestSystem) != "" ||
+ strings.TrimSpace(fc.Prompts.CodeAction.GoTestUser) != "" ||
+ strings.TrimSpace(fc.Prompts.CodeAction.SimplifySystem) != "" ||
+ strings.TrimSpace(fc.Prompts.CodeAction.SimplifyUser) != "" ||
+ len(fc.Prompts.CodeAction.Custom) > 0 {
if strings.TrimSpace(fc.Prompts.CodeAction.RewriteSystem) != "" {
out.PromptCodeActionRewriteSystem = fc.Prompts.CodeAction.RewriteSystem
}
@@ -424,6 +467,20 @@ func (fc *fileConfig) toApp() App {
if strings.TrimSpace(fc.Prompts.CodeAction.SimplifyUser) != "" {
out.PromptCodeActionSimplifyUser = fc.Prompts.CodeAction.SimplifyUser
}
+ if len(fc.Prompts.CodeAction.Custom) > 0 {
+ for _, ca := range fc.Prompts.CodeAction.Custom {
+ out.CustomActions = append(out.CustomActions, CustomAction{
+ ID: strings.TrimSpace(ca.ID),
+ Title: strings.TrimSpace(ca.Title),
+ Kind: strings.TrimSpace(ca.Kind),
+ Scope: strings.ToLower(strings.TrimSpace(ca.Scope)),
+ Hotkey: strings.TrimSpace(ca.Hotkey),
+ Instruction: ca.Instruction,
+ System: ca.System,
+ User: ca.User,
+ })
+ }
+ }
}
// cli
if (fc.Prompts.CLI != sectionPromptsCLI{}) {
@@ -439,6 +496,11 @@ func (fc *fileConfig) toApp() App {
out.PromptNativeCompletion = fc.Prompts.ProviderNative.Completion
}
+ // tmux
+ if (fc.Tmux != sectionTmux{}) {
+ out.TmuxCustomMenuHotkey = strings.TrimSpace(fc.Tmux.CustomMenuHotkey)
+ }
+
return out
}
@@ -639,6 +701,70 @@ func (a *App) mergePrompts(other *App) {
if strings.TrimSpace(other.PromptCLIExplainSystem) != "" {
a.PromptCLIExplainSystem = other.PromptCLIExplainSystem
}
+ // Custom actions
+ if len(other.CustomActions) > 0 {
+ a.CustomActions = append([]CustomAction{}, other.CustomActions...)
+ }
+ if strings.TrimSpace(other.TmuxCustomMenuHotkey) != "" {
+ a.TmuxCustomMenuHotkey = other.TmuxCustomMenuHotkey
+ }
+}
+
+// Validate checks custom actions and tmux settings for duplicates and consistency.
+func (a App) Validate() error {
+ // Normalize and check duplicates for IDs and hotkeys
+ seenID := make(map[string]struct{})
+ seenHK := make(map[string]struct{})
+ for _, ca := range a.CustomActions {
+ id := strings.ToLower(strings.TrimSpace(ca.ID))
+ if id == "" {
+ return fmt.Errorf("config: custom action missing required field id")
+ }
+ if _, ok := seenID[id]; ok {
+ return fmt.Errorf("config: duplicate custom action id: %s", ca.ID)
+ }
+ seenID[id] = struct{}{}
+ if strings.TrimSpace(ca.Title) == "" {
+ return fmt.Errorf("config: custom action %s missing required field title", ca.ID)
+ }
+ // Validate scope
+ scope := strings.TrimSpace(ca.Scope)
+ if scope != "" && scope != "selection" && scope != "diagnostics" {
+ return fmt.Errorf("config: custom action %s has invalid scope: %s", ca.ID, ca.Scope)
+ }
+ // Instruction vs user
+ hasInstr := strings.TrimSpace(ca.Instruction) != ""
+ hasUser := strings.TrimSpace(ca.User) != ""
+ if hasInstr && hasUser {
+ return fmt.Errorf("config: custom action %s must set either instruction or user, not both", ca.ID)
+ }
+ if !hasInstr && !hasUser {
+ return fmt.Errorf("config: custom action %s requires instruction or user", ca.ID)
+ }
+ // Hotkey unique (case-insensitive), one rune if provided
+ if hk := strings.TrimSpace(ca.Hotkey); hk != "" {
+ if []rune(hk) == nil || len([]rune(hk)) != 1 {
+ return fmt.Errorf("config: custom action %s hotkey must be a single character", ca.ID)
+ }
+ lhk := strings.ToLower(hk)
+ if _, ok := seenHK[lhk]; ok {
+ return fmt.Errorf("config: duplicate custom action hotkey: %s", hk)
+ }
+ seenHK[lhk] = struct{}{}
+ }
+ }
+ // Tmux custom menu hotkey validation
+ if hk := strings.TrimSpace(a.TmuxCustomMenuHotkey); hk != "" {
+ if len([]rune(hk)) != 1 {
+ return fmt.Errorf("config: invalid tmux.custom_menu_hotkey: %s", hk)
+ }
+ // built-in hotkeys in tmux TUI: r,i,c,t,p,s
+ switch strings.ToLower(hk) {
+ case "r", "i", "c", "t", "p", "s":
+ return fmt.Errorf("config: invalid tmux.custom_menu_hotkey: %s (clashes with built-in)", hk)
+ }
+ }
+ return nil
}
// mergeProviderFields merges per-provider configuration.