package main import ( "context" "fmt" "os" "path/filepath" "strings" "time" "wis-free-v3/internal/audio/media" "wis-free-v3/internal/audio/recorder" "wis-free-v3/internal/config" "wis-free-v3/internal/hotkey" "wis-free-v3/internal/logger" "wis-free-v3/internal/services/transcriber" "wis-free-v3/internal/services/whisper" "wis-free-v3/internal/system/startup" "wis-free-v3/internal/ui/overlay" "wis-free-v3/internal/ui/tray" "github.com/go-vgo/robotgo" "github.com/wailsapp/wails/v2/pkg/runtime" "golang.design/x/clipboard" ) // App struct type App struct { ctx context.Context audioRecorder *recorder.AudioRecorder hotkeyListener *hotkey.Listener transcriber *transcriber.Client config *config.Config overlay *overlay.Overlay recordingPath string isQuitting bool wasMediaPlaying bool whisperManager *whisper.Manager } // NewApp creates a new App application struct func NewApp() *App { return &App{} } // GetConfig returns the app configuration func (a *App) GetConfig() *config.Config { return a.config } // ShowSettings shows the settings window func (a *App) ShowSettings() { if a.ctx != nil { runtime.WindowShow(a.ctx) } } // startup is called when the app starts func (a *App) startup(ctx context.Context) { a.ctx = ctx // Initialize components a.startupHeadless() // Start system tray in a goroutine go tray.Start(a) } // beforeClose is called when the window is about to close func (a *App) beforeClose(ctx context.Context) (prevent bool) { if a.isQuitting { return false } runtime.WindowHide(ctx) return true } // Quit handles application exit from tray func (a *App) Quit() { a.isQuitting = true logger.Info("Application quitting...") logger.Close() runtime.Quit(a.ctx) } // StartRecording starts the audio recording func (a *App) StartRecording() { logger.Info("StartRecording triggered") // Pause media if playing a.wasMediaPlaying = media.PauseMedia() if a.wasMediaPlaying { logger.Info("Media paused for recording") } tray.UpdateStatus("Recording...") if a.overlay != nil { a.overlay.Show("Recording...") } if a.audioRecorder == nil { logger.Error("Recorder not initialized") tray.UpdateStatus("Ready") if a.overlay != nil { a.overlay.Hide() } return } // Save to a temporary file tempDir := os.TempDir() timestamp := time.Now().Format("20060102_150405") a.recordingPath = filepath.Join(tempDir, fmt.Sprintf("wis_recording_%s.wav", timestamp)) err := a.audioRecorder.Start(a.recordingPath) if err != nil { logger.Error("Failed to start recording with primary device: %v", err) // IDIOT-PROOFING: Fallback to default microphone if the selected one fails if a.config.MicrophoneDevice != nil { logger.Info("Attempting fallback to default microphone...") a.audioRecorder.SetDevice("") // Reset to default err = a.audioRecorder.Start(a.recordingPath) } if err != nil { logger.Error("Recording completely failed: %v", err) tray.UpdateStatus("Ready") if a.overlay != nil { a.overlay.Hide() } // Resume media if we paused it media.ResumeMedia(a.wasMediaPlaying) return } logger.Info("Fallback successful - using default microphone") } } // StopRecording stops the audio recording and triggers transcription func (a *App) StopRecording() { logger.Info("StopRecording triggered") // Resume media if it was playing before media.ResumeMedia(a.wasMediaPlaying) if a.wasMediaPlaying { logger.Info("Media resumed after recording") } if a.audioRecorder == nil { return } err := a.audioRecorder.Stop() if err != nil { logger.Error("Failed to stop recording: %v", err) return } // Transcribe in a goroutine to avoid blocking go a.processRecording() } // processRecording handles transcription and pasting func (a *App) processRecording() { if a.recordingPath == "" { logger.Error("No recording path set") tray.UpdateStatus("Ready") if a.overlay != nil { a.overlay.Hide() } return } logger.Info("Transcribing audio...") tray.UpdateStatus("Transcribing...") if a.overlay != nil { a.overlay.Show("Transcribing...") } var text string var err error // Check if using local whisper isLocal := strings.HasPrefix(a.config.WhisperModel, "local-") // IDIOT-PROOFING: Validate API key before attempting cloud transcription if !isLocal && !strings.HasPrefix(a.config.APIKey, "gsk_") { err = fmt.Errorf("invalid API key - must start with gsk_") } else if isLocal { // Use local whisper.cpp if a.whisperManager == nil { var mgrErr error a.whisperManager, mgrErr = whisper.NewManager() if mgrErr != nil { logger.Error("Failed to create whisper manager: %v", mgrErr) err = mgrErr } } if a.whisperManager != nil { text, err = a.whisperManager.Transcribe(a.recordingPath) } } else { // Use cloud API text, err = a.transcriber.TranscribeAudio(a.recordingPath, a.config.Language) } if err != nil { logger.Error("Transcription failed: %v", err) tray.UpdateStatus("Ready") if a.overlay != nil { a.overlay.Hide() } return } logger.Info("Transcribed: %s", text) // Refine text (optional) refinedText, err := a.transcriber.RefineText(text) if err != nil { logger.Error("Refinement failed: %v", err) // Fallback to original text refinedText = text } else { logger.Info("Refined: %s", refinedText) } // Save to history historyItem := config.HistoryItem{ Text: refinedText, Timestamp: time.Now().Format(time.RFC3339), } // Prepend to history a.config.History = append([]config.HistoryItem{historyItem}, a.config.History...) // Keep only last 50 items if len(a.config.History) > 50 { a.config.History = a.config.History[:50] } config.Save(a.config, "") // Copy to clipboard clipboard.Write(clipboard.FmtText, []byte(refinedText)) // Paste a.pasteText() // Clean up the recording file os.Remove(a.recordingPath) logger.Info("Processing complete!") tray.UpdateStatus("Ready") // Hide overlay after a short delay time.Sleep(1 * time.Second) if a.overlay != nil { a.overlay.Hide() } } // pasteText simulates Ctrl+V to paste from clipboard func (a *App) pasteText() { // Give a small delay for clipboard to update time.Sleep(100 * time.Millisecond) // Simulate Ctrl+V robotgo.KeyToggle("control", "down") robotgo.KeyTap("v") robotgo.KeyToggle("control", "up") } // GetSettings returns the current configuration func (a *App) GetSettings() map[string]interface{} { conf := make(map[string]interface{}) conf["api_key"] = a.config.APIKey conf["shortcut"] = a.config.Shortcut conf["whisper_model"] = a.config.WhisperModel conf["ai_model"] = a.config.AIModel conf["ai_prompt"] = a.config.AIPrompt conf["language"] = a.config.Language conf["microphone_device"] = a.config.MicrophoneDevice conf["history"] = a.config.History conf["startup"] = startup.IsInStartup() return conf } // SaveSettings updates the configuration func (a *App) SaveSettings(settings map[string]interface{}) string { if val, ok := settings["api_key"].(string); ok { a.config.APIKey = val } if val, ok := settings["shortcut"].(string); ok { // Validate shortcut before applying trigger, _ := hotkey.ParseShortcut(val) if len(trigger) == 0 { logger.Error("Invalid shortcut: %s (rejected)", val) return "Invalid shortcut - must have at least one modifier and a regular key" } a.config.Shortcut = val // Update existing listener with new shortcut (hot-swap) if a.hotkeyListener != nil { a.hotkeyListener.UpdateShortcut(val) } else { // Should not happen if app started correctly, but just in case a.hotkeyListener = hotkey.NewListener(val, a.StartRecording, a.StopRecording) a.hotkeyListener.Start() } } if val, ok := settings["whisper_model"].(string); ok { a.config.WhisperModel = val } if val, ok := settings["ai_model"].(string); ok { a.config.AIModel = val } if val, ok := settings["ai_prompt"].(string); ok { a.config.AIPrompt = val } if val, ok := settings["language"].(string); ok { a.config.Language = val } if val, ok := settings["microphone_device"]; ok { if val == nil { a.config.MicrophoneDevice = nil } else { // Handle float64 from JSON/JS if f, ok := val.(float64); ok { i := int(f) a.config.MicrophoneDevice = &i } } } // Save to file config.Save(a.config, "") // Re-init transcriber with new settings a.transcriber = transcriber.NewClient( a.config.APIKey, a.config.WhisperModel, a.config.AIModel, a.config.AIPrompt, ) logger.Info("Settings saved") return "Settings saved successfully" } // GetMicrophones returns available microphone devices func (a *App) GetMicrophones() []map[string]interface{} { var result []map[string]interface{} if a.audioRecorder != nil { mics, err := a.audioRecorder.GetMicrophones() if err != nil { logger.Error("Failed to enumerate microphones: %v", err) } else { for i, mic := range mics { result = append(result, map[string]interface{}{ "index": i - 1, // -1 for default, 0+ for specific devices "name": mic.Name, "id": mic.ID, }) } return result } } // Fallback to just default return []map[string]interface{}{ {"index": -1, "name": "System Default", "id": ""}, } } // ToggleStartup toggles Windows startup status func (a *App) ToggleStartup(enable bool) string { var err error if enable { err = startup.AddToStartup() } else { err = startup.RemoveFromStartup() } if err != nil { logger.Error("Startup toggle error: %v", err) return fmt.Sprintf("Error: %v", err) } return "Success" } // ClearHistory clears the transcription history func (a *App) ClearHistory() { a.config.History = []config.HistoryItem{} config.Save(a.config, "") logger.Info("History cleared") } // startupHeadless initializes the app without Wails context func (a *App) startupHeadless() { // Initialize Logger err := logger.Init() if err != nil { fmt.Printf("Failed to initialize logger: %v\n", err) } // Initialize clipboard err = clipboard.Init() if err != nil { logger.Error("Failed to initialize clipboard: %v", err) } // Load configuration configPath, err := config.GetConfigPath() if err != nil { logger.Error("Failed to get config path: %v", err) a.config = config.DefaultConfig() } else { a.config, err = config.Load(configPath) if err != nil { logger.Error("Failed to load config: %v", err) a.config = config.DefaultConfig() } } // Initialize heavy components in background for instant startup go func() { // Initialize transcriber a.transcriber = transcriber.NewClient( a.config.APIKey, a.config.WhisperModel, a.config.AIModel, a.config.AIPrompt, ) // Initialize whisper manager a.whisperManager, _ = whisper.NewManager() // Initialize Audio Recorder rec, err := recorder.NewRecorder() if err != nil { logger.Error("Error initializing recorder: %v", err) } else { a.audioRecorder = rec } // Initialize Overlay a.overlay = overlay.NewOverlay() // Connect volume feedback from recorder to overlay if a.audioRecorder != nil && a.overlay != nil { a.audioRecorder.OnVolume = func(level float64) { a.overlay.SetVolume(level) } } // Initialize Hotkey Listener a.hotkeyListener = hotkey.NewListener(a.config.Shortcut, a.StartRecording, a.StopRecording) a.hotkeyListener.Start() logger.Info("Background components initialized successfully!") }() logger.Info("Basic app components loaded, continuing startup...") } // Shutdown cleans up resources func (a *App) Shutdown(ctx context.Context) { if a.hotkeyListener != nil { a.hotkeyListener.Stop() } if a.audioRecorder != nil { a.audioRecorder.Cleanup() } if a.overlay != nil { a.overlay.Close() } logger.Close() } // Greet returns a greeting for the given name (for frontend testing) func (a *App) Greet(name string) string { return fmt.Sprintf("Hello %s, It's show time!", name) } // CheckOnline checks if internet connection is available func (a *App) CheckOnline() bool { return whisper.CheckOnline() } // IsWhisperInstalled checks if offline whisper is installed func (a *App) IsWhisperInstalled() bool { mgr, err := whisper.NewManager() if err != nil { return false } return mgr.IsInstalled() } // GetWhisperInfo returns information about installed whisper func (a *App) GetWhisperInfo() map[string]interface{} { mgr, err := whisper.NewManager() if err != nil { return map[string]interface{}{"installed": false} } if !mgr.IsInstalled() { return map[string]interface{}{"installed": false} } info, err := mgr.GetInstalledInfo() if err != nil { return map[string]interface{}{"installed": false} } return map[string]interface{}{ "installed": true, "model": info.Model, "version": info.Version, } } // InstallWhisper starts the whisper installation process func (a *App) InstallWhisper(model string) string { mgr, err := whisper.NewManager() if err != nil { return fmt.Sprintf("Error: %v", err) } if err := mgr.Install(model); err != nil { return fmt.Sprintf("Error: %v", err) } return "Installation started - check the terminal window" } // UninstallWhisper removes the whisper installation func (a *App) UninstallWhisper() string { mgr, err := whisper.NewManager() if err != nil { return fmt.Sprintf("Error: %v", err) } if err := mgr.Uninstall(); err != nil { return fmt.Sprintf("Error: %v", err) } return "Whisper uninstalled successfully" } // GetAvailableWhisperModels returns list of available whisper models func (a *App) GetAvailableWhisperModels() []map[string]string { var models []map[string]string for name, info := range whisper.Models { models = append(models, map[string]string{ "name": name, "size": info.Size, }) } return models }