workspaces: keep the hyprland slot for a workspace whose monitor is pending

This commit is contained in:
bbedward
2026-09-18 14:40:49 -04:00
parent 014c69b09a
commit 0784bdb87e
2 changed files with 146 additions and 21 deletions
+144 -21
View File
@@ -1,7 +1,14 @@
.pragma library
function placeholder() {
return { id: null, idx: null, name: "", output: "", active: false, placeholder: true };
return {
id: null,
idx: null,
name: "",
output: "",
active: false,
placeholder: true
};
}
function neighbor(workspaces, index, direction) {
@@ -25,11 +32,29 @@ function dropStale(cache, ids) {
}
function niriFallback() {
return [{ id: 1, idx: 0, name: "" }, { id: 2, idx: 1, name: "" }];
return [
{
id: 1,
idx: 0,
name: ""
},
{
id: 2,
idx: 1,
name: ""
}
];
}
function niriRecord(ws) {
return { id: ws.id, idx: ws.idx, name: ws.name ?? "", output: ws.output ?? "", active: ws.is_active === true, placeholder: false };
return {
id: ws.id,
idx: ws.idx,
name: ws.name ?? "",
output: ws.output ?? "",
active: ws.is_active === true,
placeholder: false
};
}
function niriCurrentIdx(raw, screenName, followFocus) {
@@ -92,7 +117,15 @@ function hyprlandOrder(a, b) {
}
function hyprlandRecord(ws) {
return { id: ws.id, idx: ws.id > 0 ? ws.id : null, name: ws.name ?? "", output: ws.monitor?.name ?? "", active: ws.active === true, placeholder: false, urgent: ws.urgent === true };
return {
id: ws.id,
idx: ws.id > 0 ? ws.id : null,
name: ws.name ?? "",
output: ws.monitor?.name ?? "",
active: ws.active === true,
placeholder: false,
urgent: ws.urgent === true
};
}
function hyprlandCurrentId(raw, screenName, followFocus) {
@@ -112,7 +145,10 @@ function hyprlandMonitorWorkspaces(raw, workspaces, screenName) {
function hyprlandListedWorkspaces(raw, screenName, followFocus, occupiedOnly) {
const regular = raw.workspaces.filter(ws => !hyprlandSpecial(ws));
if (regular.length === 0)
return [hyprlandRecord({ id: 1, name: "1" })];
return [hyprlandRecord({
id: 1,
name: "1"
})];
const workspaces = !screenName || followFocus ? regular.slice().sort(hyprlandOrder) : hyprlandMonitorWorkspaces(raw, regular, screenName);
if (!occupiedOnly)
return workspaces.map(hyprlandRecord);
@@ -168,8 +204,9 @@ function hyprlandPersistentWorkspaces(raw, records, screenName, followFocus, min
return records;
const perMonitor = !!screenName && !followFocus;
const taken = new Set(records.map(ws => ws.id));
// a just-created workspace has no monitor until quickshell's j/workspaces refresh lands; claiming it for another monitor drops its slot for a frame
if (perMonitor)
raw.workspaces.forEach(ws => ws.id > 0 && ws.monitor?.name !== screenName && taken.add(ws.id));
raw.workspaces.forEach(ws => ws.id > 0 && ws.monitor && ws.monitor.name !== screenName && taken.add(ws.id));
const filled = records.slice();
for (let id = 1; id <= minCount; id++) {
if (taken.has(id))
@@ -179,7 +216,13 @@ function hyprlandPersistentWorkspaces(raw, records, screenName, followFocus, min
if (bound && bound !== screenName)
continue;
}
filled.push(hyprlandRecord({ id, name: String(id), monitor: { name: perMonitor ? screenName : "" } }));
filled.push(hyprlandRecord({
id,
name: String(id),
monitor: {
name: perMonitor ? screenName : ""
}
}));
}
return filled.sort(hyprlandOrder);
}
@@ -191,7 +234,12 @@ function hyprlandWorkspacesForScreen(raw, screenName, followFocus, occupiedOnly,
function hyprlandScrollWorkspaces(raw, screenName, followFocus) {
const onScreen = !screenName || followFocus ? raw.workspaces : raw.workspaces.filter(ws => ws.lastIpcObject?.monitor === screenName);
const numbered = onScreen.filter(ws => ws.id > -1).sort((a, b) => a.id - b.id);
return (numbered.length > 0 ? numbered : [{ id: 1, name: "1" }]).map(hyprlandRecord);
return (numbered.length > 0 ? numbered : [
{
id: 1,
name: "1"
}
]).map(hyprlandRecord);
}
function hyprlandScrollCurrentId(raw, screenName) {
@@ -206,7 +254,14 @@ function hyprlandActiveWorkspace(raw, screenName) {
const name = ws.name ?? "";
if (hyprlandSpecialName(name))
return null;
return { id: ws.id, idx: ws.id > 0 ? ws.id : null, name: name !== "" && name !== String(ws.id) ? name : "", output: monitor?.name ?? screenName, active: true, placeholder: false };
return {
id: ws.id,
idx: ws.id > 0 ? ws.id : null,
name: name !== "" && name !== String(ws.id) ? name : "",
output: monitor?.name ?? screenName,
active: true,
placeholder: false
};
}
function hyprlandWindowsOnWorkspace(windows, workspace, toplevels) {
@@ -224,7 +279,16 @@ function hyprlandWorkspaceOccupied(toplevels, workspace) {
function mangoRecord(index, tag, output) {
const state = tag?.state ?? 0;
return { id: index, idx: index + 1, name: "", output, active: state === 1, placeholder: false, urgent: state === 2, occupied: (tag?.clients ?? 0) > 0 };
return {
id: index,
idx: index + 1,
name: "",
output,
active: state === 1,
placeholder: false,
urgent: state === 2,
occupied: (tag?.clients ?? 0) > 0
};
}
function mangoWorkspacesForScreen(raw, screenName, showAllTags, minCount) {
@@ -255,8 +319,17 @@ function mangoCurrentTag(raw) {
function mangoScrollWorkspaces(raw, screenName, showAllTags) {
const current = mangoScrollCurrentTag(raw);
const tags = !raw.available ? [0] : showAllTags ? Array.from({ length: raw.tagCount }, (_, i) => i) : raw.visibleTags;
return tags.map(index => ({ id: index, idx: index + 1, name: "", output: screenName, active: index === current, placeholder: false }));
const tags = !raw.available ? [0] : showAllTags ? Array.from({
length: raw.tagCount
}, (_, i) => i) : raw.visibleTags;
return tags.map(index => ({
id: index,
idx: index + 1,
name: "",
output: screenName,
active: index === current,
placeholder: false
}));
}
function mangoScrollCurrentTag(raw) {
@@ -270,7 +343,14 @@ function mangoActiveWorkspace(raw, screenName) {
const activeTags = raw.activeTags;
if (activeTags.length === 0)
return null;
return { id: activeTags[0], idx: activeTags[0] + 1, name: "", output: screenName, active: true, placeholder: false };
return {
id: activeTags[0],
idx: activeTags[0] + 1,
name: "",
output: screenName,
active: true,
placeholder: false
};
}
function mangoWindowsOnWorkspace(windows, workspace) {
@@ -302,7 +382,16 @@ function i3Order(a, b) {
}
function i3Record(ws) {
return { id: i3Key(ws), idx: ws.num !== -1 ? ws.num : null, name: i3StripNumber(ws.num, ws.name), output: ws.monitor?.name ?? "", active: ws.active === true, placeholder: false, urgent: ws.urgent === true, focused: ws.focused === true };
return {
id: i3Key(ws),
idx: ws.num !== -1 ? ws.num : null,
name: i3StripNumber(ws.num, ws.name),
output: ws.monitor?.name ?? "",
active: ws.active === true,
placeholder: false,
urgent: ws.urgent === true,
focused: ws.focused === true
};
}
function i3CurrentKey(raw, screenName, followFocus) {
@@ -313,11 +402,19 @@ function i3CurrentKey(raw, screenName, followFocus) {
function i3ListedWorkspaces(raw, screenName, followFocus) {
const workspaces = raw.workspaces;
if (workspaces.length === 0)
return [{ num: 1 }];
return [
{
num: 1
}
];
if (!screenName || followFocus)
return workspaces.slice();
const onScreen = workspaces.filter(ws => ws.monitor?.name === screenName);
return onScreen.length > 0 ? onScreen : [{ num: 1 }];
return onScreen.length > 0 ? onScreen : [
{
num: 1
}
];
}
// `workspace number N` creates the workspace on demand, so numbers up to minCount are real switch targets
@@ -332,7 +429,13 @@ function i3PersistentWorkspaces(raw, listed, screenName, followFocus, minCount)
for (let num = 1; num <= minCount; num++) {
if (taken.has(num))
continue;
filled.push({ num, name: String(num), monitor: { name: perMonitor ? screenName : "" } });
filled.push({
num,
name: String(num),
monitor: {
name: perMonitor ? screenName : ""
}
});
}
return filled;
}
@@ -344,9 +447,13 @@ function i3WorkspacesForScreen(raw, screenName, followFocus, minCount) {
function i3ScrollWorkspaces(raw, screenName, followFocus) {
const workspaces = raw.workspaces;
if (workspaces.length === 0)
return [i3Record({ num: 1 })];
return [i3Record({
num: 1
})];
const onScreen = !screenName || followFocus ? workspaces.slice() : workspaces.filter(ws => ws.monitor?.name === screenName);
return onScreen.length > 0 ? onScreen.sort((a, b) => a.num - b.num).map(i3Record) : [i3Record({ num: 1 })];
return onScreen.length > 0 ? onScreen.sort((a, b) => a.num - b.num).map(i3Record) : [i3Record({
num: 1
})];
}
function i3ActiveWorkspace(raw, screenName) {
@@ -356,7 +463,14 @@ function i3ActiveWorkspace(raw, screenName) {
return null;
const num = ws.number;
const name = i3StripNumber(num, ws.name);
return { id: num !== undefined && num !== -1 ? num : name, idx: num !== undefined && num > 0 ? num : null, name, output: ws.monitor?.name ?? screenName, active: true, placeholder: false };
return {
id: num !== undefined && num !== -1 ? num : name,
idx: num !== undefined && num > 0 ? num : null,
name,
output: ws.monitor?.name ?? screenName,
active: true,
placeholder: false
};
}
function i3WorkspaceFocused(raw, workspace) {
@@ -370,7 +484,16 @@ function i3WindowsOnWorkspace(windows, workspace) {
}
function aqueousRecord(ws, screenName) {
return { id: ws.id, idx: ws.number ?? null, name: ws.name ?? "", output: screenName, active: ws.active === true, placeholder: false, urgent: ws.urgent === true, session: ws.aqueousSession };
return {
id: ws.id,
idx: ws.number ?? null,
name: ws.name ?? "",
output: screenName,
active: ws.active === true,
placeholder: false,
urgent: ws.urgent === true,
session: ws.aqueousSession
};
}
function aqueousWorkspacesForScreen(raw, screenName, occupiedOnly, cache) {
@@ -150,6 +150,8 @@ test("hyprland padding fills numbered ids up to the minimum, skipping ids owned
assert.deepEqual(pick(model.hyprlandWorkspacesForScreen(raw, "HDMI-A-1", false, false, 0), "id"), [2, 4, -1337]);
const filled = model.hyprlandWorkspacesForScreen(raw, "HDMI-A-1", false, false, 5).find(ws => ws.id === 5);
assert.deepEqual(plain(filled), { id: 5, idx: 5, name: "5", output: "HDMI-A-1", active: false, placeholder: false, urgent: false });
raw.workspaces.push({ id: 5, name: "5", monitor: null, lastIpcObject: {} });
assert.deepEqual(pick(model.hyprlandWorkspacesForScreen(raw, "HDMI-A-1", false, false, 5), "id"), [2, 4, 5, -1337]);
});
test("hyprland padding honours workspace rules that bind ids to a monitor", () => {