From 2f04be8778e3afa1a162376deb5c09d59475f268 Mon Sep 17 00:00:00 2001 From: bbedward Date: Tue, 24 Feb 2026 15:09:04 -0500 Subject: [PATCH] wallpaper: handle initial load better, add dms randr command for quick physical scale retrieval --- core/cmd/dms/commands_common.go | 1 + core/cmd/dms/commands_randr.go | 58 ++++++ core/cmd/dms/randr_client.go | 172 ++++++++++++++++++ .../Modules/BlurredWallpaperBackground.qml | 21 +-- quickshell/Modules/WallpaperBackground.qml | 66 +++++-- quickshell/Services/CompositorService.qml | 31 ++++ 6 files changed, 315 insertions(+), 34 deletions(-) create mode 100644 core/cmd/dms/commands_randr.go create mode 100644 core/cmd/dms/randr_client.go diff --git a/core/cmd/dms/commands_common.go b/core/cmd/dms/commands_common.go index 39802de8..a5fd75b3 100644 --- a/core/cmd/dms/commands_common.go +++ b/core/cmd/dms/commands_common.go @@ -525,5 +525,6 @@ func getCommonCommands() []*cobra.Command { doctorCmd, configCmd, dlCmd, + randrCmd, } } diff --git a/core/cmd/dms/commands_randr.go b/core/cmd/dms/commands_randr.go new file mode 100644 index 00000000..fa0c9902 --- /dev/null +++ b/core/cmd/dms/commands_randr.go @@ -0,0 +1,58 @@ +package main + +import ( + "encoding/json" + "fmt" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/log" + "github.com/spf13/cobra" +) + +var randrCmd = &cobra.Command{ + Use: "randr", + Short: "Query output display information", + Long: "Query Wayland compositor for output names, scales, resolutions and refresh rates via zwlr-output-management", + Run: runRandr, +} + +func init() { + randrCmd.Flags().Bool("json", false, "Output in JSON format") +} + +type randrJSON struct { + Outputs []randrOutput `json:"outputs"` +} + +func runRandr(cmd *cobra.Command, args []string) { + outputs, err := queryRandr() + if err != nil { + log.Fatalf("%v", err) + } + + jsonFlag, _ := cmd.Flags().GetBool("json") + + if jsonFlag { + data, err := json.Marshal(randrJSON{Outputs: outputs}) + if err != nil { + log.Fatalf("failed to marshal JSON: %v", err) + } + fmt.Println(string(data)) + return + } + + for i, out := range outputs { + if i > 0 { + fmt.Println() + } + status := "enabled" + if !out.Enabled { + status = "disabled" + } + fmt.Printf("%s (%s)\n", out.Name, status) + fmt.Printf(" Scale: %.4g\n", out.Scale) + fmt.Printf(" Resolution: %dx%d\n", out.Width, out.Height) + if out.Refresh > 0 { + fmt.Printf(" Refresh: %.2f Hz\n", float64(out.Refresh)/1000.0) + } + } +} diff --git a/core/cmd/dms/randr_client.go b/core/cmd/dms/randr_client.go new file mode 100644 index 00000000..1a149ca0 --- /dev/null +++ b/core/cmd/dms/randr_client.go @@ -0,0 +1,172 @@ +package main + +import ( + "fmt" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management" + wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" +) + +type randrOutput struct { + Name string `json:"name"` + Scale float64 `json:"scale"` + Width int32 `json:"width"` + Height int32 `json:"height"` + Refresh int32 `json:"refresh"` + Enabled bool `json:"enabled"` +} + +type randrHead struct { + name string + enabled bool + scale float64 + currentModeID uint32 + modeIDs []uint32 +} + +type randrMode struct { + width int32 + height int32 + refresh int32 +} + +type randrClient struct { + display *wlclient.Display + ctx *wlclient.Context + manager *wlr_output_management.ZwlrOutputManagerV1 + heads map[uint32]*randrHead + modes map[uint32]*randrMode + done bool + err error +} + +func queryRandr() ([]randrOutput, error) { + display, err := wlclient.Connect("") + if err != nil { + return nil, fmt.Errorf("failed to connect to Wayland: %w", err) + } + + c := &randrClient{ + display: display, + ctx: display.Context(), + heads: make(map[uint32]*randrHead), + modes: make(map[uint32]*randrMode), + } + defer c.ctx.Close() + + registry, err := display.GetRegistry() + if err != nil { + return nil, fmt.Errorf("failed to get registry: %w", err) + } + + registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) { + if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName { + mgr := wlr_output_management.NewZwlrOutputManagerV1(c.ctx) + version := min(e.Version, 4) + + mgr.SetHeadHandler(func(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) { + c.handleHead(e) + }) + + mgr.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) { + c.done = true + }) + + if err := registry.Bind(e.Name, e.Interface, version, mgr); err == nil { + c.manager = mgr + } + } + }) + + // First roundtrip: discover globals and bind manager + syncCallback, err := display.Sync() + if err != nil { + return nil, fmt.Errorf("failed to sync display: %w", err) + } + syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) { + if c.manager == nil { + c.err = fmt.Errorf("zwlr_output_manager_v1 protocol not supported by compositor") + c.done = true + } + // Otherwise wait for manager's DoneHandler + }) + + for !c.done { + if err := c.ctx.Dispatch(); err != nil { + return nil, fmt.Errorf("dispatch error: %w", err) + } + } + + if c.err != nil { + return nil, c.err + } + + return c.buildOutputs(), nil +} + +func (c *randrClient) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) { + handle := e.Head + headID := handle.ID() + + head := &randrHead{ + modeIDs: make([]uint32, 0), + } + c.heads[headID] = head + + handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) { + head.name = e.Name + }) + + handle.SetEnabledHandler(func(e wlr_output_management.ZwlrOutputHeadV1EnabledEvent) { + head.enabled = e.Enabled != 0 + }) + + handle.SetScaleHandler(func(e wlr_output_management.ZwlrOutputHeadV1ScaleEvent) { + head.scale = e.Scale + }) + + handle.SetCurrentModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1CurrentModeEvent) { + head.currentModeID = e.Mode.ID() + }) + + handle.SetModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1ModeEvent) { + modeHandle := e.Mode + modeID := modeHandle.ID() + + head.modeIDs = append(head.modeIDs, modeID) + + mode := &randrMode{} + c.modes[modeID] = mode + + modeHandle.SetSizeHandler(func(e wlr_output_management.ZwlrOutputModeV1SizeEvent) { + mode.width = e.Width + mode.height = e.Height + }) + + modeHandle.SetRefreshHandler(func(e wlr_output_management.ZwlrOutputModeV1RefreshEvent) { + mode.refresh = e.Refresh + }) + }) +} + +func (c *randrClient) buildOutputs() []randrOutput { + outputs := make([]randrOutput, 0, len(c.heads)) + + for _, head := range c.heads { + out := randrOutput{ + Name: head.name, + Scale: head.scale, + Enabled: head.enabled, + } + + if mode, ok := c.modes[head.currentModeID]; ok { + out.Width = mode.width + out.Height = mode.height + out.Refresh = mode.refresh + } + + outputs = append(outputs, out) + } + + return outputs +} diff --git a/quickshell/Modules/BlurredWallpaperBackground.qml b/quickshell/Modules/BlurredWallpaperBackground.qml index 60380c60..5b584c72 100644 --- a/quickshell/Modules/BlurredWallpaperBackground.qml +++ b/quickshell/Modules/BlurredWallpaperBackground.qml @@ -85,22 +85,20 @@ Variants { } Component.onCompleted: { + if (typeof blurWallpaperWindow.updatesEnabled !== "undefined") + blurWallpaperWindow.updatesEnabled = Qt.binding(() => root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading); + if (!source) { - isInitialized = true; - updatesBindingTimer.start(); - return; + root._renderSettling = false; } - const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source); - setWallpaperImmediate(formattedSource); isInitialized = true; - updatesBindingTimer.start(); } property bool isInitialized: false property real transitionProgress: 0 readonly property bool transitioning: transitionAnimation.running property bool effectActive: false - property bool _renderSettling: false + property bool _renderSettling: true property bool useNextForEffect: false Connections { @@ -119,15 +117,6 @@ Variants { onTriggered: root._renderSettling = false } - Timer { - id: updatesBindingTimer - interval: 500 - onTriggered: { - if (typeof blurWallpaperWindow.updatesEnabled !== "undefined") - blurWallpaperWindow.updatesEnabled = Qt.binding(() => root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading); - } - } - onSourceChanged: { if (!source || source.startsWith("#")) { setWallpaperImmediate(""); diff --git a/quickshell/Modules/WallpaperBackground.qml b/quickshell/Modules/WallpaperBackground.qml index 714da9a4..1e0812e6 100644 --- a/quickshell/Modules/WallpaperBackground.qml +++ b/quickshell/Modules/WallpaperBackground.qml @@ -83,9 +83,10 @@ Variants { readonly property bool transitioning: transitionAnimation.running property bool effectActive: false - property bool _renderSettling: false + property bool _renderSettling: true property bool useNextForEffect: false property string pendingWallpaper: "" + property string _deferredSource: "" Connections { target: currentWallpaper @@ -97,21 +98,47 @@ Variants { } } + function _recheckScreenScale() { + const newScale = CompositorService.getScreenScale(modelData); + if (newScale !== root.screenScale) { + console.info("WallpaperBackground: screen scale corrected for", modelData.name + ":", root.screenScale, "->", newScale); + root.screenScale = newScale; + } + } + + Connections { + target: NiriService + function onDisplayScalesChanged() { + root._recheckScreenScale(); + } + } + + Connections { + target: WlrOutputService + function onWlrOutputAvailableChanged() { + root._recheckScreenScale(); + } + } + + Connections { + target: CompositorService + function onRandrDataReady() { + if (root._deferredSource) { + const src = root._deferredSource; + root._deferredSource = ""; + root.setWallpaperImmediate(src); + } else { + root._recheckScreenScale(); + } + } + } + Timer { id: renderSettleTimer interval: 100 onTriggered: root._renderSettling = false } - Timer { - id: updatesBindingTimer - interval: 500 - onTriggered: { - if (typeof wallpaperWindow.updatesEnabled !== "undefined") - wallpaperWindow.updatesEnabled = Qt.binding(() => root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading); - } - } - function getFillMode(modeName) { switch (modeName) { case "Stretch": @@ -136,15 +163,13 @@ Variants { } Component.onCompleted: { + if (typeof wallpaperWindow.updatesEnabled !== "undefined") + wallpaperWindow.updatesEnabled = Qt.binding(() => root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading); + if (!source) { - isInitialized = true; - updatesBindingTimer.start(); - return; + root._renderSettling = false; } - const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source); - setWallpaperImmediate(formattedSource); isInitialized = true; - updatesBindingTimer.start(); } onSourceChanged: { @@ -156,8 +181,11 @@ Variants { const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source); if (!isInitialized || !currentWallpaper.source) { + if (!CompositorService.randrReady) { + _deferredSource = formattedSource; + return; + } setWallpaperImmediate(formattedSource); - isInitialized = true; return; } if (CompositorService.isNiri && SessionData.isSwitchingMode) { @@ -173,6 +201,7 @@ Variants { root.effectActive = false; root._renderSettling = true; renderSettleTimer.restart(); + root.screenScale = CompositorService.getScreenScale(modelData); currentWallpaper.source = newSource; nextWallpaper.source = ""; } @@ -201,6 +230,7 @@ Variants { return; if (!newPath || newPath.startsWith("#")) return; + root.screenScale = CompositorService.getScreenScale(modelData); if (root.transitioning || root.effectActive) { root.pendingWallpaper = newPath; return; @@ -252,7 +282,7 @@ Variants { } readonly property int maxTextureSize: 8192 - property real screenScale: CompositorService.getScreenScale(modelData) + property real screenScale: 1 property int textureWidth: Math.min(Math.round(modelData.width * screenScale), maxTextureSize) property int textureHeight: Math.min(Math.round(modelData.height * screenScale), maxTextureSize) diff --git a/quickshell/Services/CompositorService.qml b/quickshell/Services/CompositorService.qml index 5bfa7bda..d327ea2b 100644 --- a/quickshell/Services/CompositorService.qml +++ b/quickshell/Services/CompositorService.qml @@ -29,11 +29,37 @@ Singleton { readonly property string labwcPid: Quickshell.env("LABWC_PID") property bool useNiriSorting: isNiri && NiriService + property var randrScales: ({}) + property bool randrReady: false + signal randrDataReady + property var sortedToplevels: [] property bool _sortScheduled: false signal toplevelsChanged + function fetchRandrData() { + Proc.runCommand("randr", ["dms", "randr", "--json"], (output, exitCode) => { + if (exitCode === 0 && output) { + try { + const data = JSON.parse(output.trim()); + if (data.outputs && Array.isArray(data.outputs)) { + const scales = {}; + for (const out of data.outputs) { + if (out.name && out.scale > 0) + scales[out.name] = out.scale; + } + randrScales = scales; + } + } catch (e) { + console.warn("CompositorService: failed to parse randr data:", e); + } + } + randrReady = true; + randrDataReady(); + }, 0, 3000); + } + function getScreenScale(screen) { if (!screen) return 1; @@ -42,6 +68,10 @@ Singleton { return screen.devicePixelRatio || 1; } + const randrScale = randrScales[screen.name]; + if (randrScale !== undefined && randrScale > 0) + return Math.round(randrScale * 20) / 20; + if (WlrOutputService.wlrOutputAvailable && screen) { const wlrOutput = WlrOutputService.getOutput(screen.name); if (wlrOutput?.enabled && wlrOutput.scale !== undefined && wlrOutput.scale > 0) { @@ -137,6 +167,7 @@ Singleton { } Component.onCompleted: { + fetchRandrData(); detectCompositor(); scheduleSort(); Qt.callLater(() => {