Compare commits

..

1 Commits

Author SHA1 Message Date
Renovate Bot e790af82c1 chore(deps): update dependency eslint to v9.39.3
Build and Test / build (pull_request) Successful in 26s
Build and Test / release (pull_request) Has been skipped
2026-02-24 04:21:27 +00:00
9 changed files with 145 additions and 566 deletions
-15
View File
@@ -20,7 +20,6 @@ export default class aerospike extends Extension {
enable() {
Logger.log("STARTING AEROSPIKE!")
this.windowManager._settings = this.settings;
this.bindSettings();
this.setupKeybindings();
this.windowManager.enable()
@@ -64,11 +63,6 @@ export default class aerospike extends Extension {
this.refreshKeybinding('toggle-orientation');
});
this.settings.connect('changed::reset-ratios', () => {
log(`Reset ratios keybinding changed to: ${this.settings.get_strv('reset-ratios')}`);
this.refreshKeybinding('reset-ratios');
});
this.settings.connect('changed::dropdown-option', () => {
log(`Dropdown option changed to: ${this.settings.get_string('dropdown-option')}`);
});
@@ -114,11 +108,6 @@ export default class aerospike extends Extension {
this.windowManager.toggleActiveContainerOrientation();
});
break;
case 'reset-ratios':
this.bindKeybinding('reset-ratios', () => {
this.windowManager.resetActiveContainerRatios();
});
break;
}
}
@@ -153,10 +142,6 @@ export default class aerospike extends Extension {
this.bindKeybinding('toggle-orientation', () => {
this.windowManager.toggleActiveContainerOrientation();
});
this.bindKeybinding('reset-ratios', () => {
this.windowManager.resetActiveContainerRatios();
});
}
private bindKeybinding(settingName: string, callback: () => void) {
+62 -43
View File
@@ -44,10 +44,10 @@ importers:
version: 30.0.0
eslint:
specifier: ^9.36.0
version: 9.39.2
version: 9.39.3
eslint-plugin-jsdoc:
specifier: ^62.0.0
version: 62.4.1(eslint@9.39.2)
version: 62.4.1(eslint@9.39.3)
jest:
specifier: ^30.0.0
version: 30.2.0(@types/node@25.1.0)
@@ -264,12 +264,12 @@ packages:
resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/eslintrc@3.3.3':
resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==}
'@eslint/eslintrc@3.3.4':
resolution: {integrity: sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/js@9.39.2':
resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==}
'@eslint/js@9.39.3':
resolution: {integrity: sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/object-schema@2.1.7':
@@ -724,8 +724,13 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
acorn@8.16.0:
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
engines: {node: '>=0.4.0'}
hasBin: true
ajv@6.14.0:
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
ansi-escapes@4.3.2:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
@@ -793,6 +798,10 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
balanced-match@4.0.4:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
baseline-browser-mapping@2.9.19:
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
hasBin: true
@@ -800,8 +809,9 @@ packages:
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
brace-expansion@5.0.3:
resolution: {integrity: sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==}
engines: {node: 18 || 20 || >=22}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@@ -965,8 +975,8 @@ packages:
resolution: {integrity: sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint@9.39.2:
resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==}
eslint@9.39.3:
resolution: {integrity: sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
hasBin: true
peerDependencies:
@@ -1085,11 +1095,12 @@ packages:
glob@10.5.0:
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
globals@14.0.0:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
@@ -1414,11 +1425,11 @@ packages:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
minimatch@3.1.3:
resolution: {integrity: sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
minimatch@9.0.6:
resolution: {integrity: sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==}
engines: {node: '>=16 || 14 >=14.17'}
minimist@1.2.8:
@@ -2031,9 +2042,9 @@ snapshots:
'@es-joy/resolve.exports@1.2.0': {}
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.2)':
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.3)':
dependencies:
eslint: 9.39.2
eslint: 9.39.3
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
@@ -2042,7 +2053,7 @@ snapshots:
dependencies:
'@eslint/object-schema': 2.1.7
debug: 4.4.3
minimatch: 3.1.2
minimatch: 3.1.3
transitivePeerDependencies:
- supports-color
@@ -2054,21 +2065,21 @@ snapshots:
dependencies:
'@types/json-schema': 7.0.15
'@eslint/eslintrc@3.3.3':
'@eslint/eslintrc@3.3.4':
dependencies:
ajv: 6.12.6
ajv: 6.14.0
debug: 4.4.3
espree: 10.4.0
globals: 14.0.0
ignore: 5.3.2
import-fresh: 3.3.1
js-yaml: 4.1.1
minimatch: 3.1.2
minimatch: 3.1.3
strip-json-comments: 3.1.1
transitivePeerDependencies:
- supports-color
'@eslint/js@9.39.2': {}
'@eslint/js@9.39.3': {}
'@eslint/object-schema@2.1.7': {}
@@ -2906,9 +2917,15 @@ snapshots:
dependencies:
acorn: 8.15.0
acorn-jsx@5.3.2(acorn@8.16.0):
dependencies:
acorn: 8.16.0
acorn@8.15.0: {}
ajv@6.12.6:
acorn@8.16.0: {}
ajv@6.14.0:
dependencies:
fast-deep-equal: 3.1.3
fast-json-stable-stringify: 2.1.0
@@ -2998,6 +3015,8 @@ snapshots:
balanced-match@1.0.2: {}
balanced-match@4.0.4: {}
baseline-browser-mapping@2.9.19: {}
brace-expansion@1.1.12:
@@ -3005,9 +3024,9 @@ snapshots:
balanced-match: 1.0.2
concat-map: 0.0.1
brace-expansion@2.0.2:
brace-expansion@5.0.3:
dependencies:
balanced-match: 1.0.2
balanced-match: 4.0.4
braces@3.0.3:
dependencies:
@@ -3110,7 +3129,7 @@ snapshots:
escape-string-regexp@4.0.0: {}
eslint-plugin-jsdoc@62.4.1(eslint@9.39.2):
eslint-plugin-jsdoc@62.4.1(eslint@9.39.3):
dependencies:
'@es-joy/jsdoccomment': 0.83.0
'@es-joy/resolve.exports': 1.2.0
@@ -3118,7 +3137,7 @@ snapshots:
comment-parser: 1.4.5
debug: 4.4.3
escape-string-regexp: 4.0.0
eslint: 9.39.2
eslint: 9.39.3
espree: 11.1.0
esquery: 1.7.0
html-entities: 2.6.0
@@ -3141,21 +3160,21 @@ snapshots:
eslint-visitor-keys@5.0.0: {}
eslint@9.39.2:
eslint@9.39.3:
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2)
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3)
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.21.1
'@eslint/config-helpers': 0.4.2
'@eslint/core': 0.17.0
'@eslint/eslintrc': 3.3.3
'@eslint/js': 9.39.2
'@eslint/eslintrc': 3.3.4
'@eslint/js': 9.39.3
'@eslint/plugin-kit': 0.4.1
'@humanfs/node': 0.16.7
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3
'@types/estree': 1.0.8
ajv: 6.12.6
ajv: 6.14.0
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.3
@@ -3174,7 +3193,7 @@ snapshots:
is-glob: 4.0.3
json-stable-stringify-without-jsonify: 1.0.1
lodash.merge: 4.6.2
minimatch: 3.1.2
minimatch: 3.1.3
natural-compare: 1.4.0
optionator: 0.9.4
transitivePeerDependencies:
@@ -3182,8 +3201,8 @@ snapshots:
espree@10.4.0:
dependencies:
acorn: 8.15.0
acorn-jsx: 5.3.2(acorn@8.15.0)
acorn: 8.16.0
acorn-jsx: 5.3.2(acorn@8.16.0)
eslint-visitor-keys: 4.2.1
espree@11.1.0:
@@ -3290,7 +3309,7 @@ snapshots:
dependencies:
foreground-child: 3.3.1
jackspeak: 3.4.3
minimatch: 9.0.5
minimatch: 9.0.6
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
@@ -3300,7 +3319,7 @@ snapshots:
fs.realpath: 1.0.0
inflight: 1.0.6
inherits: 2.0.4
minimatch: 3.1.2
minimatch: 3.1.3
once: 1.4.0
path-is-absolute: 1.0.1
@@ -3787,13 +3806,13 @@ snapshots:
mimic-fn@2.1.0: {}
minimatch@3.1.2:
minimatch@3.1.3:
dependencies:
brace-expansion: 1.1.12
minimatch@9.0.5:
minimatch@9.0.6:
dependencies:
brace-expansion: 2.0.2
brace-expansion: 5.0.3
minimist@1.2.8: {}
@@ -4007,7 +4026,7 @@ snapshots:
dependencies:
'@istanbuljs/schema': 0.1.3
glob: 7.2.3
minimatch: 3.1.2
minimatch: 3.1.3
tmpl@1.0.5: {}
@@ -49,18 +49,5 @@
<description>Toggles the orientation of the container holding the active window between horizontal and vertical</description>
</key>
<key name="reset-ratios" type="as">
<default><![CDATA[['<Primary>z']]]></default>
<summary>Reset container ratios to equal splits</summary>
<description>Resets all window size ratios in the active window's container to equal splits</description>
</key>
<key name="min-window-size-percent" type="d">
<default>0.10</default>
<range min="0.01" max="0.49"/>
<summary>Minimum window size percentage</summary>
<description>Minimum fraction of a container that any single window may occupy when resizing boundaries</description>
</key>
</schema>
</schemalist>
-43
View File
@@ -173,49 +173,6 @@ export default class AerospikeExtensions extends ExtensionPreferences {
})
);
keybindingsGroup.add(
new EntryRow({
title: _('Reset Container Ratios to Equal'),
settings: settings,
bind: 'reset-ratios',
map: keybindingMap
})
);
// Create sizing group
const sizingGroup = new Adw.PreferencesGroup({
title: _('Window Sizing'),
});
page.add(sizingGroup);
// Minimum window size percentage spinner
const minSizeRow = new Adw.ActionRow({
title: _('Minimum Window Size'),
subtitle: _('Smallest fraction of a container any window may occupy when resizing (e.g. 0.10 = 10%)'),
});
sizingGroup.add(minSizeRow);
const minSizeSpin = new Gtk.SpinButton({
adjustment: new Gtk.Adjustment({
lower: 0.01,
upper: 0.49,
step_increment: 0.01,
page_increment: 0.05,
value: settings.get_double('min-window-size-percent'),
}),
digits: 2,
valign: Gtk.Align.CENTER,
});
minSizeRow.add_suffix(minSizeSpin);
minSizeRow.set_activatable_widget(minSizeSpin);
minSizeSpin.connect('value-changed', () => {
settings.set_double('min-window-size-percent', minSizeSpin.get_value());
});
settings.connect('changed::min-window-size-percent', () => {
minSizeSpin.set_value(settings.get_double('min-window-size-percent'));
});
}
+6 -14
View File
@@ -6,23 +6,15 @@ export type QueuedEvent = {
callback: () => void;
}
// Pending events indexed by name so that duplicate events collapse into one.
// Only the most-recently-queued callback for a given name is kept.
const pendingEvents: Map<string, QueuedEvent> = new Map();
const queuedEvents: QueuedEvent[] = [];
export default function queueEvent(event: QueuedEvent, interval = 200) {
// Overwrite any earlier pending event with the same name — the latest
// callback is always the most up-to-date one.
pendingEvents.set(event.name, event);
queuedEvents.push(event);
GLib.timeout_add(GLib.PRIORITY_DEFAULT, interval, () => {
const e = pendingEvents.get(event.name);
if (e && e === event) {
// Only fire if this is still the current callback for this name
// (a newer call may have replaced it).
pendingEvents.delete(event.name);
const e = queuedEvents.pop()
if (e) {
e.callback();
}
return GLib.SOURCE_REMOVE;
return queuedEvents.length !== 0;
});
}
}
+39 -210
View File
@@ -9,21 +9,6 @@ enum Orientation {
VERTICAL = 1,
}
/**
* Build a split-ratio array of length `n` where every element equals 1/n,
* with the last slot absorbing any floating-point remainder so the array
* always sums to exactly 1.0.
*/
function equalRatios(n: number): number[] {
if (n <= 0) return [];
const base = 1 / n;
const ratios = Array(n).fill(base);
// Fix floating-point drift: make last slot exact
const sumExceptLast = ratios.slice(0, -1).reduce((a, b) => a + b, 0);
ratios[n - 1] = 1 - sumExceptLast;
return ratios;
}
export default class WindowContainer {
@@ -32,60 +17,12 @@ export default class WindowContainer {
_orientation: Orientation = Orientation.HORIZONTAL;
_workArea: Rect;
/**
* Per-child split ratios. Always satisfies:
* _splitRatios.length === _tiledItems.length
* _splitRatios.reduce((a,b) => a+b, 0) === 1.0 (within floating-point epsilon)
* every element >= MIN_RATIO
*/
_splitRatios: number[];
constructor(workspaceArea: Rect) {
constructor(workspaceArea: Rect,) {
this._tiledItems = [];
this._tiledWindowLookup = new Map<number, WindowWrapper>();
this._workArea = workspaceArea;
this._splitRatios = [];
}
// ─── Helpers ────────────────────────────────────────────────────────────────
/** Rebuild _splitRatios as equal fractions after any structural change. */
private _resetRatios(): void {
this._splitRatios = equalRatios(this._tiledItems.length);
}
/**
* Called after a new item has been pushed onto _tiledItems.
* The new window (last slot) gets 1/n of the space; existing windows
* are scaled down proportionally so their ratios relative to each other
* are preserved and the total remains 1.0.
*
* e.g. [0.33, 0.166, 0.5] + new → new=0.25, existing scaled by 0.75
* → [0.2475, 0.1245, 0.375, 0.25]
*/
private _addRatioForNewWindow(): void {
const n = this._tiledItems.length;
if (n <= 1) {
this._splitRatios = [1.0];
return;
}
const newRatio = 1 / n;
const scale = 1 - newRatio; // existing windows share this fraction
const scaled = this._splitRatios.map(r => r * scale);
// Absorb all floating-point drift into the last slot so sum is exactly 1.0
const partialSum = scaled.reduce((a, b) => a + b, 0) + newRatio;
scaled[scaled.length - 1] += (1.0 - partialSum);
this._splitRatios = [...scaled, newRatio];
}
/** Total dimension for the active orientation (width for H, height for V). */
private _totalDimension(): number {
return this._orientation === Orientation.HORIZONTAL
? this._workArea.width
: this._workArea.height;
}
// ─── Public API ─────────────────────────────────────────────────────────────
move(rect: Rect): void {
this._workArea = rect;
@@ -103,13 +40,13 @@ export default class WindowContainer {
addWindow(winWrap: WindowWrapper): void {
this._tiledItems.push(winWrap);
this._tiledWindowLookup.set(winWrap.getWindowId(), winWrap);
this._addRatioForNewWindow();
queueEvent({
name: "tiling-windows",
callback: () => {
this.tileWindows();
}
}, 100);
}, 100)
}
getWindow(win_id: number): WindowWrapper | undefined {
@@ -126,27 +63,27 @@ export default class WindowContainer {
return item;
}
}
return undefined;
return undefined
}
_getIndexOfWindow(win_id: number): number {
_getIndexOfWindow(win_id: number) {
for (let i = 0; i < this._tiledItems.length; i++) {
const item = this._tiledItems[i];
if (item instanceof WindowWrapper && item.getWindowId() === win_id) {
return i;
}
}
return -1;
return -1
}
removeWindow(win_id: number): void {
if (this._tiledWindowLookup.has(win_id)) {
// Get index before deleting from lookup to avoid race condition
const index = this._getIndexOfWindow(win_id);
this._tiledWindowLookup.delete(win_id);
if (index !== -1) {
this._tiledItems.splice(index, 1);
}
this._resetRatios();
} else {
for (const item of this._tiledItems) {
if (item instanceof WindowContainer) {
@@ -154,30 +91,33 @@ export default class WindowContainer {
}
}
}
this.tileWindows();
this.tileWindows()
}
disconnectSignals(): void {
this._tiledItems.forEach((item) => {
if (item instanceof WindowContainer) {
item.disconnectSignals();
} else {
item.disconnectWindowSignals();
if (item instanceof WindowContainer) {
item.disconnectSignals()
} else {
item.disconnectWindowSignals();
}
}
});
)
}
removeAllWindows(): void {
this._tiledItems = [];
this._tiledWindowLookup.clear();
this._splitRatios = [];
this._tiledItems = []
this._tiledWindowLookup.clear()
}
tileWindows() {
Logger.log("TILING WINDOWS IN CONTAINER");
Logger.log("TILING WINDOWS IN CONTAINER")
Logger.log("WorkArea", this._workArea);
this._tileItems();
return true;
this._tileItems()
return true
}
_tileItems() {
@@ -185,19 +125,16 @@ export default class WindowContainer {
return;
}
const bounds = this.getBounds();
Logger.info(`_tileItems: ratios=[${this._splitRatios.map(r => r.toFixed(3)).join(', ')}] bounds=[${bounds.map(b => `(${b.x},${b.y},${b.width},${b.height})`).join(', ')}]`);
this._tiledItems.forEach((item, index) => {
const rect = bounds[index];
if (item instanceof WindowContainer) {
item.move(rect);
} else {
Logger.info(`_tileItems: window[${index}] id=${item.getWindowId()} dragging=${item._dragging} → rect=(${rect.x},${rect.y},${rect.width},${rect.height})`);
item.safelyResizeWindow(rect);
}
});
})
}
// ─── Bounds Calculation ──────────────────────────────────────────────────────
getBounds(): Rect[] {
if (this._orientation === Orientation.HORIZONTAL) {
@@ -207,130 +144,33 @@ export default class WindowContainer {
}
getVerticalBounds(): Rect[] {
const items = this._tiledItems;
const totalHeight = this._workArea.height;
let usedHeight = 0;
const items = this._tiledItems
const containerHeight = Math.floor(this._workArea.height / items.length);
return items.map((_, index) => {
const y = this._workArea.y + usedHeight;
let height: number;
if (index === items.length - 1) {
// Last item gets the remainder to avoid pixel gaps from rounding
height = totalHeight - usedHeight;
} else {
height = Math.floor(this._splitRatios[index] * totalHeight);
}
usedHeight += height;
const y = this._workArea.y + (index * containerHeight);
return {
x: this._workArea.x,
y: y,
width: this._workArea.width,
height: height,
height: containerHeight
} as Rect;
});
}
getHorizontalBounds(): Rect[] {
const totalWidth = this._workArea.width;
let usedWidth = 0;
const windowWidth = Math.floor(this._workArea.width / this._tiledItems.length);
return this._tiledItems.map((_, index) => {
const x = this._workArea.x + usedWidth;
let width: number;
if (index === this._tiledItems.length - 1) {
// Last item gets the remainder to avoid pixel gaps from rounding
width = totalWidth - usedWidth;
} else {
width = Math.floor(this._splitRatios[index] * totalWidth);
}
usedWidth += width;
const x = this._workArea.x + (index * windowWidth);
return {
x: x,
y: this._workArea.y,
width: width,
height: this._workArea.height,
width: windowWidth,
height: this._workArea.height
} as Rect;
});
}
// ─── Boundary / Ratio Adjustment ─────────────────────────────────────────────
/**
* Adjust the boundary between item[boundaryIndex] and item[boundaryIndex+1]
* by deltaPixels (positive = move right/down, negative = move left/up).
*
* Both affected ratios are clamped to [_minRatio, 1 - _minRatio] so no
* window can be squashed below the configured minimum.
*
* Returns true if the adjustment was applied, false if it was rejected
* (e.g. out of bounds index or clamping would violate minimum).
*/
adjustBoundary(boundaryIndex: number, deltaPixels: number, minRatio: number = 0.10): boolean {
if (boundaryIndex < 0 || boundaryIndex >= this._tiledItems.length - 1) {
Logger.warn(`adjustBoundary: invalid boundaryIndex ${boundaryIndex}`);
return false;
}
const totalDim = this._totalDimension();
if (totalDim === 0) return false;
const ratioDelta = deltaPixels / totalDim;
const newLeft = this._splitRatios[boundaryIndex] + ratioDelta;
const newRight = this._splitRatios[boundaryIndex + 1] - ratioDelta;
if (newLeft < minRatio || newRight < minRatio) {
Logger.log(`adjustBoundary: clamped — newLeft=${newLeft.toFixed(3)}, newRight=${newRight.toFixed(3)}, min=${minRatio}`);
return false;
}
this._splitRatios[boundaryIndex] = newLeft;
this._splitRatios[boundaryIndex + 1] = newRight;
Logger.info(`adjustBoundary: boundary=${boundaryIndex} ratios=[${this._splitRatios.map(r => r.toFixed(3)).join(', ')}]`);
return true;
}
/**
* Adjust boundaries on BOTH axes simultaneously for corner resize ops.
* horizontalDelta applies to this container if HORIZONTAL, verticalDelta if VERTICAL.
* For nested containers the perpendicular delta is forwarded to the child container.
*
* boundaryIndex: the slot index whose right/bottom edge is being dragged.
*/
adjustBoundaryBothAxes(
boundaryIndex: number,
horizontalDelta: number,
verticalDelta: number,
): void {
if (this._orientation === Orientation.HORIZONTAL) {
this.adjustBoundary(boundaryIndex, horizontalDelta);
} else {
this.adjustBoundary(boundaryIndex, verticalDelta);
}
}
// ─── Container Lookup ────────────────────────────────────────────────────────
/**
* Returns the direct-parent WindowContainer that contains win_id as an
* immediate child (not recursed further). Returns null if not found.
*/
getContainerForWindow(win_id: number): WindowContainer | null {
for (const item of this._tiledItems) {
if (item instanceof WindowWrapper && item.getWindowId() === win_id) {
return this;
}
}
for (const item of this._tiledItems) {
if (item instanceof WindowContainer) {
const found = item.getContainerForWindow(win_id);
if (found !== null) return found;
}
}
return null;
}
getIndexOfItemNested(item: WindowWrapper): number {
for (let i = 0; i < this._tiledItems.length; i++) {
const container = this._tiledItems[i];
@@ -354,30 +194,19 @@ export default class WindowContainer {
Logger.error("Item not found in container during drag op", item.getWindowId());
return;
}
let new_index = original_index;
let new_index = this.getIndexOfItemNested(item);
this.getBounds().forEach((rect, index) => {
if (rect.x < x && rect.x + rect.width > x && rect.y < y && rect.y + rect.height > y) {
new_index = index;
}
});
})
if (original_index !== new_index) {
// Swap only the items — ratios stay with their slots.
// e.g. slot 0 = 40%, slot 1 = 60%: when the window in slot 1 drags
// into slot 0, it takes slot 0's 40% size. The window it displaces
// moves to slot 1 and takes the 60% size. The slot ratios are unchanged.
[this._tiledItems[original_index], this._tiledItems[new_index]] =
[this._tiledItems[new_index], this._tiledItems[original_index]];
Logger.info(`itemDragged: swapped slots ${original_index}<->${new_index}, ratios=[${this._splitRatios.map(r => r.toFixed(3)).join(', ')}]`);
this.tileWindows();
this._tiledItems.splice(original_index, 1);
this._tiledItems.splice(new_index, 0, item);
this.tileWindows()
}
}
/**
* Reset all split ratios in this container to equal fractions.
* Called when the user explicitly requests an equal-split reset (e.g. Ctrl+Z).
*/
resetRatios(): void {
this._resetRatios();
this.tileWindows();
}
}
}
+1 -1
View File
@@ -67,8 +67,8 @@ export default class Monitor {
tileWindows(): void {
this._workArea = global.workspace_manager.get_active_workspace().get_work_area_for_monitor(this._id);
const activeWorkspace = global.workspace_manager.get_active_workspace();
// move() already calls tileWindows() internally — don't call it again
this._workspaces[activeWorkspace.index()].move(this._workArea);
this._workspaces[activeWorkspace.index()].tileWindows()
}
removeWorkspace(workspaceId: number): void {
+17 -26
View File
@@ -99,9 +99,6 @@ export class WindowWrapper {
this._window.connect("position-changed", (_metaWindow) => {
windowManager.handleWindowPositionChanged(this);
}),
this._window.connect("size-changed", (_metaWindow) => {
windowManager.handleWindowPositionChanged(this);
}),
);
}
@@ -120,41 +117,35 @@ export class WindowWrapper {
}
}
safelyResizeWindow(rect: Rect, _retry: number = 3): void {
safelyResizeWindow(rect: Rect, _retry: number = 2): void {
// Keep minimal logging
if (this._dragging) {
Logger.info("STOPPED RESIZE BECAUSE ITEM IS BEING DRAGGED");
Logger.info("STOPPED RESIZE BECAUSE ITEM IS BEING DRAGGED")
return;
}
// Logger.log("SAFELY RESIZE", rect.x, rect.y, rect.width, rect.height);
const actor = this._window.get_compositor_private();
const actor = this._window.get_compositor_private() as Clutter.Actor | null;
if (!actor) {
Logger.log("No actor available, can't resize safely yet");
return;
}
actor.remove_all_transitions();
// Single call: move + resize atomically
let windowActor = this._window.get_compositor_private() as Clutter.Actor;
if (!windowActor) return;
windowActor.remove_all_transitions();
// Logger.info("MOVING")
this._window.move_frame(true, rect.x, rect.y);
// Logger.info("RESIZING MOVING")
this._window.move_resize_frame(true, rect.x, rect.y, rect.width, rect.height);
const new_rect = this._window.get_frame_rect();
const TOLERANCE = 2; // pixels — allow compositor rounding
const mismatch =
Math.abs(new_rect.x - rect.x) > TOLERANCE ||
Math.abs(new_rect.y - rect.y) > TOLERANCE ||
Math.abs(new_rect.width - rect.width) > TOLERANCE ||
Math.abs(new_rect.height - rect.height) > TOLERANCE;
if (_retry > 0 && mismatch) {
Logger.warn("RESIZE MISMATCH, retrying",
`want(${rect.x},${rect.y},${rect.width},${rect.height})`,
`got(${new_rect.x},${new_rect.y},${new_rect.width},${new_rect.height})`);
let new_rect = this._window.get_frame_rect();
if ( _retry > 0 && (new_rect.x != rect.x || rect.y != new_rect.y || rect.width < new_rect.width || rect.height < new_rect.height)) {
Logger.warn("RESIZING FAILED AS SMALLER", new_rect.x, new_rect.y, new_rect.width, new_rect.height, rect.x, rect.y, rect.width, rect.height);
queueEvent({
name: `delayed_resize_${this.getWindowId()}`,
name: "attempting_delayed_resize",
callback: () => {
this.safelyResizeWindow(rect, _retry - 1);
this.safelyResizeWindow(rect, _retry-1);
}
}, 50);
})
}
}
+20 -201
View File
@@ -1,5 +1,5 @@
import Meta from "gi://Meta";
import Gio from "gi://Gio";
// import Gio from "gi://Gio";
// import GLib from "gi://GLib";
import {WindowWrapper} from './window.js';
@@ -8,7 +8,6 @@ import * as Main from "resource:///org/gnome/shell/ui/main.js";
import {Logger} from "../utils/logger.js";
import Monitor from "./monitor.js";
import WindowContainer from "./container.js";
import {Rect} from "../utils/rect.js";
export interface IWindowManager {
@@ -50,23 +49,9 @@ export default class WindowManager implements IWindowManager {
_showingOverview: boolean = false;
// ── Resize-drag tracking ──────────────────────────────────────────────────
_isResizeDrag: boolean = false;
_resizeDragWindowId: number = _UNUSED_WINDOW_ID;
_resizeDragOp: Meta.GrabOp = Meta.GrabOp.NONE;
/** Mouse position at the start of each incremental resize step. */
_resizeDragLastMouseX: number = 0;
_resizeDragLastMouseY: number = 0;
/** Re-entrancy guard: true while tileWindows is propagating position-changed events. */
_isTiling: boolean = false;
constructor() {
_settings: Gio.Settings | null = null;
constructor() {}
/** Returns the live min-ratio value from settings, falling back to 0.10. */
private _getMinRatio(): number {
return this._settings?.get_double('min-window-size-percent') ?? 0.10;
}
public enable(): void {
@@ -223,65 +208,25 @@ export default class WindowManager implements IWindowManager {
}
/**
* Returns true if the grab op is a resize operation (any edge or corner).
*/
_isResizeOp(op: Meta.GrabOp): boolean {
return op === Meta.GrabOp.RESIZING_E ||
op === Meta.GrabOp.RESIZING_W ||
op === Meta.GrabOp.RESIZING_N ||
op === Meta.GrabOp.RESIZING_S ||
op === Meta.GrabOp.RESIZING_NE ||
op === Meta.GrabOp.RESIZING_NW ||
op === Meta.GrabOp.RESIZING_SE ||
op === Meta.GrabOp.RESIZING_SW;
}
handleGrabOpBegin(display: Meta.Display, window: Meta.Window, op: Meta.GrabOp): void {
Logger.log("Grab Op Start", op);
if (op === Meta.GrabOp.MOVING_UNCONSTRAINED){
if (this._isResizeOp(op)) {
// ── Resize drag ──────────────────────────────────────────────────
Logger.log("Resize drag begin, op=", op);
this._isResizeDrag = true;
this._resizeDragWindowId = window.get_id();
this._resizeDragOp = op;
const [startMouseX, startMouseY] = global.get_pointer();
this._resizeDragLastMouseX = startMouseX;
this._resizeDragLastMouseY = startMouseY;
// Mark the window as dragging so safelyResizeWindow skips it while
// we tile the other windows in response to ratio changes.
this._getWrappedWindow(window)?.startDragging();
} else {
// ── Move drag (existing behaviour) ───────────────────────────────
this._getWrappedWindow(window)?.startDragging();
this._grabbedWindowMonitor = window.get_monitor();
this._grabbedWindowId = window.get_id();
}
Logger.log("Grab Op Start", op);
Logger.log(display, window, op)
Logger.log(window.get_monitor())
this._getWrappedWindow(window)?.startDragging();
this._grabbedWindowMonitor = window.get_monitor();
this._grabbedWindowId = window.get_id();
}
handleGrabOpEnd(display: Meta.Display, window: Meta.Window, op: Meta.GrabOp): void {
Logger.log("Grab Op End ", op);
if (this._isResizeDrag) {
// ── Resize drag end ──────────────────────────────────────────────
Logger.log("Resize drag end, op=", op);
this._isResizeDrag = false;
this._resizeDragWindowId = _UNUSED_WINDOW_ID;
this._resizeDragLastMouseX = 0;
this._resizeDragLastMouseY = 0;
this._resizeDragOp = Meta.GrabOp.NONE;
// Stop suppressing the window, then snap everything to computed ratios
this._getWrappedWindow(window)?.stopDragging();
this._tileMonitors();
} else {
// ── Move drag end (existing behaviour) ───────────────────────────
Logger.log("primary display", display.get_primary_monitor())
this._grabbedWindowId = _UNUSED_WINDOW_ID;
this._getWrappedWindow(window)?.stopDragging();
this._tileMonitors();
Logger.info("monitor_start and monitor_end", this._grabbedWindowMonitor, window.get_monitor());
}
Logger.log("primary display", display.get_primary_monitor())
this._grabbedWindowId = _UNUSED_WINDOW_ID;
this._getWrappedWindow(window)?.stopDragging();
this._tileMonitors();
Logger.info("monitor_start and monitor_end", this._grabbedWindowMonitor, window.get_monitor());
}
_getWrappedWindow(window: Meta.Window): WindowWrapper | undefined {
@@ -320,21 +265,9 @@ export default class WindowManager implements IWindowManager {
}
public handleWindowPositionChanged(winWrap: WindowWrapper): void {
// Ignore position changes that we triggered ourselves via tileWindows
if (this._isTiling) {
return;
}
if (this._changingGrabbedMonitor) {
return;
}
// ── Live resize-drag handling ─────────────────────────────────────────
if (this._isResizeDrag && winWrap.getWindowId() === this._resizeDragWindowId) {
this._handleResizeDragUpdate(winWrap);
return;
}
// ── Move-drag handling (existing behaviour) ───────────────────────────
if (winWrap.getWindowId() === this._grabbedWindowId) {
const [mouseX, mouseY, _] = global.get_pointer();
@@ -348,109 +281,18 @@ export default class WindowManager implements IWindowManager {
}
}
if (monitorIndex === -1) {
return;
return
}
if (monitorIndex !== this._grabbedWindowMonitor) {
this._changingGrabbedMonitor = true;
this._moveWindowToMonitor(winWrap.getWindow(), monitorIndex);
this._changingGrabbedMonitor = false;
}
// Guard _isTiling so that tileWindows() calls triggered by itemDragged
// (which repositions the displaced window) don't re-enter this handler.
this._isTiling = true;
try {
this._monitors.get(monitorIndex)?.itemDragged(winWrap, mouseX, mouseY);
} finally {
this._isTiling = false;
this._changingGrabbedMonitor = false
}
this._monitors.get(monitorIndex)?.itemDragged(winWrap, mouseX, mouseY);
}
}
/**
* Called on every position-changed event while a resize drag is in progress.
* Computes the pixel delta from the drag-start rect, maps it to the correct
* container boundary, and calls adjustBoundary() for live feedback.
*/
private _handleResizeDragUpdate(winWrap: WindowWrapper): void {
const op = this._resizeDragOp;
const winId = winWrap.getWindowId();
// Read the current mouse position — this is unclamped by the compositor
// and always reflects the true user intent, unlike the window's frame rect
// which gets clamped when adjacent windows block expansion.
const [mouseX, mouseY] = global.get_pointer();
const dx = mouseX - this._resizeDragLastMouseX;
const dy = mouseY - this._resizeDragLastMouseY;
if (dx === 0 && dy === 0) return;
// Update last position first so even if we return early the baseline advances
this._resizeDragLastMouseX = mouseX;
this._resizeDragLastMouseY = mouseY;
// Find the container that directly holds this window
const container = this._findContainerForWindowAcrossMonitors(winId);
if (!container) {
Logger.warn("_handleResizeDragUpdate: no container found for window", winId);
return;
}
const itemIndex = container._getIndexOfWindow(winId);
if (itemIndex === -1) return;
const isHorizontal = container._orientation === 0; // Orientation.HORIZONTAL
// Map the mouse delta to the correct boundary.
//
// East/South edge → boundary AFTER the item (boundaryIndex = itemIndex)
// positive dx/dy grows this item, shrinks the next one.
// West/North edge → boundary BEFORE the item (boundaryIndex = itemIndex - 1)
// positive dx/dy moves the left edge right, growing the left neighbour
// and shrinking this item — so we negate the delta.
const minRatio = this._getMinRatio();
let adjusted = false;
if (isHorizontal) {
if (op === Meta.GrabOp.RESIZING_E || op === Meta.GrabOp.RESIZING_NE || op === Meta.GrabOp.RESIZING_SE) {
adjusted = container.adjustBoundary(itemIndex, dx, minRatio);
} else if (op === Meta.GrabOp.RESIZING_W || op === Meta.GrabOp.RESIZING_NW || op === Meta.GrabOp.RESIZING_SW) {
adjusted = container.adjustBoundary(itemIndex - 1, dx, minRatio);
}
} else {
if (op === Meta.GrabOp.RESIZING_S || op === Meta.GrabOp.RESIZING_SE || op === Meta.GrabOp.RESIZING_SW) {
adjusted = container.adjustBoundary(itemIndex, dy, minRatio);
} else if (op === Meta.GrabOp.RESIZING_N || op === Meta.GrabOp.RESIZING_NE || op === Meta.GrabOp.RESIZING_NW) {
adjusted = container.adjustBoundary(itemIndex - 1, dy, minRatio);
}
}
// Tile all windows with the updated ratios, guarded so the resulting
// position-changed events don't re-enter this handler.
if (adjusted) {
this._isTiling = true;
try {
container.tileWindows();
} finally {
this._isTiling = false;
}
}
}
/**
* Searches all monitors for the WindowContainer that directly holds win_id.
*/
private _findContainerForWindowAcrossMonitors(winId: number): WindowContainer | null {
const activeWorkspaceIndex = global.workspace_manager.get_active_workspace().index();
for (const monitor of this._monitors.values()) {
if (activeWorkspaceIndex >= monitor._workspaces.length) continue;
const workspace = monitor._workspaces[activeWorkspaceIndex];
const container = workspace.getContainerForWindow(winId);
if (container !== null) return container;
}
return null;
}
public handleWindowMinimized(winWrap: WindowWrapper): void {
const monitor_id = winWrap.getWindow().get_monitor()
@@ -530,13 +372,9 @@ export default class WindowManager implements IWindowManager {
}
_tileMonitors(): void {
this._isTiling = true;
try {
for (const monitor of this._monitors.values()) {
monitor.tileWindows();
}
} finally {
this._isTiling = false;
for (const monitor of this._monitors.values()) {
monitor.tileWindows()
}
}
@@ -617,25 +455,6 @@ export default class WindowManager implements IWindowManager {
}
}
/**
* Resets all split ratios in the active window's container to equal fractions.
* Bound to Ctrl+Z by default.
*/
public resetActiveContainerRatios(): void {
if (this._activeWindowId === null) {
Logger.warn("No active window, cannot reset container ratios");
return;
}
const activeContainer = this._findActiveContainer();
if (activeContainer) {
Logger.info("Resetting container ratios to equal splits");
activeContainer.resetRatios();
} else {
Logger.warn("Could not find container for active window");
}
}
/**
* Finds the container that directly contains the active window
* @returns The container holding the active window, or null if not found