VulcanBoard.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. #!/usr/bin/env python3
  2. # Copyright © 2024 Noah Vogt <noah@noahvogt.com>
  3. # This program is free software: you can redistribute it and/or modify
  4. # it under the terms of the GNU General Public License as published by
  5. # the Free Software Foundation, either version 3 of the License, or
  6. # (at your option) any later version.
  7. # This program is distributed in the hope that it will be useful,
  8. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. # GNU General Public License for more details.
  11. # You should have received a copy of the GNU General Public License
  12. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. # pylint: disable=invalid-name
  14. import threading
  15. import sys
  16. import asyncio
  17. import colorama
  18. from kivy.app import App
  19. from kivy.uix.gridlayout import GridLayout
  20. from kivy.utils import get_color_from_hex
  21. from kivy.config import Config as kvConfig
  22. from kivy.core.window import Window
  23. from kivy.clock import Clock
  24. from util import log, error_exit_gui
  25. from config import (
  26. get_config_path,
  27. ConfigLoader,
  28. Config,
  29. get_state_from_id,
  30. get_state_id_from_exit_code,
  31. DEFAULT_BUTTON_BG_COLOR,
  32. DEFAULT_BUTTON_FG_COLOR,
  33. EMPTY_BUTTON_BG_COLOR,
  34. DEFAULT_STATE_ID,
  35. ERROR_SINK_STATE_ID,
  36. )
  37. from ui import AutoResizeButton
  38. class VulcanBoardApp(App):
  39. def build(self):
  40. self.loop = self.ensure_asyncio_loop_running()
  41. self.button_grid = {}
  42. self.button_config_map = {}
  43. self.icon = "icon.jpg"
  44. config_loader = ConfigLoader(get_config_path())
  45. config = config_loader.get_config() # pyright: ignore
  46. if isinstance(config, str):
  47. error_exit_gui(config)
  48. else:
  49. config: Config = config
  50. Window.borderless = config.borderless
  51. kvConfig.set("kivy", "window_icon", "icon.ico")
  52. kvConfig.set("kivy", "exit_on_escape", "0")
  53. self.button_config_map = {
  54. (btn["position"][0], btn["position"][1]): btn
  55. for btn in config.buttons
  56. }
  57. layout = GridLayout(
  58. cols=config.columns,
  59. rows=config.rows,
  60. spacing=config.spacing,
  61. padding=config.padding,
  62. )
  63. # Populate grid with buttons and placeholders
  64. for row in range(config.rows):
  65. for col in range(config.columns):
  66. defined_button = self.button_config_map.get((row, col))
  67. if defined_button:
  68. states = defined_button.get("states", [])
  69. state = get_state_from_id(states, DEFAULT_STATE_ID)
  70. btn = AutoResizeButton(
  71. text=state.get("txt", ""),
  72. background_color=get_color_from_hex(
  73. state.get("bg_color", DEFAULT_BUTTON_BG_COLOR)
  74. ),
  75. color=get_color_from_hex(
  76. state.get("fg_color", DEFAULT_BUTTON_FG_COLOR)
  77. ),
  78. halign="center",
  79. valign="middle",
  80. background_normal="",
  81. state_id=DEFAULT_STATE_ID,
  82. )
  83. if defined_button.get("autostart", False):
  84. self.async_task(
  85. self.execute_command_async(defined_button, btn)
  86. )
  87. # pylint: disable=no-member
  88. btn.bind( # pyright: ignore
  89. on_release=lambda btn_instance, button=defined_button: self.async_task(
  90. self.execute_command_async(button, btn_instance)
  91. )
  92. )
  93. else:
  94. btn = AutoResizeButton(
  95. background_color=get_color_from_hex(
  96. EMPTY_BUTTON_BG_COLOR
  97. ),
  98. )
  99. self.button_grid[(row, col)] = btn
  100. layout.add_widget(btn)
  101. return layout
  102. def async_task(self, coroutine):
  103. asyncio.run_coroutine_threadsafe(coroutine, self.loop)
  104. def ensure_asyncio_loop_running(self):
  105. if hasattr(self, "loop"):
  106. return self.loop # Already created
  107. loop = asyncio.new_event_loop()
  108. def run_loop():
  109. asyncio.set_event_loop(loop)
  110. loop.run_forever()
  111. threading.Thread(target=run_loop, daemon=True).start()
  112. return loop
  113. def config_error_exit(self, popup):
  114. popup.dismiss()
  115. sys.exit(1)
  116. async def execute_command_async(self, button: dict, btn: AutoResizeButton):
  117. follow_up_state_loop = True
  118. states = button["states"]
  119. while follow_up_state_loop:
  120. state = get_state_from_id(states, btn.state_id)
  121. follow_up_state_loop = False
  122. try:
  123. process = await asyncio.create_subprocess_shell(
  124. state["cmd"], shell=True
  125. )
  126. exit_code = await process.wait()
  127. log(f"Executed command: {state['cmd']}")
  128. except Exception as e:
  129. exit_code = ERROR_SINK_STATE_ID
  130. log(f"Error executing command: {e}", color="yellow")
  131. if len(states) != 1:
  132. if isinstance(
  133. follow_up_state := state.get("follow_up_state"), int
  134. ):
  135. follow_up_state_loop = True
  136. exit_code = follow_up_state
  137. Clock.schedule_once(
  138. lambda _: self.update_button_feedback(
  139. states, btn, exit_code # pyright: ignore
  140. )
  141. )
  142. affects_buttons = button.get("affects_buttons", None)
  143. if affects_buttons:
  144. for affected_btn_dims in affects_buttons:
  145. btn_pos = (affected_btn_dims[0], affected_btn_dims[1])
  146. affected_button = self.button_grid[btn_pos]
  147. Clock.schedule_once(
  148. lambda _, btn_pos=btn_pos, affected_button=affected_button: self.update_button_feedback(
  149. self.button_config_map[btn_pos]["states"],
  150. affected_button,
  151. exit_code, # pyright: ignore
  152. )
  153. )
  154. def update_button_feedback(
  155. self, states: list, btn: AutoResizeButton, exit_code: int
  156. ):
  157. state_id = get_state_id_from_exit_code(states, exit_code)
  158. state = get_state_from_id(states, state_id)
  159. btn.text = state.get("txt", "")
  160. btn.state_id = state_id
  161. btn.background_color = get_color_from_hex(
  162. state.get("bg_color", DEFAULT_BUTTON_BG_COLOR)
  163. )
  164. btn.color = get_color_from_hex(
  165. state.get("fg_color", DEFAULT_BUTTON_FG_COLOR)
  166. )
  167. def start_asyncio_loop():
  168. asyncio.set_event_loop(asyncio.new_event_loop())
  169. asyncio.get_event_loop().run_forever()
  170. if __name__ == "__main__":
  171. colorama.init()
  172. VulcanBoardApp().run()