#!/usr/bin/env python3 """ Textual DOM Inspector - Analyze and debug Textual app widget trees """ import json import sys from pathlib import Path from typing import Dict, Any, List def inspect_textual_app(app_instance) -> Dict[str, Any]: """ Inspect a running Textual app and return detailed DOM information """ def widget_to_dict(widget) -> Dict[str, Any]: """Convert a widget to a dictionary representation""" widget_info = { 'type': widget.__class__.__name__, 'id': getattr(widget, 'id', None), 'classes': list(getattr(widget, 'classes', [])), 'styles': {}, 'size': { 'width': getattr(getattr(widget, 'size', None), 'width', 0) if hasattr(widget, 'size') else 0, 'height': getattr(getattr(widget, 'size', None), 'height', 0) if hasattr(widget, 'size') else 0 }, 'position': { 'x': getattr(getattr(widget, 'offset', None), 'x', 0) if hasattr(widget, 'offset') else 0, 'y': getattr(getattr(widget, 'offset', None), 'y', 0) if hasattr(widget, 'offset') else 0 }, 'visible': getattr(widget, 'visible', True), 'has_focus': getattr(widget, 'has_focus', False), 'children': [] } # Extract key styles if hasattr(widget, 'styles'): styles = widget.styles widget_info['styles'] = { 'display': str(getattr(styles, 'display', 'block')), 'height': str(getattr(styles, 'height', 'auto')), 'width': str(getattr(styles, 'width', 'auto')), 'margin': str(getattr(styles, 'margin', '0')), 'padding': str(getattr(styles, 'padding', '0')), 'background': str(getattr(styles, 'background', 'transparent')), } # Add widget-specific properties if hasattr(widget, 'label'): widget_info['label'] = str(widget.label) if hasattr(widget, 'text'): widget_info['text'] = str(widget.text)[:100] # Truncate long text if hasattr(widget, 'rows'): widget_info['row_count'] = len(widget.rows) if widget.rows else 0 if hasattr(widget, 'columns'): widget_info['column_count'] = len(widget.columns) if widget.columns else 0 # Recursively process children if hasattr(widget, 'children'): for child in widget.children: widget_info['children'].append(widget_to_dict(child)) return widget_info # Start with the app screen if hasattr(app_instance, 'screen'): return { 'app_title': getattr(app_instance, 'title', 'Unknown App'), 'screen_stack_size': len(getattr(app_instance, 'screen_stack', [])), 'current_screen': widget_to_dict(app_instance.screen) } else: return {'error': 'App instance does not have accessible screen'} def print_widget_tree(widget_data: Dict[str, Any], indent: int = 0) -> None: """Print a formatted widget tree""" prefix = " " * indent widget_type = widget_data.get('type', 'Unknown') widget_id = widget_data.get('id', '') widget_classes = widget_data.get('classes', []) # Format the line line = f"{prefix}šŸ“¦ {widget_type}" if widget_id: line += f" #{widget_id}" if widget_classes: line += f" .{'.'.join(widget_classes)}" # Add key properties if 'label' in widget_data: line += f" [label: {widget_data['label']}]" if 'row_count' in widget_data: line += f" [rows: {widget_data['row_count']}]" if 'column_count' in widget_data: line += f" [cols: {widget_data['column_count']}]" size = widget_data.get('size', {}) if size.get('width') or size.get('height'): line += f" [{size.get('width', 0)}x{size.get('height', 0)}]" if not widget_data.get('visible', True): line += " [HIDDEN]" if widget_data.get('has_focus', False): line += " [FOCUSED]" print(line) # Print children for child in widget_data.get('children', []): print_widget_tree(child, indent + 1) def analyze_layout_issues(widget_data: Dict[str, Any]) -> List[str]: """Analyze potential layout issues""" issues = [] def check_widget(widget, path=""): current_path = f"{path}/{widget.get('type', 'Unknown')}" if widget.get('id'): current_path += f"#{widget['id']}" # Check for zero-sized widgets that should have content size = widget.get('size', {}) if size.get('width') == 0 or size.get('height') == 0: if widget.get('type') in ['Button', 'DataTable', 'Static'] and 'label' in widget: issues.append(f"Zero-sized widget with content: {current_path}") # Check for invisible widgets with focus if not widget.get('visible', True) and widget.get('has_focus', False): issues.append(f"Invisible widget has focus: {current_path}") # Check for overlapping positioning (basic check) styles = widget.get('styles', {}) if 'absolute' in str(styles.get('position', '')): # Could add position conflict detection here pass # Recursively check children for child in widget.get('children', []): check_widget(child, current_path) check_widget(widget_data.get('current_screen', {})) return issues def create_textual_debug_snippet() -> str: """Create a code snippet to add to Textual apps for debugging""" return ''' # Add this to your Textual app for debugging def debug_widget_tree(self): """Debug method to inspect widget tree""" from textual_inspector import inspect_textual_app, print_widget_tree data = inspect_textual_app(self) print("šŸ” TEXTUAL APP INSPECTION") print("=" * 50) print_widget_tree(data.get('current_screen', {})) # You can call this from anywhere in your app: # self.debug_widget_tree() def debug_focused_widget(self): """Debug method to find focused widget""" focused = self.focused if focused: print(f"šŸŽÆ Focused widget: {focused.__class__.__name__}") if hasattr(focused, 'id'): print(f" ID: {focused.id}") if hasattr(focused, 'classes'): print(f" Classes: {list(focused.classes)}") else: print("šŸŽÆ No widget has focus") ''' def main(): print("šŸ” Textual DOM Inspector") print("=" * 50) print("This tool helps debug Textual applications.") print("\nTo use with your app, add this import:") print("from textual_inspector import inspect_textual_app, print_widget_tree") print("\nThen in your app:") print("data = inspect_textual_app(self)") print("print_widget_tree(data.get('current_screen', {}))") print("\n" + "=" * 50) print("\nšŸ“ Debug snippet:") print(create_textual_debug_snippet()) if __name__ == "__main__": main()