Compare commits

...
15 Commits
Author SHA1 Message Date
purian23 e48de4cc2a notifications: keep popup content while it exits after the notification closes
Port 2.0

(cherry picked from commit 47c9d8d89b)
2026-10-22 01:58:36 +00:00
bbedward 1683fceac5 bar: keep far-edge island hosts at sheet size so the bar never jumps on expand
fixes #3855
port 2.0

(cherry picked from commit d8786eaaf7)
2026-10-22 01:57:39 +00:00
bbedward 2a83c5c69b battery: throttle pulse animation to 20fps
ref #3849

(cherry picked from commit 1b59a474dd)
2026-10-21 14:16:02 -04:00
bbedward 1acfae1755 dash: hold panel height while dragging cards unless hung from a top bar
lyrics: ignore floored position samples and shell refresh emits

(cherry picked from commit 28f921fa02)
2026-10-21 13:19:05 -04:00
Adarsh 1e1dbebb70 Theme Antigravity IDE via the VS Code extension (#3836)
(cherry picked from commit 479b1434de)
2026-10-21 13:18:58 -04:00
bbedward 53412d1f22 theme: publish typography for dux apps
(cherry picked from commit 8f2a486006)
2026-10-21 12:42:02 -04:00
bbedward 9ece8f26fa theme: skip matugen runs whose inputs match the last applied run
fixes #3841

(cherry picked from commit afeea489c9)
2026-10-21 11:25:29 -04:00
bbedward fd1295f959 core: only use the simple animation driver for the scrolling wallpaper
fixes #3837

(cherry picked from commit 27a703367f)
2026-10-21 11:25:20 -04:00
bbedward 1e3f16ac9d bar: slide an auto-hidden island by its compact height
fixes #3838

(cherry picked from commit 8e851b3dc4)
2026-10-21 11:25:11 -04:00
bbedward 4fa6f58720 workspaces: keep the pill highlight while hovering its app icons
fixes #3839

(cherry picked from commit 112d05901f)
2026-10-21 11:25:02 -04:00
Marlon Kranz ff45b3b4b2 fix(notifications): close popups when their screen stops showing them (#3844)
(cherry picked from commit b81954454a)
2026-10-21 11:24:49 -04:00
Tomás 12e54e0225 Keep MangoWM workspace apps on their own monitor (#3832)
port 2.0

(cherry picked from commit 203b1369b3)
2026-10-21 11:24:35 -04:00
bbedward f63d8bf4fb niri: only emit displayScales when a scale changed
ref #3842
port 2.0

(cherry picked from commit 90d3c9731c)
2026-10-21 15:18:54 +00:00
Leonard 1e8423992d refactor(ui): match the dock settings page with the bar settings page (#3829)
port 2.0

(cherry picked from commit 5ffc581ecf)
2026-10-21 00:54:58 +00:00
bbedward c4ecb988b4 matugen: neovim template reads the dms config dir"
port 2.0

(cherry picked from commit 3284acab28)
2026-10-20 21:03:37 +00:00
35 changed files with 267 additions and 95 deletions
+27 -1
View File
@@ -2,8 +2,10 @@ package main
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/duxlabs-dev/dux-go/shellapp"
@@ -83,12 +85,36 @@ func preLaunch() {
ensureFontCache()
}
// The elapsed-timer driver keeps the scrolling wallpaper spring in step above 60 Hz,
// but it makes every other animation jump after a dropped frame (#3837).
func scrollingWallpaperConfigured() bool {
configDir, err := os.UserConfigDir()
if err != nil {
return false
}
data, err := os.ReadFile(filepath.Join(configDir, "dms", "settings.json"))
if err != nil {
return false
}
return wantsSimpleAnimationDriver(data)
}
func wantsSimpleAnimationDriver(settings []byte) bool {
var parsed struct {
WallpaperFillMode string `json:"wallpaperFillMode"`
}
if json.Unmarshal(settings, &parsed) != nil {
return false
}
return parsed.WallpaperFillMode == "Scrolling"
}
func dmsExtraEnv(string) []string {
var env []string
if selfPath, err := os.Executable(); err == nil {
env = append(env, "DMS_EXECUTABLE="+selfPath)
}
if os.Getenv("QSG_USE_SIMPLE_ANIMATION_DRIVER") == "" {
if os.Getenv("QSG_USE_SIMPLE_ANIMATION_DRIVER") == "" && scrollingWallpaperConfigured() {
env = append(env, "QSG_USE_SIMPLE_ANIMATION_DRIVER=1")
}
if _, set := os.LookupEnv("MALLOC_CONF"); !set {
+1
View File
@@ -965,6 +965,7 @@ var vscodeEditors = []vscodeEditor{
{"cursor", ".cursor"},
{"windsurf", ".windsurf"},
{"positron", ".positron"},
{"antigravity", ".antigravity-ide"},
{"vscode-insiders", ".vscode-insiders"},
}
Generated
+3 -3
View File
@@ -3,11 +3,11 @@
"dank-qml-common": {
"flake": false,
"locked": {
"lastModified": 1792513666,
"narHash": "sha256-RDqPdPrM5lnwRrfFGLz3IwgTjCV2Ie+nrTD3ldj1t/s=",
"lastModified": 1792597131,
"narHash": "sha256-2t9btOQ2owoRv/fgeonlr8cHVt2NUvUu9iXdZd9+QFc=",
"owner": "duxlabs-dev",
"repo": "dux-qml-common",
"rev": "ebf5e6c002da1bdaa4aaa2f319fea7b8507abc42",
"rev": "5715e8ea868f3a131a0f54c7a5875e9a95f2f253",
"type": "github"
},
"original": {
+3
View File
@@ -160,6 +160,7 @@ Singleton {
"children": [
{
"id": "dock_general",
"hidden": true,
"text": I18n.tr("General"),
"icon": "tune",
"tabIndex": 5,
@@ -167,6 +168,7 @@ Singleton {
},
{
"id": "dock_widgets",
"hidden": true,
"text": I18n.tr("Apps & widgets"),
"icon": "widgets",
"tabIndex": 57,
@@ -174,6 +176,7 @@ Singleton {
},
{
"id": "dock_appearance",
"hidden": true,
"text": I18n.tr("Appearance"),
"icon": "palette",
"tabIndex": 58,
+33 -3
View File
@@ -1876,9 +1876,9 @@ Singleton {
}
const runKey = Qt.md5(JSON.stringify(args));
const skipUnchanged = !_matugenRunKey && !colorsFileLoadFailed && typeof SettingsData !== "undefined" && !SettingsData.generateThemeAtStartup;
if (skipUnchanged && runKey === CacheData.matugenAppliedKey) {
log.info("Theme inputs unchanged since the last run, skipping startup generation");
const startupRun = !_matugenRunKey && typeof SettingsData !== "undefined" && SettingsData.generateThemeAtStartup;
if (!startupRun && !colorsFileLoadFailed && runKey === CacheData.matugenAppliedKey) {
log.info("Theme inputs unchanged since the last run, skipping generation");
return;
}
@@ -2496,6 +2496,36 @@ Singleton {
onTriggered: dynamicColorsFileView.reload()
}
readonly property bool publishTypography: typeof SettingsData !== "undefined" && SettingsData._hasLoaded && !SessionData.isGreeterMode
readonly property string typographyJson: JSON.stringify({
"fontFamily": fontFamily,
"monoFontFamily": monoFontFamily,
"displayFontFamily": displayFontFamily,
"fontWeight": fontWeight,
"fontScale": fontScale
}, null, 2)
onPublishTypographyChanged: typographyPublishDebounce.restart()
onTypographyJsonChanged: typographyPublishDebounce.restart()
FileView {
id: typographyFileView
path: root.publishTypography ? root.stateDir + "/dms-typography.json" : ""
blockLoading: false
printErrors: false
}
Timer {
id: typographyPublishDebounce
interval: 250
repeat: false
onTriggered: {
if (!root.publishTypography)
return;
typographyFileView.setText(root.typographyJson);
}
}
IpcHandler {
target: "theme"
+1 -1
View File
@@ -439,7 +439,7 @@ function mangoActiveWorkspace(raw, screenName) {
function mangoWindowsOnWorkspace(windows, workspace) {
// mangoTags are 1-based; workspace.id is 0-based.
return windows.filter(win => win && (win.mangoTags || []).includes(workspace.id + 1));
return windows.filter(win => win && win.mangoMonitor === workspace.output && (win.mangoTags || []).includes(workspace.id + 1));
}
function i3Key(ws) {
+6 -2
View File
@@ -744,8 +744,12 @@ Item {
readonly property real barSurfaceThickness: Theme.px(effectiveBarThickness + surfaceSpacing + ((renderBarConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel && spansEdge ? _wingR : 0), _dpr) + _shadowBuffer
readonly property bool islandSheetOut: islandHost?.sheetOut ?? false
readonly property real islandSatelliteShadowBuffer: isIsland && islandSatellitesEnabled && !islandSatelliteBackground ? _shadowBuffer : 0
readonly property real hostThickness: isIsland ? Math.max(islandHost?.hostThickness ?? 0, islandStripThickness + islandSatelliteShadowBuffer) : Math.max(barSurfaceThickness, islandSheetOut ? islandHost.hostThickness : 0)
readonly property real hideSlideThickness: isIsland ? hostThickness : barSurfaceThickness
// A grown layer surface jumps to its new origin before the client draws at the new size, so a far-edge host never grows for the sheet.
readonly property bool islandReservesSheet: !!islandHost && (axis.edge === "bottom" || axis.edge === "right")
readonly property real hostThickness: isIsland ? Math.max(islandHost?.hostThickness ?? 0, islandStripThickness + islandSatelliteShadowBuffer) : Math.max(barSurfaceThickness, islandSheetOut || islandReservesSheet ? islandHost.hostThickness : 0)
// The host reserves the expanded island's height, so sliding by it would move a compact island off-screen within a frame.
readonly property real islandCompactThickness: Math.max(islandStripThickness + islandSatelliteShadowBuffer, (islandHost?.outerGap ?? 0) + (islandHost?.compactThickness ?? 0) + (islandHost?.shadowPadding ?? 0))
readonly property real hideSlideThickness: isIsland ? islandCompactThickness : barSurfaceThickness
readonly property real surfaceImplicitHeight: !isVertical ? hostThickness : 0
readonly property real surfaceImplicitWidth: isVertical ? hostThickness : 0
+11 -17
View File
@@ -39,26 +39,20 @@ BasePill {
return Qt.rgba(from.r + (to.r - from.r) * progress, from.g + (to.g - from.g) * progress, from.b + (to.b - from.b) * progress, from.a + (to.a - from.a) * progress);
}
SequentialAnimation on criticalPulse {
property int criticalTick: 0
// Stepped on purpose: an Animation repaints the bar at the full refresh rate.
Timer {
interval: 50
repeat: true
running: battery.critical && battery.surfaceLive && !SettingsData.reduceMotion
loops: Animation.Infinite
onRunningChanged: {
if (!running)
battery.criticalPulse = 0;
battery.criticalTick = 0;
battery.criticalPulse = 0;
}
NumberAnimation {
from: 0
to: 1
duration: 1000
easing.type: Easing.Linear
}
NumberAnimation {
from: 1
to: 0
duration: 1000
easing.type: Easing.Linear
onTriggered: {
battery.criticalTick = (battery.criticalTick + 1) % 40;
battery.criticalPulse = 1 - Math.abs(20 - battery.criticalTick) / 20;
}
}
@@ -375,7 +375,7 @@ BasePill {
}
property bool isOccupied: root.useNativeWorkspaces && CompositorService.workspaceOccupied(record)
property bool isPlaceholder: !!(record && record.placeholder)
property bool isHovered: mouseArea.containsMouse
property bool isHovered: pillHover.hovered
property bool loadedIsUrgent: false
property bool isUrgent: root.useNativeWorkspaces ? CompositorService.workspaceUrgent(record, loadedIsUrgent) : (modelData?.urgent ?? false)
@@ -615,6 +615,11 @@ BasePill {
}
}
HoverHandler {
id: pillHover
enabled: !isPlaceholder
}
MouseArea {
id: mouseArea
anchors.fill: parent
+4 -1
View File
@@ -680,6 +680,8 @@ DPopout {
readonly property bool ready: targetHeight >= 0 && settledHeight === targetHeight
readonly property real availableHeight: root.screen ? root.screen.height - mainContainer.horizontalChromeHeight - DashMetrics.contentPadding * 2 - Theme.barHeight - Theme.spacingL * 2 : contentHeight
readonly property int rowBudget: DashMetrics.rowCapFor(availableHeight)
// Growing a panel not hung from a top bar moves the grid under a held card, which retargets the drop.
readonly property bool heightHeld: (currentItem?.cardInteracting ?? false) && root.effectiveBarPosition % 4 !== SettingsData.Top
x: (root.verticalNavigation && root.navigationEdge === SettingsData.Left ? root.navigationWidth : 0) - root.editGutter
y: (!root.verticalNavigation && root.navigationEdge !== SettingsData.Bottom ? mainContainer.horizontalChromeHeight : 0) - root.editGutter
@@ -691,12 +693,13 @@ DPopout {
clip: contentHeight > height
function updateContentHeight() {
if (targetHeight < 0)
if (targetHeight < 0 || heightHeld)
return;
settledHeight = targetHeight;
}
onTargetHeightChanged: Qt.callLater(updateContentHeight)
onHeightHeldChanged: Qt.callLater(updateContentHeight)
Column {
id: emptyContent
+1
View File
@@ -214,6 +214,7 @@ Singleton {
readonly property int mediaLyricsRequestTimeout: 16000
readonly property int mediaLyricsLoadingDelay: 300
readonly property real mediaLyricsPositionTolerance: 0.05
readonly property real mediaLyricsWholeSecondSlack: 0.005
readonly property int mediaLyricsPositionProbeInterval: 5000
readonly property real mediaLyricsPositionProbeDrift: 1.5
readonly property real mediaLyricsPositionProbeThreshold: 1
@@ -25,6 +25,7 @@ Item {
readonly property Item focusTarget: grid
readonly property bool blocksTabNavigation: editMode || addMenu.open || optionsSheet.shown
readonly property bool cardResizing: grid.sizePreview !== null
readonly property bool cardInteracting: grid.interacting
implicitHeight: Math.max(DashMetrics.tabMinHeight, grid.implicitHeight)
LayoutMirroring.enabled: I18n.isRtl
+1 -1
View File
@@ -123,7 +123,7 @@ Item {
interval: DashMetrics.mediaPositionPollInterval
running: root.live && root.activePlayer?.playbackState === MprisPlaybackState.Playing && !root.isSeeking
repeat: true
onTriggered: root.activePlayer?.positionChanged()
onTriggered: MprisController.refreshPosition(root.activePlayer)
}
function syncAudioRef(wanted) {
@@ -53,7 +53,7 @@ Card {
interval: DashMetrics.mediaPositionPollInterval
running: root.live && root.visible && root.interactive && root.showSeekbar && root.playing && !root.isSeeking
repeat: true
onTriggered: root.activePlayer?.positionSupported && root.activePlayer.positionChanged()
onTriggered: MprisController.refreshPosition(root.activePlayer)
}
Loader {
+1
View File
@@ -26,6 +26,7 @@ FocusScope {
readonly property var addable: grid.addableEntries
readonly property int usedColumns: grid.usedColumns
readonly property bool cardResizing: grid.sizePreview !== null
readonly property bool cardInteracting: grid.interacting
readonly property bool blocksTabNavigation: grid.blocksTabNavigation || detailSheet.shown || editorSheet.shown || optionsSheet.shown || addMenu.open
implicitWidth: DashMetrics.contentWidthFor(SettingsData.showWeekNumber)
+1
View File
@@ -36,6 +36,7 @@ Item {
}
]
readonly property bool blocksTabNavigation: widgets.blocksTabNavigation
readonly property bool cardInteracting: widgets.cardInteracting
readonly property var forecastOptions: [DashRegistry.choice("forecast", I18n.tr("Forecast", "weather widget option label for forecast display type"), "chart", [
{
"value": "chart",
@@ -55,7 +55,7 @@ Rectangle {
function saveDisplayImage() {
const data = root.notificationData;
if (!root.persistImage || !data?.hasDisplayImage || data.persistedImagePath)
if (!root.persistImage || !data?.notification || !data.hasDisplayImage || data.persistedImagePath)
return;
const image = data.image || "";
if (!image.startsWith("image://qsimage/") && !NotificationService.notificationIconFromImage(image).startsWith("/"))
@@ -61,6 +61,7 @@ PanelWindow {
readonly property bool cardShown: presenting && (presentationProgress > 0 || !exiting)
readonly property bool layoutPinned: !exiting && (hovered || contextMenuActive || swipeActive)
property bool exiting: false
property var exitSnapshot: null
property bool exitStarted: false
property bool collapseChromeOnExit: false
property bool _isDestroying: false
@@ -124,6 +125,7 @@ PanelWindow {
}
enterAnimation.stop();
enterDelay.stop();
exitSnapshot = snapshotNotification(notificationData);
exiting = true;
if (NotificationService.removeFromVisibleNotifications)
NotificationService.removeFromVisibleNotifications(win.notificationData);
@@ -172,6 +174,29 @@ PanelWindow {
win.exitFinished();
}
// The wrapper is destroyed as soon as its notification closes, which would blank the card mid-exit.
function snapshotNotification(data) {
if (!data)
return null;
return {
"appName": data.appName,
"appIcon": data.appIcon,
"desktopEntry": data.desktopEntry,
"summary": data.summary,
"body": data.body,
"htmlBody": data.htmlBody,
"image": data.image,
"hasDisplayImage": data.hasDisplayImage,
"displayImage": data.displayImage,
"persistedImagePath": data.persistedImagePath,
"timeStr": data.timeStr,
"urgency": data.urgency,
"actions": Array.from(data.actions || [], action => ({
"text": action.text
}))
};
}
function closeTransientUi() {
transientSurfaces.closeAll();
popupContextMenuLoader.active = false;
@@ -719,7 +744,7 @@ PanelWindow {
chipColor: Theme.notificationChipSurface
width: parent.width
height: win.inlineHeightAnimating ? Math.min(targetHeight, cardSurface.height - win.timeoutRailClearance) : targetHeight
notificationData: win.notificationData
notificationData: win.exitSnapshot ?? win.notificationData
descriptionExpanded: win.descriptionExpanded
privacyMode: SettingsData.notificationPopupPrivacyMode
bodyInvokesAction: win.bodyClickInvokesAction
@@ -557,6 +557,13 @@ QtObject {
}
Component.onCompleted: _sync(NotificationService.visibleNotifications)
Component.onDestruction: {
for (const p of popupWindows)
p?.destroy();
const screenName = manager.modelData?.name || "";
if (screenName)
ConnectedModeState.releaseSurface(screenName, "notification", "notification:" + screenName);
}
onPopupSpacingChanged: _repositionAll()
onNotificationConnectedModeChanged: _scheduleNotificationChromeSync()
onCloseGapNotificationsChanged: _scheduleNotificationChromeSync()
+35 -20
View File
@@ -12,6 +12,7 @@ Column {
property var parentModal: null
property string editingDockId: ""
property string renameDraft: ""
readonly property var dockPages: ["dock_general", "dock_widgets", "dock_appearance"].map(id => SettingsTabs.page(id)).filter(page => page)
function finishRename(value) {
const id = editingDockId;
@@ -44,11 +45,10 @@ Column {
}
SettingsCard {
width: parent.width
iconName: "dock_to_bottom"
title: I18n.tr("Docks", "noun plural, settings card title listing configured docks")
settingKey: "dockConfiguration"
tags: ["dock", "configuration", "add", "remove", "name", "show", "enable"]
tags: ["dock", "configuration", "add", "remove", "show", "enable"]
headerActions: DButton {
text: I18n.tr("Add", "verb, button that adds a new item to a list")
iconName: "add"
@@ -75,11 +75,18 @@ Column {
onDeleteRequested: root.removeDock(modelData.id)
}
}
}
SettingsCard {
iconName: "dock_to_bottom"
title: dock.config?.name ?? ""
settingKey: "dockPlacement"
tags: ["dock", "layout", "placement", "position", "edge", "name", "rename"]
visible: dock.hasConfig
SettingsRow {
title: I18n.tr("Name")
subtitle: root.editingDockId ? "" : dock.config?.name ?? ""
visible: dock.hasConfig
DActionButton {
iconName: root.editingDockId ? "check" : "edit"
@@ -120,25 +127,33 @@ Column {
}
}
}
}
SettingsCard {
width: parent.width
visible: dock.hasConfig
iconName: "dock_to_bottom"
title: I18n.tr("Layout")
settingKey: "dockPlacement"
tags: ["dock", "layout", "placement", "position", "edge"]
SettingsRow {
title: I18n.tr("Layout", "noun, settings section title for arrangement options")
SettingsLayoutPicker {
edgePlacement: true
choices: [SettingsData.Position.Top, SettingsData.Position.Bottom, SettingsData.Position.Left, SettingsData.Position.Right].map(position => ({
key: String(position),
label: dock.positionLabel(position),
enabled: dock.positionChoices.includes(position)
}))
selectedKey: String(dock.config?.position ?? SettingsData.Position.Bottom)
onSelected: key => dock.setEnabled(true, Number(key))
body: SettingsLayoutPicker {
edgePlacement: true
choices: [SettingsData.Position.Top, SettingsData.Position.Bottom, SettingsData.Position.Left, SettingsData.Position.Right].map(position => ({
key: String(position),
label: dock.positionLabel(position),
enabled: dock.positionChoices.includes(position)
}))
selectedKey: String(dock.config?.position ?? SettingsData.Position.Bottom)
onSelected: key => dock.setEnabled(true, Number(key))
}
}
Repeater {
model: root.dockPages
delegate: SettingsNavRow {
required property var modelData
iconName: modelData.icon
title: modelData.text
hint: modelData.hint ?? ""
onClicked: keyboard => root.parentModal?.navigateTo(modelData.id, keyboard)
}
}
}
}
@@ -399,12 +399,16 @@ Column {
readonly property var fillModes: ["Stretch", "Fit", "Fill", "Scrolling", "Tile", "TileVertically", "TileHorizontally", "Pad"]
readonly property var fillModeLabels: [I18n.tr("Stretch", "wallpaper fill mode"), I18n.tr("Fit", "wallpaper fill mode"), I18n.tr("Fill", "wallpaper fill mode"), I18n.tr("Scroll", "wallpaper fill mode"), I18n.tr("Tile", "wallpaper fill mode"), I18n.tr("Tile Vertically", "wallpaper fill mode"), I18n.tr("Tile Horizontally", "wallpaper fill mode"), I18n.tr("Pad", "wallpaper fill mode")]
// dms picks the animation driver at launch from this setting
readonly property bool simpleAnimationDriver: Quickshell.env("QSG_USE_SIMPLE_ANIMATION_DRIVER") === "1"
readonly property bool driverMismatch: (SettingsData.wallpaperFillMode === "Scrolling") !== simpleAnimationDriver
tab: "wallpaper"
tags: ["background", "fill", "fit", "stretch", "tile", "scale"]
settingKey: "wallpaperFillMode"
resetKeys: root.perMonitor ? [] : ["wallpaperFillMode"]
text: I18n.tr("Fill mode")
description: driverMismatch ? I18n.tr("Requires restart") : ""
visible: root.wallpaperIsImage
dropdownWidth: 190
options: fillModeLabels
@@ -17,10 +17,7 @@ SettingsDropdownRow {
function _enumerate() {
if (_enumerated)
return;
const bundled = DCommon.Fonts.bundledFamilies.filter(f => f !== root.defaultFamily);
const system = Qt.fontFamilies().filter(f => !f.startsWith(".") && f !== root.defaultFamily && !bundled.includes(f));
system.sort();
_families = ["Default"].concat(bundled, system);
_families = ["Default"].concat(DCommon.Fonts.selectableFamilies(root.defaultFamily));
_enumerated = true;
}
+1 -1
View File
@@ -468,7 +468,7 @@ Singleton {
const yKey = y.niriWindowId !== undefined ? y.niriWindowId : y.mangoWindowId;
if (xKey === undefined || yKey === undefined)
return false;
if (xKey !== yKey || x.niriWorkspaceId !== y.niriWorkspaceId || x.appId !== y.appId || x.title !== y.title || !!x.activated !== !!y.activated || !!x.fullscreen !== !!y.fullscreen || !!x.maximized !== !!y.maximized || !!x.minimized !== !!y.minimized)
if (xKey !== yKey || x.niriWorkspaceId !== y.niriWorkspaceId || x.mangoMonitor !== y.mangoMonitor || x.appId !== y.appId || x.title !== y.title || !!x.activated !== !!y.activated || !!x.fullscreen !== !!y.fullscreen || !!x.maximized !== !!y.maximized || !!x.minimized !== !!y.minimized)
return false;
}
return true;
+13 -1
View File
@@ -90,7 +90,11 @@ QtObject {
if (enabled && (state === "none" || state === "error"))
useEmbeddedText(state);
}
onPositionChanged: scheduleAnchor()
onPositionChanged: {
if (MprisController.refreshingPosition || flooredEcho())
return;
scheduleAnchor();
}
onRateChanged: updatePlaybackClock()
onPlayingChanged: {
probe = null;
@@ -379,6 +383,7 @@ QtObject {
if (!line || !Number.isFinite(line.t))
return false;
player.position = Math.max(0.1, Math.min(line.t, duration * 0.99));
reanchor();
return true;
}
@@ -434,6 +439,13 @@ QtObject {
resync();
}
// pear-desktop floors Position to whole seconds on pause, resume and track events.
function flooredEcho() {
const lead = currentTime() - position;
const whole = Math.abs(position - Math.round(position)) < DashMetrics.mediaLyricsWholeSecondSlack;
return whole && lead >= 0 && lead < 1 + DashMetrics.mediaLyricsPositionTolerance;
}
function scheduleAnchor() {
positionWall = Date.now();
if (enabled && synced && !stopped && !anchorUpdate.running)
+10 -1
View File
@@ -34,6 +34,15 @@ Singleton {
property bool _mprisPublishDirty: false
property int _mprisConnectionEpoch: 0
property int positionConsumers: 0
property bool refreshingPosition: false
function refreshPosition(player) {
if (!player?.positionSupported)
return;
refreshingPosition = true;
player.positionChanged();
refreshingPosition = false;
}
function addPositionRef() {
positionConsumers++;
@@ -398,7 +407,7 @@ Singleton {
interval: 1000
running: root.positionConsumers > 0 && root.activePlayer?.playbackState === MprisPlaybackState.Playing
repeat: true
onTriggered: root.activePlayer?.positionChanged()
onTriggered: root.refreshPosition(root.activePlayer)
}
function _hasMPRISSubscription(): bool {
+9
View File
@@ -334,9 +334,18 @@ Singleton {
}
}
if (sameScales(displayScales, scales))
return;
displayScales = scales;
}
function sameScales(a, b) {
const names = Object.keys(a);
if (names.length !== Object.keys(b).length)
return false;
return names.every(name => a[name] === b[name]);
}
function sortWindowsByLayout(windowList) {
const enriched = windowList.map(w => {
const ws = workspaces[w.workspace_id];
+1 -1
View File
@@ -227,7 +227,7 @@ Item {
isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
onFrameTicked: {
if (!root.isSeeking)
activePlayer.positionChanged();
MprisController.refreshPosition(activePlayer);
}
MouseArea {
@@ -12,7 +12,7 @@ local config_home = vim.env.XDG_CONFIG_HOME
if config_home == nil or #config_home == 0 then
config_home = vim.fs.joinpath(vim.env.HOME, ".config")
end
local settings_file_path = vim.fs.joinpath(config_home, "Dux Shell", "settings.json")
local settings_file_path = vim.fs.joinpath(config_home, "dms", "settings.json")
local settings_file = io.open(settings_file_path, "r")
if settings_file == nil then
vim.notify(
+2 -2
View File
@@ -36,8 +36,8 @@
}
},
"windows": [
{ "appId": "alacritty", "mangoTags": [3] },
{ "appId": "firefox", "mangoTags": [2, 3] },
{ "appId": "alacritty", "mangoMonitor": "DP-1", "mangoTags": [3] },
{ "appId": "firefox", "mangoMonitor": "DP-1", "mangoTags": [2, 3] },
{ "appId": "untagged" }
]
},
+8
View File
@@ -20,6 +20,14 @@ const aqueousService = source("../Services/AqueousService.qml");
const model = vm.createContext({});
vm.runInContext(source("../Common/WindowModel.js").replace(/^\.pragma.*$/m, ""), model);
test("mango monitor changes invalidate cached toplevels", () => {
const context = vm.createContext({});
lift(compositorService, "_toplevelListEquivalent", context);
const window = { mangoWindowId: 1, appId: "app1", mangoMonitor: "DP-1", mangoTags: [1] };
assert.equal(context._toplevelListEquivalent([window], [{ ...window }]), true);
assert.equal(context._toplevelListEquivalent([window], [{ ...window, mangoMonitor: "HDMI-A-1" }]), false);
});
function niriState(scenario) {
const s = fixture("niri")[scenario];
const toplevels = s.toplevels.map(t => ({ ...t }));
+17 -2
View File
@@ -291,8 +291,23 @@ test("mango scroll targets, active tag and windows", () => {
assert.deepEqual(plain(model.mangoActiveWorkspace(dp1, "DP-1")), { id: 2, idx: 3, name: "", output: "DP-1", active: true, placeholder: false });
assert.equal(model.mangoActiveWorkspace(mango("available", "HDMI-A-1"), "HDMI-A-1").id, 0);
assert.equal(model.mangoActiveWorkspace(mango("available", "DP-2"), "DP-2"), null);
assert.deepEqual(pick(model.mangoWindowsOnWorkspace(dp1.windows, { id: 2 }), "appId"), ["alacritty", "firefox"]);
assert.deepEqual(pick(model.mangoWindowsOnWorkspace(dp1.windows, { id: 1 }), "appId"), ["firefox"]);
assert.deepEqual(pick(model.mangoWindowsOnWorkspace(dp1.windows, { id: 2, output: "DP-1" }), "appId"), ["alacritty", "firefox"]);
assert.deepEqual(pick(model.mangoWindowsOnWorkspace(dp1.windows, { id: 1, output: "DP-1" }), "appId"), ["firefox"]);
});
test("mango workspace apps follow their output when windows share a tag", () => {
const windows = [
{ appId: "app1", mangoMonitor: "DP-1", mangoTags: [1] },
{ appId: "app2", mangoMonitor: "HDMI-A-1", mangoTags: [1] }
];
const dp1 = model.mangoRecord(0, {}, "DP-1");
const hdmi = model.mangoRecord(0, {}, "HDMI-A-1");
assert.deepEqual(pick(model.mangoWindowsOnWorkspace(windows, dp1), "appId"), ["app1"]);
assert.deepEqual(pick(model.mangoWindowsOnWorkspace(windows, hdmi), "appId"), ["app2"]);
windows[0].mangoMonitor = "HDMI-A-1";
assert.deepEqual(pick(model.mangoWindowsOnWorkspace(windows, dp1), "appId"), []);
assert.deepEqual(pick(model.mangoWindowsOnWorkspace(windows, hdmi), "appId"), ["app1", "app2"]);
});
test("i3 lists order numbered before named and strip the number prefix", () => {
@@ -286,6 +286,11 @@ def extract_i18n_string(value):
return None
def is_status_notice(value):
"""A description with an empty branch only shows while some state holds; it is not searchable text."""
return re.search(r"""\?\s*(""|'')\s*:|:\s*(""|'')\s*$""", value.strip()) is not None
def extract_tags(value):
match = re.search(r"\[([^\]]+)\]", value)
if not match:
@@ -489,7 +494,7 @@ def find_settings_components(content, filename, wrappers, tab_meta, hub_meta):
desc_raw = extract_property(own, "description") or extract_property(own, "summary") or defaults.get("description") or borrowed_description(block, setting_key)
description = None
if desc_raw:
if desc_raw and not is_status_notice(desc_raw):
description = extract_i18n_string(desc_raw)
visible_raw = extract_property(own_scope(block), "visible")
@@ -941,6 +941,26 @@
"icon": "dock_to_bottom",
"description": "Position, pinned apps, trash"
},
{
"section": "dockPlacement",
"label": "Dock",
"tabIndex": -1,
"category": "Dock",
"keywords": [
"dock",
"edge",
"launcher bar",
"layout",
"name",
"panel",
"placement",
"position",
"rename",
"taskbar"
],
"page": "dock",
"icon": "dock_to_bottom"
},
{
"section": "dockConfiguration",
"label": "Docks",
@@ -953,7 +973,6 @@
"docks",
"enable",
"launcher bar",
"name",
"panel",
"remove",
"show",
@@ -1407,24 +1426,6 @@
"page": "theme_surfaces",
"description": "Faint edge on cards, pills and widgets"
},
{
"section": "dockPlacement",
"label": "Layout",
"tabIndex": -1,
"category": "Dock",
"keywords": [
"dock",
"edge",
"launcher bar",
"layout",
"panel",
"placement",
"position",
"taskbar"
],
"page": "dock",
"icon": "dock_to_bottom"
},
{
"section": "iconThemeLight",
"label": "Light mode theme",
@@ -11275,7 +11276,6 @@
"tabIndex": 25,
"category": "Gamma control",
"keywords": [
"available",
"blue light",
"color",
"color temperature",
@@ -11290,15 +11290,13 @@
"output",
"outputs",
"redshift",
"requires",
"resolution",
"screen",
"screens",
"temperature",
"warm"
],
"parentLabel": "Displays",
"description": "Gamma control not available. Requires DMS API v6+."
"parentLabel": "Displays"
},
{
"section": "nightModeTransitionMinutes",
@@ -15896,12 +15894,10 @@
"schedule",
"settings",
"share",
"shared",
"tint",
"wallpaper"
],
"parentLabel": "Wallpaper & colors",
"description": "Using shared settings from Gamma Control"
"parentLabel": "Wallpaper & colors"
},
{
"section": "wallpaperCycling",
+1 -1
View File
@@ -19,7 +19,7 @@ if [[ -n "${QS_ARGS:-}" ]]; then
# shellcheck disable=SC2206
qs_args=($QS_ARGS)
else
state_file="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/danklinux.path"
state_file="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/dms.path"
config_path="$(cat "$state_file" 2>/dev/null || true)"
[[ -n "$config_path" ]] || { echo "no running dms shell ($state_file missing)" >&2; exit 1; }
qs_args=(-p "$config_path")