-
Notifications
You must be signed in to change notification settings - Fork 0
Ricecoder TUI Design
Status: ✅ Complete
Last Updated: December 25, 2025
The RiceCoder TUI (Terminal User Interface) provides a beautiful, responsive terminal experience built with ratatui. Following Domain-Driven Design principles, the TUI layer is architecturally isolated from business logic, focusing solely on rendering, user interaction, and terminal management.
Core Principle: Pure UI concerns with zero business logic dependencies.
Presentation Layer - Terminal UI rendering and user interaction handling
┌───────────────────────────────────────────────┐
│ ricecoder-tui (Presentation) │
│ - Terminal rendering (ratatui/crossterm) │
│ - Widget composition and layout │
│ - Event handling and keyboard navigation │
│ - Theme application and styling │
│ - Accessibility features │
└───────────────────────────────────────────────┘
▲
│ (injected via DI)
┌───────────────────────────────────────────────┐
│ ricecoder-application (Use Cases) │
│ - Session management (ricecoder-sessions) │
│ - Provider integration (ricecoder-providers) │
│ - Command execution (ricecoder-commands) │
└───────────────────────────────────────────────┘
- Terminal UI widgets and components
- Layout management and responsive design
- User input handling and keybindings
- Theme rendering and styling
- Screen reader accessibility
- Clipboard operations
- File picker interface
- Status bar and information display
- Session management →
ricecoder-sessions - AI provider integration →
ricecoder-providers - LSP functionality →
ricecoder-lsp - VCS operations →
ricecoder-vcs - File operations →
ricecoder-files
RiceCoder TUI follows Interface Segregation Principle with focused trait definitions:
// Core component lifecycle
pub trait Component {
fn render(&mut self, frame: &mut Frame, area: Rect);
fn handle_event(&mut self, event: &Event) -> EventResult;
}
// Optional traits for specific capabilities
pub trait Focusable {
fn focus(&mut self);
fn unfocus(&mut self);
fn is_focused(&self) -> bool;
}
pub trait Scrollable {
fn scroll_up(&mut self, lines: u16);
fn scroll_down(&mut self, lines: u16);
fn scroll_to_top(&mut self);
fn scroll_to_bottom(&mut self);
}
pub trait Themeable {
fn apply_theme(&mut self, theme: &Theme);
}Why Segregated?
- Widgets implement only needed traits
- No "fat interfaces" forcing unnecessary methods
- Clear separation of concerns
- Easy to compose new widgets
| Component | Purpose | Traits | Dependencies |
|---|---|---|---|
App |
Main TUI application | Component |
Event loop, layout manager |
ChatInputWidget |
Message input area |
Component, Focusable
|
Input analyzer, intent detection |
MessageAreaWidget |
Conversation display |
Component, Scrollable, Themeable
|
Markdown rendering, syntax highlighting |
StatusBar |
Status information |
Component, Themeable
|
Session info, provider status |
FilePickerWidget |
File selection |
Component, Focusable, Scrollable
|
File system access |
CommandPaletteWidget |
Command launcher |
Component, Focusable
|
Command registry |
DiffWidget |
Code diff display |
Component, Scrollable, Themeable
|
Diff parsing, syntax highlighting |
ImageWidget |
Image rendering | Component |
Image format detection, terminal rendering |
The TUI uses constraint-based layout with automatic adaptation:
pub struct LayoutConfig {
pub sidebar_width: u16, // 30 by default
pub min_width: u16, // 80 minimum
pub max_scrollback: usize, // 10,000 messages
pub split_ratio: f32, // 70/30 split
}
// Layout adapts to terminal size
fn calculate_layout(area: Rect) -> Vec<Rect> {
Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), // Header
Constraint::Min(10), // Main area (flex)
Constraint::Length(3), // Input
Constraint::Length(1), // Status bar
])
.split(area)
}- Chat Layout: Header + Messages + Input + Status
- Diff View: Side-by-side or unified diff rendering
- File Picker: Tree view with preview pane
- Command Palette: Overlay with fuzzy search
- Help Dialog: Modal overlay with keybind reference
- < 80 cols: Compact mode, hide sidebar
- 80-120 cols: Standard mode
- > 120 cols: Wide mode, show additional panels
- < 24 rows: Minimal UI, essential elements only
Themes are managed by ricecoder-themes crate, applied by TUI:
pub struct Theme {
pub name: String,
pub colors: ThemeColors,
pub styles: ThemeStyles,
}
pub struct ThemeColors {
pub foreground: Color,
pub background: Color,
pub accent: Color,
pub border: Color,
pub syntax: SyntaxColors,
}30+ themes including:
-
tokyo-night(default) draculanordgruvboxsolarized-dark/lightmonokai
// Themes applied at runtime via ThemeManager
impl Themeable for MessageAreaWidget {
fn apply_theme(&mut self, theme: &Theme) {
self.background = theme.colors.background;
self.text_color = theme.colors.foreground;
self.syntax_theme = theme.colors.syntax;
}
}Themes can be switched without restarting:
# In chat, press `:` for command mode
:theme tokyo-night
:theme draculapub struct ScreenReaderAnnouncer {
pub enabled: bool,
pub priority: AnnouncementPriority,
}
pub enum AnnouncementPriority {
Low, // Background updates
Medium, // User actions
High, // Errors, warnings
Critical, // System failures
}
impl ScreenReaderAnnouncer {
pub fn announce(&self, message: &str, priority: AnnouncementPriority) {
// Platform-specific screen reader integration
}
}Full keyboard-driven interface:
| Action | Default Key | Vim Mode Key |
|---|---|---|
| Send message | Enter |
Enter |
| Scroll up |
↑ / PgUp
|
k / Ctrl+u
|
| Scroll down |
↓ / PgDn
|
j / Ctrl+d
|
| Focus input | i |
i |
| Command palette | Ctrl+p |
: |
| Copy output | Ctrl+c |
yy |
| Exit | Ctrl+q |
ZZ |
Automatic high contrast theme for accessibility:
accessibility:
high_contrast: true # Enhanced visibility
focus_indicators: true # Clear focus outlines
reduced_motion: true # Disable animationspub struct FocusManager {
focused: Option<ComponentId>,
focus_ring: Vec<ComponentId>,
}
impl FocusManager {
pub fn next(&mut self) -> Option<ComponentId> {
// Tab/Shift+Tab navigation
}
pub fn set_focus(&mut self, id: ComponentId) {
// Announce focus change to screen reader
}
}pub enum Event {
Key(KeyEvent),
Mouse(MouseEvent),
Resize(u16, u16),
Tick,
Quit,
}
pub struct EventLoop {
rx: Receiver<Event>,
tick_rate: Duration,
}
impl EventLoop {
pub async fn next(&mut self) -> Option<Event> {
// Non-blocking event polling with timeout
select! {
event = self.rx.recv() => event.ok(),
_ = tokio::time::sleep(self.tick_rate) => Some(Event::Tick),
}
}
}Components handle events via trait methods:
pub enum EventResult {
Consumed, // Event handled, stop propagation
Ignored, // Event not handled, continue
Propagate(Event), // Transform and continue
}
impl Component for ChatInputWidget {
fn handle_event(&mut self, event: &Event) -> EventResult {
match event {
Event::Key(key) if key.code == KeyCode::Enter => {
self.submit_message();
EventResult::Consumed
}
_ => EventResult::Ignored
}
}
}- Capture: Top-down (App → Focused widget)
- Target: Focused widget processes event
- Bubble: Bottom-up (Widget → App)
- Target: 60 FPS with <16ms frame time
- Actual: 60 FPS in typical usage
- Strategy: Diff-based rendering (only changed areas)
- Typical: <50 MB for TUI components
- Large sessions: <200 MB with 10,000 messages
- Strategy: Streaming large content, virtual scrolling
- Diff-based Rendering: Only re-render changed widgets
- Virtual Scrolling: Render visible area only
- Layout Caching: Cache layout calculations
- Theme Precompilation: Compile styles at load time
- Event Debouncing: Batch rapid events (resize, scroll)
# Run TUI performance benchmarks
cargo bench -p ricecoder-tui
# Results (typical hardware):
# - Render frame: ~2-5ms
# - Handle keypress: <1ms
# - Apply theme: ~10ms
# - Layout calculation: ~0.5ms[dependencies]
ratatui = "0.29" # Terminal rendering
crossterm = "0.27" # Cross-platform I/O
tokio = "1.0" # Async runtime
# RiceCoder infrastructure (no business logic)
ricecoder-storage = "0.1" # Config persistence
ricecoder-themes = "0.1" # Theme management
ricecoder-keybinds = "0.1" # Keybind handling
ricecoder-images = "0.1" # Image display
ricecoder-help = "0.1" # Help system✅ Allowed: Infrastructure crates (storage, themes, images)
❌ Forbidden: Business logic crates (sessions, providers, agents)
Business logic injected via:
- Dependency Injection container
- Event callbacks
- Interface traits
ricecoder-tui/
├── src/
│ └── **/*.rs # Inline unit tests
└── tests/
├── integration/ # Integration tests
├── accessibility/ # Accessibility tests
└── layout/ # Layout tests| Area | Tests | Coverage |
|---|---|---|
| Component rendering | 45+ | 85% |
| Event handling | 38+ | 88% |
| Layout management | 25+ | 90% |
| Theme application | 20+ | 92% |
| Accessibility | 15+ | 80% |
| Total | 143+ | 87% |
- Component lifecycle (mount, update, unmount)
- Event propagation and handling
- Theme switching without state loss
- Layout adaptation to terminal size
- Keyboard navigation completeness
- Screen reader announcements
- Cross-platform compatibility (Windows, Linux, macOS)
- TUI Interface Guide - User-facing TUI guide
- DDD Layering and Boundaries - Architecture boundaries
- Architecture Overview - Full system design
- Theme System - Theme customization
- Accessibility Features - Accessibility details