VulcanBoard.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  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. contains_id,
  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.icon = "icon.jpg"
  43. config_loader = ConfigLoader(get_config_path())
  44. config = config_loader.get_config() # pyright: ignore
  45. if isinstance(config, str):
  46. error_exit_gui(config)
  47. else:
  48. config: Config = config
  49. Window.borderless = config.borderless
  50. kvConfig.set("kivy", "window_icon", "icon.ico")
  51. kvConfig.set("kivy", "exit_on_escape", "0")
  52. button_map = {
  53. (btn["position"][0], btn["position"][1]): btn
  54. for btn in config.buttons
  55. }
  56. layout = GridLayout(
  57. cols=config.columns,
  58. rows=config.rows,
  59. spacing=config.spacing,
  60. padding=config.padding,
  61. )
  62. # Populate grid with buttons and placeholders
  63. for row in range(config.rows):
  64. for col in range(config.columns):
  65. defined_button = button_map.get((row, col))
  66. if defined_button:
  67. states = defined_button.get("states", [])
  68. state_id = [DEFAULT_STATE_ID]
  69. state = get_state_from_id(states, state_id[0])
  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. )
  82. if defined_button.get("autostart", False):
  83. self.async_task(
  84. self.execute_command_async(
  85. states, state_id, btn
  86. )
  87. )
  88. # pylint: disable=no-member
  89. btn.bind( # pyright: ignore
  90. on_release=lambda btn_instance, states=states, state_id=state_id: self.async_task(
  91. self.execute_command_async(
  92. states, state_id, btn_instance
  93. )
  94. )
  95. )
  96. else:
  97. btn = AutoResizeButton(
  98. background_color=get_color_from_hex(
  99. EMPTY_BUTTON_BG_COLOR
  100. ),
  101. )
  102. layout.add_widget(btn)
  103. return layout
  104. def async_task(self, coroutine):
  105. asyncio.run_coroutine_threadsafe(coroutine, self.loop)
  106. def ensure_asyncio_loop_running(self):
  107. if hasattr(self, "loop"):
  108. return self.loop # Already created
  109. loop = asyncio.new_event_loop()
  110. def run_loop():
  111. asyncio.set_event_loop(loop)
  112. loop.run_forever()
  113. threading.Thread(target=run_loop, daemon=True).start()
  114. return loop
  115. def config_error_exit(self, popup):
  116. popup.dismiss()
  117. sys.exit(1)
  118. async def execute_command_async(
  119. self, states: list, state_id: list[int], btn: AutoResizeButton
  120. ):
  121. follow_up_state_loop = True
  122. while follow_up_state_loop:
  123. new_state_id = get_state_id_from_exit_code(states, state_id[0])
  124. state = get_state_from_id(states, new_state_id)
  125. follow_up_state_loop = False
  126. try:
  127. print(states[new_state_id]["cmd"])
  128. process = await asyncio.create_subprocess_shell(
  129. state["cmd"], shell=True
  130. )
  131. exit_code = await process.wait()
  132. print(f"EXIT {exit_code}")
  133. log(f"Executed command: {state['cmd']}")
  134. except Exception as e:
  135. exit_code = ERROR_SINK_STATE_ID
  136. log(f"Error executing command: {e}", color="yellow")
  137. if len(states) != 1:
  138. if isinstance(
  139. follow_up_state := state.get("follow_up_state"), int
  140. ):
  141. follow_up_state_loop = True
  142. exit_code = follow_up_state
  143. state_id[0] = exit_code # pyright: ignore
  144. Clock.schedule_once(
  145. lambda _: self.update_button_feedback(
  146. states, btn, exit_code # pyright: ignore
  147. )
  148. )
  149. def update_button_feedback(
  150. self, states: list, btn: AutoResizeButton, exit_code: int
  151. ):
  152. state = get_state_from_id(
  153. states, get_state_id_from_exit_code(states, exit_code)
  154. )
  155. btn.text = state.get("txt", "")
  156. btn.background_color = get_color_from_hex(
  157. state.get("bg_color", DEFAULT_BUTTON_BG_COLOR)
  158. )
  159. btn.color = get_color_from_hex(
  160. state.get("fg_color", DEFAULT_BUTTON_FG_COLOR)
  161. )
  162. def start_asyncio_loop():
  163. asyncio.set_event_loop(asyncio.new_event_loop())
  164. asyncio.get_event_loop().run_forever()
  165. if __name__ == "__main__":
  166. colorama.init()
  167. VulcanBoardApp().run()