Xidorn's Blog

Restore Window Geometry on GNOME

Like many people, I have two monitors for my computer: one in the middle as the primary display, and the other on the right side rotated vertically. I predominantly use the middle one, and put some state monitoring on the side monitor. My choice here is the System Monitor bundled with GNOME. There may be better options, but it is good enough for me. However, there is one thing that annoys me that, the application doesn’t save its window position and open at the place I put before. Every time it’s opened (at startup), it’s in the middle of my main display, and I have to move it all the way to the side one, manually, every day.

Initial Solution

It annoyed me enough that I asked Windsurf to write me a script to programmatically position and size a window with given values, and it did an amazing job in coming up with the script set_window_geometry.

Source code of the script
#!/bin/bash

if [ $# -ne 5 ]; then
    echo "Usage: $0 \"window_name\" x y width height"
    echo "Example: $0 \"Firefox\" 100 100 800 600"
    exit 1
fi

window_name="$1"
x=$2
y=$3
width=$4
height=$5

# Find window ID by name
window_id=$(wmctrl -l | grep "$window_name" | head -n1 | cut -d' ' -f1)

if [ -z "$window_id" ]; then
    echo "Error: No window found matching '$window_name'"
    exit 1
fi

# Set the window geometry
# Note: wmctrl uses gravity,x,y,width,height format
# 0 is the gravity value which means the position is relative to the top-left corner
wmctrl -i -r "$window_id" -e "0,$x,$y,$width,$height"

echo "Window geometry set successfully"

Thanks to it, I learned about the tool wmctrl, and this script served my requirements pretty well… until last month when I upgraded my system to the latest Ubuntu 25.10, and this script then becomes broken.

The reason is Ubuntu 25.10 drops X11 in favor of Wayland, but wmctrl doesn’t work with Wayland, so I started looking for alternatives.

I found wlrctl which is said to be a tool that supports various window operations on Wayland, but it doesn’t work on GNOME either. It relies on Foreign Toplevel Management interface which GNOME’s compositor Mutter doesn’t implement. I also found that GNOME’s JavaScript API can do this, and there is an API, org.gnome.Shell.Eval, exposed through D-Bus to execute JavaScript code on GNOME Shell. However, that API has also be restricted since long time ago, so this is another dead end.

Building an Extension

Not sure where I read about it, but it seems that the general idea behind Wayland is that applications shouldn’t be able to interfere with each other, and managing windows is a responsibility of the window manager. With this in mind, as well as that GNOME has internal API for this, just not accessible from applications, the next direction would be GNOME Shell extensions.

I found an extension Window State Manager stating it “automatically save/restore window state”, which sounds like exactly what I’m looking for, except, it isn’t. It’s for restoring window state across desktop layout changes, such as when a monitor is connected / disconnected. It doesn’t persist the state across logout / restart. It solves a completely different problem than what I’m trying to do. But it is inspiring nonetheless for what I could do with an extension.

Failed to find anything matching my need, I decided to build one myself.

Testing Environment

There is an old Chinese saying: a craftsman who wishes to do his work well must sharpen his tools first. While LLMs nowadays are reasonably good at writing code from descriptions, I do still need to test and iterate on the result. This is usually easy when developing a web application, where you just refresh the page, or a normal desktop application, where you just run it again, but not so for a GNOME Shell extension.

GNOME helpfully has an official guide about debugging extension, in which there is a section for reloading extensions. For Wayland, the recommended way is to run a nested GNOME Shell, and for X11, it is to restart the GNOME Shell via its Alt+F2 dialog. The other way around doesn’t work, that is, Wayland doesn’t support restart GNOME Shell without logout, and X11 doesn’t support running nested GNOME Shell.

To run nested GNOME Shell, on Ubuntu, a separate package mutter-dev-bin must be installed first via apt, then one could use dbus-run-session gnome-shell --devkit --wayland to start a windowed GNOME Shell. This initially took quite a while to show up to the extent that I thought it doesn’t work, but it eventually did.

However, this approach is not great. I only realized it when I opened my computer the next day, when I found my desktop layout was messed up. The document does mention that:

A nested instance of GNOME Shell is not fully isolated, and will not protect your system from data loss or other consequences.

But, in my opinion, it doesn’t emphasize enough that it is not at all isolated! The nested instance uses the same configuration as the host instance, and could override the host configuration in some cases. My desktop layout got lost because the nested instance has only one display and I touched the display settings for it inside. This is really not something you want when debugging an in-development extension.

So I had to go one step further on the isolation, and created a virtual machine for testing it. Since I need to logout and login a lot of times, I tweaked it slightly to make this process more streamlined:

Development

TypeScript

While GNOME Shell extensions are normally developed using JavaScript, I have recently become increasingly uncomfortable to use it in new projects because of its lack of type checking. Fortunately, it doesn’t seem that I’m alone. The official documentation actually has a page for using TypeScript for development. It includes quite a few steps and has many files to create. That process could have been simpler with a starter kit compatible with npm init or pnpm create, but this isn’t a big hurdle, and it’s also useful to have some understanding of all the files in the project.

Preferences

For this extension, naturally I don’t want to restore geometry of every single window. Some applications, such as Firefox, already have their own mechanism to restore previous state, while many others I don’t really care where they open, and the default position and size are okay. That means I need a way to specify which windows to restore.

My initial idea was to use the preferences UI to specify that. It would list all the open windows and allow user to toggle them. This was what I prompted LLM to implement, and it did a reasonable job in writing that, except that this approach just doesn’t work. The preferences showed some nonsense like ReferenceError: global is not defined. I was very confused, then I found that the document briefly mentioned:

Extension preferences run in a separate process, without access to code in GNOME Shell, …

So I guess preferences can’t really be used for my purpose, and I need an alternative that runs in the same process as GNOME Shell. 1

I was thinking about adding an icon in the indicator, but my own tray area has been very crowded, and it doesn’t feel that selecting which windows to restore their geometry deserves this level of attention. It would mostly be something that you set and forget. I ended up putting it in the quick settings panel, which is probably a good compromise. It wouldn’t be adding too much visual noise to an already-crowded area, yet it is convenient to use when needed. Most importantly, it runs within the same process as GNOME Shell and thus has access to the necessary API.

Timings

Window state management seems to be a complicated problem on Linux desktop.

The initial code connects to window-created signal for identifying new windows opened, which sounds easy, but actually at the point this signal is emitted, the window isn’t stable at all. It doesn’t even have its wm_class assigned, which is what I use in the extension to identify windows. I have to delay the remaining code a bit (with GLib.idle_add) in order to access the wm_class for further processing.

Then, after that delay, the window may still not be settled, and the API to move and resize it (Window.move_resize_frame) doesn’t work. It would have to wait for the first-frame signal of the window to come out before the API can start moving or resizing the window.

Frankly I’m not sure how reliable all the extra waitings are. Should I simply wait for first-frame in window-created? Is it guaranteed for every window to emit it? Also is it possible for a window to be destroyed before the first idle delay matures, making all the remaining processing invalid? I can’t really answer these questions. At least the current approach seems to work reliably for my use case at the moment.

Publish

While the extension was made for personal use, I feel other people may also find it useful, so I put the source code on GitHub and published it on GNOME Extensions.

Unsurprisingly, publishing on the extensions site requires review and approval before others can start installing it, and as it’s likely a program run by volunteers, I expected this process could potentially take days. However, surprisingly, a reviewer called JustPerfection was very fast in submitting reviews, within hours, sometimes minutes after I submitted, and correctly pointed out many problems in my extension, like unneeded files and timeouts potentially not being removed correctly. They also provided useful suggestions like joining Matrix channel to ask for help, and potentially adding donation link to the extension. I really appreciate such a quick review and feedback from the reviewer.

It was also very surprising that just one day after the extension was initially published, there were already a few dozen downloads, and even a user review with 5 star praising it as “the best GNOME extension of all time”. I don’t know how all the people find this extension, but this was very encouraging.

Wrapping Up

You can find the extension “Restore Geometry” on GNOME Extensions and GitHub. Feedbacks are welcome.

It is the first time I’m writing an extension for GNOME Shell, and overall it has been a positive experience. The rest is some comments I have from this experience.

Wayland

I have a lot of rants about Wayland. It broke many of my own things and I spent quite some time to make everything work again like before. The initial motivation for this extension is just one such case that got broken, and others include

It’s very disappointing to see a new technology being pushed through that doesn’t perceivably solves any real issue but breaks a lot of existing use cases. 4

Development Experience

It would be a stretch to say the development experience is good.

The documentation for extension development is reasonable, but the GJS API references are vast, and without much experience it is very hard to know where to even start looking at. Fortunately nowadays LLM can help doing a lot of research work and find proper API to start with.

Spinning up a proper testing environment takes quite some effort (requires virtual machine with UI). The suggested approach in official guide of using nested Wayland session doesn’t seem to be more performant, yet it can potentially be destructive for the host environment.

Also, iterating on changes requires packing the extension, sending it over to VM, installing it inside the VM, then logout and login again. This is cumbersome, though some of the parts could be automated to some extent. If extensions can be unloaded and loaded again in the same session, it could save quite some time. It doesn’t have to be as isolated (and thus limited) as browser extensions, but trusting that extensions can clean up their own effects then supporting unloading the resources from extensions would likely go a long way in making extension authors’ life easier.

There also doesn’t seem to be any testing framework. Some extensions appear to develop their own rudimentary testing mechanism fitting their needs, but it’s significant extra work. For smaller extensions like mine, it’s all just manual testing.

GNOME Community

My limited interaction with the GNOME community was very positive. The reviewer was quick and nice, and there are receptive and engaged users who are willing to try and provide meaningful feedback.

One suggestion in the first user review was to allow not tracking the windows all the time, but remember a specific geometry and use it every time regardless of where the window ends up being previously. I found this functionality very useful so quickly implemented it in the extension.

Another user created an issue on GitHub asking for GNOME 48 support, and helped doing the test to confirm that the extension actually works there without any change.

Footnotes

  1. During research, I found that GSConnect doesn’t use the provided preferences window, but has a quite different UI. It turned out that it uses an interesting approach that it immediately closes the preference window and opens its own preferences window in a separate process. But this wouldn’t work for me either.

  2. For this one I ended up installed Activate Window By Title and create a script to utilize it for launching calculator.

  3. I spent significant amount of time researching for a solution, looked at virtual-keyboard protocol and libei, but none of them works for my use case. I ended up using a libevdev implementation to interact with /dev/uinput directly.

  4. A counter-example I really like was PipeWire. It solves my Bluetooth headphone issues, adds a lot of flexibility to control how audio is routed, and breaks nothing.