#!/usr/bin/env python3 """Console application for S.M.A.R.T. disk health monitoring using Rich.""" import argparse import sys from datetime import datetime from rich.console import Console from rich.panel import Panel from rich.table import Table from rich import box from rich.text import Text from smart_report.core import ( collect_all_disks_data, check_smartctl, is_root, get_locale, get_message, ) def get_health_icon(health: int, lang: str = None) -> str: """Get icon based on health percentage.""" if health >= 85: return "✓" elif health >= 60: return "⚠" elif health >= 30: return "✗" else: return "💥" def get_health_color(health: int) -> str: """Get color based on health percentage.""" if health >= 85: return "green" elif health >= 60: return "yellow" elif health >= 30: return "red" else: return "bold red" def create_health_bar(health: int, width: int = 40) -> str: """Create a visual health bar.""" filled = int(width * health / 100) bar = "█" * filled + "░" * (width - filled) return bar def print_disk_panel(console: Console, data, lang: str, show_all: bool = False): """Print disk information as a rich panel.""" color = get_health_color(data.health) icon = get_health_icon(data.health, lang) health_bar = create_health_bar(data.health) # Header line header = ( f"[bold]{get_message('disk', lang)}:[/bold] {data.disk:12} " f"[bold]{get_message('model', lang)}:[/bold] {data.model:25} " f"[bold]{get_message('size', lang)}:[/bold] {data.size}" ) # Health bar health_display = f"[{color}] {icon} {health_bar} {data.health}%[/{color}]" # Build content content = f"{header}\n\n" content += f"{get_message('health', lang)}: {health_display}\n\n" if data.smart_supported: # Status line status_color = ( "green" if data.status == "GOOD" else ("red" if data.status == "BAD" else "yellow") ) status_line = ( f"[bold]{get_message('status', lang)}:[/bold] [{status_color}]{data.status:8}[/{status_color}] | " f"[bold]{get_message('temperature', lang)}:[/bold] {data.temp:8} | " f"[bold]{get_message('power_hours', lang)}:[/bold] {data.power_hours:15} | " f"[bold]{get_message('power_cycles', lang)}:[/bold] {data.power_cycles}" ) content += f"{status_line}\n\n" # Critical attributes content += f"[bold]{get_message('critical_attrs', lang)}:[/bold]\n" content += f" • {get_message('reallocated', lang)}: [red]{data.reallocated}[/red]\n" content += f" • {get_message('pending', lang)}: [red]{data.pending}[/red]\n" content += f" • {get_message('uncorrectable', lang)}: [red]{data.uncorrectable}[/red]\n" # Additional attributes for detailed view if data.remaining_lifetime < 100: content += f" • Remaining Lifetime: [green]{data.remaining_lifetime}%[/green]\n" if data.media_wearout_indicator < 100: content += f" • Media Wearout Indicator: [yellow]{data.media_wearout_indicator}%[/yellow]\n" if data.ssd_life_left < 100 and data.remaining_lifetime == 100: content += f" • SSD Life Left: [yellow]{data.ssd_life_left}%[/yellow]\n" if data.host_writes_gb > 0: content += f" • Host Writes: [cyan]{data.host_writes_gb} GB[/cyan]\n" if data.crc_errors > 0: content += f" • CRC Errors: [yellow]{data.crc_errors}[/yellow]\n" if data.program_fail_count > 0: content += f" • Program Failures: [red]{data.program_fail_count}[/red]\n" if data.erase_fail_count > 0: content += f" • Erase Failures: [red]{data.erase_fail_count}[/red]\n" if data.command_timeout > 0: content += f" • Command Timeouts: [yellow]{data.command_timeout}[/yellow]\n" if data.reallocated_event_count > 0: content += f" • Realloc Events: [yellow]{data.reallocated_event_count}[/yellow]\n" if data.reported_uncorrect > 0: content += f" • Reported Uncorrect: [red]{data.reported_uncorrect}[/red]\n" # Remove trailing newline if no additional attributes were added content = content.rstrip('\n') # Show all SMART attributes if requested if show_all and data.attrs: content += "\n\n[bold]All S.M.A.R.T. Attributes:[/bold]\n" table = Table(box=box.SIMPLE, show_header=True, header_style="bold cyan") table.add_column("ID", style="cyan") table.add_column("Name", style="white") table.add_column("Value", justify="right") table.add_column("Worst", justify="right") table.add_column("Threshold", justify="right") table.add_column("Raw", style="yellow", justify="right") for attr_id in sorted(data.attrs.keys(), key=lambda x: int(x)): attr = data.attrs[attr_id] table.add_row( attr_id, attr['name'], attr['value'], attr['worst'], attr['threshold'], attr['raw'] ) # Render table to string and add to content from rich.console import Console as RichConsole from io import StringIO output = StringIO() rich_console = RichConsole(file=output, force_terminal=True) rich_console.print(table) content += "\n" + output.getvalue() else: content += f"[yellow]{get_message('smart_not_supported', lang)}[/yellow]" # Warnings panel if data.warnings: warnings_title = f"[bold red]⚠️ {get_message('warning_reallocated_10', lang).split(':')[0].replace('🟡', '').strip()}:[/bold red]" warnings_text = "\n".join([f" • {w}" for w in data.warnings]) console.print( Panel( f"{content}\n\n{warnings_title}\n{warnings_text}", title=f"📀 {data.disk} - {data.model}", border_style="red", box=box.ROUNDED, ) ) else: console.print( Panel( content, title=f"📀 {data.disk} - {data.model}", border_style=color, box=box.ROUNDED, ) ) def print_full_smart_data(console: Console, data, lang: str): """Print full SMART data in detailed format.""" color = get_health_color(data.health) icon = get_health_icon(data.health, lang) health_bar = create_health_bar(data.health) # Header console.print(f"\n{'=' * 80}") console.print(f"[bold cyan]📀 {data.disk} - {data.model} ({data.size})[/bold cyan]") console.print(f"{'=' * 80}\n") # Health summary health_display = f"[{color}] {icon} {health_bar} {data.health}%[/{color}]" console.print(f"[bold]{get_message('health', lang)}:[/bold] {health_display}") console.print() if not data.smart_supported: console.print(f"[yellow]{get_message('smart_not_supported', lang)}[/yellow]\n") return # Status line status_color = ( "green" if data.status == "GOOD" else ("red" if data.status == "BAD" else "yellow") ) console.print( f"[bold]{get_message('status', lang)}:[/bold] [{status_color}]{data.status:8}[/{status_color}] | " f"[bold]{get_message('temperature', lang)}:[/bold] {data.temp:8} | " f"[bold]{get_message('power_hours', lang)}:[/bold] {data.power_hours:15} | " f"[bold]{get_message('power_cycles', lang)}:[/bold] {data.power_cycles}" ) console.print() # Critical attributes console.print(f"[bold red]{get_message('critical_attrs', lang)}:[/bold red]") console.print(f" • {get_message('reallocated', lang)}: [red]{data.reallocated}[/red]") console.print(f" • {get_message('pending', lang)}: [red]{data.pending}[/red]") console.print(f" • {get_message('uncorrectable', lang)}: [red]{data.uncorrectable}[/red]") console.print() # Warnings if data.warnings: console.print(f"[bold red]⚠️ WARNINGS / ПРЕДУПРЕЖДЕНИЯ:[/bold red]") for warning in data.warnings: console.print(f" [red]• {warning}[/red]") console.print() # All SMART attributes table if data.attrs: console.print(f"[bold cyan]All S.M.A.R.T. Attributes / Все атрибуты S.M.A.R.T.:[/bold cyan]\n") table = Table(box=box.ROUNDED, show_header=True, header_style="bold cyan") table.add_column("ID", style="cyan", justify="right") table.add_column("Name", style="white") table.add_column("Value", justify="right", style="green") table.add_column("Worst", justify="right", style="yellow") table.add_column("Threshold", justify="right", style="red") table.add_column("Raw", style="magenta", justify="right") for attr_id in sorted(data.attrs.keys(), key=lambda x: int(x)): attr = data.attrs[attr_id] # Highlight critical attributes if attr_id in ['5', '197', '198', '194', '9', '12']: row_style = "bold" else: row_style = "" table.add_row( attr_id, attr['name'], attr['value'], attr['worst'], attr['threshold'], attr['raw'], style=row_style ) console.print(table) console.print() def main(): """Main entry point for console application.""" parser = argparse.ArgumentParser( description=get_message('disk_monitor', 'en'), formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: sudo smart-report Show disk health summary sudo smart-report --all Show all SMART attributes sudo smart-report -a Show all SMART attributes (short form) """ ) parser.add_argument( '-a', '--all', action='store_true', help='Show all S.M.A.R.T. attributes for each disk' ) args = parser.parse_args() console = Console() lang = get_locale() # Check smartctl if not check_smartctl(): console.print(f"[bold red]{get_message('smart_not_installed', lang)}[/bold red]") console.print(f"[green]{get_message('install_command', lang)}[/green]") sys.exit(1) # Check root status if not is_root(): console.print( f"[bold red]{get_message('run_with_sudo', lang)}:[/bold red] " f"[green]sudo smart-report[/green]" ) console.print( f"[yellow]{get_message('run_with_sudo', lang)}.[/yellow]\n" ) try: disks_data = collect_all_disks_data(lang) except Exception as e: console.print(f"[bold red]{get_message('error', lang)}:[/bold red] {e}") sys.exit(1) if not disks_data: console.print(f"[bold yellow]{get_message('no_disks_found', lang)}[/bold yellow]") return # Header console.print("\n" + "=" * 75) console.print(f" [bold]{get_message('disk_monitor', lang)}[/bold]") console.print(f" {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}") if args.all: console.print(f" [cyan]Detailed Mode / Подробный режим[/cyan]") console.print("=" * 75 + "\n") # Print each disk for data in disks_data: if args.all: print_full_smart_data(console, data, lang) else: print_disk_panel(console, data, lang, show_all=False) console.print("=" * 75 + "\n") if __name__ == "__main__": try: main() except KeyboardInterrupt: print("\n\nПрограмма прервана / Program interrupted") except Exception as e: print(f"❌ Ошибка / Error: {e}") sys.exit(1)