3 Commits

Author SHA1 Message Date
Lucas Oskorep
696269d8b8 feat: adding support for multi-monitor window moving
Some checks failed
Build and Test / release (push) Has been cancelled
Build and Test / build (push) Has been cancelled
Build and Test / build (pull_request) Successful in 24s
Build and Test / release (pull_request) Has been skipped
2026-03-02 23:06:47 -05:00
Lucas Oskorep
fa021b08eb feat: adding active window selection and refactoring keybindings 2026-03-02 18:46:26 -05:00
Lucas Oskorep
8ed5f104b2 fix: when new items enter the tabbed view they should be the active window
All checks were successful
Build and Test / build (push) Successful in 25s
Build and Test / release (push) Successful in 3s
2026-03-02 11:26:28 -05:00
7 changed files with 625 additions and 268 deletions

View File

@@ -4,6 +4,7 @@ import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import Gio from 'gi://Gio';
import Shell from 'gi://Shell';
import WindowManager from './src/wm/windowManager.js'
import {Direction} from './src/wm/container.js'
import {Logger} from "./src/utils/logger.js";
export default class aerospike extends Extension {
@@ -37,14 +38,18 @@ export default class aerospike extends Extension {
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(); },
'toggle-tabbed': () => { this.windowManager.toggleActiveContainerTabbed(); },
'focus-left': () => { this.windowManager.focusInDirection(Direction.LEFT); },
'focus-right': () => { this.windowManager.focusInDirection(Direction.RIGHT); },
'focus-up': () => { this.windowManager.focusInDirection(Direction.UP); },
'focus-down': () => { this.windowManager.focusInDirection(Direction.DOWN); },
'move-left': () => { this.windowManager.moveInDirection(Direction.LEFT); },
'move-right': () => { this.windowManager.moveInDirection(Direction.RIGHT); },
'move-up': () => { this.windowManager.moveInDirection(Direction.UP); },
'move-down': () => { this.windowManager.moveInDirection(Direction.DOWN); },
};
}
@@ -56,14 +61,6 @@ export default class aerospike extends Extension {
this.refreshKeybinding(name);
});
});
this.settings.connect('changed::dropdown-option', () => {
log(`Dropdown option changed to: ${this.settings.get_string('dropdown-option')}`);
});
this.settings.connect('changed::color-selection', () => {
log(`Color selection changed to: ${this.settings.get_string('color-selection')}`);
});
}
private refreshKeybinding(settingName: string) {

156
pnpm-lock.yaml generated
View File

@@ -44,10 +44,10 @@ importers:
version: 30.0.0
eslint:
specifier: ^10.0.0
version: 10.0.2
version: 10.0.0
eslint-plugin-jsdoc:
specifier: ^62.0.0
version: 62.4.1(eslint@10.0.2)
version: 62.4.1(eslint@10.0.0)
jest:
specifier: ^30.0.0
version: 30.2.0(@types/node@25.1.0)
@@ -252,8 +252,8 @@ packages:
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
'@eslint/config-array@0.23.2':
resolution: {integrity: sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==}
'@eslint/config-array@0.23.0':
resolution: {integrity: sha512-T5Swqd+PZxBekRuMsIFCySM3NUE8GjuqyksIIsXgkF2GCuiDaqpxKyPkv9VMEKpq5D7r5DLss1tM8tCsvRSjeg==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/config-helpers@0.5.2':
@@ -264,8 +264,8 @@ packages:
resolution: {integrity: sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/object-schema@3.0.2':
resolution: {integrity: sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==}
'@eslint/object-schema@3.0.0':
resolution: {integrity: sha512-nWl20RtHQP2A2yvKU6Fee62Xo1AoNRqBLixtyg45zJhU8ljPFCyBK90d8e8XTnPns1RggSf4HH3bM8AhevkPVg==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/plugin-kit@0.6.0':
@@ -417,6 +417,14 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
'@isaacs/balanced-match@4.0.1':
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
engines: {node: 20 || >=22}
'@isaacs/brace-expansion@5.0.1':
resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==}
engines: {node: 20 || >=22}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@@ -714,13 +722,13 @@ packages:
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
acorn@8.16.0:
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
acorn@8.15.0:
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
engines: {node: '>=0.4.0'}
hasBin: true
ajv@6.14.0:
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
ansi-escapes@4.3.2:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
@@ -785,10 +793,6 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
balanced-match@4.0.4:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
baseline-browser-mapping@2.9.19:
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
hasBin: true
@@ -799,10 +803,6 @@ packages:
brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
brace-expansion@5.0.4:
resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==}
engines: {node: 18 || 20 || >=22}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
@@ -949,20 +949,20 @@ packages:
peerDependencies:
eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
eslint-scope@9.1.1:
resolution: {integrity: sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==}
eslint-scope@9.1.0:
resolution: {integrity: sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint-visitor-keys@3.4.3:
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
eslint-visitor-keys@5.0.1:
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
eslint-visitor-keys@5.0.0:
resolution: {integrity: sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint@10.0.2:
resolution: {integrity: sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==}
eslint@10.0.0:
resolution: {integrity: sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
hasBin: true
peerDependencies:
@@ -975,10 +975,6 @@ packages:
resolution: {integrity: sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
espree@11.1.1:
resolution: {integrity: sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
esprima@4.0.1:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
engines: {node: '>=4'}
@@ -1044,8 +1040,8 @@ packages:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'}
flatted@3.3.4:
resolution: {integrity: sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==}
flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
@@ -1396,15 +1392,15 @@ packages:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
minimatch@10.2.4:
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
engines: {node: 18 || 20 || >=22}
minimatch@10.1.2:
resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==}
engines: {node: 20 || >=22}
minimatch@3.1.5:
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
minimatch@9.0.9:
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
minimist@1.2.8:
@@ -2009,18 +2005,18 @@ snapshots:
'@es-joy/resolve.exports@1.2.0': {}
'@eslint-community/eslint-utils@4.9.1(eslint@10.0.2)':
'@eslint-community/eslint-utils@4.9.1(eslint@10.0.0)':
dependencies:
eslint: 10.0.2
eslint: 10.0.0
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
'@eslint/config-array@0.23.2':
'@eslint/config-array@0.23.0':
dependencies:
'@eslint/object-schema': 3.0.2
'@eslint/object-schema': 3.0.0
debug: 4.4.3
minimatch: 10.2.4
minimatch: 10.1.2
transitivePeerDependencies:
- supports-color
@@ -2032,7 +2028,7 @@ snapshots:
dependencies:
'@types/json-schema': 7.0.15
'@eslint/object-schema@3.0.2': {}
'@eslint/object-schema@3.0.0': {}
'@eslint/plugin-kit@0.6.0':
dependencies:
@@ -2503,6 +2499,12 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
'@isaacs/balanced-match@4.0.1': {}
'@isaacs/brace-expansion@5.0.1':
dependencies:
'@isaacs/balanced-match': 4.0.1
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@@ -2866,13 +2868,13 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
acorn-jsx@5.3.2(acorn@8.16.0):
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
acorn: 8.16.0
acorn: 8.15.0
acorn@8.16.0: {}
acorn@8.15.0: {}
ajv@6.14.0:
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
fast-json-stable-stringify: 2.1.0
@@ -2960,8 +2962,6 @@ snapshots:
balanced-match@1.0.2: {}
balanced-match@4.0.4: {}
baseline-browser-mapping@2.9.19: {}
brace-expansion@1.1.12:
@@ -2973,10 +2973,6 @@ snapshots:
dependencies:
balanced-match: 1.0.2
brace-expansion@5.0.4:
dependencies:
balanced-match: 4.0.4
braces@3.0.3:
dependencies:
fill-range: 7.1.1
@@ -3078,7 +3074,7 @@ snapshots:
escape-string-regexp@4.0.0: {}
eslint-plugin-jsdoc@62.4.1(eslint@10.0.2):
eslint-plugin-jsdoc@62.4.1(eslint@10.0.0):
dependencies:
'@es-joy/jsdoccomment': 0.83.0
'@es-joy/resolve.exports': 1.2.0
@@ -3086,7 +3082,7 @@ snapshots:
comment-parser: 1.4.5
debug: 4.4.3
escape-string-regexp: 4.0.0
eslint: 10.0.2
eslint: 10.0.0
espree: 11.1.0
esquery: 1.7.0
html-entities: 2.6.0
@@ -3098,7 +3094,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-scope@9.1.1:
eslint-scope@9.1.0:
dependencies:
'@types/esrecurse': 4.3.1
'@types/estree': 1.0.8
@@ -3107,13 +3103,13 @@ snapshots:
eslint-visitor-keys@3.4.3: {}
eslint-visitor-keys@5.0.1: {}
eslint-visitor-keys@5.0.0: {}
eslint@10.0.2:
eslint@10.0.0:
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.0.2)
'@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0)
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.23.2
'@eslint/config-array': 0.23.0
'@eslint/config-helpers': 0.5.2
'@eslint/core': 1.1.0
'@eslint/plugin-kit': 0.6.0
@@ -3121,13 +3117,13 @@ snapshots:
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3
'@types/estree': 1.0.8
ajv: 6.14.0
ajv: 6.12.6
cross-spawn: 7.0.6
debug: 4.4.3
escape-string-regexp: 4.0.0
eslint-scope: 9.1.1
eslint-visitor-keys: 5.0.1
espree: 11.1.1
eslint-scope: 9.1.0
eslint-visitor-keys: 5.0.0
espree: 11.1.0
esquery: 1.7.0
esutils: 2.0.3
fast-deep-equal: 3.1.3
@@ -3138,7 +3134,7 @@ snapshots:
imurmurhash: 0.1.4
is-glob: 4.0.3
json-stable-stringify-without-jsonify: 1.0.1
minimatch: 10.2.4
minimatch: 10.1.2
natural-compare: 1.4.0
optionator: 0.9.4
transitivePeerDependencies:
@@ -3146,15 +3142,9 @@ snapshots:
espree@11.1.0:
dependencies:
acorn: 8.16.0
acorn-jsx: 5.3.2(acorn@8.16.0)
eslint-visitor-keys: 5.0.1
espree@11.1.1:
dependencies:
acorn: 8.16.0
acorn-jsx: 5.3.2(acorn@8.16.0)
eslint-visitor-keys: 5.0.1
acorn: 8.15.0
acorn-jsx: 5.3.2(acorn@8.15.0)
eslint-visitor-keys: 5.0.0
esprima@4.0.1: {}
@@ -3223,10 +3213,10 @@ snapshots:
flat-cache@4.0.1:
dependencies:
flatted: 3.3.4
flatted: 3.3.3
keyv: 4.5.4
flatted@3.3.4: {}
flatted@3.3.3: {}
foreground-child@3.3.1:
dependencies:
@@ -3254,7 +3244,7 @@ snapshots:
dependencies:
foreground-child: 3.3.1
jackspeak: 3.4.3
minimatch: 9.0.9
minimatch: 9.0.5
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
@@ -3264,7 +3254,7 @@ snapshots:
fs.realpath: 1.0.0
inflight: 1.0.6
inherits: 2.0.4
minimatch: 3.1.5
minimatch: 3.1.2
once: 1.4.0
path-is-absolute: 1.0.1
@@ -3738,15 +3728,15 @@ snapshots:
mimic-fn@2.1.0: {}
minimatch@10.2.4:
minimatch@10.1.2:
dependencies:
brace-expansion: 5.0.4
'@isaacs/brace-expansion': 5.0.1
minimatch@3.1.5:
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.12
minimatch@9.0.9:
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.2
@@ -3956,7 +3946,7 @@ snapshots:
dependencies:
'@istanbuljs/schema': 0.1.3
glob: 7.2.3
minimatch: 3.1.5
minimatch: 3.1.2
tmpl@1.0.5: {}

View File

@@ -1,40 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<schema id="org.gnome.shell.extensions.aerospike" path="/org/gnome/shell/extensions/aerospike/">
<key name="dropdown-option" type="s">
<default>'option1'</default>
<summary>Dropdown selection</summary>
<description>Option selected from the dropdown menu</description>
<key name="focus-left" type="as">
<default><![CDATA[['<Super>h']]]></default>
<summary>Focus window to the left</summary>
<description>Move focus to the window to the left of the current window. In tabbed mode, switches to the previous tab.</description>
</key>
<key name="color-selection" type="s">
<default>'rgb(255,0,0)'</default>
<summary>Selected color</summary>
<description>Color chosen from the color picker</description>
<key name="focus-right" type="as">
<default><![CDATA[['<Super>l']]]></default>
<summary>Focus window to the right</summary>
<description>Move focus to the window to the right of the current window. In tabbed mode, switches to the next tab.</description>
</key>
<key name="focus-up" type="as">
<default><![CDATA[['<Super>k']]]></default>
<summary>Focus window above</summary>
<description>Move focus to the window above the current window.</description>
</key>
<key name="focus-down" type="as">
<default><![CDATA[['<Super>j']]]></default>
<summary>Focus window below</summary>
<description>Move focus to the window below the current window.</description>
</key>
<key name="move-left" type="as">
<default><![CDATA[['<Super>1']]]></default>
<summary>Keybinding for action 1</summary>
<description>Keyboard shortcut for triggering action 1</description>
<default><![CDATA[['<Super><Shift>h']]]></default>
<summary>Move window to the left</summary>
<description>Move the active window one position to the left within its container</description>
</key>
<key name="move-right" type="as">
<default><![CDATA[['<Super>2']]]></default>
<summary>Keybinding for action 2</summary>
<description>Keyboard shortcut for triggering action 2</description>
<default><![CDATA[['<Super><Shift>l']]]></default>
<summary>Move window to the right</summary>
<description>Move the active window one position to the right within its container</description>
</key>
<key name="join-with-left" type="as">
<default><![CDATA[['<Super>3']]]></default>
<summary>Keybinding for action 3</summary>
<description>Keyboard shortcut for triggering action 3</description>
<key name="move-up" type="as">
<default><![CDATA[['<Super><Shift>k']]]></default>
<summary>Move window up</summary>
<description>Move the active window one position up within its container</description>
</key>
<key name="join-with-right" type="as">
<default><![CDATA[['<Super>4']]]></default>
<summary>Keybinding for action 4</summary>
<description>Keyboard shortcut for triggering action 4</description>
<key name="move-down" type="as">
<default><![CDATA[['<Super><Shift>j']]]></default>
<summary>Move window down</summary>
<description>Move the active window one position down within its container</description>
</key>
<key name="toggle-orientation" type="as">
<default><![CDATA[['<Super>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[['<Super>z']]]></default>
<summary>Reset container ratios to equal splits</summary>
<description>Resets all window size ratios in the active window's container to equal splits</description>
</key>
<key name="toggle-tabbed" type="as">
<default><![CDATA[['<Super>slash']]]></default>
<summary>Toggle tabbed container mode</summary>
<description>Toggles the active window's container between tabbed and accordion layout modes</description>
</key>
<key name="print-tree" type="as">
@@ -43,23 +75,5 @@
<description>Prints the current tree of containers and windows per monitor to logs</description>
</key>
<key name="toggle-orientation" type="as">
<default><![CDATA[['<Primary>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>
<key name="toggle-tabbed" type="as">
<default><![CDATA[['<Primary>slash']]]></default>
<summary>Toggle tabbed container mode</summary>
<description>Toggles the active window's container between tabbed and accordion layout modes</description>
</key>
</schema>
</schemalist>

View File

@@ -2,7 +2,7 @@ import Adw from 'gi://Adw';
import Gio from 'gi://Gio';
import Gtk from 'gi://Gtk';
import Gdk from 'gi://Gdk';
import { ExtensionPreferences, gettext as _ } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
import {Logger} from "../utils/logger.js";
import {EntryRow} from "./keybindings.js";
@@ -11,160 +11,115 @@ export default class AerospikeExtensions extends ExtensionPreferences {
// Create settings object
const settings = this.getSettings('org.gnome.shell.extensions.aerospike');
// Create a preferences page
const page = new Adw.PreferencesPage({
title: _('Settings'),
icon_name: 'preferences-system-symbolic',
// Create keybindings page (top-level)
const keybindingsPage = new Adw.PreferencesPage({
title: _('Keybindings'),
icon_name: 'input-keyboard-symbolic',
});
window.add(page);
window.add(keybindingsPage);
const keybindingMap = this.createKeybindingMap();
// Create options group
const optionsGroup = new Adw.PreferencesGroup({
title: _('Options'),
});
page.add(optionsGroup);
// Add dropdown
const dropdownRow = new Adw.ComboRow({
title: _('Select an option'),
});
optionsGroup.add(dropdownRow);
// Create dropdown model
const dropdownModel = new Gtk.StringList();
dropdownModel.append(_('Option 1'));
dropdownModel.append(_('Option 2'));
dropdownModel.append(_('Option 3'));
dropdownModel.append(_('Option 4'));
dropdownRow.set_model(dropdownModel);
// Set the active option based on settings
const currentOption = settings.get_string('dropdown-option');
switch (currentOption) {
case 'option1':
dropdownRow.set_selected(0);
break;
case 'option2':
dropdownRow.set_selected(1);
break;
case 'option3':
dropdownRow.set_selected(2);
break;
case 'option4':
dropdownRow.set_selected(3);
break;
default:
dropdownRow.set_selected(0);
}
// Connect dropdown change signal
dropdownRow.connect('notify::selected', () => {
const selected = dropdownRow.get_selected();
let optionValue: string;
switch (selected) {
case 0:
optionValue = 'option1';
break;
case 1:
optionValue = 'option2';
break;
case 2:
optionValue = 'option3';
break;
case 3:
optionValue = 'option4';
break;
default:
optionValue = 'option1';
}
settings.set_string('dropdown-option', optionValue);
});
// Add color button
const colorRow = new Adw.ActionRow({
title: _('Choose a color'),
});
optionsGroup.add(colorRow);
const colorButton = new Gtk.ColorButton();
colorRow.add_suffix(colorButton);
colorRow.set_activatable_widget(colorButton);
// Set current color from settings
const colorStr = settings.get_string('color-selection');
const rgba = new Gdk.RGBA();
rgba.parse(colorStr);
colorButton.set_rgba(rgba);
// Connect color button signal
colorButton.connect('color-set', () => {
const color = colorButton.get_rgba().to_string();
settings.set_string('color-selection', color);
});
// Create keybindings group
const keybindingsGroup = new Adw.PreferencesGroup({
title: _('Keyboard Shortcuts'),
// Top-level Keybindings header group with syntax help
const keybindingsHeader = new Adw.PreferencesGroup({
title: _('Keybindings'),
description: `${_("Syntax")}: <Super>h, <Shift>g, <Super><Shift>h
${_("Legend")}: <Super> - ${_("Windows key")}, <Primary> - ${_("Control key")}
${_("Delete text to unset. Press Return key to accept.")}`,
});
page.add(keybindingsGroup);
keybindingsPage.add(keybindingsHeader);
// Add keybinding rows as EntryRows with proper mapping
// Use the helper function to create the map object
const keybindingMap = this.createKeybindingMap();
keybindingsGroup.add(
// --- Focus group ---
const focusGroup = new Adw.PreferencesGroup({
title: _('Focus'),
});
keybindingsPage.add(focusGroup);
focusGroup.add(
new EntryRow({
title: _('Action 1'),
title: _('Focus Left'),
settings: settings,
bind: 'focus-left',
map: keybindingMap
})
);
focusGroup.add(
new EntryRow({
title: _('Focus Right'),
settings: settings,
bind: 'focus-right',
map: keybindingMap
})
);
focusGroup.add(
new EntryRow({
title: _('Focus Up'),
settings: settings,
bind: 'focus-up',
map: keybindingMap
})
);
focusGroup.add(
new EntryRow({
title: _('Focus Down'),
settings: settings,
bind: 'focus-down',
map: keybindingMap
})
);
// --- Move group ---
const moveGroup = new Adw.PreferencesGroup({
title: _('Move'),
});
keybindingsPage.add(moveGroup);
moveGroup.add(
new EntryRow({
title: _('Move Left'),
settings: settings,
bind: 'move-left',
map: keybindingMap
})
);
keybindingsGroup.add(
moveGroup.add(
new EntryRow({
title: _('Action 2'),
title: _('Move Right'),
settings: settings,
bind: 'move-right',
map: keybindingMap
})
);
keybindingsGroup.add(
moveGroup.add(
new EntryRow({
title: _('Action 3'),
title: _('Move Up'),
settings: settings,
bind: 'join-with-left',
map: keybindingMap
})
);
keybindingsGroup.add(
new EntryRow({
title: _('Action 4'),
settings: settings,
bind: 'join-with-right',
bind: 'move-up',
map: keybindingMap
})
);
keybindingsGroup.add(
moveGroup.add(
new EntryRow({
title: _('Print Tree Structure'),
title: _('Move Down'),
settings: settings,
bind: 'print-tree',
bind: 'move-down',
map: keybindingMap
})
);
keybindingsGroup.add(
// --- Container Interactions group ---
const containerGroup = new Adw.PreferencesGroup({
title: _('Container Interactions'),
});
keybindingsPage.add(containerGroup);
containerGroup.add(
new EntryRow({
title: _('Toggle Orientation'),
settings: settings,
@@ -173,7 +128,7 @@ export default class AerospikeExtensions extends ExtensionPreferences {
})
);
keybindingsGroup.add(
containerGroup.add(
new EntryRow({
title: _('Reset Container Ratios to Equal'),
settings: settings,
@@ -182,7 +137,7 @@ export default class AerospikeExtensions extends ExtensionPreferences {
})
);
keybindingsGroup.add(
containerGroup.add(
new EntryRow({
title: _('Toggle Tabbed Mode'),
settings: settings,
@@ -191,6 +146,21 @@ export default class AerospikeExtensions extends ExtensionPreferences {
})
);
// --- Debugging group ---
const debuggingGroup = new Adw.PreferencesGroup({
title: _('Debugging'),
});
keybindingsPage.add(debuggingGroup);
debuggingGroup.add(
new EntryRow({
title: _('Print Tree Structure'),
settings: settings,
bind: 'print-tree',
map: keybindingMap
})
);
}
// Helper function to create a keybinding mapping object

View File

@@ -10,6 +10,13 @@ export enum Layout {
TABBED = 2,
}
export enum Direction {
LEFT = 'left',
RIGHT = 'right',
UP = 'up',
DOWN = 'down',
}
// 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 [];
@@ -48,7 +55,11 @@ export default class WindowContainer {
this._splitRatios = equalRatios(this._tiledItems.length);
}
private _addRatioForNewWindow(): void {
/**
* Proportionally shrink existing ratios to carve out space for a new item
* at the given index. If no index is supplied the ratio is appended at the end.
*/
private _addRatioForNewWindow(index?: number): void {
const n = this._tiledItems.length;
if (n <= 1) {
this._splitRatios = [1.0];
@@ -59,7 +70,10 @@ export default class WindowContainer {
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];
const insertAt = index ?? scaled.length;
scaled.splice(insertAt, 0, newRatio);
this._splitRatios = scaled;
}
private _totalDimension(): number {
@@ -161,6 +175,21 @@ export default class WindowContainer {
return this._activeTabIndex;
}
/**
* If the given window is a tab in this container, make it the active tab.
* Returns true if the window was found and activated.
*/
focusWindowTab(windowId: number): boolean {
if (!this.isTabbed()) return false;
const index = this._getIndexOfWindow(windowId);
if (index !== -1 && index !== this._activeTabIndex) {
this.setActiveTab(index);
return true;
}
return index !== -1;
}
hideTabBar(): void {
this._tabBar?.hide();
}
@@ -171,10 +200,19 @@ export default class WindowContainer {
}
}
addWindow(winWrap: WindowWrapper): void {
this._tiledItems.push(winWrap);
/**
* Add a window to this container.
* If `index` is omitted the window is appended at the end.
* A negative index (e.g. -1) is treated as "append at end".
*/
addWindow(winWrap: WindowWrapper, index?: number): void {
const insertAt = (index === undefined || index < 0)
? this._tiledItems.length
: Math.min(index, this._tiledItems.length);
this._tiledItems.splice(insertAt, 0, winWrap);
this._tiledWindowLookup.set(winWrap.getWindowId(), winWrap);
this._addRatioForNewWindow();
this._addRatioForNewWindow(insertAt);
if (this.isTabbed()) {
// TODO: make it so that when tabs are added they are made the current active tab
@@ -522,4 +560,145 @@ export default class WindowContainer {
this._resetRatios();
this.drawWindows();
}
// --- Directional Move (swap) ------------------------------------------------
/**
* Swap the window at `windowId` with its neighbour in the given direction.
* Returns true if the swap occurred, false if the window is already at the edge
* or the direction is perpendicular to the container axis.
*/
swapWindowInDirection(windowId: number, direction: Direction): boolean {
const currentIndex = this._getIndexOfWindow(windowId);
if (currentIndex === -1) return false;
if (this.isTabbed()) {
// Tabbed: left/up = swap toward start, right/down = swap toward end
const delta = (direction === Direction.LEFT || direction === Direction.UP) ? -1 : 1;
const newIndex = currentIndex + delta;
if (newIndex < 0 || newIndex >= this._tiledItems.length) return false;
this._swapItems(currentIndex, newIndex);
this._activeTabIndex = newIndex;
this._updateTabBar();
this.drawWindows();
return true;
}
// Accordion mode — only swap along the container's axis
const isAlongAxis =
(this._orientation === Layout.ACC_HORIZONTAL && (direction === Direction.LEFT || direction === Direction.RIGHT)) ||
(this._orientation === Layout.ACC_VERTICAL && (direction === Direction.UP || direction === Direction.DOWN));
if (!isAlongAxis) return false;
const delta = (direction === Direction.LEFT || direction === Direction.UP) ? -1 : 1;
const newIndex = currentIndex + delta;
if (newIndex < 0 || newIndex >= this._tiledItems.length) return false;
this._swapItems(currentIndex, newIndex);
this.drawWindows();
return true;
}
/**
* Swap two items in `_tiledItems` and their corresponding split ratios.
*/
private _swapItems(indexA: number, indexB: number): void {
[this._tiledItems[indexA], this._tiledItems[indexB]] =
[this._tiledItems[indexB], this._tiledItems[indexA]];
[this._splitRatios[indexA], this._splitRatios[indexB]] =
[this._splitRatios[indexB], this._splitRatios[indexA]];
}
// --- Directional Navigation ------------------------------------------------
/**
* Given a window inside this container and a direction, return the window ID
* that should receive focus, or null if the edge of the container is reached.
*
* Behaviour by layout mode:
* - ACC_HORIZONTAL: left/right moves to the prev/next item; up/down → null
* - ACC_VERTICAL: up/down moves to the prev/next item; left/right → null
* - TABBED: left/right moves to the prev/next tab; up/down → null
*/
getAdjacentWindowId(windowId: number, direction: Direction): number | null {
const currentIndex = this._getIndexOfWindow(windowId);
if (currentIndex === -1) return null;
if (this.isTabbed()) {
// Tabbed: left/right cycle through tabs
if (direction === Direction.LEFT || direction === Direction.UP) {
const newIndex = currentIndex - 1;
if (newIndex < 0) return null;
return this._windowIdAtIndex(newIndex);
}
if (direction === Direction.RIGHT || direction === Direction.DOWN) {
const newIndex = currentIndex + 1;
if (newIndex >= this._tiledItems.length) return null;
return this._windowIdAtIndex(newIndex);
}
return null;
}
// Accordion mode only navigate along the container's axis
const isAlongAxis =
(this._orientation === Layout.ACC_HORIZONTAL && (direction === Direction.LEFT || direction === Direction.RIGHT)) ||
(this._orientation === Layout.ACC_VERTICAL && (direction === Direction.UP || direction === Direction.DOWN));
if (!isAlongAxis) return null;
const delta = (direction === Direction.LEFT || direction === Direction.UP) ? -1 : 1;
const newIndex = currentIndex + delta;
if (newIndex < 0 || newIndex >= this._tiledItems.length) return null;
return this._windowIdAtIndex(newIndex);
}
/**
* Return the "representative" window ID for the item at `index`.
* If the item is a WindowWrapper, return its ID directly.
* If it's a nested WindowContainer, return the first (or last) leaf window.
*/
private _windowIdAtIndex(index: number): number | null {
const item = this._tiledItems[index];
if (!item) return null;
if (item instanceof WindowWrapper) {
return item.getWindowId();
}
if (item instanceof WindowContainer) {
return item._firstLeafWindowId();
}
return null;
}
/**
* Return the window ID of the first leaf window in this container (depth-first).
*/
_firstLeafWindowId(): number | null {
for (const item of this._tiledItems) {
if (item instanceof WindowWrapper) return item.getWindowId();
if (item instanceof WindowContainer) {
const id = item._firstLeafWindowId();
if (id !== null) return id;
}
}
return null;
}
/**
* Return the window ID of the last leaf window in this container (depth-first from end).
*/
_lastLeafWindowId(): number | null {
for (let i = this._tiledItems.length - 1; i >= 0; i--) {
const item = this._tiledItems[i];
if (item instanceof WindowWrapper) return item.getWindowId();
if (item instanceof WindowContainer) {
const id = item._lastLeafWindowId();
if (id !== null) return id;
}
}
return null;
}
}

View File

@@ -52,9 +52,9 @@ export default class Monitor {
}
}
addWindow(winWrap: WindowWrapper) {
addWindow(winWrap: WindowWrapper, index?: number) {
const window_workspace = winWrap.getWindow().get_workspace().index();
this._workspaces[window_workspace].addWindow(winWrap);
this._workspaces[window_workspace].addWindow(winWrap, index);
}
tileWindows(): void {
@@ -72,6 +72,12 @@ export default class Monitor {
this._workspaces.push(new WindowContainer(this._workArea));
}
focusWindowTab(windowId: number): void {
for (const container of this._workspaces) {
if (container.focusWindowTab(windowId)) return;
}
}
refreshTabTitlesForWindow(winWrap: WindowWrapper): void {
const wsId = winWrap.getWorkspace();
if (wsId >= 0 && wsId < this._workspaces.length) {

View File

@@ -5,7 +5,7 @@ import {WindowWrapper} from './window.js';
import * as Main from "resource:///org/gnome/shell/ui/main.js";
import {Logger} from "../utils/logger.js";
import Monitor from "./monitor.js";
import WindowContainer, {Layout} from "./container.js";
import WindowContainer, {Direction, Layout} from "./container.js";
import {Rect} from "../utils/rect.js";
@@ -504,6 +504,10 @@ export default class WindowManager implements IWindowManager {
if (focusWindow) {
this._activeWindowId = focusWindow.get_id();
Logger.debug(`Active window changed to: ${this._activeWindowId} (${focusWindow.get_title()})`);
// If the focused window is inside a tabbed container, make it the active tab
const monId = focusWindow.get_monitor();
this._monitors.get(monId)?.focusWindowTab(this._activeWindowId);
} else {
this._activeWindowId = null;
Logger.debug('No active window');
@@ -561,6 +565,203 @@ export default class WindowManager implements IWindowManager {
}
}
/**
* Move the active window in the given direction.
*
* 1. Find the container holding the active window.
* 2. Try to swap within the container (adjacent neighbour).
* 3. If already at the container edge, move the window to the
* nearest monitor in that direction instead.
* 4. Re-tile to apply the new layout.
*/
public moveInDirection(direction: Direction): void {
if (this._activeWindowId === null) {
Logger.warn("No active window, cannot move in direction");
return;
}
const container = this._findContainerForWindowAcrossMonitors(this._activeWindowId);
if (!container) {
Logger.warn("Could not find container for active window");
return;
}
const swapped = container.swapWindowInDirection(this._activeWindowId, direction);
if (swapped) {
Logger.info(`Moved window ${this._activeWindowId} ${direction}`);
this._tileMonitors();
return;
}
this._moveWindowCrossMonitor(this._activeWindowId, direction);
}
/**
* Move focus to the adjacent window in the given direction.
*
* 1. Find the container holding the active window.
* 2. Ask the container for the adjacent window in that direction.
* 3. If the container returns null (at the edge), try cross-monitor navigation.
* 4. Activate (focus) the target window.
*/
public focusInDirection(direction: Direction): void {
if (this._activeWindowId === null) {
Logger.warn("No active window, cannot focus in direction");
return;
}
const container = this._findContainerForWindowAcrossMonitors(this._activeWindowId);
if (!container) {
Logger.warn("Could not find container for active window");
return;
}
const targetId = container.getAdjacentWindowId(this._activeWindowId, direction);
if (targetId !== null) {
this._activateWindowById(targetId);
return;
}
// At the edge of the container — try cross-monitor navigation
const crossMonitorId = this._findCrossMonitorWindow(direction);
if (crossMonitorId !== null) {
this._activateWindowById(crossMonitorId);
}
}
/**
* Focus a window by its ID. Finds the Meta.Window and calls activate().
*/
private _activateWindowById(windowId: number): void {
for (const monitor of this._monitors.values()) {
const wrapped = monitor.getWindow(windowId);
if (wrapped) {
const metaWindow = wrapped.getWindow();
metaWindow.activate(global.get_current_time());
return;
}
}
Logger.warn(`_activateWindowById: window ${windowId} not found in any monitor`);
}
/**
* Find the adjacent monitor in the given direction from a current monitor.
* Returns the monitor ID or null if none exists in that direction.
*/
private _findAdjacentMonitorId(currentMonitorId: number, direction: Direction): number | null {
const currentMonitor = this._monitors.get(currentMonitorId)!;
const currentArea = currentMonitor._workArea;
const currentCenterX = currentArea.x + currentArea.width / 2;
const currentCenterY = currentArea.y + currentArea.height / 2;
let bestMonitorId: number | null = null;
let bestDistance = Infinity;
for (const [monId, monitor] of this._monitors.entries()) {
if (monId === currentMonitorId) continue;
const area = monitor._workArea;
const centerX = area.x + area.width / 2;
const centerY = area.y + area.height / 2;
let isInDirection = false;
let distance = Infinity;
switch (direction) {
case Direction.LEFT:
isInDirection = centerX < currentCenterX;
distance = currentCenterX - centerX;
break;
case Direction.RIGHT:
isInDirection = centerX > currentCenterX;
distance = centerX - currentCenterX;
break;
case Direction.UP:
isInDirection = centerY < currentCenterY;
distance = currentCenterY - centerY;
break;
case Direction.DOWN:
isInDirection = centerY > currentCenterY;
distance = centerY - currentCenterY;
break;
}
if (isInDirection && distance < bestDistance) {
bestDistance = distance;
bestMonitorId = monId;
}
}
return bestMonitorId;
}
/**
* Return the monitor ID that contains the given window, or null.
*/
private _findMonitorIdForWindow(windowId: number): number | null {
for (const [monId, monitor] of this._monitors.entries()) {
if (monitor.getWindow(windowId) !== undefined) return monId;
}
return null;
}
/**
* When at the edge of a container, find the nearest window on the adjacent
* monitor in the given direction.
*
* On the target monitor, picks the edge-most window:
* - Navigating LEFT/UP → last (far-edge) leaf window
* - Navigating RIGHT/DOWN → first (near-edge) leaf window
*/
private _findCrossMonitorWindow(direction: Direction): number | null {
if (this._activeWindowId === null) return null;
const currentMonitorId = this._findMonitorIdForWindow(this._activeWindowId);
if (currentMonitorId === null) return null;
const targetMonitorId = this._findAdjacentMonitorId(currentMonitorId, direction);
if (targetMonitorId === null) return null;
const targetMonitor = this._monitors.get(targetMonitorId)!;
const activeWorkspaceIndex = global.workspace_manager.get_active_workspace().index();
if (activeWorkspaceIndex >= targetMonitor._workspaces.length) return null;
const targetContainer = targetMonitor._workspaces[activeWorkspaceIndex];
if (targetContainer._tiledItems.length === 0) return null;
return (direction === Direction.LEFT || direction === Direction.UP)
? targetContainer._lastLeafWindowId()
: targetContainer._firstLeafWindowId();
}
/**
* Move a window to the adjacent monitor in the given direction.
*
* The window is inserted at the "entry edge" of the target container:
* - Moving RIGHT/DOWN → position 0 (near edge)
* - Moving LEFT/UP → end of the container (far edge)
*/
private _moveWindowCrossMonitor(windowId: number, direction: Direction): void {
const currentMonitorId = this._findMonitorIdForWindow(windowId);
if (currentMonitorId === null) return;
const targetMonitorId = this._findAdjacentMonitorId(currentMonitorId, direction);
if (targetMonitorId === null) return;
const currentMonitor = this._monitors.get(currentMonitorId)!;
const wrapped = currentMonitor.getWindow(windowId);
if (!wrapped) return;
const targetMonitor = this._monitors.get(targetMonitorId)!;
const insertIndex = (direction === Direction.RIGHT || direction === Direction.DOWN) ? 0 : undefined;
currentMonitor.removeWindow(wrapped);
targetMonitor.addWindow(wrapped, insertIndex);
this._tileMonitors();
Logger.info(`Moved window ${windowId} to monitor ${targetMonitorId} (${direction})`);
}
public printTreeStructure(): void {
Logger.info("=".repeat(80));
Logger.info("WINDOW TREE STRUCTURE");