I suspect that this title makes no sense to most people, so here’s the background.
Like most normal people, I have four1 computers in my office. I used to have three, but that was shameful, so I was very relieved to get a new laptop for my new job at MongoDB.
A while back, I bought a USB switching device with a remote. This eliminated the need to physically switch my USB hub’s cable from one computer to another.
I have two monitors connected to these computers, and I switch between inputs on the monitors when I switch computers. I used to do this manually by using the buttons on the monitors, but this was annoying. I’ve used KVM switches before but my experience has been that they’re all junk, so I didn’t want to go that route again.
Fortunately, I found an awesome project in Rust called display-switch created by Haim Gelfenbeyn. It runs on Linux, macOS, and Windows as a background service. It listens for USB connect/disconnect events and then uses DDC commands to switch the inputs on the monitor. With this configured on each computer, I can use the USB switch’s remote to switch all the USB devices and the monitors together. It’s great!
And for a while, everything worked fine. I’d switch to my Windows computer for gaming, then back to Linux for day-to-day work and computing. But for some reason when I added my work laptop to the mix, something went wrong on my personal Linux desktop.
Suddenly, when the monitors switched, mutter2 would move all the windows on my left monitor onto the right monitor. This was very, very annoying.
Surely, I thought, there must be a way to fix this. The actual issue has been discussed in various forums for quite a few years. Here’s a bug report for mutter on the topic, which has links to more bugs for Red Hat, Ubuntu, and gnome-shell.
I don’t think this had anything to do with my work laptop, exactly. Instead, it’s probably because I shifted some cabling around when I added my work laptop to the mix, moving my personal Linux desktop from HDMI1 to DisplayPort2 on my left monitor. This in turn changes the timing of when the monitor sleeps and wakes when the input is switched, and mutter reacts by moving all my windows around.
The display-switch
project lets you run arbitrary commands when the USB device disconnects and
connects. I wanted to use this to keep my windows where I put them.
In reading about the issue, I found some workarounds people had come up with, including a very
creative one using wmctrl
. But wmctrl
only works with X
and X is going away in favor of Wayland.
But then I read some more and discovered that Gnome has a
comprehensive JavaScript binding that you can invoke with some dbus
magic:
|
|
Could I use this to somehow save and restore my windows? Yes, I could! When you run this command,
you will get some output to stdout
like this:
|
|
The output is a list where the first item is a boolean indicating whether the code threw an error (I think), and the second is the error output or the value of the last statement executed.
So I wrote a little Perl script to execute the JS I needed and parse the output to check if it worked.
Here’s the code in full:
|
|
The Perl parts aren’t that interesting. It’s the JS that’s doing all the work. Here’s the code to save the window positions:
|
|
This loops through all the windows and records information for each window. It saves the monitor the window is on, its unique ID, its X & Y position, and its height & width. This gets written as JSON to a file every time the USB device is disconnected.
One odd thing is that global.get_window_actors()
includes one window with a null
title and
another window for the gnome-shell
process. I’m not sure what that null
title window is, but
it’s best to just skip it and gnome-shell
.
The restore code is even simpler:
|
|
It loads the saved window position info, then matches the current windows against the IDs of the saved windows. When there’s a match, it restores the window to the correct monitor, then set its position and size.
One other thing to note is the sleep(5)
in the Perl code’s restore
subroutine. The program needs
to wait for the monitor’s input change to take effect, or else none of this works. It’d be nice if
display-switch
offered an on_monitor_input_change_execute
config option, but I’m not sure if
that’s even possible. The sleep
is a hack, but it works fine, so it’s good enough for now.
I just got a docking station for my work laptop, so I’ll be able to connect it to both my monitors as well, and I can use this program on that computer too if I need to.
I’m quite pleased with this solution. I thought it might be anywhere from very hard to impossible, but this turned out to be fairly easy. Most of my time was spent simply reading about the problem before discovering the Gnome JS API. Once I knew that API existed, the actual implementation was fairly easy.
I also want to credit this /r/gnome post by MortimerErnest, which links to a bash script they wrote. Reading that script made it quite obvious how I could use the Gnome JS API for my own problem.
Well, more than four, because I also have a NAS, a network router, a Nintendo Switch, a PS5 in the closet, an iPad mini in the same closet, and a Raspberry Pi I bought over a year ago with which I intended to build an LCD panel clock, though I’ve not done so yet. And my phone is also a computer. This is a very normal number of computers to have. ↩︎
The default window manager for GNOME since GNOME 3. ↩︎