Design Patterns in MonoTerm
This document explains the key design patterns used in MonoTerm’s architecture. These patterns are language-independent design solutions, not Rust-specific features.Copy
┌─────────────────────────────────────────────────────────────────┐
│ PATTERN ORIGINS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ SessionActor Pattern ACK Flow Control │
│ ────────────────── ──────────────── │
│ │
│ Origin: Erlang (1986) Origin: TCP/IP (1974) │
│ Level: Concurrency Pattern Level: Network Protocol │
│ │
│ Actor Model (1973) TCP ACK (1974) │
│ │ │ │
│ ▼ ▼ │
│ Erlang/OTP Backpressure │
│ │ │ │
│ ▼ ▼ │
│ Akka (Scala), Orleans (C#) Flow Control │
│ │ │ │
│ ▼ ▼ │
│ Tokio MPSC (Rust) MonoTerm ACK │
│ │
│ Conclusion: Design pattern level, not system level │
│ │
└─────────────────────────────────────────────────────────────────┘
The Problem: Concurrent Access
Multiple parts of the system need to access session data simultaneously.Copy
┌─────────────────────────────────────────────────────────────────┐
│ THE CONCURRENT ACCESS PROBLEM │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Places that access "sessions" data: │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. User creates new tab Insert new session │ │
│ │ 2. PTY data arrives Write to session │ │
│ │ 3. User closes tab Remove session │ │
│ │ 4. Resize event Modify session │ │
│ │ 5. Tray menu lists tabs Read all sessions │ │
│ │ │ │
│ │ These can happen SIMULTANEOUSLY! │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Dangerous Situation: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Thread A: Processing PTY data... │ │
│ │ Get session "tab-1" │ │
│ │ Write data... While writing! │ │
│ │ │ │ │
│ │ Thread B: User closes tab! ▼ │ │
│ │ Remove "tab-1" Simultaneous delete! │ │
│ │ │ │
│ │ Result: Crash! │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Solution 1: Mutex (Traditional)
The traditional approach uses locks.Copy
┌─────────────────────────────────────────────────────────────────┐
│ MUTEX APPROACH │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Must acquire lock on every access │
│ │
│ Thread A Thread B Thread C │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ lock() lock() lock() │
│ │ │ WAIT │ WAIT │
│ working... │ │ │
│ │ │ │ │
│ unlock() │ │ │
│ working... │ │
│ │ │ │
│ unlock() │ │
│ working... │
│ │
│ Problems: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Lock Contention: Only one can work at a time │ │
│ │ Deadlock Risk: A waits for B, B waits for A = frozen │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Solution 2: Actor Pattern (MonoTerm)
MonoTerm uses the Actor pattern - no locks needed.Copy
┌─────────────────────────────────────────────────────────────────┐
│ ACTOR PATTERN │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Core Idea: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Don't touch data directly │ │
│ │ Send a message to the person in charge (Actor) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ │
│ Thread A ──┐ │
│ │ ┌─────────────┐ │
│ Thread B ──┼────▶│ MPSC │────▶ SessionActor │
│ │ │ Channel │ (single owner) │
│ Thread C ──┘ └─────────────┘ │ │
│ ▼ │
│ sessions │
│ (data) │
│ │
│ Rules: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Anyone can put messages in Channel (simultaneous OK) │ │
│ │ Only Actor accesses sessions (1 person = no conflicts) │ │
│ │ No locks needed! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ═══════════════════════════════════════════════════════════ │
│ │
│ Analogy: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Mutex = Multiple people trying to open refrigerator │ │
│ │ Take turns one by one (Lock) │ │
│ │ Pushing causes accidents (Deadlock) │ │
│ │ │ │
│ │ Actor = One refrigerator manager │ │
│ │ Leave note "get me a coke please" │ │
│ │ Manager processes requests in order │ │
│ │ Conflicts are impossible │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
SessionActor Structure
The SessionActor owns all session state.Copy
┌─────────────────────────────────────────────────────────────────┐
│ SESSIONACTOR STRUCTURE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Message Types (what requests are possible): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ CreateSession Create new tab │ │
│ │ CloseSession Close tab │ │
│ │ WriteData Write to PTY │ │
│ │ Resize Change terminal size │ │
│ │ GridAck ACK received from frontend │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Actor Owns: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ sessions Session data (Actor only!) │ │
│ │ grid_workers Per-session workers │ │
│ │ rx Mailbox (receives messages) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Actor Main Loop: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ loop { │ │
│ │ Get message from mailbox │ │
│ │ │ │
│ │ Match message type: │ │
│ │ CreateSession Create new session │ │
│ │ CloseSession Remove session │ │
│ │ WriteData Write to session │ │
│ │ Resize Resize session │ │
│ │ GridAck Open ACK gate │ │
│ │ } │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Actor Pattern Benefits
Copy
┌─────────────────────────────────────────────────────────────────┐
│ ACTOR BENEFITS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Frontend Tauri Commands SessionActor │
│ (TypeScript) (multiple threads) (single owner) │
│ │
│ ┌─────────┐ │
│ │ New Tab │────┐ │
│ └─────────┘ │ │
│ │ ┌──────────┐ ┌─────────────────┐ │
│ ┌─────────┐ ├─────▶│ MPSC │──────▶│ SessionActor │ │
│ │ Resize │────┤ │ Channel │ │ │ │
│ └─────────┘ │ └──────────┘ │ sessions: {...} │ │
│ │ │ ↑ │ │
│ ┌─────────┐ │ │ Only accessible │ │
│ │Close Tab│────┘ │ from here! │ │
│ └─────────┘ └─────────────────┘ │
│ │
│ ┌─────────┐ │ │
│ │PTY Data │─────────────────────────────────────┘ │
│ └─────────┘ │
│ │
│ ═══════════════════════════════════════════════════════════ │
│ │
│ Actor Benefits: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ No lock contention (single owner) │ │
│ │ No deadlocks (no locks!) │ │
│ │ Guaranteed ordering (MPSC channel) │ │
│ │ Easy to reason about (one thread owns state) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Why Rust?
The patterns work in any language, but Rust provides additional benefits.Copy
┌─────────────────────────────────────────────────────────────────┐
│ WHY RUST FOR MONOTERM │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Patterns work anywhere: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ JavaScript Python Go Rust │ │
│ │ ✓ ✓ ✓ ✓ │ │
│ │ │ │
│ │ Actor pattern and ACK flow control work in all │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Where Rust matters: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. VTE Parsing (Alacritty) │ │
│ │ Called thousands of times per second │ │
│ │ GC would cause stuttering │ │
│ │ │ │
│ │ 2. Cell Processing │ │
│ │ 120 cols x 40 rows = 4,800 cells │ │
│ │ Needs native speed │ │
│ │ │ │
│ │ 3. PTY I/O │ │
│ │ Unix socket communication │ │
│ │ Needs system-level access │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Architecture choice: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Option A: VTE(Rust) → IPC → SessionActor(JS) → IPC │ │
│ │ │ │
│ │ Option B: VTE(Rust) + SessionActor(Rust) → IPC │ │
│ │ ↑ MonoTerm uses this │ │
│ │ │ │
│ │ IPC count: A = 2 times, B = 1 time │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Complete Architecture
Copy
┌─────────────────────────────────────────────────────────────────┐
│ PATTERNS ACROSS UNIX SOCKET │
├─────────────────────────────────────────────────────────────────┤
│ │
│ PTY Daemon Unix Socket Tauri Backend │
│ ────────── ─────────── ───────────── │
│ │
│ ┌─────────┐ ┌─────────────┐ │
│ │ Shell │ │SessionActor │ │
│ │ bash │────────────────────────────────▶ │ │ │
│ │ zsh │ Raw bytes │ Sessions │ │
│ │ fish │ │ Workers │ │
│ └─────────┘ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ VTE Parser │ │
│ │ (Alacritty) │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ Tauri IPC ┌─────────────┐ │
│ Frontend ◀────────────────────────────────│ Cell Convert│ │
│ (xterm.js WebGL) │ + DiffHint │ │
│ └─────────────┘ │
│ │
│ Pattern Summary: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SessionActor: Lock-free concurrency via MPSC │ │
│ │ ACK Flow: Frontend-driven backpressure │ │
│ │ Sidecar: PTY daemon survives app crash │ │
│ │ DiffHint: Intelligent partial rendering │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Summary
Copy
┌─────────────────────────────────────────────────────────────────┐
│ DESIGN PATTERNS SUMMARY │
├─────────────────────────────────────────────────────────────────┤
│ │
│ SessionActor / ACK Flow Control: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Level: Design Pattern (language-independent) │ │
│ │ Origin: Erlang (Actor), TCP (ACK) │ │
│ │ Rust required? NO │ │
│ │ Works in JS/Python/Go? YES │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Where Rust is critical: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ VTE Parsing (Alacritty) Performance critical │ │
│ │ PTY I/O (Unix socket) System level │ │
│ │ Cell Processing Better without GC │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Why this architecture: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ VTE parsing is in Rust anyway │ │
│ │ SessionActor in Rust reduces IPC overhead │ │
│ │ Patterns are high-level, placement determines perf │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘