====== Automatically Adjust Your Window Positions ====== ===== Overview ===== I often plug my work laptop into different monitors, or sets of monitors, and this means that I have rearrange my windows each time I plug/unplug monitors. It's a giant pain in the ass so I put together a couple of scripts and systemd files to automatically move windows to explicit positions/workspaces. Since I am often plugged into a monitor in the office, but often unplug it to take to meetings, this has been life changing. ===== Prerequisites ===== The first, and most important, prerequisite is that **you must be using X11, and NOT Wayland**. This is simply because the tools used here in this tutorial and by the scripts are specific to X11. If there's a way to accomplish this with similar tools for wayland, shoot me an email at admin@splitstreams.com or open an issue on [[https://github.com/crustymonkey/linux-auto-win|Github]] with the pointers and I'll happily update these scripts to work with Wayland as well (auto-detected). ==== Packages ==== You must ensure you have the following packages installed: * **xwininfo**: This should be in your distro's package manager * **wmctrl**: Again, this should be widely available in your package manager * **edid-decode**: And yes, this should also be in your package manager There are 2 Python scripts included (below), but neither of them have any dependencies beyond the standard library. ==== The Files ==== All the files you will need for this are available here: {{ :os:linux:general:auto-win.tar.gz |auto-win.tar.gz}}. You can also find the latest version of these files on [[https://github.com/crustymonkey/linux-auto-win|Github]]. In there you will find the following files: * **adjwin.py**: This is script that will move all your windows around based on your configuration * **adjwin.json**: This is the configuration file for your monitors. While JSON sucks to write manually, it means we don't need any non-standard Python dependencies. That was the tradeoff here. * **auto-win.py**: This script detects your monitors, matches them to a profile and then runs ''adjwin.py'' to move your windows around. * **adjwin.service**: A systemd user file for running ''auto-win.py''. * **adjwin.timer**: The systemd timer companion for ''adjwin.service''. ===== Installing the Files ===== This is basically just a matter of copying the files into their proper locations. ==== The Scripts ==== These are pretty simple. Just put them somewhere in your ''$PATH''. Note that there is help available for the scripts via the standard ''--help/-h''. ==== The Systemd User Files ==== These are also simple. You should do the following: cp adjwin.service adjwin.timer ~/.config/systemd/user ==== The Config ==== I've included my config as an example/starting point. You can copy this into place and modify it per the configuration instructions below. # Note the "." prefix to make it a hidden file cp adjwin.json ~/.adjwin.json ===== Configuration ===== I'm not going to lie, this is going to be the most painful part of this, but you should only have to do this once for each of your monitor configs. After that, everything will just work. ==== Config Structure ==== Again, this is a JSON file to avoid dependencies. Therefore, I would highly recommend using some sort of JSON editor for this. VSCode works pretty well, as does good ol' ''vim''. The structure of the file is as follows. Anything shown in "<>" brackets is non-literal meta info. { "_mon_map_": { "built in": { "manufacturer": "", "model": "", "map": "" }, "": [ { "manufacturer": "", "model": "", "serial": "" }, ... ], "": [ ... ] }, "": [ { "name": "", "desk": , "xoff": , "yoff": , "width": , "height": }, ... ], "": [ ... ] } I'm going to come back to the ''_mon_map_'' section, which is only used by ''auto-win.py'', for now and start with the window configurations. ==== Window Configurations ==== As you can see, you can configure multiple window configurations. Let's go over how you can configure one of these. First, you'll likely want a configuration for your laptop itself, which we'll call "laptop". To get started, position a window where you want it, then run ''xwininfo'' and click on that window. That will result in some info like the following: xwininfo: Window id: 0x2800192 "Formatting Syntax [StuffIveLearned.org] - Google Chrome" Absolute upper-left X: 3173 Absolute upper-left Y: 49 Relative upper-left X: 3173 Relative upper-left Y: 49 Width: 1744 Height: 1382 Depth: 32 Visual: 0x5bb Visual Class: TrueColor Border width: 0 Class: InputOutput Colormap: 0x2800003 (not installed) Bit Gravity State: NorthWestGravity Window Gravity State: NorthWestGravity Backing Store State: NotUseful Save Under State: no Map State: IsViewable Override Redirect State: no Corners: +3173+49 -443+49 -443-1449 +3173-1449 -geometry 1744x1382+3173+49 The bits we're interested in here are the "Window id", the "Absolute upper-left X/Y", the "Width", and the "Height". We can just plug this into our configuration: { "laptop": [ { "name": "Chrome", "desk": 0, "xoff": 3173, "yoff": 49, "width": 1744, "height": 1382 } ] } Note that the "desk" attribute corresponds to the desktop/workspace if you are using multiple workspaces. I'm a big user of these on my laptop especially. Note that desktop IDs are zero based. Also note that the "name" is a string match for the "Window id" in the ''xwininfo'' output. You can use whatever you want from that id value to match on. It's also worth noting that you can have multiple window configurations with the same name. Personally, I have 2 Chrome windows in some of mine. In this case, the tool will arrange them in the order that they were first opened. Now, you can actually test this. Move and/or resize that window, then run the following (after saving your config file): adjwin.py laptop Your window should move/resize right back to where it was. Note that you don't //have to// automate this and can just create your configs and manually run ''adjwin.py'' for each window config. Now, just add any other windows to your laptop configuration and/or create other named window configs. At this point, we'll move on to ''auto-win.py'' and the ''_mon_map_'' section of the config. ==== Configuring for ''auto-win.py'' ===== This part of the configuration is about the ''_mon_map_'' section of the config. ''auto-win.py'' uses this section to match a monitor configuration to a window configuration to adjust the windows based on your current monitor config. There is one special part of the ''_mon_map_'' config and that is the "built in" configuration. This pertains to your laptop's built-in monitor. Before we dive into the configuration, let's dump our current monitor info using ''auto-win.py -m''. You should get some output like this: Found the following monitors: Monitor(manufacturer='DEL', model='41130', serial='810963000', built_in=False) Monitor(manufacturer='LGD', model='1699', serial='', built_in=True) Monitor(manufacturer='DEL', model='53506', serial='826497000', built_in=False) We're going to use that info in our ''_mon_map_'' config, which is going to look like the following: { "_mon_map_": { "built in": { "manufacturer": "LGD", "model": "1699", "map": "laptop" }, "home": [ { "manufacturer": "DEL", "model": "41130", "serial": "810963000" }, { "manufacturer": "DEL", "model": "53506", "serial": "826497000" } ], }, "home": [ { "name": "Chrome", "desk": 0, ... } ] } As noted above, the "built in" section refers to the monitor built in to your laptop. The "map" just points to the window config, named "laptop" in this example, to use when no external monitors are plugged in. The "home" config is literally the 2 external monitors I use at home that my laptop is plugged into. When ''auto-win.py'' detects these 2 monitors being plugged in, it will run the corresponding "home" window configuration. Once you have this all setup, it's just a matter setting up the systemd parts of this to automate it. ==== Systemd Config ==== There is one small change you need to make to the included ''adjwin.service'' file that we previously copied to ''~/.config/systemd/user/''. You just have to change the ''ExecStart'' line so that it corresponds to wherever you copied ''auto-win.py'' to. After that, we just need to run a a couple of commands to enable the timer. systemctl --user daemon-reload systemctl --user enable adjwin.timer systemctl --user start adjwin.timer That's it. Every 2 seconds ''auto-win.py'' will run and move your windows based on your monitor config.