feat(console): add curses VT220 renderer with full widget set
Implements BaseRenderer abstract interface and CursesRenderer with green-on-black VT220 aesthetic. Includes all 18 widget methods: header, footer, menu, table, detail, form, filter list, comparison view, box drawing, message dialogs, and input prompts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
152
console/renderers/base.py
Normal file
152
console/renderers/base.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
Abstract base renderer interface for the AUTOPARTES console application.
|
||||
|
||||
Every renderer (curses VT220, Textual/Rich, etc.) must subclass
|
||||
:class:`BaseRenderer` and implement all of its methods. Screens call
|
||||
these methods without knowing which backend is active.
|
||||
"""
|
||||
|
||||
|
||||
class BaseRenderer:
|
||||
"""Abstract interface that all renderers must implement.
|
||||
|
||||
Methods raise :exc:`NotImplementedError` so that missing overrides
|
||||
are caught immediately at runtime.
|
||||
"""
|
||||
|
||||
# ── Lifecycle ────────────────────────────────────────────────────
|
||||
|
||||
def init_screen(self):
|
||||
"""Initialise the terminal / display backend."""
|
||||
raise NotImplementedError
|
||||
|
||||
def cleanup(self):
|
||||
"""Restore the terminal to its original state."""
|
||||
raise NotImplementedError
|
||||
|
||||
# ── Screen queries ───────────────────────────────────────────────
|
||||
|
||||
def get_size(self) -> tuple:
|
||||
"""Return ``(height, width)`` of the usable display area."""
|
||||
raise NotImplementedError
|
||||
|
||||
# ── Primitive operations ─────────────────────────────────────────
|
||||
|
||||
def clear(self):
|
||||
"""Clear the entire screen buffer."""
|
||||
raise NotImplementedError
|
||||
|
||||
def refresh(self):
|
||||
"""Flush the screen buffer to the terminal."""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_key(self) -> int:
|
||||
"""Block until a key is pressed and return its key code."""
|
||||
raise NotImplementedError
|
||||
|
||||
# ── High-level widgets ───────────────────────────────────────────
|
||||
|
||||
def draw_header(self, title, subtitle=''):
|
||||
"""Draw the application header bar on the top two rows.
|
||||
|
||||
*title* is left-aligned; *subtitle* is right-aligned.
|
||||
Row 1 is a horizontal separator.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def draw_footer(self, key_labels):
|
||||
"""Draw the footer bar on the bottom two rows.
|
||||
|
||||
*key_labels* is a list of ``(key, description)`` tuples,
|
||||
e.g. ``[("F1", "Ayuda"), ("ESC", "Atras")]``.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def draw_menu(self, items, selected_index=0, title=''):
|
||||
"""Draw a numbered menu list starting at row 3.
|
||||
|
||||
*items* is a list of ``(number, label)`` tuples.
|
||||
Separator items have ``number == '---'``.
|
||||
The item at *selected_index* is highlighted.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def draw_table(self, headers, rows, widths, page_info=None,
|
||||
selected_row=-1):
|
||||
"""Draw a columnar data table.
|
||||
|
||||
*headers*: list of column header strings.
|
||||
*rows*: list of row tuples (each tuple matches *headers*).
|
||||
*widths*: list of int column widths.
|
||||
*page_info*: optional dict ``{page, total_pages, total_rows}``.
|
||||
*selected_row*: index of the highlighted row (-1 = none).
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def draw_detail(self, fields, title=''):
|
||||
"""Draw a detail view with label-value pairs.
|
||||
|
||||
*fields* is a list of ``(label, value)`` tuples displayed as
|
||||
``Label........: Value``.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def draw_form(self, fields, focused_index=0, title=''):
|
||||
"""Draw an editable form.
|
||||
|
||||
*fields* is a list of dicts with keys:
|
||||
``label``, ``value``, ``width``, ``type``, ``hint``.
|
||||
The field at *focused_index* uses the active style.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def draw_filter_list(self, items, filter_text, selected_index,
|
||||
title=''):
|
||||
"""Draw a filterable list with a text input at the top.
|
||||
|
||||
*items*: list of ``(number, label)`` tuples.
|
||||
*filter_text*: current filter string.
|
||||
*selected_index*: highlighted item index.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def draw_comparison(self, columns, title=''):
|
||||
"""Draw a side-by-side comparison view.
|
||||
|
||||
*columns* is a list of dicts, each with:
|
||||
``header`` (str) and ``rows`` (list of ``(label, value)``).
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
# ── Low-level drawing ────────────────────────────────────────────
|
||||
|
||||
def draw_text(self, row, col, text, style='normal'):
|
||||
"""Draw *text* at ``(row, col)`` using the named *style*."""
|
||||
raise NotImplementedError
|
||||
|
||||
def draw_box(self, top, left, height, width, title=''):
|
||||
"""Draw a box with Unicode line-drawing characters.
|
||||
|
||||
Optional *title* is rendered in the top border.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
# ── Dialogs ──────────────────────────────────────────────────────
|
||||
|
||||
def show_message(self, text, msg_type='info') -> bool:
|
||||
"""Show a centred message box.
|
||||
|
||||
*msg_type* is one of ``'info'``, ``'error'``, or ``'confirm'``.
|
||||
For ``'confirm'`` the user must press S (si) or N (no);
|
||||
returns ``True`` for S, ``False`` for N.
|
||||
For other types, waits for any key and returns ``True``.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def show_input(self, prompt, max_len=40):
|
||||
"""Show a centred input dialog.
|
||||
|
||||
Returns the entered string, or ``None`` if the user pressed
|
||||
Escape to cancel.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
Reference in New Issue
Block a user