--- /dev/null
+.idea/
+__pycache__/
+venv/
--- /dev/null
+from time import sleep
+from traceback import print_exception
+
+import gi
+import gi.repository.GLib
+import pydbus
+
+
+class BluetoothConf:
+ ADAPTER = "org.bluez.Adapter1"
+ RETRIES = 20
+ RETRY_SLEEP = 5
+
+ def __init__(self):
+ self.bus = pydbus.SystemBus().get("org.bluez", "/org/bluez/hci0")
+ self.conf = self.bus.Get(self.ADAPTER, "Powered")
+
+ def update(self, value):
+ assert isinstance(value, bool)
+ if value == self.conf:
+ return
+ for retry in range(self.RETRIES + 1):
+ try:
+ self.bus.Set(self.ADAPTER, "Powered", pydbus.Variant("b", value))
+ except gi.repository.GLib.GError as e:
+ if retry >= self.RETRIES:
+ raise
+ print_exception(e)
+ print(
+ f"Sleeping {self.RETRY_SLEEP} seconds to try again "
+ f"({retry + 1} / {self.RETRIES})..."
+ )
+ sleep(self.RETRY_SLEEP)
+ else:
+ break
+ # dbus-send --system
+ # --dest=org.bluez --print-reply /org/bluez/hci0
+ # org.freedesktop.DBus.Properties.Set
+ # string:org.bluez.Adapter1
+ # string:Powered
+ # variant:boolean:false
+ self.conf = value
--- /dev/null
+#!/usr/bin/env python3
+
+from collections.abc import Sequence
+from itertools import chain
+from math import atan2, cos, pi, sin
+
+import pygame
+
+tau = 2 * pi
+
+
+class Shape:
+ def fit(self, pos, unit):
+ raise NotImplementedError
+
+
+class Rect(Shape):
+ def __init__(self, *args):
+ if len(args) == 2:
+ args = tuple(chain.from_iterable(args))
+ self.left, self.top, self.width, self.height = args
+
+ def fit(self, pos, unit):
+ return pygame.Rect(
+ (round(pos[0] + self.left * unit[0]), round(pos[1] + self.top * unit[1])),
+ (round(self.width * unit[0]), round(self.height * unit[1])),
+ )
+
+ def draw(self, surf, pos, unit, color):
+ pygame.draw.rect(surf, color, self.fit(pos, unit))
+
+
+class Polygon(Shape):
+ def __init__(self, points):
+ self.points = list(points)
+
+ def fit(self, pos, unit):
+ return [
+ (round(pos[0] + point[0] * unit[0]), round(pos[1] + point[1] * unit[1]))
+ for point in self.points
+ ]
+
+ def draw(self, surf, pos, unit, color):
+ pygame.draw.polygon(surf, color, self.fit(pos, unit))
+
+
+class Circle(Shape):
+ def __init__(self, pos, radius):
+ self.pos = pos
+ self.radius = radius
+
+ def fit(self, pos, unit):
+ return pygame.Rect(
+ (
+ round(pos[0] + (self.pos[0] - self.radius) * unit[0]),
+ round(pos[1] + (self.pos[1] - self.radius) * unit[1]),
+ ),
+ (round(self.radius * 2 * unit[0]), round(self.radius * 2 * unit[1])),
+ )
+
+ def draw(self, surf, pos, unit, color):
+ pygame.draw.ellipse(surf, color, self.fit(pos, unit))
+
+
+class StrokeLine(Polygon):
+ num_vertices = 32
+
+ def __init__(self, start, end, width):
+ super().__init__(
+ list(self.make_line_shape(start, end, self.num_vertices, width / 2))
+ )
+
+ @staticmethod
+ def make_line_shape(p1, p2, n, shape_r):
+ angle = atan2(p1[1] - p2[1], p1[0] - p2[0])
+ if angle < 0:
+ angle += tau
+ # 0 <= angle < tau
+ # corner below 270°
+ initial_corner = int((angle + pi * 3 / 2) * n / tau + .5) % n
+ # corner below 90°
+ opposing_corner = int((angle + pi / 2) * n / tau + .5) % n
+ shape = [
+ (cos(i * tau / n) * shape_r, sin(i * tau / n) * shape_r)
+ for i in range(n)
+ ]
+ p = p1
+ i = initial_corner
+ while i <= n + initial_corner:
+ mod_i = i % n
+ s = shape[mod_i]
+ yield (p[0] + s[0], p[1] + s[1])
+ if mod_i == opposing_corner and p == p1 and p1 != p2:
+ p = p2
+ else:
+ i += 1
+
+
+class Shapes(Shape):
+ def __init__(self, shapes):
+ self.shapes = shapes
+
+ def draw(self, surf, pos, unit, color):
+ for shape in self.shapes:
+ shape.draw(surf, pos, unit, color)
+
+
+class StrokeCircle(Shapes):
+ def __init__(self, center, radius, width):
+ super().__init__([
+ StrokeLine(
+ (
+ center[0] + cos(a) * radius,
+ center[1] + sin(a) * radius,
+ ),
+ (
+ center[0] + cos(b) * radius,
+ center[1] + sin(b) * radius,
+ ),
+ width,
+ )
+ for a, b in self.get_angle_segments()
+ ])
+
+ @staticmethod
+ def get_angle_segments():
+ num_vertices = StrokeLine.num_vertices
+ a = 0
+ for i in range(1, num_vertices + 1):
+ b = tau * i / num_vertices
+ yield a, b
+ a = b
+
+
+class StrokeCircleSegment(Shapes):
+ def __init__(self, center, radius, start_angle, end_angle, width):
+ super().__init__([
+ StrokeLine(
+ (
+ center[0] + cos(a) * radius,
+ center[1] + sin(a) * radius,
+ ),
+ (
+ center[0] + cos(b) * radius,
+ center[1] + sin(b) * radius,
+ ),
+ width,
+ )
+ for a, b in self.get_angle_segments(start_angle, end_angle)
+ ])
+
+ @classmethod
+ def get_angle_segments(cls, start_angle, end_angle):
+ num_vertices = StrokeLine.num_vertices
+ start_angle, end_angle = (
+ min(start_angle, end_angle), max(start_angle, end_angle)
+ )
+ diff_angle = end_angle - start_angle
+ a = 0
+ for i in range(1, num_vertices + 1):
+ b = tau * i / num_vertices
+ if b >= diff_angle:
+ yield a + start_angle, end_angle
+ break
+ yield a + start_angle, b + start_angle
+ a = b
+
+
+laptop_single = Shapes(
+ [
+ Rect((2, 3), (16.4, 1)),
+ Rect((2, 4), (1, 8.1)),
+ Rect((17.4, 4), (1, 8.1)),
+ Rect((2, 12.1), (16.4, 1)),
+ Polygon([(2, 13.1), (18.4, 13.1), (22.4, 18.1), (6, 18.1)]),
+ ]
+)
+laptop_double = Shapes(
+ [
+ Rect((2, 3), (16.4, 1)),
+ Rect((2, 4), (1, 8.1)),
+ Rect((17.4, 4), (1, 8.1)),
+ Rect((2, 12.1), (16.4, 1)),
+ Polygon([(2, 13.1), (3, 13.1), (6.2, 17.1), (5.2, 17.1)]),
+ Polygon([(17.4, 13.1), (18.4, 13.1), (21.6, 17.1), (20.4, 17.1)]),
+ Polygon([(5.2, 17.1), (21.6, 17.1), (22.4, 18.1), (6, 18.1)]),
+ ]
+)
+laptop_vertical = Shapes(
+ [
+ Rect((0.5, 1), (17.75, 1)),
+ Rect((0.5, 2), (1, 14.4)),
+ Rect((9, 2), (1, 14.4)),
+ Rect((17.25, 2), (1, 14.4)),
+ Rect((0.5, 16.4), (17.75, 1)),
+ Polygon([(2, 17.4), (6, 22.4), (20.75, 22.4), (16.75, 17.4)]),
+ ]
+)
+FINGER_RADIUS = 1.25
+touchscreen = Shapes(
+ [
+ StrokeCircleSegment((12, 16), 5, 0, tau * 3 / 8, 1),
+ StrokeLine(
+ (12 + cos(tau * 3 / 8) * 5, 16 + sin(tau * 3 / 8) * 5),
+ (
+ 4 + cos(tau * 3 / 8) * FINGER_RADIUS,
+ 13 + sin(tau * 3 / 8) * FINGER_RADIUS,
+ ),
+ 1,
+ ),
+ StrokeCircleSegment((4, 13), FINGER_RADIUS, tau * 3 / 8, tau * 7 / 8, 1),
+ StrokeLine(
+ (
+ 4 + cos(tau * 7 / 8) * FINGER_RADIUS,
+ 13 + sin(tau * 7 / 8) * FINGER_RADIUS,
+ ),
+ (7, 13.5),
+ 1,
+ ),
+ StrokeLine((7, 13.5), (7, 6), 1),
+ StrokeCircleSegment((8.25, 6), FINGER_RADIUS, pi, tau, 1),
+ StrokeLine((9.5, 6), (9.5, 11), 1),
+ StrokeCircleSegment((11, 11.5), FINGER_RADIUS, tau * 5 / 8, tau * 7 / 8, 1),
+ StrokeCircleSegment((13.25, 12), FINGER_RADIUS, tau * 5 / 8, tau * 7 / 8, 1),
+ StrokeCircleSegment((15.5, 12.5), FINGER_RADIUS, tau * 5 / 8, tau, 1),
+ StrokeLine((16.75, 12.5), (17, 16), 1),
+ StrokeCircleSegment((8.25, 6), 3, pi, tau, 1),
+ StrokeCircleSegment((8.25, 6), 5, pi, tau, 1),
+ ]
+)
+stylus = Shapes(
+ [
+ StrokeCircleSegment((3, 3), 1.5, tau * 3 / 8, tau * 7 / 8, 1),
+ StrokeLine(
+ (3 + cos(tau * 3 / 8) * 1.5, 3 + sin(tau * 3 / 8) * 1.5),
+ (16 + cos(tau * 3 / 8) * 1.5, 16 + sin(tau * 3 / 8) * 1.5),
+ 1,
+ ),
+ StrokeLine(
+ (3 + cos(tau * 7 / 8) * 1.5, 3 + sin(tau * 7 / 8) * 1.5),
+ (16 + cos(tau * 7 / 8) * 1.5, 16 + sin(tau * 7 / 8) * 1.5),
+ 1,
+ ),
+ StrokeLine(
+ (16 + cos(tau * 3 / 8) * 1.5, 16 + sin(tau * 3 / 8) * 1.5),
+ (18, 18),
+ 1,
+ ),
+ StrokeLine(
+ (16 + cos(tau * 7 / 8) * 1.5, 16 + sin(tau * 7 / 8) * 1.5),
+ (18, 18),
+ 1,
+ ),
+ StrokeLine((11, 11), (12, 12), 1),
+ ]
+)
+shapes = [
+ {
+ "shape": laptop_single,
+ "pos": (100, 100),
+ },
+ {
+ "shape": laptop_double,
+ "pos": (600, 100),
+ },
+ {
+ "shape": laptop_vertical,
+ "pos": (1100, 100),
+ },
+ {
+ "shape": touchscreen,
+ "pos": (100, 600),
+ },
+ {
+ "shape": stylus,
+ "pos": (600, 600),
+ },
+]
+
+
+def main():
+ surf = pygame.display.set_mode((2000, 1600))
+ running = True
+ dirty = False
+ clock = pygame.time.Clock()
+ while True:
+ for ev in pygame.event.get():
+ if ev.type == pygame.QUIT:
+ running = False
+ break
+ elif ev.type == pygame.WINDOWEXPOSED:
+ dirty = True
+ elif ev.type == pygame.KEYDOWN:
+ if ev.key == pygame.K_ESCAPE:
+ running = False
+ break
+ if not running:
+ break
+ elif dirty:
+ surf.fill("black")
+ for item in shapes:
+ item["shape"].draw(surf, item["pos"], (16, 16), "green")
+ pygame.display.update()
+ dirty = False
+ clock.tick(60)
+
+
+if __name__ == "__main__":
+ main()
--- /dev/null
+import os
+import re
+import subprocess
+
+
+class XinputConf:
+ DEVICES = {
+ "ELAN9008:00 04F3:425B": {
+ "output": "eDP-1",
+ "type": "touchpad",
+ },
+ "ELAN9008:00 04F3:425B Touchpad": {
+ "output": "eDP-1",
+ "type": "touchpad",
+ },
+ "ELAN9008:00 04F3:425B Stylus Pen (0)": {
+ "output": "eDP-1",
+ "type": "stylus",
+ },
+ "ELAN9009:00 04F3:425A": {
+ "output": "eDP-2",
+ "type": "touchpad",
+ },
+ "ELAN9009:00 04F3:425A Touchpad": {
+ "output": "eDP-2",
+ "type": "touchpad",
+ },
+ "ELAN9009:00 04F3:425A Stylus Pen (0)": {
+ "output": "eDP-2",
+ "type": "stylus",
+ },
+ }
+ DEVICE_PATTERN = re.compile(
+ "^([\u239c ]\\s*\u21b3|\u223c)\\s+(([^ ]| [^ ])+)\\s*\tid=(\\d+)\t"
+ )
+
+ def __init__(self, xrandr_conf):
+ self.xrandr_conf = xrandr_conf
+ self.current_conf = self.get_conf()
+
+ def get_conf(self):
+ output = subprocess.check_output(["xinput", "list"], text=True)
+ devices = []
+ for line in output.split(os.linesep):
+ match = self.DEVICE_PATTERN.match(line)
+ if match:
+ if match.group(2) not in self.DEVICES:
+ continue
+ devices.append(
+ {
+ "name": match.group(2),
+ "enabled": not match.group(1).startswith("\u223c"),
+ "id": match.group(4),
+ **self.DEVICES[match.group(2)],
+ }
+ )
+ elif line and "core pointer" not in line and "core keyboard" not in line:
+ print("line", repr(line))
+ return devices
+
+ def update_device(self, device, enable=True):
+ if enable:
+ subprocess.run(["xinput", "enable", device["id"]])
+ subprocess.run(["xinput", "map-to-output", device["id"], device["output"]])
+ else:
+ subprocess.run(["xinput", "disable", device["id"]])
+
+ def update(self, mode):
+ enable_second = mode != "single"
+ for device in self.current_conf:
+ if device["output"] == self.xrandr_conf.OUTPUTS[0]:
+ self.update_device(device)
+ elif device["output"] == self.xrandr_conf.OUTPUTS[1]:
+ self.update_device(device, enable_second)
--- /dev/null
+import os
+import re
+import subprocess
+
+
+class XrandrConf:
+ CONNECTED_MAPPING = {
+ "connected": True,
+ "disconnected": False,
+ }
+ RESOLUTION_PATTERN = re.compile(r"(\d+)x(\d+)\+(\d+)\+(\d+)")
+ MM_SIZE_PATTERN = re.compile(r"(\d+)mm x (\d+)mm")
+ DIRECTIONS = ("normal", "left", "inverted", "right", "invalid direction")
+ REFLECTIONS = (
+ "none", "[Xx] axis", "[Yy] axis", "X and Y axis", "invalid reflection"
+ )
+ DIRECTIONS_REFLECTIONS_PATTERN = re.compile(
+ f"({'|'.join(DIRECTIONS)})( {'|'.join(REFLECTIONS)})? \\("
+ )
+ AVAILABLE_DIRECTIONS_REFLECTIONS_PATTERN = re.compile(
+ f"{'|'.join(DIRECTIONS)}|{'|'.join(REFLECTIONS)}"
+ )
+ MODE_PATTERN = re.compile(r"^\s{2}([^ ]+) \(0x([0-9A-Fa-f]+)\)\s+([^ ]+)MHz")
+ MODE_FLAGS_PATTERN = re.compile("[-+][CHV]Sync|Interlace|DoubleScan")
+ WIDTH_HEIGHT_PATTERN = re.compile(r"^\s{8}(h: width|v: height)\s+(\d+) ")
+ OUTPUTS = ("eDP-1", "eDP-2")
+
+ def __init__(self):
+ self.current_conf = self.get_conf()
+
+ @staticmethod
+ def strip_each(ss):
+ return (s.strip() for s in ss)
+
+ @staticmethod
+ def int_tuple(it):
+ return tuple(int(i) for i in it)
+
+ @classmethod
+ def parse_screen_line(cls, line):
+ screen_id, sizes = cls.strip_each(line.split(":", 1))
+ return {
+ "screen": int(screen_id[7:]),
+ "sizes": [
+ {
+ "name": name,
+ "size": cls.int_tuple(cls.strip_each(size.split("x"))),
+ } for name, size in (
+ cls.strip_each(p.split(" ", 1))
+ for p in cls.strip_each(sizes.split(","))
+ )
+ ],
+ "outputs": [],
+ }
+
+ @classmethod
+ def parse_output_line(cls, line):
+ name, connected, rest = cls.strip_each(line.split(" ", 2))
+ output = {
+ "name": name,
+ "connected": cls.CONNECTED_MAPPING.get(
+ connected, "unknown connection"
+ ),
+ }
+ if output["connected"] is False:
+ return output
+ resolution = cls.RESOLUTION_PATTERN.search(rest)
+ output["active"] = bool(resolution)
+ if resolution is None:
+ return output
+ parsed = cls.int_tuple(resolution.group(i) for i in range(1, 5))
+ direction_reflection = (
+ cls.DIRECTIONS_REFLECTIONS_PATTERN.search(rest)
+ )
+ end = direction_reflection.end()
+ output.update(
+ {
+ "size": parsed[:2],
+ "pos": parsed[2:],
+ "primary": rest.startswith("primary "),
+ "direction": direction_reflection.group(1),
+ "reflection": direction_reflection.group(2) or "none",
+ "available_rotations_reflections": [
+ m.group(0)
+ for m in cls.AVAILABLE_DIRECTIONS_REFLECTIONS_PATTERN.finditer(
+ rest[end:rest.find(")", end)]
+ )
+ ],
+ "modes": []
+ }
+ )
+ mm_size = cls.MM_SIZE_PATTERN.search(rest)
+ if mm_size:
+ output["mm_size"] = (
+ int(mm_size.group(1)), int(mm_size.group(2))
+ )
+ return output
+
+ @classmethod
+ def parse_mode(cls, line, width_line, height_line):
+ if line.endswith(" +preferred"):
+ line = line[:-11]
+ preferred = True
+ else:
+ preferred = False
+ if line.endswith(" *current"):
+ line = line[:-9]
+ current = True
+ else:
+ current = False
+ match = cls.MODE_PATTERN.match(line)
+ return {
+ "name": match.group(1),
+ "width": int(cls.WIDTH_HEIGHT_PATTERN.match(width_line).group(2)),
+ "height": int(cls.WIDTH_HEIGHT_PATTERN.match(height_line).group(2)),
+ "mode_id": int(match.group(2), 16),
+ "refresh_rate": float(match.group(3)),
+ "preferred": preferred,
+ "current": current,
+ "mode_flags": cls.MODE_FLAGS_PATTERN.findall(line),
+ }
+
+ @classmethod
+ def get_conf(cls):
+ screens = []
+ current_screen = None
+ output = subprocess.check_output(["xrandr", "--verbose"], text=True)
+ lines = output.split(os.linesep)
+ for i, line in enumerate(lines):
+ if not line:
+ continue
+ elif line.startswith("Screen "):
+ current_screen = len(screens)
+ screens.append(cls.parse_screen_line(line))
+ elif not line.startswith("\t") and not line.startswith(" "):
+ screens[current_screen]["outputs"].append(cls.parse_output_line(line))
+ elif line.startswith(" "):
+ output = screens[current_screen]["outputs"][-1]
+ if line[3] == " " or "modes" not in output:
+ continue
+ output["modes"].append(cls.parse_mode(*lines[i:i + 3]))
+ #print(json.dumps(screens, indent=4))
+ return screens
+
+ @classmethod
+ def call(cls, output, *args, **kwargs):
+ args = [f"--{arg}" for arg in args]
+ for key, value in kwargs.items():
+ args.extend((f"--{key.replace("_", "-")}", value))
+ return subprocess.run(["xrandr", "--output", cls.OUTPUTS[output], *args])
+
+ def update(self, mode):
+ if mode not in ("single", "double", "vertical"):
+ raise ValueError(f"unknown mode: {mode}")
+ self.call(1, "off", "noprimary")
+ if mode in ("single", "double"):
+ self.call(0, "auto", "primary", rotate="normal")
+ if mode == "double":
+ self.call(1, "auto", rotate="normal", below=self.OUTPUTS[0])
+ if mode == "vertical":
+ self.call(0, "auto", rotate="left")
+ self.call(1, "auto", rotate="left", left_of=self.OUTPUTS[0])
+ self.current_conf = self.get_conf()
+
+ def get_relevant_outputs(self):
+ screen = next(s for s in self.current_conf if s["screen"] == 0)
+ outputs = [None, None]
+ to_finds = list(self.OUTPUTS)
+ for output in screen["outputs"]:
+ for i, to_find in enumerate(to_finds):
+ if output["name"] == to_find:
+ outputs[self.OUTPUTS.index(to_find)] = output
+ to_finds.remove(to_find)
+ if not to_finds:
+ return outputs
+ raise ValueError(f"Outputs not found: {', '.join(self.OUTPUTS)}")
+
+ def is_active(self, mode):
+ outputs = self.get_relevant_outputs()
+ if mode == "vertical":
+ expected_direction = "left"
+ else:
+ expected_direction = "normal"
+ second_active = mode != "single"
+ return (
+ outputs[0]["active"]
+ and outputs[0]["direction"] == expected_direction
+ and outputs[1]["active"] == second_active
+ and (not second_active or outputs[1]["direction"] == expected_direction)
+ )
--- /dev/null
+#!/usr/bin/env python
+
+import pygame
+
+from bluetooth import BluetoothConf
+from vectors import laptop_double, laptop_single, laptop_vertical
+from xinput import XinputConf
+from xrandr import XrandrConf
+
+# - slider button with "undecided" state
+# - slider button for bluetooth
+# - that springs on for the 2-screen modes, because the keyboard needs it
+# - slider button for touchscreen (undecided if disabled on second screen)
+# - extra button to calibrate
+# - slider button for stylus (undecided if disabled on second screen)
+# - extra button to calibrate
+# - recognize current setting for touchscreen and stylus
+# - ability to display messages
+# - below the three screen buttons
+# - next to the slider button for touchscreen
+# - next to the slider button for stylus
+
+
+class ZenbookConf:
+ SHAPES = (
+ {
+ "name": "single",
+ "shape": laptop_single,
+ "rect": pygame.Rect((68, 68), (416, 416)),
+ },
+ {
+ "name": "double",
+ "shape": laptop_double,
+ "rect": pygame.Rect((568, 68), (416, 416)),
+ },
+ {
+ "name": "vertical",
+ "shape": laptop_vertical,
+ "rect": pygame.Rect((1068, 68), (416, 416)),
+ },
+ )
+
+ def __init__(self):
+ pygame.init()
+ self.surf = pygame.display.set_mode((1536, 768))
+ self.font = pygame.font.Font(None, size=64)
+ self.clock = pygame.time.Clock()
+ self.running = True
+ self.dirty = False
+ self.xrandr_conf = XrandrConf()
+ self.xinput_conf = XinputConf(self.xrandr_conf)
+ self.bluetooth_conf = BluetoothConf()
+ self.current_fps = -1
+
+ def handle_event(self, ev):
+ if ev.type == pygame.QUIT:
+ self.running = False
+ return False
+ elif ev.type == pygame.WINDOWEXPOSED:
+ self.dirty = True
+ elif ev.type == pygame.KEYDOWN:
+ if ev.key == pygame.K_ESCAPE:
+ self.running = False
+ return False
+ elif ev.type == pygame.MOUSEBUTTONDOWN:
+ for item in self.SHAPES:
+ if item["rect"].collidepoint(ev.pos):
+ self.xrandr_conf.update(item["name"])
+ self.xinput_conf.update(item["name"])
+ if item["name"] != "single":
+ self.bluetooth_conf.update(True)
+ self.dirty = True
+ return True
+
+ def update(self):
+ if self.clock.get_fps() != self.current_fps:
+ self.current_fps = int(self.clock.get_fps())
+ self.dirty = True
+
+ def draw(self):
+ self.surf.fill(0x333333)
+ for item in self.SHAPES:
+ if self.xrandr_conf.is_active(item["name"]):
+ pygame.draw.rect(self.surf, "lime", item["rect"], 8)
+ else:
+ pygame.draw.rect(self.surf, "gray", item["rect"], 8)
+ item["shape"].draw(
+ self.surf,
+ (item["rect"].left + 32, item["rect"].top + 32),
+ (16, 16),
+ "white",
+ )
+
+ def run(self):
+ while True:
+ for ev in pygame.event.get():
+ if not self.handle_event(ev):
+ break
+ if not self.running:
+ break
+ self.update()
+ if self.dirty:
+ self.draw()
+ pygame.display.update()
+ self.dirty = False
+ self.clock.tick(60)
+
+
+if __name__ == "__main__":
+ ZenbookConf().run()