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.
@@ -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