188 Commits
v0.5 ... v0.9.3

Author SHA1 Message Date
Peter Fajdiga
a79229da75 bump version to 0.9.3 2024-07-07 12:46:55 +02:00
Peter Fajdiga
53d04c1d33 config: don't tile xwaylandvideobridge (fixes #54) 2024-07-07 12:17:21 +02:00
Peter Fajdiga
a18ff61d9e prevent untiling maximized windows (fixes #51) 2024-07-05 16:12:23 +02:00
Peter Fajdiga
99ffad9223 use inqequality operator with MaximizedMode 2024-07-05 16:00:06 +02:00
Peter Fajdiga
e776df509b bump version to 0.9.2 2024-04-27 22:54:53 +02:00
Peter Fajdiga
63de2d4cae WindowRuleEnforcer: prevent tiling windows with pid = -1 (fixes #46) 2024-04-27 17:36:17 +02:00
Peter Fajdiga
85d361f16f Makefile: remove logs target 2024-04-27 12:41:49 +02:00
Peter Fajdiga
f3b75807be Makefile: merge config into build 2024-04-27 12:38:42 +02:00
Peter Fajdiga
88bce26456 Makefile: run tests on every build 2024-04-27 12:38:42 +02:00
Peter Fajdiga
3a75ddab0f WindowRuleEnforcer: fix rule string generation 2024-04-27 12:38:42 +02:00
Peter Fajdiga
ee0aa93308 tests: add WindowRuleEnforcer test cases 2024-04-27 12:38:42 +02:00
Peter Fajdiga
33ca138420 move test files to src/tests 2024-04-27 12:38:42 +02:00
Peter Fajdiga
79f4aaeef8 Revert "tsconfig: exclude test files"
This reverts commit ee14509228.
2024-04-27 12:38:42 +02:00
Peter Fajdiga
0aca6e1146 create unit tests 2024-04-27 12:38:40 +02:00
Peter Fajdiga
28e54434aa run-ts.sh: only delete tmp file if successful 2024-04-26 18:26:35 +02:00
Peter Fajdiga
ee14509228 tsconfig: exclude test files 2024-04-20 19:01:24 +02:00
Peter Fajdiga
1596edc43f run-ts.sh: print build errors 2024-04-20 19:00:03 +02:00
Peter Fajdiga
b897ab5b9f extern: rename .d.ts to .ts 2024-04-20 17:41:56 +02:00
Peter Fajdiga
5e6dad8459 extern: move all declarations to global.d.ts 2024-04-20 17:40:17 +02:00
Peter Fajdiga
beeba74442 extern: split type definitions and global constant declarations 2024-04-20 17:31:10 +02:00
Peter Fajdiga
3e14440180 create a typescript project for each generator script 2024-04-20 17:30:27 +02:00
Peter Fajdiga
36836ad258 split lib and main 2024-04-20 17:30:27 +02:00
Peter Fajdiga
bebc009cc6 split tsconfig 2024-04-20 17:30:27 +02:00
Peter Fajdiga
a4ba8516dc move code to src/main 2024-04-20 17:30:27 +02:00
Peter Fajdiga
675a70d907 tsconfig: clean up 2024-04-20 17:30:27 +02:00
Peter Fajdiga
3ba66f1c89 World: extract function createScroller 2024-04-20 17:30:01 +02:00
Peter Fajdiga
4556198b2e extern: define function return types 2024-04-20 15:28:06 +02:00
Peter Fajdiga
7b8de5955d extern: extract Notification into notification.d.ts 2024-04-19 17:37:44 +02:00
Peter Fajdiga
68a687b7d4 qt.d.ts: add message parameter to console.assert 2024-04-18 21:22:53 +02:00
Peter Fajdiga
20a3ece4b5 bump version to 0.9.1 2024-04-06 08:36:51 +02:00
Peter Fajdiga
3cad8102ee qt.d.ts: remove QByteArray type 2024-04-05 14:49:36 +02:00
Peter Fajdiga
7fd45eed8f kwin.d.ts: mark cursorPos and minSize as immutable 2024-04-05 14:48:56 +02:00
Peter Fajdiga
7299341608 Tiled: use clientGeometry to determine border resize 2024-04-05 14:46:19 +02:00
Peter Fajdiga
842ec1ac63 WindowRuleEnforcer: fix bug in joinRegexes 2024-04-05 13:58:50 +02:00
Peter Fajdiga
0523465b84 WindowRuleEnforcer: remove debug logs 2024-04-01 19:26:50 +02:00
Peter Fajdiga
c7cfa261b9 bump version to 0.9 2024-03-30 12:46:59 +01:00
Peter Fajdiga
56955e4df3 src/config: don't tile kded windows 2024-03-30 12:45:42 +01:00
Peter Fajdiga
bb308cfbfb config: merge X11 and Wayland class regexes in window rules 2024-03-30 12:45:40 +01:00
Peter Fajdiga
6c00245943 config: escape . in window rules 2024-03-30 12:45:38 +01:00
Peter Fajdiga
2efdbe5a7b support regex for class selector in window rules (resolves #41) 2024-03-30 12:45:36 +01:00
Peter Fajdiga
092cbf3ff1 kwin.d.ts: remove unused signal maximizedChanged 2024-03-30 12:45:33 +01:00
Peter Fajdiga
f9ae299ce8 Tiled: restore opacity after un-tiling 2024-03-30 12:45:28 +01:00
Peter Fajdiga
695f5edf6a config: add option to disable layering (resolves #41) 2024-03-30 12:45:25 +01:00
Peter Fajdiga
9b80b535a1 readme: update QML dependencies 2024-03-19 10:58:51 +01:00
Peter Fajdiga
752df86db5 bump version to 0.8.1 2024-03-19 10:46:33 +01:00
Peter Fajdiga
f05eefe19b config: add plasmashell to window rules (fixes #38) 2024-03-19 10:17:37 +01:00
Peter Fajdiga
f550285778 bump version to 0.8 2024-03-18 19:56:14 +01:00
Peter Fajdiga
5247a6a0d3 don't tile immovable windows 2024-03-18 19:31:22 +01:00
Peter Fajdiga
2b114a63dc refactor TiledMinimized unminimization 2024-03-18 19:31:22 +01:00
Peter Fajdiga
63e4015f3a don't tile popup windows 2024-03-18 19:31:22 +01:00
Peter Fajdiga
02db31266b ClientWrapper: don't try to maximize/fullscreenify unsupporting windows 2024-03-18 19:31:22 +01:00
Peter Fajdiga
67d4d89700 World: remove workaround for Qt5 bug 2024-03-18 19:31:22 +01:00
Peter Fajdiga
755cf90b1a DesktopManager: destroy removed desktops 2024-03-18 19:31:22 +01:00
Peter Fajdiga
e6a01217a5 DesktopManager.getDesktopsForClient: call getDesktops 2024-03-18 19:31:22 +01:00
Peter Fajdiga
21d7bbd6c4 DesktopManager: don't yield previously unconstructed desktops 2024-03-18 19:31:22 +01:00
Peter Fajdiga
605215acdc workspace.ts: use Clients.makeTileable 2024-03-18 19:31:22 +01:00
Peter Fajdiga
4b6808dba1 ClientWrapper: set maximizedMode in setMaximize 2024-03-18 19:31:22 +01:00
Peter Fajdiga
f9749c6f56 Tiled: use interactiveMoveResizeStarted and interactiveMoveResizeFinished 2024-03-18 19:31:22 +01:00
Peter Fajdiga
9b40b2f777 Tiled: use cursorPos to distinguish between border and single-column resize 2024-03-18 19:31:22 +01:00
Peter Fajdiga
33470b4d7b move kwinClient.tile = null to ClientWrapper 2024-03-18 19:31:22 +01:00
Peter Fajdiga
8947719621 ClientWrapper: store maximized state 2024-03-18 19:31:22 +01:00
Peter Fajdiga
4bf4f8e8a1 Tiled: use maximizedAboutToChange instead of maximizedChanged 2024-03-18 19:31:22 +01:00
Peter Fajdiga
080de7cf97 kwin.d.ts: add signal maximizedAboutToChange 2024-03-18 19:31:22 +01:00
Peter Fajdiga
c29902dc15 Pinned: change condition for un-tile on maximixedChanged 2024-03-18 19:31:22 +01:00
Peter Fajdiga
1736b0a398 kwin.d.ts: update client signals 2024-03-18 19:31:22 +01:00
Peter Fajdiga
a1c44647ca TiledMinimized: support un-minimization 2024-03-18 19:31:22 +01:00
Peter Fajdiga
0ea75d6348 confirm all desktops == empty array 2024-03-18 19:31:22 +01:00
Peter Fajdiga
12901e45ce src/keyBindings: add TODO 2024-03-18 19:31:22 +01:00
Peter Fajdiga
29b4ccd1dd port key bindings to the kwin6 ShortcutHandler system 2024-03-18 19:31:22 +01:00
Peter Fajdiga
7b547bc5b8 pass desktop to Workspace.clientArea 2024-03-18 19:31:22 +01:00
Peter Fajdiga
78a127111b pass output to Workspace.clientArea 2024-03-18 19:31:22 +01:00
Peter Fajdiga
333b7601b2 refactor desktops (WIP) 2024-03-18 19:31:22 +01:00
Peter Fajdiga
1927ae445d kwin.d.ts: add KwinDesktop (WIP) 2024-03-18 19:31:22 +01:00
Peter Fajdiga
1f563dae01 kwin.d.ts: update KwinClient properties 2024-03-18 19:31:22 +01:00
Peter Fajdiga
6b82eedbfe kwin.d.ts: move removed Workspace signals to KwinClient (WIP) 2024-03-18 19:31:22 +01:00
Peter Fajdiga
b479735130 kwin.d.ts: update Workspace properties 2024-03-18 19:31:22 +01:00
Peter Fajdiga
c8f022d66f kwin.d.ts: rename Workspace 2024-03-18 19:31:22 +01:00
Peter Fajdiga
7f71750a8e use QtQuick 6.0 2024-03-18 19:31:22 +01:00
Peter Fajdiga
13ebf24732 Makefile: fix uninstall recipe 2024-03-18 19:31:22 +01:00
Peter Fajdiga
ec6b3247b7 Makefile: use kpackagetool6 2024-03-18 19:31:22 +01:00
Peter Fajdiga
50681d3a07 update package structure for kde 6 2024-03-18 19:31:22 +01:00
Peter Fajdiga
af930a9b2f Tiled: check if user is resizing any window in the grid 2024-03-18 19:28:58 +01:00
Peter Fajdiga
489a1447e7 ClientWrapper: create a workaround for the problem with stuck off-screen windows on Wayland 2024-03-18 19:28:58 +01:00
Peter Fajdiga
b984f025ec Column: avoid setting preferredWidth of a window when another window's height in the same column is being resized by the user 2024-03-10 21:29:47 +01:00
Peter Fajdiga
4e1204f1bd DesktopManager: refactor getDesktopForClient 2024-03-10 21:02:24 +01:00
Peter Fajdiga
bbcf51783d World: fix grammar in comment 2024-03-10 15:44:17 +01:00
Peter Fajdiga
019da3766e kwin.d.ts: use unknown for Tile 2024-03-10 15:44:17 +01:00
Peter Fajdiga
1535c994b8 use === everywhere 2024-03-10 15:44:16 +01:00
Peter Fajdiga
296d0deca9 remove void keyword in QSignal params 2024-03-09 21:55:15 +01:00
Peter Fajdiga
17e7d5b46e kwin.d.ts: remove unneeded QSignal params 2024-03-09 21:50:27 +01:00
Peter Fajdiga
840a50d14d move ClientAreaOption to kwin.d.ts 2024-03-09 21:44:23 +01:00
Peter Fajdiga
4f99c4dd45 Actions: remove superfluous return values 2024-03-09 19:53:10 +01:00
Peter Fajdiga
030eddaf34 Floating: add missing ; 2024-03-09 19:34:54 +01:00
Peter Fajdiga
7246a7660e kwin.d.ts: remove superfluous comments 2024-03-09 19:19:39 +01:00
Peter Fajdiga
687256d1dd src/config: unjsonify 2024-03-09 19:19:38 +01:00
Peter Fajdiga
12bb7506cc src/keyBindings: unjsonify 2024-03-09 19:19:13 +01:00
Peter Fajdiga
1808ee0025 mark all signal properties as readonly 2024-03-09 19:07:12 +01:00
Peter Fajdiga
3021f61933 metadata.json: change description 2024-03-01 15:54:06 +01:00
Peter Fajdiga
e908138478 fix bug where resizing used manualScrollStep setting instead of manualResizeStep setting 2024-02-22 20:24:12 +01:00
Peter Fajdiga
99ad115370 rename scrollers 2024-02-18 22:11:34 +01:00
Peter Fajdiga
c5a4238f5f move scroller src files to src/behavior/scroller 2024-02-18 22:07:28 +01:00
Peter Fajdiga
0670d9c265 separate clampScrollX logic into different Clamper implementations 2024-02-18 22:06:14 +01:00
Peter Fajdiga
845874b0d0 separate increaseColumnWidth and columnWidthDecrease logic into different ColumnResizer implementations 2024-02-18 22:06:13 +01:00
Peter Fajdiga
a422a077f6 Makefile: add prerequisites to package target 2024-02-18 21:28:17 +01:00
Peter Fajdiga
2fe1be99cb Desktop.scrollCenterVisible: stop prioritizing visible columns (this is now done by ColumnRange.addNeighbors) 2024-02-18 20:21:34 +01:00
Peter Fajdiga
1a449c238d ColumnRange: prioritize nearer columns 2024-02-18 20:15:58 +01:00
Peter Fajdiga
9bda7d1a09 bump version to 0.7.1 2024-02-12 21:27:34 +01:00
Peter Fajdiga
2ce72bcee8 qt.d.ts: add console.trace 2024-02-12 21:17:37 +01:00
Peter Fajdiga
ff3f6c5d6b Desktop: fix ColumnRange when a column's width is still 0 2024-02-12 21:10:12 +01:00
Peter Fajdiga
3ab230b498 Makefile: append version number to package file name 2024-02-11 20:37:28 +01:00
Peter Fajdiga
ba9f362a1c .gitignore: ignore suffixed packages 2024-02-11 20:37:27 +01:00
Peter Fajdiga
ad6c3f1cae bump version to 0.7 2024-02-11 20:37:26 +01:00
Peter Fajdiga
ba4dd2a9c1 config.ui: relabel scrollingGrouped button 2024-02-11 20:37:25 +01:00
Peter Fajdiga
bb61853009 config.ui: reorder tabs 2024-02-11 20:37:22 +01:00
Peter Fajdiga
0cfd9b9e36 Desktop: scrollCenterRange: add parameter prioritiseVisible 2024-01-22 08:55:05 +01:00
Peter Fajdiga
43c4f7ef9a Actions: columnWidthIncrease: add steps for fully visible screen-edge columns 2024-01-22 08:55:05 +01:00
Peter Fajdiga
9cb3f33ecb Actions: extract function findNextStep 2024-01-22 08:55:05 +01:00
Peter Fajdiga
31b9e61ae3 config.ui: add manualResizeStep 2024-01-22 08:55:05 +01:00
Peter Fajdiga
668e6696ab Actions: column width increase/decrease: replace screen-relative steps with column-relative steps 2024-01-22 08:55:05 +01:00
Peter Fajdiga
e63959cfbf Actions: getWidthSteps: ignore screen-relative steps too close to existing steps 2024-01-22 08:55:05 +01:00
Peter Fajdiga
ef2650beb8 Actions: improve snapping in columnWidthDecrease 2024-01-22 08:55:05 +01:00
Peter Fajdiga
750c47c040 implement resize steps (resolves #25) 2024-01-22 08:55:05 +01:00
Peter Fajdiga
88ca0d02e1 generators/config/kcfg.ts: escape xml characters in default config values 2024-01-21 19:40:12 +01:00
Peter Fajdiga
aba786b754 Desktop: rename scrollIntoView 2024-01-21 18:39:53 +01:00
Peter Fajdiga
47aa625c99 Actions: column width increase/decrease: use getCurrentVisibleRange 2024-01-21 18:39:25 +01:00
Peter Fajdiga
03c7cc6503 Desktop.scrollToRange: simplify 2024-01-21 18:37:34 +01:00
Peter Fajdiga
9e9ff2b74f remove overscroll feature (resolves #23) 2024-01-21 18:26:18 +01:00
Peter Fajdiga
5674624e6f Desktop.equalizeVisibleColumnsWidths: simplify scroll at the end 2024-01-21 18:17:29 +01:00
Peter Fajdiga
44dd88ef7c Desktop.equalizeVisibleColumnsWidths: handle columns with limited min width 2024-01-21 18:17:29 +01:00
Peter Fajdiga
f800d6ecf0 Desktop: rewrite ColumnRange.addNeighbors 2024-01-21 18:17:29 +01:00
Peter Fajdiga
3477e17bb3 Desktop: scrollCenterRange: replace parameter requireVisible with condition 2024-01-21 18:17:29 +01:00
Peter Fajdiga
755c781646 rename parameters of doIfTiled passed functions 2024-01-21 18:17:29 +01:00
Peter Fajdiga
926345ba31 move column width increase/decrease code to Actions.ts 2024-01-21 18:17:29 +01:00
Peter Fajdiga
a2295ede43 Desktop: add method scrollCenterVisible (moved from ScrollerGrouped) 2024-01-21 18:17:29 +01:00
Peter Fajdiga
ca80a7ca28 Grid.onColumnWidthChanged: fix autoAdjustScroll call 2024-01-14 15:40:24 +01:00
Peter Fajdiga
64474b1677 readme: mention Niri 2024-01-14 09:55:57 +01:00
Peter Fajdiga
eca63cbc16 readme: update key bindings 2023-12-30 17:26:59 +01:00
Peter Fajdiga
3a8baf4cd7 bump version to 0.6 2023-12-30 17:18:44 +01:00
Peter Fajdiga
dc14171ae7 config: add Wayland window classes to window rules (resolves #24) 2023-12-27 19:45:07 +01:00
Peter Fajdiga
6dcf8979c2 config: add yakuake to window rules 2023-12-27 19:41:30 +01:00
Peter Fajdiga
fe5661c07f Grid: auto-scroll after removing a non-focused column 2023-12-24 14:58:44 +01:00
Peter Fajdiga
90b783b34b Grid: remove unused methods getLeftOffScreenColumn and getRightOffScreenColumn 2023-12-24 12:52:10 +01:00
Peter Fajdiga
fb40bd9592 Grid.decreaseColumnWidth: prevent scrolling away from focused column when reaching minimum width 2023-12-24 12:50:52 +01:00
Peter Fajdiga
768d95450d refactor arrange functions 2023-12-24 09:29:31 +01:00
Peter Fajdiga
e95a0e44c9 prevent translucent windows during resizing 2023-12-24 09:21:49 +01:00
Peter Fajdiga
c1b8d05919 defaultWindowRules: update Zoom entry 2023-12-24 09:21:44 +01:00
Peter Fajdiga
e98ce18105 add opacity settings for obscured windows 2023-12-16 12:10:26 +01:00
Peter Fajdiga
25a9efc8e4 config.ui: split up parameters and behavior tabs 2023-12-16 11:39:42 +01:00
Peter Fajdiga
db48644944 package/contents/ui: add footer spacer to take up excess space 2023-12-16 11:39:42 +01:00
Peter Fajdiga
950e0de076 Desktop: prevent scrolling when unnecessary (add dirtyScroll variable) 2023-12-16 10:07:02 +01:00
Peter Fajdiga
05ffe0895e Revert "Desktop.scrollToCenterRange: force scroll"
This reverts commit dda63d68cde58c7f4a7162b11a2fd614365d36ff.
2023-12-16 09:33:26 +01:00
Peter Fajdiga
61db5ca69f use different implementations of clampScrollX in different scrollers 2023-12-16 09:33:26 +01:00
Peter Fajdiga
f7b5dd0b9c Desktop.equalizeVisibleColumnsWidths: use Desktop.RangeImpl.fromRanges 2023-12-16 09:33:26 +01:00
Peter Fajdiga
f83f60c98f Grid: reimplement increaseColumnWidth and decreaseColumnWidth 2023-12-16 09:33:26 +01:00
Peter Fajdiga
3e8734eefb Desktop.scrollToCenterRange: force scroll 2023-12-16 09:33:26 +01:00
Peter Fajdiga
bed0ea7ed8 Actions: make gridScrollFocused center focused column again 2023-12-16 09:33:26 +01:00
Peter Fajdiga
92c99f0b87 rename focusColumn -> scrollToColumn 2023-12-16 09:33:26 +01:00
Peter Fajdiga
b2024bc8aa Actions: make scrolling actions use focusColumn 2023-12-16 09:33:26 +01:00
Peter Fajdiga
58f358313b config.ui: change description for kcfg_scrollingLazy 2023-12-16 09:33:26 +01:00
Peter Fajdiga
352a7061f6 config.ui: shorten description for kcfg_scrollingCentered 2023-12-16 09:33:26 +01:00
Peter Fajdiga
7314c0ee24 add ScrollerGrouped 2023-12-16 09:33:26 +01:00
Peter Fajdiga
c65361853c config.ui: use button groups 2023-12-16 09:32:26 +01:00
Peter Fajdiga
fa53e765b3 qt.d.ts: rename QmlTimer 2023-12-16 09:30:19 +01:00
Peter Fajdiga
4d35681ee2 qt.d.ts: rename QmlSize 2023-12-16 09:30:14 +01:00
Peter Fajdiga
1824bcdf85 fix usages of QmlRect.right and .bottom 2023-12-12 17:33:10 +01:00
Peter Fajdiga
ce1b402bf2 qt.d.ts: add comments to QmlRect fields 2023-12-10 21:08:00 +01:00
Peter Fajdiga
2df6d5d8e6 qt.d.ts: rename QmlRect 2023-12-10 21:06:03 +01:00
Peter Fajdiga
e7d33030ba read and use scrolling configuration 2023-12-05 22:15:49 +01:00
Peter Fajdiga
2bd000f0a6 config.ui: add scrolling configuration 2023-12-05 22:03:22 +01:00
Peter Fajdiga
6313d8f18e implement scrollers 2023-12-05 21:59:01 +01:00
Peter Fajdiga
464ec3bcb1 Grid.increaseColumnWidth: fix scroll adjustment after resize 2023-12-02 20:57:46 +01:00
Peter Fajdiga
fae793cb09 Desktop.equalizeVisibleColumnsWidths: adjust scroll after resizing 2023-12-02 20:50:13 +01:00
Peter Fajdiga
d7346a6fab Desktop.clampScrollX: simplify 2023-12-02 20:38:14 +01:00
Peter Fajdiga
0e5efd2be7 Desktop: receive Range in parameters instead of Column 2023-12-02 17:36:29 +01:00
Peter Fajdiga
a1a315790e turn Desktop.Range into an interface (well, into a type) 2023-12-02 14:12:41 +01:00
Peter Fajdiga
9d62499bf0 Grid.Range: make fields private 2023-12-02 14:09:23 +01:00
Peter Fajdiga
97cf61d1dd Grid: extract method calculateVisibleRange 2023-12-02 14:06:28 +01:00
Peter Fajdiga
5d83c6dd2c rename Grid.ScrollPos to Grid.Range 2023-12-02 13:55:39 +01:00
Peter Fajdiga
8915e8a9da Grid.getVisibleColumnsWidth: use getVisibleColumns 2023-12-02 12:44:57 +01:00
Peter Fajdiga
22e4c47189 add action columns-width-equalize (resolves #22) 2023-12-02 12:44:57 +01:00
Peter Fajdiga
552d2b851f readme: list QML dependencies 2023-11-12 10:02:33 +01:00
Peter Fajdiga
c4ce795359 config.ui: add stacked mode description to tooltip 2023-09-29 10:23:33 +02:00
Peter Fajdiga
1ac1fc3c2b readme: update key bindings 2023-09-29 10:21:47 +02:00
Peter Fajdiga
d0e041d16a keyBindings: add comment for column-toggle-stacked 2023-09-29 10:21:26 +02:00
Peter Fajdiga
8fc3fc976d Window: WindowState -> Window.State 2023-09-29 08:58:38 +02:00
Peter Fajdiga
e0eeace9dc ClientState.Manager: ; -> , 2023-09-29 08:58:37 +02:00
Peter Fajdiga
84e2a06b35 Tiled: define type WindowState 2023-09-29 08:56:45 +02:00
Peter Fajdiga
3373e02658 add skipSwitcher setting 2023-09-29 08:52:59 +02:00
83 changed files with 2176 additions and 1709 deletions

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
/package/contents/code/main.js
/package/contents/config/main.xml
/karousel.tar.gz
/karousel*.tar.gz
run-ts-tmp.js
/.idea

View File

@@ -1,31 +1,29 @@
.PHONY: *
TSC_SCRIPT_FLAGS = --lib es2020 ./src/extern/qt.d.ts
VERSION = $(shell grep '"Version":' ./package/metadata.json | grep -o '[0-9\.]*')
config:
build: tests
tsc -p ./src/main --outFile ./package/contents/code/main.js
mkdir -p ./package/contents/config
tsc ${TSC_SCRIPT_FLAGS} ./src/config/definition.ts ./generators/config/kcfg.ts --outFile /dev/stdout | node - > ./package/contents/config/main.xml
./run-ts.sh ./src/generators/config > ./package/contents/config/main.xml
build:
tsc --outFile ./package/contents/code/main.js
tests:
./run-ts.sh ./src/tests
install: build config
kpackagetool5 --type=KWin/Script -i ./package || kpackagetool5 --type=KWin/Script -u ./package
install: build
kpackagetool6 --type=KWin/Script -i ./package || kpackagetool6 --type=KWin/Script -u ./package
uninstall:
kpackagetool5 --type=KWin/Script -r ./package
kpackagetool6 --type=KWin/Script -r karousel
package:
tar -czf ./karousel.tar.gz ./package
logs:
journalctl -t kwin_x11 -g '^qml:|^file://.*karousel' -f
package: build
tar -czf ./karousel_${subst .,_,${VERSION}}.tar.gz ./package
docs-key-bindings-bbcode:
@tsc ${TSC_SCRIPT_FLAGS} ./src/keyBindings/definition.ts ./generators/docs/keyBindings.ts ./generators/docs/keyBindingsBbcode.ts --outFile /dev/stdout | node -
@./run-ts.sh ./src/generators/docs/keyBindingsBbcode
docs-key-bindings-table:
@tsc ${TSC_SCRIPT_FLAGS} ./src/keyBindings/definition.ts ./generators/docs/keyBindings.ts ./generators/docs/keyBindingsTable.ts --outFile /dev/stdout | node -
@./run-ts.sh ./src/generators/docs/keyBindingsTable
docs-key-bindings-fmt:
@tsc ${TSC_SCRIPT_FLAGS} ./src/keyBindings/definition.ts ./generators/docs/keyBindings.ts ./generators/docs/keyBindingsFmt.ts --outFile /dev/stdout | node -
@./run-ts.sh ./src/generators/docs/keyBindingsFmt

View File

@@ -13,9 +13,16 @@ unprompted reflow of window content.
Windows are automatically centered when possible. And when running out of width, windows can be
scrolled through horizontally.
Similar window managers include [PaperWM](https://github.com/paperwm/PaperWM) and
Similar window managers include [PaperWM](https://github.com/paperwm/PaperWM),
[Niri](https://github.com/YaLTeR/niri), and
[Cardboard](https://gitlab.com/cardboardwm/cardboard).
## Dependencies
Karousel requires the following QML modules:
- QtQuick 6.0
- org.kde.kwin 3.0
- org.kde.notification 1.0
## Limitations
- Doesn't support multiple screens
- Doesn't support windows on all desktops
@@ -39,13 +46,14 @@ Here's the default ones:
| Meta+Shift+S | Move window down |
| Meta+Shift+Home | Move window to start |
| Meta+Shift+End | Move window to end |
| Meta+X | Toggle stacked layout for focused column |
| Meta+X | Toggle stacked layout for focused column (One window in the column visible, others shaded; not supported on Wayland) |
| Meta+Ctrl+Shift+A | Move column left |
| Meta+Ctrl+Shift+D | Move column right |
| Meta+Ctrl+Shift+Home | Move column to start |
| Meta+Ctrl+Shift+End | Move column to end |
| Meta+Ctrl++ | Increase column width |
| Meta+Ctrl+- | Decrease column width |
| Meta+Ctrl+X | Equalize widths of visible columns |
| Meta+Alt+Return | Center focused window (Scrolls so that the focused window is centered in the screen) |
| Meta+Alt+A | Scroll one column to the left |
| Meta+Alt+D | Scroll one column to the right |

View File

@@ -11,12 +11,148 @@
<layout class="QVBoxLayout" name="layout_main">
<item>
<widget class="QTabWidget" name="tabContainer">
<widget class="QWidget" name="tab_general">
<widget class="QWidget" name="tab_behavior">
<attribute name="title">
<string>General</string>
<string>Behavior</string>
</attribute>
<layout class="QFormLayout" name="layout_tab_general">
<layout class="QVBoxLayout">
<item>
<widget class="QGroupBox">
<property name="flat">
<bool>true</bool>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QCheckBox" name="kcfg_untileOnDrag">
<property name="text">
<string>Un-tile windows by dragging them</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="kcfg_stackColumnsByDefault">
<property name="text">
<string>Stack columns by default</string>
</property>
<property name="toolTip">
<string>New columns start in stacked mode (one window in the column visible, others shaded). Not supported on Wayland.</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="kcfg_resizeNeighborColumn">
<property name="text">
<string>Resize neighbor column on edge resize</string>
</property>
<property name="toolTip">
<string>When resizing a column by dragging its edge, also inversely resize the column on the other side of the edge</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="kcfg_reMaximize">
<property name="text">
<string>Re-maximize tiled windows</string>
</property>
<property name="toolTip">
<string>Restore maximized and full-screen states of tiled windows on focus</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="kcfg_skipSwitcher">
<property name="text">
<string>Tiled windows skip switcher</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox">
<property name="title">
<string>Scrolling mode</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QRadioButton" name="kcfg_scrollingLazy">
<property name="text">
<string>Only scroll as necessary</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="kcfg_scrollingCentered">
<property name="text">
<string>Center focused column</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="kcfg_scrollingGrouped">
<property name="text">
<string>Center visible columns</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox">
<property name="title">
<string>Layering mode</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QRadioButton" name="kcfg_tiledKeepBelow">
<property name="text">
<string>Keep tiled windows below</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="kcfg_floatingKeepAbove">
<property name="text">
<string>Keep floating windows above</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="kcfg_noLayering">
<property name="text">
<string>No layering</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="spacer_footer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_parameters">
<attribute name="title">
<string>Parameters</string>
</attribute>
<layout class="QFormLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_gapsOuterTop">
<property name="text">
@@ -144,34 +280,13 @@
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_overscroll">
<property name="text">
<string>Overscroll amount:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QSpinBox" name="kcfg_overscroll">
<property name="suffix">
<string> px</string>
</property>
<property name="maximum">
<number>999</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_manualScrollStep">
<property name="text">
<string>Manual scroll step size:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<item row="6" column="1">
<widget class="QSpinBox" name="kcfg_manualScrollStep">
<property name="suffix">
<string> px</string>
@@ -185,112 +300,55 @@
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_manualResizeStep">
<property name="text">
<string>Manual resize step size:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QSpinBox" name="kcfg_manualResizeStep">
<property name="suffix">
<string> px</string>
</property>
<property name="maximum">
<number>999</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_offScreenOpacity">
<property name="text">
<string>Obscured window opacity:</string>
</property>
</widget>
</item>
<item row="8" column="1">
<spacer name="separator_behavior">
<property name="orientation">
<enum>Qt::Vertical</enum>
<widget class="QSpinBox" name="kcfg_offScreenOpacity">
<property name="suffix">
<string> %</string>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
<property name="maximum">
<number>100</number>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>12</height>
</size>
</property>
</spacer>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_behavior">
<property name="text">
<string>Behavior:</string>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QCheckBox" name="kcfg_untileOnDrag">
<property name="text">
<string>Un-tile windows by dragging them</string>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QCheckBox" name="kcfg_stackColumnsByDefault">
<property name="text">
<string>Stack columns by default</string>
</property>
<property name="toolTip">
<string>New columns start in stacked mode</string>
</property>
</widget>
</item>
<item row="11" column="1">
<widget class="QCheckBox" name="kcfg_resizeNeighborColumn">
<property name="text">
<string>Resize neighbor column on edge resize</string>
</property>
<property name="toolTip">
<string>When resizing a column by dragging its edge, also inversely resize the column on the other side of the edge</string>
</property>
</widget>
</item>
<item row="12" column="1">
<widget class="QCheckBox" name="kcfg_reMaximize">
<property name="text">
<string>Re-maximize tiled windows</string>
</property>
<property name="toolTip">
<string>Restore maximized and full-screen states of tiled windows on focus</string>
</property>
</widget>
</item>
<item row="13" column="1">
<spacer name="separator_layering">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>12</height>
</size>
</property>
</spacer>
</item>
<item row="14" column="0">
<widget class="QLabel" name="label_layering">
<property name="text">
<string>Layering mode:</string>
</property>
</widget>
</item>
<item row="14" column="1">
<widget class="QRadioButton" name="kcfg_tiledKeepBelow">
<property name="text">
<string>Keep tiled windows below</string>
</property>
</widget>
</item>
<item row="15" column="1">
<widget class="QRadioButton" name="kcfg_floatingKeepAbove">
<property name="text">
<string>Keep floating windows above</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_windowRules">
<attribute name="title">
<string>Window Rules</string>
</attribute>
<layout class="QVBoxLayout" name="layout_tab_windowRules">
<layout class="QVBoxLayout">
<item>
<widget class="QPlainTextEdit" name="kcfg_windowRules">
<property name="tabChangesFocus">

View File

@@ -1,7 +1,7 @@
import QtQuick 2.15
import QtQuick 6.0
import org.kde.kwin 3.0
import org.kde.notification 1.0
import "./main.js" as Karousel
import "../code/main.js" as Karousel
Item {
id: qmlBase

View File

@@ -1,20 +1,21 @@
{
"KPackageStructure": "KWin/Script",
"KPlugin": {
"Name": "Karousel",
"Description": "Manual columnar tiling extension for KWin",
"Description": "Scrollable tiling extension for KWin",
"Icon": "preferences-system-windows",
"Authors": [{
"Email": "peter.fajdiga@gmail.com",
"Name": "Peter Fajdiga"
}],
"Id": "karousel",
"ServiceTypes": ["KWin/Script"],
"Version": "0.5",
"Version": "0.9.3",
"License": "GPLv3",
"Website": "https://github.com/peterfajdiga/karousel",
"BugReportUrl": "https://github.com/peterfajdiga/karousel/issues"
},
"X-Plasma-API": "declarativescript",
"X-Plasma-MainScript": "code/main.qml",
"X-Plasma-API-Minimum-Version": "6.0",
"X-Plasma-MainScript": "ui/main.qml",
"X-KDE-ConfigModule": "kwin/effects/configs/kcm_kwin4_genericscripted"
}

2
run-ts.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
tsc -p "$1" --outFile ./run-ts-tmp.js && node ./run-ts-tmp.js && rm ./run-ts-tmp.js

View File

@@ -1,10 +0,0 @@
enum ClientAreaOption {
PlacementArea,
MovementArea,
MaximizeArea,
MaximizeFullArea,
FullScreenArea,
WorkArea,
FullArea,
ScreenArea,
}

View File

@@ -1,130 +0,0 @@
const defaultWindowRules = `[
{
"class": "ksmserver-logout-greeter",
"tile": false
},
{
"class": "kcalc",
"tile": false
},
{
"class": "kfind",
"tile": true
},
{
"class": "kruler",
"tile": false
},
{
"class": "krunner",
"tile": false
},
{
"class": "zoom",
"caption": "Zoom Cloud Meetings",
"tile": false
},
{
"class": "zoom",
"caption": "zoom",
"tile": false
},
{
"class": "jetbrains-idea",
"caption": "splash",
"tile": false
},
{
"class": "jetbrains-studio",
"caption": "splash",
"tile": false
},
{
"class": "jetbrains-idea",
"caption": "Unstash Changes|Paths Affected by stash@.*",
"tile": true
},
{
"class": "jetbrains-studio",
"caption": "Unstash Changes|Paths Affected by stash@.*",
"tile": true
}
]`;
const configDef = [
{
"name": "gapsOuterTop",
"type": "UInt",
"default": 18
},
{
"name": "gapsOuterBottom",
"type": "UInt",
"default": 18
},
{
"name": "gapsOuterLeft",
"type": "UInt",
"default": 18
},
{
"name": "gapsOuterRight",
"type": "UInt",
"default": 18
},
{
"name": "gapsInnerHorizontal",
"type": "UInt",
"default": 18
},
{
"name": "gapsInnerVertical",
"type": "UInt",
"default": 18
},
{
"name": "overscroll",
"type": "UInt",
"default": 0
},
{
"name": "manualScrollStep",
"type": "UInt",
"default": 200
},
{
"name": "untileOnDrag",
"type": "Bool",
"default": true
},
{
"name": "stackColumnsByDefault",
"type": "Bool",
"default": false
},
{
"name": "resizeNeighborColumn",
"type": "Bool",
"default": false
},
{
"name": "reMaximize",
"type": "Bool",
"default": false
},
{
"name": "tiledKeepBelow",
"type": "Bool",
"default": true
},
{
"name": "floatingKeepAbove",
"type": "Bool",
"default": false
},
{
"name": "windowRules",
"type": "String",
"default": defaultWindowRules
}
];

View File

@@ -1,6 +0,0 @@
declare const qmlBase: QmlObject;
declare const notificationInvalidWindowRules: Notification;
type Notification = {
sendEvent(): void;
};

75
src/extern/kwin.d.ts vendored
View File

@@ -1,75 +0,0 @@
declare const KWin: {
// Functions
readConfig(key: string, defaultValue: any): any;
registerShortcut(name: string, description: string, keySequence: string, callback: () => void): void;
};
declare const workspace: {
// Read-write Properties
readonly desktops: number;
readonly currentDesktop: number;
readonly currentActivity: string;
// Read-write Properties
activeClient: KwinClient;
// Signals
currentDesktopChanged: QSignal<[oldDesktopNumber: number]>
clientAdded: QSignal<[KwinClient]>;
clientRemoved: QSignal<[KwinClient]>;
clientMinimized: QSignal<[KwinClient]>;
clientUnminimized: QSignal<[KwinClient]>;
clientMaximizeSet: QSignal<[KwinClient, horizontally: boolean, vertically: boolean]>;
clientActivated: QSignal<[KwinClient]>;
numberDesktopsChanged: QSignal<[oldNumberOfVirtualDesktops: number]>;
currentActivityChanged: QSignal<[newActivity: string]>;
virtualScreenSizeChanged: QSignal<[void]>;
// Functions
clientArea(option: ClientAreaOption, screenNumber: number, desktopNumber: number);
clientList(): KwinClient[];
};
type Tile = any;
interface KwinClient {
// Read-only Properties
readonly shadeable: boolean;
readonly caption: string;
readonly minSize: QSize;
readonly transient: boolean;
readonly transientFor: KwinClient;
readonly move: boolean;
readonly resize: boolean;
readonly resizeable: boolean;
readonly screen: number;
readonly resourceClass: QByteArray;
readonly dock: boolean;
readonly normalWindow: boolean;
readonly managed: boolean;
// Read-write Properties
fullScreen: boolean;
activities: string[]; // empty array means all activities
keepAbove: boolean;
keepBelow: boolean;
shade: boolean;
minimized: boolean;
frameGeometry: QRect;
desktop: number; // -1 means all desktops
tile: Tile;
// Signals
fullScreenChanged: QSignal<[void]>;
desktopChanged: QSignal<[void]>;
activitiesChanged: QSignal<[KwinClient]>;
captionChanged: QSignal<[void]>;
tileChanged: QSignal<[Tile]>;
moveResizedChanged: QSignal<[void]>;
moveResizeCursorChanged: QSignal<[void]>;
clientStartUserMovedResized: QSignal<[void]>;
frameGeometryChanged: QSignal<[KwinClient, oldGeometry: QRect]>;
// Functions
setMaximize(vertically: boolean, horizontally: boolean): void;
}

View File

@@ -5,9 +5,20 @@ console.log(`<?xml version="1.0" encoding="UTF-8"?>
for (const entry of configDef) {
console.log(` <entry name="${entry.name}" type="${entry.type}">
<default>${entry.default}</default>
<default>${escapeXml(entry.default)}</default>
</entry>`);
}
console.log(` </group>
</kcfg>`);
function escapeXml(input: any) {
if (typeof input === "string") {
return input
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
} else {
return input;
}
}

View File

@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["../../lib/**/*", "./**/*"]
}

View File

@@ -1,27 +1,10 @@
type KeyBinding = {
name: string;
description: string;
comment?: string;
defaultKeySequence: string;
action: string;
}
type NumKeyBinding = {
name: string;
description: string;
comment?: string;
defaultModifiers: string;
fKeys: boolean;
action: string;
}
function formatComment(comment: string | undefined) {
return comment === undefined ? "" : ` (${comment})`;
}
function printCols(...columns: (string[] | string)[]) {
const nCols = columns.length;
if (nCols == 0) {
if (nCols === 0) {
return;
}
@@ -30,7 +13,7 @@ function printCols(...columns: (string[] | string)[]) {
).map(
(column: string[] | string) => column.length
));
if (nRows == Infinity) {
if (nRows === Infinity) {
// we only have single string columns
nRows = 1;
}

View File

@@ -0,0 +1,8 @@
{
"extends": "../../../tsconfig.json",
"include": [
"../../../lib/**/*",
"../keyBindings.ts",
"./**/*"
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "../../../tsconfig.json",
"include": [
"../../../lib/**/*",
"../keyBindings.ts",
"./**/*"
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "../../../tsconfig.json",
"include": [
"../../../lib/**/*",
"../keyBindings.ts",
"./**/*"
]
}

View File

@@ -1,211 +0,0 @@
const keyBindings: KeyBinding[] = [
{
"name": "window-toggle-floating",
"description": "Toggle floating",
"defaultKeySequence": "Meta+Space",
"action": "windowToggleFloating",
},
{
"name": "focus-left",
"description": "Move focus left",
"defaultKeySequence": "Meta+A",
"action": "focusLeft",
},
{
"name": "focus-right",
"description": "Move focus right",
"comment": "Clashes with default KDE shortcuts, may require manual remapping",
"defaultKeySequence": "Meta+D",
"action": "focusRight",
},
{
"name": "focus-up",
"description": "Move focus up",
"comment": "Clashes with default KDE shortcuts, may require manual remapping",
"defaultKeySequence": "Meta+W",
"action": "focusUp",
},
{
"name": "focus-down",
"description": "Move focus down",
"comment": "Clashes with default KDE shortcuts, may require manual remapping",
"defaultKeySequence": "Meta+S",
"action": "focusDown",
},
{
"name": "focus-start",
"description": "Move focus to start",
"defaultKeySequence": "Meta+Home",
"action": "focusStart",
},
{
"name": "focus-end",
"description": "Move focus to end",
"defaultKeySequence": "Meta+End",
"action": "focusEnd",
},
{
"name": "window-move-left",
"description": "Move window left",
"comment": "Moves window out of and into columns",
"defaultKeySequence": "Meta+Shift+A",
"action": "windowMoveLeft",
},
{
"name": "window-move-right",
"description": "Move window right",
"comment": "Moves window out of and into columns",
"defaultKeySequence": "Meta+Shift+D",
"action": "windowMoveRight",
},
{
"name": "window-move-up",
"description": "Move window up",
"defaultKeySequence": "Meta+Shift+W",
"action": "windowMoveUp",
},
{
"name": "window-move-down",
"description": "Move window down",
"defaultKeySequence": "Meta+Shift+S",
"action": "windowMoveDown",
},
{
"name": "window-move-start",
"description": "Move window to start",
"defaultKeySequence": "Meta+Shift+Home",
"action": "windowMoveStart",
},
{
"name": "window-move-end",
"description": "Move window to end",
"defaultKeySequence": "Meta+Shift+End",
"action": "windowMoveEnd",
},
{
"name": "column-toggle-stacked",
"description": "Toggle stacked layout for focused column",
"defaultKeySequence": "Meta+X",
"action": "columnToggleStacked",
},
{
"name": "column-move-left",
"description": "Move column left",
"defaultKeySequence": "Meta+Ctrl+Shift+A",
"action": "columnMoveLeft",
},
{
"name": "column-move-right",
"description": "Move column right",
"defaultKeySequence": "Meta+Ctrl+Shift+D",
"action": "columnMoveRight",
},
{
"name": "column-move-start",
"description": "Move column to start",
"defaultKeySequence": "Meta+Ctrl+Shift+Home",
"action": "columnMoveStart",
},
{
"name": "column-move-end",
"description": "Move column to end",
"defaultKeySequence": "Meta+Ctrl+Shift+End",
"action": "columnMoveEnd",
},
{
"name": "column-width-increase",
"description": "Increase column width",
"defaultKeySequence": "Meta+Ctrl++",
"action": "columnWidthIncrease",
},
{
"name": "column-width-decrease",
"description": "Decrease column width",
"defaultKeySequence": "Meta+Ctrl+-",
"action": "columnWidthDecrease",
},
{
"name": "grid-scroll-focused",
"description": "Center focused window",
"comment": "Scrolls so that the focused window is centered in the screen",
"defaultKeySequence": "Meta+Alt+Return",
"action": "gridScrollFocused",
},
{
"name": "grid-scroll-left-column",
"description": "Scroll one column to the left",
"defaultKeySequence": "Meta+Alt+A",
"action": "gridScrollLeftColumn",
},
{
"name": "grid-scroll-right-column",
"description": "Scroll one column to the right",
"defaultKeySequence": "Meta+Alt+D",
"action": "gridScrollRightColumn",
},
{
"name": "grid-scroll-left",
"description": "Scroll left",
"defaultKeySequence": "Meta+Alt+PgUp",
"action": "gridScrollLeft",
},
{
"name": "grid-scroll-right",
"description": "Scroll right",
"defaultKeySequence": "Meta+Alt+PgDown",
"action": "gridScrollRight",
},
{
"name": "grid-scroll-start",
"description": "Scroll to start",
"defaultKeySequence": "Meta+Alt+Home",
"action": "gridScrollStart",
},
{
"name": "grid-scroll-end",
"description": "Scroll to end",
"defaultKeySequence": "Meta+Alt+End",
"action": "gridScrollEnd",
},
];
const numKeyBindings: NumKeyBinding[] = [
{
"name": "focus-",
"description": "Move focus to column ",
"comment": "Clashes with default KDE shortcuts, may require manual remapping",
"defaultModifiers": "Meta",
"fKeys": false,
"action": "focusColumn",
},
{
"name": "window-move-to-column-",
"description": "Move window to column ",
"comment": "Requires manual remapping according to your keyboard layout, e.g. Meta+Shift+1 -> Meta+!",
"defaultModifiers": "Meta+Shift",
"fKeys": false,
"action": "windowMoveToColumn",
},
{
"name": "column-move-to-column-",
"description": "Move column to position ",
"comment": "Requires manual remapping according to your keyboard layout, e.g. Meta+Ctrl+Shift+1 -> Meta+Ctrl+!",
"defaultModifiers": "Meta+Ctrl+Shift",
"fKeys": false,
"action": "columnMoveToColumn",
},
{
"name": "column-move-to-desktop-",
"description": "Move column to desktop ",
"defaultModifiers": "Meta+Ctrl+Shift",
"fKeys": true,
"action": "columnMoveToDesktop",
},
{
"name": "tail-move-to-desktop-",
"description": "Move this and all following columns to desktop ",
"defaultModifiers": "Meta+Ctrl+Shift+Alt",
"fKeys": true,
"action": "tailMoveToDesktop",
},
];

View File

@@ -1,65 +0,0 @@
type KeyBinding = {
name: string;
description: string;
comment?: string;
defaultKeySequence: string;
action: keyof ReturnType<typeof Actions.init>;
};
type NumKeyBinding = {
name: string;
description: string;
comment?: string;
defaultModifiers: string;
fKeys: boolean;
action: keyof ReturnType<typeof Actions.initNum>;
};
function catchWrap(f: () => void) {
return () => {
try {
f();
} catch (error: any) {
log(error);
log(error.stack);
}
};
}
function registerKeyBinding(name: string, description: string, keySequence: string, callback: () => void) {
KWin.registerShortcut(
"karousel-" + name,
"Karousel: " + description,
keySequence,
catchWrap(callback),
);
}
function registerNumKeyBindings(name: string, description: string, modifiers: string, fKeys: boolean, callback: (i: number) => void) {
const numPrefix = fKeys ? "F" : "";
const n = fKeys ? 12 : 9;
for (let i = 0; i < 12; i++) {
const numKey = String(i + 1);
const keySequence = i < n ?
modifiers + "+" + numPrefix + numKey :
"";
registerKeyBinding(
name + numKey,
description + numKey,
keySequence,
() => callback(i),
);
}
}
function registerKeyBindings(world: World, config: Config) {
const actions = Actions.init(world, config);
for (const binding of keyBindings) {
registerKeyBinding(binding.name, binding.description, binding.defaultKeySequence, actions[binding.action]);
}
const numActions = Actions.initNum(world);
for (const binding of numKeyBindings) {
registerNumKeyBindings(binding.name, binding.description, binding.defaultModifiers, binding.fKeys, numActions[binding.action]);
}
}

View File

@@ -1,198 +0,0 @@
class Desktop {
public readonly grid: Grid;
public readonly desktopNumber: number;
private readonly pinManager: PinManager;
private readonly config: Desktop.Config;
private scrollX: number;
private dirty: boolean;
private dirtyPins: boolean;
public clientArea: QRect;
public tilingArea: QRect;
constructor(desktopNumber: number, pinManager: PinManager, config: Desktop.Config, layoutConfig: LayoutConfig) {
this.pinManager = pinManager;
this.config = config;
this.scrollX = 0;
this.dirty = true;
this.dirtyPins = true;
this.desktopNumber = desktopNumber;
this.grid = new Grid(this, layoutConfig);
this.clientArea = Desktop.getClientArea(desktopNumber);
this.tilingArea = Desktop.getTilingArea(this.clientArea, desktopNumber, pinManager, config);
}
private updateArea() {
const newClientArea = Desktop.getClientArea(this.desktopNumber);
if (newClientArea === this.clientArea && !this.dirtyPins) {
return;
}
this.clientArea = newClientArea;
this.tilingArea = Desktop.getTilingArea(newClientArea, this.desktopNumber, this.pinManager, this.config);
this.dirty = true;
this.dirtyPins = false;
this.grid.onScreenSizeChanged();
this.autoAdjustScroll();
}
private static getClientArea(desktopNumber: number) {
return workspace.clientArea(ClientAreaOption.PlacementArea, 0, desktopNumber);
}
private static getTilingArea(clientArea: QRect, desktopNumber: number, pinManager: PinManager, config: Desktop.Config) {
const availableSpace = pinManager.getAvailableSpace(desktopNumber, clientArea);
const top = availableSpace.top + config.marginTop;
const bottom = availableSpace.bottom - config.marginBottom;
const left = availableSpace.left + config.marginLeft;
const right = availableSpace.right - config.marginRight;
return Qt.rect(
left,
top,
right - left + 1,
bottom - top + 1,
)
}
// calculates Desktop.Pos that scrolls the column into view
public getScrollPosForColumn(column: Column) {
const left = column.getLeft();
const right = column.getRight();
const initialScrollPos = this.getCurrentScrollPos();
let targetScrollX: number;
if (left < initialScrollPos.getLeft()) {
targetScrollX = left;
} else if (right > initialScrollPos.getRight()) {
targetScrollX = right - this.tilingArea.width;
} else {
return this.getScrollPos(this.clampScrollX(this.scrollX));
}
const overscroll = this.getTargetOverscroll(targetScrollX, left < initialScrollPos.getLeft());
return this.getScrollPos(this.clampScrollX(targetScrollX + overscroll));
}
private getTargetOverscroll(targetScrollX: number, scrollLeft: boolean) {
if (this.config.overscroll === 0) {
return 0;
}
const visibleColumnsWidth = this.grid.getVisibleColumnsWidth(this.getScrollPos(targetScrollX), true);
const remainingSpace = this.tilingArea.width - visibleColumnsWidth;
const overscrollX = Math.min(this.config.overscroll, Math.round(remainingSpace / 2));
const direction = scrollLeft ? -1 : 1;
return overscrollX * direction;
}
public scrollToColumn(column: Column) {
this.setScroll(this.getScrollPosForColumn(column).x, true);
}
public scrollCenterColumn(column: Column) {
const windowCenter = column.getLeft() + column.getWidth() / 2;
const screenCenter = this.scrollX + this.tilingArea.width / 2;
this.adjustScroll(Math.round(windowCenter - screenCenter), false);
}
public autoAdjustScroll() {
const focusedColumn = this.grid.getLastFocusedColumn();
if (focusedColumn === null) {
this.removeOverscroll();
return;
}
if (focusedColumn.grid !== this.grid) {
return;
}
this.scrollToColumn(focusedColumn);
}
private getScrollPos(scrollX: number) {
return new Desktop.ScrollPos(scrollX, this.tilingArea.width);
}
public getCurrentScrollPos() {
return this.getScrollPos(this.scrollX);
}
private clampScrollX(x: number) {
let minScroll = 0;
let maxScroll = this.grid.getWidth() - this.tilingArea.width;
if (maxScroll < 0) {
const centerScroll = Math.round(maxScroll / 2);
minScroll = centerScroll;
maxScroll = centerScroll;
}
return clamp(x, minScroll, maxScroll);
}
private setScroll(x: number, force: boolean) {
const oldScrollX = this.scrollX;
this.scrollX = force ? x : this.clampScrollX(x);
if (this.scrollX !== oldScrollX) {
this.onLayoutChanged();
}
}
private applyScrollPos(scrollPos: Desktop.ScrollPos) {
this.setScroll(scrollPos.x, true);
}
public adjustScroll(dx: number, force: boolean) {
this.setScroll(this.scrollX + dx, force);
}
private removeOverscroll() {
this.setScroll(this.scrollX, false);
}
public arrange() {
// TODO (optimization): only arrange visible windows
this.updateArea();
if (!this.dirty) {
return;
}
this.grid.arrange(this.tilingArea.x - this.scrollX);
this.dirty = false;
}
public onLayoutChanged() {
this.dirty = true;
}
public onPinsChanged() {
this.dirty = true;
this.dirtyPins = true;
}
public destroy() {
this.grid.destroy();
}
}
namespace Desktop {
export type Config = {
marginTop: number,
marginBottom: number,
marginLeft: number,
marginRight: number,
overscroll: number,
};
export class ScrollPos {
public readonly x: number;
public readonly width: number;
constructor(x: number, width: number) {
this.x = x;
this.width = width;
}
public getLeft() {
return this.x;
}
public getRight() {
return this.x + this.width;
}
}
}

View File

@@ -1,47 +1,47 @@
namespace Actions {
export function init(world: World, config: Config) {
return {
focusLeft: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
export function getAction(world: World, config: Config, name: string) {
switch (name) {
case "focus-left": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) {
return;
}
prevColumn.focus();
});
},
};
focusRight: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
case "focus-right": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) {
return;
}
nextColumn.focus();
});
},
};
focusUp: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
case "focus-up": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const prevWindow = column.getPrevWindow(window);
if (prevWindow === null) {
return;
}
prevWindow.focus();
});
},
};
focusDown: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
case "focus-down": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const nextWindow = column.getNextWindow(window);
if (nextWindow === null) {
return;
}
nextWindow.focus();
});
},
};
focusStart: () => {
case "focus-start": return () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const firstColumn = grid.getFirstColumn();
@@ -50,9 +50,9 @@ namespace Actions {
}
firstColumn.focus();
});
},
};
focusEnd: () => {
case "focus-end": return () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const lastColumn = grid.getLastColumn();
@@ -61,10 +61,10 @@ namespace Actions {
}
lastColumn.focus();
});
},
};
windowMoveLeft: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
case "window-move-left": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
if (column.getWindowCount() === 1) {
// move from own column into existing column
const prevColumn = grid.getPrevColumn(column);
@@ -79,10 +79,10 @@ namespace Actions {
window.moveToColumn(newColumn);
}
});
},
};
windowMoveRight: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
case "window-move-right": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
if (column.getWindowCount() === 1) {
// move from own column into existing column
const nextColumn = grid.getNextColumn(column);
@@ -97,94 +97,100 @@ namespace Actions {
window.moveToColumn(newColumn);
}
});
},
};
windowMoveUp: () => {
case "window-move-up": return () => {
// TODO (optimization): only arrange moved windows
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveWindowUp(window);
});
},
};
windowMoveDown: () => {
case "window-move-down": return () => {
// TODO (optimization): only arrange moved windows
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveWindowDown(window);
});
},
};
windowMoveStart: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
case "window-move-start": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const newColumn = new Column(grid, null);
window.moveToColumn(newColumn);
});
},
};
windowMoveEnd: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
case "window-move-end": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const newColumn = new Column(grid, grid.getLastColumn());
window.moveToColumn(newColumn);
});
},
};
windowToggleFloating: () => {
const kwinClient = workspace.activeClient;
case "window-toggle-floating": return () => {
const kwinClient = Workspace.activeWindow;
world.do((clientManager, desktopManager) => {
clientManager.toggleFloatingClient(kwinClient);
});
},
};
columnMoveLeft: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
case "column-move-left": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
grid.moveColumnLeft(column);
});
},
};
columnMoveRight: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
case "column-move-right": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
grid.moveColumnRight(column);
});
},
};
columnMoveStart: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
case "column-move-start": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveAfter(null);
});
},
};
columnMoveEnd: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
case "column-move-end": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveAfter(grid.getLastColumn());
});
},
};
columnToggleStacked: () => {
world.doIfTiledFocused(false, (world, desktopManager, window, column, grid) => {
case "column-toggle-stacked": return () => {
world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
column.toggleStacked();
});
},
};
columnWidthIncrease: () => {
world.doIfTiledFocused(false, (world, desktopManager, window, column, grid) => {
grid.increaseColumnWidth(column);
case "column-width-increase": return () => {
world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
config.columnResizer.increaseWidth(column, config.manualResizeStep);
});
},
};
columnWidthDecrease: () => {
world.doIfTiledFocused(false, (world, desktopManager, window, column, grid) => {
grid.decreaseColumnWidth(column);
case "column-width-decrease": return () => {
world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
config.columnResizer.decreaseWidth(column, config.manualResizeStep);
});
},
};
gridScrollLeft: () => {
case "columns-width-equalize": return () => {
world.do((clientManager, desktopManager) => {
desktopManager.getCurrentDesktop().equalizeVisibleColumnsWidths();
});
};
case "grid-scroll-left": return () => {
gridScroll(world, -config.manualScrollStep);
},
};
gridScrollRight: () => {
case "grid-scroll-right": return () => {
gridScroll(world, config.manualScrollStep);
},
};
gridScrollStart: () => {
case "grid-scroll-start": return () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const firstColumn = grid.getFirstColumn();
@@ -193,9 +199,9 @@ namespace Actions {
}
grid.desktop.scrollToColumn(firstColumn);
});
},
};
gridScrollEnd: () => {
case "grid-scroll-end": return () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const lastColumn = grid.getLastColumn();
@@ -204,18 +210,18 @@ namespace Actions {
}
grid.desktop.scrollToColumn(lastColumn);
});
},
};
gridScrollFocused: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
grid.desktop.scrollCenterColumn(column);
case "grid-scroll-focused": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
grid.desktop.scrollCenterRange(column);
})
},
};
gridScrollLeftColumn: () => {
case "grid-scroll-left-column": return () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const column = grid.getLeftmostVisibleColumn(grid.desktop.getCurrentScrollPos(), true);
const column = grid.getLeftmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
if (column === null) {
return;
}
@@ -227,12 +233,12 @@ namespace Actions {
grid.desktop.scrollToColumn(prevColumn);
});
},
};
gridScrollRightColumn: () => {
case "grid-scroll-right-column": return () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const column = grid.getRightmostVisibleColumn(grid.desktop.getCurrentScrollPos(), true);
const column = grid.getRightmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
if (column === null) {
return;
}
@@ -244,39 +250,41 @@ namespace Actions {
grid.desktop.scrollToColumn(nextColumn);
});
},
};
};
default: throw new Error("unknown action: " + name);
}
}
export function initNum(world: World) {
return {
focusColumn: (columnIndex: number) => {
export function getNumAction(world: World, name: string) {
switch (name) {
case "focus-": return (columnIndex: number) => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return null;
return;
}
targetColumn.focus();
});
},
};
windowMoveToColumn: (columnIndex: number) => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
case "window-move-to-column-": return (columnIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return null;
return;
}
window.moveToColumn(targetColumn);
grid.desktop.autoAdjustScroll();
});
},
};
columnMoveToColumn: (columnIndex: number) => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
case "column-move-to-column-": return (columnIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null || targetColumn === column) {
return null;
return;
}
if (targetColumn.isAfter(column)) {
column.moveAfter(targetColumn);
@@ -284,30 +292,38 @@ namespace Actions {
column.moveAfter(grid.getPrevColumn(targetColumn));
}
});
},
};
columnMoveToDesktop: (desktopIndex: number) => {
case "column-move-to-desktop-": return (desktopIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, oldGrid) => {
const desktopNumber = desktopIndex + 1;
const newGrid = desktopManager.getDesktopInCurrentActivity(desktopNumber).grid;
const kwinDesktop = Workspace.desktops[desktopIndex];
if (kwinDesktop === undefined) {
return;
}
const newGrid = desktopManager.getDesktopInCurrentActivity(kwinDesktop).grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
column.moveToGrid(newGrid, newGrid.getLastColumn());
});
},
};
tailMoveToDesktop: (desktopIndex: number) => {
case "tail-move-to-desktop-": return (desktopIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, oldGrid) => {
const desktopNumber = desktopIndex + 1;
const newGrid = desktopManager.getDesktopInCurrentActivity(desktopNumber).grid;
const kwinDesktop = Workspace.desktops[desktopIndex];
if (kwinDesktop === undefined) {
return;
}
const newGrid = desktopManager.getDesktopInCurrentActivity(kwinDesktop).grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
oldGrid.evacuateTail(newGrid, column);
});
},
};
};
default: throw new Error("unknown num action: " + name);
}
}
function gridScroll(world: World, amount: number) {
@@ -319,5 +335,12 @@ namespace Actions {
export type Config = {
manualScrollStep: number,
manualResizeStep: number,
columnResizer: ColumnResizer,
};
export type ColumnResizer = {
increaseWidth(column: Column, step: number): void,
decreaseWidth(column: Column, step: number): void,
}
}

View File

@@ -0,0 +1,96 @@
class ContextualResizer {
public increaseWidth(column: Column, step: number) {
const grid = column.grid;
const desktop = grid.desktop;
const visibleRange = desktop.getCurrentVisibleRange();
if(!column.isVisible(visibleRange, true) || column.getWidth() >= column.getMaxWidth()) {
return;
}
let leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange, true);
let rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange, true);
if (leftVisibleColumn === null || rightVisibleColumn === null) {
console.assert(false); // should at least see self
return;
}
const leftSpace = leftVisibleColumn.getLeft() - visibleRange.getLeft();
const rightSpace = visibleRange.getRight() - rightVisibleColumn.getRight();
const newWidth = ContextualResizer.findNextStep(
[
visibleRange.getWidth(),
column.getWidth() + step,
column.getWidth() + leftSpace + rightSpace,
column.getWidth() + leftSpace + rightSpace + leftVisibleColumn.getWidth() + grid.config.gapsInnerHorizontal,
column.getWidth() + leftSpace + rightSpace + rightVisibleColumn.getWidth() + grid.config.gapsInnerHorizontal,
],
width => width - column.getWidth(),
)
if (newWidth === undefined) {
return;
}
column.setWidth(newWidth, true);
desktop.scrollCenterVisible(column);
}
public decreaseWidth(column: Column, step: number) {
const grid = column.grid;
const desktop = grid.desktop;
const visibleRange = desktop.getCurrentVisibleRange();
if(!column.isVisible(visibleRange, true) || column.getWidth() <= column.getMinWidth()) {
return;
}
const leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange, true);
const rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange, true);
if (leftVisibleColumn === null || rightVisibleColumn === null) {
console.assert(false); // should at least see self
return;
}
let leftOffScreenColumn = grid.getPrevColumn(leftVisibleColumn);
if (leftOffScreenColumn === column) {
leftOffScreenColumn = null;
}
let rightOffScreenColumn = grid.getNextColumn(rightVisibleColumn);
if (rightOffScreenColumn === column) {
rightOffScreenColumn = null;
}
const visibleColumnsWidth = rightVisibleColumn.getRight() - leftVisibleColumn.getLeft();
const unusedWidth = visibleRange.getWidth() - visibleColumnsWidth;
const leftOffScreen = leftOffScreenColumn === null ? 0 : leftOffScreenColumn.getWidth() + grid.config.gapsInnerHorizontal - unusedWidth;
const rightOffScreen = rightOffScreenColumn === null ? 0 : rightOffScreenColumn.getWidth() + grid.config.gapsInnerHorizontal - unusedWidth;
const newWidth = ContextualResizer.findNextStep(
[
visibleRange.getWidth(),
column.getWidth() - step,
column.getWidth() - leftOffScreen,
column.getWidth() - rightOffScreen,
],
width => column.getWidth() - width,
)
if (newWidth === undefined) {
return;
}
column.setWidth(newWidth, true);
desktop.scrollCenterVisible(column);
}
private static findNextStep(steps: number[], evaluate: (step: number) => number) {
let bestScore = Infinity;
let bestStep = undefined;
for (const step of steps) {
const score = evaluate(step);
if (score > 0 && score < bestScore) {
bestScore = score;
bestStep = step;
}
}
return bestStep;
}
}

View File

@@ -0,0 +1,9 @@
class RawResizer {
public increaseWidth(column: Column, step: number) {
column.adjustWidth(step, true);
}
public decreaseWidth(column: Column, step: number) {
column.adjustWidth(-step, true);
}
}

View File

@@ -0,0 +1,13 @@
class CenterClamper {
public clampScrollX(desktop: Desktop, x: number) {
const firstColumn = desktop.grid.getFirstColumn();
if (firstColumn === null) {
return 0;
}
const lastColumn = desktop.grid.getLastColumn()!;
let minScroll = Math.round((firstColumn.getWidth() - desktop.tilingArea.width) / 2);
let maxScroll = Math.round(desktop.grid.getWidth() - (desktop.tilingArea.width + lastColumn.getWidth()) / 2);
return clamp(x, minScroll, maxScroll);
}
}

View File

@@ -0,0 +1,10 @@
class EdgeClamper {
public clampScrollX(desktop: Desktop, x: number) {
let minScroll = 0;
let maxScroll = desktop.grid.getWidth() - desktop.tilingArea.width;
if (maxScroll < 0) {
return Math.round(maxScroll / 2);
}
return clamp(x, minScroll, maxScroll);
}
}

View File

@@ -0,0 +1,5 @@
class CenteredScroller {
public scrollToColumn(desktop: Desktop, column: Column) {
desktop.scrollCenterRange(column);
}
}

View File

@@ -0,0 +1,5 @@
class GroupedScroller {
public scrollToColumn(desktop: Desktop, column: Column) {
desktop.scrollCenterVisible(column);
}
}

View File

@@ -0,0 +1,5 @@
class LazyScroller {
public scrollToColumn(desktop: Desktop, column: Column) {
desktop.scrollIntoView(column);
}
}

View File

@@ -5,12 +5,17 @@ type Config = {
gapsOuterRight: number,
gapsInnerHorizontal: number,
gapsInnerVertical: number,
overscroll: number,
manualScrollStep: number,
manualResizeStep: number,
offScreenOpacity: number,
untileOnDrag: boolean,
stackColumnsByDefault: boolean,
resizeNeighborColumn: boolean,
reMaximize: boolean,
skipSwitcher: boolean,
scrollingLazy: boolean,
scrollingCentered: boolean,
scrollingGrouped: boolean,
tiledKeepBelow: boolean,
floatingKeepAbove: boolean,
windowRules: string,

View File

@@ -0,0 +1,171 @@
const defaultWindowRules = `[
{
"class": "ksmserver-logout-greeter",
"tile": false
},
{
"class": "xwaylandvideobridge",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?plasmashell",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?kded6",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?kcalc",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?kfind",
"tile": true
},
{
"class": "(org\\\\.kde\\\\.)?kruler",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?krunner",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?yakuake",
"tile": false
},
{
"class": "zoom",
"caption": "Zoom Cloud Meetings|zoom|zoom <2>",
"tile": false
},
{
"class": "jetbrains-idea",
"caption": "splash",
"tile": false
},
{
"class": "jetbrains-studio",
"caption": "splash",
"tile": false
},
{
"class": "jetbrains-idea",
"caption": "Unstash Changes|Paths Affected by stash@.*",
"tile": true
},
{
"class": "jetbrains-studio",
"caption": "Unstash Changes|Paths Affected by stash@.*",
"tile": true
}
]`;
const configDef = [
{
name: "gapsOuterTop",
type: "UInt",
default: 18,
},
{
name: "gapsOuterBottom",
type: "UInt",
default: 18,
},
{
name: "gapsOuterLeft",
type: "UInt",
default: 18,
},
{
name: "gapsOuterRight",
type: "UInt",
default: 18,
},
{
name: "gapsInnerHorizontal",
type: "UInt",
default: 18,
},
{
name: "gapsInnerVertical",
type: "UInt",
default: 18,
},
{
name: "manualScrollStep",
type: "UInt",
default: 200,
},
{
name: "manualResizeStep",
type: "UInt",
default: 600,
},
{
name: "offScreenOpacity",
type: "UInt",
default: 100,
},
{
name: "untileOnDrag",
type: "Bool",
default: true,
},
{
name: "stackColumnsByDefault",
type: "Bool",
default: false,
},
{
name: "resizeNeighborColumn",
type: "Bool",
default: false,
},
{
name: "reMaximize",
type: "Bool",
default: false,
},
{
name: "skipSwitcher",
type: "Bool",
default: false,
},
{
name: "scrollingLazy",
type: "Bool",
default: true,
},
{
name: "scrollingCentered",
type: "Bool",
default: false,
},
{
name: "scrollingGrouped",
type: "Bool",
default: false,
},
{
name: "tiledKeepBelow",
type: "Bool",
default: true,
},
{
name: "floatingKeepAbove",
type: "Bool",
default: false,
},
{
name: "noLayering",
type: "Bool",
default: false,
},
{
name: "windowRules",
type: "String",
default: defaultWindowRules,
}
];

6
src/lib/extern/global.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
declare const console: Console;
declare const Qt: Qt;
declare const KWin: KWin;
declare const Workspace: Workspace;
declare const qmlBase: QmlObject;
declare const notificationInvalidWindowRules: Notification;

103
src/lib/extern/kwin.ts vendored Normal file
View File

@@ -0,0 +1,103 @@
type KWin = {
readConfig(key: string, defaultValue: any): any;
};
type Workspace = {
readonly activities: string[];
readonly desktops: KwinDesktop[];
readonly currentDesktop: KwinDesktop;
readonly currentActivity: string;
readonly activeScreen: Output;
readonly windows: KwinClient[];
readonly cursorPos: Readonly<QmlPoint>;
activeWindow: KwinClient;
readonly currentDesktopChanged: QSignal<[]>
readonly windowAdded: QSignal<[KwinClient]>;
readonly windowRemoved: QSignal<[KwinClient]>;
readonly windowActivated: QSignal<[KwinClient]>;
readonly desktopsChanged: QSignal<[]>;
readonly activitiesChanged: QSignal<[]>;
readonly currentActivityChanged: QSignal<[]>;
readonly virtualScreenSizeChanged: QSignal<[]>;
clientArea(option: ClientAreaOption, output: Output, kwinDesktop: KwinDesktop): QmlRect;
};
const enum ClientAreaOption {
PlacementArea,
MovementArea,
MaximizeArea,
MaximizeFullArea,
FullScreenArea,
WorkArea,
FullArea,
ScreenArea,
}
const enum MaximizedMode {
Unmaximized,
Vertically,
Horizontally,
Maximized,
}
type Tile = unknown;
type Output = unknown;
interface KwinClient {
readonly shadeable: boolean;
readonly caption: string;
readonly minSize: Readonly<QmlSize>;
readonly transient: boolean;
readonly transientFor: KwinClient;
readonly clientGeometry: Readonly<QmlRect>;
readonly move: boolean;
readonly resize: boolean;
readonly moveable: boolean;
readonly resizeable: boolean;
readonly fullScreenable: boolean;
readonly maximizable: boolean;
readonly output: Output;
readonly resourceClass: string;
readonly dock: boolean;
readonly normalWindow: boolean;
readonly managed: boolean;
readonly popupWindow: boolean;
readonly pid: number;
fullScreen: boolean;
activities: string[]; // empty array means all activities
skipSwitcher: boolean;
keepAbove: boolean;
keepBelow: boolean;
shade: boolean;
minimized: boolean;
frameGeometry: QmlRect;
desktops: KwinDesktop[]; // empty array means all desktops
tile: Tile;
opacity: number;
readonly fullScreenChanged: QSignal<[]>;
readonly desktopsChanged: QSignal<[]>;
readonly activitiesChanged: QSignal<[]>;
readonly minimizedChanged: QSignal<[]>;
readonly maximizedAboutToChange: QSignal<[MaximizedMode]>
readonly captionChanged: QSignal<[]>;
readonly tileChanged: QSignal<[]>;
readonly interactiveMoveResizeStarted: QSignal<[]>;
readonly interactiveMoveResizeFinished: QSignal<[]>;
readonly frameGeometryChanged: QSignal<[oldGeometry: QmlRect]>;
setMaximize(vertically: boolean, horizontally: boolean): void;
}
interface KwinDesktop {
readonly id: string;
}
type ShortcutHandler = {
readonly activated: QSignal<[]>;
destroy(): void;
};

3
src/lib/extern/notification.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
type Notification = {
sendEvent(): void;
};

View File

@@ -1,29 +1,33 @@
declare const console: {
log(...args: any[]);
assert(boolean);
};
type Console = {
log(...args: any[]): void;
trace(): void;
assert(assertion: boolean, message?: string): void;
}
declare const Qt: {
rect(x: number, y: number, width: number, height: number): QRect;
createQmlObject(qml: string, parent: QmlObject);
};
type Qt = {
rect(x: number, y: number, width: number, height: number): QmlRect;
createQmlObject(qml: string, parent: QmlObject): QmlObject;
}
type QmlObject = unknown;
type QByteArray = string;
type QmlPoint = {
x: number;
y: number;
}
type QRect = {
type QmlRect = {
x: number;
y: number;
width: number;
height: number;
top: number;
bottom: number;
bottom: number; // top + height
left: number;
right: number;
right: number; // left + width
};
type QSize = {
type QmlSize = {
width: number;
height: number;
};
@@ -33,9 +37,9 @@ type QSignal<T extends unknown[]> = {
disconnect(handler: (...args: [...T]) => void): void;
};
type QQmlTimer = {
type QmlTimer = {
interval: number;
triggered: QSignal<[void]>;
readonly triggered: QSignal<[]>;
restart(): void;
destroy(): void;
};

View File

@@ -0,0 +1,185 @@
const keyBindings: KeyBinding[] = [
{
name: "window-toggle-floating",
description: "Toggle floating",
defaultKeySequence: "Meta+Space",
},
{
name: "focus-left",
description: "Move focus left",
defaultKeySequence: "Meta+A",
},
{
name: "focus-right",
description: "Move focus right",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+D",
},
{
name: "focus-up",
description: "Move focus up",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+W",
},
{
name: "focus-down",
description: "Move focus down",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+S",
},
{
name: "focus-start",
description: "Move focus to start",
defaultKeySequence: "Meta+Home",
},
{
name: "focus-end",
description: "Move focus to end",
defaultKeySequence: "Meta+End",
},
{
name: "window-move-left",
description: "Move window left",
comment: "Moves window out of and into columns",
defaultKeySequence: "Meta+Shift+A",
},
{
name: "window-move-right",
description: "Move window right",
comment: "Moves window out of and into columns",
defaultKeySequence: "Meta+Shift+D",
},
{
name: "window-move-up",
description: "Move window up",
defaultKeySequence: "Meta+Shift+W",
},
{
name: "window-move-down",
description: "Move window down",
defaultKeySequence: "Meta+Shift+S",
},
{
name: "window-move-start",
description: "Move window to start",
defaultKeySequence: "Meta+Shift+Home",
},
{
name: "window-move-end",
description: "Move window to end",
defaultKeySequence: "Meta+Shift+End",
},
{
name: "column-toggle-stacked",
description: "Toggle stacked layout for focused column",
comment: "One window in the column visible, others shaded; not supported on Wayland",
defaultKeySequence: "Meta+X",
},
{
name: "column-move-left",
description: "Move column left",
defaultKeySequence: "Meta+Ctrl+Shift+A",
},
{
name: "column-move-right",
description: "Move column right",
defaultKeySequence: "Meta+Ctrl+Shift+D",
},
{
name: "column-move-start",
description: "Move column to start",
defaultKeySequence: "Meta+Ctrl+Shift+Home",
},
{
name: "column-move-end",
description: "Move column to end",
defaultKeySequence: "Meta+Ctrl+Shift+End",
},
{
name: "column-width-increase",
description: "Increase column width",
defaultKeySequence: "Meta+Ctrl++",
},
{
name: "column-width-decrease",
description: "Decrease column width",
defaultKeySequence: "Meta+Ctrl+-",
},
{
name: "columns-width-equalize",
description: "Equalize widths of visible columns",
defaultKeySequence: "Meta+Ctrl+X",
},
{
name: "grid-scroll-focused",
description: "Center focused window",
comment: "Scrolls so that the focused window is centered in the screen",
defaultKeySequence: "Meta+Alt+Return",
},
{
name: "grid-scroll-left-column",
description: "Scroll one column to the left",
defaultKeySequence: "Meta+Alt+A",
},
{
name: "grid-scroll-right-column",
description: "Scroll one column to the right",
defaultKeySequence: "Meta+Alt+D",
},
{
name: "grid-scroll-left",
description: "Scroll left",
defaultKeySequence: "Meta+Alt+PgUp",
},
{
name: "grid-scroll-right",
description: "Scroll right",
defaultKeySequence: "Meta+Alt+PgDown",
},
{
name: "grid-scroll-start",
description: "Scroll to start",
defaultKeySequence: "Meta+Alt+Home",
},
{
name: "grid-scroll-end",
description: "Scroll to end",
defaultKeySequence: "Meta+Alt+End",
},
];
const numKeyBindings: NumKeyBinding[] = [
{
name: "focus-",
description: "Move focus to column ",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultModifiers: "Meta",
fKeys: false,
},
{
name: "window-move-to-column-",
description: "Move window to column ",
comment: "Requires manual remapping according to your keyboard layout, e.g. Meta+Shift+1 -> Meta+!",
defaultModifiers: "Meta+Shift",
fKeys: false,
},
{
name: "column-move-to-column-",
description: "Move column to position ",
comment: "Requires manual remapping according to your keyboard layout, e.g. Meta+Ctrl+Shift+1 -> Meta+Ctrl+!",
defaultModifiers: "Meta+Ctrl+Shift",
fKeys: false,
},
{
name: "column-move-to-desktop-",
description: "Move column to desktop ",
defaultModifiers: "Meta+Ctrl+Shift",
fKeys: true,
},
{
name: "tail-move-to-desktop-",
description: "Move this and all following columns to desktop ",
defaultModifiers: "Meta+Ctrl+Shift+Alt",
fKeys: true,
},
];

View File

@@ -0,0 +1,67 @@
type KeyBinding = {
name: string;
description: string;
comment?: string;
defaultKeySequence: string;
};
type NumKeyBinding = {
name: string;
description: string;
comment?: string;
defaultModifiers: string;
fKeys: boolean;
};
function catchWrap(f: () => void) {
return () => {
try {
f();
} catch (error: any) {
log(error);
log(error.stack);
}
};
}
function registerKeyBinding(world: World, config: Actions.Config, shortcutActions: ShortcutAction[], keyBinding: KeyBinding) {
shortcutActions.push(new ShortcutAction(
keyBinding,
catchWrap(Actions.getAction(world, config, keyBinding.name)),
));
}
function registerNumKeyBindings(world: World, shortcutActions: ShortcutAction[], numKeyBinding: NumKeyBinding) {
const numPrefix = numKeyBinding.fKeys ? "F" : "";
const n = numKeyBinding.fKeys ? 12 : 9;
for (let i = 0; i < 12; i++) {
const numKey = String(i + 1);
const keySequence = i < n ?
numKeyBinding.defaultModifiers + "+" + numPrefix + numKey :
"";
const action = Actions.getNumAction(world, numKeyBinding.name);
shortcutActions.push(new ShortcutAction(
{
name: numKeyBinding.name + numKey,
description: numKeyBinding.description + numKey,
defaultKeySequence: keySequence,
},
catchWrap(() => action(i)),
));
}
}
// TODO: refactor
function registerKeyBindings(world: World, config: Actions.Config) {
const shortcutActions: ShortcutAction[] = [];
for (const keyBinding of keyBindings) {
registerKeyBinding(world, config, shortcutActions, keyBinding);
}
for (const numKeyBinding of numKeyBindings) {
registerNumKeyBindings(world, shortcutActions, numKeyBinding);
}
return shortcutActions;
}

View File

@@ -25,7 +25,7 @@ class Column {
this.grid = targetGrid;
targetGrid.onColumnAdded(this, prevColumn);
for (const window of this.windows.iterator()) {
window.client.kwinClient.desktop = targetGrid.desktop.desktopNumber;
window.client.kwinClient.desktops = [targetGrid.desktop.kwinDesktop];
}
}
}
@@ -92,16 +92,17 @@ class Column {
public setWidth(width: number, setPreferred: boolean) {
width = clamp(width, this.getMinWidth(), this.getMaxWidth());
const oldWidth = this.width;
if (width === this.width) {
return;
}
this.width = width;
if (setPreferred) {
for (const window of this.windows.iterator()) {
window.client.preferredWidth = width;
}
}
if (width !== oldWidth) {
this.grid.onColumnWidthChanged(this, oldWidth, width);
}
this.grid.onColumnWidthChanged(this);
}
public adjustWidth(widthDelta: number, setPreferred: boolean) {
@@ -180,7 +181,14 @@ class Column {
window.focus();
}
public arrange(x: number) {
public arrange(x: number, visibleRange: Range, forceOpaque: boolean) {
if (this.grid.config.offScreenOpacity < 1.0 && !forceOpaque) {
const opacity = this.isVisible(visibleRange, true) ? 100 : this.grid.config.offScreenOpacity;
for (const window of this.windows.iterator()) {
window.client.kwinClient.opacity = opacity;
}
}
if (this.stacked && this.windows.length() >= 2 && this.canStack()) {
this.arrangeStacked(x);
return;
@@ -237,13 +245,13 @@ class Column {
return true;
}
public isVisible(scrollPos: Desktop.ScrollPos, fullyVisible: boolean) {
public isVisible(visibleRange: Desktop.Range, fullyVisible: boolean) {
if (fullyVisible) {
return this.getLeft() >= scrollPos.getLeft() &&
this.getRight() <= scrollPos.getRight();
return this.getLeft() >= visibleRange.getLeft() &&
this.getRight() <= visibleRange.getRight();
} else {
return this.getRight() + this.grid.config.gapsInnerHorizontal > scrollPos.getLeft() &&
this.getLeft() - this.grid.config.gapsInnerHorizontal < scrollPos.getRight();
return this.getRight() + this.grid.config.gapsInnerHorizontal > visibleRange.getLeft() &&
this.getLeft() - this.grid.config.gapsInnerHorizontal < visibleRange.getRight();
}
}

311
src/lib/layout/Desktop.ts Normal file
View File

@@ -0,0 +1,311 @@
class Desktop {
public readonly grid: Grid;
public readonly kwinDesktop: KwinDesktop;
private readonly pinManager: PinManager;
private readonly config: Desktop.Config;
private scrollX: number;
private dirty: boolean;
private dirtyScroll: boolean;
private dirtyPins: boolean;
public clientArea: QmlRect;
public tilingArea: QmlRect;
constructor(kwinDesktop: KwinDesktop, pinManager: PinManager, config: Desktop.Config, layoutConfig: LayoutConfig) {
this.pinManager = pinManager;
this.config = config;
this.scrollX = 0;
this.dirty = true;
this.dirtyScroll = true;
this.dirtyPins = true;
this.kwinDesktop = kwinDesktop;
this.grid = new Grid(this, layoutConfig);
this.clientArea = Desktop.getClientArea(kwinDesktop);
this.tilingArea = Desktop.getTilingArea(this.clientArea, kwinDesktop, pinManager, config);
}
private updateArea() {
const newClientArea = Desktop.getClientArea(this.kwinDesktop);
if (newClientArea === this.clientArea && !this.dirtyPins) {
return;
}
this.clientArea = newClientArea;
this.tilingArea = Desktop.getTilingArea(newClientArea, this.kwinDesktop, this.pinManager, this.config);
this.dirty = true;
this.dirtyScroll = true;
this.dirtyPins = false;
this.grid.onScreenSizeChanged();
this.autoAdjustScroll();
}
private static getClientArea(kwinDesktop: KwinDesktop) {
return Workspace.clientArea(ClientAreaOption.PlacementArea, Workspace.activeScreen, kwinDesktop);
}
private static getTilingArea(clientArea: QmlRect, kwinDesktop: KwinDesktop, pinManager: PinManager, config: Desktop.Config) {
const availableSpace = pinManager.getAvailableSpace(kwinDesktop, clientArea);
const top = availableSpace.top + config.marginTop;
const bottom = availableSpace.bottom - config.marginBottom;
const left = availableSpace.left + config.marginLeft;
const right = availableSpace.right - config.marginRight;
return Qt.rect(
left,
top,
right - left,
bottom - top,
)
}
public scrollIntoView(range: Desktop.Range) {
const left = range.getLeft();
const right = range.getRight();
const initialVisibleRange = this.getCurrentVisibleRange();
let targetScrollX: number;
if (left < initialVisibleRange.getLeft()) {
targetScrollX = left;
} else if (right > initialVisibleRange.getRight()) {
targetScrollX = right - this.tilingArea.width;
} else {
targetScrollX = initialVisibleRange.getLeft();
}
this.setScroll(targetScrollX, false);
}
public scrollCenterRange(range: Desktop.Range) {
const windowCenter = range.getLeft() + range.getWidth() / 2;
const screenCenter = this.scrollX + this.tilingArea.width / 2;
this.adjustScroll(Math.round(windowCenter - screenCenter), false);
}
public scrollCenterVisible(focusedColumn: Column) {
const columnRange = new Desktop.ColumnRange(focusedColumn);
const visibleRange = this.getCurrentVisibleRange();
columnRange.addNeighbors(visibleRange, this.grid.config.gapsInnerHorizontal);
this.scrollCenterRange(columnRange);
}
public autoAdjustScroll() {
const focusedColumn = this.grid.getLastFocusedColumn();
if (focusedColumn === null || focusedColumn.grid !== this.grid) {
return;
}
this.scrollToColumn(focusedColumn);
}
public scrollToColumn(column: Column) {
if (this.dirtyScroll || !column.isVisible(this.getCurrentVisibleRange(), true)) {
this.config.scroller.scrollToColumn(this, column);
}
}
private getVisibleRange(scrollX: number) {
return new Desktop.RangeImpl(scrollX, this.tilingArea.width);
}
public getCurrentVisibleRange() {
return this.getVisibleRange(this.scrollX);
}
private clampScrollX(x: number) {
return this.config.clamper.clampScrollX(this, x);
}
public setScroll(x: number, force: boolean) {
const oldScrollX = this.scrollX;
this.scrollX = force ? x : this.clampScrollX(x);
if (this.scrollX !== oldScrollX) {
this.onLayoutChanged();
}
this.dirtyScroll = false;
}
public adjustScroll(dx: number, force: boolean) {
this.setScroll(this.scrollX + dx, force);
}
public equalizeVisibleColumnsWidths() {
const visibleRange = this.getCurrentVisibleRange();
const visibleColumns = Array.from(this.grid.getVisibleColumns(visibleRange, true));
let remainingWidth = this.tilingArea.width - (visibleColumns.length-1) * this.grid.config.gapsInnerHorizontal;
let remainingColumns = visibleColumns.length;
const minWidths = visibleColumns.map(column => column.getMinWidth()).sort((a, b) => b - a);
for (const minWidth of minWidths) {
if (minWidth > remainingWidth / remainingColumns) {
remainingWidth -= minWidth;
remainingColumns--;
}
}
const avgWidth = remainingWidth / remainingColumns;
for (const column of visibleColumns) {
const minWidth = column.getMinWidth();
if (minWidth > avgWidth) {
column.setWidth(minWidth, true);
} else {
const columnWidth = Math.round(remainingWidth / remainingColumns);
column.setWidth(columnWidth, true);
remainingWidth -= column.getWidth();
remainingColumns--;
}
}
this.scrollCenterRange(Desktop.RangeImpl.fromRanges(
visibleColumns[0],
visibleColumns[visibleColumns.length - 1],
));
}
public arrange() {
// TODO (optimization): only arrange visible windows
this.updateArea();
if (!this.dirty) {
return;
}
this.grid.arrange(this.tilingArea.x - this.scrollX, this.getCurrentVisibleRange());
this.dirty = false;
}
public onLayoutChanged() {
this.dirty = true;
this.dirtyScroll = true;
}
public onPinsChanged() {
this.dirty = true;
this.dirtyScroll = true;
this.dirtyPins = true;
}
public destroy() {
this.grid.destroy();
}
}
namespace Desktop {
export type Config = {
marginTop: number,
marginBottom: number,
marginLeft: number,
marginRight: number,
scroller: Desktop.Scroller,
clamper: Desktop.Clamper,
};
export type Range = {
getLeft(): number;
getRight(): number;
getWidth(): number;
}
export class RangeImpl {
private readonly x: number;
private readonly width: number;
constructor(x: number, width: number) {
this.x = x;
this.width = width;
}
public getLeft() {
return this.x;
}
public getRight() {
return this.x + this.width;
}
public getWidth() {
return this.width;
}
public static fromRanges(leftRange: Range, rightRange: Range) {
const left = leftRange.getLeft();
const right = rightRange.getRight();
return new RangeImpl(left, right - left);
}
}
export class ColumnRange {
private left: Column;
private right: Column;
private width: number;
constructor(initialColumn: Column) {
this.left = initialColumn;
this.right = initialColumn;
this.width = initialColumn.getWidth();
}
public addNeighbors(visibleRange: Desktop.Range, gap: number) {
const grid = this.left.grid;
const columnRange = this;
function canFit(column: Column) {
return columnRange.width + gap + column.getWidth() <= visibleRange.getWidth();
}
function isUsable(column: Column|null) {
return column !== null && canFit(column);
}
let leftColumn = grid.getPrevColumn(this.left);
let rightColumn = grid.getNextColumn(this.right);
function checkColumns() {
if (!isUsable(leftColumn)) {
leftColumn = null;
}
if (!isUsable(rightColumn)) {
rightColumn = null;
}
}
checkColumns();
const visibleCenter = visibleRange.getLeft() + visibleRange.getWidth() / 2;
while (leftColumn !== null || rightColumn !== null) {
const leftToCenter = leftColumn === null ? Infinity : Math.abs(leftColumn.getLeft() - visibleCenter);
const rightToCenter = rightColumn === null ? Infinity : Math.abs(rightColumn.getRight() - visibleCenter);
if (leftToCenter < rightToCenter) {
this.addLeft(leftColumn!, gap);
leftColumn = grid.getPrevColumn(leftColumn!);
} else {
this.addRight(rightColumn!, gap);
rightColumn = grid.getNextColumn(rightColumn!);
}
checkColumns();
}
}
public addLeft(column: Column, gap: number) {
this.left = column;
this.width += column.getWidth() + gap;
}
public addRight(column: Column, gap: number) {
this.right = column;
this.width += column.getWidth() + gap;
}
public getLeft() {
return this.left.getLeft();
}
public getRight() {
return this.right.getRight();
}
public getWidth() {
return this.width;
}
}
export type Scroller = {
scrollToColumn(desktop: Desktop, column: Column): void;
}
export type Clamper = {
clampScrollX(desktop: Desktop, x: number): number;
}
}

View File

@@ -1,3 +1,5 @@
import Range = Desktop.Range;
class Grid {
public readonly desktop: Desktop;
public readonly config: LayoutConfig;
@@ -41,6 +43,10 @@ class Grid {
return this.width;
}
public isUserResizing() {
return this.userResize;
}
public getPrevColumn(column: Column) {
return this.columns.getPrev(column);
}
@@ -88,39 +94,41 @@ class Grid {
this.width = x - this.config.gapsInnerHorizontal;
}
public getLeftmostVisibleColumn(scrollPos: Desktop.ScrollPos, fullyVisible: boolean) {
const scrollX = scrollPos.getLeft();
public getLeftmostVisibleColumn(visibleRange: Desktop.Range, fullyVisible: boolean) {
for (const column of this.columns.iterator()) {
const x = fullyVisible ? column.getLeft() : column.getRight() + (this.config.gapsInnerHorizontal - 1);
if (x >= scrollX) {
if (column.isVisible(visibleRange, fullyVisible)) {
return column;
}
}
return null;
}
public getRightmostVisibleColumn(scrollPos: Desktop.ScrollPos, fullyVisible: boolean) {
const scrollX = scrollPos.getRight();
public getRightmostVisibleColumn(visibleRange: Desktop.Range, fullyVisible: boolean) {
let last = null;
for (const column of this.columns.iterator()) {
const x = fullyVisible ? column.getRight() : column.getLeft() - (this.config.gapsInnerHorizontal - 1);
if (x <= scrollX) {
if (column.isVisible(visibleRange, fullyVisible)) {
last = column;
} else {
} else if (last !== null) {
break;
}
}
return last;
}
public getVisibleColumnsWidth(scrollPos: Desktop.ScrollPos, fullyVisible: boolean) {
public *getVisibleColumns(visibleRange: Desktop.Range, fullyVisible: boolean) {
for (const column of this.columns.iterator()) {
if (column.isVisible(visibleRange, fullyVisible)) {
yield column;
}
}
}
public getVisibleColumnsWidth(visibleRange: Desktop.Range, fullyVisible: boolean) {
let width = 0;
let nVisible = 0;
for (const column of this.columns.iterator()) {
if (column.isVisible(scrollPos, fullyVisible)) {
width += column.getWidth();
nVisible++;
}
for (const column of this.getVisibleColumns(visibleRange, fullyVisible)) {
width += column.getWidth();
nVisible++;
}
if (nVisible > 0) {
@@ -130,84 +138,9 @@ class Grid {
return width;
}
private getLeftOffScreenColumn(scrollPos: Desktop.ScrollPos) {
const leftVisible = this.getLeftmostVisibleColumn(scrollPos, true);
if (leftVisible === null) {
return null;
}
return this.getPrevColumn(leftVisible);
}
private getRightOffScreenColumn(scrollPos: Desktop.ScrollPos) {
const rightVisible = this.getRightmostVisibleColumn(scrollPos, true);
if (rightVisible === null) {
return null;
}
return this.getNextColumn(rightVisible);
}
public increaseColumnWidth(column: Column) {
const scrollPos = this.desktop.getScrollPosForColumn(column);
if (this.width < scrollPos.width) {
column.adjustWidth(scrollPos.width - this.width, true);
return;
}
let leftColumn = this.getLeftmostVisibleColumn(scrollPos, false);
if (leftColumn === column) {
leftColumn = null;
}
let rightColumn = this.getRightmostVisibleColumn(scrollPos, false);
if (rightColumn === column) {
rightColumn = null;
}
if (leftColumn === null && rightColumn === null) {
return;
}
const leftVisibleWidth = leftColumn === null ? Infinity : leftColumn.getRight() - scrollPos.getLeft();
const rightVisibleWidth = rightColumn === null ? Infinity : scrollPos.getRight() - rightColumn.getLeft();
const expandLeft = leftVisibleWidth < rightVisibleWidth;
const widthDelta = (expandLeft ? leftVisibleWidth : rightVisibleWidth) + this.config.gapsInnerHorizontal;
if (expandLeft) {
this.desktop.adjustScroll(widthDelta, false);
}
column.adjustWidth(widthDelta, true);
}
public decreaseColumnWidth(column: Column) {
const scrollPos = this.desktop.getScrollPosForColumn(column);
if (this.width <= scrollPos.width) {
column.setWidth(Math.round(column.getWidth() / 2), true);
return;
}
let leftColumn = this.getLeftOffScreenColumn(scrollPos);
if (leftColumn === column) {
leftColumn = null;
}
let rightColumn = this.getRightOffScreenColumn(scrollPos);
if (rightColumn === column) {
rightColumn = null;
}
if (leftColumn === null && rightColumn === null) {
return;
}
const leftInvisibleWidth = leftColumn === null ? Infinity : scrollPos.getLeft() - leftColumn.getLeft();
const rightInvisibleWidth = rightColumn === null ? Infinity : rightColumn.getRight() - scrollPos.getRight();
const shrinkLeft = leftInvisibleWidth < rightInvisibleWidth;
const widthDelta = (shrinkLeft ? leftInvisibleWidth : rightInvisibleWidth);
if (shrinkLeft) {
const maxDelta = column.getWidth() - column.getMinWidth();
this.desktop.adjustScroll(-Math.min(widthDelta, maxDelta), false);
}
column.adjustWidth(-widthDelta, true);
}
public arrange(x: number) {
public arrange(x: number, visibleRange: Range) {
for (const column of this.columns.iterator()) {
column.arrange(x);
column.arrange(x, visibleRange, this.userResize);
x += column.getWidth() + this.config.gapsInnerHorizontal;
}
@@ -239,12 +172,12 @@ class Grid {
this.columns.remove(column);
this.columnsSetX(nextColumn);
this.desktop.onLayoutChanged();
if (passFocus && columnToFocus !== null) {
columnToFocus.focus();
} else {
this.desktop.autoAdjustScroll();
}
this.desktop.onLayoutChanged();
}
public onColumnMoved(column: Column, prevColumn: Column|null) {
@@ -256,13 +189,13 @@ class Grid {
this.desktop.autoAdjustScroll();
}
public onColumnWidthChanged(column: Column, oldWidth: number, width: number) {
public onColumnWidthChanged(column: Column) {
const nextColumn = this.columns.getNext(column);
this.columnsSetX(nextColumn);
this.desktop.onLayoutChanged();
if (!this.userResize) {
this.desktop.autoAdjustScroll();
}
this.desktop.onLayoutChanged();
}
public onColumnFocused(column: Column) {

View File

@@ -1,9 +1,11 @@
type LayoutConfig = {
gapsInnerHorizontal: number,
gapsInnerVertical: number,
offScreenOpacity: number,
stackColumnsByDefault: boolean,
resizeNeighborColumn: boolean,
reMaximize: boolean,
skipSwitcher: boolean,
tiledKeepBelow: boolean,
maximizedKeepAbove: boolean,
};

View File

@@ -2,7 +2,7 @@ class Window {
public column: Column;
public readonly client: ClientWrapper;
public height: number;
public readonly focusedState: WindowState;
public readonly focusedState: Window.State;
private skipArrange: boolean;
constructor(client: ClientWrapper, column: Column) {
@@ -10,8 +10,7 @@ class Window {
this.height = client.kwinClient.frameGeometry.height;
this.focusedState = {
fullScreen: false,
maximizedHorizontally: false,
maximizedVertically: false,
maximizedMode: MaximizedMode.Unmaximized,
};
this.skipArrange = false;
this.column = column;
@@ -37,8 +36,11 @@ class Window {
if (this.column.grid.config.reMaximize && this.isFocused()) {
// do this here rather than in `onFocused` to ensure it happens after placement
// (otherwise placement may not happen at all)
if (this.focusedState.maximizedVertically || this.focusedState.maximizedHorizontally) {
this.client.setMaximize(this.focusedState.maximizedVertically, this.focusedState.maximizedHorizontally);
if (this.focusedState.maximizedMode !== MaximizedMode.Unmaximized) {
this.client.setMaximize(
this.focusedState.maximizedMode === MaximizedMode.Horizontally || this.focusedState.maximizedMode === MaximizedMode.Maximized,
this.focusedState.maximizedMode === MaximizedMode.Vertically || this.focusedState.maximizedMode === MaximizedMode.Maximized,
);
maximized = true;
}
if (this.focusedState.fullScreen) {
@@ -76,8 +78,8 @@ class Window {
this.column.grid.desktop.onLayoutChanged();
}
public onMaximizedChanged(horizontally: boolean, vertically: boolean) {
const maximized = horizontally || vertically;
public onMaximizedChanged(maximizedMode: MaximizedMode) {
const maximized = maximizedMode !== MaximizedMode.Unmaximized;
this.skipArrange = maximized;
if (this.column.grid.config.tiledKeepBelow) {
this.client.kwinClient.keepBelow = !maximized;
@@ -86,8 +88,7 @@ class Window {
this.client.kwinClient.keepAbove = maximized;
}
if (this.isFocused()) {
this.focusedState.maximizedHorizontally = horizontally;
this.focusedState.maximizedVertically = vertically;
this.focusedState.maximizedMode = maximizedMode;
}
this.column.grid.desktop.onLayoutChanged();
}
@@ -106,7 +107,7 @@ class Window {
this.column.grid.desktop.onLayoutChanged();
}
public onUserResize(oldGeometry: QRect, resizeNeighborColumn: boolean) {
public onUserResize(oldGeometry: QmlRect, resizeNeighborColumn: boolean) {
const newGeometry = this.client.kwinClient.frameGeometry;
const widthDelta = newGeometry.width - oldGeometry.width;
const heightDelta = newGeometry.height - oldGeometry.height;
@@ -142,8 +143,9 @@ class Window {
}
}
type WindowState = {
fullScreen: boolean,
maximizedHorizontally: boolean,
maximizedVertically: boolean,
namespace Window {
export type State = {
fullScreen: boolean,
maximizedMode: MaximizedMode,
}
}

View File

@@ -0,0 +1,19 @@
class ClientMatcher {
private readonly regex: RegExp;
constructor(regex: RegExp) {
this.regex = regex;
}
public matches(kwinClient: KwinClient) {
return this.regex.test(ClientMatcher.getClientString(kwinClient));
}
public static getClientString(kwinClient: KwinClient) {
return ClientMatcher.getRuleString(kwinClient.resourceClass, kwinClient.caption);
}
public static getRuleString(ruleClass: string, ruleCaption: string) {
return ruleClass + "\0" + ruleCaption;
}
}

View File

@@ -0,0 +1,5 @@
type WindowRule = {
class: string | undefined,
caption: string | undefined,
tile: boolean,
};

View File

@@ -0,0 +1,95 @@
class WindowRuleEnforcer {
private readonly preferFloating: ClientMatcher;
private readonly preferTiling: ClientMatcher;
private readonly followCaption: RegExp;
constructor(windowRules: WindowRule[]) {
const [floatRegex, tileRegex, followCaptionRegex] = WindowRuleEnforcer.createWindowRuleRegexes(windowRules);
this.preferFloating = new ClientMatcher(floatRegex);
this.preferTiling = new ClientMatcher(tileRegex);
this.followCaption = followCaptionRegex;
}
public shouldTile(kwinClient: KwinClient) {
return Clients.canTileNow(kwinClient) && (
this.preferTiling.matches(kwinClient) || (
kwinClient.normalWindow &&
!kwinClient.transient &&
kwinClient.managed &&
kwinClient.pid > -1 &&
!this.preferFloating.matches(kwinClient)
)
);
}
public initClientSignalManager(world: World, kwinClient: KwinClient) {
if (!this.followCaption.test(kwinClient.resourceClass)) {
return null;
}
const enforcer = this;
const manager = new SignalManager();
manager.connect(kwinClient.captionChanged, () => {
const shouldTile = enforcer.shouldTile(kwinClient);
world.do((clientManager, desktopManager) => {
const desktop = desktopManager.getDesktopForClient(kwinClient);
if (shouldTile && desktop !== undefined) {
clientManager.tileKwinClient(kwinClient, desktop.grid);
} else {
clientManager.floatKwinClient(kwinClient);
}
});
});
return manager;
}
private static createWindowRuleRegexes(windowRules: WindowRule[]) {
const floatRegexes: string[] = [];
const tileRegexes: string[] = [];
const followCaptionRegexes: string[] = [];
for (const windowRule of windowRules) {
const ruleClass = WindowRuleEnforcer.parseRegex(windowRule.class);
const ruleCaption = WindowRuleEnforcer.parseRegex(windowRule.caption);
const ruleString = ClientMatcher.getRuleString(
WindowRuleEnforcer.wrapParens(ruleClass),
WindowRuleEnforcer.wrapParens(ruleCaption)
);
(windowRule.tile ? tileRegexes : floatRegexes).push(ruleString);
if (ruleCaption !== ".*") {
followCaptionRegexes.push(ruleClass);
}
}
return [
WindowRuleEnforcer.joinRegexes(floatRegexes),
WindowRuleEnforcer.joinRegexes(tileRegexes),
WindowRuleEnforcer.joinRegexes(followCaptionRegexes),
];
}
private static parseRegex(rawRule: string | undefined) {
if (rawRule === undefined || rawRule === "" || rawRule === ".*") {
return ".*";
} else {
return rawRule;
}
}
private static joinRegexes(regexes: string[]) {
if (regexes.length === 0) {
return new RegExp("a^"); // match nothing
}
if (regexes.length === 1) {
return new RegExp("^(" + regexes[0] + ")$");
}
const joinedRegexes = regexes.map(WindowRuleEnforcer.wrapParens).join("|");
return new RegExp("^(" + joinedRegexes + ")$");
}
private static wrapParens(str: string) {
return "(" + str + ")";
}
}

View File

@@ -1,5 +1,5 @@
class Delayer {
private readonly timer: QQmlTimer;
private readonly timer: QmlTimer;
constructor(delay: number, f: () => void) {
this.timer = initQmlTimer();
@@ -17,8 +17,8 @@ class Delayer {
}
function initQmlTimer() {
return Qt.createQmlObject(
`import QtQuick 2.15
return <QmlTimer>Qt.createQmlObject(
`import QtQuick 6.0
Timer {}`,
qmlBase
);

View File

@@ -0,0 +1,25 @@
class ShortcutAction {
private readonly shortcutHandler: ShortcutHandler;
constructor(keyBinding: KeyBinding, f: () => void) {
this.shortcutHandler = ShortcutAction.initShortcutHandler(keyBinding);
this.shortcutHandler.activated.connect(f);
}
public destroy() {
this.shortcutHandler.destroy();
}
private static initShortcutHandler(keyBinding: KeyBinding) {
return <ShortcutHandler>Qt.createQmlObject(
`import QtQuick 6.0
import org.kde.kwin 3.0
ShortcutHandler {
name: "karousel-${keyBinding.name}";
text: "Karousel: ${keyBinding.description}";
sequence: "${keyBinding.defaultKeySequence}";
}`,
qmlBase,
);
}
}

54
src/lib/workspace.ts Normal file
View File

@@ -0,0 +1,54 @@
function initWorkspaceSignalHandlers(world: World) {
const manager = new SignalManager();
manager.connect(Workspace.windowAdded, (kwinClient: KwinClient) => {
if (Clients.canTileEver(kwinClient)) {
// never open new tileable clients on all desktops or activities
Clients.makeTileable(kwinClient);
}
world.do((clientManager, desktopManager) => {
clientManager.addClient(kwinClient)
});
});
manager.connect(Workspace.windowRemoved, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => {
clientManager.removeClient(kwinClient, true);
});
});
manager.connect(Workspace.windowActivated, (kwinClient: KwinClient) => {
if (kwinClient === null) {
return;
}
world.do((clientManager, desktopManager) => {
clientManager.onClientFocused(kwinClient);
});
});
manager.connect(Workspace.currentDesktopChanged, () => {
world.do(() => {}); // re-arrange desktop
});
manager.connect(Workspace.currentActivityChanged, () => {
world.do(() => {}); // re-arrange desktop
});
manager.connect(Workspace.desktopsChanged, () => {
world.do((clientManager, desktopManager) => {
desktopManager.updateDesktops();
})
});
manager.connect(Workspace.activitiesChanged, () => {
world.do((clientManager, desktopManager) => {
desktopManager.updateActivities();
})
});
manager.connect(Workspace.virtualScreenSizeChanged, () => {
world.onScreenResized();
});
return manager;
}

View File

@@ -27,13 +27,13 @@ class ClientManager {
public addClient(kwinClient: KwinClient) {
console.assert(!this.hasClient(kwinClient));
const desktop = this.desktopManager.getDesktopForClient(kwinClient);
let constructState: (client: ClientWrapper) => ClientState.State;
if (kwinClient.dock) {
constructState = () => new ClientState.Docked(this.world, kwinClient);
} else if (this.windowRuleEnforcer.shouldTile(kwinClient)) {
const grid = this.desktopManager.getDesktopForClient(kwinClient).grid;
constructState = (client: ClientWrapper) => new ClientState.Tiled(this.world, client, grid);
} else if (this.windowRuleEnforcer.shouldTile(kwinClient) && desktop !== undefined) {
constructState = (client: ClientWrapper) => new ClientState.Tiled(this.world, client, desktop.grid);
} else {
constructState = (client: ClientWrapper) => new ClientState.Floating(this.world, client, this.config, false);
}
@@ -76,41 +76,41 @@ class ClientManager {
return;
}
if (client.stateManager.getState() instanceof ClientState.Tiled) {
client.stateManager.setState(() => new ClientState.TiledMinimized(), kwinClient === this.lastFocusedClient);
client.stateManager.setState(
() => new ClientState.TiledMinimized(this.world, client),
kwinClient === this.lastFocusedClient,
);
}
}
public unminimizeClient(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (client.stateManager.getState() instanceof ClientState.TiledMinimized) {
const grid = this.desktopManager.getDesktopForClient(kwinClient).grid;
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), false);
}
}
public tileClient(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
public tileClient(client: ClientWrapper, grid: Grid) {
if (client.stateManager.getState() instanceof ClientState.Tiled) {
return;
}
const grid = this.desktopManager.getDesktopForClient(kwinClient).grid;
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), false);
}
public untileClient(kwinClient: KwinClient) {
public floatClient(client: ClientWrapper) {
if (client.stateManager.getState() instanceof ClientState.Floating) {
return;
}
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), false);
}
public tileKwinClient(kwinClient: KwinClient, grid: Grid) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (client.stateManager.getState() instanceof ClientState.Tiled) {
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), false);
this.tileClient(client, grid);
}
public floatKwinClient(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
this.floatClient(client);
}
public pinClient(kwinClient: KwinClient) {
@@ -118,6 +118,11 @@ class ClientManager {
if (client === undefined) {
return;
}
if (client.getMaximizedMode() !== MaximizedMode.Unmaximized) {
// the client is not really kwin-tiled, just maximized
kwinClient.tile = null;
return;
}
client.stateManager.setState(() => new ClientState.Pinned(this.world, this.pinManager, this.desktopManager, kwinClient, this.config), false);
this.pinManager.addClient(kwinClient);
for (const desktop of this.desktopManager.getDesktopsForClient(kwinClient)) {
@@ -147,8 +152,11 @@ class ClientManager {
const clientState = client.stateManager.getState();
if ((clientState instanceof ClientState.Floating || clientState instanceof ClientState.Pinned) && Clients.canTileEver(kwinClient)) {
Clients.makeTileable(kwinClient);
const grid = this.desktopManager.getDesktopForClient(kwinClient).grid;
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), false);
const desktop = this.desktopManager.getDesktopForClient(kwinClient);
if (desktop === undefined) {
return;
}
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, desktop.grid), false);
} else if (clientState instanceof ClientState.Tiled) {
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), false);
}

View File

@@ -3,10 +3,12 @@ class ClientWrapper {
public readonly stateManager: ClientState.Manager;
public transientFor: ClientWrapper | null;
private readonly transients: ClientWrapper[];
private readonly signalManager: SignalManager;
private readonly rulesSignalManager: SignalManager | null;
public preferredWidth: number;
private maximizedMode: MaximizedMode | undefined;
private readonly manipulatingGeometry: Doer;
private lastPlacement: QRect | null; // workaround for issue #19
private lastPlacement: QmlRect | null; // workaround for issue #19
constructor(
kwinClient: KwinClient,
@@ -20,6 +22,7 @@ class ClientWrapper {
if (transientFor !== null) {
transientFor.addTransient(this);
}
this.signalManager = ClientWrapper.initSignalManager(this);
this.rulesSignalManager = rulesSignalManager;
this.preferredWidth = kwinClient.frameGeometry.width;
this.manipulatingGeometry = new Doer();
@@ -35,12 +38,18 @@ class ClientWrapper {
}
this.lastPlacement = Qt.rect(x, y, width, height);
this.kwinClient.frameGeometry = this.lastPlacement;
if (this.kwinClient.frameGeometry !== this.lastPlacement) {
// frameGeometry assignment failed. This sometimes happens on Wayland
// when a window is off-screen, effectively making it stuck there.
this.kwinClient.frameGeometry.x = x; // This makes it unstuck.
this.kwinClient.frameGeometry = this.lastPlacement;
}
});
}
private moveTransient(dx: number, dy: number, desktopNumber: number) {
private moveTransient(dx: number, dy: number, kwinDesktops: KwinDesktop[]) {
if (this.stateManager.getState() instanceof ClientState.Floating) {
if (this.kwinClient.desktop === desktopNumber) {
if (Clients.isOnOneOfVirtualDesktops(this.kwinClient, kwinDesktops)) {
const frame = this.kwinClient.frameGeometry;
this.kwinClient.frameGeometry = Qt.rect(
frame.x + dx,
@@ -51,32 +60,52 @@ class ClientWrapper {
}
for (const transient of this.transients) {
transient.moveTransient(dx, dy, desktopNumber);
transient.moveTransient(dx, dy, kwinDesktops);
}
}
}
public moveTransients(dx: number, dy: number) {
for (const transient of this.transients) {
transient.moveTransient(dx, dy, this.kwinClient.desktop);
transient.moveTransient(dx, dy, this.kwinClient.desktops);
}
}
public focus() {
workspace.activeClient = this.kwinClient;
Workspace.activeWindow = this.kwinClient;
}
public isFocused() {
return workspace.activeClient === this.kwinClient;
return Workspace.activeWindow === this.kwinClient;
}
public setMaximize(horizontally: boolean, vertically: boolean) {
if (!this.kwinClient.maximizable) {
return;
}
if (this.maximizedMode === undefined) {
if (horizontally && vertically) {
this.maximizedMode = MaximizedMode.Maximized;
} else if (horizontally) {
this.maximizedMode = MaximizedMode.Horizontally;
} else if (vertically) {
this.maximizedMode = MaximizedMode.Vertically;
} else {
this.maximizedMode = MaximizedMode.Unmaximized;
}
}
this.manipulatingGeometry.do(() => {
this.kwinClient.setMaximize(vertically, horizontally);
});
}
public setFullScreen(fullScreen: boolean) {
if (!this.kwinClient.fullScreenable) {
return;
}
this.manipulatingGeometry.do(() => {
this.kwinClient.fullScreen = fullScreen;
});
@@ -92,7 +121,11 @@ class ClientWrapper {
return this.kwinClient.shade;
}
public isManipulatingGeometry(newGeometry: QRect | null) {
public getMaximizedMode() {
return this.maximizedMode;
}
public isManipulatingGeometry(newGeometry: QmlRect | null) {
if (newGeometry !== null && newGeometry === this.lastPlacement) {
return true;
}
@@ -108,7 +141,7 @@ class ClientWrapper {
this.transients.splice(i, 1);
}
public ensureTransientsVisible(screenSize: QRect) {
public ensureTransientsVisible(screenSize: QmlRect) {
for (const transient of this.transients) {
if (transient.stateManager.getState() instanceof ClientState.Floating) {
transient.ensureVisible(screenSize);
@@ -117,20 +150,21 @@ class ClientWrapper {
}
}
public ensureVisible(screenSize: QRect) {
if (this.kwinClient.desktop !== workspace.currentDesktop) {
public ensureVisible(screenSize: QmlRect) {
if (!Clients.isOnVirtualDesktop(this.kwinClient, Workspace.currentDesktop)) {
return;
}
const frame = this.kwinClient.frameGeometry;
if (frame.left < 0) {
frame.x = 0;
} else if (frame.right > screenSize.width) {
frame.x = screenSize.width - frame.width;
if (frame.left < screenSize.left) {
frame.x = screenSize.left;
} else if (frame.right > screenSize.right) {
frame.x = screenSize.right - frame.width;
}
}
public destroy(passFocus: boolean) {
this.stateManager.destroy(passFocus);
this.signalManager.destroy();
if (this.rulesSignalManager !== null) {
this.rulesSignalManager.destroy();
}
@@ -141,4 +175,17 @@ class ClientWrapper {
transient.transientFor = null;
}
}
private static initSignalManager(client: ClientWrapper) {
const manager = new SignalManager();
manager.connect(client.kwinClient.maximizedAboutToChange, (maximizedMode: MaximizedMode) => {
if (maximizedMode !== MaximizedMode.Unmaximized && client.kwinClient.tile !== null) {
client.kwinClient.tile = null;
}
client.maximizedMode = maximizedMode;
});
return manager;
}
}

49
src/lib/world/Clients.ts Normal file
View File

@@ -0,0 +1,49 @@
namespace Clients {
export function canTileEver(kwinClient: KwinClient) {
return kwinClient.moveable && kwinClient.resizeable && !kwinClient.popupWindow;
}
export function canTileNow(kwinClient: KwinClient) {
return canTileEver(kwinClient) && !kwinClient.minimized && kwinClient.desktops.length === 1 && kwinClient.activities.length === 1;
}
export function makeTileable(kwinClient: KwinClient) {
if (kwinClient.minimized) {
kwinClient.minimized = false;
}
if (kwinClient.desktops.length !== 1) {
kwinClient.desktops = [Workspace.currentDesktop];
}
if (kwinClient.activities.length !== 1) {
kwinClient.activities = [Workspace.currentActivity];
}
}
export function getKwinDesktopApprox(kwinClient: KwinClient) {
switch (kwinClient.desktops.length) {
case 0:
return Workspace.currentDesktop;
case 1:
return kwinClient.desktops[0];
default:
if (kwinClient.desktops.includes(Workspace.currentDesktop)) {
return Workspace.currentDesktop;
} else {
return kwinClient.desktops[0];
}
}
}
export function isFullScreenGeometry(kwinClient: KwinClient) {
const fullScreenArea = Workspace.clientArea(ClientAreaOption.FullScreenArea, kwinClient.output, getKwinDesktopApprox(kwinClient));
return kwinClient.frameGeometry === fullScreenArea;
}
export function isOnVirtualDesktop(kwinClient: KwinClient, kwinDesktop: KwinDesktop) {
return kwinClient.desktops.length === 0 || kwinClient.desktops.includes(kwinDesktop);
}
export function isOnOneOfVirtualDesktops(kwinClient: KwinClient, kwinDesktops: KwinDesktop[]) {
return kwinClient.desktops.length === 0 || kwinClient.desktops.some(d => kwinDesktops.includes(d));
}
}

View File

@@ -0,0 +1,127 @@
class DesktopManager {
private readonly pinManager: PinManager;
private readonly config: Desktop.Config;
public readonly layoutConfig: LayoutConfig;
private readonly desktops: Map<string, Desktop>; // key is activityId|desktopId
private kwinActivities: Set<string>;
private kwinDesktops: Set<KwinDesktop>;
constructor(pinManager: PinManager, config: Desktop.Config, layoutConfig: LayoutConfig, currentActivity: string, currentDesktop: KwinDesktop) {
this.pinManager = pinManager;
this.config = config;
this.layoutConfig = layoutConfig;
this.desktops = new Map();
this.kwinActivities = new Set(Workspace.activities);
this.kwinDesktops = new Set(Workspace.desktops);
this.addDesktop(currentActivity, currentDesktop);
}
public getDesktop(activity: string, kwinDesktop: KwinDesktop) {
const desktopKey = DesktopManager.getDesktopKey(activity, kwinDesktop);
const desktop = this.desktops.get(desktopKey);
if (desktop !== undefined) {
return desktop;
} else {
return this.addDesktop(activity, kwinDesktop);
}
}
public getCurrentDesktop() {
return this.getDesktop(Workspace.currentActivity, Workspace.currentDesktop);
}
public getDesktopInCurrentActivity(kwinDesktop: KwinDesktop) {
return this.getDesktop(Workspace.currentActivity, kwinDesktop);
}
public getDesktopForClient(kwinClient: KwinClient) {
if (kwinClient.activities.length !== 1 || kwinClient.desktops.length !== 1) {
return undefined;
}
return this.getDesktop(kwinClient.activities[0], kwinClient.desktops[0]);
}
private addDesktop(activity: string, kwinDesktop: KwinDesktop) {
const desktopKey = DesktopManager.getDesktopKey(activity, kwinDesktop);
const desktop = new Desktop(kwinDesktop, this.pinManager, this.config, this.layoutConfig);
this.desktops.set(desktopKey, desktop);
return desktop;
}
private static getDesktopKey(activity: string, kwinDesktop: KwinDesktop) {
return activity + "|" + kwinDesktop.id;
}
public updateActivities() {
const newActivities = new Set(Workspace.activities);
for (const activity of this.kwinActivities) {
if (!newActivities.has(activity)) {
this.removeActivity(activity);
}
}
this.kwinActivities = newActivities;
}
public updateDesktops() {
const newDesktops = new Set(Workspace.desktops);
for (const desktop of this.kwinDesktops) {
if (!newDesktops.has(desktop)) {
this.removeKwinDesktop(desktop);
}
}
this.kwinDesktops = newDesktops;
}
private removeActivity(activity: string) {
for (const kwinDesktop of this.kwinDesktops) {
this.destroyDesktop(activity, kwinDesktop);
}
}
private removeKwinDesktop(kwinDesktop: KwinDesktop) {
for (const activity of this.kwinActivities) {
this.destroyDesktop(activity, kwinDesktop);
}
}
private destroyDesktop(activity: string, kwinDesktop: KwinDesktop) {
const desktopKey = DesktopManager.getDesktopKey(activity, kwinDesktop);
const desktop = this.desktops.get(desktopKey);
if (desktop !== undefined) {
desktop.destroy();
this.desktops.delete(desktopKey);
}
}
public destroy() {
for (const desktop of this.desktops.values()) {
desktop.destroy();
}
}
public *getAllDesktops() {
for (const desktop of this.desktops.values()) {
yield desktop;
}
}
public getDesktopsForClient(kwinClient: KwinClient) {
const desktops = this.getDesktops(kwinClient.activities, kwinClient.desktops); // workaround for QTBUG-109880
return desktops;
}
// empty array means all
public *getDesktops(activities: string[], kwinDesktops: KwinDesktop[]) {
const matchedActivities = activities.length > 0 ? activities : this.kwinActivities.keys();
const matchedDesktops = kwinDesktops.length > 0 ? kwinDesktops : this.kwinDesktops.keys();
for (const matchedActivity of matchedActivities) {
for (const matchedDesktop of matchedDesktops) {
const desktopKey = DesktopManager.getDesktopKey(matchedActivity, matchedDesktop);
const desktop = this.desktops.get(desktopKey);
if (desktop !== undefined) {
yield desktop;
}
}
}
}
}

View File

@@ -13,11 +13,11 @@ class PinManager {
this.pinnedClients.delete(kwinClient);
}
public getAvailableSpace(desktopNumber: number, screen: QRect) {
public getAvailableSpace(kwinDesktop: KwinDesktop, screen: QmlRect) {
const baseLot = new PinManager.Lot(screen.top, screen.bottom, screen.left, screen.right);
let lots = [baseLot];
for (const client of this.pinnedClients) {
if (!Clients.isOnVirtualDesktop(client, desktopNumber)) {
if (!Clients.isOnVirtualDesktop(client, kwinDesktop)) {
continue;
}
@@ -53,7 +53,7 @@ namespace PinManager {
public readonly right: number,
) {}
public split(destLots: Lot[], obstacle: QRect) {
public split(destLots: Lot[], obstacle: QmlRect) {
if (!this.contains(obstacle)) {
// don't split
destLots.push(this);
@@ -74,9 +74,9 @@ namespace PinManager {
}
}
private contains(obstacle: QRect) {
return obstacle.right >= this.left && obstacle.left <= this.right &&
obstacle.bottom >= this.top && obstacle.top <= this.bottom;
private contains(obstacle: QmlRect) {
return obstacle.right > this.left && obstacle.left < this.right &&
obstacle.bottom > this.top && obstacle.top < this.bottom;
}
public area() {

View File

@@ -4,16 +4,21 @@ class World {
public readonly clientManager: ClientManager;
private readonly pinManager: PinManager;
private readonly workspaceSignalManager: SignalManager;
private readonly shortcutActions: ShortcutAction[];
private readonly screenResizedDelayer: Delayer;
constructor(config: Config) {
this.untileOnDrag = config.untileOnDrag;
this.workspaceSignalManager = initWorkspaceSignalHandlers(this);
this.shortcutActions = registerKeyBindings(this, {
manualScrollStep: config.manualScrollStep,
manualResizeStep: config.manualResizeStep,
columnResizer: config.scrollingCentered ? new RawResizer() : new ContextualResizer(),
});
this.screenResizedDelayer = new Delayer(1000, () => {
// this delay ensures that docks get taken into account by `workspace.clientArea`
const desktopManager = this.desktopManager; // workaround for bug in Qt5's JS engine
for (const desktop of desktopManager.desktops()) {
// this delay ensures that docks are taken into account by `Workspace.clientArea`
for (const desktop of this.desktopManager.getAllDesktops()) {
desktop.onLayoutChanged();
}
this.update();
@@ -21,6 +26,18 @@ class World {
this.pinManager = new PinManager();
const layoutConfig = {
gapsInnerHorizontal: config.gapsInnerHorizontal,
gapsInnerVertical: config.gapsInnerVertical,
offScreenOpacity: config.offScreenOpacity / 100.0,
stackColumnsByDefault: config.stackColumnsByDefault,
resizeNeighborColumn: config.resizeNeighborColumn,
reMaximize: config.reMaximize,
skipSwitcher: config.skipSwitcher,
tiledKeepBelow: config.tiledKeepBelow,
maximizedKeepAbove: config.floatingKeepAbove,
};
this.desktopManager = new DesktopManager(
this.pinManager,
{
@@ -28,36 +45,39 @@ class World {
marginBottom: config.gapsOuterBottom,
marginLeft: config.gapsOuterLeft,
marginRight: config.gapsOuterRight,
overscroll: config.overscroll,
scroller: World.createScroller(config),
clamper: config.scrollingLazy ? new EdgeClamper() : new CenterClamper(),
},
{
gapsInnerHorizontal: config.gapsInnerHorizontal,
gapsInnerVertical: config.gapsInnerVertical,
stackColumnsByDefault: config.stackColumnsByDefault,
resizeNeighborColumn: config.resizeNeighborColumn,
reMaximize: config.reMaximize,
tiledKeepBelow: config.tiledKeepBelow,
maximizedKeepAbove: config.floatingKeepAbove,
},
workspace.currentActivity,
layoutConfig,
Workspace.currentActivity,
Workspace.currentDesktop,
);
this.clientManager = new ClientManager(config, this, this.desktopManager, this.pinManager);
this.addExistingClients();
this.update();
}
private static createScroller(config: Config) {
if (config.scrollingLazy) {
return new LazyScroller();
} else if (config.scrollingCentered) {
return new CenteredScroller();
} else if (config.scrollingGrouped) {
return new GroupedScroller();
} else {
log("No scrolling mode selected, using default");
return new LazyScroller();
}
}
private addExistingClients() {
const kwinClients = workspace.clientList();
const kwinClients = Workspace.windows;
for (let i = 0; i < kwinClients.length; i++) {
const kwinClient = kwinClients[i];
this.clientManager.addClient(kwinClient);
}
}
public updateDesktops() {
this.desktopManager.update();
}
private update() {
this.desktopManager.getCurrentDesktop().arrange();
}
@@ -86,11 +106,14 @@ class World {
followTransient: boolean,
f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void,
) {
this.doIfTiled(workspace.activeClient, followTransient, f);
this.doIfTiled(Workspace.activeWindow, followTransient, f);
}
public destroy() {
this.workspaceSignalManager.destroy();
for (const shortcutAction of this.shortcutActions) {
shortcutAction.destroy();
}
this.clientManager.destroy();
this.desktopManager.destroy();
}

View File

@@ -16,7 +16,7 @@ namespace ClientState {
private static initSignalManager(world: World, kwinClient: KwinClient) {
const manager = new SignalManager();
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: KwinClient, oldGeometry: QRect) => {
manager.connect(kwinClient.frameGeometryChanged, () => {
world.onScreenResized();
});
return manager;

View File

@@ -24,7 +24,11 @@ namespace ClientState {
}
private static limitHeight(client: ClientWrapper) {
const placementArea = workspace.clientArea(ClientAreaOption.PlacementArea, client.kwinClient.screen, client.kwinClient.desktop);
const placementArea = Workspace.clientArea(
ClientAreaOption.PlacementArea,
client.kwinClient.output,
Clients.getKwinDesktopApprox(client.kwinClient),
);
const clientRect = client.kwinClient.frameGeometry;
const width = client.preferredWidth;
client.place(
@@ -54,7 +58,7 @@ namespace ClientState {
clientManager.pinClient(kwinClient);
});
}
})
});
return manager;
}

View File

@@ -21,6 +21,6 @@ namespace ClientState {
}
export type State = {
destroy(passFocus: boolean): void;
destroy(passFocus: boolean): void,
};
}

View File

@@ -30,8 +30,8 @@ namespace ClientState {
private static initSignalManager(world: World, pinManager: PinManager, kwinClient: KwinClient) {
const manager = new SignalManager();
let oldDesktopNumber = kwinClient.desktop;
let oldActivities = kwinClient.activities;
let oldDesktops = kwinClient.desktops;
manager.connect(kwinClient.tileChanged, () => {
if (kwinClient.tile === null) {
@@ -41,7 +41,7 @@ namespace ClientState {
}
});
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: KwinClient, oldGeometry: QRect) => {
manager.connect(kwinClient.frameGeometryChanged, () => {
if (kwinClient.tile === null) {
world.do((clientManager, desktopManager) => {
clientManager.unpinClient(kwinClient);
@@ -56,25 +56,24 @@ namespace ClientState {
})
});
manager.connect(kwinClient.desktopChanged, () => {
const changedDesktops = oldDesktopNumber === -1 || kwinClient.desktop === -1 ?
manager.connect(kwinClient.desktopsChanged, () => {
const changedDesktops = oldDesktops.length === 0 || kwinClient.desktops.length === 0 ?
[] :
[oldDesktopNumber, kwinClient.desktop];
union(oldDesktops, kwinClient.desktops);
world.do((clientManager, desktopManager) => {
for (const desktop of desktopManager.getDesktops(changedDesktops, kwinClient.activities)) {
for (const desktop of desktopManager.getDesktops(kwinClient.activities, changedDesktops)) {
desktop.onPinsChanged();
}
});
oldDesktopNumber = kwinClient.desktop;
oldDesktops = kwinClient.desktops;
});
manager.connect(kwinClient.activitiesChanged, (kwinClient: KwinClient) => {
const desktops = kwinClient.desktop === -1 ? [] : [kwinClient.desktop];
manager.connect(kwinClient.activitiesChanged, () => {
const changedActivities = oldActivities.length === 0 || kwinClient.activities.length === 0 ?
[] :
union(oldActivities, kwinClient.activities);
world.do((clientManager, desktopManager) => {
for (const desktop of desktopManager.getDesktops(desktops, changedActivities)) {
for (const desktop of desktopManager.getDesktops(changedActivities, kwinClient.desktops)) {
desktop.onPinsChanged();
}
});

View File

@@ -1,9 +1,11 @@
namespace ClientState {
export class Tiled implements State {
public readonly window: Window;
private readonly defaultState: Tiled.WindowState;
private readonly signalManager: SignalManager;
constructor(world: World, client: ClientWrapper, grid: Grid) {
this.defaultState = { skipSwitcher: client.kwinClient.skipSwitcher };
Tiled.prepareClientForTiling(client, grid.config);
const column = new Column(grid, grid.getLastFocusedColumn() ?? grid.getLastColumn());
@@ -21,7 +23,7 @@ namespace ClientState {
const client = window.client;
window.destroy(passFocus);
Tiled.restoreClientAfterTiling(client, grid.config, grid.desktop.clientArea);
Tiled.restoreClientAfterTiling(client, grid.config, this.defaultState, grid.desktop.clientArea);
}
private static initSignalManager(world: World, window: Window) {
@@ -29,59 +31,71 @@ namespace ClientState {
const kwinClient = client.kwinClient;
const manager = new SignalManager();
manager.connect(kwinClient.desktopChanged, () => {
manager.connect(kwinClient.desktopsChanged, () => {
world.do((clientManager, desktopManager) => {
if (kwinClient.desktop === -1) {
// windows on all desktops are not supported
clientManager.untileClient(kwinClient);
const desktop = desktopManager.getDesktopForClient(kwinClient);
if (desktop === undefined) {
// windows on multiple desktops are not supported
clientManager.floatKwinClient(kwinClient);
return;
}
Tiled.moveWindowToCorrectGrid(desktopManager, window);
Tiled.moveWindowToGrid(window, desktop.grid);
});
});
manager.connect(kwinClient.activitiesChanged, () => {
world.do((clientManager, desktopManager) => {
if (kwinClient.activities.length !== 1) {
const desktop = desktopManager.getDesktopForClient(kwinClient);
if (desktop === undefined) {
// windows on multiple activities are not supported
clientManager.untileClient(kwinClient);
clientManager.floatKwinClient(kwinClient);
return;
}
Tiled.moveWindowToCorrectGrid(desktopManager, window);
Tiled.moveWindowToGrid(window, desktop.grid);
});
})
let lastResize = false;
manager.connect(kwinClient.moveResizedChanged, () => {
manager.connect(kwinClient.minimizedChanged, () => {
console.assert(kwinClient.minimized);
world.do((clientManager, desktopManager) => {
if (kwinClient.move) {
if (world.untileOnDrag) {
clientManager.untileClient(kwinClient);
}
return;
}
const grid = window.column.grid;
const resize = kwinClient.resize;
if (!lastResize && resize) {
grid.onUserResizeStarted();
}
if (lastResize && !resize) {
grid.onUserResizeFinished();
}
lastResize = resize;
clientManager.minimizeClient(kwinClient);
});
});
let cursorChangedAfterResizeStart = false;
manager.connect(kwinClient.moveResizeCursorChanged, () => {
cursorChangedAfterResizeStart = true;
});
manager.connect(kwinClient.clientStartUserMovedResized, () => {
cursorChangedAfterResizeStart = false;
manager.connect(kwinClient.maximizedAboutToChange, (maximizedMode: MaximizedMode) => {
world.do(() => {
window.onMaximizedChanged(maximizedMode);
});
});
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: KwinClient, oldGeometry: QRect) => {
let resizing = false;
let resizingBorder = false;
manager.connect(kwinClient.interactiveMoveResizeStarted, () => {
if (kwinClient.move) {
if (world.untileOnDrag) {
world.do((clientManager, desktopManager) => {
clientManager.floatKwinClient(kwinClient);
});
}
return;
}
if (kwinClient.resize) {
resizing = true;
resizingBorder = Workspace.cursorPos.x > kwinClient.clientGeometry.right ||
Workspace.cursorPos.x < kwinClient.clientGeometry.left;
window.column.grid.onUserResizeStarted();
}
});
manager.connect(kwinClient.interactiveMoveResizeFinished, () => {
if (resizing) {
resizing = false;
window.column.grid.onUserResizeFinished();
}
});
manager.connect(kwinClient.frameGeometryChanged, (oldGeometry: QmlRect) => {
// on Wayland, this fires after `tileChanged`
if (kwinClient.tile !== null) {
world.do((clientManager, desktopManager) => {
@@ -102,10 +116,11 @@ namespace ClientState {
}
if (kwinClient.resize) {
world.do(() => window.onUserResize(oldGeometry, !cursorChangedAfterResizeStart));
world.do(() => window.onUserResize(oldGeometry, resizingBorder));
} else if (
!window.column.grid.isUserResizing() &&
!client.isManipulatingGeometry(newGeometry) &&
!Clients.isMaximizedGeometry(kwinClient) &&
client.getMaximizedMode() === MaximizedMode.Unmaximized &&
!Clients.isFullScreenGeometry(kwinClient) // not using `kwinClient.fullScreen` because it may not be set yet at this point
) {
world.do(() => window.onFrameGeometryChanged());
@@ -128,21 +143,20 @@ namespace ClientState {
return manager;
}
private static moveWindowToCorrectGrid(desktopManager: DesktopManager, window: Window) {
const kwinClient = window.client.kwinClient;
const oldGrid = window.column.grid;
const newGrid = desktopManager.getDesktopForClient(kwinClient).grid;
if (oldGrid === newGrid) {
// window already on the correct grid
private static moveWindowToGrid(window: Window, grid: Grid) {
if (grid === window.column.grid) {
// window already on the given grid
return;
}
const newColumn = new Column(newGrid, newGrid.getLastFocusedColumn() ?? newGrid.getLastColumn());
const newColumn = new Column(grid, grid.getLastFocusedColumn() ?? grid.getLastColumn());
window.moveToColumn(newColumn);
}
private static prepareClientForTiling(client: ClientWrapper, config: LayoutConfig) {
if (config.skipSwitcher) {
client.kwinClient.skipSwitcher = true;
}
if (config.tiledKeepBelow) {
client.kwinClient.keepBelow = true;
}
@@ -153,10 +167,16 @@ namespace ClientState {
client.setMaximize(false, false);
}
private static restoreClientAfterTiling(client: ClientWrapper, config: LayoutConfig, screenSize: QRect) {
private static restoreClientAfterTiling(client: ClientWrapper, config: LayoutConfig, defaultState: Tiled.WindowState, screenSize: QmlRect) {
if (config.skipSwitcher) {
client.kwinClient.skipSwitcher = defaultState.skipSwitcher;
}
if (config.tiledKeepBelow) {
client.kwinClient.keepBelow = false;
}
if (config.offScreenOpacity < 1.0) {
client.kwinClient.opacity = 1.0;
}
client.setShade(false);
client.setFullScreen(false);
if (client.kwinClient.tile === null) {
@@ -165,4 +185,10 @@ namespace ClientState {
client.ensureVisible(screenSize);
}
}
namespace Tiled {
export type WindowState = {
skipSwitcher: boolean,
}
}
}

View File

@@ -0,0 +1,31 @@
namespace ClientState {
export class TiledMinimized implements State {
private readonly signalManager: SignalManager;
constructor(world: World, client: ClientWrapper) {
this.signalManager = TiledMinimized.initSignalManager(world, client);
}
public destroy(passFocus: boolean) {
this.signalManager.destroy();
}
private static initSignalManager(world: World, client: ClientWrapper) {
const manager = new SignalManager();
manager.connect(client.kwinClient.minimizedChanged, () => {
console.assert(!client.kwinClient.minimized);
world.do((clientManager, desktopManager) => {
const desktop = desktopManager.getDesktopForClient(client.kwinClient);
if (desktop !== undefined) {
clientManager.tileClient(client, desktop.grid);
} else {
clientManager.floatClient(client);
}
});
});
return manager;
}
}
}

View File

@@ -1,6 +0,0 @@
function init() {
const config = loadConfig();
const world = new World(config);
registerKeyBindings(world, config);
return world;
}

3
src/main/main.ts Normal file
View File

@@ -0,0 +1,3 @@
function init() {
return new World(loadConfig());
}

4
src/main/tsconfig.json Normal file
View File

@@ -0,0 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["../lib/**/*", "./**/*"]
}

View File

@@ -1,15 +0,0 @@
class ClientMatcher {
private readonly rules: Map<string, RegExp>;
constructor(rules: Map<string, RegExp>) {
this.rules = rules;
}
public matches(kwinClient: KwinClient) {
const rule = this.rules.get(kwinClient.resourceClass);
if (rule === undefined) {
return false;
}
return rule.test(kwinClient.caption);
}
}

View File

@@ -1,5 +0,0 @@
type WindowRule = {
class: string,
caption: string,
tile: boolean,
};

View File

@@ -1,89 +0,0 @@
class WindowRuleEnforcer {
private readonly preferFloating: ClientMatcher;
private readonly preferTiling: ClientMatcher;
private readonly followCaption: Set<string>;
constructor(windowRules: WindowRule[]) {
const [mapFloat, mapTile] = WindowRuleEnforcer.createWindowRuleMaps(windowRules);
this.preferFloating = new ClientMatcher(mapFloat);
this.preferTiling = new ClientMatcher(mapTile);
this.followCaption = new Set([...mapFloat.keys(), ...mapTile.keys()]);
}
public shouldTile(kwinClient: KwinClient) {
return Clients.canTileNow(kwinClient) && (
this.preferTiling.matches(kwinClient) || (
kwinClient.normalWindow &&
!kwinClient.transient &&
kwinClient.managed &&
!this.preferFloating.matches(kwinClient)
)
);
}
public initClientSignalManager(world: World, kwinClient: KwinClient) {
if (!this.followCaption.has(kwinClient.resourceClass)) {
return null;
}
const enforcer = this;
const manager = new SignalManager();
manager.connect(kwinClient.captionChanged, () => {
const shouldTile = enforcer.shouldTile(kwinClient);
world.do((clientManager, desktopManager) => {
if (shouldTile) {
clientManager.tileClient(kwinClient);
} else {
clientManager.untileClient(kwinClient);
}
});
});
return manager;
}
private static createWindowRuleMaps(windowRules: WindowRule[]) {
const mapFloat = new Map<string, string[]>();
const mapTile = new Map<string, string[]>();
for (const windowRule of windowRules) {
const map = windowRule.tile ? mapTile : mapFloat;
let captions = map.get(windowRule.class);
if (captions === undefined) {
captions = [];
map.set(windowRule.class, captions);
}
if (windowRule.caption !== undefined) {
captions.push(windowRule.caption);
}
}
return [
WindowRuleEnforcer.createWindowRuleRegexMap(mapFloat),
WindowRuleEnforcer.createWindowRuleRegexMap(mapTile),
];
}
private static createWindowRuleRegexMap(windowRuleMap: Map<string, string[]>) {
const regexMap = new Map<string, RegExp>;
for (const [k, v] of windowRuleMap) {
regexMap.set(k, WindowRuleEnforcer.joinRegexes(v));
}
return regexMap;
}
private static joinRegexes(regexes: string[]) {
if (regexes.length == 0) {
return new RegExp("");
}
if (regexes.length == 1) {
return new RegExp("^" + regexes[0] + "$");
}
const joinedRegexes = regexes.map(WindowRuleEnforcer.wrapParens).join("|");
return new RegExp("^" + joinedRegexes + "$");
}
private static wrapParens(str: string) {
return "(" + str + ")";
}
}

View File

@@ -0,0 +1,36 @@
{
const testCases = [
{tiledByDefault: true, resourceClass: "unknown", caption: "anything", shouldTile: true},
{tiledByDefault: false, resourceClass: "unknown", caption: "anything", shouldTile: false},
{tiledByDefault: true, resourceClass: "ksmserver-logout-greeter", caption: "anything", shouldTile: false},
{tiledByDefault: true, resourceClass: "org.kde.plasmashell", caption: "something", shouldTile: false},
{tiledByDefault: true, resourceClass: "plasmashell", caption: "something", shouldTile: false},
{tiledByDefault: false, resourceClass: "org.kde.kfind", caption: "something", shouldTile: true},
{tiledByDefault: false, resourceClass: "kfind", caption: "something", shouldTile: true},
{tiledByDefault: true, resourceClass: "zoom", caption: "something", shouldTile: true},
{tiledByDefault: true, resourceClass: "zoom", caption: "zoom", shouldTile: false},
];
const enforcer = new WindowRuleEnforcer(JSON.parse(defaultWindowRules));
for (const testCase of testCases) {
const kwinClient: any = createKwinClient(testCase.tiledByDefault, testCase.resourceClass, testCase.caption);
assert(enforcer.shouldTile(kwinClient) === testCase.shouldTile, "failed case: " + JSON.stringify(testCase));
}
function createKwinClient(normalWindow: boolean, resoureClass: string, caption: string) {
return {
normalWindow: normalWindow,
transient: false,
managed: true,
pid: 100,
moveable: true,
resizeable: true,
popupWindow: false,
minimized: false,
desktops: [1],
activities: [1],
resourceClass: resoureClass,
caption: caption,
}
}
}

17
src/tests/tests.ts Normal file
View File

@@ -0,0 +1,17 @@
declare const process: {
exit(code?: number): void,
};
function assert(assertion: boolean, message?: string) {
if (assertion) {
return;
}
if (message != undefined) {
console.assert(assertion, message);
} else {
console.assert(assertion);
}
console.trace();
process.exit(1);
}

4
src/tests/tsconfig.json Normal file
View File

@@ -0,0 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["../lib/**/*", "./**/*"]
}

12
src/tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "es2020",
"lib": ["es2020"],
"module": "none",
"allowJs": false,
"esModuleInterop": false,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}

View File

@@ -1,72 +0,0 @@
function initWorkspaceSignalHandlers(world: World) {
const manager = new SignalManager();
manager.connect(workspace.clientAdded, (kwinClient: KwinClient) => {
if (Clients.canTileEver(kwinClient)) {
// never open new tileable clients on all desktops or activities
if (kwinClient.desktop <= 0) {
kwinClient.desktop = workspace.currentDesktop;
}
if (kwinClient.activities.length !== 1) {
kwinClient.activities = [workspace.currentActivity];
}
}
world.do((clientManager, desktopManager) => {
clientManager.addClient(kwinClient)
});
});
manager.connect(workspace.clientRemoved, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => {
clientManager.removeClient(kwinClient, true);
});
});
manager.connect(workspace.clientMinimized, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => {
clientManager.minimizeClient(kwinClient);
});
});
manager.connect(workspace.clientUnminimized, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => {
clientManager.unminimizeClient(kwinClient);
});
});
manager.connect(workspace.clientMaximizeSet, (kwinClient: KwinClient, horizontally: boolean, vertically: boolean) => {
if ((horizontally || vertically) && kwinClient.tile !== null) {
kwinClient.tile = null;
}
world.doIfTiled(kwinClient, false, (world, desktopManager, window, column, grid) => {
window.onMaximizedChanged(horizontally, vertically);
});
});
manager.connect(workspace.clientActivated, (kwinClient: KwinClient) => {
if (kwinClient === null) {
return;
}
world.do((clientManager, desktopManager) => {
clientManager.onClientFocused(kwinClient);
});
});
manager.connect(workspace.currentDesktopChanged, () => {
world.do(() => {}); // re-arrange desktop
});
manager.connect(workspace.currentActivityChanged, () => {
world.do(() => {}); // re-arrange desktop
});
manager.connect(workspace.numberDesktopsChanged, (oldNumberOfVirtualDesktops: number) => {
world.updateDesktops();
});
manager.connect(workspace.virtualScreenSizeChanged, () => {
world.onScreenResized();
});
return manager;
}

View File

@@ -1,35 +0,0 @@
namespace Clients {
export function canTileEver(kwinClient: KwinClient) {
return kwinClient.resizeable;
}
export function canTileNow(kwinClient: KwinClient) {
return canTileEver(kwinClient) && !kwinClient.minimized && kwinClient.desktop > 0 && kwinClient.activities.length === 1;
}
export function makeTileable(kwinClient: KwinClient) {
if (kwinClient.minimized) {
kwinClient.minimized = false;
}
if (kwinClient.desktop <= 0) {
kwinClient.desktop = workspace.currentDesktop;
}
if (kwinClient.activities.length !== 1) {
kwinClient.activities = [workspace.currentActivity];
}
}
export function isMaximizedGeometry(kwinClient: KwinClient) {
const maximizeArea = workspace.clientArea(ClientAreaOption.MaximizeArea, kwinClient.screen, kwinClient.desktop);
return kwinClient.frameGeometry === maximizeArea;
}
export function isFullScreenGeometry(kwinClient: KwinClient) {
const fullScreenArea = workspace.clientArea(ClientAreaOption.FullScreenArea, kwinClient.screen, kwinClient.desktop);
return kwinClient.frameGeometry === fullScreenArea;
}
export function isOnVirtualDesktop(kwinClient: KwinClient, desktopNumber: number) {
return kwinClient.desktop === desktopNumber || kwinClient.desktop === -1;
}
}

View File

@@ -1,147 +0,0 @@
class DesktopManager {
private readonly pinManager: PinManager;
private readonly config: Desktop.Config;
public readonly layoutConfig: LayoutConfig;
private readonly desktopsPerActivity: Map<string, Desktop[]>;
private nVirtualDesktops: number;
constructor(pinManager: PinManager, config: Desktop.Config, layoutConfig: LayoutConfig, currentActivity: string) {
this.pinManager = pinManager;
this.config = config;
this.layoutConfig = layoutConfig;
this.desktopsPerActivity = new Map();
this.nVirtualDesktops = 0;
this.update()
this.addActivity(currentActivity);
}
public update() {
this.setNVirtualDesktops(workspace.desktops);
}
public getDesktop(activity: string, desktopNumber: number) {
const desktopIndex = desktopNumber - 1;
if (desktopIndex >= this.nVirtualDesktops || desktopIndex < 0) {
throw new Error("invalid desktop number: " + String(desktopNumber));
}
if (!this.desktopsPerActivity.has(activity)) {
this.addActivity(activity);
}
return this.desktopsPerActivity.get(activity)![desktopIndex];
}
public getCurrentDesktop() {
return this.getDesktop(workspace.currentActivity, workspace.currentDesktop);
}
public getDesktopInCurrentActivity(desktopNumber: number) {
return this.getDesktop(workspace.currentActivity, desktopNumber);
}
public getDesktopForClient(kwinClient: KwinClient) {
console.assert(kwinClient.activities.length === 1 && kwinClient.desktop > 0);
return this.getDesktop(kwinClient.activities[0], kwinClient.desktop);
}
private setNVirtualDesktops(nVirtualDesktops: number) {
if (nVirtualDesktops > this.nVirtualDesktops) {
this.addDesktopsToActivities(nVirtualDesktops - this.nVirtualDesktops);
} else if (nVirtualDesktops < this.nVirtualDesktops) {
this.removeDesktopsFromActivities(this.nVirtualDesktops - nVirtualDesktops);
}
this.nVirtualDesktops = nVirtualDesktops;
}
private addDesktopsToActivities(n: number) {
for (const desktops of this.desktopsPerActivity.values()) {
this.addDesktops(desktops, n);
}
}
private addDesktops(desktops: Desktop[], n: number) {
const nStart = desktops.length;
for (let i = 0; i < n; i++) {
const desktopNumber = nStart + i + 1;
desktops.push(new Desktop(desktopNumber, this.pinManager, this.config, this.layoutConfig));
}
}
private removeDesktopsFromActivities(n: number) {
const lastRemainingDesktopIndex = this.nVirtualDesktops - n - 1;
for (const desktops of this.desktopsPerActivity.values()) {
const targetDesktop = desktops[lastRemainingDesktopIndex];
for (let i = 0; i < n; i++) {
const removedDesktop = desktops.pop()!;
removedDesktop.grid.evacuate(targetDesktop.grid);
}
}
}
private addActivity(activity: string) {
const desktops: Desktop[] = [];
this.addDesktops(desktops, this.nVirtualDesktops);
this.desktopsPerActivity.set(activity, desktops);
}
private removeActivity(activity: string) {
const removedDesktops = this.desktopsPerActivity.get(activity)!;
this.desktopsPerActivity.delete(activity);
const targetActivityDesktops = this.desktopsPerActivity.values().next().value;
for (let i = 0; i < removedDesktops.length; i++) {
removedDesktops[i].grid.evacuate(targetActivityDesktops[i]);
}
}
public destroy() {
for (const desktop of this.desktops()) {
desktop.destroy();
}
}
public *desktops() {
for (const desktops of this.desktopsPerActivity.values()) {
for (const desktop of desktops) {
yield desktop;
}
}
}
public *getDesktopsForClient(kwinClient: KwinClient) {
const activities = kwinClient.activities.length > 0 ? kwinClient.activities : this.desktopsPerActivity.keys();
for (const activity of activities) {
if (!this.desktopsPerActivity.has(activity)) {
this.addActivity(activity);
}
const activityDesktops = this.desktopsPerActivity.get(activity)!;
if (kwinClient.desktop === -1) {
for (const desktop of activityDesktops) {
yield desktop;
}
} else {
const desktopIndex = kwinClient.desktop - 1;
yield activityDesktops[desktopIndex];
}
}
}
// empty array means all
public *getDesktops(desktopNumbers: number[], inputActivities: string[]) {
const activities = inputActivities.length > 0 ? inputActivities : this.desktopsPerActivity.keys();
for (const activity of activities) {
if (!this.desktopsPerActivity.has(activity)) {
this.addActivity(activity);
}
const activityDesktops = this.desktopsPerActivity.get(activity)!;
if (desktopNumbers.length === 0) {
for (const desktop of activityDesktops) {
yield desktop;
}
} else {
for (const desktopNumber of desktopNumbers) {
const desktopIndex = desktopNumber - 1;
yield activityDesktops[desktopIndex];
}
}
}
}
}

View File

@@ -1,5 +0,0 @@
namespace ClientState {
export class TiledMinimized implements State {
public destroy(passFocus: boolean) {}
}
}

View File

@@ -1,108 +0,0 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"lib": [
"es2020",
], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "none", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
"allowJs": false, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./ts_built.js", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": false, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": [
"src/**/*"
]
}