Skip to content

Commit 5fd99ce

Browse files
authored
enh: readme external links converter
convert docs/index.md to README.md with working external links
1 parent 6f9988a commit 5fd99ce

File tree

4 files changed

+366
-3
lines changed

4 files changed

+366
-3
lines changed

Makefile

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
MANIFEST := pyproject.toml
22
TAG := $(shell grep "^version" $(MANIFEST) | pz --search '"(\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.?\d+)?)?"')
3+
README_PATH := asset/readme-readonly.md
34

4-
.PHONY: release validate pre-check
5+
.PHONY: release validate pre-check readme
56
default: release
67

78
validate:
89
@echo "Validating pyproject extras..."
910
@extra/validate_dependencies.py || (echo "Validation failed. Aborting." && exit 1)
1011

11-
release: validate
12+
release: readme-with-commit validate
1213
@echo "Tagging release $(TAG)"
1314
git tag $(TAG)
1415
git push origin $(TAG)
@@ -20,3 +21,16 @@ pre-check:
2021
pre-commit install && \
2122
pre-commit autoupdate && \
2223
pre-commit install --hook-type commit-msg -f
24+
25+
readme-with-commit: readme
26+
@if git diff --quiet $(README_PATH); then \
27+
echo "No changes detected in $(README_PATH)"; \
28+
else \
29+
echo "Changes detected. Committing..."; \
30+
git add $(README_PATH); \
31+
git commit -m "docs: readme"; \
32+
fi
33+
34+
readme:
35+
@echo "Generating asset/readme-readonly.md from docs/index.md..."
36+
@python3 extra/convert_readme.py || (echo "README generation failed. Aborting." && exit 1)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
docs/index.md
1+
asset/readme-readonly.md

asset/readme-readonly.md

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
# Mininterface – access to GUI, TUI, web, CLI and config files
2+
[![Build Status](https://github.com/CZ-NIC/mininterface/actions/workflows/run-unittest.yml/badge.svg?branch=main)](https://github.com/CZ-NIC/mininterface/actions)
3+
[![Downloads](https://static.pepy.tech/badge/mininterface)](https://pepy.tech/project/mininterface)
4+
5+
Write the program core, do not bother with the input/output.
6+
7+
![Hello world example: GUI window](asset/hello-gui.avif "A minimal use case – GUI")
8+
![Hello world example: TUI fallback](asset/hello-tui.avif "A minimal use case – TUI fallback")
9+
10+
Check out the code, which is surprisingly short, that displays such a window or its textual fallback.
11+
12+
```python
13+
from dataclasses import dataclass
14+
from mininterface import run
15+
16+
@dataclass
17+
class Env:
18+
""" This calculates something. """
19+
20+
my_flag: bool = False
21+
""" This switches the functionality """
22+
23+
my_number: int = 4
24+
""" This number is very important """
25+
26+
if __name__ == "__main__":
27+
m = run(Env, title="My application")
28+
m.form()
29+
# Attributes are suggested by the IDE
30+
# along with the hint text 'This number is very important'.
31+
print(m.env.my_number)
32+
```
33+
34+
# Contents
35+
- [You got CLI](#you-got-cli)
36+
- [You got config file management](#you-got-config-file-management)
37+
- [You got dialogs](#you-got-dialogs)
38+
- [Background](#background)
39+
- [Installation](#installation)
40+
- [Docs](#docs)
41+
- [Gallery](#gallery)
42+
- [Examples](#examples)
43+
* [Hello world](#hello-world)
44+
* [Goodbye argparse world](#goodbye-argparse-world)
45+
46+
## You got CLI
47+
It was all the code you need. No lengthy blocks of code imposed by an external dependency. Besides the GUI/TUI/web, you receive powerful YAML-configurable CLI parsing.
48+
49+
50+
```bash
51+
$ ./program.py --help
52+
usage: program.py [-h] [-v] [--my-flag | --no-my-flag] [--my-number INT]
53+
54+
This calculates something.
55+
56+
╭─ options ───────────────────────────────────────────────────────────────╮
57+
│ -h, --help show this help message and exit
58+
│ -v, --verbose Verbosity level. Can be used twice to increase. │
59+
│ --my-flag, --no-my-flag │
60+
│ This switches the functionality (default: False) │
61+
│ --my-number INT This number is very important (default: 4) │
62+
╰─────────────────────────────────────────────────────────────────────────╯
63+
```
64+
65+
## You got config file management
66+
Loading config file is a piece of cake. Alongside `program.py`, write some of its arguments to `program.yaml`. They are seamlessly taken as defaults.
67+
68+
```yaml
69+
my_number: 555
70+
```
71+
72+
```bash
73+
$ program.py --help
74+
...
75+
│ --my-number INT This number is very important (default: 555) │
76+
```
77+
78+
## You got dialogs
79+
Check out several useful methods to handle user dialogs. Here we bound the interface to a `with` statement that redirects stdout directly to the window.
80+
81+
```python
82+
with run(Env) as m:
83+
print(f"Your important number is {m.env.my_number}")
84+
boolean = m.confirm("Is that alright?")
85+
```
86+
87+
![Small window with the text 'Your important number'](asset/hello-with-statement.webp "With statement to redirect the output")
88+
![The same in terminal'](asset/hello-with-statement-tui.avif "With statement in TUI fallback")
89+
90+
# Background
91+
92+
Wrapper between various libraries that provide a user interface.
93+
94+
It allows you to focus on writing the program's core logic without worrying about input/output. It generates CLI arguments, parses config file, and shows a UI. Depending on the endpoint, the dialogs automatically use either a GUI, a mouse-clickable TUI, a lightweight terminal text interface, or be available via HTTP — all while maintaining exactly the same functionality.
95+
96+
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.
97+
98+
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:
99+
100+
* Call it as `program.py --help` to display full help.
101+
* Use any flag in CLI: `program.py --my-flag` causes `env.my_flag` be set to `True`.
102+
* The main benefit: Launch it without parameters as `program.py` to get a fully working window with all the flags ready to be edited.
103+
* Running on a remote machine? Automatic regression to the text interface.
104+
* Or access your program via [web browser](http://127.0.0.1:8000/Interfaces/#webinterface-or-web).
105+
106+
# Installation
107+
108+
Install with a single command from [PyPi](https://pypi.org/project/mininterface/).
109+
110+
```bash
111+
pip install "mininterface[all]<2" # GPLv3 and compatible
112+
```
113+
114+
## Bundles
115+
116+
There are various bundles. We mark the least permissive licence in the bundle.
117+
118+
| bundle | size | licence | description |
119+
| ------ | ---- | ----------- | ---- |
120+
| mininterface | 1 MB | LGPL | minimal – only text dialogs |
121+
| mininterface[basic] | 25 MB | LGPL | CLI, GUI, TUI |
122+
| mininterface[web] | 40 MB | LGPL | including [WebInterface](Interfaces.md#webinterface-or-web) |
123+
| mininterface[img] | 40 MB | LGPL | images |
124+
| mininterface[tui] | 40 MB | LGPL | images |
125+
| mininterface[gui] | 70 MB | GPL | images, combobox, calendar |
126+
| mininterface[ui] | 90 MB | GPL | full installation |
127+
| mininterface[all] | 90 MB | GPL | full installation, same as `ui`, reserved for future use (big dependencies, optional interfaces) |
128+
129+
Apart from the minimal bundle (which lacks CLI and dataclass support), they have the same functionality, differring only in the user experience.
130+
131+
!!! tip
132+
For automated testing (e.g., in CI environments), the `mininterface[basic]` bundle is sufficient.
133+
134+
## MacOS GUI
135+
136+
If the GUI does not work on MacOS, you might need to launch: `brew install python-tk`
137+
138+
# Docs
139+
See the docs overview at [https://cz-nic.github.io/mininterface/](https://cz-nic.github.io/mininterface/Overview/).
140+
141+
# Gallery
142+
143+
These projects have the code base reduced thanks to the mininterface:
144+
145+
* **[deduplidog](https://github.com/CZ-NIC/deduplidog/)** – Find duplicates in a scattered directory structure
146+
* **[touch-timestamp](https://github.com/CZ-NIC/touch-timestamp/)** – A powerful dialog to change the files' timestamp
147+
148+
# Examples
149+
## Hello world
150+
151+
Take a look at the following example.
152+
153+
1. We define any Env class.
154+
2. Then, we initialize mininterface with [`run(Env)`](https://cz-nic.github.io/mininterface/run/) – the missing fields will be prompted for
155+
3. Then, we use various dialog methods, like [`confirm`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.confirm), [`select`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.select) or [`form`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.form).
156+
157+
Below, you find the screenshots how the program looks in various environments ([graphic](Interfaces.md#guiinterface-or-tkinterface-or-gui) interface, [web](Interfaces.md#webinterface-or-web) interface...).
158+
159+
```python
160+
from dataclasses import dataclass
161+
from pathlib import Path
162+
from mininterface import run
163+
164+
@dataclass
165+
class Env:
166+
my_file: Path # This is my help text
167+
my_flag: bool = False
168+
my_number: int = 4
169+
170+
if __name__ == "__main__":
171+
# Here, the user will be prompted
172+
# for missing parameters (`my_file`) automatically
173+
with run(Env) as m:
174+
175+
# You can lean on the typing
176+
# Ex. directly read from the file object:
177+
print("The file contents:", m.env.my_file.read_text())
178+
179+
# You can use various dialog methods,
180+
# like `confirm` for bool
181+
if m.confirm("Do you want to continue?"):
182+
183+
# or `select` for choosing a value
184+
fruit = m.select(("apple", "banana", "sirup"), "Choose a fruit")
185+
186+
if fruit == "apple":
187+
# or `form` for an arbitrary values
188+
m.form({
189+
"How many": 0,
190+
"Choose another file": m.env.my_file
191+
})
192+
```
193+
194+
Launch with `./program.py`:
195+
196+
![Tutorial](asset/tutorial_tk1.avif)
197+
![Tutorial](asset/tutorial_tk2.avif)
198+
![Tutorial](asset/tutorial_tk3.avif)
199+
![Tutorial](asset/tutorial_tk4.avif)
200+
201+
Or at the remote machine `MININTERFACE_INTERFACE=tui ./program.py`:
202+
203+
![Tutorial](asset/tutorial_textual1.avif)
204+
![Tutorial](asset/tutorial_textual2.avif)
205+
![Tutorial](asset/tutorial_textual3.avif)
206+
![Tutorial](asset/tutorial_textual4.avif)
207+
208+
Or via the plain text `MININTERFACE_INTERFACE=text ./program.py`:
209+
210+
![Tutorial](asset/tutorial_text.avif)
211+
212+
Or via web browser `MININTERFACE_INTERFACE=web ./program.py`:
213+
214+
![Tutorial](asset/tutorial_web.avif)
215+
216+
You can always set Env via CLI or a config file:
217+
218+
```bash
219+
$ ./program.py --help
220+
usage: program.py [-h] [OPTIONS]
221+
222+
╭─ options ──────────────────────────────────────────────────────────────╮
223+
│ -h, --help show this help message and exit
224+
│ -v, --verbose Verbosity level. Can be used twice to increase. │
225+
│ --my-file PATH This is my help text (required) │
226+
│ --my-flag, --no-my-flag │
227+
│ (default: False) │
228+
│ --my-number INT (default: 4) │
229+
╰────────────────────────────────────────────────────────────────────────╯
230+
```
231+
232+
## Goodbye argparse world
233+
234+
You want to try out the Mininterface with your current [`ArgumentParser`](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser)?
235+
236+
You're using positional arguments, subparsers, types in the ArgumentParser... Mininterface will give you immediate benefit. Just wrap it inside the [`run`](https://cz-nic.github.io/mininterface/run/) method.
237+
238+
```python
239+
#!/usr/bin/env python3
240+
from argparse import ArgumentParser
241+
from datetime import time
242+
from pathlib import Path
243+
244+
from mininterface import run
245+
246+
parser = ArgumentParser()
247+
subparsers = parser.add_subparsers(dest="command", required=True)
248+
sub1 = subparsers.add_parser("build", help="Build something.")
249+
sub1.add_argument("--optimize", action="store_true", help="Enable optimizations.")
250+
parser.add_argument("input_file", type=Path, help="Path to the input file.")
251+
parser.add_argument("--time", type=time, help="Given time")
252+
253+
# Old version
254+
# env = parser.parse_args()
255+
# env.input_file # a Path object
256+
257+
# New version
258+
m = run(parser)
259+
m.env.input_file # a Path object
260+
261+
# Live edit of the fields
262+
m.form()
263+
```
264+
265+
Now, the help text looks much better. Try it in the terminal to see the colours.
266+
267+
```
268+
$ ./program.py --help
269+
usage: program.py [-h] [OPTIONS] PATH
270+
271+
╭─ positional arguments ──────────────────────────────────────────────────╮
272+
│ PATH Path to the input file. (required) │
273+
╰─────────────────────────────────────────────────────────────────────────╯
274+
╭─ options ───────────────────────────────────────────────────────────────╮
275+
│ -h, --help show this help message and exit │
276+
│ -v, --verbose Verbosity level. Can be used twice to increase. │
277+
│ --time HH:MM[:SS[…]] Given time (default: 00:00:00) │
278+
╰─────────────────────────────────────────────────────────────────────────╯
279+
╭─ build options ─────────────────────────────────────────────────────────╮
280+
│ --build.optimize, --build.no-optimize │
281+
│ Enable optimizations. (default: False) │
282+
╰─────────────────────────────────────────────────────────────────────────╯
283+
```
284+
285+
And what happens when you launch the program? First, *Mininterface* asks you to provide the missing required arguments. Note the button to raise a file picker dialog.
286+
287+
![Positional fields](asset/argparse_required.avif)
288+
289+
Then, a `.form()` call will create a dialog with all the fields.
290+
291+
![Whole form](asset/argparse_form.avif)
292+
293+
You will access the arguments through [`m.env`](https://cz-nic.github.io/mininterface/Mininterface/#mininterface.Mininterface.env)
294+
295+
```python
296+
print(m.env.time) # -> 14:21
297+
```
298+
299+
If you're sure enough to start using *Mininterface*, convert the argparse into a dataclass. Then, the IDE will auto-complete the hints as you type.
300+
301+
!!! warning
302+
The argparse support is considered mostly for your evaluation as there are some [`differences`](Argparse-caveats.md) for advanced argparse CLI interfaces.

extra/convert_readme.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env python3
2+
import sys
3+
import re
4+
from pathlib import Path
5+
6+
7+
def generate_readme(index_path="docs/index.md", readme_path = Path("asset/readme-readonly.md")):
8+
index_file = Path(index_path)
9+
if not index_file.exists():
10+
print(f"❌ Error: {index_path} not found.")
11+
return False
12+
13+
text = index_file.read_text(encoding="utf-8")
14+
15+
base_url = "https://cz-nic.github.io/mininterface"
16+
17+
def replace_ref(match):
18+
link_text = match.group(1).strip()
19+
ref = match.group(2).strip()
20+
parts = ref.split(".")
21+
22+
# Case 1: mininterface.run → base/run/
23+
if len(parts) == 2:
24+
url = f"{base_url}/{parts[1]}/"
25+
26+
# Case 2: mininterface.Mininterface.confirm → base/Mininterface/#mininterface.Mininterface.confirm
27+
elif len(parts) == 3:
28+
url = f"{base_url}/{parts[1]}/#{ref}"
29+
30+
# Fallback
31+
else:
32+
url = f"{base_url}/#{ref}"
33+
34+
return f"[{link_text}]({url})"
35+
36+
37+
new_text = re.sub(r"\[([^\[\]]+)\]\[([^\[\]]+)\]", replace_ref, text)
38+
39+
Path(readme_path).write_text(new_text, encoding="utf-8")
40+
41+
print(f"✅ README.md generated successfully from {index_path}")
42+
return True
43+
44+
45+
if __name__ == "__main__":
46+
if not generate_readme():
47+
sys.exit(1)

0 commit comments

Comments
 (0)