Update TUI controls and TTL editing

This commit is contained in:
Jimmy Allen
2026-04-01 20:12:29 -04:00
parent a994900a6a
commit 2f48ae0233
4 changed files with 351 additions and 14 deletions

View File

@ -2,6 +2,7 @@ import asyncio
import argparse
import contextlib
import logging
import re
from typing import Any
import obd
@ -31,6 +32,8 @@ MODE_LABELS = {
9: "Mode 09 Vehicle Info",
}
MODE_ORDER = [1, 2, 3, 4, 6, 7, 9]
def format_metric_value(report: Report, metric_id: str) -> str:
values = {
@ -43,6 +46,22 @@ def format_metric_value(report: Report, metric_id: str) -> str:
return values[metric_id]
def parse_duration_ms(value: str) -> int:
match = re.fullmatch(r"\s*(\d+(?:\.\d+)?)\s*(ms|s|m|h)?\s*", value, re.IGNORECASE)
if match is None:
raise ValueError(f"Invalid duration: {value}")
amount = float(match.group(1))
unit = (match.group(2) or "ms").lower()
multipliers = {
"ms": 1,
"s": 1000,
"m": 60_000,
"h": 3_600_000,
}
return max(0, int(round(amount * multipliers[unit])))
class RichLogHandler(logging.Handler):
def __init__(self, app: "OBD2App") -> None:
super().__init__()
@ -64,6 +83,12 @@ class OBD2App(App[None]):
BINDINGS = [
("q", "quit", "Quit"),
("b", "toggle_border", "Toggle border"),
("e", "focus_ttl", "Edit TTL"),
("escape", "focus_table", "Focus Table"),
("left", "previous_mode", "Prev Mode"),
("right", "next_mode", "Next Mode"),
("shift+up", "jump_table_top", "Top"),
("shift+down", "jump_table_bottom", "Bottom"),
("1", "select_mode(1)", "Mode 1"),
("2", "select_mode(2)", "Mode 2"),
("3", "select_mode(3)", "Mode 3"),
@ -103,7 +128,7 @@ class OBD2App(App[None]):
DataTable(id="commands-table"),
Container(
Label("Selected TTL (ms)", id="ttl-editor-label"),
Input(placeholder="TTL in ms", id="ttl-editor"),
Input(placeholder="e.g. 10ms, 30s, 1m, 1h", id="ttl-editor"),
id="ttl-editor-pane",
),
id="table-pane",
@ -123,13 +148,14 @@ class OBD2App(App[None]):
classes="metric-card",
)
def on_mount(self) -> None:
async def on_mount(self) -> None:
self.configure_logging()
self.interface = self.interface_factory(
self.logger,
query_rate_limit=self.query_rate_limit,
ttl_config_path=self.ttl_config_path,
)
await self.interface.reload_ttl_config(force=True)
self.configure_commands_table()
self.load_mode_table(self.selected_mode)
self.poll_task = asyncio.create_task(self.poll_commands())
@ -157,6 +183,41 @@ class OBD2App(App[None]):
self.load_mode_table(mode)
self.logger.info("Selected %s", MODE_LABELS[mode])
def action_previous_mode(self) -> None:
index = MODE_ORDER.index(self.selected_mode)
self.action_select_mode(MODE_ORDER[(index - 1) % len(MODE_ORDER)])
def action_next_mode(self) -> None:
index = MODE_ORDER.index(self.selected_mode)
self.action_select_mode(MODE_ORDER[(index + 1) % len(MODE_ORDER)])
def action_focus_ttl(self) -> None:
editor = self.query_one("#ttl-editor", Input)
editor.focus()
editor.action_end()
def action_focus_table(self) -> None:
self.query_one("#commands-table", DataTable).focus()
def action_jump_table_top(self) -> None:
table = self.query_one("#commands-table", DataTable)
table.focus()
if table.row_count > 0:
table.move_cursor(row=0)
table.scroll_to(y=0, animate=False)
def action_jump_table_bottom(self) -> None:
table = self.query_one("#commands-table", DataTable)
table.focus()
if table.row_count > 0:
last_row = table.row_count - 1
table.move_cursor(row=last_row)
table.scroll_to(y=table.max_scroll_y, animate=False)
def on_click(self, event) -> None:
if getattr(event.widget, "id", None) in {"ttl-editor", "ttl-editor-pane", "ttl-editor-label"}:
self.action_focus_ttl()
def configure_logging(self) -> None:
self.log_handler = RichLogHandler(self)
self.log_handler.setFormatter(
@ -186,11 +247,12 @@ class OBD2App(App[None]):
table = self.query_one("#commands-table", DataTable)
table.cursor_type = "row"
table.zebra_stripes = True
table.add_column("Code", key="code", width=10)
table.add_column("Name", key="name", width=28)
table.add_column("Description", key="desc", width=44)
table.add_column("TTL (ms)", key="ttl", width=12)
table.add_column("Value", key="value", width=24)
table.show_row_labels = False
table.add_column("Code", key="code", width=8)
table.add_column("Name", key="name", width=24)
table.add_column("Description", key="desc", width=34)
table.add_column("TTL (ms)", key="ttl", width=10)
table.add_column("Value", key="value", width=22)
def load_mode_table(self, mode: int) -> None:
table = self.query_one("#commands-table", DataTable)
@ -312,7 +374,9 @@ class OBD2App(App[None]):
continue
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
self.set_ttl_editor(str(event.row_key))
row = self.query_one("#commands-table", DataTable).get_row(event.row_key)
if len(row) >= 2:
self.set_ttl_editor(str(row[1]))
async def on_input_submitted(self, event: Input.Submitted) -> None:
if event.input.id != "ttl-editor" or self.interface is None or self.ttl_edit_command is None:
@ -320,7 +384,7 @@ class OBD2App(App[None]):
raw_value = event.value.strip()
try:
ttl_ms = int(raw_value)
ttl_ms = parse_duration_ms(raw_value)
except ValueError:
self.logger.error("Invalid TTL value: %s", raw_value)
self.set_ttl_editor(self.ttl_edit_command)
@ -330,6 +394,7 @@ class OBD2App(App[None]):
self.query_one("#commands-table", DataTable).update_cell(self.ttl_edit_command, "ttl", str(ttl_ms))
self.logger.info("Updated TTL for %s to %d ms", self.ttl_edit_command, ttl_ms)
self.set_ttl_editor(self.ttl_edit_command)
self.action_focus_table()
async def poll_commands(self) -> None:
while True: