mirror of https://github.com/chriskiehl/Gooey.git
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
418 lines
13 KiB
import json
|
|
from base64 import b64encode
|
|
from typing import Optional, List, Dict, Any, Union, Callable
|
|
|
|
from typing_extensions import TypedDict
|
|
import wx
|
|
|
|
from gooey.gui import events
|
|
from gooey.gui.lang.i18n import _
|
|
from gooey.python_bindings.types import GooeyParams, Item, Group, TopLevelParser, EnrichedItem, \
|
|
FieldValue
|
|
from gooey.util.functional import associn, assoc, associnMany, compact
|
|
from gooey.gui.formatters import formatArgument
|
|
from gooey.python_bindings.types import FormField
|
|
from gooey.gui.constants import VALUE_PLACEHOLDER
|
|
from gooey.gui.formatters import add_placeholder
|
|
from gooey.python_bindings.types import CommandPieces, PublicGooeyState
|
|
|
|
|
|
class TimingEvent(TypedDict):
|
|
elapsed_time: Optional[str]
|
|
estimatedRemaining: Optional[str]
|
|
|
|
class ProgressEvent(TypedDict):
|
|
progress: Optional[int]
|
|
|
|
class ButtonState(TypedDict):
|
|
id: str
|
|
style: str
|
|
label_id: str
|
|
show: bool
|
|
enabled: bool
|
|
|
|
class ProgressState(TypedDict):
|
|
show: bool
|
|
range: int
|
|
value: int
|
|
|
|
class TimingState(TypedDict):
|
|
show: bool
|
|
elapsedTime: Optional[str]
|
|
estimated_remaining: Optional[str]
|
|
|
|
class GooeyState(GooeyParams):
|
|
fetchingUpdate: bool
|
|
screen: str
|
|
title: str
|
|
subtitle: str
|
|
images: Dict[str, str]
|
|
image: str
|
|
buttons: List[ButtonState]
|
|
progress: ProgressState
|
|
timing: TimingState
|
|
subcommands: List[str]
|
|
activeSelection: int
|
|
show_error_alert: bool
|
|
|
|
class FullGooeyState(GooeyState):
|
|
forms: Dict[str, List[FormField]]
|
|
widgets: Dict[str, Dict[str, Any]]
|
|
|
|
|
|
|
|
|
|
|
|
def extract_items(groups: List[Group]) -> List[Item]:
|
|
if not groups:
|
|
return []
|
|
group = groups[0]
|
|
return group['items'] \
|
|
+ extract_items(groups[1:]) \
|
|
+ extract_items(group['groups'])
|
|
|
|
|
|
def widgets(descriptor: TopLevelParser) -> List[Item]:
|
|
return extract_items(descriptor['contents'])
|
|
|
|
|
|
def enrichValue(formState: List[FormField], items: List[Item]) -> List[EnrichedItem]:
|
|
formIndex = {k['id']:k for k in formState}
|
|
return [EnrichedItem(field=formIndex[item['id']], **item) for item in items] # type: ignore
|
|
|
|
|
|
def positional(items: List[Union[Item, EnrichedItem]]):
|
|
return [item for item in items if item['cli_type'] == 'positional']
|
|
|
|
|
|
def optional(items: List[Union[Item, EnrichedItem]]):
|
|
return [item for item in items if item['cli_type'] != 'positional']
|
|
|
|
|
|
def cli_pieces(state: FullGooeyState, formatter=formatArgument) -> CommandPieces:
|
|
parserName = state['subcommands'][state['activeSelection']]
|
|
parserSpec = state['widgets'][parserName]
|
|
formState = state['forms'][parserName]
|
|
subcommand = parserSpec['command'] if parserSpec['command'] != '::gooey/default' else ''
|
|
items = enrichValue(formState, widgets(parserSpec))
|
|
positional_args = [formatter(item) for item in positional(items)] # type: ignore
|
|
optional_args = [formatter(item) for item in optional(items)] # type: ignore
|
|
ignoreFlag = '' if state['suppress_gooey_flag'] else '--ignore-gooey'
|
|
return CommandPieces(
|
|
target=state['target'],
|
|
subcommand=subcommand,
|
|
positionals=compact(positional_args),
|
|
optionals=compact(optional_args),
|
|
ignoreFlag=ignoreFlag
|
|
)
|
|
|
|
|
|
def activeFormState(state: FullGooeyState):
|
|
subcommand = state['subcommands'][state['activeSelection']]
|
|
return state['forms'][subcommand]
|
|
|
|
|
|
def buildInvocationCmd(state: FullGooeyState):
|
|
pieces = cli_pieces(state)
|
|
return u' '.join(compact([
|
|
pieces.target,
|
|
pieces.subcommand,
|
|
*pieces.optionals,
|
|
pieces.ignoreFlag,
|
|
'--' if pieces.positionals else '',
|
|
*pieces.positionals]))
|
|
|
|
|
|
def buildFormValidationCmd(state: FullGooeyState):
|
|
pieces = cli_pieces(state, formatter=cmdOrPlaceholderOrNone)
|
|
serializedForm = json.dumps({'active_form': activeFormState(state)})
|
|
b64ecoded = b64encode(serializedForm.encode('utf-8'))
|
|
return ' '.join(compact([
|
|
pieces.target,
|
|
pieces.subcommand,
|
|
*pieces.optionals,
|
|
'--gooey-validate-form',
|
|
'--gooey-state ' + b64ecoded.decode('utf-8'),
|
|
'--' if pieces.positionals else '',
|
|
*pieces.positionals]))
|
|
|
|
|
|
def buildOnCompleteCmd(state: FullGooeyState, was_success: bool):
|
|
pieces = cli_pieces(state)
|
|
serializedForm = json.dumps({'active_form': activeFormState(state)})
|
|
b64ecoded = b64encode(serializedForm.encode('utf-8'))
|
|
return u' '.join(compact([
|
|
pieces.target,
|
|
pieces.subcommand,
|
|
*pieces.optionals,
|
|
'--gooey-state ' + b64ecoded.decode('utf-8'),
|
|
'--gooey-run-is-success' if was_success else '--gooey-run-is-failure',
|
|
'--' if pieces.positionals else '',
|
|
*pieces.positionals]))
|
|
|
|
|
|
def buildOnSuccessCmd(state: FullGooeyState):
|
|
return buildOnCompleteCmd(state, True)
|
|
|
|
def buildOnErrorCmd(state: FullGooeyState):
|
|
return buildOnCompleteCmd(state, False)
|
|
|
|
|
|
def cmdOrPlaceholderOrNone(item: EnrichedItem) -> Optional[str]:
|
|
# Argparse has a fail-fast-and-exit behavior for any missing
|
|
# values. This poses a problem for dynamic validation, as we
|
|
# want to collect _all_ errors to be more useful to the user.
|
|
# As such, if there is no value currently available, we pass
|
|
# through a stock placeholder values which allows GooeyParser
|
|
# to handle it being missing without Argparse exploding due to
|
|
# it actually being missing.
|
|
if item['cli_type'] == 'positional':
|
|
return formatArgument(item) or VALUE_PLACEHOLDER
|
|
elif item['cli_type'] != 'positional' and item['required']:
|
|
# same rationale applies here. We supply the argument
|
|
# along with a fixed placeholder (when relevant i.e. `store`
|
|
# actions)
|
|
return formatArgument(item) or formatArgument(assoc(item, 'field', add_placeholder(item['field'])))
|
|
else:
|
|
# Optional values are, well, optional. So, like usual, we send
|
|
# them if present or drop them if not.
|
|
return formatArgument(item)
|
|
|
|
|
|
|
|
|
|
def combine(state: GooeyState, params: GooeyParams, formState: List[FormField]) -> FullGooeyState:
|
|
"""
|
|
I'm leaving the refactor of the form elements to another day.
|
|
For now, we'll just merge in the state of the form fields as tracked
|
|
in the UI into the main state blob as needed.
|
|
"""
|
|
subcommand = list(params['widgets'].keys())[state['activeSelection']]
|
|
return FullGooeyState(**{
|
|
**state,
|
|
**params,
|
|
'forms': {subcommand: formState}
|
|
})
|
|
|
|
|
|
def enable_buttons(state, to_enable: List[str]):
|
|
updated = [{**btn, 'enabled': btn['label_id'] in to_enable}
|
|
for btn in state['buttons']]
|
|
return assoc(state, 'buttons', updated)
|
|
|
|
|
|
|
|
def activeCommand(state, params: GooeyParams):
|
|
"""
|
|
Retrieve the active sub-parser command as determined by the
|
|
current selection.
|
|
"""
|
|
return list(params['widgets'].keys())[state['activeSelection']]
|
|
|
|
|
|
def mergeExternalState(state: FullGooeyState, extern: PublicGooeyState) -> FullGooeyState:
|
|
# TODO: insane amounts of helpful validation
|
|
subcommand = state['subcommands'][state['activeSelection']]
|
|
formItems: List[FormField] = state['forms'][subcommand]
|
|
hostForm: List[FormField] = extern['active_form']
|
|
return associn(state, ['forms', subcommand], hostForm)
|
|
|
|
|
|
def show_alert(state: FullGooeyState):
|
|
return assoc(state, 'show_error_alert', True)
|
|
|
|
def has_errors(state: FullGooeyState):
|
|
"""
|
|
Searches through the form elements (including down into
|
|
RadioGroup's internal options to find the presence of
|
|
any errors.
|
|
"""
|
|
return any([item['error'] or any(x['error'] for x in item.get('options', []))
|
|
for items in state['forms'].values()
|
|
for item in items])
|
|
|
|
|
|
def initial_state(params: GooeyParams) -> GooeyState:
|
|
buttons = [
|
|
('cancel', events.WINDOW_CANCEL, wx.ID_CANCEL),
|
|
('start', events.WINDOW_START, wx.ID_OK),
|
|
('stop', events.WINDOW_STOP, wx.ID_OK),
|
|
('edit', events.WINDOW_EDIT,wx.ID_OK),
|
|
('restart', events.WINDOW_RESTART, wx.ID_OK),
|
|
('close', events.WINDOW_CLOSE, wx.ID_OK),
|
|
]
|
|
# helping out the type system
|
|
params: Dict[str, Any] = params
|
|
return GooeyState(
|
|
**params,
|
|
fetchingUpdate=False,
|
|
screen='FORM',
|
|
title=params['program_name'],
|
|
subtitle=params['program_description'],
|
|
image=params['images']['configIcon'],
|
|
buttons=[ButtonState(
|
|
id=event_id,
|
|
style=style,
|
|
label_id=label,
|
|
show=label in ('cancel', 'start'),
|
|
enabled=True)
|
|
for label, event_id, style in buttons],
|
|
progress=ProgressState(
|
|
show=False,
|
|
range=100,
|
|
value=0 if params['progress_regex'] else -1
|
|
),
|
|
timing=TimingState(
|
|
show=False,
|
|
elapsed_time=None,
|
|
estimatedRemaining=None,
|
|
),
|
|
show_error_alert=False,
|
|
subcommands=list(params['widgets'].keys()),
|
|
activeSelection=0
|
|
)
|
|
|
|
def header_props(state, params):
|
|
return {
|
|
'background_color': params['header_bg_color'],
|
|
'title': params['program_name'],
|
|
'subtitle': params['program_description'],
|
|
'height': params['header_height'],
|
|
'image_uri': ims['images']['configIcon'],
|
|
'image_size': (six.MAXSIZE, params['header_height'] - 10)
|
|
}
|
|
|
|
|
|
def form_page(state):
|
|
return {
|
|
**state,
|
|
'buttons': [{**btn, 'show': btn['label_id'] in ('start', 'cancel')}
|
|
for btn in state['buttons']]
|
|
}
|
|
|
|
|
|
def consoleScreen(_: Callable[[str], str], state: GooeyState):
|
|
return {
|
|
**state,
|
|
'screen': 'CONSOLE',
|
|
'title': _("running_title"),
|
|
'subtitle': _('running_msg'),
|
|
'image': state['images']['runningIcon'],
|
|
'buttons': [{**btn,
|
|
'show': btn['label_id'] == 'stop',
|
|
'enabled': True}
|
|
for btn in state['buttons']],
|
|
'progress': {
|
|
'show': not state['disable_progress_bar_animation'],
|
|
'range': 100,
|
|
'value': 0 if state['progress_regex'] else -1
|
|
},
|
|
'timing': {
|
|
'show': state['timing_options']['show_time_remaining'],
|
|
'elapsed_time': None,
|
|
'estimatedRemaining': None
|
|
},
|
|
'show_error_alert': False
|
|
}
|
|
|
|
|
|
def editScreen(_: Callable[[str], str], state: FullGooeyState):
|
|
use_buttons = ('cancel', 'start')
|
|
return associnMany(
|
|
state,
|
|
('screen', 'FORM'),
|
|
('buttons', [{**btn,
|
|
'show': btn['label_id'] in use_buttons,
|
|
'enabled': True}
|
|
for btn in state['buttons']]),
|
|
('image', state['images']['configIcon']),
|
|
('title', state['program_name']),
|
|
('subtitle', state['program_description']))
|
|
|
|
|
|
def beginUpdate(state: GooeyState):
|
|
return {
|
|
**enable_buttons(state, ['cancel']),
|
|
'fetchingUpdate': True
|
|
}
|
|
|
|
def finishUpdate(state: GooeyState):
|
|
return {
|
|
**enable_buttons(state, ['cancel', 'start']),
|
|
'fetchingUpdate': False
|
|
}
|
|
|
|
|
|
def finalScreen(_: Callable[[str], str], state: GooeyState) -> GooeyState:
|
|
use_buttons = ('edit', 'restart', 'close')
|
|
return associnMany(
|
|
state,
|
|
('screen', 'CONSOLE'),
|
|
('buttons', [{**btn,
|
|
'show': btn['label_id'] in use_buttons,
|
|
'enabled': True}
|
|
for btn in state['buttons']]),
|
|
('image', state['images']['successIcon']),
|
|
('title', _('finished_title')),
|
|
('subtitle', _('finished_msg')),
|
|
('progress.show', False),
|
|
('timing.show', not state['timing_options']['hide_time_remaining_on_complete']))
|
|
|
|
|
|
def successScreen(_: Callable[[str], str], state: GooeyState) -> GooeyState:
|
|
return associnMany(
|
|
finalScreen(_, state),
|
|
('image', state['images']['successIcon']),
|
|
('title', _('finished_title')),
|
|
('subtitle', _('finished_msg')))
|
|
|
|
|
|
def errorScreen(_: Callable[[str], str], state: GooeyState) -> GooeyState:
|
|
return associnMany(
|
|
finalScreen(_, state),
|
|
('image', state['images']['errorIcon']),
|
|
('title', _('finished_title')),
|
|
('subtitle', _('finished_error')))
|
|
|
|
|
|
def interruptedScreen(_: Callable[[str], str], state: GooeyState):
|
|
next_state = errorScreen(_, state) if state['force_stop_is_error'] else successScreen(_, state)
|
|
return assoc(next_state, 'subtitle', _('finished_forced_quit'))
|
|
|
|
|
|
def updateProgress(state, event: ProgressEvent):
|
|
return associn(state, ['progress', 'value'], event['progress'] or 0)
|
|
|
|
|
|
def updateTime(state, event):
|
|
return associnMany(
|
|
state,
|
|
('timing.elapsed_time', event['elapsed_time']),
|
|
('timing.estimatedRemaining', event['estimatedRemaining'])
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_time(state, event: TimingEvent):
|
|
return {
|
|
**state,
|
|
'timer': {
|
|
**state['timer'],
|
|
'elapsed_time': event['elapsed_time'],
|
|
'estimatedRemaining': event['estimatedRemaining']
|
|
}
|
|
}
|
|
|
|
|
|
def present_time(timer):
|
|
estimate_time_remaining = timer['estimatedRemaining']
|
|
elapsed_time_value = timer['elapsed_time']
|
|
if elapsed_time_value is None:
|
|
return ''
|
|
elif estimate_time_remaining is not None:
|
|
return f'{elapsed_time_value}<{estimate_time_remaining}'
|
|
else:
|
|
return f'{elapsed_time_value}'
|