ThreeDimensionModeller 1.0.0__py3-none-any.whl
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.
- ThreeDimensionModeller/__init__.py +8 -0
- ThreeDimensionModeller/__main__.py +6 -0
- ThreeDimensionModeller/about_page.py +200 -0
- ThreeDimensionModeller/check_system.py +521 -0
- ThreeDimensionModeller/cli.py +367 -0
- ThreeDimensionModeller/menu_language.py +754 -0
- ThreeDimensionModeller/menu_model.py +276 -0
- ThreeDimensionModeller/menu_painter.py +487 -0
- ThreeDimensionModeller/menu_session.py +118 -0
- ThreeDimensionModeller/model.py +835 -0
- ThreeDimensionModeller/self_management.py +79 -0
- ThreeDimensionModeller/system_log.py +92 -0
- ThreeDimensionModeller/tui.py +475 -0
- threedimensionmodeller-1.0.0.dist-info/METADATA +386 -0
- threedimensionmodeller-1.0.0.dist-info/RECORD +19 -0
- threedimensionmodeller-1.0.0.dist-info/WHEEL +5 -0
- threedimensionmodeller-1.0.0.dist-info/entry_points.txt +2 -0
- threedimensionmodeller-1.0.0.dist-info/licenses/LICENSE.md +21 -0
- threedimensionmodeller-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Keystroke state for the text menu.
|
|
3
|
+
# requirement-python-oop — class MenuModel.
|
|
4
|
+
# =============================================================================
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import curses
|
|
8
|
+
|
|
9
|
+
from .menu_painter import MenuPainter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MenuModel:
|
|
14
|
+
"""Keystroke state for the front board. The bottom frame is the input box.
|
|
15
|
+
|
|
16
|
+
boards, when set, replaces the rows for this session. Omit boards and
|
|
17
|
+
the painter supplies the live rows, so a language save repaints them.
|
|
18
|
+
Front rows are model, system-log, language, self-management, and Exit.
|
|
19
|
+
System-log is under 3. Language is under 4.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, boards: dict | None = None, logger=None, painter=None) -> None:
|
|
23
|
+
self.logger = logger
|
|
24
|
+
if logger is not None:
|
|
25
|
+
logger.log_message("instantiated", component="MenuModel")
|
|
26
|
+
if painter is None:
|
|
27
|
+
self.painter = MenuPainter(logger=logger)
|
|
28
|
+
else:
|
|
29
|
+
self.painter = painter
|
|
30
|
+
self.boards = boards
|
|
31
|
+
self.layer = "front"
|
|
32
|
+
self.index = 0
|
|
33
|
+
self.buffer = ""
|
|
34
|
+
self.cursor = 0
|
|
35
|
+
self.focus = "list"
|
|
36
|
+
self.error = ""
|
|
37
|
+
self.notice = ""
|
|
38
|
+
self.phase = "board"
|
|
39
|
+
self.result_text = ""
|
|
40
|
+
self.result_offset = 0
|
|
41
|
+
|
|
42
|
+
def edge_keys(self) -> tuple[int, int]:
|
|
43
|
+
return (getattr(curses, "KEY_HOME", -2), getattr(curses, "KEY_END", -3))
|
|
44
|
+
|
|
45
|
+
def highlighted_row(self):
|
|
46
|
+
"""The row under the highlight, or None when this board has no rows."""
|
|
47
|
+
rows = self.rows()
|
|
48
|
+
if not rows or self.index < 0 or self.index >= len(rows):
|
|
49
|
+
return None
|
|
50
|
+
return rows[self.index]
|
|
51
|
+
|
|
52
|
+
def buffer_text(self) -> str:
|
|
53
|
+
"""Text currently sitting in the bottom input box."""
|
|
54
|
+
return self.buffer
|
|
55
|
+
|
|
56
|
+
def rows(self) -> tuple:
|
|
57
|
+
if self.boards is not None:
|
|
58
|
+
found = self.boards.get(self.layer)
|
|
59
|
+
if found is not None:
|
|
60
|
+
return found
|
|
61
|
+
return self.painter.display_rows(self.layer)
|
|
62
|
+
|
|
63
|
+
def show_result(self, text: str) -> None:
|
|
64
|
+
self.result_text = text
|
|
65
|
+
self.result_offset = 0
|
|
66
|
+
self.phase = "result"
|
|
67
|
+
self.layer = "front"
|
|
68
|
+
self.index = 0
|
|
69
|
+
self.buffer = ""
|
|
70
|
+
self.cursor = 0
|
|
71
|
+
self.focus = "list"
|
|
72
|
+
self.error = ""
|
|
73
|
+
|
|
74
|
+
def apply_key(self, key: int, screen_height: int = 24) -> str | None:
|
|
75
|
+
"""Return 'exit', a row kind, or None to stay.
|
|
76
|
+
|
|
77
|
+
Esc (27) leaves a result page or a nested board for the front board.
|
|
78
|
+
It is not row 9 Exit. End-of-input (-1) still leaves the program from
|
|
79
|
+
the front board so a closed script does not spin. The one-second clock
|
|
80
|
+
wait never arrives here as -1. A saved-language notice lasts until
|
|
81
|
+
the next key.
|
|
82
|
+
"""
|
|
83
|
+
self.notice = ""
|
|
84
|
+
if key == 27:
|
|
85
|
+
if self.phase == "result" or self.layer != "front":
|
|
86
|
+
self.show_front()
|
|
87
|
+
return None
|
|
88
|
+
if key == -1:
|
|
89
|
+
if self.phase == "result":
|
|
90
|
+
self.show_front()
|
|
91
|
+
return None
|
|
92
|
+
if self.layer != "front":
|
|
93
|
+
self.show_front()
|
|
94
|
+
return None
|
|
95
|
+
return "exit"
|
|
96
|
+
if self.phase == "result":
|
|
97
|
+
lines = self.result_text.splitlines() or [""]
|
|
98
|
+
if self.painter._result_overflow(screen_height, len(lines)) and key in (
|
|
99
|
+
curses.KEY_UP,
|
|
100
|
+
ord("k"),
|
|
101
|
+
):
|
|
102
|
+
self._scroll_result(-1, screen_height)
|
|
103
|
+
return None
|
|
104
|
+
if self.painter._result_overflow(screen_height, len(lines)) and key in (
|
|
105
|
+
curses.KEY_DOWN,
|
|
106
|
+
ord("j"),
|
|
107
|
+
):
|
|
108
|
+
self._scroll_result(1, screen_height)
|
|
109
|
+
return None
|
|
110
|
+
self.show_front()
|
|
111
|
+
return None
|
|
112
|
+
if key in (curses.KEY_UP, curses.KEY_DOWN) or (
|
|
113
|
+
self.focus == "list" and key in (ord("k"), ord("j"))
|
|
114
|
+
):
|
|
115
|
+
self._arrow(key)
|
|
116
|
+
return None
|
|
117
|
+
if key in (curses.KEY_LEFT, curses.KEY_RIGHT) or key in self.edge_keys():
|
|
118
|
+
self._slide(key)
|
|
119
|
+
return None
|
|
120
|
+
if key in (curses.KEY_BACKSPACE, 127, 8):
|
|
121
|
+
self._backspace()
|
|
122
|
+
return None
|
|
123
|
+
if key in (10, 13, curses.KEY_ENTER):
|
|
124
|
+
return self._commit()
|
|
125
|
+
if 32 <= key < 127:
|
|
126
|
+
self._insert(chr(key))
|
|
127
|
+
return None
|
|
128
|
+
self.error = self.painter.language.text("invalid")
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def show_front(self) -> None:
|
|
132
|
+
self.phase = "board"
|
|
133
|
+
self.layer = "front"
|
|
134
|
+
self.index = 0
|
|
135
|
+
self.buffer = ""
|
|
136
|
+
self.cursor = 0
|
|
137
|
+
self.focus = "list"
|
|
138
|
+
self.error = ""
|
|
139
|
+
self.result_offset = 0
|
|
140
|
+
|
|
141
|
+
def _scroll_result(self, delta: int, screen_height: int) -> None:
|
|
142
|
+
"""Move the result window. Up/Down stay on the page when it overflows."""
|
|
143
|
+
lines = self.result_text.splitlines() or [""]
|
|
144
|
+
room = self.painter._result_room(screen_height, len(lines))
|
|
145
|
+
max_offset = max(0, len(lines) - room)
|
|
146
|
+
self.result_offset = min(max(0, self.result_offset + delta), max_offset)
|
|
147
|
+
|
|
148
|
+
def _arrow(self, key: int) -> None:
|
|
149
|
+
"""Up/Down walks the rows, then the bottom box, then the rows again."""
|
|
150
|
+
last = len(self.rows()) - 1
|
|
151
|
+
down = key in (curses.KEY_DOWN, ord("j"))
|
|
152
|
+
if self.focus == "input":
|
|
153
|
+
self.focus = "list"
|
|
154
|
+
self.index = 0 if down else last
|
|
155
|
+
return
|
|
156
|
+
if down:
|
|
157
|
+
if self.index < last:
|
|
158
|
+
self.index += 1
|
|
159
|
+
else:
|
|
160
|
+
self.focus = "input"
|
|
161
|
+
self.cursor = len(self.buffer)
|
|
162
|
+
return
|
|
163
|
+
if self.index > 0:
|
|
164
|
+
self.index -= 1
|
|
165
|
+
return
|
|
166
|
+
self.focus = "input"
|
|
167
|
+
self.cursor = len(self.buffer)
|
|
168
|
+
|
|
169
|
+
def _slide(self, key: int) -> None:
|
|
170
|
+
if self.focus != "input":
|
|
171
|
+
return
|
|
172
|
+
home, end = self.edge_keys()
|
|
173
|
+
if key == curses.KEY_LEFT:
|
|
174
|
+
self.cursor = max(0, self.cursor - 1)
|
|
175
|
+
elif key == curses.KEY_RIGHT:
|
|
176
|
+
self.cursor = min(len(self.buffer), self.cursor + 1)
|
|
177
|
+
elif key == home:
|
|
178
|
+
self.cursor = 0
|
|
179
|
+
elif key == end:
|
|
180
|
+
self.cursor = len(self.buffer)
|
|
181
|
+
|
|
182
|
+
def _insert(self, ch: str) -> None:
|
|
183
|
+
if self.focus != "input":
|
|
184
|
+
self.focus = "input"
|
|
185
|
+
self.cursor = len(self.buffer)
|
|
186
|
+
self.cursor = min(max(self.cursor, 0), len(self.buffer))
|
|
187
|
+
self.buffer = self.buffer[: self.cursor] + ch + self.buffer[self.cursor :]
|
|
188
|
+
self.cursor += 1
|
|
189
|
+
|
|
190
|
+
def _backspace(self) -> None:
|
|
191
|
+
if self.focus != "input":
|
|
192
|
+
self.focus = "input"
|
|
193
|
+
self.cursor = len(self.buffer)
|
|
194
|
+
if self.cursor <= 0 or not self.buffer:
|
|
195
|
+
self.cursor = 0
|
|
196
|
+
return
|
|
197
|
+
self.cursor = min(self.cursor, len(self.buffer))
|
|
198
|
+
self.buffer = self.buffer[: self.cursor - 1] + self.buffer[self.cursor :]
|
|
199
|
+
self.cursor -= 1
|
|
200
|
+
|
|
201
|
+
def _reject(self) -> None:
|
|
202
|
+
self.error = self.painter.language.text("invalid")
|
|
203
|
+
self.focus = "input"
|
|
204
|
+
self.cursor = len(self.buffer)
|
|
205
|
+
|
|
206
|
+
def _commit(self) -> str | None:
|
|
207
|
+
token = self.buffer.strip()
|
|
208
|
+
self.buffer = ""
|
|
209
|
+
self.cursor = 0
|
|
210
|
+
if token == "":
|
|
211
|
+
number, _short, _explain, kind = self.rows()[self.index]
|
|
212
|
+
return self._activate(number, kind)
|
|
213
|
+
if token.isdigit():
|
|
214
|
+
return self._activate_number(int(token))
|
|
215
|
+
for number, short, _explain, kind in self.rows():
|
|
216
|
+
if token == short:
|
|
217
|
+
return self._activate(number, kind)
|
|
218
|
+
mapped = self.painter.language.token_kind(token, self.layer)
|
|
219
|
+
if mapped is not None:
|
|
220
|
+
return self._activate(0, mapped)
|
|
221
|
+
kind = MenuPainter.ANY_BOARD.get(token)
|
|
222
|
+
if kind is not None:
|
|
223
|
+
return self._activate(0, kind)
|
|
224
|
+
self._reject()
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
def _activate_number(self, number: int) -> str | None:
|
|
228
|
+
for row_number, _short, _explain, kind in self.rows():
|
|
229
|
+
if row_number == number:
|
|
230
|
+
return self._activate(number, kind)
|
|
231
|
+
self._reject()
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
def _activate(self, number: int, kind: str) -> str | None:
|
|
235
|
+
self.error = ""
|
|
236
|
+
self.focus = "list"
|
|
237
|
+
self.cursor = 0
|
|
238
|
+
if kind == "exit":
|
|
239
|
+
return "exit"
|
|
240
|
+
if kind == "back":
|
|
241
|
+
self.layer = "front"
|
|
242
|
+
self.index = 0
|
|
243
|
+
return None
|
|
244
|
+
if kind == "self-management":
|
|
245
|
+
self.layer = "self"
|
|
246
|
+
self.index = 0
|
|
247
|
+
self.focus = "list"
|
|
248
|
+
self.buffer = ""
|
|
249
|
+
self.cursor = 0
|
|
250
|
+
return None
|
|
251
|
+
if kind == "system-log":
|
|
252
|
+
self.layer = "log"
|
|
253
|
+
self.index = 0
|
|
254
|
+
self.focus = "list"
|
|
255
|
+
self.buffer = ""
|
|
256
|
+
self.cursor = 0
|
|
257
|
+
return None
|
|
258
|
+
if kind == "language":
|
|
259
|
+
self.layer = "lang"
|
|
260
|
+
self.index = 0
|
|
261
|
+
self.focus = "list"
|
|
262
|
+
self.buffer = ""
|
|
263
|
+
self.cursor = 0
|
|
264
|
+
return None
|
|
265
|
+
if kind == "model":
|
|
266
|
+
self.layer = "folders"
|
|
267
|
+
self.index = 0
|
|
268
|
+
self.focus = "list"
|
|
269
|
+
self.buffer = ""
|
|
270
|
+
self.cursor = 0
|
|
271
|
+
return None
|
|
272
|
+
if kind.startswith("set-"):
|
|
273
|
+
return kind
|
|
274
|
+
if kind in ("version", "about"):
|
|
275
|
+
return kind
|
|
276
|
+
return kind
|