VulcanBoard.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  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.png"
  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. if config.set_window_pos:
  52. Window.left = config.window_pos_x
  53. Window.top = config.window_pos_y
  54. if config.use_auto_fullscreen_mode:
  55. Window.fullscreen = "auto"
  56. kvConfig.set("kivy", "exit_on_escape", "0")
  57. self.button_config_map = {
  58. (btn["position"][0], btn["position"][1]): btn
  59. for btn in config.buttons
  60. }
  61. layout = GridLayout(
  62. cols=config.columns,
  63. rows=config.rows,
  64. spacing=config.spacing,
  65. padding=config.padding,
  66. )
  67. # Populate grid with buttons and placeholders
  68. for row in range(config.rows):
  69. for col in range(config.columns):
  70. defined_button = self.button_config_map.get((row, col))
  71. if defined_button:
  72. states = defined_button.get("states", [])
  73. state = get_state_from_id(states, DEFAULT_STATE_ID)
  74. btn = AutoResizeButton(
  75. text=state.get("txt", ""),
  76. background_color=get_color_from_hex(
  77. state.get("bg_color", DEFAULT_BUTTON_BG_COLOR)
  78. ),
  79. color=get_color_from_hex(
  80. state.get("fg_color", DEFAULT_BUTTON_FG_COLOR)
  81. ),
  82. halign="center",
  83. valign="middle",
  84. background_normal="",
  85. state_id=DEFAULT_STATE_ID,
  86. )
  87. if defined_button.get("autostart", False):
  88. self.async_task(
  89. self.execute_command_async(defined_button, btn)
  90. )
  91. # pylint: disable=no-member
  92. btn.bind( # pyright: ignore
  93. on_release=lambda btn_instance, button=defined_button: self.async_task(
  94. self.execute_command_async(button, btn_instance)
  95. )
  96. )
  97. else:
  98. btn = AutoResizeButton(
  99. background_color=get_color_from_hex(
  100. EMPTY_BUTTON_BG_COLOR
  101. ),
  102. )
  103. self.button_grid[(row, col)] = btn
  104. layout.add_widget(btn)
  105. return layout
  106. def async_task(self, coroutine):
  107. asyncio.run_coroutine_threadsafe(coroutine, self.loop)
  108. def ensure_asyncio_loop_running(self):
  109. if hasattr(self, "loop"):
  110. return self.loop # Already created
  111. loop = asyncio.new_event_loop()
  112. def run_loop():
  113. asyncio.set_event_loop(loop)
  114. loop.run_forever()
  115. threading.Thread(target=run_loop, daemon=True).start()
  116. return loop
  117. def config_error_exit(self, popup):
  118. popup.dismiss()
  119. sys.exit(1)
  120. async def execute_command_async(self, button: dict, btn: AutoResizeButton):
  121. follow_up_state_loop = True
  122. states = button["states"]
  123. while follow_up_state_loop:
  124. state = get_state_from_id(states, btn.state_id)
  125. follow_up_state_loop = False
  126. try:
  127. process = await asyncio.create_subprocess_shell(
  128. state["cmd"], shell=True
  129. )
  130. exit_code = await process.wait()
  131. log(f"Executed command: {state['cmd']}")
  132. except Exception as e:
  133. exit_code = ERROR_SINK_STATE_ID
  134. log(f"Error executing command: {e}", color="yellow")
  135. if len(states) != 1:
  136. if isinstance(
  137. follow_up_state := state.get("follow_up_state"), int
  138. ):
  139. follow_up_state_loop = True
  140. exit_code = follow_up_state
  141. Clock.schedule_once(
  142. lambda _: self.update_button_feedback(
  143. states, btn, exit_code # pyright: ignore
  144. )
  145. )
  146. affects_buttons = button.get("affects_buttons", None)
  147. if affects_buttons:
  148. for affected_btn_dims in affects_buttons:
  149. btn_pos = (affected_btn_dims[0], affected_btn_dims[1])
  150. affected_button = self.button_grid[btn_pos]
  151. Clock.schedule_once(
  152. lambda _, btn_pos=btn_pos, affected_button=affected_button: self.update_button_feedback(
  153. self.button_config_map[btn_pos]["states"],
  154. affected_button,
  155. exit_code, # pyright: ignore
  156. )
  157. )
  158. def update_button_feedback(
  159. self, states: list, btn: AutoResizeButton, exit_code: int
  160. ):
  161. state_id = get_state_id_from_exit_code(states, exit_code)
  162. state = get_state_from_id(states, state_id)
  163. btn.text = state.get("txt", "")
  164. btn.state_id = state_id
  165. btn.background_color = get_color_from_hex(
  166. state.get("bg_color", DEFAULT_BUTTON_BG_COLOR)
  167. )
  168. btn.color = get_color_from_hex(
  169. state.get("fg_color", DEFAULT_BUTTON_FG_COLOR)
  170. )
  171. def start_asyncio_loop():
  172. asyncio.set_event_loop(asyncio.new_event_loop())
  173. asyncio.get_event_loop().run_forever()
  174. if __name__ == "__main__":
  175. colorama.init()
  176. VulcanBoardApp().run()