From 0618d6d325e9fc3d76d685a24d71c18b10feb390 Mon Sep 17 00:00:00 2001 From: mar77i Date: Wed, 17 Sep 2025 22:15:38 +0200 Subject: [PATCH] make that keyboard pretty great --- keyboard/clock.py | 126 +++++++++++++++++++++++++ keyboard/draggable.py | 98 +++++++++++++++++++ keyboard/key.py | 4 +- keyboard/keyboard.py | 93 +++++++++++++----- keyboard/layout.py | 212 ++++++++++++++++++++++++++++++++++++++++++ keyboard/mousepad.py | 83 +++++++++++++++++ keyboard/settings.py | 27 +++--- launch.py | 59 ++++++------ memory.py | 9 +- ui/__init__.py | 3 +- ui/fps_widget.py | 2 +- ui/modal.py | 7 ++ 12 files changed, 646 insertions(+), 77 deletions(-) create mode 100644 keyboard/clock.py create mode 100644 keyboard/draggable.py create mode 100644 keyboard/layout.py create mode 100644 keyboard/mousepad.py diff --git a/keyboard/clock.py b/keyboard/clock.py new file mode 100644 index 0000000..31c6a2b --- /dev/null +++ b/keyboard/clock.py @@ -0,0 +1,126 @@ +from datetime import datetime +from math import cos, sin, tau + +import pygame + +from ui import Child + + +class ClockWidget(Child): + CLOCK_RADIUS = 3 / 8 + MARKINGS = (3 / 4, 15 / 16) + HANDS = ( + (-1 / 16, 3 / 16), # fullday + (-1 / 16, 7 / 16), # hour + (-1 / 16, 3 / 4), # minute + (-1 / 8, 13 / 16), # second + ) + WIDTHS = ( + 0.0325, # fullday + 0.02625, # hour + 0.0175, # minute + 0.005, # second + ) + + def __init__(self, parent, rect): + super().__init__(parent) + self.rect = rect + min_rect_size = min(rect.size) + self.radius = min_rect_size * self.CLOCK_RADIUS + self.widths = tuple(w * min_rect_size for w in self.WIDTHS) + self.state = (0, 0, 0, 0, 0, 0) + + def update(self): + now = datetime.now() + state = ( + now.strftime("%b"), + now.day, + now.hour, + now.minute, + now.second, + now.microsecond, + ) + if state != self.state: + self.state = state + self.dirty = True + + def draw(self): + month, day, hour, minute, second, microsecond = self.state + second = second + microsecond / 1000000 + fractional_minute = minute + second / 60 + fractional_hour = hour + fractional_minute / 60 + self.draw_markings() + self.draw_date(month, "gold") + self.draw_date(str(day), "gold", True) + self.draw_hand_polygon( + fractional_hour * tau / 24, self.HANDS[0], self.widths[0], "purple" + ) + if fractional_hour >= 13: + fractional_hour -= 12 + self.draw_hand_polygon( + fractional_hour * tau / 12, self.HANDS[1], self.widths[1], "lightgreen" + ) + self.draw_hand_polygon( + fractional_minute * tau / 60, self.HANDS[2], self.widths[2], "darkcyan" + ) + self.draw_hand_polygon(second * tau / 60, self.HANDS[3], self.widths[3], "blue") + + def draw_markings(self): + center = self.rect.center + radius = self.radius + markings = tuple(m * radius for m in self.MARKINGS) + for i in range(12): + angle = tau * i / 12 + fractions = cos(angle), -sin(angle) + pygame.draw.line( + self.surf, + "green", + tuple(c + (f * markings[0]) for c, f in zip(center, fractions)), + tuple(c + (f * radius) for c, f in zip(center, fractions)), + ) + for j in range(i * 5 + 1, (i + 1) * 5): + angle = tau * j / 60 + fractions = cos(angle), -sin(angle) + pygame.draw.line( + self.surf, + "yellow", + tuple(c + (f * markings[1]) for c, f in zip(center, fractions)), + tuple(c + (f * radius) for c, f in zip(center, fractions)), + ) + pygame.draw.circle(self.surf, "green", center, radius, 1) + + def draw_date(self, string, color, right=False): + fs = self.font.render(string, True, color) + if right: + x_offset = self.MARKINGS[0] * self.radius - fs.get_width() + else: + x_offset = -self.MARKINGS[0] * self.radius + self.surf.blit( + fs, + ( + self.rect.centerx + x_offset, + self.rect.centery - fs.get_height() / 2, + ), + ) + + def draw_hand_polygon(self, angle, interval, width, color): + # fix origin and direction + angle = tau / 4 - angle + center = self.rect.center + radius = self.radius + fractions = cos(angle), -sin(angle) + points = ( + tuple(c + (f * interval[0] * radius) for c, f in zip(center, fractions)), + tuple(c + (f * interval[1] * radius) for c, f in zip(center, fractions)), + ) + offset = (cos(angle + tau / 4) * width / 2, -sin(angle + tau / 4) * width / 2) + pygame.draw.polygon( + self.surf, + color, + [ + (points[0][0] - offset[0], points[0][1] - offset[1]), + (points[0][0] + offset[0], points[0][1] + offset[1]), + (points[1][0] + offset[0], points[1][1] + offset[1]), + (points[1][0] - offset[0], points[1][1] - offset[1]), + ] + ) diff --git a/keyboard/draggable.py b/keyboard/draggable.py new file mode 100644 index 0000000..462aec5 --- /dev/null +++ b/keyboard/draggable.py @@ -0,0 +1,98 @@ +import pygame + +from ui import Child +from ui.multitouch import MultitouchHandler + + +class DraggableButton(Child): + def __init__(self, parent, rect, surf, dests, callback, highlight=False): + super().__init__(parent) + assert rect.size == surf.get_size() + self.rect = rect + self.surface = surf + self.dests = dests + self.callback = callback + self.highlight = highlight + self.pushed = None + self.drag_pos = None + self.drag_rel = None + + def draw(self): + if not self.pushed: + value_color = "lime" if self.highlight else "gray" + colors = ("black", value_color, value_color) + else: + colors = ("darkgray", "lightgray", "black") + pygame.draw.rect(self.surf, colors[0], self.rect) + pygame.draw.rect(self.surf, colors[1], self.rect, 8) + self.surf.blit(self.surface, self.rect.topleft) + if self.drag_pos is not None: + for dest in self.dests: + width = 6 + if dest.collidepoint(self.drag_pos): + dest = dest.inflate((128, 128)) + width *= 2 + pygame.draw.rect(self.surf, "red", dest, width) + self.surf.blit( + self.surface, + ( + self.drag_pos[0] - self.drag_rel[0], + self.drag_pos[1] - self.drag_rel[1], + ), + ) + + def toggle(self, pos=None, pushed=None): + if pos is None: + self.drag_pos = None + self.drag_rel = None + else: + if self.drag_rel is None: + self.drag_rel = (pos[0] - self.rect.left, pos[1] - self.rect.top) + self.drag_pos = pos + self.pushed = pushed + self.dirty = True + + def handle_mousebuttondown(self, ev): + if ev.button == 1 and self.rect.collidepoint(ev.pos): + self.toggle(ev.pos, True) + + def handle_mousemotion(self, ev): + if self.pushed is not True: + return + if not ev.buttons[0]: + self.toggle() + else: + self.drag_pos = ev.pos + + def handle_mousebuttonup(self, ev): + if ev.button != 1 or self.pushed is not True: + return + self.drop(ev.pos) + self.toggle() + + def drop(self, pos): + for i, dest in enumerate(self.dests): + if dest.collidepoint(pos): + self.rect, self.dests[i] = dest, self.rect + self.callback() + break + + def handle_fingerdown(self, event): + pos = MultitouchHandler.map_coords((event.x, event.y), self.surf.get_size()) + if self.pushed is None and self.rect.collidepoint(pos): + self.toggle(pos, (event.touch_id, event.finger_id)) + + def handle_fingermotion(self, event): + if self.pushed != (event.touch_id, event.finger_id): + return + self.drag_pos = MultitouchHandler.map_coords( + (event.x, event.y), self.surf.get_size() + ) + + def handle_fingerup(self, event): + if self.pushed != (event.touch_id, event.finger_id): + return + self.drop( + MultitouchHandler.map_coords((event.x, event.y), self.surf.get_size()) + ) + self.toggle() diff --git a/keyboard/key.py b/keyboard/key.py index 1e00c12..6a63df8 100644 --- a/keyboard/key.py +++ b/keyboard/key.py @@ -177,7 +177,7 @@ class KeyButton(Button): self.press() def handle_mousemotion(self, event): - if event.buttons[0] and not self.rect.collidepoint(event.pos): + if not event.buttons[0] or not self.rect.collidepoint(event.pos): self.release() def handle_mousebuttonup(self, event): @@ -220,7 +220,7 @@ def get_key_size(size, x_sums_iter, x_sums_len): return (size[0] / x_sum, size[1] / x_sums_len) -def get_keyboard_keys(rect, parent, callback, keys): +def get_keyboard_keys(parent, rect, callback, keys): result = [] x_sums = [0] y_spans = [] diff --git a/keyboard/keyboard.py b/keyboard/keyboard.py index 74af025..1ca36f3 100644 --- a/keyboard/keyboard.py +++ b/keyboard/keyboard.py @@ -1,14 +1,16 @@ #!/usr/bin/env python3 from pynput.keyboard import Controller as KeyboardController, Key, KeyCode -from pynput._util.xorg_keysyms import KEYPAD_KEYS +from pynput.mouse import Controller as MouseController, Button from launch import pygame from multitouch import MultitouchHandler from ui import BaseRoot, FPSWidget -from .key import CURSOR_KEYS, KEYBOARD, KEYPAD_KEYS, get_keyboard_keys -from .settings import Settings +from .clock import ClockWidget +from .key import KEYBOARD, get_keyboard_keys +from .mousepad import MousePadWidget +from .settings import SettingsModal # add a launcher symbol @@ -21,44 +23,85 @@ class Root(BaseRoot): pygame.display.set_mode((0, 0), pygame.NOFRAME, display=1), pygame.font.SysFont("DejaVu Sans", size=64), ) - size = self.surf.get_size() - widget_size = (size[0] // 3, size[1] // 2) - get_keyboard_keys( - pygame.Rect((0, 0), (size[0], widget_size[1])), self, self.key_cb, KEYBOARD - ) - get_keyboard_keys( - pygame.Rect((0, widget_size[1]), widget_size), - self, - self.key_cb, - CURSOR_KEYS, - ) - get_keyboard_keys( - pygame.Rect(widget_size, widget_size), self, self.key_cb, KEYPAD_KEYS - ) self.pushed = set() self.mt = MultitouchHandler( MultitouchHandler.find_device(name="ELAN9009:00 04F3:425A") ) self.keyboard = KeyboardController() + self.mouse = MouseController() + self.mouse_fingers = {} self.display = self.keyboard._display self.led_mask = self.display.get_keyboard_control().led_mask self.muted = False FPSWidget(self) - self.settings = Settings( + size = self.surf.get_size() + self.settings_modal = SettingsModal( self, pygame.Rect((size[0] // 4, size[1] // 4), (size[0] // 2, size[1] // 2)), ) + self.keys_index = len(self.children) + self.setup_widgets() + + def setup_widgets(self): + while len(self.children) > self.keys_index: + self.children.pop() + size = self.surf.get_size() + half_height = size[1] // 2 + for row in range(2): + widget_top = half_height * row + if row == self.settings_modal.layout_modal.keyboard_row: + get_keyboard_keys( + self, + pygame.Rect((widget_top, 0), (size[0], half_height)), + self.key_cb, + KEYBOARD, + ) + continue + widget_row = self.settings_modal.layout_modal.widget_row + widget_width = size[0] // (3 if len(widget_row) == 3 else 2) + x = 0 if len(widget_row) > 0 else size[0] // 4 + for keyset in widget_row: + rect = pygame.Rect((x, half_height), (widget_width, half_height)) + if keyset is MousePadWidget: + keyset(self, rect, self.click_cb, self.move_cb) + elif keyset is ClockWidget: + keyset(self, rect) + else: + get_keyboard_keys(self, rect, self.key_cb, keyset) + x += widget_width def key_cb(self, key, release=False): if key == "Settings": - self.settings.activate() + self.settings_modal.activate() if isinstance(key, (Key, KeyCode)): - (self.keyboard.press, self.keyboard.release)[release](key) - if release and key in self.pushed: - self.pushed.remove(key) - elif not release: + if release: + self.keyboard.release(key) + if key in self.pushed: + self.pushed.remove(key) + else: + self.keyboard.press(key) self.pushed.add(key) + def click_cb(self, button, *args): + if len(args) > 2: + raise TypeError(f"Too many arguments to click_callback: {args}") + elif len(args) == 2: + n, release = args + else: + n, release = None, *args + if release: + if button in self.pushed: + self.mouse.release(button) + self.pushed.remove(button) + elif n is not None: + self.mouse.click(button, n) + else: + self.mouse.press(button) + self.pushed.add(button) + + def move_cb(self, pos, scroll=False): + (self.mouse.scroll if scroll else self.mouse.move)(*pos) + def handle_events(self): self.mt.handle_events() super().handle_events() @@ -69,6 +112,8 @@ class Root(BaseRoot): if led_mask != self.led_mask: self.led_mask = led_mask self.dirty = True +# if self.mouse_frame: +# self.move_mouse() def run(self): try: @@ -76,4 +121,4 @@ class Root(BaseRoot): super().run() finally: for key in self.pushed: - self.keyboard.release(key) + (self.mouse if isinstance(key, Button) else self.keyboard).release(key) diff --git a/keyboard/layout.py b/keyboard/layout.py new file mode 100644 index 0000000..bb9aa15 --- /dev/null +++ b/keyboard/layout.py @@ -0,0 +1,212 @@ +import pygame + +from ui import Child, QuittableModal, Rect + +from .clock import ClockWidget +from .draggable import DraggableButton +from .key import apply_y_spans, get_key_size +from .mousepad import MousePadWidget +from .touchbutton import TouchButton +from .key import KEYBOARD, KEYPAD_KEYS, CURSOR_KEYS + + + + +class LayoutWidget(Child): + padding = 8 + + @classmethod + def get_keyboard_keys(cls, surf, size, keys): + result = [] + x_sums = [0] + y_spans = [] + for key in keys: + if key == "\n": + x_sums.append(0) + apply_y_spans(x_sums, y_spans) + elif len(key) > 2: + width = key[2].get("x_hint", 1) + x_sums[-1] += width + y_hint = key[2].get("y_hint", 1) + if y_hint > 1: + y_spans.append([width, y_hint - 1]) + else: + x_sums[-1] += 1 + apply_y_spans(x_sums, y_spans) + assert all(item[1] <= 0 for item in y_spans) + x, y = (0, 0) + x_sums_iter = iter(x_sums) + x_sums_len = len(x_sums) + key_size = get_key_size(size, x_sums_iter, x_sums_len) + for key in keys: + if key == "\n": + x, y = 0, y + key_size[1] + key_size = get_key_size(size, x_sums_iter, x_sums_len) + continue + key_width, key_height = key_size + if key != (None, None): + if len(key) > 2: + key_width *= key[2].get("x_hint", 1) + key_height *= key[2].get("y_hint", 1) + rect = pygame.Rect( + (x + cls.padding / 2, y + cls.padding / 2), + (key_width - cls.padding, key_height - cls.padding), + ) + pygame.draw.rect(surf, "gray", rect, 1) + pygame.draw.ellipse( + surf, + "0x333333", + pygame.Rect( + ( + rect.centerx - key_size[0] / 6, + rect.centery - key_size[1] / 4, + ), + (key_size[0] / 3, key_size[1] / 2), + ) + ) + x += key_width + return result + + def get_widget_row_rects(self, num_widgets): + if self.parent.keyboard_row == 0: + top = self.rect.centery + else: + top = self.rect.top + size = (self.rect.width / 3, self.rect.height / 2) + rects = [] + if num_widgets == 1: + rects.append( + pygame.Rect( + (self.rect.left + size[0], top), + size, + ) + ) + elif num_widgets == 2: + rects.append( + pygame.Rect( + (self.rect.left + self.rect.width / 4 - size[0] / 2, top), + size, + ) + ) + rects.append( + pygame.Rect( + (self.rect.left + self.rect.width * 3 / 4 - size[0] / 2, top), + size, + ) + ) + elif num_widgets == 3: + for i in range(3): + rects.append( + pygame.Rect( + (self.rect.left + i * size[0], top), + size, + ) + ) + return rects + + def get_widget_row_rect_dests(self, len_widget_row, keyset, source_rect): + if keyset in self.parent.widget_row: + keypad_dests = self.get_widget_row_rects(len_widget_row) + keypad_idx = self.parent.widget_row.index(KEYPAD_KEYS) + keypad_dests[keypad_idx], source_rect = source_rect, keypad_dests[keypad_idx] + elif len_widget_row < 3: + keypad_dests = self.get_widget_row_rects(len_widget_row + 1) + else: + keypad_dests = [] + return source_rect, keypad_dests + + def __init__(self, parent, rect): + super().__init__(parent) + self.rect = rect + keyboard_size = (rect.width, rect.height / 2) + self.keyboard_rects = [ + pygame.Rect(rect.topleft, keyboard_size), + pygame.Rect((rect.left, rect.centery), keyboard_size), + ] + widget_size = (rect.width / 3, keyboard_size[1]) + self.widget_rects = [ + [ + pygame.Rect( + (rect.left + rect.width / 4 - widget_size[0] / 2, rect.top), + widget_size, + ), + pygame.Rect( + (rect.left + rect.width * 3 / 4 - widget_size[0] / 2, rect.top), + widget_size, + ) + ], + [ + pygame.Rect((rect.left, rect.top), widget_size), + pygame.Rect((rect.left + widget_size[0], rect.top), widget_size), + pygame.Rect((rect.left + 2 * widget_size[1], rect.top), widget_size), + ], + ] + surf = pygame.Surface(keyboard_size) + surf.fill("black") + self.get_keyboard_keys(surf, keyboard_size, KEYBOARD) + self.keyboard = DraggableButton( + self.parent, + self.keyboard_rects[self.parent.keyboard_row], + surf, + [self.keyboard_rects[not self.parent.keyboard_row]], + self.keyboard_callback, + ) + #self.keypad_rect = pygame.Rect( + # ( + # self.parent.rect.left + ( + # self.rect.left - self.parent.rect.left + # ) / 2 - self.rect.width / 6, + # self.parent.rect.top + 96, + # ), + # (self.rect.width / 3, self.rect.height / 2), + #) + #surf = pygame.Surface(self.keypad_rect.size) + #surf.fill("black") + #self.get_keyboard_keys(surf, self.keypad_rect.size, KEYPAD_KEYS) + #keypad_rect, keypad_dests = self.get_widget_row_rect_dests( + # len(parent.widget_row), KEYPAD_KEYS, self.keypad_rect + #) + #self.keypad = DraggableButton( + # self.parent, + # keypad_rect, + # surf, + # keypad_dests, + # self.keypad_callback, + #) + + def keyboard_callback(self): + self.parent.keyboard_row = int( + (self.keyboard.rect.top - self.rect.top) * 2 / self.rect.height + ) + # move the visual widget row out of the way here + + def keypad_callback(self): + if KEYPAD_KEYS in self.parent.widget_row: + self.parent.widget_row.remove(KEYPAD_KEYS) + if self.keypad.rect != self.keypad_rect: + if len(self.parent.widget_row) == 0: + self.parent.widget_row.append(KEYPAD_KEYS) + return + # insert KEYPAD_KEYS to widget_row at the right index + # rearrange the visual widget row + + def draw(self): + pygame.draw.rect(self.surf, "gray", self.rect.inflate((-2, -2)), 1) + + +class LayoutModal(QuittableModal): + def __init__(self, parent): + super().__init__(parent) + size = self.surf.get_size() + self.rect = pygame.Rect(150, 150, size[0] - 300, size[1] - 300) + Rect(self, self.rect, "black", "gray") + TouchButton( + self, + pygame.Rect((self.rect.centerx - 128, self.rect.bottom - 128), (256, 128)), + "Close", + self.deactivate, + ) + self.keyboard_row = 0 + self.widget_row = [CURSOR_KEYS, KEYPAD_KEYS, ClockWidget] + half_size = (size[0] / 2, size[1] / 2) + LayoutWidget(self, pygame.Rect((half_size[0] / 2, half_size[1] / 2), half_size)) diff --git a/keyboard/mousepad.py b/keyboard/mousepad.py new file mode 100644 index 0000000..050c3dc --- /dev/null +++ b/keyboard/mousepad.py @@ -0,0 +1,83 @@ +from functools import partial + +import pygame +from pynput.mouse import Button + +from ui import Child +from ui.multitouch import MultitouchHandler + +from .touchbutton import TouchButton + + +BUTTONS = ( + ("Dbl←", (Button.left, 2)), + ("←", (Button.left,)), + ("↓", (Button.middle,)), + ("→", (Button.right,)), +) + + +class MousePadWidget(Child): + def __init__(self, parent, rect, click_cb, move_cb): + super().__init__(parent) + self.rect = rect + button_size = (rect.width / len(BUTTONS), rect.height / 6) + for i, (value, args) in enumerate(BUTTONS): + TouchButton( + parent, + pygame.Rect( + ( + rect.left + i * button_size[0], + rect.top + rect.height - button_size[1], + ), + button_size, + ), + value, + partial(click_cb, *args) + ) + MousePadArea( + parent, + pygame.Rect(rect.topleft, (rect.width, rect.height - button_size[1])), + move_cb, + ) + + +class MousePadArea(Child): + def __init__(self, parent, rect, move_cb): + super().__init__(parent) + self.rect = rect + self.move_cb = move_cb + self.fingers = {} + + def draw(self): + pygame.draw.rect(self.surf, "yellow", self.rect, 1) + + def handle_fingerdown(self, event): + pos = (event.x, event.y) + if self.rect.collidepoint( + MultitouchHandler.map_coords(pos, self.surf.get_size()) + ): + self.fingers[(event.touch_id, event.finger_id)] = [pos] + + def handle_fingermotion(self, event): + key = (event.touch_id, event.finger_id) + if key not in self.fingers: + return + pos = (event.x, event.y) + if self.rect.collidepoint( + MultitouchHandler.map_coords(pos, self.surf.get_size()) + ): + self.fingers[key].append(pos) + else: + self.fingers.pop(key) + + def handle_fingerup(self, event): + key = (event.touch_id, event.finger_id) + if key not in self.fingers: + return + pos = (event.x, event.y) + if self.rect.collidepoint( + MultitouchHandler.map_coords(pos, self.surf.get_size()) + ): + pass + self.fingers.pop(key) diff --git a/keyboard/settings.py b/keyboard/settings.py index f88f23e..430c438 100644 --- a/keyboard/settings.py +++ b/keyboard/settings.py @@ -1,12 +1,14 @@ import os import sys +from functools import partial from pathlib import Path import pygame -from ui import Modal, Rect +from ui import QuittableModal, Rect from .touchbutton import TouchButton +from .layout import LayoutModal # what settings do we need? # - configure widgets @@ -17,35 +19,32 @@ from .touchbutton import TouchButton # - three available widgets: touchpad mouse, number keypad, cursor and navkeys -class Settings(Modal): +class SettingsModal(QuittableModal): def __init__(self, parent, rect): super().__init__(parent) self.rect = rect - Rect(self, self.rect, "black", "gray") + Rect(self, rect, "black", "gray") width = rect.width * 2 / 3 height = rect.height / 7 left = rect.left + (rect.width - width) / 2 + 8 y = rect.top + height + 8 rect_size = (width - 16, height - 16) - self.buttons = [] + self.layout_modal = LayoutModal(parent) for args in ( - ("Mute", self.mute, self.root.muted), - ("Layout...", self.layout), + ("Mute", None, self.root.muted), + ("Layout...", self.layout_modal.activate), ("Restart", self.restart), ("Exit", self.root.handle_quit), ("Back", self.deactivate), ): - self.buttons.append( - TouchButton(self, pygame.Rect((left, y), rect_size), *args) - ) + button = TouchButton(self, pygame.Rect((left, y), rect_size), *args) + if args[0] == "Mute": + button.callback = partial(self.mute, button) y += height - def mute(self): + def mute(self, button): self.root.muted ^= True - self.buttons[0].highlight = self.root.muted - - def layout(self): - print("stub layout") + button.highlight = self.root.muted def restart(self): executable = Path(sys.executable).name diff --git a/launch.py b/launch.py index 95d345d..ae75ddb 100755 --- a/launch.py +++ b/launch.py @@ -15,6 +15,7 @@ from time import time from traceback import format_exception executable = Path(sys.executable).name +pkgs = ("pygame", "evdev", "pynput") def tty_connected(): @@ -73,9 +74,9 @@ def run_cmd(cmd, expect_returncode=0): raise ChildProcessError(os.linesep.join(lines)) -def check_has_pygame(): - return b"No module named pygame.__main__;" in run_cmd( - (executable, "-m", "pygame"), 1 +def check_has_pkg(pkg): + return f"No module named {pkg}.__main__;".encode() in run_cmd( + (executable, "-m", pkg), 1 ).stderr @@ -121,10 +122,13 @@ def setup_venv(): 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() + has_pkgs = ( + venv_dir_is_dir + and all(check_has_pkg(pkg) for pkg in pkgs) + ) if get_ipv4_is_connected(): yesterday = time() - 86400 - if not has_pygame or all( + if not has_pkgs or all( p.stat().st_mtime < yesterday for p in walk_outer(venv_dir) ): run_cmd( @@ -135,34 +139,35 @@ def setup_venv(): "install", "-qU", "pip", - "pygame", - "pynput", - "evdev", + *pkgs ) ) venv_dir.touch(0o755, True) - elif not has_pygame: + elif not has_pkgs: 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:], - ) - exit(1) - pygame.init() - return pygame + modules = [] + for pkg in pkgs: + if pkg in sys.modules: + modules.append(sys.modules[pkg]) + continue + try: + with redirect_stdout(None): + modules.append(import_module(pkg)) + except ImportError: + with exception_wrapper(): + setup_venv() + os.execl( + sys.executable, + executable, + *sys.orig_argv[sys.orig_argv[0] == executable:], + ) + exit(1) + if pkg == "pygame": + modules[-1].init() + return modules def resolve_symlink(path): @@ -205,7 +210,7 @@ def main(): try_run(".".join((module_name, module_name))) -pygame = pre_run() +pygame, evdev, pynput = pre_run() if __name__ == "__main__": main() diff --git a/memory.py b/memory.py index 1d5ba17..3952314 100755 --- a/memory.py +++ b/memory.py @@ -5,14 +5,7 @@ from pathlib import Path from secrets import choice from launch import pygame -from ui import BaseRoot, Button, Child, Label, Modal, TextInput - - -class QuittableModal(Modal): - def handle_quit(self, _=None): - self.parent.handle_quit() - - key_methods = {frozenset(): {pygame.K_ESCAPE: handle_quit}} +from ui import BaseRoot, Button, Child, Label, QuittableModal, TextInput class NameMenu(QuittableModal): diff --git a/ui/__init__.py b/ui/__init__.py index e422676..cb4cbff 100644 --- a/ui/__init__.py +++ b/ui/__init__.py @@ -8,7 +8,7 @@ from .icon import Icon from .icon_button import IconButton from .label import Label from .message_box import MessageBox -from .modal import Modal +from .modal import Modal, QuittableModal from .parent import Parent from .rect import Rect from .root import BaseRoot @@ -34,6 +34,7 @@ __all__ = [ "MessageBox", "Modal", "Parent", + "QuittableModal", "Rect", "RepeatButton", "Scroll", diff --git a/ui/fps_widget.py b/ui/fps_widget.py index 25530ad..a33bbc7 100644 --- a/ui/fps_widget.py +++ b/ui/fps_widget.py @@ -6,7 +6,7 @@ class FPSWidget(Child): def __init__(self, parent): super().__init__(parent) - self.current_fps = None + self.current_fps = 0 def update(self): new_fps = int(self.root.clock.get_fps()) diff --git a/ui/modal.py b/ui/modal.py index 4639ab3..2a29cf1 100644 --- a/ui/modal.py +++ b/ui/modal.py @@ -33,3 +33,10 @@ class Modal(Focusable, Parent, Child): def draw(self): self.draw_modal() super().draw() + + +class QuittableModal(Modal): + def handle_quit(self, _=None): + self.parent.handle_quit() + + key_methods = {frozenset(): {pygame.K_ESCAPE: handle_quit}} -- 2.51.0