diff --git a/docs/layouts.md b/docs/layouts.md
index 27819ea..a2885cb 100644
--- a/docs/layouts.md
+++ b/docs/layouts.md
@@ -9,11 +9,11 @@ The JSON keys represent the total number of windows on a monitor. The values are
```json
{
"1": [
- {"x": 0, "y": 0, "w": 100, "h": 100, "id": 1}
+ {"x": 0, "y": 0, "w": 100, "h": 100, "id": 0}
],
"2": [
- {"x": 0, "y": 0, "w": 50, "h": 100, "id": 1},
- {"x": 50, "y": 0, "w": 50, "h": 100, "id": 2}
+ {"x": 0, "y": 0, "w": 50, "h": 100, "id": 0},
+ {"x": 50, "y": 0, "w": 50, "h": 100, "id": 1}
]
}
```
@@ -23,7 +23,7 @@ The JSON keys represent the total number of windows on a monitor. The values are
- `y`: Y percentage offset (0 to 100)
- `w`: Width percentage (0 to 100)
- `h`: Height percentage (0 to 100)
-- `id` (Required): The logical 1-indexed ID this window occupies. The IDs map to the insertion order of windows. E.g., `id: 1` is the oldest window, `id: 2` is the second oldest. It MUST be unique and cover 1 to the window count.
+- `id` (Required): The logical 0-indexed ID this window occupies. The IDs map to the insertion order of windows. E.g., `id: 0` is the oldest window, `id: 1` is the second oldest. It MUST be unique and cover 0 to `count - 1`.
## Fallback
If the window count exceeds the highest key defined in the JSON, extra windows will fall back to floating mode (unmanaged by the auto-tiler).
diff --git a/lib/controller.js b/lib/controller.js
index 47af81e..317b1d5 100644
--- a/lib/controller.js
+++ b/lib/controller.js
@@ -207,6 +207,11 @@ export class TilingController {
if (monitorIndex < 0) monitorIndex = global.display.get_current_monitor();
if (!workspace) return null;
+
+ // Guard against transient GNOME states during monitor unplug
+ if (monitorIndex >= this.monitorManager.getMonitorCount()) {
+ return null;
+ }
const isRestoring = this._restoringWindows.has(window);
const preferredSlot = isRestoring ? this._restoringWindows.get(window) : undefined;
@@ -244,7 +249,11 @@ export class TilingController {
const layout = this.workspaceManager.getLayout(workspace);
const isEvacuated = this.monitorManager.isEvacuated(window);
- if ((window.minimized || isEvacuated) && !isRestoring) {
+ if (isEvacuated && !window.minimized) {
+ this.monitorManager.clearEvacuation(window);
+ }
+
+ if (window.minimized && !isRestoring) {
layout.untrackWindow(window, monitorId);
} else {
layout.trackWindow(window, monitorId, preferredSlot);
diff --git a/lib/editor/preview.js b/lib/editor/preview.js
index cd948b7..ac180e7 100644
--- a/lib/editor/preview.js
+++ b/lib/editor/preview.js
@@ -146,7 +146,7 @@ function getDefaultSplit(layoutBase, targetCount) {
});
newLayoutEstates.push(e2);
newLayoutEstates.forEach((item, i) => {
- item.id = i + 1;
+ item.id = i;
});
return newLayoutEstates;
@@ -197,7 +197,7 @@ class LayoutPreviewPage extends Adw.PreferencesPage {
y: Math.round(e.y),
w: Math.round(e.w),
h: Math.round(e.h),
- id: parseInt(e.id || 1, 10)
+ id: parseInt(e.id || 0, 10)
})));
}
});
@@ -470,15 +470,15 @@ class LayoutPreviewPage extends Adw.PreferencesPage {
splitVal = Math.round(pctX);
if (splitVal >= estate.pct_x + 5 && splitVal <= estate.pct_x + estate.pct_w - 5) {
splitOk = true;
- e1 = { x: estate.pct_x, y: estate.pct_y, w: splitVal - estate.pct_x, h: estate.pct_h, id: estate.id || (index + 1) };
- e2 = { x: splitVal, y: estate.pct_y, w: estate.pct_x + estate.pct_w - splitVal, h: estate.pct_h, id: targetCount };
+ e1 = { x: estate.pct_x, y: estate.pct_y, w: splitVal - estate.pct_x, h: estate.pct_h, id: estate.id !== undefined ? estate.id : index };
+ e2 = { x: splitVal, y: estate.pct_y, w: estate.pct_x + estate.pct_w - splitVal, h: estate.pct_h, id: targetCount - 1 };
}
} else {
splitVal = Math.round(pctY);
if (splitVal >= estate.pct_y + 5 && splitVal <= estate.pct_y + estate.pct_h - 5) {
splitOk = true;
- e1 = { x: estate.pct_x, y: estate.pct_y, w: estate.pct_w, h: splitVal - estate.pct_y, id: estate.id || (index + 1) };
- e2 = { x: estate.pct_x, y: splitVal, w: estate.pct_w, h: estate.pct_y + estate.pct_h - splitVal, id: targetCount };
+ e1 = { x: estate.pct_x, y: estate.pct_y, w: estate.pct_w, h: splitVal - estate.pct_y, id: estate.id !== undefined ? estate.id : index };
+ e2 = { x: estate.pct_x, y: splitVal, w: estate.pct_w, h: estate.pct_y + estate.pct_h - splitVal, id: targetCount - 1 };
}
}
@@ -488,12 +488,12 @@ class LayoutPreviewPage extends Adw.PreferencesPage {
if (i === index) {
newLayoutEstates.push(e1);
} else {
- newLayoutEstates.push({ x: e.pct_x, y: e.pct_y, w: e.pct_w, h: e.pct_h, id: e.id || (i + 1) });
+ newLayoutEstates.push({ x: e.pct_x, y: e.pct_y, w: e.pct_w, h: e.pct_h, id: e.id !== undefined ? e.id : i });
}
});
newLayoutEstates.push(e2);
newLayoutEstates.forEach((item, i) => {
- item.id = i + 1;
+ item.id = i;
});
onSplitSuccess(newLayoutEstates);
@@ -558,7 +558,7 @@ class LayoutPreviewPage extends Adw.PreferencesPage {
margin_bottom: 24
});
addFirstBtn.connect('clicked', () => {
- const initial = { "1": [{ "x": 0, "y": 0, "w": 100, "h": 100, "id": 1 }] };
+ const initial = { "1": [{ "x": 0, "y": 0, "w": 100, "h": 100, "id": 0 }] };
this.settings.set_string('custom-layouts', JSON.stringify(initial, null, 2));
this._editingCount = 1;
this._initializeDrafts();
@@ -595,7 +595,7 @@ class LayoutPreviewPage extends Adw.PreferencesPage {
y: Math.round(e.pct_y),
w: Math.round(e.pct_w),
h: Math.round(e.pct_h),
- id: e.id || (idx + 1)
+ id: e.id !== undefined ? e.id : idx
}));
}
}
@@ -1002,7 +1002,7 @@ class LayoutPreviewPage extends Adw.PreferencesPage {
const c = colors[k % colors.length];
const label = new Gtk.Label({
- label: `● Slot ${estate.id || (k + 1)}:`,
+ label: `● Slot ${(estate.id !== undefined ? estate.id : k) + 1}:`,
use_markup: true,
width_request: 80,
halign: Gtk.Align.START
diff --git a/lib/layout.js b/lib/layout.js
index ec09b51..ca34873 100644
--- a/lib/layout.js
+++ b/lib/layout.js
@@ -1,11 +1,13 @@
import { getEdgingSlotForEstates } from './utils/geometry.js';
+import { Logger } from './logger.js';
/**
* ScreenEstate: Immutable data object holding percentages (0-100).
*/
export class ScreenEstate {
- constructor(pct_x, pct_y, pct_w, pct_h) {
+ constructor(id, pct_x, pct_y, pct_w, pct_h) {
+ this.id = id;
this.EPSILON = 0.01
if (pct_x < 0 || pct_y < 0 || (pct_x + pct_w) > (100 + this.EPSILON) || (pct_y + pct_h) > (100 + this.EPSILON)) {
throw new Error(`ScreenEstate out of bounds: x=${pct_x}, y=${pct_y}, w=${pct_w}, h=${pct_h}`);
@@ -74,8 +76,14 @@ export class Layout {
);
}
- getEstate(index) {
- return this.estates[index] || null;
+ // Invariant: estates[i].id === i for all i. This holds because the parser
+ // sorts estates by id before constructing the Layout.
+ getEstate(id) {
+ const estate = this.estates[id] || null;
+ if (estate && estate.id !== id) {
+ Logger.error(`Layout invariant violated: estates[${id}].id is ${estate.id}`);
+ }
+ return estate;
}
get size() {
@@ -154,34 +162,30 @@ export class LayoutParser {
throw new Error(`Layout for count ${count} contains ${arr.length} estates`);
}
- const parsedArray = arr.map(e => {
+ const parsedEstates = arr.map(e => {
if (e.id === undefined) {
throw new Error(`Layout for count ${count} has estate without an id`);
}
const idVal = parseInt(e.id, 10);
- if (isNaN(idVal) || idVal < 1) {
+ if (isNaN(idVal) || idVal < 0) {
throw new Error(`Layout for count ${count} has invalid id ${e.id}`);
}
- return {
- estate: new ScreenEstate(e.x, e.y, e.w, e.h),
- slot: idVal - 1
- };
+ return new ScreenEstate(idVal, e.x, e.y, e.w, e.h);
});
- // Validate uniqueness and completeness of ids
- const idSet = new Set(parsedArray.map(item => item.slot + 1));
+ // Validate uniqueness and completeness of ids (0 to count-1)
+ const idSet = new Set(parsedEstates.map(e => e.id));
if (idSet.size !== count) {
- throw new Error(`Layout for count ${count} must have unique ids from 1 to ${count}`);
+ throw new Error(`Layout for count ${count} must have unique ids from 0 to ${count - 1}`);
}
- for (let i = 1; i <= count; i++) {
+ for (let i = 0; i < count; i++) {
if (!idSet.has(i)) {
throw new Error(`Layout for count ${count} is missing id ${i}`);
}
}
- parsedArray.sort((a, b) => a.slot - b.slot);
- const estates = parsedArray.map(item => item.estate);
- const layout = new Layout(estates);
+ parsedEstates.sort((a, b) => a.id - b.id);
+ const layout = new Layout(parsedEstates);
LayoutValidator.validateCoverage(layout);
diff --git a/lib/monitor.js b/lib/monitor.js
index 14b42fb..7d08d47 100644
--- a/lib/monitor.js
+++ b/lib/monitor.js
@@ -33,6 +33,14 @@ export class MonitorManager {
}
}
+ getMonitorCount() {
+ try {
+ return global.backend.get_monitor_manager().get_logical_monitors().length;
+ } catch (e) {
+ return global.display.get_n_monitors();
+ }
+ }
+
getMonitorId(monitorIndex) {
try {
const manager = global.backend.get_monitor_manager();
@@ -85,6 +93,10 @@ export class MonitorManager {
this._evacuatedWindows.clear();
}
+ clearEvacuation(window) {
+ this._evacuatedWindows.delete(window);
+ }
+
checkEvacuation(window, wrapper, newMonitorId, newWorkspace) {
const oldMonitorId = wrapper.monitorId;
const oldWorkspace = wrapper.workspace;
@@ -117,7 +129,9 @@ export class MonitorManager {
const oldGrid = this.controller.workspaceManager.getLayout(oldWorkspace);
slot = oldGrid._getTracker(oldMonitorId).getSlot(window);
oldGrid.untrackWindow(window, oldMonitorId);
- } catch (e) {}
+ } catch (e) {
+ Logger.warn(`Evacuation untrack failed for "${wrapper.title}"`, e);
+ }
// Track by window reference with original monitor info and slot.
this.recordEvacuation(window, oldMonitorId, oldWorkspace, slot);
diff --git a/lib/state.js b/lib/state.js
index a2bed55..3be4718 100644
--- a/lib/state.js
+++ b/lib/state.js
@@ -1,55 +1,79 @@
import { Logger } from './logger.js';
/**
- * StateTracker class. Maps windows to slots.
+ * StateTracker: Ordered window list where array position equals slot id.
+ * Invariant: this._windows contains no duplicates. Position = slot.
*/
export class StateTracker {
constructor() {
- this._windowToSlot = new Map();
+ this._windows = [];
+ }
+ /**
+ * Registers a window at a specific position (insert) or appends to end.
+ * Rejects already-tracked windows (insert-only semantics).
+ */
+ startTracking(window, index) {
+ if (this._windows.includes(window)) return;
+ if (index !== undefined && index >= 0 && index <= this._windows.length) {
+ this._windows.splice(index, 0, window);
+ } else {
+ this._windows.push(window);
+ }
+ const slot = this._windows.indexOf(window);
+ Logger.debug(`StateTracker: startTracking window ID ${window.get_id ? window.get_id() : 'unknown'} ("${window.get_title ? window.get_title() : 'unknown'}") at slot ${slot}`);
}
- track(window, index) {
- Logger.debug(`StateTracker: Tracked new window ID ${window.get_id ? window.get_id() : 'unknown'} ("${window.get_title ? window.get_title() : 'unknown'}") to slot ${index}`);
- this._windowToSlot.set(window, index);
+ /**
+ * Unregisters a window. Remaining windows shift down naturally.
+ */
+ stopTracking(window) {
+ const idx = this._windows.indexOf(window);
+ if (idx !== -1) {
+ this._windows.splice(idx, 1);
+ Logger.debug(`StateTracker: stopTracking window ID ${window.get_id ? window.get_id() : 'unknown'} ("${window.get_title ? window.get_title() : 'unknown'}") from slot ${idx}`);
+ }
}
- untrack(window) {
- const slot = this._windowToSlot.get(window);
- this._windowToSlot.delete(window);
- Logger.debug(`StateTracker: Untracked window ID ${window.get_id ? window.get_id() : 'unknown'} ("${window.get_title ? window.get_title() : 'unknown'}") from slot ${slot}`);
+ /**
+ * Atomically replaces one window with another in the same slot.
+ * Avoids transient state from sequential stopTracking+startTracking.
+ */
+ replace(oldWindow, newWindow) {
+ const idx = this._windows.indexOf(oldWindow);
+ if (idx === -1) return;
+ this._windows[idx] = newWindow;
+ Logger.debug(`StateTracker: replace window ID ${oldWindow.get_id ? oldWindow.get_id() : 'unknown'} with ${newWindow.get_id ? newWindow.get_id() : 'unknown'} at slot ${idx}`);
}
swapWindows(win1, win2) {
- if (!this._windowToSlot.has(win1) || !this._windowToSlot.has(win2)) return;
- const slot1 = this._windowToSlot.get(win1);
- const slot2 = this._windowToSlot.get(win2);
- this._windowToSlot.set(win1, slot2);
- this._windowToSlot.set(win2, slot1);
- Logger.debug(`StateTracker: Swapped windows ID ${win1.get_id ? win1.get_id() : 'unknown'} (slot ${slot1} -> ${slot2}) and ID ${win2.get_id ? win2.get_id() : 'unknown'} (slot ${slot2} -> ${slot1})`);
+ const i = this._windows.indexOf(win1);
+ const j = this._windows.indexOf(win2);
+ if (i === -1 || j === -1) return;
+ [this._windows[i], this._windows[j]] = [this._windows[j], this._windows[i]];
+ Logger.debug(`StateTracker: Swapped windows ID ${win1.get_id ? win1.get_id() : 'unknown'} (slot ${i} -> ${j}) and ID ${win2.get_id ? win2.get_id() : 'unknown'} (slot ${j} -> ${i})`);
}
getSlot(window) {
- return this._windowToSlot.get(window);
+ const idx = this._windows.indexOf(window);
+ return idx === -1 ? undefined : idx;
}
get windows() {
- return [...this._windowToSlot.entries()]
- .sort((a, b) => a[1] - b[1])
- .map(entry => entry[0]);
+ return [...this._windows]; // defensive copy, already ordered
}
get size() {
- return this._windowToSlot.size;
+ return this._windows.length;
}
clear() {
- this._windowToSlot.clear();
+ this._windows = [];
}
swapWith(otherTracker) {
- const tempWindowToSlot = this._windowToSlot;
- this._windowToSlot = otherTracker._windowToSlot;
- otherTracker._windowToSlot = tempWindowToSlot;
+ const temp = this._windows;
+ this._windows = otherTracker._windows;
+ otherTracker._windows = temp;
}
}
diff --git a/lib/workspace.js b/lib/workspace.js
index b9d0d6c..f1a6884 100644
--- a/lib/workspace.js
+++ b/lib/workspace.js
@@ -19,17 +19,7 @@ export class WorkspaceLayout {
trackWindow(window, monitorId, preferredSlot) {
const tracker = this._getTracker(monitorId);
if (tracker.getSlot(window) === undefined) {
- if (preferredSlot !== undefined) {
- tracker.windows.forEach(w => {
- const s = tracker.getSlot(w);
- if (s >= preferredSlot) {
- tracker.track(w, s + 1);
- }
- });
- tracker.track(window, preferredSlot);
- } else {
- tracker.track(window, tracker.size);
- }
+ tracker.startTracking(window, preferredSlot);
}
}
@@ -38,7 +28,7 @@ export class WorkspaceLayout {
*/
untrackWindow(window, monitorId) {
const tracker = this._getTracker(monitorId);
- tracker.untrack(window);
+ tracker.stopTracking(window);
}
/**
@@ -46,11 +36,7 @@ export class WorkspaceLayout {
*/
replaceWindow(oldWindow, newWindow, monitorId) {
const tracker = this._getTracker(monitorId);
- const slot = tracker.getSlot(oldWindow);
- if (slot !== undefined) {
- tracker.untrack(oldWindow);
- tracker.track(newWindow, slot);
- }
+ tracker.replace(oldWindow, newWindow);
}
/**
@@ -81,8 +67,6 @@ export class WorkspaceLayout {
if (!layout) return [];
return tracker.windows.map((win, index) => {
- // Re-normalize slots
- tracker.track(win, index);
const estate = layout.getEstate(index);
if (!estate) return null;
return {
@@ -169,11 +153,11 @@ export class WorkspaceLayout {
if (targetEdgingSlot !== -1) {
const targetWindow = targetTracker.windows.find(w => targetTracker.getSlot(w) === targetEdgingSlot);
if (targetWindow) {
- sourceTracker.untrack(window);
- targetTracker.untrack(targetWindow);
+ sourceTracker.stopTracking(window);
+ targetTracker.stopTracking(targetWindow);
- targetTracker.track(window, targetEdgingSlot);
- sourceTracker.track(targetWindow, sourceSlot);
+ targetTracker.startTracking(window, targetEdgingSlot);
+ sourceTracker.startTracking(targetWindow, sourceSlot);
return { swappedWindow: targetWindow, behavior: 'swap' };
}
@@ -181,7 +165,7 @@ export class WorkspaceLayout {
}
}
- sourceTracker.untrack(window);
+ sourceTracker.stopTracking(window);
const targetLayout = this.escalator.getLayoutForCount(targetTracker.size + 1);
let preferredSlot = targetTracker.size;
diff --git a/schemas/org.gnome.shell.extensions.workflow-tiling.gschema.xml b/schemas/org.gnome.shell.extensions.workflow-tiling.gschema.xml
index 01828d3..6612dcc 100644
--- a/schemas/org.gnome.shell.extensions.workflow-tiling.gschema.xml
+++ b/schemas/org.gnome.shell.extensions.workflow-tiling.gschema.xml
@@ -173,7 +173,7 @@
-
+
Custom Layouts JSON
JSON string defining custom window layouts.
diff --git a/tests/adversarial.test.js b/tests/adversarial.test.js
index dd2a541..0a821c2 100644
--- a/tests/adversarial.test.js
+++ b/tests/adversarial.test.js
@@ -3,7 +3,7 @@ import { TilingController } from '../lib/controller.js';
import { LayoutParser } from '../lib/layout.js';
import Meta from 'gi://Meta';
-const DEFAULT_JSON = '{"1":[{"x":0,"y":0,"w":100,"h":100,"id":1}],"2":[{"x":0,"y":0,"w":50,"h":100,"id":1},{"x":50,"y":0,"w":50,"h":100,"id":2}],"3":[{"x":0,"y":0,"w":50,"h":100,"id":1},{"x":50,"y":0,"w":50,"h":50,"id":2},{"x":50,"y":50,"w":50,"h":50,"id":3}]}';
+const DEFAULT_JSON = '{"1":[{"x":0,"y":0,"w":100,"h":100,"id":0}],"2":[{"x":0,"y":0,"w":50,"h":100,"id":0},{"x":50,"y":0,"w":50,"h":100,"id":1}],"3":[{"x":0,"y":0,"w":50,"h":100,"id":0},{"x":50,"y":0,"w":50,"h":50,"id":1},{"x":50,"y":50,"w":50,"h":50,"id":2}]}';
describe('Adversarial Tests', () => {
describe('enteringEdge calculation in tilingRequest', () => {
diff --git a/tests/controller.test.js b/tests/controller.test.js
index a2e5152..2a1284b 100644
--- a/tests/controller.test.js
+++ b/tests/controller.test.js
@@ -3,7 +3,7 @@ import { TilingController } from '../lib/controller.js';
import { LayoutParser } from '../lib/layout.js';
import Meta from 'gi://Meta';
-const DEFAULT_JSON = '{"1":[{"x":0,"y":0,"w":100,"h":100,"id":1}],"2":[{"x":0,"y":0,"w":50,"h":100,"id":1},{"x":50,"y":0,"w":50,"h":100,"id":2}],"3":[{"x":0,"y":0,"w":50,"h":100,"id":1},{"x":50,"y":0,"w":50,"h":50,"id":2},{"x":50,"y":50,"w":50,"h":50,"id":3}]}';
+const DEFAULT_JSON = '{"1":[{"x":0,"y":0,"w":100,"h":100,"id":0}],"2":[{"x":0,"y":0,"w":50,"h":100,"id":0},{"x":50,"y":0,"w":50,"h":100,"id":1}],"3":[{"x":0,"y":0,"w":50,"h":100,"id":0},{"x":50,"y":0,"w":50,"h":50,"id":1},{"x":50,"y":50,"w":50,"h":50,"id":2}]}';
describe('TilingController', () => {
let controller;
@@ -288,6 +288,41 @@ describe('TilingController', () => {
expect(layout.monitors.get('monitor-1').windows).toContain(win);
});
+ it('should tile evacuated window when manually unminimized on remaining monitor', () => {
+ const ws = {
+ id: 'ws1',
+ get_work_area_for_monitor: vi.fn(() => ({ x: 0, y: 0, width: 1000, height: 1000 })),
+ list_windows: () => [win]
+ };
+
+ const win = createMockWindow(1, ws, 1);
+ controller.tilingRequest(win);
+ expect(controller._windowWrappers.get(win).monitorId).toBe('monitor-1');
+
+ // Remove monitor-1 -> triggers evacuation
+ const manager = Meta.Backend.get_monitor_manager();
+ vi.mocked(manager.get_logical_monitors).mockReturnValue([
+ { get_monitors: () => [{ get_stable_id: () => 'monitor-0', get_connector: () => 'DP-1' }] }
+ ]);
+ vi.mocked(win.get_monitor).mockReturnValue(0);
+ controller.tilingRequest(win);
+
+ expect(win.minimize).toHaveBeenCalled();
+ expect(win.minimized).toBe(true);
+ expect(controller.monitorManager.isEvacuated(win)).toBe(true);
+
+ controller.monitorManager.handleMonitorsChanged();
+
+ // User manually unminimizes window on remaining monitor (monitor-0)
+ win.minimized = false;
+ controller.tilingRequest(win);
+
+ // Evacuation flag should be cleared and window tracked on monitor-0
+ expect(controller.monitorManager.isEvacuated(win)).toBe(false);
+ const layout = controller.workspaceManager.getLayout(ws);
+ expect(layout.monitors.get('monitor-0').windows).toContain(win);
+ });
+
it('should handle monitor index shifting via hydration sweep', () => {
const ws = {
id: 'ws1',
diff --git a/tests/drag.test.js b/tests/drag.test.js
index a6c8fd3..a202172 100644
--- a/tests/drag.test.js
+++ b/tests/drag.test.js
@@ -3,7 +3,7 @@ import { TilingController } from '../lib/controller.js';
import { LayoutParser } from '../lib/layout.js';
import Meta from 'gi://Meta';
-const DEFAULT_JSON = '{"1":[{"x":0,"y":0,"w":100,"h":100,"id":1}],"2":[{"x":0,"y":0,"w":50,"h":100,"id":1},{"x":50,"y":0,"w":50,"h":100,"id":2}],"3":[{"x":0,"y":0,"w":50,"h":100,"id":1},{"x":50,"y":0,"w":50,"h":50,"id":2},{"x":50,"y":50,"w":50,"h":50,"id":3}]}';
+const DEFAULT_JSON = '{"1":[{"x":0,"y":0,"w":100,"h":100,"id":0}],"2":[{"x":0,"y":0,"w":50,"h":100,"id":0},{"x":50,"y":0,"w":50,"h":100,"id":1}],"3":[{"x":0,"y":0,"w":50,"h":100,"id":0},{"x":50,"y":0,"w":50,"h":50,"id":1},{"x":50,"y":50,"w":50,"h":50,"id":2}]}';
describe('DragManager Cross-Monitor', () => {
let controller;
diff --git a/tests/layout.test.js b/tests/layout.test.js
index 3372c38..1504908 100644
--- a/tests/layout.test.js
+++ b/tests/layout.test.js
@@ -4,7 +4,7 @@ import { ScreenEstate, Layout, LayoutParser, LayoutValidator } from '../lib/layo
describe('ScreenEstate', () => {
it('should calculate absolute coordinates with gaps', () => {
// TilingConfig.GAPS.OUTER = 4, INNER = 6
- const estate = new ScreenEstate(0, 0, 100, 100);
+ const estate = new ScreenEstate(0, 0, 0, 100, 100);
const monitor = { x: 0, y: 0, width: 1000, height: 1000 };
const absolute = estate.toAbsolute(monitor);
@@ -17,14 +17,14 @@ describe('ScreenEstate', () => {
});
it('should calculate inner gaps correctly in a split', () => {
- const leftEstate = new ScreenEstate(0, 0, 50, 100);
+ const leftEstate = new ScreenEstate(0, 0, 0, 50, 100);
const monitor = { x: 0, y: 0, width: 1000, height: 1000 };
const leftAbs = leftEstate.toAbsolute(monitor);
expect(leftAbs.x).toBe(4);
expect(leftAbs.width).toBe(493);
- const rightEstate = new ScreenEstate(50, 0, 50, 100);
+ const rightEstate = new ScreenEstate(1, 50, 0, 50, 100);
const rightAbs = rightEstate.toAbsolute(monitor);
expect(rightAbs.x).toBe(503);
@@ -34,30 +34,30 @@ describe('ScreenEstate', () => {
});
it('should throw on out of bounds', () => {
- expect(() => new ScreenEstate(-1, 0, 100, 100)).toThrow();
+ expect(() => new ScreenEstate(0, -1, 0, 100, 100)).toThrow();
});
});
describe('Validation & Immutability', () => {
it('should allow correct layouts', () => {
expect(() => new Layout([
- new ScreenEstate(0, 0, 50, 100),
- new ScreenEstate(50, 0, 50, 100)
+ new ScreenEstate(0, 0, 0, 50, 100),
+ new ScreenEstate(1, 50, 0, 50, 100)
])).not.toThrow();
});
it('should throw on out of bounds ScreenEstate', () => {
- expect(() => new ScreenEstate(0, 0, 110, 100)).toThrow('out of bounds');
+ expect(() => new ScreenEstate(0, 0, 0, 110, 100)).toThrow('out of bounds');
});
it('should throw on overlapping estates in Layout', () => {
- const e1 = new ScreenEstate(0, 0, 60, 100);
- const e2 = new ScreenEstate(40, 0, 60, 100);
+ const e1 = new ScreenEstate(0, 0, 0, 60, 100);
+ const e2 = new ScreenEstate(1, 40, 0, 60, 100);
expect(() => new Layout([e1, e2])).toThrow('Overlap detected');
});
it('should be immutable', () => {
- const estate = new ScreenEstate(0, 0, 100, 100);
+ const estate = new ScreenEstate(0, 0, 0, 100, 100);
expect(() => { estate.pct_x = 10; }).toThrow();
const layout = new Layout([estate]);
@@ -66,7 +66,7 @@ describe('Validation & Immutability', () => {
});
it('should validate the default escalator layouts', () => {
- const defaultJson = '{"1":[{"x":0,"y":0,"w":100,"h":100,"id":1}],"2":[{"x":0,"y":0,"w":50,"h":100,"id":1},{"x":50,"y":0,"w":50,"h":100,"id":2}],"3":[{"x":0,"y":0,"w":50,"h":100,"id":1},{"x":50,"y":0,"w":50,"h":50,"id":2},{"x":50,"y":50,"w":50,"h":50,"id":3}]}';
+ const defaultJson = '{"1":[{"x":0,"y":0,"w":100,"h":100,"id":0}],"2":[{"x":0,"y":0,"w":50,"h":100,"id":0},{"x":50,"y":0,"w":50,"h":100,"id":1}],"3":[{"x":0,"y":0,"w":50,"h":100,"id":0},{"x":50,"y":0,"w":50,"h":50,"id":1},{"x":50,"y":50,"w":50,"h":50,"id":2}]}';
const escalator = LayoutParser.parse(defaultJson);
for (let i = 1; i <= 3; i++) {
const layout = escalator.getLayoutForCount(i);
@@ -80,15 +80,15 @@ describe('LayoutValidator', () => {
describe('validateCoverage', () => {
it('should pass for exact 10000 area', () => {
const layout = new Layout([
- new ScreenEstate(0, 0, 100, 100)
+ new ScreenEstate(0, 0, 0, 100, 100)
]);
expect(() => LayoutValidator.validateCoverage(layout)).not.toThrow();
});
it('should throw if area is less than 10000', () => {
const layout = new Layout([
- new ScreenEstate(0, 0, 50, 100),
- new ScreenEstate(50, 0, 40, 100)
+ new ScreenEstate(0, 0, 0, 50, 100),
+ new ScreenEstate(1, 50, 0, 40, 100)
]);
expect(() => LayoutValidator.validateCoverage(layout)).toThrow(/expected 10000/);
});
@@ -97,9 +97,9 @@ describe('LayoutValidator', () => {
describe('Arbitrary Transitions', () => {
it('should allow arbitrary transitions like 50/50 split to 33/33/33 split', () => {
const json = JSON.stringify({
- "1": [ { "x": 0, "y": 0, "w": 100, "h": 100, "id": 1 } ],
- "2": [ { "x": 0, "y": 0, "w": 50, "h": 100, "id": 1 }, { "x": 50, "y": 0, "w": 50, "h": 100, "id": 2 } ],
- "3": [ { "x": 0, "y": 0, "w": 33.33, "h": 100, "id": 1 }, { "x": 33.33, "y": 0, "w": 33.33, "h": 100, "id": 2 }, { "x": 66.66, "y": 0, "w": 33.34, "h": 100, "id": 3 } ]
+ "1": [ { "x": 0, "y": 0, "w": 100, "h": 100, "id": 0 } ],
+ "2": [ { "x": 0, "y": 0, "w": 50, "h": 100, "id": 0 }, { "x": 50, "y": 0, "w": 50, "h": 100, "id": 1 } ],
+ "3": [ { "x": 0, "y": 0, "w": 33.33, "h": 100, "id": 0 }, { "x": 33.33, "y": 0, "w": 33.33, "h": 100, "id": 1 }, { "x": 66.66, "y": 0, "w": 33.34, "h": 100, "id": 2 } ]
});
const escalator = LayoutParser.parse(json);
expect(escalator).not.toBeNull();
@@ -126,8 +126,8 @@ describe('LayoutParser', () => {
it('should parse valid json and return escalator', () => {
const json = JSON.stringify({
- "1": [ { "x": 0, "y": 0, "w": 100, "h": 100, "id": 1 } ],
- "2": [ { "x": 0, "y": 0, "w": 50, "h": 100, "id": 1 }, { "x": 50, "y": 0, "w": 50, "h": 100, "id": 2 } ]
+ "1": [ { "x": 0, "y": 0, "w": 100, "h": 100, "id": 0 } ],
+ "2": [ { "x": 0, "y": 0, "w": 50, "h": 100, "id": 0 }, { "x": 50, "y": 0, "w": 50, "h": 100, "id": 1 } ]
});
const escalator = LayoutParser.parse(json);
expect(escalator).not.toBeNull();
@@ -141,9 +141,9 @@ describe('LayoutParser', () => {
it('should sort estates by id property', () => {
const json = JSON.stringify({
"3": [
- { "x": 33.33, "y": 0, "w": 33.33, "h": 100, "id": 3 },
- { "x": 66.66, "y": 0, "w": 33.34, "h": 100, "id": 2 },
- { "x": 0, "y": 0, "w": 33.33, "h": 100, "id": 1 }
+ { "x": 33.33, "y": 0, "w": 33.33, "h": 100, "id": 2 },
+ { "x": 66.66, "y": 0, "w": 33.34, "h": 100, "id": 1 },
+ { "x": 0, "y": 0, "w": 33.33, "h": 100, "id": 0 }
]
});
const escalator = LayoutParser.parse(json);
@@ -156,7 +156,7 @@ describe('LayoutParser', () => {
it('should throw if any estate is missing an id', () => {
const json = JSON.stringify({
"2": [
- { "x": 0, "y": 0, "w": 50, "h": 100, "id": 1 },
+ { "x": 0, "y": 0, "w": 50, "h": 100, "id": 0 },
{ "x": 50, "y": 0, "w": 50, "h": 100 }
]
});
@@ -166,8 +166,8 @@ describe('LayoutParser', () => {
it('should throw if duplicate ids are present', () => {
const json = JSON.stringify({
"2": [
- { "x": 0, "y": 0, "w": 50, "h": 100, "id": 1 },
- { "x": 50, "y": 0, "w": 50, "h": 100, "id": 1 }
+ { "x": 0, "y": 0, "w": 50, "h": 100, "id": 0 },
+ { "x": 50, "y": 0, "w": 50, "h": 100, "id": 0 }
]
});
expect(() => LayoutParser.parse(json)).toThrow(/must have unique ids/);
@@ -176,10 +176,10 @@ describe('LayoutParser', () => {
it('should throw if id is out of bounds', () => {
const json = JSON.stringify({
"2": [
- { "x": 0, "y": 0, "w": 50, "h": 100, "id": 1 },
- { "x": 50, "y": 0, "w": 50, "h": 100, "id": 3 }
+ { "x": 0, "y": 0, "w": 50, "h": 100, "id": 0 },
+ { "x": 50, "y": 0, "w": 50, "h": 100, "id": 2 }
]
});
- expect(() => LayoutParser.parse(json)).toThrow(/is missing id 2/);
+ expect(() => LayoutParser.parse(json)).toThrow(/is missing id 1/);
});
});
diff --git a/tests/monitor-transition.test.js b/tests/monitor-transition.test.js
index 5d67cad..992c1f8 100644
--- a/tests/monitor-transition.test.js
+++ b/tests/monitor-transition.test.js
@@ -5,11 +5,11 @@ import { SettingsManager } from '../lib/settings.js';
import Meta from 'gi://Meta';
import Gio from 'gi://Gio';
-const DEFAULT_JSON = '{"1":[{"x":0,"y":0,"w":100,"h":100,"id":1}],"2":[{"x":0,"y":0,"w":50,"h":100,"id":1},{"x":50,"y":0,"w":50,"h":100,"id":2}],"3":[{"x":0,"y":0,"w":50,"h":100,"id":1},{"x":50,"y":0,"w":50,"h":50,"id":2},{"x":50,"y":50,"w":50,"h":50,"id":3}]}';
+const DEFAULT_JSON = '{"1":[{"x":0,"y":0,"w":100,"h":100,"id":0}],"2":[{"x":0,"y":0,"w":50,"h":100,"id":0},{"x":50,"y":0,"w":50,"h":100,"id":1}],"3":[{"x":0,"y":0,"w":50,"h":100,"id":0},{"x":50,"y":0,"w":50,"h":50,"id":1},{"x":50,"y":50,"w":50,"h":50,"id":2}]}';
describe('Edging Tile Identification', () => {
it('should identify edging tile for layout of size 1', () => {
- const estates = [new ScreenEstate(0, 0, 100, 100)];
+ const estates = [new ScreenEstate(0, 0, 0, 100, 100)];
const layout = new Layout(estates);
expect(layout.getEdgingSlot('left')).toBe(0);
expect(layout.getEdgingSlot('right')).toBe(0);
@@ -20,8 +20,8 @@ describe('Edging Tile Identification', () => {
it('should identify edging tile for layout of size 2 (tie-break right/upper)', () => {
// Vertical split: left slot 0, right slot 1
const estates = [
- new ScreenEstate(0, 0, 50, 100),
- new ScreenEstate(50, 0, 50, 100)
+ new ScreenEstate(0, 0, 0, 50, 100),
+ new ScreenEstate(1, 50, 0, 50, 100)
];
const layout = new Layout(estates);
expect(layout.getEdgingSlot('left')).toBe(0);
@@ -34,9 +34,9 @@ describe('Edging Tile Identification', () => {
it('should identify edging tile for layout of size 3 (escalator layout)', () => {
// Left slot 0, top-right slot 1, bottom-right slot 2
const estates = [
- new ScreenEstate(0, 0, 50, 100),
- new ScreenEstate(50, 0, 50, 50),
- new ScreenEstate(50, 50, 50, 50)
+ new ScreenEstate(0, 0, 0, 50, 100),
+ new ScreenEstate(1, 50, 0, 50, 50),
+ new ScreenEstate(2, 50, 50, 50, 50)
];
const layout = new Layout(estates);
expect(layout.getEdgingSlot('left')).toBe(0);
@@ -51,9 +51,9 @@ describe('Edging Tile Identification', () => {
it('should prioritize longest edge for edging tile identification', () => {
// Left slot 0 (30 width), top-right slot 1 (70 width, 40 height), bottom-right slot 2 (70 width, 60 height)
const estates = [
- new ScreenEstate(0, 0, 30, 100),
- new ScreenEstate(30, 0, 70, 40),
- new ScreenEstate(30, 40, 70, 60)
+ new ScreenEstate(0, 0, 0, 30, 100),
+ new ScreenEstate(1, 30, 0, 70, 40),
+ new ScreenEstate(2, 30, 40, 70, 60)
];
const layout = new Layout(estates);
expect(layout.getEdgingSlot('left')).toBe(0);
diff --git a/tests/regressions.test.js b/tests/regressions.test.js
index 411e990..0965f69 100644
--- a/tests/regressions.test.js
+++ b/tests/regressions.test.js
@@ -4,7 +4,7 @@ import { LayoutParser } from '../lib/layout.js';
import { WorkspaceLayout } from '../lib/workspace.js';
import Meta from 'gi://Meta';
-const DEFAULT_JSON = '{"1":[{"x":0,"y":0,"w":100,"h":100,"id":1}],"2":[{"x":0,"y":0,"w":50,"h":100,"id":1},{"x":50,"y":0,"w":50,"h":100,"id":2}],"3":[{"x":0,"y":0,"w":50,"h":100,"id":1},{"x":50,"y":0,"w":50,"h":50,"id":2},{"x":50,"y":50,"w":50,"h":50,"id":3}]}';
+const DEFAULT_JSON = '{"1":[{"x":0,"y":0,"w":100,"h":100,"id":0}],"2":[{"x":0,"y":0,"w":50,"h":100,"id":0},{"x":50,"y":0,"w":50,"h":100,"id":1}],"3":[{"x":0,"y":0,"w":50,"h":100,"id":0},{"x":50,"y":0,"w":50,"h":50,"id":1},{"x":50,"y":50,"w":50,"h":50,"id":2}]}';
const escalator = LayoutParser.parse(DEFAULT_JSON);
describe('Regressions', () => {
diff --git a/tests/workspace.test.js b/tests/workspace.test.js
index 6e61209..cb5eda0 100644
--- a/tests/workspace.test.js
+++ b/tests/workspace.test.js
@@ -3,7 +3,7 @@ import { WorkspaceLayout, WorkspaceManager } from '../lib/workspace.js';
import { LayoutParser } from '../lib/layout.js';
describe('WorkspaceLayout', () => {
- const defaultJson = '{"1":[{"x":0,"y":0,"w":100,"h":100,"id":1}],"2":[{"x":0,"y":0,"w":50,"h":100,"id":1},{"x":50,"y":0,"w":50,"h":100,"id":2}],"3":[{"x":0,"y":0,"w":50,"h":100,"id":1},{"x":50,"y":0,"w":50,"h":50,"id":2},{"x":50,"y":50,"w":50,"h":50,"id":3}]}';
+ const defaultJson = '{"1":[{"x":0,"y":0,"w":100,"h":100,"id":0}],"2":[{"x":0,"y":0,"w":50,"h":100,"id":0},{"x":50,"y":0,"w":50,"h":100,"id":1}],"3":[{"x":0,"y":0,"w":50,"h":100,"id":0},{"x":50,"y":0,"w":50,"h":50,"id":1},{"x":50,"y":50,"w":50,"h":50,"id":2}]}';
const escalator = LayoutParser.parse(defaultJson);
const controller = { escalator };
const monitorRect = { x: 0, y: 0, width: 1000, height: 1000 };
@@ -232,7 +232,7 @@ describe('WorkspaceManager', () => {
const w = this._windowWrappers.get(win);
if (w) { w.monitorId = id; w.monitorIndex = idx; }
},
- escalator: LayoutParser.parse('{"1":[{"x":0,"y":0,"w":100,"h":100,"id":1}],"2":[{"x":0,"y":0,"w":50,"h":100,"id":1},{"x":50,"y":0,"w":50,"h":100,"id":2}],"3":[{"x":0,"y":0,"w":50,"h":100,"id":1},{"x":50,"y":0,"w":50,"h":50,"id":2},{"x":50,"y":50,"w":50,"h":50,"id":3}]}')
+ escalator: LayoutParser.parse('{"1":[{"x":0,"y":0,"w":100,"h":100,"id":0}],"2":[{"x":0,"y":0,"w":50,"h":100,"id":0},{"x":50,"y":0,"w":50,"h":100,"id":1}],"3":[{"x":0,"y":0,"w":50,"h":100,"id":0},{"x":50,"y":0,"w":50,"h":50,"id":1},{"x":50,"y":50,"w":50,"h":50,"id":2}]}')
};
manager = new WorkspaceManager(controller);
});
@@ -331,7 +331,7 @@ describe('WorkspaceManager', () => {
});
describe('WorkspaceLayout Cross-Monitor Fallback', () => {
- const defaultJson = '{"1":[{"x":0,"y":0,"w":100,"h":100,"id":1}],"2":[{"x":0,"y":0,"w":50,"h":100,"id":1},{"x":50,"y":0,"w":50,"h":100,"id":2}],"3":[{"x":0,"y":0,"w":50,"h":100,"id":1},{"x":50,"y":0,"w":50,"h":50,"id":2},{"x":50,"y":50,"w":50,"h":50,"id":3}]}';
+ const defaultJson = '{"1":[{"x":0,"y":0,"w":100,"h":100,"id":0}],"2":[{"x":0,"y":0,"w":50,"h":100,"id":0},{"x":50,"y":0,"w":50,"h":100,"id":1}],"3":[{"x":0,"y":0,"w":50,"h":100,"id":0},{"x":50,"y":0,"w":50,"h":50,"id":1},{"x":50,"y":50,"w":50,"h":50,"id":2}]}';
const escalator = LayoutParser.parse(defaultJson);
const controller = { escalator };