Browse Source

fix hexcolors + use gui error exit

Noah Vogt 6 tháng trước cách đây
mục cha
commit
5c281b5122
3 tập tin đã thay đổi với 155 bổ sung70 xóa
  1. 4 5
      README.md
  2. 113 65
      VulcanBoard.py
  3. 38 0
      vulcanboard.kv

+ 4 - 5
README.md

@@ -7,9 +7,9 @@ There are many hotkey solutions and programs like that use either
 - touchscreens (LioranBoard, MacroDeck, StreamPi)
 
 They are often very bloated but lack basic features like
-- multitouch support on the desktop
+- multitouch support for the desktop client
 - asynchronous command execution
-- fullscreen mode
+- a fullscreen mode
 
 They also crash way too often, so especially given their intended use for livestreaming production that greatly values stability, *VulcanBoard* aims to be a rock-solid alternative.
 
@@ -22,13 +22,12 @@ To setup you need to have python3 installed. In addition, to install the depende
 ## Project State
 It is currently still under heavy development, here are some planned changes:
 - add documentation for the configuration and use of VulcanBoard
-- display error_exits in a gui window
 - add gui window to configure keys
     - add multiple boards to config.yml
     - add edit history cache
 - add button merging
 - add possibility to choose the font family used for button texts
-- add rounded corners
-- add delayed fullscreen mode (maybe using a cmdline switch)
+- add rounded corners for buttons
 - use constants / constant dict for default values
 - add folders
+- add button signals (changing button color / text based on certain conditions)

+ 113 - 65
VulcanBoard.py

@@ -29,11 +29,7 @@ from kivy.app import App
 from kivy.uix.button import Button
 from kivy.uix.gridlayout import GridLayout
 from kivy.utils import get_color_from_hex
-
-
-def error_msg(msg: str):
-    print(colored("[*] Error: {}".format(msg), "red"))
-    sys.exit(1)
+from kivy.factory import Factory
 
 
 def log(message: str, color="green") -> None:
@@ -47,13 +43,15 @@ def get_config_path():
     return path.join(xdg_config_home, "VulcanBoard", "config.yml")
 
 
+VALID_HEX_COLORS = list("0123456789abcdef")
+
+
 def is_valid_hexcolor(hexcolor: str) -> bool:
     if len(hexcolor) != 6:
         return False
 
-    valid_hex_chars = list("012345789abcdef")
     for char in hexcolor.lower():
-        if char not in valid_hex_chars:
+        if char not in VALID_HEX_COLORS:
             return False
 
     return True
@@ -64,41 +62,59 @@ class Config:
     columns: int
     rows: int
     buttons: list[dict]
+    spacing: int
+    padding: int
+
+
+class CustomException(Exception):
+    pass
 
 
 @dataclass
 class ConfigLoader:
     config_path: str
 
-    def __post_init__(  # pylint: disable=inconsistent-return-statements
-        self,
-    ) -> Config:
+    def __post_init__(self) -> None:
+        self.columns = 0
+        self.rows = 0
+        self.buttons = []
+        self.padding = 0
+        self.spacing = 0
+
+    def get_config(self) -> Config | str:
         try:
             with open(self.config_path, "r", encoding="utf-8") as config_reader:
                 yaml_config = yaml.safe_load(config_reader)
                 self.columns = yaml_config.get("columns")
                 self.rows = yaml_config.get("rows")
                 self.buttons = yaml_config.get("buttons")
+                self.padding = yaml_config.get("padding", 5)
+                self.spacing = yaml_config.get("spacing", 5)
                 return self.__interpret_config()
         except (FileNotFoundError, PermissionError, IOError) as error:
-            error_msg(
-                f"Error: Could not access config file at {self.config_path}. Reason: {error}"
-            )
-        except yaml.YAMLError as error:
-            error_msg(f"Error parsing config file. Reason: {error}")
+            return f"Error: Could not access config file at {self.config_path}. Reason: {error}"
+        except (yaml.YAMLError, CustomException) as error:
+            return f"Error parsing config file. Reason: {error}"
 
     def __interpret_config(self) -> Config:
         self.__validate_dimensions()
         self.__validate_buttons()
+        self.__validate_styling()
 
-        return Config(self.columns, self.rows, self.buttons)
+        return Config(
+            self.columns, self.rows, self.buttons, self.spacing, self.padding
+        )
 
     def __validate_buttons(self) -> None:
         if not isinstance(self.buttons, list):
-            error_msg("invalid button config. needs to be a list of dicts.")
+            raise CustomException(
+                "invalid button config. needs to be a list of dicts."
+            )
         for button in self.buttons:
             if not isinstance(button, dict):
-                error_msg("invalid button config. needs to be a list of dicts.")
+                raise CustomException(
+                    "invalid button config. needs to be a list of dicts."
+                )
 
             if (
                 not isinstance(dimensions := button.get("position", ""), list)
@@ -107,80 +123,112 @@ class ConfigLoader:
                 or (0 > dimensions[0] or dimensions[0] > self.rows - 1)
                 or (0 > dimensions[1] or dimensions[1] > self.columns - 1)
             ):
-                error_msg(f"invalid button 'position' subentry: '{dimensions}'")
+                raise CustomException(
+                    f"invalid button 'position' subentry: '{dimensions}'"
+                )
 
             for entry in ("txt", "cmd"):
                 if not isinstance(result := button.get(entry, ""), str):
-                    error_msg(f"invalid button '{entry}' subentry: '{result}'")
+                    raise CustomException(
+                        f"invalid button '{entry}' subentry: '{result}'"
+                    )
 
             if not isinstance(
                 bg_color := button.get("bg_color", "cccccc"), str
             ) or not is_valid_hexcolor(bg_color):
-                error_msg(f"invalid button 'bg_color' subentry: '{bg_color}'")
+                raise CustomException(
+                    f"invalid button 'bg_color' subentry: '{bg_color}'"
+                )
 
             if not isinstance(
                 fg_color := button.get("fg_color", "ffffff"), str
             ) or not is_valid_hexcolor(bg_color):
-                error_msg(f"invalid button 'fg_color' subentry: '{fg_color}'")
+                raise CustomException(
+                    f"invalid button 'fg_color' subentry: '{fg_color}'"
+                )
 
             if (
                 not isinstance(fontsize := button.get("fontsize", ""), int)
                 or 0 > fontsize
             ):
-                error_msg(f"invalid button 'fontsize' subentry: '{fontsize}'")
+                raise CustomException(
+                    f"invalid button 'fontsize' subentry: '{fontsize}'"
+                )
 
     def __validate_dimensions(self) -> None:
         for dimension in (self.columns, self.rows):
             if not isinstance(dimension, int) or (dimension <= 0):
-                error_msg(f"invalid dimension: {dimension}")
+                raise CustomException(f"invalid dimension: {dimension}")
 
+    def __validate_styling(self) -> None:
+        for styling in (self.spacing, self.padding):
+            if not isinstance(styling, int) or (styling <= 0):
+                raise CustomException(f"invalid styling: {styling}")
 
-class VulcanBoardApp(App):
-    def build(self) -> GridLayout:
-        config = ConfigLoader(get_config_path())
-
-        button_map = {
-            (btn["position"][0], btn["position"][1]): btn
-            for btn in config.buttons
-        }
 
-        layout = GridLayout(
-            cols=config.columns, rows=config.rows, spacing=5, padding=5
-        )
+class VulcanBoardApp(App):
+    def build(self):
+        config_loader = ConfigLoader(get_config_path())
+        config = config_loader.get_config()  # pyright: ignore
+        print(type(config))
+        print(config)
+        if isinstance(config, str):
+            popup = Factory.ErrorPopup()
+            popup.message.text = config
+            popup.open()
+            popup.error_exit = lambda: sys.exit(1)
+        else:
+            config: Config = config
+            button_map = {
+                (btn["position"][0], btn["position"][1]): btn
+                for btn in config.buttons
+            }
+
+            layout = GridLayout(
+                cols=config.columns,
+                rows=config.rows,
+                spacing=config.spacing,
+                padding=config.padding,
+            )
 
-        # Populate grid with buttons and placeholders
-        for row in range(config.rows):
-            for col in range(config.columns):
-                defined_button = button_map.get((row, col))
-                if defined_button:
-                    btn = Button(
-                        text=defined_button.get("txt", ""),
-                        background_color=get_color_from_hex(
-                            defined_button.get("bg_color", "aaaaff")
-                        ),
-                        color=get_color_from_hex(
-                            defined_button.get("fg_color", "ffffff")
-                        ),
-                        font_size=defined_button.get("fontsize", 14),
-                        halign="center",
-                        valign="middle",
-                        background_normal="",
-                    )
+            # Populate grid with buttons and placeholders
+            for row in range(config.rows):
+                for col in range(config.columns):
+                    defined_button = button_map.get((row, col))
+                    if defined_button:
+                        btn = Button(
+                            text=defined_button.get("txt", ""),
+                            background_color=get_color_from_hex(
+                                defined_button.get("bg_color", "aaaaff")
+                            ),
+                            color=get_color_from_hex(
+                                defined_button.get("fg_color", "ffffff")
+                            ),
+                            font_size=defined_button.get("fontsize", 14),
+                            halign="center",
+                            valign="middle",
+                            background_normal="",
+                        )
 
-                    cmd = defined_button.get("cmd", "")
-                    # pylint: disable=no-member
-                    btn.bind(  # pyright: ignore
-                        on_release=lambda instance, cmd=cmd: self.execute_command_async(
-                            cmd
+                        cmd = defined_button.get("cmd", "")
+                        # pylint: disable=no-member
+                        btn.bind(  # pyright: ignore
+                            on_release=lambda _, cmd=cmd: self.execute_command_async(
+                                cmd
+                            )
                         )
-                    )
-                else:
-                    btn = Button(
-                        background_color=get_color_from_hex("cccccc"),
-                    )
-                layout.add_widget(btn)
+                    else:
+                        btn = Button(
+                            background_color=get_color_from_hex("cccccc"),
+                        )
+                    layout.add_widget(btn)
+
+            return layout
 
-        return layout
+    def config_error_exit(self, popup):
+        print("wow")
+        popup.dismiss()
+        sys.exit(1)
 
     def execute_command_async(self, cmd):
         if cmd:

+ 38 - 0
vulcanboard.kv

@@ -0,0 +1,38 @@
+<ErrorPopup@Popup>:
+    message: message
+    auto_dismiss: False
+    title: "Error"
+    size_hint: None, None
+    width: grid.width + dp(25)
+    height: grid.height + root.title_size + dp(48)
+
+    GridLayout:
+        id: grid
+        size_hint: None, None
+        size: self.minimum_size
+        padding: [10, 5]
+        cols: 1
+        AnchorLayout:
+            anchor_x: "center"
+            anchor_y: "bottom"
+            size_hint: None, None
+            height: message.height
+            width: max(message.width, butt.width)
+            Label:
+                id: message
+                size_hint: None, None
+                size: self.texture_size
+                padding: [10, 5]
+        AnchorLayout:
+            anchor_x: "center"
+            anchor_y: "bottom"
+            size_hint: None, None
+            height: butt.height
+            width: max(message.width, butt.width)
+            Button:
+                id: butt
+                text: 'Close'
+                size_hint: None, None
+                size: self.texture_size
+                padding: [10, 5]
+                on_release: root.error_exit();