#!/usr/bin/env python3 ############################################## # Auto Qtile keybindings - image generator # # Qmade version of the configuration # ############################################## import getopt import os import sys import cairocffi as cairo from cairocffi import ImageSurface this_dir = os.path.dirname(__file__) base_dir = os.path.abspath(os.path.join(this_dir, "..")) sys.path.insert(0, base_dir) BUTTON_NAME_Y = 65 BUTTON_NAME_X = 10 COMMAND_Y = 20 COMMAND_X = 10 LEGEND = ["modifiers", "layout", "group", "window", "other"] CUSTOM_KEYS = { "Backspace": 2, "Tab": 1.5, "\\": 1.5, "Return": 2.4533, "shift": 2, "space": 5, } class Button: def __init__(self, key, x, y, width, height): self.key = key self.x = x self.y = y self.width = width self.height = height class Pos: WIDTH = 78 HEIGHT = 70 GAP = 5 def __init__(self, x, y): self.x = x self.row_x = x self.y = y self.custom_width = {} for i, val in CUSTOM_KEYS.items(): self.custom_width[i] = val * self.WIDTH def get_pos(self, name): if name in self.custom_width: width = self.custom_width[name] else: width = self.WIDTH info = Button(name, self.x, self.y, width, self.HEIGHT) self.x = self.x + self.GAP + width return info def skip_x(self, times=1): self.x = self.x + self.GAP + times * self.WIDTH def next_row(self): self.x = self.row_x self.y = self.y + self.GAP + self.HEIGHT class KeyboardPNGFactory: def __init__(self, modifiers, keys): self.keys = keys self.modifiers = modifiers.split("-") self.key_pos = self.calculate_pos(20, 140) def rgb_red(self, context): context.set_source_rgb(0.8431372549, 0.3725490196, 0.3725490196) def rgb_green(self, context): context.set_source_rgb(0.6862745098, 0.6862745098, 0) def rgb_yellow(self, context): context.set_source_rgb(1, 0.6862745098, 0) def rgb_cyan(self, context): context.set_source_rgb(0.5137254902, 0.6784313725, 0.6784313725) def rgb_violet(self, context): context.set_source_rgb(0.831372549, 0.5215686275, 0.6784313725) def calculate_pos(self, x, y): pos = Pos(x, y) key_pos = {} for c in "`1234567890-=": key_pos[c] = pos.get_pos(c) key_pos["Backspace"] = pos.get_pos("Backspace") pos.next_row() key_pos["Tab"] = pos.get_pos("Tab") for c in "qwertyuiop[]\\": key_pos[c] = pos.get_pos(c) pos.next_row() pos.skip_x(1.6) for c in "asdfghjkl;'": key_pos[c] = pos.get_pos(c) key_pos["Return"] = pos.get_pos("Return") pos.next_row() key_pos["shift"] = pos.get_pos("shift") for c in "zxcvbnm": key_pos[c] = pos.get_pos(c) key_pos["period"] = pos.get_pos("period") key_pos["comma"] = pos.get_pos("comma") key_pos["/"] = pos.get_pos("/") pos.next_row() key_pos["control"] = pos.get_pos("control") pos.skip_x() key_pos["mod4"] = pos.get_pos("mod4") key_pos["mod1"] = pos.get_pos("mod1") key_pos["space"] = pos.get_pos("space") key_pos["Print"] = pos.get_pos("Print") pos.skip_x(3) key_pos["Up"] = pos.get_pos("Up") pos.next_row() pos.skip_x(12.33) key_pos["Left"] = pos.get_pos("Left") key_pos["Down"] = pos.get_pos("Down") key_pos["Right"] = pos.get_pos("Right") pos.next_row() for legend in LEGEND: key_pos[legend] = pos.get_pos(legend) pos.skip_x(5) key_pos["Button1"] = pos.get_pos("Button1") key_pos["Button2"] = pos.get_pos("Button2") key_pos["Button3"] = pos.get_pos("Button3") pos.next_row() key_pos["FN_KEYS"] = pos.get_pos("FN_KEYS") return key_pos def render(self, filename): surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1280, 800) context = cairo.Context(surface) with context: context.set_source_rgb(1, 1, 1) context.paint() context.move_to(40, 50) context.set_font_size(34) context.show_text("Qmade - Keybindings for Qtile") context.move_to(40, 80) context.set_font_size(22) if len([i for i in self.modifiers if i]): context.show_text("Modifiers: " + ", ".join(self.modifiers)) else: context.show_text("No modifiers used.") for i in self.key_pos.values(): if i.key in ["FN_KEYS"]: continue self.draw_button(context, i.key, i.x, i.y, i.width, i.height) # draw functional fn = [i for i in keys.values() if i.key[:4] == "XF86"] if len(fn): fn_pos = self.key_pos["FN_KEYS"] x = fn_pos.x for i in fn: self.draw_button(context, i.key, x, fn_pos.y, fn_pos.width, fn_pos.height) x += Pos.GAP + Pos.WIDTH # draw mouse base context.rectangle(830, 670, 244, 90) context.set_source_rgb(0, 0, 0) context.stroke() context.set_font_size(28) context.move_to(900, 730) context.show_text("MOUSE") surface.write_to_png(filename) def draw_button(self, context, key, x, y, width, height): radius = 5 # Radius for the rounded corners fn = False if key[:4] == "XF86": fn = True if key in LEGEND: if key == "modifiers": self.rgb_red(context) elif key == "group": self.rgb_green(context) elif key == "layout": self.rgb_cyan(context) elif key == "window": self.rgb_yellow(context) else: self.rgb_violet(context) self.rounded_rectangle(context, x, y, width, height, radius) context.fill() if key in self.modifiers: self.rounded_rectangle(context, x, y, width, height, radius) self.rgb_red(context) context.fill() if key in self.keys: k = self.keys[key] self.rounded_rectangle(context, x, y, width, height, radius) self.set_key_color(context, k) context.fill() self.show_multiline(context, x + COMMAND_X, y + COMMAND_Y, k) self.rounded_rectangle(context, x, y, width, height, radius) context.set_source_rgb(0, 0, 0) context.stroke() if fn: key = key[4:] context.set_font_size(10) else: context.set_font_size(14) context.move_to(x + BUTTON_NAME_X, y + BUTTON_NAME_Y) context.show_text(self.translate(key)) def rounded_rectangle(self, context, x, y, width, height, radius): context.new_path() context.arc(x + radius, y + radius, radius, 2 * (3.14 / 2), 3 * (3.14 / 2)) context.arc(x + width - radius, y + radius, radius, 3 * (3.14 / 2), 4 * (3.14 / 2)) context.arc(x + width - radius, y + height - radius, radius, 0, 3.14 / 2) context.arc(x + radius, y + height - radius, radius, 3.14 / 2, 2 * (3.14 / 2)) context.close_path() def show_multiline(self, context, x, y, key): """Cairo doesn't support multiline. Added with word wrapping.""" c_width = 14 if key.key in CUSTOM_KEYS: c_width *= CUSTOM_KEYS[key.key] context.set_font_size(10) context.set_source_rgb(0, 0, 0) context.move_to(x, y) words = key.command.split(" ") words.reverse() printable = last_word = words.pop() while len(words): last_word = words.pop() if len(printable + " " + last_word) < c_width: printable += " " + last_word continue context.show_text(printable) y += 10 context.move_to(x, y) printable = last_word if last_word is not None: context.show_text(printable) def set_key_color(self, context, key): if key.scope == "group": self.rgb_green(context) elif key.scope == "layout": self.rgb_cyan(context) elif key.scope == "window": self.rgb_yellow(context) else: self.rgb_violet(context) def translate(self, text): dictionary = { "period": ",", "comma": ".", "Left": "←", "Down": "↓", "Right": "→", "Up": "↑", "AudioRaiseVolume": "Volume up", "AudioLowerVolume": "Volume down", "AudioMute": "Audio mute", "AudioMicMute": "Mic mute", "MonBrightnessUp": "Brightness up", "MonBrightnessDown": "Brightness down", } if text not in dictionary: return text return dictionary[text] class KInfo: NAME_MAP = { "togroup": "to group", "toscreen": "to screen", } def __init__(self, key): self.key = key.key self.command = self.get_command(key) self.scope = self.get_scope(key) def get_command(self, key): if hasattr(key, "desc") and key.desc: return key.desc cmd = key.commands[0] command = cmd.name if command in self.NAME_MAP: command = self.NAME_MAP[command] command = command.replace("_", " ") if len(cmd.args): if isinstance(cmd.args[0], str): command += " " + cmd.args[0] return command def get_scope(self, key): selectors = key.commands[0].selectors if len(selectors): return selectors[0][0] class MInfo(KInfo): def __init__(self, mouse): self.key = mouse.button self.command = self.get_command(mouse) self.scope = self.get_scope(mouse) def get_kb_map(config_path=None): from libqtile.confreader import Config c = Config(config_path) if config_path: c.load() kb_map = {} for key in c.keys: mod = "-".join(key.modifiers) if mod not in kb_map: kb_map[mod] = {} info = KInfo(key) kb_map[mod][info.key] = info for mouse in c.mouse: mod = "-".join(mouse.modifiers) if mod not in kb_map: kb_map[mod] = {} info = MInfo(mouse) kb_map[mod][info.key] = info return kb_map help_doc = """ usage: gen-keybinding-img [-h] [-c CONFIGFILE] [-o OUTPUT_DIR] Qtile keybindings image generator optional arguments: -h, --help show this help message and exit -c CONFIGFILE, --config CONFIGFILE use specified configuration file. If no presented default will be used -o OUTPUT_DIR, --output-dir OUTPUT_DIR set directory to export all images to """ if __name__ == "__main__": config_path = os.path.expanduser("~/.config/qtile/config.py") # Set default config path output_dir = "" try: opts, args = getopt.getopt(sys.argv[1:], "hc:o:", ["help=", "config=", "output-dir="]) except getopt.GetoptError: print(help_doc) sys.exit(2) for opt, arg in opts: if opt in ("-h", "--help"): print(help_doc) sys.exit() elif opt in ("-c", "--config"): config_path = arg elif opt in ("-o", "--output-dir"): output_dir = arg kb_map = get_kb_map(config_path) for modifier, keys in kb_map.items(): if not modifier: filename = "keybinding_no_modifier.png" else: filename = "keybinding_{}.png".format(modifier) output_file = os.path.abspath(os.path.join(output_dir, filename)) f = KeyboardPNGFactory(modifier, keys) f.render(output_file)