codeplain 0.2.3__py3-none-any.whl → 0.2.5__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.
- {codeplain-0.2.3.dist-info → codeplain-0.2.5.dist-info}/METADATA +3 -3
- {codeplain-0.2.3.dist-info → codeplain-0.2.5.dist-info}/RECORD +43 -43
- codeplain_REST_api.py +57 -43
- config/system_config.yaml +1 -16
- file_utils.py +5 -4
- git_utils.py +43 -13
- memory_management.py +1 -1
- module_renderer.py +114 -0
- plain2code.py +91 -30
- plain2code_console.py +3 -5
- plain2code_exceptions.py +26 -6
- plain2code_logger.py +11 -5
- plain2code_utils.py +7 -5
- plain_file.py +15 -37
- plain_modules.py +1 -4
- plain_spec.py +24 -6
- render_machine/actions/create_dist.py +1 -1
- render_machine/actions/exit_with_error.py +1 -1
- render_machine/actions/prepare_testing_environment.py +1 -1
- render_machine/actions/render_conformance_tests.py +2 -4
- render_machine/actions/render_functional_requirement.py +6 -6
- render_machine/actions/run_conformance_tests.py +3 -2
- render_machine/actions/run_unit_tests.py +1 -1
- render_machine/render_context.py +3 -3
- render_machine/render_utils.py +14 -6
- standard_template_library/golang-console-app-template.plain +2 -2
- standard_template_library/python-console-app-template.plain +2 -2
- standard_template_library/typescript-react-app-template.plain +2 -2
- system_config.py +3 -11
- tests/test_imports.py +2 -2
- tests/test_plainfile.py +2 -2
- tests/test_plainfileparser.py +10 -10
- tests/test_plainspec.py +2 -2
- tests/test_requires.py +2 -1
- tui/components.py +311 -103
- tui/plain2code_tui.py +101 -52
- tui/state_handlers.py +94 -47
- tui/styles.css +240 -52
- tui/widget_helpers.py +43 -47
- {codeplain-0.2.3.dist-info → codeplain-0.2.5.dist-info}/WHEEL +0 -0
- {codeplain-0.2.3.dist-info → codeplain-0.2.5.dist-info}/entry_points.txt +0 -0
- {codeplain-0.2.3.dist-info → codeplain-0.2.5.dist-info}/licenses/LICENSE +0 -0
- /spinner.py → /tui/spinner.py +0 -0
tui/components.py
CHANGED
|
@@ -1,18 +1,32 @@
|
|
|
1
|
+
import time
|
|
1
2
|
from enum import Enum
|
|
3
|
+
from typing import Optional
|
|
2
4
|
|
|
3
5
|
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
4
6
|
from textual.message import Message
|
|
5
7
|
from textual.widgets import Button, Static
|
|
6
8
|
|
|
7
|
-
from spinner import Spinner
|
|
8
|
-
|
|
9
9
|
from .models import Substate
|
|
10
|
+
from .spinner import Spinner
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CustomFooter(Horizontal):
|
|
14
|
+
"""A custom footer with keyboard shortcuts and render ID."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, render_id: str = "", **kwargs):
|
|
17
|
+
super().__init__(**kwargs)
|
|
18
|
+
self.render_id = render_id
|
|
19
|
+
|
|
20
|
+
def compose(self):
|
|
21
|
+
yield Static("ctrl+c: quit * ctrl+l: toggle logs", classes="custom-footer-text")
|
|
22
|
+
if self.render_id:
|
|
23
|
+
yield Static(f"render id: {self.render_id}", classes="custom-footer-render-id")
|
|
10
24
|
|
|
11
25
|
|
|
12
26
|
class ScriptOutputType(str, Enum):
|
|
13
|
-
UNIT_TEST_OUTPUT_TEXT = "
|
|
14
|
-
CONFORMANCE_TEST_OUTPUT_TEXT = "
|
|
15
|
-
TESTING_ENVIRONMENT_OUTPUT_TEXT = "
|
|
27
|
+
UNIT_TEST_OUTPUT_TEXT = "Unit tests output: "
|
|
28
|
+
CONFORMANCE_TEST_OUTPUT_TEXT = "Conformance tests output: "
|
|
29
|
+
TESTING_ENVIRONMENT_OUTPUT_TEXT = "Testing environment preparation execution output: "
|
|
16
30
|
|
|
17
31
|
@staticmethod
|
|
18
32
|
def get_max_label_width(active_types: list["ScriptOutputType"]) -> int:
|
|
@@ -28,17 +42,14 @@ class ScriptOutputType(str, Enum):
|
|
|
28
42
|
return 0
|
|
29
43
|
return max(len(script_type.value) for script_type in active_types)
|
|
30
44
|
|
|
31
|
-
def get_padded_label(self
|
|
32
|
-
"""Get the label
|
|
33
|
-
|
|
34
|
-
Args:
|
|
35
|
-
active_types: List of ScriptOutputType enum members that are currently active
|
|
45
|
+
def get_padded_label(self) -> str:
|
|
46
|
+
"""Get the label left-aligned (no padding).
|
|
36
47
|
|
|
37
48
|
Returns:
|
|
38
|
-
|
|
49
|
+
Label without padding (left-aligned)
|
|
39
50
|
"""
|
|
40
|
-
|
|
41
|
-
return self.value
|
|
51
|
+
# Return label as-is without padding for left alignment
|
|
52
|
+
return self.value
|
|
42
53
|
|
|
43
54
|
|
|
44
55
|
class TUIComponents(str, Enum):
|
|
@@ -57,6 +68,9 @@ class TUIComponents(str, Enum):
|
|
|
57
68
|
FRID_PROGRESS_REFACTORING = "frid-progress-refactoring"
|
|
58
69
|
FRID_PROGRESS_CONFORMANCE_TEST = "frid-progress-conformance-test"
|
|
59
70
|
|
|
71
|
+
# Test scripts container widgets
|
|
72
|
+
TEST_SCRIPTS_CONTAINER = "test-scripts-container"
|
|
73
|
+
|
|
60
74
|
CONTENT_SWITCHER = "content-switcher"
|
|
61
75
|
DASHBOARD_VIEW = "dashboard-view"
|
|
62
76
|
LOG_VIEW = "log-view"
|
|
@@ -64,6 +78,47 @@ class TUIComponents(str, Enum):
|
|
|
64
78
|
LOG_FILTER = "log-filter"
|
|
65
79
|
|
|
66
80
|
|
|
81
|
+
class SubstateLine(Horizontal):
|
|
82
|
+
"""A single substate row with an attached timer."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, text: str, indent: str, **kwargs):
|
|
85
|
+
super().__init__(**kwargs)
|
|
86
|
+
self.text = text
|
|
87
|
+
self.indent = indent
|
|
88
|
+
self.start_time = time.monotonic()
|
|
89
|
+
self._line_widget: Static | None = None
|
|
90
|
+
|
|
91
|
+
def compose(self):
|
|
92
|
+
self._line_widget = Static(self._format_line(), classes="substate-line-text")
|
|
93
|
+
yield self._line_widget
|
|
94
|
+
|
|
95
|
+
def on_mount(self) -> None:
|
|
96
|
+
self._refresh_timer()
|
|
97
|
+
self.set_interval(1, self._refresh_timer)
|
|
98
|
+
|
|
99
|
+
def _format_timer(self) -> str:
|
|
100
|
+
elapsed = int(time.monotonic() - self.start_time)
|
|
101
|
+
if elapsed < 60:
|
|
102
|
+
return f"{elapsed}s"
|
|
103
|
+
minutes = elapsed // 60
|
|
104
|
+
seconds = elapsed % 60
|
|
105
|
+
if minutes < 60:
|
|
106
|
+
return f"{minutes}m {seconds}s"
|
|
107
|
+
hours = minutes // 60
|
|
108
|
+
return f"{hours}h {minutes % 60}m"
|
|
109
|
+
|
|
110
|
+
def _format_line(self) -> str:
|
|
111
|
+
timer = self._format_timer()
|
|
112
|
+
return f"{self.indent} └ {self.text} [#888888]({timer})[/#888888]"
|
|
113
|
+
|
|
114
|
+
def _refresh_timer(self) -> None:
|
|
115
|
+
try:
|
|
116
|
+
if self._line_widget:
|
|
117
|
+
self._line_widget.update(self._format_line())
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
|
|
67
122
|
class ProgressItem(Vertical):
|
|
68
123
|
"""A vertical container for a status, description, and substates."""
|
|
69
124
|
|
|
@@ -160,7 +215,7 @@ class ProgressItem(Vertical):
|
|
|
160
215
|
|
|
161
216
|
for substate in substates:
|
|
162
217
|
# Render the current substate
|
|
163
|
-
substate_widget =
|
|
218
|
+
substate_widget = SubstateLine(substate.text, indent, classes="substate-row")
|
|
164
219
|
await container.mount(substate_widget)
|
|
165
220
|
|
|
166
221
|
# Recursively render children if they exist
|
|
@@ -176,6 +231,111 @@ class ProgressItem(Vertical):
|
|
|
176
231
|
pass
|
|
177
232
|
|
|
178
233
|
|
|
234
|
+
class RenderingInfoBox(Vertical):
|
|
235
|
+
"""Responsive container for module and functionality information."""
|
|
236
|
+
|
|
237
|
+
def __init__(self, **kwargs):
|
|
238
|
+
super().__init__(**kwargs)
|
|
239
|
+
self.module_text = ""
|
|
240
|
+
self.functionality_text = ""
|
|
241
|
+
self.module_widget: Static | None = None
|
|
242
|
+
self.functionality_widget: Static | None = None
|
|
243
|
+
|
|
244
|
+
def update_module(self, text: str) -> None:
|
|
245
|
+
"""Update the module name display."""
|
|
246
|
+
self.module_text = text
|
|
247
|
+
self._refresh_content()
|
|
248
|
+
|
|
249
|
+
def update_functionality(self, text: str) -> None:
|
|
250
|
+
"""Update the functionality text display."""
|
|
251
|
+
self.functionality_text = text
|
|
252
|
+
self._refresh_content()
|
|
253
|
+
|
|
254
|
+
def _refresh_content(self) -> None:
|
|
255
|
+
"""Refresh text inside the box."""
|
|
256
|
+
if self.module_widget is not None:
|
|
257
|
+
self.module_widget.update(self.module_text or "")
|
|
258
|
+
if self.functionality_widget is not None:
|
|
259
|
+
self.functionality_widget.update(self.functionality_text or "")
|
|
260
|
+
|
|
261
|
+
def on_mount(self) -> None:
|
|
262
|
+
"""Initialize default labels on mount."""
|
|
263
|
+
self.module_text = "Module: "
|
|
264
|
+
self.functionality_text = "Functionality:"
|
|
265
|
+
self._refresh_content()
|
|
266
|
+
|
|
267
|
+
def compose(self):
|
|
268
|
+
self.module_widget = Static(self.module_text, classes="rendering-info-row")
|
|
269
|
+
self.functionality_widget = Static(self.functionality_text, classes="rendering-info-row")
|
|
270
|
+
yield Static("module status", classes="rendering-info-title")
|
|
271
|
+
with Vertical(classes="rendering-info-box"):
|
|
272
|
+
yield self.module_widget
|
|
273
|
+
yield self.functionality_widget
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class TestScriptsContainer(Vertical):
|
|
277
|
+
"""Container with ASCII border for test script outputs."""
|
|
278
|
+
|
|
279
|
+
def __init__(
|
|
280
|
+
self,
|
|
281
|
+
show_unit_test: bool = True,
|
|
282
|
+
show_conformance_test: bool = True,
|
|
283
|
+
show_testing_env: bool = True,
|
|
284
|
+
**kwargs,
|
|
285
|
+
):
|
|
286
|
+
super().__init__(**kwargs)
|
|
287
|
+
self.show_unit_test = show_unit_test
|
|
288
|
+
self.show_conformance_test = show_conformance_test
|
|
289
|
+
self.show_testing_env = show_testing_env
|
|
290
|
+
self.unit_test_text = ScriptOutputType.UNIT_TEST_OUTPUT_TEXT.value
|
|
291
|
+
self.conformance_test_text = ScriptOutputType.CONFORMANCE_TEST_OUTPUT_TEXT.value
|
|
292
|
+
self.testing_env_text = ScriptOutputType.TESTING_ENVIRONMENT_OUTPUT_TEXT.value
|
|
293
|
+
self.unit_widget: Static | None = None
|
|
294
|
+
self.conformance_widget: Static | None = None
|
|
295
|
+
self.testing_widget: Static | None = None
|
|
296
|
+
|
|
297
|
+
def update_unit_test(self, text: str) -> None:
|
|
298
|
+
"""Update unit test output and refresh."""
|
|
299
|
+
self.unit_test_text = text
|
|
300
|
+
self._refresh_content()
|
|
301
|
+
|
|
302
|
+
def update_conformance_test(self, text: str) -> None:
|
|
303
|
+
"""Update conformance test output and refresh."""
|
|
304
|
+
self.conformance_test_text = text
|
|
305
|
+
self._refresh_content()
|
|
306
|
+
|
|
307
|
+
def update_testing_env(self, text: str) -> None:
|
|
308
|
+
"""Update testing env output and refresh."""
|
|
309
|
+
self.testing_env_text = text
|
|
310
|
+
self._refresh_content()
|
|
311
|
+
|
|
312
|
+
def _refresh_content(self) -> None:
|
|
313
|
+
"""Refresh the test script rows."""
|
|
314
|
+
if self.unit_widget is not None:
|
|
315
|
+
self.unit_widget.update(self.unit_test_text)
|
|
316
|
+
self.unit_widget.display = self.show_unit_test
|
|
317
|
+
if self.conformance_widget is not None:
|
|
318
|
+
self.conformance_widget.update(self.conformance_test_text)
|
|
319
|
+
self.conformance_widget.display = self.show_conformance_test
|
|
320
|
+
if self.testing_widget is not None:
|
|
321
|
+
self.testing_widget.update(self.testing_env_text)
|
|
322
|
+
self.testing_widget.display = self.show_testing_env
|
|
323
|
+
|
|
324
|
+
def on_mount(self) -> None:
|
|
325
|
+
"""Initialize the box on mount."""
|
|
326
|
+
self._refresh_content()
|
|
327
|
+
|
|
328
|
+
def compose(self):
|
|
329
|
+
yield Static("testing status", classes="test-scripts-title")
|
|
330
|
+
with Vertical(classes="test-scripts-box"):
|
|
331
|
+
self.unit_widget = Static(self.unit_test_text, classes="test-script-row")
|
|
332
|
+
self.conformance_widget = Static(self.conformance_test_text, classes="test-script-row")
|
|
333
|
+
self.testing_widget = Static(self.testing_env_text, classes="test-script-row")
|
|
334
|
+
yield self.unit_widget
|
|
335
|
+
yield self.conformance_widget
|
|
336
|
+
yield self.testing_widget
|
|
337
|
+
|
|
338
|
+
|
|
179
339
|
class FRIDProgress(Vertical):
|
|
180
340
|
"""A widget to display the status of subcomponent tasks."""
|
|
181
341
|
|
|
@@ -185,16 +345,24 @@ class FRIDProgress(Vertical):
|
|
|
185
345
|
REFACTORING_TEXT = "Refactoring"
|
|
186
346
|
CONFORMANCE_TEST_VALIDATION_TEXT = "Conformance tests"
|
|
187
347
|
|
|
188
|
-
RENDERING_MODULE_TEXT = "
|
|
189
|
-
RENDERING_FUNCTIONALITY_TEXT = "
|
|
348
|
+
RENDERING_MODULE_TEXT = "Module: "
|
|
349
|
+
RENDERING_FUNCTIONALITY_TEXT = "Functionality:"
|
|
190
350
|
|
|
191
|
-
def __init__(
|
|
351
|
+
def __init__(
|
|
352
|
+
self,
|
|
353
|
+
unittests_script: Optional[str],
|
|
354
|
+
conformance_tests_script: Optional[str],
|
|
355
|
+
**kwargs,
|
|
356
|
+
):
|
|
192
357
|
super().__init__(**kwargs)
|
|
358
|
+
self.unittests_script = unittests_script
|
|
359
|
+
self.conformance_tests_script = conformance_tests_script
|
|
193
360
|
|
|
194
361
|
def update_fr_text(self, text: str) -> None:
|
|
195
362
|
try:
|
|
196
|
-
|
|
197
|
-
|
|
363
|
+
# Update the rendering info box instead
|
|
364
|
+
info_box = self.query_one(RenderingInfoBox)
|
|
365
|
+
info_box.update_functionality(text)
|
|
198
366
|
except Exception:
|
|
199
367
|
pass
|
|
200
368
|
|
|
@@ -209,45 +377,31 @@ class FRIDProgress(Vertical):
|
|
|
209
377
|
self.border_title = "FRID Progress"
|
|
210
378
|
|
|
211
379
|
def compose(self):
|
|
212
|
-
yield
|
|
213
|
-
yield Static(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
self.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
super().__init__(arrow, **kwargs)
|
|
238
|
-
self.classes = "log-arrow"
|
|
239
|
-
|
|
240
|
-
def on_click(self, event):
|
|
241
|
-
"""Notify parent to toggle expansion."""
|
|
242
|
-
# Bubble up to parent CollapsibleLogEntry
|
|
243
|
-
event.stop()
|
|
244
|
-
parent = self.parent
|
|
245
|
-
if isinstance(parent, CollapsibleLogEntry):
|
|
246
|
-
parent.toggle_expansion()
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
class CollapsibleLogEntry(Horizontal):
|
|
250
|
-
"""A single collapsible log entry that can be clicked to expand/collapse."""
|
|
380
|
+
yield RenderingInfoBox()
|
|
381
|
+
yield Static("rendering status", classes="frid-state-machine-title")
|
|
382
|
+
with Vertical(classes="frid-state-machine-box"):
|
|
383
|
+
yield ProgressItem(
|
|
384
|
+
self.IMPLEMENTING_FUNCTIONALITY_TEXT,
|
|
385
|
+
id=TUIComponents.FRID_PROGRESS_RENDER_FR.value,
|
|
386
|
+
)
|
|
387
|
+
if self.unittests_script is not None:
|
|
388
|
+
yield ProgressItem(
|
|
389
|
+
self.UNIT_TEST_VALIDATION_TEXT,
|
|
390
|
+
id=TUIComponents.FRID_PROGRESS_UNIT_TEST.value,
|
|
391
|
+
)
|
|
392
|
+
yield ProgressItem(
|
|
393
|
+
self.REFACTORING_TEXT,
|
|
394
|
+
id=TUIComponents.FRID_PROGRESS_REFACTORING.value,
|
|
395
|
+
)
|
|
396
|
+
if self.conformance_tests_script is not None:
|
|
397
|
+
yield ProgressItem(
|
|
398
|
+
self.CONFORMANCE_TEST_VALIDATION_TEXT,
|
|
399
|
+
id=TUIComponents.FRID_PROGRESS_CONFORMANCE_TEST.value,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class LogEntry(Vertical):
|
|
404
|
+
"""A single log entry that can be expanded to show details."""
|
|
251
405
|
|
|
252
406
|
def __init__(self, logger_name: str, level: str, message: str, timestamp: str = "", **kwargs):
|
|
253
407
|
super().__init__(**kwargs)
|
|
@@ -259,36 +413,58 @@ class CollapsibleLogEntry(Horizontal):
|
|
|
259
413
|
self.classes = f"log-entry log-{level.lower()}"
|
|
260
414
|
|
|
261
415
|
def compose(self):
|
|
262
|
-
#
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
416
|
+
# Main row: just the message with a clickable indicator
|
|
417
|
+
with Horizontal(classes="log-main-row"):
|
|
418
|
+
# Expandable indicator
|
|
419
|
+
yield Static("▶", classes="log-expand-indicator")
|
|
420
|
+
|
|
421
|
+
time_part = self.timestamp.split()[-1] if self.timestamp else ""
|
|
422
|
+
time_prefix = f"[#888888][{time_part}][/#888888] " if time_part else ""
|
|
423
|
+
indent_spaces = len(f"[{time_part}] ") if time_part else 0
|
|
424
|
+
|
|
425
|
+
message_body = self.message
|
|
426
|
+
if any(
|
|
427
|
+
keyword in self.message.lower()
|
|
428
|
+
for keyword in ["completed", "success", "successfully", "passed", "done", "✓"]
|
|
429
|
+
):
|
|
430
|
+
message_body = f"[green]✓[/green] {message_body}"
|
|
431
|
+
|
|
432
|
+
if indent_spaces and "\n" in message_body:
|
|
433
|
+
message_body = message_body.replace("\n", "\n" + " " * indent_spaces)
|
|
434
|
+
|
|
435
|
+
yield Static(f"{time_prefix}{message_body}", classes="log-col-message")
|
|
436
|
+
|
|
437
|
+
# Details row (hidden by default) - vertical layout
|
|
438
|
+
with Vertical(id=f"log-details-{id(self)}", classes="log-details-row"):
|
|
439
|
+
location = self.logger_name
|
|
440
|
+
if len(location) > 20:
|
|
441
|
+
location = location[:20] + "..."
|
|
442
|
+
yield Static(f" [#888]level:[/#888] {self.level}", classes="log-details-text")
|
|
443
|
+
yield Static(f" [#888]location:[/#888] {location}", classes="log-details-text")
|
|
444
|
+
|
|
445
|
+
def on_mount(self) -> None:
|
|
446
|
+
"""Hide details on mount."""
|
|
447
|
+
try:
|
|
448
|
+
details = self.query_one(f"#log-details-{id(self)}")
|
|
449
|
+
details.display = False
|
|
450
|
+
except Exception:
|
|
451
|
+
pass
|
|
452
|
+
|
|
453
|
+
def on_click(self) -> None:
|
|
454
|
+
"""Toggle details visibility on click."""
|
|
455
|
+
try:
|
|
456
|
+
details = self.query_one(f"#log-details-{id(self)}")
|
|
457
|
+
indicator = self.query_one(".log-expand-indicator", Static)
|
|
458
|
+
|
|
459
|
+
self.is_expanded = not self.is_expanded
|
|
460
|
+
details.display = self.is_expanded
|
|
461
|
+
indicator.update("▼" if self.is_expanded else "▶")
|
|
462
|
+
except Exception:
|
|
463
|
+
pass
|
|
288
464
|
|
|
289
465
|
|
|
290
466
|
class StructuredLogView(VerticalScroll):
|
|
291
|
-
"""A scrollable container for
|
|
467
|
+
"""A scrollable container for log entries displayed as a table."""
|
|
292
468
|
|
|
293
469
|
# Log level hierarchy (lower number = lower priority)
|
|
294
470
|
LOG_LEVELS = {
|
|
@@ -296,7 +472,6 @@ class StructuredLogView(VerticalScroll):
|
|
|
296
472
|
"INFO": 1,
|
|
297
473
|
"WARNING": 2,
|
|
298
474
|
"ERROR": 3,
|
|
299
|
-
"CRITICAL": 4,
|
|
300
475
|
}
|
|
301
476
|
|
|
302
477
|
def __init__(self, **kwargs):
|
|
@@ -311,7 +486,17 @@ class StructuredLogView(VerticalScroll):
|
|
|
311
486
|
|
|
312
487
|
async def add_log(self, logger_name: str, level: str, message: str, timestamp: str = ""):
|
|
313
488
|
"""Add a new log entry."""
|
|
314
|
-
|
|
489
|
+
# Check if this is a success message that should have spacing before it
|
|
490
|
+
is_success_message = any(
|
|
491
|
+
keyword in message.lower() for keyword in ["completed", "success", "successfully", "passed", "done", "✓"]
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
# Add empty line before success messages
|
|
495
|
+
if is_success_message:
|
|
496
|
+
spacer = Static("", classes="log-spacer")
|
|
497
|
+
await self.mount(spacer)
|
|
498
|
+
|
|
499
|
+
entry = LogEntry(logger_name, level, message, timestamp)
|
|
315
500
|
|
|
316
501
|
# Only show if level is >= minimum level
|
|
317
502
|
if not self._should_show_log(level):
|
|
@@ -326,7 +511,7 @@ class StructuredLogView(VerticalScroll):
|
|
|
326
511
|
self.min_level = min_level
|
|
327
512
|
|
|
328
513
|
# Update visibility of all existing log entries
|
|
329
|
-
for entry in self.query(
|
|
514
|
+
for entry in self.query(LogEntry):
|
|
330
515
|
entry.display = self._should_show_log(entry.level)
|
|
331
516
|
|
|
332
517
|
|
|
@@ -341,17 +526,34 @@ class LogFilterChanged(Message):
|
|
|
341
526
|
class LogLevelFilter(Horizontal):
|
|
342
527
|
"""Filter logs by minimum level with buttons."""
|
|
343
528
|
|
|
344
|
-
LEVELS = ["
|
|
529
|
+
LEVELS = ["debug", "info", "warning", "error"]
|
|
530
|
+
|
|
531
|
+
# Make the widget focusable to receive keyboard events
|
|
532
|
+
can_focus = True
|
|
345
533
|
|
|
346
534
|
def __init__(self, **kwargs):
|
|
347
535
|
super().__init__(**kwargs)
|
|
348
|
-
self.current_level = "
|
|
536
|
+
self.current_level = "INFO"
|
|
537
|
+
self.current_index = self.LEVELS.index(self.current_level.lower())
|
|
349
538
|
|
|
350
539
|
def compose(self):
|
|
351
|
-
yield Static("
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
540
|
+
yield Static("level: ", classes="filter-label")
|
|
541
|
+
with Horizontal(classes="filter-buttons-container"):
|
|
542
|
+
for level in self.LEVELS:
|
|
543
|
+
variant = "primary" if level.upper() == self.current_level else "default"
|
|
544
|
+
btn = Button(level.upper(), id=f"filter-{level.lower()}", variant=variant, classes="filter-button") # type: ignore[arg-type]
|
|
545
|
+
btn.can_focus = False # Prevent buttons from receiving focus
|
|
546
|
+
yield btn
|
|
547
|
+
|
|
548
|
+
def on_key(self, event):
|
|
549
|
+
"""Handle tab key to cycle through levels."""
|
|
550
|
+
if event.key == "tab":
|
|
551
|
+
# Move to next level
|
|
552
|
+
self.current_index = (self.current_index + 1) % len(self.LEVELS)
|
|
553
|
+
new_level = self.LEVELS[self.current_index].upper()
|
|
554
|
+
self._update_level(new_level)
|
|
555
|
+
event.prevent_default()
|
|
556
|
+
event.stop()
|
|
355
557
|
|
|
356
558
|
def on_button_pressed(self, event):
|
|
357
559
|
"""Handle level button press."""
|
|
@@ -359,14 +561,20 @@ class LogLevelFilter(Horizontal):
|
|
|
359
561
|
button_id = event.button.id
|
|
360
562
|
if button_id and button_id.startswith("filter-"):
|
|
361
563
|
level = button_id.replace("filter-", "").upper()
|
|
362
|
-
self.
|
|
564
|
+
self.current_index = self.LEVELS.index(level.lower())
|
|
565
|
+
self._update_level(level)
|
|
363
566
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
567
|
+
def _update_level(self, level: str):
|
|
568
|
+
"""Update the current level and button states."""
|
|
569
|
+
self.current_level = level
|
|
570
|
+
|
|
571
|
+
# Update button variants
|
|
572
|
+
for btn in self.query(Button):
|
|
573
|
+
if btn.id == f"filter-{level.lower()}":
|
|
574
|
+
btn.variant = "primary"
|
|
575
|
+
else:
|
|
576
|
+
btn.variant = "default"
|
|
577
|
+
btn.refresh() # Force immediate visual update
|
|
370
578
|
|
|
371
|
-
|
|
372
|
-
|
|
579
|
+
# Notify parent to refresh log visibility
|
|
580
|
+
self.post_message(LogFilterChanged(level))
|