# Ratatui TUI Guide
This guide documents the enhanced terminal UI system using `ratatui` for `mecha10 dev` mode.
## Overview
**Why Ratatui?**
We've added `ratatui` (formerly tui-rs) to provide sophisticated terminal UIs with features like:
- ✅ Split-pane layouts (sidebar for logs)
- ✅ Scrollable text widgets
- ✅ Real-time updates
- ✅ Color-coded logs
- ✅ Professional terminal interface
**Current Status:** Framework is in place, integration pending.
---
## Architecture
### Components
1. **`LogBuffer`** (`dev_tui.rs`)
- Thread-safe log collection (Arc<Mutex<Vec<String>>>)
- Automatic size management (keeps last N logs)
- Clone-able for sharing across tasks
2. **`DevModeTui`** (`dev_tui.rs`)
- Example TUI with logs sidebar
- 60/40 split layout (main content / logs)
- Scrollable logs with PgUp/PgDn
3. **`TeleopTui`** (`teleop_tui.rs`)
- Enhanced teleop with real-time logs
- Status display (velocity, commands sent)
- Color-coded log levels
---
## Usage Examples
### Basic Example: Standalone TUI
```rust
use mecha10_cli::ui::{LogBuffer, DevModeTui};
#[tokio::main]
async fn main() -> Result<()> {
// Create log buffer (keeps last 1000 logs)
let log_buffer = LogBuffer::new(1000);
// Populate with some test logs
log_buffer.push("INFO: Starting system".to_string());
log_buffer.push("WARN: Low battery".to_string());
log_buffer.push("ERROR: Connection timeout".to_string());
// Create and run TUI
let mut tui = DevModeTui::new(log_buffer.clone());
tui.run()?;
Ok(())
}
```
### Integration Example: Teleop with Logs
```rust
use mecha10_cli::ui::{LogBuffer, TeleopTui};
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
#[tokio::main]
async fn main() -> Result<()> {
let redis_url = "redis://localhost:6379".to_string();
let log_buffer = LogBuffer::new(1000);
// Spawn background task to collect logs from Redis
tokio::spawn({
let buffer = log_buffer.clone();
async move {
// Subscribe to node logs and populate buffer
// (implementation depends on your logging setup)
collect_logs_from_redis(buffer, redis_url.clone()).await
}
});
// Create teleop TUI with logs sidebar
let running = Arc::new(AtomicBool::new(true));
let mut teleop = TeleopTui::new(redis_url, log_buffer);
teleop.run_with_interrupt(running).await?;
Ok(())
}
```
### Collecting Logs from Nodes
**Option 1: Redis Pub/Sub**
```rust
async fn collect_logs_from_redis(log_buffer: LogBuffer, redis_url: String) -> Result<()> {
let client = redis::Client::open(redis_url)?;
let conn = client.get_connection()?;
let mut pubsub = conn.as_pubsub();
// Subscribe to log topics
pubsub.psubscribe("/logs/*")?;
// Stream logs into buffer
loop {
let msg = pubsub.get_message()?;
if let Ok(log_line) = msg.get_payload::<String>() {
log_buffer.push(log_line);
}
}
}
```
**Option 2: Tracing Subscriber**
```rust
use tracing_subscriber::layer::SubscriberExt;
// Custom layer that sends logs to LogBuffer
struct LogBufferLayer {
buffer: LogBuffer,
}
impl<S> Layer<S> for LogBufferLayer
where
S: Subscriber,
{
fn on_event(&self, event: &Event, _ctx: Context<S>) {
let log_line = format!("{:?}", event);
self.buffer.push(log_line);
}
}
// Setup
let log_buffer = LogBuffer::new(1000);
let subscriber = tracing_subscriber::registry()
.with(LogBufferLayer { buffer: log_buffer.clone() });
tracing::subscriber::set_global_default(subscriber)?;
```
---
## Layout System
Ratatui uses a constraint-based layout system:
```rust
use ratatui::layout::{Layout, Direction, Constraint};
// Horizontal split: 60% left, 40% right
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(60), // Main content
Constraint::Percentage(40), // Logs sidebar
])
.split(frame.area());
// Vertical split: fixed header, flexible content, fixed footer
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // Header
Constraint::Min(0), // Content (fills remaining)
Constraint::Length(1), // Footer
])
.split(frame.area());
```
---
## Styling
### Colors
```rust
use ratatui::style::{Color, Style, Modifier};
// Basic colors
Style::default().fg(Color::Red)
Style::default().bg(Color::Blue)
// RGB colors
Style::default().fg(Color::Rgb(255, 128, 0))
// Modifiers
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC)
```
### Log Level Colors
```rust
fn style_for_log(log: &str) -> Style {
if log.contains("ERROR") {
Style::default().fg(Color::Red)
} else if log.contains("WARN") {
Style::default().fg(Color::Yellow)
} else if log.contains("INFO") {
Style::default().fg(Color::Green)
} else if log.contains("DEBUG") {
Style::default().fg(Color::Blue)
} else {
Style::default().fg(Color::White)
}
}
```
---
## Widgets
### List (for logs)
```rust
use ratatui::widgets::{List, ListItem};
let items: Vec<ListItem> = logs
.iter()
.map(|log| {
let style = style_for_log(log);
ListItem::new(log.as_str()).style(style)
})
.collect();
let list = List::new(items)
.block(Block::default()
.borders(Borders::ALL)
.title(" Logs "));
frame.render_widget(list, area);
```
### Paragraph (for status)
```rust
use ratatui::widgets::Paragraph;
use ratatui::text::{Line, Span};
let text = vec![
Line::from(vec![
Span::raw("Status: "),
Span::styled("Running", Style::default().fg(Color::Green)),
]),
Line::from(vec![
Span::raw("Linear: "),
Span::styled(format!("{:.2}", velocity), Style::default().fg(Color::Cyan)),
]),
];
let paragraph = Paragraph::new(text)
.block(Block::default()
.borders(Borders::ALL)
.title(" Status "));
frame.render_widget(paragraph, area);
```
---
## Integration into `mecha10 dev`
### Step 1: Add Log Collection
In `ops.rs` or `session.rs`:
```rust
pub async fn run_interactive_mode(
ctx: &mut CliContext,
config: InteractiveModeConfig<'_>,
state: &mut InteractiveModeState,
) -> Result<()> {
// Create log buffer
let log_buffer = LogBuffer::new(1000);
// Spawn background log collector
let redis_url = ctx.redis_url().to_string();
tokio::spawn({
let buffer = log_buffer.clone();
async move {
collect_node_logs(buffer, redis_url).await
}
});
// ... rest of implementation
}
```
### Step 2: Replace Teleop UI
In `enter_teleop_mode()`:
```rust
pub async fn enter_teleop_mode(
config: &ProjectConfig,
pids: &HashMap<String, u32>,
redis_url: &str,
running: &Arc<AtomicBool>,
log_buffer: LogBuffer, // Add this parameter
) -> Result<()> {
// Check teleop availability...
if has_teleop && teleop_running {
// Use enhanced TUI instead of simple UI
let mut teleop_tui = TeleopTui::new(redis_url.to_string(), log_buffer);
teleop_tui.run_with_interrupt(running.clone()).await?;
}
Ok(())
}
```
### Step 3: Add Feature Flag (Optional)
```toml
# In mecha10.json or as CLI flag
"dev": {
"enhanced_ui": true, // Enable ratatui TUI
"logs_sidebar": true
}
```
```rust
// In code
if config.dev.enhanced_ui {
// Use ratatui TUI
} else {
// Use simple crossterm UI
}
```
---
## Keyboard Controls
### Standard Controls
- `↑ / ↓` or `PgUp / PgDn` - Scroll logs
- `q` or `Esc` - Exit
- `Ctrl+C` - Force exit
### Teleop Controls
- `W / ↑` - Forward
- `S / ↓` - Backward
- `A / ←` - Turn left
- `D / →` - Turn right
- `Space / X` - Stop
- `E` - Emergency stop
---
## Performance Considerations
### Log Buffer Size
```rust
// For development (lots of logs)
let log_buffer = LogBuffer::new(10000);
// For embedded/low memory
let log_buffer = LogBuffer::new(500);
```
### Update Rate
```rust
// In event loop
if event::poll(Duration::from_millis(100))? { // 10 FPS
// Handle events and redraw
}
// For high-frequency updates
if event::poll(Duration::from_millis(16))? { // ~60 FPS
// Handle events and redraw
}
```
### Log Filtering
```rust
impl LogBuffer {
pub fn push_filtered(&self, log: String, min_level: &str) {
// Only add logs above minimum level
if should_log(&log, min_level) {
self.push(log);
}
}
}
fn should_log(log: &str, min_level: &str) -> bool {
match min_level {
"ERROR" => log.contains("ERROR"),
"WARN" => log.contains("WARN") || log.contains("ERROR"),
"INFO" => !log.contains("DEBUG"),
_ => true,
}
}
```
---
## Advanced Features
### Multiple Panes
```rust
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(33), // Controls
Constraint::Percentage(34), // Logs
Constraint::Percentage(33), // Metrics
])
.split(frame.area());
```
### Tabs
```rust
use ratatui::widgets::Tabs;
let tabs = Tabs::new(vec!["Logs", "Metrics", "Diagnostics"])
.select(selected_tab)
.style(Style::default().fg(Color::White))
.highlight_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD));
frame.render_widget(tabs, area);
```
### Charts
```rust
use ratatui::widgets::{Chart, Dataset, GraphType, Axis};
let datasets = vec![
Dataset::default()
.name("Velocity")
.marker(Marker::Dot)
.graph_type(GraphType::Line)
.style(Style::default().fg(Color::Cyan))
.data(&velocity_history),
];
let chart = Chart::new(datasets)
.block(Block::default().title("Velocity History"))
.x_axis(Axis::default().title("Time"))
.y_axis(Axis::default().title("m/s"));
frame.render_widget(chart, area);
```
---
## Debugging TUI
### Terminal State Issues
If terminal gets corrupted:
```bash
# Reset terminal
reset
# Or clear and reset
clear && reset
```
### Adding Debug Logs
```rust
// Write debug logs to file instead of terminal
use std::fs::OpenOptions;
use std::io::Write;
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open("/tmp/mecha10_tui_debug.log")?;
writeln!(file, "DEBUG: Current state: {:?}", self)?;
```
---
## Next Steps
1. **Integrate into dev mode**
- Add `LogBuffer` to `DevSession`
- Spawn log collection task
- Replace simple UI with ratatui TUI
2. **Add metrics panel**
- CPU, memory, network usage
- Message throughput
- Node status
3. **Add search/filter**
- Filter logs by level
- Search logs by keyword
- Regex matching
4. **Add export**
- Save logs to file
- Export metrics to CSV
- Screenshot current view
---
## Resources
- **Ratatui Docs**: https://ratatui.rs/
- **Examples**: https://github.com/ratatui/ratatui/tree/main/examples
- **Widgets**: https://docs.rs/ratatui/latest/ratatui/widgets/index.html
- **Crossterm Backend**: https://docs.rs/crossterm/latest/crossterm/
---
## FAQ
**Q: Why not just use `println!` for logs?**
A: In teleop mode, you need to see both controls and logs simultaneously. A TUI provides this without terminal conflicts.
**Q: Can I use this in embedded systems?**
A: Yes, but consider memory constraints. Use a smaller log buffer and lower update rate.
**Q: Does this work over SSH?**
A: Yes! Ratatui works perfectly over SSH connections.
**Q: Can I add custom panels?**
A: Absolutely! See "Advanced Features" section for examples.