Compare commits

..

7 Commits

Author SHA1 Message Date
Lucas Oskorep 656e448927 feat: add tabbed container layout mode with tab bar UI
Build and Test / build (push) Successful in 25s
Build and Test / release (push) Successful in 3s
fix: tab bars no longer shown in overview.  Tab bars show name of app with pipe and then title of the app
2026-02-26 21:45:40 -05:00
Lucas Oskorep 93516b31fb fix: new bug from combining resize and move commands - if window has min-size set and resize goes smaller window would not move or resize causing vesktop and steam to frequently break when in arrays on a smaller monitors
Build and Test / build (push) Successful in 39s
Build and Test / release (push) Successful in 10s
2026-02-26 01:54:49 -05:00
Lucas Oskorep 918c07c419 Merge pull request 'chore(deps): update dependency eslint to v10' (#16) from renovate/major-eslint-monorepo into main
Build and Test / build (push) Successful in 25s
Build and Test / release (push) Successful in 2s
Reviewed-on: #16
2026-02-25 17:18:42 -05:00
Lucas Oskorep 15188b9990 Merge branch 'main' into renovate/major-eslint-monorepo
Build and Test / build (pull_request) Successful in 24s
Build and Test / release (pull_request) Has been skipped
2026-02-25 17:02:44 -05:00
Lucas Oskorep 19696298d0 Merge pull request 'feat/percentage-based-container-sizing' (#18) from feat/percentage-based-container-sizing into main
Build and Test / build (push) Successful in 23s
Build and Test / release (push) Successful in 11s
Reviewed-on: #18
2026-02-25 11:36:39 -05:00
Lucas Oskorep 4be7602316 feat: adding support for changing the size of windows during window resize events to aerospike. Also cleaning up imports and simplifying some of the settings logic.
Build and Test / build (pull_request) Successful in 23s
Build and Test / release (pull_request) Has been skipped
2026-02-25 11:35:59 -05:00
Renovate Bot 8f6e8582c9 chore(deps): update dependency eslint to v10
Build and Test / build (pull_request) Successful in 24s
Build and Test / release (pull_request) Has been skipped
2026-02-06 23:13:10 +00:00
13 changed files with 809 additions and 790 deletions
+30 -102
View File
@@ -15,15 +15,19 @@ export default class aerospike extends Extension {
super(metadata); super(metadata);
this.settings = this.getSettings('org.gnome.shell.extensions.aerospike'); this.settings = this.getSettings('org.gnome.shell.extensions.aerospike');
this.keyBindings = new Map(); this.keyBindings = new Map();
this.windowManager = new WindowManager(); this.windowManager = new WindowManager(this.settings);
} }
enable() { enable() {
try {
Logger.log("STARTING AEROSPIKE!") Logger.log("STARTING AEROSPIKE!")
this.windowManager._settings = this.settings;
this.bindSettings(); this.bindSettings();
this.setupKeybindings(); this.setupKeybindings();
this.windowManager.enable() this.windowManager.enable()
Logger.log("AEROSPIKE ENABLED SUCCESSFULLY")
} catch (e) {
Logger.error("AEROSPIKE ENABLE FAILED", e);
}
} }
disable() { disable() {
@@ -31,42 +35,26 @@ export default class aerospike extends Extension {
this.removeKeybindings() this.removeKeybindings()
} }
private keybindingActions(): Record<string, () => void> {
return {
'move-left': () => { Logger.info('Keybinding 1 was pressed!'); },
'move-right': () => { Logger.info('Keybinding 2 was pressed!'); },
'join-with-left': () => { Logger.info('Keybinding 3 was pressed!'); },
'join-with-right': () => { Logger.info('Keybinding 4 was pressed!'); },
'print-tree': () => { this.windowManager.printTreeStructure(); },
'toggle-orientation': () => { this.windowManager.toggleActiveContainerOrientation(); },
'reset-ratios': () => { this.windowManager.resetActiveContainerRatios(); },
'toggle-tabbed': () => { this.windowManager.toggleActiveContainerTabbed(); },
};
}
private bindSettings() { private bindSettings() {
// Monitor settings changes const keybindings = Object.keys(this.keybindingActions());
this.settings.connect('changed::move-left', () => { keybindings.forEach(name => {
log(`Keybinding 1 changed to: ${this.settings.get_strv('move-left')}`); this.settings.connect(`changed::${name}`, () => {
this.refreshKeybinding('move-left'); log(`${name} keybinding changed to: ${this.settings.get_strv(name)}`);
this.refreshKeybinding(name);
}); });
this.settings.connect('changed::move-right', () => {
log(`Keybinding 2 changed to: ${this.settings.get_strv('move-right')}`);
this.refreshKeybinding('move-right');
});
this.settings.connect('changed::join-with-left', () => {
log(`Keybinding 3 changed to: ${this.settings.get_strv('join-with-left')}`);
this.refreshKeybinding('join-with-left');
});
this.settings.connect('changed::join-with-right', () => {
log(`Keybinding 4 changed to: ${this.settings.get_strv('join-with-right')}`);
this.refreshKeybinding('join-with-right');
});
this.settings.connect('changed::print-tree', () => {
log(`Print tree keybinding changed to: ${this.settings.get_strv('print-tree')}`);
this.refreshKeybinding('print-tree');
});
this.settings.connect('changed::toggle-orientation', () => {
log(`Toggle orientation keybinding changed to: ${this.settings.get_strv('toggle-orientation')}`);
this.refreshKeybinding('toggle-orientation');
});
this.settings.connect('changed::reset-ratios', () => {
log(`Reset ratios keybinding changed to: ${this.settings.get_strv('reset-ratios')}`);
this.refreshKeybinding('reset-ratios');
}); });
this.settings.connect('changed::dropdown-option', () => { this.settings.connect('changed::dropdown-option', () => {
@@ -77,49 +65,15 @@ export default class aerospike extends Extension {
log(`Color selection changed to: ${this.settings.get_string('color-selection')}`); log(`Color selection changed to: ${this.settings.get_string('color-selection')}`);
}); });
} }
private refreshKeybinding(settingName: string) { private refreshKeybinding(settingName: string) {
if (this.keyBindings.has(settingName)) { if (this.keyBindings.has(settingName)) {
Main.wm.removeKeybinding(settingName); Main.wm.removeKeybinding(settingName);
this.keyBindings.delete(settingName); this.keyBindings.delete(settingName);
} }
switch (settingName) { const action = this.keybindingActions()[settingName];
case 'move-left': if (action) this.bindKeybinding(settingName, action);
this.bindKeybinding('move-left', () => {
Logger.info('Keybinding 1 was pressed!');
});
break;
case 'move-right':
this.bindKeybinding('move-right', () => {
Logger.info('Keybinding 2 was pressed!');
});
break;
case 'join-with-left':
this.bindKeybinding('join-with-left', () => {
Logger.info('Keybinding 3 was pressed!');
});
break;
case 'join-with-right':
this.bindKeybinding('join-with-right', () => {
Logger.info('Keybinding 4 was pressed!');
});
break;
case 'print-tree':
this.bindKeybinding('print-tree', () => {
this.windowManager.printTreeStructure();
});
break;
case 'toggle-orientation':
this.bindKeybinding('toggle-orientation', () => {
this.windowManager.toggleActiveContainerOrientation();
});
break;
case 'reset-ratios':
this.bindKeybinding('reset-ratios', () => {
this.windowManager.resetActiveContainerRatios();
});
break;
}
} }
private removeKeybindings() { private removeKeybindings() {
@@ -130,33 +84,10 @@ export default class aerospike extends Extension {
} }
private setupKeybindings() { private setupKeybindings() {
this.bindKeybinding('move-left', () => { const actions = this.keybindingActions();
Logger.info('Keybinding 1 was pressed!'); for (const [name, action] of Object.entries(actions)) {
}); this.bindKeybinding(name, action);
}
this.bindKeybinding('move-right', () => {
Logger.info('Keybinding 2 was pressed!');
});
this.bindKeybinding('join-with-left', () => {
Logger.info('Keybinding 3 was pressed!');
});
this.bindKeybinding('join-with-right', () => {
Logger.info('Keybinding 4 was pressed!');
});
this.bindKeybinding('print-tree', () => {
this.windowManager.printTreeStructure();
});
this.bindKeybinding('toggle-orientation', () => {
this.windowManager.toggleActiveContainerOrientation();
});
this.bindKeybinding('reset-ratios', () => {
this.windowManager.resetActiveContainerRatios();
});
} }
private bindKeybinding(settingName: string, callback: () => void) { private bindKeybinding(settingName: string, callback: () => void) {
@@ -176,7 +107,4 @@ export default class aerospike extends Extension {
this.keyBindings.set(settingName, keyBindingAction); this.keyBindings.set(settingName, keyBindingAction);
} }
} }
+1 -1
View File
@@ -25,7 +25,7 @@
"@girs/gnome-shell": "49.1.0", "@girs/gnome-shell": "49.1.0",
"@jest/globals": "^30.0.0", "@jest/globals": "^30.0.0",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"eslint": "^9.36.0", "eslint": "^10.0.0",
"eslint-plugin-jsdoc": "^62.0.0", "eslint-plugin-jsdoc": "^62.0.0",
"jest": "^30.0.0", "jest": "^30.0.0",
"ts-jest": "^29.1.2", "ts-jest": "^29.1.2",
+80 -141
View File
@@ -43,11 +43,11 @@ importers:
specifier: ^30.0.0 specifier: ^30.0.0
version: 30.0.0 version: 30.0.0
eslint: eslint:
specifier: ^9.36.0 specifier: ^10.0.0
version: 9.39.2 version: 10.0.0
eslint-plugin-jsdoc: eslint-plugin-jsdoc:
specifier: ^62.0.0 specifier: ^62.0.0
version: 62.4.1(eslint@9.39.2) version: 62.4.1(eslint@10.0.0)
jest: jest:
specifier: ^30.0.0 specifier: ^30.0.0
version: 30.2.0(@types/node@25.1.0) version: 30.2.0(@types/node@25.1.0)
@@ -252,33 +252,25 @@ packages:
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
'@eslint/config-array@0.21.1': '@eslint/config-array@0.23.0':
resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} resolution: {integrity: sha512-T5Swqd+PZxBekRuMsIFCySM3NUE8GjuqyksIIsXgkF2GCuiDaqpxKyPkv9VMEKpq5D7r5DLss1tM8tCsvRSjeg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/config-helpers@0.4.2': '@eslint/config-helpers@0.5.2':
resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} resolution: {integrity: sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/core@0.17.0': '@eslint/core@1.1.0':
resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} resolution: {integrity: sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/eslintrc@3.3.3': '@eslint/object-schema@3.0.0':
resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} resolution: {integrity: sha512-nWl20RtHQP2A2yvKU6Fee62Xo1AoNRqBLixtyg45zJhU8ljPFCyBK90d8e8XTnPns1RggSf4HH3bM8AhevkPVg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/js@9.39.2': '@eslint/plugin-kit@0.6.0':
resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} resolution: {integrity: sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/object-schema@2.1.7':
resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/plugin-kit@0.4.1':
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@girs/accountsservice-1.0@1.0.0-4.0.0-beta.38': '@girs/accountsservice-1.0@1.0.0-4.0.0-beta.38':
resolution: {integrity: sha512-6QzytM5dztmMynF2bxN73EuNK9ArMFxkP2L8wUC7IH45zBeBOfYcqL85BFh2PmkGmqRk+Rli5EFR8dAkx3Ig5Q==} resolution: {integrity: sha512-6QzytM5dztmMynF2bxN73EuNK9ArMFxkP2L8wUC7IH45zBeBOfYcqL85BFh2PmkGmqRk+Rli5EFR8dAkx3Ig5Q==}
@@ -425,6 +417,14 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'} 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': '@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -574,6 +574,9 @@ packages:
'@types/babel__traverse@7.28.0': '@types/babel__traverse@7.28.0':
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
'@types/esrecurse@4.3.1':
resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
'@types/estree@1.0.8': '@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@@ -762,9 +765,6 @@ packages:
argparse@1.0.10: argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
babel-jest@30.2.0: babel-jest@30.2.0:
resolution: {integrity: sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==} resolution: {integrity: sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
@@ -949,25 +949,21 @@ packages:
peerDependencies: peerDependencies:
eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
eslint-scope@8.4.0: eslint-scope@9.1.0:
resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} resolution: {integrity: sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint-visitor-keys@3.4.3: eslint-visitor-keys@3.4.3:
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
eslint-visitor-keys@4.2.1:
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint-visitor-keys@5.0.0: eslint-visitor-keys@5.0.0:
resolution: {integrity: sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==} resolution: {integrity: sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint@9.39.2: eslint@10.0.0:
resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} resolution: {integrity: sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
jiti: '*' jiti: '*'
@@ -975,10 +971,6 @@ packages:
jiti: jiti:
optional: true optional: true
espree@10.4.0:
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
espree@11.1.0: espree@11.1.0:
resolution: {integrity: sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==} resolution: {integrity: sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
@@ -1085,15 +1077,12 @@ packages:
glob@10.5.0: glob@10.5.0:
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true hasBin: true
glob@7.2.3: glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
globals@14.0.0:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'}
graceful-fs@4.2.11: graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
@@ -1121,10 +1110,6 @@ packages:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
import-local@3.2.0: import-local@3.2.0:
resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1329,10 +1314,6 @@ packages:
resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
hasBin: true hasBin: true
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
jsdoc-type-pratt-parser@7.1.0: jsdoc-type-pratt-parser@7.1.0:
resolution: {integrity: sha512-SX7q7XyCwzM/MEDCYz0l8GgGbJAACGFII9+WfNYr5SLEKukHWRy2Jk3iWRe7P+lpYJNs7oQ+OSei4JtKGUjd7A==} resolution: {integrity: sha512-SX7q7XyCwzM/MEDCYz0l8GgGbJAACGFII9+WfNYr5SLEKukHWRy2Jk3iWRe7P+lpYJNs7oQ+OSei4JtKGUjd7A==}
engines: {node: '>=20.0.0'} engines: {node: '>=20.0.0'}
@@ -1384,9 +1365,6 @@ packages:
lodash.memoize@4.1.2: lodash.memoize@4.1.2:
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
lru-cache@10.4.3: lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
@@ -1414,6 +1392,10 @@ packages:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'} engines: {node: '>=6'}
minimatch@10.1.2:
resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==}
engines: {node: 20 || >=22}
minimatch@3.1.2: minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@@ -1493,10 +1475,6 @@ packages:
package-json-from-dist@1.0.1: package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
parse-imports-exports@0.2.4: parse-imports-exports@0.2.4:
resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==}
@@ -1572,10 +1550,6 @@ packages:
resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==}
engines: {node: '>=8'} engines: {node: '>=8'}
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
resolve-from@5.0.0: resolve-from@5.0.0:
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -2031,50 +2005,34 @@ snapshots:
'@es-joy/resolve.exports@1.2.0': {} '@es-joy/resolve.exports@1.2.0': {}
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.2)': '@eslint-community/eslint-utils@4.9.1(eslint@10.0.0)':
dependencies: dependencies:
eslint: 9.39.2 eslint: 10.0.0
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {} '@eslint-community/regexpp@4.12.2': {}
'@eslint/config-array@0.21.1': '@eslint/config-array@0.23.0':
dependencies: dependencies:
'@eslint/object-schema': 2.1.7 '@eslint/object-schema': 3.0.0
debug: 4.4.3 debug: 4.4.3
minimatch: 3.1.2 minimatch: 10.1.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@eslint/config-helpers@0.4.2': '@eslint/config-helpers@0.5.2':
dependencies: dependencies:
'@eslint/core': 0.17.0 '@eslint/core': 1.1.0
'@eslint/core@0.17.0': '@eslint/core@1.1.0':
dependencies: dependencies:
'@types/json-schema': 7.0.15 '@types/json-schema': 7.0.15
'@eslint/eslintrc@3.3.3': '@eslint/object-schema@3.0.0': {}
'@eslint/plugin-kit@0.6.0':
dependencies: dependencies:
ajv: 6.12.6 '@eslint/core': 1.1.0
debug: 4.4.3
espree: 10.4.0
globals: 14.0.0
ignore: 5.3.2
import-fresh: 3.3.1
js-yaml: 4.1.1
minimatch: 3.1.2
strip-json-comments: 3.1.1
transitivePeerDependencies:
- supports-color
'@eslint/js@9.39.2': {}
'@eslint/object-schema@2.1.7': {}
'@eslint/plugin-kit@0.4.1':
dependencies:
'@eslint/core': 0.17.0
levn: 0.4.1 levn: 0.4.1
'@girs/accountsservice-1.0@1.0.0-4.0.0-beta.38': '@girs/accountsservice-1.0@1.0.0-4.0.0-beta.38':
@@ -2541,6 +2499,12 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {} '@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': '@isaacs/cliui@8.0.2':
dependencies: dependencies:
string-width: 5.1.2 string-width: 5.1.2
@@ -2808,6 +2772,8 @@ snapshots:
dependencies: dependencies:
'@babel/types': 7.28.6 '@babel/types': 7.28.6
'@types/esrecurse@4.3.1': {}
'@types/estree@1.0.8': {} '@types/estree@1.0.8': {}
'@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-coverage@2.0.6': {}
@@ -2942,8 +2908,6 @@ snapshots:
dependencies: dependencies:
sprintf-js: 1.0.3 sprintf-js: 1.0.3
argparse@2.0.1: {}
babel-jest@30.2.0(@babel/core@7.28.6): babel-jest@30.2.0(@babel/core@7.28.6):
dependencies: dependencies:
'@babel/core': 7.28.6 '@babel/core': 7.28.6
@@ -3110,7 +3074,7 @@ snapshots:
escape-string-regexp@4.0.0: {} escape-string-regexp@4.0.0: {}
eslint-plugin-jsdoc@62.4.1(eslint@9.39.2): eslint-plugin-jsdoc@62.4.1(eslint@10.0.0):
dependencies: dependencies:
'@es-joy/jsdoccomment': 0.83.0 '@es-joy/jsdoccomment': 0.83.0
'@es-joy/resolve.exports': 1.2.0 '@es-joy/resolve.exports': 1.2.0
@@ -3118,7 +3082,7 @@ snapshots:
comment-parser: 1.4.5 comment-parser: 1.4.5
debug: 4.4.3 debug: 4.4.3
escape-string-regexp: 4.0.0 escape-string-regexp: 4.0.0
eslint: 9.39.2 eslint: 10.0.0
espree: 11.1.0 espree: 11.1.0
esquery: 1.7.0 esquery: 1.7.0
html-entities: 2.6.0 html-entities: 2.6.0
@@ -3130,39 +3094,36 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-scope@8.4.0: eslint-scope@9.1.0:
dependencies: dependencies:
'@types/esrecurse': 4.3.1
'@types/estree': 1.0.8
esrecurse: 4.3.0 esrecurse: 4.3.0
estraverse: 5.3.0 estraverse: 5.3.0
eslint-visitor-keys@3.4.3: {} eslint-visitor-keys@3.4.3: {}
eslint-visitor-keys@4.2.1: {}
eslint-visitor-keys@5.0.0: {} eslint-visitor-keys@5.0.0: {}
eslint@9.39.2: eslint@10.0.0:
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0)
'@eslint-community/regexpp': 4.12.2 '@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.21.1 '@eslint/config-array': 0.23.0
'@eslint/config-helpers': 0.4.2 '@eslint/config-helpers': 0.5.2
'@eslint/core': 0.17.0 '@eslint/core': 1.1.0
'@eslint/eslintrc': 3.3.3 '@eslint/plugin-kit': 0.6.0
'@eslint/js': 9.39.2
'@eslint/plugin-kit': 0.4.1
'@humanfs/node': 0.16.7 '@humanfs/node': 0.16.7
'@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3 '@humanwhocodes/retry': 0.4.3
'@types/estree': 1.0.8 '@types/estree': 1.0.8
ajv: 6.12.6 ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.6 cross-spawn: 7.0.6
debug: 4.4.3 debug: 4.4.3
escape-string-regexp: 4.0.0 escape-string-regexp: 4.0.0
eslint-scope: 8.4.0 eslint-scope: 9.1.0
eslint-visitor-keys: 4.2.1 eslint-visitor-keys: 5.0.0
espree: 10.4.0 espree: 11.1.0
esquery: 1.7.0 esquery: 1.7.0
esutils: 2.0.3 esutils: 2.0.3
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
@@ -3173,19 +3134,12 @@ snapshots:
imurmurhash: 0.1.4 imurmurhash: 0.1.4
is-glob: 4.0.3 is-glob: 4.0.3
json-stable-stringify-without-jsonify: 1.0.1 json-stable-stringify-without-jsonify: 1.0.1
lodash.merge: 4.6.2 minimatch: 10.1.2
minimatch: 3.1.2
natural-compare: 1.4.0 natural-compare: 1.4.0
optionator: 0.9.4 optionator: 0.9.4
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
espree@10.4.0:
dependencies:
acorn: 8.15.0
acorn-jsx: 5.3.2(acorn@8.15.0)
eslint-visitor-keys: 4.2.1
espree@11.1.0: espree@11.1.0:
dependencies: dependencies:
acorn: 8.15.0 acorn: 8.15.0
@@ -3304,8 +3258,6 @@ snapshots:
once: 1.4.0 once: 1.4.0
path-is-absolute: 1.0.1 path-is-absolute: 1.0.1
globals@14.0.0: {}
graceful-fs@4.2.11: {} graceful-fs@4.2.11: {}
handlebars@4.7.8: handlebars@4.7.8:
@@ -3327,11 +3279,6 @@ snapshots:
ignore@5.3.2: {} ignore@5.3.2: {}
import-fresh@3.3.1:
dependencies:
parent-module: 1.0.1
resolve-from: 4.0.0
import-local@3.2.0: import-local@3.2.0:
dependencies: dependencies:
pkg-dir: 4.2.0 pkg-dir: 4.2.0
@@ -3719,10 +3666,6 @@ snapshots:
argparse: 1.0.10 argparse: 1.0.10
esprima: 4.0.1 esprima: 4.0.1
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
jsdoc-type-pratt-parser@7.1.0: {} jsdoc-type-pratt-parser@7.1.0: {}
jsesc@3.1.0: {} jsesc@3.1.0: {}
@@ -3760,8 +3703,6 @@ snapshots:
lodash.memoize@4.1.2: {} lodash.memoize@4.1.2: {}
lodash.merge@4.6.2: {}
lru-cache@10.4.3: {} lru-cache@10.4.3: {}
lru-cache@5.1.1: lru-cache@5.1.1:
@@ -3787,6 +3728,10 @@ snapshots:
mimic-fn@2.1.0: {} mimic-fn@2.1.0: {}
minimatch@10.1.2:
dependencies:
'@isaacs/brace-expansion': 5.0.1
minimatch@3.1.2: minimatch@3.1.2:
dependencies: dependencies:
brace-expansion: 1.1.12 brace-expansion: 1.1.12
@@ -3856,10 +3801,6 @@ snapshots:
package-json-from-dist@1.0.1: {} package-json-from-dist@1.0.1: {}
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
parse-imports-exports@0.2.4: parse-imports-exports@0.2.4:
dependencies: dependencies:
parse-statements: 1.0.11 parse-statements: 1.0.11
@@ -3918,8 +3859,6 @@ snapshots:
dependencies: dependencies:
resolve-from: 5.0.0 resolve-from: 5.0.0
resolve-from@4.0.0: {}
resolve-from@5.0.0: {} resolve-from@5.0.0: {}
semver@6.3.1: {} semver@6.3.1: {}
@@ -44,7 +44,7 @@
</key> </key>
<key name="toggle-orientation" type="as"> <key name="toggle-orientation" type="as">
<default><![CDATA[['<Super><Shift>comma']]]></default> <default><![CDATA[['<Primary>comma']]]></default>
<summary>Toggle active container orientation</summary> <summary>Toggle active container orientation</summary>
<description>Toggles the orientation of the container holding the active window between horizontal and vertical</description> <description>Toggles the orientation of the container holding the active window between horizontal and vertical</description>
</key> </key>
@@ -55,11 +55,10 @@
<description>Resets all window size ratios in the active window's container to equal splits</description> <description>Resets all window size ratios in the active window's container to equal splits</description>
</key> </key>
<key name="min-window-size-percent" type="d"> <key name="toggle-tabbed" type="as">
<default>0.10</default> <default><![CDATA[['<Primary>slash']]]></default>
<range min="0.01" max="0.49"/> <summary>Toggle tabbed container mode</summary>
<summary>Minimum window size percentage</summary> <description>Toggles the active window's container between tabbed and accordion layout modes</description>
<description>Minimum fraction of a container that any single window may occupy when resizing boundaries</description>
</key> </key>
</schema> </schema>
+87 -9
View File
@@ -20,25 +20,38 @@ jest.mock('../utils/events.js', () => ({
describe('Container Logic Tests', () => { describe('Container Logic Tests', () => {
describe('Orientation Toggle Logic', () => { describe('Orientation Toggle Logic', () => {
enum Orientation { enum Layout {
HORIZONTAL = 0, HORIZONTAL = 0,
VERTICAL = 1, VERTICAL = 1,
TABBED = 2,
} }
const toggleOrientation = (current: Orientation): Orientation => { const toggleOrientation = (current: Layout): Layout => {
return current === Orientation.HORIZONTAL if (current === Layout.TABBED) return Layout.HORIZONTAL;
? Orientation.VERTICAL return current === Layout.HORIZONTAL
: Orientation.HORIZONTAL; ? Layout.VERTICAL
: Layout.HORIZONTAL;
}; };
test('should toggle from HORIZONTAL to VERTICAL', () => { test('should toggle from HORIZONTAL to VERTICAL', () => {
const result = toggleOrientation(Orientation.HORIZONTAL); const result = toggleOrientation(Layout.HORIZONTAL);
expect(result).toBe(Orientation.VERTICAL); expect(result).toBe(Layout.VERTICAL);
}); });
test('should toggle from VERTICAL to HORIZONTAL', () => { test('should toggle from VERTICAL to HORIZONTAL', () => {
const result = toggleOrientation(Orientation.VERTICAL); const result = toggleOrientation(Layout.VERTICAL);
expect(result).toBe(Orientation.HORIZONTAL); expect(result).toBe(Layout.HORIZONTAL);
});
test('should toggle from TABBED to HORIZONTAL', () => {
const result = toggleOrientation(Layout.TABBED);
expect(result).toBe(Layout.HORIZONTAL);
});
test('enum reverse mapping should return string names', () => {
expect(Layout[Layout.HORIZONTAL]).toBe('HORIZONTAL');
expect(Layout[Layout.VERTICAL]).toBe('VERTICAL');
expect(Layout[Layout.TABBED]).toBe('TABBED');
}); });
}); });
@@ -100,6 +113,71 @@ describe('Container Logic Tests', () => {
}); });
}); });
describe('Tabbed Bounds Calculation', () => {
const TAB_BAR_HEIGHT = 24;
test('should give all items the same content rect in tabbed mode', () => {
const workArea = { x: 100, y: 0, width: 1000, height: 500 };
const itemCount = 3;
const contentRect = {
x: workArea.x,
y: workArea.y + TAB_BAR_HEIGHT,
width: workArea.width,
height: workArea.height - TAB_BAR_HEIGHT,
};
const bounds = Array.from({ length: itemCount }, () => contentRect);
expect(bounds.length).toBe(3);
// All bounds should be identical
bounds.forEach(b => {
expect(b.x).toBe(100);
expect(b.y).toBe(TAB_BAR_HEIGHT);
expect(b.width).toBe(1000);
expect(b.height).toBe(500 - TAB_BAR_HEIGHT);
});
});
test('tab bar rect should occupy top of work area', () => {
const workArea = { x: 200, y: 50, width: 800, height: 600 };
const tabBarRect = {
x: workArea.x,
y: workArea.y,
width: workArea.width,
height: TAB_BAR_HEIGHT,
};
expect(tabBarRect.x).toBe(200);
expect(tabBarRect.y).toBe(50);
expect(tabBarRect.width).toBe(800);
expect(tabBarRect.height).toBe(TAB_BAR_HEIGHT);
});
test('active tab index should clamp after removal', () => {
let activeTabIndex = 2;
const itemCount = 2; // after removing one from 3
if (activeTabIndex >= itemCount) {
activeTabIndex = itemCount - 1;
}
expect(activeTabIndex).toBe(1);
});
test('active tab index should stay at 0 when first item removed', () => {
let activeTabIndex = 0;
const itemCount = 2; // after removing one from 3
if (activeTabIndex >= itemCount) {
activeTabIndex = itemCount - 1;
}
expect(activeTabIndex).toBe(0);
});
});
describe('Window Index Finding', () => { describe('Window Index Finding', () => {
test('should find window index in array', () => { test('should find window index in array', () => {
const windows = [ const windows = [
+8 -34
View File
@@ -182,40 +182,14 @@ export default class AerospikeExtensions extends ExtensionPreferences {
}) })
); );
// Create sizing group keybindingsGroup.add(
const sizingGroup = new Adw.PreferencesGroup({ new EntryRow({
title: _('Window Sizing'), title: _('Toggle Tabbed Mode'),
}); settings: settings,
page.add(sizingGroup); bind: 'toggle-tabbed',
map: keybindingMap
// Minimum window size percentage spinner })
const minSizeRow = new Adw.ActionRow({ );
title: _('Minimum Window Size'),
subtitle: _('Smallest fraction of a container any window may occupy when resizing (e.g. 0.10 = 10%)'),
});
sizingGroup.add(minSizeRow);
const minSizeSpin = new Gtk.SpinButton({
adjustment: new Gtk.Adjustment({
lower: 0.01,
upper: 0.49,
step_increment: 0.01,
page_increment: 0.05,
value: settings.get_double('min-window-size-percent'),
}),
digits: 2,
valign: Gtk.Align.CENTER,
});
minSizeRow.add_suffix(minSizeSpin);
minSizeRow.set_activatable_widget(minSizeSpin);
minSizeSpin.connect('value-changed', () => {
settings.set_double('min-window-size-percent', minSizeSpin.get_value());
});
settings.connect('changed::min-window-size-percent', () => {
minSizeSpin.set_value(settings.get_double('min-window-size-percent'));
});
} }
-7
View File
@@ -1,25 +1,18 @@
import GLib from "gi://GLib"; import GLib from "gi://GLib";
export type QueuedEvent = { export type QueuedEvent = {
name: string; name: string;
callback: () => void; callback: () => void;
} }
// Pending events indexed by name so that duplicate events collapse into one.
// Only the most-recently-queued callback for a given name is kept.
const pendingEvents: Map<string, QueuedEvent> = new Map(); const pendingEvents: Map<string, QueuedEvent> = new Map();
export default function queueEvent(event: QueuedEvent, interval = 200) { export default function queueEvent(event: QueuedEvent, interval = 200) {
// Overwrite any earlier pending event with the same name — the latest
// callback is always the most up-to-date one.
pendingEvents.set(event.name, event); pendingEvents.set(event.name, event);
GLib.timeout_add(GLib.PRIORITY_DEFAULT, interval, () => { GLib.timeout_add(GLib.PRIORITY_DEFAULT, interval, () => {
const e = pendingEvents.get(event.name); const e = pendingEvents.get(event.name);
if (e && e === event) { if (e && e === event) {
// Only fire if this is still the current callback for this name
// (a newer call may have replaced it).
pendingEvents.delete(event.name); pendingEvents.delete(event.name);
e.callback(); e.callback();
} }
+297 -162
View File
@@ -1,45 +1,40 @@
import {WindowWrapper} from "./window.js"; import {WindowWrapper} from "./window.js";
import {Logger} from "../utils/logger.js"; import {Logger} from "../utils/logger.js";
import Meta from "gi://Meta";
import queueEvent from "../utils/events.js"; import queueEvent from "../utils/events.js";
import {Rect} from "../utils/rect.js"; import {Rect} from "../utils/rect.js";
import {TabBar, TAB_BAR_HEIGHT} from "./tabBar.js";
enum Orientation { export enum Layout {
HORIZONTAL = 0, ACC_HORIZONTAL = 0,
VERTICAL = 1, ACC_VERTICAL = 1,
TABBED = 2,
} }
/** // Returns equal ratios summing exactly to 1.0, with float drift absorbed by the last slot.
* Build a split-ratio array of length `n` where every element equals 1/n,
* with the last slot absorbing any floating-point remainder so the array
* always sums to exactly 1.0.
*/
function equalRatios(n: number): number[] { function equalRatios(n: number): number[] {
if (n <= 0) return []; if (n <= 0) return [];
const base = 1 / n; const base = 1 / n;
const ratios = Array(n).fill(base); const ratios = Array(n).fill(base);
// Fix floating-point drift: make last slot exact
const sumExceptLast = ratios.slice(0, -1).reduce((a, b) => a + b, 0); const sumExceptLast = ratios.slice(0, -1).reduce((a, b) => a + b, 0);
ratios[n - 1] = 1 - sumExceptLast; ratios[n - 1] = 1 - sumExceptLast;
return ratios; return ratios;
} }
export default class WindowContainer { export default class WindowContainer {
_tiledItems: (WindowWrapper | WindowContainer)[]; _tiledItems: (WindowWrapper | WindowContainer)[];
_tiledWindowLookup: Map<number, WindowWrapper>; _tiledWindowLookup: Map<number, WindowWrapper>;
_orientation: Orientation = Orientation.HORIZONTAL; _orientation: Layout = Layout.ACC_HORIZONTAL;
_workArea: Rect; _workArea: Rect;
/** // -- Accordion Mode States
* Per-child split ratios. Always satisfies:
* _splitRatios.length === _tiledItems.length
* _splitRatios.reduce((a,b) => a+b, 0) === 1.0 (within floating-point epsilon)
* every element >= MIN_RATIO
*/
_splitRatios: number[]; _splitRatios: number[];
// -- Tabbed mode state -----------------------------------------------------
_activeTabIndex: number = 0;
_tabBar: TabBar | null = null;
constructor(workspaceArea: Rect) { constructor(workspaceArea: Rect) {
this._tiledItems = []; this._tiledItems = [];
this._tiledWindowLookup = new Map<number, WindowWrapper>(); this._tiledWindowLookup = new Map<number, WindowWrapper>();
@@ -47,22 +42,12 @@ export default class WindowContainer {
this._splitRatios = []; this._splitRatios = [];
} }
// ─── Helpers ──────────────────────────────────────────────────────────────── // --- Helpers ----------------------------------------------------------------
/** Rebuild _splitRatios as equal fractions after any structural change. */
private _resetRatios(): void { private _resetRatios(): void {
this._splitRatios = equalRatios(this._tiledItems.length); this._splitRatios = equalRatios(this._tiledItems.length);
} }
/**
* Called after a new item has been pushed onto _tiledItems.
* The new window (last slot) gets 1/n of the space; existing windows
* are scaled down proportionally so their ratios relative to each other
* are preserved and the total remains 1.0.
*
* e.g. [0.33, 0.166, 0.5] + new → new=0.25, existing scaled by 0.75
* → [0.2475, 0.1245, 0.375, 0.25]
*/
private _addRatioForNewWindow(): void { private _addRatioForNewWindow(): void {
const n = this._tiledItems.length; const n = this._tiledItems.length;
if (n <= 1) { if (n <= 1) {
@@ -70,45 +55,136 @@ export default class WindowContainer {
return; return;
} }
const newRatio = 1 / n; const newRatio = 1 / n;
const scale = 1 - newRatio; // existing windows share this fraction const scale = 1 - newRatio;
const scaled = this._splitRatios.map(r => r * scale); const scaled = this._splitRatios.map(r => r * scale);
// Absorb all floating-point drift into the last slot so sum is exactly 1.0
const partialSum = scaled.reduce((a, b) => a + b, 0) + newRatio; const partialSum = scaled.reduce((a, b) => a + b, 0) + newRatio;
scaled[scaled.length - 1] += (1.0 - partialSum); scaled[scaled.length - 1] += (1.0 - partialSum);
this._splitRatios = [...scaled, newRatio]; this._splitRatios = [...scaled, newRatio];
} }
/** Total dimension for the active orientation (width for H, height for V). */
private _totalDimension(): number { private _totalDimension(): number {
return this._orientation === Orientation.HORIZONTAL return this._orientation === Layout.ACC_HORIZONTAL
? this._workArea.width ? this._workArea.width
: this._workArea.height; : this._workArea.height;
} }
// ─── Public API ───────────────────────────────────────────────────────────── isTabbed(): boolean {
return this._orientation === Layout.TABBED;
}
// --- Public API -------------------------------------------------------------
move(rect: Rect): void { move(rect: Rect): void {
this._workArea = rect; this._workArea = rect;
this.tileWindows(); this.drawWindows();
} }
toggleOrientation(): void { toggleOrientation(): void {
this._orientation = this._orientation === Orientation.HORIZONTAL if (this._orientation === Layout.TABBED) {
? Orientation.VERTICAL // Tabbed → Horizontal: restore accordion mode
: Orientation.HORIZONTAL; this.setAccordion(Layout.ACC_HORIZONTAL);
Logger.info(`Container orientation toggled to ${this._orientation === Orientation.HORIZONTAL ? 'HORIZONTAL' : 'VERTICAL'}`); } else {
this.tileWindows(); this._orientation = this._orientation === Layout.ACC_HORIZONTAL
? Layout.ACC_VERTICAL
: Layout.ACC_HORIZONTAL;
Logger.info(`Container orientation toggled to ${Layout[this._orientation]}`);
this.drawWindows();
}
}
/**
* Switch this container to tabbed mode.
*/
setTabbed(): void {
if (this._orientation === Layout.TABBED) return;
Logger.info("Container switching to TABBED mode");
this._orientation = Layout.TABBED;
// Clamp active tab index
if (this._activeTabIndex < 0 || this._activeTabIndex >= this._tiledItems.length) {
this._activeTabIndex = 0;
}
// Create tab bar
this._tabBar = new TabBar((index) => {
this.setActiveTab(index);
});
this.drawWindows();
}
/**
* Switch this container back to accordion (H or V) mode.
*/
setAccordion(orientation: Layout.ACC_HORIZONTAL | Layout.ACC_VERTICAL): void {
if (this._orientation !== Layout.TABBED) {
// Already accordion — just set the orientation
this._orientation = orientation;
this.drawWindows();
return;
}
Logger.info(`Container switching from TABBED to ${Layout[orientation]}`);
this._orientation = orientation;
// Destroy tab bar
if (this._tabBar) {
this._tabBar.destroy();
this._tabBar = null;
}
// Show all windows (they may have been hidden in tabbed mode)
this._showAllWindows();
this.drawWindows();
}
/**
* Set the active tab by index. Shows that window, hides others, updates tab bar.
*/
setActiveTab(index: number): void {
if (!this.isTabbed()) return;
if (index < 0 || index >= this._tiledItems.length) return;
this._activeTabIndex = index;
Logger.info(`Active tab set to ${index}`);
this._applyTabVisibility();
this._updateTabBar();
// Tile to resize the active window to the content area
this.drawWindows();
}
getActiveTabIndex(): number {
return this._activeTabIndex;
}
hideTabBar(): void {
this._tabBar?.hide();
}
showTabBar(): void {
if (this.isTabbed() && this._tabBar) {
this._tabBar.show();
}
} }
addWindow(winWrap: WindowWrapper): void { addWindow(winWrap: WindowWrapper): void {
this._tiledItems.push(winWrap); this._tiledItems.push(winWrap);
this._tiledWindowLookup.set(winWrap.getWindowId(), winWrap); this._tiledWindowLookup.set(winWrap.getWindowId(), winWrap);
this._addRatioForNewWindow(); this._addRatioForNewWindow();
if (this.isTabbed()) {
// TODO: make it so that when tabs are added they are made the current active tab
this._applyTabVisibility();
this._updateTabBar();
}
queueEvent({ queueEvent({
name: "tiling-windows", name: "tiling-windows",
callback: () => { callback: () => this.drawWindows(),
this.tileWindows();
}
}, 100); }, 100);
} }
@@ -119,9 +195,7 @@ export default class WindowContainer {
for (const item of this._tiledItems) { for (const item of this._tiledItems) {
if (item instanceof WindowContainer) { if (item instanceof WindowContainer) {
const win = item.getWindow(win_id); const win = item.getWindow(win_id);
if (win) { if (win) return win;
return win;
}
} else if (item.getWindowId() === win_id) { } else if (item.getWindowId() === win_id) {
return item; return item;
} }
@@ -144,9 +218,25 @@ export default class WindowContainer {
const index = this._getIndexOfWindow(win_id); const index = this._getIndexOfWindow(win_id);
this._tiledWindowLookup.delete(win_id); this._tiledWindowLookup.delete(win_id);
if (index !== -1) { if (index !== -1) {
// If removing the window that was hidden in tabbed mode,
// make sure to show it first so it doesn't stay invisible
const item = this._tiledItems[index];
if (item instanceof WindowWrapper) {
item.showWindow();
}
this._tiledItems.splice(index, 1); this._tiledItems.splice(index, 1);
} }
this._resetRatios(); this._resetRatios();
if (this.isTabbed()) {
if (this._tiledItems.length === 0) {
this._activeTabIndex = 0;
} else if (this._activeTabIndex >= this._tiledItems.length) {
this._activeTabIndex = this._tiledItems.length - 1;
}
this._applyTabVisibility();
this._updateTabBar();
}
} else { } else {
for (const item of this._tiledItems) { for (const item of this._tiledItems) {
if (item instanceof WindowContainer) { if (item instanceof WindowContainer) {
@@ -154,7 +244,7 @@ export default class WindowContainer {
} }
} }
} }
this.tileWindows(); this.drawWindows();
} }
disconnectSignals(): void { disconnectSignals(): void {
@@ -168,104 +258,178 @@ export default class WindowContainer {
} }
removeAllWindows(): void { removeAllWindows(): void {
// tabbed mode hides all windows - this ensures they are available before removal
this._showAllWindows();
if (this._tabBar) {
this._tabBar.destroy();
this._tabBar = null;
}
this._tiledItems = []; this._tiledItems = [];
this._tiledWindowLookup.clear(); this._tiledWindowLookup.clear();
this._splitRatios = []; this._splitRatios = [];
this._activeTabIndex = 0;
} }
tileWindows() { drawWindows(): void {
Logger.log("TILING WINDOWS IN CONTAINER"); Logger.log("TILING WINDOWS IN CONTAINER");
Logger.log("WorkArea", this._workArea); Logger.log("WorkArea", this._workArea);
this._tileItems();
return true; if (this.isTabbed()) {
this._tileTab();
} else {
this._tileAccordion();
}
} }
_tileItems() { _tileAccordion() {
if (this._tiledItems.length === 0) { if (this._tiledItems.length === 0) return;
return;
}
const bounds = this.getBounds(); const bounds = this.getBounds();
Logger.info(`_tileItems: ratios=[${this._splitRatios.map(r => r.toFixed(3)).join(', ')}] bounds=[${bounds.map(b => `(${b.x},${b.y},${b.width},${b.height})`).join(', ')}]`); Logger.info(`_tileAccordion: ratios=[${this._splitRatios.map(r => r.toFixed(3)).join(', ')}] bounds=[${bounds.map(b => `(${b.x},${b.y},${b.width},${b.height})`).join(', ')}]`);
this._tiledItems.forEach((item, index) => { this._tiledItems.forEach((item, index) => {
const rect = bounds[index]; const rect = bounds[index];
if (item instanceof WindowContainer) { if (item instanceof WindowContainer) {
item.move(rect); item.move(rect);
} else { } else {
Logger.info(`_tileItems: window[${index}] id=${item.getWindowId()} dragging=${item._dragging} → rect=(${rect.x},${rect.y},${rect.width},${rect.height})`); Logger.info(`_tileAccordion: window[${index}] id=${item.getWindowId()} dragging=${item._dragging} → rect=(${rect.x},${rect.y},${rect.width},${rect.height})`);
item.safelyResizeWindow(rect); item.safelyResizeWindow(rect);
} }
}); });
} }
// ─── Bounds Calculation ────────────────────────────────────────────────────── private _tileTab(): void {
if (this._tiledItems.length === 0) return;
getBounds(): Rect[] { const tabBarRect: Rect = {
if (this._orientation === Orientation.HORIZONTAL) {
return this.getHorizontalBounds();
}
return this.getVerticalBounds();
}
getVerticalBounds(): Rect[] {
const items = this._tiledItems;
const totalHeight = this._workArea.height;
let usedHeight = 0;
return items.map((_, index) => {
const y = this._workArea.y + usedHeight;
let height: number;
if (index === items.length - 1) {
// Last item gets the remainder to avoid pixel gaps from rounding
height = totalHeight - usedHeight;
} else {
height = Math.floor(this._splitRatios[index] * totalHeight);
}
usedHeight += height;
return {
x: this._workArea.x, x: this._workArea.x,
y: y,
width: this._workArea.width,
height: height,
} as Rect;
});
}
getHorizontalBounds(): Rect[] {
const totalWidth = this._workArea.width;
let usedWidth = 0;
return this._tiledItems.map((_, index) => {
const x = this._workArea.x + usedWidth;
let width: number;
if (index === this._tiledItems.length - 1) {
// Last item gets the remainder to avoid pixel gaps from rounding
width = totalWidth - usedWidth;
} else {
width = Math.floor(this._splitRatios[index] * totalWidth);
}
usedWidth += width;
return {
x: x,
y: this._workArea.y, y: this._workArea.y,
width: width, width: this._workArea.width,
height: this._workArea.height, height: TAB_BAR_HEIGHT,
} as Rect; };
});
const contentRect: Rect = {
x: this._workArea.x,
y: this._workArea.y + TAB_BAR_HEIGHT,
width: this._workArea.width,
height: this._workArea.height - TAB_BAR_HEIGHT,
};
// Position and show the tab bar
if (this._tabBar) {
this._tabBar.setPosition(tabBarRect);
if (!this._tabBar.isVisible()) {
this._rebuildAndShowTabBar();
}
} }
// ─── Boundary / Ratio Adjustment ───────────────────────────────────────────── this._applyTabVisibility();
const activeItem = this._tiledItems[this._activeTabIndex];
if (activeItem) {
if (activeItem instanceof WindowContainer) {
activeItem.move(contentRect);
} else {
Logger.info(`_tileTabbed: active tab[${this._activeTabIndex}] id=${activeItem.getWindowId()} → rect=(${contentRect.x},${contentRect.y},${contentRect.width},${contentRect.height})`);
activeItem.safelyResizeWindow(contentRect);
}
}
}
/** /**
* Adjust the boundary between item[boundaryIndex] and item[boundaryIndex+1] * Show the active tab window, hide all others.
* by deltaPixels (positive = move right/down, negative = move left/up).
*
* Both affected ratios are clamped to [_minRatio, 1 - _minRatio] so no
* window can be squashed below the configured minimum.
*
* Returns true if the adjustment was applied, false if it was rejected
* (e.g. out of bounds index or clamping would violate minimum).
*/ */
adjustBoundary(boundaryIndex: number, deltaPixels: number, minRatio: number = 0.10): boolean { private _applyTabVisibility(): void {
this._tiledItems.forEach((item, index) => {
if (item instanceof WindowWrapper) {
if (index === this._activeTabIndex) {
item.showWindow();
} else {
item.hideWindow();
}
}
});
}
/**
* Show all windows (used when leaving tabbed mode).
*/
private _showAllWindows(): void {
this._tiledItems.forEach((item) => {
if (item instanceof WindowWrapper) {
item.showWindow();
}
});
}
/**
* Rebuild the tab bar buttons and show it.
*/
private _rebuildAndShowTabBar(): void {
if (!this._tabBar) return;
const windowItems = this._tiledItems.filter(
(item): item is WindowWrapper => item instanceof WindowWrapper
);
this._tabBar.rebuild(windowItems, this._activeTabIndex);
this._tabBar.show();
}
/**
* Update tab bar state (active highlight, titles) without a full rebuild.
*/
private _updateTabBar(): void {
if (!this._tabBar) return;
// Rebuild is cheap — just recreate buttons from the current items
const windowItems = this._tiledItems.filter(
(item): item is WindowWrapper => item instanceof WindowWrapper
);
this._tabBar.rebuild(windowItems, this._activeTabIndex);
}
getBounds(): Rect[] {
if (this._orientation === Layout.TABBED) {
// In tabbed mode, all items share the same content rect
const contentRect: Rect = {
x: this._workArea.x,
y: this._workArea.y + TAB_BAR_HEIGHT,
width: this._workArea.width,
height: this._workArea.height - TAB_BAR_HEIGHT,
};
return this._tiledItems.map(() => contentRect);
}
return this._orientation === Layout.ACC_HORIZONTAL
? this._computeBounds('horizontal')
: this._computeBounds('vertical');
}
private _computeBounds(axis: 'horizontal' | 'vertical'): Rect[] {
const isHorizontal = axis === 'horizontal';
const total = isHorizontal ? this._workArea.width : this._workArea.height;
let used = 0;
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};
});
}
adjustBoundary(boundaryIndex: number, deltaPixels: number): boolean {
// No boundary adjustment in tabbed mode
if (this.isTabbed()) return false;
if (boundaryIndex < 0 || boundaryIndex >= this._tiledItems.length - 1) { if (boundaryIndex < 0 || boundaryIndex >= this._tiledItems.length - 1) {
Logger.warn(`adjustBoundary: invalid boundaryIndex ${boundaryIndex}`); Logger.warn(`adjustBoundary: invalid boundaryIndex ${boundaryIndex}`);
return false; return false;
@@ -275,12 +439,11 @@ export default class WindowContainer {
if (totalDim === 0) return false; if (totalDim === 0) return false;
const ratioDelta = deltaPixels / totalDim; const ratioDelta = deltaPixels / totalDim;
const newLeft = this._splitRatios[boundaryIndex] + ratioDelta; const newLeft = this._splitRatios[boundaryIndex] + ratioDelta;
const newRight = this._splitRatios[boundaryIndex + 1] - ratioDelta; const newRight = this._splitRatios[boundaryIndex + 1] - ratioDelta;
if (newLeft < minRatio || newRight < minRatio) { if (newLeft <= 0 || newRight <= 0) {
Logger.log(`adjustBoundary: clamped — newLeft=${newLeft.toFixed(3)}, newRight=${newRight.toFixed(3)}, min=${minRatio}`); Logger.log(`adjustBoundary: clamped — newLeft=${newLeft.toFixed(3)}, newRight=${newRight.toFixed(3)}`);
return false; return false;
} }
@@ -291,38 +454,13 @@ export default class WindowContainer {
return true; return true;
} }
/** // --- Container Lookup --------------------------------------------------------
* Adjust boundaries on BOTH axes simultaneously for corner resize ops.
* horizontalDelta applies to this container if HORIZONTAL, verticalDelta if VERTICAL.
* For nested containers the perpendicular delta is forwarded to the child container.
*
* boundaryIndex: the slot index whose right/bottom edge is being dragged.
*/
adjustBoundaryBothAxes(
boundaryIndex: number,
horizontalDelta: number,
verticalDelta: number,
): void {
if (this._orientation === Orientation.HORIZONTAL) {
this.adjustBoundary(boundaryIndex, horizontalDelta);
} else {
this.adjustBoundary(boundaryIndex, verticalDelta);
}
}
// ─── Container Lookup ────────────────────────────────────────────────────────
/**
* Returns the direct-parent WindowContainer that contains win_id as an
* immediate child (not recursed further). Returns null if not found.
*/
getContainerForWindow(win_id: number): WindowContainer | null { getContainerForWindow(win_id: number): WindowContainer | null {
for (const item of this._tiledItems) { for (const item of this._tiledItems) {
if (item instanceof WindowWrapper && item.getWindowId() === win_id) { if (item instanceof WindowWrapper && item.getWindowId() === win_id) {
return this; return this;
} }
}
for (const item of this._tiledItems) {
if (item instanceof WindowContainer) { if (item instanceof WindowContainer) {
const found = item.getContainerForWindow(win_id); const found = item.getContainerForWindow(win_id);
if (found !== null) return found; if (found !== null) return found;
@@ -335,10 +473,7 @@ export default class WindowContainer {
for (let i = 0; i < this._tiledItems.length; i++) { for (let i = 0; i < this._tiledItems.length; i++) {
const container = this._tiledItems[i]; const container = this._tiledItems[i];
if (container instanceof WindowContainer) { if (container instanceof WindowContainer) {
const index = container.getIndexOfItemNested(item); if (container.getIndexOfItemNested(item) !== -1) return i;
if (index !== -1) {
return i;
}
} else if (container.getWindowId() === item.getWindowId()) { } else if (container.getWindowId() === item.getWindowId()) {
return i; return i;
} }
@@ -348,36 +483,36 @@ export default class WindowContainer {
// TODO: update this to work with nested containers - all other logic should already be working // TODO: update this to work with nested containers - all other logic should already be working
itemDragged(item: WindowWrapper, x: number, y: number): void { itemDragged(item: WindowWrapper, x: number, y: number): void {
let original_index = this.getIndexOfItemNested(item); // In tabbed mode, dragging reorders tabs but doesn't change layout
if (this.isTabbed()) {
// Don't reorder during tabbed mode — tabs have a fixed visual layout
return;
}
const original_index = this.getIndexOfItemNested(item);
if (original_index === -1) { if (original_index === -1) {
Logger.error("Item not found in container during drag op", item.getWindowId()); Logger.error("Item not found in container during drag op", item.getWindowId());
return; return;
} }
let new_index = original_index; let new_index = original_index;
this.getBounds().forEach((rect, index) => { this.getBounds().forEach((rect, index) => {
if (rect.x < x && rect.x + rect.width > x && rect.y < y && rect.y + rect.height > y) { if (rect.x < x && rect.x + rect.width > x && rect.y < y && rect.y + rect.height > y) {
new_index = index; new_index = index;
} }
}); });
if (original_index !== new_index) { if (original_index !== new_index) {
// Swap only the items — ratios stay with their slots. Logger.info(`itemDragged: swapped slots ${original_index}<->${new_index}, ratios=[${this._splitRatios.map(r => r.toFixed(3)).join(', ')}]`);
// e.g. slot 0 = 40%, slot 1 = 60%: when the window in slot 1 drags
// into slot 0, it takes slot 0's 40% size. The window it displaces
// moves to slot 1 and takes the 60% size. The slot ratios are unchanged.
[this._tiledItems[original_index], this._tiledItems[new_index]] = [this._tiledItems[original_index], this._tiledItems[new_index]] =
[this._tiledItems[new_index], this._tiledItems[original_index]]; [this._tiledItems[new_index], this._tiledItems[original_index]];
Logger.info(`itemDragged: swapped slots ${original_index}<->${new_index}, ratios=[${this._splitRatios.map(r => r.toFixed(3)).join(', ')}]`); this.drawWindows();
this.tileWindows();
} }
} }
/**
* Reset all split ratios in this container to equal fractions.
* Called when the user explicitly requests an equal-split reset (e.g. Ctrl+Z).
*/
resetRatios(): void { resetRatios(): void {
this._resetRatios(); this._resetRatios();
this.tileWindows(); this.drawWindows();
} }
} }
+17 -13
View File
@@ -1,12 +1,8 @@
import {WindowWrapper} from "./window.js"; import {WindowWrapper} from "./window.js";
import {Rect} from "../utils/rect.js"; import {Rect} from "../utils/rect.js";
import queueEvent from "../utils/events.js";
import {Logger} from "../utils/logger.js"; import {Logger} from "../utils/logger.js";
import Meta from "gi://Meta";
import Mtk from "@girs/mtk-17";
import WindowContainer from "./container.js"; import WindowContainer from "./container.js";
import Window = Meta.Window;
export default class Monitor { export default class Monitor {
@@ -20,7 +16,7 @@ export default class Monitor {
this._workArea = workspace.get_work_area_for_monitor(this._id); this._workArea = workspace.get_work_area_for_monitor(this._id);
Logger.log("CREATING MONITOR", monitorId); Logger.log("CREATING MONITOR", monitorId);
Logger.log("WorkArea", this._workArea.x, this._workArea.y, this._workArea.width, this._workArea.height); Logger.log("WorkArea", this._workArea.x, this._workArea.y, this._workArea.width, this._workArea.height);
const workspaceCount = global.workspace_manager.get_n_workspaces() const workspaceCount = global.workspace_manager.get_n_workspaces();
Logger.log("Workspace Count", workspaceCount); Logger.log("Workspace Count", workspaceCount);
for (let i = 0; i < workspaceCount; i++) { for (let i = 0; i < workspaceCount; i++) {
this._workspaces.push(new WindowContainer(this._workArea)); this._workspaces.push(new WindowContainer(this._workArea));
@@ -42,9 +38,7 @@ export default class Monitor {
getWindow(windowId: number): WindowWrapper | undefined { getWindow(windowId: number): WindowWrapper | undefined {
for (const container of this._workspaces) { for (const container of this._workspaces) {
const win = container.getWindow(windowId); const win = container.getWindow(windowId);
if (win) { if (win) return win;
return win;
}
} }
return undefined; return undefined;
} }
@@ -52,8 +46,7 @@ export default class Monitor {
removeWindow(winWrap: WindowWrapper) { removeWindow(winWrap: WindowWrapper) {
const windowId = winWrap.getWindowId(); const windowId = winWrap.getWindowId();
for (const container of this._workspaces) { for (const container of this._workspaces) {
const win = container.getWindow(windowId); if (container.getWindow(windowId)) {
if (win) {
container.removeWindow(windowId); container.removeWindow(windowId);
} }
} }
@@ -65,9 +58,9 @@ export default class Monitor {
} }
tileWindows(): void { tileWindows(): void {
this._workArea = global.workspace_manager.get_active_workspace().get_work_area_for_monitor(this._id);
const activeWorkspace = global.workspace_manager.get_active_workspace(); const activeWorkspace = global.workspace_manager.get_active_workspace();
// move() already calls tileWindows() internally — don't call it again this._workArea = activeWorkspace.get_work_area_for_monitor(this._id);
// move() calls tileWindows() internally
this._workspaces[activeWorkspace.index()].move(this._workArea); this._workspaces[activeWorkspace.index()].move(this._workArea);
} }
@@ -79,8 +72,19 @@ export default class Monitor {
this._workspaces.push(new WindowContainer(this._workArea)); this._workspaces.push(new WindowContainer(this._workArea));
} }
hideTabBars(): void {
for (const container of this._workspaces) {
container.hideTabBar();
}
}
showTabBars(): void {
for (const container of this._workspaces) {
container.showTabBar();
}
}
itemDragged(item: WindowWrapper, x: number, y: number): void { itemDragged(item: WindowWrapper, x: number, y: number): void {
this._workspaces[item.getWorkspace()].itemDragged(item, x, y); this._workspaces[item.getWorkspace()].itemDragged(item, x, y);
} }
} }
+125
View File
@@ -0,0 +1,125 @@
import Clutter from 'gi://Clutter';
import St from 'gi://St';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import {Logger} from "../utils/logger.js";
import {WindowWrapper} from "./window.js";
import {Rect} from "../utils/rect.js";
export const TAB_BAR_HEIGHT = 24;
type TabClickedCallback = (index: number) => void;
export class TabBar {
private _bar: St.BoxLayout;
private _buttons: St.Button[] = [];
private _activeIndex: number = 0;
private _onTabClicked: TabClickedCallback;
private _visible: boolean = false;
constructor(onTabClicked: TabClickedCallback) {
this._onTabClicked = onTabClicked;
this._bar = new St.BoxLayout({
style_class: 'aerospike-tab-bar',
vertical: false,
reactive: true,
can_focus: false,
track_hover: false,
});
}
/**
* Rebuild all tab buttons from the current list of window items.
*/
rebuild(items: WindowWrapper[], activeIndex: number): void {
// Remove old buttons
this._bar.destroy_all_children();
this._buttons = [];
items.forEach((item, index) => {
const button = new St.Button({
style_class: 'aerospike-tab',
reactive: true,
can_focus: false,
track_hover: true,
x_expand: true,
child: new St.Label({
text: item.getTabLabel(),
style_class: 'aerospike-tab-label',
y_align: Clutter.ActorAlign.CENTER,
x_align: Clutter.ActorAlign.CENTER,
x_expand: true,
}),
});
button.connect('clicked', () => {
this._onTabClicked(index);
});
this._bar.add_child(button);
this._buttons.push(button);
});
this.setActive(activeIndex);
}
/**
* Update just the title text of a single tab (e.g. when a window title changes).
*/
updateTabTitle(index: number, title: string): void {
if (index < 0 || index >= this._buttons.length) return;
const label = this._buttons[index].get_child() as St.Label;
if (label) label.set_text(title);
}
/**
* Highlight the active tab and dim the rest.
*/
setActive(index: number): void {
this._activeIndex = index;
this._buttons.forEach((btn, i) => {
if (i === index) {
btn.add_style_class_name('aerospike-tab-active');
} else {
btn.remove_style_class_name('aerospike-tab-active');
}
});
}
/**
* Position and size the tab bar at the given screen rect.
*/
setPosition(rect: Rect): void {
this._bar.set_position(rect.x, rect.y);
this._bar.set_size(rect.width, rect.height);
}
show(): void {
if (this._visible) return;
this._visible = true;
Main.layoutManager.uiGroup.add_child(this._bar);
this._bar.show();
Logger.log("TabBar shown");
}
hide(): void {
if (!this._visible) return;
this._visible = false;
this._bar.hide();
if (this._bar.get_parent()) {
Main.layoutManager.uiGroup.remove_child(this._bar);
}
Logger.log("TabBar hidden");
}
destroy(): void {
this.hide();
this._bar.destroy_all_children();
this._buttons = [];
this._bar.destroy();
Logger.log("TabBar destroyed");
}
isVisible(): boolean {
return this._visible;
}
}
+53 -37
View File
@@ -11,6 +11,8 @@ type WindowMinimizedHandler = (window: WindowWrapper) => void;
type WindowWorkspaceChangedHandler = (window: WindowWrapper) => void; type WindowWorkspaceChangedHandler = (window: WindowWrapper) => void;
export class WindowWrapper { export class WindowWrapper {
private static readonly RESIZE_TOLERANCE = 2;
readonly _window: Meta.Window; readonly _window: Meta.Window;
readonly _windowMinimizedHandler: WindowMinimizedHandler; readonly _windowMinimizedHandler: WindowMinimizedHandler;
readonly _signals: number[] = []; readonly _signals: number[] = [];
@@ -45,44 +47,48 @@ export class WindowWrapper {
return this._window.get_frame_rect(); return this._window.get_frame_rect();
} }
getTabLabel(): string {
const appName = this._window.get_wm_class() ?? '';
const title = this._window.get_title() ?? 'Untitled';
if (appName && appName.toLowerCase() !== title.toLowerCase()) {
return `${appName} | ${title}`;
}
return title;
}
hideWindow(): void {
const actor = this._window.get_compositor_private() as Clutter.Actor | null;
if (actor) actor.hide();
}
showWindow(): void {
const actor = this._window.get_compositor_private() as Clutter.Actor | null;
if (actor) actor.show();
}
startDragging(): void { startDragging(): void {
this._dragging = true; this._dragging = true;
} }
stopDragging(): void { stopDragging(): void {
Logger.log("STOPPED DRAGGING") Logger.log("STOPPED DRAGGING")
this._dragging = false; this._dragging = false;
} }
// setParent(parent: WindowContainer): void { connectWindowSignals(windowManager: IWindowManager): void {
// this._parent = parent; const windowId = this._window.get_id();
// }
//
// getParent(): WindowContainer | null {
// if (this._parent == null) {
// Logger.warn(`Attempting to get parent for window without parent ${JSON.stringify(this)}`);
// }
// return this._parent
// }
connectWindowSignals(
windowManager: IWindowManager,
): void {
const windowId = this._window.get_id()
// Handle window destruction
this._signals.push( this._signals.push(
this._window.connect('unmanaging', window => { this._window.connect('unmanaging', () => {
Logger.log("REMOVING WINDOW", windowId); Logger.log("REMOVING WINDOW", windowId);
windowManager.handleWindowClosed(this) windowManager.handleWindowClosed(this);
}), }),
this._window.connect('notify::minimized', (we) => { this._window.connect('notify::minimized', () => {
if (this._window.minimized) { if (this._window.minimized) {
Logger.log(`Window minimized: ${windowId}`); Logger.log(`Window minimized: ${windowId}`);
windowManager.handleWindowMinimized(this); windowManager.handleWindowMinimized(this);
} else {
} else if (!this._window.minimized) {
Logger.log(`Window unminimized: ${windowId}`); Logger.log(`Window unminimized: ${windowId}`);
windowManager.handleWindowUnminimized(this); windowManager.handleWindowUnminimized(this);
} }
}), }),
this._window.connect('notify::maximized-horizontally', () => { this._window.connect('notify::maximized-horizontally', () => {
@@ -92,21 +98,20 @@ export class WindowWrapper {
Logger.log(`Window unmaximized: ${windowId}`); Logger.log(`Window unmaximized: ${windowId}`);
} }
}), }),
this._window.connect("workspace-changed", (_metaWindow) => { this._window.connect("workspace-changed", () => {
Logger.log("WORKSPACE CHANGED FOR WINDOW", this._window.get_id()); Logger.log("WORKSPACE CHANGED FOR WINDOW", this._window.get_id());
windowManager.handleWindowChangedWorkspace(this); windowManager.handleWindowChangedWorkspace(this);
}), }),
this._window.connect("position-changed", (_metaWindow) => { this._window.connect("position-changed", () => {
windowManager.handleWindowPositionChanged(this); windowManager.handleWindowPositionChanged(this);
}), }),
this._window.connect("size-changed", (_metaWindow) => { this._window.connect("size-changed", () => {
windowManager.handleWindowPositionChanged(this); windowManager.handleWindowPositionChanged(this);
}), }),
); );
} }
disconnectWindowSignals(): void { disconnectWindowSignals(): void {
if (this._signals) { if (this._signals) {
this._signals.forEach(signal => { this._signals.forEach(signal => {
try { try {
@@ -134,29 +139,40 @@ export class WindowWrapper {
actor.remove_all_transitions(); actor.remove_all_transitions();
// Single call: move + resize atomically // Move first to guarantee the window reaches the correct position even
// if the subsequent resize is clamped by minimum-size hints.
this._window.move_frame(true, rect.x, rect.y);
this._window.move_resize_frame(true, rect.x, rect.y, rect.width, rect.height); this._window.move_resize_frame(true, rect.x, rect.y, rect.width, rect.height);
const new_rect = this._window.get_frame_rect(); const new_rect = this._window.get_frame_rect();
const TOLERANCE = 2; // pixels — allow compositor rounding
const mismatch = const mismatch =
Math.abs(new_rect.x - rect.x) > TOLERANCE || Math.abs(new_rect.x - rect.x) > WindowWrapper.RESIZE_TOLERANCE ||
Math.abs(new_rect.y - rect.y) > TOLERANCE || Math.abs(new_rect.y - rect.y) > WindowWrapper.RESIZE_TOLERANCE ||
Math.abs(new_rect.width - rect.width) > TOLERANCE || Math.abs(new_rect.width - rect.width) > WindowWrapper.RESIZE_TOLERANCE ||
Math.abs(new_rect.height - rect.height) > TOLERANCE; Math.abs(new_rect.height - rect.height) > WindowWrapper.RESIZE_TOLERANCE;
if (_retry > 0 && mismatch) { if (_retry > 0 && mismatch) {
// If the window's actual size is larger than requested, it has a
// minimum-size constraint — retrying won't help. Just make sure
// it's at the correct position with its actual size.
const sizeConstrained =
new_rect.width > rect.width + WindowWrapper.RESIZE_TOLERANCE ||
new_rect.height > rect.height + WindowWrapper.RESIZE_TOLERANCE;
if (sizeConstrained) {
Logger.info("Window has min-size constraint, accepting actual size",
`want(${rect.x},${rect.y},${rect.width},${rect.height})`,
`actual(${new_rect.x},${new_rect.y},${new_rect.width},${new_rect.height})`);
this._window.move_frame(true, rect.x, rect.y);
} else {
Logger.warn("RESIZE MISMATCH, retrying", Logger.warn("RESIZE MISMATCH, retrying",
`want(${rect.x},${rect.y},${rect.width},${rect.height})`, `want(${rect.x},${rect.y},${rect.width},${rect.height})`,
`got(${new_rect.x},${new_rect.y},${new_rect.width},${new_rect.height})`); `got(${new_rect.x},${new_rect.y},${new_rect.width},${new_rect.height})`);
queueEvent({ queueEvent({
name: `delayed_resize_${this.getWindowId()}`, name: `delayed_resize_${this.getWindowId()}`,
callback: () => { callback: () => this.safelyResizeWindow(rect, _retry - 1),
this.safelyResizeWindow(rect, _retry - 1);
}
}, 50); }, 50);
} }
} }
}
} }
+65 -231
View File
@@ -1,21 +1,17 @@
import Meta from "gi://Meta"; import Meta from "gi://Meta";
import Gio from "gi://Gio"; import Gio from "gi://Gio";
// import GLib from "gi://GLib";
import {WindowWrapper} from './window.js'; import {WindowWrapper} from './window.js';
import * as Main from "resource:///org/gnome/shell/ui/main.js"; import * as Main from "resource:///org/gnome/shell/ui/main.js";
// import Mtk from "@girs/mtk-16";
import {Logger} from "../utils/logger.js"; import {Logger} from "../utils/logger.js";
import Monitor from "./monitor.js"; import Monitor from "./monitor.js";
import WindowContainer from "./container.js"; import WindowContainer, {Layout} from "./container.js";
import {Rect} from "../utils/rect.js"; import {Rect} from "../utils/rect.js";
export interface IWindowManager { export interface IWindowManager {
_activeWindowId: number | null; _activeWindowId: number | null;
// addWindow(window: Meta.Window): void;
handleWindowClosed(winWrap: WindowWrapper): void; handleWindowClosed(winWrap: WindowWrapper): void;
handleWindowMinimized(winWrap: WindowWrapper): void; handleWindowMinimized(winWrap: WindowWrapper): void;
@@ -30,8 +26,8 @@ export interface IWindowManager {
} }
const _UNUSED_MONITOR_ID = -1 const _UNUSED_MONITOR_ID = -1;
const _UNUSED_WINDOW_ID = -1 const _UNUSED_WINDOW_ID = -1;
export default class WindowManager implements IWindowManager { export default class WindowManager implements IWindowManager {
_displaySignals: number[] = []; _displaySignals: number[] = [];
@@ -41,37 +37,29 @@ export default class WindowManager implements IWindowManager {
_activeWindowId: number | null = null; _activeWindowId: number | null = null;
_monitors: Map<number, Monitor> = new Map<number, Monitor>(); _monitors: Map<number, Monitor> = new Map<number, Monitor>();
_minimizedItems: Map<number, WindowWrapper> = new Map<number, WindowWrapper>(); _minimizedItems: Map<number, WindowWrapper> = new Map<number, WindowWrapper>();
_grabbedWindowMonitor: number = _UNUSED_MONITOR_ID; _grabbedWindowMonitor: number = _UNUSED_MONITOR_ID;
_grabbedWindowId: number = _UNUSED_WINDOW_ID; _grabbedWindowId: number = _UNUSED_WINDOW_ID;
_changingGrabbedMonitor: boolean = false; _changingGrabbedMonitor: boolean = false;
_showingOverview: boolean = false; _showingOverview: boolean = false;
// ── Resize-drag tracking ────────────────────────────────────────────────── // -- Resize-drag tracking --------------------------------------------------
_isResizeDrag: boolean = false; _isResizeDrag: boolean = false;
_resizeDragWindowId: number = _UNUSED_WINDOW_ID; _resizeDragWindowId: number = _UNUSED_WINDOW_ID;
_resizeDragOp: Meta.GrabOp = Meta.GrabOp.NONE; _resizeDragOp: Meta.GrabOp = Meta.GrabOp.NONE;
/** Mouse position at the start of each incremental resize step. */
_resizeDragLastMouseX: number = 0; _resizeDragLastMouseX: number = 0;
_resizeDragLastMouseY: number = 0; _resizeDragLastMouseY: number = 0;
/** Re-entrancy guard: true while tileWindows is propagating position-changed events. */
_isTiling: boolean = false; _isTiling: boolean = false;
_settings: Gio.Settings | null = null; private readonly _settings: Gio.Settings;
constructor() {} constructor(settings: Gio.Settings) {
this._settings = settings;
/** Returns the live min-ratio value from settings, falling back to 0.10. */
private _getMinRatio(): number {
return this._settings?.get_double('min-window-size-percent') ?? 0.10;
} }
public enable(): void { public enable(): void {
Logger.log("Starting Aerospike Window Manager"); Logger.log("Starting Aerospike Window Manager");
// Connect window signals
this.instantiateDisplaySignals(); this.instantiateDisplaySignals();
const mon_count = global.display.get_n_monitors(); const mon_count = global.display.get_n_monitors();
@@ -80,8 +68,6 @@ export default class WindowManager implements IWindowManager {
} }
this.captureExistingWindows(); this.captureExistingWindows();
// Sync the initially focused window
this.syncActiveWindow(); this.syncActiveWindow();
} }
@@ -108,7 +94,6 @@ export default class WindowManager implements IWindowManager {
global.display.connect('notify::focus-window', () => { global.display.connect('notify::focus-window', () => {
this.syncActiveWindow(); this.syncActiveWindow();
}), }),
global.display.connect("showing-desktop-changed", () => { global.display.connect("showing-desktop-changed", () => {
Logger.log("SHOWING DESKTOP CHANGED"); Logger.log("SHOWING DESKTOP CHANGED");
}), }),
@@ -119,13 +104,7 @@ export default class WindowManager implements IWindowManager {
global.display.connect("in-fullscreen-changed", () => { global.display.connect("in-fullscreen-changed", () => {
Logger.log("IN FULL SCREEN CHANGED"); Logger.log("IN FULL SCREEN CHANGED");
}), }),
) );
// this._windowManagerSignals = [
// global.window_manager.connect("show-tile-preview", (_, _metaWindow, _rect, _num) => {
// Logger.log("SHOW TITLE PREVIEW!")
// }),
// ];
this._workspaceManagerSignals = [ this._workspaceManagerSignals = [
global.workspace_manager.connect("showing-desktop-changed", () => { global.workspace_manager.connect("showing-desktop-changed", () => {
@@ -150,45 +129,37 @@ export default class WindowManager implements IWindowManager {
this._overviewSignals = [ this._overviewSignals = [
Main.overview.connect("hiding", () => { Main.overview.connect("hiding", () => {
// this.fromOverview = true;
Logger.log("HIDING OVERVIEW") Logger.log("HIDING OVERVIEW")
this._showingOverview = false; this._showingOverview = false;
this._tileMonitors(); this._tileMonitors();
// const eventObj = { for (const monitor of this._monitors.values()) {
// name: "focus-after-overview", monitor.showTabBars();
// callback: () => { }
// Logger.log("FOCUSING AFTER OVERVIEW");
// },
// };
// this.queueEvent(eventObj);
}), }),
Main.overview.connect("showing", () => { Main.overview.connect("showing", () => {
this._showingOverview = true; this._showingOverview = true;
Logger.log("SHOWING OVERVIEW"); Logger.log("SHOWING OVERVIEW");
for (const monitor of this._monitors.values()) {
monitor.hideTabBars();
}
}), }),
]; ];
} }
public disable(): void { public disable(): void {
Logger.log("DISABLED AEROSPIKE WINDOW MANAGER!") Logger.log("DISABLED AEROSPIKE WINDOW MANAGER!")
// Disconnect the focus signal and remove any existing borders
this.disconnectSignals(); this.disconnectSignals();
this.removeAllWindows(); this.removeAllWindows();
} }
removeAllWindows(): void { removeAllWindows(): void {
// Disconnect signals from minimized windows before clearing
this.disconnectMinimizedSignals(); this.disconnectMinimizedSignals();
this._minimizedItems.clear(); this._minimizedItems.clear();
this._monitors.forEach((monitor: Monitor) => { this._monitors.forEach((monitor: Monitor) => {
monitor.removeAllWindows(); monitor.removeAllWindows();
}) })
} }
disconnectSignals(): void { disconnectSignals(): void {
this.disconnectDisplaySignals(); this.disconnectDisplaySignals();
this.disconnectMonitorSignals(); this.disconnectMonitorSignals();
@@ -222,10 +193,6 @@ export default class WindowManager implements IWindowManager {
}) })
} }
/**
* Returns true if the grab op is a resize operation (any edge or corner).
*/
_isResizeOp(op: Meta.GrabOp): boolean { _isResizeOp(op: Meta.GrabOp): boolean {
return op === Meta.GrabOp.RESIZING_E || return op === Meta.GrabOp.RESIZING_E ||
op === Meta.GrabOp.RESIZING_W || op === Meta.GrabOp.RESIZING_W ||
@@ -241,7 +208,6 @@ export default class WindowManager implements IWindowManager {
Logger.log("Grab Op Start", op); Logger.log("Grab Op Start", op);
if (this._isResizeOp(op)) { if (this._isResizeOp(op)) {
// ── Resize drag ──────────────────────────────────────────────────
Logger.log("Resize drag begin, op=", op); Logger.log("Resize drag begin, op=", op);
this._isResizeDrag = true; this._isResizeDrag = true;
this._resizeDragWindowId = window.get_id(); this._resizeDragWindowId = window.get_id();
@@ -249,11 +215,8 @@ export default class WindowManager implements IWindowManager {
const [startMouseX, startMouseY] = global.get_pointer(); const [startMouseX, startMouseY] = global.get_pointer();
this._resizeDragLastMouseX = startMouseX; this._resizeDragLastMouseX = startMouseX;
this._resizeDragLastMouseY = startMouseY; this._resizeDragLastMouseY = startMouseY;
// Mark the window as dragging so safelyResizeWindow skips it while
// we tile the other windows in response to ratio changes.
this._getWrappedWindow(window)?.startDragging(); this._getWrappedWindow(window)?.startDragging();
} else { } else {
// ── Move drag (existing behaviour) ───────────────────────────────
this._getWrappedWindow(window)?.startDragging(); this._getWrappedWindow(window)?.startDragging();
this._grabbedWindowMonitor = window.get_monitor(); this._grabbedWindowMonitor = window.get_monitor();
this._grabbedWindowId = window.get_id(); this._grabbedWindowId = window.get_id();
@@ -264,19 +227,15 @@ export default class WindowManager implements IWindowManager {
Logger.log("Grab Op End ", op); Logger.log("Grab Op End ", op);
if (this._isResizeDrag) { if (this._isResizeDrag) {
// ── Resize drag end ──────────────────────────────────────────────
Logger.log("Resize drag end, op=", op); Logger.log("Resize drag end, op=", op);
this._isResizeDrag = false; this._isResizeDrag = false;
this._resizeDragWindowId = _UNUSED_WINDOW_ID; this._resizeDragWindowId = _UNUSED_WINDOW_ID;
this._resizeDragLastMouseX = 0; this._resizeDragLastMouseX = 0;
this._resizeDragLastMouseY = 0; this._resizeDragLastMouseY = 0;
this._resizeDragOp = Meta.GrabOp.NONE; this._resizeDragOp = Meta.GrabOp.NONE;
// Stop suppressing the window, then snap everything to computed ratios
this._getWrappedWindow(window)?.stopDragging(); this._getWrappedWindow(window)?.stopDragging();
this._tileMonitors(); this._tileMonitors();
} else { } else {
// ── Move drag end (existing behaviour) ───────────────────────────
Logger.log("primary display", display.get_primary_monitor())
this._grabbedWindowId = _UNUSED_WINDOW_ID; this._grabbedWindowId = _UNUSED_WINDOW_ID;
this._getWrappedWindow(window)?.stopDragging(); this._getWrappedWindow(window)?.stopDragging();
this._tileMonitors(); this._tileMonitors();
@@ -288,9 +247,7 @@ export default class WindowManager implements IWindowManager {
let wrapped: WindowWrapper | undefined = undefined; let wrapped: WindowWrapper | undefined = undefined;
for (const monitor of this._monitors.values()) { for (const monitor of this._monitors.values()) {
wrapped = monitor.getWindow(window.get_id()); wrapped = monitor.getWindow(window.get_id());
if (wrapped !== undefined) { if (wrapped !== undefined) break;
break;
}
} }
return wrapped; return wrapped;
} }
@@ -320,21 +277,13 @@ export default class WindowManager implements IWindowManager {
} }
public handleWindowPositionChanged(winWrap: WindowWrapper): void { public handleWindowPositionChanged(winWrap: WindowWrapper): void {
// Ignore position changes that we triggered ourselves via tileWindows if (this._isTiling || this._changingGrabbedMonitor) return;
if (this._isTiling) {
return;
}
if (this._changingGrabbedMonitor) {
return;
}
// ── Live resize-drag handling ─────────────────────────────────────────
if (this._isResizeDrag && winWrap.getWindowId() === this._resizeDragWindowId) { if (this._isResizeDrag && winWrap.getWindowId() === this._resizeDragWindowId) {
this._handleResizeDragUpdate(winWrap); this._handleResizeDragUpdate(winWrap);
return; return;
} }
// ── Move-drag handling (existing behaviour) ───────────────────────────
if (winWrap.getWindowId() === this._grabbedWindowId) { if (winWrap.getWindowId() === this._grabbedWindowId) {
const [mouseX, mouseY, _] = global.get_pointer(); const [mouseX, mouseY, _] = global.get_pointer();
@@ -347,17 +296,14 @@ export default class WindowManager implements IWindowManager {
break; break;
} }
} }
if (monitorIndex === -1) { if (monitorIndex === -1) return;
return;
}
if (monitorIndex !== this._grabbedWindowMonitor) { if (monitorIndex !== this._grabbedWindowMonitor) {
this._changingGrabbedMonitor = true; this._changingGrabbedMonitor = true;
this._moveWindowToMonitor(winWrap.getWindow(), monitorIndex); this._moveWindowToMonitor(winWrap.getWindow(), monitorIndex);
this._changingGrabbedMonitor = false; this._changingGrabbedMonitor = false;
} }
// Guard _isTiling so that tileWindows() calls triggered by itemDragged
// (which repositions the displaced window) don't re-enter this handler.
this._isTiling = true; this._isTiling = true;
try { try {
this._monitors.get(monitorIndex)?.itemDragged(winWrap, mouseX, mouseY); this._monitors.get(monitorIndex)?.itemDragged(winWrap, mouseX, mouseY);
@@ -367,29 +313,19 @@ export default class WindowManager implements IWindowManager {
} }
} }
/**
* Called on every position-changed event while a resize drag is in progress.
* Computes the pixel delta from the drag-start rect, maps it to the correct
* container boundary, and calls adjustBoundary() for live feedback.
*/
private _handleResizeDragUpdate(winWrap: WindowWrapper): void { private _handleResizeDragUpdate(winWrap: WindowWrapper): void {
const op = this._resizeDragOp; const op = this._resizeDragOp;
const winId = winWrap.getWindowId(); const winId = winWrap.getWindowId();
// Read the current mouse position — this is unclamped by the compositor
// and always reflects the true user intent, unlike the window's frame rect
// which gets clamped when adjacent windows block expansion.
const [mouseX, mouseY] = global.get_pointer(); const [mouseX, mouseY] = global.get_pointer();
const dx = mouseX - this._resizeDragLastMouseX; const dx = mouseX - this._resizeDragLastMouseX;
const dy = mouseY - this._resizeDragLastMouseY; const dy = mouseY - this._resizeDragLastMouseY;
if (dx === 0 && dy === 0) return; if (dx === 0 && dy === 0) return;
// Update last position first so even if we return early the baseline advances
this._resizeDragLastMouseX = mouseX; this._resizeDragLastMouseX = mouseX;
this._resizeDragLastMouseY = mouseY; this._resizeDragLastMouseY = mouseY;
// Find the container that directly holds this window
const container = this._findContainerForWindowAcrossMonitors(winId); const container = this._findContainerForWindowAcrossMonitors(winId);
if (!container) { if (!container) {
Logger.warn("_handleResizeDragUpdate: no container found for window", winId); Logger.warn("_handleResizeDragUpdate: no container found for window", winId);
@@ -399,59 +335,44 @@ export default class WindowManager implements IWindowManager {
const itemIndex = container._getIndexOfWindow(winId); const itemIndex = container._getIndexOfWindow(winId);
if (itemIndex === -1) return; if (itemIndex === -1) return;
const isHorizontal = container._orientation === 0; // Orientation.HORIZONTAL const isHorizontal = container._orientation === Layout.ACC_HORIZONTAL;
// Map the mouse delta to the correct boundary. // E/S edge → boundary after the item; W/N edge → boundary before it.
//
// East/South edge → boundary AFTER the item (boundaryIndex = itemIndex)
// positive dx/dy grows this item, shrinks the next one.
// West/North edge → boundary BEFORE the item (boundaryIndex = itemIndex - 1)
// positive dx/dy moves the left edge right, growing the left neighbour
// and shrinking this item — so we negate the delta.
const minRatio = this._getMinRatio();
let adjusted = false; let adjusted = false;
if (isHorizontal) { if (isHorizontal) {
if (op === Meta.GrabOp.RESIZING_E || op === Meta.GrabOp.RESIZING_NE || op === Meta.GrabOp.RESIZING_SE) { if (op === Meta.GrabOp.RESIZING_E || op === Meta.GrabOp.RESIZING_NE || op === Meta.GrabOp.RESIZING_SE) {
adjusted = container.adjustBoundary(itemIndex, dx, minRatio); adjusted = container.adjustBoundary(itemIndex, dx);
} else if (op === Meta.GrabOp.RESIZING_W || op === Meta.GrabOp.RESIZING_NW || op === Meta.GrabOp.RESIZING_SW) { } else if (op === Meta.GrabOp.RESIZING_W || op === Meta.GrabOp.RESIZING_NW || op === Meta.GrabOp.RESIZING_SW) {
adjusted = container.adjustBoundary(itemIndex - 1, dx, minRatio); adjusted = container.adjustBoundary(itemIndex - 1, dx);
} }
} else { } else {
if (op === Meta.GrabOp.RESIZING_S || op === Meta.GrabOp.RESIZING_SE || op === Meta.GrabOp.RESIZING_SW) { if (op === Meta.GrabOp.RESIZING_S || op === Meta.GrabOp.RESIZING_SE || op === Meta.GrabOp.RESIZING_SW) {
adjusted = container.adjustBoundary(itemIndex, dy, minRatio); adjusted = container.adjustBoundary(itemIndex, dy);
} else if (op === Meta.GrabOp.RESIZING_N || op === Meta.GrabOp.RESIZING_NE || op === Meta.GrabOp.RESIZING_NW) { } else if (op === Meta.GrabOp.RESIZING_N || op === Meta.GrabOp.RESIZING_NE || op === Meta.GrabOp.RESIZING_NW) {
adjusted = container.adjustBoundary(itemIndex - 1, dy, minRatio); adjusted = container.adjustBoundary(itemIndex - 1, dy);
} }
} }
// Tile all windows with the updated ratios, guarded so the resulting
// position-changed events don't re-enter this handler.
if (adjusted) { if (adjusted) {
this._isTiling = true; this._isTiling = true;
try { try {
container.tileWindows(); container.drawWindows();
} finally { } finally {
this._isTiling = false; this._isTiling = false;
} }
} }
} }
/**
* Searches all monitors for the WindowContainer that directly holds win_id.
*/
private _findContainerForWindowAcrossMonitors(winId: number): WindowContainer | null { private _findContainerForWindowAcrossMonitors(winId: number): WindowContainer | null {
const activeWorkspaceIndex = global.workspace_manager.get_active_workspace().index(); const activeWorkspaceIndex = global.workspace_manager.get_active_workspace().index();
for (const monitor of this._monitors.values()) { for (const monitor of this._monitors.values()) {
if (activeWorkspaceIndex >= monitor._workspaces.length) continue; if (activeWorkspaceIndex >= monitor._workspaces.length) continue;
const workspace = monitor._workspaces[activeWorkspaceIndex]; const container = monitor._workspaces[activeWorkspaceIndex].getContainerForWindow(winId);
const container = workspace.getContainerForWindow(winId);
if (container !== null) return container; if (container !== null) return container;
} }
return null; return null;
} }
public handleWindowMinimized(winWrap: WindowWrapper): void { public handleWindowMinimized(winWrap: WindowWrapper): void {
const monitor_id = winWrap.getWindow().get_monitor() const monitor_id = winWrap.getWindow().get_monitor()
this._minimizedItems.set(winWrap.getWindowId(), winWrap); this._minimizedItems.set(winWrap.getWindowId(), winWrap);
@@ -465,7 +386,6 @@ export default class WindowManager implements IWindowManager {
this._tileMonitors() this._tileMonitors()
} }
public handleWindowChangedWorkspace(winWrap: WindowWrapper): void { public handleWindowChangedWorkspace(winWrap: WindowWrapper): void {
const monitor = winWrap.getWindow().get_monitor(); const monitor = winWrap.getWindow().get_monitor();
this._monitors.get(monitor)?.removeWindow(winWrap); this._monitors.get(monitor)?.removeWindow(winWrap);
@@ -484,41 +404,26 @@ export default class WindowManager implements IWindowManager {
this._tileMonitors(); this._tileMonitors();
} }
handleWindowCreated(display: Meta.Display, window: Meta.Window) { handleWindowCreated(display: Meta.Display, window: Meta.Window) {
Logger.log("WINDOW CREATED ON DISPLAY", window, display); Logger.log("WINDOW CREATED ON DISPLAY", window, display);
if (!this._isWindowTileable(window)) { if (!this._isWindowTileable(window)) return;
return;
}
Logger.log("WINDOW IS TILABLE"); Logger.log("WINDOW IS TILABLE");
this.addWindowToMonitor(window); this.addWindowToMonitor(window);
} }
/**
* Handle window closed event
*/
handleWindowClosed(window: WindowWrapper): void { handleWindowClosed(window: WindowWrapper): void {
const mon_id = window._window.get_monitor(); const mon_id = window._window.get_monitor();
this._monitors.get(mon_id)?.removeWindow(window); this._monitors.get(mon_id)?.removeWindow(window);
window.disconnectWindowSignals() window.disconnectWindowSignals()
// Remove from managed windows
this.syncActiveWindow(); this.syncActiveWindow();
// Retile remaining windows
this._tileMonitors(); this._tileMonitors();
} }
public addWindowToMonitor(window: Meta.Window) { public addWindowToMonitor(window: Meta.Window) {
Logger.log("ADDING WINDOW TO MONITOR", window, window); Logger.log("ADDING WINDOW TO MONITOR", window, window);
var wrapper = new WindowWrapper(window, (winWrap) => this.handleWindowMinimized(winWrap)) var wrapper = new WindowWrapper(window, (winWrap) => this.handleWindowMinimized(winWrap))
wrapper.connectWindowSignals(this); wrapper.connectWindowSignals(this);
this._addWindowWrapperToMonitor(wrapper); this._addWindowWrapperToMonitor(wrapper);
} }
_addWindowWrapperToMonitor(winWrap: WindowWrapper) { _addWindowWrapperToMonitor(winWrap: WindowWrapper) {
@@ -535,6 +440,8 @@ export default class WindowManager implements IWindowManager {
for (const monitor of this._monitors.values()) { for (const monitor of this._monitors.values()) {
monitor.tileWindows(); monitor.tileWindows();
} }
} catch (e) {
Logger.error("_tileMonitors FAILED", e);
} finally { } finally {
this._isTiling = false; this._isTiling = false;
} }
@@ -559,17 +466,12 @@ export default class WindowManager implements IWindowManager {
} }
_isWindowTileable(window: Meta.Window) { _isWindowTileable(window: Meta.Window) {
if (!window || !window.get_compositor_private()) return false;
if (this._isWindowTilingBlocked(window)) return false;
if (!window || !window.get_compositor_private()) {
return false;
}
if (this._isWindowTilingBlocked(window)) {
return false;
}
const windowType = window.get_window_type(); const windowType = window.get_window_type();
Logger.log("WINDOW TILING CHECK",); Logger.log("WINDOW TILING CHECK",);
// Skip certain types of windows
return !window.is_skip_taskbar() && return !window.is_skip_taskbar() &&
windowType !== Meta.WindowType.DESKTOP && windowType !== Meta.WindowType.DESKTOP &&
windowType !== Meta.WindowType.DOCK && windowType !== Meta.WindowType.DOCK &&
@@ -579,14 +481,6 @@ export default class WindowManager implements IWindowManager {
windowType !== Meta.WindowType.MENU; windowType !== Meta.WindowType.MENU;
} }
/**
* Synchronizes the active window with GNOME's currently active window
*
* This function queries GNOME Shell for the current focused window and
* updates the extension's active window tracking to match.
*
* @returns The window ID of the active window, or null if no window is active
*/
public syncActiveWindow(): number | null { public syncActiveWindow(): number | null {
const focusWindow = global.display.focus_window; const focusWindow = global.display.focus_window;
if (focusWindow) { if (focusWindow) {
@@ -599,102 +493,56 @@ export default class WindowManager implements IWindowManager {
return this._activeWindowId; return this._activeWindowId;
} }
/**
* Toggles the orientation of the active container (the container holding the active window)
*/
public toggleActiveContainerOrientation(): void { public toggleActiveContainerOrientation(): void {
if (this._activeWindowId === null) { if (this._activeWindowId === null) {
Logger.warn("No active window, cannot toggle container orientation"); Logger.warn("No active window, cannot toggle container orientation");
return; return;
} }
const container = this._findContainerForWindowAcrossMonitors(this._activeWindowId);
// Find the active window's container if (container) {
const activeContainer = this._findActiveContainer(); container.toggleOrientation();
if (activeContainer) {
activeContainer.toggleOrientation();
} else { } else {
Logger.warn("Could not find container for active window"); Logger.warn("Could not find container for active window");
} }
} }
/**
* Resets all split ratios in the active window's container to equal fractions.
* Bound to Ctrl+Z by default.
*/
public resetActiveContainerRatios(): void { public resetActiveContainerRatios(): void {
if (this._activeWindowId === null) { if (this._activeWindowId === null) {
Logger.warn("No active window, cannot reset container ratios"); Logger.warn("No active window, cannot reset container ratios");
return; return;
} }
const container = this._findContainerForWindowAcrossMonitors(this._activeWindowId);
const activeContainer = this._findActiveContainer(); if (container) {
if (activeContainer) {
Logger.info("Resetting container ratios to equal splits"); Logger.info("Resetting container ratios to equal splits");
activeContainer.resetRatios(); container.resetRatios();
} else { } else {
Logger.warn("Could not find container for active window"); Logger.warn("Could not find container for active window");
} }
} }
/** public toggleActiveContainerTabbed(): void {
* Finds the container that directly contains the active window
* @returns The container holding the active window, or null if not found
*/
private _findActiveContainer(): WindowContainer | null {
if (this._activeWindowId === null) { if (this._activeWindowId === null) {
return null; Logger.warn("No active window, cannot toggle tabbed mode");
return;
} }
const container = this._findContainerForWindowAcrossMonitors(this._activeWindowId);
for (const monitor of this._monitors.values()) { if (container) {
const activeWorkspaceIndex = global.workspace_manager.get_active_workspace().index(); if (container.isTabbed()) {
container.setAccordion(Layout.ACC_HORIZONTAL);
// Bounds check to prevent accessing invalid workspace } else {
if (activeWorkspaceIndex >= monitor._workspaces.length || activeWorkspaceIndex < 0) { // Set the active tab to the focused window
Logger.warn(`Active workspace index ${activeWorkspaceIndex} out of bounds for monitor with ${monitor._workspaces.length} workspaces`); const activeIndex = container._getIndexOfWindow(this._activeWindowId);
continue; if (activeIndex !== -1) {
container._activeTabIndex = activeIndex;
} }
container.setTabbed();
const workspace = monitor._workspaces[activeWorkspaceIndex]; }
this._tileMonitors();
// Check if the window is directly in the workspace container } else {
const windowWrapper = workspace.getWindow(this._activeWindowId); Logger.warn("Could not find container for active window");
if (windowWrapper) {
// Try to find the parent container
const container = this._findContainerHoldingWindow(workspace, this._activeWindowId);
return container;
} }
} }
return null;
}
/**
* Recursively finds the container that directly contains a specific window
* @param container The container to search
* @param windowId The window ID to find
* @returns The container that directly contains the window, or null if not found
*/
private _findContainerHoldingWindow(container: WindowContainer, windowId: number): WindowContainer | null {
// Check if this container directly contains the window
for (const item of container._tiledItems) {
if (item instanceof WindowContainer) {
// Recursively search nested containers
const result = this._findContainerHoldingWindow(item, windowId);
if (result) {
return result;
}
} else if (item.getWindowId() === windowId) {
// Found it! Return this container as it directly holds the window
return container;
}
}
return null;
}
/**
* Prints the tree structure of all monitors, workspaces, containers, and windows to the logs
*/
public printTreeStructure(): void { public printTreeStructure(): void {
Logger.info("=".repeat(80)); Logger.info("=".repeat(80));
Logger.info("WINDOW TREE STRUCTURE"); Logger.info("WINDOW TREE STRUCTURE");
@@ -707,19 +555,18 @@ export default class WindowManager implements IWindowManager {
this._monitors.forEach((monitor: Monitor, monitorId: number) => { this._monitors.forEach((monitor: Monitor, monitorId: number) => {
const isActiveMonitor = this._activeWindowId !== null && const isActiveMonitor = this._activeWindowId !== null &&
monitor.getWindow(this._activeWindowId) !== undefined; monitor.getWindow(this._activeWindowId) !== undefined;
const monitorMarker = isActiveMonitor ? ' *' : '';
Logger.info(`Monitor ${monitorId}${monitorMarker}:`); Logger.info(`Monitor ${monitorId}${isActiveMonitor ? ' *' : ''}:`);
Logger.info(` Work Area: x=${monitor._workArea.x}, y=${monitor._workArea.y}, w=${monitor._workArea.width}, h=${monitor._workArea.height}`); Logger.info(` Work Area: x=${monitor._workArea.x}, y=${monitor._workArea.y}, w=${monitor._workArea.width}, h=${monitor._workArea.height}`);
monitor._workspaces.forEach((workspace, workspaceIndex) => { monitor._workspaces.forEach((workspace, workspaceIndex) => {
const isActiveWorkspace = workspaceIndex === activeWorkspaceIndex; const isActiveWorkspace = workspaceIndex === activeWorkspaceIndex;
const workspaceMarker = isActiveWorkspace && isActiveMonitor ? ' *' : ''; Logger.info(` Workspace ${workspaceIndex}${isActiveWorkspace && isActiveMonitor ? ' *' : ''}:`);
Logger.info(` Orientation: ${Layout[workspace._orientation]}`);
Logger.info(` Workspace ${workspaceIndex}${workspaceMarker}:`);
Logger.info(` Orientation: ${workspace._orientation === 0 ? 'HORIZONTAL' : 'VERTICAL'}`);
Logger.info(` Items: ${workspace._tiledItems.length}`); Logger.info(` Items: ${workspace._tiledItems.length}`);
if (workspace.isTabbed()) {
Logger.info(` Active Tab: ${workspace._activeTabIndex}`);
}
this._printContainerTree(workspace, 4); this._printContainerTree(workspace, 4);
}); });
}); });
@@ -727,31 +574,20 @@ export default class WindowManager implements IWindowManager {
Logger.info("=".repeat(80)); Logger.info("=".repeat(80));
} }
/** private _printContainerTree(container: WindowContainer, indentLevel: number): void {
* Recursively prints the container tree structure
* @param container The container to print
* @param indentLevel The indentation level (number of spaces)
*/
private _printContainerTree(container: any, indentLevel: number): void {
const indent = " ".repeat(indentLevel); const indent = " ".repeat(indentLevel);
container._tiledItems.forEach((item: any, index: number) => { container._tiledItems.forEach((item, index) => {
if (item instanceof WindowContainer) { if (item instanceof WindowContainer) {
// Check if this container contains the active window const containsActive = this._activeWindowId !== null &&
const containsActiveWindow = this._activeWindowId !== null &&
item.getWindow(this._activeWindowId) !== undefined; item.getWindow(this._activeWindowId) !== undefined;
const containerMarker = containsActiveWindow ? ' *' : ''; Logger.info(`${indent}[${index}] Container (${Layout[item._orientation]})${containsActive ? ' *' : ''}:`);
Logger.info(`${indent}[${index}] Container (${item._orientation === 0 ? 'HORIZONTAL' : 'VERTICAL'})${containerMarker}:`);
Logger.info(`${indent} Items: ${item._tiledItems.length}`); Logger.info(`${indent} Items: ${item._tiledItems.length}`);
Logger.info(`${indent} Work Area: x=${item._workArea.x}, y=${item._workArea.y}, w=${item._workArea.width}, h=${item._workArea.height}`); Logger.info(`${indent} Work Area: x=${item._workArea.x}, y=${item._workArea.y}, w=${item._workArea.width}, h=${item._workArea.height}`);
this._printContainerTree(item, indentLevel + 4); this._printContainerTree(item, indentLevel + 4);
} else { } else {
const window = item.getWindow(); const window = item.getWindow();
const isActiveWindow = this._activeWindowId === item.getWindowId(); Logger.info(`${indent}[${index}] Window ID: ${item.getWindowId()}${this._activeWindowId === item.getWindowId() ? ' *' : ''}`);
const windowMarker = isActiveWindow ? ' *' : '';
Logger.info(`${indent}[${index}] Window ID: ${item.getWindowId()}${windowMarker}`);
Logger.info(`${indent} Title: "${window.get_title()}"`); Logger.info(`${indent} Title: "${window.get_title()}"`);
Logger.info(`${indent} Class: ${window.get_wm_class()}`); Logger.info(`${indent} Class: ${window.get_wm_class()}`);
const rect = item.getRect(); const rect = item.getRect();
@@ -759,6 +595,4 @@ export default class WindowManager implements IWindowManager {
} }
}); });
} }
} }
+26 -32
View File
@@ -1,37 +1,31 @@
/* Add your custom extension styling here */ .aerospike-tab-bar {
.active-window-border { background-color: rgba(30, 30, 30, 0.95);
/*border: 2px solid rgba(191, 0, 255, 0.8);*/ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
/*border-radius: 3px;*/ spacing: 1px;
padding: 2px 2px 0 2px;
/* border-image-source: linear-gradient(to left, #743ad5, #d53a9d);*/
/* !*border: 4px solid transparent;*!*/
/* !*border-radius: 5px;*!*/
/* !*!* Gradient border using border-image *!*/
/* border-image: linear-gradient(45deg, red, orange, yellow, green, blue, indigo, violet) 1;*/
} }
/*.border-gradient-purple {*/ .aerospike-tab {
/* border-image-source: linear-gradient(to left, #743ad5, #d53a9d);*/ background-color: rgba(50, 50, 50, 0.8);
/*}*/ border-radius: 6px 6px 0 0;
padding: 2px 12px;
margin: 0 1px;
color: rgba(255, 255, 255, 0.5);
font-size: 11px;
font-weight: 400;
}
/*@keyframes rainbow-border {*/ .aerospike-tab:hover {
/* 0% {*/ background-color: rgba(70, 70, 70, 0.9);
/* border-image: linear-gradient(0deg, red, orange, yellow, green, blue, indigo, violet, red) 1;*/ color: rgba(255, 255, 255, 0.8);
/* }*/ }
/* 100% {*/
/* border-image: linear-gradient(360deg, red, orange, yellow, green, blue, indigo, violet, red) 1;*/
/* }*/
/*}*/
/*.active-window-border {*/ .aerospike-tab-active {
/* border: 4px solid transparent;*/ background-color: rgba(80, 80, 80, 1);
/* border-radius: 5px;*/ color: rgba(255, 255, 255, 1);
font-weight: 500;
}
/* !* Initial gradient border *!*/ .aerospike-tab-label {
/* border-image: linear-gradient(0deg, red, orange, yellow, green, blue, indigo, violet, red) 1;*/ font-size: 11px;
}
/* !* Apply animation *!*/
/* animation: rainbow-border 5s linear infinite;*/
/*}*/