]> git.mar77i.info Git - zenbook_gui/commitdiff
clean up launcher. rename focused to is_focused. and then some...
authormar77i <mar77i@protonmail.ch>
Mon, 1 Sep 2025 09:13:46 +0000 (11:13 +0200)
committermar77i <mar77i@protonmail.ch>
Mon, 1 Sep 2025 09:14:00 +0000 (11:14 +0200)
16 files changed:
bookpaint/bookpaint.py
bookpaint/menu.py
connect_four.py
launch.py
memory.py
requirements.txt [new file with mode: 0644]
rps/rps.py
ui/__init__.py
ui/child.py
ui/focus.py
ui/multitouch.py [new file with mode: 0755]
ui/root.py
ui/spinner.py
ui/text_input.py
vs_memory.py
zenbook_conf/zenbook_conf.py

index 1912efd569311d4cadf92d07cff96496f044becf..2a4c169fabc4ef9ee87f9f0505007a0f4874746c 100644 (file)
@@ -1,9 +1,8 @@
 from functools import partial
 from pathlib import Path
 
-import pygame
-
-from ui import Root
+from launch import pygame
+from ui import BaseRoot
 
 from .book_manager import BookManager
 from .color_menu import ColorMenu
@@ -13,7 +12,7 @@ from .menu import Menu
 from .page_menu import PageMenu
 
 
-class BookPaint(Root):
+class Root(BaseRoot):
     BACKGROUND_COLOR = "black"
 
     def setup_deactivate_keys(self):
@@ -23,7 +22,6 @@ class BookPaint(Root):
                 menu.key_methods[frozenset()][key] = menu.deactivate.__func__
 
     def __init__(self):
-        pygame.init()
         super().__init__(
             pygame.display.set_mode((0, 0), pygame.FULLSCREEN),
             pygame.font.Font(None, size=96),
index f471025c85cba73e6e50b686513a29119c3c3c6b..592a41462c501dad8c224fe978090d4372e452f4 100644 (file)
@@ -7,9 +7,9 @@ from ui import Button, Modal
 
 class Menu(Modal):
     def __init__(self, parent):
-        from .bookpaint import BookPaint
+        from .bookpaint import Root as BookPaintRoot
 
-        self.parent: BookPaint
+        self.parent: BookPaintRoot
         super().__init__(parent)
         self.suspended = False
         size = self.surf.get_size()
index 92f4a299cb0102bee4a95870473946a95bf1a82d..7b2f426d0df64d51e0c15912306d91ba327fbb9c 100755 (executable)
@@ -4,8 +4,8 @@ from functools import partial
 from math import pi, sqrt
 from time import time
 
-from launch import run, pygame
-from ui import Button, MessageBox, Rect, Root
+from launch import pygame
+from ui import BaseRoot, Button, MessageBox, Rect
 from vectors import StrokeCircleSegment
 
 
@@ -45,7 +45,7 @@ class ColorButton(Button):
         self.draw_value(colors[2])
 
 
-class ConnectFour(Root):
+class Root(BaseRoot):
     FIELD_SIZE = (7, 6)
     FRAME_COLOR = "blue"
     PLAYER_COLORS = ("red", "green")
@@ -54,7 +54,6 @@ class ConnectFour(Root):
     PULL_TIMEOUT = 1
 
     def __init__(self):
-        pygame.init()
         super().__init__(
             pygame.display.set_mode((0, 0), pygame.FULLSCREEN, display=0),
             pygame.font.Font(None, size=96),
@@ -284,4 +283,4 @@ class ConnectFour(Root):
 
 
 if __name__ == "__main__":
-    run()
+    Root().run()
index 01a7c8e9c56b592330fa043aa5b69458a747a050..7156e309fa41e2985142bf92c5e319b37fae2ea4 100755 (executable)
--- a/launch.py
+++ b/launch.py
 #!/usr/bin/env python3
 
 import os
-import shlex
 import sys
 from argparse import ArgumentParser
-from contextlib import redirect_stdout
+from contextlib import contextmanager, redirect_stdout
+from http.server import BaseHTTPRequestHandler, HTTPServer
 from importlib import import_module
-from io import StringIO
 from pathlib import Path
-from shutil import rmtree
+from queue import SimpleQueue
+from shutil import which
+from subprocess import PIPE, run
+from threading import Thread
 from time import time
+from traceback import format_exception
 
+executable = Path(sys.executable).name
 
-def setup_venv():
-    venv_dir = (Path(__file__).parent / "venv").absolute()
-    install_venv = True
-    if venv_dir.is_dir():
-        for p in (
-            venv_dir,
-            *(
-                base / ent
-                for base, dirs, files in venv_dir.walk()
-                for ent in (*dirs, *files)
-            )
-        ):
-            if p.stat().st_mtime >= time() - 86400:
-                install_venv = False
-                break
+
+def tty_connected():
+    """
+    Check if there is a terminal attached to this process.
+
+    No need to mimic ps' major and minor device number decoding.
+    """
+    with open("/proc/self/stat", "rb") as fh:
+        return int(next(fh).split(b") ")[1].split(b" ", 5)[4]) != 0
+
+
+class MessageRequestHandler(BaseHTTPRequestHandler):
+    queue = SimpleQueue()
+    msg_bytes: bytes
+
+    def do_GET(self):
+        self.send_response(200)
+        self.send_header("Content-Length", str(len(self.msg_bytes)))
+        self.send_header("Content-Type", "text/plain")
+        self.end_headers()
+        self.wfile.write(self.msg_bytes)
+        thread = Thread(target=self.server.shutdown)
+        thread.start()
+        self.queue.put(thread)
+
+    def log_message(self, *args):
+        pass
+
+
+@contextmanager
+def exception_wrapper():
+    try:
+        yield
+    except Exception as exc:
+        if tty_connected() or not which("xdg-open"):
+            raise
+        MessageRequestHandler.msg_bytes = "".join(format_exception(exc)).encode()
     else:
-        if venv_dir.exists():
-            rmtree(venv_dir)
-        assert not venv_dir.exists()
-        os.system(shlex.join((sys.executable, "-m", "venv", str(venv_dir))))
+        return
+    s = HTTPServer(("127.0.0.1", 0), MessageRequestHandler)
+    run(("xdg-open", f"http://{':'.join(str(s) for s in s.socket.getsockname())}/"))
+    s.serve_forever()
+    MessageRequestHandler.queue.get().join()
+
+
+def run_cmd(cmd, expect_returncode=0):
+    cp = run(cmd, stdout=PIPE, stderr=PIPE)
+    if cp.returncode == expect_returncode:
+        return cp
+    lines = [f"Subprocess {cmd} failed with status {cp.returncode}"]
+    for attr in ("stdout", "stderr"):
+        output = getattr(cp, attr)
+        if output:
+            lines.extend(("", f"{attr}:", output.decode(errors="backslashreplace")))
+    raise ChildProcessError(os.linesep.join(lines))
+
+
+def check_has_pygame():
+    return b"No module named pygame.__main__;" in run_cmd(
+        (executable, "-m", "pygame"), 1
+    ).stderr
+
+
+def get_ipv4_is_connected():
+    """Read the default gateway directly from /proc."""
+    with open("/proc/net/route") as fh:
+        for line in fh:
+            fields = line.rstrip().split("\t")
+            if fields[1] == '00000000' and int(fields[3], 16) & 2:
+                return True
+    return False
+
+
+def walk_outer(path):
+    return (
+        path,
+        *(
+            base / ent
+            for base, dirs, files in path.walk()
+            for ent in (*dirs, *files)
+        )
+    )
+
+
+def get_venv_environ(venv_dir):
     new_env = {"VIRTUAL_ENV": str(venv_dir)}
     venv_bin_str = str(venv_dir / "bin")
     if venv_bin_str not in os.environ['PATH'].split(os.pathsep):
         new_env["PATH"] = f"{venv_bin_str}{os.pathsep}{os.environ['PATH']}"
-    os.environ.update(new_env)
-    if install_venv:
-        os.system(
-            shlex.join(
-                (
-                    Path(sys.executable).name,
-                    "-m",
-                    "pip",
-                    "install",
-                    "-qU",
-                    "pip",
-                    "pygame",
-                )
+    return new_env
+
+
+def setup_venv():
+    """
+    import pygame failed, but this could just be the missing
+    VIRTUAL_ENV environment varirable.
+    Load the venv and check again.
+    """
+    venv_dir = (Path(__file__).parent / "venv").absolute()
+    venv_dir_is_dir = venv_dir.is_dir()
+    if not venv_dir_is_dir:
+        if venv_dir.exists():
+            raise SystemError("'venv' is not a directory.")
+        run_cmd((sys.executable, "-m", "venv", str(venv_dir)))
+    os.environ.update(get_venv_environ(venv_dir))
+    has_pygame = venv_dir_is_dir and check_has_pygame()
+    if get_ipv4_is_connected():
+        yesterday = time() - 86400
+        if not has_pygame or all(
+            p.stat().st_mtime < yesterday for p in walk_outer(venv_dir)
+        ):
+            run_cmd((executable, "-m", "pip", "install", "-qU", "pip", "pygame"))
+            venv_dir.touch(0o755, True)
+    elif not has_pygame:
+        raise ConnectionError("Internet needed to install requirement: pygame")
+
+
+def pre_run():
+    pg = "pygame"
+    if pg in sys.modules:
+        return sys.modules[pg]
+    try:
+        with redirect_stdout(None):
+            pygame = import_module(pg)
+    except ImportError:
+        with exception_wrapper():
+            setup_venv()
+            os.execl(
+                sys.executable,
+                executable,
+                *sys.orig_argv[sys.orig_argv[0] == executable:],
             )
-        )
-        venv_dir.touch(0o755, True)
-    os.execl(sys.executable, Path(sys.executable).name, *sys.argv)
-    exit(1)
+        exit(1)
+    pygame.init()
+    return pygame
+
 
+def resolve_symlink(path):
+    path = path.absolute()
+    if not path.is_symlink():
+        return path
+    dest = path.readlink()
+    if dest.is_absolute():
+        return dest
+    return path.parent / dest
 
-def find_root(module_name, module_globals):
-    from ui import Root
 
-    for key, value in module_globals.items():
-        if key.startswith("__") and key.endswith("__"):
-            continue
-        if value is not Root and isinstance(value, type) and issubclass(value, Root):
-            value().run()
-            break
+def find_module(module_name):
+    file_path = resolve_symlink(Path(__file__).absolute())
+    module_name_py = f"{module_name}.py"
+    dest = file_path.parent / module_name_py
+    if dest.exists():
+        if resolve_symlink(dest) != file_path:
+            return module_name
+    assert (file_path.parent / module_name / module_name_py).exists()
+    return ".".join((module_name, module_name))
+
+
+def try_run(module_name):
+    module = import_module(module_name)
+    if hasattr(module, "Root"):
+        module.Root().run()
     else:
-        raise ValueError(f"Module not found: {module_name}")
-    return
+        module.main()
 
 
 def main():
+    ap = ArgumentParser()
+    ap.add_argument("--module", "-m")
+    module_name = find_module(Path(ap.parse_args().module or sys.argv[0]).stem)
     try:
-        with redirect_stdout(StringIO()):
-            # ruff: noqa: F401
-            import pygame  # type: ignore
-    except ImportError:
-        setup_venv()
+        try_run(module_name)
+    except (ImportError, AttributeError):
         raise
+        try_run(".".join((module_name, module_name)))
 
-    ap = ArgumentParser()
-    ap.add_argument("--module", "-m")
-    args = ap.parse_args()
-    module_name = Path(args.module or sys.argv[0]).stem
-    find_root(
-        module_name,
-        import_module(f"{module_name}.{module_name}", module_name).__dict__,
-    )
 
+pygame = pre_run()
 
 if __name__ == "__main__":
-    main()
\ No newline at end of file
+    main()
index 129c98fa8f128ea012c507cf8a37963a604fb8dd..1d5ba179478a327ddb63f6d90bccd29fce86ed6f 100755 (executable)
--- a/memory.py
+++ b/memory.py
@@ -4,8 +4,8 @@ from functools import partial
 from pathlib import Path
 from secrets import choice
 
-from launch import run, pygame
-from ui import Button, Child, Label, Modal, Root, TextInput
+from launch import pygame
+from ui import BaseRoot, Button, Child, Label, Modal, TextInput
 
 
 class QuittableModal(Modal):
@@ -196,7 +196,7 @@ class MemoryCard(Child):
             self.parent.check_turn()
 
 
-class MemoryGame(Root):
+class Root(BaseRoot):
     BACKGROUND_COLOR = "gray34"
 
     def __init__(self):
@@ -304,4 +304,4 @@ class MemoryGame(Root):
 
 
 if __name__ == "__main__":
-    run()
+    Root().run()
diff --git a/requirements.txt b/requirements.txt
new file mode 100644 (file)
index 0000000..0f13c09
--- /dev/null
@@ -0,0 +1,2 @@
+evdev==1.9.2
+pygame==2.6.1
index 34edc100bad6f91438df8fad7588d5d8b467ec7e..90596f9525fa420f8dd84b761e28600fc864fb1d 100644 (file)
@@ -1,9 +1,8 @@
 from argparse import ArgumentParser
 from functools import partial
 
-import pygame
-
-from ui import Button, Root
+from launch import pygame
+from ui import BaseRoot, Button
 from .rps_button import RPSButton
 
 
@@ -38,7 +37,7 @@ class FingerButton(Button):
             self.dirty = True
 
 
-class RockPaperScissors(Root):
+class Root(BaseRoot):
     BACKGROUND_COLOR = "black"
     LABELS = RPSButton.LABELS
 
@@ -46,7 +45,6 @@ class RockPaperScissors(Root):
         ap = ArgumentParser()
         ap.add_argument("--display", type=int, default=0)
         args = ap.parse_args()
-        pygame.init()
         num_displays = len(pygame.display.get_desktop_sizes())
         if args.display < -num_displays or args.display >= num_displays:
             raise ValueError(f"Invalid display: {args.display}")
index 824209fbd172729242912eaf01e8b73fb2c83801..e422676577565c1eff673f8de05cbc9844cb1fcf 100644 (file)
@@ -11,7 +11,7 @@ from .message_box import MessageBox
 from .modal import Modal
 from .parent import Parent
 from .rect import Rect
-from .root import Root
+from .root import BaseRoot
 from .scroll import Scroll
 from .slider import Slider
 from .spinner import FloatSpinner, RepeatButton, Spinner
@@ -20,6 +20,7 @@ from .tab_bar import TabBar
 from .text_input import TextInput
 
 __all__ = [
+    "BaseRoot",
     "Button",
     "Child",
     "ColorButton",
@@ -35,7 +36,6 @@ __all__ = [
     "Parent",
     "Rect",
     "RepeatButton",
-    "Root",
     "Scroll",
     "Slider",
     "Spinner",
index d197021cb3a0bb635c5789d1405158e2402759bd..d0f8f0401e640e8a80557aa6ea54b4441d21d5c9 100644 (file)
@@ -1,7 +1,7 @@
 from functools import cached_property
 
 from .event_method_dispatcher import EventMethodDispatcher
-from .root import Root
+from .root import BaseRoot
 
 
 class Child(EventMethodDispatcher):
@@ -16,7 +16,7 @@ class Child(EventMethodDispatcher):
         parent = self.parent
         while hasattr(parent, "parent"):
             parent = parent.root or parent.parent
-        if not isinstance(parent, Root):
+        if not isinstance(parent, BaseRoot):
             raise AttributeError(f"No root found for {self}")
         return parent
 
index 2d3c609c0d7909eb1cba479af0b3149dafc21dec..1432a38e207e0ae81e542845535fbf0b9a3f44e1 100644 (file)
@@ -1,12 +1,12 @@
-from .root import Root
+from .root import BaseRoot
 
 
 class Focusable:
-    root: Root
+    root: BaseRoot
     dirty: bool
 
     @property
-    def focused(self):
+    def is_focused(self):
         return self.root.focus_stack[-1] is self
 
     def activate(self):
@@ -15,6 +15,6 @@ class Focusable:
         self.dirty = True
 
     def deactivate(self):
-        assert self.focused
+        assert self.is_focused
         self.root.focus_stack.pop()
         self.dirty = True
diff --git a/ui/multitouch.py b/ui/multitouch.py
new file mode 100755 (executable)
index 0000000..5336cc9
--- /dev/null
@@ -0,0 +1,86 @@
+from evdev import InputDevice, ecodes, list_devices
+
+
+class MultitouchHandler:
+    @staticmethod
+    def find_device(**kwargs):
+        for path in list_devices():
+            device = InputDevice(path)
+            for k, v in kwargs.items():
+                if getattr(device, k) != v:
+                    break
+            else:
+                return device
+        return None
+
+    @staticmethod
+    def multitouch_ranges(device):
+        return {
+            v: (info.min, info.max)
+            for v, info in (
+                (v, device.absinfo(v))
+                for v in (
+                    ecodes.ABS_X,
+                    ecodes.ABS_Y,
+                    ecodes.ABS_MT_POSITION_X,
+                    ecodes.ABS_MT_POSITION_Y,
+                )
+            )
+        }
+
+    def __init__(self, **kwargs):
+        self.device = self.find_device(**kwargs)
+        self.info = self.multitouch_ranges(self.device)
+        self.context_app = kwargs.get("context_app")
+        self.slots = [{}]
+        self.slot = 0
+
+    def current_slot(self):
+        while self.slot >= len(self.slots):
+            self.slots.append({})
+        return self.slots[self.slot]
+
+    def handle_event(self, event):
+        if event.type == ecodes.EV_SYN:
+            current_slot = self.current_slot()
+            if current_slot.get(ecodes.ABS_MT_TRACKING_ID) == -1:
+                current_slot.clear()
+        elif event.type == ecodes.EV_ABS:
+            if event.code == ecodes.ABS_MT_SLOT:
+                self.slot = event.value
+            else:
+                self.current_slot()[event.code] = event.value
+        elif event.type == ecodes.EV_MSC:
+            self.current_slot()[event.code] = event.value
+        elif event.type == ecodes.EV_KEY:
+            if event.code == ecodes.BTN_TOUCH:
+                if event.value == 0:
+                    for s in self.slots:
+                        s.clear()
+
+    def handle_events(self):
+        while (event := self.device.read_one()) is not None:
+            self.handle_event(event)
+
+    def map_coords(self, rect):
+        for slot in self.slots:
+            if ecodes.ABS_MT_POSITION_X in slot and ecodes.ABS_MT_POSITION_Y in slot:
+                infos = (
+                    self.info[ecodes.ABS_MT_POSITION_X],
+                    self.info[ecodes.ABS_MT_POSITION_Y],
+                )
+                pos = (slot[ecodes.ABS_MT_POSITION_X], slot[ecodes.ABS_MT_POSITION_Y])
+            elif ecodes.ABS_X in slot and ecodes.ABS_Y in slot:
+                infos = (self.info[ecodes.ABS_X], self.info[ecodes.ABS_Y])
+                pos = (slot[ecodes.ABS_X], slot[ecodes.ABS_Y])
+            else:
+                continue
+            yield tuple(
+                offset + (value - info[0]) * scale / (info[1] - info[0])
+                for value, info, offset, scale in zip(
+                    pos,
+                    infos,
+                    rect.topleft,
+                    rect.size,
+                )
+            ), slot
index d561d84e2f657f303c04e1ec4f543a2c6bf06de5..37264825dcb73b5518bf4823cad8ebd9ffeff50e 100644 (file)
@@ -3,15 +3,22 @@ import pygame
 from .parent import Parent
 
 
-class Root(Parent):
+class BaseRoot(Parent):
     BACKGROUND_COLOR: pygame.Color
+    ACTIVATE_EVENTS = (
+        pygame.WINDOWENTER,
+        pygame.WINDOWEXPOSED,
+        pygame.WINDOWFOCUSGAINED,
+        pygame.WINDOWMAXIMIZED,
+    )
+    FPS = 120
 
     def __init__(self, surf, font=None):
         super().__init__()
+        self.surf = surf
         self.font = font
         self.running = True
-        self.dirty = False
-        self.surf = surf
+        self.dirty = True
         self.clock = pygame.time.Clock()
         self.stop_event = False
         self.root = self
@@ -23,11 +30,11 @@ class Root(Parent):
     key_methods = {frozenset(): {pygame.K_ESCAPE: handle_quit}}
 
     @property
-    def focused(self):
+    def is_focused(self):
         return self.focus_stack[-1] is self
 
     def handle_event(self, ev):
-        if ev.type in (pygame.WINDOWEXPOSED, pygame.ACTIVEEVENT):
+        if ev.type in self.ACTIVATE_EVENTS:
             self.dirty = True
             return
         focused = self.focus_stack[-1]
@@ -50,12 +57,10 @@ class Root(Parent):
                 self.stop_event = False
                 self.handle_event(ev)
                 if not self.running:
-                    break
-            if not self.running:
-                break
+                    return
             self.update()
             if self.dirty:
                 self.draw()
                 pygame.display.update()
                 self.dirty = False
-            self.clock.tick(60)
+            self.clock.tick(self.FPS)
index 726b254a45cce8284223310b4e7f676cea62d073..962513bfcf5460d57e2e5ab78a6545abbb6e5b39 100644 (file)
@@ -155,7 +155,7 @@ class FloatSpinner(Spinner):
             return
         if float_value == self.value:
             return
-        self.value = float_value
+        self._value = float_value
         str_value = str(float_value)
         if str_value != self.text_input.value:
             self.text_input.value = str_value
index 325ca1e0d440613865a7112f5efcc9b79aa733bc..60d0ff7fbbb39b2955c187ee18c71e7572159fc6 100644 (file)
@@ -156,7 +156,7 @@ class TextInput(Focusable, Child):
 
     def draw(self):
         pygame.draw.rect(self.surf, "black", self.rect)
-        if self.focused:
+        if self.is_focused:
             fs = self.get_focused_font_surface()
         else:
             fs = self.get_unfocused_font_surface()
@@ -196,7 +196,7 @@ class TextInput(Focusable, Child):
                 self.cursor.pos = self.pos_from_offset(
                     ev.pos[0] - self.padded_rect.left + self.offset
                 )
-        elif self.focused:
+        elif self.is_focused:
             self.deactivate(True)
 
     @contextmanager
@@ -257,13 +257,13 @@ class TextInput(Focusable, Child):
             self.cursor.pos = pos
 
     def activate(self):
-        if self.focused:
+        if self.is_focused:
             return
         super().activate()
         self.cursor = Cursor(self.value)
 
     def deactivate(self, restore=False):
-        if not self.focused:
+        if not self.is_focused:
             return
         if restore:
             self.value = self.cursor.old_value
@@ -291,7 +291,7 @@ class TextInput(Focusable, Child):
     }
 
     def handle_keydown(self, ev):
-        if not self.focused:
+        if not self.is_focused:
             return
         if (key_method := self.get_key_method(ev.key, ev.mod)) is not None:
             key_callback = key_method
@@ -301,7 +301,7 @@ class TextInput(Focusable, Child):
             return
         with self.check_dirty():
             key_callback()
-        if self.focused:
+        if self.is_focused:
             self.cursor.press_key(key_callback, ev.key)
 
     def handle_keyup(self, ev):
index 8f88a100e653819dbcdacec3be400c70606904d6..bcb5c66e1b1855a1fd2d6bba2e58d3fdffc05719 100755 (executable)
@@ -4,8 +4,8 @@ from pathlib import Path
 from secrets import choice, randbelow
 from time import time
 
-from launch import run, pygame
-from ui import Button, Child, FloatSpinner, Label, Root, Spinner
+from launch import pygame
+from ui import BaseRoot, Button, Child, FloatSpinner, Label, Spinner
 
 
 class Team:
@@ -76,7 +76,7 @@ class MemoryCard(Child):
         self.surf.blit(self.team.logo, self.rect.topleft)
 
 
-class VSMemory(Root):
+class Root(BaseRoot):
     BACKGROUND_COLOR = "black"
     TOP = 186
     LEFT = 256
@@ -98,7 +98,6 @@ class VSMemory(Root):
         return f"Score: {self.failed} failed, {self.successful} successful"
 
     def __init__(self):
-        pygame.init()
         super().__init__(
             pygame.display.set_mode((0, 0), pygame.FULLSCREEN),
             pygame.font.Font(None, size=96),
@@ -106,11 +105,11 @@ class VSMemory(Root):
         self.teams = tuple(
             Team(file)
             for file in (Path(__file__).parent / "logos").iterdir()
-            if file.stem != "National League"
+            if file.stem not in ("National League", "Challenge League")
         )
         len_teams = len(self.teams)
         # fits one column of memory destinations (todo)
-        assert len_teams < 15 and len_teams % 2 == 0
+        #assert len_teams < 15 and len_teams % 2 == 0, len_teams
         self.num_pairs = len_teams // 2
         self.failed = 0
         self.successful = 0
@@ -262,4 +261,4 @@ class VSMemory(Root):
 
 
 if __name__ == "__main__":
-    run()
+    Root().run()
index 4d776f3673627248e1332dc45a1fb60ba87ee38d..16171ec00d30ecd8e1d8a255ef834a59199477a8 100644 (file)
@@ -1,9 +1,9 @@
 from functools import partial
 
-import pygame
+from launch import pygame
+from ui import BaseRoot, Button, FPSWidget, Icon, IconButton, Switch
 
 from .bluetooth import BluetoothConf
-from ui import Button, FPSWidget, Icon, IconButton, Root, Switch
 from .shapes import (
     bluetooth,
     laptop_double,
@@ -16,11 +16,10 @@ from .xinput import XinputConf
 from .xrandr import XrandrConf
 
 
-class ZenbookConf(Root):
+class Root(BaseRoot):
     BACKGROUND_COLOR = 0x333333
 
     def __init__(self):
-        pygame.init()
         pygame.display.set_icon(self.get_icon())
         pygame.display.set_caption("Zenbook Config")
         window_size = (1536, 1200)