|
@@ -19,6 +19,7 @@
|
|
from os import path, getenv, name
|
|
from os import path, getenv, name
|
|
import subprocess
|
|
import subprocess
|
|
import sys
|
|
import sys
|
|
|
|
+from dataclasses import dataclass
|
|
|
|
|
|
import yaml
|
|
import yaml
|
|
from termcolor import colored
|
|
from termcolor import colored
|
|
@@ -46,45 +47,119 @@ def get_config_path():
|
|
return path.join(xdg_config_home, "VulcanBoard", "config.yml")
|
|
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 = {
|
|
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
|
|
# 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))
|
|
defined_button = button_map.get((row, col))
|
|
if defined_button:
|
|
if defined_button:
|
|
btn = Button(
|
|
btn = Button(
|
|
text=defined_button.get("txt", ""),
|
|
text=defined_button.get("txt", ""),
|
|
background_color=get_color_from_hex(
|
|
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),
|
|
font_size=defined_button.get("fontsize", 14),
|
|
halign="center",
|
|
halign="center",
|
|
@@ -93,6 +168,7 @@ class VulcanBoardApp(App):
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
), # Enable text alignment within button
|
|
), # Enable text alignment within button
|
|
|
|
+ background_normal="",
|
|
)
|
|
)
|
|
|
|
|
|
# pylint: disable=no-member
|
|
# pylint: disable=no-member
|
|
@@ -111,11 +187,6 @@ class VulcanBoardApp(App):
|
|
|
|
|
|
return layout
|
|
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):
|
|
def execute_command_async(self, cmd):
|
|
if cmd:
|
|
if cmd:
|
|
try:
|
|
try:
|