"""Display and formatting functions for Brother printer status reports.""" from __future__ import annotations import sys from typing import TYPE_CHECKING # ── Shared display helpers ─────────────────────────────────────────── # Re-exported through __all__: tests/test_display.py imports these names from # this module. Only the un-patched supply *maths* moved to _supply -- everything # the tests reach with mock.patch has to stay resolvable here. from python_pkg.brother_printer._severity import ( _SEVERITY_COLORS, _SEVERITY_ICONS, _SEVERITY_SUMMARIES, ) from python_pkg.brother_printer._supply import ( _classify_percentage_level, _classify_supply_level, _collect_supply_items, _format_supply_bar, _parse_supply_value, _process_supply_item, render_life_bar, ) from python_pkg.brother_printer.constants import ( BOLD, CYAN, DIM, DRUM_RATED_PAGES, GREEN, RED, RESET, TONER_RATED_PAGES, YELLOW, _out, get_status_info, ) from python_pkg.brother_printer.consumables import ( check_page_delivery, estimate_consumable_life, ) from python_pkg.brother_printer.cups_queue import ( display_cups_queue_status, get_cups_queue_status, ) if TYPE_CHECKING: from python_pkg.brother_printer.data_classes import ( NetworkResult, USBResult, ) __all__ = [ "_classify_percentage_level", "_classify_supply_level", "_collect_supply_items", "_format_supply_bar", "_parse_supply_value", "_process_supply_item", "display_network_results", "display_usb_results", ] def _display_report_header() -> None: """Print the report banner box.""" _out() _out(f"{BOLD}╔══════════════════════════════════════════════════╗{RESET}") _out(f"{BOLD}║ Brother Laser Printer Status Report ║{RESET}") _out(f"{BOLD}╚══════════════════════════════════════════════════╝{RESET}") _out() def _display_page_delivery_warning(printer_total: int, *, queue_idle: bool) -> None: """Warn when CUPS claims more pages than the printer actually counted. Args: printer_total: Lifetime count from the printer's own counter. queue_idle: False while a job is queued or printing, when the two counters legitimately disagree. """ check = check_page_delivery(printer_total, queue_idle=queue_idle) if not check.suspected: return _out(f"{BOLD}── Dropped Pages ──{RESET}") _out() _out( f" {RED}{BOLD}⚠ {check.dropped} pages did not print.{RESET}" f" {RED}CUPS sent {check.cups_pages} pages since the last check;" f" the printer's own counter advanced by" f" {check.printer_pages}.{RESET}" ) _out() _out( f" {DIM}The printer discards pages whose 600 dpi raster does not fit" f" its memory, stays READY, and reports no error - and CUPS still calls" f" the job successful. Reprint at a lower resolution:{RESET}" ) _out(f" {DIM} lp -o Resolution=300dpi {RESET}") _out() def _display_page_count_estimate(printer_total: int = 0) -> None: """Show estimated consumable life based on the printer's page count. Args: printer_total: Lifetime count from the printer's own counter, or zero when it could not be read and the CUPS page log has to stand in. """ estimate = estimate_consumable_life(printer_total) if estimate.total_pages <= 0: return _out(f"{BOLD}── Page Count Estimate ──{RESET}") _out() _out( f" {BOLD}Total pages printed:{RESET} {estimate.total_pages}" f" (toner: {estimate.toner_pages} since replacement," f" drum: {estimate.drum_pages} since replacement)" ) if estimate.approximate: _out( f" {YELLOW}Approximate: counted from the CUPS log, not the" f" printer's own counter, so it misses pages CUPS never saw" f" and reads high.{RESET}" ) _out() render_life_bar( "Toner:", estimate.toner_pct_remaining, exhausted=estimate.toner_exhausted, low=estimate.toner_low, exhausted_note=" ← REPLACE NOW", low_note=" ← order soon", ) render_life_bar( "Drum: ", estimate.drum_pct_remaining, low=estimate.drum_near_end, low_note=" ← nearing end", ) _out( f" {DIM}Based on pages since last replacement" f" vs rated capacity (toner ~{TONER_RATED_PAGES}," f" drum ~{DRUM_RATED_PAGES}).{RESET}" ) _out(f" {DIM}Reset after replacing: --reset-toner or --reset-drum{RESET}") if estimate.toner_exhausted: _out() _out( f" {RED}{BOLD}⚠ Toner is likely exhausted." f" This is probably why the orange light is flashing.{RESET}" ) _out() def _display_consumables_reference() -> None: """Print compatible consumables reference.""" _out(f"{BOLD}── Compatible Consumables ──{RESET}") _out() _out(f" {BOLD}Toner:{RESET} TN-1050 / TN-1030 (or compatible third-party)") _out(f" {BOLD}Drum:{RESET} DR-1050 / DR-1030 (or compatible third-party)") _out(f" {DIM} Toner rated ~1000 pages; Drum rated ~10000 pages.{RESET}") _out() # ── USB display helpers ────────────────────────────────────────────── def _display_usb_device_info(result: USBResult) -> None: """Print device info block for USB results.""" _out(f"{BOLD}Printer:{RESET} {result.product or 'Unknown'}") _out(f"{BOLD}Connection:{RESET} USB") if result.serial: _out(f"{BOLD}Serial:{RESET} {result.serial}") if result.online == "TRUE": _out(f"{BOLD}Online:{RESET} {GREEN}Yes{RESET}") elif result.online == "FALSE": _out(f"{BOLD}Online:{RESET} {YELLOW}No (needs attention){RESET}") _out() if result.economode: if result.economode == "ON": _out( f"{BOLD}Toner Save:{RESET} {GREEN}ON{RESET}" " (extends toner life, lighter prints)" ) else: _out(f"{BOLD}Toner Save:{RESET} OFF") def _format_status_detail( severity: str, short_text: str, action: str, result: USBResult ) -> None: """Print severity icon, display text, and action.""" color = _SEVERITY_COLORS.get(severity, GREEN) icon = _SEVERITY_ICONS.get(severity, "✓") _out(f" {color}{BOLD}{icon} {short_text}{RESET}") if result.display and result.display != short_text: _out(f" {DIM}Display: {result.display}{RESET}") _out(f" {DIM}Status code: {result.status_code}{RESET}") if action: _out() _out(f" {color}{BOLD}Action:{RESET} {color}{action}{RESET}") _out() _out(_SEVERITY_SUMMARIES.get(severity, "")) def _display_pjl_status(result: USBResult) -> None: """Display PJL status code interpretation.""" _out() _out(f"{BOLD}── Printer Status ──{RESET}") _out() if not result.status_code: _out(f" {YELLOW}Could not read status from printer.{RESET}") if result.display: _out(f" Display message: {BOLD}{result.display}{RESET}") return severity, short_text, action = get_status_info(result.status_code) _format_status_detail(severity, short_text, action, result) def _display_cups_fallback_note(result: USBResult) -> None: """Show a note when running in CUPS fallback mode.""" _out() if result.port_status is not None: _out( f" {DIM}Note: Hardware status obtained via USB port query." f" Toner/drum percentages not available.{RESET}" ) else: _out( f" {DIM}Note: pyusb not available; status obtained via" f" CUPS only. Detailed toner/drum levels are not" f" available in this mode.{RESET}" ) # ── USB results display ───────────────────────────────────────────── def display_usb_results(result: USBResult) -> None: """Print a formatted report for USB PJL query results.""" if result.error: _out(f"{RED}Error: {result.error}{RESET}") sys.exit(1) _display_report_header() _display_usb_device_info(result) _display_pjl_status(result) if result.device == "cups": _display_cups_fallback_note(result) _out() printer_total = int(result.page_count) if result.page_count.isdigit() else 0 queue = get_cups_queue_status() _display_page_delivery_warning(printer_total, queue_idle=not queue.jobs) _display_page_count_estimate(printer_total) _display_consumables_reference() display_cups_queue_status(queue) # ── Network supply level helpers ───────────────────────────────────── def _display_supply_warnings(*, needs_replacement: bool, warnings: list[str]) -> None: """Display supply level warnings summary.""" _out() if needs_replacement: _out(f"{RED}{BOLD}⚠ ACTION NEEDED:{RESET}") for w in warnings: _out(f" {RED}• {w}{RESET}") elif warnings: _out(f"{YELLOW}{BOLD}⚡ HEADS UP:{RESET}") for w in warnings: _out(f" {YELLOW}• {w}{RESET}") else: _out(f"{GREEN}{BOLD}✓ All consumables are at healthy levels.{RESET}") def _display_supply_levels(result: NetworkResult) -> None: """Display consumable supply levels section.""" _out() _out(f"{BOLD}── Consumable Levels ──{RESET}") _out() needs_replacement = False warnings: list[str] = [] items, descs = _collect_supply_items(result) for desc, item in zip(descs, items, strict=True): _out( f" {BOLD}{desc:<25}{RESET}" f" {item.color}{item.bar_text} {item.status_text}{RESET}" ) if item.needs_replacement: needs_replacement = True if item.warning: warnings.append(item.warning) _display_supply_warnings(needs_replacement=needs_replacement, warnings=warnings) def _display_network_device_info(result: NetworkResult) -> None: """Display device info section for network results.""" _out(f"{BOLD}Printer:{RESET} {result.product or 'Unknown'}") _out(f"{BOLD}Connection:{RESET} Network ({result.ip})") if result.serial: _out(f"{BOLD}Serial:{RESET} {result.serial}") if result.display: _out(f"{BOLD}Display:{RESET} {result.display}") if result.page_count and result.page_count.isdigit(): _out(f"{BOLD}Pages:{RESET} {result.page_count} total") # ── Network results display ────────────────────────────────────────── def display_network_results(result: NetworkResult) -> None: """Print a formatted report for SNMP network query results.""" if result.error: _out(f"{RED}Error: {result.error}{RESET}") sys.exit(1) _display_report_header() _display_network_device_info(result) _display_supply_levels(result) _out() _out( f"{CYAN}Tip: Visit http://{result.ip} for the full web management" f" interface.{RESET}" ) _out()