17 Commits

Author SHA1 Message Date
Lucas Oskorep
15188b9990 Merge branch 'main' into renovate/major-eslint-monorepo
All checks were successful
Build and Test / build (pull_request) Successful in 24s
Build and Test / release (pull_request) Has been skipped
2026-02-25 17:02:44 -05:00
Lucas Oskorep
19696298d0 Merge pull request 'feat/percentage-based-container-sizing' (#18) from feat/percentage-based-container-sizing into main
All checks were successful
Build and Test / build (push) Successful in 23s
Build and Test / release (push) Successful in 11s
Reviewed-on: #18
2026-02-25 11:36:39 -05:00
Lucas Oskorep
4be7602316 feat: adding support for changing the size of windows during window resize events to aerospike. Also cleaning up imports and simplifying some of the settings logic.
All checks were successful
Build and Test / build (pull_request) Successful in 23s
Build and Test / release (pull_request) Has been skipped
2026-02-25 11:35:59 -05:00
Renovate Bot
8f6e8582c9 chore(deps): update dependency eslint to v10
All checks were successful
Build and Test / build (pull_request) Successful in 24s
Build and Test / release (pull_request) Has been skipped
2026-02-06 23:13:10 +00:00
Lucas Oskorep
3d2da0a4bc Update .gitea/workflows/build.yaml
All checks were successful
Build and Test / build (push) Successful in 24s
Build and Test / release (push) Successful in 11s
2026-01-29 02:47:15 -05:00
Lucas Oskorep
a17441cda3 Merge pull request 'chore(deps): update all-dependencies' (#12) from renovate/all into main
Some checks failed
Build and Test / build (push) Successful in 24s
Build and Test / release (push) Has been cancelled
Reviewed-on: #12
2026-01-29 02:46:04 -05:00
Lucas Oskorep
c4f5835424 fix: renovate updating past v3 for upload when v4+ is unsupported in gitea
Some checks failed
Build and Test / build (pull_request) Successful in 33s
Build and Test / release (pull_request) Has been cancelled
2026-01-29 02:45:04 -05:00
Lucas Oskorep
7773c3b808 feat: update code to work with new libs
Some checks failed
Build and Test / build (pull_request) Failing after 24s
Build and Test / release (pull_request) Has been cancelled
2026-01-29 02:41:59 -05:00
Renovate Bot
b8e861cf9e chore(deps): update all-dependencies
Some checks failed
Build and Test / build (pull_request) Failing after 24s
Build and Test / release (pull_request) Has been cancelled
2026-01-29 02:38:21 -05:00
Lucas Oskorep
318ce6c064 Update .gitea/workflows/build.yaml
Some checks failed
Build and Test / build (push) Failing after 39s
Build and Test / release (push) Has been cancelled
2026-01-29 02:36:39 -05:00
Lucas Oskorep
1a4d8253f7 Merge pull request 'chore(deps): update all-dependencies' (#10) from renovate/all into main
Some checks failed
Build and Test / build (push) Has been cancelled
Build and Test / release (push) Has been cancelled
Reviewed-on: #10
2026-01-29 01:57:18 -05:00
Renovate Bot
86786ba831 chore(deps): update all-dependencies
Some checks failed
renovate/artifacts Artifact file update failure
Build and Test / build (pull_request) Has been cancelled
Build and Test / release (pull_request) Has been cancelled
2026-01-29 06:55:32 +00:00
Lucas Oskorep
bfd027f6c3 Merge pull request 'chore: Configure Renovate' (#8) from renovate/configure into main
Some checks failed
Build and Test / build (push) Has been cancelled
Build and Test / release (push) Has been cancelled
Reviewed-on: #8
Reviewed-by: Lucas Oskorep <lucas.oskorep@gmail.com>
2026-01-29 01:46:16 -05:00
Renovate Bot
b46810bc0f Add renovate.json
Some checks failed
Build and Test / build (pull_request) Has been cancelled
Build and Test / release (pull_request) Has been cancelled
2026-01-29 06:45:35 +00:00
Lucas Oskorep
c4abd1e8e5 revert df9bc3e1eb
Some checks failed
Build and Test / build (push) Has been cancelled
Build and Test / release (push) Has been cancelled
revert Merge pull request 'chore: Configure Renovate' (#1) from renovate/configure into main

Reviewed-on: #1
2026-01-29 01:36:37 -05:00
Lucas Oskorep
df9bc3e1eb Merge pull request 'chore: Configure Renovate' (#1) from renovate/configure into main
Some checks failed
Build and Test / build (push) Has been cancelled
Build and Test / release (push) Has been cancelled
Reviewed-on: #1
2026-01-29 01:05:11 -05:00
Renovate Bot
90a4e19751 Add renovate.json
Some checks failed
Build and Test / build (pull_request) Has been cancelled
Build and Test / release (pull_request) Has been cancelled
2026-01-29 05:44:36 +00:00
14 changed files with 1706 additions and 1391 deletions

View File

@@ -10,13 +10,13 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: archlinux
container: container:
image: node:20 image: node:25
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Install pnpm and TypeScript - name: Install pnpm and TypeScript
run: npm install -g pnpm typescript@5.8.3 run: npm install -g pnpm typescript@5.8.3
@@ -48,16 +48,16 @@ jobs:
release: release:
needs: build needs: build
runs-on: ubuntu-latest runs-on: archlinux
if: github.ref == 'refs/heads/main' && github.event_name == 'push' if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: environment:
name: production name: production
container: container:
image: node:20 image: node:25
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Download artifact - name: Download artifact
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3

2
.nvmrc
View File

@@ -1 +1 @@
v20 24

View File

@@ -1,5 +1,5 @@
import Meta from 'gi://Meta'; import Meta from 'gi://Meta';
import {Extension, ExtensionMetadata} from 'resource:///org/gnome/shell/extensions/extension.js'; import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import Gio from 'gi://Gio'; import Gio from 'gi://Gio';
import Shell from 'gi://Shell'; import Shell from 'gi://Shell';
@@ -11,11 +11,11 @@ export default class aerospike extends Extension {
keyBindings: Map<string, number>; keyBindings: Map<string, number>;
windowManager: WindowManager; windowManager: WindowManager;
constructor(metadata: ExtensionMetadata) { constructor(metadata: ConstructorParameters<typeof Extension>[0]) {
super(metadata); super(metadata);
this.settings = this.getSettings('org.gnome.shell.extensions.aerospike'); this.settings = this.getSettings('org.gnome.shell.extensions.aerospike');
this.keyBindings = new Map(); this.keyBindings = new Map();
this.windowManager = new WindowManager(); this.windowManager = new WindowManager(this.settings);
} }
enable() { enable() {
@@ -30,37 +30,25 @@ export default class aerospike extends Extension {
this.removeKeybindings() this.removeKeybindings()
} }
private keybindingActions(): Record<string, () => void> {
return {
'move-left': () => { Logger.info('Keybinding 1 was pressed!'); },
'move-right': () => { Logger.info('Keybinding 2 was pressed!'); },
'join-with-left': () => { Logger.info('Keybinding 3 was pressed!'); },
'join-with-right': () => { Logger.info('Keybinding 4 was pressed!'); },
'print-tree': () => { this.windowManager.printTreeStructure(); },
'toggle-orientation': () => { this.windowManager.toggleActiveContainerOrientation(); },
'reset-ratios': () => { this.windowManager.resetActiveContainerRatios(); },
};
}
private bindSettings() { private bindSettings() {
// Monitor settings changes const keybindings = Object.keys(this.keybindingActions());
this.settings.connect('changed::move-left', () => { keybindings.forEach(name => {
log(`Keybinding 1 changed to: ${this.settings.get_strv('move-left')}`); this.settings.connect(`changed::${name}`, () => {
this.refreshKeybinding('move-left'); log(`${name} keybinding changed to: ${this.settings.get_strv(name)}`);
}); this.refreshKeybinding(name);
});
this.settings.connect('changed::move-right', () => {
log(`Keybinding 2 changed to: ${this.settings.get_strv('move-right')}`);
this.refreshKeybinding('move-right');
});
this.settings.connect('changed::join-with-left', () => {
log(`Keybinding 3 changed to: ${this.settings.get_strv('join-with-left')}`);
this.refreshKeybinding('join-with-left');
});
this.settings.connect('changed::join-with-right', () => {
log(`Keybinding 4 changed to: ${this.settings.get_strv('join-with-right')}`);
this.refreshKeybinding('join-with-right');
});
this.settings.connect('changed::print-tree', () => {
log(`Print tree keybinding changed to: ${this.settings.get_strv('print-tree')}`);
this.refreshKeybinding('print-tree');
});
this.settings.connect('changed::toggle-orientation', () => {
log(`Toggle orientation keybinding changed to: ${this.settings.get_strv('toggle-orientation')}`);
this.refreshKeybinding('toggle-orientation');
}); });
this.settings.connect('changed::dropdown-option', () => { this.settings.connect('changed::dropdown-option', () => {
@@ -71,44 +59,15 @@ export default class aerospike extends Extension {
log(`Color selection changed to: ${this.settings.get_string('color-selection')}`); log(`Color selection changed to: ${this.settings.get_string('color-selection')}`);
}); });
} }
private refreshKeybinding(settingName: string) { private refreshKeybinding(settingName: string) {
if (this.keyBindings.has(settingName)) { if (this.keyBindings.has(settingName)) {
Main.wm.removeKeybinding(settingName); Main.wm.removeKeybinding(settingName);
this.keyBindings.delete(settingName); this.keyBindings.delete(settingName);
} }
switch (settingName) { const action = this.keybindingActions()[settingName];
case 'move-left': if (action) this.bindKeybinding(settingName, action);
this.bindKeybinding('move-left', () => {
Logger.info('Keybinding 1 was pressed!');
});
break;
case 'move-right':
this.bindKeybinding('move-right', () => {
Logger.info('Keybinding 2 was pressed!');
});
break;
case 'join-with-left':
this.bindKeybinding('join-with-left', () => {
Logger.info('Keybinding 3 was pressed!');
});
break;
case 'join-with-right':
this.bindKeybinding('join-with-right', () => {
Logger.info('Keybinding 4 was pressed!');
});
break;
case 'print-tree':
this.bindKeybinding('print-tree', () => {
this.windowManager.printTreeStructure();
});
break;
case 'toggle-orientation':
this.bindKeybinding('toggle-orientation', () => {
this.windowManager.toggleActiveContainerOrientation();
});
break;
}
} }
private removeKeybindings() { private removeKeybindings() {
@@ -119,29 +78,10 @@ export default class aerospike extends Extension {
} }
private setupKeybindings() { private setupKeybindings() {
this.bindKeybinding('move-left', () => { const actions = this.keybindingActions();
Logger.info('Keybinding 1 was pressed!'); for (const [name, action] of Object.entries(actions)) {
}); this.bindKeybinding(name, action);
}
this.bindKeybinding('move-right', () => {
Logger.info('Keybinding 2 was pressed!');
});
this.bindKeybinding('join-with-left', () => {
Logger.info('Keybinding 3 was pressed!');
});
this.bindKeybinding('join-with-right', () => {
Logger.info('Keybinding 4 was pressed!');
});
this.bindKeybinding('print-tree', () => {
this.windowManager.printTreeStructure();
});
this.bindKeybinding('toggle-orientation', () => {
this.windowManager.toggleActiveContainerOrientation();
});
} }
private bindKeybinding(settingName: string, callback: () => void) { private bindKeybinding(settingName: string, callback: () => void) {
@@ -161,7 +101,4 @@ export default class aerospike extends Extension {
this.keyBindings.set(settingName, keyBindingAction); this.keyBindings.set(settingName, keyBindingAction);
} }
}
}

View File

@@ -8,7 +8,7 @@ packages:
build: packages && build-schemas build: packages && build-schemas
rm -rf dist/* rm -rf dist/*
tsc pnpm exec tsc
cp metadata.json dist/ cp metadata.json dist/
cp stylesheet.css dist/ cp stylesheet.css dist/
mkdir -p dist/schemas mkdir -p dist/schemas

View File

@@ -22,12 +22,12 @@
}, },
"devDependencies": { "devDependencies": {
"@girs/gjs": "4.0.0-beta.38", "@girs/gjs": "4.0.0-beta.38",
"@girs/gnome-shell": "49.0.1", "@girs/gnome-shell": "49.1.0",
"@jest/globals": "^29.7.0", "@jest/globals": "^30.0.0",
"@types/jest": "^29.5.12", "@types/jest": "^30.0.0",
"eslint": "^9.36.0", "eslint": "^10.0.0",
"eslint-plugin-jsdoc": "^50.8.0", "eslint-plugin-jsdoc": "^62.0.0",
"jest": "^29.7.0", "jest": "^30.0.0",
"ts-jest": "^29.1.2", "ts-jest": "^29.1.2",
"typescript": "^5.9.2" "typescript": "^5.9.2"
}, },

2256
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

12
renovate.json Normal file
View File

@@ -0,0 +1,12 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
],
"packageRules": [
{
"matchPackageNames": ["actions/upload-artifact", "actions/download-artifact"],
"allowedVersions": "<4"
}
]
}

View File

@@ -49,5 +49,11 @@
<description>Toggles the orientation of the container holding the active window between horizontal and vertical</description> <description>Toggles the orientation of the container holding the active window between horizontal and vertical</description>
</key> </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>
</schema> </schema>
</schemalist> </schemalist>

View File

@@ -173,6 +173,14 @@ export default class AerospikeExtensions extends ExtensionPreferences {
}) })
); );
keybindingsGroup.add(
new EntryRow({
title: _('Reset Container Ratios to Equal'),
settings: settings,
bind: 'reset-ratios',
map: keybindingMap
})
);
} }

View File

@@ -1,20 +1,21 @@
import GLib from "gi://GLib"; import GLib from "gi://GLib";
export type QueuedEvent = { export type QueuedEvent = {
name: string; name: string;
callback: () => void; callback: () => void;
} }
const queuedEvents: QueuedEvent[] = []; const pendingEvents: Map<string, QueuedEvent> = new Map();
export default function queueEvent(event: QueuedEvent, interval = 200) { export default function queueEvent(event: QueuedEvent, interval = 200) {
queuedEvents.push(event); pendingEvents.set(event.name, event);
GLib.timeout_add(GLib.PRIORITY_DEFAULT, interval, () => { GLib.timeout_add(GLib.PRIORITY_DEFAULT, interval, () => {
const e = queuedEvents.pop() const e = pendingEvents.get(event.name);
if (e) { if (e && e === event) {
pendingEvents.delete(event.name);
e.callback(); e.callback();
} }
return queuedEvents.length !== 0; return GLib.SOURCE_REMOVE;
}); });
} }

View File

@@ -1,6 +1,5 @@
import {WindowWrapper} from "./window.js"; import {WindowWrapper} from "./window.js";
import {Logger} from "../utils/logger.js"; import {Logger} from "../utils/logger.js";
import Meta from "gi://Meta";
import queueEvent from "../utils/events.js"; import queueEvent from "../utils/events.js";
import {Rect} from "../utils/rect.js"; import {Rect} from "../utils/rect.js";
@@ -9,6 +8,15 @@ enum Orientation {
VERTICAL = 1, VERTICAL = 1,
} }
// Returns equal ratios summing exactly to 1.0, with float drift absorbed by the last slot.
function equalRatios(n: number): number[] {
if (n <= 0) return [];
const base = 1 / n;
const ratios = Array(n).fill(base);
const sumExceptLast = ratios.slice(0, -1).reduce((a, b) => a + b, 0);
ratios[n - 1] = 1 - sumExceptLast;
return ratios;
}
export default class WindowContainer { export default class WindowContainer {
@@ -16,13 +24,42 @@ export default class WindowContainer {
_tiledWindowLookup: Map<number, WindowWrapper>; _tiledWindowLookup: Map<number, WindowWrapper>;
_orientation: Orientation = Orientation.HORIZONTAL; _orientation: Orientation = Orientation.HORIZONTAL;
_workArea: Rect; _workArea: Rect;
_splitRatios: number[];
constructor(workspaceArea: Rect,) { constructor(workspaceArea: Rect) {
this._tiledItems = []; this._tiledItems = [];
this._tiledWindowLookup = new Map<number, WindowWrapper>(); this._tiledWindowLookup = new Map<number, WindowWrapper>();
this._workArea = workspaceArea; this._workArea = workspaceArea;
this._splitRatios = [];
} }
// ─── Helpers ────────────────────────────────────────────────────────────────
private _resetRatios(): void {
this._splitRatios = equalRatios(this._tiledItems.length);
}
private _addRatioForNewWindow(): void {
const n = this._tiledItems.length;
if (n <= 1) {
this._splitRatios = [1.0];
return;
}
const newRatio = 1 / n;
const scale = 1 - newRatio;
const scaled = this._splitRatios.map(r => r * scale);
const partialSum = scaled.reduce((a, b) => a + b, 0) + newRatio;
scaled[scaled.length - 1] += (1.0 - partialSum);
this._splitRatios = [...scaled, newRatio];
}
private _totalDimension(): number {
return this._orientation === Orientation.HORIZONTAL
? this._workArea.width
: this._workArea.height;
}
// ─── Public API ─────────────────────────────────────────────────────────────
move(rect: Rect): void { move(rect: Rect): void {
this._workArea = rect; this._workArea = rect;
@@ -40,13 +77,11 @@ export default class WindowContainer {
addWindow(winWrap: WindowWrapper): void { addWindow(winWrap: WindowWrapper): void {
this._tiledItems.push(winWrap); this._tiledItems.push(winWrap);
this._tiledWindowLookup.set(winWrap.getWindowId(), winWrap); this._tiledWindowLookup.set(winWrap.getWindowId(), winWrap);
this._addRatioForNewWindow();
queueEvent({ queueEvent({
name: "tiling-windows", name: "tiling-windows",
callback: () => { callback: () => this.tileWindows(),
this.tileWindows(); }, 100);
}
}, 100)
} }
getWindow(win_id: number): WindowWrapper | undefined { getWindow(win_id: number): WindowWrapper | undefined {
@@ -56,24 +91,22 @@ export default class WindowContainer {
for (const item of this._tiledItems) { for (const item of this._tiledItems) {
if (item instanceof WindowContainer) { if (item instanceof WindowContainer) {
const win = item.getWindow(win_id); const win = item.getWindow(win_id);
if (win) { if (win) return win;
return win;
}
} else if (item.getWindowId() === win_id) { } else if (item.getWindowId() === win_id) {
return item; return item;
} }
} }
return undefined return undefined;
} }
_getIndexOfWindow(win_id: number) { _getIndexOfWindow(win_id: number): number {
for (let i = 0; i < this._tiledItems.length; i++) { for (let i = 0; i < this._tiledItems.length; i++) {
const item = this._tiledItems[i]; const item = this._tiledItems[i];
if (item instanceof WindowWrapper && item.getWindowId() === win_id) { if (item instanceof WindowWrapper && item.getWindowId() === win_id) {
return i; return i;
} }
} }
return -1 return -1;
} }
removeWindow(win_id: number): void { removeWindow(win_id: number): void {
@@ -84,6 +117,7 @@ export default class WindowContainer {
if (index !== -1) { if (index !== -1) {
this._tiledItems.splice(index, 1); this._tiledItems.splice(index, 1);
} }
this._resetRatios();
} else { } else {
for (const item of this._tiledItems) { for (const item of this._tiledItems) {
if (item instanceof WindowContainer) { if (item instanceof WindowContainer) {
@@ -91,94 +125,120 @@ export default class WindowContainer {
} }
} }
} }
this.tileWindows() this.tileWindows();
} }
disconnectSignals(): void { disconnectSignals(): void {
this._tiledItems.forEach((item) => { this._tiledItems.forEach((item) => {
if (item instanceof WindowContainer) { if (item instanceof WindowContainer) {
item.disconnectSignals() item.disconnectSignals();
} else { } else {
item.disconnectWindowSignals(); item.disconnectWindowSignals();
}
} }
) });
} }
removeAllWindows(): void { removeAllWindows(): void {
this._tiledItems = [] this._tiledItems = [];
this._tiledWindowLookup.clear() this._tiledWindowLookup.clear();
this._splitRatios = [];
} }
tileWindows() { tileWindows(): void {
Logger.log("TILING WINDOWS IN CONTAINER") Logger.log("TILING WINDOWS IN CONTAINER");
Logger.log("WorkArea", this._workArea); Logger.log("WorkArea", this._workArea);
this._tileItems();
this._tileItems()
return true
} }
_tileItems() { _tileItems() {
if (this._tiledItems.length === 0) { if (this._tiledItems.length === 0) return;
return;
}
const bounds = this.getBounds(); 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) => { this._tiledItems.forEach((item, index) => {
const rect = bounds[index]; const rect = bounds[index];
if (item instanceof WindowContainer) { if (item instanceof WindowContainer) {
item.move(rect); item.move(rect);
} else { } else {
Logger.info(`_tileItems: window[${index}] id=${item.getWindowId()} dragging=${item._dragging} → rect=(${rect.x},${rect.y},${rect.width},${rect.height})`);
item.safelyResizeWindow(rect); item.safelyResizeWindow(rect);
} }
}) });
} }
// ─── Bounds Calculation ──────────────────────────────────────────────────────
getBounds(): Rect[] { getBounds(): Rect[] {
if (this._orientation === Orientation.HORIZONTAL) { return this._orientation === Orientation.HORIZONTAL
return this.getHorizontalBounds(); ? this._computeBounds('horizontal')
} : this._computeBounds('vertical');
return this.getVerticalBounds();
} }
getVerticalBounds(): Rect[] { private _computeBounds(axis: 'horizontal' | 'vertical'): Rect[] {
const items = this._tiledItems const isHorizontal = axis === 'horizontal';
const containerHeight = Math.floor(this._workArea.height / items.length); const total = isHorizontal ? this._workArea.width : this._workArea.height;
return items.map((_, index) => { let used = 0;
const y = this._workArea.y + (index * containerHeight);
return {
x: this._workArea.x,
y: y,
width: this._workArea.width,
height: containerHeight
} as Rect;
});
}
getHorizontalBounds(): Rect[] {
const windowWidth = Math.floor(this._workArea.width / this._tiledItems.length);
return this._tiledItems.map((_, index) => { return this._tiledItems.map((_, index) => {
const x = this._workArea.x + (index * windowWidth); const offset = used;
return { const size = index === this._tiledItems.length - 1
x: x, ? total - used
y: this._workArea.y, : Math.floor(this._splitRatios[index] * total);
width: windowWidth, used += size;
height: this._workArea.height
} as Rect; return isHorizontal
? { x: this._workArea.x + offset, y: this._workArea.y, width: size, height: this._workArea.height }
: { x: this._workArea.x, y: this._workArea.y + offset, width: this._workArea.width, height: size };
}); });
} }
// ─── Boundary Adjustment ─────────────────────────────────────────────────────
adjustBoundary(boundaryIndex: number, deltaPixels: number): 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 <= 0 || newRight <= 0) {
Logger.log(`adjustBoundary: clamped — newLeft=${newLeft.toFixed(3)}, newRight=${newRight.toFixed(3)}`);
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;
}
// ─── Container Lookup ────────────────────────────────────────────────────────
getContainerForWindow(win_id: number): WindowContainer | null {
for (const item of this._tiledItems) {
if (item instanceof WindowWrapper && item.getWindowId() === win_id) {
return this;
}
if (item instanceof WindowContainer) {
const found = item.getContainerForWindow(win_id);
if (found !== null) return found;
}
}
return null;
}
getIndexOfItemNested(item: WindowWrapper): number { getIndexOfItemNested(item: WindowWrapper): number {
for (let i = 0; i < this._tiledItems.length; i++) { for (let i = 0; i < this._tiledItems.length; i++) {
const container = this._tiledItems[i]; const container = this._tiledItems[i];
if (container instanceof WindowContainer) { if (container instanceof WindowContainer) {
const index = container.getIndexOfItemNested(item); if (container.getIndexOfItemNested(item) !== -1) return i;
if (index !== -1) {
return i;
}
} else if (container.getWindowId() === item.getWindowId()) { } else if (container.getWindowId() === item.getWindowId()) {
return i; return i;
} }
@@ -188,25 +248,30 @@ export default class WindowContainer {
// TODO: update this to work with nested containers - all other logic should already be working // TODO: update this to work with nested containers - all other logic should already be working
itemDragged(item: WindowWrapper, x: number, y: number): void { itemDragged(item: WindowWrapper, x: number, y: number): void {
let original_index = this.getIndexOfItemNested(item); const original_index = this.getIndexOfItemNested(item);
if (original_index === -1) { if (original_index === -1) {
Logger.error("Item not found in container during drag op", item.getWindowId()); Logger.error("Item not found in container during drag op", item.getWindowId());
return; return;
} }
let new_index = this.getIndexOfItemNested(item);
let new_index = original_index;
this.getBounds().forEach((rect, index) => { this.getBounds().forEach((rect, index) => {
if (rect.x < x && rect.x + rect.width > x && rect.y < y && rect.y + rect.height > y) { if (rect.x < x && rect.x + rect.width > x && rect.y < y && rect.y + rect.height > y) {
new_index = index; new_index = index;
} }
}) });
if (original_index !== new_index) {
this._tiledItems.splice(original_index, 1);
this._tiledItems.splice(new_index, 0, item);
this.tileWindows()
}
if (original_index !== new_index) {
Logger.info(`itemDragged: swapped slots ${original_index}<->${new_index}, ratios=[${this._splitRatios.map(r => r.toFixed(3)).join(', ')}]`);
[this._tiledItems[original_index], this._tiledItems[new_index]] =
[this._tiledItems[new_index], this._tiledItems[original_index]];
this.tileWindows();
}
} }
resetRatios(): void {
} this._resetRatios();
this.tileWindows();
}
}

View File

@@ -1,12 +1,9 @@
import {WindowWrapper} from "./window.js"; import {WindowWrapper} from "./window.js";
import {Rect} from "../utils/rect.js"; import {Rect} from "../utils/rect.js";
import queueEvent from "../utils/events.js";
import {Logger} from "../utils/logger.js"; import {Logger} from "../utils/logger.js";
import Meta from "gi://Meta"; import Meta from "gi://Meta";
import Mtk from "@girs/mtk-17";
import WindowContainer from "./container.js"; import WindowContainer from "./container.js";
import Window = Meta.Window;
export default class Monitor { export default class Monitor {
@@ -20,7 +17,7 @@ export default class Monitor {
this._workArea = workspace.get_work_area_for_monitor(this._id); this._workArea = workspace.get_work_area_for_monitor(this._id);
Logger.log("CREATING MONITOR", monitorId); Logger.log("CREATING MONITOR", monitorId);
Logger.log("WorkArea", this._workArea.x, this._workArea.y, this._workArea.width, this._workArea.height); Logger.log("WorkArea", this._workArea.x, this._workArea.y, this._workArea.width, this._workArea.height);
const workspaceCount = global.workspace_manager.get_n_workspaces() const workspaceCount = global.workspace_manager.get_n_workspaces();
Logger.log("Workspace Count", workspaceCount); Logger.log("Workspace Count", workspaceCount);
for (let i = 0; i < workspaceCount; i++) { for (let i = 0; i < workspaceCount; i++) {
this._workspaces.push(new WindowContainer(this._workArea)); this._workspaces.push(new WindowContainer(this._workArea));
@@ -42,9 +39,7 @@ export default class Monitor {
getWindow(windowId: number): WindowWrapper | undefined { getWindow(windowId: number): WindowWrapper | undefined {
for (const container of this._workspaces) { for (const container of this._workspaces) {
const win = container.getWindow(windowId); const win = container.getWindow(windowId);
if (win) { if (win) return win;
return win;
}
} }
return undefined; return undefined;
} }
@@ -52,8 +47,7 @@ export default class Monitor {
removeWindow(winWrap: WindowWrapper) { removeWindow(winWrap: WindowWrapper) {
const windowId = winWrap.getWindowId(); const windowId = winWrap.getWindowId();
for (const container of this._workspaces) { for (const container of this._workspaces) {
const win = container.getWindow(windowId); if (container.getWindow(windowId)) {
if (win) {
container.removeWindow(windowId); container.removeWindow(windowId);
} }
} }
@@ -65,10 +59,10 @@ export default class Monitor {
} }
tileWindows(): void { 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(); const activeWorkspace = global.workspace_manager.get_active_workspace();
this._workArea = activeWorkspace.get_work_area_for_monitor(this._id);
// move() calls tileWindows() internally
this._workspaces[activeWorkspace.index()].move(this._workArea); this._workspaces[activeWorkspace.index()].move(this._workArea);
this._workspaces[activeWorkspace.index()].tileWindows()
} }
removeWorkspace(workspaceId: number): void { removeWorkspace(workspaceId: number): void {
@@ -82,5 +76,4 @@ export default class Monitor {
itemDragged(item: WindowWrapper, x: number, y: number): void { itemDragged(item: WindowWrapper, x: number, y: number): void {
this._workspaces[item.getWorkspace()].itemDragged(item, x, y); this._workspaces[item.getWorkspace()].itemDragged(item, x, y);
} }
}
}

View File

@@ -11,6 +11,8 @@ type WindowMinimizedHandler = (window: WindowWrapper) => void;
type WindowWorkspaceChangedHandler = (window: WindowWrapper) => void; type WindowWorkspaceChangedHandler = (window: WindowWrapper) => void;
export class WindowWrapper { export class WindowWrapper {
private static readonly RESIZE_TOLERANCE = 2;
readonly _window: Meta.Window; readonly _window: Meta.Window;
readonly _windowMinimizedHandler: WindowMinimizedHandler; readonly _windowMinimizedHandler: WindowMinimizedHandler;
readonly _signals: number[] = []; readonly _signals: number[] = [];
@@ -48,41 +50,26 @@ export class WindowWrapper {
startDragging(): void { startDragging(): void {
this._dragging = true; this._dragging = true;
} }
stopDragging(): void { stopDragging(): void {
Logger.log("STOPPED DRAGGING") Logger.log("STOPPED DRAGGING")
this._dragging = false; this._dragging = false;
} }
// setParent(parent: WindowContainer): void { connectWindowSignals(windowManager: IWindowManager): void {
// this._parent = parent; const windowId = this._window.get_id();
// }
//
// getParent(): WindowContainer | null {
// if (this._parent == null) {
// Logger.warn(`Attempting to get parent for window without parent ${JSON.stringify(this)}`);
// }
// return this._parent
// }
connectWindowSignals(
windowManager: IWindowManager,
): void {
const windowId = this._window.get_id()
// Handle window destruction
this._signals.push( this._signals.push(
this._window.connect('unmanaging', window => { this._window.connect('unmanaging', () => {
Logger.log("REMOVING WINDOW", windowId); Logger.log("REMOVING WINDOW", windowId);
windowManager.handleWindowClosed(this) windowManager.handleWindowClosed(this);
}), }),
this._window.connect('notify::minimized', (we) => { this._window.connect('notify::minimized', () => {
if (this._window.minimized) { if (this._window.minimized) {
Logger.log(`Window minimized: ${windowId}`); Logger.log(`Window minimized: ${windowId}`);
windowManager.handleWindowMinimized(this); windowManager.handleWindowMinimized(this);
} else {
} else if (!this._window.minimized) {
Logger.log(`Window unminimized: ${windowId}`); Logger.log(`Window unminimized: ${windowId}`);
windowManager.handleWindowUnminimized(this); windowManager.handleWindowUnminimized(this);
} }
}), }),
this._window.connect('notify::maximized-horizontally', () => { this._window.connect('notify::maximized-horizontally', () => {
@@ -92,18 +79,20 @@ export class WindowWrapper {
Logger.log(`Window unmaximized: ${windowId}`); Logger.log(`Window unmaximized: ${windowId}`);
} }
}), }),
this._window.connect("workspace-changed", (_metaWindow) => { this._window.connect("workspace-changed", () => {
Logger.log("WORKSPACE CHANGED FOR WINDOW", this._window.get_id()); Logger.log("WORKSPACE CHANGED FOR WINDOW", this._window.get_id());
windowManager.handleWindowChangedWorkspace(this); windowManager.handleWindowChangedWorkspace(this);
}), }),
this._window.connect("position-changed", (_metaWindow) => { this._window.connect("position-changed", () => {
windowManager.handleWindowPositionChanged(this);
}),
this._window.connect("size-changed", () => {
windowManager.handleWindowPositionChanged(this); windowManager.handleWindowPositionChanged(this);
}), }),
); );
} }
disconnectWindowSignals(): void { disconnectWindowSignals(): void {
if (this._signals) { if (this._signals) {
this._signals.forEach(signal => { this._signals.forEach(signal => {
try { try {
@@ -117,37 +106,36 @@ export class WindowWrapper {
} }
} }
safelyResizeWindow(rect: Rect, _retry: number = 2): void { safelyResizeWindow(rect: Rect, _retry: number = 3): void {
// Keep minimal logging
if (this._dragging) { if (this._dragging) {
Logger.info("STOPPED RESIZE BECAUSE ITEM IS BEING DRAGGED") Logger.info("STOPPED RESIZE BECAUSE ITEM IS BEING DRAGGED");
return; 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) { if (!actor) {
Logger.log("No actor available, can't resize safely yet"); Logger.log("No actor available, can't resize safely yet");
return; return;
} }
let windowActor = this._window.get_compositor_private() as Clutter.Actor;
if (!windowActor) return; actor.remove_all_transitions();
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); this._window.move_resize_frame(true, rect.x, rect.y, rect.width, 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)) { const new_rect = this._window.get_frame_rect();
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); const mismatch =
Math.abs(new_rect.x - rect.x) > WindowWrapper.RESIZE_TOLERANCE ||
Math.abs(new_rect.y - rect.y) > WindowWrapper.RESIZE_TOLERANCE ||
Math.abs(new_rect.width - rect.width) > WindowWrapper.RESIZE_TOLERANCE ||
Math.abs(new_rect.height - rect.height) > WindowWrapper.RESIZE_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})`);
queueEvent({ queueEvent({
name: "attempting_delayed_resize", name: `delayed_resize_${this.getWindowId()}`,
callback: () => { callback: () => this.safelyResizeWindow(rect, _retry - 1),
this.safelyResizeWindow(rect, _retry-1); }, 50);
}
})
} }
} }
} }

View File

@@ -1,20 +1,17 @@
import Meta from "gi://Meta"; 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'; import {WindowWrapper} from './window.js';
import * as Main from "resource:///org/gnome/shell/ui/main.js"; import * as Main from "resource:///org/gnome/shell/ui/main.js";
// import Mtk from "@girs/mtk-16";
import {Logger} from "../utils/logger.js"; import {Logger} from "../utils/logger.js";
import Monitor from "./monitor.js"; import Monitor from "./monitor.js";
import WindowContainer from "./container.js"; import WindowContainer from "./container.js";
import {Rect} from "../utils/rect.js";
export interface IWindowManager { export interface IWindowManager {
_activeWindowId: number | null; _activeWindowId: number | null;
// addWindow(window: Meta.Window): void;
handleWindowClosed(winWrap: WindowWrapper): void; handleWindowClosed(winWrap: WindowWrapper): void;
handleWindowMinimized(winWrap: WindowWrapper): void; handleWindowMinimized(winWrap: WindowWrapper): void;
@@ -29,8 +26,8 @@ export interface IWindowManager {
} }
const _UNUSED_MONITOR_ID = -1 const _UNUSED_MONITOR_ID = -1;
const _UNUSED_WINDOW_ID = -1 const _UNUSED_WINDOW_ID = -1;
export default class WindowManager implements IWindowManager { export default class WindowManager implements IWindowManager {
_displaySignals: number[] = []; _displaySignals: number[] = [];
@@ -40,23 +37,29 @@ export default class WindowManager implements IWindowManager {
_activeWindowId: number | null = null; _activeWindowId: number | null = null;
_monitors: Map<number, Monitor> = new Map<number, Monitor>(); _monitors: Map<number, Monitor> = new Map<number, Monitor>();
_minimizedItems: Map<number, WindowWrapper> = new Map<number, WindowWrapper>(); _minimizedItems: Map<number, WindowWrapper> = new Map<number, WindowWrapper>();
_grabbedWindowMonitor: number = _UNUSED_MONITOR_ID; _grabbedWindowMonitor: number = _UNUSED_MONITOR_ID;
_grabbedWindowId: number = _UNUSED_WINDOW_ID; _grabbedWindowId: number = _UNUSED_WINDOW_ID;
_changingGrabbedMonitor: boolean = false; _changingGrabbedMonitor: boolean = false;
_showingOverview: boolean = false; _showingOverview: boolean = false;
constructor() { // ── Resize-drag tracking ──────────────────────────────────────────────────
_isResizeDrag: boolean = false;
_resizeDragWindowId: number = _UNUSED_WINDOW_ID;
_resizeDragOp: Meta.GrabOp = Meta.GrabOp.NONE;
_resizeDragLastMouseX: number = 0;
_resizeDragLastMouseY: number = 0;
_isTiling: boolean = false;
private readonly _settings: Gio.Settings;
constructor(settings: Gio.Settings) {
this._settings = settings;
} }
public enable(): void { public enable(): void {
Logger.log("Starting Aerospike Window Manager"); Logger.log("Starting Aerospike Window Manager");
// Connect window signals
this.instantiateDisplaySignals(); this.instantiateDisplaySignals();
const mon_count = global.display.get_n_monitors(); const mon_count = global.display.get_n_monitors();
@@ -65,8 +68,6 @@ export default class WindowManager implements IWindowManager {
} }
this.captureExistingWindows(); this.captureExistingWindows();
// Sync the initially focused window
this.syncActiveWindow(); this.syncActiveWindow();
} }
@@ -93,7 +94,6 @@ export default class WindowManager implements IWindowManager {
global.display.connect('notify::focus-window', () => { global.display.connect('notify::focus-window', () => {
this.syncActiveWindow(); this.syncActiveWindow();
}), }),
global.display.connect("showing-desktop-changed", () => { global.display.connect("showing-desktop-changed", () => {
Logger.log("SHOWING DESKTOP CHANGED"); Logger.log("SHOWING DESKTOP CHANGED");
}), }),
@@ -104,13 +104,7 @@ export default class WindowManager implements IWindowManager {
global.display.connect("in-fullscreen-changed", () => { global.display.connect("in-fullscreen-changed", () => {
Logger.log("IN FULL SCREEN CHANGED"); Logger.log("IN FULL SCREEN CHANGED");
}), }),
) );
// this._windowManagerSignals = [
// global.window_manager.connect("show-tile-preview", (_, _metaWindow, _rect, _num) => {
// Logger.log("SHOW TITLE PREVIEW!")
// }),
// ];
this._workspaceManagerSignals = [ this._workspaceManagerSignals = [
global.workspace_manager.connect("showing-desktop-changed", () => { global.workspace_manager.connect("showing-desktop-changed", () => {
@@ -135,45 +129,31 @@ export default class WindowManager implements IWindowManager {
this._overviewSignals = [ this._overviewSignals = [
Main.overview.connect("hiding", () => { Main.overview.connect("hiding", () => {
// this.fromOverview = true;
Logger.log("HIDING OVERVIEW") Logger.log("HIDING OVERVIEW")
this._showingOverview = false; this._showingOverview = false;
this._tileMonitors(); this._tileMonitors();
// const eventObj = {
// name: "focus-after-overview",
// callback: () => {
// Logger.log("FOCUSING AFTER OVERVIEW");
// },
// };
// this.queueEvent(eventObj);
}), }),
Main.overview.connect("showing", () => { Main.overview.connect("showing", () => {
this._showingOverview = true; this._showingOverview = true;
Logger.log("SHOWING OVERVIEW"); Logger.log("SHOWING OVERVIEW");
}), }),
]; ];
} }
public disable(): void { public disable(): void {
Logger.log("DISABLED AEROSPIKE WINDOW MANAGER!") Logger.log("DISABLED AEROSPIKE WINDOW MANAGER!")
// Disconnect the focus signal and remove any existing borders
this.disconnectSignals(); this.disconnectSignals();
this.removeAllWindows(); this.removeAllWindows();
} }
removeAllWindows(): void { removeAllWindows(): void {
// Disconnect signals from minimized windows before clearing
this.disconnectMinimizedSignals(); this.disconnectMinimizedSignals();
this._minimizedItems.clear(); this._minimizedItems.clear();
this._monitors.forEach((monitor: Monitor) => { this._monitors.forEach((monitor: Monitor) => {
monitor.removeAllWindows(); monitor.removeAllWindows();
}) })
} }
disconnectSignals(): void { disconnectSignals(): void {
this.disconnectDisplaySignals(); this.disconnectDisplaySignals();
this.disconnectMonitorSignals(); this.disconnectMonitorSignals();
@@ -207,35 +187,61 @@ export default class WindowManager implements IWindowManager {
}) })
} }
_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 { handleGrabOpBegin(display: Meta.Display, window: Meta.Window, op: Meta.GrabOp): void {
if (op === Meta.GrabOp.MOVING_UNCONSTRAINED){
}
Logger.log("Grab Op Start", op); Logger.log("Grab Op Start", op);
Logger.log(display, window, op)
Logger.log(window.get_monitor()) if (this._isResizeOp(op)) {
this._getWrappedWindow(window)?.startDragging(); Logger.log("Resize drag begin, op=", op);
this._grabbedWindowMonitor = window.get_monitor(); this._isResizeDrag = true;
this._grabbedWindowId = window.get_id(); this._resizeDragWindowId = window.get_id();
this._resizeDragOp = op;
const [startMouseX, startMouseY] = global.get_pointer();
this._resizeDragLastMouseX = startMouseX;
this._resizeDragLastMouseY = startMouseY;
this._getWrappedWindow(window)?.startDragging();
} else {
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 { handleGrabOpEnd(display: Meta.Display, window: Meta.Window, op: Meta.GrabOp): void {
Logger.log("Grab Op End ", op); Logger.log("Grab Op End ", op);
Logger.log("primary display", display.get_primary_monitor())
this._grabbedWindowId = _UNUSED_WINDOW_ID; if (this._isResizeDrag) {
this._getWrappedWindow(window)?.stopDragging(); Logger.log("Resize drag end, op=", op);
this._tileMonitors(); this._isResizeDrag = false;
Logger.info("monitor_start and monitor_end", this._grabbedWindowMonitor, window.get_monitor()); this._resizeDragWindowId = _UNUSED_WINDOW_ID;
this._resizeDragLastMouseX = 0;
this._resizeDragLastMouseY = 0;
this._resizeDragOp = Meta.GrabOp.NONE;
this._getWrappedWindow(window)?.stopDragging();
this._tileMonitors();
} else {
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 { _getWrappedWindow(window: Meta.Window): WindowWrapper | undefined {
let wrapped: WindowWrapper | undefined = undefined; let wrapped: WindowWrapper | undefined = undefined;
for (const monitor of this._monitors.values()) { for (const monitor of this._monitors.values()) {
wrapped = monitor.getWindow(window.get_id()); wrapped = monitor.getWindow(window.get_id());
if (wrapped !== undefined) { if (wrapped !== undefined) break;
break;
}
} }
return wrapped; return wrapped;
} }
@@ -265,9 +271,13 @@ export default class WindowManager implements IWindowManager {
} }
public handleWindowPositionChanged(winWrap: WindowWrapper): void { public handleWindowPositionChanged(winWrap: WindowWrapper): void {
if (this._changingGrabbedMonitor) { if (this._isTiling || this._changingGrabbedMonitor) return;
if (this._isResizeDrag && winWrap.getWindowId() === this._resizeDragWindowId) {
this._handleResizeDragUpdate(winWrap);
return; return;
} }
if (winWrap.getWindowId() === this._grabbedWindowId) { if (winWrap.getWindowId() === this._grabbedWindowId) {
const [mouseX, mouseY, _] = global.get_pointer(); const [mouseX, mouseY, _] = global.get_pointer();
@@ -280,19 +290,82 @@ export default class WindowManager implements IWindowManager {
break; break;
} }
} }
if (monitorIndex === -1) { if (monitorIndex === -1) return;
return
}
if (monitorIndex !== this._grabbedWindowMonitor) { if (monitorIndex !== this._grabbedWindowMonitor) {
this._changingGrabbedMonitor = true; this._changingGrabbedMonitor = true;
this._moveWindowToMonitor(winWrap.getWindow(), monitorIndex); this._moveWindowToMonitor(winWrap.getWindow(), monitorIndex);
this._changingGrabbedMonitor = false this._changingGrabbedMonitor = false;
}
this._isTiling = true;
try {
this._monitors.get(monitorIndex)?.itemDragged(winWrap, mouseX, mouseY);
} finally {
this._isTiling = false;
} }
this._monitors.get(monitorIndex)?.itemDragged(winWrap, mouseX, mouseY);
} }
} }
private _handleResizeDragUpdate(winWrap: WindowWrapper): void {
const op = this._resizeDragOp;
const winId = winWrap.getWindowId();
const [mouseX, mouseY] = global.get_pointer();
const dx = mouseX - this._resizeDragLastMouseX;
const dy = mouseY - this._resizeDragLastMouseY;
if (dx === 0 && dy === 0) return;
this._resizeDragLastMouseX = mouseX;
this._resizeDragLastMouseY = mouseY;
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;
// E/S edge → boundary after the item; W/N edge → boundary before it.
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);
} else if (op === Meta.GrabOp.RESIZING_W || op === Meta.GrabOp.RESIZING_NW || op === Meta.GrabOp.RESIZING_SW) {
adjusted = container.adjustBoundary(itemIndex - 1, dx);
}
} else {
if (op === Meta.GrabOp.RESIZING_S || op === Meta.GrabOp.RESIZING_SE || op === Meta.GrabOp.RESIZING_SW) {
adjusted = container.adjustBoundary(itemIndex, dy);
} else if (op === Meta.GrabOp.RESIZING_N || op === Meta.GrabOp.RESIZING_NE || op === Meta.GrabOp.RESIZING_NW) {
adjusted = container.adjustBoundary(itemIndex - 1, dy);
}
}
if (adjusted) {
this._isTiling = true;
try {
container.tileWindows();
} finally {
this._isTiling = false;
}
}
}
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 container = monitor._workspaces[activeWorkspaceIndex].getContainerForWindow(winId);
if (container !== null) return container;
}
return null;
}
public handleWindowMinimized(winWrap: WindowWrapper): void { public handleWindowMinimized(winWrap: WindowWrapper): void {
const monitor_id = winWrap.getWindow().get_monitor() const monitor_id = winWrap.getWindow().get_monitor()
@@ -307,7 +380,6 @@ export default class WindowManager implements IWindowManager {
this._tileMonitors() this._tileMonitors()
} }
public handleWindowChangedWorkspace(winWrap: WindowWrapper): void { public handleWindowChangedWorkspace(winWrap: WindowWrapper): void {
const monitor = winWrap.getWindow().get_monitor(); const monitor = winWrap.getWindow().get_monitor();
this._monitors.get(monitor)?.removeWindow(winWrap); this._monitors.get(monitor)?.removeWindow(winWrap);
@@ -326,41 +398,26 @@ export default class WindowManager implements IWindowManager {
this._tileMonitors(); this._tileMonitors();
} }
handleWindowCreated(display: Meta.Display, window: Meta.Window) { handleWindowCreated(display: Meta.Display, window: Meta.Window) {
Logger.log("WINDOW CREATED ON DISPLAY", window, display); Logger.log("WINDOW CREATED ON DISPLAY", window, display);
if (!this._isWindowTileable(window)) { if (!this._isWindowTileable(window)) return;
return;
}
Logger.log("WINDOW IS TILABLE"); Logger.log("WINDOW IS TILABLE");
this.addWindowToMonitor(window); this.addWindowToMonitor(window);
} }
/**
* Handle window closed event
*/
handleWindowClosed(window: WindowWrapper): void { handleWindowClosed(window: WindowWrapper): void {
const mon_id = window._window.get_monitor(); const mon_id = window._window.get_monitor();
this._monitors.get(mon_id)?.removeWindow(window); this._monitors.get(mon_id)?.removeWindow(window);
window.disconnectWindowSignals() window.disconnectWindowSignals()
// Remove from managed windows
this.syncActiveWindow(); this.syncActiveWindow();
// Retile remaining windows
this._tileMonitors(); this._tileMonitors();
} }
public addWindowToMonitor(window: Meta.Window) { public addWindowToMonitor(window: Meta.Window) {
Logger.log("ADDING WINDOW TO MONITOR", window, window); Logger.log("ADDING WINDOW TO MONITOR", window, window);
var wrapper = new WindowWrapper(window, (winWrap) => this.handleWindowMinimized(winWrap)) var wrapper = new WindowWrapper(window, (winWrap) => this.handleWindowMinimized(winWrap))
wrapper.connectWindowSignals(this); wrapper.connectWindowSignals(this);
this._addWindowWrapperToMonitor(wrapper); this._addWindowWrapperToMonitor(wrapper);
} }
_addWindowWrapperToMonitor(winWrap: WindowWrapper) { _addWindowWrapperToMonitor(winWrap: WindowWrapper) {
@@ -372,9 +429,13 @@ export default class WindowManager implements IWindowManager {
} }
_tileMonitors(): void { _tileMonitors(): void {
this._isTiling = true;
for (const monitor of this._monitors.values()) { try {
monitor.tileWindows() for (const monitor of this._monitors.values()) {
monitor.tileWindows();
}
} finally {
this._isTiling = false;
} }
} }
@@ -382,7 +443,7 @@ export default class WindowManager implements IWindowManager {
"org.gnome.Shell.Extensions", "org.gnome.Shell.Extensions",
] ]
_isWindowTilingBlocked(window: Meta.Window) : boolean { _isWindowTilingBlocked(window: Meta.Window): boolean {
Logger.info("title", window.get_title()); Logger.info("title", window.get_title());
Logger.info("description", window.get_description()); Logger.info("description", window.get_description());
Logger.info("class", window.get_wm_class()); Logger.info("class", window.get_wm_class());
@@ -397,17 +458,12 @@ export default class WindowManager implements IWindowManager {
} }
_isWindowTileable(window: Meta.Window) { _isWindowTileable(window: Meta.Window) {
if (!window || !window.get_compositor_private()) return false;
if (this._isWindowTilingBlocked(window)) return false;
if (!window || !window.get_compositor_private()) {
return false;
}
if (this._isWindowTilingBlocked(window)) {
return false;
}
const windowType = window.get_window_type(); const windowType = window.get_window_type();
Logger.log("WINDOW TILING CHECK",); Logger.log("WINDOW TILING CHECK",);
// Skip certain types of windows
return !window.is_skip_taskbar() && return !window.is_skip_taskbar() &&
windowType !== Meta.WindowType.DESKTOP && windowType !== Meta.WindowType.DESKTOP &&
windowType !== Meta.WindowType.DOCK && windowType !== Meta.WindowType.DOCK &&
@@ -417,14 +473,6 @@ export default class WindowManager implements IWindowManager {
windowType !== Meta.WindowType.MENU; windowType !== Meta.WindowType.MENU;
} }
/**
* Synchronizes the active window with GNOME's currently active window
*
* This function queries GNOME Shell for the current focused window and
* updates the extension's active window tracking to match.
*
* @returns The window ID of the active window, or null if no window is active
*/
public syncActiveWindow(): number | null { public syncActiveWindow(): number | null {
const focusWindow = global.display.focus_window; const focusWindow = global.display.focus_window;
if (focusWindow) { if (focusWindow) {
@@ -437,83 +485,33 @@ export default class WindowManager implements IWindowManager {
return this._activeWindowId; return this._activeWindowId;
} }
/**
* Toggles the orientation of the active container (the container holding the active window)
*/
public toggleActiveContainerOrientation(): void { public toggleActiveContainerOrientation(): void {
if (this._activeWindowId === null) { if (this._activeWindowId === null) {
Logger.warn("No active window, cannot toggle container orientation"); Logger.warn("No active window, cannot toggle container orientation");
return; return;
} }
const container = this._findContainerForWindowAcrossMonitors(this._activeWindowId);
// Find the active window's container if (container) {
const activeContainer = this._findActiveContainer(); container.toggleOrientation();
if (activeContainer) {
activeContainer.toggleOrientation();
} else { } else {
Logger.warn("Could not find container for active window"); Logger.warn("Could not find container for active window");
} }
} }
/** public resetActiveContainerRatios(): void {
* Finds the container that directly contains the active window
* @returns The container holding the active window, or null if not found
*/
private _findActiveContainer(): WindowContainer | null {
if (this._activeWindowId === null) { if (this._activeWindowId === null) {
return null; Logger.warn("No active window, cannot reset container ratios");
return;
} }
const container = this._findContainerForWindowAcrossMonitors(this._activeWindowId);
for (const monitor of this._monitors.values()) { if (container) {
const activeWorkspaceIndex = global.workspace_manager.get_active_workspace().index(); Logger.info("Resetting container ratios to equal splits");
container.resetRatios();
// Bounds check to prevent accessing invalid workspace } else {
if (activeWorkspaceIndex >= monitor._workspaces.length || activeWorkspaceIndex < 0) { Logger.warn("Could not find container for active window");
Logger.warn(`Active workspace index ${activeWorkspaceIndex} out of bounds for monitor with ${monitor._workspaces.length} workspaces`);
continue;
}
const workspace = monitor._workspaces[activeWorkspaceIndex];
// Check if the window is directly in the workspace container
const windowWrapper = workspace.getWindow(this._activeWindowId);
if (windowWrapper) {
// Try to find the parent container
const container = this._findContainerHoldingWindow(workspace, this._activeWindowId);
return container;
}
} }
return null;
} }
/**
* Recursively finds the container that directly contains a specific window
* @param container The container to search
* @param windowId The window ID to find
* @returns The container that directly contains the window, or null if not found
*/
private _findContainerHoldingWindow(container: WindowContainer, windowId: number): WindowContainer | null {
// Check if this container directly contains the window
for (const item of container._tiledItems) {
if (item instanceof WindowContainer) {
// Recursively search nested containers
const result = this._findContainerHoldingWindow(item, windowId);
if (result) {
return result;
}
} else if (item.getWindowId() === windowId) {
// Found it! Return this container as it directly holds the window
return container;
}
}
return null;
}
/**
* Prints the tree structure of all monitors, workspaces, containers, and windows to the logs
*/
public printTreeStructure(): void { public printTreeStructure(): void {
Logger.info("=".repeat(80)); Logger.info("=".repeat(80));
Logger.info("WINDOW TREE STRUCTURE"); Logger.info("WINDOW TREE STRUCTURE");
@@ -526,19 +524,15 @@ export default class WindowManager implements IWindowManager {
this._monitors.forEach((monitor: Monitor, monitorId: number) => { this._monitors.forEach((monitor: Monitor, monitorId: number) => {
const isActiveMonitor = this._activeWindowId !== null && const isActiveMonitor = this._activeWindowId !== null &&
monitor.getWindow(this._activeWindowId) !== undefined; monitor.getWindow(this._activeWindowId) !== undefined;
const monitorMarker = isActiveMonitor ? ' *' : '';
Logger.info(`Monitor ${monitorId}${monitorMarker}:`); Logger.info(`Monitor ${monitorId}${isActiveMonitor ? ' *' : ''}:`);
Logger.info(` Work Area: x=${monitor._workArea.x}, y=${monitor._workArea.y}, w=${monitor._workArea.width}, h=${monitor._workArea.height}`); Logger.info(` Work Area: x=${monitor._workArea.x}, y=${monitor._workArea.y}, w=${monitor._workArea.width}, h=${monitor._workArea.height}`);
monitor._workspaces.forEach((workspace, workspaceIndex) => { monitor._workspaces.forEach((workspace, workspaceIndex) => {
const isActiveWorkspace = workspaceIndex === activeWorkspaceIndex; const isActiveWorkspace = workspaceIndex === activeWorkspaceIndex;
const workspaceMarker = isActiveWorkspace && isActiveMonitor ? ' *' : ''; Logger.info(` Workspace ${workspaceIndex}${isActiveWorkspace && isActiveMonitor ? ' *' : ''}:`);
Logger.info(` Workspace ${workspaceIndex}${workspaceMarker}:`);
Logger.info(` Orientation: ${workspace._orientation === 0 ? 'HORIZONTAL' : 'VERTICAL'}`); Logger.info(` Orientation: ${workspace._orientation === 0 ? 'HORIZONTAL' : 'VERTICAL'}`);
Logger.info(` Items: ${workspace._tiledItems.length}`); Logger.info(` Items: ${workspace._tiledItems.length}`);
this._printContainerTree(workspace, 4); this._printContainerTree(workspace, 4);
}); });
}); });
@@ -546,31 +540,20 @@ export default class WindowManager implements IWindowManager {
Logger.info("=".repeat(80)); Logger.info("=".repeat(80));
} }
/** private _printContainerTree(container: WindowContainer, indentLevel: number): void {
* Recursively prints the container tree structure
* @param container The container to print
* @param indentLevel The indentation level (number of spaces)
*/
private _printContainerTree(container: any, indentLevel: number): void {
const indent = " ".repeat(indentLevel); const indent = " ".repeat(indentLevel);
container._tiledItems.forEach((item: any, index: number) => { container._tiledItems.forEach((item, index) => {
if (item instanceof WindowContainer) { if (item instanceof WindowContainer) {
// Check if this container contains the active window const containsActive = this._activeWindowId !== null &&
const containsActiveWindow = this._activeWindowId !== null && item.getWindow(this._activeWindowId) !== undefined;
item.getWindow(this._activeWindowId) !== undefined; Logger.info(`${indent}[${index}] Container (${item._orientation === 0 ? 'HORIZONTAL' : 'VERTICAL'})${containsActive ? ' *' : ''}:`);
const containerMarker = containsActiveWindow ? ' *' : '';
Logger.info(`${indent}[${index}] Container (${item._orientation === 0 ? 'HORIZONTAL' : 'VERTICAL'})${containerMarker}:`);
Logger.info(`${indent} Items: ${item._tiledItems.length}`); Logger.info(`${indent} Items: ${item._tiledItems.length}`);
Logger.info(`${indent} Work Area: x=${item._workArea.x}, y=${item._workArea.y}, w=${item._workArea.width}, h=${item._workArea.height}`); Logger.info(`${indent} Work Area: x=${item._workArea.x}, y=${item._workArea.y}, w=${item._workArea.width}, h=${item._workArea.height}`);
this._printContainerTree(item, indentLevel + 4); this._printContainerTree(item, indentLevel + 4);
} else { } else {
const window = item.getWindow(); const window = item.getWindow();
const isActiveWindow = this._activeWindowId === item.getWindowId(); Logger.info(`${indent}[${index}] Window ID: ${item.getWindowId()}${this._activeWindowId === item.getWindowId() ? ' *' : ''}`);
const windowMarker = isActiveWindow ? ' *' : '';
Logger.info(`${indent}[${index}] Window ID: ${item.getWindowId()}${windowMarker}`);
Logger.info(`${indent} Title: "${window.get_title()}"`); Logger.info(`${indent} Title: "${window.get_title()}"`);
Logger.info(`${indent} Class: ${window.get_wm_class()}`); Logger.info(`${indent} Class: ${window.get_wm_class()}`);
const rect = item.getRect(); const rect = item.getRect();
@@ -578,6 +561,4 @@ export default class WindowManager implements IWindowManager {
} }
}); });
} }
} }