diff --git a/.github/workflows/platform_tests.yml b/.github/workflows/platform_tests.yml index 07102071..153f18f3 100644 --- a/.github/workflows/platform_tests.yml +++ b/.github/workflows/platform_tests.yml @@ -21,7 +21,7 @@ jobs: go-version: ${{ matrix.go-version }} - name: Get dependencies - run: sudo apt-get update && sudo apt-get install gcc libgl1-mesa-dev libegl1-mesa-dev libgles2-mesa-dev libx11-dev xorg-dev bc + run: sudo apt-get update && sudo apt-get install gcc libgl1-mesa-dev libegl1-mesa-dev libgles2-mesa-dev libx11-dev xorg-dev bc libpam-dev if: ${{ runner.os == 'Linux' }} - name: Tests diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index ba75b1c9..9e6e1816 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -16,7 +16,7 @@ jobs: - name: Get dependencies run: | - sudo apt-get update && sudo apt-get install gcc libgl1-mesa-dev libegl1-mesa-dev libgles2-mesa-dev libx11-dev xorg-dev + sudo apt-get update && sudo apt-get install gcc libgl1-mesa-dev libegl1-mesa-dev libgles2-mesa-dev libx11-dev xorg-dev libpam-dev go install golang.org/x/tools/cmd/goimports@latest go install github.com/fzipp/gocyclo/cmd/gocyclo@latest go install golang.org/x/lint/golint@latest diff --git a/desk.go b/desk.go index 900b00a0..c04b8193 100644 --- a/desk.go +++ b/desk.go @@ -25,6 +25,9 @@ type Desktop interface { Desktop() int SetDesktop(int) + + DelayScreenSaver() + TriggerScreenSaver() } var instance Desktop diff --git a/go.mod b/go.mod index feaa77c2..7f005ae8 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,12 @@ module fyshos.com/fynedesk go 1.19 require ( - fyne.io/fyne/v2 v2.5.5-0.20250208132135-967702162d53 + fyne.io/fyne/v2 v2.5.5-0.20250208220124-36dce94b9595 github.com/BurntSushi/xgb v0.0.0-20201008132610-5f9e7b3c49cd github.com/BurntSushi/xgbutil v0.0.0-20160919175755-f7c97cef3b4e github.com/FyshOS/appie v0.0.0-20250103211310-00f097d8e19d github.com/FyshOS/backgrounds v0.0.0-20230616202904-0a8b6ebaa184 + github.com/FyshOS/saver v0.0.0-20250125211336-528339462781 github.com/Knetic/govaluate v3.0.0+incompatible github.com/disintegration/imaging v1.6.2 github.com/godbus/dbus/v5 v5.1.0 diff --git a/go.sum b/go.sum index 780f9461..69ad9d3b 100644 --- a/go.sum +++ b/go.sum @@ -37,8 +37,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -fyne.io/fyne/v2 v2.5.5-0.20250208132135-967702162d53 h1:qXq2uL+bCtlg9uNnlXGdj+9eamAjMhuL4fQVDJrHVqo= -fyne.io/fyne/v2 v2.5.5-0.20250208132135-967702162d53/go.mod h1:6/uEYg4FEhspAcWgsokutm9wFMHDNSYuEHCKTYWSho8= +fyne.io/fyne/v2 v2.5.5-0.20250208220124-36dce94b9595 h1:oWbNIUNmmRx3DjnGQ5lKzKGvmEemtxcSrcAZKvSY/IE= +fyne.io/fyne/v2 v2.5.5-0.20250208220124-36dce94b9595/go.mod h1:6/uEYg4FEhspAcWgsokutm9wFMHDNSYuEHCKTYWSho8= fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg= fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= github.com/ActiveState/termtest/conpty v0.5.0 h1:JLUe6YDs4Jw4xNPCU+8VwTpniYOGeKzQg4SM2YHQNA8= @@ -59,6 +59,8 @@ github.com/FyshOS/appie v0.0.0-20250103211310-00f097d8e19d h1:bkIQPpxGMalOufoLbL github.com/FyshOS/appie v0.0.0-20250103211310-00f097d8e19d/go.mod h1:Gtvb1fKDXbE9HjMtoYdB2MPSSGy6PdtMl0TC9dSF8q0= github.com/FyshOS/backgrounds v0.0.0-20230616202904-0a8b6ebaa184 h1:Za0NHFsT0CCXf/X4hEaywjvEECccrM/xVVL8BzAy6JI= github.com/FyshOS/backgrounds v0.0.0-20230616202904-0a8b6ebaa184/go.mod h1:cOUmJ3HUVmH3W3u9Gj5hM73ZgrDxGNHKMr5T/sBKqLU= +github.com/FyshOS/saver v0.0.0-20250125211336-528339462781 h1:jGmHNeimIf4DDEe8jkoUNX9r10+qGtkUG+3PMpjGSEs= +github.com/FyshOS/saver v0.0.0-20250125211336-528339462781/go.mod h1:yIuAuYqnEyKaiNU4MD4xmyLcZl9QdDr2uTmT7lt02tI= github.com/Knetic/govaluate v3.0.0+incompatible h1:7o6+MAPhYTCF0+fdvoz1xDedhRb4f6s9Tn1Tt7/WTEg= github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= diff --git a/internal/ui/dbus/ScreenSaver.xml b/internal/ui/dbus/ScreenSaver.xml new file mode 100644 index 00000000..9966828b --- /dev/null +++ b/internal/ui/dbus/ScreenSaver.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/internal/ui/desk.go b/internal/ui/desk.go index 6bb8751d..dbade076 100644 --- a/internal/ui/desk.go +++ b/internal/ui/desk.go @@ -115,7 +115,12 @@ func (l *desktop) ShowMenuAt(menu *fyne.Menu, pos fyne.Position) { } func (l *desktop) updateBackgrounds(path string) { - l.root.Content().(*fyne.Container).Objects[0].(*background).updateBackground(path) + root := l.root.Content().(*fyne.Container).Objects[0] + if back, ok := root.(*background); ok { + back.updateBackground(path) + } else { // embed mode has another container + root.(*fyne.Container).Objects[0].(*background).updateBackground(path) + } } func (l *desktop) createPrimaryContent() fyne.CanvasObject { @@ -150,6 +155,7 @@ func (l *desktop) RecentApps() []appie.AppData { func (l *desktop) Run() { go l.wm.Run() + go l.watchScreenActivity() l.run() // use the configured run method } @@ -343,19 +349,9 @@ func (l *desktop) registerShortcuts() { l.AddShortcut(fynedesk.NewShortcut("Calculator", fynedesk.KeyCalculator, 0), l.calculator) l.AddShortcut(fynedesk.NewShortcut("Lock screen", fyne.KeyL, fynedesk.UserModifier), - l.lockScreen) -} - -func (l *desktop) startXscreensaver() { - _, err := exec.LookPath("xscreensaver") - if err != nil { - fyne.LogError("xscreensaver command not found", err) - return - } - err = exec.Command("xscreensaver", "-no-splash").Start() - if err != nil { - fyne.LogError("Failed to lock screen", err) - } + func() { + l.TriggerScreenSaver() + }) } // Screens returns the screens provider of the current desktop environment for access to screen functionality. @@ -374,7 +370,9 @@ func NewDesktop(app fyne.App, mgr fynedesk.WindowManager, icons appie.Provider, desk.setupRoot() wm.StartAuthAgent() - go desk.startXscreensaver() + if desk.Settings().ScreenSaverType() == "XScreensaver" { + go desk.startXscreensaver() + } return desk } @@ -383,12 +381,14 @@ func NewDesktop(app fyne.App, mgr fynedesk.WindowManager, icons appie.Provider, // If run during CI for testing it will return an in-memory window using the // fyne/test package. func NewEmbeddedDesktop(app fyne.App, icons appie.Provider) fynedesk.Desktop { - desk := newDesktop(app, &embededWM{}, icons) + wm := &embededWM{} + desk := newDesktop(app, wm, icons) desk.run = desk.runEmbed desk.showMenu = desk.showMenuEmbed desk.root = desk.newDesktopWindowEmbed() - desk.root.SetContent(desk.createPrimaryContent()) + over := wm.setWindow(desk.root) + desk.root.SetContent(container.NewStack(desk.createPrimaryContent(), over)) return desk } @@ -411,16 +411,17 @@ func (l *desktop) calculator() { } } -func (l *desktop) lockScreen() { - _, err := exec.LookPath("xscreensaver-command") - if err != nil { - fyne.LogError("xscreensaver-command not found", err) - l.WindowManager().Blank() - return - } - err = exec.Command("xscreensaver-command", "-lock").Start() - if err != nil { - fyne.LogError("Failed to lock screen", err) - l.WindowManager().Blank() - } -} +//func (l *desktop) runCommand() { +// w := l.app.NewWindow("Run Command") +// input := widget.NewEntry() +// // TODO add history etc... +// run := widget.NewButton("Run", func() { +// +// }) +// run.Importance = widget.HighImportance +// +// w.SetContent(container.NewVBox(widget.NewLabel("Enter command to run:"), +// container.NewBorder(nil, nil, nil, run, input))) +// w.Resize(fyne.NewSize(250, 40)) +// w.Show() +//} diff --git a/internal/ui/desk_test.go b/internal/ui/desk_test.go index dc372e84..9f87a694 100644 --- a/internal/ui/desk_test.go +++ b/internal/ui/desk_test.go @@ -66,7 +66,7 @@ func TestBackgroundChange(t *testing.T) { l.settings = wmTest.NewSettings() l.setupRoot() - bg := l.root.Content().(*fyne.Container).Objects[0].(*background) + bg := l.root.Content().(*fyne.Container).Objects[0].(*fyne.Container).Objects[0].(*background) workingDir, err := os.Getwd() if err != nil { diff --git a/internal/ui/embed_wm.go b/internal/ui/embed_wm.go index 3761a6aa..73d49688 100644 --- a/internal/ui/embed_wm.go +++ b/internal/ui/embed_wm.go @@ -2,14 +2,21 @@ package ui import ( "image" - - "fyne.io/fyne/v2" + "image/color" "fyshos.com/fynedesk" + "github.com/FyshOS/saver" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + deskDriver "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/widget" ) type embededWM struct { windows []fynedesk.Window + root fyne.Window } func (e *embededWM) AddWindow(win fynedesk.Window) { @@ -79,3 +86,55 @@ func (e *embededWM) Close() { windows[0].Close() // ensure our root is asked to close as well } } + +var visible bool + +func (e *embededWM) ShowScreensaver(s *saver.ScreenSaver) { + if visible { + return + } + + visible = true + over := container.NewStack(canvas.NewRectangle(color.Black)) + + s.OnUnlocked = func() { + visible = false + e.root.Canvas().Overlays().Remove(over) + } + + over.Add(s.MakeUI(e.root)) + over.Resize(e.root.Canvas().Size()) + e.root.Canvas().Overlays().Add(over) +} + +func (e *embededWM) setWindow(win fyne.Window) fyne.CanvasObject { + e.root = win + + return newSaverMonitor(fynedesk.Instance().DelayScreenSaver) +} + +type saverMonitor struct { + widget.BaseWidget + + cb func() +} + +func newSaverMonitor(cb func()) fyne.CanvasObject { + s := &saverMonitor{cb: cb} + s.ExtendBaseWidget(s) + return s +} + +func (s *saverMonitor) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(canvas.NewRectangle(color.Transparent)) +} + +func (s *saverMonitor) MouseIn(*deskDriver.MouseEvent) { +} + +func (s *saverMonitor) MouseMoved(*deskDriver.MouseEvent) { + s.cb() +} + +func (s *saverMonitor) MouseOut() { +} diff --git a/internal/ui/generated/screensaver.go b/internal/ui/generated/screensaver.go new file mode 100644 index 00000000..d4f96fef --- /dev/null +++ b/internal/ui/generated/screensaver.go @@ -0,0 +1,93 @@ +// Code generated by dbus-codegen-go DO NOT EDIT. +package screensaver + +import ( + "context" + + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" +) + +var ( + // Introspection for org.freedesktop.ScreenSaver + IntrospectDataScreenSaver = introspect.Interface{ + Name: "org.freedesktop.ScreenSaver", + Methods: []introspect.Method{{Name: "Inhibit", Args: []introspect.Arg{ + {Name: "application_name", Type: "s", Direction: "in"}, + {Name: "reason_for_inhibit", Type: "s", Direction: "in"}, + {Name: "cookie", Type: "u", Direction: "out"}, + }}, + {Name: "UnInhibit", Args: []introspect.Arg{ + {Name: "cookie", Type: "u", Direction: "in"}, + }}, + }, + Signals: []introspect.Signal{}, + Properties: []introspect.Property{}, + Annotations: []introspect.Annotation{}, + } +) + +// Interface name constants. +const ( + InterfaceScreenSaver = "org.freedesktop.ScreenSaver" +) + +// ScreenSaverer is org.freedesktop.ScreenSaver interface. +type ScreenSaverer interface { + // Inhibit is org.freedesktop.ScreenSaver.Inhibit method. + Inhibit(applicationName string, reasonForInhibit string) (cookie uint32, err *dbus.Error) + // UnInhibit is org.freedesktop.ScreenSaver.UnInhibit method. + UnInhibit(cookie uint32) (err *dbus.Error) +} + +// ExportScreenSaver exports the given object that implements org.freedesktop.ScreenSaver on the bus. +func ExportScreenSaver(conn *dbus.Conn, path dbus.ObjectPath, v ScreenSaverer) error { + return conn.ExportSubtreeMethodTable(map[string]interface{}{ + "Inhibit": v.Inhibit, + "UnInhibit": v.UnInhibit, + }, path, InterfaceScreenSaver) +} + +// UnexportScreenSaver unexports org.freedesktop.ScreenSaver interface on the named path. +func UnexportScreenSaver(conn *dbus.Conn, path dbus.ObjectPath) error { + return conn.Export(nil, path, InterfaceScreenSaver) +} + +// UnimplementedScreenSaver can be embedded to have forward compatible server implementations. +type UnimplementedScreenSaver struct{} + +func (*UnimplementedScreenSaver) iface() string { + return InterfaceScreenSaver +} + +func (*UnimplementedScreenSaver) Inhibit(applicationName string, reasonForInhibit string) (cookie uint32, err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*UnimplementedScreenSaver) UnInhibit(cookie uint32) (err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +// NewScreenSaver creates and allocates org.freedesktop.ScreenSaver. +func NewScreenSaver(object dbus.BusObject) *ScreenSaver { + return &ScreenSaver{object} +} + +// ScreenSaver implements org.freedesktop.ScreenSaver D-Bus interface. +type ScreenSaver struct { + object dbus.BusObject +} + +// Inhibit calls org.freedesktop.ScreenSaver.Inhibit method. +func (o *ScreenSaver) Inhibit(ctx context.Context, applicationName string, reasonForInhibit string) (cookie uint32, err error) { + err = o.object.CallWithContext(ctx, InterfaceScreenSaver+".Inhibit", 0, applicationName, reasonForInhibit).Store(&cookie) + return +} + +// UnInhibit calls org.freedesktop.ScreenSaver.UnInhibit method. +func (o *ScreenSaver) UnInhibit(ctx context.Context, cookie uint32) (err error) { + err = o.object.CallWithContext(ctx, InterfaceScreenSaver+".UnInhibit", 0, cookie).Store() + return +} diff --git a/internal/ui/menu.go b/internal/ui/menu.go index fe75ed12..3894efdd 100644 --- a/internal/ui/menu.go +++ b/internal/ui/menu.go @@ -109,11 +109,14 @@ func (w *widgetPanel) showAccountMenu(_ fyne.CanvasObject) { w2.Close() }}} isEmbed := w.desk.(*desktop).root.Title() != RootWindowName + items1 = append(items1, &widget.Button{Icon: wmtheme.LockIcon, Importance: widget.LowImportance, OnTapped: func() { + w2.Close() + go func() { + time.Sleep(time.Millisecond * 300) + w.desk.TriggerScreenSaver() + }() + }}) if !isEmbed { - items1 = append(items1, &widget.Button{Icon: wmtheme.LockIcon, Importance: widget.LowImportance, OnTapped: func() { - w2.Close() - w.desk.(*desktop).lockScreen() - }}) if os.Getenv("FYNE_DESK_RUNNER") != "" { items1 = append(items1, &widget.Button{Icon: theme.ViewRefreshIcon(), Importance: widget.LowImportance, OnTapped: func() { os.Exit(5) diff --git a/internal/ui/screensaver.go b/internal/ui/screensaver.go new file mode 100644 index 00000000..2a20e168 --- /dev/null +++ b/internal/ui/screensaver.go @@ -0,0 +1,126 @@ +// Note that you need to have github.com/knightpp/dbus-codegen-go installed +//go:generate dbus-codegen-go -prefix org.freedesktop -package screensaver -output generated/screensaver.go dbus/ScreenSaver.xml + +package ui + +import ( + "math/rand" + "os/exec" + "time" + + screensaver "fyshos.com/fynedesk/internal/ui/generated" + "github.com/FyshOS/saver" + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" + "github.com/godbus/dbus/v5/prop" + + "fyne.io/fyne/v2" +) + +var inhibitCount = 0 + +func (l *desktop) startXscreensaver() { + _, err := exec.LookPath("xscreensaver") + if err != nil { + fyne.LogError("xscreensaver command not found", err) + return + } + err = exec.Command("xscreensaver", "-no-splash").Start() + if err != nil { + fyne.LogError("Failed to lock screen", err) + } +} + +func (l *desktop) TriggerScreenSaver() { + s := saver.NewScreenSaver(nil) + s.ClockFormat = l.settings.ClockFormatting() + if l.settings.ScreenSaverClock() { + s.Label = "(clock)" + } else { + s.Label = l.settings.ScreenSaverLabel() + } + s.Lock = true + + fyne.Do(func() { + l.wm.ShowScreensaver(s) + }) +} + +var lastActivity time.Time + +func (l *desktop) DelayScreenSaver() { + lastActivity = time.Now() +} + +func (l *desktop) watchScreenActivity() { + watchDBus() + idle := false + to := time.NewTicker(5 * time.Second) + + for range to.C { + if inhibitCount == 0 && lastActivity.Add(time.Minute*5).Before(time.Now()) { + + if !idle { + idle = true + + l.TriggerScreenSaver() + } + } else { + idle = false + } + } +} + +func watchDBus() { + conn, err := dbus.ConnectSessionBus() + if err != nil { + fyne.LogError("failed to connect to DBus to watch for screensaver inhibits", err) + return + } + + name := "org.freedesktop.ScreenSaver" + r, err := conn.RequestName(name, dbus.NameFlagDoNotQueue) + if err != nil || r != dbus.RequestNameReplyPrimaryOwner { + fyne.LogError("could not watch DBus screensaver, another is registered", err) + return + } + + s := &screenSaverWatcher{} + path := "/org/freedesktop/ScreenSaver" + err = conn.ExportAll(s, dbus.ObjectPath(path), "org.freedesktop.ScreenSaver") + if err != nil { + fyne.LogError("failed to export inhibits", err) + return + } + + node := introspect.Node{ + Name: path, + Interfaces: []introspect.Interface{ + introspect.IntrospectData, + prop.IntrospectData, + screensaver.IntrospectDataScreenSaver, + }, + } + err = conn.Export(introspect.NewIntrospectable(&node), dbus.ObjectPath(path), + "org.freedesktop.DBus.Introspectable") + if err != nil { + fyne.LogError("could not export our node data", err) + } +} + +type screenSaverWatcher struct { +} + +func (s *screenSaverWatcher) Inhibit(_ dbus.Sender, who, why string) (uint, *dbus.Error) { + id := rand.Uint32() + inhibitCount++ + + // TODO also check these are still alive every so often + return uint(id), nil +} + +func (s *screenSaverWatcher) UnInhibit(_ dbus.Sender, cookie uint32) *dbus.Error { + // TODO compare to the cookies logged + inhibitCount-- + return nil +} diff --git a/internal/ui/settings.go b/internal/ui/settings.go index 47f0e5bb..7c3687db 100644 --- a/internal/ui/settings.go +++ b/internal/ui/settings.go @@ -2,6 +2,7 @@ package ui import ( "os" + "os/exec" "runtime" "strings" "sync" @@ -27,6 +28,8 @@ type deskSettings struct { moduleNames []string narrowPanel, narrowLeftLauncher bool + screenSaverClock bool + screenSaver, screenSaverLabel string listenerLock sync.Mutex changeListeners []func(fynedesk.DeskSettings) @@ -76,6 +79,18 @@ func (d *deskSettings) NarrowLeftLauncher() bool { return d.narrowLeftLauncher } +func (d *deskSettings) ScreenSaverClock() bool { + return d.screenSaverClock +} + +func (d *deskSettings) ScreenSaverType() string { + return d.screenSaver +} + +func (d *deskSettings) ScreenSaverLabel() string { + return d.screenSaverLabel +} + func (d *deskSettings) BorderButtonPosition() string { return d.borderButtonPosition } @@ -177,6 +192,31 @@ func (d *deskSettings) setNarrowWidgetPanel(narrow bool) { d.apply() } +func (d *deskSettings) setScreenSaver(saver string) { + oldSaver := d.screenSaver + d.screenSaver = saver + + fyne.CurrentApp().Preferences().SetString("savertype", saver) + + if oldSaver == "XScreensaver" && saver != "XScreensaver" { + cmd := exec.Command("xscreensaver-command", "-exit") + _ = cmd.Start() + } else if oldSaver != "XScreensaver" && saver == "XScreensaver" { + cmd := exec.Command("xscreensaver", "--no-splash") + _ = cmd.Start() + } +} + +func (d *deskSettings) setScreenSaverClock(show bool) { + d.screenSaverClock = show + fyne.CurrentApp().Preferences().SetBool("saverclock", show) +} + +func (d *deskSettings) setScreenSaverLabel(text string) { + d.screenSaverLabel = text + fyne.CurrentApp().Preferences().SetString("saverlabel", text) +} + func (d *deskSettings) setBorderButtonPosition(pos string) { d.borderButtonPosition = pos fyne.CurrentApp().Preferences().SetString("borderbuttonposition", d.borderButtonPosition) @@ -244,6 +284,9 @@ func (d *deskSettings) load() { d.narrowPanel = fyne.CurrentApp().Preferences().BoolWithFallback("narrowpanel", true) d.borderButtonPosition = fyne.CurrentApp().Preferences().StringWithFallback("borderbuttonposition", "Left") + d.screenSaver = fyne.CurrentApp().Preferences().StringWithFallback("savertype", "FyshOS") + d.screenSaverClock = fyne.CurrentApp().Preferences().BoolWithFallback("saverclock", true) + d.screenSaverLabel = fyne.CurrentApp().Preferences().StringWithFallback("saverlabel", "FyneDesk") d.clockFormatting = fyne.CurrentApp().Preferences().StringWithFallback("clockformatting", "12h") d.loadRecents() diff --git a/internal/ui/settings_ui.go b/internal/ui/settings_ui.go index 276f4df6..d47f68d3 100644 --- a/internal/ui/settings_ui.go +++ b/internal/ui/settings_ui.go @@ -111,6 +111,14 @@ func (d *settingsUI) loadAppearanceScreen() fyne.CanvasObject { borderButton := &widget.Select{Options: []string{"Left", "Right"}} borderButton.SetSelected(d.settings.BorderButtonPosition()) + saverLabel := widget.NewLabelWithStyle("Screensaver", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}) + saverType := &widget.RadioGroup{Options: []string{"FyshOS", "XScreensaver"}, Required: true, Horizontal: true} + saverType.SetSelected(d.settings.ScreenSaverType()) + saverText := widget.NewEntry() + saverText.SetText(d.settings.ScreenSaverLabel()) + saverClock := widget.NewCheck("Clock", nil) + saverClock.Checked = d.settings.ScreenSaverClock() + themeLabel := widget.NewLabel(d.settings.IconTheme()) themeIcons := container.NewHBox() d.populateThemeIcons(themeIcons, d.settings.IconTheme()) @@ -129,7 +137,9 @@ func (d *settingsUI) loadAppearanceScreen() fyne.CanvasObject { lay := container.NewBorder(nil, nil, layoutLabel, container.NewGridWithColumns(2, narrowBar, narrowWidget)) border := container.NewBorder(nil, nil, borderButtonLabel, borderButton) - top := container.NewVBox(bg, time, lay, border) + saver := container.NewBorder(nil, nil, container.NewVBox(saverLabel, widget.NewLabel("")), + container.NewVBox(saverType, container.NewBorder(nil, nil, saverClock, nil, saverText))) + top := container.NewVBox(bg, time, lay, border, saver) themeFormLabel := widget.NewLabelWithStyle("Icon Theme", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}) themeCurrent := container.NewHBox(layout.NewSpacer(), themeLabel, themeIcons) @@ -143,6 +153,9 @@ func (d *settingsUI) loadAppearanceScreen() fyne.CanvasObject { d.settings.setBorderButtonPosition(borderButton.Selected) d.settings.setNarrowLeftLauncher(narrowBar.Checked) d.settings.setNarrowWidgetPanel(narrowWidget.Checked) + d.settings.setScreenSaver(saverType.Selected) + d.settings.setScreenSaverClock(saverClock.Checked) + d.settings.setScreenSaverLabel(saverText.Text) }}) return container.NewBorder(top, applyButton, nil, nil, bottom) @@ -401,7 +414,9 @@ func (d *settingsUI) loadThemeScreen() fyne.CanvasObject { fyne.LogError("Unable to list themes - missing?", err) } else { for _, l := range list { - themeList = append(themeList, l.Name()) + if false { // TODO with 1.21 } !slices.Contains(themeList, l.Name()) { + themeList = append(themeList, l.Name()) + } } } diff --git a/internal/x11/win/frame.go b/internal/x11/win/frame.go index 415742fd..d0eada7c 100644 --- a/internal/x11/win/frame.go +++ b/internal/x11/win/frame.go @@ -607,7 +607,7 @@ func (f *frame) mouseMotion(x, y int16) { cursor := x11.DefaultCursor if obj != nil { if cur, ok := obj.(desktop.Cursorable); ok { - if cur.Cursor() == wm.CloseCursor { + if cur.Cursor() == desktop.PointerCursor { cursor = x11.CloseCursor } } diff --git a/internal/x11/wm/desk.go b/internal/x11/wm/desk.go index 26e77f02..1494ef60 100644 --- a/internal/x11/wm/desk.go +++ b/internal/x11/wm/desk.go @@ -15,6 +15,7 @@ import ( "github.com/BurntSushi/xgb" "github.com/BurntSushi/xgb/randr" + "github.com/BurntSushi/xgb/screensaver" "github.com/BurntSushi/xgb/xproto" "github.com/BurntSushi/xgbutil" "github.com/BurntSushi/xgbutil/ewmh" @@ -125,7 +126,8 @@ func NewX11WindowManager(a fyne.App) (fynedesk.WindowManager, error) { xproto.EventMaskButtonRelease | xproto.EventMaskKeyPress | xproto.EventMaskStructureNotify | - xproto.EventMaskSubstructureRedirect + xproto.EventMaskSubstructureRedirect | + screensaver.EventNotifyMask | screensaver.EventCycleMask if err := xproto.ChangeWindowAttributesChecked(conn.Conn(), root, xproto.CwEventMask, []uint32{uint32(eventMask)}).Check(); err != nil { conn.Conn().Close() @@ -163,6 +165,7 @@ func NewX11WindowManager(a fyne.App) (fynedesk.WindowManager, error) { } x11.LoadCursors(conn) + mgr.initScreensaver() a.Settings().AddListener(func(_ fyne.Settings) { mgr.updateBackgrounds() @@ -417,9 +420,7 @@ func (x *x11WM) runLoop() { case xproto.ClientMessageEvent: x.handleClientMessage(ev) case xproto.ConfigureNotifyEvent: - if ev.Window == x.x.RootWin() { - x.configureRoots() - } + x.notifyConfigure(ev) case xproto.ConfigureRequestEvent: x.configureWindow(ev.Window, ev) case xproto.CreateNotifyEvent: @@ -452,6 +453,8 @@ func (x *x11WM) runLoop() { x.hideWindow(ev.Window) case xproto.VisibilityNotifyEvent: x.handleVisibilityChange(ev) + case screensaver.NotifyEvent: + // screensaver activate, except we manage it with an internal timer } } @@ -514,6 +517,12 @@ func (x *x11WM) configureRoots() { go x.updateBackgrounds() } +func (x *x11WM) notifyConfigure(ev xproto.ConfigureNotifyEvent) { + if ev.Window == x.x.RootWin() { + x.configureRoots() + } +} + func (x *x11WM) configureWindow(win xproto.Window, ev xproto.ConfigureRequestEvent) { c := x.clientForWin(win) xcoord := ev.X diff --git a/internal/x11/wm/events.go b/internal/x11/wm/events.go index c600816e..3eb7d1e5 100644 --- a/internal/x11/wm/events.go +++ b/internal/x11/wm/events.go @@ -170,6 +170,10 @@ func (x *x11WM) handleInitialHints(ev xproto.ClientMessageEvent, hint string) { } func (x *x11WM) handleKeyPress(ev xproto.KeyPressEvent) { + if screenSaverActive { + return + } + userMod := ev.State&xproto.ModMask4 != 0 if fynedesk.Instance().Settings().KeyboardModifier() == fyne.KeyModifierAlt { userMod = ev.State&xproto.ModMask1 != 0 @@ -216,6 +220,10 @@ func (x *x11WM) handleKeyPress(ev xproto.KeyPressEvent) { } func (x *x11WM) handleKeyRelease(ev xproto.KeyReleaseEvent) { + if screenSaverActive { + return + } + userMod := keyCodeSuper if fynedesk.Instance().Settings().KeyboardModifier() == fyne.KeyModifierAlt { userMod = keyCodeAlt diff --git a/internal/x11/wm/screensaver.go b/internal/x11/wm/screensaver.go new file mode 100644 index 00000000..09e60cfe --- /dev/null +++ b/internal/x11/wm/screensaver.go @@ -0,0 +1,68 @@ +//go:build linux || openbsd || freebsd || netbsd +// +build linux openbsd freebsd netbsd + +package wm + +import ( + "log" + "os/exec" + "time" + + "fyshos.com/fynedesk" + "github.com/BurntSushi/xgb/screensaver" + "github.com/BurntSushi/xgb/xproto" + "github.com/FyshOS/saver" +) + +func (x *x11WM) initScreensaver() { + err := screensaver.Init(x.x.Conn()) + if err != nil { + log.Println("Failed to init screensaver extension") + return + } + + //screensaver.SelectInput(conn.Conn(), xproto.Drawable(conn.Screen().Root), + // screensaver.EventNotifyMask) + go x.watchScreensaver() +} + +func (x *x11WM) watchScreensaver() { + to := time.NewTicker(5 * time.Second) + + for range to.C { + info, err := screensaver.QueryInfo(x.x.Conn(), xproto.Drawable(x.x.Screen().Root)).Reply() + if err != nil { + log.Println("ERR", err) + continue + } + + if info.MsSinceUserInput <= 5500 { + fynedesk.Instance().DelayScreenSaver() + } + } +} + +var screenSaverActive bool + +func (x *x11WM) ShowScreensaver(s *saver.ScreenSaver) { + if fynedesk.Instance().Settings().ScreenSaverType() == "XScreensaver" { + task := "-activate" + if s.Lock { + task = "-lock" + } + cmd := exec.Command("xscreensaver-command", task) + cmd.Start() + return + } + + if screenSaverActive { + return + } + + screenSaverActive = true + s.OnUnlocked = func() { + screenSaverActive = false + } + + s.ShowWindow() +} diff --git a/settings.go b/settings.go index d94954d3..94728d9f 100644 --- a/settings.go +++ b/settings.go @@ -19,6 +19,9 @@ type DeskSettings interface { KeyboardModifier() fyne.KeyModifier ModuleNames() []string + ScreenSaverType() string + ScreenSaverClock() bool + ScreenSaverLabel() string AddChangeListener(listener func(DeskSettings)) } diff --git a/test/desktop.go b/test/desktop.go index 9e533e8c..5b8f6c5f 100644 --- a/test/desktop.go +++ b/test/desktop.go @@ -120,3 +120,9 @@ func (td *Desktop) ShowMenuAt(menu *fyne.Menu, pos fyne.Position) { func (td *Desktop) WindowManager() fynedesk.WindowManager { return td.wm } + +// DelayScreenSaver is called each time the user interacts with the system. +func (td *Desktop) DelayScreenSaver() {} + +// TriggerScreenSaver can be called to immediately show the screensaver. +func (td *Desktop) TriggerScreenSaver() {} diff --git a/test/settings.go b/test/settings.go index ca8205cd..70afb770 100644 --- a/test/settings.go +++ b/test/settings.go @@ -158,6 +158,21 @@ func (s *Settings) ClockFormatting() string { return s.clockFormatting } +// ScreenSaverClock returns if the text on the screensaver should be a clock. +func (s *Settings) ScreenSaverClock() bool { + return true +} + +// ScreenSaverLabel returns the string to use in the screensaver (if not a clock). +func (s *Settings) ScreenSaverLabel() string { + return "FyshOS" +} + +// ScreenSaverType returns whether this user should use FyshOS or XScreensaver savers. +func (s *Settings) ScreenSaverType() string { + return "FyshOS" +} + // SetClockFormatting support setting the format that the clock should display func (s *Settings) SetClockFormatting(format string) { if format == "24h" { diff --git a/wm.go b/wm.go index 47ad37e7..8169687a 100644 --- a/wm.go +++ b/wm.go @@ -3,6 +3,8 @@ package fynedesk import ( "image" + "github.com/FyshOS/saver" + "fyne.io/fyne/v2" ) @@ -18,6 +20,8 @@ type WindowManager interface { ShowOverlay(fyne.Window, fyne.Size, fyne.Position) ShowMenuOverlay(*fyne.Menu, fyne.Size, fyne.Position) ShowModal(fyne.Window, fyne.Size) + + ShowScreensaver(saver *saver.ScreenSaver) } // Stack describes an ordered list of windows.