File: pip_deploy.py

import subprocess

subprocess.call('python setup.py sdist')
subprocess.call('python setup.py bdist_wheel --universal')
subprocess.call('twine upload dist/*')



File: setup.py

"""Script for setuptools."""
import sys
from setuptools import setup, find_packages


with open('README.md') as readme:
    long_description = readme.read()

version = '1.2.0-ALPHA'

deps = [
    'Pillow>=4.3.0',
    'psutil>=5.4.2',
    'colored>=1.3.93',
    'pygtrie>=2.3.3',
    're-wx>=0.0.9',
    'typing-extensions==3.10.0.2',
    'wxpython>=4.1.0',
    "dataclasses>=0.8;python_version<'3.7'",
]

setup(
    name='Gooey',
    version=version,
    url='http://pypi.python.org/pypi/Gooey/',
    author='Chris Kiehl',
    author_email='audionautic@gmail.com',
    description=('Turn (almost) any command line program into a full GUI '
                 'application with one line'),
    license='MIT',
    python_requires='>=3.6',
    packages=find_packages(),
    install_requires=deps,
    include_package_data=True,
    dependency_links = ["http://www.wxpython.org/download.php"],
    classifiers = [
        'Development Status :: 4 - Beta',
        'Intended Audience :: Developers',
        'Topic :: Desktop Environment',
        'Topic :: Software Development :: Build Tools',
        'Topic :: Software Development :: Widget Sets',
        'Programming Language :: Python :: 3',
        'License :: OSI Approved :: MIT License'
    ],
    long_description='''

Gooey (Beta)
############


Turn (almost) any Python Console Program into a GUI application with one line
-----------------------------------------------------------------------------

.. image:: https://cloud.githubusercontent.com/assets/1408720/7904381/f54f97f6-07c5-11e5-9bcb-c3c102920769.png


Quick Start
-----------

Gooey is attached to your code via a simple decorator on your `main` method.

.. code-block::

  from gooey import Gooey
  @Gooey      <--- all it takes! :)
  def main():
      # rest of code



With the decorator attached, run your program and the GUI will now appear!

Checkout the full documentation, instructions, and source on `Github <https://github.com/chriskiehl/Gooey>`_'''
)



File: gooey/__init__.py

import os
from gooey.python_bindings.gooey_decorator import Gooey
from gooey.python_bindings.gooey_parser import GooeyParser
from gooey.gui.util.freeze import localResourcePath as local_resource_path
from gooey.python_bindings import constants
from gooey.python_bindings.constants import Events
from gooey.gui.components.filtering.prefix_filter import PrefixTokenizers
from gooey.gui.components.options import options
from gooey.python_bindings import types
types = types
__version__ = '1.2.0-ALPHA'



File: gooey/__main__.py

# '''
# Delegates arguments to the main Gooey runner
#
# For use when run directly from command line with the -m (module) flag:
#
#   e.g. $ python -m gooey
#
# '''
#
# from gooey import application
#
# application.main()



File: gooey/util/__init__.py




File: gooey/util/functional.py

"""
A collection of functional utilities/helpers
"""
from functools import reduce, wraps
from copy import deepcopy
from itertools import chain, dropwhile
from typing import Tuple, Any, List, Union

from gooey.python_bindings.types import Try, Success, Failure


def getin(m, path, default=None):
    """returns the value in a nested dict"""
    keynotfound = ':com.gooey-project/not-found'
    result = reduce(lambda acc, val: acc.get(val, {keynotfound: None}), path, m)
    # falsey values like 0 would incorrectly trigger the default to be returned
    # so the keynotfound val is used to signify a miss vs just a falesy val
    if isinstance(result, dict) and keynotfound in result:
        return default
    return result


def assoc(m, key, val):
    """Copy-on-write associates a value in a dict"""
    cpy = deepcopy(m)
    cpy[key] = val
    return cpy

def dissoc(m, key, val):
    cpy = deepcopy(m)
    del cpy[key]
    return cpy

def associn(m, path, value):
    """ Copy-on-write associates a value in a nested dict """
    def assoc_recursively(m, path, value):
        if not path:
            return value
        p = path[0]
        return assoc(m, p, assoc_recursively(m.get(p,{}), path[1:], value))
    return assoc_recursively(m, path, value)


def associnMany(m, *args: Tuple[Union[str, List[str]], Any]):
    def apply(_m, change: Tuple[Union[str, List[str]], Any]):
        path, value = change
        if isinstance(path, list):
            return associn(_m, path, value)
        else:
            return associn(_m, path.split('.'), value)
    return reduce(apply, args, m)



def merge(*maps):
    """Merge all maps left to right"""
    copies = map(deepcopy, maps)
    return reduce(lambda acc, val: acc.update(val) or acc, copies)


def flatmap(f, coll):
    """Applies concat to the result of applying f to colls"""
    return list(chain(*map(f, coll)))


def indexunique(f, coll):
    """Build a map from the collection keyed off of f
    e.g.
        [{id:1,..}, {id:2, ...}] => {1: {id:1,...}, 2: {id:2,...}}

    Note: duplicates, if present, are overwritten
    """
    return zipmap(map(f, coll), coll)


def zipmap(keys, vals):
    """Return a map from keys to values"""
    return dict(zip(keys, vals))


def compact(coll):
    """Returns a new list with all falsy values removed"""
    if isinstance(coll, dict):
        return {k:v for k,v in coll.items() if v is not None}
    else:
        return list(filter(None, coll))


def ifPresent(f):
    """Execute f only if value is present and not None"""
    def inner(value):
        if value:
            return f(value)
        else:
            return True
    return inner


def identity(x):
    """Identity function always returns the supplied argument"""
    return x


def unit(val):
    return val


def bind(val, f):
    return f(val) if val else None


def lift(f):
    @wraps(f)
    def inner(x) -> Try:
        try:
            return Success(f(x))
        except Exception as e:
            return Failure(e)
    return inner




File: gooey/images/__init__.py




File: gooey/languages/__init__.py




File: gooey/python_bindings/argparse_to_json.py

"""
Converts argparse parser actions into json "Build Specs"
"""
import argparse
import json
import os
import sys
from argparse import (
    _CountAction,
    _HelpAction,
    _StoreConstAction,
    _StoreFalseAction,
    _StoreTrueAction,
    _StoreAction,
    _SubParsersAction,
    _VersionAction, _MutuallyExclusiveGroup)
from collections import OrderedDict
from functools import partial
from uuid import uuid4

from gooey.python_bindings.gooey_parser import GooeyParser
from gooey.util.functional import merge, getin, identity, assoc
from gooey.gui.components.options.validators import validators
from gooey.gui.components.options.validators import collect_errors

VALID_WIDGETS = (
    'FileChooser',
    'MultiFileChooser',
    'FileSaver',
    'DirChooser',
    'DateChooser',
    'TimeChooser',
    'TextField',
    'Dropdown',
    'Counter',
    'RadioGroup',
    'CheckBox',
    'BlockCheckbox',
    'MultiDirChooser',
    'Textarea',
    'PasswordField',
    'Listbox',
    'FilterableDropdown',
    'IntegerField',
    'DecimalField',
    'Slider'
)


# TODO: validate Listbox. When required, nargs must be +

class UnknownWidgetType(Exception):
    pass


class UnsupportedConfiguration(Exception):
    pass



# TODO: merge the default foreground and bg colors from the
# baseline build_spec
item_default = {
    'error_color': '#ea7878',
    'label_color': '#000000',
    'help_color': '#363636',
    'full_width': False,
    'validator': {
        'type': 'local',
        'test': 'lambda x: True',
        'message': ''
    },
    'external_validator': {
        'cmd': '',
    }
}


def convert(parser, **kwargs):
    """
    Converts a parser into a JSON representation

    TODO:
    This is in desperate need of refactor. It wasn't build with supporting
    all (or any) of this configuration in mind. The use of global defaults
    are actively getting in the way of easily adding more configuration options.

    Pain points:
        - global data sprinkled throughout the calls
        - local data threaded through calls
        - totally unclear what the data structures even hold
        - everything is just mushed together and gross. unwinding argparse also
          builds validators, handles coercion, and so on...
        - converts to an entirely bespoke json mini-language that mirrors
          the internal structure of argparse.
    Refactor plan:
        - Investigate restructuring the core data representation. As is, it is ad-hoc
          and largely tied to argparse's goofy internal structure. May be worth moving to
          something "standard." Though, not sure what the options are.
        - standardize how these things read from the environment. No global in some local in others.
        - Investigate splitting the whole thing into phases (ala Ring). Current thinking is that
          a lot of this stuff could be modelled more like pluggable upgrades to the base structure.
        - I want to add a helpful validation stage to catch user errors like invalid gooey_options
    """

    group_defaults = {
        'legacy': {
            'required_cols': kwargs['required_cols'],
            'optional_cols': kwargs['optional_cols']
        },
        'columns': 2,
        'padding': 10,
        'show_border': False
    }

    assert_subparser_constraints(parser)
    x = {
        'layout': 'standard',
        'widgets': OrderedDict(
            (choose_name(name, sub_parser), {
                'command': name,
                'name': choose_name(name, sub_parser),
                'help': get_subparser_help(sub_parser),
                'description': '',
                'contents': process(sub_parser,
                                    getattr(sub_parser, 'widgets', {}),
                                    getattr(sub_parser, 'options', {}),
                                    group_defaults)
            }) for name, sub_parser in iter_parsers(parser))
    }

    if kwargs.get('use_legacy_titles'):
        return apply_default_rewrites(x)
    return x


def process(parser, widget_dict, options, group_defaults):
    mutex_groups = parser._mutually_exclusive_groups
    raw_action_groups = [extract_groups(group, group_defaults) for group in parser._action_groups
                         if group._group_actions]
    corrected_action_groups = reapply_mutex_groups(mutex_groups, raw_action_groups)

    return categorize2(strip_empty(corrected_action_groups), widget_dict, options)


def strip_empty(groups):
    return [group for group in groups if group['items']]


def assert_subparser_constraints(parser):
    if has_subparsers(parser._actions):
        if has_required(parser._actions):
            raise UnsupportedConfiguration(
                "Gooey doesn't currently support top level required arguments "
                "when subparsers are present.")


def iter_parsers(parser):
    ''' Iterate over name, parser pairs '''
    try:
        return get_subparser(parser._actions).choices.items()
    except:
        return iter([('::gooey/default', parser)])


def get_subparser_help(parser):
    if isinstance(parser, GooeyParser):
        return getattr(parser.parser, 'usage', '')
    else:
        return getattr(parser, 'usage', '')


def extract_groups(action_group, group_defaults):
    '''
    Recursively extract argument groups and associated actions
    from ParserGroup objects
    '''
    return {
        'name': action_group.title,
        'description': action_group.description,
        'items': [action for action in action_group._group_actions
                  if not is_help_message(action)],
        'groups': [extract_groups(group, group_defaults)
                   for group in action_group._action_groups],
        'options': handle_option_merge(
            group_defaults,
            getattr(action_group, 'gooey_options', {}),
            action_group.title)
    }


def handle_option_merge(group_defaults, incoming_options, title):
    """
    Merges a set of group defaults with incoming options.

    A bunch of ceremony here is to ensure backwards compatibility with the old
    num_required_cols and num_optional_cols decorator args. They are used as
    the seed values for the new group defaults which keeps the old behavior
    _mostly_ in tact.

    Known failure points:
        * Using custom groups / names. No 'positional arguments' group
          means no required_cols arg being honored
        * Non-positional args marked as required. It would take group
          shuffling along the lines of that required to make
          mutually exclusive groups show in the correct place. In short, not
          worth the complexity for a legacy feature that's been succeeded by
          a much more powerful alternative.
    """
    if title == 'positional arguments':
        # the argparse default 'required' bucket
        req_cols = getin(group_defaults, ['legacy', 'required_cols'], 2)
        new_defaults = assoc(group_defaults, 'columns', req_cols)
        return merge(new_defaults, incoming_options)
    else:
        opt_cols = getin(group_defaults, ['legacy', 'optional_cols'], 2)
        new_defaults = assoc(group_defaults, 'columns', opt_cols)
        return merge(new_defaults, incoming_options)



def apply_default_rewrites(spec):
    top_level_subgroups = list(spec['widgets'].keys())

    for subgroup in top_level_subgroups:
        path = ['widgets', subgroup, 'contents']
        contents = getin(spec, path)
        for group in contents:
            if group['name'] == 'positional arguments':
                group['name'] = 'required_args_msg'
            if group['name'] == 'optional arguments':
                group['name'] = 'optional_args_msg'
    return spec


def contains_actions(a, b):
    ''' check if any actions(a) are present in actions(b) '''
    return set(a).intersection(set(b))


def reapply_mutex_groups(mutex_groups, action_groups):
    # argparse stores mutually exclusive groups independently
    # of all other groups. So, they must be manually re-combined
    # with the groups/subgroups to which they were originally declared
    # in order to have them appear in the correct location in the UI.
    #
    # Order is attempted to be preserved by inserting the MutexGroup
    # into the _actions list at the first occurrence of any item
    # where the two groups intersect
    def swap_actions(actions):
        for mutexgroup in mutex_groups:
            mutex_actions = mutexgroup._group_actions
            if contains_actions(mutex_actions, actions):
                # make a best guess as to where we should store the group
                targetindex = actions.index(mutexgroup._group_actions[0])
                # insert the _ArgumentGroup container
                actions[targetindex] = mutexgroup
                # remove the duplicated individual actions
                actions = [action for action in actions
                        if action not in mutex_actions]
        return actions

    return [group.update({'items': swap_actions(group['items'])}) or group
            for group in action_groups]


def categorize2(groups, widget_dict, options):
    defaults = {'label_color': '#000000', 'description_color': '#363636'}
    return [{
        'name': group['name'],
        'items': list(categorize(group['items'], widget_dict, options)),
        'groups': categorize2(group['groups'], widget_dict, options),
        'description': group['description'],
        'options': merge(defaults ,group['options'])
    } for group in groups]


def categorize(actions, widget_dict, options):
    _get_widget = partial(get_widget, widget_dict)
    for action in actions:
        if is_version(action):
            yield action_to_json(action, _get_widget(action, 'CheckBox'), options)

        elif is_mutex(action):
            yield build_radio_group(action, widget_dict, options)

        elif is_standard(action):
            yield action_to_json(action, _get_widget(action, 'TextField'), options)
        
        elif is_writemode_file(action):
            yield action_to_json(action, _get_widget(action, 'FileSaver'), options)

        elif is_readmode_file(action):
            yield action_to_json(action, _get_widget(action, 'FileChooser'), options)

        elif is_choice(action):
            yield action_to_json(action, _get_widget(action, 'Dropdown'), options)

        elif is_flag(action):
            yield action_to_json(action, _get_widget(action, 'CheckBox'), options)

        elif is_counter(action):
            _json = action_to_json(action, _get_widget(action, 'Counter'), options)
            # pre-fill the 'counter' dropdown
            _json['data']['choices'] = list(map(str, range(0, 11)))
            yield _json
        else:
            raise UnknownWidgetType(action)


def get_widget(widgets, action, default):
    supplied_widget = widgets.get(action.dest, None)
    return supplied_widget or default


def is_required(action):
    '''
    _actions possessing the `required` flag and not implicitly optional
    through `nargs` being '*' or '?'
    '''
    return not isinstance(action, _SubParsersAction) and (
    action.required == True and action.nargs not in ['*', '?'])


def is_mutex(action):
    return isinstance(action, argparse._MutuallyExclusiveGroup)


def has_required(actions):
    return list(filter(None, list(filter(is_required, actions))))


def is_subparser(action):
    return isinstance(action, _SubParsersAction)


def has_subparsers(actions):
    return list(filter(is_subparser, actions))


def get_subparser(actions):
    return list(filter(is_subparser, actions))[0]


def is_optional(action):
    '''
    _actions either not possessing the `required` flag or implicitly optional
    through `nargs` being '*' or '?'
    '''
    return (not action.required) or action.nargs in ['*', '?']


def is_choice(action):
    ''' action with choices supplied '''
    return action.choices

def is_file(action):
    ''' action with FileType '''
    return isinstance(action.type, argparse.FileType)

def is_readmode_file(action):
    return is_file(action) and 'r' in action.type._mode

def is_writemode_file(action):
    # FileType uses the same modes as the builtin `open`
    # as such, all modes that aren't explicitly `r` (which is
    # also the default) are writable or read/writable, thus
    # making a FileChooser a good choice.
    return is_file(action) and 'r' not in action.type._mode

def is_version(action):
    return isinstance(action, _VersionAction) or issubclass(type(action), _VersionAction)


def is_standard(action):
    """ actions which are general "store" instructions.
    e.g. anything which has an argument style like:
       $ script.py -f myfilename.txt
    """
    boolean_actions = (
        _StoreConstAction, _StoreFalseAction,
        _StoreTrueAction
    )
    return (not action.choices
            and not isinstance(action.type, argparse.FileType)
            and not isinstance(action, (_CountAction, _HelpAction))
            # subclass checking is to handle the GooeyParser case
            # where Action get wrapped in a custom class
            and not issubclass(type(action), boolean_actions)
            and type(action) not in boolean_actions)


def is_flag(action):
    """ _actions which are either storeconst, store_bool, etc.. """
    # TODO: refactor to isinstance tuple form
    action_types = [_StoreTrueAction, _StoreFalseAction, _StoreConstAction]
    return (any(list(map(lambda Action: isinstance(action, Action), action_types)))
            or issubclass(type(action), (_StoreTrueAction, _StoreFalseAction, _StoreConstAction)))



def is_counter(action):
    """ _actions which are of type _CountAction """
    return isinstance(action, _CountAction)


def is_default_progname(name, subparser):
    return subparser.prog == '{} {}'.format(os.path.split(sys.argv[0])[-1], name)


def is_help_message(action):
    return isinstance(action, _HelpAction)


def choose_name(name, subparser):
    return name if is_default_progname(name, subparser) else subparser.prog


def build_radio_group(mutex_group, widget_group, options):
  dests = [action.dest for action in mutex_group._group_actions]
  return {
    'id': 'group_' + '_'.join(dests),
    'type': 'RadioGroup',
    'cli_type': 'optional',
    'group_name': 'Choose Option',
    'required': mutex_group.required,
    'options': merge(item_default, getattr(mutex_group, 'gooey_options', {})),
    'data': {
      'commands': [action.option_strings for action in mutex_group._group_actions],
      'widgets': list(categorize(mutex_group._group_actions, widget_group, options))
    }
  }


def action_to_json(action, widget, options):
    dropdown_types = {'Listbox', 'Dropdown', 'Counter'}
    if action.required:
        # Text fields get a default check that user input is present
        # and not just spaces, dropdown types get a simplified
        # is-it-present style check
        validator = ('user_input and not user_input.isspace()'
                     if widget not in dropdown_types
                     else 'user_input')
        error_msg = 'This field is required'
    else:
        # not required; do nothing;
        validator = 'True'
        error_msg = ''

    base = merge(item_default, {
        'validator': {
            'type': 'ExpressionValidator',
            'test': validator,
            'message': error_msg
        },
    })

    if (options.get(action.dest) or {}).get('initial_value') != None:
        value = options[action.dest]['initial_value']
        options[action.dest]['initial_value'] = handle_initial_values(action, widget, value)
    default = handle_initial_values(action, widget, action.default)
    if default == argparse.SUPPRESS:
        default = None



    final_options = merge(base, options.get(action.dest) or {})
    validate_gooey_options(action, widget, final_options)

    return {
        'id': action.dest,
        'type': widget,
        'cli_type': choose_cli_type(action),
        'required': action.required,
        'data': {
            'display_name': action.metavar or action.dest,
            'help': (action.help or '').replace('%%', '%'),
            'required': action.required,
            'nargs': action.nargs or '',
            'commands': action.option_strings,
            'choices': list(map(str, action.choices)) if action.choices else [],
            'default': default,
            'dest': action.dest,
        },
        'options': final_options
    }


def validate_gooey_options(action, widget, options):
    """Very basic field validation / sanity checking for
    the time being.

    Future plans are to assert against the options and actions together
    to facilitate checking that certain options like `initial_selection` in
    RadioGroups map to a value which actually exists (rather than exploding
    at runtime with an unhelpful error)

    Additional problems with the current approach is that no feedback is given
    as to WHERE the issue took place (in terms of stacktrace). Which means we should
    probably explode in GooeyParser proper rather than trying to collect all the errors here.
    It's not super ideal in that the user will need to run multiple times to
    see all the issues, but, ultimately probably less annoying that trying to
    debug which `gooey_option` key had an issue in a large program.

    That said "better is the enemy of done." This is good enough for now. It'll be
    a TODO: better validation 
    """
    errors = collect_errors(validators, options)
    if errors:
        from pprint import pformat
        raise ValueError(str(action.dest) + str(pformat(errors)))


def choose_cli_type(action):
    return 'positional' \
            if action.required and not action.option_strings \
            else 'optional'


def coerce_default(default, widget):
    """coerce a default value to the best appropriate type for
    ingestion into wx"""
    dispatcher = {
        'Listbox': clean_list_defaults,
        'Dropdown': safe_string,
        'Counter': safe_string
    }
    # Issue #321:
    # Defaults for choice types must be coerced to strings
    # to be able to match the stringified `choices` used by `wx.ComboBox`
    cleaned = clean_default(default)

    # dispatch to the appropriate cleaning function, or return the value
    # as is if no special handler is present
    return dispatcher.get(widget, identity)(cleaned)


def handle_initial_values(action, widget, value):
    handlers = [
        [textinput_with_nargs_and_list_default, coerse_nargs_list],
        [is_widget('Listbox'), clean_list_defaults],
        [is_widget('Dropdown'), coerce_str],
        [is_widget('Counter'), safe_string]
    ]
    for matches, apply_coercion in handlers:
        if matches(action, widget):
            return apply_coercion(value)
    return clean_default(value)


def coerse_nargs_list(default):
    """
    nargs=* and defaults which are collection types
    must be transformed into a CLI equivalent form. So, for
    instance, ['one two', 'three'] => "one two" "three"

    This only applies when the target widget is a text input. List
    based widgets such as Listbox should keep their defaults in list form

    Without this transformation, `add_arg('--foo', default=['a b'], nargs='*')` would show up in
    the UI as the literal string `['a b']` brackets and all.
    """
    return ' '.join('"{}"'.format(x) for x in default)

def is_widget(name):
    def equals(action, widget):
        return widget == name
    return equals


def textinput_with_nargs_and_list_default(action, widget):
    """
    Vanilla TextInputs which have nargs options which produce lists (i.e.
    nargs +, *, N, or REMAINDER) need to have their default values transformed
    into CLI style space-separated entries when they're supplied as a list of values
    on the Python side.
    """
    return (
        widget in {'TextField', 'Textarea', 'PasswordField'}
        and (isinstance(action.default, list) or isinstance(action.default, tuple))
        and is_list_based_nargs(action))


def is_list_based_nargs(action):
    """ """
    return isinstance(action.nargs, int) or action.nargs in {'*', '+', '...'}



def clean_list_defaults(default_values):
    """
    Listbox's default's can be passed as a single value
    or a collection of values (due to multiple selections). The list interface
    is standardized on for ease.
    """
    wrapped_values = ([default_values]
                      if isinstance(default_values, str)
                      else default_values)
    return [safe_string(value) for value in wrapped_values or []]


def clean_default(default):
    """
    Attempts to safely coalesce the default value down to
    a valid JSON type.
    """
    try:
        json.dumps(default)
        return default
    except TypeError as e:
        # see: Issue #377
        # if is isn't json serializable (i.e. primitive data) there's nothing
        # useful for Gooey to do with it (since Gooey deals in primitive data
        # types). So the correct behavior is dropping them. This affects ONLY
        # gooey, not the client code.
        return None


def safe_string(value):
    """
    Coerce a type to string as long as it isn't None or Boolean
    TODO: why do I have this special boolean case..?
    """
    if value is None or isinstance(value, bool):
        return value
    else:
        return str(value)


def coerce_str(value):
    """
    Coerce the incoming type to string as long as it isn't None
    """
    return str(value) if value is not None else value



def this_is_a_comment(action, widget):
    """
    TODO:
     - better handling of nargs.
     - allowing a class of "Narg'able" widget variants that allow dynamically adding options.
    Below are some rough notes on the current widgets and their nargs behavior (or lack of)
    """

    asdf = [
        # choosers are all currently treated as
        # singular inputs regardless of nargs status.
        'FileChooser',
        'MultiFileChooser',
        'FileSaver',
        'DirChooser',
        'DateChooser',
        'TimeChooser',

        # radiogroup requires no special logic. Delegates to internal widgets
        'RadioGroup',
        # nargs invalid
        'CheckBox',
        # nargs invalid
        'BlockCheckbox',

        # currently combines everything into a single, system _sep separated string
        # potential nargs behavior
        #   input: - each item gets a readonly textbox?
        #          - each item is its own editable widget?
        #          - problem with this idea is selected a ton of files would break the UI.
        #            maybe a better option would be to expose what's been added as a small
        #            list view? That way its a fixed size even if they choose 100s of files.
        #
        'MultiDirChooser',
        # special case. convert default to list of strings
        'Listbox',

        # special cases. coerce default to string
        'Dropdown',
        'Counter',

        # nargs behavior:
        #   - convert to space separated list of strings
        'TextField',
        'Textarea',
        'PasswordField',
    ]


File: gooey/python_bindings/gooey_parser.py

from argparse import ArgumentParser, _SubParsersAction
from argparse import _MutuallyExclusiveGroup, _ArgumentGroup


class GooeySubParser(_SubParsersAction):
    def __init__(self, *args, **kwargs):
        super(GooeySubParser, self).__init__(*args, **kwargs)


# TODO: figure out how to correctly dispatch all of these
#       so that the individual wrappers aren't needed
class GooeyArgumentGroup(_ArgumentGroup):
    def __init__(self, parser, widgets, options, *args, **kwargs):
        self.parser = parser
        self.widgets = widgets
        self.options = options
        super(GooeyArgumentGroup, self).__init__(self.parser, *args, **kwargs)

    def add_argument(self, *args, **kwargs):
        widget = kwargs.pop('widget', None)
        metavar = kwargs.pop('metavar', None)
        options = kwargs.pop('gooey_options', None)

        action = super(GooeyArgumentGroup, self).add_argument(*args, **kwargs)
        self.parser._actions[-1].metavar = metavar
        self.widgets[self.parser._actions[-1].dest] = widget
        self.options[self.parser._actions[-1].dest] = options
        return action

    def add_argument_group(self, *args, **kwargs):
        options = kwargs.pop('gooey_options', {})
        group = GooeyArgumentGroup(self.parser, self.widgets, self.options, *args, **kwargs)
        group.gooey_options = options
        self._action_groups.append(group)
        return group

    def add_mutually_exclusive_group(self, *args, **kwargs):
        options = kwargs.pop('gooey_options', {})
        container = self
        group = GooeyMutuallyExclusiveGroup(container, self.parser, self.widgets, self.options, *args, **kwargs)
        group.gooey_options = options
        self.parser._mutually_exclusive_groups.append(group)
        return group


class GooeyMutuallyExclusiveGroup(_MutuallyExclusiveGroup):
    def __init__(self, container, parser, widgets, options, *args, **kwargs):
        self.parser = parser
        self.widgets = widgets
        self.options = options
        super(GooeyMutuallyExclusiveGroup, self).__init__(container, *args, **kwargs)

    def add_argument(self, *args, **kwargs):
        widget = kwargs.pop('widget', None)
        metavar = kwargs.pop('metavar', None)
        options = kwargs.pop('gooey_options', None)

        super(GooeyMutuallyExclusiveGroup, self).add_argument(*args, **kwargs)
        self.parser._actions[-1].metavar = metavar
        self.widgets[self.parser._actions[-1].dest] = widget
        self.options[self.parser._actions[-1].dest] = options


class MyArgumentParser(ArgumentParser):
    def __init__(self, **kwargs):
        self._errors = []
        super(MyArgumentParser, self).__init__(**kwargs)

    def error(self, message):
        self._errors.append(message)


def lift_relevant(**kwargs):
    """
    Lifts the user's (likely) partial function into
    total one of type `String -> Either Error a`
    """
    try:
        # Not all Parser Actions accept a type function. Rather
        # than track what allows what explicitly, we just try to
        # pass the `type` var to constructor. If is doesn't
        # explode, then we're good and we use the lifted type. Otherwise
        # we use the original kwargs
        p = ArgumentParser()
        lifted_kwargs = {**kwargs, 'type': lift(kwargs.get('type', identity))}
        p.add_argument('-a', **lifted_kwargs)
        return lifted_kwargs
    except TypeError as e:
        return kwargs


def cls_wrapper(cls, **options):
    def inner(*args, **kwargs):
        class ActionWrapper(cls):
            def __call__(self, p, namespace, values, option_string, **qkwargs):
                # print('hello from', options, namespace, values, option_string, qkwargs)
                super(ActionWrapper, self).__call__(p, namespace, values, option_string, **qkwargs)
        return ActionWrapper(*args, **kwargs)
    return inner


class GooeyParser(object):
    def __init__(self, **kwargs):
        on_success = kwargs.pop('on_success', None)
        on_error = kwargs.pop('on_error', None)
        self.__dict__['parser'] = ArgumentParser(**kwargs)
        self.widgets = {}
        self.options = {}
        self.on_gooey_success = on_success
        self.on_gooey_error = on_error
        if 'parents' in kwargs:
            for parent in kwargs['parents']:
                if isinstance(parent, self.__class__):
                    self.widgets.update(parent.widgets)
                    self.options.update(parent.options)

    @property
    def _mutually_exclusive_groups(self):
        return self.parser._mutually_exclusive_groups

    @property
    def _actions(self):
        return self.parser._actions

    @property
    def description(self):
        return self.parser.description

    def add_argument(self, *args, **kwargs):
        widget = kwargs.pop('widget', None)
        metavar = kwargs.pop('metavar', None)
        options = kwargs.pop('gooey_options', None)

        # TODO: move this to the control module. No need to do it
        #       at creation time.
        # lifted_kwargs = lift_relevant(**kwargs)
        #
        # action_cls = self.parser._pop_action_class(kwargs)
        # enhanced_action = cls_wrapper(action_cls, **(options if options else {}))
        #
        # action = self.parser.add_argument(*args, **{**lifted_kwargs, 'action': enhanced_action})

        action = self.parser.add_argument(*args, **kwargs)

        self.parser._actions[-1].metavar = metavar

        action_dest = self.parser._actions[-1].dest
        if action_dest not in self.widgets or self.widgets[action_dest] is None:
            self.widgets[action_dest] = widget

        if action_dest not in self.options or self.options[action_dest] is None:
            self.options[self.parser._actions[-1].dest] = options

        self._validate_constraints(
            self.parser._actions[-1],
            widget,
            options or {},
            **kwargs
        )
        return action

    def add_mutually_exclusive_group(self, *args, **kwargs):
        options = kwargs.pop('gooey_options', {})
        group = GooeyMutuallyExclusiveGroup(self, self.parser, self.widgets, self.options, *args, **kwargs)
        group.gooey_options = options
        self.parser._mutually_exclusive_groups.append(group)
        return group

    def add_argument_group(self, *args, **kwargs):
        options = kwargs.pop('gooey_options', {})
        group = GooeyArgumentGroup(self.parser, self.widgets, self.options, *args, **kwargs)
        group.gooey_options = options
        self.parser._action_groups.append(group)
        return group

    def parse_args(self, args=None, namespace=None):
        return self.parser.parse_args(args, namespace)

    def add_subparsers(self, **kwargs):
        if self._subparsers is not None:
            self.error(_('cannot have multiple subparser arguments'))

        # add the parser class to the arguments if it's not present
        kwargs.setdefault('parser_class', type(self))

        if 'title' in kwargs or 'description' in kwargs:
            title = kwargs.pop('title', 'subcommands')
            description = kwargs.pop('description', None)
            self._subparsers = self.add_argument_group(title, description)
        else:
            self._subparsers = self._positionals

        # prog defaults to the usage message of this parser, skipping
        # optional arguments and with no "usage:" prefix
        if kwargs.get('prog') is None:
            formatter = self._get_formatter()
            positionals = self._get_positional_actions()
            groups = self._mutually_exclusive_groups
            formatter.add_usage(self.usage, positionals, groups, '')
            kwargs['prog'] = formatter.format_help().strip()

        # create the parsers action and add it to the positionals list
        parsers_class = self._pop_action_class(kwargs, 'parsers')
        action = parsers_class(option_strings=[], **kwargs)
        self._subparsers._add_action(action)

        # return the created parsers action
        return action

    def _validate_constraints(self, parser_action, widget, options, **kwargs):
        from gooey.python_bindings import constraints
        constraints.assert_listbox_constraints(widget, **kwargs)
        constraints.assert_visibility_requirements(parser_action, options)



    def __getattr__(self, item):
        return getattr(self.parser, item)

    def __setattr__(self, key, value):
        return setattr(self.parser, key, value)




File: gooey/python_bindings/control.py

"""
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!DEBUGGING NOTE!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

PyCharm will inject addition params into stdout when starting
a new process. This can make debugging VERY VERY CONFUSING as
the thing being injected starts complaining about unknown
arguments...

TL;DR: disable the "Attaach to subprocess automatically" option
in the Debugger settings, and the world will be sane again.

See: https://youtrack.jetbrains.com/issue/PY-24929
and: https://www.jetbrains.com/help/pycharm/2017.1/python-debugger.html

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!DEBUGGING NOTE!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
"""
import json
import os
import sys
import traceback
from argparse import ArgumentParser
from copy import deepcopy
from typing import List, Dict

from gooey.python_bindings.dynamics import monkey_patch_for_form_validation
from gooey.python_bindings.dynamics import patch_argument, collect_errors
from gooey.python_bindings.types import GooeyParams
from gooey.python_bindings.coms import serialize_outbound, decode_payload
from gooey.python_bindings.types import PublicGooeyState
from . import cmd_args
from . import config_generator


def noop(*args, **kwargs):
    """
    No-op for dev/null-ing handlers which
    haven't been specified by the user.
    """
    return None


def bypass_gooey(params):
    """
    Bypasses all the Gooey machinery and runs the user's code directly.
    """
    def parse_args(self: ArgumentParser, args=None, namespace=None):
        # We previously mutated sys.argv directly to remove
        # the --ignore-gooey flag. But this caused lots of issues
        # See: https://github.com/chriskiehl/Gooey/issues/686
        # So, we instead modify the parser to transparently
        # consume the extra token.
        patched_parser = patch_argument(self, '--ignore-gooey', action='store_true')
        args = patched_parser.original_parse_args(args, namespace)  # type: ignore
        # removed from the arg object so the user doesn't have
        # to deal with it or be confused by it
        del args.ignore_gooey
        return args
    return parse_args


def boostrap_gooey(params: GooeyParams):
    """Bootstraps the Gooey UI."""
    def parse_args(self: ArgumentParser, args=None, namespace=None):
        # This import is delayed so it is not in the --ignore-gooey codepath.
        from gooey.gui import bootstrap
        source_path = sys.argv[0]

        build_spec = None
        if params['load_build_config']:
            try:
                exec_dir = os.path.dirname(sys.argv[0])
                open_path = os.path.join(exec_dir, params['load_build_config'])  # type: ignore
                build_spec = json.load(open(open_path, "r"))
            except Exception as e:
                print('Exception loading Build Config from {0}: {1}'.format(params['load_build_config'], e))
                sys.exit(1)

        if not build_spec:
            if params['use_cmd_args']:
                cmd_args.parse_cmd_args(self, args)

            build_spec = config_generator.create_from_parser(
                self,
                source_path,
                **params)

        if params['dump_build_config']:
            config_path = os.path.join(os.path.dirname(sys.argv[0]), 'gooey_config.json')
            print('Writing Build Config to: {}'.format(config_path))
            with open(config_path, 'w') as f:
                f.write(json.dumps(build_spec, indent=2))
        bootstrap.run(build_spec)
    return parse_args


def validate_form(params: GooeyParams, write=print, exit=sys.exit):
    """
    Validates the user's current form.
    """
    def merge_errors(state: PublicGooeyState, errors: Dict[str, str]) -> PublicGooeyState:
        changes = deepcopy(state['active_form'])
        for item in changes:
            if item['type'] == 'RadioGroup':
                for subitem in item['options']:  # type: ignore
                    subitem['error'] = errors.get(subitem['id'], None)
                item['error'] = any(x['error'] for x in item['options'])   # type: ignore
            else:
                item['error'] = errors.get(item['id'], None)  # type: ignore

        return PublicGooeyState(active_form=changes)

    def parse_args(self: ArgumentParser, args=None, namespace=None):
        error_registry: Dict[str, Exception] = {}
        patched_parser = monkey_patch_for_form_validation(error_registry, self)
        try:
            args = patched_parser.original_parse_args(args, namespace)  # type: ignore
            state = args.gooey_state
            next_state = merge_errors(state, collect_errors(patched_parser, error_registry, vars(args)))
            write(serialize_outbound(next_state))
            exit(0)
        except Exception as e:
            write(e)
            exit(1)
    return parse_args


def validate_field(params):
    def parse_args(self: ArgumentParser, args=None, namespace=None):
        raise NotImplementedError
    return parse_args


def handle_completed_run(params, write=print, exit=sys.exit):
    def parse_args(self: ArgumentParser, args=None, namespace=None):
        # because we're running under the context of a successful
        # invocation having just completed, the arguments supplied to
        # the parser to trigger it are thus, by definition, safe to parse.
        # So, we don't need any error patching monkey business and just need
        # to attach our specific arg to parse the extra option Gooey passes

        patch_argument(self, '--gooey-state', action='store', type=decode_payload)
        patch_argument(self, '--gooey-run-is-success', default=False, action='store_true')
        patch_argument(self, '--gooey-run-is-failure', default=False, action='store_true')

        try:
            args = self.original_parse_args(args, namespace)  # type: ignore
            form_state = args.gooey_state
            was_success = args.gooey_run_is_success
            # removing the injected gooey value so as not
            # to clutter the user's object
            del args.gooey_state
            del args.gooey_run_is_success
            del args.gooey_run_is_failure
            if was_success:
                next_state = getattr(self, 'on_gooey_success', noop)(args, form_state)  # type: ignore
            else:
                next_state = getattr(self, 'on_gooey_error', noop)(args, form_state)  # type: ignore
            write(serialize_outbound(next_state))
            exit(0)
        except Exception as e:
            write(''.join(traceback.format_stack()))
            write(e)
            exit(1)
    return parse_args


def handle_error(params):
    def parse_args(self: ArgumentParser, args=None, namespace=None):
        raise NotImplementedError
    return parse_args


def handle_field_update(params):
    def parse_args(self: ArgumentParser, args=None, namespace=None):
        raise NotImplementedError
    return parse_args


def handle_submit(params):
    def parse_args(self: ArgumentParser, args=None, namespace=None):
        raise NotImplementedError
    return parse_args


def choose_hander(params: GooeyParams, cliargs: List[str]):
    """
    Dispatches to the appropriate handler based on values
    found in the CLI arguments
    """
    with open('tmp.txt', 'w') as f:
        f.write(str(sys.argv))
    if '--gooey-validate-form' in cliargs:
        return validate_form(params)
    elif '--gooey-run-is-success' in cliargs or '--gooey-run-is-failure' in cliargs:
        return handle_completed_run(params)
    elif '--ignore-gooey' in cliargs:
        return bypass_gooey(params)
    else:
        return boostrap_gooey(params)






File: gooey/python_bindings/constants.py

from collections import namedtuple

SIDEBAR = 'SIDEBAR'
TABBED = 'TABBED'
INLINE = 'INLINE'
HIDDEN = 'HIDDEN'

FONTWEIGHT_THIN = 100
FONTWEIGHT_EXTRALIGHT = 200
FONTWEIGHT_LIGHT = 300
FONTWEIGHT_NORMAL = 400
FONTWEIGHT_MEDIUM = 500
FONTWEIGHT_SEMIBOLD = 600
FONTWEIGHT_BOLD = 700
FONTWEIGHT_EXTRABOLD = 800
FONTWEIGHT_HEAVY = 900
FONTWEIGHT_EXTRAHEAVY = 1000


Events = namedtuple('Events', [
    'VALIDATE_FORM', 'ON_SUCCESS', 'ON_ERROR'
])('VALIDATE_FORM', 'ON_SUCCESS', 'ON_ERROR')

# class Events:
#     VALIDATE_FORM = 'VALIDATE_FORM'
#     ON_SUCCESS = 'ON_SUCCESS'
#     ON_ERROR = 'ON_ERROR'




File: gooey/python_bindings/__init__.py

__author__ = 'Chris'



File: gooey/python_bindings/types.py

from typing import Optional, Tuple, List, Union, Mapping, Any, TypeVar, Generic, Dict

from dataclasses import dataclass
from typing_extensions import TypedDict


class MenuHtmlDialog(TypedDict):
    type: str
    menuTitle: str
    caption: Optional[str]
    html: str

class MenuLink(TypedDict):
    type: str
    menuTitle: str
    url: str


class MenuMessageDialog(TypedDict):
    type: str
    menuTitle: str
    message: str
    caption: Optional[str]

class MenuAboutDialog(TypedDict):
    type: str
    menuTitle: str
    name: Optional[str]
    description: Optional[str]
    version: Optional[str]
    copyright: Optional[str]
    license: Optional[str]
    website: Optional[str]
    developer: Optional[str]


MenuItem = Union[
    MenuLink,
    MenuMessageDialog,
    MenuAboutDialog,
    MenuHtmlDialog
]



class TimingOptions(TypedDict):
    show_time_remaining: bool
    hide_time_remaining_on_complete: bool


class GooeyParams(TypedDict):
    # when running with a custom target, there is no need to inject
    # --ignore-gooey into the CLI args
    show_preview_warning: bool
    suppress_gooey_flag: bool
    advanced: bool
    language: str
    target: Optional[str]
    program_name: Optional[str]
    program_description: Optional[str]
    sidebar_title: str
    default_size: Tuple[int, int]
    auto_start: bool
    show_advanced: bool
    run_validators: bool
    encoding: str
    show_stop_warning: bool
    show_success_modal: bool
    show_failure_modal: bool
    force_stop_is_error: bool
    poll_external_updates: bool  # BEING DEPRECATED
    return_to_config: bool
    show_restart_button: bool
    requires_shell: bool
    menu: List[MenuItem]
    clear_before_run: bool
    fullscreen: bool
    # Legacy/Backward compatibility interop
    use_legacy_titles: bool
    required_cols: int
    optional_cols: int
    manual_start: bool
    monospace_display: bool

    image_dir: str
    language_dir: str
    progress_regex: Optional[str]
    progress_expr: Optional[str]
    hide_progress_msg: bool
    timing_options: TimingOptions
    disable_progress_bar_animation: bool
    disable_stop_button: bool
    shutdown_signal: int
    use_events: List[str]

    # Layouts
    navigation: str
    show_sidebar: bool
    tabbed_groups: bool
    group_by_type: bool

    # styles
    body_bg_color: str
    header_bg_color: str
    header_height: int
    header_show_title: bool
    header_show_subtitle: bool
    header_image_center: bool
    footer_bg_color: str
    sidebar_bg_color: str

    # font family, weight, and size are determined at runtime
    terminal_panel_color: str
    terminal_font_color: str
    terminal_font_family: Optional[str]
    terminal_font_weight: Optional[int]
    terminal_font_size: Optional[int]
    richtext_controls: bool
    error_color: str

    use_cmd_args: bool
    dump_build_config: bool
    load_build_config: Optional[str]

# TODO:
# use the syntax here rather than inheritance, as the latter is a type error
# https://jdkandersson.com/2020/01/27/python-typeddict-arbitrary-key-names-with-totality/
# class BuildSpecification(GooeyParams):
#     target: str
#     widgets: str


class BasicField(TypedDict):
    id: str
    type: str
    # required: bool
    # positional: bool
    error: Optional[str]
    enabled: bool
    visible: bool

class Dropdown(BasicField):
    selected: int
    choices: List[str]

class Chooser(BasicField):
    btn_label: str
    value: str


class MultiFileChooser(Chooser):
    pass

class FileChooser(Chooser):
    pass

class FileSaver(Chooser):
    pass

class DirChooser(Chooser):
    pass

class MultiDirChooser(Chooser):
    pass

class DateChooser(Chooser):
    pass

class TimeChooser(Chooser):
    pass

class ColourChooser(Chooser):
    pass

class Command(BasicField):
    value: str
    placeholder: str

class Counter(BasicField):
    selected: int
    choices: List[str]

class DropdownFilterable(BasicField):
    value: str
    choices: List[str]

class Listbox(BasicField):
    selected: List[str]
    choices: List[str]

class IntegerField(BasicField):
    value: str
    min: int
    max: int

class DecimalField(BasicField):
    value: float
    min: float
    max: float

class Slider(BasicField):
    value: float
    min: float
    max: float

class Textarea(BasicField):
    value: float
    height: int

class TextField(BasicField):
    value: str
    placeholder: str


class PasswordField(TextField):
    pass


class Checkbox(BasicField):
    checked: bool


class RadioGroup(BasicField):
    selected: Optional[int]
    options: List['FormField']


FormField = Union[
    Textarea,
    Slider,
    Command,
    Counter,
    Checkbox,
    TextField,
    Dropdown,
    Chooser,
    RadioGroup,
    DropdownFilterable,
    Listbox,
    IntegerField
]




class FieldValue(TypedDict):
    """
    The current value of a widget in the UI.
    TODO: Why are things like cmd and cli type tracked IN the
    UI and returned as part of the getValue() call?
    What the hell, young me?
    """
    id: str
    cmd: Optional[str]
    rawValue: str
    placeholder: str
    positional: bool
    required: bool
    enabled: bool
    visible: bool
    test: bool
    error: Optional[str]
    clitype: str
    meta: Any


class PublicGooeyState(TypedDict):
    """
    A minimal representation of Gooey's current UI state
    """
    active_form: List[FormField]


class Group(TypedDict):
    name: str
    items: List['Item']
    groups: List['Group']
    description: str
    options: Dict[Any, Any]


class Item(TypedDict):
    id: str
    type: str
    cli_type: str
    group_name: str
    required: bool
    options: Dict[Any, Any]
    data: 'ItemData'


class EnrichedItem(Item):
    """
    An argparse item paired with its associated Gooey form
    field and current state.
    """
    field: FormField


ItemData = Union['StandardData', 'RadioData']

class StandardData(TypedDict):
    display_name: str
    help: str
    required: bool
    nargs: str
    commands: List[str]
    choices: List[str]
    default: Union[str, List[str]]
    dest: str

class RadioData(TypedDict):
    commands: List[List[str]]
    widgets: List[Item]


class TopLevelParser(TypedDict):
    command: str
    name: str
    help: Optional[str]
    description: str
    contents: List[Group]

A = TypeVar('A')


## TODO: dynamic types

@dataclass(frozen=True, eq=True)
class CommandDetails:
    target: str
    subcommand: str
    positionals: List[FieldValue]
    optionals: List[FieldValue]

@dataclass(frozen=True, eq=True)
class CommandPieces:
    target: str
    subcommand: str
    positionals: List[str]
    optionals: List[str]
    ignoreFlag: str

@dataclass(frozen=True, eq=True)
class Success(Generic[A]):
    value: A

    def map(self, f):
        return Success(f(self.value))
    def flatmap(self, f):
        return f(self.value)
    def onSuccess(self, f):
        f(self.value)
        return self
    def onError(self, f):
        return self
    def isSuccess(self):
        return True
    def getOrThrow(self):
        return self.value

@dataclass(frozen=True, eq=True)
class Failure:
    error: Exception

    def map(self, f):
        return Failure(self.error)
    def flatmap(self, f):
        return Failure(self.error)
    def onSuccess(self, f):
        return self
    def onError(self, f):
        f(self.error)
        return self
    def isSuccess(self):
        return False
    def getOrThrow(self):
        raise self.error

Try = Union[Success[A], Failure]



ValidationResponse = Mapping[str, str]


class InvalidChoiceException(ValueError):
    pass



File: gooey/python_bindings/signal_support.py

"""
Utilities for patching Windows so that CTRL-C signals
can be received by process groups.

The best resource for understanding why this is required is the Python
Issue here: https://bugs.python.org/issue13368

**The official docs from both Python and Microsoft cannot be trusted due to inaccuracies**

The two sources directly conflict with regards to what signals are possible on Windows
under which circumstances.


Python's docs:
    https://bugs.python.org/issue13368
    On Windows, SIGTERM is an alias for terminate(). CTRL_C_EVENT and
    CTRL_BREAK_EVENT can be sent to processes started with a creationflags
    parameter which includes CREATE_NEW_PROCESS_GROUP.

Microsoft's docs:
    https://docs.microsoft.com/en-us/windows/console/generateconsolectrlevent
    Generates a CTRL+C signal. This signal cannot be generated for process groups.

Another piece of the puzzle:
    https://docs.microsoft.com/en-us/windows/console/handlerroutine#remarks
    Each console process has its own list of HandlerRoutine functions. Initially,
    this list contains only a default handler function that calls ExitProcess. A
    console process adds or removes additional handler functions by calling the
    SetConsoleCtrlHandler function, which does not affect the list of handler
    functions for other processes.

The most important line here is: "Initially, this list contains only the default
handler function that calls exit Process"

So, despite what Microsoft's docs for ctrlcevent state, it IS possible to send
the ctrl+c signal to process groups **IFF** the leader for that process group has
the appropriate handler installed.

We can solve this transparently in Gooey land by installing the handler on behalf
of the user when required.
"""
import sys
import signal
from textwrap import dedent


def requires_special_handler(platform, requested_signal):
    """
    Checks whether we need to attach additional handlers
    to this process to deal with ctrl-C signals
    """
    return platform.startswith("win") and requested_signal == signal.CTRL_C_EVENT


def install_handler():
    """
    Adds a Ctrl+C 'handler routine'. This allows the CTRL-C
    signal to be received even when the process was created in
    a new process group.

    See module docstring for additional info.
    """
    assert sys.platform.startswith("win")
    import ctypes

    help_msg = dedent('''
    Please open an issue here: https://github.com/chriskiehl/Gooey/issues
        
    GOOD NEWS: THERE IS A WORK AROUND: 
    
    Gooey only needs to attach special Windows handlers for the CTRL-C event. Consider using 
    the `CTRL_BREAK_EVENT` or `SIGTERM` instead to get past this error. 
    
    See the Graceful Shutdown docs for information on how to catch alternative signals. 
    
    https://github.com/chriskiehl/Gooey/tree/master/docs 
    ''')

    try:
        kernel32 = ctypes.WinDLL('kernel32')
        if kernel32.SetConsoleCtrlHandler(None, 0) == 0:
            raise Exception(dedent('''
            Gooey was unable to install the handler required for processing
            CTRL-C signals. This is a **very** unexpected failure. 
            ''') + help_msg)
    except OSError as e:
        raise OSError(dedent('''
            Gooey failed while trying to find the kernel32 module. 
            Gooey requires this module in order to attach handlers for CTRL-C signal. 
            Not being able to find this is **very** unexpected. 
            ''') + help_msg)



File: gooey/python_bindings/gooey_decorator.py

"""
Created on Jan 24, 2014  <-- so long ago!
"""
import sys
from argparse import ArgumentParser
from functools import wraps

from gooey.python_bindings.control import choose_hander
from gooey.python_bindings.parameters import gooey_params
from gooey.python_bindings.types import GooeyParams

IGNORE_COMMAND = '--ignore-gooey'


def Gooey(f=None, **gkwargs):
    """
    Decoration entry point for the Gooey process.
    See types.GooeyParams for kwargs options
    """
    params: GooeyParams = gooey_params(**gkwargs)

    @wraps(f)
    def inner(*args, **kwargs):
        parser_handler = choose_hander(params, gkwargs.get('cli', sys.argv))
        # monkey patch parser
        ArgumentParser.original_parse_args = ArgumentParser.parse_args
        ArgumentParser.parse_args = parser_handler
        # return the wrapped, now monkey-patched, user function
        # to be later invoked
        return f(*args, **kwargs)

    def thunk(func):
        """
        This just handles the case where the decorator is called
        with arguments (i.e. @Gooey(foo=bar) rather than @Gooey).

        Cause python is weird, when a decorator is called (e.g. @decorator())
        rather than just declared (e.g. @decorator), in complete and utter
        defiance of what your lying eyes see, it changes from a higher order
        function, to a function that takes an arbitrary argument *and then*
        returns a higher order function. i.e.

        decorate :: (a -> b) -> (a -> b)
        decorate() :: c -> (a -> b) -> (a -> b)

        wat.
        """
        return Gooey(func, **params)

    return inner if callable(f) else thunk




File: gooey/python_bindings/parameters.py

import signal
import sys
import textwrap

import os
from typing import List

from gooey.python_bindings.constants import Events
from gooey.python_bindings import constants
from gooey.gui.util.freeze import getResourcePath
from gooey.python_bindings.types import GooeyParams
from gooey.util.functional import merge



def _get_font_weight(kwargs):
    error_msg = textwrap.dedent('''
    Unknown font weight {}. 

    The available weights can be found in the `constants` module. 
    They're prefixed with "FONTWEIGHT" (e.g. `FONTWEIGHT_BOLD`)

    example code:    

    ```
    from gooey import constants
    @Gooey(terminal_font_weight=constants.FONTWEIGHT_NORMAL)
    ```   
    ''')
    weights = {
        constants.FONTWEIGHT_THIN,
        constants.FONTWEIGHT_EXTRALIGHT,
        constants.FONTWEIGHT_LIGHT,
        constants.FONTWEIGHT_NORMAL,
        constants.FONTWEIGHT_MEDIUM,
        constants.FONTWEIGHT_SEMIBOLD,
        constants.FONTWEIGHT_BOLD,
        constants.FONTWEIGHT_EXTRABOLD,
        constants.FONTWEIGHT_HEAVY,
        constants.FONTWEIGHT_EXTRAHEAVY
    }
    weight = kwargs.get('terminal_font_weight', constants.FONTWEIGHT_NORMAL)
    if weight not in weights:
        raise ValueError(error_msg.format(weight))
    return weight


# python can't type kwargs? wtf..
def gooey_params(**kwargs) -> GooeyParams:
    """
    Builds the full GooeyParams object from an arbitrary subset of supplied values
    """
    return GooeyParams(**{  # type: ignore
        'show_preview_warning': kwargs.get('show_preview_warning', True),
        'language': kwargs.get('language', 'english'),
        'target': kwargs.get('target'),

        'dump_build_config': kwargs.get('dump_build_config', False),
        'load_build_config': kwargs.get('load_build_config'),
        'use_cmd_args': kwargs.get('use_cmd_args', False),

        'suppress_gooey_flag': kwargs.get('suppress_gooey_flag') or False,
        # TODO: I should not read from the environment.
        # remains here for legacy reasons pending refactor
        'program_name': kwargs.get('program_name') or os.path.basename(sys.argv[0]).replace('.py', ''),
        'program_description': kwargs.get('program_description') or '',
        'sidebar_title': kwargs.get('sidebar_title', 'Actions'),
        'default_size': kwargs.get('default_size', (610, 530)),
        'auto_start': kwargs.get('auto_start', False),
        'advanced': kwargs.get('advanced', True),
        'run_validators': kwargs.get('run_validators', True),
        'encoding': kwargs.get('encoding', 'utf-8'),
        'show_stop_warning': kwargs.get('show_stop_warning', True),
        'show_success_modal': kwargs.get('show_success_modal', True),
        'show_failure_modal': kwargs.get('show_failure_modal', True),
        'force_stop_is_error': kwargs.get('force_stop_is_error', True),
        'poll_external_updates': kwargs.get('poll_external_updates', False),
        'return_to_config': kwargs.get('return_to_config', False),
        'show_restart_button': kwargs.get('show_restart_button', True),
        'requires_shell': kwargs.get('requires_shell', True),
        'menu': kwargs.get('menu', []),
        'clear_before_run': kwargs.get('clear_before_run', False),
        'fullscreen': kwargs.get('fullscreen', False),

        'use_legacy_titles': kwargs.get('use_legacy_titles', True),
        'required_cols': kwargs.get('required_cols', 2),
        'optional_cols': kwargs.get('optional_cols', 2),
        'manual_start': False,
        'monospace_display': kwargs.get('monospace_display', False),

        'image_dir': kwargs.get('image_dir', '::gooey/default'),
        # TODO: this directory resolution shouldn't happen here!
        # TODO: leaving due to legacy for now
        'language_dir': kwargs.get('language_dir', getResourcePath('languages')),
        'progress_regex': kwargs.get('progress_regex'),
        'progress_expr': kwargs.get('progress_expr'),
        'hide_progress_msg': kwargs.get('hide_progress_msg', False),

        'timing_options': merge({
            'show_time_remaining': False,
            'hide_time_remaining_on_complete': True
        }, kwargs.get('timing_options', {})),
        'disable_progress_bar_animation': kwargs.get('disable_progress_bar_animation', False),
        'disable_stop_button': kwargs.get('disable_stop_button'),
        'shutdown_signal': kwargs.get('shutdown_signal', signal.SIGTERM),
        'use_events': parse_events(kwargs.get('use_events', [])),


        'navigation': kwargs.get('navigation', constants.SIDEBAR),
        'show_sidebar': kwargs.get('show_sidebar', False),
        'tabbed_groups': kwargs.get('tabbed_groups', False),
        'group_by_type': kwargs.get('group_by_type', True),


        'body_bg_color': kwargs.get('body_bg_color', '#f0f0f0'),
        'header_bg_color': kwargs.get('header_bg_color', '#ffffff'),
        'header_height': kwargs.get('header_height', 90),
        'header_show_title': kwargs.get('header_show_title', True),
        'header_show_subtitle': kwargs.get('header_show_subtitle', True),
        'header_image_center': kwargs.get('header_image_center', False),
        'footer_bg_color': kwargs.get('footer_bg_color', '#f0f0f0'),
        'sidebar_bg_color': kwargs.get('sidebar_bg_color', '#f2f2f2'),

        'terminal_panel_color': kwargs.get('terminal_panel_color', '#F0F0F0'),
        'terminal_font_color': kwargs.get('terminal_font_color', '#000000'),
        'terminal_font_family': kwargs.get('terminal_font_family', None),
        'terminal_font_weight': _get_font_weight(kwargs),
        'terminal_font_size': kwargs.get('terminal_font_size', None),
        'richtext_controls': kwargs.get('richtext_controls', False),
        'error_color': kwargs.get('error_color', '#ea7878'),
        # TODO: remove. Only useful for testing
        'cli': kwargs.get('cli', sys.argv),
    })


def parse_events(events: List[str]) -> List[str]:
    if not isinstance(events, list):
        raise TypeError(
            f"`use_events` requires a list of events. You provided "
            "{events}. \n"
            "Example: \n"
            "\tfrom gooey import Events"
            "\t@Gooey(use_events=[Events.VALIDATE_FORM]")

    unknown_events = set(events) - set(Events)
    if unknown_events:
        raise ValueError(
            f'nrecognized event(s) were passed to `use_events`: {unknown_events}\n'
            f'Must be one of {Events._fields}\n'
            f'Consider using the `Events` object: `from gooey import Events`')
    else:
        return events



File: gooey/python_bindings/constraints.py

"""
Basic constraints to ensure GooeyParser is fed all the info it needs
for various widget classes.

TODO: this should all live in the build_config stage here where it is used
within the GooeyParser directly. As is, logic is fragmented across files. Some
assertions happen in argparse_to_json, while others happen in GooeyParser.

Whenever refactoring happens, these should be removed from GooeyParser.
"""
from textwrap import dedent

def is_required(action):
    return action.required

def is_hidden(options):
    return not options.get('visible', True)

def has_validator(options):
    return bool(options.get('validator'))

def has_default(action):
    return bool(action.default)

def assert_visibility_requirements(action, options):
    if action.required and is_hidden(options) \
            and not (has_validator(options) or has_default(action)):
        raise ValueError(dedent(
            '''
            When using Gooey's hidden field functionality, you must either '
      
              (a) provide a default value, or '
              (b) provide a custom validator'
              
            Without one of those, your users will be unable to advance past 
            the configuration screen as they cannot interact with your 
            hidden field, and the default validator requires something to 
            be present for fields marked as `required`.  
            '''
        ))

def assert_listbox_constraints(widget, **kwargs):
    if widget and widget == 'Listbox':
        if not 'nargs' in kwargs or kwargs['nargs'] not in ['*', '+']:
            raise ValueError(
                'Gooey\'s Listbox widget requires that nargs be specified.\n'
                'Nargs must be set to either `*` or `+` (e.g. nargs="*")'
            )




File: gooey/python_bindings/cmd_args.py

'''
Created on Jan 15 2019

@author: Jonathan Schultz

This file contains code that allows the default argument values to be specified
on the command line.
'''

from argparse import _SubParsersAction

def parse_cmd_args(self, args=None):

  def prepare_to_read_cmd_args(item):
    '''
    Before reading the command-line arguments, we need to fudge a few things:
      1. If there are subparsers, we need a dest in order to know in which
         subparser the command-line values should be stored.
      2. Any required argument or mutex group needs to be made not required,
         otherwise it will be compulsory to enter those values on the command
         line.
    We save the everything as it was before the fudge, so we can restore later.
    '''
    for action in item._actions:
      if isinstance(action, _SubParsersAction):
        action.save_dest = action.dest
        if not action.dest:
          action.dest = '_subparser'
      else:
        action.save_required = action.required
        action.required = False
        action.save_nargs = action.nargs
        if action.nargs == '+':
          action.nargs = '*'
        elif action.nargs is None:
          action.nargs = '?'

    for mutex_group in item._mutually_exclusive_groups:
      mutex_group.save_required = mutex_group.required
      mutex_group.required = False

  def overwrite_default_values(item, cmd_args):
    '''
    Subsistute arguments provided on the command line in the place of the
    default values provided to argparse.
    '''
    for action in item._actions:
      if isinstance(action, _SubParsersAction):
        subparser_arg = getattr(cmd_args, action.dest, None)
        if subparser_arg:
          overwrite_default_values(action.choices[subparser_arg], cmd_args)
      else:
        dest = getattr(action, 'dest', None)
        if dest:
          cmd_arg = getattr(cmd_args, dest, None)
          if cmd_arg:
            action.default = cmd_arg

  def restore_original_configuration(item):
    '''
    Restore the old values as they were to start with.
    '''
    for action in item._actions:
      if isinstance(action, _SubParsersAction):
        action.dest = action.save_dest
        del action.save_dest
      else:
        action.required = action.save_required
        del action.save_required
        action.nargs = action.save_nargs
        del action.save_nargs

    for mutex_group in item._mutually_exclusive_groups:
      mutex_group.required = mutex_group.save_required
      del mutex_group.save_required

  prepare_to_read_cmd_args(self)
  overwrite_default_values(self, self.original_parse_args(args))
  restore_original_configuration(self)



File: gooey/python_bindings/parser_exceptions.py

'''
Created on Feb 10, 2014

@author: Chris
'''


class ParserError(Exception):
  """Thrown when the parser can't find argparse functions the client code"""
  pass


class ArgumentError(Exception):
  """Thrown when the parser is supplied with an incorrect argument format"""
  pass


if __name__ == '__main__':
  pass


File: gooey/python_bindings/config_generator.py

import os
import sys
import signal
import warnings
import textwrap
from gooey.python_bindings import argparse_to_json
from gooey.gui.util.quoting import quote
from gooey.python_bindings import constants
from gooey.python_bindings import gooey_decorator
from gooey.gui.util.functional import merge_dictionaries

default_layout = {
    'widgets': [{
      'type': 'CommandField',
      'required': True,
      'data': {
        'display_name': 'Enter Commands',
        'help': 'Enter command line arguments',
        'nargs': '',
        'commands': '',
        'choices': [],
        'default': None,
      }
    }],
}

# TODO: deprecate me
def create_from_parser(parser, source_path, **kwargs):

  run_cmd = kwargs.get('target')
  if run_cmd is None:
    if hasattr(sys, 'frozen'):
      run_cmd = quote(source_path)
    else:
      run_cmd = '{} -u {}'.format(quote(sys.executable), quote(source_path))

  build_spec = {**kwargs, 'target': run_cmd}

  if build_spec['monospace_display']:
      warnings.warn('Gooey Option `monospace_display` is a legacy option.\n'
                    'See the terminal_font_x options for more flexible control '
                    'over Gooey\'s text formatting')


  build_spec['program_description'] = build_spec['program_description'] or parser.description or ''

  layout_data = (argparse_to_json.convert(parser, **build_spec)
                   if build_spec['advanced']
                   else default_layout.items())

  build_spec.update(layout_data)

  if len(build_spec['widgets']) > 1:
    # there are subparsers involved
    build_spec['show_sidebar'] = True

  return build_spec






File: gooey/python_bindings/schema.py

from typing import Dict, Any

from gooey.python_bindings.types import PublicGooeyState
from gooey.python_bindings import types as t


def validate_public_state(state: Dict[str, Any]) -> PublicGooeyState:
    """
    Very, very minimal validation the shape of the incoming state
    is inline with the PublicGooeyState type.

    TODO: turn this into something useful.
    """
    top_level_keys = PublicGooeyState.__annotations__.keys()
    assert set(top_level_keys) == set(state.keys())
    for item in state['active_form']:
        assert 'type' in item
        expected_keys = getattr(t, item['type']).__annotations__.keys()
        a = set(expected_keys)
        b = set(item.keys())
        assert set(expected_keys) == set(item.keys())
    return state



File: gooey/python_bindings/coms.py

"""
Because Gooey communicates with the host program
over stdin/out, we have to be able to differentiate what's
coming from gooey and structured, versus what is arbitrary
junk coming from the host's own logging.

To do this, we just prefix all written by gooey with the
literal string 'gooey::'. This lets us dig through all the
noisy stdout to find just the structured Gooey data we're
after.
"""

import json
from base64 import b64decode
from typing import Dict, Any

from gooey.python_bindings.schema import validate_public_state
from gooey.python_bindings.types import PublicGooeyState

prefix = 'gooey::'


def serialize_outbound(out: PublicGooeyState):
    """
    Attaches a prefix to whatever is about to be written
    to stdout so that we can differentiate it in the
    sea of other stdout writes
    """
    return prefix + json.dumps(out)


def deserialize_inbound(stdout: bytes, encoding):
    """
    Deserializes the incoming stdout payload after
    finding the relevant sections give the gooey prefix.
    e.g.
    std='foo\nbar\nstarting run\ngooey::{active_form: [...]}\n'
    => {active_form: [...]}
    """
    data = json.loads(stdout.decode(encoding).split(prefix)[-1])
    return validate_public_state(data)


def decode_payload(x):
    """
    To avoid quoting shenanigans, the json state sent from
    Gooey is b64ecoded for ease of CLI transfer. Argparse will
    usually barf when trying to parse json directly
    """
    return json.loads(b64decode(x))



File: gooey/python_bindings/dynamics.py

"""
All things Dynamic Updates & Validation.

Hear me all ye who enter!
=========================

This is a module of disgusting hacks and monkey patching. Control flow
is all over the place and a comprised of hodgepodge of various strategies.
This is all because Argparse's internal parsing design (a) really,
really, REALLY wants to fail and sys.exit at the first error it
finds, and (b) does these program ending validations at seemingly random
points throughout its code base. Meaning, there is no single centralized
validation module, class, or function which could be overridden in order to
achieve the desired behavior.

All that means is that it takes a fair amount of indirect, non-standard, and
gross monkey-patching to get Argparse to collect all its errors as it parses
rather than violently explode each time it finds one.

For additional background, see the original design here:
https://github.com/chriskiehl/Gooey/issues/755


"""
from argparse import ArgumentParser, _SubParsersAction, _MutuallyExclusiveGroup
from functools import wraps
from typing import Union, Any, Mapping, Dict, Callable

from gooey.python_bindings.types import Success, Failure, Try, InvalidChoiceException
from gooey.python_bindings.argparse_to_json import is_subparser
from gooey.util.functional import lift, identity, merge
from gooey.gui.constants import VALUE_PLACEHOLDER
from gooey.python_bindings.constants import Events
from gooey.python_bindings.coms import decode_payload
from gooey.gui.constants import RADIO_PLACEHOLDER

unexpected_exit_explanations = f'''
+=======================+
|Gooey Unexpected Error!|
+=======================+

Gooey encountered an unexpected error while trying to communicate 
with your program to process one of the {Events._fields} events.

These features are new and experimental! You may have encountered a bug! 

You can open a ticket with a small reproducible example here
https://github.com/chriskiehl/Gooey/issues
'''  # type: ignore


deserialize_failure_explanations = f'''
+==================================+
|Gooey Event Deserialization Error!|
+==================================+

Gooey was unable to deserialize the payload returned from your 
program when processing one of the {Events._fields} events. 

The payload *MUST* be in the `GooeyPublicState` schema. You can 
view the type information in `gooey.python_bindings.types.py`

Note, these features are new an experimental. This may be a bug on 
Gooey's side! 

You can open a ticket with a small reproducible example here:  
https://github.com/chriskiehl/Gooey/issues
'''


def check_value(registry: Dict[str, Exception], original_fn):
    """
    A Monkey Patch for `Argparse._check_value` which changes its
    behavior from one which throws an exception, to one which swallows
    the exception and silently records the failure.

    For certain argument types, Argparse calls a
    one-off `check_value` method. This method is inconvenient for us
    as it either returns nothing or throws an ArgumentException (thus leading
    to a sys.exit). Because our goal is to collect ALL
    errors for the entire parser, we must patch around this behavior.
    """
    @wraps(original_fn)
    def inner(self, action, value: Union[Any, Success, Failure]):
        def update_reg(_self, _action, _value):
            try:
                original_fn(_action, _value)
            except Exception as e:
                # check_value exclusively handles validating that the
                # supplied argument is a member of the `choices` set.
                # by default, it pops an exception containing all of the
                # available choices. However, since we're in a UI environment
                # all of that is redundant information. It's also *way too much*
                # information for things like FilterableDropdown. Thus we just
                # remap it to a 'simple' exception here.
                error = InvalidChoiceException("Selected option is not a valid choice")
                # IMPORTANT! note that this mutates the
                # reference that is passed in!
                registry[action.dest] = error

        # Inside of Argparse, `type_func` gets applied before the calls
        # to `check_value`. A such, depending on the type, this may already
        # be a lifted value.
        if isinstance(value, Success) and not isinstance(value, Failure):
            update_reg(self, action, value.value)
        elif isinstance(value, list) and all(x.isSuccess() for x in value):
            update_reg(self, action, [x.value for x in value])
        else:
            update_reg(self, action, value)
    return inner


def patch_args(*args, **kwargs):
    def inner(parser):
        return patch_argument(parser, *args, **kwargs)
    return inner

def patch_argument(parser, *args, **kwargs):
    """
    Mutates the supplied parser to append the arguments (args, kwargs) to
    the root parser and all subparsers.

    Example: `patch_argument(parser, '--ignore-gooey', action='store_true')

    This is used to punch additional cli arguments into the user's
    existing parser definition. By adding our arguments everywhere it allows
    us to use the `parse_args` machinery 'for free' without needing to
    worry about context shifts (like a repeated `dest` between subparsers).
    """
    parser.add_argument(*args, **kwargs)
    subparsers = list(filter(is_subparser, parser._actions))
    if subparsers:
        for sub in subparsers[0].choices.values():  # type: ignore
            patch_argument(sub, *args, **kwargs)
    return parser


def patch_all_parsers(patch_fn: Callable[[ArgumentParser], None], parser):
    subparsers = list(filter(is_subparser, parser._actions))
    if subparsers:
        for sub in subparsers[0].choices.values():  # type: ignore
            patch_all_parsers(patch_fn, sub)
    return parser


def recursively_patch_parser(parser, fn, *args):
    fn(parser, *args)
    subparsers = list(filter(is_subparser, parser._actions))
    if subparsers:
        for sub in subparsers[0].choices.values():  # type: ignore
            recursively_patch_parser(sub, fn, *args)
    return parser


def recursively_patch_actions(parser, fn):
    for action in parser._actions:
        if issubclass(type(action), _SubParsersAction):
            for subparser in action.choices.values():
                recursively_patch_actions(subparser, fn)
        else:
            fn(action)

def lift_action_type(action):
    """"""
    action.type = lift(action.type or identity)

def lift_actions_mutating(parser):
    """
    Mutates the supplied parser to lift all of its (likely) partial
    functions into total functions. See module docs for additional
    background. TL;DR: we have to "trick" Argparse into thinking
    every value is valid so that it doesn't short-circuit and sys.exit
    when it encounters a validation error. As such, we wrap everything
    in an Either/Try, and defer deciding the actual success/failure of
    the type transform until later in the execution when we have control.
    """
    recursively_patch_actions(parser, lift_action_type)
    # for action in parser._actions:
    #     if issubclass(type(action), _SubParsersAction):
    #         for subparser in action.choices.values():
    #             lift_actions_mutating(subparser)
    #     else:
    #         action.type = lift(action.type or identity)


def collect_errors(parser, error_registry: Dict[str, Exception], args: Dict[str, Try]) -> Dict[str, str]:
    """
    Merges all the errors from the Args mapping and error registry
    into a final dict.
    """
    # As is a theme throughout this module, to avoid Argparse
    # short-circuiting during parse-time, we pass a placeholder string
    # for required positional arguments which haven't yet been provided
    # by the user. So, what's happening here is that we're now collecting
    # all the args which have the placeholders so that we can flag them
    # all as required and missing.
    # Again, to be hyper clear, this is about being able to collect ALL
    # errors, versus just ONE error (Argparse default).
    required_but_missing = {k: 'This field is required'
                            for k, v in args.items()
                            if isinstance(v, Success) and v.value == VALUE_PLACEHOLDER}

    mutexes_required_but_missing = collect_mutex_errors(parser, args)

    errors = {k: str(v.error)
              for k, v in args.items()
              if v is not None and isinstance(v, Failure)}
    # Secondary errors are those which get frustratingly applied by
    # Argparse in a way which can't be easily tracked with patching
    # or higher order functions. See: `check_value` for more details.
    secondary = {k: str(e) for k, e in error_registry.items() if e}
    return merge(required_but_missing, errors, secondary, mutexes_required_but_missing)


def collect_mutex_errors(parser, args: Dict[str, Try]):
    """
    RadioGroups / MutuallyExclusiveGroup require extra care.
    Mutexes are not normal actions. They're not argument targets
    themselves, they have no `dest`, they're just parse-time containers
    for arguments. As such, there's no top-level argument destination
    we can tie a single error to. So, the strategy here is to mark _all_ of
    a radio group's children with an error if *any* of them are missing.

    It's a bit clunky, but what we've got to work with.
    """
    def dest_targets(group: _MutuallyExclusiveGroup):
        return [action.dest for action in group._group_actions]

    mutexes_missing = {dest for dest, v in args.items()
                     if isinstance(v, Success) and v.value == RADIO_PLACEHOLDER}

    return {dest: 'One of these must be provided'
            for group in parser._mutually_exclusive_groups
            for dest in dest_targets(group)
            # if the group is required and we've got one of its
            # children marked as missing
            if group.required and set(dest_targets(group)).intersection(mutexes_missing)}









def patch(obj, old_fn, new_fn):
    setattr(obj, old_fn, new_fn.__get__(obj, ArgumentParser))

def monkey_patch_check_value(parser, new_fn):
    parser._check_value = new_fn.__get__(parser, ArgumentParser)
    return parser

def monkey_patch(patcher, error_registry: Dict[str, Exception], parser):
    lift_actions_mutating(parser)
    patcher(parser)
    new_check_value = check_value(error_registry, parser._check_value)
    # https://stackoverflow.com/questions/28127874/monkey-patching-python-an-instance-method
    # parser._check_value = new_check_value.__get__(parser, ArgumentParser)

    return parser



def monkey_patch_for_form_validation(error_registry: Dict[str, Exception], parser):
    """
    Applies all the crufty monkey patching required to
    process a validate_form event
    """
    lift_actions_mutating(parser)
    patch_argument(parser, '--gooey-validate-form', action='store_true')
    patch_argument(parser, '--gooey-state', action='store', type=decode_payload)
    new_check_value = check_value(error_registry, parser._check_value)
    recursively_patch_parser(parser, monkey_patch_check_value, new_check_value)
    # https://stackoverflow.com/questions/28127874/monkey-patching-python-an-instance-method
    # patch(parser, '_check_value', new_check_value)
    # parser._check_value = new_check_value.__get__(parser, ArgumentParser)
    return monkey_patch_check_value(parser, new_check_value)




File: gooey/python_bindings/parser/gooey_parser.py




File: gooey/gui/bootstrap.py

'''
Main runner entry point for Gooey.
'''
from typing import Any, Tuple

import wx  # type: ignore
# wx.html and wx.xml imports required here to make packaging with
# pyinstaller on OSX possible without manually specifying `hidden_imports`
# in the build.spec
import wx.html  # type: ignore
import wx.lib.inspection  # type: ignore
import wx.richtext  # type: ignore
import wx.xml  # type: ignore

from gooey.gui import image_repository
from gooey.gui.application.application import RGooey
from gooey.gui.lang import i18n
from gooey.util.functional import merge
from rewx import render, create_element  # type: ignore


def run(build_spec):
    app, _ = build_app(build_spec)
    app.MainLoop()


def build_app(build_spec):
    app = wx.App(False)
    return _build_app(build_spec, app)


def _build_app(build_spec, app) -> Tuple[Any, wx.Frame]:
    """
    Note: this method is broken out with app as
    an argument to facilitate testing.
    """
    # use actual program name instead of script file name in macOS menu
    app.SetAppDisplayName(build_spec['program_name'])

    i18n.load(build_spec['language_dir'], build_spec['language'], build_spec['encoding'])
    imagesPaths = image_repository.loadImages(build_spec['image_dir'])
    gapp2 = render(create_element(RGooey, merge(build_spec, imagesPaths)), None)
    # wx.lib.inspection.InspectionTool().Show()
    # gapp.Show()
    gapp2.Show()
    return (app, gapp2)



File: gooey/gui/formatters.py

import os

import itertools

from gooey.gui.util.quoting import quote
from gooey.python_bindings.types import EnrichedItem, FormField
from gooey.gui.constants import VALUE_PLACEHOLDER, RADIO_PLACEHOLDER
from gooey.util.functional import assoc, associnMany


def value(field: FormField):
    if field['type'] in ['Checkbox', 'BlockCheckbox']:
        return field['checked']   # type: ignore
    elif field['type'] in ['Dropdown', 'Listbox', 'Counter']:
        return field['selected']   # type: ignore
    elif field['type'] == 'RadioGroup':
        if field['selected'] is not None:  # type: ignore
            return value(field['options'][field['selected']])  # type: ignore
        else:
            return None
    else:
        return field['value'] # type: ignore


def add_placeholder(field: FormField, placeholder=VALUE_PLACEHOLDER):
    """
    TODO: Docs about placeholders
    """
    if field['type'] in ['Checkbox', 'CheckBox', 'BlockCheckbox']:
        # there's no sane placeholder we can make for this one, as
        # it's kind of a nonsensical case: a required optional flag.
        # We set it to True here, which is equally nonsensical, but
        # ultimately will allow the validation to pass. We have no
        # way of passing a placeholder without even MORE monket patching
        # of the user's parser to rewrite the action type
        return assoc(field, 'checked', True)
    elif field['type'] in ['Dropdown', 'Listbox', 'Counter']:
        return assoc(field, 'selected', placeholder)
    elif field['type'] == 'RadioGroup':
        # We arbitrarily attach a placeholder for first RadioGroup option
        # and mark it as the selected one.
        return {
            **field,
            'selected': 0,
            'options': [
                add_placeholder(field['options'][0], placeholder=RADIO_PLACEHOLDER),  # type: ignore
                *field['options'][1:]  # type: ignore
            ]
        }
    else:
        return assoc(field, 'value', placeholder)


def formatArgument(item: EnrichedItem):
    if item['type'] in ['Checkbox', 'CheckBox', 'BlockCheckbox']:
        return checkbox(item['data'], value(item['field']))
    elif item['type'] == 'MultiFileChooser':
        return multiFileChooser(item['data'], value(item['field']))
    elif item['type'] == 'Textarea':
        return textArea(item['data'], value(item['field']))
    elif item['type'] == 'CommandField':
        return textArea(item['data'], value(item['field']))
    elif item['type'] == 'Counter':
        return counter(item['data'], value(item['field']))
    elif item['type'] == 'Dropdown':
        return dropdown(item['data'], value(item['field']))
    elif item['type'] == 'Listbox':
        return listbox(item['data'], value(item['field']))
    elif item['type'] == 'RadioGroup':
        selected = item['field']['selected']  # type: ignore
        if selected is not None:
            formField = item['field']['options'][selected]  # type: ignore
            argparseDefinition = item['data']['widgets'][selected]  # type: ignore
            return formatArgument(assoc(argparseDefinition, 'field', formField))  # type: ignore
        else:
            return None
    else:
        return general(item['data'], value(item['field']))


def placeholder(item: EnrichedItem):
    pass


def checkbox(metadata, value):
    return metadata['commands'][0] if value else None


def multiFileChooser(metadata, value):
    paths = ' '.join(quote(x) for x in value.split(os.pathsep) if x)
    if metadata['commands'] and paths:
        return u'{} {}'.format(metadata['commands'][0], paths)
    return paths or None


def textArea(metadata, value):
    if metadata['commands'] and value:
        return '{} {}'.format(metadata['commands'][0], quote(value.encode('unicode_escape')))
    else:
        return quote(value.encode('unicode_escape')) if value else ''


def commandField(metadata, value):
    if metadata['commands'] and value:
        return u'{} {}'.format(metadata['commands'][0], value)
    else:
        return value or None


def counter(metatdata, value):
    '''
    Returns
      str(option_string * DropDown Value)
      e.g.
      -vvvvv
    '''
    if not str(value).isdigit():
        return None
    command = str(metatdata['commands'][0]).strip()
    return ' '.join(itertools.repeat(command, int(value)))


def dropdown(metadata, value):
    if value == 'Select Option':
        return None
    elif metadata['commands'] and value:
        return u'{} {}'.format(metadata['commands'][0], quote(value))
    else:
        return quote(value) if value else ''


def listbox(meta, value):
    if meta['commands'] and value:
        return u'{} {}'.format(meta['commands'][0], ' '.join(map(quote, value)))
    else:
        return ' '.join(map(quote, value)) if value else ''


def general(metadata, value):
    if metadata.get('commands') and value:
        if not metadata.get('nargs'):
            v = quote(value)
        else:
            v = value
        return u'{0} {1}'.format(metadata['commands'][0], v)
    else:
        if not value:
            return None
        elif not metadata.get('nargs'):
            return quote(value)
        else:
            return value




File: gooey/gui/host.py

from concurrent.futures import ThreadPoolExecutor
from threading import Thread
from typing import Callable, Dict, Any

from gooey.gui import seeder
from gooey.gui import state as s
from gooey.gui.state import FullGooeyState
from gooey.python_bindings.types import Try, PublicGooeyState


def communicateFormValidation(state: FullGooeyState, callback: Callable[[Try[Dict[str, str]]], None]) -> None:
    communicateAsync(s.buildFormValidationCmd(state), state, callback)


def communicateSuccessState(state: FullGooeyState, callback: Callable[[Try[PublicGooeyState]], None]) -> None:
    communicateAsync(s.buildOnSuccessCmd(state), state, callback)


def communicateErrorState(state: FullGooeyState, callback: Callable[[Try[PublicGooeyState]], None]) -> None:
    communicateAsync(s.buildOnErrorCmd(state), state, callback)


def fetchFieldValidation():
    pass



def fetchFieldAction():
    pass

def fetchFormAction():
    pass


def communicateAsync(cmd: str, state: FullGooeyState, callback: Callable[[Any], None]):
    """
    Callable MUST be wrapped in wx.CallAfter if its going to
    modify the UI.
    """
    def work():
        result = seeder.communicate(cmd, state['encoding'])
        callback(result)
    thread = Thread(target=work)
    thread.start()




File: gooey/gui/events.py

"""
App wide event registry

Everything in the application is communicated via pubsub. These are the events
that tie everything together.
"""

import wx  # type: ignore

WINDOW_STOP     = wx.Window.NewControlId()
WINDOW_CANCEL   = wx.Window.NewControlId()
WINDOW_CLOSE    = wx.Window.NewControlId()
WINDOW_START    = wx.Window.NewControlId()
WINDOW_RESTART  = wx.Window.NewControlId()
WINDOW_EDIT     = wx.Window.NewControlId()

WINDOW_CHANGE   = wx.Window.NewControlId()
PANEL_CHANGE    = wx.Window.NewControlId()
LIST_BOX        = wx.Window.NewControlId()

CONSOLE_UPDATE  = wx.Window.NewControlId()
EXECUTION_COMPLETE = wx.Window.NewControlId()
PROGRESS_UPDATE = wx.Window.NewControlId()
TIME_UPDATE    = wx.Window.NewControlId()

USER_INPUT = wx.Window.NewControlId()

LEFT_DOWN = wx.Window.NewControlId()




File: gooey/gui/validators.py



def runValidator(f, value) -> bool:
    """
    Attempt to run the user supplied validation function

    Fall back to False in the even of any errors
    """
    try:
        return f(value)
    except:
        return False






File: gooey/gui/constants.py


VALUE_PLACEHOLDER = '::gooey/placeholder'
RADIO_PLACEHOLDER = '::gooey/radio-placeholder'




File: gooey/gui/__init__.py

__author__ = 'Chris'



File: gooey/gui/processor.py

import os
import re
import signal
import subprocess
import sys
from functools import partial
from threading import Thread

import psutil  # type: ignore

from gooey.gui import events
from gooey.gui.pubsub import pub
from gooey.gui.util.casting import safe_float
from gooey.util.functional import unit, bind
from gooey.python_bindings.types import GooeyParams


try:
    import _winapi
    creationflag = subprocess.CREATE_NEW_PROCESS_GROUP
except ModuleNotFoundError:
    # default Popen creation flag
    creationflag = 0


class ProcessController(object):

    @classmethod
    def of(cls, params: GooeyParams):
        return cls(
            params.get('progress_regex'),
            params.get('progress_expr'),
            params.get('hide_progress_msg'),
            params.get('encoding'),
            params.get('requires_shell'),
            params.get('shutdown_signal', signal.SIGTERM)
        )

    def __init__(self, progress_regex, progress_expr, hide_progress_msg,
                 encoding, shell=True, shutdown_signal=signal.SIGTERM, testmode=False):
        self._process = None
        self.progress_regex = progress_regex
        self.progress_expr = progress_expr
        self.hide_progress_msg = hide_progress_msg
        self.encoding = encoding
        self.wasForcefullyStopped = False
        self.shell_execution = shell
        self.shutdown_signal = shutdown_signal
        self.testMode = testmode

    def was_success(self):
        self._process.communicate()
        return self._process.returncode == 0

    def poll(self):
        if not self._process:
            raise Exception('Not started!')
        return self._process.poll()

    def stop(self):
        """
        Sends a signal of the user's choosing (default SIGTERM) to
        the child process.
        """
        if self.running():
            self.wasForcefullyStopped = True
            self.send_shutdown_signal()

    def send_shutdown_signal(self):
        self._send_signal(self.shutdown_signal)

    def _send_signal(self, sig):
        parent = psutil.Process(self._process.pid)
        for child in parent.children(recursive=True):
            child.send_signal(sig)
        parent.send_signal(sig)

    def running(self):
        return self._process and self.poll() is None

    def run(self, command):
        """
        Kicks off the user's code in a subprocess.

        Implementation Note: CREATE_NEW_SUBPROCESS is required to have signals behave sanely
        on windows. See the signal_support module for full background.
        """
        self.wasForcefullyStopped = False
        env = os.environ.copy()
        env["GOOEY"] = "1"
        env["PYTHONIOENCODING"] = self.encoding
        # TODO: why is this try/catch here..?
        try:
            self._process = subprocess.Popen(
                command.encode(sys.getfilesystemencoding()),
                stdout=subprocess.PIPE, stdin=subprocess.PIPE,
                stderr=subprocess.STDOUT, shell=self.shell_execution, env=env,
                creationflags=creationflag)
        except:
            self._process = subprocess.Popen(
                command,
                stdout=subprocess.PIPE, stdin=subprocess.PIPE,
                stderr = subprocess.STDOUT, shell = self.shell_execution, env=env,
                creationflags=creationflag
            )

        # the message pump depends on the wx instance being initiated and its
        # mainloop running (to dispatch pubsub messages). This makes testing difficult
        # so we only spin up the thread when we're not testing.
        if not self.testMode:
            t = Thread(target=self._forward_stdout, args=(self._process,))
            t.start()

    def _forward_stdout(self, process):
        '''
        Reads the stdout of `process` and forwards lines and progress
        to any interested subscribers
        '''
        while True:
            line = process.stdout.readline()
            if not line:
                break
            _progress = self._extract_progress(line)

            pub.send_message(events.PROGRESS_UPDATE, progress=_progress)
            if _progress is None or self.hide_progress_msg is False:
                pub.send_message(events.CONSOLE_UPDATE,
                                 msg=line.decode(self.encoding))
        pub.send_message(events.EXECUTION_COMPLETE)

    def _extract_progress(self, text):
        '''
        Finds progress information in the text using the
        user-supplied regex and calculation instructions
        '''
        # monad-ish dispatch to avoid the if/else soup
        find = partial(re.search, string=text.strip().decode(self.encoding))
        regex = unit(self.progress_regex)
        match = bind(regex, find)
        result = bind(match, self._calculate_progress)
        return result

    def _calculate_progress(self, match):
        '''
        Calculates the final progress value found by the regex
        '''
        if not self.progress_expr:
            return safe_float(match.group(1))
        else:
            return self._eval_progress(match)

    def _eval_progress(self, match):
        '''
        Runs the user-supplied progress calculation rule
        '''
        _locals = {k: safe_float(v) for k, v in match.groupdict().items()}
        if "x" not in _locals:
            _locals["x"] = [safe_float(x) for x in match.groups()]
        try:
            return int(eval(self.progress_expr, {}, _locals))
        except:
            return None




File: gooey/gui/three_to_four.py

'''
Util for supporting WxPython 3 & 4
'''

import wx  # type: ignore
try:
    import wx.adv  # type: ignore
except ImportError:
    pass

isLatestVersion = wx.version().startswith('4')


class Constants:
    if isLatestVersion:
        WX_FONTSTYLE_NORMAL = wx.FONTSTYLE_NORMAL
        WX_DP_DROPDOWN = wx.adv.DP_DROPDOWN
    else:
        WX_FONTSTYLE_NORMAL = wx.FONTWEIGHT_NORMAL
        WX_DP_DROPDOWN = wx.DP_DROPDOWN


class Classes:
    if isLatestVersion:
        DatePickerCtrl = wx.adv.DatePickerCtrl
    else:
        DatePickerCtrl = wx.DatePickerCtrl

    if isLatestVersion:
        TimePickerCtrl = wx.adv.TimePickerCtrl
    else:
        TimePickerCtrl = wx.TimePickerCtrl




def imageFromBitmap(bitmap):
    if isLatestVersion:
        return bitmap.ConvertToImage()
    else:
        return wx.ImageFromBitmap(bitmap)


def bitmapFromImage(image):
    if isLatestVersion:
        return wx.Bitmap(image)
    else:
        return wx.BitmapFromImage(image)


def bitmapFromBufferRGBA(im, rgba):
    if isLatestVersion:
        return wx.Bitmap.FromBufferRGBA(im.size[0], im.size[1], rgba)
    else:
        return wx.BitmapFromBufferRGBA(im.size[0], im.size[1], rgba)

def AboutDialog():
    if isLatestVersion:
        return wx.adv.AboutDialogInfo()
    else:
        return wx.AboutDialogInfo()


def AboutBox(aboutDialog):
    return (wx.adv.AboutBox(aboutDialog)
            if isLatestVersion
            else wx.AboutBox(aboutDialog))




File: gooey/gui/seeder.py

"""
Util for talking to the client program in order to retrieve
dynamic defaults for the UI
"""
import subprocess
from json import JSONDecodeError
from subprocess import CalledProcessError

from gooey.python_bindings.types import Try, Success, Failure
from gooey.python_bindings.coms import deserialize_inbound


def communicate(cmd, encoding) -> Try:
    """
    Invoke the processes specified by `cmd`.
    Assumes that the process speaks JSON over stdout. Non-json response
    are treated as an error.

    Implementation Note: I don't know why, but `Popen` is like ~5-6x faster
    than `check_output`. in practice, it means waiting for ~1/10th
    of a second rather than ~7/10ths of a second. A
    difference which is pretty weighty when there's a
    user waiting on the other end.
    """
    try:
        proc = subprocess.Popen(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE
        )
        out, err = proc.communicate()
        if out and proc.poll() == 0:
            return Success(deserialize_inbound(out, encoding))
        else:
            return Failure(CalledProcessError(proc.returncode, cmd, output=out, stderr=err))
    except JSONDecodeError as e:
        return Failure(e)




File: gooey/gui/cli.py

import json
from itertools import chain

from copy import deepcopy

from gooey.util.functional import compact
from typing import List, Optional

from gooey.gui.constants import VALUE_PLACEHOLDER
from gooey.gui.formatters import formatArgument
from gooey.python_bindings.types import FieldValue, Group, Item
from gooey.util.functional import merge  # type: ignore
from gooey.gui.state import FullGooeyState

'''
primary :: Target -> Command -> Array Arg -> Array Arg -> Boolean -> CliString
validateForm :: Target -> Command -> Array Arg -> Array Arg -> CliString
validateField :: Target -> Command -> Array Arg -> Array Arg -> ArgId -> CliString
completed :: Target -> Command -> FromState -> CliString
failed :: Target -> Command -> FromState -> CliString
fieldAction :: Target -> Command ->   

'''


def buildSuccessCmd(state: FullGooeyState):
    subcommand = state['subcommands'][state['activeSelection']]
    widgets = state['widgets'][subcommand]




def onSuccessCmd(target: str, subCommand: str, formState: List[str]) -> str:
    command = subCommand if not subCommand == '::gooey/default' else ''
    return f'{target} {command} --gooey-on-success {json.dumps(formState)}'


def onErrorCmd(target: str, subCommand: str, formState: List[str]) -> str:
    command = subCommand if not subCommand == '::gooey/default' else ''
    return f'{target} {command} --gooey-on-error {json.dumps(formState)}'


def formValidationCmd(target: str, subCommand: str, positionals: List[FieldValue], optionals: List[FieldValue]) -> str:
    positional_args = [cmdOrPlaceholderOrNone(x) for x in positionals]
    optional_args = [cmdOrPlaceholderOrNone(x) for x in optionals]
    command = subCommand if not subCommand == '::gooey/default' else ''
    return u' '.join(compact([
        target,
        command,
        *optional_args,
        '--gooey-validate-form',
        '--' if positional_args else '',
        *positional_args]))


def cliCmd(target: str,
           subCommand: str,
           positionals: List[FieldValue],
           optionals: List[FieldValue],
           suppress_gooey_flag=False) -> str:
    positional_args = [arg['cmd'] for arg in positionals]
    optional_args = [arg['cmd'] for arg in optionals]
    command = subCommand if not subCommand == '::gooey/default' else ''
    ignore_flag = '' if suppress_gooey_flag else '--ignore-gooey'
    return u' '.join(compact([
        target,
        command,
        *optional_args,
        ignore_flag,
        '--' if positional_args else '',
        *positional_args]))


def cmdOrPlaceholderOrNone(field: FieldValue) -> 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 field['clitype'] == 'positional':
        return field['cmd'] or VALUE_PLACEHOLDER
    elif field['clitype'] != 'positional' and field['meta']['required']:
        # same rationale applies here. We supply the argument
        # along with a fixed placeholder (when relevant i.e. `store`
        # actions)
        return field['cmd'] or formatArgument(field['meta'], VALUE_PLACEHOLDER)
    else:
        # Optional values are, well, optional. So, like usual, we send
        # them if present or drop them if not.
        return field['cmd']


def buildCliString(target, subCommand, positional, optional, suppress_gooey_flag=False):
    positionals = deepcopy(positional)
    if positionals:
        positionals.insert(0, "--")

    arguments = ' '.join(compact(chain(optional, positionals)))

    if subCommand != '::gooey/default':
        arguments = u'{} {}'.format(subCommand, arguments)

    ignore_flag = '' if suppress_gooey_flag else '--ignore-gooey'
    return u'{} {} {}'.format(target, ignore_flag, arguments)



File: gooey/gui/pubsub.py

import wx  # type: ignore
from collections import defaultdict

__ALL__ = ['pub']


class PubSub(object):
    """
    A super simplified clone of Wx.lib.pubsub since it doesn't exist on linux
    """

    def __init__(self):
        self.registry = defaultdict(list)

    def subscribe(self, event, handler):
        self.registry[event].append(handler)

    def send_message(self, event, **kwargs):
        for event_handler in self.registry.get(event, []):
            wx.CallAfter(event_handler, **kwargs)

    def send_message_sync(self, event, **kwargs):
        """
        ===== THIS IS NOT THREAD SAFE =====
        Synchronously sends the message to all relevant consumers
        and blocks until a response is received.

        This MUST ONLY be used for communication within
        the same thread! It exists primarily as an escape
        hatch for bubbling up messages (which would be garbage
        collected in the CallAfter form) to interested components
        """
        for event_handler in self.registry.get(event, []):
            event_handler(**kwargs)

pub = PubSub()



File: gooey/gui/imageutil.py

'''
Utilities for loading, resizing and converting between PIL and WX image formats
'''

import six
from PIL import Image  # type: ignore
import wx  # type: ignore

from gooey.gui.three_to_four import bitmapFromBufferRGBA



def loadImage(img_path):
    return Image.open(img_path)


def resizeImage(im, targetHeight):
    im.thumbnail((six.MAXSIZE, targetHeight))
    return im


def wrapBitmap(im, parent):
    try:
        rgba = im.convert('RGBA').tobytes()
    except AttributeError:
        rgba = im.convert('RGBA').tostring()

    bitmapData = bitmapFromBufferRGBA(im, rgba)
    return wx.StaticBitmap(parent, bitmap=bitmapData)


if __name__ == '__main__':
    pass



File: gooey/gui/state.py

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}'



File: gooey/gui/image_repository.py

'''
Collection of the image paths.

The module is meant to act as a singleton, hence the globals() abuse.

Image credit: kidcomic.net
'''
import os
from functools import partial

from gooey.gui.util.freeze import getResourcePath
from gooey.util.functional import merge

filenames = {
    'programIcon': 'program_icon.png',
    'successIcon': 'success_icon.png',
    'runningIcon': 'running_icon.png',
    'configIcon': 'config_icon.png',
    'errorIcon': 'error_icon.png'
}


def loadImages(targetDir):
    defaultImages = resolvePaths(getResourcePath('images'), filenames)
    return {'images': merge(defaultImages, collectOverrides(targetDir, filenames))}


def getImageDirectory(targetDir):
    return getResourcePath('images') \
           if targetDir == 'default' \
           else targetDir


def collectOverrides(targetDir, filenames):
    if targetDir == '::gooey/default':
        return {}

    pathto = partial(os.path.join, targetDir)
    if not os.path.isdir(targetDir):
        raise IOError('Unable to find the user supplied directory {}'.format(
            targetDir))

    return {varname: pathto(filename)
            for varname, filename in filenames.items()
            if os.path.exists(pathto(filename))}


def resolvePaths(dirname, filenames):
    return {key:  os.path.join(dirname, filename)
            for key, filename in filenames.items()}





File: gooey/gui/validation.py

from typing import Mapping

from gooey import Events
from gooey.python_bindings.types import Try
from gooey.util.functional import merge


def validateForm(self) -> Try[Mapping[str, str]]:  # or Exception
    config = self.navbar.getActiveConfig()
    localErrors: Mapping[str, str] = config.getErrors()
    dynamicResult: Try[Mapping[str, str]] = self.fetchDynamicValidations()

    combineErrors = lambda m: merge(localErrors, m)
    return dynamicResult.map(combineErrors)


def fetchDynamicValidations(self) -> Try[Mapping[str, str]]:
    # only run the dynamic validation if the user has
    # specifically subscribed to that event
    if Events.VALIDATE_FORM in self.buildSpec.get('use_events', []):
        cmd = self.getCommandDetails()
        return seeder.communicate2(cli.formValidationCmd(
            cmd.target,
            cmd.subcommand + 'baba',
            cmd.positionals,
            cmd.optionals
        ), self.buildSpec['encoding'])
    else:
        # shim response if nothing to do.
        return Success({})


File: gooey/gui/util/time.py

"""
Module for evaluating time elapsed & time remaining from progress
"""
import wx  # type: ignore
from gooey.gui.pubsub import pub
from gooey.gui import events

class Timing(object):

    def __init__(self, parent):
        self.startTime = 0
        self.estimatedRemaining = None
        self.wxTimer = wx.Timer(parent)
        self.parent = parent
        parent.Bind(wx.EVT_TIMER, self.publishTime, self.wxTimer)

        pub.subscribe(events.PROGRESS_UPDATE, self._updateEstimate)

    def _updateEstimate(self, *args, **kwargs):
        prog = kwargs.get('progress')
        if(not prog): 
            self.estimatedRemaining = None
            return
        if(prog > 0):
            self.estimatedRemaining = estimate_time_remaining(prog,self.startTime)

    def publishTime(self, *args, **kwargs):
        pub.send_message(
            events.TIME_UPDATE,
            start=self.startTime,
            current=get_current_time(),
            elapsed_time=format_interval(get_elapsed_time(self.startTime)),
            estimatedRemaining=format_interval(self.estimatedRemaining))

    def start(self):
        self.startTime = get_current_time()
        self.estimatedRemaining = None
        self.wxTimer.Start()

    def stop(self):
        self.wxTimer.Stop()

def format_interval(timeValue):
    """
    Formats a number of seconds as a clock time, [H:]MM:SS
    Parameters
    ----------
    t  : int
        Number of seconds.
    Returns
    -------
    out  : str
        [H:]MM:SS
    """
    # https://github.com/tqdm/tqdm/blob/0cd9448b2bc08125e74538a2aea6af42ee1a7b6f/tqdm/std.py#L228
    try:
        mins, s = divmod(int(timeValue), 60)
        h, m = divmod(mins, 60)
        if h:
            return '{0:d}:{1:02d}:{2:02d}'.format(h, m, s)
        else:
            return '{0:02d}:{1:02d}'.format(m, s)
    except:
        return None

def get_elapsed_time(startTime):
    """
    Get elapsed time in form of seconds. Provide a start time in seconds as float.

    Args:
        startTime (float): Start time to compare against in seconds.

    Returns:
        float: Time between start time and now
    """
    return get_current_time() - startTime

def estimate_time_remaining(progress,startTime):
    # https://github.com/tqdm/tqdm/blob/0cd9448b2bc08125e74538a2aea6af42ee1a7b6f/tqdm/std.py#L392
    # https://github.com/tqdm/tqdm/blob/0cd9448b2bc08125e74538a2aea6af42ee1a7b6f/tqdm/std.py#L417
    _rate = progress / get_elapsed_time(startTime)
    return ((100 - progress) / _rate)

def get_current_time():
    """
    Returns a float of the current time in seconds. Attempt to import perf_counter (more accurate in 3.4+), otherwise utilise timeit.

    Returns:
        float: Current time in seconds from performance counter.
    """
    try:
        from time import perf_counter
        return perf_counter()
    except:
        import timeit
        return timeit.default_timer()



File: gooey/gui/util/__init__.py

__author__ = 'Chris'



File: gooey/gui/util/casting.py



def safe_int(n):
  return _safe_cast(int, n)

def safe_float(n):
  return _safe_cast(float, n)


def _safe_cast(_type, val):
  try:
    return _type(val)
  except ValueError:
    return None




File: gooey/gui/util/functional.py

'''
Utils for functional methodologies throughout Gooey

'''


def merge_dictionaries(x,y):
    """
    Merge 2 dictionaries with y taking overwriting x if a key collision is found

    This is mainly useful for maintaining the dictionary arguments to allow for more expressive & extensible arguments.
    https://stackoverflow.com/questions/38987/how-do-i-merge-two-dictionaries-in-a-single-expression-in-python-taking-union-o

    Args:
        x (dict): Input dictionary
        y (dict): Input dictionary

    Returns:
        The combined dictionary of x & y with y taking preference on the occasion of key collision
    """
    if x is None:
        x = {}
    if y is None:
        y = {}
    try:
        return {**x,**y}
    except:
        z = x.copy()
        z.update(y)
        return z



File: gooey/gui/util/quoting.py

import sys


if sys.platform.startswith("win"):
  def quote(value):
    return u'"{}"'.format(u'{}'.format(value).replace(u'"', u'""'))
else:  # POSIX shell
  def quote(value):
    return u"'{}'".format(u'{}'.format(value).replace(u"'", u"'\\''"))




File: gooey/gui/util/freeze.py

'''
Utils for retrieving resources when when in a frozen state.

MEIPASS explanation:
https://pythonhosted.org/PyInstaller/#run-time-operation
'''
import os
import sys


def is_frozen():
    return getattr(sys, 'frozen', False)


def getResourcePath(*args):
    if is_frozen():
        # MEIPASS explanation:
        # https://pythonhosted.org/PyInstaller/#run-time-operation
        basedir = getattr(sys, '_MEIPASS', None)
        if not basedir:
            basedir = os.path.dirname(sys.executable)
        resource_dir = os.path.join(basedir, 'gooey')
        if not os.path.isdir(resource_dir):
            raise IOError(
                (
                "Cannot locate Gooey resources. It seems that the program was frozen, "
                "but resource files were not copied into directory of the executable "
                "file. Please copy `languages` and `images` folders from gooey module "
                "directory into `{}{}` directory. Using PyInstaller, a.datas in .spec "
                "file must be specified.".format(resource_dir, os.sep)))
    else:
        resource_dir = os.path.normpath(
            os.path.join(os.path.dirname(__file__), '..', '..'))
    return os.path.join(resource_dir, *args)


def localResourcePath(path):
    """
    A packaging aware util for getting the path to the local working directory.
    When non-packaged, this is os.getcwd(), when packaged, it will be the local
    (dynamic) directory where PyInstaller decompresses content.
    """
    if is_frozen():
        basedir = getattr(sys, '_MEIPASS', None)
        return os.path.join(basedir or sys.executable, path)
    else:
        return os.path.join(os.getcwd(), path)



File: gooey/gui/util/wx_util.py

"""
Collection of Utility methods for creating often used, pre-styled wx Widgets
"""
from functools import wraps

import wx  # type: ignore
from contextlib import contextmanager

from gooey.gui.three_to_four import Constants


def callafter(f):
    """
    Wraps the supplied function in a wx.CallAfter
    for Thread-safe interop with WX.
    """
    @wraps(f)
    def inner(*args, **kwargs):
        wx.CallAfter(f, *args, **kwargs)
    return inner


@contextmanager
def transactUI(obj):
    """
    Coarse grain UI locking to avoid glitchy UI updates
    """
    obj.Freeze()
    try:
        yield
    finally:
        obj.Layout()
        obj.Thaw()


styles = {
    'h0': (wx.FONTFAMILY_DEFAULT, Constants.WX_FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False),
    'h1': (wx.FONTFAMILY_DEFAULT, Constants.WX_FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False),
    'h2': (wx.FONTFAMILY_DEFAULT, Constants.WX_FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False),
    'bold': (wx.FONTFAMILY_DEFAULT, Constants.WX_FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False)
}




def make_bold(statictext):
    pointsize = statictext.GetFont().GetPointSize()
    font = wx.Font(pointsize, *styles['bold'])
    statictext.SetFont(font)


def dark_grey(statictext):
    return withColor(statictext, (54, 54, 54))


def withColor(statictext, hex):
    statictext.SetForegroundColour(hex)
    return statictext


def h0(parent, label):
    text = wx.StaticText(parent, label=label)
    font_size = text.GetFont().GetPointSize()
    font = wx.Font(int(font_size * 1.4, *styles['h0']))
    text.SetFont(font)
    return text


def h1(parent, label):
    return _header(parent, label, styles['h1'])


def h2(parent, label):
    return _header(parent, label, styles['h2'])


def _header(parent, label, styles):
    text = wx.StaticText(parent, label=label)
    font_size = text.GetFont().GetPointSize()
    font = wx.Font(int(font_size * 1.2), *styles)
    text.SetFont(font)
    return text


def horizontal_rule(parent):
    return _rule(parent, wx.LI_HORIZONTAL)


def vertical_rule(parent):
    return _rule(parent, wx.LI_VERTICAL)


def _rule(parent, direction):
    line = wx.StaticLine(parent, -1, style=direction)
    line.SetSize((10, 10))
    return line



File: gooey/gui/util/filedrop.py

import wx  # type: ignore


class FileDrop(wx.FileDropTarget):
    def __init__(self, window, dropStrategy=None):
        wx.FileDropTarget.__init__(self)
        self.window = window
        self.dropHandler = dropStrategy or self._defaultStrategy

    def OnDropFiles(self, x, y, filenames):
        return self.dropHandler(x, y, filenames)

    def _defaultStrategy(self, x, y, filenames):
        for name in filenames:
            self.window.WriteText(name)
        return True



File: gooey/gui/lang/i18n_config.py

"""
Configuration settings for the program.
More or less, this is just used so that the i18n module
can load the right language file at runtime.
"""

LANG = 'english'



File: gooey/gui/lang/__init__.py

__author__ = 'Chris'



File: gooey/gui/lang/i18n.py

'''
Created on Jan 25, 2014

@author: Chris

Provides Internationalization for all text within the program.

'''

import io
import os
import json

__all__ = ['load', '_']

_DICTIONARY = None

def load(language_dir, filename, encoding):
  ''' Open and return the supplied json file '''
  global _DICTIONARY
  try:
    json_file = filename + '.json'
    with io.open(os.path.join(language_dir, json_file), 'r', encoding=encoding) as f:
      _DICTIONARY = json.load(f)
  except IOError:
    raise IOError('{0} Language file not found at location {1}. '
                  'Make sure that your translation file is in the '
                  'listed language directory'.format(filename.title(), language_dir))

def translate(key):
  return _DICTIONARY.get(key, '(Translate me!) {}'.format(key))

def _(key):
  return translate(key)



File: gooey/gui/components/menubar.py

import webbrowser
from functools import partial

import wx  # type: ignore

from gooey.gui import three_to_four
from gooey.gui.components.dialogs import HtmlDialog


class MenuBar(wx.MenuBar):
    """
    Wx.MenuBar handles converting the users list of Menu Groups into
    concrete wx.Menu instances.
    """

    def __init__(self, buildSpec, *args, **kwargs):
        super(MenuBar,self).__init__(*args, **kwargs)
        self.buildSpec = buildSpec
        self.makeMenuItems(buildSpec.get('menu', []))


    def makeMenuItems(self, menuGroups):
        """
        Assign the menu groups list to wx.Menu instances
        and bind the appropriate handlers.
        """
        for menuGroup in menuGroups:
            menu = wx.Menu()
            for item in menuGroup.get('items'):
                option = menu.Append(wx.NewId(), item.get('menuTitle', ''))
                self.Bind(wx.EVT_MENU, self.handleMenuAction(item), option)
            self.Append(menu, '&' + menuGroup.get('name'))


    def handleMenuAction(self, item):
        """
        Dispatch based on the value of the type field.
        """
        handlers = {
            'Link': self.openBrowser,
            'AboutDialog': self.spawnAboutDialog,
            'MessageDialog': self.spawnMessageDialog,
            'HtmlDialog': self.spawnHtmlDialog
        }
        f = handlers[item['type']]
        return partial(f, item)


    def openBrowser(self, item, *args, **kwargs):
        """
        Open the supplied URL in the user's default browser.
        """
        webbrowser.open(item.get('url'))


    def spawnMessageDialog(self, item, *args, **kwargs):
        """
        Show a simple message dialog with the user's message and caption.
        """
        wx.MessageDialog(self, item.get('message', ''),
                               caption=item.get('caption', '')).ShowModal()


    def spawnHtmlDialog(self, item, *args, **kwargs):
        HtmlDialog(caption=item.get('caption', ''), html=item.get('html')).ShowModal()


    def spawnAboutDialog(self, item, *args, **kwargs):
        """
        Fill the wx.AboutBox with any relevant info the user provided
        and launch the dialog
        """
        aboutOptions = {
            'name': 'SetName',
            'version': 'SetVersion',
            'description': 'SetDescription',
            'copyright': 'SetCopyright',
            'website': 'SetWebSite',
            'developer': 'AddDeveloper',
            'license': 'SetLicense'
        }
        about = three_to_four.AboutDialog()
        for field, method in aboutOptions.items():
            if field in item:
                getattr(about, method)(item[field])

        three_to_four.AboutBox(about)





File: gooey/gui/components/sidebar.py

import wx  # type: ignore

from gooey.gui.util import wx_util


class Sidebar(wx.Panel):
    """
    Sidebar handles the show/hide logic so that it mirrors the functionality
    of the wx.Notebook class (which wants to control everything)
    """
    def __init__(self, parent, buildSpec, configPanels, *args, **kwargs):
        super(Sidebar, self).__init__(parent, *args, **kwargs)
        self._parent = parent
        self.buildSpec = buildSpec
        self.configPanels = configPanels
        self.activeSelection = 0
        self.options = list(self.buildSpec['widgets'].keys())
        self.leftPanel = wx.Panel(self)
        self.label = wx_util.h1(self.leftPanel, self.buildSpec.get('sidebar_title'))
        self.listbox = wx.ListBox(self.leftPanel, -1, choices=self.options)
        self.Bind(wx.EVT_LISTBOX, self.swapConfigPanels, self.listbox)
        self.layoutComponent()
        self.listbox.SetSelection(0)


    def getSelectedGroup(self):
        """Return the currently active 'group' i.e. the root SubParser """
        return self.options[self.activeSelection]


    def getActiveConfig(self):
        """Return the currently visible config screen"""
        return self.configPanels[self.activeSelection]


    def swapConfigPanels(self, event):
        """Hide/show configuration panels based on the currently selected
         option in the sidebar """
        for id, panel in enumerate(self.configPanels):
            panel.Hide()
        self.activeSelection = event.Selection
        self.configPanels[event.Selection].Show()
        self._parent.Layout()


    def layoutComponent(self):
        left = self.layoutLeftSide()

        hsizer = wx.BoxSizer(wx.HORIZONTAL)
        hsizer.Add(left, 0, wx.EXPAND)

        if not self.buildSpec['tabbed_groups']:
            # only add it for non-tabbed layouts as it looks
            # weird against the tabbed ones
            hsizer.Add(wx_util.vertical_rule(self), 0, wx.EXPAND)

        for body in self.configPanels:
            body.Reparent(self)
            hsizer.Add(body, 1, wx.EXPAND)
            body.Hide()
        self.configPanels[0].Show()
        self.SetSizer(hsizer)

        if not self.buildSpec['show_sidebar']:
            left.Show(False)

        self.Layout()


    def layoutLeftSide(self):
        self.leftPanel.SetBackgroundColour(self.buildSpec['sidebar_bg_color'])
        self.leftPanel.SetSize((180, 0))
        self.leftPanel.SetMinSize((180, 0))

        container = wx.BoxSizer(wx.VERTICAL)
        container.AddSpacer(15)
        container.Add(self.label, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 10)
        container.AddSpacer(5)

        container.Add(self.listbox, 1, wx.LEFT | wx.RIGHT | wx.EXPAND, 10)
        container.AddSpacer(20)
        self.leftPanel.SetSizer(container)
        return self.leftPanel






File: gooey/gui/components/console.py

import webbrowser

import wx  # type: ignore

from gooey.gui.lang.i18n import _
from .widgets.basictextconsole import BasicTextConsole


class Console(wx.Panel):
    '''
    Textbox console/terminal displayed during the client program's execution.
    '''
    self_managed = True

    def __init__(self, parent, buildSpec, **kwargs):
        wx.Panel.__init__(self, parent, name='console', **kwargs)
        self.buildSpec = buildSpec

        self.text = wx.StaticText(self, label=_("status"))
        if buildSpec["richtext_controls"]:
            from .widgets.richtextconsole import RichTextConsole
            self.textbox = RichTextConsole(self)
        else:
            self.textbox = BasicTextConsole(self)

        self.defaultFont = self.textbox.GetFont()

        self.textbox.SetFont(wx.Font(
            self.buildSpec['terminal_font_size'] or self.defaultFont.GetPointSize(),
            self.getFontStyle(),
            wx.NORMAL,
            self.buildSpec['terminal_font_weight'] or wx.NORMAL,
            False,
            self.getFontFace(),
        ))
        self.textbox.SetForegroundColour(self.buildSpec['terminal_font_color'])
         
        self.layoutComponent()
        self.Layout()
        self.Bind(wx.EVT_TEXT_URL, self.evtUrl, self.textbox)

    def evtUrl(self, event):
        if event.MouseEvent.LeftUp():
            # The rich console provides the embedded URL via GetString()
            # but the basic console does not
            webbrowser.open(
                event.GetString() or
                self.textbox.GetRange(event.URLStart,event.URLEnd))
        event.Skip()


    def getFontStyle(self):
        """
        Force wx.Modern style to support legacy
        monospace_display param when present
        """
        return (wx.MODERN
                if self.buildSpec['monospace_display']
                else wx.DEFAULT)


    def getFontFace(self):
        """Choose the best font face available given the user options"""
        userFace = self.buildSpec['terminal_font_family'] or self.defaultFont.GetFaceName()
        return (''
                if self.buildSpec['monospace_display']
                else userFace)


    def logOutput(self, *args, **kwargs):
        """Event Handler for console updates coming from the client's program"""
        self.appendText(kwargs.get('msg'))


    def appendText(self, txt):
        """
        Append the text to the main TextCtrl.

        Note! Must be called from a Wx specific thread handler to avoid
        multi-threaded explosions (e.g. wx.CallAfter)
        """
        self.textbox.AppendText(txt)

    def clear(self):
        """
            Clear the the main TextCtrl.
        """
        self.textbox.Clear()


    def getText(self):
        return self.textbox.GetValue()

    def layoutComponent(self):
        self.SetBackgroundColour(self.buildSpec.get('terminal_panel_color', '#F0F0F0'))
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.AddSpacer(10)
        sizer.Add(self.text, 0, wx.LEFT, 20)
        sizer.AddSpacer(10)
        sizer.Add(self.textbox, 1, wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND, 20)
        sizer.AddSpacer(20)
        self.SetSizer(sizer)





File: gooey/gui/components/config.py

from typing import Mapping, List

import wx  # type: ignore
from wx.lib.scrolledpanel import ScrolledPanel  # type: ignore

from gooey.gui.components.mouse import notifyMouseEvent
from gooey.gui.components.util.wrapped_static_text import AutoWrappedStaticText
from gooey.gui.lang.i18n import _
from gooey.gui.util import wx_util
from gooey.python_bindings.types import FormField
from gooey.util.functional import getin, flatmap, indexunique


class ConfigPage(ScrolledPanel):
    self_managed = True

    def __init__(self, parent, rawWidgets, buildSpec,  *args, **kwargs):
        super(ConfigPage, self).__init__(parent, *args, **kwargs)

        self.SetupScrolling(scroll_x=False, scrollToTop=False)
        self.rawWidgets = rawWidgets
        self.buildSpec = buildSpec
        self.reifiedWidgets = []
        self.layoutComponent()
        self.Layout()
        self.widgetsMap = indexunique(lambda x: x._id, self.reifiedWidgets)
        self.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
        ## TODO: need to rethink what uniquely identifies an argument.
        ## Out-of-band IDs, while simple, make talking to the client program difficult
        ## unless they're agreed upon before hand. Commands, as used here, have the problem
        ## of (a) not being nearly granular enough (for instance,  `-v` could represent totally different
        ## things given context/parser position), and (b) cannot identify positional args.

    def getName(self, group):
        """
        retrieve the group name from the group object while accounting for
        legacy fixed-name manual translation requirements.
        """
        name = group['name']
        return (_(name)
                if name in {'optional_args_msg', 'required_args_msg'}
                else name)


    def firstCommandIfPresent(self, widget):
        commands = widget._meta['commands']
        return commands[0] if commands else ''

    def getPositionalArgs(self):
        return [widget.getValue()['cmd'] for widget in self.reifiedWidgets
                if widget.info['cli_type'] == 'positional']

    def getOptionalArgs(self):
        return [widget.getValue()['cmd'] for widget in self.reifiedWidgets
                if widget.info['cli_type'] != 'positional']


    def getPositionalValues(self):
        return [widget.getValue() for widget in self.reifiedWidgets
                if widget.info['cli_type'] == 'positional']


    def getOptionalValues(self):
        return [widget.getValue() for widget in self.reifiedWidgets
                if widget.info['cli_type'] != 'positional']


    def getFormState(self) -> List[FormField]:
        return [widget.getUiState()
                for widget in self.reifiedWidgets]


    def syncFormState(self, formState: List[FormField]):
        for item in formState:
            self.widgetsMap[item['id']].syncUiState(item)

    def isValid(self):
        return not any(self.getErrors())

    def getErrors(self):
        states = [widget.getValue() for widget in self.reifiedWidgets]
        return {state['meta']['dest']: state['error'] for state in states
                if state['error']}

    def seedUI(self, seeds):
        radioWidgets = self.indexInternalRadioGroupWidgets()
        for id, values in seeds.items():
            if id in self.widgetsMap:
                self.widgetsMap[id].setOptions(values)
            if id in radioWidgets:
                radioWidgets[id].setOptions(values)


    def setErrors(self, errorMap: Mapping[str, str]):
        self.resetErrors()
        radioWidgets = self.indexInternalRadioGroupWidgets()
        widgetsByDest = {v._meta['dest']: v for k,v in self.widgetsMap.items()
                         if v.info['type'] != 'RadioGroup'}

        # if there are any errors, then all error blocks should
        # be displayed so that the UI elements remain inline with
        # each other.
        if errorMap:
            for widget in self.widgetsMap.values():
                widget.showErrorString(True)

        for id, message in errorMap.items():
            if id in widgetsByDest:
                widgetsByDest[id].setErrorString(message)
                widgetsByDest[id].showErrorString(True)
            if id in radioWidgets:
                radioWidgets[id].setErrorString(message)
                radioWidgets[id].showErrorString(True)


    def indexInternalRadioGroupWidgets(self):
        groups = filter(lambda x: x.info['type'] == 'RadioGroup', self.reifiedWidgets)
        widgets = flatmap(lambda group: group.widgets, groups)
        return indexunique(lambda x: x._meta['dest'], widgets)


    def displayErrors(self):
        states = [widget.getValue() for widget in self.reifiedWidgets]
        errors = [state for state in states if state['error']]
        for error in errors:
            widget = self.widgetsMap[error['id']]
            widget.setErrorString(error['error'])
            widget.showErrorString(True)
            while widget.GetParent():
                widget.Layout()
                widget = widget.GetParent()

    def resetErrors(self):
        for widget in self.reifiedWidgets:
            widget.setErrorString('')
            widget.showErrorString(False)

    def hideErrors(self):
        for widget in self.reifiedWidgets:
            widget.hideErrorString()


    def layoutComponent(self):
        sizer = wx.BoxSizer(wx.VERTICAL)
        for item in self.rawWidgets['contents']:
            self.makeGroup(self, sizer, item, 0, wx.EXPAND | wx.ALL, 10)
        self.SetSizer(sizer)

    def makeGroup(self, parent, thissizer, group, *args):
        '''
        Messily builds the (potentially) nested and grouped layout

        Note! Mutates `self.reifiedWidgets` in place with the widgets as they're
        instantiated! I cannot figure out how to split out the creation of the
        widgets from their styling without WxPython violently exploding

        TODO: sort out the WX quirks and clean this up.
        '''

        # determine the type of border , if any, the main sizer will use
        if getin(group, ['options', 'show_border'], False):
            boxDetails = wx.StaticBox(parent, -1, self.getName(group) or '')
            boxSizer = wx.StaticBoxSizer(boxDetails, wx.VERTICAL)
        else:
            boxSizer = wx.BoxSizer(wx.VERTICAL)
            boxSizer.AddSpacer(10)
            if group['name']:
                groupName = wx_util.h1(parent, self.getName(group) or '')
                groupName.SetForegroundColour(getin(group, ['options', 'label_color']))
                groupName.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
                boxSizer.Add(groupName, 0, wx.TOP | wx.BOTTOM | wx.LEFT, 8)

        group_description = getin(group, ['description'])
        if group_description:
            description = AutoWrappedStaticText(parent, label=group_description, target=boxSizer)
            description.SetForegroundColour(getin(group, ['options', 'description_color']))
            description.SetMinSize((0, -1))
            description.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
            boxSizer.Add(description, 1,  wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)

        # apply an underline when a grouping border is not specified
        # unless the user specifically requests not to show it
        if not getin(group, ['options', 'show_border'], False) and group['name'] \
                and getin(group, ['options', 'show_underline'], True):
            boxSizer.Add(wx_util.horizontal_rule(parent), 0, wx.EXPAND | wx.LEFT, 10)

        ui_groups = self.chunkWidgets(group)

        for uigroup in ui_groups:
            sizer = wx.BoxSizer(wx.HORIZONTAL)
            for item in uigroup:
                widget = self.reifyWidget(parent, item)
                if not getin(item, ['options', 'visible'], True):
                    widget.Hide()
                # !Mutate the reifiedWidgets instance variable in place
                self.reifiedWidgets.append(widget)
                sizer.Add(widget, 1, wx.ALL | wx.EXPAND, 5)
            boxSizer.Add(sizer, 0, wx.ALL | wx.EXPAND, 5)

        # apply the same layout rules recursively for subgroups
        hs = wx.BoxSizer(wx.HORIZONTAL)
        for e, subgroup in enumerate(group['groups']):
            self.makeGroup(parent, hs, subgroup, 1, wx.EXPAND)
            if len(group['groups']) != e:
                hs.AddSpacer(5)

            # self.makeGroup(parent, hs, subgroup, 1, wx.ALL | wx.EXPAND, 5)
            itemsPerColumn = getin(group, ['options', 'columns'], 2)
            if e % itemsPerColumn or (e + 1) == len(group['groups']):
                boxSizer.Add(hs, *args)
                hs = wx.BoxSizer(wx.HORIZONTAL)


        group_top_margin = getin(group, ['options', 'margin_top'], 1)

        marginSizer = wx.BoxSizer(wx.VERTICAL)
        marginSizer.Add(boxSizer, 1, wx.EXPAND | wx.TOP, group_top_margin)

        thissizer.Add(marginSizer, *args)


    def chunkWidgets(self, group):
        ''' chunk the widgets up into groups based on their sizing hints '''
        ui_groups = []
        subgroup = []
        for index, item in enumerate(group['items']):
            if getin(item, ['options', 'full_width'], False):
                ui_groups.append(subgroup)
                ui_groups.append([item])
                subgroup = []
            else:
                subgroup.append(item)
            if len(subgroup) == getin(group, ['options', 'columns'], 2) \
                    or item == group['items'][-1]:
                ui_groups.append(subgroup)
                subgroup = []
        return ui_groups


    def reifyWidget(self, parent, item):
        ''' Convert a JSON description of a widget into a WxObject '''
        from gooey.gui.components import widgets
        widgetClass = getattr(widgets, item['type'])
        return widgetClass(parent, item)



class TabbedConfigPage(ConfigPage):
    """
    Splits top-level groups across tabs
    """

    def layoutComponent(self):
        # self.rawWidgets['contents'] = self.rawWidgets['contents'][1:2]
        self.notebook = wx.Notebook(self, style=wx.BK_DEFAULT)

        panels = [wx.Panel(self.notebook) for _ in self.rawWidgets['contents']]
        sizers = [wx.BoxSizer(wx.VERTICAL) for _ in panels]

        for group, panel, sizer in zip(self.rawWidgets['contents'], panels, sizers):
            self.makeGroup(panel, sizer, group, 0, wx.EXPAND)
            panel.SetSizer(sizer)
            panel.Layout()
            self.notebook.AddPage(panel, self.getName(group))
            self.notebook.Layout()


        _sizer = wx.BoxSizer(wx.VERTICAL)
        _sizer.Add(self.notebook, 1, wx.EXPAND)
        self.SetSizer(_sizer)
        self.Layout()


    def snapToErrorTab(self):
        pass




File: gooey/gui/components/header.py

'''
Created on Dec 23, 2013

@author: Chris
'''

import wx  # type: ignore
from rewx import wsx
import rewx.components as c

from gooey.gui import imageutil, image_repository
from gooey.gui.util import wx_util
from gooey.gui.three_to_four import bitmapFromImage
from gooey.util.functional import getin
from gooey.gui.components.mouse import notifyMouseEvent

PAD_SIZE = 10





class FrameHeader(wx.Panel):
    def __init__(self, parent, buildSpec, **kwargs):
        wx.Panel.__init__(self, parent, **kwargs)
        self.SetDoubleBuffered(True)

        self.buildSpec = buildSpec

        self._header = None
        self._subheader = None
        self.settings_img = None
        self.running_img = None
        self.check_mark = None
        self.error_symbol = None

        self.images = []

        self.layoutComponent()
        self.bindMouseEvents()



    def setTitle(self, title):
        self._header.SetLabel(title)

    def setSubtitle(self, subtitle):
        self._subheader.SetLabel(subtitle)

    def setImage(self, image):
        for img in self.images:
            img.Show(False)
        getattr(self, image).Show(True)
        self.Layout()


    def layoutComponent(self):
        self.SetBackgroundColour(self.buildSpec['header_bg_color'])
        self.SetSize((30, self.buildSpec['header_height']))
        self.SetMinSize((120, self.buildSpec['header_height']))

        self._header = wx_util.h1(self, label=self.buildSpec['program_name'])
        self._subheader = wx.StaticText(self, label=self.buildSpec['program_description'])

        images = self.buildSpec['images']
        targetHeight = self.buildSpec['header_height'] - 10
        self.settings_img = self._load_image(images['configIcon'], targetHeight)
        self.running_img = self._load_image(images['runningIcon'], targetHeight)
        self.check_mark = self._load_image(images['successIcon'], targetHeight)
        self.error_symbol = self._load_image(images['errorIcon'], targetHeight)

        self.images = [
            self.settings_img,
            self.running_img,
            self.check_mark,
            self.error_symbol
        ]

        vsizer = wx.BoxSizer(wx.VERTICAL)
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        headings_sizer = self.build_heading_sizer()
        sizer.Add(headings_sizer, 1,
                  wx.ALIGN_LEFT | wx.EXPAND | wx.LEFT,
                  PAD_SIZE)
        sizer.Add(self.settings_img, 0, wx.EXPAND | wx.RIGHT, PAD_SIZE)
        sizer.Add(self.running_img, 0, wx.EXPAND | wx.RIGHT, PAD_SIZE)
        sizer.Add(self.check_mark, 0, wx.EXPAND | wx.RIGHT, PAD_SIZE)
        sizer.Add(self.error_symbol, 0, wx.EXPAND | wx.RIGHT, PAD_SIZE)
        self.running_img.Hide()
        self.check_mark.Hide()
        self.error_symbol.Hide()
        vsizer.Add(sizer, 1, wx.EXPAND)
        self.SetSizer(vsizer)


    def _load_image(self, imgPath, targetHeight):
        rawImage = imageutil.loadImage(imgPath)
        sizedImage = imageutil.resizeImage(rawImage, targetHeight)
        return imageutil.wrapBitmap(sizedImage, self)


    def build_heading_sizer(self):
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.AddStretchSpacer(1)
        if self.buildSpec['header_show_title']:
            sizer.Add(self._header, 0)
        else:
            self._header.Hide()

        if self.buildSpec['header_show_subtitle']:
            sizer.Add(self._subheader, 0)
        else:
            self._subheader.Hide()
        sizer.AddStretchSpacer(1)
        return sizer

    def bindMouseEvents(self):
        """
        Manually binding all LEFT_DOWN events.
        See: gooey.gui.mouse for background.
        """
        self.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
        self._header.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
        self._subheader.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
        for image in self.images:
            image.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)


File: gooey/gui/components/__init__.py




File: gooey/gui/components/tabbar.py

import wx  # type: ignore

from gooey.gui import events
from gooey.gui.pubsub import pub
from gooey.gui.util import wx_util


class Tabbar(wx.Panel):
    def __init__(self, parent, buildSpec, configPanels, *args, **kwargs):
        super(Tabbar, self).__init__(parent, *args, **kwargs)
        self._parent = parent
        self.notebook = wx.Notebook(self, style=wx.BK_DEFAULT)
        self.buildSpec = buildSpec
        self.configPanels = configPanels
        self.options = list(self.buildSpec['widgets'].keys())
        self.layoutComponent()


    def layoutComponent(self):
        for group, panel in zip(self.options, self.configPanels):
            panel.Reparent( self.notebook)
            self.notebook.AddPage(panel, group)
            self.notebook.Layout()

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.notebook, 1, wx.EXPAND)
        self.SetSizer(sizer)
        self.Layout()

    def getSelectedGroup(self):
        return self.options[self.notebook.Selection]

    def getActiveConfig(self):
        return self.configPanels[self.notebook.Selection]

    def show(self, b):
        self.Show(b)



File: gooey/gui/components/footer.py

import sys
import wx  # type: ignore

from gooey.gui import events
from gooey.gui.lang.i18n import _
from gooey.gui.pubsub import pub
from gooey.gui.components.mouse import notifyMouseEvent


class Footer(wx.Panel):
    '''
    Footer section used on the configuration
    screen of the application
    '''

    def __init__(self, parent, buildSpec, **kwargs):
        wx.Panel.__init__(self, parent, **kwargs)
        self.buildSpec = buildSpec

        self.SetMinSize((30, 53))
        # TODO: The was set to True for the timer addition
        #       however, it leads to 'tearing' issues when resizing
        #       the GUI in windows. Disabling until I can dig into it.
        self.SetDoubleBuffered(False)
        # components
        self.cancel_button = None
        self.start_button = None
        self.progress_bar = None
        self.close_button = None
        self.stop_button = None
        self.restart_button = None
        self.edit_button = None
        self.buttons = []

        self.layouts = {}

        self._init_components()
        self._do_layout()

        for button in self.buttons:
            self.Bind(wx.EVT_BUTTON, self.dispatch_click, button)
            self.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent, button)
        self.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)


    def updateTimeRemaining(self,*args,**kwargs):
        estimate_time_remaining = kwargs.get('estimatedRemaining')
        elapsed_time_value = kwargs.get('elapsed_time')
        if elapsed_time_value is None:
            return
        elif estimate_time_remaining is not None:
            self.time_remaining_text.SetLabel(f"{elapsed_time_value}<{estimate_time_remaining}")
            return
        else:
            self.time_remaining_text.SetLabel(f"{elapsed_time_value}")


    def updateProgressBar(self, *args, **kwargs):
        '''
         value, disable_animation=False
        :param args:
        :param kwargs:
        :return:
        '''
        value = kwargs.get('progress')
        pb = self.progress_bar
        if value is None:
            return
        if value < 0:
            pb.Pulse()
        else:
            value = min(int(value), pb.GetRange())
            if pb.GetValue() != value:
                # Windows 7 progress bar animation hack
                # http://stackoverflow.com/questions/5332616/disabling-net-progressbar-animation-when-changing-value
                if self.buildSpec['disable_progress_bar_animation'] \
                        and sys.platform.startswith("win"):
                    if pb.GetRange() == value:
                        pb.SetValue(value)
                        pb.SetValue(value - 1)
                    else:
                        pb.SetValue(value + 1)
                pb.SetValue(value)


    def showButtons(self, *buttonsToShow):
        for button in self.buttons:
            button.Show(False)
        for button in buttonsToShow:
            getattr(self, button).Show(True)
        self.Layout()


    def _init_components(self):
        self.cancel_button = self.button(_('cancel'), wx.ID_CANCEL, event_id=events.WINDOW_CANCEL)
        self.stop_button = self.button(_('stop'), wx.ID_OK, event_id=events.WINDOW_STOP)
        self.start_button = self.button(_('start'), wx.ID_OK, event_id=int(events.WINDOW_START))
        self.close_button = self.button(_("close"), wx.ID_OK, event_id=int(events.WINDOW_CLOSE))
        self.restart_button = self.button(_('restart'), wx.ID_OK, event_id=int(events.WINDOW_RESTART))
        self.edit_button = self.button(_('edit'), wx.ID_OK, event_id=int(events.WINDOW_EDIT))

        self.progress_bar = wx.Gauge(self, range=100)

        self.time_remaining_text = wx.StaticText(self)

        self.buttons = [self.cancel_button, self.start_button,
                        self.stop_button, self.close_button,
                        self.restart_button, self.edit_button]

        if self.buildSpec['disable_stop_button']:
            self.stop_button.Enable(False)


    def _do_layout(self):
        self.SetBackgroundColour(self.buildSpec['footer_bg_color'])
        self.stop_button.Hide()
        self.restart_button.Hide()

        v_sizer = wx.BoxSizer(wx.VERTICAL)
        h_sizer = wx.BoxSizer(wx.HORIZONTAL)

        h_sizer.Add(self.progress_bar, 1,
                    wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 20)
        
        h_sizer.Add(self.time_remaining_text,0,wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 20)

        h_sizer.AddStretchSpacer(1)
        h_sizer.Add(self.cancel_button, 0,wx.RIGHT, 20)
        h_sizer.Add(self.start_button, 0, wx.RIGHT, 20)
        h_sizer.Add(self.stop_button, 0, wx.RIGHT, 20)

        v_sizer.AddStretchSpacer(1)
        v_sizer.Add(h_sizer, 0, wx.EXPAND)

        h_sizer.Add(self.edit_button, 0, wx.RIGHT, 10)
        h_sizer.Add(self.restart_button, 0, wx.RIGHT, 10)
        h_sizer.Add(self.close_button, 0, wx.RIGHT, 20)
        self.edit_button.Hide()
        self.restart_button.Hide()
        self.close_button.Hide()
        # self.progress_bar.Hide()

        v_sizer.AddStretchSpacer(1)
        self.SetSizer(v_sizer)

    def button(self, label=None, style=None, event_id=-1):
        return wx.Button(
            parent=self,
            id=event_id,
            size=(90, -1),
            label=label,
            style=style)

    def dispatch_click(self, event):
        if event.EventObject.Enabled:
            pub.send_message(event.GetId())

    def hide_all_buttons(self):
        for button in self.buttons:
            button.Hide()



File: gooey/gui/components/modals.py

"""
All of the dialogs used throughout Gooey
"""
from collections import namedtuple

import wx  # type: ignore

from gooey.gui.lang.i18n import _


# These don't seem to be specified anywhere in WX for some reason
DialogConstants = namedtuple('DialogConstants', 'YES NO')(5103, 5104)  # type: ignore


def showDialog(title, content, style):
    dlg = wx.MessageDialog(None, content, title, style)
    dlg.SetYesNoLabels(_('dialog_button_yes'), _('dialog_button_no'))
    dlg.SetOKLabel(_('dialog_button_ok'))
    result = dlg.ShowModal()
    dlg.Destroy()
    return result


def missingArgsDialog():
    showDialog(_('error_title'), _('error_required_fields'), wx.ICON_ERROR)


def validationFailure():
    showDialog(_('error_title'), _('validation_failed'), wx.ICON_WARNING)


def showSuccess():
    showDialog(_('execution_finished'), _('success_message'), wx.ICON_INFORMATION)


def showFailure():
    showDialog(_('execution_finished'), _('uh_oh'), wx.ICON_ERROR)


def confirmExit():
    result = showDialog(_('sure_you_want_to_exit'), _('close_program'), wx.YES_NO | wx.ICON_INFORMATION)
    return result == DialogConstants.YES


def confirmForceStop():
    result = showDialog(_('stop_task'), _('sure_you_want_to_stop'), wx.YES_NO | wx.ICON_WARNING)
    return result == DialogConstants.YES




File: gooey/gui/components/mouse.py

"""
WxPython lacks window level event hooks. Meaning, there's no
general way to subscribe to every mouse event that goes on within
the application.

To implement features which respond to clicks outside of their
immediate scope, for instance, dropdowns, a workaround in the form
of manually binding all mouse events, for every component, to a single
top level handler needs to be done.

Normally, this type of functionality would be handled by wx.PopupTransientWindow.
However, there's a long standing bug with it and the ListBox/Ctrl
classes which prevents its usage and thus forcing this garbage.

See: https://github.com/wxWidgets/Phoenix/blob/705aa63d75715f8abe484f4559a37cb6b09decb3/demo/PopupWindow.py
"""


from gooey.gui.pubsub import pub
import gooey.gui.events as events

def notifyMouseEvent(event):
    """
    Notify interested listeners of the LEFT_DOWN mouse event
    """
    # TODO: is there ever a situation where this wouldn't be skipped..?
    event.Skip()
    pub.send_message_sync(events.LEFT_DOWN, wxEvent=event)


File: gooey/gui/components/dialogs.py

import rewx.components as c  # type: ignore
import wx  # type: ignore
import wx.html2  # type: ignore
from rewx import wsx, render  # type: ignore


def _html_window(html):
    return wsx(
        [c.Block, {'orient': wx.VERTICAL, 'flag': wx.EXPAND},
         [c.HtmlWindow, {'style': wx.TE_READONLY, 'flag': wx.EXPAND | wx.ALL,
                         'proportion': 1, 'value': html}]]
    )


class HtmlDialog(wx.Dialog):
    """
    A MessageDialog where the central contents are an HTML window
    customizable by the user.
    """
    def __init__(self, *args, **kwargs):
        caption = kwargs.pop('caption', '')
        html = kwargs.pop('html', '')
        super(HtmlDialog, self).__init__(None, *args, **kwargs)

        wx.InitAllImageHandlers()

        self.SetTitle(caption)
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(render(_html_window(html), self), 1, wx.EXPAND)

        # in addition to creating the sizer, this actually attached
        # a few common handlers which makes it feel more dialog-y. Thus
        # it being done here rather than in rewx
        btnSizer = self.CreateStdDialogButtonSizer(wx.OK)
        sizer.Add(btnSizer, 0, wx.ALL | wx.EXPAND, 9)
        self.SetSizer(sizer)
        self.Layout()






File: gooey/gui/components/options/options.py

from gooey.gui.components.filtering.prefix_filter import PrefixTokenizers



def _include_layout_docs(f):
    """
    Combines the layout_options docsstring with the
    wrapped function's doc string.
    """
    f.__doc__ = (f.__doc__ or '') + (LayoutOptions.__doc__ or '')
    return f


def _include_global_option_docs(f):
    """
    Combines docstrings for options available to
    all widget types.
    """
    _doc = """:param initial_value:  Sets the initial value in the UI. 
    """
    f.__doc__ = (f.__doc__ or '') + _doc
    return f

def _include_chooser_msg_wildcard_docs(f):
    """
    Combines the basic Chooser options (wildard, message) docsstring
    with the wrapped function's doc string.
    """
    _doc = """:param wildcard: Sets the wildcard, which can contain multiple file types, for 
                     example: "BMP files (.bmp)|.bmp|GIF files (.gif)|.gif"
    :param message:  Sets the message that will be displayed on the dialog.
    """
    f.__doc__ = (f.__doc__ or '') + _doc
    return f

def _include_choose_dir_file_docs(f):
    """
        Combines the basic Chooser options (wildard, message) docsstring
        with the wrapped function's doc string.
        """
    _doc = """:param default_dir: The default directory selected when the dialog spawns 
    :param default_file: The default filename used in the dialog
    """
    f.__doc__ = (f.__doc__ or '') + _doc
    return f



def LayoutOptions(label_color=None,
                  label_bg_color=None,
                  help_color=None,
                  help_bg_color=None,
                  error_color=None,
                  error_bg_color=None,
                  show_label=True,
                  show_help=True,
                  visible=True,
                  full_width=False):
    """
    Layout Options:
    ---------------

    Color options can be passed either as a hex string ('#ff0000') or as
    a collection of RGB values (e.g. `[255, 0, 0]` or `(255, 0, 0)`)

    :param label_color:    The foreground color of the label text
    :param label_bg_color: The background color of the label text.
    :param help_color:     The foreground color of the help text.
    :param help_bg_color:  The background color of the help text.
    :param error_color:    The foreground color of the error text (when visible).
    :param error_bg_color: The background color of the error text (when visible).
    :param show_label:     Toggles whether or not to display the label text
    :param show_help:      Toggles whether or not to display the help text
    :param visible:        Hides the entire widget when False. Note: the widget
                           is still present in the UI and will still send along any
                           default values that have been provided in code. This option
                           is here for when you want to hide certain advanced / dangerous
                           inputs from your GUI users.
    :param full_width:     This is a layout hint for this widget. When True the widget
                           will fill the entire available space within a given row.
                           Otherwise, it will be sized based on the column rules
                           provided elsewhere.
    """
    return _clean(locals())



@_include_layout_docs
@_include_global_option_docs
def TextField(initial_value=None, validator=None, **layout_options):
    return _clean(locals())


@_include_layout_docs
@_include_global_option_docs
def PasswordField(initial_value=None, validator=None, **layout_options):
    return _clean(locals())


@_include_layout_docs
@_include_global_option_docs
def IntegerField(initial_value=None, validator=None, min=0, max=100, increment=1, **layout_options):
    """
    :param min: The minimum value allowed
    :param max: The maximum value allowed
    :param increment: The step size of the spinner
    """
    return _clean(locals())

@_include_layout_docs
@_include_global_option_docs
def Slider(initial_value=None, validator=None, min=0, max=100, increment=1, **layout_options):
    """
    :param min: The minimum value allowed
    :param max: The maximum value allowed
    :param increment: The step size of the slider
    """
    return _clean(locals())


@_include_layout_docs
@_include_global_option_docs
def DecimalField(validator=None,
                 initial_value=None,
                 min=0.0,
                 max=1.0,
                 increment=0.01,
                 precision=2,
                 **layout_options):
    """
    :param min: The minimum value allowed
    :param max: The maximum value allowed
    :param increment: The step size of the spinner
    :param precision: The precision of the decimal (0-20)
    """
    return _clean(locals())


@_include_layout_docs
@_include_global_option_docs
def TextArea(initial_value=None, height=None, readonly=False, validator=None, **layout_options):
    """
    :param height:   The height of the TextArea.
    :param readonly: Controls whether or not user's may modify the contents
    """
    return _clean(locals())


@_include_layout_docs
@_include_global_option_docs
def RichTextConsole(**layout_options):
    return _clean(locals())


@_include_layout_docs
@_include_global_option_docs
def ListBox(initial_value=None, height=None, **layout_options):
    """
    :param height: The height of the Listbox
    """
    return _clean(locals())

# TODO: what are this guy's layout options..?
def MutexGroup(initial_selection=None, title=None, **layout_options):
    """
    :param initial_selection: The index of the option which should be initially selected.
    :param title:             Adds the supplied title above the RadioGroup options (when present)
    """
    return _clean(locals())


@_include_layout_docs
@_include_global_option_docs
def Dropdown(initial_value=None, **layout_options):
    return _clean(locals())


@_include_layout_docs
@_include_global_option_docs
def Counter(initial_value=None, **layout_options):
    return _clean(locals())


@_include_layout_docs
@_include_global_option_docs
def CheckBox(initial_value=None, **layout_options):
    return _clean(locals())


@_include_layout_docs
@_include_global_option_docs
def BlockCheckBox(initial_value=None, checkbox_label=None, **layout_options):
    return _clean(locals())


@_include_layout_docs
@_include_global_option_docs
def FilterableDropdown(placeholder=None,
                       empty_message=None,
                       max_size=80,
                       search_strategy=None,
                       initial_value=None,
                       **layout_options):
    """
    :param placeholder:     Text to display when the user has provided no input
    :param empty_message:   Text to display if the user's query doesn't match anything
    :param max_size:        maximum height of the dropdown
    :param search_strategy: see: PrefixSearchStrategy
    """
    return _clean(locals())


def PrefixSearchStrategy(
                   choice_tokenizer=PrefixTokenizers.WORDS,
                   input_tokenizer=PrefixTokenizers.REGEX('\s'),
                   ignore_case=True,
                   operator='AND',
                   index_suffix=False):
    """
    :param choice_tokenizer: See: PrefixTokenizers - sets the tokenization strategy
                             for the `choices`
    :param input_tokenizer:  See: PrefixTokenizers sets how the users's `input` get tokenized.
    :param ignore_case:      Controls whether or not to honor case while searching
    :param operator:         see: `OperatorType` - controls whether or not individual
                             search tokens
                             get `AND`ed or `OR`d together when evaluating a match.
    :param index_suffix:     When enabled, generates a suffix-tree to enable efficient
                             partial-matching against any of the tokens.
    """
    return {**_clean(locals()), 'type': 'PrefixFilter'}


@_include_layout_docs
@_include_global_option_docs
@_include_choose_dir_file_docs
@_include_chooser_msg_wildcard_docs
def FileChooser(wildcard=None,
                default_dir=None,
                default_file=None,
                message=None,
                initial_value=None,
                **layout_options):
    return _clean(locals())


@_include_layout_docs
@_include_global_option_docs
@_include_chooser_msg_wildcard_docs
def DirectoryChooser(wildcard=None,
                    default_path=None,
                    message=None,
                    initial_value=None,
                    **layout_options):
    """
    :param default_path: The default path selected when the dialog spawns
    """
    return _clean(locals())


@_include_layout_docs
@_include_global_option_docs
@_include_choose_dir_file_docs
@_include_chooser_msg_wildcard_docs
def FileSaver(wildcard=None,
              default_dir=None,
              default_file=None,
              message=None,
              initial_value=None,
              **layout_options):
    return _clean(locals())


@_include_layout_docs
@_include_global_option_docs
@_include_choose_dir_file_docs
@_include_chooser_msg_wildcard_docs
def MultiFileSaver(wildcard=None,
              default_dir=None,
              default_file=None,
              message=None,
              initial_value=None,
              **layout_options):
    return _clean(locals())


def ExpressionValidator(test=None, message=None):
    """
    Creates the data for a basic expression validator.

    Your test function can be made up of any valid Python expression.
    It receives the variable user_input as an argument against which to
    perform its validation. Note that all values coming from Gooey
    are in the form of a string, so you'll have to cast as needed
    in order to perform your validation.
    """
    return {**_clean(locals()), 'type': 'ExpressionValidator'}


def RegexValidator(test=None, message=None):
    """
    Creates the data for a basic RegexValidator.

    :param test:    the regex expression. This should be the expression
                    directly (i.e. `test='\d+'`). Gooey will test
                    that the user's input satisfies this expression.
    :param message: The message to display if the input doesn't match
                    the regex
    """
    return {**_clean(locals()), 'type': 'RegexValidator'}


def ArgumentGroup(show_border=False,
                  show_underline=True,
                  label_color=None,
                  columns=None,
                  margin_top=None):
    """
    :param show_border:    When True a labeled border will surround all widgets added to this group.
    :param show_underline: Controls whether or not to display the underline when using the default border style
    :param label_color:    The foreground color for the group name
    :param columns:        Controls the number of widgets on each row
    :param margin_top:     specifies the top margin in pixels for this group
    """
    return _clean(locals())





def _clean(options):
    cleaned = {k: v for k, v in options.items()
               if v is not None and k != "layout_options"}
    return {**options.get('layout_options', {}), **cleaned}




File: gooey/gui/components/options/validators.py

import re
from functools import wraps

from gooey.gui.components.filtering.prefix_filter import OperatorType


class SuperBool(object):
    """
    A boolean which keeps with it the rationale
    for when it is false.
    """
    def __init__(self, value, rationale):
        self.value = value
        self.rationale = rationale

    def __bool__(self):
        return self.value

    __nonzero__ = __bool__

    def __str__(self):
        return str(self.value)


def lift(f):
    """
    Lifts a basic predicate to the SuperBool type
    stealing the docstring as the rationale message.

    This is largely just goofing around and experimenting
    since it's a private internal API.
    """
    @wraps(f)
    def inner(value):
        result = f(value)
        return SuperBool(result, f.__doc__) if not isinstance(result, SuperBool) else result
    return inner


@lift
def is_tuple_or_list(value):
    """Must be either a list or tuple"""
    return isinstance(value, list) or isinstance(value, tuple)


@lift
def is_str(value):
    """Must be of type `str`"""
    return isinstance(value, str)

@lift
def is_str_or_coll(value):
    """
    Colors must be either a hex string or collection of RGB values.
    e.g.
        Hex string: #fff0ce
        RGB Collection: [0, 255, 128] or (0, 255, 128)
    """
    return bool(is_str(value)) or bool(is_tuple_or_list(value))


@lift
def has_valid_channel_values(rgb_coll):
    """Colors in an RGB collection must all be in the range 0-255"""
    return all([is_0to255(c) and is_int(c) for c in rgb_coll])


@lift
def is_three_channeled(value):
    """Missing channels! Colors in an RGB collection should be of the form [R,G,B] or (R,G,B)"""
    return len(value) == 3

@lift
def is_hex_string(value: str):
    """Invalid hexadecimal format. Expected: "#FFFFFF" """
    return isinstance(value, str) and bool(re.match('^#[\dABCDEF]{6}$', value, flags=2))


@lift
def is_bool(value):
    """Must be of type Boolean"""
    return isinstance(value, bool)

@lift
def non_empty_string(value):
    """Must be a non-empty non-blank string"""
    return bool(value) and bool(value.strip())

@lift
def is_tokenization_operator(value):
    """Operator must be a valid OperatorType i.e. one of: (AND, OR)"""
    return bool(value) in (OperatorType.AND, OperatorType.OR)

@lift
def is_tokenizer(value):
    """Tokenizers must be valid Regular expressions. see: options.PrefixTokenizers"""
    return bool(non_empty_string(value))


@lift
def is_int(value):
    """Invalid type. Expected `int`"""
    return isinstance(value, int)

@lift
def is_0to255(value):
    """RGB values must be in the range 0 - 255 (inclusive)"""
    return 0 <= value <= 255


def is_0to20(value):
    """Precision values must be in the range 0 - 20 (inclusive)"""
    return 0 <= value <= 20

@lift
def is_valid_color(value):
    """Must be either a valid hex string or RGB list"""
    if is_str(value):
        return is_hex_string(value)
    elif is_tuple_or_list(value):
        return (is_tuple_or_list(value)
                and is_three_channeled(value)
                and has_valid_channel_values(value))
    else:
        return is_str_or_coll(value)


validators = {
    'label_color': is_valid_color,
    'label_bg_color': is_valid_color,
    'help_color': is_valid_color,
    'help_bg_color': is_valid_color,
    'error_color': is_valid_color,
    'error_bg_color': is_valid_color,
    'show_label': is_bool,
    'show_help': is_bool,
    'visible': is_bool,
    'full_width': is_bool,
    'height': is_int,
    'readonly': is_bool,
    'initial_selection': is_int,
    'title': non_empty_string,
    'checkbox_label': non_empty_string,
    'placeholder': non_empty_string,
    'empty_message': non_empty_string,
    'max_size': is_int,
    'choice_tokenizer': is_tokenizer,
    'input_tokenizer': is_tokenizer,
    'ignore_case': is_bool,
    'operator': is_tokenization_operator,
    'index_suffix': is_bool,
    'wildcard': non_empty_string,
    'default_dir': non_empty_string,
    'default_file': non_empty_string,
    'default_path': non_empty_string,
    'message': non_empty_string,
    'precision': is_0to20
}



def collect_errors(predicates, m):
    return {
        k:predicates[k](v).rationale
        for k,v in m.items()
        if k in predicates and not predicates[k](v)}


def validate(pred, value):
    result = pred(value)
    if not result:
        raise ValueError(result.rationale)



if __name__ == '__main__':
    # TODO: there should be tests
    pass
    # print(validateColor((1, 'ergerg', 1234)))
    # print(validateColor(1234))
    # print(validateColor(123.234))
    # print(validateColor('123.234'))
    # print(validateColor('FFFAAA'))
    # print(validateColor('#FFFAAA'))
    # print(validateColor([]))
    # print(validateColor(()))
    # print(validateColor((1, 2)))
    # print(validateColor((1, 2, 1234)))
    # print(is_lifted(lift(is_int)))
    # print(is_lifted(is_int))
    # print(OR(is_poop, is_int)('poop'))
    # print(AND(is_poop, is_lower, is_lower)('pooP'))
    # print(OR(is_poop, is_int))
    # print(is_lifted(OR(is_poop, is_int)))
    # print(validate(is_valid_color, [255, 255, 256]))
    # print(is_valid_color('#fff000'))
    # print(is_valid_color([255, 244, 256]))
    # print(non_empty_string('asdf') and non_empty_string('asdf'))
    # validate(is_valid_color, 1234)





File: gooey/gui/components/options/__init__.py




File: gooey/gui/components/util/__init__.py




File: gooey/gui/components/util/wrapped_static_text.py

import wx  # type: ignore
from wx.lib.wordwrap import wordwrap  # type: ignore



class AutoWrappedStaticText(wx.StaticText):
    """
    Copy/pasta of wx.lib.agw.infobar.AutoWrapStaticText with 3 modifications:

        1. Extends wx.StaticText rather than GenStaticText
        2. Does not set the fore/background colors to sys defaults
        3. takes an optional `target` parameter for sizing info

    The behavior of GenStaticText's background color is pretty buggy cross-
    platform. It doesn't reliably match its parent components background
    colors[0] (for instance when rendered inside of a Notebook) which leads to
    ugly 'boxing' around the text components.

    There is either a bug in WX, or or human error on my end, which causes
    EVT_SIZE events to continuously spawn from this (and AutoWrapStaticText) but
    with ever decreasing widths (in response to the SetLabel action in the
    wrap handler). The end result is a single skinny column of letters.

    The work around is to respond the EVT_SIZE event, but follow the size of the
    `target` component rather than relying on the size of the event.

    [0] more specifically, they'll match 1:1 on paper, but still ultimately
    render differently.
    """

    def __init__(self, parent, *args, **kwargs):
        self.target = kwargs.pop('target', None)
        super(AutoWrappedStaticText, self).__init__(parent, *args, **kwargs)
        self.label = kwargs.get('label')
        self.Bind(wx.EVT_SIZE, self.OnSize)
        self.parent = parent


    def OnSize(self, event):
        """
        Handles the ``wx.EVT_SIZE`` event for :class:`AutoWrapStaticText`.

        :param `event`: a :class:`wx.SizeEvent` event to be processed.
        """

        event.Skip()
        if self.target:
            self.Wrap(self.target.GetSize().width)
        else:
            self.Wrap(self.parent.GetSize()[0])

    def Wrap(self, width):
        """
        This functions wraps the controls label so that each of its lines becomes at
        most `width` pixels wide if possible (the lines are broken at words boundaries
        so it might not be the case if words are too long).

        If `width` is negative, no wrapping is done.

        :param integer `width`: the maximum available width for the text, in pixels.

        :note: Note that this `width` is not necessarily the total width of the control,
         since a few pixels for the border (depending on the controls border style) may be added.
        """

        if width < 0:
            return

        self.Freeze()

        dc = wx.ClientDC(self)
        dc.SetFont(self.GetFont())
        text = wordwrap(self.label, width, dc)
        self.SetLabel(text, wrapped=True)

        self.Thaw()

    def SetLabel(self, label, wrapped=False):
        """
        Sets the :class:`AutoWrapStaticText` label.

        All "&" characters in the label are special and indicate that the following character is
        a mnemonic for this control and can be used to activate it from the keyboard (typically
        by using ``Alt`` key in combination with it). To insert a literal ampersand character, you
        need to double it, i.e. use "&&". If this behaviour is undesirable, use :meth:`~Control.SetLabelText` instead.

        :param string `label`: the new :class:`AutoWrapStaticText` text label;
        :param bool `wrapped`: ``True`` if this method was called by the developer using :meth:`~AutoWrapStaticText.SetLabel`,
         ``False`` if it comes from the :meth:`~AutoWrapStaticText.OnSize` event handler.

        :note: Reimplemented from :class:`wx.Control`.
        """

        if not wrapped:
            self.label = label

        wx.StaticText.SetLabel(self, label)



File: gooey/gui/components/filtering/__init__.py




File: gooey/gui/components/filtering/prefix_filter.py

import re

import pygtrie as trie  # type: ignore
from functools import reduce

__ALL__ = ('PrefixTokenizers', 'PrefixSearch')



class PrefixTokenizers:
    # This string here is just an arbitrary long string so that
    # re.split finds no matches and returns the entire phrase
    ENTIRE_PHRASE = '::gooey/tokenization/entire-phrase'
    # \s == any whitespace character
    WORDS = r'\s'

    @classmethod
    def REGEX(cls, expression):
        return expression

class OperatorType:
    AND = 'AND'
    OR = 'OR'

class SearchOptions:
    def __init__(self,
                 choice_tokenizer=PrefixTokenizers.ENTIRE_PHRASE,
                 input_tokenizer=PrefixTokenizers.ENTIRE_PHRASE,
                 ignore_case=True,
                 operator='AND',
                 index_suffix= False,
                 **kwargs):
        self.choice_tokenizer = choice_tokenizer
        self.input_tokenizer = input_tokenizer
        self.ignore_case = ignore_case
        self.operator = operator
        self.index_suffix = index_suffix



class PrefixSearch(object):
    """
    A trie backed index for quickly finding substrings
    in a list of options.
    """

    def __init__(self, choices, options={}, *args, **kwargs):
        self.choices = sorted(filter(None, choices))
        self.options: SearchOptions = SearchOptions(**options)
        self.searchtree = self.buildSearchTrie(choices)

    def updateChoices(self, choices):
        self.choices = sorted(filter(None, choices))
        self.searchtree = self.buildSearchTrie(choices)

    def findMatches(self, token):
        if not token:
            return sorted(self.choices)
        tokens = self.tokenizeInput(token)
        matches = [set(flatten(self._vals(self.searchtree, prefix=t))) for t in tokens]
        op = intersection if self.options.operator == 'AND' else union
        return sorted(reduce(op, matches))

    def tokenizeInput(self, token):
        """
        Cleans and tokenizes the user's input.

        empty characters and spaces are trimmed to prevent
        matching all paths in the index.
        """
        return list(filter(None, re.split(self.options.input_tokenizer, self.clean(token))))

    def tokenizeChoice(self, choice):
        """
        Splits the `choice` into a series of tokens based on
        the user's criteria.

        If suffix indexing is enabled, the individual tokens
        are further broken down and indexed by their suffix offsets. e.g.

            'Banana', 'anana', 'nana', 'ana'
        """
        choice_ = self.clean(choice)
        tokens = re.split(self.options.choice_tokenizer, choice_)
        if self.options.index_suffix:
            return [token[i:]
                    for token in tokens
                    for i in range(len(token) - 2)]
        else:
            return tokens

    def clean(self, text):
        return text.lower() if self.options.ignore_case else text

    def buildSearchTrie(self, choices):
        searchtrie = trie.Trie()
        for choice in choices:
            for token in self.tokenizeChoice(choice):
                if not searchtrie.has_key(token):
                    searchtrie[token] = []
                searchtrie[token].append(choice)
        return searchtrie

    def _vals(self, searchtrie, **kwargs):
        try:
            return searchtrie.values(**kwargs)
        except KeyError:
            return []


def intersection(a, b):
    return a.intersection(b)


def union(a, b):
    return a.union(b)


def flatten(xs):
    return [item for x in xs for item in x]



File: gooey/gui/components/layouts/__init__.py




File: gooey/gui/components/layouts/layouts.py

import wx  # type: ignore


def standard_layout(title, subtitle, widget):
    container = wx.BoxSizer(wx.VERTICAL)

    container.Add(title)
    container.AddSpacer(2)

    if subtitle:
        container.Add(subtitle, 1, wx.EXPAND)
        container.AddSpacer(2)
    else:
        container.AddStretchSpacer(1)

    container.Add(widget, 0, wx.EXPAND)
    return container



File: gooey/gui/components/widgets/textarea.py

import os
import wx  # type: ignore
from functools import reduce

from gooey.gui.components.widgets.core.text_input import MultilineTextInput
from gooey.gui.components.widgets.textfield import TextField
from gooey.gui.components.widgets.bases import TextContainer
from gooey.gui import formatters
from gooey.python_bindings import types as t
from gooey.python_bindings.types import FormField


class Textarea(TextContainer):

    def getWidget(self, parent, *args, **options):
        widgetHeight = self._options.get('height', -1)
        return wx.TextCtrl(
            parent=parent,
            size=(-1, widgetHeight),
            style=self.getModifiers()
        )

    def getModifiers(self):
        readonly = (wx.TE_READONLY
                    if self._options.get('readonly', False)
                    # using TE_MUTLI as a safe OR-able no-op value
                    else wx.TE_MULTILINE)
        return reduce(lambda acc, val: acc | val, [wx.TE_MULTILINE, readonly])

    def getWidgetValue(self):
        return self.widget.GetValue()

    def setValue(self, value):
        self.widget.Clear()
        self.widget.AppendText(str(value))
        self.widget.SetInsertionPoint(0)

    def formatOutput(self, metatdata, value: str):
        return formatters.general(metatdata, value.replace('\n', os.linesep))

    def syncUiState(self, state: FormField):
        self.setValue(state['value'])  # type: ignore
        self.error.SetLabel(state['error'] or '')
        self.error.Show(state['error'] is not None and state['error'] is not '')

    def getUiState(self) -> t.FormField:
        return t.TextField(
            id=self._id,
            type=self.widgetInfo['type'],
            value=self.getWidgetValue(),
            placeholder=self.widget.GetHint(),
            error=self.error.GetLabel().replace('\n', ' '),
            enabled=self.IsEnabled(),
            visible=self.IsShown()
        )



File: gooey/gui/components/widgets/command.py

from gooey.gui.components.widgets.textfield import TextField
from gooey.python_bindings import types as t



__ALL__ = ('CommandField',)

class CommandField(TextField):

    def getUiState(self) -> t.FormField:
        return t.Command(**super().getUiState())  # type: ignore



File: gooey/gui/components/widgets/choosers.py

from gooey.gui import formatters
from gooey.gui.components.widgets import core
from gooey.gui.components.widgets.bases import TextContainer, BaseChooser


__ALL__ = [
    'FileChooser',
    'MultiFileChooser',
    'FileSaver',
    'DirChooser',
    'MultiDirChooser',
    'DateChooser',
    'ColourChooser',
    'TimeChooser'
]


class FileChooser(BaseChooser):
    widget_class = core.FileChooser


class MultiFileChooser(BaseChooser):
    widget_class = core.MultiFileChooser

    def formatOutput(self, metatdata, value):
        return formatters.multiFileChooser(metatdata, value)


class FileSaver(BaseChooser):
    widget_class = core.FileSaver


class DirChooser(BaseChooser):
    widget_class = core.DirChooser


class MultiDirChooser(BaseChooser):
    widget_class = core.MultiDirChooser

    def formatOutput(self, metadata, value):
        return formatters.multiFileChooser(metadata, value)


class DateChooser(BaseChooser):
    widget_class = core.DateChooser


class ColourChooser(BaseChooser):
    widget_class = core.ColourChooser


class TimeChooser(BaseChooser):
    widget_class = core.TimeChooser



File: gooey/gui/components/widgets/dropdown_filterable.py

from contextlib import contextmanager

import wx  # type: ignore
import wx.html  # type: ignore

import gooey.gui.events as events
from gooey.gui.components.filtering.prefix_filter import PrefixSearch
from gooey.gui.components.mouse import notifyMouseEvent
from gooey.gui.components.widgets.dropdown import Dropdown
from gooey.gui.lang.i18n import _
from gooey.gui.pubsub import pub
from gooey.python_bindings import types as t

__ALL__ = ('FilterableDropdown',)


class FilterableDropdown(Dropdown):
    """
    TODO: tests for gooey_options
    TODO: documentation
    A dropdown with auto-complete / filtering behaviors.

    This is largely a recreation of the `AutoComplete` functionality baked
    into WX itself.

    Background info:
    The Dropdown's listbox and its Autocomplete dialog are different components.
    This means that if the former is open, the latter cannot be used. Additionally,
    this leads to annoying UX quirks like the boxes having different styles and sizes.
    If we ignore the UX issues, a possible solution for still leveraging the current built-in
    AutoComplete functionality would have been to capture EVT_TEXT and conditionally
    close the dropdown while spawning the AutoComplete dialog, but due to
    (a) non-overridable behaviors and (b) lack a fine grained events, this cannot be
    done in a seamless manner.

    FAQ:
    Q: Why does it slide down rather than hover over elements like the native ComboBox?
    A: The only mechanism for layering in WX is the wx.PopupTransientWindow. There's a long
       standing issue in wxPython which prevents Listbox/Ctrl from capturing events when
       inside of a PopupTransientWindow (see: https://tinyurl.com/y28ngh7v)

    Q: Why is visibility handled by changing its size rather than using Show/Hide?
    A: WX's Layout engine is strangely quirky when it comes to toggling visibility.
       Repeated calls to Layout() after the first show/hide cycle no longer produce
       the same results. I have no idea why. I keep checking it thinking I'm crazy, but
       alas... seems to be the case.
    """
    gooey_options = {
        'placeholder': str,
        'empty_message': str,
        'max_size': str
    }
    def __init__(self, *args, **kwargs):
        # these are declared here and created inside
        # of getWidget() because the structure of all
        # this inheritance garbage is broken.
        self.listbox = None
        self.model = None
        super(FilterableDropdown, self).__init__(*args, **kwargs)
        self.SetDoubleBuffered(True)

    def interpretState(self, model):
        """
        Updates the UI to reflect the current state of the model.
        """
        if self.widget.GetValue() != self.model.displayValue:
            self.widget.ChangeValue(model.displayValue)

        self.listbox.Clear()
        self.listbox.SetItemCount(len(self.model.suggestions))
        if len(self.model.suggestions) == 1:
            # I have no clue why this is required, but without
            # manually flicking the virtualized listbox off/on
            # it won't paint the update when there's only a single
            # item being displayed
            self.listbox.Show(False)
            self.listbox.Show(self.model.suggestionsVisible)
        if model.selectedSuggestion > -1:
            self.listbox.SetSelection(model.selectedSuggestion)
            self.widget.SetInsertionPoint(-1)
            self.widget.SetSelection(999, -1)
        else:
            self.listbox.SetSelection(-1)
        self.estimateBestSize()
        self.listbox.Show(self.model.suggestionsVisible)
        self.Layout()
        self.GetParent().Layout()

    def onComponentInitialized(self):
        self.widget.GetTextCtrl().Bind(wx.EVT_TEXT, self.onTextInput)
        self.widget.GetTextCtrl().Bind(wx.EVT_CHAR_HOOK, self.onKeyboardControls)
        self.widget.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
        self.widget.GetTextCtrl().Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
        self.listbox.Bind(wx.EVT_LISTBOX, self.onClickSuggestion)
        pub.subscribe(events.LEFT_DOWN, self.onMouseClick)
        self.widget.SetHint(self._options.get('placeholder', ''))

    def getWidget(self, parent, *args, **options):
        # self.widget = wx.ComboCtrl(parent)
        self.comboCtrl = wx.ComboCtrl(parent)
        self.comboCtrl.OnButtonClick = self.onButton
        self.foo = ListCtrlComboPopup()
        self.comboCtrl.SetPopupControl(self.foo)
        self.listbox = VirtualizedListBox(self)
        self.listbox.OnGetItem = self.OnGetItem
        # model is created here because the design of these widget
        # classes is broken.
        self.model = FilterableDropdownModel(self._meta['choices'], self._options, listeners=[self.interpretState])
        # overriding this to false removes it from tab behavior.
        # and keeps the tabbing at the top-level widget level
        self.listbox.AcceptsFocusFromKeyboard = lambda *args, **kwargs: False
        return self.comboCtrl

    def getUiState(self) -> t.FormField:
        widget: wx.ComboBox = self.widget
        return t.DropdownFilterable(
            id=self._id,
            type=self.widgetInfo['type'],
            value=self.model.actualValue,
            choices=self.model.choices,
            error=self.error.GetLabel() or None,
            enabled=self.IsEnabled(),
            visible=self.IsShown()
        )

    def syncUiState(self, state: t.DropdownFilterable):  # type: ignore
        self.setOptions(state['choices'])
        if state['value'] is not None:
            self.setValue(state['value'])
        self.error.SetLabel(state['error'] or '')
        self.error.Show(state['error'] is not None and state['error'] is not '')

    def OnGetItem(self, n):
        return self.model.suggestions[n]

    def getSublayout(self, *args, **kwargs):
        verticalSizer = wx.BoxSizer(wx.VERTICAL)
        layout = wx.BoxSizer(wx.HORIZONTAL)
        layout.Add(self.widget, 1, wx.EXPAND)
        verticalSizer.Add(layout, 0, wx.EXPAND)
        verticalSizer.Add(self.listbox, 0, wx.EXPAND)
        self.listbox.SetMaxSize(self.model.maxSize)
        self.listbox.Hide()
        self.Layout()
        return verticalSizer

    def setOptions(self, options):
        self.model.updateChoices(options)
        if not self.model.actualValue in options:
            self.model.updateActualValue('')

    def setValue(self, value):
        self.model.updateActualValue(value)

    def onButton(self):
        if self.model.suggestionsVisible:
            self.model.hideSuggestions()
        else:
            self.model.showSuggestions()

    def onClickSuggestion(self, event):
        self.model.acceptSuggestion(self.model.suggestions[event.Selection])
        event.Skip()

    def onMouseClick(self, wxEvent):
        """
        Closes the suggestions when the user clicks anywhere
        outside of the current widget.
        """
        if wxEvent.EventObject not in (self.widget, self.widget.GetTextCtrl()):
            self.model.hideSuggestions()
            wxEvent.Skip()
        else:
            wxEvent.Skip()

    def onTextInput(self, event):
        """Processes the user's input and show relevant suggestions"""
        self.model.handleTextInput(event.GetString())

    def onKeyboardControls(self, event):
        """
        Handles any keyboard events relevant to the
        control/navigation of the suggestion box.
        All other events are passed through via `Skip()`
        and bubble up to `onTextInput` to be handled.
        """
        if event.GetKeyCode() == wx.WXK_ESCAPE:
            self.model.ignoreSuggestions()
        elif event.GetKeyCode() in (wx.WXK_TAB, wx.WXK_RETURN):
            self.model.acceptSuggestion(self.model.displayValue)
            event.Skip()
        elif event.GetKeyCode() in (wx.WXK_DOWN, wx.WXK_UP):
            if not self.model.suggestionsVisible:
                self.model.generateSuggestions(self.model.displayValue)
                self.model.showSuggestions()
            else:
                if self.listbox.OnGetItem(0) != self.model.noMatch:
                    self.ignore = True
                    if event.GetKeyCode() == wx.WXK_DOWN:
                        self.model.incSelectedSuggestion()
                    else:
                        self.model.decSelectedSuggestion()
        else:
            # for some reason deleting text doesn't
            # trigger the usual evt_text event, even though
            # it IS a modification of the text... so handled here.
            if event.GetKeyCode() == wx.WXK_DELETE:
                self.model.handleTextInput('')
            event.Skip()

    def estimateBestSize(self):
        """
        Restricts the size of the dropdown based on the number
        of items within it. This is a rough estimate based on the
        current font size.
        """
        padding = 11
        rowHeight = self.listbox.GetFont().GetPixelSize()[1] + padding
        maxHeight = self.model.maxSize[1]
        self.listbox.SetMaxSize((-1, min(maxHeight, len(self.model.suggestions) * rowHeight)))
        self.listbox.SetMinSize((-1, min(maxHeight, len(self.model.suggestions) * rowHeight)))
        self.listbox.SetSize((-1, -1))



class VirtualizedListBox(wx.html.HtmlListBox):
    def __init__(self, *args, **kwargs):
        super(VirtualizedListBox, self).__init__(*args, **kwargs)
        self.SetItemCount(1)

    def OnGetItem(self, n):
        return ''




class FilterableDropdownModel(object):
    """
    The model/state for the FilterableDropdown. While this is still one
    big ball of mutation (hard to get away from in WX), it serves the purpose
    of keeping data transforms independent of presentation concerns.
    """
    gooey_options = {
        'placeholder': str,
        'empty_message': str,
        'max_size': str
    }
    def __init__(self, choices, options, listeners=[], *args, **kwargs):
        self.listeners = listeners
        self.actualValue = ''
        self.displayValue = ''
        self.dropEvent = False
        self.suggestionsVisible = False
        self.noMatch = options.get('no_matches', _('dropdown.no_matches'))
        self.choices = choices
        self.suggestions = choices
        self.selectedSuggestion = -1
        self.suggestionsVisible = False
        self.maxSize = (-1, options.get('max_size', 80))
        self.strat = PrefixSearch(choices, options.get('search_strategy', {}))

    def __str__(self):
        return str(vars(self))

    @contextmanager
    def notify(self):
        try:
            yield
        finally:
            for listener in self.listeners:
                listener(self)

    def updateChoices(self, choices):
        """Update the available choices in response
        to a dynamic update"""
        self.choices = choices
        self.strat.updateChoices(choices)

    def handleTextInput(self, value):
        if self.dropEvent:
            self.dropEvent = False
        else:
            with self.notify():
                self.actualValue = value
                self.displayValue = value
                self.selectedSuggestion = -1
                self.generateSuggestions(value)
                self.suggestionsVisible = True

    def updateActualValue(self, value):
        with self.notify():
            self.actualValue = value
            self.displayValue = value

    def acceptSuggestion(self, suggestion):
        """Accept the currently selected option as the user's input"""
        with self.notify():
            self.actualValue = suggestion
            self.displayValue = suggestion
            self.suggestionsVisible = False
            self.selectedSuggestion = -1

    def ignoreSuggestions(self):
        """
        Ignore the suggested values and replace the
        user's original input.
        """
        with self.notify():
            self.displayValue = self.actualValue
            self.suggestionsVisible = False
            self.selectedSuggestion = -1

    def generateSuggestions(self, prompt):
        suggestions = self.strat.findMatches(prompt)
        final_suggestions = suggestions if suggestions else [self.noMatch]
        self.suggestions = final_suggestions

    def incSelectedSuggestion(self):
        with self.notify():
            nextIndex = (self.selectedSuggestion + 1) % len(self.suggestions)
            suggestion = self.suggestions[nextIndex]
            self.selectedSuggestion = nextIndex
            self.displayValue = suggestion
            self.dropEvent = True

    def decSelectedSuggestion(self):
        with self.notify():
            currentIndex = max(-1, self.selectedSuggestion - 1)
            nextIndex = currentIndex % len(self.suggestions)
            nextDisplay = self.suggestions[nextIndex]
            self.displayValue = nextDisplay
            self.selectedSuggestion = nextIndex
            self.dropEvent = True

    def hideSuggestions(self):
        with self.notify():
            self.suggestionsVisible = False

    def showSuggestions(self):
        with self.notify():
            self.generateSuggestions(self.displayValue)
            self.suggestionsVisible = True

    def isShowingSuggestions(self):
        """
        Check if we're currently showing the suggestion dropdown
        by checking if we've made it's height non-zero.
        """
        return self.suggestionsVisible


class ListCtrlComboPopup(wx.ComboPopup):
    """
    This is an empty placeholder to satisfy the interface of
    the ComboCtrl which uses it. All Popup behavior is handled
    inside of `FilterableDropdown`. See its docs for additional
    details.
    """
    def __init__(self):
        wx.ComboPopup.__init__(self)
        self.lc = None

    def Create(self, parent):
        # this ComboCtrl requires a real Wx widget be created
        # thus creating a blank static text object
        self.lc = wx.StaticText(parent)
        return True

    def GetControl(self):
        return self.lc




File: gooey/gui/components/widgets/radio_group.py

from typing import Optional

import wx  # type: ignore
from gooey.gui.components.widgets.bases import BaseWidget
from gooey.gui.lang.i18n import _
from gooey.gui.util import wx_util
from gooey.gui.components.widgets import CheckBox
from gooey.util.functional import getin, merge
from gooey.python_bindings import types as t


class RadioGroup(BaseWidget):
    """
    """

    def __init__(self, parent, widgetInfo, *args, **kwargs):
        super(RadioGroup, self).__init__(parent, *args, **kwargs)
        self._parent = parent
        self.info = widgetInfo
        self._id = widgetInfo['id']
        self._options = widgetInfo['options']
        self.widgetInfo = widgetInfo
        self.error = wx.StaticText(self, label='')
        self.radioButtons = self.createRadioButtons()
        self.selected = None
        self.widgets = self.createWidgets()
        self.arrange()


        for button in self.radioButtons:
            button.Bind(wx.EVT_LEFT_DOWN, self.handleButtonClick)

        initialSelection = getin(self.info, ['options', 'initial_selection'], None)
        if initialSelection is not None:
            self.selected = self.radioButtons[initialSelection]
            self.selected.SetValue(True)
        self.handleImplicitCheck()

        self.applyStyleRules()


    def getValue(self):
        for button, widget in zip(self.radioButtons, self.widgets):
            if button.GetValue():  # is Checked
                return merge(widget.getValue(), {'id': self._id})
        else:
            # just return the first widget's value even though it's
            # not active so that the expected interface is satisfied
            return self.widgets[0].getValue()


    def syncUiState(self, state: t.RadioGroup):
        if state['selected'] is not None:
            self.radioButtons[state['selected']].SetValue(True)
        for option, widget in zip(state['options'], self.widgets):
            widget.syncUiState(option)
        # Fit required here to force WX to actually
        # show newly Enabled/Shown things for some reason.
        self.Fit()

    def getUiState(self):
        return t.RadioGroup(
            id=self._id,
            type=self.widgetInfo['type'],
            error=self.error.GetLabel(),
            enabled=self.Enabled,
            visible=self.Shown,
            selected=self.getSelectedIndex(),
            options=[x.getUiState() for x in self.widgets]
        )

    def getSelectedIndex(self) -> Optional[int]:
        for index, btn in enumerate(self.radioButtons):
            if btn.GetValue():
                return index
        return None

    def setErrorString(self, message):
        for button, widget in zip(self.radioButtons, self.widgets):
            if button.GetValue():  # is Checked
                widget.setErrorString(message)
        self.Layout()

    def showErrorString(self, b):
        for button, widget in zip(self.radioButtons, self.widgets):
            if button.GetValue():  # is Checked
                widget.showErrorString(b)


    def arrange(self, *args, **kwargs):
        title = getin(self.widgetInfo, ['options', 'title'], _('choose_one'))
        if getin(self.widgetInfo, ['options', 'show_border'], False):
            boxDetails = wx.StaticBox(self, -1, title)
            boxSizer = wx.StaticBoxSizer(boxDetails, wx.VERTICAL)
        else:
            title = wx_util.h1(self, title)
            title.SetForegroundColour(self._options['label_color'])
            boxSizer = wx.BoxSizer(wx.VERTICAL)
            boxSizer.AddSpacer(10)
            boxSizer.Add(title, 0)

        for btn, widget in zip(self.radioButtons, self.widgets):
            sizer = wx.BoxSizer(wx.HORIZONTAL)
            sizer.Add(btn,0, wx.RIGHT, 4)
            sizer.Add(widget, 1, wx.EXPAND)
            boxSizer.Add(sizer, 0, wx.ALL | wx.EXPAND, 5)
        self.SetSizer(boxSizer)


    def handleButtonClick(self, event):
        currentSelection = self.selected
        nextSelection = event.EventObject

        if not self.isSameRadioButton(currentSelection, nextSelection):
            self.selected = nextSelection
            self.selected.SetValue(True)
        else:
            # user clicked on an already enabled radio button.
            # if it is not in the required section, allow it to be deselected
            if not self.widgetInfo['required']:
                self.selected.SetValue(False)
                self.selected = None

        self.applyStyleRules()
        self.handleImplicitCheck()


    def isSameRadioButton(self, radioButton1, radioButton2):
        return (getattr(radioButton1, 'Id', 'r1-not-found') ==
                getattr(radioButton2, 'Id', 'r2-not-found'))


    def applyStyleRules(self):
        """
        Conditionally disabled/enables form fields based on the current
        section in the radio group
        """
        # for reasons I have been completely unable to figure out
        # or understand, IFF you've interacted with one of the radio Buttons's
        # child components, then the act of disabling that component will
        # reset the state of the radioButtons thus causing it to forget
        # what should be selected. So, that is why we're collected the initial
        # state of all the buttons and resetting each button's state as we go.
        # it's wonky as hell
        states = [x.GetValue() for x in self.radioButtons]
        for widget in self.widgets:
            widget.Enable()
        for button, selected, widget in zip(self.radioButtons, states, self.widgets):
            if isinstance(widget, CheckBox):
                widget.hideInput()
            if not selected: # not checked
                widget.Disable()
            else:
                # More "I don't understand" style code
                # Under some conditions, Enable() doesn't cascade
                # as listed in the docs. We have to manually drill
                # into the children to enable everything.
                widget = widget
                while widget:
                    widget.Enable()
                    widget = getattr(widget, 'widget', None)
            button.SetValue(selected)

    def handleImplicitCheck(self):
        """
        Checkboxes are hidden when inside of a RadioGroup as a selection of
        the Radio button is an implicit selection of the Checkbox. As such, we have
        to manually "check" any checkbox as needed.
        """
        for button, widget in zip(self.radioButtons, self.widgets):
            if isinstance(widget, CheckBox):
                if button.GetValue(): # checked
                    widget.setValue(True)
                else:
                    widget.setValue(False)


    def createRadioButtons(self):
        # button groups in wx are statefully determined via a style flag
        # on the first button (what???). All button instances are part of the
        # same group until a new button is created with the style flag RG_GROUP
        # https://wxpython.org/Phoenix/docs/html/wx.RadioButton.html
        # (What???)
        firstButton = wx.RadioButton(self, style=wx.RB_GROUP)
        firstButton.SetValue(False)
        buttons = [firstButton]

        for _ in getin(self.widgetInfo, ['data','widgets'], [])[1:]:
            buttons.append(wx.RadioButton(self))
        return buttons

    def createWidgets(self):
        """
        Instantiate the Gooey Widgets that are used within the RadioGroup
        """
        from gooey.gui.components import widgets
        widgets = [getattr(widgets, item['type'])(self, item)
                   for item in getin(self.widgetInfo, ['data', 'widgets'], [])]
        # widgets should be disabled unless
        # explicitly selected
        for widget in widgets:
            widget.Disable()
        return widgets



File: gooey/gui/components/widgets/__init__.py

from __future__ import absolute_import

from .textfield import TextField
from .textarea import Textarea
from .password import PasswordField
from .command import CommandField
from .dropdown import Dropdown
from .listbox import Listbox
from .checkbox import CheckBox
from .checkbox import BlockCheckbox
from .counter import Counter
from .radio_group import RadioGroup
from .choosers import *
from .dropdown_filterable import FilterableDropdown
from .numeric_fields import IntegerField, DecimalField
from .slider import Slider



File: gooey/gui/components/widgets/basictextconsole.py

import wx  # type: ignore

class BasicTextConsole(wx.TextCtrl):
    def __init__(self, parent):
        super(BasicTextConsole, self).__init__(parent, -1, "", style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH | wx.TE_AUTO_URL )



File: gooey/gui/components/widgets/slider.py

import wx  # type: ignore

from gooey.gui import formatters
from gooey.gui.components.widgets.bases import TextContainer
from gooey.python_bindings import types as t


class Slider(TextContainer):
    """
    An integer input field
    """
    widget_class = wx.Slider
    def getWidget(self, *args, **options):
        widget = self.widget_class(self,
                                   minValue=self._options.get('min', 0),
                                   maxValue=self._options.get('max', 100),
                                   style=wx.SL_MIN_MAX_LABELS | wx.SL_VALUE_LABEL)
        return widget

    def getWidgetValue(self):
        return self.widget.GetValue()

    def setValue(self, value):
        self.widget.SetValue(value)

    def formatOutput(self, metatdata, value):
        return formatters.general(metatdata, str(value))

    def getUiState(self) -> t.FormField:
        widget: wx.Slider = self.widget
        return t.Slider(
            id=self._id,
            type=self.widgetInfo['type'],
            value=self.getWidgetValue(),
            min=widget.GetMin(),
            max=widget.GetMax(),
            error=self.error.GetLabel() or None,
            enabled=self.IsEnabled(),
            visible=self.IsShown()
        )



File: gooey/gui/components/widgets/textfield.py

import wx  # type: ignore

from gooey.gui import formatters
from gooey.gui.components.widgets.bases import TextContainer
from gooey.gui.components.widgets.core.text_input import TextInput
from gooey.python_bindings import types as t

class TextField(TextContainer):
    widget_class = TextInput

    def getWidgetValue(self):
        return self.widget.getValue()

    def setValue(self, value):
        self.widget.setValue(str(value))

    def setPlaceholder(self, value):
        self.widget.SetHint(value)

    def formatOutput(self, metatdata, value):
        return formatters.general(metatdata, value)

    def syncUiState(self, state: t.TextField):  # type: ignore
        textctr: wx.TextCtrl = self.widget.widget
        textctr.SetValue(state['value'])
        textctr.SetHint(state['placeholder'])
        textctr.Enable(state['enabled'])
        self.Show(state['visible'])
        self.error.SetLabel(state['error'] or '')
        self.error.Show(state['error'] is not None and state['error'] is not '')
        self.Layout()



File: gooey/gui/components/widgets/password.py

from gooey.gui.components.widgets.core.text_input import PasswordInput
from gooey.gui.components.widgets.textfield import TextField
from gooey.python_bindings import types as t

__ALL__ = ('PasswordField',)

class PasswordField(TextField):
    widget_class = PasswordInput  # type: ignore

    def __init__(self, *args, **kwargs):
        super(PasswordField, self).__init__(*args, **kwargs)

    def getUiState(self) -> t.FormField:  # type: ignore
        return t.PasswordField(**super().getUiState())  # type: ignore




File: gooey/gui/components/widgets/dropdown.py

from contextlib import contextmanager

from gooey.gui.components.widgets.bases import TextContainer
import wx  # type: ignore

from gooey.gui import formatters
from gooey.gui.lang.i18n import _
from gooey.python_bindings import types as t
from gooey.python_bindings.types import FormField


class Dropdown(TextContainer):
    _gooey_options = {
        'placeholder': str,
        'readonly': bool,
        'enable_autocomplete': bool
    }
    def getWidget(self, parent, *args, **options):
        default = _('select_option')
        return wx.ComboBox(
            parent=parent,
            id=-1,
            # str conversion allows using stringyfiable values in addition to pure strings
            value=str(default),
            choices=[str(default)] + [str(choice) for choice in self._meta['choices']],
            style=wx.CB_DROPDOWN)

    def setOptions(self, options):
        with self.retainSelection():
            self.widget.Clear()
            self.widget.SetItems([_('select_option')] + options)

    def setValue(self, value):
        ## +1 to offset the default placeholder value
        index = self._meta['choices'].index(value) + 1
        self.widget.SetSelection(index)

    def getWidgetValue(self):
        value = self.widget.GetValue()
        # filter out the extra default option that's
        # appended during creation
        if value == _('select_option'):
            return None
        return value

    def formatOutput(self, metadata, value):
        return formatters.dropdown(metadata, value)


    def syncUiState(self, state: FormField):
        self.setOptions(state['choices'])  # type: ignore
        if state['selected'] is not None:  # type: ignore
            self.setValue(state['selected'])  # type: ignore
        self.error.SetLabel(state['error'] or '')
        self.error.Show(state['error'] is not None and state['error'] is not '')

    def getUiState(self) -> t.FormField:
        widget: wx.ComboBox = self.widget
        return t.Dropdown(
            id=self._id,
            type=self.widgetInfo['type'],
            selected=self.getWidgetValue(),
            choices=widget.GetStrings(),
            error=self.error.GetLabel() or None,
            enabled=self.IsEnabled(),
            visible=self.IsShown()
        )

    @contextmanager
    def retainSelection(self):
        """"
        Retains the selected dropdown option (when possible)
        across mutations due to dynamic updates.
        """
        prevSelection = self.widget.GetSelection()
        prevValue = self.widget.GetValue()
        try:
            yield
        finally:
            current_at_index = self.widget.GetString(prevSelection)
            if prevValue == current_at_index:
                self.widget.SetSelection(prevSelection)
            else:
                self.widget.SetSelection(0)



File: gooey/gui/components/widgets/listbox.py

import wx  # type: ignore

from gooey.gui import formatters
from gooey.gui.components.widgets.bases import TextContainer
from gooey.python_bindings import types as t


class Listbox(TextContainer):

    def getWidget(self, parent, *args, **options):
        height = self._options.get('height', 60)
        return wx.ListBox(
            parent=parent,
            choices=self._meta['choices'],
            size=(-1, height),
            style=wx.LB_MULTIPLE
        )

    def setOptions(self, options):
        self.widget.Clear()
        for option in options:
            self.widget.Append(option)

    def setValue(self, values):
        for string in values:
            self.widget.SetStringSelection(string)

    def getWidgetValue(self):
        return [self.widget.GetString(index)
                for index in self.widget.GetSelections()]

    def formatOutput(self, metadata, value):
        return formatters.listbox(metadata, value)

    def getUiState(self) -> t.FormField:
        widget: wx.ComboBox = self.widget
        return t.Listbox(
            id=self._id,
            type=self.widgetInfo['type'],
            selected=self.getWidgetValue(),
            choices=self._meta['choices'],
            error=self.error.GetLabel() or None,
            enabled=self.IsEnabled(),
            visible=self.IsShown()
        )

    def syncUiState(self, state: t.Listbox):  # type: ignore
        widget: wx.ComboBox = self.widget
        widget.Clear()
        widget.AppendItems(state.get('choices', []))
        for string in state['selected']:
            widget.SetStringSelection(string)
        self.error.SetLabel(state['error'] or '')
        self.error.Show(state['error'] is not None and state['error'] is not '')



File: gooey/gui/components/widgets/numeric_fields.py

import wx  # type: ignore

from gooey.gui import formatters
from gooey.gui.components.widgets.bases import TextContainer
from gooey.python_bindings import types as t

class IntegerField(TextContainer):
    """
    An integer input field
    """
    widget_class = wx.SpinCtrl
    def getWidget(self, *args, **options):
        widget = self.widget_class(self,
                             value='',
                             min=self._options.get('min', 0),
                             max=self._options.get('max', 100))
        return widget

    def getWidgetValue(self):
        return self.widget.GetValue()

    def setValue(self, value):
        self.widget.SetValue(int(value))

    def formatOutput(self, metatdata, value):
        # casting to string so that the generic formatter
        # doesn't treat 0 as false/None
        return formatters.general(metatdata, str(value))

    def getUiState(self) -> t.FormField:
        widget: wx.SpinCtrl = self.widget
        return t.IntegerField(
            id=self._id,
            type=self.widgetInfo['type'],
            value=self.getWidgetValue(),
            min=widget.GetMin(),
            max=widget.GetMax(),
            error=self.error.GetLabel() or None,
            enabled=self.IsEnabled(),
            visible=self.IsShown()
        )

class DecimalField(IntegerField):
    """
    A decimal input field
    """
    widget_class = wx.SpinCtrlDouble

    def getWidget(self, *args, **options):
        widget = self.widget_class(self,
                             value='',
                             min=self._options.get('min', 0),
                             max=self._options.get('max', 100),
                             inc=self._options.get('increment', 0.01))
        widget.SetDigits(self._options.get('precision', widget.GetDigits()))
        return widget


    def setValue(self, value):
        self.widget.SetValue(value)

    def getUiState(self) -> t.FormField:
        widget: wx.SpinCtrlDouble = self.widget
        return t.IntegerField(
            id=self._id,
            type=self.widgetInfo['type'],
            value=self.getWidgetValue(),
            min=widget.GetMin(),
            max=widget.GetMax(),
            error=self.error.GetLabel() or None,
            enabled=self.IsEnabled(),
            visible=self.IsShown()
        )





File: gooey/gui/components/widgets/counter.py

import wx  # type: ignore

from gooey.gui.components.widgets.dropdown import Dropdown
from gooey.python_bindings import types as t
from gooey.gui import formatters


class Counter(Dropdown):

    def setValue(self, value):
        index = self._meta['choices'].index(value) + 1
        self.widget.SetSelection(index)

    def getUiState(self) -> t.FormField:
        widget: wx.ComboBox = self.widget
        return t.Counter(
            id=self._id,
            type=self.widgetInfo['type'],
            selected=self.getWidgetValue(),
            choices=widget.GetStrings(),
            error=self.error.GetLabel() or None,
            enabled=self.IsEnabled(),
            visible=self.IsShown()
        )



    def formatOutput(self, metadata, value):
        return formatters.counter(metadata, value)



File: gooey/gui/components/widgets/checkbox.py

import wx  # type: ignore

from gooey.gui import formatters
from gooey.gui.components.widgets.bases import TextContainer
from gooey.gui.lang.i18n import _
from gooey.gui.util import wx_util
from gooey.python_bindings import types as t


class CheckBox(TextContainer):

    widget_class = wx.CheckBox

    def arrange(self, *args, **kwargs):
        wx_util.make_bold(self.label)
        wx_util.withColor(self.label, self._options['label_color'])
        wx_util.withColor(self.help_text, self._options['help_color'])
        wx_util.withColor(self.error, self._options['error_color'])
        self.error.Hide()

        self.help_text.SetMinSize((0,-1))

        layout = wx.BoxSizer(wx.VERTICAL)
        if self._options.get('show_label', True):
            layout.Add(self.label, 0, wx.EXPAND)
        else:
            self.label.Show(False)
            layout.AddStretchSpacer(1)

        layout.AddSpacer(2)
        if self.help_text:
            hsizer = wx.BoxSizer(wx.HORIZONTAL)
            hsizer.Add(self.widget, 0)
            hsizer.Add(self.help_text, 1)
            layout.Add(hsizer, 1, wx.EXPAND)
            layout.AddSpacer(2)
        else:
            layout.Add(self.widget, 0, wx.EXPAND)
            layout.AddStretchSpacer(1)
        return layout


    def setValue(self, value):
        self.widget.SetValue(value)

    def getWidgetValue(self):
        return self.widget.GetValue()


    def formatOutput(self, metatdata, value):
        return formatters.checkbox(metatdata, value)


    def hideInput(self):
        self.widget.Hide()


    def getUiState(self) -> t.FormField:
        return t.Checkbox(
            id=self._id,
            type='Checkbox',
            checked=self.widget.GetValue(),
            error=self.error.GetLabel() or None,  # type: ignore
            enabled=self.IsEnabled(),
            visible=self.IsShown()
        )

    def syncUiState(self, state: t.Checkbox):  # type: ignore
        checkbox: wx.CheckBox = self.widget
        checkbox.SetValue(state['checked'])
        checkbox.Enable(state['enabled'])
        self.Show(state['visible'])
        self.error.SetLabel(state['error'] or '')
        self.error.Show(state['error'] is not None and state['error'] is not '')





class BlockCheckbox(CheckBox):
    """
    A block style layout which places the help text in the normal
    location rather than inline next to the checkbox. A replacement label
    called `block_label` is shown next to the checkbox control.

         +-----------------+
         |label            |
         |help_text        |
         |[ ] block_label  |
         +-----------------+
    This option tends to look better when there is a large amount of
    help text.
    """


    def arrange(self, *args, **kwargs):
        wx_util.make_bold(self.label)
        wx_util.withColor(self.label, self._options['label_color'])
        wx_util.withColor(self.help_text, self._options['help_color'])
        wx_util.withColor(self.error, self._options['error_color'])
        self.error.Hide()

        self.help_text.SetMinSize((0,-1))

        layout = wx.BoxSizer(wx.VERTICAL)

        if self._options.get('show_label', True):
            layout.Add(self.label, 0, wx.EXPAND)
        else:
            layout.AddStretchSpacer(1)

        layout.AddSpacer(2)
        if self.help_text and self._options.get('show_help', True):
            layout.Add(self.help_text, 1, wx.EXPAND)
            layout.AddSpacer(2)
        else:
            layout.AddStretchSpacer(1)

        layout.AddSpacer(2)

        block_label = self._options.get('checkbox_label', _('checkbox_label'))
        hsizer = wx.BoxSizer(wx.HORIZONTAL)
        hsizer.Add(self.widget, 0)
        hsizer.Add(wx.StaticText(self, label=block_label), 1)
        layout.Add(hsizer, 1, wx.EXPAND)
        layout.AddSpacer(2)

        return layout


File: gooey/gui/components/widgets/bases.py

import re
from functools import reduce
from typing import Optional, Callable, Any, Type, Union

import wx  # type: ignore

from gooey.gui import formatters, events
from gooey.gui.util import wx_util
from gooey.python_bindings.types import FormField
from gooey.util.functional import getin, ifPresent
from gooey.gui.validators import runValidator
from gooey.gui.components.util.wrapped_static_text import AutoWrappedStaticText
from gooey.gui.components.mouse import notifyMouseEvent
from gooey.python_bindings import types as t


class BaseWidget(wx.Panel):
    widget_class: Any

    def arrange(self, label, text):
        raise NotImplementedError

    def getWidget(self, parent: wx.Window, **options):
        return self.widget_class(parent, **options)

    def connectSignal(self):
        raise NotImplementedError

    def getSublayout(self, *args, **kwargs):
        raise NotImplementedError

    def setValue(self, value):
        raise NotImplementedError

    def setPlaceholder(self, value):
        raise NotImplementedError

    def receiveChange(self, *args, **kwargs):
        raise NotImplementedError

    def dispatchChange(self, value, **kwargs):
        raise NotImplementedError

    def formatOutput(self, metatdata, value):
        raise NotImplementedError


class TextContainer(BaseWidget):
    # TODO: fix this busted-ass inheritance hierarchy.
    # Cracking at the seems for more advanced widgets
    # problems:
    #   - all the usual textbook problems of inheritance
    #   - assumes there will only ever be ONE widget created
    #   - assumes those widgets are all created in `getWidget`
    #   - all the above make for extremely awkward lifecycle management
    #      - no clear point at which binding is correct.
    #   - I think the core problem here is that I couple the interface
    #     for shared presentation layout with the specification of
    #     a behavioral interface
    #     - This should be broken apart.
    #     - presentation can be ad-hoc or composed
    #     - behavioral just needs a typeclass of get/set/format for Gooey's purposes
    widget_class = None  # type: ignore

    def __init__(self, parent, widgetInfo, *args, **kwargs):
        super(TextContainer, self).__init__(parent, *args, **kwargs)

        self.info = widgetInfo
        self._id = widgetInfo['id']
        self.widgetInfo = widgetInfo
        self._meta = widgetInfo['data']
        self._options = widgetInfo['options']
        self.label = wx.StaticText(self, label=widgetInfo['data']['display_name'])
        self.help_text = AutoWrappedStaticText(self, label=widgetInfo['data']['help'] or '')
        self.error = AutoWrappedStaticText(self, label='')
        self.error.Hide()
        self.widget = self.getWidget(self)
        self.layout = self.arrange(*args, **kwargs)
        self.setColors()
        self.SetSizer(self.layout)
        self.bindMouseEvents()
        self.Bind(wx.EVT_SIZE, self.onSize)

        # 1.0.7 initial_value should supersede default when both are present
        if self._options.get('initial_value') is not None:
            self.setValue(self._options['initial_value'])
        # Checking for None instead of truthiness means False-evaluaded defaults can be used.
        elif self._meta['default'] is not None:
            self.setValue(self._meta['default'])

        if self._options.get('placeholder'):
            self.setPlaceholder(self._options.get('placeholder'))

        self.onComponentInitialized()

    def onComponentInitialized(self):
        pass

    def bindMouseEvents(self):
        """
        Send any LEFT DOWN mouse events to interested
        listeners via pubsub. see: gooey.gui.mouse for background.
        """
        self.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
        self.label.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
        self.help_text.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
        self.error.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
        self.widget.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)

    def arrange(self, *args, **kwargs):
        wx_util.make_bold(self.label)
        wx_util.withColor(self.label, self._options['label_color'])
        wx_util.withColor(self.help_text, self._options['help_color'])
        wx_util.withColor(self.error, self._options['error_color'])

        self.help_text.SetMinSize((0,-1))

        layout = wx.BoxSizer(wx.VERTICAL)

        if self._options.get('show_label', True):
            layout.Add(self.label, 0, wx.EXPAND)
        else:
            self.label.Show(False)
            layout.AddStretchSpacer(1)

        layout.AddSpacer(2)
        if self.help_text and self._options.get('show_help', True):
            layout.Add(self.help_text, 1, wx.EXPAND)
            layout.AddSpacer(2)
        else:
            self.help_text.Show(False)
            layout.AddStretchSpacer(1)
        layout.Add(self.getSublayout(), 0, wx.EXPAND)
        layout.Add(self.error, 1, wx.EXPAND)

        # self.error.SetLabel("HELLOOOOO??")
        # self.error.Show()
        # print(self.error.Shown)
        return layout


    def setColors(self):
        wx_util.make_bold(self.label)
        wx_util.withColor(self.label, self._options['label_color'])
        wx_util.withColor(self.help_text, self._options['help_color'])
        wx_util.withColor(self.error, self._options['error_color'])
        if self._options.get('label_bg_color'):
            self.label.SetBackgroundColour(self._options.get('label_bg_color'))
        if self._options.get('help_bg_color'):
            self.help_text.SetBackgroundColour(self._options.get('help_bg_color'))
        if self._options.get('error_bg_color'):
            self.error.SetBackgroundColour(self._options.get('error_bg_color'))

    def getWidget(self, *args, **options):
        return self.widget_class(*args, **options)

    def getWidgetValue(self):
        raise NotImplementedError

    def getSublayout(self, *args, **kwargs):
        layout = wx.BoxSizer(wx.HORIZONTAL)
        layout.Add(self.widget, 1, wx.EXPAND)
        return layout

    def onSize(self, event):
        # print(self.GetSize())
        # self.error.Wrap(self.GetSize().width)
        # self.help_text.Wrap(500)
        # self.Layout()
        event.Skip()

    def getUiState(self) -> t.FormField:
        return t.TextField(
            id=self._id,
            type=self.widgetInfo['type'],
            value=self.getWidgetValue(),
            placeholder=self.widget.widget.GetHint(),
            error=self.error.GetLabel().replace('\n', ' '),
            enabled=self.IsEnabled(),
            visible=self.IsShown()
        )

    def syncUiState(self, state: FormField):  # type: ignore
        self.widget.setValue(state['value'])  # type: ignore
        self.error.SetLabel(state['error'] or '')
        self.error.Show(state['error'] is not None and state['error'] is not '')


    def getValue(self) -> t.FieldValue:
        regexFunc: Callable[[str], bool] = lambda x: bool(re.match(userValidator, x))

        userValidator = getin(self._options, ['validator', 'test'], 'True')
        message = getin(self._options, ['validator', 'message'], '')
        testFunc = regexFunc \
                   if getin(self._options, ['validator', 'type'], None) == 'RegexValidator'\
                   else eval('lambda user_input: bool(%s)' % userValidator)
        satisfies = testFunc if self._meta['required'] else ifPresent(testFunc)
        value = self.getWidgetValue()

        return t.FieldValue(  # type: ignore
            id=self._id,
            cmd=self.formatOutput(self._meta, value),
            meta=self._meta,
            rawValue= value,
            # type=self.info['type'],
            enabled=self.IsEnabled(),
            visible=self.IsShown(),
            test= runValidator(satisfies, value),
            error=None if runValidator(satisfies, value) else message,
            clitype=('positional'
                        if self._meta['required'] and not self._meta['commands']
                        else 'optional')
        )

    def setValue(self, value):
        self.widget.SetValue(value)

    def setPlaceholder(self, value):
        if getattr(self.widget, 'SetHint', None):
            self.widget.SetHint(value)

    def setErrorString(self, message):
        self.error.SetLabel(message)
        self.error.Wrap(self.Size.width)
        self.Layout()

    def showErrorString(self, b):
        self.error.Wrap(self.Size.width)
        self.error.Show(b)

    def setOptions(self, values):
        return None

    def receiveChange(self, metatdata, value):
        raise NotImplementedError

    def dispatchChange(self, value, **kwargs):
        raise NotImplementedError

    def formatOutput(self, metadata, value) -> str:
        raise NotImplementedError




class BaseChooser(TextContainer):
    """ Base Class for the Chooser widget types """

    def setValue(self, value):
        self.widget.setValue(value)

    def setPlaceholder(self, value):
        self.widget.SetHint(value)

    def getWidgetValue(self):
        return self.widget.getValue()

    def formatOutput(self, metatdata, value):
        return formatters.general(metatdata, value)

    def getUiState(self) -> t.FormField:
        btn: wx.Button = self.widget.button  # type: ignore
        return t.Chooser(
            id=self._id,
            type=self.widgetInfo['type'],
            value=self.widget.getValue(),
            btn_label=btn.GetLabel(),
            error=self.error.GetLabel() or None,
            enabled=self.IsEnabled(),
            visible=self.IsShown()
        )



File: gooey/gui/components/widgets/richtextconsole.py

import wx  # type: ignore
import wx.richtext  # type: ignore
import colored  # type: ignore
import re
from gooey.python_bindings import types as t


kColorList = ["#000000", "#800000", "#008000", "#808000", "#000080", "#800080", "#008080", "#c0c0c0",
    "#808080", "#ff0000", "#00ff00", "#ffff00", "#0000ff", "#ff00ff", "#00ffff", "#ffffff", "#000000",
    "#00005f", "#000087", "#0000af", "#0000d7", "#0000ff", "#005f00", "#005f5f", "#005f87", "#005faf",
    "#005fd7", "#005fff", "#008700", "#00875f", "#008787", "#0087af", "#0087d7", "#0087ff", "#00af00",
    "#00af5f", "#00af87", "#00afaf", "#00afd7", "#00afff", "#00d700", "#00d75f", "#00d787", "#00d7af",
    "#00d7d7", "#00d7ff", "#00ff00", "#00ff5f", "#00ff87", "#00ffaf", "#00ffd7", "#00ffff", "#5f0000",
    "#5f005f", "#5f0087", "#5f00af", "#5f00d7", "#5f00ff", "#5f5f00", "#5f5f5f", "#5f5f87", "#5f5faf",
    "#5f5fd7", "#5f5fff", "#5f8700", "#5f875f", "#5f8787", "#5f87af", "#5f87d7", "#5f87ff", "#5faf00",
    "#5faf5f", "#5faf87", "#5fafaf", "#5fafd7", "#5fafff", "#5fd700", "#5fd75f", "#5fd787", "#5fd7af",
    "#5fd7d7", "#5fd7ff", "#5fff00", "#5fff5f", "#5fff87", "#5fffaf", "#5fffd7", "#5fffff", "#870000",
    "#87005f", "#870087", "#8700af", "#8700d7", "#8700ff", "#875f00", "#875f5f", "#875f87", "#875faf",
    "#875fd7", "#875fff", "#878700", "#87875f", "#878787", "#8787af", "#8787d7", "#8787ff", "#87af00",
    "#87af5f", "#87af87", "#87afaf", "#87afd7", "#87afff", "#87d700", "#87d75f", "#87d787", "#87d7af",
    "#87d7d7", "#87d7ff", "#87ff00", "#87ff5f", "#87ff87", "#87ffaf", "#87ffd7", "#87ffff", "#af0000",
    "#af005f", "#af0087", "#af00af", "#af00d7", "#af00ff", "#af5f00", "#af5f5f", "#af5f87", "#af5faf",
    "#af5fd7", "#af5fff", "#af8700", "#af875f", "#af8787", "#af87af", "#af87d7", "#af87ff", "#afaf00",
    "#afaf5f", "#afaf87", "#afafaf", "#afafd7", "#afafff", "#afd700", "#afd75f", "#afd787", "#afd7af",
    "#afd7d7", "#afd7ff", "#afff00", "#afff5f", "#afff87", "#afffaf", "#afffd7", "#afffff", "#d70000",
    "#d7005f", "#d70087", "#d700af", "#d700d7", "#d700ff", "#d75f00", "#d75f5f", "#d75f87", "#d75faf",
    "#d75fd7", "#d75fff", "#d78700", "#d7875f", "#d78787", "#d787af", "#d787d7", "#d787ff", "#d7af00",
    "#d7af5f", "#d7af87", "#d7afaf", "#d7afd7", "#d7afff", "#d7d700", "#d7d75f", "#d7d787", "#d7d7af",
    "#d7d7d7", "#d7d7ff", "#d7ff00", "#d7ff5f", "#d7ff87", "#d7ffaf", "#d7ffd7", "#d7ffff", "#ff0000",
    "#ff005f", "#ff0087", "#ff00af", "#ff00d7", "#ff00ff", "#ff5f00", "#ff5f5f", "#ff5f87", "#ff5faf",
    "#ff5fd7", "#ff5fff", "#ff8700", "#ff875f", "#ff8787", "#ff87af", "#ff87d7", "#ff87ff", "#ffaf00",
    "#ffaf5f", "#ffaf87", "#ffafaf", "#ffafd7", "#ffafff", "#ffd700", "#ffd75f", "#ffd787", "#ffd7af",
    "#ffd7d7", "#ffd7ff", "#ffff00", "#ffff5f", "#ffff87", "#ffffaf", "#ffffd7", "#ffffff", "#080808",
    "#121212", "#1c1c1c", "#262626", "#303030", "#3a3a3a", "#444444", "#4e4e4e", "#585858", "#626262",
    "#6c6c6c", "#767676", "#808080", "#8a8a8a", "#949494", "#9e9e9e", "#a8a8a8", "#b2b2b2", "#bcbcbc",
    "#c6c6c6", "#d0d0d0", "#dadada", "#e4e4e4", "#eeeeee"]

class RichTextConsole(wx.richtext.RichTextCtrl):
    """
        An advanced rich test console panel supporting some Xterm control codes.
    """

    def __init__(self, parent):
        super(wx.richtext.RichTextCtrl, self).__init__(parent, -1, "", style=wx.richtext.RE_MULTILINE | wx.richtext.RE_READONLY)
        self.regex_urls=re.compile(r'\b((?:file://|https?://|mailto:)[^][\s<>|]*)')
        self.url_colour = wx.Colour(0,0,255)
        self.esc = colored.style.ESC
        self.end = colored.style.END
        self.noop = lambda *args, **kwargs: None

        self.actionsMap = {
            colored.style.BOLD: self.BeginBold,
            colored.style.RES_BOLD: self.EndBold,
            colored.style.UNDERLINED: self.BeginUnderline,
            colored.style.RES_UNDERLINED: self.EndUnderline,
            colored.style.RESET: self.EndAllStyles,
        }

        # Actions for coloring text
        for index, hex in enumerate(kColorList):
            escSeq = '{}{}{}'.format(colored.fore.ESC, index, colored.fore.END)
            wxcolor = wx.Colour(int(hex[1:3],16), int(hex[3:5],16), int(hex[5:],16), alpha=wx.ALPHA_OPAQUE)
            # NB : we use a default parameter to force the evaluation of the binding
            self.actionsMap[escSeq] = lambda bindedColor=wxcolor: self.BeginTextColour(bindedColor)
            
        self.Bind(wx.EVT_MOUSEWHEEL, self.onMouseWheel)


    def PreprocessAndWriteText(self, content):
        """Write text into console, while capturing URLs and making 
        them blue, underlined, and clickable.
        """
        textStream=iter(re.split(self.regex_urls, content))
        # The odd elements in textStream are plaintext;
        # the even elements are URLs.
        for plaintext in textStream:
            url=next(textStream, None)
            self.WriteText(plaintext)
            if url:    
                self.BeginTextColour(self.url_colour)
                self.BeginUnderline()
                self.BeginURL(url)
                self.WriteText(url)
                self.EndURL()
                self.EndUnderline()
                self.EndTextColour()
            
    def AppendText(self, content):
        """
        wx method overridden to capture the terminal control character and translate them into wx styles.
        Complexity : o(len(content))
        """
        self.SetInsertionPointEnd()
        unprocIndex = 0
        while True:
            # Invariant : unprocIndex is the starting index of the unprocessed part of the buffer
            escPos = content.find(self.esc, unprocIndex)
            if escPos == -1:
                break
            # Invariant : found an escape sequence starting at escPos
            # NB : we flush all the characters before the escape sequence, if any
            if content[unprocIndex:escPos]:
                self.PreprocessAndWriteText(content[unprocIndex:escPos])
            endEsc = content.find(self.end, escPos)
            if endEsc == -1:
                unprocIndex = escPos + len(self.esc)
                continue
            # Invariant : end of sequence has been found
            self.actionsMap.get(content[escPos:endEsc+1], self.noop)()
            unprocIndex = endEsc + 1
        # Invariant : unprocessed end of buffer is escape-free, ready to be printed
        self.PreprocessAndWriteText(content[unprocIndex:])
        self.ShowPosition(self.GetInsertionPoint())

    def onMouseWheel(self, event):
        if event.GetModifiers()==2 and event.GetWheelAxis()==wx.MOUSE_WHEEL_VERTICAL:
            if event.GetWheelRotation() >= event.GetWheelDelta():
                r=1.1
            elif event.GetWheelRotation() <= -event.GetWheelDelta():
                r=1.0/1.1
            else:
                return
            self.SetFontScale(self.GetFontScale() * r, True)
        else:
           event.Skip()



File: gooey/gui/components/widgets/core/text_input.py

import wx  # type: ignore

from gooey.gui.util.filedrop import FileDrop
from gooey.util.functional import merge
from gooey.gui.components.mouse import notifyMouseEvent


class TextInput(wx.Panel):
    def __init__(self, parent, *args, **kwargs):
        super(TextInput, self).__init__(parent)
        self.widget = wx.TextCtrl(self, *args, **kwargs)
        dt = FileDrop(self.widget)
        self.widget.SetDropTarget(dt)
        self.widget.SetMinSize((0, -1))
        self.widget.SetDoubleBuffered(True)
        self.widget.AppendText('')
        self.layout()

        self.widget.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)

    def layout(self):
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.widget, 0, wx.EXPAND)
        self.SetSizer(sizer)


    def setValue(self, value):
        self.widget.Clear()
        self.widget.AppendText(str(value))
        self.widget.SetInsertionPoint(0)

    def getValue(self):
        return self.widget.GetValue()

    def SetHint(self, value):
        self.widget.SetHint(value)

    def SetDropTarget(self, target):
        self.widget.SetDropTarget(target)



def PasswordInput(_, parent, *args, **kwargs):
    style = {'style': wx.TE_PASSWORD}
    return TextInput(parent, *args, **merge(kwargs, style))


def MultilineTextInput(_, parent, *args, **kwargs):
    style = {'style': wx.TE_MULTILINE}
    return TextInput(parent, *args, **merge(kwargs, style))



File: gooey/gui/components/widgets/core/__init__.py

from . chooser import Chooser, FileChooser, FileSaver, DirChooser, DateChooser, TimeChooser, MultiFileChooser, MultiDirChooser, ColourChooser
from . text_input import PasswordInput, MultilineTextInput, TextInput



File: gooey/gui/components/widgets/core/chooser.py

import wx  # type: ignore
import wx.lib.agw.multidirdialog as MDD  # type: ignore
import os
import re

from gooey.gui.components.widgets.core.text_input import TextInput
from gooey.gui.components.widgets.dialogs.calender_dialog import CalendarDlg
from gooey.gui.components.widgets.dialogs.time_dialog import TimeDlg
from gooey.gui.lang.i18n import _
from gooey.util.functional import merge
from gooey.gui.util.filedrop import FileDrop


class Chooser(wx.Panel):
    """
    TODO: Tests!
    TODO: Document GooeyOptions!
    Base 'Chooser' type.

    Launches a Dialog box that allows the user to pick files, directories,
    dates, etc.. and places the result into a TextInput in the UI

    TODO: oh, young me. DRY != Good Abstraction
    TODO: this is another weird inheritance hierarchy that's hard
          to follow. Why do subclasses reach into, not their parent
          class, but their _physical_ UI parent to grab the Gooey Options?
          All this could be simplified to make the data flow
          more apparent.
    """
    _gooey_options = {
        'pathsep': str
    }
    def __init__(self, parent, *args, **kwargs):
        super(Chooser, self).__init__(parent)
        self.options = parent._options
        buttonLabel = kwargs.pop('label', _('browse'))
        self.widget = TextInput(self, *args, **kwargs)
        self.button = wx.Button(self, label=buttonLabel)
        self.button.Bind(wx.EVT_BUTTON, self.spawnDialog)
        self.dropTarget = FileDrop(self.widget, self.dropHandler)
        self.widget.SetDropTarget(self.dropTarget)
        self.layout()

    def dropHandler(self, x, y, filenames):
        sep = self.options.get('pathsep', os.pathsep)
        self.widget.setValue(sep.join(filenames))
        return True

    def layout(self):
        layout = wx.BoxSizer(wx.HORIZONTAL)
        layout.Add(self.widget, 1, wx.EXPAND | wx.TOP, 2)
        layout.Add(self.button, 0, wx.LEFT, 10)

        v = wx.BoxSizer(wx.VERTICAL)
        v.Add(layout, 1, wx.EXPAND, wx.TOP, 1)
        self.SetSizer(v)

    def spawnDialog(self, event):
        fd = self.getDialog()
        if fd.ShowModal() == wx.ID_CANCEL:
            return
        self.processResult(self.getResult(fd))

    def getDialog(self):
        return wx.FileDialog(self, _('open_file'))

    def getResult(self, dialog):
        return dialog.GetPath()

    def processResult(self, result):
        self.setValue(result)

    def setValue(self, value):
        self.widget.setValue(value)

    def SetHint(self, value):
        self.widget.SetHint(value)

    def getValue(self):
        return self.widget.getValue()


class FileChooser(Chooser):
    """ Retrieve an existing file from the system """
    def getDialog(self):
        options = self.Parent._options
        return wx.FileDialog(self, message=options.get('message', _('open_file')),
                             style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST,
                             defaultFile=options.get('default_file', _("enter_filename")),
                             defaultDir=options.get('default_dir', _('')),
                             wildcard=options.get('wildcard', wx.FileSelectorDefaultWildcardStr))


class MultiFileChooser(Chooser):
    """ Retrieve an multiple files from the system """
    def getDialog(self):
        options = self.Parent._options
        return wx.FileDialog(self, message=options.get('message', _('open_files')),
                             style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE,
                             defaultFile=options.get('default_file', _("enter_filename")),
                             defaultDir=options.get('default_dir', _('')),
                             wildcard=options.get('wildcard', wx.FileSelectorDefaultWildcardStr))

    def getResult(self, dialog):
        return os.pathsep.join(dialog.GetPaths())


class FileSaver(Chooser):
    """ Specify the path to save a new file """
    def getDialog(self):
        options = self.Parent._options
        return wx.FileDialog(
            self,
            style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT,
            defaultFile=options.get('default_file', _("enter_filename")),
            defaultDir=options.get('default_dir', _('')),
            message=options.get('message', _('choose_file')),
            wildcard=options.get('wildcard', wx.FileSelectorDefaultWildcardStr)
        )


class DirChooser(Chooser):
    """ Retrieve a path to the supplied directory """
    def getDialog(self):
        options = self.Parent._options
        return wx.DirDialog(self, message=options.get('message', _('choose_folder')),
                            defaultPath=options.get('default_path', os.getcwd()))

class MultiDirChooser(Chooser):
    """ Retrieve multiple directories from the system """
    def getDialog(self):
        options = self.Parent._options
        return MDD.MultiDirDialog(self,
                                  message=options.get('message', _('choose_folders')),
                                  title=_('choose_folders_title'),
                                  defaultPath=options.get('default_path', os.getcwd()),
                                  agwStyle=MDD.DD_MULTIPLE | MDD.DD_DIR_MUST_EXIST)
    def getResult(self, dialog):
        paths = dialog.GetPaths()
        # Remove volume labels from Windows paths
        if 'nt' == os.name:
            for i, path in enumerate(paths):
                if path:
                    parts = path.split(os.sep)
                    vol = parts[0]
                    drives = re.match(r'.*\((?P<drive>\w:)\)', vol)
                    paths[i] = os.sep.join([drives.group('drive')] + parts[1:])

        return os.pathsep.join(paths)


class DateChooser(Chooser):
    """ Launches a date picker which returns an ISO Date """
    def __init__(self, *args, **kwargs):
        defaults = {'label': _('choose_date')}
        super(DateChooser, self).__init__(*args, **merge(kwargs, defaults))

    def getDialog(self):
        return CalendarDlg(self)


class TimeChooser(Chooser):
    """ Launches a time picker which returns and ISO Time """
    def __init__(self, *args, **kwargs):
        defaults = {'label': _('choose_time')}
        super(TimeChooser, self).__init__(*args, **merge(kwargs, defaults))

    def getDialog(self):
        return TimeDlg(self)


class ColourChooser(Chooser):
    """ Launches a color picker which returns a hex color code"""
    def __init__(self, *args, **kwargs):
        defaults = {'label': _('choose_colour'),
                    'style': wx.TE_RICH}
        super(ColourChooser, self).__init__(*args, **merge(kwargs, defaults))

    def setValue(self, value):
        colour = wx.Colour(value)
        self.widget.widget.SetForegroundColour(colour)
        self.widget.widget.SetBackgroundColour(colour)
        self.widget.setValue(value)

    def getResult(self, dialog):
        colour = dialog.GetColourData().GetColour()

        # Set text box back/foreground to selected colour
        self.widget.widget.SetForegroundColour(colour)
        self.widget.widget.SetBackgroundColour(colour)

        return colour.GetAsString(wx.C2S_HTML_SYNTAX)

    def getDialog(self):
        return wx.ColourDialog(self)



File: gooey/gui/components/widgets/dialogs/base_dialog.py

from gooey.gui.lang.i18n import _

import wx  # type: ignore

from gooey.gui.three_to_four import Constants


class BaseDialog(wx.Dialog):
  """
    Common base for CalendarDlg and TimeDlg.
  """
  def __init__(self, parent, pickerClass, pickerGetter, localizedPickerLabel):
    wx.Dialog.__init__(self, parent, title=localizedPickerLabel)

    self.SetBackgroundColour('#ffffff')

    self.ok_button = wx.Button(self, wx.ID_OK, label=_('ok'))
    self.picker = pickerClass(self, style=Constants.WX_DP_DROPDOWN)
    self.pickerGetter = pickerGetter

    vertical_container = wx.BoxSizer(wx.VERTICAL)
    vertical_container.AddSpacer(10)
    vertical_container.Add(self.picker, 0, wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER, 15)

    vertical_container.AddSpacer(10)
    button_sizer = wx.BoxSizer(wx.HORIZONTAL)
    button_sizer.AddStretchSpacer(1)
    button_sizer.Add(self.ok_button, 0)

    vertical_container.Add(button_sizer, 0, wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER, 15)
    vertical_container.AddSpacer(20)
    self.SetSizerAndFit(vertical_container)

    self.Bind(wx.EVT_BUTTON, self.onOkButton, self.ok_button)

  def onOkButton(self, event):
    self.EndModal(wx.ID_OK)
    event.Skip()

  def onCancelButton(self, event):
    try:
      return None
    except:
      self.Close()

  def GetPath(self):
    """
      Return the value chosen in the picker.
      The method is called GetPath() instead of getPath() to emulate the WX Pickers API.
      This allows the Chooser class to work same way with native WX dialogs or children of BaseDialog.
    """

    return self.pickerGetter(self.picker)



File: gooey/gui/components/widgets/dialogs/time_dialog.py


from .base_dialog import BaseDialog
from gooey.gui.three_to_four import Classes
from gooey.gui.lang.i18n import _


class TimeDlg(BaseDialog):
	def __init__(self, parent):
		super(TimeDlg, self).__init__(parent, 
			pickerClass=Classes.TimePickerCtrl,
			pickerGetter=lambda datepicker: datepicker.GetValue().FormatISOTime(),
			localizedPickerLabel=_('select_time'))



File: gooey/gui/components/widgets/dialogs/__init__.py




File: gooey/gui/components/widgets/dialogs/calender_dialog.py



from .base_dialog import BaseDialog
from gooey.gui.three_to_four import Classes
from gooey.gui.lang.i18n import _


class CalendarDlg(BaseDialog):
    def __init__(self, parent):
        super(CalendarDlg, self).__init__(parent, 
            pickerClass=Classes.DatePickerCtrl,
            pickerGetter=lambda datepicker: datepicker.GetValue().FormatISODate(),
            localizedPickerLabel=_('select_date'))


File: gooey/gui/containers/__init__.py




File: gooey/gui/containers/application.py

"""
Primary orchestration and control point for Gooey.
"""
import queue
import sys
import threading
from contextlib import contextmanager
from functools import wraps
from json import JSONDecodeError
from pprint import pprint
from subprocess import CalledProcessError
from threading import Thread, get_ident
from typing import Mapping, Dict, Type, Iterable

import six
import wx  # type: ignore

from gooey.gui.state import FullGooeyState
from gooey.python_bindings.types import PublicGooeyState
from rewx.widgets import set_basic_props

from gooey.gui.components.mouse import notifyMouseEvent
from gooey.gui.state import initial_state, present_time, form_page, ProgressEvent, TimingEvent
from gooey.gui import state as s
from gooey.gui.three_to_four import Constants
from rewx.core import Component, Ref, updatewx, patch
from typing_extensions import TypedDict

from rewx import wsx, render, create_element, mount, update
from rewx import components as c
from wx.adv import TaskBarIcon  # type: ignore
import signal

from gooey import Events
from gooey.gui import cli
from gooey.gui import events
from gooey.gui import seeder
from gooey.gui.components import modals
from gooey.gui.components.config import ConfigPage, TabbedConfigPage
from gooey.gui.components.console import Console
from gooey.gui.components.footer import Footer
from gooey.gui.components.header import FrameHeader
from gooey.gui.components.menubar import MenuBar
from gooey.gui.components.sidebar import Sidebar
from gooey.gui.components.tabbar import Tabbar
from gooey.gui.lang.i18n import _
from gooey.gui.processor import ProcessController
from gooey.gui.util.time import Timing
from gooey.gui.pubsub import pub
from gooey.gui.util import wx_util
from gooey.gui.util.wx_util import transactUI
from gooey.python_bindings import constants
from gooey.python_bindings.types import Failure, Success, CommandDetails, Try
from gooey.util.functional import merge, associn, assoc
from gooey.gui.image_repository import loadImages
from gooey.gui import host


from threading import Lock

from gooey.util.functional import associnMany

lock = Lock()

class GooeyApplication(wx.Frame):
    """
    Main window for Gooey.
    """

    def __init__(self, buildSpec, *args, **kwargs):
        super(GooeyApplication, self).__init__(None, *args, **kwargs)
        self._state = {}
        self.buildSpec = buildSpec

        self.applyConfiguration()
        self.menu = MenuBar(buildSpec)
        self.SetMenuBar(self.menu)
        self.header = FrameHeader(self, buildSpec)
        self.configs = self.buildConfigPanels(self)
        self.navbar = self.buildNavigation()
        self.footer = Footer(self, buildSpec)
        self.console = Console(self, buildSpec)

        self.props = {
            'background_color': self.buildSpec['header_bg_color'],
            'title': self.buildSpec['program_name'],
            'subtitle': self.buildSpec['program_description'],
            'height': self.buildSpec['header_height'],
            'image_uri': self.buildSpec['images']['configIcon'],
            'image_size': (six.MAXSIZE, self.buildSpec['header_height'] - 10)}

        state = form_page(initial_state(self.buildSpec))

        self.fprops = {
            'buttons': state['buttons'],
            'progress': state['progress'],
            'timing': state['timing'],
            'bg_color': self.buildSpec['footer_bg_color']
        }

        # self.hhh = render(create_element(RHeader, self.props), self)
        # self.fff = render(create_element(RFooter, self.fprops), self)
        # patch(self.hhh, create_element(RHeader, {**self.props, 'image_uri': self.buildSpec['images']['runningIcon']}))
        self.layoutComponent()
        self.timer = Timing(self)

        self.clientRunner = ProcessController(
            self.buildSpec.get('progress_regex'),
            self.buildSpec.get('progress_expr'),
            self.buildSpec.get('hide_progress_msg'),
            self.buildSpec.get('encoding'),
            self.buildSpec.get('requires_shell'),
            self.buildSpec.get('shutdown_signal', signal.SIGTERM)
        )

        pub.subscribe(events.WINDOW_START, self.onStart)
        pub.subscribe(events.WINDOW_RESTART, self.onStart)
        pub.subscribe(events.WINDOW_STOP, self.onStopExecution)
        pub.subscribe(events.WINDOW_CLOSE, self.onClose)
        pub.subscribe(events.WINDOW_CANCEL, self.onCancel)
        pub.subscribe(events.WINDOW_EDIT, self.onEdit)
        pub.subscribe(events.CONSOLE_UPDATE, self.console.logOutput)
        pub.subscribe(events.EXECUTION_COMPLETE, self.onComplete)
        pub.subscribe(events.PROGRESS_UPDATE, self.footer.updateProgressBar)
        pub.subscribe(events.TIME_UPDATE, self.footer.updateTimeRemaining)
        # Top level wx close event
        # self.Bind(wx.EVT_CLOSE, self.onClose)

        # TODO: handle child focus for per-field level validation.
        # self.Bind(wx.EVT_CHILD_FOCUS, self.handleFocus)

        if self.buildSpec.get('auto_start', False):
            self.onStart()


    def applyConfiguration(self):
        self.SetTitle(self.buildSpec['program_name'])
        self.SetBackgroundColour(self.buildSpec.get('body_bg_color'))


    def onStart(self, *args, **kwarg):
        """
        Verify user input and kick off the client's program if valid
        """
        # navigates away from the button because a
        # disabled focused button still looks enabled.
        self.footer.cancel_button.Disable()
        self.footer.start_button.Disable()
        self.footer.start_button.Navigate()
        if Events.VALIDATE_FORM in self.buildSpec.get('use_events', []):
            # TODO: make this wx thread safe so that it can
            # actually run asynchronously
            Thread(target=self.onStartAsync).run()
        else:
            Thread(target=self.onStartAsync).run()

    def onStartAsync(self, *args, **kwargs):
        with transactUI(self):
            try:
                errors = self.validateForm().getOrThrow()
                if errors:  # TODO
                    config = self.navbar.getActiveConfig()
                    config.setErrors(errors)
                    self.Layout()
                    # TODO: account for tabbed layouts
                    # TODO: scroll the first error into view
                    # TODO: rather than just snapping to the top
                    self.configs[0].Scroll(0, 0)
                else:
                    if self.buildSpec['clear_before_run']:
                        self.console.clear()
                    self.clientRunner.run(self.buildCliString())
                    self.showConsole()
            except CalledProcessError as e:
                self.showError()
                self.console.appendText(str(e))
                self.console.appendText(
                    '\n\nThis failure happens when Gooey tries to invoke your '
                    'code for the VALIDATE_FORM event and receives an expected '
                    'error code in response.'
                )
                wx.CallAfter(modals.showFailure)
            except JSONDecodeError as e:
                self.showError()
                self.console.appendText(str(e))
                self.console.appendText(
                    '\n\nGooey was unable to parse the response to the VALIDATE_FORM event. '
                    'This can happen if you have additional logs to stdout beyond what Gooey '
                    'expects.'
                )
                wx.CallAfter(modals.showFailure)
            # for some reason, we have to delay the re-enabling of
            # the buttons by a few ms otherwise they pickup pending
            # events created while they were disabled. Trial and error
            # let to this solution.
            wx.CallLater(20, self.footer.start_button.Enable)
            wx.CallLater(20, self.footer.cancel_button.Enable)


    def onEdit(self):
        """Return the user to the settings screen for further editing"""
        with transactUI(self):
            for config in self.configs:
                config.resetErrors()
            self.showSettings()


    def onComplete(self, *args, **kwargs):
        """
        Display the appropriate screen based on the success/fail of the
        host program
        """
        with transactUI(self):
            if self.clientRunner.was_success():
                if self.buildSpec.get('return_to_config', False):
                    self.showSettings()
                else:
                    self.showSuccess()
                    if self.buildSpec.get('show_success_modal', True):
                        wx.CallAfter(modals.showSuccess)
            else:
                if self.clientRunner.wasForcefullyStopped:
                    self.showForceStopped()
                else:
                    self.showError()
                    if self.buildSpec.get('show_failure_modal'):
                        wx.CallAfter(modals.showFailure)

    def onCancel(self):
        """Close the program after confirming

        We treat the behavior of the "cancel" button slightly
        differently than the general window close X button only
        because this is 'part of' the form.
        """
        if modals.confirmExit():
            self.onClose()


    def onStopExecution(self):
        """Displays a scary message and then force-quits the executing
        client code if the user accepts"""
        if self.shouldStopExecution():
            self.clientRunner.stop()


    def onClose(self, *args, **kwargs):
        """Stop any actively running client program, cleanup the top
        level WxFrame and shutdown the current process"""
        # issue #592 - we need to run the same onStopExecution machinery
        # when the exit button is clicked to ensure everything is cleaned
        # up correctly.
        if self.clientRunner.running():
            if self.shouldStopExecution():
                self.clientRunner.stop()
                self.destroyGooey()
        else:
            self.destroyGooey()

    def buildCliString(self) -> str:
        """
        Collect all of the required information from the config screen and
        build a CLI string which can be used to invoke the client program
        """
        cmd = self.getCommandDetails()
        return cli.cliCmd(
            cmd.target,
            cmd.subcommand,
            cmd.positionals,
            cmd.optionals,
            suppress_gooey_flag=self.buildSpec['suppress_gooey_flag']
        )

    def validateForm(self) -> Try[Mapping[str, str]]:
        config = self.navbar.getActiveConfig()
        localErrors: Mapping[str, str] = config.getErrors()
        dynamicResult: Try[Mapping[str, str]] = self.fetchDynamicValidations()

        combineErrors = lambda m: merge(localErrors, m)
        return dynamicResult.map(combineErrors)


    def fetchDynamicValidations(self) -> Try[Mapping[str, str]]:
        # only run the dynamic validation if the user has
        # specifically subscribed to that event
        if Events.VALIDATE_FORM in self.buildSpec.get('use_events', []):
            cmd = self.getCommandDetails()
            return seeder.communicate(cli.formValidationCmd(
                cmd.target,
                cmd.subcommand,
                cmd.positionals,
                cmd.optionals
            ), self.buildSpec['encoding'])
        else:
            # shim response if nothing to do.
            return Success({})


    def getCommandDetails(self) -> CommandDetails:
        """
        Temporary helper for getting the state of the current Config.

        To be deprecated upon (the desperately needed) refactor.
        """
        config = self.navbar.getActiveConfig()
        group = self.buildSpec['widgets'][self.navbar.getSelectedGroup()]
        return CommandDetails(
            self.buildSpec['target'],
            group['command'],
            config.getPositionalValues(),
            config.getOptionalValues(),
        )


    def shouldStopExecution(self):
        return not self.buildSpec['show_stop_warning'] or modals.confirmForceStop()


    def destroyGooey(self):
        self.Destroy()
        sys.exit()

    def block(self, **kwargs):
        pass


    def layoutComponent(self):
        sizer = wx.BoxSizer(wx.VERTICAL)
        # sizer.Add(self.hhh, 0, wx.EXPAND)
        sizer.Add(self.header, 0, wx.EXPAND)
        sizer.Add(wx_util.horizontal_rule(self), 0, wx.EXPAND)

        sizer.Add(self.navbar, 1, wx.EXPAND)
        sizer.Add(self.console, 1, wx.EXPAND)
        sizer.Add(wx_util.horizontal_rule(self), 0, wx.EXPAND)
        # sizer.Add(self.fff, 0, wx.EXPAND)
        sizer.Add(self.footer, 0, wx.EXPAND)
        self.SetMinSize((400, 300))
        self.SetSize(self.buildSpec['default_size'])
        self.SetSizer(sizer)
        self.console.Hide()
        self.Layout()
        if self.buildSpec.get('fullscreen', True):
            self.ShowFullScreen(True)
        # Program Icon (Windows)
        icon = wx.Icon(self.buildSpec['images']['programIcon'], wx.BITMAP_TYPE_PNG)
        self.SetIcon(icon)
        if sys.platform != 'win32':
            # OSX needs to have its taskbar icon explicitly set
            # bizarrely, wx requires the TaskBarIcon to be attached to the Frame
            # as instance data (self.). Otherwise, it will not render correctly.
            self.taskbarIcon = TaskBarIcon(iconType=wx.adv.TBI_DOCK)
            self.taskbarIcon.SetIcon(icon)


    def buildNavigation(self):
        """
        Chooses the appropriate layout navigation component based on user prefs
        """
        if self.buildSpec['navigation'] == constants.TABBED:
            navigation = Tabbar(self, self.buildSpec, self.configs)
        else:
            navigation = Sidebar(self, self.buildSpec, self.configs)
            if self.buildSpec['navigation'] == constants.HIDDEN:
                navigation.Hide()
        return navigation


    def buildConfigPanels(self, parent):
        page_class = TabbedConfigPage if self.buildSpec['tabbed_groups'] else ConfigPage

        return [page_class(parent, widgets, self.buildSpec)
                for widgets in self.buildSpec['widgets'].values()]


    def showSettings(self):
        self.navbar.Show(True)
        self.console.Show(False)
        self.header.setImage('settings_img')
        self.header.setTitle(_("settings_title"))
        self.header.setSubtitle(self.buildSpec['program_description'])
        self.footer.showButtons('cancel_button', 'start_button')
        self.footer.progress_bar.Show(False)
        self.footer.time_remaining_text.Show(False)


    def showConsole(self):
        self.navbar.Show(False)
        self.console.Show(True)
        self.header.setImage('running_img')
        self.header.setTitle(_("running_title"))
        self.header.setSubtitle(_('running_msg'))
        self.footer.showButtons('stop_button')
        if not self.buildSpec.get('disable_progress_bar_animation', False):
            self.footer.progress_bar.Show(True)
        self.footer.time_remaining_text.Show(False)
        if self.buildSpec.get('timing_options')['show_time_remaining']:
            self.timer.start()
            self.footer.time_remaining_text.Show(True)
        if not self.buildSpec['progress_regex']:
            self.footer.progress_bar.Pulse()


    def showComplete(self):
        self.navbar.Show(False)
        self.console.Show(True)
        buttons = (['edit_button', 'restart_button', 'close_button']
                   if self.buildSpec.get('show_restart_button', True)
                   else ['edit_button', 'close_button'])
        self.footer.showButtons(*buttons)
        self.footer.progress_bar.Show(False)
        if self.buildSpec.get('timing_options')['show_time_remaining']:
            self.timer.stop()
        self.footer.time_remaining_text.Show(True)
        if self.buildSpec.get('timing_options')['hide_time_remaining_on_complete']:
            self.footer.time_remaining_text.Show(False)


    def showSuccess(self):
        self.showComplete()
        self.header.setImage('check_mark')
        self.header.setTitle(_('finished_title'))
        self.header.setSubtitle(_('finished_msg'))
        self.Layout()


    def showError(self):
        self.showComplete()
        self.header.setImage('error_symbol')
        self.header.setTitle(_('finished_title'))
        self.header.setSubtitle(_('finished_error'))


    def showForceStopped(self):
        self.showComplete()
        if self.buildSpec.get('force_stop_is_error', True):
            self.showError()
        else:
            self.showSuccess()
        self.header.setSubtitle(_('finished_forced_quit'))









File: gooey/gui/application/__init__.py




File: gooey/gui/application/application.py

import sys
from json import JSONDecodeError

import six
import wx  # type: ignore

from gooey import Events
from gooey.gui import events
from gooey.gui import host
from gooey.gui import state as s
from gooey.gui.application.components import RHeader, ProgressSpinner, ErrorWarning, RTabbedLayout, \
    RSidebar, RFooter
from gooey.gui.components import modals
from gooey.gui.components.config import ConfigPage
from gooey.gui.components.config import TabbedConfigPage
from gooey.gui.components.console import Console
from gooey.gui.components.menubar import MenuBar
from gooey.gui.lang.i18n import _
from gooey.gui.processor import ProcessController
from gooey.gui.pubsub import pub
from gooey.gui.state import FullGooeyState
from gooey.gui.state import initial_state, ProgressEvent, TimingEvent
from gooey.gui.util.wx_util import transactUI, callafter
from gooey.python_bindings import constants
from gooey.python_bindings.dynamics import unexpected_exit_explanations, \
    deserialize_failure_explanations
from gooey.python_bindings.types import PublicGooeyState
from gooey.python_bindings.types import Try
from gooey.util.functional import assoc
from gooey.gui.util.time import Timing
from rewx import components as c  # type: ignore
from rewx import wsx  # type: ignore
from rewx.core import Component, Ref  # type: ignore


class RGooey(Component):
    """
    Main Application container for Gooey.

    State Management
    ----------------

    Pending further refactor, state is tracked in two places:
    1. On this instance (React style)
    2. In the WX Form Elements themselves[0]

    As needed, these two states are merged to form the `FullGooeyState`, which
    is the canonical state object against which all logic runs.


    Dynamic Updates
    ---------------




    [0] this is legacy and will (eventually) be refactored away

    """
    def __init__(self, props):
        super().__init__(props)
        self.frameRef = Ref()
        self.consoleRef = Ref()
        self.configRef = Ref()

        self.buildSpec = props
        self.state = initial_state(props)
        self.headerprops = lambda state: {
            'background_color': self.buildSpec['header_bg_color'],
            'title': state['title'],
            'show_title': state['header_show_title'],
            'subtitle': state['subtitle'],
            'show_subtitle': state['header_show_subtitle'],
            'flag': wx.EXPAND,
            'height': self.buildSpec['header_height'],
            'image_uri': state['image'],
            'image_size': (six.MAXSIZE, self.buildSpec['header_height'] - 10)}

        self.fprops = lambda state: {
            'buttons': state['buttons'],
            'progress': state['progress'],
            'timing': state['timing'],
            'bg_color': self.buildSpec['footer_bg_color'],
            'flag': wx.EXPAND,
        }
        self.clientRunner = ProcessController.of(self.buildSpec)
        self.timer = None


    def component_did_mount(self):
        pub.subscribe(events.WINDOW_START, self.onStart)
        pub.subscribe(events.WINDOW_RESTART, self.onStart)
        pub.subscribe(events.WINDOW_STOP, self.handleInterrupt)
        pub.subscribe(events.WINDOW_CLOSE, self.handleClose)
        pub.subscribe(events.WINDOW_CANCEL, self.handleCancel)
        pub.subscribe(events.WINDOW_EDIT, self.handleEdit)
        pub.subscribe(events.CONSOLE_UPDATE, self.consoleRef.instance.logOutput)
        pub.subscribe(events.EXECUTION_COMPLETE, self.handleComplete)
        pub.subscribe(events.PROGRESS_UPDATE, self.updateProgressBar)
        pub.subscribe(events.TIME_UPDATE, self.updateTime)
        # # Top level wx close event
        frame: wx.Frame = self.frameRef.instance
        frame.Bind(wx.EVT_CLOSE, self.handleClose)
        frame.SetMenuBar(MenuBar(self.buildSpec))
        self.timer = Timing(frame)

        if self.state['fullscreen']:
            frame.ShowFullScreen(True)

        if self.state['show_preview_warning'] and not 'unittest' in sys.modules.keys():
            wx.MessageDialog(None, caption='YOU CAN DISABLE THIS MESSAGE',
                             message="""
                This is a preview build of 1.2.0! There may be instability or 
                broken functionality. If you encounter any issues, please open an issue 
                here: https://github.com/chriskiehl/Gooey/issues 
                
                The current stable version is 1.0.8. 
                
                NOTE! You can disable this message by setting `show_preview_warning` to False. 
                
                e.g. 
                `@Gooey(show_preview_warning=False)`
                """).ShowModal()

    def getActiveConfig(self):
        return [item
                for child in self.configRef.instance.Children
                # we descend down another level of children  to account
                # for Notebook layouts (which have wrapper objects)
                for item in [child] + list(child.Children)
                if isinstance(item, ConfigPage)
                or isinstance(item, TabbedConfigPage)][self.state['activeSelection']]

    def getActiveFormState(self):
        """
        This boiler-plate and manual interrogation of the UIs
        state is required until we finish porting the Config Form
        over to rewx (which is a battle left for another day given
        its complexity)
        """
        return self.getActiveConfig().getFormState()


    def fullState(self):
        """
        Re: final porting is a to do. For now we merge the UI
        state into the main tracked state.
        """
        formState = self.getActiveFormState()
        return s.combine(self.state, self.props, formState)


    def onStart(self, *args, **kwargs):
        """
        Dispatches the start behavior.
        """
        if Events.VALIDATE_FORM in self.state['use_events']:
            self.runAsyncValidation()
        else:
            self.startRun()


    def startRun(self):
        """
        Kicks off a run by invoking the host's code
        and pumping its stdout to Gooey's Console window.
        """
        state = self.fullState()
        if state['clear_before_run']:
            self.consoleRef.instance.Clear()
        self.set_state(s.consoleScreen(_, state))
        self.clientRunner.run(s.buildInvocationCmd(state))
        self.timer.start()
        self.frameRef.instance.Layout()
        for child in self.frameRef.instance.Children:
            child.Layout()


    def syncExternalState(self, state: FullGooeyState):
        """
        Sync the UI's state to what the host program has requested.
        """
        self.getActiveConfig().syncFormState(s.activeFormState(state))
        self.frameRef.instance.Layout()
        for child in self.frameRef.instance.Children:
            child.Layout()


    def handleInterrupt(self, *args, **kwargs):
        if self.shouldStopExecution():
            self.clientRunner.stop()

    def handleComplete(self, *args, **kwargs):
        self.timer.stop()
        if self.clientRunner.was_success():
            self.handleSuccessfulRun()
            if Events.ON_SUCCESS in self.state['use_events']:
                self.runAsyncExternalOnCompleteHandler(was_success=True)
        else:
            self.handleErrantRun()
            if Events.ON_ERROR in self.state['use_events']:
                self.runAsyncExternalOnCompleteHandler(was_success=False)

    def handleSuccessfulRun(self):
        if self.state['return_to_config']:
            self.set_state(s.editScreen(_, self.state))
        else:
            self.set_state(s.successScreen(_, self.state))
            if self.state['show_success_modal']:
                wx.CallAfter(modals.showSuccess)


    def handleErrantRun(self):
        if self.clientRunner.wasForcefullyStopped:
            self.set_state(s.interruptedScreen(_, self.state))
        else:
            self.set_state(s.errorScreen(_, self.state))
            if self.state['show_failure_modal']:
                wx.CallAfter(modals.showFailure)


    def successScreen(self):
        strings = {'title': _('finished_title'), 'subtitle': _('finished_msg')}
        self.set_state(s.success(self.state, strings, self.buildSpec))


    def handleEdit(self, *args, **kwargs):
        self.set_state(s.editScreen(_, self.state))

    def handleCancel(self, *args, **kwargs):
        if modals.confirmExit():
            self.handleClose()

    def handleClose(self, *args, **kwargs):
        """Stop any actively running client program, cleanup the top
        level WxFrame and shutdown the current process"""
        # issue #592 - we need to run the same onStopExecution machinery
        # when the exit button is clicked to ensure everything is cleaned
        # up correctly.
        frame: wx.Frame = self.frameRef.instance
        if self.clientRunner.running():
            if self.shouldStopExecution():
                self.clientRunner.stop()
                frame.Destroy()
                # TODO: NOT exiting here would allow
                # spawing the gooey to input params then
                # returning control to the CLI
                sys.exit()
        else:
            frame.Destroy()
            sys.exit()

    def shouldStopExecution(self):
        return not self.state['show_stop_warning'] or modals.confirmForceStop()

    def updateProgressBar(self, *args, progress=None):
        self.set_state(s.updateProgress(self.state, ProgressEvent(progress=progress)))

    def updateTime(self, *args, elapsed_time=None, estimatedRemaining=None, **kwargs):
        event = TimingEvent(elapsed_time=elapsed_time, estimatedRemaining=estimatedRemaining)
        self.set_state(s.updateTime(self.state, event))

    def handleSelectAction(self, event):
        self.set_state(assoc(self.state, 'activeSelection', event.Selection))


    def runAsyncValidation(self):
        def handleHostResponse(hostState: PublicGooeyState):
            self.set_state(s.finishUpdate(self.state))
            currentState = self.fullState()
            self.syncExternalState(s.mergeExternalState(currentState, hostState))
            if not s.has_errors(self.fullState()):
                self.startRun()
            else:
                self.set_state(s.editScreen(_, s.show_alert(self.fullState())))

        def onComplete(result: Try[PublicGooeyState]):
            result.onSuccess(handleHostResponse)
            result.onError(self.handleHostError)

        self.set_state(s.beginUpdate(self.state))
        fullState = self.fullState()
        host.communicateFormValidation(fullState, callafter(onComplete))


    def runAsyncExternalOnCompleteHandler(self, was_success):
        def handleHostResponse(hostState):
            if hostState:
                self.syncExternalState(s.mergeExternalState(self.fullState(), hostState))

        def onComplete(result: Try[PublicGooeyState]):
            result.onError(self.handleHostError)
            result.onSuccess(handleHostResponse)

        if was_success:
            host.communicateSuccessState(self.fullState(), callafter(onComplete))
        else:
            host.communicateErrorState(self.fullState(), callafter(onComplete))


    def handleHostError(self, ex):
        """
        All async errors get pumped here where we dump out the
        error and they hopefully provide a lot of helpful debugging info
        for the user.
        """
        try:
            self.set_state(s.errorScreen(_, self.state))
            self.consoleRef.instance.appendText(str(ex))
            self.consoleRef.instance.appendText(str(getattr(ex, 'output', '')))
            self.consoleRef.instance.appendText(str(getattr(ex, 'stderr', '')))
            raise ex
        except JSONDecodeError as e:
            self.consoleRef.instance.appendText(deserialize_failure_explanations)
        except Exception as e:
            self.consoleRef.instance.appendText(unexpected_exit_explanations)
        finally:
            self.set_state({**self.state, 'fetchingUpdate': False})


    def render(self):
        return wsx(
            [c.Frame, {'title': self.buildSpec['program_name'],
                       'background_color': self.buildSpec['body_bg_color'],
                       'double_buffered': True,
                       'min_size': (400, 300),
                       'icon_uri': self.state['images']['programIcon'],
                       'size': self.buildSpec['default_size'],
                       'ref': self.frameRef},
             [c.Block, {'orient': wx.VERTICAL},
              [RHeader, self.headerprops(self.state)],
              [c.StaticLine, {'style': wx.LI_HORIZONTAL, 'flag': wx.EXPAND}],
              [ProgressSpinner, {'show': self.state['fetchingUpdate']}],
              [ErrorWarning, {'show': self.state['show_error_alert'],
                              'uri': self.state['images']['errorIcon']}],
              [Console, {**self.buildSpec,
                         'flag': wx.EXPAND,
                         'proportion': 1,
                         'show': self.state['screen'] == 'CONSOLE',
                         'ref': self.consoleRef}],
              [RTabbedLayout if self.buildSpec['navigation'] == constants.TABBED else RSidebar,
               {'bg_color': self.buildSpec['sidebar_bg_color'],
                'label': 'Some Action!',
                'tabbed_groups': self.buildSpec['tabbed_groups'],
                'show_sidebar': self.state['show_sidebar'],
                'ref': self.configRef,
                'show': self.state['screen'] == 'FORM',
                'activeSelection': self.state['activeSelection'],
                'options': list(self.buildSpec['widgets'].keys()),
                'on_change': self.handleSelectAction,
                'config': self.buildSpec['widgets'],
                'flag': wx.EXPAND,
                'proportion': 1}],
              [c.StaticLine, {'style': wx.LI_HORIZONTAL, 'flag': wx.EXPAND}],
              [RFooter, self.fprops(self.state)]]]
        )







File: gooey/gui/application/components.py

"""
Houses all the supporting rewx components for
the main application window.
"""
import wx  # type: ignore
from typing_extensions import TypedDict

from gooey.gui.components.config import ConfigPage, TabbedConfigPage
from gooey.gui.components.console import Console
from gooey.gui.components.mouse import notifyMouseEvent
from gooey.gui.components.sidebar import Sidebar
from gooey.gui.components.tabbar import Tabbar
from gooey.gui.lang.i18n import _
from gooey.gui.pubsub import pub
from gooey.gui.state import present_time
from gooey.gui.three_to_four import Constants
from gooey.python_bindings import constants
from rewx import components as c  # type: ignore
from rewx import wsx, mount, update  # type: ignore
from rewx.core import Component, Ref  # type: ignore
from rewx.widgets import set_basic_props  # type: ignore


def attach_notifier(parent):
    """
    Recursively attaches the mouseEvent notifier
    to all elements in the tree
    """
    parent.Bind(wx.EVT_LEFT_DOWN, notifyMouseEvent)
    for child in parent.Children:
        attach_notifier(child)


class HeaderProps(TypedDict):
    background_color: str
    title: str
    show_title: bool
    subtitle: str
    show_subtitle: bool


class RHeader(Component):
    def __init__(self, props):
        super().__init__(props)
        self.parentRef = Ref()

    def component_did_mount(self):
        attach_notifier(self.parentRef.instance)

    def render(self):
        if 'running' not in self.props['image_uri']:
            imageProps = {
                'uri': self.props['image_uri'],
                'size': self.props['image_size'],
                'flag': wx.RIGHT,
                'border': 10}
        else:
            imageProps = {
                'size': self.props['image_size'],
                'flag': wx.RIGHT,
                'border': 10}
        return wsx(
            [c.Block, {'orient': wx.HORIZONTAL,
                       'ref': self.parentRef,
                       'min_size': (120, self.props['height']),
                       'background_color': self.props['background_color']},
             [c.Block, {'orient': wx.VERTICAL,
                        'flag': wx.ALIGN_CENTER_VERTICAL | wx.ALL,
                        'proportion': 1,
                        'border': 10},
              [TitleText, {'label': self.props['title'],
                           'show': self.props['show_title'],
                           'wx_name': 'header_title'}],
              [c.StaticText, {'label': self.props['subtitle'],
                              'show': self.props['show_subtitle'],
                              'wx_name': 'header_subtitle'}]],
             [c.StaticBitmap, imageProps]]
        )



class RFooter(Component):
    def __init__(self, props):
        super().__init__(props)
        self.ref = Ref()

    def component_did_mount(self):
        """
        We have to manually wire up LEFT_DOWN handlers
        for every component due to wx limitations.
        See: mouse.py docs for background.
        """
        block: wx.BoxSizer = self.ref.instance
        attach_notifier(block)

    def handle(self, btn):
        def inner(*args, **kwargs):
            pub.send_message(btn['id'])
        return inner

    def render(self):
        return wsx(
            [c.Block, {'orient': wx.VERTICAL,
                       'min_size': (30, 53),
                       'background_color': self.props['bg_color']},
             [c.Block, {'orient': wx.VERTICAL, 'proportion': 1}],
             [c.Block, {'orient': wx.HORIZONTAL,
                        'border': 20,
                        'flag': wx.EXPAND | wx.LEFT | wx.RIGHT,
                        'ref': self.ref},
              [c.Gauge, {'range': 100,
                         'proportion': 1,
                         'value': self.props['progress']['value'],
                         'show': self.props['progress']['show']}],
              [c.StaticText, {'label': present_time(self.props['timing']),
                              'flag': wx.LEFT,
                              'wx_name': 'timing',
                              'show': self.props['timing']['show'],
                              'border': 20}],
              [c.Block, {'orient': wx.HORIZONTAL, 'proportion': 1}],
              *[[c.Button, {**btn,
                            'label': _(btn['label_id']),
                            'min_size': (90, 23),
                            'flag': wx.LEFT,
                            'border': 10,
                            'on_click': self.handle(btn)
                            }]
                for btn in self.props['buttons']]],
             [c.Block, {'orient': wx.VERTICAL, 'proportion': 1}]]
        )


class RNavbar(Component):
    def __init__(self, props):
        super().__init__(props)

    # if self.buildSpec['navigation'] == constants.TABBED:
    #     navigation = Tabbar(self, self.buildSpec, self.configs)
    # else:
    #     navigation = Sidebar(self, self.buildSpec, self.configs)
    #     if self.buildSpec['navigation'] == constants.HIDDEN:
    #         navigation.Hide()
    def render(self):
        return wsx(

        )

def VerticalSpacer(props):
    return wsx([c.Block, {'orient': wx.VERTICAL, 'min_size': (-1, props['height'])}])

def SidebarControls(props):
    return wsx(
        [c.Block, {'orient': wx.VERTICAL,
                   'min_size': (180, 0),
                   'size': (180, 0),
                   'show': props.get('show', True),
                   'flag': wx.EXPAND,
                   'proportion': 0,
                   'background_color': props['bg_color']},
         [c.Block, {'orient': wx.VERTICAL,
                    'min_size': (180, 0),
                    'size': (180, 0),
                    'flag': wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM,
                    'border': 10,
                    'proportion': 1,
                    'background_color': props['bg_color']},
          [VerticalSpacer, {'height': 15}],
          [TitleText, {'label': props['label']}],
          [VerticalSpacer, {'height': 5}],
          [c.ListBox, {'choices': props['options'],
                       'value': props['activeSelection'],
                       'proportion': 1,
                       'on_change': props['on_change'],
                       'flag': wx.EXPAND}],
          [VerticalSpacer, {'height': 10}]]]
    )


def ProgressSpinner(props):
    return wsx(
        [c.Block, {'flag': wx.EXPAND, 'show': props['show']},
         [c.Gauge, {'flag': wx.EXPAND,
                    'value': -1,
                    'size': (-1, 4)}],
         [c.StaticLine, {'style': wx.LI_HORIZONTAL,
                         'flag': wx.EXPAND}]]
    )


def ErrorWarning(props):
    return wsx(
        [c.Block, {'orient': wx.HORIZONTAL,
                   'background_color': '#fdeded',
                   'style': wx.SIMPLE_BORDER,
                   'flag': wx.EXPAND | wx.ALL,
                   'proportion': 0,
                   'border': 5,
                   'min_size': (-1, 45),
                   'show': props.get('show', True)},
         [c.StaticBitmap, {'size': (24, 24),
                           'flag': wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL,
                           'border': 6,
                           'uri': props['uri']}],
         [c.StaticText, {'label': 'Whoops! You have some errors which must be corrected',
                         'flag': wx.ALIGN_CENTER_VERTICAL}]]
    )

def RSidebar(props):
    return wsx(
        [c.Block, {'orient': wx.HORIZONTAL,
                   'show': props.get('show', True),
                   'flag': props['flag'],
                   'proportion': props['proportion'],
                   'ref': props['ref']},
         [SidebarControls, {**props, 'show': props['show_sidebar']}],
         [c.StaticLine, {'style': wx.LI_VERTICAL,
                         'flag': wx.EXPAND,
                         'min_size': (1, -1)}],
         *[[TabbedConfigPage if props['tabbed_groups'] else ConfigPage,
            {'flag': wx.EXPAND,
             'proportion': 3,
             'config': config,
             'show': i == props['activeSelection']}]
           for i, config in enumerate(props['config'].values())]
         ]
    )


def RTabbedLayout(props):
    return wsx(
        [c.Notebook, {'flag': wx.EXPAND | wx.ALL,
                      'show': props.get('show', True),
                      'proportion': 1,
                      'on_change': props['on_change'],
                      'ref': props['ref']},
         *[[c.NotebookItem,
            {'title': props['options'][i], 'selected': props['activeSelection'] == i},
            [TabbedConfigPage if props['tabbed_groups'] else ConfigPage,
             {'flag': wx.EXPAND,
              'proportion': 3,
              'config': config,
              'show': i == props['activeSelection']}]]
           for i, config in enumerate(props['config'].values())]]
    )



def layout_choose():
    def buildNavigation(self):
        """
        Chooses the appropriate layout navigation component based on user prefs
        """
        if self.buildSpec['navigation'] == constants.TABBED:
            navigation = Tabbar(self, self.buildSpec, self.configs)
        else:
            navigation = Sidebar(self, self.buildSpec, self.configs)
            if self.buildSpec['navigation'] == constants.HIDDEN:
                navigation.Hide()
        return navigation


    def buildConfigPanels(self, parent):
        page_class = TabbedConfigPage if self.buildSpec['tabbed_groups'] else ConfigPage

        return [page_class(parent, widgets, self.buildSpec)
                for widgets in self.buildSpec['widgets'].values()]















class TitleText(Component):
    def __init__(self, props):
        super().__init__(props)
        self.ref = Ref()

    def component_did_mount(self):
        text: wx.StaticText = self.ref.instance
        font_size = text.GetFont().GetPointSize()
        text.SetFont(wx.Font(
            int(font_size * 1.2),
            wx.FONTFAMILY_DEFAULT,
            Constants.WX_FONTSTYLE_NORMAL,
            wx.FONTWEIGHT_BOLD,
            False
        ))

    def render(self):
        return wsx([c.StaticText, {**self.props, 'label': self.props['label'], 'ref': self.ref}])


##
## REWX definitions:
##

@mount.register(ConfigPage)  # type: ignore
def config(element, parent):
    return update(element, ConfigPage(parent, element['props']['config'], {'contents': []}))

@update.register(ConfigPage)  # type: ignore
def config(element, instance: ConfigPage):
    set_basic_props(instance, element['props'])
    return instance

@mount.register(TabbedConfigPage)  # type: ignore
def tabbedconfig(element, parent):
    return update(element, TabbedConfigPage(parent, element['props']['config'], {'contents': []}))

@update.register(TabbedConfigPage)  # type: ignore
def tabbedconfig(element, instance: TabbedConfigPage):
    set_basic_props(instance, element['props'])
    return instance

@mount.register(Console)  # type: ignore
def console(element, parent):
    return update(element, Console(parent, element['props']))

@update.register(Console)  # type: ignore
def console(element, instance: Console):
    set_basic_props(instance, element['props'])
    if 'show' in element['props']:
        instance.Show(element['props']['show'])
    return instance




# Gooey 
  

Turn (almost) any Python 3 Console Program into a GUI application with one line

<p align="center">
    <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/1-0-4-title-card.png" />
</p>


Table of Contents
-----------------  

- [Gooey](#gooey)
- [Table of contents](#table-of-contents)
- [Latest Update](#latest-update)
- [Quick Start](#quick-start)
    - [Installation Instructions](#installation-instructions)
    - [Usage](#usage)
    - [Examples](#examples)
- [What It Is](#what-is-it)
- [Why Is It](#why)
- [Who is this for](#who-is-this-for)
- [How does it work](#how-does-it-work)
- [Internationalization](#internationalization)
- [Global Configuration](#global-configuration)
- [Layout Customization](#layout-customization)
- [Run Modes](#run-modes)
    - [Full/Advanced](#advanced)
    - [Basic](#basic)
    - [No Config](#no-config)
- [Menus](#menus)    
- [Dynamic Validation](#dynamic-validation)
- [Lifecycle Events and UI control](#lifecycle-events-and-ui-control)
- [Showing Progress](#showing-progress)
    - [Elapsed / Remaining Time](#elapsed--remaining-time)
- [Customizing Icons](#customizing-icons)
- [Packaging](#packaging)
- [Screenshots](#screenshots)
- [Contributing](#wanna-help)
- [Image Credits](#image-credits)



----------------  


## Quick Start


### Installation instructions


The easiest way to install Gooey is via `pip`

    pip install Gooey 

Alternatively, you can install Gooey by cloning the project to your local directory

    git clone https://github.com/chriskiehl/Gooey.git

run `setup.py` 

    python setup.py install
    


### Usage  

Gooey is attached to your code via a simple decorator on whichever method has your `argparse` declarations (usually `main`).

    from gooey import Gooey

    @Gooey      <--- all it takes! :)
    def main():
      parser = ArgumentParser(...)
      # rest of code

Different styling and functionality can be configured by passing arguments into the decorator.

    # options
    @Gooey(advanced=Boolean,          # toggle whether to show advanced config or not 
           language=language_string,  # Translations configurable via json
           auto_start=True,           # skip config screens all together
           target=executable_cmd,     # Explicitly set the subprocess executable arguments
           program_name='name',       # Defaults to script name
           program_description,       # Defaults to ArgParse Description
           default_size=(610, 530),   # starting size of the GUI
           required_cols=1,           # number of columns in the "Required" section
           optional_cols=2,           # number of columns in the "Optional" section
           dump_build_config=False,   # Dump the JSON Gooey uses to configure itself
           load_build_config=None,    # Loads a JSON Gooey-generated configuration
           monospace_display=False)   # Uses a mono-spaced font in the output screen
    )
    def main():
      parser = ArgumentParser(...)
      # rest of code
            
See: [How does it Work](#how-does-it-work) section for details on each option.

Gooey will do its best to choose sensible widget defaults to display in the GUI. However, if more fine tuning is desired, you can use the drop-in replacement `GooeyParser` in place of `ArgumentParser`. This lets you control which widget displays in the GUI. See: [GooeyParser](#gooeyparser)

    from gooey import Gooey, GooeyParser

    @Gooey
    def main():
      parser = GooeyParser(description="My Cool GUI Program!") 
      parser.add_argument('Filename', widget="FileChooser")
      parser.add_argument('Date', widget="DateChooser")
      ...

### Examples

Gooey downloaded and installed? Great! Wanna see it in action? Head over the the [Examples Repository](https://github.com/chriskiehl/GooeyExamples) to download a few ready-to-go example scripts. They'll give you a quick tour of all Gooey's various layouts, widgets, and features. 

[Direct Download](https://github.com/chriskiehl/GooeyExamples/archive/master.zip)


    
What is it? 
-----------  

Gooey converts your Console Applications into end-user-friendly GUI applications. It lets you focus on building robust, configurable programs in a familiar way, all without having to worry about how it will be presented to and interacted with by your average user. 

Why?
---  

Because as much as we love the command prompt, the rest of the world looks at it like an ugly relic from the early '80s. On top of that, more often than not programs need to do more than just one thing, and that means giving options, which previously meant either building a GUI, or trying to explain how to supply arguments to a Console Application. Gooey was made to (hopefully) solve those problems. It makes programs easy to use, and pretty to look at! 

Who is this for?
----------------  

If you're building utilities for yourself, other programmers, or something which produces a result that you want to capture and pipe over to another console application (e.g. *nix philosophy utils), Gooey probably isn't the tool for you. However, if you're building 'run and done,' around-the-office-style scripts, things that shovel bits from point A to point B, or simply something that's targeted at a non-programmer, Gooey is the perfect tool for the job. It lets you build as complex of an application as your heart desires all while getting the GUI side for free. 


How does it work?
-----------------

Gooey is attached to your code via a simple decorator on whichever method has your `argparse` declarations.

    @Gooey
    def my_run_func():
      parser = ArgumentParser(...)
      # rest of code

At run-time, it parses your Python script for all references to `ArgumentParser`. (The older `optparse` is currently not supported.) These references are then extracted, assigned a `component type` based on the `'action'` they provide, and finally used to assemble the GUI.  

#### Mappings: 

Gooey does its best to choose sensible defaults based on the options it finds. Currently, `ArgumentParser._actions` are mapped to the following `WX` components. 

| Parser Action    | Widget    | Example |
|:----------------------|-----------|------|
| store  |  TextCtrl |  <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/f54e9f5e-07c5-11e5-86e5-82f011c538cf.png"/>|
| store_const | CheckBox |<img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/f538c850-07c5-11e5-8cbe-864badfa54a9.png"/>|
| store_true | CheckBox | <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/f538c850-07c5-11e5-8cbe-864badfa54a9.png"/>|
| store_False | CheckBox|  <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/f538c850-07c5-11e5-8cbe-864badfa54a9.png"/>   |
| version | CheckBox|  <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/f538c850-07c5-11e5-8cbe-864badfa54a9.png"/>   |
| append | TextCtrl |  <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/f54e9f5e-07c5-11e5-86e5-82f011c538cf.png"/>  | 
| count | DropDown &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/f53ccbe4-07c5-11e5-80e5-510e2aa22922.png"/> | 
| Mutually Exclusive Group | RadioGroup | <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/f553feb8-07c5-11e5-9d5b-eaa4772075a9.png"/>
|choice &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|        DropDown | <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/f54e4da6-07c5-11e5-9e66-d8e6d7f18ac6.png"/> |

### GooeyParser

If the above defaults aren't cutting it, you can control the exact widget type by using the drop-in `ArgumentParser` replacement `GooeyParser`. This gives you the additional keyword argument `widget`, to which you can supply the name of the component you want to display. Best part? You don't have to change any of your `argparse` code to use it. Drop it in, and you're good to go. 

**Example:**

    from argparse import ArgumentParser
    ....
    
    def main(): 
        parser = ArgumentParser(description="My Cool Gooey App!")
        parser.add_argument('filename', help="name of the file to process") 

Given then above, Gooey would select a normal `TextField` as the widget type like this: 
<p align="center">
    <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/f5393e20-07c5-11e5-88e9-c153fc3ecfaa.PNG">
</p>

However, by dropping in `GooeyParser` and supplying a `widget` name, you can display a much more user friendly `FileChooser`


    from gooey import GooeyParser
    ....
    
    def main(): 
        parser = GooeyParser(description="My Cool Gooey App!")
        parser.add_argument('filename', help="name of the file to process", widget='FileChooser') 
        
<p align="center"><img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/f53ae23e-07c5-11e5-8757-c8aa6f3013b5.PNG"></p>

**Custom Widgets:**

| Widget         |           Example            | 
|----------------|------------------------------| 
| DirChooser, FileChooser, MultiFileChooser, FileSaver, MultiFileSaver   | <p align="center"><img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/f5483b28-07c5-11e5-9d01-1935635fc22d.gif" width="400"></p> | 
| DateChooser/TimeChooser   &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;| <p align="center"><img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/f544756a-07c5-11e5-86d6-862ac146ad35.gif" width="400"></p> <p>Please note that for both of these widgets the values passed to the application will always be in [ISO format](https://www.wxpython.org/Phoenix/docs/html/wx.DateTime.html#wx.DateTime.FormatISOTime) while localized values may appear in some parts of the GUI depending on end-user settings.</p> |
| PasswordField | <p align="center"><img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/28953722-eae72cca-788e-11e7-8fa1-9a1ef332a053.png" width="400"></p> |
| Listbox | ![image](https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/31590191-fadd06f2-b1c0-11e7-9a49-7cbf0c6d33d1.png) |
| BlockCheckbox | ![image](https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/46922288-9296f200-cfbb-11e8-8b0d-ddde08064247.png) <br/> The default InlineCheck box can look less than ideal if a large help text block is present. `BlockCheckbox` moves the text block to the normal position and provides a short-form `block_label` for display next to the control. Use `gooey_options.checkbox_label` to control the label text | 
|  ColourChooser   &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;| <p align="center"><img src="https://user-images.githubusercontent.com/21027844/72672451-0752aa80-3a0f-11ea-86ed-8303bd3e54b5.gif" width="400"></p> |
| FilterableDropdown | <p align="center"><img src="https://raw.githubusercontent.com/chriskiehl/GooeyImages/images/readme-images/filterable-dropdown.gif" width="400"></p> |
| IntegerField | <p align="center"><img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/integer-field.PNG" width="400"></p> |
| DecimalField | <p align="center"><img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/decimal-field.PNG" width="400"></p> |
| Slider | <p align="center"><img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/slider.PNG" width="400"></p> |



 
  
Internationalization
-------------------- 

<img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/f52e9f1a-07c5-11e5-8f31-36a8fc14ac02.jpg" align="right" />

Gooey is international ready and easily ported to your host language. Languages are controlled via an argument to the `Gooey` decorator. 

    @Gooey(language='russian')
    def main(): 
        ... 

All program text is stored externally in `json` files. So adding new language support is as easy as pasting a few key/value pairs in the `gooey/languages/` directory. 

Thanks to some awesome [contributors](https://github.com/chriskiehl/Gooey/graphs/contributors), Gooey currently comes pre-stocked with over 18 different translations! 

Want to add another one? Submit a [pull request!](https://github.com/chriskiehl/Gooey/compare)


-------------------------------------------    



Global Configuration 
--------------------

Just about everything in Gooey's overall look and feel can be customized by passing arguments to the decorator. 

| Parameter | Summary | 
|-----------|---------|
| encoding | Text encoding to use when displaying characters (default: 'utf-8') | 
| use_legacy_titles | Rewrites the default argparse group name from "Positional" to "Required". This is primarily for retaining backward compatibility with previous versions of Gooey (which had poor support/awareness of groups and did its own naive bucketing of arguments). |
| advanced | Toggles whether to show the 'full' configuration screen, or a simplified version |
| auto_start | Skips the configuration all together and runs the program immediately |
| language | Tells Gooey which language set to load from the `gooey/languages` directory.|
| target | Tells Gooey how to re-invoke itself. By default Gooey will find python, but this allows you to specify the program (and arguments if supplied).|
| suppress_gooey_flag | Should be set when using a custom `target`. Prevent Gooey from injecting additional CLI params |
|program_name | The name displayed in the title bar of the GUI window. If not supplied, the title defaults to the script name pulled from `sys.argv[0]`. |
| program_description | Sets the text displayed in the top panel of the `Settings` screen. Defaults to the description pulled from `ArgumentParser`. |
| default_size | Initial size of the window | 
| fullscreen | start Gooey in fullscreen mode |
| required_cols | Controls how many columns are in the Required Arguments section <br> :warning: **Deprecation notice:** See [Layout Customization](https://github.com/chriskiehl/Gooey#layout-customization) for modern layout controls|
| optional_cols | Controls how many columns are in the Optional Arguments section <br> :warning: **Deprecation notice:** See [Layout Customization](https://github.com/chriskiehl/Gooey#layout-customization) for modern layout controls|
| dump_build_config | Saves a `json` copy of its build configuration on disk for reuse/editing | 
| load_build_config | Loads a `json` copy of its build configuration from disk | 
| monospace_display | Uses a mono-spaced font in the output screen <br> :warning: **Deprecation notice:** See [Layout Customization](https://github.com/chriskiehl/Gooey#layout-customization) for modern font configuration| 
| image_dir | Path to the directory in which Gooey should look for custom images/icons |
| language_dir | Path to the directory in which Gooey should look for custom languages files |
| disable_stop_button | Disable the `Stop` button when running |
| show_stop_warning | Displays a warning modal before allowing the user to force termination of your program |
| force_stop_is_error | Toggles whether an early termination by the shows the success or error screen |
| show_success_modal | Toggles whether or not to show a summary modal after a successful run |
| show_failure_modal | Toggles whether or not to show a summary modal on failure |
| show_restart_button | Toggles whether or not to show the restart button at the end of execution |
| run_validators | Controls whether or not to have Gooey perform validation before calling your program |
| poll_external_updates | (Experimental!) When True, Gooey will call your code with a `gooey-seed-ui` CLI argument and use the response to fill out dynamic values in the UI (See: [Using Dynamic Values](#using-dynamic-values))|
| use_cmd_args | Substitute any command line arguments provided at run time for the default values specified in the Gooey configuration |
| return_to_config | When True, Gooey will return to the configuration settings window upon successful run |
| progress_regex | A text regex used to pattern match runtime progress information. See: [Showing Progress](#showing-progress) for a detailed how-to | 
| progress_expr | A python expression applied to any matches found via the `progress_regex`. See: [Showing Progress](#showing-progress) for a detailed how-to |
| hide_progress_msg | Option to hide textual progress updates which match the `progress_regex`. See: [Showing Progress](#showing-progress) for a detailed how-to |
| disable_progress_bar_animation | Disable the progress bar |
| timing_options | This contains the options for displaying time remaining and elapsed time, to be used with `progress_regex` and `progress_expr`. [Elapsed / Remaining Time](#elapsed--remaining-time). Contained as a dictionary with the options `show_time_remaining` and `hide_time_remaining_on_complete`. Eg: `timing_options={'show_time_remaining':True,'hide_time_remaining_on_complete':True}` |
| show_time_remaining | Disable the time remaining text see [Elapsed / Remaining Time](#elapsed--remaining-time) |
| hide_time_remaining_on_complete | Hide time remaining on complete screen see [Elapsed / Remaining Time](#elapsed--remaining-time) |
| requires_shell | Controls whether or not the `shell` argument is used when invoking your program. [More info here](https://stackoverflow.com/questions/3172470/actual-meaning-of-shell-true-in-subprocess#3172488) |
| shutdown_signal | Specifies the `signal` to send to the child process when the `stop` button is pressed. See [Gracefully Stopping](https://github.com/chriskiehl/Gooey/tree/master/docs) in the docs for more info. | 
| navigation | Sets the "navigation" style of Gooey's top level window. <br>Options: <table> <thead> <tr><th>TABBED</th><th>SIDEBAR</th></tr></thead> <tbody> <tr> <td><img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/34464826-2a946ba2-ee47-11e7-92a4-4afeb49dc9ca.png" width="200" height="auto"></td><td><img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/34464847-9918fbb0-ee47-11e7-8d5f-0d42631c2bc0.png" width="200" height="auto"></td></tr></tbody></table>|
| sidebar_title | <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/34472159-1bfedbd0-ef10-11e7-8bc3-b6d69febb8c3.png" width="250" height="auto" align="right"> Controls the heading title above the SideBar's navigation pane. Defaults to: "Actions" |
| show_sidebar | Show/Hide the sidebar in when navigation mode == `SIDEBAR` |
| body_bg_color | HEX value of the main Gooey window |
| header_bg_color | HEX value of the header background | 
| header_height | height in pixels of the header | 
| header_show_title | Show/Hide the header title | 
| header_show_subtitle | Show/Hide the header subtitle | 
| footer_bg_color | HEX value of the Footer background | 
| sidebar_bg_color | HEX value of the Sidebar's background | 
| terminal_panel_color | HEX value of the terminal's panel | 
| terminal_font_color | HEX value of the font displayed in Gooey's terminal | 
| terminal_font_family | Name of the Font Family to use in the terminal | 
| terminal_font_weight | Weight of the font (`constants.FONTWEIGHT_NORMAL`, `constants.FONTWEIGHT_XXX`) | 
| terminal_font_size | Point size of the font displayed in the terminal | 
| error_color | HEX value of the text displayed when a validation error occurs |
| richtext_controls | Switch on/off the console support for terminal control sequences (limited support for font weight and color). Defaults to : False. See [docs](https://github.com/chriskiehl/Gooey/tree/master/docs) for additional details |
| menus | Show custom menu groups and items (see: [Menus](#menus) |
| clear_before_run | When true, previous output will be cleared from the terminal when running program again |



Layout Customization
--------------------

You can achieve fairly flexible layouts with Gooey by using a few simple customizations. 

At the highest level, you have several overall layout options controllable via various arguments to the Gooey decorator.


| `show_sidebar=True` | `show_sidebar=False` | `navigation='TABBED'` |  `tabbed_groups=True` |
|---------------------|----------------------|----------------------|------------------------|
|<img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/34464847-9918fbb0-ee47-11e7-8d5f-0d42631c2bc0.png" width="400"> |<img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/35487799-762aa308-0434-11e8-8eb3-1e9fab2d13ae.png" width="400"> |<img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/34464835-5ba9b0e4-ee47-11e7-9561-55e3647c2165.png" width="400"> |<img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/34464826-2a946ba2-ee47-11e7-92a4-4afeb49dc9ca.png" width="400"> |


**Grouping Inputs**

By default, if you're using Argparse with Gooey, your inputs will be split into two buckets: `positional` and `optional`. However, these aren't always the most descriptive groups to present to your user. You can arbitrarily bucket inputs into logic groups and customize the layout of each. 

With `argparse` this is done via `add_argument_group()`

<img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/35487956-a4c9915e-0436-11e8-8a11-fd21528aedf0.png" align="right" width="410">

```
parser = ArgumentParser()
search_group = parser.add_argument_group(
    "Search Options", 
    "Customize the search options"
)
```

You can add arguments to the group as normal 

```
search_group.add_argument(
    '--query', 
    help='Base search string'
) 
```

Which will display them as part of the group within the UI. 




Run Modes
---------

Gooey has a handful of presentation modes so you can tailor its layout to your content type and user's level or experience. 




### Advanced 




The default view is the "full" or "advanced" configuration screen. It has two different layouts depending on the type of command line interface it's wrapping. For most applications, the flat layout will be the one to go with, as its layout matches best to the familiar CLI schema of a primary command followed by many options (e.g. Curl, FFMPEG). 

On the other side is the Column Layout. This one is best suited for CLIs that have multiple paths or are made up of multiple little tools each with their own arguments and options (think: git). It displays the primary paths along the left column, and their corresponding arguments in the right. This is a great way to package a lot of varied functionality into a single app. 

<p align="center">
<img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/f06a36cc-08ad-11e5-843e-9322df96d4d6.png">
</p>

Both views present each action in the `Argument Parser` as a unique GUI component. It makes it ideal for presenting the program to users which are unfamiliar with command line options and/or Console Programs in general. Help messages are displayed along side each component to make it as clear as possible which each widget does.

**Setting the layout style:**

Currently, the layouts can't be explicitly specified via a parameter (on the TODO!). The layouts are built depending on whether or not there are `subparsers` used in your code base. So, if you want to trigger the `Column Layout`, you'll need to add a `subparser` to your `argparse` code. 

It can be toggled via the `advanced` parameter in the `Gooey` decorator. 


    @gooey(advanced=True)
    def main():
        # rest of code   
        


--------------------------------------------  



### Basic  

The basic view is best for times when the user is familiar with Console Applications, but you still want to present something a little more polished than a simple terminal. The basic display is accessed by setting the `advanced` parameter in the `gooey` decorator to `False`. 

    @gooey(advanced=False)
    def main():
        # rest of code  

<p align="center">
    <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/f53a4306-07c5-11e5-8e63-b510d6db9953.png">
</p>


----------------------------------------------  

### No Config

No Config pretty much does what you'd expect: it doesn't show a configuration screen. It hops right to the `display` section and begins execution of the host program. This is the one for improving the appearance of little one-off scripts. 

To use this mode, set `auto_start=True` in the Gooey decorator. 

```python
@Gooey(auto_start=True) 
def main (): 
    ... 
```

<p align="center">
    <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/f54fe6f2-07c5-11e5-92e4-f72a2ae12862.png">
</p>


--------------------------------------


### Menus 


![image](https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/47250909-74782a00-d3df-11e8-88ac-182d06c4435a.png)

>Added 1.0.2

You can add a Menu Bar to the top of Gooey with customized menu groups and items.

Menus are specified on the main `@Gooey` decorator as a list of maps. 

```
@Gooey(menu=[{}, {}, ...])
```

Each map is made up of two key/value pairs 

1. `name` - the name for this menu group
2. `items` - the individual menu items within this group 

You can have as many menu groups as you want. They're passed as a list to the `menu` argument on the `@Gooey` decorator.

```
@Gooey(menu=[{'name': 'File', 'items: []},
             {'name': 'Tools', 'items': []},
             {'name': 'Help', 'items': []}])
```

Individual menu items in a group are also just maps of key / value pairs. Their exact key set varies based on their `type`, but two keys will always be present: 

* `type` - this controls the behavior that will be attached to the menu item as well as the keys it needs specified
* `menuTitle` - the name for this MenuItem  


Currently, three types of menu options are supported: 

 * AboutDialog 
 * MessageDialog
 * Link
 * HtmlDialog
 

<img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/47251026-9ffc1400-d3e1-11e8-9095-982a6367561b.png" width="400" height="auto" align="right" />

**About Dialog** is your run-of-the-mill About Dialog. It displays program information such as name, version, and license info in a standard native AboutBox.

Schema 

 * `name` - (_optional_) 
 * `description` - (_optional_) 
 * `version` - (_optional_)  
 * `copyright` - (_optional_) 
 * `license` - (_optional_)
 * `website` - (_optional_)
 * `developer` - (_optional_)

Example: 

```
{
    'type': 'AboutDialog',
    'menuTitle': 'About',
    'name': 'Gooey Layout Demo',
    'description': 'An example of Gooey\'s layout flexibility',
    'version': '1.2.1',
    'copyright': '2018',
    'website': 'https://github.com/chriskiehl/Gooey',
    'developer': 'http://chriskiehl.com/',
    'license': 'MIT'
}
```

<img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/47250925-bbfeb600-d3df-11e8-88a8-5ba838e9466d.png" width="400" height="auto" align="right" />

**MessageDialog** is a generic informational dialog box. You can display anything from small alerts, to long-form informational text to the user.

Schema: 

 * `message` - (_required_) the text to display in the body of the modal 
 * `caption` - (_optional_) the caption in the title bar of the modal    

Example: 

```python
{
    'type': 'MessageDialog',
    'menuTitle': 'Information',
    'message': 'Hey, here is some cool info for ya!',
    'caption': 'Stuff you should know'
}
```

**Link** is for sending the user to an external website. This will spawn their default browser at the URL you specify. 

Schema: 

 * `url` - (_required_) - the fully qualified URL to visit

Example:

```python
{
    'type': 'Link',
    'menuTitle': 'Visit Out Site',
    'url': 'http://www.example.com'
}
```


<img src="https://github.com/chriskiehl/GooeyImages/raw/images/docs/menus/html-dialog.PNG" width="400" height="auto" align="right" />

**HtmlDialog** gives you full control over what's displayed in the message dialog (bonus: people can copy/paste text from this one!). 



Schema: 

 * `caption` - (_optional_) the caption in the title bar of the modal   
 * `html` - (_required_) the html you want displayed in the dialog. Note: only a small subset of HTML is supported. [See the WX docs for more info](https://wxpython.org/Phoenix/docs/html/html_overview.html). 

Example: 

```python
{
    'type': 'HtmlDialog',
    'menuTitle': 'Fancy Dialog!',
    'caption': 'Demo of the HtmlDialog',
    'html': '''
    <body bgcolor="white">
        <img src=/path/to/your/image.png" /> 
        <h1>Hello world!</h1> 
        <p><font color="red">Lorem ipsum dolor sit amet, consectetur</font></p>
    </body>
    '''
}

```

**A full example:**

Two menu groups ("File" and "Help") with four menu items between them. 

```python
@Gooey(
    program_name='Advanced Layout Groups',
    menu=[{
        'name': 'File',
        'items': [{
                'type': 'AboutDialog',
                'menuTitle': 'About',
                'name': 'Gooey Layout Demo',
                'description': 'An example of Gooey\'s layout flexibility',
                'version': '1.2.1',
                'copyright': '2018',
                'website': 'https://github.com/chriskiehl/Gooey',
                'developer': 'http://chriskiehl.com/',
                'license': 'MIT'
            }, {
                'type': 'MessageDialog',
                'menuTitle': 'Information',
                'caption': 'My Message',
                'message': 'I am demoing an informational dialog!'
            }, {
                'type': 'Link',
                'menuTitle': 'Visit Our Site',
                'url': 'https://github.com/chriskiehl/Gooey'
            }]
        },{
        'name': 'Help',
        'items': [{
            'type': 'Link',
            'menuTitle': 'Documentation',
            'url': 'https://www.readthedocs.com/foo'
        }]
    }]
)
```


---------------------------------------  


### Dynamic Validation 


<img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/34464861-0e82c214-ee48-11e7-8f4a-a8e00721efef.png" width="400" height="auto" align="right" />

>:warning: 
>Note! This functionality is experimental and likely to be unstable. Its API may be changed or removed altogether. Feedback/thoughts on this feature is welcome and encouraged!
 
>:warning: 
>See [Release Notes]() for guidance on upgrading from 1.0.8 to 1.2.0 


Before passing the user's inputs to your program, Gooey can optionally run a special pre-flight validation to check that all arguments pass your specified validations.  

**How does it work?**   

Gooey piggy backs on the `type` parameter available to most Argparse Argument types. 

```python
parser.add_argument('--some-number', type=int)
parser.add_argument('--some-number', type=float)
```

In addition to simple builtins like `int` and `float`, you can supply your own function to the `type` parameter to vet the incoming values. 

```python
def must_be_exactly_ten(value): 
    number = int(value) 
    if number == 10:
        return number
    else: 
        raise TypeError("Hey! you need to provide exactly the number 10!")
        
        
def main(): 
    parser = ArgumentParser()
    parser.add_argument('--ten', type=must_be_exactly_ten)
```

**How to enable the pre-flight validation**

By default, Gooey won't run the validation. Why? This feature is fairly experimental and does a lot of intense Monkey Patching behind the scenes. As such, it's currently opt-in. 

You enable to validation by telling Gooey you'd like to subscribe to the `VALIDATE_FORM` event. 

```python
from gooey import Gooey, Events 

@Gooey(use_events=[Events.VALIDATE_FORM])
def main(): 
    ... 
```


<img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/dynamic-validation-1-2-0.JPG" />

Now, when you run Gooey, before it invokes your main program, it'll send a separate pre-validation check and record any issues raised from your `type` functions.  


**Full Code Example**

```
from gooey import Gooey, Events
from argparse import ArgumentParser

def must_be_exactly_ten(value):
    number = int(value)
    if number == 10:
        return number
    else:
        raise TypeError("Hey! you need to provide exactly the number 10!")

@Gooey(program_name='Validation Example', use_events=[Events.VALIDATE_FORM])
def main():
    parser = ArgumentParser(description="Checkout this validation!")
    parser.add_argument('--ten', metavar='This field should be 10', type=must_be_exactly_ten)
    args = parser.parse_args()
    print(args)
```




---------------------------------------
  

## Lifecycle Events and UI control

>:warning: 
>Note! This functionality is experimental. Its API may be changed or removed altogether. Feedback on this feature is welcome and encouraged! 

As of 1.2.0, Gooey now exposes coarse grain lifecycle hooks to your program. This means you can now take additional follow-up actions in response to successful runs or failures and even control the current state of the UI itself! 

Currently, two primary hooks are exposed: 

* `on_success`
* `on_error`

These fire exactly when you'd expect: after your process has completed. 


**Anatomy of an lifecycle handler**:

Both `on_success` and `on_error` have the same type signature. 

```python
from typing import Mapping, Any, Optional
from gooey.types import PublicGooeyState  

def on_success(args: Mapping[str, Any], state: PublicGooeyState) -> Optional[PublicGooeyState]:
    """
    You can do anything you want in the handler including 
    returning an updated UI state for your next run!   
    """ 
    return state
    
def on_error(args: Mapping[str, Any], state: PublicGooeyState) -> Optional[PublicGooeyState]:
    """
    You can do anything you want in the handler including 
    returning an updated UI state for your next run!   
    """ 
    return state    
```

* **args** This is the parsed Argparse object (e.g. the output of `parse_args()`). This will be a mapping of the user's arguments as existed when your program was invoked.
* **state** This is the current state of Gooey's UI. If your program uses subparsers, this currently just lists the state of the active parser/form. Whatever updated version of this state you return will be reflected in the UI!    


**Attaching the handlers:**

Handlers are attached when instantiating the `GooeyParser`.

```python
parser = GooeyParser(
    on_success=my_success_handler,
    on_failure=my_failure_handler)
``` 


**Subscribing to the lifecycle events**

Just like [Validation](#dynamic-validation), these lifecycle events are opt-in. Pass the event you'd like to subscribe to into the `use_events` Gooey decorator argument. 

```python
from gooey import Gooey, Events 

@Gooey(use_events=[Events.ON_SUCCESS, Events.ON_ERROR])
def main(): 
    ... 
```



-------------------------------------

## Showing Progress

<img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/45590349-55bbda80-b8eb-11e8-9aed-b4fe377756ac.png" align="right" width="420"/>

Giving visual progress feedback with Gooey is easy! If you're already displaying textual progress updates, you can tell Gooey to hook into that existing output in order to power its Progress Bar. 

For simple cases, output strings which resolve to a numeric representation of the completion percentage (e.g. `Progress 83%`) can be pattern matched and turned into a progress bar status with a simple regular expression (e.g. `@Gooey(progress_regex=r"^progress: (\d+)%$")`). 

For more complicated outputs, you can pass in a custom evaluation expression (`progress_expr`) to transform regular expression matches as needed. 

Output strings which satisfy the regular expression can be hidden from the console via the `hide_progress_msg` parameter (e.g. `@Gooey(progress_regex=r"^progress: (\d+)%$", hide_progress_msg=True)`.

**Regex and Processing Expression**

```python
@Gooey(progress_regex=r"^progress: (?P<current>\d+)/(?P<total>\d+)$",
       progress_expr="current / total * 100")
```

**Program Output:**

```
progress: 1/100
progress: 2/100
progress: 3/100
...
```

There are lots of options for telling Gooey about progress as your program is running. Checkout the [Gooey Examples](https://github.com/chriskiehl/GooeyExamples) repository for more detailed usage and examples! 

### Elapsed / Remaining Time

Gooey also supports tracking elapsed / remaining time when progress is used! This is done in a similar manner to that of the project [tqdm](https://github.com/tqdm/tqdm). This can be enabled with `timing_options`, the `timing_options` argument takes in a dictionary with the keys `show_time_remaining` and `hide_time_remaining_on_complete`. The default behavior is True for `show_time_remaining` and False for `hide_time_remaining_on_complete`. This will only work when `progress_regex` and `progress_expr` are used.

```python
@Gooey(progress_regex=r"^progress: (?P<current>\d+)/(?P<total>\d+)$",
       progress_expr="current / total * 100",
       timing_options = {
        'show_time_remaining':True,
        'hide_time_remaining_on_complete':True,
    })
```

--------------------------------------


## Customizing Icons

Gooey comes with a set of six default icons. These can be overridden with your own custom images/icons by telling Gooey to search additional directories when initializing. This is done via the `image_dir` argument to the `Gooey` decorator. 

    @Gooey(program_name='Custom icon demo', image_dir='/path/to/my/image/directory')
    def main():
        # rest of program
        
Images are discovered by Gooey based on their _filenames_. So, for example, in order to supply a custom configuration icon, simply place an image with the filename `config_icon.png` in your images directory. These are the filenames which can be overridden:

* program_icon.png
* success_icon.png
* running_icon.png
* loading_icon.gif
* config_icon.png
* error_icon.png


## Packaging

Thanks to some [awesome contributors](https://github.com/chriskiehl/Gooey/issues/58), packaging Gooey as an executable is super easy. 

The tl;dr [pyinstaller](https://github.com/pyinstaller/pyinstaller) version is to drop this [build.spec](https://raw.githubusercontent.com/chriskiehl/Gooey/master/docs/packaging/build-win.spec) into the root directory of your application. Edit its contents so that the `APPPNAME` and `name` are relevant to your project and the `pathex` value points to your applications root, then execute `pyinstaller -F --windowed build.spec` to bundle your app into a ready-to-go executable. 

Detailed step by step instructions can be found [here](https://github.com/chriskiehl/Gooey/blob/master/docs/packaging/Packaging-Gooey.md). 


Screenshots
------------  

| Flat Layout | Column Layout |Success Screen | Error Screen | Warning Dialog |
|-------------|---------------|---------------|--------------|----------------|
| <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/4414e54e-0965-11e5-964b-f717a7adaac6.jpg"> | <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/4411b824-0965-11e5-905a-3a2b5df0efb3.jpg"> | <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/44165442-0965-11e5-8edf-b8305353285f.jpg"> | <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/4410dcce-0965-11e5-8243-c1d832c05887.jpg"> | <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/4415432c-0965-11e5-9190-17f55460faf3.jpg"> | 

| Custom Groups | Tabbed Groups | Tabbed Navigation | Sidebar Navigation | Input Validation |
|-------------|---------------|---------------|--------------|----------------|
| <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/34464824-c044d57a-ee46-11e7-9c35-6e701a7c579a.png"> | <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/34464826-2a946ba2-ee47-11e7-92a4-4afeb49dc9ca.png"> | <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/34464835-5ba9b0e4-ee47-11e7-9561-55e3647c2165.png"> | <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/34464847-9918fbb0-ee47-11e7-8d5f-0d42631c2bc0.png"> | <img src="https://github.com/chriskiehl/GooeyImages/raw/images/readme-images/34464861-0e82c214-ee48-11e7-8f4a-a8e00721efef.png"> | 





----------------------------------------------  






Wanna help?
-----------  

Code, translation, documentation, or graphics? All pull requests are welcome. Just make sure to checkout [the contributing guidelines](https://github.com/chriskiehl/Gooey/blob/master/CONTRIBUTING.md) first.




