123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- #!/usr/bin/env python3
- # Copyright © 2024 Noah Vogt <noah@noahvogt.com>
- # 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 <http://www.gnu.org/licenses/>.
- # 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()
|