package main import ( "context" "embed" "fmt" "os" "os/exec" "strconv" "sync" "github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/pkg/menu/keys" "github.com/wailsapp/wails/v2/pkg/runtime" ) //go:embed docs var usageGuides embed.FS // App struct type App struct { ctx context.Context AppMode string LogPort int bridge *Bridge settings *SettingsManager isQuitting bool logsMu sync.Mutex settingsCmd *exec.Cmd logsCmd *exec.Cmd helpCmd *exec.Cmd } // NewApp creates a new App application struct func NewApp(mode string, logPort int) *App { return &App{ AppMode: mode, LogPort: logPort, bridge: NewBridge(), settings: NewSettingsManager(), } } // startup is called when the app starts. The context is saved // so we can call the runtime methods func (a *App) startup(ctx context.Context) { a.ctx = ctx // Bind logger a.bridge.SetLogger(func(msg string) { runtime.EventsEmit(ctx, "log", msg) }) // Bind client count a.bridge.SetCountCallback(func(count int) { runtime.EventsEmit(ctx, "client_count", count) }) // Bind reload callback a.bridge.SetReloadCallback(func() { a.Reload() }) a.bridge.SetForwarderStatusProvider(func() RemoteForwarderStatus { status := a.bridge.GetRemoteForwarderStatus() status.AutoReconnect = a.settings.Get().RemoteAutoConnect return status }) a.bridge.SetForwarderConnectHandler(func() { a.bridge.StartRemoteForwarderWithSettings(a.settings.Get(), true) }) a.bridge.SetForwarderDisconnectHandler(func() { a.bridge.StopRemoteForwarder() }) // Bind client connect a.bridge.SetClientConnectCallback(func(clientInfo string) { runtime.EventsEmit(ctx, "client_connected", clientInfo) }) if a.AppMode == "main" { // Start Log Server if err := a.bridge.StartLogServer(); err != nil { fmt.Printf("Failed to start log server: %v\n", err) } else { a.LogPort = a.bridge.logPort a.bridge.Log(fmt.Sprintf("Log server started on port %d", a.LogPort)) } a.bridge.ConfigureRemoteForwarder(a.settings.Get()) } } func (a *App) domReady(ctx context.Context) { // Add any dom ready logic } func (a *App) beforeClose(ctx context.Context) (prevent bool) { return false } func (a *App) shutdown(ctx context.Context) { a.Cleanup() } func (a *App) Cleanup() { if a.AppMode == "main" { a.bridge.StopServer() a.bridge.StopLogServer() a.bridge.StopRemoteForwarder() // Kill child windows if a.settingsCmd != nil && a.settingsCmd.Process != nil { a.settingsCmd.Process.Kill() } if a.logsCmd != nil && a.logsCmd.Process != nil { a.logsCmd.Process.Kill() } if a.helpCmd != nil && a.helpCmd.Process != nil { a.helpCmd.Process.Kill() } } } func (a *App) Quit() { a.isQuitting = true if a.ctx != nil { runtime.Quit(a.ctx) } else { os.Exit(0) } } // Wails Methods func (a *App) GetUsageGuide() string { lang := a.settings.Get().Language filename := "docs/usage_guide_en.md" if lang == "zh-CN" { filename = "docs/usage_guide_zh.md" } content, err := usageGuides.ReadFile(filename) if err != nil { return "# Error\n\nFailed to load usage guide: " + err.Error() } return string(content) } func (a *App) GetSettings() AppSettings { return a.settings.Get() } func (a *App) SaveSettings(s AppSettings) error { return a.settings.Save(s) } func (a *App) GetPrinters() ([]PrinterInfo, error) { return a.bridge.GetPrinters() } // PrintCurrentView uses the CDP hack to silent print the current WebView content func (a *App) StartServer(port, key string) error { return a.bridge.StartServer(port, key) } func (a *App) StopServer() error { return a.bridge.StopServer() } func (a *App) GetAppMode() string { return a.AppMode } func (a *App) Restart() { runtime.Quit(a.ctx) } func (a *App) ShowHelp() { a.logsMu.Lock() defer a.logsMu.Unlock() a.spawnWindow("help", &a.helpCmd) } func (a *App) ShowLogs() { a.logsMu.Lock() defer a.logsMu.Unlock() a.spawnWindow("logs", &a.logsCmd) } func (a *App) ShowSettings() { a.logsMu.Lock() defer a.logsMu.Unlock() a.spawnWindow("settings", &a.settingsCmd) } func (a *App) spawnWindow(mode string, cmdStore **exec.Cmd) { exe, err := os.Executable() if err != nil { a.bridge.Log(fmt.Sprintf("Failed to get executable: %v", err)) return } cmd := exec.Command(exe, mode, strconv.Itoa(a.LogPort)) cmd.Start() *cmdStore = cmd } func (a *App) GetLogPort() int { return a.LogPort } func (a *App) GetRemoteForwarderStatus() RemoteForwarderStatus { return a.bridge.GetRemoteForwarderStatus() } func (a *App) DisconnectRemoteForwarder() { a.bridge.StopRemoteForwarder() } func (a *App) ConnectRemoteForwarder() { a.bridge.StartRemoteForwarderWithSettings(a.settings.Get(), true) } func (a *App) CreateMenu(lang string) *menu.Menu { // Ensure locales are loaded LoadLocales(lang) appMenu := menu.NewMenu() // Main App Configuration menuTitle := T("menu.title") settingsTitle := T("menu.settings") logsTitle := T("menu.logs") helpTitle := T("menu.help") quitTitle := T("menu.quit") // Menu (菜单) MenuMenu := appMenu.AddSubmenu(menuTitle) MenuMenu.AddText(logsTitle, keys.CmdOrCtrl("l"), func(_ *menu.CallbackData) { a.ShowLogs() }) MenuMenu.AddSeparator() MenuMenu.AddText(quitTitle, keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) { a.Quit() }) // Help (帮助) HelpMenu := appMenu.AddSubmenu(helpTitle) HelpMenu.AddText(helpTitle, keys.CmdOrCtrl("h"), func(_ *menu.CallbackData) { a.ShowHelp() }) // Settings (设置) SettingsMenu := appMenu.AddSubmenu(settingsTitle) SettingsMenu.AddText(settingsTitle, keys.CmdOrCtrl("i"), func(_ *menu.CallbackData) { a.ShowSettings() }) return appMenu } func (a *App) UpdateUI(lang string) { if a.ctx != nil && a.AppMode == "main" { runtime.MenuSetApplicationMenu(a.ctx, a.CreateMenu(lang)) // Emit event for frontend updates runtime.EventsEmit(a.ctx, "reload_settings") } } func (a *App) Reload() { // Reload settings from disk a.settings.Load() // Reload locale LoadLocales(a.settings.Get().Language) // Update menu and UI a.UpdateUI(a.settings.Get().Language) a.bridge.ConfigureRemoteForwarder(a.settings.Get()) a.bridge.Log("Settings reloaded") }