187 lines
7.0 KiB
Python
187 lines
7.0 KiB
Python
#!/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() |