-
-
Save larsyencken/1348e47ce864b3174e855e801602d9d9 to your computer and use it in GitHub Desktop.
Linux: move the active window to a fixed location on the screen
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
# | |
# tile.py | |
# xfce-window-tiling | |
# | |
""" | |
A tiling script on top of wmctrl command. | |
""" | |
from dataclasses import dataclass | |
from typing import Tuple, List, Dict | |
import re | |
import sys | |
import sh | |
import click | |
# how much space do we want to avoid using | |
HORIZONTAL_MARGIN = 0.35 | |
# how much gap is ok at the bottom of the screen | |
VERTICAL_TOLERANCE = 10 | |
# how much of the screen to use when centering | |
CENTER_PROPORTION = 0.5 | |
CENTER_PROPORTION_NARROW = 0.4 | |
@click.command() | |
@click.argument('variant') | |
def tile(variant: str): | |
""" | |
Tile the currently active window either left, right or center. | |
""" | |
window = Window.get_active() | |
if variant == 'left': | |
tile_left(window) | |
elif variant == 'right': | |
tile_right(window) | |
elif variant == 'center': | |
tile_center(window, narrow=False) | |
elif variant == 'center-narrow': | |
tile_center(window, narrow=True) | |
else: | |
print('ERROR: tile variant must be left/right/center', file=sys.stderr) | |
sys.exit(1) | |
log(f'Exit ok') | |
@dataclass | |
class Desktop: | |
desktop_id: int | |
size: Tuple[int, int] | |
is_active: bool | |
@classmethod | |
def from_line(cls, l: str) -> "Desktop": | |
""" | |
Parse Desktop from wmctrl output, like: | |
"0 * DG: 3440x1440 VP: 0,0 WA: 0,0 3440x1403 Workspace 1" | |
""" | |
l_norm = l.replace(": ", ":").replace(" ", " ") | |
parts = l_norm.split() | |
assert len(parts) == 8 | |
desktop_id = int(parts[0]) | |
is_active = parts[1] == "*" | |
size = tuple(map(int, parts[5].split("x"))) | |
return cls(desktop_id=desktop_id, is_active=is_active, size=size) | |
@staticmethod | |
def find_all() -> "List[Desktop]": | |
lines = sh.wmctrl("-d").stdout.decode().splitlines() | |
return [Desktop.from_line(l) for l in lines] | |
@staticmethod | |
def get_active() -> "Desktop": | |
(desktop,) = [d for d in Desktop.find_all() if d.is_active] | |
return desktop | |
@dataclass | |
class Geometry: | |
xmin: int | |
ymin: int | |
width: int | |
height: int | |
def vshrink(self, delta: int): | |
return Geometry(self.xmin, self.ymin, self.width, max(0, self.height - delta)) | |
@dataclass | |
class Window: | |
window_id: int | |
desktop_id: int | |
name: str | |
geometry: Geometry | |
@classmethod | |
def from_line(cls, l): | |
l_norm = re.sub(" +", " ", l) | |
parts = l_norm.strip().split(" ", 7) | |
window_id_hex, desktop_id, xmin, ymin, width, height, _, name = parts | |
window_id = int(window_id_hex, 16) | |
geometry = Geometry( | |
xmin=int(xmin), ymin=int(ymin), width=int(width), height=int(height) | |
) | |
return cls( | |
window_id=window_id, | |
desktop_id=int(desktop_id), | |
name=name, | |
geometry=geometry, | |
) | |
@classmethod | |
def find_all(cls) -> "Dict[int, Window]": | |
lines = sh.wmctrl("-l", "-G").stdout.decode().splitlines() | |
return {w.window_id: w for w in [cls.from_line(l) for l in lines]} | |
@classmethod | |
def get_active(cls) -> "Window": | |
window_id = int(sh.xdotool("getwindowfocus").stdout.decode()) | |
windows = cls.find_all() | |
return windows[window_id] | |
def move(self, target: Geometry): | |
mvarg = f"0,{target.xmin},{target.ymin},{target.width},{target.height}" | |
args = ("-R", self.window_id, "-i", "-e", mvarg) | |
sh.wmctrl(*args) | |
self.refresh() | |
def refresh(self): | |
windows = Window.find_all() | |
w_new = windows[self.window_id] | |
self.geometry = w_new.geometry | |
def tile_left(window: Window): | |
desktop_width, desktop_height = Desktop.get_active().size | |
# work out the x position | |
midpoint = desktop_width // 2 | |
xmin = int(midpoint * HORIZONTAL_MARGIN) | |
# assume full height | |
target = Geometry(xmin=xmin, width=midpoint - xmin, ymin=0, height=desktop_height) | |
# attempt first move, might not be correct | |
automove(window, target, desktop_height) | |
def tile_right(window: Window): | |
desktop_width, desktop_height = Desktop.get_active().size | |
# work out the x position | |
midpoint = desktop_width // 2 | |
xmin = midpoint | |
width = int(midpoint * (1 - HORIZONTAL_MARGIN)) | |
# assume full height | |
target = Geometry(xmin=xmin, width=width, ymin=0, height=desktop_height) | |
automove(window, target, desktop_height) | |
def tile_center(window: Window, narrow: bool): | |
desktop_width, desktop_height = Desktop.get_active().size | |
proportion = CENTER_PROPORTION if not narrow else CENTER_PROPORTION_NARROW | |
# work out the x position | |
width = int(desktop_width * proportion) | |
xmin = int(desktop_width * (1 - proportion) / 2) | |
# assume full height | |
target = Geometry(xmin=xmin, width=width, ymin=0, height=desktop_height) | |
# attempt first move, might not be correct | |
automove(window, target, desktop_height) | |
def automove(window: Window, target: Geometry, desktop_height: int) -> None: | |
# attempt first move, might not be correct | |
window.move(target) | |
# see where we landed and adjust the height | |
new_ymin = window.geometry.ymin | |
if new_ymin + window.geometry.height >= desktop_height: | |
# the second move is the correct one | |
new_height = desktop_height - new_ymin // 2 | |
target.height = new_height | |
target.ymin = 0 | |
window.move(target) | |
if __name__ == "__main__": | |
tile() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment