Compare commits

...
17 Commits
Author SHA1 Message Date
purian23 47c9d8d89b notifications: keep popup content while it exits after the notification closes
Port 2.0
2026-10-21 21:58:13 -04:00
bbedward d8786eaaf7 bar: keep far-edge island hosts at sheet size so the bar never jumps on expand
fixes #3855
port 2.0
2026-10-21 21:57:20 -04:00
Anton Kesy 4f969c8e10 docs: use dux project names in README (#3847) 2026-10-21 15:50:06 -04:00
bbedward 1b59a474dd battery: throttle pulse animation to 20fps
ref #3849
2026-10-21 13:59:43 -04:00
bbedward 28f921fa02 dash: hold panel height while dragging cards unless hung from a top bar
lyrics: ignore floored position samples and shell refresh emits
2026-10-21 13:15:53 -04:00
bbedward 8f2a486006 theme: publish typography for dux apps 2026-10-21 12:26:05 -04:00
bbedward 27a703367f core: only use the simple animation driver for the scrolling wallpaper
fixes #3837
2026-10-21 11:18:18 -04:00
bbedward 8e851b3dc4 bar: slide an auto-hidden island by its compact height
fixes #3838
2026-10-21 11:05:57 -04:00
bbedward 112d05901f workspaces: keep the pill highlight while hovering its app icons
fixes #3839
2026-10-21 10:39:40 -04:00
bbedward afeea489c9 theme: skip matugen runs whose inputs match the last applied run
fixes #3841
2026-10-21 10:34:22 -04:00
bbedward 90d3c9731c niri: only emit displayScales when a scale changed
ref #3842
port 2.0
2026-10-21 10:14:52 -04:00
Marlon Kranz b81954454a fix(notifications): close popups when their screen stops showing them (#3844) 2026-10-21 08:02:49 -04:00
Adarsh 479b1434de Theme Antigravity IDE via the VS Code extension (#3836) 2026-10-21 03:57:45 -04:00
Tomás 203b1369b3 Keep MangoWM workspace apps on their own monitor (#3832)
port 2.0
2026-10-20 20:54:58 -04:00
Leonard 5ffc581ecf refactor(ui): match the dock settings page with the bar settings page (#3829)
port 2.0
2026-10-20 20:54:40 -04:00
bbedward 3284acab28 matugen: neovim template reads the dms config dir"
port 2.0
2026-10-20 17:03:12 -04:00
bbedward 1fb9f5271a ci: cache go modules from core/go.sum, retry downloads 2026-10-20 15:24:59 -04:00
40 changed files with 283 additions and 102 deletions
+5
View File
@@ -26,6 +26,11 @@ jobs:
uses: actions/setup-go@v6
with:
go-version-file: core/go.mod
cache-dependency-path: core/go.sum
- name: Download Go modules
working-directory: core
run: for attempt in 1 2 3; do go mod download && exit 0; sleep 15; done; exit 1
- name: run pre-commit hooks
uses: j178/prek-action@v2
+2
View File
@@ -73,6 +73,7 @@ jobs:
uses: actions/setup-go@v6
with:
go-version-file: ./core/go.mod
cache-dependency-path: core/go.sum
- name: Format check
if: matrix.goos == 'linux'
@@ -247,6 +248,7 @@ jobs:
uses: actions/setup-go@v6
with:
go-version-file: ./core/go.mod
cache-dependency-path: core/go.sum
- name: Download core artifacts
uses: actions/download-artifact@v5
+1
View File
@@ -308,6 +308,7 @@ jobs:
uses: actions/setup-go@v6
with:
go-version-file: ./core/go.mod
cache-dependency-path: core/go.sum
- name: Install OSC
run: |
+1
View File
@@ -100,6 +100,7 @@ jobs:
uses: actions/setup-go@v6
with:
go-version-file: ./core/go.mod
cache-dependency-path: core/go.sum
cache: false
- name: Install build dependencies
+7 -7
View File
@@ -100,7 +100,7 @@ Notification center with grouping, rich text support, and keyboard navigation.
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
**Session Management**
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and a settings front-end for [dank-greeter](https://github.com/duxlabs-dev/dux-greeter).
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and a settings front-end for [dux-greeter](https://github.com/duxlabs-dev/dux-greeter).
**Plugin System**
Extend functionality with the [plugin registry](https://plugins.duxlabs.dev). DMS keeps
@@ -138,16 +138,16 @@ dms plugins restore ~/plugins.lock.json
- **Plugins:** [Development guide](https://duxlabs.dev/docs/dms/plugins-overview)
- **Support:** [Ko-fi](https://ko-fi.com/duxlabs)
## Dank Projects
## Dux Projects
DMS is one piece of the suite. The rest lives in its own repos:
- [dank-greeter](https://github.com/duxlabs-dev/dux-greeter) - greetd login screen with the Dux Shell aesthetic. The Greeter tab in DMS settings is the front-end for it.
- [dankcalendar](https://github.com/duxlabs-dev/dux-calendar) - Local, Google, Microsoft, and CalDAV calendars for the Dux desktop.
- [dux-greeter](https://github.com/duxlabs-dev/dux-greeter) - greetd login screen with the Dux Shell aesthetic. The Greeter tab in DMS settings is the front-end for it.
- [dux-calendar](https://github.com/duxlabs-dev/dux-calendar) - Local, Google, Microsoft, and CalDAV calendars for the Dux desktop.
- [dgop](https://github.com/duxlabs-dev/dux-gop) - System monitoring TUI and Go library; its library powers the process list and dashboard widgets inside the dms daemon.
- [dsearch](https://github.com/duxlabs-dev/dux-search) - Fast file search that powers file results in the launcher.
- [dux-qml-common](https://github.com/duxlabs-dev/dux-qml-common) - Shared QML widgets and components used by DMS, dank-greeter, and dankcalendar.
- [dankgo](https://github.com/duxlabs-dev/dux-go) - Common Go modules behind the single binary apps.
- [dux-qml-common](https://github.com/duxlabs-dev/dux-qml-common) - Shared QML widgets and components used by DMS, dux-greeter, and dux-calendar.
- [dux-go](https://github.com/duxlabs-dev/dux-go) - Common Go modules behind the single binary apps.
## Development
@@ -193,7 +193,7 @@ Contributions welcome. Bug fixes, widgets, features, documentation, and plugins
3. Test thoroughly
4. Open a pull request
For documentation contributions, see [DankLinux-Docs](https://github.com/duxlabs-dev/dux-docs).
For documentation contributions, see [dux-docs](https://github.com/duxlabs-dev/dux-docs).
## Credits
+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")