import subprocess, sys, time
from Xlib import X, XK, Xatom, error, display
class YazgooWM:
def get_or_first(self, lst, index):
count = len(lst)
if count == 0:
return None
if index < count:
return lst[index]
return lst[0]
def string_to_keycode(self, dpy, key):
return dpy.keysym_to_keycode(XK.string_to_keysym(key))
def keycode_to_char(self, dpy, key):
return dpy.lookup_string(dpy.keycode_to_keysym(key, 0))
def wm_action(self, dpy, key, conf):
char = self.keycode_to_char(dpy, key)
return conf["wm_actions"][char] if char in conf["wm_actions"] else None
def geometries_bsp(self, i, window_count, left, top, width, height, vertical=1):
if window_count == 0:
return []
if window_count == 1:
return [[left, top, width, height]]
if i % 2 == vertical:
return [[left, top, width, height / 2]] + self.geometries_bsp(i + 1, window_count - 1, left, top + height / 2, width, height / 2, vertical)
return [[left, top, width / 2, height]] + self.geometries_bsp(i + 1, window_count - 1, left + width / 2, top, width / 2, height, vertical)
def resize_workspace_windows(self, windows_by_workspaces, current_workspace, dpy, conf, float_windows, layout, foci_by_workspace):
geos = []
count = 0
if layout in ('BSPV', 'BSPH'):
windows = []
for window in windows_by_workspaces[current_workspace]:
if not window in float_windows:
windows.append(window)
count = len(windows)
if count == 0:
return
geos = self.geometries_bsp(0, count, 0, 0, dpy.screen(
).width_in_pixels, dpy.screen().height_in_pixels, 1 if layout == 'BSPV' else 0)
elif layout == 'Monocle':
windows = windows_by_workspaces[current_workspace]
if len(windows) == 0:
return
count = 1
windows = [self.get_or_first(windows, foci_by_workspace[current_workspace])]
geos = self.geometries_bsp(0, 1, 0, 0, dpy.screen().width_in_pixels, dpy.screen().height_in_pixels)
for i in range(count):
geo = geos[i]
windows[i].configure(x=geo[0], y=geo[1], width=geo[2] -
2 * conf["border"]["width"], height=geo[3] - 2 * conf["border"]["width"])
def configure_window_border(self, windows_by_workspaces, current_workspace, foci_by_workspace, conf, border_colors, border_kind, stack_mode):
window = self.get_or_first(windows_by_workspaces[current_workspace], foci_by_workspace[current_workspace])
window.configure(border_width=conf["border"]["width"], stack_mode=stack_mode)
window.change_attributes(None, border_pixel=border_colors[border_kind])
return window
def get_wm_class(self, event):
for _ in range(5):
try:
return event.window.get_wm_class()
except error.BadWindow:
time.sleep(0.05)
return None
def get_window_type(self, dpy, event):
window_type = None
try:
window_type = event.window.get_full_property(
dpy.intern_atom('_NET_WM_WINDOW_TYPE', True), Xatom.ATOM)
except error.BadAtom:
pass
return window_type
def enable_event_listening(self, dpy, conf):
for key in conf["wm_actions"].keys() + conf["custom_actions"].keys() + conf["workspaces"]:
dpy.screen().root.grab_key(self.string_to_keycode(dpy, key),
conf["meta"], 1, X.GrabModeAsync, X.GrabModeAsync)
if key in conf["workspaces"]:
dpy.screen().root.grab_key(self.string_to_keycode(dpy, key), conf["meta"] | X.ShiftMask, 1,
X.GrabModeAsync, X.GrabModeAsync)
for button in [1, 3]:
dpy.screen().root.grab_button(button, conf["meta"], 1, X.ButtonPressMask | X.ButtonReleaseMask | X.PointerMotionMask,
X.GrabModeAsync, X.GrabModeAsync, X.NONE, X.NONE)
dpy.screen().root.change_attributes(event_mask=X.SubstructureNotifyMask)
def __init__(self, conf):
self.dpy = display.Display()
self.layouts = ['BSPV', 'Monocle', 'BSPH']
self.current_workspace = conf["workspaces"][0]
self.float_windows = []
self.windows_by_workspaces = {workspace: [] for workspace in conf["workspaces"]}
self.layouts_by_workspaces = {workspace: 0 for workspace in conf["workspaces"]}
self.foci_by_workspace = {workspace: 0 for workspace in conf["workspaces"]}
self.mouse_move_start = None
self.border_colors = {name : self.dpy.screen().default_colormap.alloc_named_color(conf["border"][name]).pixel for name in ["focus", "normal"]}
self.auto_float_types = [self.dpy.intern_atom('_NET_WM_WINDOW_TYPE_' + typ.upper()) for typ in conf["auto_float_types"]]
self.enable_event_listening(self.dpy, conf)
self.conf = conf
def setup_new_window(self, event):
wm_class = self.get_wm_class(event)
if wm_class is None:
return
window_type = self.get_window_type(self.dpy, event)
if window_type is not None and window_type.value[0] in self.auto_float_types:
return
event.window.configure(border_width=self.conf["border"]["width"])
self.windows_by_workspaces[self.current_workspace].append(event.window)
float_window = wm_class is not None and wm_class[0] in self.conf["float_classes"]
self.foci_by_workspace[self.current_workspace] = len(self.windows_by_workspaces[self.current_workspace]) - 1
if float_window:
self.float_windows.append(event.window)
else:
self.resize_workspace_windows(self.windows_by_workspaces, self.current_workspace, self.dpy, self.conf, self.float_windows,
self.layouts[self.layouts_by_workspaces[self.current_workspace]], self.foci_by_workspace)
def destroy_window(self, event):
if event.window in self.float_windows:
self.float_windows.remove(event.window)
for workspace, workspace_windows in self.windows_by_workspaces.items():
if event.window in workspace_windows:
workspace_windows.remove(event.window)
self.foci_by_workspace[workspace] = 0
self.resize_workspace_windows(self.windows_by_workspaces, workspace, self.dpy, self.conf, self.float_windows,
self.layouts[self.layouts_by_workspaces[workspace]], self.foci_by_workspace)
def change_workspace(self, event):
if event.state & X.ShiftMask and len(self.windows_by_workspaces[self.current_workspace]) > 0:
window = self.get_or_first(self.windows_by_workspaces[self.current_workspace], self.foci_by_workspace[self.current_workspace])
self.windows_by_workspaces[self.current_workspace].remove(window)
self.foci_by_workspace[self.current_workspace] = 0
destination_workspace = self.keycode_to_char(self.dpy, event.detail)
self.windows_by_workspaces[destination_workspace].append(window)
for workspace in (self.current_workspace, destination_workspace):
self.resize_workspace_windows(self.windows_by_workspaces, workspace, self.dpy, self.conf, self.float_windows,
self.layouts[self.layouts_by_workspaces[workspace]], self.foci_by_workspace)
for window in self.windows_by_workspaces[self.current_workspace]:
window.unmap()
self.current_workspace = self.keycode_to_char(self.dpy, event.detail)
for window in self.windows_by_workspaces[self.current_workspace]:
window.map()
if len(self.windows_by_workspaces[self.current_workspace]) > 0:
window = self.get_or_first(self.windows_by_workspaces[self.current_workspace], self.foci_by_workspace[self.current_workspace])
window.set_input_focus(X.RevertToParent, 0)
def switch_window(self):
window_count = len(self.windows_by_workspaces[self.current_workspace])
if window_count > 0:
self.configure_window_border(self.windows_by_workspaces, self.current_workspace,
self.foci_by_workspace, self.conf, self.border_colors, "normal", X.Below)
self.dpy.sync()
self.foci_by_workspace[self.current_workspace] += 1
self.foci_by_workspace[self.current_workspace] = self.foci_by_workspace[self.current_workspace] % window_count
window = self.configure_window_border(
self.windows_by_workspaces, self.current_workspace, self.foci_by_workspace, self.conf, self.border_colors, "focus", X.Above)
window.set_input_focus(X.RevertToParent, X.CurrentTime)
self.resize_workspace_windows(self.windows_by_workspaces, self.current_workspace, self.dpy, self.conf, self.float_windows,
self.layouts[self.layouts_by_workspaces[self.current_workspace]], self.foci_by_workspace)
self.dpy.sync()
def change_layout(self):
self.layouts_by_workspaces[self.current_workspace] = (
self.layouts_by_workspaces[self.current_workspace] + 1) % len(self.layouts)
self.resize_workspace_windows(self.windows_by_workspaces, self.current_workspace, self.dpy, self.conf, self.float_windows,
self.layouts[self.layouts_by_workspaces[self.current_workspace]], self.foci_by_workspace)
def close_window(self):
window_count = len(self.windows_by_workspaces[self.current_workspace])
if window_count > 0:
window = self.get_or_first(self.windows_by_workspaces[self.current_workspace], self.foci_by_workspace[self.current_workspace])
self.windows_by_workspaces[self.current_workspace].remove(window)
window.destroy()
def resize_window(self, event):
xdiff = event.root_x - self.mouse_move_start.root_x
ydiff = event.root_y - self.mouse_move_start.root_y
self.mouse_move_start.child.configure(
x=self.attr.x + (self.mouse_move_start.detail == 1 and xdiff or 0),
y=self.attr.y + (self.mouse_move_start.detail == 1 and ydiff or 0),
width=max(1, self.attr.width +
(self.mouse_move_start.detail == 3 and xdiff or 0)),
height=max(1, self.attr.height + (self.mouse_move_start.detail == 3 and ydiff or 0)))
def run(self):
while True:
event = self.dpy.next_event()
if event.type == X.MapNotify and not event.window in self.windows_by_workspaces[self.current_workspace]:
self.setup_new_window(event)
elif event.type == X.DestroyNotify:
self.destroy_window(event)
elif event.type == X.KeyPress and self.keycode_to_char(self.dpy, event.detail) in self.windows_by_workspaces.keys():
self.change_workspace(event)
elif event.type == X.KeyRelease and self.wm_action(self.dpy, event.detail, self.conf) == 'switch_window':
self.switch_window()
elif event.type == X.KeyRelease and self.keycode_to_char(self.dpy, event.detail) in self.conf["custom_actions"].keys():
self.conf["custom_actions"][self.keycode_to_char(self.dpy, event.detail)]()
elif event.type == X.KeyRelease and self.wm_action(self.dpy, event.detail, self.conf) == 'change_layout':
self.change_layout()
elif event.type == X.KeyRelease and self.wm_action(self.dpy, event.detail, self.conf) == 'close_window':
self.close_window()
elif event.type == X.KeyPress and event.child != X.NONE:
event.child.configure(stack_mode=X.Above)
elif event.type == X.ButtonPress and event.child != X.NONE:
self.attr = event.child.get_geometry()
self.mouse_move_start = event
elif event.type == X.MotionNotify and self.mouse_move_start:
self.resize_window(event)
elif event.type == X.ButtonRelease:
self.mouse_move_start = None
YazgooWM(
conf = {
"meta" : X.Mod4Mask if sys.argv[1] == "mod4" else X.Mod1Mask,
"border": {"width": 2, "focus": "#906cff", "normal": "black"},
"workspaces": ["a", "u", "i", "o", "p"],
"custom_actions": {"r": lambda: subprocess.call(["rofi", "-show", "run"]), "t": lambda: subprocess.call(["kitty"]), "q": lambda: sys.exit(1)},
"wm_actions": {" ": 'switch_window', "w": 'close_window', "f": 'change_layout'},
"float_classes": ('screenkey', 'audacious', 'Download', 'dropbox', 'file_progress', 'file-roller', 'gimp',
'Komodo_confirm_repl', 'Komodo_find2', 'pidgin', 'skype', 'Transmission', 'Update', 'Xephyr', 'obs', 'zoom'),
"auto_float_types": ('notification', 'toolbar', 'splash', 'dialog', 'popup_menu', 'utility', 'tooltip'),
}
).run()