User Tools

Site Tools


os:linux:general:auto-win

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 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: auto-win.tar.gz. You can also find the latest version of these files on 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": "<monitor manufacturer short name>",
            "model": "<monitor model number>",
            "map": "<maps to a window config below>"
        },
        "<window config name (matching below)>": [
            {
                "manufacturer": "<monitor manufacturer short name>",
                "model": "<monitor model number>",
                "serial": "<monitor serial number>"
            },
            ...
        ],
        "<window config name (matching below)>": [
        ...
        ]
    },
    "<window config name>": [
        {
            "name": "<window name>",
            "desk": <desktop/workspace number>,
            "xoff": <window X offset>,
            "yoff": <window Y offset>,
            "width": <window width>,
            "height": <window height>
        },
        ...
    ],
    "<window config name>": [
    ...
    ]
}       

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.

os/linux/general/auto-win.txt · Last modified: 2024/07/19 18:08 by jay