package ui import ( "errors" "gioui.org/app" "git.wxccs.org/iceking2nd/winauth-go/internal/global" "git.wxccs.org/iceking2nd/winauth-go/internal/hotkey" "git.wxccs.org/iceking2nd/winauth-go/internal/win32" ) // registerAllHotkeys walks every entry and registers its configured // hotkey. Failure on a single row is logged and skipped so the rest // keep working. Called once at startup; applyHotkey handles incremental // updates. func (st *appState) registerAllHotkeys() { const fn = "internal.ui.appState.registerAllHotkeys" st.mu.Lock() defer st.mu.Unlock() for _, en := range st.entries { if en.Hotkey == "" { continue } h, err := hotkey.Parse(en.Hotkey) if err != nil { global.Log.WithField("func", fn).WithField("entry", en.Name). WithError(err).Warn("bad hotkey string; skipping") continue } id, err := st.hkMgr.Register(h) if err != nil { global.Log.WithField("func", fn).WithField("entry", en.Name). WithField("hotkey", en.Hotkey).WithError(err). Warn("hotkey registration failed (already taken?)") continue } en.hotkeyID = id } } // applyHotkey updates target.Hotkey to value (empty string clears), // unregistering the old binding and registering the new one. Errors are // surfaced via st.saveErr so the user sees them. func (st *appState) applyHotkey(target *entry, value string) { const fn = "internal.ui.appState.applyHotkey" st.mu.Lock() defer st.mu.Unlock() if target.hotkeyID != 0 { if err := st.hkMgr.Unregister(target.hotkeyID); err != nil { global.Log.WithField("func", fn).WithError(err). Warn("unregister old hotkey failed") } target.hotkeyID = 0 } target.Hotkey = value if value == "" { return } h, err := hotkey.Parse(value) if err != nil { st.saveErr = err.Error() target.Hotkey = "" return } id, err := st.hkMgr.Register(h) if err != nil { st.saveErr = err.Error() target.Hotkey = "" return } target.hotkeyID = id } // runHotkeyLoop drains the manager's Events channel and triggers the // Auto-type flow for whichever entry owns the fired ID. Runs as a // daemon goroutine until the events channel closes (Stop). func (st *appState) runHotkeyLoop(w *app.Window) { const fn = "internal.ui.appState.runHotkeyLoop" for ev := range st.hkMgr.Events() { target := st.findEntryByHotkeyID(ev.ID) if target == nil { continue } code, err := target.Auth.CurrentCode() if err != nil { global.Log.WithField("func", fn).WithError(err).Warn("compute OTP failed") continue } st.autoType(code) w.Invalidate() } } // findEntryByHotkeyID is a tiny lookup with the lock held just for the // scan. Returns nil if no entry matches. func (st *appState) findEntryByHotkeyID(id int32) *entry { st.mu.Lock() defer st.mu.Unlock() for _, en := range st.entries { if en.hotkeyID == id { return en } } return nil } // autoType pushes the OTP onto the clipboard and types it into the // currently-foreground window. If GetForegroundWindow points at // winauth-go itself (because the user was looking at it when the // hotkey fired) we only copy — typing would inject into our own // editor field which is almost never useful. func (st *appState) autoType(code string) { const fn = "internal.ui.appState.autoType" if err := win32.SetClipboardText(code); err != nil { if !errors.Is(err, win32.ErrUnsupported) { global.Log.WithField("func", fn).WithError(err).Warn("clipboard write failed") } } // We do not have a HWND for our own Gio window via the public API, // so we cannot reliably detect "foreground is us." In practice the // global hotkey almost always fires while another window is on top // (that's the whole point), so we just inject blindly. The OTP is // also on the clipboard as a safety net. if err := win32.TypeUnicode(code); err != nil { if !errors.Is(err, win32.ErrUnsupported) { global.Log.WithField("func", fn).WithError(err).Warn("send input failed") } } }