package main import ( "fmt" "os" "github.com/spf13/cobra" "git.wxccs.org/iceking2nd/winauth-go/internal/config" "git.wxccs.org/iceking2nd/winauth-go/internal/global" "git.wxccs.org/iceking2nd/winauth-go/internal/i18n" "git.wxccs.org/iceking2nd/winauth-go/internal/logging" "git.wxccs.org/iceking2nd/winauth-go/internal/ui" "git.wxccs.org/iceking2nd/winauth-go/internal/version" "git.wxccs.org/iceking2nd/winauth-go/internal/win32" ) const ( // singleInstanceMutex is the name used by AcquireInstanceLock. The // "Global\" prefix would make the lock cross-session; without it // each Windows user sees their own copy, which is what we want // (config.yaml is per-user anyway). singleInstanceMutex = "winauth-go-single-instance-mutex" // windowTitle is what AcquireInstanceLock's duplicate path tries to // foreground. Must stay in sync with ui.Run's app.Title option. windowTitle = "WinAuth" ) func main() { var ( console bool logLevel string logFile string configPath string ) root := &cobra.Command{ Use: "winauth-go", Short: "WinAuth (Go port) — TOTP/HOTP authenticator", RunE: func(cmd *cobra.Command, args []string) error { if console { logging.AttachConsole() } closer, err := logging.Init(logging.Options{ Level: logLevel, File: logFile, Console: console, }) if err != nil { return err } if closer != nil { defer func() { _ = closer.Close() }() } const fn = "cmd.winauth.main" global.Log.WithField("func", fn). WithField("version", version.String()).Info("winauth-go starting") release, alreadyRunning, err := win32.AcquireInstanceLock(singleInstanceMutex) if err != nil { global.Log.WithField("func", fn).WithError(err).Warn("instance lock failed; continuing without it") } if alreadyRunning { global.Log.WithField("func", fn).Info("another instance already running; activating it") if err := win32.ActivateOtherInstance(windowTitle); err != nil { global.Log.WithField("func", fn).WithError(err).Warn("could not activate existing window") } return nil } if release != nil { defer release() } // Peek the config (without password) to recover the user's // last language choice before initialising i18n. Encrypted // configs still expose Language at the YAML top level so we // can localise the password prompt itself. lang := preferredLanguage(configPath) if err := i18n.Init(lang); err != nil { global.Log.WithField("func", fn).WithError(err).Warn("i18n init failed; falling back to keys") } return ui.Run(configPath) }, } root.Flags().BoolVar(&console, "console", false, "Show console window (Windows GUI builds)") root.Flags().StringVar(&logLevel, "log-level", "info", "Log level: panic|fatal|error|warn|info|debug|trace (or 0..6)") root.Flags().StringVar(&logFile, "log-file", "", "If set, also write logs to this file in addition to the console") root.Flags().StringVar(&configPath, "config", "", "Path to the YAML config file (default: %APPDATA%\\winauth-go\\config.yaml or $XDG_CONFIG_HOME/winauth-go/config.yaml)") if err := root.Execute(); err != nil { fmt.Fprintln(os.Stderr, "error:", err) os.Exit(1) } } // preferredLanguage reads the user's saved language tag out of the YAML // config without requiring a passphrase. We resolve the default config // path when configPath is blank and silently return "" on any failure; // callers fall back to the i18n default in that case. Encrypted configs // still expose the top-level Language field so this works for them too. func preferredLanguage(configPath string) string { if configPath == "" { configPath = config.DefaultPath() } cfg, _ := config.LoadYAML(configPath, nil) if cfg == nil { return "" } return cfg.Language }