from __future__ import annotations

import math
import sys
from functools import partial

import click

try:
    from PyQt5 import QtCore, QtGui, QtWidgets
except:
    pass
else:
    MAX_ROWS = 21

    try:
        import qdarkstyle

        _has_qdarkstyle = True
    except ModuleNotFoundError:
        _has_qdarkstyle = False

    _GTypeRole = QtCore.Qt.UserRole
    _missing = object()

    class GStyle:
        _base_style = """
            ._OptionLabel {
                font-size: 16px;
                font: bold;
                font-family: arial;
                }
            ._HelpLabel {
                font-family: arial;
                font-size: 14px;
                }
            ._InputComboBox{
                font-size: 16px;
                }
            ._InputLineEdit{
                font-size: 16px;
                }
            ._InputCheckBox{
                font-size: 16px;
                }
            ._InputSpinBox{
                font-size: 16px;
                }
            ._InputTabWidget{
                font: bold;
                font-size: 16px;
                }
            .GListView{
                font-size: 16px;
                }
            QToolTip{
                font-family: arial;
                }
            """

        def __init__(self, style=""):
            if not GStyle.check_style(style):
                self.text_color = "black"
                self.placehoder_color = "#898b8d"
                self.stylesheet = (
                    GStyle._base_style
                    + """
                        ._Spliter{
                            border: 1px inset gray;
                            }
                        """
                )
            elif style == "qdarkstyle":
                self.text_color = "#eff0f1"
                self.placehoder_color = "#898b8d"
                self.stylesheet = (
                    qdarkstyle.load_stylesheet_pyqt5()
                    + GStyle._base_style
                    + """
                        .GListView{
                            padding: 5px;
                            }
                        ._Spliter{
                            border: 5px solid gray;
                            }
                        """
                )

        @staticmethod
        def check_style(style):
            if style == "qdarkstyle":
                return _has_qdarkstyle
            return False

    _gstyle = GStyle()

    class GListView(QtWidgets.QListView):
        def __init__(self, opt):
            super().__init__()
            self.nargs = opt.nargs
            self.model = GItemModel(
                opt.nargs, parent=self, opt_type=opt.type, default=opt.default
            )
            self.setModel(self.model)
            self.delegate = GEditDelegate(self)
            self.setItemDelegate(self.delegate)
            self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
            if self.nargs == -1:
                self.keyPressEvent = self.key_press
                self.setToolTip(
                    "'a': add a new item blow the selected one\n"
                    "'d': delete the selected item"
                )

        def key_press(self, e):
            if self.nargs == -1:
                if e.key() == QtCore.Qt.Key_A:
                    if len(self.selectedIndexes()) == 0:
                        self.model.insertRow(0)
                    else:
                        for i in self.selectedIndexes():
                            self.model.insertRow(i.row() + 1)
                if e.key() == QtCore.Qt.Key_D:
                    si = self.selectedIndexes()
                    for i in si:
                        self.model.removeRow(i.row())
            super().keyPressEvent(e)

    class GItemModel(QtGui.QStandardItemModel):
        def __init__(self, n, parent=None, opt_type=click.STRING, default=None):
            super(QtGui.QStandardItemModel, self).__init__(0, 1, parent)
            self.type = opt_type
            for row in range(n):
                if hasattr(default, "__len__"):
                    self.insertRow(row, default[row])
                else:
                    self.insertRow(row, default)

        def insertRow(self, idx, val=""):
            super().insertRow(idx)

            index = self.index(idx, 0, QtCore.QModelIndex())
            if val is None or val == "":
                self.setData(
                    index,
                    QtGui.QBrush(QtGui.QColor(_gstyle.placehoder_color)),
                    role=QtCore.Qt.ForegroundRole,
                )
            else:
                self.setData(index, val)

        def data(self, index, role=QtCore.Qt.DisplayRole):
            if role == QtCore.Qt.DisplayRole:
                dstr = QtGui.QStandardItemModel.data(self, index, role)
                if dstr == "" or dstr is None:
                    if isinstance(self.type, click.types.Tuple):
                        row = index.row()
                        if 0 <= row < len(self.type.types):
                            tp = self.type.types[row]
                            dstr = tp.name
                    else:
                        dstr = self.type.name
                    return dstr

            if role == _GTypeRole:
                tp = click.STRING
                if isinstance(self.type, click.types.Tuple):
                    row = index.row()
                    if 0 <= row < len(self.type.types):
                        tp = self.type.types[row]
                elif isinstance(self.type, click.types.ParamType):
                    tp = self.type
                return tp

            return QtGui.QStandardItemModel.data(self, index, role)

    class GEditDelegate(QtWidgets.QStyledItemDelegate):
        def createEditor(self, parent, option, index):
            tp = index.data(role=_GTypeRole)
            if isinstance(tp, click.Path):
                led = GLineEdit_path.from_option(tp, parent)
            else:
                led = QtWidgets.QLineEdit(parent)
            led.setPlaceholderText(tp.name)
            led.setValidator(select_type_validator(tp))
            return led

        def setEditorData(self, editor, index):
            item_var = index.data(role=QtCore.Qt.EditRole)
            if item_var is not None:
                editor.setText(str(item_var))

        def setModelData(self, editor, model, index):
            data_str = editor.text()
            if data_str == "" or data_str is None:
                model.setData(
                    index,
                    QtGui.QBrush(QtGui.QColor(_gstyle.placehoder_color)),
                    role=QtCore.Qt.ForegroundRole,
                )
            else:
                model.setData(
                    index,
                    QtGui.QBrush(QtGui.QColor(_gstyle.text_color)),
                    role=QtCore.Qt.ForegroundRole,
                )
            QtWidgets.QStyledItemDelegate.setModelData(self, editor, model, index)

    def generate_label(opt):
        show_name = getattr(opt, "show_name", _missing)
        show_name = opt.name if show_name is _missing else show_name
        param = _OptionLabel(show_name)
        param.setToolTip(getattr(opt, "help", None))
        return param

    class GStringLineEditor(click.types.StringParamType):
        def to_widget(self, opt, validator=None):
            value = _InputLineEdit()
            value.setPlaceholderText(self.name)
            if opt.default:
                value.setText(str(opt.default))
            if getattr(opt, "hide_input", False):
                value.setEchoMode(QtWidgets.QLineEdit.Password)
            value.setValidator(validator)

            def to_command():
                return [opt.opts[0], value.text()]

            return [generate_label(opt), value], to_command

    class GIntLineEditor(GStringLineEditor):
        def to_widget(self, opt):
            return GStringLineEditor.to_widget(
                self, opt, validator=QtGui.QIntValidator()
            )

    class GFloatLineEditor(GStringLineEditor):
        def to_widget(self, opt):
            return GStringLineEditor.to_widget(
                self, opt, validator=QtGui.QDoubleValidator()
            )

    class GFileDialog(QtWidgets.QFileDialog):
        def __init__(
            self, *args, exists=False, file_okay=True, dir_okay=True, **kwargs
        ):
            super().__init__(*args, **kwargs)
            self.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, True)
            self.setLabelText(QtWidgets.QFileDialog.Accept, "Select")
            if (exists, file_okay, dir_okay) == (True, True, False):
                self.setFileMode(QtWidgets.QFileDialog.ExistingFile)
            elif (exists, file_okay, dir_okay) == (False, True, False):
                self.setFileMode(QtWidgets.QFileDialog.AnyFile)
            elif (exists, file_okay, dir_okay) == (True, False, True):
                self.setFileMode(QtWidgets.QFileDialog.Directory)
            elif (exists, file_okay, dir_okay) == (False, False, True):
                self.setFileMode(QtWidgets.QFileDialog.Directory)
            elif exists == True:
                self.setFileMode(QtWidgets.QFileDialog.ExistingFile)
                self.accept = self.accept_all
            elif exists == False:
                self.setFileMode(QtWidgets.QFileDialog.AnyFile)
                self.accept = self.accept_all

        def accept_all(self):
            super().done(QtWidgets.QFileDialog.Accepted)

    class GLineEdit_path(QtWidgets.QLineEdit):
        def __init__(self, parent=None, exists=False, file_okay=True, dir_okay=True):
            super().__init__(parent)
            self.action = self.addAction(
                self.style().standardIcon(QtWidgets.QStyle.SP_DirIcon),
                QtWidgets.QLineEdit.TrailingPosition,
            )
            self.fdlg = GFileDialog(
                self,
                "Select File Dialog",
                "./",
                "*",
                exists=exists,
                file_okay=file_okay,
                dir_okay=dir_okay,
            )
            self.action.triggered.connect(self.run_dialog)

        def run_dialog(self):
            if self.fdlg.exec() == QtWidgets.QFileDialog.Accepted:
                self.setText(self.fdlg.selectedFiles()[0])

        @staticmethod
        def from_option(opt, parent=None):
            return GLineEdit_path(
                parent=parent,
                exists=opt.exists,
                file_okay=opt.file_okay,
                dir_okay=opt.dir_okay,
            )

    class GPathGLindEidt_path(click.types.Path):
        def to_widget(self, opt):
            value = GLineEdit_path(
                exists=self.exists, file_okay=self.file_okay, dir_okay=self.dir_okay
            )
            value.setPlaceholderText(self.name)
            if opt.default:
                value.setText(str(opt.default))

            def to_command():
                return [opt.opts[0], value.text()]

            return [generate_label(opt), value], to_command

    class _GLabeledSlider(QtWidgets.QSlider):
        def __init__(self, min, max, val):
            super().__init__(QtCore.Qt.Horizontal)
            self.min, self.max = min, max

            self.setMinimum(min)
            self.setMaximum(max)
            self.setValue(val)

            self.label = self.__init_label()

        def __init_label(self):
            l = max(
                math.ceil(math.log10(abs(x))) if x != 0 else 1
                for x in [self.min, self.max]
            )
            l += 1
            return QtWidgets.QLabel("0" * l)

    def argument_command(to_command):
        def tc():
            a = to_command()
            return a[1:]

        return tc

    class GSlider(QtWidgets.QHBoxLayout):
        def __init__(self, min=0, max=10, default=None, *args, **kwargs):
            super(QtWidgets.QHBoxLayout, self).__init__()

            self.min, self.max, self.default = min, max, default
            self.label = self.__init_label()
            self.slider = self.__init_slider()

            self.label.setText(str(self.default))

            self.addWidget(self.slider)
            self.addWidget(self.label)

        def value(self):
            return self.slider.value()

        def __init_slider(self):
            slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
            slider.setMinimum(self.min)
            slider.setMaximum(self.max)
            default_val = (self.min + self.max) // 2
            if isinstance(self.default, int):
                if self.min <= self.default <= self.max:
                    default_val = self.default
            self.default = default_val
            slider.setValue(default_val)
            slider.valueChanged.connect(lambda x: self.label.setText(str(x)))
            return slider

        def __init_label(self):
            l = max(
                math.ceil(math.log10(abs(x))) if x != 0 else 1
                for x in [self.min, self.max]
            )
            l += 1
            return QtWidgets.QLabel("0" * l)

    class GIntRangeGSlider(click.types.IntRange):
        def to_widget(self, opt):
            value = GSlider(min=self.min, max=self.max, default=opt.default)

            def to_command():
                return [opt.opts[0], str(value.value())]

            return [generate_label(opt), value], to_command

    class GIntRangeSlider(click.types.IntRange):
        def to_widget(self, opt):
            value = QtWidgets.QSlider(QtCore.Qt.Horizontal)
            value.setMinimum(self.min)
            value.setMaximum(self.max)

            default_val = (self.min + self.max) // 2
            if isinstance(opt.default, int):
                if self.min <= opt.default <= self.max:
                    default_val = opt.default
            value.setValue(default_val)

            def to_command():
                return [opt.opts[0], str(value.value())]

            return [generate_label(opt), value], to_command

    class GIntRangeLineEditor(click.types.IntRange):
        def to_widget(self, opt):
            value = QtWidgets.QLineEdit()
            # TODO: set validator

            def to_command():
                return [opt.opts[0], value.text()]

            return [generate_label(opt), value], to_command

    def bool_flag_option(opt):
        checkbox = _InputCheckBox(opt.name)
        if opt.default:
            checkbox.setCheckState(2)
        # set tip
        checkbox.setToolTip(opt.help)

        def to_command():
            if checkbox.checkState():
                return [opt.opts[0]]
            else:
                return opt.secondary_opts

        return [checkbox], to_command

    class GChoiceComboBox(click.types.Choice):
        def to_widget(self, opt):
            cb = _InputComboBox()
            cb.addItems(self.choices)

            def to_command():
                return [opt.opts[0], cb.currentText()]

            return [generate_label(opt), cb], to_command

    def count_option(opt):
        sb = _InputSpinBox()

        def to_command():
            return [opt.opts[0]] * int(sb.text())

        return [generate_label(opt), sb], to_command

    class GTupleGListView(click.Tuple):
        def to_widget(self, opt):
            view = GListView(opt)

            def to_command():
                _ = [opt.opts[0]]
                for idx in range(view.model.rowCount()):
                    _.append(view.model.item(idx).text())
                return _

            return [generate_label(opt), view], to_command

    def multi_text_arguement(opt):
        value = GListView(opt)

        def to_command():
            _ = []
            for idx in range(value.model.rowCount()):
                _.append(value.model.item(idx).text())
            # if opt.required and value.model.rowCount() == 0:
            # raise click.exceptions.BadParameter("Required")
            # print(opt.__dict__)
            return _

        # return [QtWidgets.QLabel(opt.name), value], to_command
        return [_OptionLabel(opt.name), value], to_command

    def select_type_validator(tp: click.types.ParamType) -> QtGui.QValidator:
        """select the right validator for `tp`"""

        if isinstance(tp, click.types.IntParamType):
            return QtGui.QIntValidator()
        elif isinstance(tp, click.types.FloatParamType):
            return QtGui.QDoubleValidator()

        return None

    def select_opt_validator(opt):
        """select the right validator for `opt`"""

        return select_type_validator(opt.type)

    def opt_to_widget(opt):
        if opt.nargs > 1:
            return GTupleGListView.to_widget(opt.type, opt)
        elif getattr(opt, "is_bool_flag", False):
            return bool_flag_option(opt)
        elif getattr(opt, "count", False):
            return count_option(opt)
        elif isinstance(opt.type, click.types.Choice):
            return GChoiceComboBox.to_widget(opt.type, opt)
        elif isinstance(opt.type, click.types.Path):
            return GPathGLindEidt_path.to_widget(opt.type, opt)
        elif isinstance(opt.type, click.types.IntRange):
            return GIntRangeGSlider.to_widget(opt.type, opt)
        elif isinstance(opt.type, click.types.IntParamType):
            return GIntLineEditor.to_widget(opt.type, opt)
        elif isinstance(opt.type, click.types.FloatParamType):
            return GFloatLineEditor.to_widget(opt.type, opt)
        else:
            return GStringLineEditor.to_widget(opt.type, opt)

    def _to_widget(opt):
        # customed widget

        if isinstance(opt.type, click.types.FuncParamType):
            if hasattr(opt.type.func, "to_widget"):
                return opt.type.func.to_widget()
        elif hasattr(opt.type, "to_widget"):
            return opt.type.to_widget()

        if isinstance(opt, click.core.Argument):
            if opt.nargs == 1:
                w, tc = opt_to_widget(opt)

                return w, argument_command(tc)
            elif opt.nargs > 1 or opt.nargs == -1:
                return multi_text_arguement(opt)
        else:
            return opt_to_widget(opt)

    def layout_append_opts(layout, opts):
        params_func = []
        widgets = []
        i = 0

        for i, para in enumerate(opts):
            widget, value_func = _to_widget(para)
            widgets.append(widget)
            params_func.append(value_func)

            for idx, w in enumerate(widget):
                if isinstance(w, QtWidgets.QLayout):
                    layout.addLayout(w, i, idx)
                else:
                    layout.addWidget(w, i, idx)

        return layout, params_func, widgets

    def generate_sysargv(cmd_list):
        argv_list = []

        for name, func_list in cmd_list:
            argv_list.append(name)

            for value_func in func_list:
                argv_list += value_func()

        return argv_list

    class _Spliter(QtWidgets.QFrame):
        def __init__(self, parent=None):
            super().__init__(parent=parent)
            self.setFrameShape(QtWidgets.QFrame.HLine)

    class _InputComboBox(QtWidgets.QComboBox):
        pass

    class _InputTabWidget(QtWidgets.QTabWidget):
        pass

    class _HelpLabel(QtWidgets.QLabel):
        pass

    class _OptionLabel(QtWidgets.QLabel):
        pass

    class _InputLineEdit(QtWidgets.QLineEdit):
        pass

    class _InputCheckBox(QtWidgets.QCheckBox):
        pass

    class _InputSpinBox(QtWidgets.QSpinBox):
        pass

    class CommandLayout(QtWidgets.QGridLayout):
        def __init__(self, func, run_exit, parent_layout=None):
            super().__init__()
            self.parent_layout = parent_layout
            self.func = func
            self.run_exit = run_exit

            if func.help:
                label = _HelpLabel(func.help)
                label.setWordWrap(True)
                self.addWidget(label, 0, 0, 1, 2)
                frame = _Spliter()
                self.addWidget(frame, 1, 0, 1, 4)
            self.params_func, self.widgets = self.append_opts(self.func.params)

        def add_sysargv(self):
            if hasattr(self.parent_layout, "add_sysargv"):
                self.parent_layout.add_sysargv()
            sys.argv += generate_sysargv([(self.func.name, self.params_func)])

        def append_opts(self, opts):
            params_func = []
            widgets = []

            def _nwidgets(x):
                return -len(_to_widget(x)[0])

            opts = sorted(opts, key=_nwidgets)
            row, col = 0, 0
            for para in opts:
                widget, value_func = _to_widget(para)
                widgets.append(widget)
                params_func.append(value_func)

                for idx, w in enumerate(widget):
                    if isinstance(w, QtWidgets.QLayout):
                        self.addLayout(w, row + 2, 2 * col + idx)
                    else:
                        self.addWidget(w, row + 2, 2 * col + idx)
                self.setRowStretch(row, 5)
                row += 1
                if row >= MAX_ROWS - 1:
                    row = 0
                    col += 1

            return params_func, widgets

        def generate_cmd_button(self, label, cmd_slot, tooltip=""):
            button = QtWidgets.QPushButton(label)
            button.setToolTip(tooltip)
            button.clicked.connect(self.clean_sysargv)
            button.clicked.connect(self.add_sysargv)
            button.clicked.connect(cmd_slot)

            return button

        def add_cmd_button(self, label, cmd_slot, pos=None):
            run_button = self.generate_cmd_button(label, cmd_slot)

            if pos is None:
                # pos = self.rowCount() + 1, 0
                pos = 0, 2
            self.addWidget(run_button, pos[0], pos[1])

        def add_cmd_buttons(self, args):
            # row = self.rowCount() + 1
            row = 0
            cmd_layout = QtWidgets.QGridLayout()
            cmd_layout.setHorizontalSpacing(20)

            for col, arg in enumerate(args):
                button = self.generate_cmd_button(**arg)
                cmd_layout.addWidget(button, col, 0)
            self.addLayout(cmd_layout, row, 2, 1, 2)

        @QtCore.pyqtSlot()
        def clean_sysargv(self):
            sys.argv = []

    class RunCommand(QtCore.QRunnable):
        def __init__(self, func, run_exit):
            super().__init__()
            self.func = func
            self.run_exit = run_exit

        @QtCore.pyqtSlot()
        def run(self):
            print(sys.argv)
            try:
                self.func(standalone_mode=self.run_exit)
            except click.exceptions.BadParameter as bpe:
                # warning message
                msg = QtWidgets.QMessageBox()
                msg.setIcon(QtWidgets.QMessageBox.Warning)
                msg.setText(bpe.format_message())
                msg.exec_()
            except Exception as bpe:
                msg = QtWidgets.QMessageBox()
                msg.setIcon(QtWidgets.QMessageBox.Warning)
                msg.setText(repr(bpe))
                msg.exec_()
            # if self.outputEdit is not None:
            # self.outputEdit.show()

    class GCommand(click.Command):
        def __init__(self, new_thread=True, *arg, **args):
            super().__init__(*arg, **args)
            self.new_thread = new_thread

    class GOption(click.Option):
        def __init__(self, *arg, show_name=_missing, **args):
            super().__init__(*arg, **args)
            self.show_name = show_name

    # def normalOutputWritten(t):
    # """Append text to the QTextEdit."""
    # Maybe QTextEdit.append() works as well, but this is how I do it:
    # cursor = text.textCursor()
    # cursor.movePosition(QtGui.QTextCursor.End)
    # cursor.insertText(t)
    # text.setTextCursor(cursor)
    # text.ensureCursorVisible()

    class GuiStream(QtCore.QObject):
        textWritten = QtCore.pyqtSignal(str)

        def flush(self):
            pass

        def write(self, text):
            self.textWritten.emit(str(text))

    class OutputEdit(QtWidgets.QTextEdit):
        def print(self, text):
            cursor = self.textCursor()
            cursor.movePosition(QtGui.QTextCursor.End)
            cursor.insertText(text)
            self.setTextCursor(cursor)
            self.ensureCursorVisible()

    class App(QtWidgets.QWidget):
        def __init__(
            self,
            func,
            run_exit,
            new_thread,
            output="gui",
            left=10,
            top=10,
            width=800,
            height=280,
        ):
            """
            Parameters
            ----------
            output : str
                'gui': [default] redirect screen output to the gui
                'term': do nothing
            """
            super().__init__()
            self.new_thread = new_thread
            self.title = func.name
            self.func = func
            self.initUI(run_exit, QtCore.QRect(left, top, width, height))
            self.threadpool = QtCore.QThreadPool()
            self.outputEdit = self.initOutput(output)

        def initOutput(self, output):
            if output == "gui":
                sys.stdout = GuiStream()
                sys.stderr = sys.stdout
                text = OutputEdit()
                text.setReadOnly(True)
                sys.stdout.textWritten.connect(text.print)
                sys.stdout.textWritten.connect(text.show)
                # sys.stdout.textWritten.connect(text.append)
                # text.show()

                return text
            else:
                return None

        def initCommandUI(self, func, run_exit, parent_layout=None):
            opt_set = CommandLayout(func, run_exit, parent_layout=parent_layout)

            if isinstance(func, click.MultiCommand):
                tabs = _InputTabWidget()

                for cmd, f in func.commands.items():
                    sub_opt_set = self.initCommandUI(f, run_exit, parent_layout=opt_set)
                    tab = QtWidgets.QWidget()
                    tab.setLayout(sub_opt_set)
                    tabs.addTab(tab, cmd)
                opt_set.addWidget(tabs, opt_set.rowCount(), 0, 1, 4)
                # return opt_set
            elif isinstance(func, click.Command):
                new_thread = getattr(func, "new_thread", self.new_thread)
                opt_set.add_cmd_buttons(
                    args=[
                        {
                            "label": "&Run",
                            "cmd_slot": partial(self.run_cmd, new_thread=new_thread),
                            "tooltip": "run command",
                        },
                        {
                            "label": "&Copy",
                            "cmd_slot": self.copy_cmd,
                            "tooltip": "copy command to clipboard",
                        },
                    ]
                )

            return opt_set

        def initUI(self, run_exit, geometry):
            self.run_exit = run_exit
            self.setWindowTitle(self.title)
            # self.setGeometry(self.left, self.top, self.width, self.height)
            self.setGeometry(geometry)
            self.opt_set = self.initCommandUI(
                self.func,
                run_exit,
            )
            self.setLayout(self.opt_set)
            self.show()

        @QtCore.pyqtSlot()
        def copy_cmd(self):
            cb = QtWidgets.QApplication.clipboard()
            cb.clear(mode=cb.Clipboard)
            cmd_text = " ".join(sys.argv)
            cb.setText(cmd_text, mode=cb.Clipboard)

            msg = QtWidgets.QMessageBox()
            msg.setIcon(QtWidgets.QMessageBox.Information)
            msg.setText(f"copy '{cmd_text}' to clipboard")
            msg.exec_()

        def run_cmd(self, new_thread):
            runcmd = RunCommand(self.func, self.run_exit)

            if new_thread:
                self.threadpool.start(runcmd)
            else:
                runcmd.run()

    def gui_it(click_func, style="qdarkstyle", **argvs) -> None:
        """
        Parameters
        ----------
        click_func
        `new_thread` is used for qt-based func, like matplotlib
        """
        global _gstyle
        _gstyle = GStyle(style)
        app = QtWidgets.QApplication(sys.argv)
        app.setStyleSheet(_gstyle.stylesheet)

        # set the default value for argvs
        argvs["run_exit"] = argvs.get("run_exit", False)
        argvs["new_thread"] = argvs.get("new_thread", False)

        _ = App(click_func, **argvs)
        sys.exit(app.exec_())

    def gui_option(f: click.core.BaseCommand) -> click.core.BaseCommand:
        """decorator for adding '--gui' option to command"""

        # TODO: add run_exit, new_thread
        def run_gui_it(ctx, param, value):
            if not value or ctx.resilient_parsing:
                return
            f.params = [p for p in f.params if not p.name == "gui"]
            gui_it(f)
            ctx.exit()

        return click.option(
            "--gui",
            is_flag=True,
            callback=run_gui_it,
            help="run with gui",
            expose_value=False,
            is_eager=False,
        )(f)
