#!/usr/bin/env python3 # Copyright © 2024 Noah Vogt # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # pylint: disable=invalid-name import threading import sys import asyncio import colorama from kivy.app import App from kivy.uix.gridlayout import GridLayout from kivy.utils import get_color_from_hex from kivy.config import Config as kvConfig from kivy.core.window import Window from kivy.clock import Clock from util import log, error_exit_gui from config import ( get_config_path, ConfigLoader, Config, get_state_from_id, get_state_id_from_exit_code, DEFAULT_BUTTON_BG_COLOR, DEFAULT_BUTTON_FG_COLOR, EMPTY_BUTTON_BG_COLOR, DEFAULT_STATE_ID, ERROR_SINK_STATE_ID, ) from ui import AutoResizeButton class VulcanBoardApp(App): def build(self): self.loop = self.ensure_asyncio_loop_running() self.button_grid = {} self.button_config_map = {} self.icon = "icon.png" config_loader = ConfigLoader(get_config_path()) config = config_loader.get_config() # pyright: ignore if isinstance(config, str): error_exit_gui(config) else: config: Config = config Window.borderless = config.borderless if config.set_window_pos: Window.left = config.window_pos_x Window.top = config.window_pos_y if config.use_auto_fullscreen_mode: Window.fullscreen = "auto" kvConfig.set("kivy", "exit_on_escape", "0") self.button_config_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 = self.button_config_map.get((row, col)) if defined_button: states = defined_button.get("states", []) state = get_state_from_id(states, DEFAULT_STATE_ID) btn = AutoResizeButton( text=state.get("txt", ""), background_color=get_color_from_hex( state.get("bg_color", DEFAULT_BUTTON_BG_COLOR) ), color=get_color_from_hex( state.get("fg_color", DEFAULT_BUTTON_FG_COLOR) ), halign="center", valign="middle", background_normal="", state_id=DEFAULT_STATE_ID, ) if defined_button.get("autostart", False): self.async_task( self.execute_command_async(defined_button, btn) ) # pylint: disable=no-member btn.bind( # pyright: ignore on_release=lambda btn_instance, button=defined_button: self.async_task( self.execute_command_async(button, btn_instance) ) ) else: btn = AutoResizeButton( background_color=get_color_from_hex( EMPTY_BUTTON_BG_COLOR ), ) self.button_grid[(row, col)] = btn layout.add_widget(btn) return layout def async_task(self, coroutine): asyncio.run_coroutine_threadsafe(coroutine, self.loop) def ensure_asyncio_loop_running(self): if hasattr(self, "loop"): return self.loop # Already created loop = asyncio.new_event_loop() def run_loop(): asyncio.set_event_loop(loop) loop.run_forever() threading.Thread(target=run_loop, daemon=True).start() return loop def config_error_exit(self, popup): popup.dismiss() sys.exit(1) async def execute_command_async(self, button: dict, btn: AutoResizeButton): follow_up_state_loop = True states = button["states"] while follow_up_state_loop: state = get_state_from_id(states, btn.state_id) follow_up_state_loop = False try: process = await asyncio.create_subprocess_shell( state["cmd"], shell=True ) exit_code = await process.wait() log(f"Executed command: {state['cmd']}") except Exception as e: exit_code = ERROR_SINK_STATE_ID log(f"Error executing command: {e}", color="yellow") if len(states) != 1: if isinstance( follow_up_state := state.get("follow_up_state"), int ): follow_up_state_loop = True exit_code = follow_up_state Clock.schedule_once( lambda _: self.update_button_feedback( states, btn, exit_code # pyright: ignore ) ) affects_buttons = button.get("affects_buttons", None) if affects_buttons: for affected_btn_dims in affects_buttons: btn_pos = (affected_btn_dims[0], affected_btn_dims[1]) affected_button = self.button_grid[btn_pos] Clock.schedule_once( lambda _, btn_pos=btn_pos, affected_button=affected_button: self.update_button_feedback( self.button_config_map[btn_pos]["states"], affected_button, exit_code, # pyright: ignore ) ) def update_button_feedback( self, states: list, btn: AutoResizeButton, exit_code: int ): state_id = get_state_id_from_exit_code(states, exit_code) state = get_state_from_id(states, state_id) btn.text = state.get("txt", "") btn.state_id = state_id btn.background_color = get_color_from_hex( state.get("bg_color", DEFAULT_BUTTON_BG_COLOR) ) btn.color = get_color_from_hex( state.get("fg_color", DEFAULT_BUTTON_FG_COLOR) ) def start_asyncio_loop(): asyncio.set_event_loop(asyncio.new_event_loop()) asyncio.get_event_loop().run_forever() if __name__ == "__main__": colorama.init() VulcanBoardApp().run()