Selaa lähdekoodia

fix button bg_color + add fg_color option + add config validation

Noah Vogt 6 kuukautta sitten
vanhempi
sitoutus
d2bd8fd899
2 muutettua tiedostoa jossa 111 lisäystä ja 33 poistoa
  1. 11 4
      README.md
  2. 100 29
      VulcanBoard.py

+ 11 - 4
README.md

@@ -8,13 +8,20 @@ There are many hotkey solutions and programs like that use either
 
 They are often very bloated but lack basic features like asynchronous command execution. They also crash way to often, so especially given their intended use for livestream production that greatly values stability, *VulcanBoard* aims to be a rock-solid alternative.
 
-It is currently still under development, here are some planned changes:
-- add extensive config validator
+## Installation
+
+To setup you need to have python3 installed. In addition, to install the dependencies using pip:
+
+    pip install pyyaml kivy
+
+## 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 list of dependencies
 - 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 fullscreen mode
+- add fullscreen mode (maybe using a cmdline switch)

+ 100 - 29
VulcanBoard.py

@@ -19,6 +19,7 @@
 from os import path, getenv, name
 import subprocess
 import sys
+from dataclasses import dataclass
 
 import yaml
 from termcolor import colored
@@ -46,45 +47,119 @@ def get_config_path():
     return path.join(xdg_config_home, "VulcanBoard", "config.yml")
 
 
-def load_config(  # pylint: disable=inconsistent-return-statements
-    config_file_path,
-):
-    try:
-        with open(config_file_path, "r", encoding="utf-8") as config_reader:
-            return yaml.safe_load(config_reader)
-    except (FileNotFoundError, PermissionError, IOError) as error:
-        error_msg(
-            f"Error: Could not access config file at {config_file_path}. Reason: {error}"
-        )
-    except yaml.YAMLError as error:
-        error_msg(f"Error parsing config file. Reason: {error}")
+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:
+            return False
 
-class VulcanBoardApp(App):
-    def build(self) -> GridLayout:
-        config = load_config(get_config_path())
+    return True
+
+
+@dataclass
+class Config:
+    columns: int
+    rows: int
+    buttons: list[dict]
+
+
+@dataclass
+class ConfigLoader:
+    def __interpret_config(self, yaml_config) -> Config:
+        columns = yaml_config.get("columns")
+        rows = yaml_config.get("rows")
+        self.__validate_dimensions(columns, rows)
+
+        buttons = yaml_config.get("buttons")
+        self.__validate_buttons(buttons, rows, columns)
+
+        return Config(columns, rows, buttons)
+
+    def __validate_buttons(self, buttons, rows, columns) -> None:
+        if not isinstance(buttons, list):
+            error_msg("invalid button config. needs to be a list of dicts.")
+        for button in buttons:
+            if not isinstance(button, dict):
+                error_msg("invalid button config. needs to be a list of dicts.")
+
+            if (
+                not isinstance(dimensions := button.get("button", ""), list)
+                or (not isinstance(dimensions[0], int))
+                or (not isinstance(dimensions[1], int))
+                or (0 > dimensions[0] or dimensions[0] > rows - 1)
+                or (0 > dimensions[1] or dimensions[1] > columns - 1)
+            ):
+                error_msg(f"invalid button 'button' subentry: '{dimensions}'")
+
+            for entry in ("txt", "cmd"):
+                if not isinstance(result := button.get(entry, ""), str):
+                    error_msg(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}'")
+
+            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}'")
 
-        columns = config.get("COLUMNS")
-        rows = config.get("ROWS")
-        self.validate_dimensions(columns, rows)
+            if (
+                not isinstance(fontsize := button.get("fontsize", ""), int)
+                or 0 > fontsize
+            ):
+                error_msg(f"invalid button 'fontsize' subentry: '{fontsize}'")
 
-        buttons_config = config.get("BUTTONS", [])
+    def __validate_dimensions(self, columns, rows) -> None:
+        for dimension in (columns, rows):
+            if not isinstance(dimension, int) and (dimension <= 0):
+                error_msg(f"invalid dimension: {dimension}")
+
+    def load_config(  # pylint: disable=inconsistent-return-statements
+        self,
+        config_file_path,
+    ) -> Config:
+        try:
+            with open(config_file_path, "r", encoding="utf-8") as config_reader:
+                read_config = yaml.safe_load(config_reader)
+                return self.__interpret_config(read_config)
+        except (FileNotFoundError, PermissionError, IOError) as error:
+            error_msg(
+                f"Error: Could not access config file at {config_file_path}. Reason: {error}"
+            )
+        except yaml.YAMLError as error:
+            error_msg(f"Error parsing config file. Reason: {error}")
+
+
+class VulcanBoardApp(App):
+    def build(self) -> GridLayout:
+        config_loader = ConfigLoader()
+        config = config_loader.load_config(get_config_path())
 
         button_map = {
-            (btn["button"][0], btn["button"][1]): btn for btn in buttons_config
+            (btn["button"][0], btn["button"][1]): btn for btn in config.buttons
         }
 
-        layout = GridLayout(cols=columns, rows=rows, spacing=5, padding=5)
+        layout = GridLayout(
+            cols=config.columns, rows=config.rows, spacing=5, padding=5
+        )
 
         # Populate grid with buttons and placeholders
-        for row in range(rows):
-            for col in range(columns):
+        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("color", "ffffff")
+                            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",
@@ -93,6 +168,7 @@ class VulcanBoardApp(App):
                             None,
                             None,
                         ),  # Enable text alignment within button
+                        background_normal="",
                     )
 
                     # pylint: disable=no-member
@@ -111,11 +187,6 @@ class VulcanBoardApp(App):
 
         return layout
 
-    def validate_dimensions(self, columns, rows):
-        for dimension in (columns, rows):
-            if not isinstance(dimension, int) and (dimension <= 0):
-                error_msg(f"invalid dimension: {dimension}")
-
     def execute_command_async(self, cmd):
         if cmd:
             try: