package tui import ( "fmt" "strings" "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" "github.com/leonardotrapani/hyprvoice/internal/config" "github.com/leonardotrapani/hyprvoice/internal/provider" ) // ConfigureResult holds the configuration result from the TUI type ConfigureResult struct { Config *config.Config Cancelled bool } // ConfigSection represents a configuration section type ConfigSection string const ( SectionProviders ConfigSection = "providers" SectionTranscription ConfigSection = "transcription" SectionLLM ConfigSection = "llm" SectionKeywords ConfigSection = "keywords" SectionInjection ConfigSection = "injection" SectionNotifications ConfigSection = "notifications" SectionFullSetup ConfigSection = "full_setup" ) // Run starts the TUI configuration wizard func Run(existingConfig *config.Config) (*ConfigureResult, error) { // Detect if config has user changes (providers configured) if existingConfig != nil && hasUserChanges(existingConfig) { return runEditExisting(existingConfig) } return runFreshInstall(existingConfig) } // hasUserChanges detects if config has user modifications func hasUserChanges(cfg *config.Config) bool { // If providers map has entries, user has configured something if len(cfg.Providers) > 0 { return true } // If legacy api_key is set, user has configured something if cfg.Transcription.APIKey != "" { return true } return false } // runEditExisting runs the section-based edit flow for existing configs func runEditExisting(cfg *config.Config) (*ConfigureResult, error) { fmt.Println(Logo()) fmt.Println() fmt.Println(StyleMuted.Render("Configuration detected. Select sections to edit.")) fmt.Println() // Section picker sections, err := selectSections() if err != nil { return &ConfigureResult{Cancelled: true}, nil } if len(sections) == 0 { return &ConfigureResult{Cancelled: true}, nil } // Check if full setup requested for _, s := range sections { if s == SectionFullSetup { return runFreshInstall(cfg) } } // Track which providers are configured (for smart detection) configuredProviders := getConfiguredProviders(cfg) // Process each selected section for _, section := range sections { switch section { case SectionProviders: if err := editProviders(cfg); err != nil { return &ConfigureResult{Cancelled: true}, nil } configuredProviders = getConfiguredProviders(cfg) case SectionTranscription: var err error configuredProviders, err = editTranscription(cfg, configuredProviders) if err != nil { return &ConfigureResult{Cancelled: true}, nil } case SectionLLM: var err error configuredProviders, err = editLLM(cfg, configuredProviders) if err != nil { return &ConfigureResult{Cancelled: true}, nil } case SectionKeywords: keywords, err := inputKeywords(cfg.Keywords) if err != nil { return &ConfigureResult{Cancelled: true}, nil } cfg.Keywords = keywords case SectionInjection: backends, err := selectBackends(cfg.Injection.Backends) if err != nil { return &ConfigureResult{Cancelled: true}, nil } cfg.Injection.Backends = backends case SectionNotifications: enabled, err := configureNotifications(cfg.Notifications.Enabled) if err != nil { return &ConfigureResult{Cancelled: true}, nil } cfg.Notifications.Enabled = enabled } } // Summary and confirm confirmed, err := showSummary(cfg) if err != nil || !confirmed { return &ConfigureResult{Cancelled: true}, nil } return &ConfigureResult{Config: cfg, Cancelled: false}, nil } func selectSections() ([]ConfigSection, error) { options := []huh.Option[ConfigSection]{ huh.NewOption("Providers - API keys", SectionProviders), huh.NewOption("Transcription - speech-to-text settings", SectionTranscription), huh.NewOption("LLM - post-processing settings", SectionLLM), huh.NewOption("Keywords - spelling hints", SectionKeywords), huh.NewOption("Injection - text input backends", SectionInjection), huh.NewOption("Notifications - desktop alerts", SectionNotifications), huh.NewOption("Full Setup - reconfigure everything", SectionFullSetup), } var selected []ConfigSection form := huh.NewForm( huh.NewGroup( huh.NewMultiSelect[ConfigSection](). Title("What do you want to configure?"). Description("Select one or more sections to edit"). Options(options...). Value(&selected), ), ).WithTheme(getTheme()) if err := form.Run(); err != nil { return nil, err } return selected, nil } // getConfiguredProviders returns list of providers with API keys func getConfiguredProviders(cfg *config.Config) []string { var providers []string for name, pc := range cfg.Providers { if pc.APIKey != "" { providers = append(providers, name) } } return providers } // editProviders handles the providers section edit func editProviders(cfg *config.Config) error { // Show current providers with option to add/edit allProviders := []string{"openai", "groq", "mistral", "elevenlabs"} var options []huh.Option[string] for _, name := range allProviders { label := strings.Title(name) if _, exists := cfg.Providers[name]; exists && cfg.Providers[name].APIKey != "" { label += " (configured)" } switch name { case "openai": options = append(options, huh.NewOption(label+" - Whisper + GPT", name)) case "groq": options = append(options, huh.NewOption(label+" - Whisper + Llama", name)) case "mistral": options = append(options, huh.NewOption(label+" - Voxtral", name)) case "elevenlabs": options = append(options, huh.NewOption(label+" - Scribe", name)) } } var selected []string form := huh.NewForm( huh.NewGroup( huh.NewMultiSelect[string](). Title("Configure API keys for:"). Description("Select providers to add or update API keys"). Options(options...). Value(&selected), ), ).WithTheme(getTheme()) if err := form.Run(); err != nil { return err } // Input API keys for selected providers for _, providerName := range selected { apiKey, err := inputAPIKey(providerName) if err != nil { return err } if cfg.Providers == nil { cfg.Providers = make(map[string]config.ProviderConfig) } cfg.Providers[providerName] = config.ProviderConfig{APIKey: apiKey} } return nil } // editTranscription handles the transcription section edit with smart provider detection func editTranscription(cfg *config.Config, configuredProviders []string) ([]string, error) { // Build transcription options from configured providers var transcriptionOptions []huh.Option[string] for _, name := range configuredProviders { p := provider.GetProvider(name) if p != nil && p.SupportsTranscription() { switch name { case "openai": transcriptionOptions = append(transcriptionOptions, huh.NewOption("OpenAI Whisper", "openai")) case "groq": transcriptionOptions = append(transcriptionOptions, huh.NewOption("Groq Whisper (transcription)", "groq-transcription"), huh.NewOption("Groq Whisper (translate to English)", "groq-translation")) case "mistral": transcriptionOptions = append(transcriptionOptions, huh.NewOption("Mistral Voxtral", "mistral-transcription")) case "elevenlabs": transcriptionOptions = append(transcriptionOptions, huh.NewOption("ElevenLabs Scribe", "elevenlabs")) } } } // Add options for unconfigured providers (will prompt for key) unconfiguredOptions := getUnconfiguredTranscriptionOptions(configuredProviders) if len(unconfiguredOptions) > 0 { transcriptionOptions = append(transcriptionOptions, unconfiguredOptions...) } if len(transcriptionOptions) == 0 { return configuredProviders, fmt.Errorf("no transcription providers available") } // Set default to current provider or first option selectedProvider := cfg.Transcription.Provider if selectedProvider == "" && len(transcriptionOptions) > 0 { selectedProvider = transcriptionOptions[0].Value } providerForm := huh.NewForm( huh.NewGroup( huh.NewSelect[string](). Title("Transcription Provider"). Description("Choose which service to use for speech-to-text"). Options(transcriptionOptions...). Value(&selectedProvider), ), ).WithTheme(getTheme()) if err := providerForm.Run(); err != nil { return configuredProviders, err } // Smart detection: if provider not configured, prompt for API key configuredProviders = ensureProviderConfigured(cfg, selectedProvider, configuredProviders) cfg.Transcription.Provider = selectedProvider // Model selection modelOptions := getTranscriptionModelOptions(selectedProvider) selectedModel := cfg.Transcription.Model if selectedModel == "" && len(modelOptions) > 0 { selectedModel = modelOptions[0].Value } language := cfg.Transcription.Language modelForm := huh.NewForm( huh.NewGroup( huh.NewSelect[string](). Title("Transcription Model"). Options(modelOptions...). Value(&selectedModel), huh.NewInput(). Title("Language"). Description("ISO-639-1 code (e.g., 'en', 'es', 'fr') or empty for auto-detect"). Placeholder("auto-detect"). Value(&language), ), ).WithTheme(getTheme()) if err := modelForm.Run(); err != nil { return configuredProviders, err } cfg.Transcription.Model = selectedModel cfg.Transcription.Language = language return configuredProviders, nil } // editLLM handles the LLM section edit with smart provider detection func editLLM(cfg *config.Config, configuredProviders []string) ([]string, error) { // Check if any LLM-capable providers are configured var llmProviders []string for _, name := range configuredProviders { p := provider.GetProvider(name) if p != nil && p.SupportsLLM() { llmProviders = append(llmProviders, name) } } // Default post-processing postProcessing := cfg.LLM.PostProcessing if !postProcessing.RemoveStutters && !postProcessing.AddPunctuation && !postProcessing.FixGrammar && !postProcessing.RemoveFillerWords { postProcessing = config.LLMPostProcessingConfig{ RemoveStutters: true, AddPunctuation: true, FixGrammar: true, RemoveFillerWords: true, } } customPrompt := cfg.LLM.CustomPrompt // Ask if user wants LLM enableLLM := cfg.LLM.Enabled enableForm := huh.NewForm( huh.NewGroup( huh.NewConfirm(). Title("Enable LLM Post-Processing? (Recommended)"). Description("LLM improves transcription by fixing grammar, removing stutters, and cleaning up text"). Affirmative("Yes (Recommended)"). Negative("No"). Value(&enableLLM), ), ).WithTheme(getTheme()) if err := enableForm.Run(); err != nil { return configuredProviders, err } if !enableLLM { cfg.LLM.Enabled = false return configuredProviders, nil } // Build LLM provider options var llmOptions []huh.Option[string] for _, name := range llmProviders { switch name { case "openai": llmOptions = append(llmOptions, huh.NewOption("OpenAI GPT", "openai")) case "groq": llmOptions = append(llmOptions, huh.NewOption("Groq Llama (fast)", "groq")) } } // Add unconfigured LLM providers unconfiguredLLM := getUnconfiguredLLMOptions(configuredProviders) if len(unconfiguredLLM) > 0 { llmOptions = append(llmOptions, unconfiguredLLM...) } if len(llmOptions) == 0 { fmt.Println(StyleError.Render("No LLM providers available. Please configure OpenAI or Groq first.")) cfg.LLM.Enabled = false return configuredProviders, nil } selectedProvider := cfg.LLM.Provider if selectedProvider == "" && len(llmOptions) > 0 { selectedProvider = llmOptions[0].Value } providerForm := huh.NewForm( huh.NewGroup( huh.NewSelect[string](). Title("LLM Provider"). Description("Choose which service to use for text post-processing"). Options(llmOptions...). Value(&selectedProvider), ), ).WithTheme(getTheme()) if err := providerForm.Run(); err != nil { return configuredProviders, err } // Smart detection: if provider not configured, prompt for API key configuredProviders = ensureProviderConfigured(cfg, selectedProvider, configuredProviders) cfg.LLM.Provider = selectedProvider // Model selection modelOptions := getLLMModelOptions(selectedProvider) selectedModel := cfg.LLM.Model if selectedModel == "" && len(modelOptions) > 0 { selectedModel = modelOptions[0].Value } modelForm := huh.NewForm( huh.NewGroup( huh.NewSelect[string](). Title("LLM Model"). Options(modelOptions...). Value(&selectedModel), ), ).WithTheme(getTheme()) if err := modelForm.Run(); err != nil { return configuredProviders, err } cfg.LLM.Model = selectedModel // Post-processing options ppForm := huh.NewForm( huh.NewGroup( huh.NewConfirm(). Title("Remove stutters"). Description("Remove repeated words like 'I I I think'"). Value(&postProcessing.RemoveStutters), huh.NewConfirm(). Title("Add punctuation"). Description("Add proper punctuation to text"). Value(&postProcessing.AddPunctuation), huh.NewConfirm(). Title("Fix grammar"). Description("Correct grammatical errors"). Value(&postProcessing.FixGrammar), huh.NewConfirm(). Title("Remove filler words"). Description("Remove 'um', 'uh', 'like', etc."). Value(&postProcessing.RemoveFillerWords), ), ).WithTheme(getTheme()) if err := ppForm.Run(); err != nil { return configuredProviders, err } cfg.LLM.PostProcessing = postProcessing // Custom prompt enableCustomPrompt := customPrompt.Enabled customPromptText := customPrompt.Prompt customForm := huh.NewForm( huh.NewGroup( huh.NewConfirm(). Title("Add custom prompt?"). Description("Add extra instructions for the LLM"). Value(&enableCustomPrompt), ), ).WithTheme(getTheme()) if err := customForm.Run(); err != nil { return configuredProviders, err } if enableCustomPrompt { promptForm := huh.NewForm( huh.NewGroup( huh.NewText(). Title("Custom Prompt"). Description("Additional instructions (e.g., 'Format as bullet points')"). Value(&customPromptText). CharLimit(500), ), ).WithTheme(getTheme()) if err := promptForm.Run(); err != nil { return configuredProviders, err } cfg.LLM.CustomPrompt.Enabled = true cfg.LLM.CustomPrompt.Prompt = customPromptText } else { cfg.LLM.CustomPrompt.Enabled = false } cfg.LLM.Enabled = true return configuredProviders, nil } // getUnconfiguredTranscriptionOptions returns options for providers not yet configured func getUnconfiguredTranscriptionOptions(configuredProviders []string) []huh.Option[string] { configured := make(map[string]bool) for _, p := range configuredProviders { configured[p] = true } var options []huh.Option[string] if !configured["openai"] { options = append(options, huh.NewOption("OpenAI Whisper (needs API key)", "openai")) } if !configured["groq"] { options = append(options, huh.NewOption("Groq Whisper transcription (needs API key)", "groq-transcription"), huh.NewOption("Groq Whisper translation (needs API key)", "groq-translation")) } if !configured["mistral"] { options = append(options, huh.NewOption("Mistral Voxtral (needs API key)", "mistral-transcription")) } if !configured["elevenlabs"] { options = append(options, huh.NewOption("ElevenLabs Scribe (needs API key)", "elevenlabs")) } return options } // getUnconfiguredLLMOptions returns options for LLM providers not yet configured func getUnconfiguredLLMOptions(configuredProviders []string) []huh.Option[string] { configured := make(map[string]bool) for _, p := range configuredProviders { configured[p] = true } var options []huh.Option[string] if !configured["openai"] { options = append(options, huh.NewOption("OpenAI GPT (needs API key)", "openai")) } if !configured["groq"] { options = append(options, huh.NewOption("Groq Llama (needs API key)", "groq")) } return options } // ensureProviderConfigured prompts for API key if provider not configured func ensureProviderConfigured(cfg *config.Config, selectedProvider string, configuredProviders []string) []string { // Map transcription provider to actual provider name providerName := selectedProvider switch selectedProvider { case "groq-transcription", "groq-translation": providerName = "groq" case "mistral-transcription": providerName = "mistral" } // Check if already configured for _, p := range configuredProviders { if p == providerName { return configuredProviders } } // Not configured - prompt for API key fmt.Println() fmt.Println(StyleMuted.Render(fmt.Sprintf("%s not configured. Please enter API key.", strings.Title(providerName)))) apiKey, err := inputAPIKey(providerName) if err != nil { return configuredProviders } if cfg.Providers == nil { cfg.Providers = make(map[string]config.ProviderConfig) } cfg.Providers[providerName] = config.ProviderConfig{APIKey: apiKey} return append(configuredProviders, providerName) } // runFreshInstall runs the full configuration wizard for fresh installs func runFreshInstall(cfg *config.Config) (*ConfigureResult, error) { // Welcome screen fmt.Println(Logo()) fmt.Println() fmt.Println(StyleMuted.Render("Voice-powered typing for Wayland/Hyprland")) fmt.Println() // Step 1: Provider selection selectedProviders, err := selectProviders() if err != nil { return &ConfigureResult{Cancelled: true}, nil } if len(selectedProviders) == 0 { return &ConfigureResult{Cancelled: true}, fmt.Errorf("no providers selected") } // Initialize providers map if cfg.Providers == nil { cfg.Providers = make(map[string]config.ProviderConfig) } // Step 2: API keys for selected providers for _, providerName := range selectedProviders { apiKey, err := inputAPIKey(providerName) if err != nil { return &ConfigureResult{Cancelled: true}, nil } cfg.Providers[providerName] = config.ProviderConfig{APIKey: apiKey} } // Step 3: Transcription configuration transcriptionProvider, transcriptionModel, language, err := configureTranscription(selectedProviders, cfg) if err != nil { return &ConfigureResult{Cancelled: true}, nil } cfg.Transcription.Provider = transcriptionProvider cfg.Transcription.Model = transcriptionModel cfg.Transcription.Language = language // Step 4: LLM configuration llmEnabled, llmProvider, llmModel, postProcessing, customPrompt, err := configureLLM(selectedProviders, cfg) if err != nil { return &ConfigureResult{Cancelled: true}, nil } cfg.LLM.Enabled = llmEnabled cfg.LLM.Provider = llmProvider cfg.LLM.Model = llmModel cfg.LLM.PostProcessing = postProcessing cfg.LLM.CustomPrompt = customPrompt // Step 5: Keywords keywords, err := inputKeywords(cfg.Keywords) if err != nil { return &ConfigureResult{Cancelled: true}, nil } cfg.Keywords = keywords // Step 6: Injection backends backends, err := selectBackends(cfg.Injection.Backends) if err != nil { return &ConfigureResult{Cancelled: true}, nil } cfg.Injection.Backends = backends // Step 7: Notifications notificationsEnabled, err := configureNotifications(cfg.Notifications.Enabled) if err != nil { return &ConfigureResult{Cancelled: true}, nil } cfg.Notifications.Enabled = notificationsEnabled // Step 8: Summary and confirm confirmed, err := showSummary(cfg) if err != nil || !confirmed { return &ConfigureResult{Cancelled: true}, nil } return &ConfigureResult{Config: cfg, Cancelled: false}, nil } func selectProviders() ([]string, error) { allProviders := []string{"openai", "groq", "mistral", "elevenlabs"} options := []huh.Option[string]{ huh.NewOption("OpenAI - Whisper transcription + GPT for LLM", "openai"), huh.NewOption("Groq - Fast Whisper transcription + Llama for LLM", "groq"), huh.NewOption("Mistral - Voxtral transcription (European languages)", "mistral"), huh.NewOption("ElevenLabs - Scribe transcription (99 languages)", "elevenlabs"), } var selected []string form := huh.NewForm( huh.NewGroup( huh.NewMultiSelect[string](). Title("Which providers do you want to configure?"). Description("Select all providers you have API keys for"). Options(options...). Value(&selected), ), ).WithTheme(getTheme()) if err := form.Run(); err != nil { return nil, err } // Validate selected providers exist valid := make([]string, 0) for _, s := range selected { for _, p := range allProviders { if s == p { valid = append(valid, s) break } } } return valid, nil } func inputAPIKey(providerName string) (string, error) { p := provider.GetProvider(providerName) displayName := strings.Title(providerName) if p != nil { displayName = strings.Title(p.Name()) } var apiKey string form := huh.NewForm( huh.NewGroup( huh.NewInput(). Title(fmt.Sprintf("%s API Key", displayName)). Description(fmt.Sprintf("Enter your %s API key", displayName)). EchoMode(huh.EchoModePassword). Value(&apiKey). Validate(func(s string) error { if s == "" { return fmt.Errorf("API key is required") } if p != nil && !p.ValidateAPIKey(s) { return fmt.Errorf("invalid API key format for %s", displayName) } return nil }), ), ).WithTheme(getTheme()) if err := form.Run(); err != nil { return "", err } return apiKey, nil } func configureTranscription(configuredProviders []string, cfg *config.Config) (string, string, string, error) { // Filter to only transcription-capable configured providers var transcriptionOptions []huh.Option[string] for _, name := range configuredProviders { p := provider.GetProvider(name) if p != nil && p.SupportsTranscription() { // Map provider name to transcription provider name switch name { case "openai": transcriptionOptions = append(transcriptionOptions, huh.NewOption("OpenAI Whisper", "openai")) case "groq": transcriptionOptions = append(transcriptionOptions, huh.NewOption("Groq Whisper (transcription)", "groq-transcription"), huh.NewOption("Groq Whisper (translate to English)", "groq-translation")) case "mistral": transcriptionOptions = append(transcriptionOptions, huh.NewOption("Mistral Voxtral", "mistral-transcription")) case "elevenlabs": transcriptionOptions = append(transcriptionOptions, huh.NewOption("ElevenLabs Scribe", "elevenlabs")) } } } if len(transcriptionOptions) == 0 { return "", "", "", fmt.Errorf("no transcription-capable providers configured") } var selectedProvider string if cfg.Transcription.Provider != "" { selectedProvider = cfg.Transcription.Provider } else if len(transcriptionOptions) > 0 { selectedProvider = transcriptionOptions[0].Value } providerForm := huh.NewForm( huh.NewGroup( huh.NewSelect[string](). Title("Transcription Provider"). Description("Choose which service to use for speech-to-text"). Options(transcriptionOptions...). Value(&selectedProvider), ), ).WithTheme(getTheme()) if err := providerForm.Run(); err != nil { return "", "", "", err } // Get model options for selected provider modelOptions := getTranscriptionModelOptions(selectedProvider) var selectedModel string if cfg.Transcription.Model != "" { selectedModel = cfg.Transcription.Model } else if len(modelOptions) > 0 { selectedModel = modelOptions[0].Value } var language string if cfg.Transcription.Language != "" { language = cfg.Transcription.Language } modelForm := huh.NewForm( huh.NewGroup( huh.NewSelect[string](). Title("Transcription Model"). Options(modelOptions...). Value(&selectedModel), huh.NewInput(). Title("Language"). Description("ISO-639-1 code (e.g., 'en', 'es', 'fr') or empty for auto-detect"). Placeholder("auto-detect"). Value(&language), ), ).WithTheme(getTheme()) if err := modelForm.Run(); err != nil { return "", "", "", err } return selectedProvider, selectedModel, language, nil } func getTranscriptionModelOptions(provider string) []huh.Option[string] { switch provider { case "openai": return []huh.Option[string]{ huh.NewOption("whisper-1", "whisper-1"), } case "groq-transcription": return []huh.Option[string]{ huh.NewOption("whisper-large-v3-turbo (faster)", "whisper-large-v3-turbo"), huh.NewOption("whisper-large-v3 (standard)", "whisper-large-v3"), } case "groq-translation": return []huh.Option[string]{ huh.NewOption("whisper-large-v3 (only option)", "whisper-large-v3"), } case "mistral-transcription": return []huh.Option[string]{ huh.NewOption("voxtral-mini-latest (recommended)", "voxtral-mini-latest"), huh.NewOption("voxtral-mini-2507", "voxtral-mini-2507"), } case "elevenlabs": return []huh.Option[string]{ huh.NewOption("scribe_v1 (99 languages, best accuracy)", "scribe_v1"), huh.NewOption("scribe_v2 (real-time, lower latency)", "scribe_v2"), } default: return []huh.Option[string]{} } } func configureLLM(configuredProviders []string, cfg *config.Config) (bool, string, string, config.LLMPostProcessingConfig, config.LLMCustomPromptConfig, error) { // Filter to only LLM-capable configured providers var llmProviders []string for _, name := range configuredProviders { p := provider.GetProvider(name) if p != nil && p.SupportsLLM() { llmProviders = append(llmProviders, name) } } // Default values postProcessing := config.LLMPostProcessingConfig{ RemoveStutters: true, AddPunctuation: true, FixGrammar: true, RemoveFillerWords: true, } customPrompt := config.LLMCustomPromptConfig{ Enabled: false, Prompt: "", } // If no LLM providers configured, skip LLM config if len(llmProviders) == 0 { return false, "", "", postProcessing, customPrompt, nil } // Ask if user wants LLM post-processing var enableLLM bool = true enableForm := huh.NewForm( huh.NewGroup( huh.NewConfirm(). Title("Enable LLM Post-Processing? (Recommended)"). Description("LLM improves transcription by fixing grammar, removing stutters, and cleaning up text"). Affirmative("Yes (Recommended)"). Negative("No"). Value(&enableLLM), ), ).WithTheme(getTheme()) if err := enableForm.Run(); err != nil { return false, "", "", postProcessing, customPrompt, err } if !enableLLM { return false, "", "", postProcessing, customPrompt, nil } // LLM provider selection var llmOptions []huh.Option[string] for _, name := range llmProviders { p := provider.GetProvider(name) if p != nil { switch name { case "openai": llmOptions = append(llmOptions, huh.NewOption("OpenAI GPT", "openai")) case "groq": llmOptions = append(llmOptions, huh.NewOption("Groq Llama (fast)", "groq")) } } } var selectedProvider string if cfg.LLM.Provider != "" { selectedProvider = cfg.LLM.Provider } else if len(llmOptions) > 0 { selectedProvider = llmOptions[0].Value } providerForm := huh.NewForm( huh.NewGroup( huh.NewSelect[string](). Title("LLM Provider"). Description("Choose which service to use for text post-processing"). Options(llmOptions...). Value(&selectedProvider), ), ).WithTheme(getTheme()) if err := providerForm.Run(); err != nil { return false, "", "", postProcessing, customPrompt, err } // Model selection modelOptions := getLLMModelOptions(selectedProvider) var selectedModel string if cfg.LLM.Model != "" { selectedModel = cfg.LLM.Model } else if len(modelOptions) > 0 { selectedModel = modelOptions[0].Value } modelForm := huh.NewForm( huh.NewGroup( huh.NewSelect[string](). Title("LLM Model"). Options(modelOptions...). Value(&selectedModel), ), ).WithTheme(getTheme()) if err := modelForm.Run(); err != nil { return false, "", "", postProcessing, customPrompt, err } // Post-processing options if cfg.LLM.PostProcessing.RemoveStutters || cfg.LLM.PostProcessing.AddPunctuation || cfg.LLM.PostProcessing.FixGrammar || cfg.LLM.PostProcessing.RemoveFillerWords { postProcessing = cfg.LLM.PostProcessing } ppForm := huh.NewForm( huh.NewGroup( huh.NewConfirm(). Title("Remove stutters"). Description("Remove repeated words like 'I I I think'"). Value(&postProcessing.RemoveStutters), huh.NewConfirm(). Title("Add punctuation"). Description("Add proper punctuation to text"). Value(&postProcessing.AddPunctuation), huh.NewConfirm(). Title("Fix grammar"). Description("Correct grammatical errors"). Value(&postProcessing.FixGrammar), huh.NewConfirm(). Title("Remove filler words"). Description("Remove 'um', 'uh', 'like', etc."). Value(&postProcessing.RemoveFillerWords), ), ).WithTheme(getTheme()) if err := ppForm.Run(); err != nil { return false, "", "", postProcessing, customPrompt, err } // Custom prompt var enableCustomPrompt bool var customPromptText string if cfg.LLM.CustomPrompt.Enabled { enableCustomPrompt = true customPromptText = cfg.LLM.CustomPrompt.Prompt } customForm := huh.NewForm( huh.NewGroup( huh.NewConfirm(). Title("Add custom prompt?"). Description("Add extra instructions for the LLM"). Value(&enableCustomPrompt), ), ).WithTheme(getTheme()) if err := customForm.Run(); err != nil { return false, "", "", postProcessing, customPrompt, err } if enableCustomPrompt { promptForm := huh.NewForm( huh.NewGroup( huh.NewText(). Title("Custom Prompt"). Description("Additional instructions (e.g., 'Format as bullet points')"). Value(&customPromptText). CharLimit(500), ), ).WithTheme(getTheme()) if err := promptForm.Run(); err != nil { return false, "", "", postProcessing, customPrompt, err } customPrompt.Enabled = true customPrompt.Prompt = customPromptText } return true, selectedProvider, selectedModel, postProcessing, customPrompt, nil } func getLLMModelOptions(provider string) []huh.Option[string] { switch provider { case "openai": return []huh.Option[string]{ huh.NewOption("gpt-4o-mini (recommended)", "gpt-4o-mini"), huh.NewOption("gpt-4o", "gpt-4o"), huh.NewOption("gpt-4-turbo", "gpt-4-turbo"), huh.NewOption("gpt-3.5-turbo", "gpt-3.5-turbo"), } case "groq": return []huh.Option[string]{ huh.NewOption("llama-3.3-70b-versatile (recommended)", "llama-3.3-70b-versatile"), huh.NewOption("llama-3.1-8b-instant (faster)", "llama-3.1-8b-instant"), huh.NewOption("mixtral-8x7b-32768", "mixtral-8x7b-32768"), } default: return []huh.Option[string]{} } } func inputKeywords(existingKeywords []string) ([]string, error) { var keywordsInput string if len(existingKeywords) > 0 { keywordsInput = strings.Join(existingKeywords, ", ") } form := huh.NewForm( huh.NewGroup( huh.NewInput(). Title("Keywords"). Description("Comma-separated words to help with spelling (names, technical terms, etc.)"). Placeholder("e.g., Kubernetes, PostgreSQL, John Smith"). Value(&keywordsInput), ), ).WithTheme(getTheme()) if err := form.Run(); err != nil { return nil, err } // Parse keywords if keywordsInput == "" { return nil, nil } parts := strings.Split(keywordsInput, ",") keywords := make([]string, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(p) if p != "" { keywords = append(keywords, p) } } return keywords, nil } func selectBackends(existingBackends []string) ([]string, error) { options := []huh.Option[string]{ huh.NewOption("ydotool - Best for Chromium/Electron (needs ydotoold)", "ydotool"), huh.NewOption("wtype - Native Wayland typing", "wtype"), huh.NewOption("clipboard - Copy to clipboard only", "clipboard"), } var selected []string if len(existingBackends) > 0 { selected = existingBackends } else { selected = []string{"ydotool", "wtype", "clipboard"} } form := huh.NewForm( huh.NewGroup( huh.NewMultiSelect[string](). Title("Text Injection Backends"). Description("Backends are tried in order until one succeeds (fallback chain)"). Options(options...). Value(&selected), ), ).WithTheme(getTheme()) if err := form.Run(); err != nil { return nil, err } if len(selected) == 0 { return nil, fmt.Errorf("at least one backend required") } return selected, nil } func configureNotifications(existingEnabled bool) (bool, error) { enabled := existingEnabled form := huh.NewForm( huh.NewGroup( huh.NewConfirm(). Title("Enable desktop notifications?"). Description("Show notifications for recording status changes"). Value(&enabled), ), ).WithTheme(getTheme()) if err := form.Run(); err != nil { return false, err } return enabled, nil } func showSummary(cfg *config.Config) (bool, error) { fmt.Println() fmt.Println(StyleHeader.Render("Configuration Summary")) fmt.Println() // Providers var providers []string for name := range cfg.Providers { providers = append(providers, name) } fmt.Printf(" %s %s\n", StyleLabel.Render("Providers:"), strings.Join(providers, ", ")) // Transcription fmt.Printf(" %s %s (%s)\n", StyleLabel.Render("Transcription:"), cfg.Transcription.Provider, cfg.Transcription.Model) if cfg.Transcription.Language != "" { fmt.Printf(" %s %s\n", StyleLabel.Render("Language:"), cfg.Transcription.Language) } // LLM if cfg.LLM.Enabled { fmt.Printf(" %s %s (%s)\n", StyleLabel.Render("LLM:"), cfg.LLM.Provider, cfg.LLM.Model) var ppOpts []string if cfg.LLM.PostProcessing.RemoveStutters { ppOpts = append(ppOpts, "remove stutters") } if cfg.LLM.PostProcessing.AddPunctuation { ppOpts = append(ppOpts, "add punctuation") } if cfg.LLM.PostProcessing.FixGrammar { ppOpts = append(ppOpts, "fix grammar") } if cfg.LLM.PostProcessing.RemoveFillerWords { ppOpts = append(ppOpts, "remove fillers") } if len(ppOpts) > 0 { fmt.Printf(" %s %s\n", StyleLabel.Render("Post-processing:"), strings.Join(ppOpts, ", ")) } } else { fmt.Printf(" %s disabled\n", StyleLabel.Render("LLM:")) } // Keywords if len(cfg.Keywords) > 0 { fmt.Printf(" %s %s\n", StyleLabel.Render("Keywords:"), strings.Join(cfg.Keywords, ", ")) } // Backends fmt.Printf(" %s %s\n", StyleLabel.Render("Backends:"), strings.Join(cfg.Injection.Backends, " -> ")) // Notifications if cfg.Notifications.Enabled { fmt.Printf(" %s enabled\n", StyleLabel.Render("Notifications:")) } else { fmt.Printf(" %s disabled\n", StyleLabel.Render("Notifications:")) } fmt.Println() var confirmed bool form := huh.NewForm( huh.NewGroup( huh.NewConfirm(). Title("Save this configuration?"). Affirmative("Save"). Negative("Cancel"). Value(&confirmed), ), ).WithTheme(getTheme()) if err := form.Run(); err != nil { return false, err } return confirmed, nil } func getTheme() *huh.Theme { t := huh.ThemeBase() // Primary colors t.Focused.Title = lipgloss.NewStyle().Foreground(ColorPrimary).Bold(true) t.Focused.Description = lipgloss.NewStyle().Foreground(ColorMuted) t.Focused.Base = lipgloss.NewStyle().BorderForeground(ColorPrimary) t.Focused.SelectedOption = lipgloss.NewStyle().Foreground(ColorSecondary) t.Focused.UnselectedOption = lipgloss.NewStyle().Foreground(ColorText) // Blurred (unfocused) t.Blurred.Title = lipgloss.NewStyle().Foreground(ColorMuted) t.Blurred.Description = lipgloss.NewStyle().Foreground(ColorSubtle) return t }