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
|
||||
537
console/renderers/curses_renderer.py
Normal file
537
console/renderers/curses_renderer.py
Normal file
@@ -0,0 +1,537 @@
|
||||
"""
|
||||
Curses-based VT220 renderer for the AUTOPARTES console application.
|
||||
|
||||
Implements :class:`BaseRenderer` with a green-on-black aesthetic inspired
|
||||
by classic Pick/UNIX VT220 terminals. All drawing is done through Python's
|
||||
built-in :mod:`curses` library.
|
||||
"""
|
||||
|
||||
import curses
|
||||
|
||||
from console.config import COLORS_VT220
|
||||
from console.renderers.base import BaseRenderer
|
||||
from console.utils.formatting import pad_right, truncate
|
||||
|
||||
# ── Colour-name-to-curses mapping ────────────────────────────────────
|
||||
|
||||
_CURSES_COLORS = {
|
||||
"black": curses.COLOR_BLACK,
|
||||
"red": curses.COLOR_RED,
|
||||
"green": curses.COLOR_GREEN,
|
||||
"yellow": curses.COLOR_YELLOW,
|
||||
"blue": curses.COLOR_BLUE,
|
||||
"magenta": curses.COLOR_MAGENTA,
|
||||
"cyan": curses.COLOR_CYAN,
|
||||
"white": curses.COLOR_WHITE,
|
||||
}
|
||||
|
||||
# Box-drawing characters
|
||||
_BOX_H = "\u2500" # ─
|
||||
_BOX_V = "\u2502" # │
|
||||
_BOX_TL = "\u250c" # ┌
|
||||
_BOX_TR = "\u2510" # ┐
|
||||
_BOX_BL = "\u2514" # └
|
||||
_BOX_BR = "\u2518" # ┘
|
||||
|
||||
|
||||
class CursesRenderer(BaseRenderer):
|
||||
"""Full curses implementation of the VT220 green-on-black renderer."""
|
||||
|
||||
def __init__(self):
|
||||
self._screen = None
|
||||
self._color_pairs: dict[str, int] = {}
|
||||
|
||||
# ── Lifecycle ────────────────────────────────────────────────────
|
||||
|
||||
def init_screen(self):
|
||||
"""Set up curses: raw mode, no echo, hidden cursor, colours."""
|
||||
self._screen = curses.initscr()
|
||||
curses.noecho()
|
||||
curses.cbreak()
|
||||
curses.curs_set(0)
|
||||
self._screen.keypad(True)
|
||||
self._init_colors()
|
||||
|
||||
def cleanup(self):
|
||||
"""Restore the terminal to a usable state."""
|
||||
if self._screen is None:
|
||||
return
|
||||
try:
|
||||
curses.nocbreak()
|
||||
self._screen.keypad(False)
|
||||
curses.echo()
|
||||
except curses.error:
|
||||
pass
|
||||
curses.endwin()
|
||||
self._screen = None
|
||||
|
||||
# ── Screen queries ───────────────────────────────────────────────
|
||||
|
||||
def get_size(self) -> tuple:
|
||||
"""Return ``(height, width)``."""
|
||||
return self._screen.getmaxyx()
|
||||
|
||||
# ── Primitive operations ─────────────────────────────────────────
|
||||
|
||||
def clear(self):
|
||||
self._screen.erase()
|
||||
|
||||
def refresh(self):
|
||||
self._screen.refresh()
|
||||
|
||||
def get_key(self) -> int:
|
||||
return self._screen.getch()
|
||||
|
||||
# ── Colour helpers ───────────────────────────────────────────────
|
||||
|
||||
def _init_colors(self):
|
||||
"""Initialise curses colour pairs from ``COLORS_VT220``."""
|
||||
curses.start_color()
|
||||
curses.use_default_colors()
|
||||
for idx, (name, (fg, bg)) in enumerate(COLORS_VT220.items(), start=1):
|
||||
curses.init_pair(idx, _CURSES_COLORS[fg], _CURSES_COLORS[bg])
|
||||
self._color_pairs[name] = idx
|
||||
|
||||
def _attr(self, style: str) -> int:
|
||||
"""Return the curses attribute for a named style.
|
||||
|
||||
Falls back to the *normal* pair if *style* is unknown.
|
||||
"""
|
||||
pair_id = self._color_pairs.get(style,
|
||||
self._color_pairs.get("normal", 1))
|
||||
attr = curses.color_pair(pair_id)
|
||||
if style in ("header", "title"):
|
||||
attr |= curses.A_BOLD
|
||||
return attr
|
||||
|
||||
# ── Safe drawing helpers ─────────────────────────────────────────
|
||||
|
||||
def _safe_addstr(self, row, col, text, attr=None):
|
||||
"""Write *text* at (row, col), silently ignoring edge overflows."""
|
||||
if attr is None:
|
||||
attr = self._attr("normal")
|
||||
h, w = self.get_size()
|
||||
if row < 0 or row >= h or col >= w:
|
||||
return
|
||||
# Truncate to fit within the screen width
|
||||
max_chars = w - col
|
||||
if max_chars <= 0:
|
||||
return
|
||||
text = text[:max_chars]
|
||||
try:
|
||||
self._screen.addstr(row, col, text, attr)
|
||||
except curses.error:
|
||||
# Writing to the bottom-right corner raises an error after
|
||||
# the character is actually drawn. Safe to ignore.
|
||||
pass
|
||||
|
||||
def _hline(self, row, col, width, char=_BOX_H, style="border"):
|
||||
"""Draw a horizontal line of *char* across *width* columns."""
|
||||
self._safe_addstr(row, col, char * width, self._attr(style))
|
||||
|
||||
# ── High-level widgets ───────────────────────────────────────────
|
||||
|
||||
def draw_header(self, title, subtitle=''):
|
||||
h, w = self.get_size()
|
||||
attr = self._attr("header")
|
||||
# Row 0: title (left) + subtitle (right)
|
||||
header_line = pad_right(title, w)
|
||||
if subtitle:
|
||||
sub = subtitle[:w - len(title) - 1]
|
||||
header_line = (title
|
||||
+ " " * max(w - len(title) - len(sub), 0)
|
||||
+ sub)
|
||||
header_line = pad_right(header_line, w)
|
||||
self._safe_addstr(0, 0, header_line, attr | curses.A_BOLD)
|
||||
# Row 1: separator
|
||||
self._hline(1, 0, w)
|
||||
|
||||
def draw_footer(self, key_labels):
|
||||
h, w = self.get_size()
|
||||
if h < 3:
|
||||
return
|
||||
# Row h-2: separator
|
||||
self._hline(h - 2, 0, w)
|
||||
# Row h-1: key labels
|
||||
attr = self._attr("footer")
|
||||
parts = [f"{k}={d}" for k, d in key_labels]
|
||||
line = " ".join(parts)
|
||||
self._safe_addstr(h - 1, 0, pad_right(line, w), attr)
|
||||
|
||||
def draw_menu(self, items, selected_index=0, title=''):
|
||||
h, w = self.get_size()
|
||||
start_row = 3
|
||||
|
||||
if title:
|
||||
self._safe_addstr(start_row, 2, title, self._attr("title"))
|
||||
start_row += 2
|
||||
|
||||
visible = h - start_row - 3 # leave room for footer
|
||||
if visible < 1:
|
||||
return
|
||||
|
||||
# Scrolling offset
|
||||
offset = 0
|
||||
if selected_index >= visible:
|
||||
offset = selected_index - visible + 1
|
||||
|
||||
drawn = 0
|
||||
for idx, (num, label) in enumerate(items):
|
||||
if drawn >= visible:
|
||||
break
|
||||
if idx < offset:
|
||||
continue
|
||||
row = start_row + drawn
|
||||
|
||||
# Separator
|
||||
if num == "\u2500" or num == "---":
|
||||
self._hline(row, 2, w - 4)
|
||||
drawn += 1
|
||||
continue
|
||||
|
||||
marker = "\u25b8 " if idx == selected_index else " "
|
||||
text = f"{marker}{num}. {label}"
|
||||
style = "highlight" if idx == selected_index else "normal"
|
||||
self._safe_addstr(row, 2, pad_right(text, w - 4),
|
||||
self._attr(style))
|
||||
drawn += 1
|
||||
|
||||
def draw_table(self, headers, rows, widths, page_info=None,
|
||||
selected_row=-1):
|
||||
h, w = self.get_size()
|
||||
start_row = 3
|
||||
|
||||
# Header row
|
||||
header_cells = [pad_right(hdr, wd) for hdr, wd in zip(headers, widths)]
|
||||
header_text = " # " + " \u2502 ".join(header_cells)
|
||||
self._safe_addstr(start_row, 0, pad_right(header_text, w),
|
||||
self._attr("title"))
|
||||
# Separator
|
||||
self._hline(start_row + 1, 0, w)
|
||||
|
||||
visible = h - start_row - 5 # room for header, sep, footer
|
||||
if visible < 1:
|
||||
return
|
||||
|
||||
for i, row_data in enumerate(rows):
|
||||
if i >= visible:
|
||||
break
|
||||
row_num = start_row + 2 + i
|
||||
row_idx_str = pad_right(str(i + 1), 3)
|
||||
cells = [pad_right(str(v), wd) for v, wd in zip(row_data, widths)]
|
||||
line = f" {row_idx_str}\u2502 " + " \u2502 ".join(cells)
|
||||
style = "highlight" if i == selected_row else "normal"
|
||||
self._safe_addstr(row_num, 0, pad_right(line, w),
|
||||
self._attr(style))
|
||||
|
||||
# Page info
|
||||
if page_info:
|
||||
info_row = start_row + 2 + min(len(rows), visible)
|
||||
page = page_info.get("page", 1)
|
||||
total = page_info.get("total_pages", 1)
|
||||
total_rows = page_info.get("total_rows", len(rows))
|
||||
info_text = (f" Pagina {page}/{total}"
|
||||
f" ({total_rows} registros)")
|
||||
self._safe_addstr(info_row, 0, info_text, self._attr("info"))
|
||||
|
||||
def draw_detail(self, fields, title=''):
|
||||
h, w = self.get_size()
|
||||
start_row = 3
|
||||
|
||||
if title:
|
||||
self._safe_addstr(start_row, 2, title, self._attr("title"))
|
||||
self._hline(start_row + 1, 2, w - 4)
|
||||
start_row += 3
|
||||
|
||||
# Determine max label width for alignment
|
||||
max_label = max((len(lbl) for lbl, _ in fields), default=10)
|
||||
dot_total = max_label + 4 # label + dots
|
||||
|
||||
for i, (label, value) in enumerate(fields):
|
||||
row = start_row + i
|
||||
if row >= h - 3:
|
||||
break
|
||||
dots = "." * (dot_total - len(label))
|
||||
label_part = f" {label}{dots}: "
|
||||
self._safe_addstr(row, 0, label_part,
|
||||
self._attr("field_label"))
|
||||
self._safe_addstr(row, len(label_part), str(value),
|
||||
self._attr("field_value"))
|
||||
|
||||
def draw_form(self, fields, focused_index=0, title=''):
|
||||
h, w = self.get_size()
|
||||
start_row = 3
|
||||
|
||||
if title:
|
||||
self._safe_addstr(start_row, 2, title, self._attr("title"))
|
||||
self._hline(start_row + 1, 2, w - 4)
|
||||
start_row += 3
|
||||
|
||||
max_label = max((len(f.get("label", "")) for f in fields),
|
||||
default=10)
|
||||
dot_total = max_label + 4
|
||||
|
||||
for i, field in enumerate(fields):
|
||||
row = start_row + i * 2 # space between fields
|
||||
if row >= h - 3:
|
||||
break
|
||||
|
||||
label = field.get("label", "")
|
||||
value = field.get("value", "")
|
||||
fw = field.get("width", 20)
|
||||
hint = field.get("hint", "")
|
||||
|
||||
dots = "." * (dot_total - len(label))
|
||||
num_str = f"{i + 1}. "
|
||||
label_part = f" {num_str}{label}{dots}: "
|
||||
|
||||
self._safe_addstr(row, 0, label_part,
|
||||
self._attr("field_label"))
|
||||
|
||||
# Editable field value in brackets
|
||||
style = "field_active" if i == focused_index else "field_value"
|
||||
display_val = pad_right(str(value), fw)
|
||||
field_text = f"[{display_val}]"
|
||||
self._safe_addstr(row, len(label_part), field_text,
|
||||
self._attr(style))
|
||||
|
||||
# Optional hint
|
||||
if hint:
|
||||
hint_col = len(label_part) + len(field_text) + 2
|
||||
self._safe_addstr(row, hint_col, hint,
|
||||
self._attr("info"))
|
||||
|
||||
def draw_filter_list(self, items, filter_text, selected_index,
|
||||
title=''):
|
||||
h, w = self.get_size()
|
||||
start_row = 3
|
||||
|
||||
if title:
|
||||
self._safe_addstr(start_row, 2, title, self._attr("title"))
|
||||
start_row += 1
|
||||
|
||||
# Separator
|
||||
self._hline(start_row, 2, w - 4)
|
||||
start_row += 1
|
||||
|
||||
# Filter input
|
||||
prompt = "Filtro: "
|
||||
self._safe_addstr(start_row, 2, prompt,
|
||||
self._attr("field_label"))
|
||||
self._safe_addstr(start_row, 2 + len(prompt),
|
||||
filter_text + "_",
|
||||
self._attr("field_active"))
|
||||
start_row += 1
|
||||
|
||||
# Separator
|
||||
self._hline(start_row, 2, w - 4)
|
||||
start_row += 1
|
||||
|
||||
# Scrollable list
|
||||
visible = h - start_row - 4
|
||||
if visible < 1:
|
||||
return
|
||||
|
||||
offset = 0
|
||||
if selected_index >= visible:
|
||||
offset = selected_index - visible + 1
|
||||
|
||||
drawn = 0
|
||||
for idx, (num, label) in enumerate(items):
|
||||
if drawn >= visible:
|
||||
break
|
||||
if idx < offset:
|
||||
continue
|
||||
row = start_row + drawn
|
||||
marker = "\u25b8 " if idx == selected_index else " "
|
||||
text = f"{marker}{num}. {label}"
|
||||
style = "highlight" if idx == selected_index else "normal"
|
||||
self._safe_addstr(row, 2, pad_right(text, w - 4),
|
||||
self._attr(style))
|
||||
drawn += 1
|
||||
|
||||
# Count at bottom
|
||||
count_row = start_row + min(drawn, visible)
|
||||
count_text = f" {len(items)} elementos"
|
||||
self._safe_addstr(count_row, 2, count_text, self._attr("info"))
|
||||
|
||||
def draw_comparison(self, columns, title=''):
|
||||
h, w = self.get_size()
|
||||
start_row = 3
|
||||
|
||||
if title:
|
||||
self._safe_addstr(start_row, 2, title, self._attr("title"))
|
||||
self._hline(start_row + 1, 2, w - 4)
|
||||
start_row += 3
|
||||
|
||||
n_cols = len(columns)
|
||||
if n_cols == 0:
|
||||
return
|
||||
|
||||
# Determine label width from the first column's row labels
|
||||
all_labels = []
|
||||
for col in columns:
|
||||
for lbl, _ in col.get("rows", []):
|
||||
all_labels.append(lbl)
|
||||
label_w = max((len(l) for l in all_labels), default=8) + 2
|
||||
|
||||
# Available width for data columns
|
||||
avail = w - label_w - 4
|
||||
col_w = max(avail // n_cols, 10)
|
||||
|
||||
# Header row
|
||||
header_line = pad_right("", label_w)
|
||||
for col in columns:
|
||||
header_line += " \u2502 " + pad_right(col.get("header", ""), col_w)
|
||||
self._safe_addstr(start_row, 2, header_line, self._attr("title"))
|
||||
|
||||
# Separator
|
||||
self._hline(start_row + 1, 2, w - 4)
|
||||
|
||||
# Data rows — use the first column's labels as the canonical set
|
||||
if not columns[0].get("rows"):
|
||||
return
|
||||
n_rows = len(columns[0]["rows"])
|
||||
for i in range(n_rows):
|
||||
row = start_row + 2 + i
|
||||
if row >= h - 3:
|
||||
break
|
||||
lbl = columns[0]["rows"][i][0] if i < len(columns[0]["rows"]) else ""
|
||||
line = pad_right(lbl, label_w)
|
||||
for col in columns:
|
||||
rows_data = col.get("rows", [])
|
||||
val = rows_data[i][1] if i < len(rows_data) else ""
|
||||
line += " \u2502 " + pad_right(str(val), col_w)
|
||||
self._safe_addstr(row, 2, line, self._attr("normal"))
|
||||
|
||||
# ── Low-level drawing ────────────────────────────────────────────
|
||||
|
||||
def draw_text(self, row, col, text, style='normal'):
|
||||
self._safe_addstr(row, col, text, self._attr(style))
|
||||
|
||||
def draw_box(self, top, left, height, width, title=''):
|
||||
if height < 2 or width < 2:
|
||||
return
|
||||
attr = self._attr("border")
|
||||
|
||||
# Top border
|
||||
top_line = _BOX_TL + _BOX_H * (width - 2) + _BOX_TR
|
||||
if title:
|
||||
t = truncate(title, width - 4)
|
||||
top_line = (_BOX_TL + _BOX_H + t
|
||||
+ _BOX_H * (width - 3 - len(t)) + _BOX_TR)
|
||||
self._safe_addstr(top, left, top_line, attr)
|
||||
|
||||
# Side borders
|
||||
for r in range(1, height - 1):
|
||||
self._safe_addstr(top + r, left, _BOX_V, attr)
|
||||
self._safe_addstr(top + r, left + width - 1, _BOX_V, attr)
|
||||
|
||||
# Bottom border
|
||||
bottom_line = _BOX_BL + _BOX_H * (width - 2) + _BOX_BR
|
||||
self._safe_addstr(top + height - 1, left, bottom_line, attr)
|
||||
|
||||
# ── Dialogs ──────────────────────────────────────────────────────
|
||||
|
||||
def show_message(self, text, msg_type='info') -> bool:
|
||||
h, w = self.get_size()
|
||||
lines = text.split("\n")
|
||||
box_w = max(max((len(l) for l in lines), default=20) + 6, 30)
|
||||
box_h = len(lines) + 4
|
||||
top = max((h - box_h) // 2, 0)
|
||||
left = max((w - box_w) // 2, 0)
|
||||
|
||||
style = "error" if msg_type == "error" else "info"
|
||||
self.draw_box(top, left, box_h, box_w)
|
||||
|
||||
# Fill interior with spaces
|
||||
interior_attr = self._attr(style)
|
||||
for r in range(1, box_h - 1):
|
||||
self._safe_addstr(top + r, left + 1,
|
||||
" " * (box_w - 2), interior_attr)
|
||||
|
||||
# Draw message lines
|
||||
for i, line in enumerate(lines):
|
||||
x = left + max((box_w - len(line)) // 2, 2)
|
||||
self._safe_addstr(top + 1 + i, x, line, interior_attr)
|
||||
|
||||
# Prompt line
|
||||
if msg_type == "confirm":
|
||||
prompt = "[S]i / [N]o"
|
||||
else:
|
||||
prompt = "Presione cualquier tecla..."
|
||||
px = left + max((box_w - len(prompt)) // 2, 2)
|
||||
self._safe_addstr(top + box_h - 2, px, prompt,
|
||||
self._attr("highlight"))
|
||||
self.refresh()
|
||||
|
||||
# Wait for key
|
||||
if msg_type == "confirm":
|
||||
while True:
|
||||
key = self.get_key()
|
||||
if key in (ord("s"), ord("S")):
|
||||
return True
|
||||
if key in (ord("n"), ord("N"), 27): # 27 = ESC
|
||||
return False
|
||||
else:
|
||||
self.get_key()
|
||||
return True
|
||||
|
||||
def show_input(self, prompt, max_len=40):
|
||||
h, w = self.get_size()
|
||||
box_w = max(len(prompt) + max_len + 8, 30)
|
||||
box_h = 5
|
||||
top = max((h - box_h) // 2, 0)
|
||||
left = max((w - box_w) // 2, 0)
|
||||
|
||||
buf = []
|
||||
|
||||
try:
|
||||
curses.curs_set(1) # show cursor during input
|
||||
except curses.error:
|
||||
pass
|
||||
|
||||
while True:
|
||||
self.draw_box(top, left, box_h, box_w)
|
||||
# Fill interior
|
||||
interior_attr = self._attr("normal")
|
||||
for r in range(1, box_h - 1):
|
||||
self._safe_addstr(top + r, left + 1,
|
||||
" " * (box_w - 2), interior_attr)
|
||||
|
||||
# Prompt
|
||||
self._safe_addstr(top + 1, left + 2, prompt,
|
||||
self._attr("field_label"))
|
||||
|
||||
# Input field
|
||||
val = "".join(buf)
|
||||
field_text = "[" + pad_right(val, max_len) + "]"
|
||||
self._safe_addstr(top + 2, left + 2, field_text,
|
||||
self._attr("field_active"))
|
||||
|
||||
# Hint
|
||||
hint = "ENTER=Aceptar ESC=Cancelar"
|
||||
hx = left + max((box_w - len(hint)) // 2, 2)
|
||||
self._safe_addstr(top + 3, hx, hint, self._attr("info"))
|
||||
|
||||
self.refresh()
|
||||
|
||||
key = self.get_key()
|
||||
if key == 27: # ESC
|
||||
try:
|
||||
curses.curs_set(0)
|
||||
except curses.error:
|
||||
pass
|
||||
return None
|
||||
elif key in (10, curses.KEY_ENTER): # ENTER
|
||||
try:
|
||||
curses.curs_set(0)
|
||||
except curses.error:
|
||||
pass
|
||||
return "".join(buf)
|
||||
elif key in (127, curses.KEY_BACKSPACE, 8): # BACKSPACE
|
||||
if buf:
|
||||
buf.pop()
|
||||
elif 32 <= key <= 126: # printable ASCII
|
||||
if len(buf) < max_len:
|
||||
buf.append(chr(key))
|
||||
Reference in New Issue
Block a user