Skip to content

Commit 0f23e43

Browse files
kaja-sTjaz Erzen
authored andcommitted
Redesigned the logs display
1 parent d751780 commit 0f23e43

5 files changed

Lines changed: 255 additions & 117 deletions

File tree

.gitignore

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
__pycache__
22
.DS_Store
33

4-
# Plain
4+
55
examples/**/build*/
66
examples/**/conformance_tests/
77
examples/**/conformance_tests.backup/
@@ -23,10 +23,10 @@ examples/**/node_plain_modules/
2323
*.log
2424

2525
.venv
26-
build
2726
dist
2827
*.egg-info
2928

3029
.env
3130

32-
.coverage
31+
.coverage
32+
logging_config.yaml

plain2code_logger.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def __init__(self, event_bus: EventBus):
4242
super().__init__()
4343
self.event_bus = event_bus
4444
self._buffer = []
45+
self.start_time = time.time() # Record start time for offset calculation
4546

4647
# Register to be notified when event bus is ready
4748
self.event_bus.on_ready(self._flush_buffer)
@@ -50,7 +51,15 @@ def emit(self, record):
5051
try:
5152
# Extract structured data from the log record
5253

53-
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(record.created))
54+
# Original timestamp format (absolute time):
55+
# timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(record.created))
56+
57+
# Calculate offset time from start of generation
58+
offset_seconds = record.created - self.start_time
59+
minutes = int(offset_seconds // 60)
60+
seconds = int(offset_seconds % 60)
61+
milliseconds = int((offset_seconds % 1) * 100)
62+
timestamp = f"{minutes:02d}:{seconds:02d}:{milliseconds:02d}"
5463
event = LogMessageEmitted(
5564
logger_name=record.name,
5665
level=record.levelname,

tui/components.py

Lines changed: 87 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@
99
from .models import Substate
1010

1111

12+
class CustomFooter(Horizontal):
13+
"""A custom footer with keyboard shortcuts and render ID."""
14+
15+
def __init__(self, render_id: str = "", **kwargs):
16+
super().__init__(**kwargs)
17+
self.render_id = render_id
18+
19+
def compose(self):
20+
yield Static("ctrl+c: quit * ctrl+l: toggle logs", classes="custom-footer-text")
21+
if self.render_id:
22+
yield Static(f"render id: {self.render_id}", classes="custom-footer-render-id")
23+
24+
1225
class ScriptOutputType(str, Enum):
1326
UNIT_TEST_OUTPUT_TEXT = "Latest unit test script execution output: "
1427
CONFORMANCE_TEST_OUTPUT_TEXT = "Latest conformance tests script execution output: "
@@ -229,74 +242,50 @@ def compose(self):
229242
)
230243

231244

232-
class ClickableArrow(Static):
233-
"""A clickable arrow widget for expanding/collapsing logs."""
234-
235-
def __init__(self, is_expanded: bool = False, **kwargs):
236-
arrow = "▼" if is_expanded else "▶"
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."""
245+
class LogEntry(Horizontal):
246+
"""A single log entry displayed as a table row with 4 columns."""
251247

252248
def __init__(self, logger_name: str, level: str, message: str, timestamp: str = "", **kwargs):
253249
super().__init__(**kwargs)
254250
self.logger_name = logger_name
255251
self.level = level
256252
self.message = message
257253
self.timestamp = timestamp
258-
self.is_expanded = False
259254
self.classes = f"log-entry log-{level.lower()}"
260255

261256
def compose(self):
262-
# Start with collapsed view - arrow and full message text side by side
263-
yield ClickableArrow(id="arrow")
264-
yield Static(self.message, classes="log-summary")
265-
266-
def toggle_expansion(self):
267-
"""Toggle the expansion state of this log entry."""
268-
self.is_expanded = not self.is_expanded
269-
self.call_later(self.refresh_display)
270-
271-
async def refresh_display(self):
272-
"""Update the display based on expanded state."""
273-
# Remove all children
274-
await self.remove_children()
275-
276-
if self.is_expanded:
277-
# Expanded view: shows arrow, full message, and structured metadata
278-
await self.mount(ClickableArrow(is_expanded=True))
279-
expanded_text = f"{self.message}\n"
280-
expanded_text += f" Level: {self.level}\n"
281-
expanded_text += f" Location: {self.logger_name}\n"
282-
expanded_text += f" Time: {self.timestamp}"
283-
await self.mount(Static(expanded_text, classes="log-expanded"))
284-
else:
285-
# Collapsed view: shows arrow and full message only
286-
await self.mount(ClickableArrow(is_expanded=False))
287-
await self.mount(Static(self.message, classes="log-summary"))
257+
# Column 1: Offset time with brackets (just the time portion, not full timestamp)
258+
time_part = self.timestamp.split()[-1] if self.timestamp else ""
259+
yield Static(f"[{time_part}]", classes="log-col-time")
260+
261+
# Column 2: Level
262+
yield Static(self.level, classes="log-col-level")
263+
264+
# Column 3: Location (logger name) - truncate if longer than 20 characters
265+
location = self.logger_name
266+
if len(location) > 9:
267+
location = location[:9] + "..."
268+
yield Static(location, classes="log-col-location")
269+
270+
# Column 4: Message with green checkmark for successful outcomes
271+
message_text = self.message
272+
# Add green checkmark for messages indicating success
273+
if any(keyword in message_text.lower() for keyword in [
274+
"completed", "success", "successfully", "passed", "done", "✓"
275+
]):
276+
message_text = f"[green]✓[/green] {message_text}"
277+
yield Static(message_text, classes="log-col-message")
288278

289279

290280
class StructuredLogView(VerticalScroll):
291-
"""A scrollable container for collapsible log entries."""
281+
"""A scrollable container for log entries displayed as a table."""
292282

293283
# Log level hierarchy (lower number = lower priority)
294284
LOG_LEVELS = {
295285
"DEBUG": 0,
296286
"INFO": 1,
297287
"WARNING": 2,
298288
"ERROR": 3,
299-
"CRITICAL": 4,
300289
}
301290

302291
def __init__(self, **kwargs):
@@ -311,7 +300,17 @@ def _should_show_log(self, level: str) -> bool:
311300

312301
async def add_log(self, logger_name: str, level: str, message: str, timestamp: str = ""):
313302
"""Add a new log entry."""
314-
entry = CollapsibleLogEntry(logger_name, level, message, timestamp)
303+
# Check if this is a success message that should have spacing before it
304+
is_success_message = any(keyword in message.lower() for keyword in [
305+
"completed", "success", "successfully", "passed", "done", "✓"
306+
])
307+
308+
# Add empty line before success messages
309+
if is_success_message:
310+
spacer = Static("", classes="log-spacer")
311+
await self.mount(spacer)
312+
313+
entry = LogEntry(logger_name, level, message, timestamp)
315314

316315
# Only show if level is >= minimum level
317316
if not self._should_show_log(level):
@@ -326,7 +325,7 @@ def filter_logs(self, min_level: str):
326325
self.min_level = min_level
327326

328327
# Update visibility of all existing log entries
329-
for entry in self.query(CollapsibleLogEntry):
328+
for entry in self.query(LogEntry):
330329
entry.display = self._should_show_log(entry.level)
331330

332331

@@ -341,32 +340,55 @@ def __init__(self, min_level: str):
341340
class LogLevelFilter(Horizontal):
342341
"""Filter logs by minimum level with buttons."""
343342

344-
LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
343+
LEVELS = ["debug", "info", "warning", "error"]
344+
345+
# Make the widget focusable to receive keyboard events
346+
can_focus = True
345347

346348
def __init__(self, **kwargs):
347349
super().__init__(**kwargs)
348-
self.current_level = "DEBUG"
350+
self.current_level = "INFO"
351+
self.current_index = self.LEVELS.index(self.current_level.lower())
349352

350353
def compose(self):
351-
yield Static("Level: ", classes="filter-label")
352-
for level in self.LEVELS:
353-
variant = "primary" if level == self.current_level else "default"
354-
yield Button(level, id=f"filter-{level.lower()}", variant=variant, classes="filter-button") # type: ignore[arg-type]
354+
yield Static("level: ", classes="filter-label")
355+
with Horizontal(classes="filter-buttons-container"):
356+
for level in self.LEVELS:
357+
variant = "primary" if level.upper() == self.current_level else "default"
358+
btn = Button(level.upper(), id=f"filter-{level.lower()}", variant=variant, classes="filter-button") # type: ignore[arg-type]
359+
btn.can_focus = False # Prevent buttons from receiving focus
360+
yield btn
361+
362+
def on_key(self, event):
363+
"""Handle tab key to cycle through levels."""
364+
if event.key == "tab":
365+
# Move to next level
366+
self.current_index = (self.current_index + 1) % len(self.LEVELS)
367+
new_level = self.LEVELS[self.current_index].upper()
368+
self._update_level(new_level)
369+
event.prevent_default()
370+
event.stop()
355371

356372
def on_button_pressed(self, event):
357373
"""Handle level button press."""
358374
# Extract level from button ID
359375
button_id = event.button.id
360376
if button_id and button_id.startswith("filter-"):
361377
level = button_id.replace("filter-", "").upper()
362-
self.current_level = level
378+
self.current_index = self.LEVELS.index(level.lower())
379+
self._update_level(level)
363380

364-
# Update button variants
365-
for btn in self.query(Button):
366-
if btn.id == f"filter-{level.lower()}":
367-
btn.variant = "primary"
368-
else:
369-
btn.variant = "default"
381+
def _update_level(self, level: str):
382+
"""Update the current level and button states."""
383+
self.current_level = level
384+
385+
# Update button variants
386+
for btn in self.query(Button):
387+
if btn.id == f"filter-{level.lower()}":
388+
btn.variant = "primary"
389+
else:
390+
btn.variant = "default"
391+
btn.refresh() # Force immediate visual update
370392

371-
# Notify parent to refresh log visibility
372-
self.post_message(LogFilterChanged(level))
393+
# Notify parent to refresh log visibility
394+
self.post_message(LogFilterChanged(level))

tui/plain2code_tui.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from textual.app import App, ComposeResult
77
from textual.containers import Vertical, VerticalScroll
8-
from textual.widgets import ContentSwitcher, Footer, Header, Static
8+
from textual.widgets import ContentSwitcher, Header, Static
99
from textual.worker import Worker, WorkerState
1010

1111
from event_bus import EventBus
@@ -28,6 +28,7 @@
2828
ScriptOutputType,
2929
StructuredLogView,
3030
TUIComponents,
31+
CustomFooter,
3132
)
3233
from .state_handlers import (
3334
ConformanceTestsHandler,
@@ -119,13 +120,17 @@ def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
119120

120121
def compose(self) -> ComposeResult:
121122
"""Create child widgets for the app."""
122-
yield Header()
123123
with ContentSwitcher(id=TUIComponents.CONTENT_SWITCHER.value, initial=TUIComponents.DASHBOARD_VIEW.value):
124124
with Vertical(id=TUIComponents.DASHBOARD_VIEW.value):
125125
with VerticalScroll():
126126
yield Static(
127-
f"Render ID: {self.render_id}",
128-
id=TUIComponents.RENDER_ID_WIDGET.value,
127+
f" ___ ___ __| | ___ _ __ | | __ _(_)_ __\n"
128+
f" / __/ _ \\ / _` |/ _ \\ '_ \\| |/ _` | | '_ \\\n"
129+
f"| (_| (_) | (_| | __/ |_) | | (_| | | | | |\n"
130+
f" \\___\\___/ \\__,_|\\___| .__/|_|\\__,_|_|_| |_|\n"
131+
f" |_|",
132+
id="codeplain-header",
133+
classes="codeplain-header"
129134
)
130135
yield Static(
131136
"Rendering in progress...",
@@ -158,8 +163,9 @@ def compose(self) -> ComposeResult:
158163
)
159164
with Vertical(id=TUIComponents.LOG_VIEW.value):
160165
yield LogLevelFilter(id=TUIComponents.LOG_FILTER.value)
166+
yield Static("", classes="filter-spacer")
161167
yield StructuredLogView(id=TUIComponents.LOG_WIDGET.value)
162-
yield Footer()
168+
yield CustomFooter(render_id=self.render_id)
163169

164170
def action_toggle_logs(self) -> None:
165171
"""Toggle between dashboard and log view."""

0 commit comments

Comments
 (0)