mininterface 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,236 @@
1
+ Metadata-Version: 2.1
2
+ Name: mininterface
3
+ Version: 0.1.1
4
+ Summary: A minimal access to GUI, TUI, CLI and config
5
+ Home-page: https://github.com/CZ-NIC/mininterface
6
+ License: GPL-3.0-or-later
7
+ Author: Edvard Rejthar
8
+ Author-email: edvard.rejthar@nic.cz
9
+ Requires-Python: >=3.10,<4.0
10
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Dist: envelope
16
+ Requires-Dist: pyyaml
17
+ Requires-Dist: requests
18
+ Requires-Dist: tkinter-tooltip
19
+ Requires-Dist: tkinter_form
20
+ Requires-Dist: tyro
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Mininterface – access to GUI, TUI, CLI and config files
24
+ [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
25
+
26
+ Write the program core, do not bother with the input/output.
27
+
28
+ ![hello world example](asset/hello-world.png "A minimal use case")
29
+
30
+ Check out the code that displays such window, just the code you need. No lengthy blocks of code imposed by an external dependency.
31
+
32
+ ```python
33
+ from dataclasses import dataclass
34
+ from mininterface import run
35
+
36
+ @dataclass
37
+ class Config:
38
+ """Set of options."""
39
+ test: bool = False
40
+ """My testing flag"""
41
+ important_number: int = 4
42
+ """This number is very important"""
43
+
44
+ if __name__ == "__main__":
45
+ args: Config = run(Config, prog="My application").get_args()
46
+ print(args.important_number) # suggested by the IDE with the hint text "This number is very important"
47
+ ```
48
+
49
+ Or bound the interface to a `with` statement that redirects stdout directly to the window.
50
+
51
+ ```python
52
+ with run(Config) as m:
53
+ print(f"Your important number is {m}")
54
+ boolean = m.is_yes("Is that alright?")
55
+ ```
56
+
57
+ TODO img
58
+
59
+ Loading config file is a piece of cake. Alongside `program.py`, put `program.yaml`. Instantly loaded.
60
+
61
+ ```yaml
62
+ important_number: 555
63
+ ```
64
+
65
+ TODO img
66
+
67
+ - [Mininterface – GUI, TUI, CLI and config](#mininterface-gui-tui-cli-and-config)
68
+ - [Background](#background)
69
+ - [Installation](#installation)
70
+ - [Docs](#docs)
71
+ * [`mininterface`](#mininterface)
72
+ + [`run(config=None, interface=GuiInterface, **kwargs)`](#runconfignone-interfaceguiinterface-kwargs)
73
+ * [Interfaces](#interfaces)
74
+ + [`Mininterface(title: str = '')`](#Mininterface-title-str--)
75
+ + [`alert(self, text: str)`](#alert-self-text-str)
76
+ + [`ask(self, text: str) -> str`](#ask-self-text-str-str)
77
+ + [`ask_args(self) -> ~ConfigInstance`](#ask-args-self-configinstance)
78
+ + [`ask_form(self, args: FormDict, title="") -> int`](#ask-form-self-args-FormDict-title)
79
+ + [`ask_number(self, text: str) -> int`](#ask-number-self-text-str-int)
80
+ + [`get_args(self, ask_on_empty_cli=True) -> ~ConfigInstance`](#get-args-self-ask-on-empty-cli-true-configinstance)
81
+ + [`is_no(self, text: str) -> bool`](#is-no-self-text-str-bool)
82
+ + [`is_yes(self, text: str) -> bool`](#is-yes-self-text-str-bool)
83
+ + [`parse_args(self, config: Callable[..., ~ConfigInstance], config_file: pathlib.Path | None = None, **kwargs) -> ~ConfigInstance`](#parse-args-self-config-callable-configinstance-config-file-pathlibpath-none-none-kwargs-configinstance)
84
+ * [Standalone](#standalone)
85
+
86
+ <small><i><a href='http://ecotrust-canada.github.io/markdown-toc/'>Table of contents generated with markdown-toc</a></i></small>
87
+
88
+
89
+ # Background
90
+
91
+ Wrapper between the [tyro](https://github.com/brentyi/tyro) `argparse` replacement and [tkinter_form](https://github.com/JohanEstebanCuervo/tkinter_form/) that converts dicts into a GUI.
92
+
93
+ Writing a small and useful program might be a task that takes fifteen minutes. Adding a CLI to specify the parameters is not so much overhead. But building a simple GUI around it? HOURS! Hours spent on researching GUI libraries, wondering why the Python desktop app ecosystem lags so far behind the web world. All you need is a few input fields validated through a clickable window... You do not deserve to add hundred of lines of the code just to define some editable fields. `mininterface` is here to help.
94
+
95
+ The config variables needed by your program are kept in cozy dataclasses. Write less! The syntax of [tyro](https://github.com/brentyi/tyro) does not require any overhead (as its `argparse` alternatives do). You just annotate a class attribute, append a simple docstring and get a fully functional application:
96
+ * Call it as `program.py --help` to display full help.
97
+ * Use any flag in CLI: `program.py --test` causes `args.test` be set to `True`.
98
+ * The main benefit: Launch it without parameters as `program.py` to get a full working window with all the flags ready to be edited.
99
+ * Running on a remote machine? Automatic regression to the text interface.
100
+
101
+ # Installation
102
+
103
+ Install with a single command from [PyPi](https://pypi.org/project/mininterface/).
104
+
105
+ ```python3
106
+ pip install mininterface
107
+ ```
108
+
109
+ # Docs
110
+
111
+ You can easily nest the configuration. (See also [Tyro Hierarchical Configs](https://brentyi.github.io/tyro/examples/02_nesting/01_nesting/)).
112
+
113
+ Just put another dataclass inside the config file:
114
+
115
+ ```python3
116
+ @dataclass
117
+ class FurtherConfig:
118
+ token: str
119
+ host: str = "example.org"
120
+
121
+ @dataclass
122
+ class Config:
123
+ further: FurtherConfig
124
+
125
+ ...
126
+ print(config.further.host) # example.org
127
+ ```
128
+
129
+ A subset might be defaulted in YAML:
130
+
131
+ ```yaml
132
+ further:
133
+ host: example.com
134
+ ```
135
+
136
+ Or by CLI:
137
+
138
+ ```
139
+ $./program.py --further.host example.net
140
+ ```
141
+
142
+ ## `mininterface`
143
+
144
+ ### `run(config=None, interface=GuiInterface, **kwargs)`
145
+ Wrap your configuration dataclass into `run` to access the interface. Normally, an interface is chosen automatically. We prefer the graphical one, regressed to a text interface on a machine without display.
146
+ Besides, if given a configuration dataclass, the function enriches it with the CLI commands and possibly with the default from a config file if such exists. It searches the config file in the current working directory, with the program name ending on *.yaml*, ex: `program.py` will fetch `./program.yaml`.
147
+
148
+ * `config:ConfigClass`: Dataclass with the configuration.
149
+ * `interface`: Which interface to prefer. By default, we use the GUI, the fallback is the REPL.
150
+ * `**kwargs`: The same as for [`argparse.ArgumentParser`](https://docs.python.org/3/library/argparse.html).
151
+ * Returns: `interface` Interface used.
152
+
153
+ You cay context manage the function by a `with` statement. The stdout will be redirected to the interface (GUI window).
154
+
155
+ See the [initial examples](#mininterface-gui-tui-cli-and-config).
156
+
157
+ ## Interfaces
158
+
159
+ Several interfaces exist:
160
+
161
+ * `Mininterface` – The base interface. Does not require any user input and hence is suitable for headless testing.
162
+ * `GuiInterface` – A tkinter window.
163
+ * `TuiInterface` – An interactive terminal.
164
+ * `ReplInterface` – A debug terminal. Invokes a breakpoint after every dialog.
165
+
166
+ You can invoke one directly instead of using [mininterface.run](#run-config-none-interface-guiinterface-kwargs). Then, you can connect a configuration object to the CLI and config file with `parse_args` if needed.
167
+
168
+ ```python
169
+ with TuiInterface("My program") as m:
170
+ number = m.ask_number("Returns number")
171
+ ```
172
+
173
+ ### `Mininterface(title: str = '')`
174
+ Initialize.
175
+ ### `alert(self, text: str)`
176
+ Prompt the user to confirm the text.
177
+ ### `ask(self, text: str) -> str`
178
+ Prompt the user to input a text.
179
+ ### `ask_args(self) -> ~ConfigInstance`
180
+ Allow the user to edit whole configuration. (Previously fetched from CLI and config file by parse_args.)
181
+ ### `ask_form(self, args: FormDict, title="") -> dict`
182
+ Prompt the user to fill up whole form.
183
+ * `args`: Dict of `{labels: default value}`. The form widget infers from the default value type.
184
+ The dict can be nested, it can contain a subgroup.
185
+ The default value might be `mininterface.Value` that allows you to add descriptions.
186
+ A checkbox example: `{"my label": Value(True, "my description")}`
187
+ * `title`: Optional form title.
188
+ ### `ask_number(self, text: str) -> int`
189
+ Prompt the user to input a number. Empty input = 0.
190
+ ### `get_args(self, ask_on_empty_cli=True) -> ~ConfigInstance`
191
+ Returns whole configuration (previously fetched from CLI and config file by parse_args).
192
+ If program was launched with no arguments (empty CLI), invokes self.ask_args() to edit the fields.
193
+ ### `is_no(self, text: str) -> bool`
194
+ Display confirm box, focusing no.
195
+ ### `is_yes(self, text: str) -> bool`
196
+ Display confirm box, focusing yes.
197
+
198
+ ```python
199
+ m = run(prog="My program")
200
+ print(m.ask_yes("Is it true?")) # True/False
201
+ ```
202
+
203
+ ### `parse_args(self, config: Callable[..., ~ConfigInstance], config_file: pathlib.Path | None = None, **kwargs) -> ~ConfigInstance`
204
+ Parse CLI arguments, possibly merged from a config file.
205
+ * `config`: Dataclass with the configuration.
206
+ * `config_file`: File to load YAML to be merged with the configuration. You do not have to re-define all the settings, you can choose a few.
207
+ * `**kwargs` The same as for argparse.ArgumentParser.
208
+ * Returns: `ConfigInstance` Configuration namespace.
209
+
210
+ ## Standalone
211
+
212
+ When invoked directly, it creates simple GUI dialogs.
213
+
214
+ ```bash
215
+ $ mininterface --help
216
+ usage: Mininterface [-h] [OPTIONS]
217
+
218
+ Simple GUI dialog. Outputs the value the user entered.
219
+
220
+ ╭─ options ─────────────────────────────────────────────────────────────────────────────────╮
221
+ │ -h, --help show this help message and exit │
222
+ │ --alert STR Display the OK dialog with text. (default: '') │
223
+ │ --ask STR Prompt the user to input a text. (default: '') │
224
+ │ --ask-number STR Prompt the user to input a number. Empty input = 0. (default: '') │
225
+ │ --is-yes STR Display confirm box, focusing yes. (default: '') │
226
+ │ --is-no STR Display confirm box, focusing no. (default: '') │
227
+ ╰───────────────────────────────────────────────────────────────────────────────────────────╯
228
+ ```
229
+
230
+ You can fetch a value to i.e. a bash script.
231
+
232
+ ```bash
233
+ $ mininterface --ask-number "What's your age?" # GUI window invoked
234
+ 18
235
+ ```
236
+
@@ -0,0 +1,213 @@
1
+ # Mininterface – access to GUI, TUI, CLI and config files
2
+ [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
3
+
4
+ Write the program core, do not bother with the input/output.
5
+
6
+ ![hello world example](asset/hello-world.png "A minimal use case")
7
+
8
+ Check out the code that displays such window, just the code you need. No lengthy blocks of code imposed by an external dependency.
9
+
10
+ ```python
11
+ from dataclasses import dataclass
12
+ from mininterface import run
13
+
14
+ @dataclass
15
+ class Config:
16
+ """Set of options."""
17
+ test: bool = False
18
+ """My testing flag"""
19
+ important_number: int = 4
20
+ """This number is very important"""
21
+
22
+ if __name__ == "__main__":
23
+ args: Config = run(Config, prog="My application").get_args()
24
+ print(args.important_number) # suggested by the IDE with the hint text "This number is very important"
25
+ ```
26
+
27
+ Or bound the interface to a `with` statement that redirects stdout directly to the window.
28
+
29
+ ```python
30
+ with run(Config) as m:
31
+ print(f"Your important number is {m}")
32
+ boolean = m.is_yes("Is that alright?")
33
+ ```
34
+
35
+ TODO img
36
+
37
+ Loading config file is a piece of cake. Alongside `program.py`, put `program.yaml`. Instantly loaded.
38
+
39
+ ```yaml
40
+ important_number: 555
41
+ ```
42
+
43
+ TODO img
44
+
45
+ - [Mininterface – GUI, TUI, CLI and config](#mininterface-gui-tui-cli-and-config)
46
+ - [Background](#background)
47
+ - [Installation](#installation)
48
+ - [Docs](#docs)
49
+ * [`mininterface`](#mininterface)
50
+ + [`run(config=None, interface=GuiInterface, **kwargs)`](#runconfignone-interfaceguiinterface-kwargs)
51
+ * [Interfaces](#interfaces)
52
+ + [`Mininterface(title: str = '')`](#Mininterface-title-str--)
53
+ + [`alert(self, text: str)`](#alert-self-text-str)
54
+ + [`ask(self, text: str) -> str`](#ask-self-text-str-str)
55
+ + [`ask_args(self) -> ~ConfigInstance`](#ask-args-self-configinstance)
56
+ + [`ask_form(self, args: FormDict, title="") -> int`](#ask-form-self-args-FormDict-title)
57
+ + [`ask_number(self, text: str) -> int`](#ask-number-self-text-str-int)
58
+ + [`get_args(self, ask_on_empty_cli=True) -> ~ConfigInstance`](#get-args-self-ask-on-empty-cli-true-configinstance)
59
+ + [`is_no(self, text: str) -> bool`](#is-no-self-text-str-bool)
60
+ + [`is_yes(self, text: str) -> bool`](#is-yes-self-text-str-bool)
61
+ + [`parse_args(self, config: Callable[..., ~ConfigInstance], config_file: pathlib.Path | None = None, **kwargs) -> ~ConfigInstance`](#parse-args-self-config-callable-configinstance-config-file-pathlibpath-none-none-kwargs-configinstance)
62
+ * [Standalone](#standalone)
63
+
64
+ <small><i><a href='http://ecotrust-canada.github.io/markdown-toc/'>Table of contents generated with markdown-toc</a></i></small>
65
+
66
+
67
+ # Background
68
+
69
+ Wrapper between the [tyro](https://github.com/brentyi/tyro) `argparse` replacement and [tkinter_form](https://github.com/JohanEstebanCuervo/tkinter_form/) that converts dicts into a GUI.
70
+
71
+ Writing a small and useful program might be a task that takes fifteen minutes. Adding a CLI to specify the parameters is not so much overhead. But building a simple GUI around it? HOURS! Hours spent on researching GUI libraries, wondering why the Python desktop app ecosystem lags so far behind the web world. All you need is a few input fields validated through a clickable window... You do not deserve to add hundred of lines of the code just to define some editable fields. `mininterface` is here to help.
72
+
73
+ The config variables needed by your program are kept in cozy dataclasses. Write less! The syntax of [tyro](https://github.com/brentyi/tyro) does not require any overhead (as its `argparse` alternatives do). You just annotate a class attribute, append a simple docstring and get a fully functional application:
74
+ * Call it as `program.py --help` to display full help.
75
+ * Use any flag in CLI: `program.py --test` causes `args.test` be set to `True`.
76
+ * The main benefit: Launch it without parameters as `program.py` to get a full working window with all the flags ready to be edited.
77
+ * Running on a remote machine? Automatic regression to the text interface.
78
+
79
+ # Installation
80
+
81
+ Install with a single command from [PyPi](https://pypi.org/project/mininterface/).
82
+
83
+ ```python3
84
+ pip install mininterface
85
+ ```
86
+
87
+ # Docs
88
+
89
+ You can easily nest the configuration. (See also [Tyro Hierarchical Configs](https://brentyi.github.io/tyro/examples/02_nesting/01_nesting/)).
90
+
91
+ Just put another dataclass inside the config file:
92
+
93
+ ```python3
94
+ @dataclass
95
+ class FurtherConfig:
96
+ token: str
97
+ host: str = "example.org"
98
+
99
+ @dataclass
100
+ class Config:
101
+ further: FurtherConfig
102
+
103
+ ...
104
+ print(config.further.host) # example.org
105
+ ```
106
+
107
+ A subset might be defaulted in YAML:
108
+
109
+ ```yaml
110
+ further:
111
+ host: example.com
112
+ ```
113
+
114
+ Or by CLI:
115
+
116
+ ```
117
+ $./program.py --further.host example.net
118
+ ```
119
+
120
+ ## `mininterface`
121
+
122
+ ### `run(config=None, interface=GuiInterface, **kwargs)`
123
+ Wrap your configuration dataclass into `run` to access the interface. Normally, an interface is chosen automatically. We prefer the graphical one, regressed to a text interface on a machine without display.
124
+ Besides, if given a configuration dataclass, the function enriches it with the CLI commands and possibly with the default from a config file if such exists. It searches the config file in the current working directory, with the program name ending on *.yaml*, ex: `program.py` will fetch `./program.yaml`.
125
+
126
+ * `config:ConfigClass`: Dataclass with the configuration.
127
+ * `interface`: Which interface to prefer. By default, we use the GUI, the fallback is the REPL.
128
+ * `**kwargs`: The same as for [`argparse.ArgumentParser`](https://docs.python.org/3/library/argparse.html).
129
+ * Returns: `interface` Interface used.
130
+
131
+ You cay context manage the function by a `with` statement. The stdout will be redirected to the interface (GUI window).
132
+
133
+ See the [initial examples](#mininterface-gui-tui-cli-and-config).
134
+
135
+ ## Interfaces
136
+
137
+ Several interfaces exist:
138
+
139
+ * `Mininterface` – The base interface. Does not require any user input and hence is suitable for headless testing.
140
+ * `GuiInterface` – A tkinter window.
141
+ * `TuiInterface` – An interactive terminal.
142
+ * `ReplInterface` – A debug terminal. Invokes a breakpoint after every dialog.
143
+
144
+ You can invoke one directly instead of using [mininterface.run](#run-config-none-interface-guiinterface-kwargs). Then, you can connect a configuration object to the CLI and config file with `parse_args` if needed.
145
+
146
+ ```python
147
+ with TuiInterface("My program") as m:
148
+ number = m.ask_number("Returns number")
149
+ ```
150
+
151
+ ### `Mininterface(title: str = '')`
152
+ Initialize.
153
+ ### `alert(self, text: str)`
154
+ Prompt the user to confirm the text.
155
+ ### `ask(self, text: str) -> str`
156
+ Prompt the user to input a text.
157
+ ### `ask_args(self) -> ~ConfigInstance`
158
+ Allow the user to edit whole configuration. (Previously fetched from CLI and config file by parse_args.)
159
+ ### `ask_form(self, args: FormDict, title="") -> dict`
160
+ Prompt the user to fill up whole form.
161
+ * `args`: Dict of `{labels: default value}`. The form widget infers from the default value type.
162
+ The dict can be nested, it can contain a subgroup.
163
+ The default value might be `mininterface.Value` that allows you to add descriptions.
164
+ A checkbox example: `{"my label": Value(True, "my description")}`
165
+ * `title`: Optional form title.
166
+ ### `ask_number(self, text: str) -> int`
167
+ Prompt the user to input a number. Empty input = 0.
168
+ ### `get_args(self, ask_on_empty_cli=True) -> ~ConfigInstance`
169
+ Returns whole configuration (previously fetched from CLI and config file by parse_args).
170
+ If program was launched with no arguments (empty CLI), invokes self.ask_args() to edit the fields.
171
+ ### `is_no(self, text: str) -> bool`
172
+ Display confirm box, focusing no.
173
+ ### `is_yes(self, text: str) -> bool`
174
+ Display confirm box, focusing yes.
175
+
176
+ ```python
177
+ m = run(prog="My program")
178
+ print(m.ask_yes("Is it true?")) # True/False
179
+ ```
180
+
181
+ ### `parse_args(self, config: Callable[..., ~ConfigInstance], config_file: pathlib.Path | None = None, **kwargs) -> ~ConfigInstance`
182
+ Parse CLI arguments, possibly merged from a config file.
183
+ * `config`: Dataclass with the configuration.
184
+ * `config_file`: File to load YAML to be merged with the configuration. You do not have to re-define all the settings, you can choose a few.
185
+ * `**kwargs` The same as for argparse.ArgumentParser.
186
+ * Returns: `ConfigInstance` Configuration namespace.
187
+
188
+ ## Standalone
189
+
190
+ When invoked directly, it creates simple GUI dialogs.
191
+
192
+ ```bash
193
+ $ mininterface --help
194
+ usage: Mininterface [-h] [OPTIONS]
195
+
196
+ Simple GUI dialog. Outputs the value the user entered.
197
+
198
+ ╭─ options ─────────────────────────────────────────────────────────────────────────────────╮
199
+ │ -h, --help show this help message and exit │
200
+ │ --alert STR Display the OK dialog with text. (default: '') │
201
+ │ --ask STR Prompt the user to input a text. (default: '') │
202
+ │ --ask-number STR Prompt the user to input a number. Empty input = 0. (default: '') │
203
+ │ --is-yes STR Display confirm box, focusing yes. (default: '') │
204
+ │ --is-no STR Display confirm box, focusing no. (default: '') │
205
+ ╰───────────────────────────────────────────────────────────────────────────────────────────╯
206
+ ```
207
+
208
+ You can fetch a value to i.e. a bash script.
209
+
210
+ ```bash
211
+ $ mininterface --ask-number "What's your age?" # GUI window invoked
212
+ 18
213
+ ```
@@ -0,0 +1,174 @@
1
+ import sys
2
+ from typing import Any, Callable
3
+
4
+ try:
5
+ from tkinter import TclError, LEFT, Button, Frame, Label, Text, Tk
6
+ from tktooltip import ToolTip
7
+ from tkinter_form import Form
8
+ except ImportError:
9
+ from mininterface.common import InterfaceNotAvailable
10
+ raise InterfaceNotAvailable
11
+
12
+
13
+ from .common import InterfaceNotAvailable
14
+ from .auxiliary import FormDict, RedirectText, dataclass_to_dict, dict_to_dataclass, recursive_set_focus, normalize_types
15
+ from .Mininterface import Cancelled, ConfigInstance, Mininterface
16
+
17
+
18
+ class GuiInterface(Mininterface):
19
+ def __init__(self, *args, **kwargs):
20
+ try:
21
+ super().__init__(*args, **kwargs)
22
+ except TclError:
23
+ raise InterfaceNotAvailable
24
+ self.window = TkWindow(self)
25
+ self._always_shown = False
26
+ self._original_stdout = sys.stdout
27
+
28
+ def __enter__(self) -> "Mininterface":
29
+ """ When used in the with statement, the GUI window does not vanish between dialogues. """
30
+ self._always_shown = True
31
+ sys.stdout = RedirectText(self.window.text_widget, self.window.pending_buffer, self.window)
32
+ return self
33
+
34
+ def __exit__(self, *_):
35
+ self._always_shown = False
36
+ sys.stdout = self._original_stdout
37
+ if self.window.pending_buffer: # display text sent to the window but not displayed
38
+ print("".join(self.window.pending_buffer), end="")
39
+
40
+ def alert(self, text: str) -> None:
41
+ """ Display the OK dialog with text. """
42
+ return self.window.buttons(text, [("Ok", None)])
43
+
44
+ def ask(self, text: str) -> str:
45
+ return self.window.run_dialog({text: ""})[text]
46
+
47
+ def ask_args(self) -> ConfigInstance:
48
+ """ Display a window form with all parameters. """
49
+ params_ = dataclass_to_dict(self.args, self.descriptions)
50
+
51
+ # fetch the dict of dicts values from the form back to the namespace of the dataclasses
52
+ data = self.window.run_dialog(params_)
53
+ dict_to_dataclass(self.args, data)
54
+ return self.args
55
+
56
+ def ask_form(self, args: FormDict, title: str = "") -> dict:
57
+ """ Prompt the user to fill up whole form.
58
+ :param args: Dict of `{labels: default value}`. The form widget infers from the default value type.
59
+ The dict can be nested, it can contain a subgroup.
60
+ The default value might be `mininterface.Value` that allows you to add descriptions.
61
+ A checkbox example: {"my label": Value(True, "my description")}
62
+ :param title: Optional form title.
63
+ """
64
+ return self.window.run_dialog(args, title=title)
65
+
66
+ def ask_number(self, text: str) -> int:
67
+ return self.window.run_dialog({text: 0})[text]
68
+
69
+ def is_yes(self, text):
70
+ return self.window.yes_no(text, False)
71
+
72
+ def is_no(self, text):
73
+ return self.window.yes_no(text, True)
74
+
75
+
76
+ class TkWindow(Tk):
77
+ """ An editing window. """
78
+
79
+ def __init__(self, interface: GuiInterface):
80
+ super().__init__()
81
+ self.params = None
82
+ self._result = None
83
+ self._event_bindings = {}
84
+ self.interface = interface
85
+ self.title(interface.title)
86
+ self.bind('<Escape>', lambda _: self._ok(Cancelled))
87
+
88
+ self.frame = Frame(self)
89
+ """ dialog frame """
90
+
91
+ self.text_widget = Text(self, wrap='word', height=20, width=80)
92
+ self.text_widget.pack_forget()
93
+ self.pending_buffer = []
94
+ """ Text that has been written to the text widget but might not be yet seen by user. Because no mainloop was invoked. """
95
+
96
+ def run_dialog(self, formDict: FormDict, title: str = "") -> dict:
97
+ """ Let the user edit the form_dict values in a GUI window.
98
+ On abrupt window close, the program exits.
99
+ """
100
+ if title:
101
+ label = Label(self.frame, text=title)
102
+ label.pack(pady=10)
103
+
104
+ self.form = Form(self.frame,
105
+ name_form="",
106
+ form_dict=formDict,
107
+ name_config="Ok",
108
+ )
109
+ self.form.pack()
110
+
111
+ # Set the enter and exit options
112
+ self.form.button.config(command=self._ok)
113
+ # allow Enter for single field, otherwise Ctrl+Enter
114
+ tip, keysym = ("Ctrl+Enter", '<Control-Return>') if len(formDict) > 1 else ("Enter", "<Return>")
115
+ ToolTip(self.form.button, msg=tip) # NOTE is not destroyed in _clear
116
+ self._bind_event(keysym, self._ok)
117
+ self.protocol("WM_DELETE_WINDOW", lambda: sys.exit(0))
118
+
119
+ # focus the first element and run
120
+ recursive_set_focus(self.form)
121
+ return self.mainloop(lambda: self.validate(formDict, title))
122
+
123
+ def validate(self, formDict: FormDict, title: str):
124
+ if data := normalize_types(formDict, self.form.get()):
125
+ return data
126
+ return self.run_dialog(formDict, title)
127
+
128
+ def yes_no(self, text: str, focus_no=True):
129
+ return self.buttons(text, [("Yes", True), ("No", False)], int(focus_no)+1)
130
+
131
+ def buttons(self, text: str, buttons: list[tuple[str, Any]], focused: int = 1):
132
+ label = Label(self.frame, text=text)
133
+ label.pack(pady=10)
134
+
135
+ for text, value in buttons:
136
+ button = Button(self.frame, text=text, command=lambda v=value: self._ok(v))
137
+ button.bind("<Return>", lambda _: button.invoke())
138
+ button.pack(side=LEFT, padx=10)
139
+ self.frame.winfo_children()[focused].focus_set()
140
+ return self.mainloop()
141
+
142
+ def _bind_event(self, event, handler):
143
+ self._event_bindings[event] = handler
144
+ self.bind(event, handler)
145
+
146
+ def mainloop(self, callback: Callable = None):
147
+ self.frame.pack(pady=5)
148
+ self.deiconify() # show if hidden
149
+ self.pending_buffer.clear()
150
+ super().mainloop()
151
+ if not self.interface._always_shown:
152
+ self.withdraw() # hide
153
+
154
+ if self._result is Cancelled:
155
+ raise Cancelled
156
+ if callback:
157
+ return callback()
158
+ return self._result
159
+
160
+ def _ok(self, val=None):
161
+ # self.destroy()
162
+ self.quit()
163
+ # self.withdraw()
164
+ self._clear_dialog()
165
+ self._result = val
166
+
167
+ def _clear_dialog(self):
168
+ self.frame.pack_forget()
169
+ for widget in self.frame.winfo_children():
170
+ widget.destroy()
171
+ for key in self._event_bindings:
172
+ self.unbind(key)
173
+ self._event_bindings.clear()
174
+ self._result = None
@@ -0,0 +1,116 @@
1
+ import logging
2
+ import sys
3
+ from argparse import ArgumentParser
4
+ from dataclasses import MISSING
5
+ from pathlib import Path
6
+ from types import SimpleNamespace
7
+
8
+ import yaml
9
+ from tyro.extras import get_parser
10
+
11
+ from .auxiliary import (ConfigClass, ConfigInstance, FormDict, get_args_allow_missing,
12
+ get_descriptions)
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class Cancelled(SystemExit):
18
+ # We inherit from SystemExit so that the program exits without a traceback on GUI Escape.
19
+ pass
20
+
21
+
22
+ class Mininterface:
23
+ """ The base interface.
24
+ Does not require any user input and hence is suitable for headless testing.
25
+ """
26
+
27
+ def __init__(self, title: str = ""):
28
+ self.title = title or "Mininterface"
29
+ self.args: ConfigInstance = SimpleNamespace()
30
+ """ Parsed arguments, fetched from cli by parse.args """
31
+ self.descriptions = {}
32
+ """ Field descriptions """
33
+
34
+ def __enter__(self) -> "Mininterface":
35
+ """ When used in the with statement, the GUI window does not vanish between dialogs
36
+ and it redirects the stdout to a text area. """
37
+ return self
38
+
39
+ def __exit__(self, *_):
40
+ pass
41
+
42
+ def alert(self, text: str) -> None:
43
+ """ Prompt the user to confirm the text. """
44
+ print("Alert text", text)
45
+ return
46
+
47
+ def ask(self, text: str) -> str:
48
+ """ Prompt the user to input a text. """
49
+ print("Asking", text)
50
+ raise Cancelled(".. cancelled")
51
+
52
+ def ask_args(self) -> ConfigInstance:
53
+ """ Allow the user to edit whole configuration. (Previously fetched from CLI and config file by parse_args.) """
54
+ print("Asking the args", self.args)
55
+ return self.args
56
+
57
+ def ask_form(self, args: FormDict, title: str = "") -> dict:
58
+ """ Prompt the user to fill up whole form.
59
+ :param args: Dict of `{labels: default value}`. The form widget infers from the default value type.
60
+ The dict can be nested, it can contain a subgroup.
61
+ The default value might be `mininterface.Value` that allows you to add descriptions.
62
+ A checkbox example: `{"my label": Value(True, "my description")}`
63
+ """
64
+ print(f"Asking the form {title}", args)
65
+ return args # NOTE – this should return dict, not FormDict (get rid of auxiliary.Value values)
66
+
67
+ def ask_number(self, text: str) -> int:
68
+ """ Prompt the user to input a number. Empty input = 0. """
69
+ print("Asking number", text)
70
+ return 0
71
+
72
+ def get_args(self, ask_on_empty_cli=True) -> ConfigInstance:
73
+ """ Returns whole configuration (previously fetched from CLI and config file by parse_args).
74
+ If program was launched with no arguments (empty CLI), invokes self.ask_args() to edit the fields. """
75
+ # Empty CLI → GUI edit
76
+ if ask_on_empty_cli and len(sys.argv) <= 1:
77
+ return self.ask_args()
78
+ return self.args
79
+
80
+ def parse_args(self, config: ConfigClass,
81
+ config_file: Path | None = None,
82
+ **kwargs) -> ConfigInstance:
83
+ """ Parse CLI arguments, possibly merged from a config file.
84
+
85
+ :param config: Class with the configuration.
86
+ :param config_file: File to load YAML to be merged with the configuration. You do not have to re-define all the settings, you can choose a few.
87
+ :param **kwargs The same as for argparse.ArgumentParser.
88
+ :return: Configuration namespace.
89
+ """
90
+ # Load config file
91
+ if config_file:
92
+ disk = yaml.safe_load(config_file.read_text()) or {} # empty file is ok
93
+ # Nested dataclasses have to be properly initialized. YAML gave them as dicts only.
94
+ for key in (key for key, val in disk.items() if isinstance(val, dict)):
95
+ disk[key] = config.__annotations__[key](**disk[key])
96
+ # To ensure the configuration file does not need to contain all keys, we have to fill in the missing ones.
97
+ # Otherwise, tyro will spawn warnings about missing fields.
98
+ static = {key: getattr(config, key, MISSING)
99
+ for key in config.__annotations__ if not key.startswith("__") and not key in disk}
100
+ kwargs["default"] = SimpleNamespace(**(disk | static))
101
+
102
+ # Load configuration from CLI
103
+ parser: ArgumentParser = get_parser(config, **kwargs)
104
+ self.descriptions = get_descriptions(parser)
105
+ self.args = get_args_allow_missing(config, kwargs, parser)
106
+ return self.args
107
+
108
+ def is_yes(self, text: str) -> bool:
109
+ """ Display confirm box, focusing yes. """
110
+ print("Asking yes:", text)
111
+ return True
112
+
113
+ def is_no(self, text: str) -> bool:
114
+ """ Display confirm box, focusing no. """
115
+ print("Asking no:", text)
116
+ return False
@@ -0,0 +1,77 @@
1
+ from pprint import pprint
2
+ from .auxiliary import ConfigInstance, FormDict, dataclass_to_dict, dict_to_dataclass
3
+ from .Mininterface import Cancelled, Mininterface
4
+
5
+
6
+ class TuiInterface(Mininterface):
7
+
8
+ def alert(self, text: str):
9
+ """ Display text and let the user hit any key. """
10
+ input(text + " Hit any key.")
11
+
12
+ def ask(self, text: str = None):
13
+ try:
14
+ txt = input(text + ": ") if text else input()
15
+ except EOFError:
16
+ txt = "x"
17
+ if txt == "x":
18
+ raise Cancelled(".. cancelled")
19
+ return txt
20
+
21
+ def ask_args(self) -> ConfigInstance:
22
+ # NOTE: This is minimal implementation that should rather go the ReplInterface.
23
+ # I might build some menu of changing dict through:
24
+ # params_ = dataclass_to_dict(self.args, self.descriptions)
25
+ # data = FormDict → dict self.window.run_dialog(params_)
26
+ # dict_to_dataclass(self.args, params_)
27
+ return self.ask_form(self.args)
28
+
29
+ def ask_form(self, args: FormDict) -> dict:
30
+ # NOTE: This is minimal implementation that should rather go the ReplInterface.
31
+ print("Access `v` (as var) and change values. Then (c)ontinue.")
32
+ pprint(args)
33
+ v = args
34
+ try:
35
+ import ipdb
36
+ ipdb.set_trace()
37
+ except ImportError:
38
+ import pdb
39
+ pdb.set_trace()
40
+ print("*Continuing*")
41
+ print(args)
42
+ return args
43
+
44
+ def ask_number(self, text):
45
+ """
46
+ Let user write number. Empty input = 0.
47
+ """
48
+ while True:
49
+ try:
50
+ t = self.ask(text=text)
51
+ if not t:
52
+ return 0
53
+ return int(t)
54
+ except ValueError:
55
+ print("This is not a number")
56
+
57
+ def is_yes(self, text: str):
58
+ return self.ask(text=text + " [y]/n").lower() in ("y", "yes", "")
59
+
60
+ def is_no(self, text):
61
+ return self.ask(text=text + " y/[n]").lower() in ("n", "no", "")
62
+
63
+
64
+ class ReplInterface(TuiInterface):
65
+ """ Same as the base TuiInterface, except it starts the REPL. """
66
+
67
+ def __getattr__(self, name):
68
+ """ Run _Mininterface method if exists and starts a REPL. """
69
+ attr = getattr(super(), name, None)
70
+ if callable(attr):
71
+ def wrapper(*args, **kwargs):
72
+ result = attr(*args, **kwargs)
73
+ breakpoint()
74
+ return result
75
+ return wrapper
76
+ else:
77
+ raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
@@ -0,0 +1,56 @@
1
+ import sys
2
+ from pathlib import Path
3
+ from typing import TYPE_CHECKING, Type
4
+ from unittest.mock import patch
5
+
6
+ try:
7
+ from mininterface.GuiInterface import GuiInterface
8
+ except ImportError:
9
+ if TYPE_CHECKING:
10
+ pass
11
+ else:
12
+ GuiInterface = None
13
+
14
+ from mininterface.Mininterface import ConfigClass, ConfigInstance, Mininterface
15
+ from mininterface.TuiInterface import ReplInterface, TuiInterface
16
+ from mininterface.auxiliary import Value
17
+
18
+ # TODO auto-handle verbosity https://brentyi.github.io/tyro/examples/04_additional/12_counters/ ?
19
+ # TODO example on missing required options.
20
+
21
+
22
+ def run(config: ConfigClass | None = None,
23
+ interface: Type[Mininterface] = GuiInterface or ReplInterface, # NOTE we shuold use TuiInterface as a fallback
24
+ **kwargs) -> Mininterface:
25
+ """
26
+ Wrap your configuration dataclass into `run` to access the interface. Normally, an interface is chosen automatically.
27
+ We prefer the graphical one, regressed to a text interface on a machine without display.
28
+ Besides, if given a configuration dataclass, the function enriches it with the CLI commands and possibly
29
+ with the default from a config file if such exists.
30
+ It searches the config file in the current working directory, with the program name ending on *.yaml*, ex: `program.py` will fetch `./program.yaml`.
31
+
32
+ :param config: Dataclass with the configuration.
33
+ :param interface: Which interface to prefer. By default, we use the GUI, the fallback is the REPL.
34
+ :param **kwargs The same as for [argparse.ArgumentParser](https://docs.python.org/3/library/argparse.html).
35
+ :return: Interface used.
36
+
37
+ Undocumented: The `config` may be function as well. We invoke its paramters.
38
+ However, Mininterface.args stores the output of the function instead of the Argparse namespace
39
+ and methods like `Mininterface.ask_args()` will work unpredictibly..
40
+ """
41
+ # Build the interface
42
+ prog = kwargs.get("prog") or sys.argv[0]
43
+ # try:
44
+ interface: GuiInterface | Mininterface = interface(prog)
45
+ # except InterfaceNotAvailable: # Fallback to a different interface
46
+ # interface = TuiInterface(prog)
47
+
48
+ # Load configuration from CLI and a config file
49
+ if config:
50
+ cf = Path(sys.argv[0]).with_suffix(".yaml")
51
+ interface.parse_args(config, cf if cf.exists() and not kwargs.get("default") else None, **kwargs)
52
+
53
+ return interface
54
+
55
+
56
+ __all__ = ["run", "Value"]
@@ -0,0 +1,40 @@
1
+ from dataclasses import dataclass
2
+ from typing import List
3
+
4
+ from .GuiInterface import GuiInterface
5
+
6
+ from . import run
7
+
8
+ from tyro.conf import UseCounterAction, UseAppendAction
9
+ __doc__ = """Simple GUI dialog. Outputs the value the user entered."""
10
+
11
+
12
+ @dataclass
13
+ class CliInteface:
14
+ alert: str = ""
15
+ """ Display the OK dialog with text. """
16
+ ask: str = ""
17
+ """ Prompt the user to input a text. """
18
+ ask_number: str = ""
19
+ """ Prompt the user to input a number. Empty input = 0. """
20
+ is_yes: str = ""
21
+ """ Display confirm box, focusing yes. """
22
+ is_no: str = ""
23
+ """ Display confirm box, focusing no. """
24
+
25
+ # TODO does not work in REPL interface: mininterface --alert "ahoj"
26
+ def main():
27
+ # It does make sense to invoke GuiInterface only. Other interface would use STDOUT, hence make this impractical when fetching variable to i.e. a bash script.
28
+ # TODO It DOES make sense. Change in README. It s a good fallback.
29
+ result = []
30
+ with run(CliInteface, prog="Mininterface", description=__doc__) as m:
31
+ for method, label in vars(m.args).items():
32
+ if label:
33
+ result.append(getattr(m, method)(label))
34
+ # Displays each result on a new line. Currently, this is an undocumented feature.
35
+ # As we use the script for a single value only and it is not currently possible
36
+ # to ask two numbers or determine a dialog order etc.
37
+ [print(val) for val in result]
38
+
39
+ if __name__ == "__main__":
40
+ main()
@@ -0,0 +1,195 @@
1
+ import logging
2
+ import os
3
+ import re
4
+ from argparse import Action, ArgumentParser
5
+ from dataclasses import dataclass
6
+ from typing import Any, Callable, Literal, Optional, TypeVar, Union, get_args, get_type_hints
7
+ from unittest.mock import patch
8
+
9
+ try:
10
+ # NOTE this should be clean up and tested on a machine without tkinter installable
11
+ from tkinter import END, Entry, Text, Tk, Widget
12
+ from tkinter.ttk import Checkbutton, Combobox
13
+ except ImportError:
14
+ tkinter = None
15
+ END, Entry, Text, Tk, Widget = (None,)*5
16
+
17
+ from tkinter_form import Value
18
+ from tyro import cli
19
+ from tyro._argparse_formatter import TyroArgumentParser
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ @dataclass
25
+ class Value(Value):
26
+ """ Override val/description class with additional stuff. """
27
+
28
+ annotation: Any | None = None
29
+ """ Used for validation. To convert an empty '' to None. """
30
+
31
+ def __post_init__(self):
32
+ self._original_desc = self.description
33
+
34
+ def set_error_text(self, s):
35
+ self.description = f"{s} {self._original_desc}"
36
+
37
+
38
+ ConfigInstance = TypeVar("ConfigInstance")
39
+ ConfigClass = Callable[..., ConfigInstance]
40
+ FormDict = dict[str, Union[Value, Any, 'FormDict']]
41
+ """ Nested form that can have descriptions (through Value) instead of plain values. """
42
+
43
+
44
+ def dataclass_to_dict(args: ConfigInstance, descr: dict, _path="") -> FormDict:
45
+ """ Convert the dataclass produced by tyro into dict of dicts. """
46
+ main = ""
47
+ params = {main: {}} if not _path else {}
48
+ for param, val in vars(args).items():
49
+ annotation = None
50
+ if val is None:
51
+ wanted_type = get_type_hints(args.__class__).get(param)
52
+ if wanted_type in (Optional[int], Optional[str]):
53
+ # Since tkinter_form does not handle None yet, we have help it.
54
+ # We need it to be able to write a number and if empty, return None.
55
+ # This would fail: `severity: int | None = None`
56
+ # Here, we convert None to str(""), in normalize_types we convert it back.
57
+ annotation = wanted_type
58
+ val = ""
59
+ else:
60
+ # An unknown type annotation encountered-
61
+ # Since tkinter_form does not handle None yet, this will display as checkbox.
62
+ # Which is not probably wanted.
63
+ val = False
64
+ logger.warn(f"Annotation {wanted_type} of `{param}` not supported by Mininterface."
65
+ "None converted to False.")
66
+ if hasattr(val, "__dict__"): # nested config hierarchy
67
+ params[param] = dataclass_to_dict(val, descr, _path=f"{_path}{param}.")
68
+ elif not _path: # scalar value in root
69
+ params[main][param] = Value(val, descr.get(param), annotation)
70
+ else: # scalar value in nested
71
+ params[param] = Value(val, descr.get(f"{_path}{param}"), annotation)
72
+ return params
73
+
74
+
75
+ def normalize_types(origin: FormDict, data: dict) -> dict:
76
+ """ Run validators of all Value objects. If fails, outputs info.
77
+ Return corrected data. (Ex: Some values might be nulled from "".)
78
+ """
79
+ for (group, params), params2 in zip(data.items(), origin.values()):
80
+ for (key, val), pattern in zip(params.items(), params2.values()):
81
+ if isinstance(pattern, Value) and pattern.annotation:
82
+ if val == "" and type(None) in get_args(pattern.annotation):
83
+ # The user is not able to set the value to None, they left it empty.
84
+ # Cast back to None as None is one of the allowed types.
85
+ # Ex: `severity: int | None = None`
86
+ data[group][key] = val = None
87
+ elif pattern.annotation == Optional[int]:
88
+ try:
89
+ data[group][key] = val = int(val)
90
+ except ValueError:
91
+ pass
92
+
93
+ if not isinstance(val, pattern.annotation):
94
+ pattern.set_error_text(f"Type must be `{pattern.annotation}`!")
95
+ return False
96
+ return data
97
+
98
+
99
+ def dict_to_dataclass(args: ConfigInstance, data: dict):
100
+ """ Convert the dict of dicts from the GUI back into the object holding the configuration. """
101
+ for group, params in data.items():
102
+ for key, val in params.items():
103
+ if group:
104
+ setattr(getattr(args, group), key, val)
105
+ else:
106
+ setattr(args, key, val)
107
+
108
+
109
+ def get_terminal_size():
110
+ try:
111
+ # XX when piping the input IN, it writes
112
+ # echo "434" | convey -f base64 --debug
113
+ # stty: 'standard input': Inappropriate ioctl for device
114
+ # I do not know how to suppress this warning.
115
+ height, width = (int(s) for s in os.popen('stty size', 'r').read().split())
116
+ return height, width
117
+ except (OSError, ValueError):
118
+ return 0, 0
119
+
120
+
121
+ def get_args_allow_missing(config: ConfigClass, kwargs: dict, parser: ArgumentParser):
122
+ """ Fetch missing required options in GUI. """
123
+ # On missing argument, tyro fail. We cannot determine which one was missing, except by intercepting
124
+ # the error message function. Then, we reconstruct the missing options.
125
+ # NOTE But we should rather invoke a GUI with the missing options only.
126
+ original_error = TyroArgumentParser.error
127
+ eavesdrop = ""
128
+
129
+ def custom_error(self, message: str):
130
+ nonlocal eavesdrop
131
+ if not message.startswith("the following arguments are required:"):
132
+ return original_error(self, message)
133
+ eavesdrop = message
134
+ raise SystemExit(2) # will be catched
135
+ try:
136
+ with patch.object(TyroArgumentParser, 'error', custom_error):
137
+ return cli(config, **kwargs)
138
+ except BaseException as e:
139
+ if hasattr(e, "code") and e.code == 2 and eavesdrop: # Some arguments are missing. Determine which.
140
+ for arg in eavesdrop.partition(":")[2].strip().split(", "):
141
+ argument: Action = next(iter(p for p in parser._actions if arg in p.option_strings))
142
+ argument.default = "HALO"
143
+ if "." in argument.dest: # missing nested required argument handler not implemented, we make tyro fail in CLI
144
+ pass
145
+ else:
146
+ match argument.metavar:
147
+ case "INT":
148
+ setattr(kwargs["default"], argument.dest, 0)
149
+ case "STR":
150
+ setattr(kwargs["default"], argument.dest, "")
151
+ case _:
152
+ pass # missing handler not implemented, we make tyro fail in CLI
153
+ return cli(config, **kwargs) # second attempt
154
+ raise
155
+
156
+
157
+ def get_descriptions(parser: ArgumentParser) -> dict:
158
+ """ Load descriptions from the parser. Strip argparse info about the default value as it will be editable in the form. """
159
+ return {action.dest.replace("-", "_"): re.sub(r"\(default.*\)", "", action.help)
160
+ for action in parser._actions}
161
+
162
+
163
+ class RedirectText:
164
+ """ Helps to redirect text from stdout to a text widget. """
165
+
166
+ def __init__(self, widget: Text, pending_buffer: list, window: Tk) -> None:
167
+ self.widget = widget
168
+ self.max_lines = 1000
169
+ self.pending_buffer = pending_buffer
170
+ self.window = window
171
+
172
+ def write(self, text):
173
+ self.widget.pack(expand=True, fill='both')
174
+ self.widget.insert(END, text)
175
+ self.widget.see(END) # scroll to the end
176
+ self.trim()
177
+ self.window.update_idletasks()
178
+ self.pending_buffer.append(text)
179
+
180
+ def flush(self):
181
+ pass # required by sys.stdout
182
+
183
+ def trim(self):
184
+ lines = int(self.widget.index('end-1c').split('.')[0])
185
+ if lines > self.max_lines:
186
+ self.widget.delete(1.0, f"{lines - self.max_lines}.0")
187
+
188
+
189
+ def recursive_set_focus(widget: Widget):
190
+ for child in widget.winfo_children():
191
+ if isinstance(child, (Entry, Checkbutton, Combobox)):
192
+ child.focus_set()
193
+ return True
194
+ if recursive_set_focus(child):
195
+ return True
@@ -0,0 +1,2 @@
1
+ class InterfaceNotAvailable(ImportError):
2
+ pass
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["poetry-core>=1.0.0"]
3
+ build-backend = "poetry.core.masonry.api"
4
+
5
+ [tool.poetry]
6
+ name = "mininterface"
7
+ version = "0.1.1"
8
+ description = "A minimal access to GUI, TUI, CLI and config"
9
+ authors = ["Edvard Rejthar <edvard.rejthar@nic.cz>"]
10
+ license = "GPL-3.0-or-later"
11
+ homepage = "https://github.com/CZ-NIC/mininterface"
12
+ readme = "README.md"
13
+
14
+ [tool.poetry.dependencies]
15
+ python = "^3.10"
16
+ tyro = "*"
17
+ pyyaml = "*"
18
+ envelope = "*"
19
+ requests = "*"
20
+ tkinter-tooltip = "*"
21
+ tkinter_form = "*"
22
+
23
+ [tool.poetry.scripts]
24
+ mininterface = "mininterface.__main__:main"