This document provides a comprehensive overview of Emu's architecture, design patterns, and implementation details.
- Overview
- System Architecture
- Core Components
- Design Patterns
- Data Flow
- Performance Architecture
- Error Handling Strategy
- Testing Architecture
Emu is built using a layered, async-first architecture that prioritizes performance, maintainability, and cross-platform compatibility. The application uses Rust's type system and ownership model to ensure memory safety and thread safety while providing a responsive terminal user interface.
- Separation of Concerns: Clear boundaries between UI, business logic, and platform-specific code
- Async-First: Non-blocking operations with proper task coordination
- Trait-Based Abstraction: Platform-agnostic interfaces with concrete implementations
- Performance Optimization: Background loading, caching, and debounced updates
- Comprehensive Testing: Extensive test coverage for reliability
┌─────────────────────────────────────────────────────────────┐
│ Terminal UI Layer │
│ Three-panel layout: 30% | 30% | 40% │
├─────────────────────────────────────────────────────────────┤
│ Application Core Layer │
├─────────────────────────────────────────────────────────────┤
│ Device Management Layer │
├─────────────────────────────────────────────────────────────┤
│ System Layer │
└─────────────────────────────────────────────────────────────┘
- Rendering: Terminal UI rendering using Ratatui
- Themes: Color schemes and visual styling
- Widgets: Custom UI components and layouts
- Input Handling: Keyboard event processing
- State Management: Centralized application state
- Event Processing: User action handling and coordination
- Background Tasks: Async task management and coordination
- Business Logic: Core application workflows
- Platform Abstraction: Unified device operation interface
- Android Management: AVD lifecycle and logcat streaming
- iOS Management: Simulator control via simctl
- Caching: Device metadata and detail caching
- Command Execution: Safe system command execution
- Error Handling: Error types and user-friendly formatting
- Configuration: Application settings and platform detection
- Logging: Structured logging and debug output
The AppState struct serves as the central state container:
pub struct AppState {
// Device data
pub android_devices: Vec<AndroidDevice>,
pub ios_devices: Vec<IosDevice>,
// UI state
pub active_panel: Panel,
pub selected_android: usize,
pub selected_ios: usize,
pub mode: Mode,
// API Level Management (New in v2.0)
pub api_level_management: Option<ApiLevelManagementState>,
// Background operations
pub is_loading: bool,
pub device_operation_status: Option<String>,
// Caching
pub cached_device_details: Option<DeviceDetails>,
pub device_cache: Arc<RwLock<DeviceCache>>,
// Logging and notifications
pub device_logs: VecDeque<LogEntry>,
pub notifications: VecDeque<Notification>,
// Dialogs
pub create_device_form: CreateDeviceForm,
pub confirm_delete_dialog: Option<ConfirmDeleteDialog>,
pub confirm_wipe_dialog: Option<ConfirmWipeDialog>,
}Key responsibilities:
- Device State: Tracks all device information and status
- UI Coordination: Manages panel focus, selection, and modal states
- Cache Management: Handles device detail and metadata caching
- Notification System: Manages user feedback and status messages
The DeviceManager trait provides a unified interface for device operations:
pub trait DeviceManager: Send + Sync + Clone {
fn list_devices(&self) -> impl Future<Output = Result<Vec<Device>>> + Send;
fn start_device(&self, id: &str) -> impl Future<Output = Result<()>> + Send;
fn stop_device(&self, id: &str) -> impl Future<Output = Result<()>> + Send;
fn create_device(&self, config: &DeviceConfig) -> impl Future<Output = Result<()>> + Send;
fn delete_device(&self, id: &str) -> impl Future<Output = Result<()>> + Send;
fn wipe_device(&self, id: &str) -> impl Future<Output = Result<()>> + Send;
}Platform implementations:
- AndroidManager: Manages AVDs using Android SDK tools
- IosManager: Controls iOS simulators via Xcode simctl
The main App struct coordinates all application components:
pub struct App {
state: Arc<Mutex<AppState>>,
android_manager: AndroidManager,
ios_manager: Option<IosManager>,
log_update_handle: Option<JoinHandle<()>>,
detail_update_handle: Option<JoinHandle<()>>,
}Responsibilities:
- Event Loop: Processes user input and system events
- Task Coordination: Manages background tasks and cancellation
- UI Coordination: Coordinates between state and rendering
- Platform Integration: Manages platform-specific operations
Purpose: Provide platform-agnostic interfaces while maintaining type safety.
// Common interface
pub trait DeviceManager: Send + Sync + Clone {
fn list_devices(&self) -> impl Future<Output = Result<Vec<Device>>> + Send;
}
// Platform-specific implementations
impl DeviceManager for AndroidManager {
fn list_devices(&self) -> impl Future<Output = Result<Vec<AndroidDevice>>> + Send {
async {
// Android-specific implementation
}
}
}
impl DeviceManager for IosManager {
fn list_devices(&self) -> impl Future<Output = Result<Vec<IosDevice>>> + Send {
async {
// iOS-specific implementation
}
}
}Benefits:
- Code reuse across platforms
- Easy testing with mock implementations
- Clear separation of platform-specific logic
Purpose: Dynamic system image management for Android devices.
pub struct ApiLevelManagementState {
pub api_levels: Vec<ApiLevel>,
pub selected_index: usize,
pub is_loading: bool,
pub install_progress: Option<InstallProgress>,
pub scroll_offset: usize,
}Features:
- Real-time installation progress tracking
- Scrollable UI with keyboard navigation
- Automatic cache invalidation on changes
- Background installation with progress callbacks
Purpose: Provide thread-safe state access with non-blocking operations.
pub struct App {
state: Arc<Mutex<AppState>>,
}
impl App {
async fn update_device_status(&self, device_id: &str, status: DeviceStatus) {
let mut state = self.state.lock().await;
if let Some(device) = state.find_device_mut(device_id) {
device.status = status;
}
}
}Benefits:
- Safe concurrent access to shared state
- Non-blocking UI with background updates
- Proper task coordination and cancellation
Purpose: Perform expensive operations without blocking the UI.
impl App {
fn start_background_device_loading(&mut self) {
let state_clone = Arc::clone(&self.state);
let android_manager = self.android_manager.clone();
tokio::spawn(async move {
// Load devices in background
let devices = android_manager.list_devices().await?;
let mut state = state_clone.lock().await;
state.android_devices = devices;
state.is_loading = false;
});
}
}Benefits:
- Fast application startup
- Responsive UI during heavy operations
- Proper resource cleanup and cancellation
Purpose: Minimize expensive operations while maintaining data freshness.
impl AppState {
pub fn smart_clear_cached_device_details(&mut self, new_panel: Panel) {
if let Some(ref cached) = self.cached_device_details {
if cached.platform != new_panel {
self.clear_cached_device_details();
}
}
}
}Benefits:
- Reduced API calls and command executions
- Faster UI responsiveness
- Intelligent cache invalidation
Purpose: Prevent UI stuttering during rapid user interactions.
impl App {
async fn schedule_device_details_update(&mut self) {
// Cancel previous update
if let Some(handle) = self.detail_update_handle.take() {
handle.abort();
}
// Schedule new update with delay
let handle = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
// Perform update
});
self.detail_update_handle = Some(handle);
}
}Benefits:
- Smooth user experience during rapid navigation
- Reduced system load from excessive updates
- Proper task cancellation to prevent resource leaks
User Input → Event Processing → State Update → UI Rendering
↓ ↓ ↓ ↑
Keyboard → App::run() → AppState → ui::render()
User Action → Device Manager → System Command → State Update → UI Update
↓ ↓ ↓ ↓ ↑
Press 'c' → create_device() → avdmanager → update_state() → render()
App Start → Background Task → API Call → State Update → UI Refresh
↓ ↓ ↓ ↓ ↑
App::new() → tokio::spawn() → list_devices() → state.devices = ... → render()
- Immediate UI Rendering: Show interface within ~50ms
- Background Data Loading: Load device lists asynchronously
- Progressive Enhancement: Add features as data becomes available
- Cache Preloading: Preload device types and API levels at startup
- Target Performance: Startup time < 150ms (typical: ~104ms)
- Debounced Updates: 50-100ms delays prevent UI stuttering during rapid navigation
- Smart Caching: Cache expensive API calls and command outputs with platform-aware invalidation
- Selective Rendering: Only update changed UI components
- Task Management: Proper cleanup and cancellation of background tasks
- Performance Benchmarks: Panel switching < 100ms, device navigation < 50ms, log streaming < 10ms latency
- Log Rotation: Automatic cleanup of old log entries (1000 entries max)
- Cache Expiration: Remove stale cached data
- Resource Cleanup: Proper disposal of system resources
- Background Task Limits: Prevent unlimited task spawning
// Custom application errors
#[derive(thiserror::Error, Debug)]
pub enum DeviceError {
#[error("Device not found: {name}")]
NotFound { name: String },
#[error("Invalid device configuration: {reason}")]
InvalidConfig { reason: String },
#[error("Platform not supported: {platform}")]
UnsupportedPlatform { platform: String },
}
// Error propagation with context
fn create_device(config: &DeviceConfig) -> Result<()> {
validate_config(config)
.with_context(|| format!("Invalid config for device '{}'", config.name))?;
execute_creation(config)
.with_context(|| "Failed to create device")?;
Ok(())
}- Graceful Degradation: Continue operation with reduced functionality
- User Feedback: Provide clear, actionable error messages
- Automatic Retry: Retry transient failures with backoff
- Fallback Options: Provide alternative approaches when primary fails
pub fn format_user_error(error: &anyhow::Error) -> String {
match error.downcast_ref::<DeviceError>() {
Some(DeviceError::NotFound { name }) => {
format!("Device '{}' not found. Please check if it exists.", name)
}
Some(DeviceError::InvalidConfig { reason }) => {
format!("Configuration error: {}. Please check your settings.", reason)
}
_ => format!("An error occurred: {}", error),
}
}The project has 15 test files with 31+ test functions covering:
- Location: Alongside source code in
#[cfg(test)]modules - Purpose: Test individual functions and methods
- Focus: Logic validation, edge cases, error conditions
- Device Lifecycle: Complete device management workflows (
comprehensive_integration_test.rs) - Performance Tests: Startup time and responsiveness validation (
responsiveness_validation_test.rs) - UI Tests: Navigation, focus management, state coordination (
ui_focus_and_theme_test.rs) - Device Operations: Creation, status tracking, operations (
device_operations_status_test.rs) - Navigation Tests: Field navigation, circular navigation (
device_creation_navigation_test.rs) - Error Handling: Error conditions and recovery scenarios
- Startup Time: < 150ms (typical: ~104ms)
- Panel Switching: < 100ms
- Device Navigation: < 50ms
- Log Streaming: Real-time with < 10ms latency
#[tokio::test]
async fn test_device_creation() {
let manager = AndroidManager::new().unwrap();
let config = DeviceConfig::new("test_device", "pixel_7", "31");
let result = manager.create_device(&config).await;
assert!(result.is_ok());
}#[test]
fn test_state_consistency() {
let mut state = AppState::new();
// Setup initial state
state.add_device(mock_device());
// Perform operation
state.select_device(0);
// Verify state consistency
assert_eq!(state.selected_android, 0);
assert!(state.get_selected_device().is_some());
}#[tokio::test]
async fn test_startup_performance() {
let start = Instant::now();
let app = App::new(Config::default()).await?;
let duration = start.elapsed();
assert!(duration < Duration::from_millis(150));
println!("Startup time: {:?}", duration); // Typical: ~104ms
}- mockall: Mock external dependencies and system commands
- Test Doubles: Controlled test environments
- Isolation: Independent test execution
- assert_cmd: Command-line interface testing
- predicates: Complex assertion conditions
- Custom Assertions: Domain-specific test helpers
- Dynamic System Image Discovery: Real-time detection of available system images
- Installation Progress Tracking: Live progress updates during installation
- Smart Cache Invalidation: Automatic cache refresh on system image changes
- Architecture Detection: Automatic selection of optimal architecture (x86_64/arm64)
- Device Creation Cache: Pre-loaded device types and API levels
- Background Refresh: Automatic cache updates without blocking UI
- Context-Aware Invalidation: Cache cleared on relevant operations
- Scrollable Dialogs: Better handling of long lists
- Loading Indicators: Clear feedback during async operations
- Keyboard Navigation: Circular navigation in device lists
- Real-time Status Updates: Live device status monitoring
- Automatic App Lifecycle: Simulator.app opens automatically when starting devices
- Smart Cleanup: Simulator.app quits automatically when last device stops
- Graceful Shutdown: Uses AppleScript for clean app termination with fallback
- Dock Management: Prevents Simulator.app icon from lingering in Dock
The application uses a modular constants system (constants/):
constants/
├── commands.rs # CLI tool names and arguments
├── defaults.rs # Default values and configurations
├── env_vars.rs # Environment variable names
├── files.rs # File paths and extensions
├── messages.rs # User-facing strings and messages
├── patterns.rs # Regular expressions for parsing
└── performance.rs # Performance tuning parameters
This architecture provides a solid foundation for building a responsive, maintainable, and cross-platform terminal application while ensuring reliability through comprehensive testing.