Compare commits
2 Commits
832b3dcc68
...
93b57979ec
| Author | SHA1 | Date | |
|---|---|---|---|
| 93b57979ec | |||
| b46e4ed7fa |
+1
-1
@@ -273,7 +273,7 @@ Track progress here. Mark tasks complete as they land and pass review.
|
|||||||
- [x] T4.3 — Extract `job_dialog.go`
|
- [x] T4.3 — Extract `job_dialog.go`
|
||||||
- [x] T4.4 — Extract `history_view.go`
|
- [x] T4.4 — Extract `history_view.go`
|
||||||
- [x] T4.5 — Extract `settings_view.go`
|
- [x] T4.5 — Extract `settings_view.go`
|
||||||
- [ ] T4.6 — Extract `tray.go`, `singleinstance.go`, `layout.go`
|
- [x] T4.6 — Extract `tray.go`, `singleinstance.go`, `layout.go`
|
||||||
- [ ] T4.7 — Confirm app.go is gone; smoke test both platforms
|
- [ ] T4.7 — Confirm app.go is gone; smoke test both platforms
|
||||||
|
|
||||||
### Phase 5 — Hardening & docs
|
### Phase 5 — Hardening & docs
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -91,35 +91,3 @@ func newMainView(w fyne.Window) (fyne.CanvasObject, func(time.Duration, bool)) {
|
|||||||
|
|
||||||
return tabs, recordStartup
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.mixdep.ru/mix/gosentry/assets"
|
"gitea.mixdep.ru/mix/gosentry/assets"
|
||||||
@@ -11,12 +8,9 @@ import (
|
|||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
fyneapp "fyne.io/fyne/v2/app"
|
fyneapp "fyne.io/fyne/v2/app"
|
||||||
fynedesktop "fyne.io/fyne/v2/driver/desktop"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const appID = "ru.mixdep.gosentry.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
|
// Run is the application entry point. It owns the process lifecycle — single
|
||||||
// instance arbitration, Fyne app + window construction, tray wiring, and the
|
// instance arbitration, Fyne app + window construction, tray wiring, and the
|
||||||
@@ -64,88 +58,3 @@ func Run(startInTray bool) {
|
|||||||
func loadAppIcon() fyne.Resource {
|
func loadAppIcon() fyne.Resource {
|
||||||
return assets.Icon()
|
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()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user