diff --git a/src/ui/layout.go b/src/ui/layout.go new file mode 100644 index 0000000..798178b --- /dev/null +++ b/src/ui/layout.go @@ -0,0 +1,37 @@ +package ui + +import ( + "fyne.io/fyne/v2" +) + +type minWidthLayout struct { + width float32 +} + +func (l minWidthLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + width := l.width + var height float32 + for _, object := range objects { + if !object.Visible() { + continue + } + min := object.MinSize() + if min.Width > width { + width = min.Width + } + if min.Height > height { + height = min.Height + } + } + return fyne.NewSize(width, height) +} + +func (l minWidthLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + for _, object := range objects { + if !object.Visible() { + continue + } + object.Move(fyne.NewPos(0, 0)) + object.Resize(size) + } +} diff --git a/src/ui/mainwindow.go b/src/ui/mainwindow.go index 49e22fd..6ed0a18 100644 --- a/src/ui/mainwindow.go +++ b/src/ui/mainwindow.go @@ -91,35 +91,3 @@ func newMainView(w fyne.Window) (fyne.CanvasObject, func(time.Duration, bool)) { return tabs, recordStartup } - -type minWidthLayout struct { - width float32 -} - -func (l minWidthLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { - width := l.width - var height float32 - for _, object := range objects { - if !object.Visible() { - continue - } - min := object.MinSize() - if min.Width > width { - width = min.Width - } - if min.Height > height { - height = min.Height - } - } - return fyne.NewSize(width, height) -} - -func (l minWidthLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { - for _, object := range objects { - if !object.Visible() { - continue - } - object.Move(fyne.NewPos(0, 0)) - object.Resize(size) - } -} diff --git a/src/ui/run.go b/src/ui/run.go index 9684c9d..f087f82 100644 --- a/src/ui/run.go +++ b/src/ui/run.go @@ -1,9 +1,6 @@ package ui import ( - "io" - "net" - "strings" "time" "gitea.mixdep.ru/mix/gosentry/assets" @@ -11,12 +8,9 @@ import ( "fyne.io/fyne/v2" fyneapp "fyne.io/fyne/v2/app" - fynedesktop "fyne.io/fyne/v2/driver/desktop" ) const appID = "ru.mixdep.gosentry.desktop" -const singleInstanceAddress = "127.0.0.1:37653" -const singleInstanceShowCommand = "show" // Run is the application entry point. It owns the process lifecycle — single // instance arbitration, Fyne app + window construction, tray wiring, and the @@ -64,88 +58,3 @@ func Run(startInTray bool) { func loadAppIcon() fyne.Resource { return assets.Icon() } - -func configureSystemTray(a fyne.App, w fyne.Window) { - desk, ok := a.(fynedesktop.App) - if !ok { - // Not every Fyne driver exposes desktop tray features. Returning silently - // keeps the same binary usable on platforms or sessions without a tray. - return - } - - // IsQuit marks this as the tray's quit item. Without it Fyne's - // addMissingQuitForMenu appends a second, localized Quit (e.g. "Выход" on a - // Russian system) because it only recognizes an existing quit by matching the - // localized label — which our literal "Quit" does not. Setting IsQuit makes - // Fyne reuse this item instead of adding a duplicate, regardless of locale. - quit := fyne.NewMenuItem("Quit", func() { - a.Quit() - }) - quit.IsQuit = true - menu := fyne.NewMenu("GoSentry", - fyne.NewMenuItem("Show", func() { - w.Show() - w.RequestFocus() - }), - fyne.NewMenuItemSeparator(), - quit, - ) - desk.SetSystemTrayMenu(menu) - w.SetCloseIntercept(func() { - // Closing hides the window instead of quitting because scheduler tools are - // expected to keep working in the background. The explicit Quit tray item - // remains the way to stop the process. - w.Hide() - }) -} - -func acquireSingleInstance(showExisting bool) (net.Listener, bool) { - listener, err := net.Listen("tcp", singleInstanceAddress) - if err == nil { - return listener, true - } - - connection, dialErr := net.DialTimeout("tcp", singleInstanceAddress, time.Second) - if dialErr == nil { - // The first instance listens only on localhost and understands one tiny - // command: "show". That keeps the implementation dependency-free and easy - // to inspect, which matters more here than introducing a named-pipe or - // platform-specific IPC abstraction just to focus an existing window. - if showExisting { - _, _ = io.WriteString(connection, singleInstanceShowCommand) - } - _ = connection.Close() - return nil, false - } - - // If the port is unavailable but does not answer as GoSentry, continue - // startup instead of making the application impossible to open because of an - // unrelated local listener. In the normal duplicate-start case the dial above - // succeeds and this process exits after waking the first instance. - return nil, true -} - -func serveSingleInstance(listener net.Listener, w fyne.Window) { - if listener == nil { - return - } - go func() { - for { - connection, err := listener.Accept() - if err != nil { - return - } - command, _ := io.ReadAll(io.LimitReader(connection, 32)) - _ = connection.Close() - if strings.TrimSpace(string(command)) != singleInstanceShowCommand { - continue - } - // Accept runs on its own goroutine, so focusing the window must be - // marshaled onto the main thread like every other widget update. - fyne.Do(func() { - w.Show() - w.RequestFocus() - }) - } - }() -} diff --git a/src/ui/singleinstance.go b/src/ui/singleinstance.go new file mode 100644 index 0000000..59b59f8 --- /dev/null +++ b/src/ui/singleinstance.go @@ -0,0 +1,64 @@ +package ui + +import ( + "io" + "net" + "strings" + "time" + + "fyne.io/fyne/v2" +) + +const singleInstanceAddress = "127.0.0.1:37653" +const singleInstanceShowCommand = "show" + +func acquireSingleInstance(showExisting bool) (net.Listener, bool) { + listener, err := net.Listen("tcp", singleInstanceAddress) + if err == nil { + return listener, true + } + + connection, dialErr := net.DialTimeout("tcp", singleInstanceAddress, time.Second) + if dialErr == nil { + // The first instance listens only on localhost and understands one tiny + // command: "show". That keeps the implementation dependency-free and easy + // to inspect, which matters more here than introducing a named-pipe or + // platform-specific IPC abstraction just to focus an existing window. + if showExisting { + _, _ = io.WriteString(connection, singleInstanceShowCommand) + } + _ = connection.Close() + return nil, false + } + + // If the port is unavailable but does not answer as GoSentry, continue + // startup instead of making the application impossible to open because of an + // unrelated local listener. In the normal duplicate-start case the dial above + // succeeds and this process exits after waking the first instance. + return nil, true +} + +func serveSingleInstance(listener net.Listener, w fyne.Window) { + if listener == nil { + return + } + go func() { + for { + connection, err := listener.Accept() + if err != nil { + return + } + command, _ := io.ReadAll(io.LimitReader(connection, 32)) + _ = connection.Close() + if strings.TrimSpace(string(command)) != singleInstanceShowCommand { + continue + } + // Accept runs on its own goroutine, so focusing the window must be + // marshaled onto the main thread like every other widget update. + fyne.Do(func() { + w.Show() + w.RequestFocus() + }) + } + }() +} diff --git a/src/ui/tray.go b/src/ui/tray.go new file mode 100644 index 0000000..012e5e7 --- /dev/null +++ b/src/ui/tray.go @@ -0,0 +1,40 @@ +package ui + +import ( + "fyne.io/fyne/v2" + fynedesktop "fyne.io/fyne/v2/driver/desktop" +) + +func configureSystemTray(a fyne.App, w fyne.Window) { + desk, ok := a.(fynedesktop.App) + if !ok { + // Not every Fyne driver exposes desktop tray features. Returning silently + // keeps the same binary usable on platforms or sessions without a tray. + return + } + + // IsQuit marks this as the tray's quit item. Without it Fyne's + // addMissingQuitForMenu appends a second, localized Quit (e.g. "Выход" on a + // Russian system) because it only recognizes an existing quit by matching the + // localized label — which our literal "Quit" does not. Setting IsQuit makes + // Fyne reuse this item instead of adding a duplicate, regardless of locale. + quit := fyne.NewMenuItem("Quit", func() { + a.Quit() + }) + quit.IsQuit = true + menu := fyne.NewMenu("GoSentry", + fyne.NewMenuItem("Show", func() { + w.Show() + w.RequestFocus() + }), + fyne.NewMenuItemSeparator(), + quit, + ) + desk.SetSystemTrayMenu(menu) + w.SetCloseIntercept(func() { + // Closing hides the window instead of quitting because scheduler tools are + // expected to keep working in the background. The explicit Quit tray item + // remains the way to stop the process. + w.Hide() + }) +}