mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-24 03:57:46 -04:00
updated to support workspace options better in aqueous
This commit is contained in:
1 parent
2ac1fe670c
commit
a39196e992
10 files changed
+299
-36
No files matched your search
+45
-22
@@ -5,9 +5,11 @@ This working tree integrates DMS with Aqueous master
|
||||
no released minimum Aqueous or DMS version has been established.
|
||||
|
||||
The tested DMS baseline is `5baef07048656867a374d210d4305911b8a76e4f`, with
|
||||
common QML `26396ce432d6c71c3f5367438f96f4a8d667e160` plus the working-tree
|
||||
optional `parentWindow` compatibility fix. The runtime was Arch's
|
||||
`noctalia-qs 0.0.12`; its version output supplies no source revision.
|
||||
common QML `26396ce432d6c71c3f5367438f96f4a8d667e160` unchanged. The current
|
||||
verification uses Quickshell `2d3b3e9c70ef380dff751b61d334dc88df016c29`, built
|
||||
locally with its existing `parentWindow` API. Earlier runs used Arch's
|
||||
`noctalia-qs 0.0.12` with temporary popup compatibility edits; those edits have
|
||||
been removed. See [the implementation audit](aqueous-minimal-change-audit.md).
|
||||
The Aqueous compositor, CLI and `aqueous-config` helper were built from the same
|
||||
pinned master. The helper reports 0.7.1, protocol 1. The system-installed older
|
||||
Aqueous and helper binaries were not used for integration testing.
|
||||
@@ -121,22 +123,41 @@ For screenshots use `dms screenshot window --seat SEAT` or
|
||||
active-window capture errors. Selected-output routing still works on empty
|
||||
workspaces and during layer focus.
|
||||
|
||||
WorkspaceSwitcher consumes a shared service-owned view for Aqueous and
|
||||
ext-workspace. Aqueous rows include authoritative window membership and captured
|
||||
session identities. Ext rows use native object identity, including when IDs are
|
||||
absent or names repeat; membership remains unknown rather than being guessed from
|
||||
Aqueous labels. Padding, labels, activation and wheel navigation share the same
|
||||
widget path. Aqueous icon grouping, sizing and occupancy use the same eligible
|
||||
window list, with captured window actions retained through rendering.
|
||||
WorkspaceSwitcher uses direct Aqueous branches alongside its existing compositor
|
||||
branches. AqueousService supplies authoritative window/workspace membership and
|
||||
session identities; the existing ext-workspace path keeps native handles. No
|
||||
shared workspace adapter or new provider interface is required. Aqueous icons
|
||||
reuse the widget's existing grouping, cached icon list, sizing and hit-test path,
|
||||
carrying the captured session with the window ID.
|
||||
|
||||
Set `DMS_FORCE_EXTWS=1` in the shell's launch environment to select ext-workspace
|
||||
even when the Aqueous adapter is healthy. If the protocol is unavailable, the
|
||||
forced Aqueous workspace view is unavailable. Without the override, a disconnected
|
||||
forced workspace path is unavailable. Without the override, a disconnected
|
||||
Aqueous adapter falls back to ext-workspace when available and returns to its
|
||||
richer model after reconnection. The override changes workspace selection only;
|
||||
other Aqueous consumers and native overview remain available. Ext activation uses
|
||||
the protocol's native method and does not expose explicit-seat selection.
|
||||
|
||||
Workspace settings expose app icons, follow-monitor-focus, occupied-only filtering,
|
||||
reverse scrolling and workspace state colors. The app-icon and occupied-only
|
||||
switches are disabled while the Aqueous model is unavailable or `DMS_FORCE_EXTWS=1`,
|
||||
because the generic workspace path has no authoritative window membership.
|
||||
Saved preferences are retained for when the Aqueous model is used again.
|
||||
|
||||
The existing workspace rename dialog supports Aqueous:
|
||||
|
||||
```sh
|
||||
dms ipc call workspace-rename open
|
||||
dms ipc call workspace-rename toggle
|
||||
dms ipc call workspace-rename close
|
||||
```
|
||||
|
||||
Opening captures the selected seat's active workspace ID and compositor session.
|
||||
Changing focus while the dialog is open does not redirect the rename. The dialog
|
||||
closes after an `applied` result and retains the draft on failure. Missing state,
|
||||
ambiguous seat selection, lock or observation-only policy prevents opening it.
|
||||
Rename changes runtime state; it does not write persistent configuration.
|
||||
|
||||
Native overview controls are available through workspace right-click and:
|
||||
|
||||
```sh
|
||||
@@ -182,18 +203,20 @@ The following passed against the pinned build on 2026-09-05:
|
||||
switching, overview, active-window crops, helper validation/save/conflicts,
|
||||
keybind writes, one QML-owned watcher, reconnect after killing the watcher,
|
||||
orderly logout and process cleanup.
|
||||
- Shared workspace tests reproduce the prior override, padding and icon-count
|
||||
failures. The corrected default and forced-ext paths pass real QML widget checks
|
||||
for activation/wheel navigation, duplicate-name renames, output-following and
|
||||
independent overview. Default mode additionally checks grouping, padding,
|
||||
horizontal/vertical sizing, actual icon actions and stale cached actions across
|
||||
fallback/reconnect. Forced-ext also passed with XWayland capture.
|
||||
- Workspace tests exercise the established compositor selection, the forced-ext
|
||||
override, Aqueous padding/grouping/icon counts, native handle identity and
|
||||
captured sessions. The runtime harness exercises the actual icon hit test,
|
||||
duplicate-name renames, output following and fallback/reconnect. Settings/dialog
|
||||
tests cover control visibility and search conditions, forced-ext membership
|
||||
controls, captured rename targets, retained drafts, lock/stale-target rejection
|
||||
and late callbacks after reopening. Real UI runs exercise the settings controls
|
||||
and rename IPC/dialog, including focus changes and correction after failure.
|
||||
|
||||
Specialized Niri/Hyprland/Mango/I3-family selection was checked with synthetic inputs;
|
||||
new live sessions for those compositors were not available for this correction.
|
||||
|
||||
The UI runs included the ungrouped taskbar, dock and keyboard widget.
|
||||
The XWayland DMS run also captured a synthetic XWayland window (632×612), with
|
||||
The XWayland DMS run also captured a synthetic XWayland window (404×304), with
|
||||
rotated/fractional native capture at 350×1145 and positive output origins.
|
||||
|
||||
Earlier standalone-bar runs produced 632×660 and 350×1205 window crops. The connected
|
||||
@@ -205,6 +228,7 @@ Reproduce the focused desktop checks with a Pixman-compatible diagnostic build:
|
||||
|
||||
```sh
|
||||
node quickshell/tests/workspace-view.test.mjs
|
||||
node quickshell/tests/workspace-settings.test.mjs
|
||||
node quickshell/tests/aqueous-service.test.mjs
|
||||
python3 scripts/test-aqueous-service.py
|
||||
node quickshell/tests/display-apply.test.mjs
|
||||
@@ -212,7 +236,7 @@ node quickshell/tests/aqueous-displays.test.mjs
|
||||
LD_LIBRARY_PATH=/path/to/patched-wlroots/lib \
|
||||
python3 scripts/test-aqueous-integration.py \
|
||||
--aqueous-source /path/to/Aqueous --bin-dir /path/to/matching/binaries
|
||||
# Add --force-ext to verify the shared widget through ext-workspace.
|
||||
# Add --force-ext to verify the workspace widget through ext-workspace.
|
||||
# Add --frame to exercise connected frame reservations.
|
||||
# Add --xwayland for the XWayland capture check (positive output origins).
|
||||
```
|
||||
@@ -232,6 +256,5 @@ a separate release gate. Headless checks do not establish those results.
|
||||
DankLinux-Docs support tables, packaging and plugin registry publication remain
|
||||
work for their owning repositories after release dependencies are established.
|
||||
|
||||
Keep the generic DPMS/result fixes and optional `parentWindow` compatibility changes
|
||||
independently reviewable. The common-QML fix is an uncommitted submodule change;
|
||||
an upstream landing will need a common-QML commit and coordinated dependency pin.
|
||||
Keep the generic DPMS/result and UWSM logout fixes independently reviewable.
|
||||
Popup compatibility changes are outside this integration and have been removed.
|
||||
@@ -1863,6 +1863,8 @@ Item {
|
||||
function open(): string {
|
||||
root.workspaceRenameModalLoader.active = true;
|
||||
if (root.workspaceRenameModalLoader.item) {
|
||||
if (CompositorService.isAqueous)
|
||||
return root.workspaceRenameModalLoader.item.show("") ? "WORKSPACE_RENAME_MODAL_OPENED" : "WORKSPACE_RENAME_UNAVAILABLE";
|
||||
const ws = NiriService.workspaces[NiriService.focusedWorkspaceId];
|
||||
root.workspaceRenameModalLoader.item.show(ws?.name || "");
|
||||
return "WORKSPACE_RENAME_MODAL_OPENED";
|
||||
@@ -1885,6 +1887,8 @@ Item {
|
||||
root.workspaceRenameModalLoader.item.hide();
|
||||
return "WORKSPACE_RENAME_MODAL_CLOSED";
|
||||
}
|
||||
if (CompositorService.isAqueous)
|
||||
return root.workspaceRenameModalLoader.item.show("") ? "WORKSPACE_RENAME_MODAL_OPENED" : "WORKSPACE_RENAME_UNAVAILABLE";
|
||||
const ws = NiriService.workspaces[NiriService.focusedWorkspaceId];
|
||||
root.workspaceRenameModalLoader.item.show(ws?.name || "");
|
||||
return "WORKSPACE_RENAME_MODAL_OPENED";
|
||||
|
||||
@@ -9,6 +9,8 @@ DankFloatingWindow {
|
||||
readonly property var log: Log.scoped("WorkspaceRenameModal")
|
||||
|
||||
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
||||
property var aqueousWorkspace: null
|
||||
property bool renaming: false
|
||||
|
||||
objectName: "workspaceRenameModal"
|
||||
title: I18n.tr("Rename Workspace")
|
||||
@@ -19,9 +21,24 @@ DankFloatingWindow {
|
||||
onClosed: hide()
|
||||
|
||||
function show(name) {
|
||||
if (CompositorService.isAqueous) {
|
||||
if (renaming)
|
||||
return false;
|
||||
const workspace = AqueousService.workspaces.find(ws => ws.output === AqueousService.seat?.output && ws.active);
|
||||
if (!AqueousService.available || !AqueousService.seat || !workspace || !AqueousService.capabilities.commands || AqueousService.locked) {
|
||||
ToastService.showError(I18n.tr("Error"), I18n.tr("Unavailable"));
|
||||
return false;
|
||||
}
|
||||
aqueousWorkspace = {
|
||||
id: workspace.id,
|
||||
session: workspace.aqueousSession
|
||||
};
|
||||
name = workspace.name;
|
||||
}
|
||||
nameInput.text = name;
|
||||
visible = true;
|
||||
Qt.callLater(() => nameInput.forceActiveFocus());
|
||||
return true;
|
||||
}
|
||||
|
||||
function hide() {
|
||||
@@ -29,6 +46,24 @@ DankFloatingWindow {
|
||||
}
|
||||
|
||||
function submitAndClose() {
|
||||
if (aqueousWorkspace || CompositorService.isAqueous) {
|
||||
if (!aqueousWorkspace || renaming)
|
||||
return;
|
||||
const target = aqueousWorkspace;
|
||||
renaming = true;
|
||||
AqueousService.command("workspace.rename", {
|
||||
id: target.id,
|
||||
session: target.session,
|
||||
name: nameInput.text
|
||||
}, success => {
|
||||
if (root.aqueousWorkspace !== target)
|
||||
return;
|
||||
root.renaming = false;
|
||||
if (success)
|
||||
root.hide();
|
||||
});
|
||||
return;
|
||||
}
|
||||
renameWorkspace(nameInput.text);
|
||||
hide();
|
||||
}
|
||||
@@ -49,6 +84,8 @@ DankFloatingWindow {
|
||||
return;
|
||||
}
|
||||
nameInput.text = "";
|
||||
aqueousWorkspace = null;
|
||||
renaming = false;
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
@@ -133,7 +170,7 @@ DankFloatingWindow {
|
||||
textColor: Theme.surfaceText
|
||||
placeholderText: I18n.tr("Workspace name")
|
||||
backgroundColor: "transparent"
|
||||
enabled: root.visible
|
||||
enabled: root.visible && !root.renaming
|
||||
onAccepted: submitAndClose()
|
||||
}
|
||||
}
|
||||
@@ -190,6 +227,7 @@ DankFloatingWindow {
|
||||
|
||||
MouseArea {
|
||||
id: renameArea
|
||||
enabled: !root.renaming
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
@@ -183,7 +183,7 @@ SettingsCard {
|
||||
"label": I18n.tr("Custom", "workspace color option")
|
||||
})]
|
||||
|
||||
readonly property bool workspaceStateColorsVisible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
|
||||
readonly property bool workspaceStateColorsVisible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isAqueous
|
||||
readonly property bool urgentWorkspaceColorsVisible: workspaceStateColorsVisible || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
|
||||
|
||||
function isFocusedAppearanceSection(section) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -61,7 +62,8 @@ Item {
|
||||
text: I18n.tr("Show Workspace Apps")
|
||||
description: I18n.tr("Display application icons in workspace indicators")
|
||||
checked: SettingsData.showWorkspaceApps
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isAqueous
|
||||
enabled: !CompositorService.isAqueous || (AqueousService.available && Quickshell.env("DMS_FORCE_EXTWS") !== "1")
|
||||
onToggled: checked => SettingsData.set("showWorkspaceApps", checked)
|
||||
}
|
||||
|
||||
@@ -153,7 +155,7 @@ Item {
|
||||
text: I18n.tr("Follow Monitor Focus")
|
||||
description: I18n.tr("Show workspaces of the currently focused monitor")
|
||||
checked: SettingsData.workspaceFollowFocus
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isAqueous
|
||||
onToggled: checked => SettingsData.set("workspaceFollowFocus", checked)
|
||||
}
|
||||
|
||||
@@ -163,7 +165,8 @@ Item {
|
||||
text: I18n.tr("Show Occupied Workspaces Only")
|
||||
description: I18n.tr("Display only workspaces that contain windows")
|
||||
checked: SettingsData.showOccupiedWorkspacesOnly
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isAqueous
|
||||
enabled: !CompositorService.isAqueous || (AqueousService.available && Quickshell.env("DMS_FORCE_EXTWS") !== "1")
|
||||
onToggled: checked => SettingsData.set("showOccupiedWorkspacesOnly", checked)
|
||||
}
|
||||
|
||||
@@ -173,7 +176,7 @@ Item {
|
||||
text: I18n.tr("Reverse Scrolling Direction")
|
||||
description: I18n.tr("Reverse workspace switch direction when scrolling over the bar")
|
||||
checked: SettingsData.reverseScrolling
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isAqueous
|
||||
onToggled: checked => SettingsData.set("reverseScrolling", checked)
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ Singleton {
|
||||
"isAqueous": () => CompositorService.isAqueous,
|
||||
"nativeOverviewCapable": () => CompositorService.isNiri || CompositorService.isAqueous,
|
||||
"smartDockCapable": () => CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isAqueous,
|
||||
"workspaceFollowFocusCapable": () => CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isAqueous,
|
||||
"isHyprlandOrNiri": () => CompositorService.isHyprland || CompositorService.isNiri,
|
||||
"windowRulesCapable": () => CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango,
|
||||
"layoutCapable": () => CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango,
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import assert from "node:assert/strict";
|
||||
import {readFileSync} from "node:fs";
|
||||
import vm from "node:vm";
|
||||
|
||||
const read = path => readFileSync(new URL(path, import.meta.url), "utf8");
|
||||
function methods(context, source, names) {
|
||||
for (const name of names) {
|
||||
const start = source.indexOf(" function " + name + "(");
|
||||
assert.notEqual(start, -1);
|
||||
vm.runInContext(source.slice(start, source.indexOf("\n }", start) + 6), context);
|
||||
}
|
||||
}
|
||||
const settings = read("../Modules/Settings/WorkspacesTab.qml");
|
||||
function setting(context, key, property) {
|
||||
const start = settings.indexOf('settingKey: "' + key + '"');
|
||||
const row = settings.slice(start, settings.indexOf("onToggled:", start));
|
||||
const expression = row.match(new RegExp(property + ": ([^\\n]+)"))?.[1];
|
||||
return expression ? vm.runInContext(expression, context) : true;
|
||||
}
|
||||
const compositor = {};
|
||||
let forced = false;
|
||||
const conditions = vm.createContext({CompositorService: compositor, AqueousService: {available: true}, Quickshell: {env: () => forced ? "1" : "0"}});
|
||||
const search = read("../Services/SettingsSearchService.qml");
|
||||
const conditionStart = search.indexOf("readonly property var conditionMap: (") + "readonly property var conditionMap: ".length;
|
||||
const conditionMap = vm.runInContext(search.slice(conditionStart, search.indexOf("})", conditionStart) + 2), conditions);
|
||||
const index = JSON.parse(read("../translations/settings_search_index.json"));
|
||||
for (const backend of ["Aqueous", "Niri", "Hyprland", "Mango", "Sway", "Scroll", "Miracle", "Labwc"]) {
|
||||
for (const key of Object.keys(compositor)) delete compositor[key];
|
||||
compositor["is" + backend] = true;
|
||||
for (const key of ["showWorkspaceApps", "showOccupiedWorkspacesOnly", "reverseScrolling", "workspaceFollowFocus"]) {
|
||||
const visible = setting(conditions, key, "visible");
|
||||
const expected = ["Aqueous", "Niri", "Hyprland", "Mango", ...(key === "workspaceFollowFocus" ? ["Sway", "Scroll", "Miracle"] : [])].includes(backend);
|
||||
assert.equal(!!visible, expected, backend + ": " + key);
|
||||
assert.equal(!!conditionMap[index.find(e => e.section === key).conditionKey](), expected, "search disagrees with settings");
|
||||
}
|
||||
}
|
||||
for (const key of Object.keys(compositor)) delete compositor[key];
|
||||
compositor.isAqueous = true;
|
||||
for (const [available, force, expected] of [[true, false, true], [true, true, false], [false, false, false]]) {
|
||||
conditions.AqueousService.available = available; forced = force;
|
||||
for (const key of ["showWorkspaceApps", "showOccupiedWorkspacesOnly"])
|
||||
assert.equal(setting(conditions, key, "enabled"), expected);
|
||||
assert.equal(setting(conditions, "workspaceFollowFocus", "enabled"), true);
|
||||
assert.equal(setting(conditions, "reverseScrolling", "enabled"), true);
|
||||
}
|
||||
const appearance = read("../Modules/Settings/WorkspaceAppearanceCard.qml");
|
||||
assert.equal(vm.runInContext(appearance.match(/workspaceStateColorsVisible: ([^\n]+)/)[1], conditions), true);
|
||||
|
||||
const service = vm.createContext({available: true, locked: false, session: "session-a", capabilities: {commands: true},
|
||||
seat: {id: "seat0", output: "output-a"}, outputs: [], workspaces: [
|
||||
{kind: "workspace", id: "one", output: "output-a", active: true, name: "Duplicate", aqueousSession: "session-a"},
|
||||
{kind: "workspace", id: "two", output: "output-b", active: true, name: "Duplicate", aqueousSession: "session-a"}
|
||||
]});
|
||||
service.entities = service.workspaces; service.seats = [service.seat];
|
||||
methods(service, read("../Services/AqueousService.qml"), ["commandArguments", "byteLength"]);
|
||||
const callbacks = [], commands = [], errors = [], legacy = [];
|
||||
service.command = (action, fields, callback) => {
|
||||
try { commands.push(service.commandArguments(action, fields)); callbacks.push(callback); }
|
||||
catch (error) { errors.push(String(error)); callback(false); }
|
||||
};
|
||||
const modal = vm.createContext({CompositorService: {isAqueous: true}, AqueousService: service,
|
||||
aqueousWorkspace: null, renaming: false, nameInput: {text: "", forceActiveFocus() {}},
|
||||
Qt: {callLater: fn => fn()}, I18n: {tr: text => text}, ToastService: {showError: (...args) => errors.push(args)},
|
||||
NiriService: {renameWorkspace: name => legacy.push(name)}, HyprlandService: {renameWorkspace: name => legacy.push(name)}});
|
||||
modal.root = modal;
|
||||
const modalSource = read("../Modals/WorkspaceRenameModal.qml");
|
||||
methods(modal, modalSource, ["show", "hide", "submitAndClose", "renameWorkspace"]);
|
||||
const visibleStart = modalSource.indexOf(" onVisibleChanged: {") + " onVisibleChanged: ".length;
|
||||
vm.runInContext("function visibilityChanged() " + modalSource.slice(visibleStart, modalSource.indexOf("\n }", visibleStart) + 6), modal);
|
||||
let visible = false;
|
||||
Object.defineProperty(modal, "visible", {get: () => visible, set: value => { visible = value; modal.visibilityChanged(); }});
|
||||
assert(modal.show("ignored"));
|
||||
assert.equal(modal.nameInput.text, "Duplicate");
|
||||
assert.equal(modal.aqueousWorkspace.id, "one");
|
||||
service.seat.output = "output-b";
|
||||
modal.nameInput.text = "日本語 🫧";
|
||||
modal.submitAndClose(); modal.submitAndClose();
|
||||
assert.equal(commands.length, 1, "double submission");
|
||||
assert.deepEqual(Array.from(commands[0]), ["aqueousctl", "workspace", "rename", "--id", "one", "--name", "日本語 🫧", "--json"]);
|
||||
assert(modal.visible && modal.renaming, "closed before command result");
|
||||
callbacks.shift()(false);
|
||||
assert(modal.visible && !modal.renaming);
|
||||
assert.equal(modal.nameInput.text, "日本語 🫧", "failure discarded the draft");
|
||||
modal.submitAndClose(); callbacks.shift()(true);
|
||||
assert(!modal.visible && !modal.aqueousWorkspace);
|
||||
|
||||
for (const mutate of [() => service.locked = true, () => service.session = "session-b", () => service.entities = [], () => service.capabilities.commands = false, () => service.available = false]) {
|
||||
Object.assign(service, {available: true, locked: false, session: "session-a", entities: service.workspaces, capabilities: {commands: true}});
|
||||
assert(modal.show("")); modal.nameInput.text = "Retained draft";
|
||||
mutate(); const previous = commands.length; modal.submitAndClose();
|
||||
assert.equal(commands.length, previous);
|
||||
assert(modal.visible && !modal.renaming);
|
||||
assert.equal(modal.nameInput.text, "Retained draft");
|
||||
modal.hide();
|
||||
}
|
||||
Object.assign(service, {available: true, locked: false, session: "session-a", entities: service.workspaces, capabilities: {commands: true}});
|
||||
for (const name of ["bad\nname", "🫧".repeat(257)]) {
|
||||
assert(modal.show("")); modal.nameInput.text = name; modal.submitAndClose();
|
||||
assert(modal.visible && !modal.renaming); modal.hide();
|
||||
}
|
||||
assert(modal.show("")); modal.nameInput.text = "Old dialog"; modal.submitAndClose();
|
||||
modal.hide(); assert(modal.show(""));
|
||||
callbacks.shift()(true);
|
||||
assert(modal.visible, "late result closed a newly opened dialog"); modal.hide();
|
||||
service.seat = null; assert.equal(modal.show(""), false); assert(!modal.visible);
|
||||
modal.CompositorService = {isNiri: true}; assert(modal.show("Niri")); modal.submitAndClose();
|
||||
modal.CompositorService = {isHyprland: true}; assert(modal.show("Hyprland")); modal.submitAndClose();
|
||||
assert.deepEqual(legacy, ["Niri", "Hyprland"]);
|
||||
process.stdout.write("PASS: workspace settings/search, forced-ext controls, captured rename target, failures and existing compositor dispatch\n");
|
||||
@@ -407,7 +407,9 @@ def find_settings_components(content, filename, wrappers):
|
||||
visible_raw = extract_property(block, "visible")
|
||||
condition_key = FILE_CONDITION_MAP.get(filename)
|
||||
if visible_raw:
|
||||
if "CompositorService.isAqueous" in visible_raw:
|
||||
if filename == "WorkspacesTab.qml" and setting_key == "workspaceFollowFocus":
|
||||
condition_key = "workspaceFollowFocusCapable"
|
||||
elif "CompositorService.isAqueous" in visible_raw:
|
||||
if "CompositorService.isHyprland" in visible_raw:
|
||||
condition_key = "smartDockCapable"
|
||||
elif "CompositorService.isNiri" in visible_raw:
|
||||
|
||||
@@ -1462,7 +1462,7 @@
|
||||
"workspaces"
|
||||
],
|
||||
"description": "Show workspaces of the currently focused monitor",
|
||||
"conditionKey": "windowRulesCapable"
|
||||
"conditionKey": "workspaceFollowFocusCapable"
|
||||
},
|
||||
{
|
||||
"section": "workspaceSettings",
|
||||
@@ -1487,7 +1487,7 @@
|
||||
],
|
||||
"icon": "view_module",
|
||||
"description": "Show workspace index numbers in the top bar workspace switcher",
|
||||
"conditionKey": "windowRulesCapable"
|
||||
"conditionKey": "smartDockCapable"
|
||||
},
|
||||
{
|
||||
"section": "groupActiveWorkspaceApps",
|
||||
@@ -1610,7 +1610,7 @@
|
||||
"workspace"
|
||||
],
|
||||
"description": "Reverse workspace switch direction when scrolling over the bar",
|
||||
"conditionKey": "windowRulesCapable"
|
||||
"conditionKey": "smartDockCapable"
|
||||
},
|
||||
{
|
||||
"section": "workspaceUnfocusedMonitorSeparateAppearance",
|
||||
@@ -1695,7 +1695,7 @@
|
||||
"workspaces"
|
||||
],
|
||||
"description": "Display only workspaces that contain windows",
|
||||
"conditionKey": "windowRulesCapable"
|
||||
"conditionKey": "smartDockCapable"
|
||||
},
|
||||
{
|
||||
"section": "showWorkspaceApps",
|
||||
@@ -1724,7 +1724,7 @@
|
||||
"workspace"
|
||||
],
|
||||
"description": "Display application icons in workspace indicators",
|
||||
"conditionKey": "windowRulesCapable"
|
||||
"conditionKey": "smartDockCapable"
|
||||
},
|
||||
{
|
||||
"section": "showWorkspaceIndex",
|
||||
|
||||
@@ -204,9 +204,50 @@ def main():
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function renameModal() {
|
||||
return dmsShellLoader.item?.children.find(c => c.workspaceRenameModalLoader !== undefined)?.workspaceRenameModalLoader?.item;
|
||||
}
|
||||
function renameInput(item) {
|
||||
if (!item)
|
||||
return null;
|
||||
if (item.placeholderText === I18n.tr("Workspace name"))
|
||||
return item;
|
||||
for (const child of item.children || []) {
|
||||
const found = renameInput(child);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
IpcHandler {
|
||||
target: "aqueousTest"
|
||||
function state(): string { return JSON.stringify(AqueousService.state); }
|
||||
function workspaceSetting(key: string): string {
|
||||
const row = SettingsSearchService.registeredCards[key]?.item;
|
||||
return JSON.stringify(row ? {visible: row.visible, enabled: row.enabled, checked: row.checked} : {visible: false, tab: PopoutService.settingsModal?.currentTabIndex, registered: Object.keys(SettingsSearchService.registeredCards)});
|
||||
}
|
||||
function toggleWorkspaceSetting(key: string): string {
|
||||
const row = SettingsSearchService.registeredCards[key]?.item;
|
||||
if (!row?.visible || !row.enabled)
|
||||
return "UNAVAILABLE";
|
||||
row.toggled(!row.checked);
|
||||
return "TOGGLED";
|
||||
}
|
||||
function renameStatus(): string {
|
||||
const modal = entrypoint.renameModal();
|
||||
const input = entrypoint.renameInput(modal?.contentItem);
|
||||
return JSON.stringify(modal ? {visible: modal.visible, pending: modal.renaming,
|
||||
target: modal.aqueousWorkspace, text: input?.text, inputEnabled: input?.enabled} : null);
|
||||
}
|
||||
function renameSubmit(name: string): string {
|
||||
const modal = entrypoint.renameModal();
|
||||
const input = entrypoint.renameInput(modal?.contentItem);
|
||||
if (!modal?.visible || !input)
|
||||
return "UNAVAILABLE";
|
||||
input.text = name;
|
||||
modal.submitAndClose();
|
||||
return "REQUESTED";
|
||||
}
|
||||
function workspaceStatus(): string {
|
||||
return JSON.stringify({backend: workspaceTest.useAqueous ? "aqueous" : workspaceTest.useExtWorkspace ? "ext" : "none", output: workspaceTest.effectiveScreenName,
|
||||
rows: workspaceTest.workspaceList.map(w => ({name: w.name, active: w.active, number: w.number,
|
||||
@@ -281,9 +322,23 @@ def main():
|
||||
assert len(view["delegates"]) == len(view["rows"]), view
|
||||
if args.frame:
|
||||
wait_for(lambda: len(entities("output")) == 2 and all(o["usable_bounds"]["width"] < o["bounds"]["width"] and o["usable_bounds"]["height"] < o["bounds"]["height"] for o in entities("output")))
|
||||
wait_for(lambda: ipc("settings", "openWith", "typography").startswith("SETTINGS_OPEN_SUCCESS"))
|
||||
wait_for(lambda: ipc("settings", "openWith", "workspaces").startswith("SETTINGS_OPEN_SUCCESS"))
|
||||
time.sleep(2)
|
||||
assert "Type SettingsModal unavailable" not in (base / "dms.log").read_text()
|
||||
for key in ("showWorkspaceApps", "workspaceFollowFocus", "showOccupiedWorkspacesOnly", "reverseScrolling"):
|
||||
def setting_state():
|
||||
value = json.loads(ipc("aqueousTest", "workspaceSetting", key))
|
||||
(base / "workspace-setting.json").write_text(json.dumps(dict(key=key, state=value), indent=2))
|
||||
return value
|
||||
control = wait_for(lambda: (value if (value := setting_state()) and value["visible"] else None))
|
||||
assert control["enabled"] == (not args.force_ext or key not in ("showWorkspaceApps", "showOccupiedWorkspacesOnly")), (key, control)
|
||||
if control["enabled"]:
|
||||
assert ipc("aqueousTest", "toggleWorkspaceSetting", key) == "TOGGLED"
|
||||
wait_for(lambda: setting_state()["checked"] != control["checked"])
|
||||
assert ipc("aqueousTest", "toggleWorkspaceSetting", key) == "TOGGLED"
|
||||
wait_for(lambda: setting_state()["checked"] == control["checked"])
|
||||
else:
|
||||
assert ipc("aqueousTest", "toggleWorkspaceSetting", key) == "UNAVAILABLE"
|
||||
assert ipc("settings", "close") == "SETTINGS_CLOSE_SUCCESS"
|
||||
windows = [spawn([str(fixture), "window"], f"window-{i}.log") for i in range(2)]
|
||||
clients = wait_for(lambda: (w if len(w := [w for w in entities("window") if w["app_id"] == "aq-shell-test"]) == 2 else None))
|
||||
@@ -353,6 +408,34 @@ def main():
|
||||
assert ipc("aqueous", "overview", "hide", output["name"]) == "OVERVIEW_REQUESTED"
|
||||
wait_for(lambda: entities("session")[0]["overview_output"] is None)
|
||||
|
||||
# Use the real rename IPC/dialog and keep its captured target across a focus change.
|
||||
def rename_status():
|
||||
value = json.loads(ipc("aqueousTest", "renameStatus"))
|
||||
(base / "rename-status.json").write_text(json.dumps(value, indent=2))
|
||||
return value
|
||||
target = next(w for w in entities("workspace") if w["output"] == output["id"] and w["active"])
|
||||
destination = next(w for w in entities("workspace") if w["output"] == output["id"] and w["id"] != target["id"])
|
||||
assert ipc("workspace-rename", "open") == "WORKSPACE_RENAME_MODAL_OPENED"
|
||||
draft = wait_for(lambda: (value if (value := rename_status()) and value["visible"] else None))
|
||||
assert draft["target"] == dict(id=target["id"], session=session), draft
|
||||
assert draft["text"] == target["name"], draft
|
||||
command("workspace.activate", id=destination["id"], seat=seat)
|
||||
assert ipc("aqueousTest", "renameSubmit", "日本語 🫧") == "REQUESTED"
|
||||
wait_for(lambda: not rename_status()["visible"])
|
||||
wait_for(lambda: next(w for w in entities("workspace") if w["id"] == target["id"])["name"] == "日本語 🫧")
|
||||
assert next(w for w in entities("workspace") if w["id"] == destination["id"])["name"] == destination["name"]
|
||||
assert ipc("workspace-rename", "toggle") == "WORKSPACE_RENAME_MODAL_OPENED"
|
||||
wait_for(lambda: rename_status()["visible"])
|
||||
assert rename_status()["target"]["id"] == destination["id"]
|
||||
invalid_name = "x" * 1025
|
||||
assert ipc("aqueousTest", "renameSubmit", invalid_name) == "REQUESTED"
|
||||
draft = rename_status()
|
||||
assert draft["visible"] and not draft["pending"] and draft["text"] == invalid_name, draft
|
||||
assert ipc("aqueousTest", "renameSubmit", "Renamed after error") == "REQUESTED"
|
||||
wait_for(lambda: not rename_status()["visible"])
|
||||
wait_for(lambda: next(w for w in entities("workspace") if w["id"] == destination["id"])["name"] == "Renamed after error")
|
||||
command("window.activate", id=window, seat=seat)
|
||||
|
||||
def capture(name):
|
||||
command("window.activate", id=window, seat=seat)
|
||||
run([str(binaries / "dms"), "screenshot", "window", "--seat", seat, "--no-clipboard", "--no-notify", "--dir", str(base), "--filename", name])
|
||||
@@ -420,7 +503,7 @@ def main():
|
||||
os.killpg(dms.pid, signal.SIGTERM)
|
||||
dms.wait(timeout=10)
|
||||
assert not any(Path("/proc", pid).exists() for pid in watch_pids), "watcher survived DMS shutdown"
|
||||
print("PASS: daemon/UI, duplicate identities, keyboard, overview, screenshots, helper conflicts, keybinds, single watcher, workspace widget and orderly exit", flush=True)
|
||||
print("PASS: daemon/UI, duplicate identities, keyboard, overview, screenshots, helper conflicts, keybinds, single watcher, workspace settings/widget/rename and orderly exit", flush=True)
|
||||
finally:
|
||||
for child in reversed(children):
|
||||
if child.poll() is None:
|
||||
|
||||
Reference in new issue
Block a user