You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

418 lines
13 KiB

2 years ago
  1. import json
  2. from base64 import b64encode
  3. from typing import Optional, List, Dict, Any, Union, Callable
  4. from typing_extensions import TypedDict
  5. import wx
  6. from gooey.gui import events
  7. from gooey.gui.lang.i18n import _
  8. from gooey.python_bindings.types import GooeyParams, Item, Group, TopLevelParser, EnrichedItem, \
  9. FieldValue
  10. from gooey.util.functional import associn, assoc, associnMany, compact
  11. from gooey.gui.formatters import formatArgument
  12. from gooey.python_bindings.types import FormField
  13. from gooey.gui.constants import VALUE_PLACEHOLDER
  14. from gooey.gui.formatters import add_placeholder
  15. from gooey.python_bindings.types import CommandPieces, PublicGooeyState
  16. class TimingEvent(TypedDict):
  17. elapsed_time: Optional[str]
  18. estimatedRemaining: Optional[str]
  19. class ProgressEvent(TypedDict):
  20. progress: Optional[int]
  21. class ButtonState(TypedDict):
  22. id: str
  23. style: str
  24. label_id: str
  25. show: bool
  26. enabled: bool
  27. class ProgressState(TypedDict):
  28. show: bool
  29. range: int
  30. value: int
  31. class TimingState(TypedDict):
  32. show: bool
  33. elapsedTime: Optional[str]
  34. estimated_remaining: Optional[str]
  35. class GooeyState(GooeyParams):
  36. fetchingUpdate: bool
  37. screen: str
  38. title: str
  39. subtitle: str
  40. images: Dict[str, str]
  41. image: str
  42. buttons: List[ButtonState]
  43. progress: ProgressState
  44. timing: TimingState
  45. subcommands: List[str]
  46. activeSelection: int
  47. show_error_alert: bool
  48. class FullGooeyState(GooeyState):
  49. forms: Dict[str, List[FormField]]
  50. widgets: Dict[str, Dict[str, Any]]
  51. def extract_items(groups: List[Group]) -> List[Item]:
  52. if not groups:
  53. return []
  54. group = groups[0]
  55. return group['items'] \
  56. + extract_items(groups[1:]) \
  57. + extract_items(group['groups'])
  58. def widgets(descriptor: TopLevelParser) -> List[Item]:
  59. return extract_items(descriptor['contents'])
  60. def enrichValue(formState: List[FormField], items: List[Item]) -> List[EnrichedItem]:
  61. formIndex = {k['id']:k for k in formState}
  62. return [EnrichedItem(field=formIndex[item['id']], **item) for item in items] # type: ignore
  63. def positional(items: List[Union[Item, EnrichedItem]]):
  64. return [item for item in items if item['cli_type'] == 'positional']
  65. def optional(items: List[Union[Item, EnrichedItem]]):
  66. return [item for item in items if item['cli_type'] != 'positional']
  67. def cli_pieces(state: FullGooeyState, formatter=formatArgument) -> CommandPieces:
  68. parserName = state['subcommands'][state['activeSelection']]
  69. parserSpec = state['widgets'][parserName]
  70. formState = state['forms'][parserName]
  71. subcommand = parserSpec['command'] if parserSpec['command'] != '::gooey/default' else ''
  72. items = enrichValue(formState, widgets(parserSpec))
  73. positional_args = [formatter(item) for item in positional(items)] # type: ignore
  74. optional_args = [formatter(item) for item in optional(items)] # type: ignore
  75. ignoreFlag = '' if state['suppress_gooey_flag'] else '--ignore-gooey'
  76. return CommandPieces(
  77. target=state['target'],
  78. subcommand=subcommand,
  79. positionals=compact(positional_args),
  80. optionals=compact(optional_args),
  81. ignoreFlag=ignoreFlag
  82. )
  83. def activeFormState(state: FullGooeyState):
  84. subcommand = state['subcommands'][state['activeSelection']]
  85. return state['forms'][subcommand]
  86. def buildInvocationCmd(state: FullGooeyState):
  87. pieces = cli_pieces(state)
  88. return u' '.join(compact([
  89. pieces.target,
  90. pieces.subcommand,
  91. *pieces.optionals,
  92. pieces.ignoreFlag,
  93. '--' if pieces.positionals else '',
  94. *pieces.positionals]))
  95. def buildFormValidationCmd(state: FullGooeyState):
  96. pieces = cli_pieces(state, formatter=cmdOrPlaceholderOrNone)
  97. serializedForm = json.dumps({'active_form': activeFormState(state)})
  98. b64ecoded = b64encode(serializedForm.encode('utf-8'))
  99. return ' '.join(compact([
  100. pieces.target,
  101. pieces.subcommand,
  102. *pieces.optionals,
  103. '--gooey-validate-form',
  104. '--gooey-state ' + b64ecoded.decode('utf-8'),
  105. '--' if pieces.positionals else '',
  106. *pieces.positionals]))
  107. def buildOnCompleteCmd(state: FullGooeyState, was_success: bool):
  108. pieces = cli_pieces(state)
  109. serializedForm = json.dumps({'active_form': activeFormState(state)})
  110. b64ecoded = b64encode(serializedForm.encode('utf-8'))
  111. return u' '.join(compact([
  112. pieces.target,
  113. pieces.subcommand,
  114. *pieces.optionals,
  115. '--gooey-state ' + b64ecoded.decode('utf-8'),
  116. '--gooey-run-is-success' if was_success else '--gooey-run-is-failure',
  117. '--' if pieces.positionals else '',
  118. *pieces.positionals]))
  119. def buildOnSuccessCmd(state: FullGooeyState):
  120. return buildOnCompleteCmd(state, True)
  121. def buildOnErrorCmd(state: FullGooeyState):
  122. return buildOnCompleteCmd(state, False)
  123. def cmdOrPlaceholderOrNone(item: EnrichedItem) -> Optional[str]:
  124. # Argparse has a fail-fast-and-exit behavior for any missing
  125. # values. This poses a problem for dynamic validation, as we
  126. # want to collect _all_ errors to be more useful to the user.
  127. # As such, if there is no value currently available, we pass
  128. # through a stock placeholder values which allows GooeyParser
  129. # to handle it being missing without Argparse exploding due to
  130. # it actually being missing.
  131. if item['cli_type'] == 'positional':
  132. return formatArgument(item) or VALUE_PLACEHOLDER
  133. elif item['cli_type'] != 'positional' and item['required']:
  134. # same rationale applies here. We supply the argument
  135. # along with a fixed placeholder (when relevant i.e. `store`
  136. # actions)
  137. return formatArgument(item) or formatArgument(assoc(item, 'field', add_placeholder(item['field'])))
  138. else:
  139. # Optional values are, well, optional. So, like usual, we send
  140. # them if present or drop them if not.
  141. return formatArgument(item)
  142. def combine(state: GooeyState, params: GooeyParams, formState: List[FormField]) -> FullGooeyState:
  143. """
  144. I'm leaving the refactor of the form elements to another day.
  145. For now, we'll just merge in the state of the form fields as tracked
  146. in the UI into the main state blob as needed.
  147. """
  148. subcommand = list(params['widgets'].keys())[state['activeSelection']]
  149. return FullGooeyState(**{
  150. **state,
  151. **params,
  152. 'forms': {subcommand: formState}
  153. })
  154. def enable_buttons(state, to_enable: List[str]):
  155. updated = [{**btn, 'enabled': btn['label_id'] in to_enable}
  156. for btn in state['buttons']]
  157. return assoc(state, 'buttons', updated)
  158. def activeCommand(state, params: GooeyParams):
  159. """
  160. Retrieve the active sub-parser command as determined by the
  161. current selection.
  162. """
  163. return list(params['widgets'].keys())[state['activeSelection']]
  164. def mergeExternalState(state: FullGooeyState, extern: PublicGooeyState) -> FullGooeyState:
  165. # TODO: insane amounts of helpful validation
  166. subcommand = state['subcommands'][state['activeSelection']]
  167. formItems: List[FormField] = state['forms'][subcommand]
  168. hostForm: List[FormField] = extern['active_form']
  169. return associn(state, ['forms', subcommand], hostForm)
  170. def show_alert(state: FullGooeyState):
  171. return assoc(state, 'show_error_alert', True)
  172. def has_errors(state: FullGooeyState):
  173. """
  174. Searches through the form elements (including down into
  175. RadioGroup's internal options to find the presence of
  176. any errors.
  177. """
  178. return any([item['error'] or any(x['error'] for x in item.get('options', []))
  179. for items in state['forms'].values()
  180. for item in items])
  181. def initial_state(params: GooeyParams) -> GooeyState:
  182. buttons = [
  183. ('cancel', events.WINDOW_CANCEL, wx.ID_CANCEL),
  184. ('start', events.WINDOW_START, wx.ID_OK),
  185. ('stop', events.WINDOW_STOP, wx.ID_OK),
  186. ('edit', events.WINDOW_EDIT,wx.ID_OK),
  187. ('restart', events.WINDOW_RESTART, wx.ID_OK),
  188. ('close', events.WINDOW_CLOSE, wx.ID_OK),
  189. ]
  190. # helping out the type system
  191. params: Dict[str, Any] = params
  192. return GooeyState(
  193. **params,
  194. fetchingUpdate=False,
  195. screen='FORM',
  196. title=params['program_name'],
  197. subtitle=params['program_description'],
  198. image=params['images']['configIcon'],
  199. buttons=[ButtonState(
  200. id=event_id,
  201. style=style,
  202. label_id=label,
  203. show=label in ('cancel', 'start'),
  204. enabled=True)
  205. for label, event_id, style in buttons],
  206. progress=ProgressState(
  207. show=False,
  208. range=100,
  209. value=0 if params['progress_regex'] else -1
  210. ),
  211. timing=TimingState(
  212. show=False,
  213. elapsed_time=None,
  214. estimatedRemaining=None,
  215. ),
  216. show_error_alert=False,
  217. subcommands=list(params['widgets'].keys()),
  218. activeSelection=0
  219. )
  220. def header_props(state, params):
  221. return {
  222. 'background_color': params['header_bg_color'],
  223. 'title': params['program_name'],
  224. 'subtitle': params['program_description'],
  225. 'height': params['header_height'],
  226. 'image_uri': ims['images']['configIcon'],
  227. 'image_size': (six.MAXSIZE, params['header_height'] - 10)
  228. }
  229. def form_page(state):
  230. return {
  231. **state,
  232. 'buttons': [{**btn, 'show': btn['label_id'] in ('start', 'cancel')}
  233. for btn in state['buttons']]
  234. }
  235. def consoleScreen(_: Callable[[str], str], state: GooeyState):
  236. return {
  237. **state,
  238. 'screen': 'CONSOLE',
  239. 'title': _("running_title"),
  240. 'subtitle': _('running_msg'),
  241. 'image': state['images']['runningIcon'],
  242. 'buttons': [{**btn,
  243. 'show': btn['label_id'] == 'stop',
  244. 'enabled': True}
  245. for btn in state['buttons']],
  246. 'progress': {
  247. 'show': not state['disable_progress_bar_animation'],
  248. 'range': 100,
  249. 'value': 0 if state['progress_regex'] else -1
  250. },
  251. 'timing': {
  252. 'show': state['timing_options']['show_time_remaining'],
  253. 'elapsed_time': None,
  254. 'estimatedRemaining': None
  255. },
  256. 'show_error_alert': False
  257. }
  258. def editScreen(_: Callable[[str], str], state: FullGooeyState):
  259. use_buttons = ('cancel', 'start')
  260. return associnMany(
  261. state,
  262. ('screen', 'FORM'),
  263. ('buttons', [{**btn,
  264. 'show': btn['label_id'] in use_buttons,
  265. 'enabled': True}
  266. for btn in state['buttons']]),
  267. ('image', state['images']['configIcon']),
  268. ('title', state['program_name']),
  269. ('subtitle', state['program_description']))
  270. def beginUpdate(state: GooeyState):
  271. return {
  272. **enable_buttons(state, ['cancel']),
  273. 'fetchingUpdate': True
  274. }
  275. def finishUpdate(state: GooeyState):
  276. return {
  277. **enable_buttons(state, ['cancel', 'start']),
  278. 'fetchingUpdate': False
  279. }
  280. def finalScreen(_: Callable[[str], str], state: GooeyState) -> GooeyState:
  281. use_buttons = ('edit', 'restart', 'close')
  282. return associnMany(
  283. state,
  284. ('screen', 'CONSOLE'),
  285. ('buttons', [{**btn,
  286. 'show': btn['label_id'] in use_buttons,
  287. 'enabled': True}
  288. for btn in state['buttons']]),
  289. ('image', state['images']['successIcon']),
  290. ('title', _('finished_title')),
  291. ('subtitle', _('finished_msg')),
  292. ('progress.show', False),
  293. ('timing.show', not state['timing_options']['hide_time_remaining_on_complete']))
  294. def successScreen(_: Callable[[str], str], state: GooeyState) -> GooeyState:
  295. return associnMany(
  296. finalScreen(_, state),
  297. ('image', state['images']['successIcon']),
  298. ('title', _('finished_title')),
  299. ('subtitle', _('finished_msg')))
  300. def errorScreen(_: Callable[[str], str], state: GooeyState) -> GooeyState:
  301. return associnMany(
  302. finalScreen(_, state),
  303. ('image', state['images']['errorIcon']),
  304. ('title', _('finished_title')),
  305. ('subtitle', _('finished_error')))
  306. def interruptedScreen(_: Callable[[str], str], state: GooeyState):
  307. next_state = errorScreen(_, state) if state['force_stop_is_error'] else successScreen(_, state)
  308. return assoc(next_state, 'subtitle', _('finished_forced_quit'))
  309. def updateProgress(state, event: ProgressEvent):
  310. return associn(state, ['progress', 'value'], event['progress'] or 0)
  311. def updateTime(state, event):
  312. return associnMany(
  313. state,
  314. ('timing.elapsed_time', event['elapsed_time']),
  315. ('timing.estimatedRemaining', event['estimatedRemaining'])
  316. )
  317. def update_time(state, event: TimingEvent):
  318. return {
  319. **state,
  320. 'timer': {
  321. **state['timer'],
  322. 'elapsed_time': event['elapsed_time'],
  323. 'estimatedRemaining': event['estimatedRemaining']
  324. }
  325. }
  326. def present_time(timer):
  327. estimate_time_remaining = timer['estimatedRemaining']
  328. elapsed_time_value = timer['elapsed_time']
  329. if elapsed_time_value is None:
  330. return ''
  331. elif estimate_time_remaining is not None:
  332. return f'{elapsed_time_value}<{estimate_time_remaining}'
  333. else:
  334. return f'{elapsed_time_value}'