Skip to content

Commit 834f523

Browse files
committed
massive refactor and update for GNOME 49
1 parent f275146 commit 834f523

27 files changed

+5058
-1505
lines changed

.gitignore

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
smart-auto-move@khimaros.com.shell-extension.zip
1+
/build
22
smart-auto-move.dconf
33
*.ui~
4-
/.aider*
4+
AGENTS.md
5+
CLAUDE.md
6+
GEMINI.md

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
11
# CHANGELOG
22

3+
## version 36
4+
5+
### New Features
6+
7+
- huge refactor to use event driven rather than timeout driven tracking
8+
- fixed issues with windows which change their titles after startup
9+
- better handling of multi-monitor
10+
- track last seen and first seen times for each window
11+
- automatic cleanup of old/unseen saved windows
12+
- "Choose Window" button implemented
13+
14+
### Breaking Changes
15+
16+
**⚠️ IMPORTANT: Automatic Migration from v35**
17+
18+
When upgrading from v35 or earlier, the extension will automatically:
19+
- **Clear all saved window positions** - The internal data structure has changed significantly and cannot be automatically converted
20+
- **Preserve your overrides** - Window and application overrides (IGNORE/RESTORE rules and thresholds) will be migrated automatically
21+
- **Preserve all settings** - Global preferences like sync mode, match threshold, and ignore options are maintained
22+
23+
Your saved windows will be re-learned as you use applications after the upgrade. If you had many windows configured, you may want to open and position them again to rebuild the saved state.
24+
25+
**Data Structure Changes:**
26+
- Internal saved windows format completely redesigned for the new event-driven architecture
27+
- New `config-version` setting added to track data format versions for future migrations
28+
- Configuration timing parameters changed from `startup-delay`/`sync-frequency`/`save-frequency` to new event-driven parameters
29+
330
## version 35
431

532
- persist the "Ignore Monitor" setting

CONTRIBUTING.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,28 @@ see commit 9edd9e3210a1541d5c2915943c7a2b238ce7a856 for an end-to-end example.
3131

3232
## manual tests
3333

34+
- disable the extension
35+
- reset the dconf settings
36+
- enable the extension
37+
- open calculator, move, resize, close, reopen (should restore)
38+
- open extension settings
39+
- switch default sync to IGNORE
40+
- open calculator, move, close, reopen (should NOT restore)
41+
- add an app override for calculator
42+
- set calculator override sync mode to RESTORE
43+
- click spin button down to 0.10 threshold
44+
- open calculator, move, close, reopen (should restore)
45+
- open nautilus, move, close, reopen (should NOT restore)
46+
- add an app override for firefox
47+
- open three firefox windows with three different websites
48+
- tile one per workspace, menu -> quit, reopen (all should restore)
49+
- switch tabs on one of the windows, menu -> quit, reopen (all should restore)
50+
- add app override for terminal
51+
- open three terminals and move them to slightly different locations
52+
- close and reopen terminals in a random order and make sure they fill open slots
53+
54+
## archive
55+
3456
### calculator
3557

3658
- open calculator

Makefile

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,45 @@
1-
pack: smart-auto-move@khimaros.com.shell-extension.zip
2-
.PHONY: pack
1+
NAME = smart-auto-move
2+
UUID = $(NAME)@khimaros.com
3+
4+
build:
5+
mkdir -p build/
6+
gnome-extensions pack -f \
7+
--extra-source=metadata.json \
8+
--extra-source=extension.js \
9+
--extra-source=prefs.js \
10+
--extra-source=common.js \
11+
--extra-source=migrations.js \
12+
--extra-source=lib/ \
13+
--extra-source=ui/ \
14+
-o build/
15+
.PHONY: build
316

417
clean:
5-
rm -f ./smart-auto-move@khimaros.com.shell-extension.zip ./smart-auto-move@khimaros.com/schemas/gschemas.compiled
18+
rm -rf ./build/ ./src/schemas/gschemas.compiled
619
.PHONY: clean
720

8-
smart-auto-move@khimaros.com.shell-extension.zip: schemas ui ./smart-auto-move@khimaros.com/*
9-
gnome-extensions pack --force --extra-source=./lib/ --extra-source=./ui/ ./smart-auto-move@khimaros.com/
21+
install: uninstall build
22+
gnome-extensions install -f build/$(UUID).shell-extension.zip
23+
.PHONY: install
1024

11-
smart-auto-move@khimaros.com/schemas/gschemas.compiled: smart-auto-move@khimaros.com/schemas/*.gschema.xml
12-
glib-compile-schemas ./smart-auto-move@khimaros.com/schemas/
25+
uninstall:
26+
rm -rf $(HOME)/.local/share/gnome-shell/extensions/$(UUID)
27+
.PHONY: uninstall
1328

14-
schemas: smart-auto-move@khimaros.com/schemas/gschemas.compiled
29+
schemas: schemas/gschemas.compiled
1530
.PHONY: schemas
1631

17-
ui: smart-auto-move@khimaros.com/ui/prefs-gtk4.ui smart-auto-move@khimaros.com/ui/templates-gtk4.ui
18-
.PHONY: ui
19-
20-
test:
21-
gjs -m -I smart-auto-move@khimaros.com/lib/ ./smart-auto-move@khimaros.com/test/common.test.js
22-
.PHONY: test
32+
schemas/gschemas.compiled: schemas/*.gschema.xml
33+
glib-compile-schemas ./schemas/
2334

2435
log:
2536
journalctl -f /usr/bin/gnome-shell /usr/bin/gjs
2637
.PHONY: log
2738

28-
install: smart-auto-move@khimaros.com.shell-extension.zip
29-
#rsync -av ./smart-auto-move@khimaros.com/ $(HOME)/.local/share/gnome-shell/extensions/smart-auto-move@khimaros.com/
30-
gnome-extensions install --force $<
31-
.PHONY: install
32-
33-
uninstall:
34-
rm -rf $(HOME)/.local/share/gnome-shell/extensions/smart-auto-move@khimaros.com/
35-
.PHONY: uninstall
36-
3739
start: install
3840
MUTTER_DEBUG_DUMMY_MODE_SPECS=1600x900 dbus-run-session -- gnome-shell --nested --wayland
3941
.PHONY: start
4042

4143
start-prefs: install
42-
gnome-extensions prefs smart-auto-move@khimaros.com
44+
gnome-extensions prefs $(UUID)
4345
.PHONY: start-prefs

README.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ the first step is to choose your **Default Synchronization Mode**: `IGNORE` or `
2626

2727
next is to choose your global **Match Threshold**, the default works well for most use cases. a number closer to `0.0` will match windows with less similar attributes, whereas `1.0` requires an exact match.
2828

29-
advanced users can also tune extension resource usage. adjust **Sync Frequency** (memory and CPU) and **Save Frequency** (disk I/O).
29+
advanced users can tune window matching behavior with parameters like **New Window Max Wait Time**, **Title Stability**, and other timing settings in the preferences.
3030

31-
after you've dialed in your overrides, the learning apparatus can be paused. enable **Freeze Saves** to prevent changes to Saved Windows. N.B. this lose track of windows if their titles change.
31+
after you've dialed in your overrides, the learning apparatus can be paused. enable **Freeze Saves** to prevent changes to Saved Windows. N.B. this will lose track of windows if their titles change.
3232

3333
### overrides
3434

@@ -44,6 +44,18 @@ you can change the IGNORE/RESTORE behavior here for apps and windows.
4444

4545
for applications, a custom **Match Threshold** can be set.
4646

47+
## upgrading
48+
49+
### upgrading from v35 or earlier
50+
51+
when upgrading from version 35 or earlier to version 36+, the extension will automatically migrate your settings:
52+
53+
- **saved window positions are cleared** - the internal data format has changed significantly and cannot be converted automatically. your windows will be re-learned as you use them.
54+
- **overrides are preserved** - all your IGNORE/RESTORE rules and custom thresholds will be migrated automatically.
55+
- **settings are preserved** - global preferences remain unchanged.
56+
57+
no manual intervention is required. the migration happens automatically on first load.
58+
4759
## limitations
4860

4961
LIMITATION: terminals which include the current directory in the title may not reach the match threshold if they do not preserve the working directory across restarts. WORKAROUND: create a per-app override (see above) and set the threshold to a lower value, eg. `0.2`
@@ -84,10 +96,10 @@ set the minimum window/title match threshold to 50%:
8496
$ dconf write /org/gnome/shell/extensions/smart-auto-move/match-threshold 0.5
8597
```
8698

87-
set the window synchronization (update/restore) frequency to 50ms:
99+
set the maximum wait time for new window title stabilization to 15 seconds:
88100

89101
```
90-
$ dconf write /org/gnome/shell/extensions/smart-auto-move/sync-frequency 50
102+
$ dconf write /org/gnome/shell/extensions/smart-auto-move/new-window-max-wait-ms 15000
91103
```
92104

93105
default to ignoring windows unless explicitly defined. restore all windows of the gnome-calculator app, all firefox windows except for the profile chooser, and Nautilus only if the window title is "Downloads":

common.js

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
"use strict";
2+
3+
export const SECONDS_PER_HOUR = 3600;
4+
5+
// D-Bus constants
6+
export const DBUS_NAME = "org.gnome.shell.extensions.SmartAutoMove";
7+
export const DBUS_PATH = "/org/gnome/shell/extensions/SmartAutoMove";
8+
export const DBUS_INTERFACE = `
9+
<node>
10+
<interface name="org.gnome.shell.extensions.SmartAutoMove">
11+
<method name="ListWindows">
12+
<arg type="s" direction="out" name="windows_json"/>
13+
</method>
14+
<method name="RefreshFromCurrentActors">
15+
</method>
16+
</interface>
17+
</node>
18+
`;
19+
20+
// Settings constants for prefs.js
21+
export const SETTINGS_KEY_DEBUG_LOGGING = "debug-logging";
22+
export const SETTINGS_KEY_CONFIG_VERSION = "config-version";
23+
export const SETTINGS_KEY_SAVED_WINDOWS = "saved-windows";
24+
export const SETTINGS_KEY_OVERRIDES = "overrides";
25+
export const SETTINGS_KEY_MATCH_THRESHOLD = "match-threshold";
26+
27+
// Sync mode constants
28+
export const SYNC_MODE_IGNORE = 0;
29+
export const SYNC_MODE_RESTORE = 1;
30+
export const SYNC_MODE_DEFAULT = "DEFAULT";
31+
32+
// Settings configuration for prefs.js binding
33+
export const SETTINGS_CONFIG = [
34+
{
35+
key: "debug-logging",
36+
property: "active",
37+
widgetId: "debug-logging-switch",
38+
},
39+
{
40+
key: "new-window-max-wait-ms",
41+
property: "value",
42+
widgetId: "new-window-max-wait-spin",
43+
},
44+
{
45+
key: "new-window-title-stability-ms",
46+
property: "value",
47+
widgetId: "new-window-title-stability-spin",
48+
},
49+
{
50+
key: "generic-title-max-length",
51+
property: "value",
52+
widgetId: "generic-title-max-length-spin",
53+
},
54+
{
55+
key: "ambiguity-threshold",
56+
property: "value",
57+
widgetId: "ambiguity-threshold-generic-spin",
58+
},
59+
{
60+
key: "min-score-spread",
61+
property: "value",
62+
widgetId: "min-score-spread-spin",
63+
},
64+
{
65+
key: "generic-title-extended-wait",
66+
property: "value",
67+
widgetId: "generic-title-extended-wait-spin",
68+
},
69+
{
70+
key: "max-unseen-age",
71+
property: "value",
72+
widgetId: "max-unseen-age-spin",
73+
},
74+
{
75+
key: "match-threshold",
76+
property: "value",
77+
widgetId: "match-threshold-spin",
78+
},
79+
{
80+
key: "sync-mode",
81+
property: "active-id",
82+
widgetId: "sync-mode-combo",
83+
},
84+
{
85+
key: "freeze-saves",
86+
property: "active",
87+
widgetId: "freeze-saves-switch",
88+
},
89+
{
90+
key: "activate-workspace",
91+
property: "active",
92+
widgetId: "activate-workspace-switch",
93+
},
94+
{
95+
key: "ignore-position",
96+
property: "active",
97+
widgetId: "ignore-position-switch",
98+
},
99+
{
100+
key: "ignore-workspace",
101+
property: "active",
102+
widgetId: "ignore-workspace-switch",
103+
},
104+
{
105+
key: "ignore-monitor",
106+
property: "active",
107+
widgetId: "ignore-monitor-switch",
108+
},
109+
];
110+
111+
export function getActionId(action) {
112+
if (action === SYNC_MODE_IGNORE || action === "IGNORE") return "IGNORE";
113+
if (action === SYNC_MODE_RESTORE || action === "RESTORE") return "RESTORE";
114+
return "DEFAULT";
115+
}
116+
117+
export function parseOverrides(overridesStr) {
118+
try {
119+
return JSON.parse(overridesStr || "{}");
120+
} catch (e) {
121+
return {};
122+
}
123+
}
124+
125+
export function deleteNonOccupiedWindows(savedWindows) {
126+
Object.keys(savedWindows).forEach((wsh) => {
127+
savedWindows[wsh] = savedWindows[wsh].filter((sw) => sw.occupied);
128+
if (savedWindows[wsh].length === 0) {
129+
delete savedWindows[wsh];
130+
}
131+
});
132+
}
133+
134+
export function ignoreSavedWindow(
135+
savedWindows,
136+
overrides,
137+
wsh,
138+
swi,
139+
threshold,
140+
ignoreAny,
141+
) {
142+
if (!savedWindows[wsh] || !savedWindows[wsh][swi]) return;
143+
144+
const sw = savedWindows[wsh][swi];
145+
146+
if (!overrides[wsh]) {
147+
overrides[wsh] = [];
148+
}
149+
150+
if (ignoreAny) {
151+
overrides[wsh].push({
152+
action: "IGNORE",
153+
threshold: threshold,
154+
});
155+
} else {
156+
overrides[wsh].unshift({
157+
title: sw.title,
158+
action: "IGNORE",
159+
});
160+
}
161+
}

0 commit comments

Comments
 (0)