|
@@ -29,11 +29,7 @@ from kivy.app import App
|
|
from kivy.uix.button import Button
|
|
from kivy.uix.button import Button
|
|
from kivy.uix.gridlayout import GridLayout
|
|
from kivy.uix.gridlayout import GridLayout
|
|
from kivy.utils import get_color_from_hex
|
|
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:
|
|
def log(message: str, color="green") -> None:
|
|
@@ -47,13 +43,15 @@ def get_config_path():
|
|
return path.join(xdg_config_home, "VulcanBoard", "config.yml")
|
|
return path.join(xdg_config_home, "VulcanBoard", "config.yml")
|
|
|
|
|
|
|
|
|
|
|
|
+VALID_HEX_COLORS = list("0123456789abcdef")
|
|
|
|
+
|
|
|
|
+
|
|
def is_valid_hexcolor(hexcolor: str) -> bool:
|
|
def is_valid_hexcolor(hexcolor: str) -> bool:
|
|
if len(hexcolor) != 6:
|
|
if len(hexcolor) != 6:
|
|
return False
|
|
return False
|
|
|
|
|
|
- valid_hex_chars = list("012345789abcdef")
|
|
|
|
for char in hexcolor.lower():
|
|
for char in hexcolor.lower():
|
|
- if char not in valid_hex_chars:
|
|
|
|
|
|
+ if char not in VALID_HEX_COLORS:
|
|
return False
|
|
return False
|
|
|
|
|
|
return True
|
|
return True
|
|
@@ -64,41 +62,59 @@ class Config:
|
|
columns: int
|
|
columns: int
|
|
rows: int
|
|
rows: int
|
|
buttons: list[dict]
|
|
buttons: list[dict]
|
|
|
|
+ spacing: int
|
|
|
|
+ padding: int
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+class CustomException(Exception):
|
|
|
|
+ pass
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
@dataclass
|
|
class ConfigLoader:
|
|
class ConfigLoader:
|
|
config_path: str
|
|
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:
|
|
try:
|
|
with open(self.config_path, "r", encoding="utf-8") as config_reader:
|
|
with open(self.config_path, "r", encoding="utf-8") as config_reader:
|
|
yaml_config = yaml.safe_load(config_reader)
|
|
yaml_config = yaml.safe_load(config_reader)
|
|
self.columns = yaml_config.get("columns")
|
|
self.columns = yaml_config.get("columns")
|
|
self.rows = yaml_config.get("rows")
|
|
self.rows = yaml_config.get("rows")
|
|
self.buttons = yaml_config.get("buttons")
|
|
self.buttons = yaml_config.get("buttons")
|
|
|
|
+ self.padding = yaml_config.get("padding", 5)
|
|
|
|
+ self.spacing = yaml_config.get("spacing", 5)
|
|
return self.__interpret_config()
|
|
return self.__interpret_config()
|
|
except (FileNotFoundError, PermissionError, IOError) as error:
|
|
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:
|
|
def __interpret_config(self) -> Config:
|
|
self.__validate_dimensions()
|
|
self.__validate_dimensions()
|
|
self.__validate_buttons()
|
|
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:
|
|
def __validate_buttons(self) -> None:
|
|
if not isinstance(self.buttons, list):
|
|
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:
|
|
for button in self.buttons:
|
|
if not isinstance(button, dict):
|
|
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 (
|
|
if (
|
|
not isinstance(dimensions := button.get("position", ""), list)
|
|
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[0] or dimensions[0] > self.rows - 1)
|
|
or (0 > dimensions[1] or dimensions[1] > self.columns - 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"):
|
|
for entry in ("txt", "cmd"):
|
|
if not isinstance(result := button.get(entry, ""), str):
|
|
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(
|
|
if not isinstance(
|
|
bg_color := button.get("bg_color", "cccccc"), str
|
|
bg_color := button.get("bg_color", "cccccc"), str
|
|
) or not is_valid_hexcolor(bg_color):
|
|
) 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(
|
|
if not isinstance(
|
|
fg_color := button.get("fg_color", "ffffff"), str
|
|
fg_color := button.get("fg_color", "ffffff"), str
|
|
) or not is_valid_hexcolor(bg_color):
|
|
) 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 (
|
|
if (
|
|
not isinstance(fontsize := button.get("fontsize", ""), int)
|
|
not isinstance(fontsize := button.get("fontsize", ""), int)
|
|
or 0 > fontsize
|
|
or 0 > fontsize
|
|
):
|
|
):
|
|
- error_msg(f"invalid button 'fontsize' subentry: '{fontsize}'")
|
|
|
|
|
|
+ raise CustomException(
|
|
|
|
+ f"invalid button 'fontsize' subentry: '{fontsize}'"
|
|
|
|
+ )
|
|
|
|
|
|
def __validate_dimensions(self) -> None:
|
|
def __validate_dimensions(self) -> None:
|
|
for dimension in (self.columns, self.rows):
|
|
for dimension in (self.columns, self.rows):
|
|
if not isinstance(dimension, int) or (dimension <= 0):
|
|
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):
|
|
def execute_command_async(self, cmd):
|
|
if cmd:
|
|
if cmd:
|