1 Commits

Author SHA1 Message Date
Lucas Oskorep
dd565da626 feat: add resizing 2025-10-28 12:59:11 -04:00
20 changed files with 998 additions and 3785 deletions

View File

@@ -1,99 +0,0 @@
name: Build and Test
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: archlinux
container:
image: node:25
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install pnpm and TypeScript
run: npm install -g pnpm typescript@5.8.3
- name: Install just
run: |
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin
- name: Install system dependencies
run: |
apt-get update
apt-get install -y libglib2.0-dev glib-networking zip
- name: Install project dependencies
run: pnpm install
- name: Run unit tests
run: just test
- name: Build and package extension
run: just build-package
- name: Upload extension package
uses: actions/upload-artifact@v3
with:
name: aerospike-extension
path: aerospike.zip
retention-days: 30
release:
needs: build
runs-on: archlinux
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment:
name: production
container:
image: node:25
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download artifact
uses: actions/download-artifact@v3
with:
name: aerospike-extension
- name: Get version from package.json
id: get_version
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=v${VERSION}" >> $GITHUB_OUTPUT
- name: Create Gitea Release
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
curl -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"tag_name": "${{ steps.get_version.outputs.version }}",
"name": "Release ${{ steps.get_version.outputs.version }}",
"body": "Automated release of aerospike GNOME extension ${{ steps.get_version.outputs.version }}\n\n## Installation\nDownload aerospike.zip and install it as a GNOME extension."
}' \
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases"
- name: Upload Release Asset
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
RELEASE_ID=$(curl -H "Authorization: token ${GITEA_TOKEN}" \
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${{ steps.get_version.outputs.version }}" | \
grep -Po '"id":\s*\K[0-9]+' | head -1)
curl -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: multipart/form-data" \
-F "attachment=@aerospike.zip" \
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=aerospike.zip"

2
.nvmrc
View File

@@ -1 +1 @@
24
v20

View File

@@ -1 +1 @@
# Aerospike Gnome (Tiling Window Manager)
# Aerospike Gnome (Tiling Window Manager)

View File

@@ -1,5 +1,5 @@
import Meta from 'gi://Meta';
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
import {Extension, ExtensionMetadata} from 'resource:///org/gnome/shell/extensions/extension.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import Gio from 'gi://Gio';
import Shell from 'gi://Shell';
@@ -11,11 +11,11 @@ export default class aerospike extends Extension {
keyBindings: Map<string, number>;
windowManager: WindowManager;
constructor(metadata: ConstructorParameters<typeof Extension>[0]) {
constructor(metadata: ExtensionMetadata) {
super(metadata);
this.settings = this.getSettings('org.gnome.shell.extensions.aerospike');
this.keyBindings = new Map();
this.windowManager = new WindowManager(this.settings);
this.windowManager = new WindowManager();
}
enable() {
@@ -30,25 +30,32 @@ export default class aerospike extends Extension {
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() {
const keybindings = Object.keys(this.keybindingActions());
keybindings.forEach(name => {
this.settings.connect(`changed::${name}`, () => {
log(`${name} keybinding changed to: ${this.settings.get_strv(name)}`);
this.refreshKeybinding(name);
});
// Monitor settings changes
this.settings.connect('changed::move-left', () => {
log(`Keybinding 1 changed to: ${this.settings.get_strv('move-left')}`);
this.refreshKeybinding('move-left');
});
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::remove-all-dividers', () => {
log(`Keybinding remove-all-dividers changed to: ${this.settings.get_strv('remove-all-dividers')}`);
this.refreshKeybinding('remove-all-dividers');
});
this.settings.connect('changed::dropdown-option', () => {
@@ -59,15 +66,40 @@ export default class aerospike extends Extension {
log(`Color selection changed to: ${this.settings.get_string('color-selection')}`);
});
}
private refreshKeybinding(settingName: string) {
if (this.keyBindings.has(settingName)) {
Main.wm.removeKeybinding(settingName);
this.keyBindings.delete(settingName);
}
const action = this.keybindingActions()[settingName];
if (action) this.bindKeybinding(settingName, action);
switch (settingName) {
case 'move-left':
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 'remove-all-dividers':
this.bindKeybinding('remove-all-dividers', () => {
Logger.info('Remove all dividers keybinding pressed!');
this.windowManager.removeAllDividersFromActiveContainer();
});
break;
}
}
private removeKeybindings() {
@@ -78,10 +110,26 @@ export default class aerospike extends Extension {
}
private setupKeybindings() {
const actions = this.keybindingActions();
for (const [name, action] of Object.entries(actions)) {
this.bindKeybinding(name, action);
}
this.bindKeybinding('move-left', () => {
Logger.info('Keybinding 1 was pressed!');
});
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('remove-all-dividers', () => {
Logger.info('Remove all dividers keybinding pressed!');
this.windowManager.removeAllDividersFromActiveContainer();
});
}
private bindKeybinding(settingName: string, callback: () => void) {
@@ -101,4 +149,7 @@ export default class aerospike extends Extension {
this.keyBindings.set(settingName, keyBindingAction);
}
}
}

View File

@@ -1,23 +0,0 @@
export default {
preset: 'ts-jest/presets/default-esm',
testEnvironment: 'node',
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
transform: {
'^.+\\.ts$': [
'ts-jest',
{
useESM: true,
},
],
},
testMatch: ['**/__tests__/**/*.test.ts', '**/?(*.)+(spec|test).ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/__tests__/**',
],
modulePathIgnorePatterns: ['<rootDir>/dist/', '<rootDir>/node_modules/'],
};

View File

@@ -8,7 +8,7 @@ packages:
build: packages && build-schemas
rm -rf dist/*
pnpm exec tsc
tsc
cp metadata.json dist/
cp stylesheet.css dist/
mkdir -p dist/schemas
@@ -28,25 +28,13 @@ install: build
cp -r dist/* ~/.local/share/gnome-shell/extensions/{{NAME}}@{{DOMAIN}}/
run:
env MUTTER_DEBUG_DUMMY_MODE_SPECS=1280x720 dbus-run-session -- gnome-shell --devkit --wayland
env MUTTER_DEBUG_DUMMY_MODE_SPECS=1280x720 dbus-run-session -- gnome-shell --nested --wayland
install-and-run: install run
live-debug:
journalctl /usr/bin/gnome-shell -f -o cat | tee debug.log
test:
pnpm test
test-watch:
pnpm test:watch
test-coverage:
pnpm test:coverage
ci-local:
act -W .gitea/workflows/build.yaml
#pack: build
# gnome-extensions pack dist \
# --force \
@@ -54,4 +42,4 @@ ci-local:
# --schema ../schemas/org.gnome.shell.extensions.aerospike.gschema.xml
#
#install-pack: pack
# gnome-extensions install ./{{FULL_NAME}}.shell-extension.zip --force
# gnome-extensions install ./{{FULL_NAME}}.shell-extension.zip --force

View File

@@ -15,20 +15,11 @@
},
"homepage": "https://github.com/example/my-extension#readme",
"sideEffects": false,
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"devDependencies": {
"@girs/gjs": "4.0.0-beta.38",
"@girs/gnome-shell": "49.1.0",
"@jest/globals": "^30.0.0",
"@types/jest": "^30.0.0",
"@girs/gnome-shell": "49.0.1",
"eslint": "^9.36.0",
"eslint-plugin-jsdoc": "^62.0.0",
"jest": "^30.0.0",
"ts-jest": "^29.1.2",
"eslint-plugin-jsdoc": "^50.8.0",
"typescript": "^5.9.2"
},
"dependencies": {

2964
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -37,22 +37,10 @@
<description>Keyboard shortcut for triggering action 4</description>
</key>
<key name="print-tree" type="as">
<default><![CDATA[['<Super><Shift>x']]]></default>
<summary>Print window tree structure</summary>
<description>Prints the current tree of containers and windows per monitor to logs</description>
</key>
<key name="toggle-orientation" type="as">
<default><![CDATA[['<Super><Shift>comma']]]></default>
<summary>Toggle active container orientation</summary>
<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 name="remove-all-dividers" type="as">
<default><![CDATA[['<Alt>z']]]></default>
<summary>Remove all dividers from active container</summary>
<description>Keyboard shortcut for removing all dividers from the container with the active window</description>
</key>
</schema>

View File

@@ -1,182 +0,0 @@
import { describe, test, expect, jest, beforeEach } from '@jest/globals';
// Mock the dependencies
jest.mock('../utils/logger.js', () => ({
Logger: {
log: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
},
}));
jest.mock('../utils/events.js', () => ({
default: jest.fn(),
}));
// Since we can't import the actual WindowContainer that depends on GNOME APIs,
// we'll test the logic patterns used in the container
describe('Container Logic Tests', () => {
describe('Orientation Toggle Logic', () => {
enum Orientation {
HORIZONTAL = 0,
VERTICAL = 1,
}
const toggleOrientation = (current: Orientation): Orientation => {
return current === Orientation.HORIZONTAL
? Orientation.VERTICAL
: Orientation.HORIZONTAL;
};
test('should toggle from HORIZONTAL to VERTICAL', () => {
const result = toggleOrientation(Orientation.HORIZONTAL);
expect(result).toBe(Orientation.VERTICAL);
});
test('should toggle from VERTICAL to HORIZONTAL', () => {
const result = toggleOrientation(Orientation.VERTICAL);
expect(result).toBe(Orientation.HORIZONTAL);
});
});
describe('Window Bounds Calculation', () => {
test('should calculate horizontal bounds correctly', () => {
const workArea = { x: 0, y: 0, width: 1000, height: 500 };
const itemCount = 3;
const windowWidth = Math.floor(workArea.width / itemCount);
const bounds = Array.from({ length: itemCount }, (_, index) => ({
x: workArea.x + (index * windowWidth),
y: workArea.y,
width: windowWidth,
height: workArea.height,
}));
expect(bounds.length).toBe(3);
expect(bounds[0].x).toBe(0);
expect(bounds[1].x).toBe(333);
expect(bounds[2].x).toBe(666);
expect(bounds[0].width).toBe(333);
});
test('should calculate vertical bounds correctly', () => {
const workArea = { x: 0, y: 0, width: 1000, height: 900 };
const itemCount = 3;
const windowHeight = Math.floor(workArea.height / itemCount);
const bounds = Array.from({ length: itemCount }, (_, index) => ({
x: workArea.x,
y: workArea.y + (index * windowHeight),
width: workArea.width,
height: windowHeight,
}));
expect(bounds.length).toBe(3);
expect(bounds[0].y).toBe(0);
expect(bounds[1].y).toBe(300);
expect(bounds[2].y).toBe(600);
expect(bounds[0].height).toBe(300);
});
test('should handle single window bounds', () => {
const workArea = { x: 100, y: 50, width: 800, height: 600 };
const itemCount = 1;
const windowWidth = Math.floor(workArea.width / itemCount);
const bounds = [{
x: workArea.x,
y: workArea.y,
width: windowWidth,
height: workArea.height,
}];
expect(bounds[0].x).toBe(100);
expect(bounds[0].y).toBe(50);
expect(bounds[0].width).toBe(800);
expect(bounds[0].height).toBe(600);
});
});
describe('Window Index Finding', () => {
test('should find window index in array', () => {
const windows = [
{ id: 1, title: 'Window 1' },
{ id: 2, title: 'Window 2' },
{ id: 3, title: 'Window 3' },
];
const findIndex = (id: number) => {
for (let i = 0; i < windows.length; i++) {
if (windows[i].id === id) {
return i;
}
}
return -1;
};
expect(findIndex(2)).toBe(1);
expect(findIndex(3)).toBe(2);
expect(findIndex(999)).toBe(-1);
});
test('should safely remove window by index', () => {
const windows = [
{ id: 1, title: 'Window 1' },
{ id: 2, title: 'Window 2' },
{ id: 3, title: 'Window 3' },
];
const removeWindow = (id: number) => {
const index = windows.findIndex(w => w.id === id);
if (index !== -1) {
windows.splice(index, 1);
return true;
}
return false;
};
const removed = removeWindow(2);
expect(removed).toBe(true);
expect(windows.length).toBe(2);
expect(windows.find(w => w.id === 2)).toBeUndefined();
});
});
describe('Container Item Reordering', () => {
test('should reorder items correctly', () => {
const items = ['A', 'B', 'C', 'D'];
const originalIndex = 1; // 'B'
const newIndex = 3;
// Remove from original position and insert at new position
const [item] = items.splice(originalIndex, 1);
items.splice(newIndex, 0, item);
expect(items).toEqual(['A', 'C', 'D', 'B']);
});
test('should handle reordering to same position', () => {
const items = ['A', 'B', 'C'];
const originalIndex = 1;
const newIndex = 1;
if (originalIndex !== newIndex) {
const [item] = items.splice(originalIndex, 1);
items.splice(newIndex, 0, item);
}
expect(items).toEqual(['A', 'B', 'C']);
});
test('should handle moving first item to last', () => {
const items = ['A', 'B', 'C'];
const [item] = items.splice(0, 1);
items.splice(2, 0, item);
expect(items).toEqual(['B', 'C', 'A']);
});
});
});

View File

@@ -1,75 +0,0 @@
import { describe, test, expect } from '@jest/globals';
import type { Rect } from '../utils/rect.js';
describe('Rect Type Tests', () => {
test('should create a valid Rect object', () => {
const rect: Rect = {
x: 10,
y: 20,
width: 100,
height: 200,
};
expect(rect.x).toBe(10);
expect(rect.y).toBe(20);
expect(rect.width).toBe(100);
expect(rect.height).toBe(200);
});
test('should handle zero dimensions', () => {
const rect: Rect = {
x: 0,
y: 0,
width: 0,
height: 0,
};
expect(rect.width).toBe(0);
expect(rect.height).toBe(0);
});
test('should handle negative coordinates', () => {
const rect: Rect = {
x: -50,
y: -100,
width: 200,
height: 300,
};
expect(rect.x).toBe(-50);
expect(rect.y).toBe(-100);
});
test('should calculate rect area correctly', () => {
const rect: Rect = {
x: 0,
y: 0,
width: 100,
height: 50,
};
const area = rect.width * rect.height;
expect(area).toBe(5000);
});
test('should determine if point is inside rect', () => {
const rect: Rect = {
x: 10,
y: 10,
width: 100,
height: 100,
};
const pointInside = { x: 50, y: 50 };
const pointOutside = { x: 200, y: 200 };
const isInside = (point: { x: number; y: number }, r: Rect) =>
point.x >= r.x &&
point.x <= r.x + r.width &&
point.y >= r.y &&
point.y <= r.y + r.height;
expect(isInside(pointInside, rect)).toBe(true);
expect(isInside(pointOutside, rect)).toBe(false);
});
});

View File

@@ -155,32 +155,6 @@ export default class AerospikeExtensions extends ExtensionPreferences {
})
);
keybindingsGroup.add(
new EntryRow({
title: _('Print Tree Structure'),
settings: settings,
bind: 'print-tree',
map: keybindingMap
})
);
keybindingsGroup.add(
new EntryRow({
title: _('Toggle Orientation'),
settings: settings,
bind: 'toggle-orientation',
map: keybindingMap
})
);
keybindingsGroup.add(
new EntryRow({
title: _('Reset Container Ratios to Equal'),
settings: settings,
bind: 'reset-ratios',
map: keybindingMap
})
);
}

View File

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

View File

@@ -1,87 +1,49 @@
import {WindowWrapper} from "./window.js";
import {Logger} from "../utils/logger.js";
import Meta from "gi://Meta";
import queueEvent from "../utils/events.js";
import {Rect} from "../utils/rect.js";
import {Divider} from "./divider.js";
enum Orientation {
HORIZONTAL = 0,
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;
}
type ContainerItem = WindowWrapper | WindowContainer | Divider;
export default class WindowContainer {
_tiledItems: (WindowWrapper | WindowContainer)[];
_tiledItems: ContainerItem[];
_tiledWindowLookup: Map<number, WindowWrapper>;
_orientation: Orientation = Orientation.HORIZONTAL;
_workArea: Rect;
_splitRatios: number[];
constructor(workspaceArea: Rect) {
constructor(workspaceArea: Rect,) {
// this._id = monitorId;
this._tiledItems = [];
this._tiledWindowLookup = new Map<number, WindowWrapper>();
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 {
this._workArea = rect;
this.tileWindows();
}
toggleOrientation(): void {
this._orientation = this._orientation === Orientation.HORIZONTAL
? Orientation.VERTICAL
: Orientation.HORIZONTAL;
Logger.info(`Container orientation toggled to ${this._orientation === Orientation.HORIZONTAL ? 'HORIZONTAL' : 'VERTICAL'}`);
this.tileWindows();
}
addWindow(winWrap: WindowWrapper): void {
// Add window to managed windows
this._tiledItems.push(winWrap);
this._tiledWindowLookup.set(winWrap.getWindowId(), winWrap);
this._addRatioForNewWindow();
// winWrap.setParent(this);
queueEvent({
name: "tiling-windows",
callback: () => this.tileWindows(),
}, 100);
callback: () => {
this.tileWindows();
}
}, 100)
}
getWindow(win_id: number): WindowWrapper | undefined {
@@ -89,35 +51,36 @@ export default class WindowContainer {
return this._tiledWindowLookup.get(win_id);
}
for (const item of this._tiledItems) {
if (item instanceof WindowContainer) {
if (Divider.isDivider(item)) {
continue; // Skip dividers
} else if (item instanceof WindowContainer) {
const win = item.getWindow(win_id);
if (win) return win;
} else if (item.getWindowId() === win_id) {
if (win) {
return win;
}
} else if (item instanceof WindowWrapper && item.getWindowId() === win_id) {
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();
const index = this._getIndexOfWindow(win_id)
this._tiledItems.splice(index, 1);
this._cleanupInvalidDividers();
} else {
for (const item of this._tiledItems) {
if (item instanceof WindowContainer) {
@@ -125,121 +88,272 @@ export default class WindowContainer {
}
}
}
this.tileWindows();
this.tileWindows()
}
/**
* Removes invalid dividers from the items list.
* Invalid dividers are:
* - Dividers at the start or end of the list (no window on one side)
* - Consecutive dividers (two dividers in a row)
*/
_cleanupInvalidDividers(): void {
let i = 0;
while (i < this._tiledItems.length) {
const item = this._tiledItems[i];
if (Divider.isDivider(item)) {
// Check if divider is at start or end
const isAtStart = i === 0;
const isAtEnd = i === this._tiledItems.length - 1;
// Check if next item is also a divider
const nextIsDivider = i < this._tiledItems.length - 1 &&
Divider.isDivider(this._tiledItems[i + 1]);
if (isAtStart || isAtEnd || nextIsDivider) {
Logger.log(`Removing invalid divider at index ${i}`);
this._tiledItems.splice(i, 1);
continue; // Don't increment i, check the same position again
}
}
i++;
}
}
disconnectSignals(): void {
this._tiledItems.forEach((item) => {
if (item instanceof WindowContainer) {
item.disconnectSignals();
} else {
item.disconnectWindowSignals();
if (Divider.isDivider(item)) {
// Skip dividers - they don't have signals
return;
} else if (item instanceof WindowContainer) {
item.disconnectSignals()
} else if (item instanceof WindowWrapper) {
item.disconnectWindowSignals();
}
}
});
)
}
removeAllWindows(): void {
this._tiledItems = [];
this._tiledWindowLookup.clear();
this._splitRatios = [];
this._tiledItems = []
this._tiledWindowLookup.clear()
}
tileWindows(): void {
Logger.log("TILING WINDOWS IN CONTAINER");
tileWindows() {
Logger.log("TILING WINDOWS IN CONTAINER")
Logger.log("WorkArea", this._workArea);
this._tileItems();
// Get all windows for current workspaceArea
this._tileItems()
return true
}
_tileItems() {
if (this._tiledItems.length === 0) return;
if (this._tiledItems.length === 0) {
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];
// Apply bounds to non-divider items
let boundsIndex = 0;
this._tiledItems.forEach((item) => {
if (Divider.isDivider(item)) {
return; // Skip dividers
}
const rect = bounds[boundsIndex];
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})`);
} else if (item instanceof WindowWrapper) {
item.safelyResizeWindow(rect);
}
});
boundsIndex++;
})
}
// ─── Bounds Calculation ──────────────────────────────────────────────────────
getBounds(): Rect[] {
return this._orientation === Orientation.HORIZONTAL
? this._computeBounds('horizontal')
: this._computeBounds('vertical');
if (this._orientation === Orientation.HORIZONTAL) {
return this.getHorizontalBounds();
}
return this.getVerticalBounds();
}
private _computeBounds(axis: 'horizontal' | 'vertical'): Rect[] {
const isHorizontal = axis === 'horizontal';
const total = isHorizontal ? this._workArea.width : this._workArea.height;
let used = 0;
getVerticalBounds(): Rect[] {
// Filter out dividers to get only windows/containers
const nonDividerItems = this._tiledItems.filter(item => !Divider.isDivider(item));
return this._tiledItems.map((_, index) => {
const offset = used;
const size = index === this._tiledItems.length - 1
? total - used
: Math.floor(this._splitRatios[index] * total);
used += size;
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;
if (nonDividerItems.length === 0) {
return [];
}
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;
// If no dividers, use equal distribution
const hasDividers = this._tiledItems.some(item => Divider.isDivider(item));
if (!hasDividers) {
const containerHeight = Math.floor(this._workArea.height / nonDividerItems.length);
return nonDividerItems.map((_, index) => {
const y = this._workArea.y + (index * containerHeight);
return {
x: this._workArea.x,
y: y,
width: this._workArea.width,
height: containerHeight
} as Rect;
});
}
this._splitRatios[boundaryIndex] = newLeft;
this._splitRatios[boundaryIndex + 1] = newRight;
// Calculate bounds based on divider positions
const bounds: Rect[] = [];
let currentY = this._workArea.y;
let itemIndex = 0;
Logger.info(`adjustBoundary: boundary=${boundaryIndex} ratios=[${this._splitRatios.map(r => r.toFixed(3)).join(', ')}]`);
return true;
}
for (let i = 0; i < this._tiledItems.length; i++) {
const item = this._tiledItems[i];
// ─── Container Lookup ────────────────────────────────────────────────────────
if (Divider.isDivider(item)) {
// Next segment starts at divider position
currentY = this._workArea.y + Math.floor(item.getPosition() * this._workArea.height);
} else {
// Find the end position for this item
let endY: number = this._workArea.y + this._workArea.height;
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;
// Look ahead to find next divider or end of container
for (let j = i + 1; j < this._tiledItems.length; j++) {
if (Divider.isDivider(this._tiledItems[j])) {
const divider = this._tiledItems[j] as Divider;
endY = this._workArea.y + Math.floor(divider.getPosition() * this._workArea.height);
break;
}
}
// Count non-divider items until next divider
let itemCount = 0;
let itemsInSegment: number[] = [];
for (let j = i; j < this._tiledItems.length; j++) {
if (Divider.isDivider(this._tiledItems[j])) {
break;
}
itemsInSegment.push(j);
itemCount++;
}
// Divide space equally among items in this segment
const segmentHeight = endY - currentY;
const itemHeight = Math.floor(segmentHeight / itemCount);
for (let k = 0; k < itemsInSegment.length; k++) {
const itemY = currentY + (k * itemHeight);
bounds.push({
x: this._workArea.x,
y: itemY,
width: this._workArea.width,
height: itemHeight
} as Rect);
}
// Skip the items we just processed
i += itemCount - 1;
currentY = endY;
}
}
return null;
return bounds;
}
getHorizontalBounds(): Rect[] {
// Filter out dividers to get only windows/containers
const nonDividerItems = this._tiledItems.filter(item => !Divider.isDivider(item));
if (nonDividerItems.length === 0) {
return [];
}
// If no dividers, use equal distribution
const hasDividers = this._tiledItems.some(item => Divider.isDivider(item));
if (!hasDividers) {
const windowWidth = Math.floor(this._workArea.width / nonDividerItems.length);
return nonDividerItems.map((_, index) => {
const x = this._workArea.x + (index * windowWidth);
return {
x: x,
y: this._workArea.y,
width: windowWidth,
height: this._workArea.height
} as Rect;
});
}
// Calculate bounds based on divider positions
const bounds: Rect[] = [];
let currentX = this._workArea.x;
for (let i = 0; i < this._tiledItems.length; i++) {
const item = this._tiledItems[i];
if (Divider.isDivider(item)) {
// Next segment starts at divider position
currentX = this._workArea.x + Math.floor(item.getPosition() * this._workArea.width);
} else {
// Find the end position for this item
let endX: number = this._workArea.x + this._workArea.width;
// Look ahead to find next divider or end of container
for (let j = i + 1; j < this._tiledItems.length; j++) {
if (Divider.isDivider(this._tiledItems[j])) {
const divider = this._tiledItems[j] as Divider;
endX = this._workArea.x + Math.floor(divider.getPosition() * this._workArea.width);
break;
}
}
// Count non-divider items until next divider
let itemCount = 0;
let itemsInSegment: number[] = [];
for (let j = i; j < this._tiledItems.length; j++) {
if (Divider.isDivider(this._tiledItems[j])) {
break;
}
itemsInSegment.push(j);
itemCount++;
}
// Divide space equally among items in this segment
const segmentWidth = endX - currentX;
const itemWidth = Math.floor(segmentWidth / itemCount);
for (let k = 0; k < itemsInSegment.length; k++) {
const itemX = currentX + (k * itemWidth);
bounds.push({
x: itemX,
y: this._workArea.y,
width: itemWidth,
height: this._workArea.height
} as Rect);
}
// Skip the items we just processed
i += itemCount - 1;
currentX = endX;
}
}
return bounds;
}
getIndexOfItemNested(item: WindowWrapper): number {
for (let i = 0; i < this._tiledItems.length; i++) {
const container = this._tiledItems[i];
if (container instanceof WindowContainer) {
if (container.getIndexOfItemNested(item) !== -1) return i;
} else if (container.getWindowId() === item.getWindowId()) {
if (Divider.isDivider(container)) {
continue; // Skip dividers
} else if (container instanceof WindowContainer) {
const index = container.getIndexOfItemNested(item);
if (index !== -1) {
return i;
}
} else if (container instanceof WindowWrapper && container.getWindowId() === item.getWindowId()) {
return i;
}
}
@@ -248,30 +362,272 @@ export default class WindowContainer {
// TODO: update this to work with nested containers - all other logic should already be working
itemDragged(item: WindowWrapper, x: number, y: number): void {
const original_index = this.getIndexOfItemNested(item);
// Find the actual index in _tiledItems (including dividers)
const original_actual_index = this._getIndexOfWindow(item.getWindowId());
if (original_index === -1) {
if (original_actual_index === -1) {
Logger.error("Item not found in container during drag op", item.getWindowId());
return;
}
let new_index = original_index;
this.getBounds().forEach((rect, index) => {
// Find which visual slot (non-divider index) we're moving to
let new_visual_index = this.getIndexOfItemNested(item);
const bounds = this.getBounds();
bounds.forEach((rect, index) => {
if (rect.x < x && rect.x + rect.width > x && rect.y < y && rect.y + rect.height > y) {
new_index = index;
new_visual_index = index;
}
});
})
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();
// Get current visual index (counting only non-dividers before this item)
let original_visual_index = 0;
for (let i = 0; i < original_actual_index; i++) {
if (!Divider.isDivider(this._tiledItems[i])) {
original_visual_index++;
}
}
}
resetRatios(): void {
this._resetRatios();
if (original_visual_index === new_visual_index) {
return; // No movement needed
}
Logger.log(`Swapping window from visual index ${original_visual_index} to ${new_visual_index}`);
// Find the target window at the new visual index
let target_actual_index = -1;
let visual_count = 0;
for (let i = 0; i < this._tiledItems.length; i++) {
if (!Divider.isDivider(this._tiledItems[i])) {
if (visual_count === new_visual_index) {
target_actual_index = i;
break;
}
visual_count++;
}
}
if (target_actual_index === -1) {
Logger.warn("Could not find target position for drag");
return;
}
// Simply swap the two windows in place, leaving dividers where they are
const temp = this._tiledItems[original_actual_index];
this._tiledItems[original_actual_index] = this._tiledItems[target_actual_index];
this._tiledItems[target_actual_index] = temp;
this.tileWindows();
}
}
/**
* Handles window resize operations. Creates or updates dividers based on resize direction.
* @param item - The window being resized
* @param resizeEdge - The edge being resized (N, S, E, W, etc.)
* @param newRect - The new rectangle after resize
*/
handleWindowResize(item: WindowWrapper, resizeEdge: Meta.GrabOp, newRect: Rect): void {
const itemIndex = this._getIndexOfWindow(item.getWindowId());
if (itemIndex === -1) {
Logger.warn("Window not found in container during resize", item.getWindowId());
return;
}
// Determine if this is a valid resize for this container orientation
const isHorizontalResize = this._isHorizontalResizeOp(resizeEdge);
const isVerticalResize = this._isVerticalResizeOp(resizeEdge);
// Only allow horizontal resizes in horizontal containers
// Only allow vertical resizes in vertical containers
if (this._orientation === Orientation.HORIZONTAL && !isHorizontalResize) {
Logger.log("Ignoring vertical resize in horizontal container");
return;
}
if (this._orientation === Orientation.VERTICAL && !isVerticalResize) {
Logger.log("Ignoring horizontal resize in vertical container");
return;
}
// Determine which edge is being resized and find adjacent window
let adjacentIndex = -1;
let dividerPosition = 0;
if (this._orientation === Orientation.HORIZONTAL) {
// East/West resize
if (this._isEastResizeOp(resizeEdge)) {
// Resizing east edge - divider goes after this window
adjacentIndex = itemIndex + 1;
// Calculate divider position as ratio of container width
const rightEdge = newRect.x + newRect.width;
dividerPosition = (rightEdge - this._workArea.x) / this._workArea.width;
} else if (this._isWestResizeOp(resizeEdge)) {
// Resizing west edge - divider goes before this window
adjacentIndex = itemIndex - 1;
dividerPosition = (newRect.x - this._workArea.x) / this._workArea.width;
}
} else {
// Vertical orientation - North/South resize
if (this._isSouthResizeOp(resizeEdge)) {
// Resizing south edge - divider goes after this window
adjacentIndex = itemIndex + 1;
const bottomEdge = newRect.y + newRect.height;
dividerPosition = (bottomEdge - this._workArea.y) / this._workArea.height;
} else if (this._isNorthResizeOp(resizeEdge)) {
// Resizing north edge - divider goes before this window
adjacentIndex = itemIndex - 1;
dividerPosition = (newRect.y - this._workArea.y) / this._workArea.height;
}
}
// Make sure there's an adjacent item
if (adjacentIndex < 0 || adjacentIndex >= this._tiledItems.length) {
Logger.log("No adjacent window for resize operation");
return;
}
// Skip if adjacent item is already a divider
if (Divider.isDivider(this._tiledItems[adjacentIndex])) {
// Update existing divider
const divider = this._tiledItems[adjacentIndex] as Divider;
divider.setPosition(dividerPosition);
Logger.log(`Updated divider at index ${adjacentIndex} to position ${dividerPosition}`);
} else {
// Insert new divider between items
const dividerIndex = Math.max(itemIndex, adjacentIndex);
const newDivider = new Divider(dividerPosition, this._orientation);
this._tiledItems.splice(dividerIndex, 0, newDivider);
Logger.log(`Inserted new divider at index ${dividerIndex} with position ${dividerPosition}`);
}
this.tileWindows();
}
private _isHorizontalResizeOp(op: Meta.GrabOp): boolean {
return op === Meta.GrabOp.RESIZING_E ||
op === Meta.GrabOp.RESIZING_W ||
op === Meta.GrabOp.RESIZING_NE ||
op === Meta.GrabOp.RESIZING_NW ||
op === Meta.GrabOp.RESIZING_SE ||
op === Meta.GrabOp.RESIZING_SW;
}
private _isVerticalResizeOp(op: Meta.GrabOp): boolean {
return 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;
}
private _isEastResizeOp(op: Meta.GrabOp): boolean {
return op === Meta.GrabOp.RESIZING_E ||
op === Meta.GrabOp.RESIZING_NE ||
op === Meta.GrabOp.RESIZING_SE;
}
private _isWestResizeOp(op: Meta.GrabOp): boolean {
return op === Meta.GrabOp.RESIZING_W ||
op === Meta.GrabOp.RESIZING_NW ||
op === Meta.GrabOp.RESIZING_SW;
}
private _isSouthResizeOp(op: Meta.GrabOp): boolean {
return op === Meta.GrabOp.RESIZING_S ||
op === Meta.GrabOp.RESIZING_SE ||
op === Meta.GrabOp.RESIZING_SW;
}
private _isNorthResizeOp(op: Meta.GrabOp): boolean {
return op === Meta.GrabOp.RESIZING_N ||
op === Meta.GrabOp.RESIZING_NE ||
op === Meta.GrabOp.RESIZING_NW;
}
/**
* Removes all dividers from this container, reverting to equal space distribution
*/
removeAllDividers(): void {
Logger.log("Removing all dividers from container");
this._tiledItems = this._tiledItems.filter(item => !Divider.isDivider(item));
this.tileWindows();
}
/**
* Updates divider position during a live resize operation (or creates if doesn't exist)
* This is called repeatedly during resize for live feedback
*/
updateDividerDuringResize(item: WindowWrapper, resizeEdge: Meta.GrabOp, newRect: Rect): void {
const itemIndex = this._getIndexOfWindow(item.getWindowId());
if (itemIndex === -1) {
return;
}
// Determine if this is a valid resize for this container orientation
const isHorizontalResize = this._isHorizontalResizeOp(resizeEdge);
const isVerticalResize = this._isVerticalResizeOp(resizeEdge);
if (this._orientation === Orientation.HORIZONTAL && !isHorizontalResize) {
return;
}
if (this._orientation === Orientation.VERTICAL && !isVerticalResize) {
return;
}
// Determine which edge is being resized and find adjacent window
let adjacentIndex = -1;
let dividerPosition = 0;
if (this._orientation === Orientation.HORIZONTAL) {
if (this._isEastResizeOp(resizeEdge)) {
adjacentIndex = itemIndex + 1;
const rightEdge = newRect.x + newRect.width;
dividerPosition = (rightEdge - this._workArea.x) / this._workArea.width;
} else if (this._isWestResizeOp(resizeEdge)) {
adjacentIndex = itemIndex - 1;
dividerPosition = (newRect.x - this._workArea.x) / this._workArea.width;
}
} else {
if (this._isSouthResizeOp(resizeEdge)) {
adjacentIndex = itemIndex + 1;
const bottomEdge = newRect.y + newRect.height;
dividerPosition = (bottomEdge - this._workArea.y) / this._workArea.height;
} else if (this._isNorthResizeOp(resizeEdge)) {
adjacentIndex = itemIndex - 1;
dividerPosition = (newRect.y - this._workArea.y) / this._workArea.height;
}
}
// Make sure there's an adjacent item (window or container, not out of bounds)
if (adjacentIndex < 0 || adjacentIndex >= this._tiledItems.length) {
Logger.log(`No adjacent item at index ${adjacentIndex}`);
return;
}
// Determine where divider should be inserted/updated
// For East/South resizes: divider between current (itemIndex) and next (itemIndex+1)
// For West/North resizes: divider between previous (itemIndex-1) and current (itemIndex)
let dividerIndex: number;
if (this._orientation === Orientation.HORIZONTAL) {
dividerIndex = this._isEastResizeOp(resizeEdge) ? itemIndex + 1 : itemIndex;
} else {
dividerIndex = this._isSouthResizeOp(resizeEdge) ? itemIndex + 1 : itemIndex;
}
// Check if there's already a divider at this position
if (dividerIndex < this._tiledItems.length && Divider.isDivider(this._tiledItems[dividerIndex])) {
// Update existing divider
const divider = this._tiledItems[dividerIndex] as Divider;
divider.setPosition(dividerPosition);
} else {
// Insert new divider
const newDivider = new Divider(dividerPosition, this._orientation);
this._tiledItems.splice(dividerIndex, 0, newDivider);
}
// Retile to show live updates
this.tileWindows();
}
}

45
src/wm/divider.ts Normal file
View File

@@ -0,0 +1,45 @@
import {Logger} from "../utils/logger.js";
enum Orientation {
HORIZONTAL = 0,
VERTICAL = 1,
}
/**
* Represents a divider between windows in a container.
* Dividers track the split position as a ratio (0-1) of the container's size.
*/
export class Divider {
private _position: number; // Position as ratio 0-1
private _orientation: Orientation;
/**
* Creates a new divider
* @param position - Position as ratio between 0 and 1
* @param orientation - Orientation of the divider (HORIZONTAL or VERTICAL)
*/
constructor(position: number, orientation: Orientation) {
this._position = Math.max(0, Math.min(1, position)); // Clamp between 0 and 1
this._orientation = orientation;
}
getPosition(): number {
return this._position;
}
setPosition(position: number): void {
this._position = Math.max(0, Math.min(1, position)); // Clamp between 0 and 1
Logger.log(`Divider position updated to ${this._position}`);
}
getOrientation(): Orientation {
return this._orientation;
}
/**
* Check if this is a divider instance
*/
static isDivider(item: any): item is Divider {
return item instanceof Divider;
}
}

View File

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

View File

@@ -11,13 +11,12 @@ type WindowMinimizedHandler = (window: WindowWrapper) => void;
type WindowWorkspaceChangedHandler = (window: WindowWrapper) => void;
export class WindowWrapper {
private static readonly RESIZE_TOLERANCE = 2;
readonly _window: Meta.Window;
readonly _windowMinimizedHandler: WindowMinimizedHandler;
readonly _signals: number[] = [];
_parent: WindowContainer | null = null;
_dragging: boolean = false;
_resizing: boolean = false;
constructor(
window: Meta.Window,
@@ -50,26 +49,53 @@ export class WindowWrapper {
startDragging(): void {
this._dragging = true;
}
stopDragging(): void {
Logger.log("STOPPED DRAGGING")
this._dragging = false;
}
connectWindowSignals(windowManager: IWindowManager): void {
const windowId = this._window.get_id();
startResizing(): void {
this._resizing = true;
}
stopResizing(): void {
this._resizing = false;
}
// setParent(parent: WindowContainer): void {
// this._parent = parent;
// }
//
// 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._window.connect('unmanaging', () => {
this._window.connect('unmanaging', window => {
Logger.log("REMOVING WINDOW", windowId);
windowManager.handleWindowClosed(this);
windowManager.handleWindowClosed(this)
}),
this._window.connect('notify::minimized', () => {
this._window.connect('notify::minimized', (we) => {
if (this._window.minimized) {
Logger.log(`Window minimized: ${windowId}`);
windowManager.handleWindowMinimized(this);
} else {
} else if (!this._window.minimized) {
Logger.log(`Window unminimized: ${windowId}`);
windowManager.handleWindowUnminimized(this);
}
}),
this._window.connect('notify::has-focus', () => {
if (this._window.has_focus()) {
windowManager._activeWindowId = windowId;
}
}),
this._window.connect('notify::maximized-horizontally', () => {
@@ -79,20 +105,18 @@ export class WindowWrapper {
Logger.log(`Window unmaximized: ${windowId}`);
}
}),
this._window.connect("workspace-changed", () => {
this._window.connect("workspace-changed", (_metaWindow) => {
Logger.log("WORKSPACE CHANGED FOR WINDOW", this._window.get_id());
windowManager.handleWindowChangedWorkspace(this);
}),
this._window.connect("position-changed", () => {
windowManager.handleWindowPositionChanged(this);
}),
this._window.connect("size-changed", () => {
this._window.connect("position-changed", (_metaWindow) => {
windowManager.handleWindowPositionChanged(this);
}),
);
}
disconnectWindowSignals(): void {
if (this._signals) {
this._signals.forEach(signal => {
try {
@@ -106,36 +130,43 @@ export class WindowWrapper {
}
}
safelyResizeWindow(rect: Rect, _retry: number = 3): void {
if (this._dragging) {
Logger.info("STOPPED RESIZE BECAUSE ITEM IS BEING DRAGGED");
safelyResizeWindow(rect: Rect, _retry: number = 2): void {
// Keep minimal logging
if (this._dragging && !this._resizing) {
// During drag operations (not resize), skip this entirely
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();
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 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;
// Don't retry during live resize operations - it causes spam and isn't needed
if (this._resizing) {
return;
}
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()}`,
callback: () => this.safelyResizeWindow(rect, _retry - 1),
}, 50);
name: "attempting_delayed_resize",
callback: () => {
this.safelyResizeWindow(rect, _retry-1);
}
})
}
}
}

View File

@@ -1,17 +1,19 @@
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 * as Main from "resource:///org/gnome/shell/ui/main.js";
// import Mtk from "@girs/mtk-16";
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 {
_activeWindowId: number | null;
// addWindow(window: Meta.Window): void;
handleWindowClosed(winWrap: WindowWrapper): void;
handleWindowMinimized(winWrap: WindowWrapper): void;
@@ -26,8 +28,8 @@ export interface IWindowManager {
}
const _UNUSED_MONITOR_ID = -1;
const _UNUSED_WINDOW_ID = -1;
const _UNUSED_MONITOR_ID = -1
const _UNUSED_WINDOW_ID = -1
export default class WindowManager implements IWindowManager {
_displaySignals: number[] = [];
@@ -37,29 +39,24 @@ export default class WindowManager implements IWindowManager {
_activeWindowId: number | null = null;
_monitors: Map<number, Monitor> = new Map<number, Monitor>();
_minimizedItems: Map<number, WindowWrapper> = new Map<number, WindowWrapper>();
_grabbedWindowMonitor: number = _UNUSED_MONITOR_ID;
_grabbedWindowId: number = _UNUSED_WINDOW_ID;
_grabbedOp: Meta.GrabOp | null = null;
_changingGrabbedMonitor: boolean = false;
_showingOverview: boolean = false;
// ── 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;
constructor() {
private readonly _settings: Gio.Settings;
constructor(settings: Gio.Settings) {
this._settings = settings;
}
public enable(): void {
Logger.log("Starting Aerospike Window Manager");
// Connect window signals
this.instantiateDisplaySignals();
const mon_count = global.display.get_n_monitors();
@@ -68,7 +65,6 @@ export default class WindowManager implements IWindowManager {
}
this.captureExistingWindows();
this.syncActiveWindow();
}
instantiateDisplaySignals(): void {
@@ -91,9 +87,7 @@ export default class WindowManager implements IWindowManager {
global.display.connect('window-created', (display, window) => {
this.handleWindowCreated(display, window);
}),
global.display.connect('notify::focus-window', () => {
this.syncActiveWindow();
}),
global.display.connect("showing-desktop-changed", () => {
Logger.log("SHOWING DESKTOP CHANGED");
}),
@@ -104,7 +98,13 @@ export default class WindowManager implements IWindowManager {
global.display.connect("in-fullscreen-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 = [
global.workspace_manager.connect("showing-desktop-changed", () => {
@@ -129,31 +129,42 @@ export default class WindowManager implements IWindowManager {
this._overviewSignals = [
Main.overview.connect("hiding", () => {
// this.fromOverview = true;
Logger.log("HIDING OVERVIEW")
this._showingOverview = false;
this._tileMonitors();
// const eventObj = {
// name: "focus-after-overview",
// callback: () => {
// Logger.log("FOCUSING AFTER OVERVIEW");
// },
// };
// this.queueEvent(eventObj);
}),
Main.overview.connect("showing", () => {
this._showingOverview = true;
Logger.log("SHOWING OVERVIEW");
}),
];
}
public disable(): void {
Logger.log("DISABLED AEROSPIKE WINDOW MANAGER!")
// Disconnect the focus signal and remove any existing borders
this.disconnectSignals();
this.removeAllWindows();
}
removeAllWindows(): void {
this.disconnectMinimizedSignals();
this._minimizedItems.clear();
this._monitors.forEach((monitor: Monitor) => {
monitor.removeAllWindows();
})
this._minimizedItems.clear();
}
disconnectSignals(): void {
this.disconnectDisplaySignals();
this.disconnectMonitorSignals();
@@ -187,67 +198,75 @@ 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 ||
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(display, window, op)
Logger.log(window.get_monitor())
const winWrap = this._getWrappedWindow(window);
if (this._isResizeOp(op)) {
winWrap?.startResizing();
} else {
winWrap?.startDragging();
}
this._grabbedWindowMonitor = window.get_monitor();
this._grabbedWindowId = window.get_id();
this._grabbedOp = op;
}
handleGrabOpEnd(display: Meta.Display, window: Meta.Window, op: Meta.GrabOp): void {
Logger.log("Grab Op End ", op);
Logger.log("primary display", display.get_primary_monitor())
// Handle resize operations
if (this._isResizeOp(op)) {
const winWrap = this._getWrappedWindow(window);
if (winWrap && this._grabbedOp) {
const newRect = window.get_frame_rect();
const monitorId = window.get_monitor();
Logger.log(`Handling resize operation: ${op}, new rect:`, newRect);
this._monitors.get(monitorId)?.handleWindowResize(winWrap, this._grabbedOp, newRect);
winWrap.stopResizing();
}
} else {
this._getWrappedWindow(window)?.stopDragging();
}
this._grabbedWindowId = _UNUSED_WINDOW_ID;
this._grabbedOp = null;
this._tileMonitors();
Logger.info("monitor_start and monitor_end", this._grabbedWindowMonitor, window.get_monitor());
}
private _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 (this._isResizeOp(op)) {
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;
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 {
Logger.log("Grab Op End ", op);
if (this._isResizeDrag) {
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;
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 {
let wrapped: WindowWrapper | undefined = undefined;
let wrapped = undefined;
for (const monitor of this._monitors.values()) {
wrapped = monitor.getWindow(window.get_id());
if (wrapped !== undefined) break;
if (wrapped !== undefined) {
break;
}
}
return wrapped;
}
_getAndRemoveWrappedWindow(window: Meta.Window): WindowWrapper | undefined {
let wrapped: WindowWrapper | undefined = undefined;
let wrapped = undefined;
for (const monitor of this._monitors.values()) {
wrapped = monitor.getWindow(window.get_id());
if (wrapped !== undefined) {
@@ -262,7 +281,7 @@ export default class WindowManager implements IWindowManager {
let wrapped = this._getAndRemoveWrappedWindow(window);
if (wrapped === undefined) {
Logger.error("WINDOW NOT DEFINED")
wrapped = new WindowWrapper(window, (winWrap) => this.handleWindowMinimized(winWrap));
wrapped = new WindowWrapper(window, this.handleWindowMinimized);
wrapped.connectWindowSignals(this);
}
let new_mon = this._monitors.get(monitorId);
@@ -271,10 +290,16 @@ export default class WindowManager implements IWindowManager {
}
public handleWindowPositionChanged(winWrap: WindowWrapper): void {
if (this._isTiling || this._changingGrabbedMonitor) return;
if (this._changingGrabbedMonitor) {
return;
}
if (this._isResizeDrag && winWrap.getWindowId() === this._resizeDragWindowId) {
this._handleResizeDragUpdate(winWrap);
// Handle resize operations - update dividers in real-time
if (this._grabbedOp && this._isResizeOp(this._grabbedOp)) {
const window = winWrap.getWindow();
const newRect = window.get_frame_rect();
const monitorId = window.get_monitor();
this._monitors.get(monitorId)?.updateDividerDuringResize(winWrap, this._grabbedOp, newRect);
return;
}
@@ -290,82 +315,19 @@ export default class WindowManager implements IWindowManager {
break;
}
}
if (monitorIndex === -1) return;
if (monitorIndex === -1) {
return
}
if (monitorIndex !== this._grabbedWindowMonitor) {
this._changingGrabbedMonitor = true;
this._moveWindowToMonitor(winWrap.getWindow(), monitorIndex);
this._changingGrabbedMonitor = false;
}
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);
}
}
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 {
const monitor_id = winWrap.getWindow().get_monitor()
@@ -380,6 +342,7 @@ export default class WindowManager implements IWindowManager {
this._tileMonitors()
}
public handleWindowChangedWorkspace(winWrap: WindowWrapper): void {
const monitor = winWrap.getWindow().get_monitor();
this._monitors.get(monitor)?.removeWindow(winWrap);
@@ -398,26 +361,41 @@ export default class WindowManager implements IWindowManager {
this._tileMonitors();
}
handleWindowCreated(display: Meta.Display, window: Meta.Window) {
Logger.log("WINDOW CREATED ON DISPLAY", window, display);
if (!this._isWindowTileable(window)) return;
if (!this._isWindowTileable(window)) {
return;
}
Logger.log("WINDOW IS TILABLE");
this.addWindowToMonitor(window);
}
/**
* Handle window closed event
*/
handleWindowClosed(window: WindowWrapper): void {
const mon_id = window._window.get_monitor();
this._monitors.get(mon_id)?.removeWindow(window);
window.disconnectWindowSignals()
// Remove from managed windows
this.syncActiveWindow();
// Retile remaining windows
this._tileMonitors();
}
public addWindowToMonitor(window: Meta.Window) {
Logger.log("ADDING WINDOW TO MONITOR", window, window);
var wrapper = new WindowWrapper(window, (winWrap) => this.handleWindowMinimized(winWrap))
var wrapper = new WindowWrapper(window, this.handleWindowMinimized)
wrapper.connectWindowSignals(this);
this._addWindowWrapperToMonitor(wrapper);
}
_addWindowWrapperToMonitor(winWrap: WindowWrapper) {
@@ -429,13 +407,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()
}
}
@@ -443,7 +417,7 @@ export default class WindowManager implements IWindowManager {
"org.gnome.Shell.Extensions",
]
_isWindowTilingBlocked(window: Meta.Window): boolean {
_isWindowTilingBlocked(window: Meta.Window) : boolean {
Logger.info("title", window.get_title());
Logger.info("description", window.get_description());
Logger.info("class", window.get_wm_class());
@@ -458,12 +432,17 @@ export default class WindowManager implements IWindowManager {
}
_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();
Logger.log("WINDOW TILING CHECK",);
// Skip certain types of windows
return !window.is_skip_taskbar() &&
windowType !== Meta.WindowType.DESKTOP &&
windowType !== Meta.WindowType.DOCK &&
@@ -473,92 +452,38 @@ export default class WindowManager implements IWindowManager {
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 {
const focusWindow = global.display.focus_window;
if (focusWindow) {
this._activeWindowId = focusWindow.get_id();
Logger.debug(`Active window changed to: ${this._activeWindowId} (${focusWindow.get_title()})`);
} else {
this._activeWindowId = null;
Logger.debug('No active window');
}
return this._activeWindowId;
return null;
}
public toggleActiveContainerOrientation(): void {
if (this._activeWindowId === null) {
Logger.warn("No active window, cannot toggle container orientation");
/**
* Removes all dividers from the container with the currently active window
*/
public removeAllDividersFromActiveContainer(): void {
const activeWindow = global.display.focus_window;
if (!activeWindow) {
Logger.log("No active window, cannot remove dividers");
return;
}
const container = this._findContainerForWindowAcrossMonitors(this._activeWindowId);
if (container) {
container.toggleOrientation();
const monitorId = activeWindow.get_monitor();
const monitor = this._monitors.get(monitorId);
if (monitor) {
Logger.log(`Removing all dividers from monitor ${monitorId}`);
monitor.removeAllDividersFromActiveContainer();
} else {
Logger.warn("Could not find container for active window");
Logger.warn(`Monitor ${monitorId} not found`);
}
}
public resetActiveContainerRatios(): void {
if (this._activeWindowId === null) {
Logger.warn("No active window, cannot reset container ratios");
return;
}
const container = this._findContainerForWindowAcrossMonitors(this._activeWindowId);
if (container) {
Logger.info("Resetting container ratios to equal splits");
container.resetRatios();
} else {
Logger.warn("Could not find container for active window");
}
}
public printTreeStructure(): void {
Logger.info("=".repeat(80));
Logger.info("WINDOW TREE STRUCTURE");
Logger.info("=".repeat(80));
Logger.info(`Active Window ID: ${this._activeWindowId ?? 'none'}`);
Logger.info("=".repeat(80));
const activeWorkspaceIndex = global.workspace_manager.get_active_workspace().index();
this._monitors.forEach((monitor: Monitor, monitorId: number) => {
const isActiveMonitor = this._activeWindowId !== null &&
monitor.getWindow(this._activeWindowId) !== undefined;
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}`);
monitor._workspaces.forEach((workspace, workspaceIndex) => {
const isActiveWorkspace = workspaceIndex === activeWorkspaceIndex;
Logger.info(` Workspace ${workspaceIndex}${isActiveWorkspace && isActiveMonitor ? ' *' : ''}:`);
Logger.info(` Orientation: ${workspace._orientation === 0 ? 'HORIZONTAL' : 'VERTICAL'}`);
Logger.info(` Items: ${workspace._tiledItems.length}`);
this._printContainerTree(workspace, 4);
});
});
Logger.info("=".repeat(80));
}
private _printContainerTree(container: WindowContainer, indentLevel: number): void {
const indent = " ".repeat(indentLevel);
container._tiledItems.forEach((item, index) => {
if (item instanceof WindowContainer) {
const containsActive = this._activeWindowId !== null &&
item.getWindow(this._activeWindowId) !== undefined;
Logger.info(`${indent}[${index}] Container (${item._orientation === 0 ? 'HORIZONTAL' : 'VERTICAL'})${containsActive ? ' *' : ''}:`);
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}`);
this._printContainerTree(item, indentLevel + 4);
} else {
const window = item.getWindow();
Logger.info(`${indent}[${index}] Window ID: ${item.getWindowId()}${this._activeWindowId === item.getWindowId() ? ' *' : ''}`);
Logger.info(`${indent} Title: "${window.get_title()}"`);
Logger.info(`${indent} Class: ${window.get_wm_class()}`);
const rect = item.getRect();
Logger.info(`${indent} Rect: x=${rect.x}, y=${rect.y}, w=${rect.width}, h=${rect.height}`);
}
});
}
}

View File

@@ -5,8 +5,6 @@
"outDir": "./dist",
"sourceMap": false,
"strict": true,
"noImplicitAny": false,
"skipLibCheck": true,
"target": "ES2022",
"lib": [
"ES2022"