VulcanBoard.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  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. from functools import partial
  18. import colorama
  19. from kivy.app import App
  20. from kivy.uix.gridlayout import GridLayout
  21. from kivy.utils import get_color_from_hex
  22. from kivy.config import Config as kvConfig
  23. from kivy.core.window import Window
  24. from kivy.clock import Clock
  25. from util import log, error_exit_gui
  26. from config import (
  27. get_config_path,
  28. ConfigLoader,
  29. Config,
  30. get_state_from_id,
  31. get_state_id_from_exit_code,
  32. DEFAULT_BUTTON_BG_COLOR,
  33. DEFAULT_BUTTON_FG_COLOR,
  34. EMPTY_BUTTON_BG_COLOR,
  35. DEFAULT_STATE_ID,
  36. ERROR_SINK_STATE_ID,
  37. )
  38. from ui import AutoResizeButton
  39. class VulcanBoardApp(App):
  40. def build(self):
  41. self.loop = self.ensure_asyncio_loop_running()
  42. self.button_grid = {}
  43. self.button_config_map = {}
  44. self.icon = "icon.jpg"
  45. config_loader = ConfigLoader(get_config_path())
  46. config = config_loader.get_config() # pyright: ignore
  47. if isinstance(config, str):
  48. error_exit_gui(config)
  49. else:
  50. config: Config = config
  51. Window.borderless = config.borderless
  52. kvConfig.set("kivy", "window_icon", "icon.ico")
  53. kvConfig.set("kivy", "exit_on_escape", "0")
  54. self.button_config_map = {
  55. (btn["position"][0], btn["position"][1]): btn
  56. for btn in config.buttons
  57. }
  58. layout = GridLayout(
  59. cols=config.columns,
  60. rows=config.rows,
  61. spacing=config.spacing,
  62. padding=config.padding,
  63. )
  64. # Populate grid with buttons and placeholders
  65. for row in range(config.rows):
  66. for col in range(config.columns):
  67. defined_button = self.button_config_map.get((row, col))
  68. if defined_button:
  69. states = defined_button.get("states", [])
  70. state_id = [DEFAULT_STATE_ID]
  71. state = get_state_from_id(states, state_id[0])
  72. btn = AutoResizeButton(
  73. text=state.get("txt", ""),
  74. background_color=get_color_from_hex(
  75. state.get("bg_color", DEFAULT_BUTTON_BG_COLOR)
  76. ),
  77. color=get_color_from_hex(
  78. state.get("fg_color", DEFAULT_BUTTON_FG_COLOR)
  79. ),
  80. halign="center",
  81. valign="middle",
  82. background_normal="",
  83. )
  84. if defined_button.get("autostart", False):
  85. self.async_task(
  86. self.execute_command_async(
  87. defined_button, state_id, btn
  88. )
  89. )
  90. # pylint: disable=no-member
  91. btn.bind( # pyright: ignore
  92. on_release=lambda btn_instance, button=defined_button, state_id=state_id: self.async_task(
  93. self.execute_command_async(
  94. button, state_id, btn_instance
  95. )
  96. )
  97. )
  98. else:
  99. btn = AutoResizeButton(
  100. background_color=get_color_from_hex(
  101. EMPTY_BUTTON_BG_COLOR
  102. ),
  103. )
  104. self.button_grid[(row, col)] = btn
  105. layout.add_widget(btn)
  106. return layout
  107. def async_task(self, coroutine):
  108. asyncio.run_coroutine_threadsafe(coroutine, self.loop)
  109. def ensure_asyncio_loop_running(self):
  110. if hasattr(self, "loop"):
  111. return self.loop # Already created
  112. loop = asyncio.new_event_loop()
  113. def run_loop():
  114. asyncio.set_event_loop(loop)
  115. loop.run_forever()
  116. threading.Thread(target=run_loop, daemon=True).start()
  117. return loop
  118. def config_error_exit(self, popup):
  119. popup.dismiss()
  120. sys.exit(1)
  121. async def execute_command_async(
  122. self, button: dict, state_id: list[int], btn: AutoResizeButton
  123. ):
  124. follow_up_state_loop = True
  125. states = button["states"]
  126. while follow_up_state_loop:
  127. new_state_id = get_state_id_from_exit_code(states, state_id[0])
  128. state = get_state_from_id(states, new_state_id)
  129. follow_up_state_loop = False
  130. try:
  131. process = await asyncio.create_subprocess_shell(
  132. state["cmd"], shell=True
  133. )
  134. exit_code = await process.wait()
  135. log(f"Executed command: {state['cmd']}")
  136. except Exception as e:
  137. exit_code = ERROR_SINK_STATE_ID
  138. log(f"Error executing command: {e}", color="yellow")
  139. if len(states) != 1:
  140. if isinstance(
  141. follow_up_state := state.get("follow_up_state"), int
  142. ):
  143. follow_up_state_loop = True
  144. exit_code = follow_up_state
  145. state_id[0] = exit_code # pyright: ignore
  146. Clock.schedule_once(
  147. lambda _: self.update_button_feedback(
  148. states, btn, exit_code # pyright: ignore
  149. )
  150. )
  151. affects_buttons = button.get("affects_buttons", None)
  152. if affects_buttons:
  153. for affected_btn_dims in affects_buttons:
  154. btn_pos = (affected_btn_dims[0], affected_btn_dims[1])
  155. affected_button = self.button_grid[btn_pos]
  156. # TODO: check if also works if cmd is not the same for each state
  157. Clock.schedule_once(
  158. lambda _, btn_pos=btn_pos, affected_button=affected_button: self.update_button_feedback(
  159. self.button_config_map[btn_pos]["states"],
  160. affected_button,
  161. exit_code, # pyright: ignore
  162. )
  163. )
  164. def update_button_feedback(
  165. self, states: list, btn: AutoResizeButton, exit_code: int
  166. ):
  167. state = get_state_from_id(
  168. states, get_state_id_from_exit_code(states, exit_code)
  169. )
  170. btn.text = state.get("txt", "")
  171. btn.background_color = get_color_from_hex(
  172. state.get("bg_color", DEFAULT_BUTTON_BG_COLOR)
  173. )
  174. btn.color = get_color_from_hex(
  175. state.get("fg_color", DEFAULT_BUTTON_FG_COLOR)
  176. )
  177. def start_asyncio_loop():
  178. asyncio.set_event_loop(asyncio.new_event_loop())
  179. asyncio.get_event_loop().run_forever()
  180. if __name__ == "__main__":
  181. colorama.init()
  182. VulcanBoardApp().run()