Skip to content

Instantly share code, notes, and snippets.

@charlieman
Last active March 1, 2022 01:32
Show Gist options
  • Save charlieman/651c03dedc1c3ade5f1e to your computer and use it in GitHub Desktop.
Save charlieman/651c03dedc1c3ade5f1e to your computer and use it in GitHub Desktop.
Tool for tiling windows in X11
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# The MIT License (MIT)
#
# Copyright (c) 2015 Carlos Zúñiga
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
wtiler
~~~~~~
Tool for tiling windows in X11
"""
from __future__ import print_function, division
import Xlib.display
import os
# Todo:
# open in the center of the window focused
# deal with windowmanager bars and exact resizes
# remove the EWMH dependency
ewmh = None
try:
import Tkinter as Tk
from Tkinter import N, S, E, W
import ttk
except ImportError:
import tkinter as Tk
from tkinter import N, S, E, W
from tkinter import ttk
from ewmh import EWMH
ewmh = EWMH()
from collections import namedtuple
try:
from configparser import ConfigParser, NoOptionError
except ImportError:
from ConfigParser import ConfigParser, NoOptionError
Geometry = namedtuple('Geometry', 'x y width height')
Point = namedtuple('Point', 'x y')
def get_active_window():
display = Xlib.display.Display()
window = display.get_input_focus().focus
wmname = window.get_wm_name()
wmclass = window.get_wm_class()
if wmclass is None and wmname is None:
window = window.query_tree().parent
wmname = window.get_wm_name()
return window
def get_root(window):
parent = window.query_tree().parent
while parent != 0:
window = window.query_tree().parent
parent = window.query_tree().parent
return window
def unmaximize(window):
if ewmh is None: return
ewmh.setWmState(window, 0, "_NET_WM_STATE_MAXIMIZED_VERT")
ewmh.setWmState(window, 0, "_NET_WM_STATE_MAXIMIZED_HORZ")
ewmh.display.flush()
def resize(window, geometry):
"""Resizes an xlib window
:param window: a xlib.display.Window
:param geometry: a Geometry object
"""
unmaximize(window)
window.configure(x=geometry.x, y=geometry.y, width=geometry.width, height=geometry.height)
window.change_attributes(win_gravity=Xlib.X.NorthWestGravity, bit_gravity=Xlib.X.StaticGravity) # this doesn't seem to do anything
a = window.get_attributes() # this actually does the resizing
def clamp(val, min_, max_):
return min(max_, max(min_, val))
def get_config_dir(appname):
return os.path.join(os.path.expanduser('~'), '.config', appname)
class WTiler(object):
"""Widget for selecting the new geometry for a window"""
geometry = None
right_keys = ('Right', 'l', 'L')
left_keys = ('Left', 'h', 'H')
down_keys = ('Down', 'j', 'J')
up_keys = ('Up', 'k', 'K')
movement_keys = right_keys + left_keys + down_keys + up_keys
def __init__(self, display_width, display_height):
self.display_width = display_width
self.display_height = display_height
self.ratio = display_height / display_width
self.current = Point(0, 0)
self.anchor = Point(0, 0)
self.tile_width = 40
self.root = Tk.Tk()
self.root.title('WTiler')
self.canvas = None
self.config_file = os.path.join(get_config_dir('wtiler'), 'config.ini')
self.setup_style()
self.read_configuration()
self.setup_grid()
self.root.bind('<Key>', self.keypress)
self.root.bind('<Shift-Key>', lambda e: self.keypress(e, 'shift'))
self.root.bind('<Control-Key>', self.save_macro)
self.root.bind('<Alt-Key>', self.resize)
self.root.bind('<Button-1>', self.dragstart)
self.root.bind('<ButtonRelease-1>', self.dragend)
self.root.bind('<Return>', self.accept)
self.root.bind('<Escape>', lambda e: self.root.destroy())
def read_configuration(self):
self.config = ConfigParser()
self.config.add_section('wtiler')
self.config.add_section('macros')
self.config.read(self.config_file)
try:
self.x_tiles = self.config.getint('wtiler', 'x_tiles') # fallback=9
except NoOptionError:
self.x_tiles = 9
try:
self.y_tiles = self.config.getint('wtiler', 'y_tiles') # fallback=6
except NoOptionError:
self.y_tiles = 6
self.macros = {}
for key, points in self.config.items('macros'):
if key in self.movement_keys:
continue
try:
p = [int(i) for i in points.split(" ")]
self.macros[key] = (Point(p[0], p[1]), Point(p[2], p[3]))
except IndexError:
pass
def save_configuration(self):
self.config.set('wtiler', 'x_tiles', str(self.x_tiles))
self.config.set('wtiler', 'y_tiles', str(self.y_tiles))
for key, points in self.macros.items():
self.config.set('macros', key, "{p.x} {p.y} {q.x} {q.y}".format(p=points[0], q=points[1]))
try:
os.makedirs(os.path.dirname(self.config_file)) # exist_ok=True
except OSError:
pass
with open(self.config_file, 'w') as config_file:
self.config.write(config_file)
def i_to_point(self, i):
return Point(i // self.x_tiles, y_tiles % i)
def point_to_i(self, point):
return point.x + self.x_tiles * point.y
def xy_to_i(self, x, y):
return x + self.x_tiles * y
def coord_to_point(self, x, y):
col = int(x / (self.tile_width + self.padding))
row = int(y / (self.tile_width * self.ratio + self.padding))
col = clamp(col, 0, self.x_tiles - 1)
row = clamp(row, 0, self.y_tiles - 1)
return Point(col, row)
def setup_style(self):
self._normal = '#C6C6C6'
self._selected = '#80AEF7'
self._current = '#488BF7'
self.padding = 2
self.width = 400
def setup_grid(self):
if self.canvas:
self.canvas.destroy()
tile_width = self.width // self.x_tiles
tile_height = self.width * self.ratio // self.y_tiles
self.canvas = Tk.Canvas(self.root,
width=(self.padding + (tile_width + self.padding) * self.x_tiles),
height=(self.padding + (tile_height + self.padding) * self.y_tiles))
self.canvas.pack()
self.tiles = []
for y in range(self.y_tiles):
for x in range(self.x_tiles):
tile = self.canvas.create_rectangle(
self.padding * (x + 1) + tile_width * x,
self.padding * (y + 1) + tile_height * y,
self.padding * (x + 1) + tile_width * (x + 1),
self.padding * (y + 1) + tile_height * (y + 1),
fill=self._normal
)
self.tiles.append(tile)
self.colorize()
def set_geometry(self):
minp, maxp = self.reorder_points(self.anchor, self.current)
x = self.display_width * minp.x // self.x_tiles
y = self.display_height * minp.y // self.y_tiles
width = self.display_width * (maxp.x - minp.x + 1) // self.x_tiles
height = self.display_height * (maxp.y - minp.y + 1) // self.y_tiles
self.geometry = Geometry(x, y, width, height)
def accept(self, event=None):
self.set_geometry()
self.save_configuration()
self.root.destroy()
def keypress(self, event, mod=None):
if event.keysym in self.right_keys:
selected = Point((self.current.x + 1) % self.x_tiles, self.current.y)
elif event.keysym in self.left_keys:
selected = Point((self.current.x - 1) % self.x_tiles, self.current.y)
elif event.keysym in self.down_keys:
selected = Point(self.current.x, (self.current.y + 1) % self.y_tiles)
elif event.keysym in self.up_keys:
selected = Point(self.current.x, (self.current.y - 1) % self.y_tiles)
else:
points = self.macros.get(event.keysym)
if points:
self.anchor = points[0]
self.current = points[1]
self.fix_points_outofbound()
self.colorize()
return
self.current = selected
if mod == None: # if not pressing shift
self.anchor = selected
self.colorize()
def resize(self, event):
if event.keysym in self.right_keys:
self.x_tiles = max(1, self.x_tiles + 1)
elif event.keysym in self.left_keys:
self.x_tiles = max(1, self.x_tiles - 1)
elif event.keysym in self.down_keys:
self.y_tiles = max(1, self.y_tiles + 1)
elif event.keysym in self.up_keys:
self.y_tiles = max(1, self.y_tiles - 1)
self.fix_points_outofbound()
self.setup_grid()
def fix_points_outofbound(self):
self.anchor = Point(min(self.anchor.x, self.x_tiles - 1), min(self.anchor.y, self.y_tiles - 1))
self.current = Point(min(self.current.x, self.x_tiles - 1), min(self.current.y, self.y_tiles - 1))
def save_macro(self, event):
if event.keysym in self.movement_keys:
return
minp, maxp = self.reorder_points(self.anchor, self.current)
self.macros[event.keysym] = (minp, maxp)
def reorder_points(self, point1, point2):
min_x, max_x = min(point1.x, point2.x), max(point1.x, point2.x)
min_y, max_y = min(point1.y, point2.y), max(point1.y, point2.y)
return Point(min_x, min_y), Point(max_x, max_y)
def colorize(self):
minp, maxp = self.reorder_points(self.anchor, self.current)
for tile in self.tiles:
self.canvas.itemconfig(tile, fill=self._normal)
for y in range(minp.y, maxp.y + 1):
for x in range(minp.x, maxp.x + 1):
self.canvas.itemconfig(self.tiles[self.xy_to_i(x, y)], fill=self._selected)
self.canvas.itemconfig(self.tiles[self.point_to_i(self.current)], fill=self._current)
def dragstart(self, event):
if event.widget is not self.canvas: return
x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
selected = self.coord_to_point(x, y)
self.anchor = selected
self.current = selected
self.root.bind('<Motion>', self.motion)
self.colorize()
def dragend(self, event):
self.root.unbind('<Motion>')
self.accept()
def motion(self, event):
if event.widget is not self.canvas: return
x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
selected = self.coord_to_point(x, y)
self.current = selected
self.colorize()
if __name__ == '__main__':
window = get_active_window()
root = get_root(window)
rgeometry = root.get_geometry()
tiler = WTiler(rgeometry.width, rgeometry.height)
tiler.root.eval('tk::PlaceWindow %s center' % tiler.root.winfo_pathname(tiler.root.winfo_id()))
tiler.root.mainloop()
if tiler.geometry:
resize(window, tiler.geometry)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment