Skip to content

Commit 2688f35

Browse files
committed
enh: TextInterface mnemonic
1 parent b26298d commit 2688f35

File tree

6 files changed

+121
-31
lines changed

6 files changed

+121
-31
lines changed

docs/Settings.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ The difference when using such configuration file.
6060

6161
### Inheritance
6262

63-
The individual setting items are inherited, while the descendants have the higher priority.
63+
The individual setting items are inherited, while the descendants have the higher priority. Ex. `TuiSettings` works as a default for `TextSettings` and `TextualSettings`.
6464

6565
```mermaid
6666
graph LR
@@ -70,7 +70,7 @@ TextualSettings --> TuiSettings
7070
TextSettings --> TuiSettings
7171
```
7272

73-
Ex. this config file sets the `UiSettings` item [`mnemonic`][mininterface.settings.UiSettings.mnemonic] to None for `TuiSettings` and more specifically to False for `TextSettings`.
73+
Ex. this config file sets the `UiSettings` item [`mnemonic`][mininterface.settings.UiSettings.mnemonic] to `None` for `TuiSettings` and more specifically to `False` for `TextSettings`.
7474

7575
```yaml
7676
mininterface:
@@ -99,5 +99,17 @@ For the different interfaces, the value varies this way:
9999
show_root_full_path: false
100100

101101
::: mininterface.settings.GuiSettings
102+
options:
103+
show_root_full_path: false
104+
105+
::: mininterface.settings.TuiSettings
106+
options:
107+
show_root_full_path: false
108+
109+
::: mininterface.settings.TextualSettings
110+
options:
111+
show_root_full_path: false
112+
113+
::: mininterface.settings.TextSettings
102114
options:
103115
show_root_full_path: false

mininterface/_mininterface/adaptor.py

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -40,31 +40,34 @@ def run_dialog(self, form: TagDict, title: str = "", submit: bool | str = True)
4040
Setups the facet._fetch_from_adaptor.
4141
"""
4242
self.facet._fetch_from_adaptor(form)
43+
if self.settings.mnemonic is not False:
44+
self._determine_mnemonic(form, self.settings.mnemonic is True)
4345

46+
def _determine_mnemonic(self, form: TagDict, also_nones=False):
47+
""" also_nones – Also determine those tags when Tag.mnemonic=None. """
4448
# Determine mnemonic
45-
if self.settings.mnemonic is not False:
46-
used_mnemonic = set()
47-
to_be_determined: list[Tag] = []
48-
for tag in flatten(form):
49-
if tag.mnemonic is False:
50-
continue
51-
if isinstance(tag.mnemonic, str):
52-
used_mnemonic.add(tag.mnemonic)
53-
tag._mnemonic = tag.mnemonic
54-
elif self.settings.mnemonic or tag.mnemonic:
55-
# .settings.mnemonic=None + tag.mnemonic=True OR
56-
# .settings.mnemonic=True + tag.mnemonic=None
57-
to_be_determined.append(tag)
58-
59-
# Find free mnemonic for Tag
60-
for tag in to_be_determined:
61-
# try every char in label
62-
# then, if no free letter, give a random letter
63-
for c in chain((c.lower() for c in tag.label if c.isalpha()), ascii_lowercase):
64-
if c not in used_mnemonic:
65-
used_mnemonic.add(c)
66-
tag._mnemonic = c
67-
break
49+
used_mnemonic = set()
50+
to_be_determined: list[Tag] = []
51+
for tag in flatten(form):
52+
if tag.mnemonic is False:
53+
continue
54+
if isinstance(tag.mnemonic, str):
55+
used_mnemonic.add(tag.mnemonic)
56+
tag._mnemonic = tag.mnemonic
57+
elif also_nones or tag.mnemonic:
58+
# .settings.mnemonic=None + tag.mnemonic=True OR
59+
# .settings.mnemonic=True + tag.mnemonic=None
60+
to_be_determined.append(tag)
61+
62+
# Find free mnemonic for Tag
63+
for tag in to_be_determined:
64+
# try every char in label
65+
# then, if no free letter, give a random letter
66+
for c in chain((c.lower() for c in tag.label if c.isalpha()), ascii_lowercase):
67+
if c not in used_mnemonic:
68+
used_mnemonic.add(c)
69+
tag._mnemonic = c
70+
break
6871

6972
def submit_done(self) -> bool:
7073
if action := self.post_submit_action:

mininterface/_text_interface/adaptor.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ def _get_tag_val(self, val: Tag | dict):
9494
case dict() as d:
9595
return f"... ({len(d)}×)"
9696

97+
def _get_tag_mnemonic(self, val: Tag | dict):
98+
if isinstance(val, Tag) and val._mnemonic:
99+
return f"[{val._mnemonic}] "
100+
return ""
101+
97102
def run_dialog(self, form: TagDict, title: str = "", submit: bool | str = True) -> TagDict:
98103
""" Let the user edit the form_dict values in a GUI window.
99104
On abrupt window close, the program exits.
@@ -121,7 +126,7 @@ def _run_dialog(self, form: TagDict, title: str = "", submit: bool | str = True)
121126
if single:
122127
key = next(iter(form))
123128
else:
124-
index = self._choose([f"{key}{self._get_tag_val(val)}" for key,
129+
index = self._choose([f"{self._get_tag_mnemonic(val)}{key}{self._get_tag_val(val)}" for key,
125130
val in form.items()], append_ok=True)
126131
key = list(form)[index]
127132

@@ -155,7 +160,10 @@ def _choose(self, items: list, title=None, append_ok=False, multiple: bool = Fal
155160
kwargs = {}
156161
if not multiple:
157162
if len(items) < 10:
158-
it = [f"[{i+1}] {item}" for i, item in enumerate(items)]
163+
# use number as shorcuts when no shortcuts are given `[c]`
164+
it = [item if item.startswith("[") # field already starts with a shortcut, ex. `[f] foo`
165+
else f"[{i+1}] {item}" # add a number shorctu, ex. `[1] foo`
166+
for i, item in enumerate(items)]
159167
else:
160168
kwargs = {"show_search_hint": True}
161169

@@ -187,3 +195,8 @@ def _choose(self, items: list, title=None, append_ok=False, multiple: bool = Fal
187195
raise Submit
188196
index -= 1
189197
return index
198+
199+
def _determine_mnemonic(self, form: TagDict, also_nones=False):
200+
if self.settings.mnemonic_over_number is False:
201+
return
202+
super()._determine_mnemonic(form, also_nones=also_nones and self.settings.mnemonic_over_number is True)

mininterface/_textual_interface/textual_app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
from typing import TYPE_CHECKING
23
from textual import events
34
from textual.app import App

mininterface/_textual_interface/widgets.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ def get_ui_value(self):
3232
else:
3333
return self.value
3434

35+
# NOTE, Tag.mnemonic is unfortunately not supported
36+
# because terminal key sending is a mess.
37+
# In some terminals (gnome-terminal, terminator), alt+f sends ctrl+right
38+
# (yeah, alt+f should go word forward, but this is awfaul)
39+
# and hitting alt will trigger an escape sequence while escape is mapped to exit the app.
40+
# I failed setting a timeout to work around this. Not taking account the different Win behaviour.
41+
# This should really be handled by a lower level, ex. prompt_toolkit seem to have better handling than textual.
42+
# def on_key(self, event: events.Key) -> None:
43+
# return
44+
# if event.key == "alt+" + self.tag._mnemonic:
45+
# self.focus()
46+
# event.stop()
47+
3548

3649
class TagWidgetWithInput(TagWidget):
3750
"""Base class for widgets that contain an input element"""

mininterface/settings.py

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,65 @@ class TextualSettings(TuiSettings):
5858

5959
@dataclass
6060
class TextSettings(TuiSettings):
61-
...
61+
mnemonic_over_number: Optional[bool] = None
62+
""" Even when mnemonic can be determined, use rather number as a shortcut.
63+
64+
By default if `None`, determine those with `Tag(mnemonic=char|True)`:
65+
66+
```bash
67+
> ok
68+
[1] foo1: ×
69+
[2] foo2: ×
70+
[g] foo3: ×
71+
```
72+
73+
If `True`, determine also those having `Tag(mnemonic=None)`:
74+
75+
```bash
76+
> ok
77+
[f] foo1: ×
78+
[o] foo2: ×
79+
[g] foo3: ×
80+
```
81+
82+
If `False`, we prefer numbers:
83+
84+
```bash
85+
> ok
86+
[1] foo1: ×
87+
[2] foo2: ×
88+
[3] foo3: ×
89+
```
90+
91+
The original code:
92+
93+
```python
94+
from dataclasses import dataclass
95+
from typing import Annotated
96+
from mininterface import Tag, run
97+
from mininterface.settings import MininterfaceSettings, TextSettings
98+
99+
100+
@dataclass
101+
class Env:
102+
foo1: bool = False
103+
foo2: bool = False
104+
foo3: Annotated[bool, Tag(mnemonic="g")] = False
105+
106+
107+
m = run(Env, settings=MininterfaceSettings(text=TextSettings(mnemonic_over_number=True)))
108+
m.form()
109+
quit()
110+
```
111+
"""
62112

63113

64114
@dataclass
65115
class WebSettings(TextualSettings):
116+
# This is ready and working and waiting for the demand.
66117
...
67118

68119

69-
# NOTE elaborate in the docs when more examples exist
70-
# TuiSettings works as a default for TextSettings and TextualSettings
71-
72120
@dataclass
73121
class MininterfaceSettings:
74122
ui: UiSettings = field(default_factory=UiSettings)

0 commit comments

Comments
 (0)