Skip to main content

boxmux_lib/model/
common.rs

1use serde::Deserializer;
2use serde_json::Value;
3use std::{collections::HashMap, error::Error, hash::Hash};
4
5use crate::{
6    color_utils::{get_bg_color, get_fg_color},
7    model::choice::Choice,
8    screen_bounds, screen_height, screen_width,
9    utils::input_bounds_to_bounds,
10    AppContext, AppGraph, Layout, Message, MuxBox,
11};
12use serde::{Deserialize, Serialize};
13use std::time::Duration;
14
15// F0220: ExecutionMode Enum Definition - Replace thread+pty boolean flags with single enum
16#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq, Default)]
17pub enum ExecutionMode {
18    /// Synchronous execution on UI thread but still flowing through stream architecture
19    #[default]
20    Immediate,
21    /// Background execution in thread pool with stream updates when complete
22    Thread,
23    /// Real-time PTY execution with continuous stream updates
24    Pty,
25}
26
27impl ExecutionMode {
28    /// Get descriptive string for execution mode
29    pub fn description(&self) -> &'static str {
30        match self {
31            ExecutionMode::Immediate => "Synchronous execution on UI thread",
32            ExecutionMode::Thread => "Background execution in thread pool",
33            ExecutionMode::Pty => "Real-time PTY execution with continuous output",
34        }
35    }
36
37    /// Check if execution mode should create streams
38    pub fn creates_streams(&self) -> bool {
39        true // All execution modes create streams - no bypassing stream architecture
40    }
41
42    /// Check if execution mode is real-time
43    pub fn is_realtime(&self) -> bool {
44        match self {
45            ExecutionMode::Immediate => false,
46            ExecutionMode::Thread => false,
47            ExecutionMode::Pty => true,
48        }
49    }
50
51    /// Check if execution mode runs in background
52    pub fn is_background(&self) -> bool {
53        match self {
54            ExecutionMode::Immediate => false,
55            ExecutionMode::Thread => true,
56            ExecutionMode::Pty => true,
57        }
58    }
59
60    /// F0224: Get stream suffix for execution mode identification
61    pub fn as_stream_suffix(&self) -> &'static str {
62        match self {
63            ExecutionMode::Immediate => "immediate",
64            ExecutionMode::Thread => "thread",
65            ExecutionMode::Pty => "pty",
66        }
67    }
68
69    /// Check if execution mode is PTY-based
70    pub fn is_pty(&self) -> bool {
71        matches!(self, ExecutionMode::Pty)
72    }
73
74    /// Convert legacy thread+pty boolean flags to ExecutionMode enum
75    /// PTY takes precedence when both flags are true
76    pub fn from_legacy(thread: bool, pty: bool) -> Self {
77        match (thread, pty) {
78            (_, true) => ExecutionMode::Pty, // PTY takes precedence
79            (true, false) => ExecutionMode::Thread,
80            (false, false) => ExecutionMode::Immediate,
81        }
82    }
83}
84
85// UNIFIED EXECUTION ARCHITECTURE - T0300-T0305: New message types for unified execution system
86
87/// T0300: ExecuteScript message struct - Universal entry point for all script execution
88#[derive(Debug, Clone, PartialEq, Hash, Eq)]
89pub struct ExecuteScript {
90    pub script: Vec<String>,             // Script commands to execute
91    pub source: ExecutionSource,         // Source information and reference
92    pub execution_mode: ExecutionMode,   // How to execute (Batch/Thread/PTY)
93    pub target_box_id: String,           // Where to create the stream
94    pub libs: Vec<String>,               // Library dependencies
95    pub redirect_output: Option<String>, // Optional output redirection
96    pub append_output: bool,             // Append vs replace mode
97    pub stream_id: String,               // Stream ID from source registry
98    pub target_bounds: Option<Bounds>,   // Target muxbox bounds for PTY sizing
99}
100
101/// T0301: ExecutionSource and SourceType enums - Track what triggered the execution
102#[derive(Debug, Clone, PartialEq, Hash, Eq)]
103pub struct ExecutionSource {
104    pub source_type: SourceType,           // What kind of source this is
105    pub source_id: String,                 // Unique identifier for this execution
106    pub source_reference: SourceReference, // Actual source data/object
107}
108
109#[derive(Debug, Clone, PartialEq, Hash, Eq)]
110pub enum SourceType {
111    Choice(String),   // Choice ID that triggered execution
112    StaticScript,     // Box-level YAML script (one-time execution)
113    PeriodicRefresh,  // Periodic refresh script execution
114    SocketUpdate,     // Dynamic socket-based script
115    RedirectedScript, // Script with output redirection
116    HotkeyScript,     // Hotkey-triggered script
117    ScheduledScript,  // Timer/scheduled execution
118}
119
120#[derive(Debug, Clone, PartialEq, Hash, Eq)]
121pub enum SourceReference {
122    Choice(Choice),         // Full choice object
123    StaticConfig(String),   // YAML configuration reference (one-time)
124    PeriodicConfig(String), // YAML configuration for periodic refresh
125    SocketCommand(String),  // Socket command that triggered this
126    HotkeyBinding(String),  // Hotkey that triggered this
127    Schedule(String),       // Schedule configuration
128}
129
130/// T0302: StreamUpdate message struct - Universal content updates from any execution mode
131#[derive(Debug, Clone, PartialEq, Hash, Eq)]
132pub struct StreamUpdate {
133    pub stream_id: String,             // Target stream to update
134    pub target_box_id: String,         // Target box to create/update stream in
135    pub content_update: String,        // New content to append
136    pub source_state: SourceState,     // Current state of execution source
137    pub execution_mode: ExecutionMode, // Mode that generated this update
138}
139
140/// T0303: SourceState enums - Track execution status for each mode
141#[derive(Debug, Clone, PartialEq, Hash, Eq)]
142pub enum SourceState {
143    Batch(BatchSourceState),
144    Thread(ThreadSourceState),
145    Pty(PtySourceState),
146}
147
148#[derive(Debug, Clone, PartialEq, Hash, Eq)]
149pub struct BatchSourceState {
150    pub task_id: String,           // Task queue identifier
151    pub queue_wait_time: Duration, // Time spent waiting in queue
152    pub execution_time: Duration,  // Actual execution duration
153    pub exit_code: Option<i32>,    // Process exit code
154    pub status: BatchStatus,       // Current status
155}
156
157#[derive(Debug, Clone, PartialEq, Hash, Eq)]
158pub struct ThreadSourceState {
159    pub thread_id: String,             // Thread identifier
160    pub execution_time: Duration,      // How long execution took
161    pub exit_code: Option<i32>,        // Process exit code
162    pub status: ExecutionThreadStatus, // Current status
163}
164
165#[derive(Debug, Clone, PartialEq, Hash, Eq)]
166pub struct PtySourceState {
167    pub process_id: u32,            // PTY process ID
168    pub runtime: Duration,          // How long process has been running
169    pub exit_code: Option<i32>,     // Exit code if completed
170    pub status: ExecutionPtyStatus, // Current process status
171}
172
173#[derive(Debug, Clone, PartialEq, Hash, Eq)]
174pub enum BatchStatus {
175    Queued,         // Waiting in task queue
176    Executing,      // Currently being executed
177    Completed,      // Finished successfully - no more updates
178    Failed(String), // Failed with error message - no more updates
179}
180
181#[derive(Debug, Clone, PartialEq, Hash, Eq)]
182pub enum ExecutionThreadStatus {
183    Running,        // Thread is executing
184    Completed,      // Finished successfully - no more updates
185    Failed(String), // Failed with error message - no more updates
186}
187
188#[derive(Debug, Clone, PartialEq, Hash, Eq)]
189pub enum ExecutionPtyStatus {
190    Starting,       // PTY process starting up
191    Running,        // Process is running normally - more updates expected
192    Completed,      // Process completed successfully - no more updates
193    Failed(String), // Process failed with error - no more updates
194    Terminated,     // Process was killed/terminated - no more updates
195}
196
197/// T0304: SourceAction message struct - Lifecycle management for executing sources
198#[derive(Debug, Clone, PartialEq, Hash, Eq)]
199pub struct SourceAction {
200    pub action: ActionType,            // What action to perform
201    pub source_id: String,             // Source identifier to act on
202    pub execution_mode: ExecutionMode, // Mode of the target source
203}
204
205#[derive(Debug, Clone, PartialEq, Hash, Eq)]
206pub enum ActionType {
207    Kill,   // Terminate execution
208    Query,  // Get status information
209    Pause,  // Pause execution (if supported)
210    Resume, // Resume paused execution
211}
212
213impl SourceState {
214    /// Determine if this source expects more updates based on status (NO is_final flag needed)
215    pub fn expects_more_updates(&self) -> bool {
216        match self {
217            SourceState::Batch(state) => {
218                matches!(state.status, BatchStatus::Queued | BatchStatus::Executing)
219            }
220            SourceState::Thread(state) => matches!(state.status, ExecutionThreadStatus::Running),
221            SourceState::Pty(state) => matches!(
222                state.status,
223                ExecutionPtyStatus::Starting | ExecutionPtyStatus::Running
224            ),
225        }
226    }
227}
228
229#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
230pub enum EntityType {
231    AppContext,
232    App,
233    Layout,
234    MuxBox,
235}
236
237#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
238pub enum StreamType {
239    Content,
240    Choices,
241    RedirectedOutput(String), // Named redirect output
242    PTY,
243    Plugin(String),
244    // F0210: Complete StreamType Enum - Add missing variants for source tracking
245    ChoiceExecution(String), // Track choice executions as streams
246    RedirectSource(String),  // Track redirect output sources
247    ExternalSocket,          // External socket connections
248    PtySession(String),      // PTY session with command info
249    OwnScript,               // Box's own script execution stream
250}
251
252/// Unified source object that tracks all execution sources with their stream IDs
253#[derive(Debug, Clone, PartialEq, Hash, Eq)]
254pub struct UnifiedExecutionSource {
255    pub source_id: String,     // Unique source identifier
256    pub stream_id: String,     // Stream ID for this source
257    pub target_box_id: String, // Which box receives updates
258    pub source_type: ExecutionSourceType,
259    pub created_at: std::time::SystemTime,
260    pub status: ExecutionSourceStatus,
261}
262
263#[derive(Debug, Clone, PartialEq, Hash, Eq)]
264pub enum ExecutionSourceType {
265    StaticContent(String),  // Box's static content
266    PeriodicScript(String), // Box's periodic refresh script
267    ChoiceExecution {
268        // Choice script execution
269        choice_id: String,
270        script: Vec<String>,
271        redirect_output: Option<String>,
272    },
273    PtyProcess {
274        // PTY process
275        process_id: Option<u32>,
276        command: Vec<String>,
277    },
278    SocketUpdate {
279        // Socket-based updates
280        command_type: String,
281    },
282    HotkeyScript {
283        // Hotkey-triggered script
284        hotkey: String,
285        script: Vec<String>,
286    },
287}
288
289#[derive(Debug, Clone, PartialEq, Hash, Eq)]
290pub enum ExecutionSourceStatus {
291    Pending,        // Waiting to start
292    Running,        // Currently executing
293    Completed,      // Finished successfully
294    Failed(String), // Failed with error
295    Terminated,     // Killed/stopped
296}
297
298#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
299pub struct Stream {
300    pub id: String,
301    pub stream_type: StreamType,
302    pub label: String,
303    pub content: Vec<String>,
304    pub choices: Option<Vec<Choice>>, // For choices stream
305    // F0212: Stream Source Tracking - enable stream termination when tabs closed
306    pub source: Option<StreamSource>,
307    // F0216: Stream Change Detection - track content changes for efficient updates
308    #[serde(skip, default = "default_content_hash")]
309    pub content_hash: u64,
310    #[serde(skip, default = "default_system_time")]
311    pub last_updated: std::time::SystemTime,
312    #[serde(skip, default = "default_system_time")]
313    pub created_at: std::time::SystemTime,
314}
315
316// Helper functions for default values
317fn default_content_hash() -> u64 {
318    0
319}
320fn default_system_time() -> std::time::SystemTime {
321    std::time::SystemTime::now()
322}
323
324// F0217: Stream rendering behavior traits - exclusive content OR choices
325pub trait ContentStreamTrait {
326    fn get_content_lines(&self) -> &Vec<String>;
327    fn set_content_lines(&mut self, content: Vec<String>);
328}
329
330pub trait ChoicesStreamTrait {
331    fn get_choices(&self) -> &Vec<Choice>;
332    fn get_choices_mut(&mut self) -> &mut Vec<Choice>;
333    fn set_choices(&mut self, choices: Vec<Choice>);
334}
335
336// F0212: Base trait for all stream sources with lifecycle management
337pub trait StreamSourceTrait {
338    fn source_type(&self) -> &'static str;
339    fn source_id(&self) -> String;
340    fn can_terminate(&self) -> bool;
341    fn cleanup(&self) -> Result<(), String>;
342    fn get_metadata(&self) -> std::collections::HashMap<String, String>;
343}
344
345// F0227: ExecutionMode Stream Source Traits - Execution-mode-specific source traits
346// These traits provide specialized lifecycle management for different execution modes
347
348/// Trait for immediate execution sources - synchronous UI thread execution
349pub trait ImmediateSource: StreamSourceTrait {
350    /// Get the execution result if available
351    fn get_execution_result(&self) -> Option<Result<String, String>>;
352    /// Check if execution is complete
353    fn is_complete(&self) -> bool;
354    /// Get execution duration
355    fn get_execution_duration(&self) -> Option<std::time::Duration>;
356}
357
358/// Trait for thread pool execution sources - background thread execution
359pub trait ThreadPoolSource: StreamSourceTrait {
360    /// Get thread handle for cancellation
361    fn get_thread_id(&self) -> Option<String>;
362    /// Check if thread is still running
363    fn is_thread_running(&self) -> bool;
364    /// Cancel the background thread
365    fn cancel_thread(&self) -> Result<(), String>;
366    /// Get thread execution status
367    fn get_thread_status(&self) -> ThreadStatus;
368    /// Set timeout for thread execution
369    fn set_timeout(&mut self, timeout_seconds: u32);
370}
371
372/// Thread execution status for ThreadPoolSource
373#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash)]
374pub enum ThreadStatus {
375    NotStarted,
376    Running,
377    Completed,
378    Failed,
379    Cancelled,
380    TimedOut,
381}
382
383/// Trait for PTY session sources - real-time process execution
384pub trait PtySessionSource: StreamSourceTrait {
385    /// Get PTY process ID
386    fn get_process_id(&self) -> Option<u32>;
387    /// Check if PTY process is still running
388    fn is_process_running(&self) -> bool;
389    /// Send input to PTY process
390    fn send_input(&self, input: &str) -> Result<(), String>;
391    /// Kill the PTY process
392    fn kill_process(&self) -> Result<(), String>;
393    /// Resize the PTY terminal
394    fn resize_terminal(&self, rows: u16, cols: u16) -> Result<(), String>;
395    /// Get terminal size
396    fn get_terminal_size(&self) -> (u16, u16);
397    /// Get command being executed
398    fn get_command(&self) -> String;
399    /// Get working directory
400    fn get_working_directory(&self) -> Option<String>;
401}
402
403// F0212: Static content sources (no lifecycle management needed)
404#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
405pub struct StaticContentSource {
406    pub content_type: String, // "default", "choices", "manual"
407    pub created_at: std::time::SystemTime,
408}
409
410impl StreamSourceTrait for StaticContentSource {
411    fn source_type(&self) -> &'static str {
412        "static_content"
413    }
414    fn source_id(&self) -> String {
415        format!("static_{}", self.content_type)
416    }
417    fn can_terminate(&self) -> bool {
418        false
419    }
420    fn cleanup(&self) -> Result<(), String> {
421        Ok(())
422    }
423    fn get_metadata(&self) -> std::collections::HashMap<String, String> {
424        let mut meta = std::collections::HashMap::new();
425        meta.insert("content_type".to_string(), self.content_type.clone());
426        meta.insert("created_at".to_string(), format!("{:?}", self.created_at));
427        meta
428    }
429}
430
431// F0212: Choice execution sources with thread management
432#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
433pub struct ChoiceExecutionSource {
434    pub choice_id: String,
435    pub muxbox_id: String,
436    pub thread_id: Option<String>, // For thread tracking
437    pub process_id: Option<u32>,   // For process-based choices
438    pub execution_type: String,    // "threaded", "process", "pty"
439    pub started_at: std::time::SystemTime,
440    pub timeout_seconds: Option<u32>,
441}
442
443/// Periodic refresh execution source - manages periodic script execution with persistent stream
444#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
445pub struct PeriodicRefreshSource {
446    pub source_id: String,                             // Unique source identifier
447    pub box_id: String,                                // Target box for updates
448    pub script: Vec<String>,                           // Script to execute periodically
449    pub stream_id: String,                             // EMBEDDED: Stream ID for consistent updates
450    pub execution_mode: ExecutionMode,                 // How to execute the script
451    pub refresh_interval: u64,                         // Milliseconds between executions
452    pub last_execution: Option<std::time::SystemTime>, // Last execution time
453    pub created_at: std::time::SystemTime,             // When source was created
454    pub execution_count: u64,                          // Number of times executed
455}
456
457impl StreamSourceTrait for ChoiceExecutionSource {
458    fn source_type(&self) -> &'static str {
459        "choice_execution"
460    }
461    fn source_id(&self) -> String {
462        self.choice_id.clone()
463    }
464    fn can_terminate(&self) -> bool {
465        true
466    }
467    fn cleanup(&self) -> Result<(), String> {
468        match self.execution_type.as_str() {
469            "process" => {
470                if let Some(pid) = self.process_id {
471                    // Terminate the process
472                    let _ = std::process::Command::new("kill")
473                        .arg("-9")
474                        .arg(pid.to_string())
475                        .output();
476                }
477                Ok(())
478            }
479            "threaded" => {
480                // Thread cleanup would be handled by ThreadManager
481                Ok(())
482            }
483            _ => Ok(()),
484        }
485    }
486    fn get_metadata(&self) -> std::collections::HashMap<String, String> {
487        let mut meta = std::collections::HashMap::new();
488        meta.insert("choice_id".to_string(), self.choice_id.clone());
489        meta.insert("muxbox_id".to_string(), self.muxbox_id.clone());
490        meta.insert("execution_type".to_string(), self.execution_type.clone());
491        if let Some(pid) = self.process_id {
492            meta.insert("process_id".to_string(), pid.to_string());
493        }
494        if let Some(thread_id) = &self.thread_id {
495            meta.insert("thread_id".to_string(), thread_id.clone());
496        }
497        meta
498    }
499}
500
501impl StreamSourceTrait for PeriodicRefreshSource {
502    fn source_type(&self) -> &'static str {
503        "periodic_refresh"
504    }
505    fn source_id(&self) -> String {
506        self.source_id.clone()
507    }
508    fn can_terminate(&self) -> bool {
509        true // Periodic refresh can always be terminated
510    }
511    fn cleanup(&self) -> Result<(), String> {
512        log::info!("Cleaning up periodic refresh source: {}", self.source_id);
513        Ok(())
514    }
515    fn get_metadata(&self) -> std::collections::HashMap<String, String> {
516        let mut metadata = std::collections::HashMap::new();
517        metadata.insert("source_id".to_string(), self.source_id.clone());
518        metadata.insert("box_id".to_string(), self.box_id.clone());
519        metadata.insert("stream_id".to_string(), self.stream_id.clone());
520        metadata.insert(
521            "execution_mode".to_string(),
522            format!("{:?}", self.execution_mode),
523        );
524        metadata.insert(
525            "refresh_interval".to_string(),
526            self.refresh_interval.to_string(),
527        );
528        metadata.insert(
529            "execution_count".to_string(),
530            self.execution_count.to_string(),
531        );
532        metadata
533    }
534}
535
536// F0227: ExecutionMode-specific source implementations
537// These provide specialized lifecycle management for each execution mode
538
539/// Immediate execution source - for synchronous UI thread execution
540#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
541pub struct ImmediateExecutionSource {
542    pub choice_id: String,
543    pub muxbox_id: String,
544    pub script: Vec<String>,
545    pub started_at: std::time::SystemTime,
546    pub completed_at: Option<std::time::SystemTime>,
547    pub execution_result: Option<Result<String, String>>,
548    pub execution_duration: Option<std::time::Duration>,
549}
550
551impl StreamSourceTrait for ImmediateExecutionSource {
552    fn source_type(&self) -> &'static str {
553        "immediate_execution"
554    }
555    fn source_id(&self) -> String {
556        format!("immediate_{}", self.choice_id)
557    }
558    fn can_terminate(&self) -> bool {
559        false // Immediate execution cannot be cancelled once started
560    }
561    fn cleanup(&self) -> Result<(), String> {
562        Ok(()) // No cleanup needed for immediate execution
563    }
564    fn get_metadata(&self) -> std::collections::HashMap<String, String> {
565        let mut meta = std::collections::HashMap::new();
566        meta.insert("choice_id".to_string(), self.choice_id.clone());
567        meta.insert("muxbox_id".to_string(), self.muxbox_id.clone());
568        meta.insert("script_lines".to_string(), self.script.len().to_string());
569        meta.insert("started_at".to_string(), format!("{:?}", self.started_at));
570        if let Some(completed_at) = self.completed_at {
571            meta.insert("completed_at".to_string(), format!("{:?}", completed_at));
572        }
573        if let Some(duration) = self.execution_duration {
574            meta.insert("duration_ms".to_string(), duration.as_millis().to_string());
575        }
576        meta.insert("is_complete".to_string(), self.is_complete().to_string());
577        meta
578    }
579}
580
581impl ImmediateSource for ImmediateExecutionSource {
582    fn get_execution_result(&self) -> Option<Result<String, String>> {
583        self.execution_result.clone()
584    }
585
586    fn is_complete(&self) -> bool {
587        self.execution_result.is_some()
588    }
589
590    fn get_execution_duration(&self) -> Option<std::time::Duration> {
591        self.execution_duration
592    }
593}
594
595/// Thread pool execution source - for background thread execution
596#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
597pub struct ThreadPoolExecutionSource {
598    pub choice_id: String,
599    pub muxbox_id: String,
600    pub script: Vec<String>,
601    pub thread_id: Option<String>,
602    pub started_at: std::time::SystemTime,
603    pub timeout_seconds: Option<u32>,
604    pub thread_status: ThreadStatus,
605    pub completion_result: Option<Result<String, String>>,
606    pub execution_duration: Option<std::time::Duration>,
607}
608
609impl StreamSourceTrait for ThreadPoolExecutionSource {
610    fn source_type(&self) -> &'static str {
611        "thread_pool_execution"
612    }
613    fn source_id(&self) -> String {
614        format!("thread_{}", self.choice_id)
615    }
616    fn can_terminate(&self) -> bool {
617        matches!(
618            self.thread_status,
619            ThreadStatus::Running | ThreadStatus::NotStarted
620        )
621    }
622    fn cleanup(&self) -> Result<(), String> {
623        if let Some(thread_id) = &self.thread_id {
624            // Note: Actual thread cancellation would be handled by ThreadManager
625            log::info!("Cleanup requested for thread pool execution: {}", thread_id);
626            Ok(())
627        } else {
628            Ok(())
629        }
630    }
631    fn get_metadata(&self) -> std::collections::HashMap<String, String> {
632        let mut meta = std::collections::HashMap::new();
633        meta.insert("choice_id".to_string(), self.choice_id.clone());
634        meta.insert("muxbox_id".to_string(), self.muxbox_id.clone());
635        meta.insert("script_lines".to_string(), self.script.len().to_string());
636        meta.insert(
637            "thread_status".to_string(),
638            format!("{:?}", self.thread_status),
639        );
640        meta.insert("started_at".to_string(), format!("{:?}", self.started_at));
641        if let Some(thread_id) = &self.thread_id {
642            meta.insert("thread_id".to_string(), thread_id.clone());
643        }
644        if let Some(timeout) = self.timeout_seconds {
645            meta.insert("timeout_seconds".to_string(), timeout.to_string());
646        }
647        if let Some(duration) = self.execution_duration {
648            meta.insert("duration_ms".to_string(), duration.as_millis().to_string());
649        }
650        meta
651    }
652}
653
654impl ThreadPoolSource for ThreadPoolExecutionSource {
655    fn get_thread_id(&self) -> Option<String> {
656        self.thread_id.clone()
657    }
658
659    fn is_thread_running(&self) -> bool {
660        matches!(self.thread_status, ThreadStatus::Running)
661    }
662
663    fn cancel_thread(&self) -> Result<(), String> {
664        if matches!(
665            self.thread_status,
666            ThreadStatus::Running | ThreadStatus::NotStarted
667        ) {
668            // Note: Actual cancellation would be implemented by ThreadManager
669            log::warn!(
670                "Thread cancellation requested for choice: {}",
671                self.choice_id
672            );
673            Ok(())
674        } else {
675            Err(format!(
676                "Cannot cancel thread in status: {:?}",
677                self.thread_status
678            ))
679        }
680    }
681
682    fn get_thread_status(&self) -> ThreadStatus {
683        self.thread_status.clone()
684    }
685
686    fn set_timeout(&mut self, timeout_seconds: u32) {
687        self.timeout_seconds = Some(timeout_seconds);
688    }
689}
690
691/// PTY session execution source - for real-time process execution
692#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
693pub struct PtySessionExecutionSource {
694    pub choice_id: String,
695    pub muxbox_id: String,
696    pub command: String,
697    pub args: Vec<String>,
698    pub working_dir: Option<String>,
699    pub process_id: Option<u32>,
700    pub terminal_size: (u16, u16),
701    pub started_at: std::time::SystemTime,
702    pub reader_thread_id: Option<String>,
703    pub is_process_running: bool,
704}
705
706impl StreamSourceTrait for PtySessionExecutionSource {
707    fn source_type(&self) -> &'static str {
708        "pty_session_execution"
709    }
710    fn source_id(&self) -> String {
711        format!("pty_{}", self.choice_id)
712    }
713    fn can_terminate(&self) -> bool {
714        self.process_id.is_some() && self.is_process_running
715    }
716    fn cleanup(&self) -> Result<(), String> {
717        if let Some(pid) = self.process_id {
718            if self.is_process_running {
719                let result = std::process::Command::new("kill")
720                    .arg("-9")
721                    .arg(pid.to_string())
722                    .output();
723                match result {
724                    Ok(_) => Ok(()),
725                    Err(e) => Err(format!("Failed to terminate PTY process {}: {}", pid, e)),
726                }
727            } else {
728                Ok(()) // Already terminated
729            }
730        } else {
731            Ok(()) // No process to clean up
732        }
733    }
734    fn get_metadata(&self) -> std::collections::HashMap<String, String> {
735        let mut meta = std::collections::HashMap::new();
736        meta.insert("choice_id".to_string(), self.choice_id.clone());
737        meta.insert("muxbox_id".to_string(), self.muxbox_id.clone());
738        meta.insert("command".to_string(), self.command.clone());
739        meta.insert("args".to_string(), self.args.join(" "));
740        meta.insert(
741            "is_running".to_string(),
742            self.is_process_running.to_string(),
743        );
744        meta.insert("started_at".to_string(), format!("{:?}", self.started_at));
745        meta.insert(
746            "terminal_size".to_string(),
747            format!("{}x{}", self.terminal_size.0, self.terminal_size.1),
748        );
749        if let Some(pid) = self.process_id {
750            meta.insert("process_id".to_string(), pid.to_string());
751        }
752        if let Some(thread_id) = &self.reader_thread_id {
753            meta.insert("reader_thread_id".to_string(), thread_id.clone());
754        }
755        if let Some(wd) = &self.working_dir {
756            meta.insert("working_dir".to_string(), wd.clone());
757        }
758        meta
759    }
760}
761
762impl PtySessionSource for PtySessionExecutionSource {
763    fn get_process_id(&self) -> Option<u32> {
764        self.process_id
765    }
766
767    fn is_process_running(&self) -> bool {
768        self.is_process_running
769    }
770
771    fn send_input(&self, input: &str) -> Result<(), String> {
772        if self.process_id.is_some() && self.is_process_running {
773            // Note: Actual input sending would be implemented by PTY manager
774            log::info!(
775                "PTY input requested for choice {}: {}",
776                self.choice_id,
777                input
778            );
779            Ok(())
780        } else {
781            Err("PTY process is not running".to_string())
782        }
783    }
784
785    fn kill_process(&self) -> Result<(), String> {
786        if let Some(pid) = self.process_id {
787            if self.is_process_running {
788                let result = std::process::Command::new("kill")
789                    .arg("-TERM")
790                    .arg(pid.to_string())
791                    .output();
792                match result {
793                    Ok(_) => Ok(()),
794                    Err(e) => Err(format!("Failed to kill PTY process {}: {}", pid, e)),
795                }
796            } else {
797                Err("Process is not running".to_string())
798            }
799        } else {
800            Err("No process ID available".to_string())
801        }
802    }
803
804    fn resize_terminal(&self, rows: u16, cols: u16) -> Result<(), String> {
805        if self.process_id.is_some() && self.is_process_running {
806            // Note: Actual terminal resizing would be implemented by PTY manager
807            log::info!(
808                "PTY resize requested for choice {}: {}x{}",
809                self.choice_id,
810                rows,
811                cols
812            );
813            Ok(())
814        } else {
815            Err("PTY process is not running".to_string())
816        }
817    }
818
819    fn get_terminal_size(&self) -> (u16, u16) {
820        self.terminal_size
821    }
822
823    fn get_command(&self) -> String {
824        if self.args.is_empty() {
825            self.command.clone()
826        } else {
827            format!("{} {}", self.command, self.args.join(" "))
828        }
829    }
830
831    fn get_working_directory(&self) -> Option<String> {
832        self.working_dir.clone()
833    }
834}
835
836// F0212: PTY sources with process and terminal management
837#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
838pub struct PTYSource {
839    pub pty_id: String,
840    pub process_id: u32,
841    pub command: String,
842    pub args: Vec<String>,
843    pub working_dir: Option<String>,
844    pub reader_thread_id: Option<String>,
845    pub started_at: std::time::SystemTime,
846    pub terminal_size: (u16, u16), // (rows, cols)
847}
848
849impl StreamSourceTrait for PTYSource {
850    fn source_type(&self) -> &'static str {
851        "pty"
852    }
853    fn source_id(&self) -> String {
854        self.pty_id.clone()
855    }
856    fn can_terminate(&self) -> bool {
857        true
858    }
859    fn cleanup(&self) -> Result<(), String> {
860        // Terminate PTY process
861        let result = std::process::Command::new("kill")
862            .arg("-9")
863            .arg(self.process_id.to_string())
864            .output();
865        match result {
866            Ok(_) => Ok(()),
867            Err(e) => Err(format!(
868                "Failed to terminate PTY process {}: {}",
869                self.process_id, e
870            )),
871        }
872    }
873    fn get_metadata(&self) -> std::collections::HashMap<String, String> {
874        let mut meta = std::collections::HashMap::new();
875        meta.insert("pty_id".to_string(), self.pty_id.clone());
876        meta.insert("process_id".to_string(), self.process_id.to_string());
877        meta.insert("command".to_string(), self.command.clone());
878        meta.insert("args".to_string(), self.args.join(" "));
879        meta.insert(
880            "terminal_size".to_string(),
881            format!("{}x{}", self.terminal_size.0, self.terminal_size.1),
882        );
883        if let Some(ref thread_id) = self.reader_thread_id {
884            meta.insert("reader_thread_id".to_string(), thread_id.clone());
885        }
886        meta
887    }
888}
889
890// F0212: Redirect sources with source tracking
891#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
892pub struct RedirectSource {
893    pub source_muxbox_id: String,
894    pub source_choice_id: Option<String>,
895    pub redirect_name: String,
896    pub redirect_type: String, // "choice", "script", "pty", "external"
897    pub created_at: std::time::SystemTime,
898    pub source_process_id: Option<u32>, // For process-based redirects
899}
900
901impl StreamSourceTrait for RedirectSource {
902    fn source_type(&self) -> &'static str {
903        "redirect"
904    }
905    fn source_id(&self) -> String {
906        format!("{}_{}", self.source_muxbox_id, self.redirect_name)
907    }
908    fn can_terminate(&self) -> bool {
909        self.source_process_id.is_some()
910    }
911    fn cleanup(&self) -> Result<(), String> {
912        if let Some(pid) = self.source_process_id {
913            let result = std::process::Command::new("kill")
914                .arg("-9")
915                .arg(pid.to_string())
916                .output();
917            match result {
918                Ok(_) => Ok(()),
919                Err(e) => Err(format!(
920                    "Failed to terminate redirect source process {}: {}",
921                    pid, e
922                )),
923            }
924        } else {
925            Ok(())
926        }
927    }
928    fn get_metadata(&self) -> std::collections::HashMap<String, String> {
929        let mut meta = std::collections::HashMap::new();
930        meta.insert(
931            "source_muxbox_id".to_string(),
932            self.source_muxbox_id.clone(),
933        );
934        meta.insert("redirect_name".to_string(), self.redirect_name.clone());
935        meta.insert("redirect_type".to_string(), self.redirect_type.clone());
936        if let Some(ref choice_id) = self.source_choice_id {
937            meta.insert("source_choice_id".to_string(), choice_id.clone());
938        }
939        if let Some(pid) = self.source_process_id {
940            meta.insert("source_process_id".to_string(), pid.to_string());
941        }
942        meta
943    }
944}
945
946// F0212: Socket sources with connection management
947#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
948pub struct SocketSource {
949    pub connection_id: String,
950    pub socket_path: Option<String>,
951    pub client_info: String,
952    pub protocol_version: String,
953    pub connected_at: std::time::SystemTime,
954    pub last_activity: std::time::SystemTime,
955}
956
957impl StreamSourceTrait for SocketSource {
958    fn source_type(&self) -> &'static str {
959        "socket"
960    }
961    fn source_id(&self) -> String {
962        self.connection_id.clone()
963    }
964    fn can_terminate(&self) -> bool {
965        true
966    }
967    fn cleanup(&self) -> Result<(), String> {
968        // Socket cleanup would be handled by socket manager
969        Ok(())
970    }
971    fn get_metadata(&self) -> std::collections::HashMap<String, String> {
972        let mut meta = std::collections::HashMap::new();
973        meta.insert("connection_id".to_string(), self.connection_id.clone());
974        meta.insert("client_info".to_string(), self.client_info.clone());
975        meta.insert(
976            "protocol_version".to_string(),
977            self.protocol_version.clone(),
978        );
979        if let Some(ref socket_path) = self.socket_path {
980            meta.insert("socket_path".to_string(), socket_path.clone());
981        }
982        meta
983    }
984}
985
986// F0212: Unified stream source enum containing all source types
987// F0227: Updated to include execution-mode-specific sources
988#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
989pub enum StreamSource {
990    StaticContent(StaticContentSource),
991    ChoiceExecution(ChoiceExecutionSource), // Legacy - kept for compatibility
992    PeriodicRefresh(PeriodicRefreshSource), // Periodic refresh execution source
993    PTY(PTYSource),
994    Redirect(RedirectSource),
995    Socket(SocketSource),
996    // F0227: ExecutionMode-specific sources for enhanced lifecycle management
997    ImmediateExecution(ImmediateExecutionSource),
998    ThreadPoolExecution(ThreadPoolExecutionSource),
999    PtySessionExecution(PtySessionExecutionSource),
1000}
1001
1002// F0212: Implementation of StreamSourceTrait for the unified enum
1003// F0227: Updated to handle execution-mode-specific sources
1004impl StreamSourceTrait for StreamSource {
1005    fn source_type(&self) -> &'static str {
1006        match self {
1007            StreamSource::StaticContent(s) => s.source_type(),
1008            StreamSource::ChoiceExecution(s) => s.source_type(),
1009            StreamSource::PeriodicRefresh(s) => s.source_type(),
1010            StreamSource::PTY(s) => s.source_type(),
1011            StreamSource::Redirect(s) => s.source_type(),
1012            StreamSource::Socket(s) => s.source_type(),
1013            // F0227: ExecutionMode-specific sources
1014            StreamSource::ImmediateExecution(s) => s.source_type(),
1015            StreamSource::ThreadPoolExecution(s) => s.source_type(),
1016            StreamSource::PtySessionExecution(s) => s.source_type(),
1017        }
1018    }
1019
1020    fn source_id(&self) -> String {
1021        match self {
1022            StreamSource::StaticContent(s) => s.source_id(),
1023            StreamSource::ChoiceExecution(s) => s.source_id(),
1024            StreamSource::PeriodicRefresh(s) => s.source_id(),
1025            StreamSource::PTY(s) => s.source_id(),
1026            StreamSource::Redirect(s) => s.source_id(),
1027            StreamSource::Socket(s) => s.source_id(),
1028            // F0227: ExecutionMode-specific sources
1029            StreamSource::ImmediateExecution(s) => s.source_id(),
1030            StreamSource::ThreadPoolExecution(s) => s.source_id(),
1031            StreamSource::PtySessionExecution(s) => s.source_id(),
1032        }
1033    }
1034
1035    fn can_terminate(&self) -> bool {
1036        match self {
1037            StreamSource::StaticContent(s) => s.can_terminate(),
1038            StreamSource::ChoiceExecution(s) => s.can_terminate(),
1039            StreamSource::PeriodicRefresh(s) => s.can_terminate(),
1040            StreamSource::PTY(s) => s.can_terminate(),
1041            StreamSource::Redirect(s) => s.can_terminate(),
1042            StreamSource::Socket(s) => s.can_terminate(),
1043            // F0227: ExecutionMode-specific sources
1044            StreamSource::ImmediateExecution(s) => s.can_terminate(),
1045            StreamSource::ThreadPoolExecution(s) => s.can_terminate(),
1046            StreamSource::PtySessionExecution(s) => s.can_terminate(),
1047        }
1048    }
1049
1050    fn cleanup(&self) -> Result<(), String> {
1051        match self {
1052            StreamSource::StaticContent(s) => s.cleanup(),
1053            StreamSource::ChoiceExecution(s) => s.cleanup(),
1054            StreamSource::PeriodicRefresh(s) => s.cleanup(),
1055            StreamSource::PTY(s) => s.cleanup(),
1056            StreamSource::Redirect(s) => s.cleanup(),
1057            StreamSource::Socket(s) => s.cleanup(),
1058            // F0227: ExecutionMode-specific sources
1059            StreamSource::ImmediateExecution(s) => s.cleanup(),
1060            StreamSource::ThreadPoolExecution(s) => s.cleanup(),
1061            StreamSource::PtySessionExecution(s) => s.cleanup(),
1062        }
1063    }
1064
1065    fn get_metadata(&self) -> std::collections::HashMap<String, String> {
1066        match self {
1067            StreamSource::StaticContent(s) => s.get_metadata(),
1068            StreamSource::ChoiceExecution(s) => s.get_metadata(),
1069            StreamSource::PeriodicRefresh(s) => s.get_metadata(),
1070            StreamSource::PTY(s) => s.get_metadata(),
1071            StreamSource::Redirect(s) => s.get_metadata(),
1072            StreamSource::Socket(s) => s.get_metadata(),
1073            // F0227: ExecutionMode-specific sources
1074            StreamSource::ImmediateExecution(s) => s.get_metadata(),
1075            StreamSource::ThreadPoolExecution(s) => s.get_metadata(),
1076            StreamSource::PtySessionExecution(s) => s.get_metadata(),
1077        }
1078    }
1079}
1080
1081// F0227: ExecutionMode Stream Source Factory Functions
1082// These functions create execution-mode-specific sources for stream integration
1083
1084impl StreamSource {
1085    /// Create an immediate execution source for synchronous UI thread execution
1086    pub fn create_immediate_execution_source(
1087        choice_id: String,
1088        muxbox_id: String,
1089        script: Vec<String>,
1090    ) -> Self {
1091        StreamSource::ImmediateExecution(ImmediateExecutionSource {
1092            choice_id,
1093            muxbox_id,
1094            script,
1095            started_at: std::time::SystemTime::now(),
1096            completed_at: None,
1097            execution_result: None,
1098            execution_duration: None,
1099        })
1100    }
1101
1102    /// Create a thread pool execution source for background thread execution
1103    pub fn create_thread_pool_execution_source(
1104        choice_id: String,
1105        muxbox_id: String,
1106        script: Vec<String>,
1107        thread_id: Option<String>,
1108        timeout_seconds: Option<u32>,
1109    ) -> Self {
1110        StreamSource::ThreadPoolExecution(ThreadPoolExecutionSource {
1111            choice_id,
1112            muxbox_id,
1113            script,
1114            thread_id,
1115            started_at: std::time::SystemTime::now(),
1116            timeout_seconds,
1117            thread_status: ThreadStatus::NotStarted,
1118            completion_result: None,
1119            execution_duration: None,
1120        })
1121    }
1122
1123    /// Create a PTY session execution source for real-time process execution
1124    pub fn create_pty_session_execution_source(
1125        choice_id: String,
1126        muxbox_id: String,
1127        command: String,
1128        args: Vec<String>,
1129        working_dir: Option<String>,
1130        terminal_size: (u16, u16),
1131    ) -> Self {
1132        StreamSource::PtySessionExecution(PtySessionExecutionSource {
1133            choice_id,
1134            muxbox_id,
1135            command,
1136            args,
1137            working_dir,
1138            process_id: None,
1139            terminal_size,
1140            started_at: std::time::SystemTime::now(),
1141            reader_thread_id: None,
1142            is_process_running: false,
1143        })
1144    }
1145
1146    /// Convert ExecutionMode to the appropriate source type
1147    pub fn from_execution_mode(
1148        execution_mode: &ExecutionMode,
1149        choice_id: String,
1150        muxbox_id: String,
1151        script: Vec<String>,
1152        additional_params: Option<std::collections::HashMap<String, String>>,
1153    ) -> Self {
1154        match execution_mode {
1155            ExecutionMode::Immediate => {
1156                Self::create_immediate_execution_source(choice_id, muxbox_id, script)
1157            }
1158            ExecutionMode::Thread => {
1159                let thread_id = additional_params
1160                    .as_ref()
1161                    .and_then(|p| p.get("thread_id"))
1162                    .cloned();
1163                let timeout_seconds = additional_params
1164                    .as_ref()
1165                    .and_then(|p| p.get("timeout_seconds"))
1166                    .and_then(|s| s.parse().ok());
1167                Self::create_thread_pool_execution_source(
1168                    choice_id,
1169                    muxbox_id,
1170                    script,
1171                    thread_id,
1172                    timeout_seconds,
1173                )
1174            }
1175            ExecutionMode::Pty => {
1176                let command = if script.is_empty() {
1177                    "sh".to_string()
1178                } else {
1179                    script[0].clone()
1180                };
1181                let args = if script.len() > 1 {
1182                    script[1..].to_vec()
1183                } else {
1184                    vec![]
1185                };
1186                let working_dir = additional_params
1187                    .as_ref()
1188                    .and_then(|p| p.get("working_dir"))
1189                    .cloned();
1190                let terminal_size = additional_params
1191                    .as_ref()
1192                    .and_then(|p| {
1193                        let rows = p.get("terminal_rows")?.parse().ok()?;
1194                        let cols = p.get("terminal_cols")?.parse().ok()?;
1195                        Some((rows, cols))
1196                    })
1197                    .unwrap_or((24, 80));
1198                Self::create_pty_session_execution_source(
1199                    choice_id,
1200                    muxbox_id,
1201                    command,
1202                    args,
1203                    working_dir,
1204                    terminal_size,
1205                )
1206            }
1207        }
1208    }
1209
1210    /// Check if this source supports a specific execution mode trait
1211    pub fn supports_immediate_source(&self) -> bool {
1212        matches!(self, StreamSource::ImmediateExecution(_))
1213    }
1214
1215    pub fn supports_thread_pool_source(&self) -> bool {
1216        matches!(self, StreamSource::ThreadPoolExecution(_))
1217    }
1218
1219    pub fn supports_pty_session_source(&self) -> bool {
1220        matches!(self, StreamSource::PtySessionExecution(_))
1221    }
1222
1223    /// Get execution mode-specific source as trait object
1224    pub fn as_immediate_source(&self) -> Option<&dyn ImmediateSource> {
1225        match self {
1226            StreamSource::ImmediateExecution(source) => Some(source),
1227            _ => None,
1228        }
1229    }
1230
1231    pub fn as_thread_pool_source(&self) -> Option<&dyn ThreadPoolSource> {
1232        match self {
1233            StreamSource::ThreadPoolExecution(source) => Some(source),
1234            _ => None,
1235        }
1236    }
1237
1238    pub fn as_pty_session_source(&self) -> Option<&dyn PtySessionSource> {
1239        match self {
1240            StreamSource::PtySessionExecution(source) => Some(source),
1241            _ => None,
1242        }
1243    }
1244}
1245
1246// F0216: Stream Change Detection Implementation
1247impl Stream {
1248    /// Create a new stream with proper change detection initialization
1249    pub fn new(
1250        id: String,
1251        stream_type: StreamType,
1252        label: String,
1253        content: Vec<String>,
1254        choices: Option<Vec<Choice>>,
1255        source: Option<StreamSource>,
1256    ) -> Self {
1257        let now = std::time::SystemTime::now();
1258        let mut stream = Self {
1259            id,
1260            stream_type,
1261            label,
1262            content,
1263            choices,
1264            source,
1265            content_hash: 0,
1266            last_updated: now,
1267            created_at: now,
1268        };
1269        stream.update_content_hash();
1270        stream
1271    }
1272
1273    /// Update the content hash for change detection
1274    pub fn update_content_hash(&mut self) {
1275        use std::collections::hash_map::DefaultHasher;
1276        use std::hash::{Hash, Hasher};
1277
1278        let mut hasher = DefaultHasher::new();
1279        self.content.hash(&mut hasher);
1280        if let Some(ref choices) = self.choices {
1281            choices.hash(&mut hasher);
1282        }
1283        self.content_hash = hasher.finish();
1284        self.last_updated = std::time::SystemTime::now();
1285    }
1286
1287    /// Check if content has changed since last hash update
1288    pub fn has_content_changed(&self) -> bool {
1289        use std::collections::hash_map::DefaultHasher;
1290        use std::hash::{Hash, Hasher};
1291
1292        let mut hasher = DefaultHasher::new();
1293        self.content.hash(&mut hasher);
1294        if let Some(ref choices) = self.choices {
1295            choices.hash(&mut hasher);
1296        }
1297        let current_hash = hasher.finish();
1298        current_hash != self.content_hash
1299    }
1300
1301    /// Update content and automatically refresh change detection
1302    pub fn update_content(&mut self, new_content: Vec<String>) {
1303        self.content = new_content;
1304        self.update_content_hash();
1305    }
1306
1307    /// Update choices and automatically refresh change detection  
1308    pub fn update_choices(&mut self, new_choices: Option<Vec<Choice>>) {
1309        self.choices = new_choices;
1310        self.update_content_hash();
1311    }
1312
1313    /// Get time since last update for staleness detection
1314    pub fn time_since_last_update(
1315        &self,
1316    ) -> Result<std::time::Duration, std::time::SystemTimeError> {
1317        std::time::SystemTime::now().duration_since(self.last_updated)
1318    }
1319
1320    /// F0219: Check if stream can be closed (has close button)
1321    pub fn is_closeable(&self) -> bool {
1322        matches!(
1323            &self.stream_type,
1324            StreamType::RedirectedOutput(_)
1325                | StreamType::ChoiceExecution(_)
1326                | StreamType::PtySession(_)
1327                | StreamType::ExternalSocket
1328        )
1329    }
1330}
1331
1332// F0217: Implement ContentStreamTrait for content-type streams
1333impl ContentStreamTrait for Stream {
1334    fn get_content_lines(&self) -> &Vec<String> {
1335        match self.stream_type {
1336            StreamType::Content
1337            | StreamType::RedirectedOutput(_)
1338            | StreamType::PTY
1339            | StreamType::Plugin(_)
1340            | StreamType::ChoiceExecution(_)
1341            | StreamType::PtySession(_)
1342            | StreamType::OwnScript => &self.content,
1343            _ => panic!(
1344                "ContentStreamTrait called on non-content stream: {:?}",
1345                self.stream_type
1346            ),
1347        }
1348    }
1349
1350    fn set_content_lines(&mut self, content: Vec<String>) {
1351        match self.stream_type {
1352            StreamType::Content
1353            | StreamType::RedirectedOutput(_)
1354            | StreamType::PTY
1355            | StreamType::Plugin(_)
1356            | StreamType::ChoiceExecution(_)
1357            | StreamType::PtySession(_)
1358            | StreamType::OwnScript => {
1359                self.content = content;
1360                self.update_content_hash();
1361            }
1362            _ => panic!(
1363                "ContentStreamTrait called on non-content stream: {:?}",
1364                self.stream_type
1365            ),
1366        }
1367    }
1368}
1369
1370// F0217: Implement ChoicesStreamTrait for choices-type streams
1371impl ChoicesStreamTrait for Stream {
1372    fn get_choices(&self) -> &Vec<Choice> {
1373        match self.stream_type {
1374            StreamType::Choices => self
1375                .choices
1376                .as_ref()
1377                .expect("Choices stream must have choices"),
1378            _ => panic!(
1379                "ChoicesStreamTrait called on non-choices stream: {:?}",
1380                self.stream_type
1381            ),
1382        }
1383    }
1384
1385    fn get_choices_mut(&mut self) -> &mut Vec<Choice> {
1386        match self.stream_type {
1387            StreamType::Choices => self
1388                .choices
1389                .as_mut()
1390                .expect("Choices stream must have choices"),
1391            _ => panic!(
1392                "ChoicesStreamTrait called on non-choices stream: {:?}",
1393                self.stream_type
1394            ),
1395        }
1396    }
1397
1398    fn set_choices(&mut self, choices: Vec<Choice>) {
1399        match self.stream_type {
1400            StreamType::Choices => {
1401                self.choices = Some(choices);
1402                self.update_content_hash();
1403            }
1404            _ => panic!(
1405                "ChoicesStreamTrait called on non-choices stream: {:?}",
1406                self.stream_type
1407            ),
1408        }
1409    }
1410}
1411
1412// Represents a granular field update
1413#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
1414pub struct FieldUpdate {
1415    pub entity_type: EntityType,   // The type of entity being updated
1416    pub entity_id: Option<String>, // The ID of the entity (App, Layout, or MuxBox)
1417    pub field_name: String,        // The field name to be updated
1418    pub new_value: Value,          // The new value for the field
1419}
1420
1421// The Updatable trait
1422pub trait Updatable {
1423    // Generate a diff of changes from another instance
1424    fn generate_diff(&self, other: &Self) -> Vec<FieldUpdate>;
1425
1426    // Apply a list of updates to the current instance
1427    fn apply_updates(&mut self, updates: Vec<FieldUpdate>);
1428}
1429
1430#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1431pub struct Config {
1432    pub frame_delay: u64,
1433    pub locked: bool, // Disable muxbox resizing and moving when true
1434    #[serde(default)]
1435    pub calibrate: bool,
1436}
1437
1438impl Hash for Config {
1439    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1440        self.frame_delay.hash(state);
1441        self.locked.hash(state);
1442        self.calibrate.hash(state);
1443    }
1444}
1445
1446impl Default for Config {
1447    fn default() -> Self {
1448        Config {
1449            frame_delay: 30,
1450            locked: false, // Default to unlocked (resizable/movable)
1451            calibrate: false,
1452        }
1453    }
1454}
1455
1456impl Config {
1457    pub fn new(frame_delay: u64) -> Self {
1458        let result = Config {
1459            frame_delay,
1460            locked: false, // Default to unlocked
1461            calibrate: false,
1462        };
1463        result.validate();
1464        result
1465    }
1466
1467    pub fn new_with_lock(frame_delay: u64, locked: bool) -> Self {
1468        let result = Config {
1469            frame_delay,
1470            locked,
1471            calibrate: false,
1472        };
1473        result.validate();
1474        result
1475    }
1476
1477    pub fn new_with_lock_and_calibration(frame_delay: u64, locked: bool, calibrate: bool) -> Self {
1478        let result = Config {
1479            frame_delay,
1480            locked,
1481            calibrate,
1482        };
1483        result.validate();
1484        result
1485    }
1486    pub fn validate(&self) {
1487        if self.frame_delay == 0 {
1488            panic!("Validation error: frame_delay cannot be 0");
1489        }
1490    }
1491}
1492
1493#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
1494pub enum SocketFunction {
1495    ReplaceBoxContent {
1496        box_id: String,
1497        success: bool,
1498        content: String,
1499    },
1500    ReplaceBoxScript {
1501        box_id: String,
1502        script: Vec<String>,
1503    },
1504    StopBoxRefresh {
1505        box_id: String,
1506    },
1507    StartBoxRefresh {
1508        box_id: String,
1509    },
1510    ReplaceBox {
1511        box_id: String,
1512        new_box: MuxBox,
1513    },
1514    SwitchActiveLayout {
1515        layout_id: String,
1516    },
1517    AddBox {
1518        layout_id: String,
1519        muxbox: MuxBox,
1520    },
1521    RemoveBox {
1522        box_id: String,
1523    },
1524    // F0137: Socket PTY Control - Kill and restart PTY processes
1525    KillPtyProcess {
1526        box_id: String,
1527    },
1528    RestartPtyProcess {
1529        box_id: String,
1530    },
1531    // F0138: Socket PTY Query - Get PTY status and info
1532    QueryPtyStatus {
1533        box_id: String,
1534    },
1535    // F0136: Socket PTY Spawn - Spawn PTY processes via socket commands
1536    SpawnPtyProcess {
1537        box_id: String,
1538        script: Vec<String>,
1539        libs: Option<Vec<String>>,
1540        redirect_output: Option<String>,
1541    },
1542    // F0139: Socket PTY Input - Send input to PTY processes remotely
1543    SendPtyInput {
1544        box_id: String,
1545        input: String,
1546    },
1547}
1548
1549pub fn run_socket_function(
1550    socket_function: SocketFunction,
1551    app_context: &AppContext,
1552) -> Result<(AppContext, Vec<Message>), Box<dyn Error>> {
1553    let mut app_context = app_context.clone();
1554    let mut messages = Vec::new();
1555    match socket_function {
1556        SocketFunction::ReplaceBoxContent {
1557            box_id,
1558            success,
1559            content,
1560        } => {
1561            // T0328: Replace MuxBoxOutputUpdate with StreamUpdateMessage
1562            messages.push(Message::StreamUpdateMessage(
1563                crate::model::common::StreamUpdate {
1564                    stream_id: format!("socket-{}", box_id),
1565                    target_box_id: box_id.clone(),
1566                    content_update: content,
1567                    source_state: crate::model::common::SourceState::Batch(
1568                        crate::model::common::BatchSourceState {
1569                            task_id: format!("socket-{}", box_id),
1570                            queue_wait_time: std::time::Duration::from_millis(0),
1571                            execution_time: std::time::Duration::from_millis(0),
1572                            exit_code: if success { Some(0) } else { Some(1) },
1573                            status: if success {
1574                                crate::model::common::BatchStatus::Completed
1575                            } else {
1576                                crate::model::common::BatchStatus::Failed(
1577                                    "Unknown error".to_string(),
1578                                )
1579                            },
1580                        },
1581                    ),
1582                    execution_mode: crate::model::common::ExecutionMode::Immediate,
1583                },
1584            ));
1585        }
1586        SocketFunction::ReplaceBoxScript { box_id, script } => {
1587            messages.push(Message::MuxBoxScriptUpdate(box_id, script));
1588        }
1589        SocketFunction::StopBoxRefresh { box_id } => {
1590            messages.push(Message::StopBoxRefresh(box_id));
1591        }
1592        SocketFunction::StartBoxRefresh { box_id } => {
1593            messages.push(Message::StartBoxRefresh(box_id));
1594        }
1595        SocketFunction::ReplaceBox { box_id, new_box } => {
1596            messages.push(Message::ReplaceMuxBox(box_id, new_box));
1597        }
1598        SocketFunction::SwitchActiveLayout { layout_id } => {
1599            messages.push(Message::SwitchActiveLayout(layout_id));
1600        }
1601        SocketFunction::AddBox { layout_id, muxbox } => {
1602            messages.push(Message::AddBox(layout_id, muxbox));
1603        }
1604        SocketFunction::RemoveBox { box_id } => {
1605            messages.push(Message::RemoveBox(box_id));
1606        }
1607        // F0137: Socket PTY Control - Kill and restart PTY processes
1608        SocketFunction::KillPtyProcess { box_id } => {
1609            if let Some(pty_manager) = &app_context.pty_manager {
1610                // Get the stream ID from the PTY process source object
1611                let stream_id = pty_manager
1612                    .get_stream_id(&box_id)
1613                    .unwrap_or_else(|| format!("error-no-pty-{}", box_id));
1614
1615                match pty_manager.kill_pty_process(&box_id) {
1616                    Ok(_) => {
1617                        messages.push(Message::StreamUpdateMessage(
1618                            crate::model::common::StreamUpdate {
1619                                target_box_id: box_id.clone(),
1620                                stream_id,
1621                                content_update: format!("PTY process killed for box {}", box_id),
1622                                source_state: crate::model::common::SourceState::Pty(
1623                                    crate::model::common::PtySourceState {
1624                                        process_id: 0,
1625                                        runtime: std::time::Duration::from_millis(0),
1626                                        exit_code: Some(0),
1627                                        status: crate::model::common::ExecutionPtyStatus::Completed,
1628                                    },
1629                                ),
1630                                execution_mode: crate::model::common::ExecutionMode::Pty,
1631                            },
1632                        ));
1633                    }
1634                    Err(err) => {
1635                        messages.push(Message::StreamUpdateMessage(
1636                            crate::model::common::StreamUpdate {
1637                                target_box_id: box_id.clone(),
1638                                stream_id,
1639                                content_update: format!("Failed to kill PTY process: {}", err),
1640                                source_state: crate::model::common::SourceState::Pty(
1641                                    crate::model::common::PtySourceState {
1642                                        process_id: 0,
1643                                        runtime: std::time::Duration::from_millis(0),
1644                                        exit_code: Some(1),
1645                                        status: crate::model::common::ExecutionPtyStatus::Failed(
1646                                            "Error".to_string(),
1647                                        ),
1648                                    },
1649                                ),
1650                                execution_mode: crate::model::common::ExecutionMode::Pty,
1651                            },
1652                        ));
1653                    }
1654                }
1655            } else {
1656                messages.push(Message::StreamUpdateMessage(
1657                    crate::model::common::StreamUpdate {
1658                        target_box_id: box_id.clone(),
1659                        stream_id: format!("error-no-pty-{}", box_id),
1660                        content_update: "PTY manager not available".to_string(),
1661                        source_state: crate::model::common::SourceState::Pty(
1662                            crate::model::common::PtySourceState {
1663                                process_id: 0,
1664                                runtime: std::time::Duration::from_millis(0),
1665                                exit_code: Some(1),
1666                                status: crate::model::common::ExecutionPtyStatus::Failed(
1667                                    "PTY manager not available".to_string(),
1668                                ),
1669                            },
1670                        ),
1671                        execution_mode: crate::model::common::ExecutionMode::Pty,
1672                    },
1673                ));
1674            }
1675        }
1676        SocketFunction::RestartPtyProcess { box_id } => {
1677            if let Some(pty_manager) = &app_context.pty_manager {
1678                match pty_manager.restart_pty_process(&box_id) {
1679                    Ok(_) => {
1680                        messages.push(Message::StreamUpdateMessage(
1681                            crate::model::common::StreamUpdate {
1682                                target_box_id: box_id.clone(),
1683                                stream_id: pty_manager
1684                                    .get_stream_id(&box_id)
1685                                    .unwrap_or_else(|| format!("error-no-pty-{}", box_id)),
1686                                content_update: format!("PTY process restarted for box {}", box_id),
1687                                source_state: crate::model::common::SourceState::Pty(
1688                                    crate::model::common::PtySourceState {
1689                                        process_id: 0,
1690                                        runtime: std::time::Duration::from_millis(0),
1691                                        exit_code: Some(0),
1692                                        status: crate::model::common::ExecutionPtyStatus::Completed,
1693                                    },
1694                                ),
1695                                execution_mode: crate::model::common::ExecutionMode::Pty,
1696                            },
1697                        ));
1698                    }
1699                    Err(err) => {
1700                        messages.push(Message::StreamUpdateMessage(
1701                            crate::model::common::StreamUpdate {
1702                                target_box_id: box_id.clone(),
1703                                stream_id: pty_manager
1704                                    .get_stream_id(&box_id)
1705                                    .unwrap_or_else(|| format!("error-no-pty-{}", box_id)),
1706                                content_update: format!("Failed to restart PTY process: {}", err),
1707                                source_state: crate::model::common::SourceState::Pty(
1708                                    crate::model::common::PtySourceState {
1709                                        process_id: 0,
1710                                        runtime: std::time::Duration::from_millis(0),
1711                                        exit_code: Some(1),
1712                                        status: crate::model::common::ExecutionPtyStatus::Failed(
1713                                            "Error".to_string(),
1714                                        ),
1715                                    },
1716                                ),
1717                                execution_mode: crate::model::common::ExecutionMode::Pty,
1718                            },
1719                        ));
1720                    }
1721                }
1722            } else {
1723                messages.push(Message::StreamUpdateMessage(
1724                    crate::model::common::StreamUpdate {
1725                        target_box_id: box_id.clone(),
1726                        stream_id: format!("error-no-pty-{}", box_id),
1727                        content_update: "PTY manager not available".to_string(),
1728                        source_state: crate::model::common::SourceState::Pty(
1729                            crate::model::common::PtySourceState {
1730                                process_id: 0,
1731                                runtime: std::time::Duration::from_millis(0),
1732                                exit_code: Some(1),
1733                                status: crate::model::common::ExecutionPtyStatus::Failed(
1734                                    "PTY manager not available".to_string(),
1735                                ),
1736                            },
1737                        ),
1738                        execution_mode: crate::model::common::ExecutionMode::Pty,
1739                    },
1740                ));
1741            }
1742        }
1743        // F0138: Socket PTY Query - Get PTY status and info
1744        SocketFunction::QueryPtyStatus { box_id } => {
1745            if let Some(pty_manager) = &app_context.pty_manager {
1746                if let Some(info) = pty_manager.get_detailed_process_info(&box_id) {
1747                    let status_info = format!(
1748                        "PTY Status - Box: {}, PID: {:?}, Status: {:?}, Running: {}, Buffer Lines: {}",
1749                        info.muxbox_id, info.process_id, info.status, info.is_running, info.buffer_lines
1750                    );
1751                    messages.push(Message::StreamUpdateMessage(
1752                        crate::model::common::StreamUpdate {
1753                            target_box_id: box_id.clone(),
1754                            stream_id: format!("error-no-manager-{}", box_id),
1755                            content_update: status_info,
1756                            source_state: crate::model::common::SourceState::Pty(
1757                                crate::model::common::PtySourceState {
1758                                    process_id: 0,
1759                                    runtime: std::time::Duration::from_millis(0),
1760                                    exit_code: Some(0),
1761                                    status: crate::model::common::ExecutionPtyStatus::Completed,
1762                                },
1763                            ),
1764                            execution_mode: crate::model::common::ExecutionMode::Pty,
1765                        },
1766                    ));
1767                } else {
1768                    messages.push(Message::StreamUpdateMessage(
1769                        crate::model::common::StreamUpdate {
1770                            target_box_id: box_id.clone(),
1771                            stream_id: format!("error-no-manager-{}", box_id),
1772                            content_update: format!("No PTY process found for box {}", box_id),
1773                            source_state: crate::model::common::SourceState::Pty(
1774                                crate::model::common::PtySourceState {
1775                                    process_id: 0,
1776                                    runtime: std::time::Duration::from_millis(0),
1777                                    exit_code: Some(1),
1778                                    status: crate::model::common::ExecutionPtyStatus::Failed(
1779                                        "Error".to_string(),
1780                                    ),
1781                                },
1782                            ),
1783                            execution_mode: crate::model::common::ExecutionMode::Pty,
1784                        },
1785                    ));
1786                }
1787            } else {
1788                messages.push(Message::StreamUpdateMessage(
1789                    crate::model::common::StreamUpdate {
1790                        target_box_id: box_id.clone(),
1791                        stream_id: format!("error-no-pty-{}", box_id),
1792                        content_update: "PTY manager not available".to_string(),
1793                        source_state: crate::model::common::SourceState::Pty(
1794                            crate::model::common::PtySourceState {
1795                                process_id: 0,
1796                                runtime: std::time::Duration::from_millis(0),
1797                                exit_code: Some(1),
1798                                status: crate::model::common::ExecutionPtyStatus::Failed(
1799                                    "PTY manager not available".to_string(),
1800                                ),
1801                            },
1802                        ),
1803                        execution_mode: crate::model::common::ExecutionMode::Pty,
1804                    },
1805                ));
1806            }
1807        }
1808        // F0136: Socket PTY Spawn - Spawn PTY processes via socket commands
1809        SocketFunction::SpawnPtyProcess {
1810            box_id,
1811            script,
1812            libs,
1813            redirect_output,
1814        } => {
1815            // First register execution source to get stream_id
1816            let stream_id = app_context.app.register_execution_source(
1817                ExecutionSourceType::SocketUpdate {
1818                    command_type: "spawn_pty_process".to_string(),
1819                },
1820                box_id.clone(),
1821            );
1822
1823            // Route through unified execution architecture with registered stream_id
1824            let execute_script_msg =
1825                crate::thread_manager::Message::ExecuteScriptMessage(ExecuteScript {
1826                    script,
1827                    source: ExecutionSource {
1828                        source_type: SourceType::SocketUpdate,
1829                        source_id: box_id.clone(),
1830                        source_reference: SourceReference::SocketCommand(
1831                            "spawn_pty_process".to_string(),
1832                        ),
1833                    },
1834                    execution_mode: ExecutionMode::Pty,
1835                    target_box_id: box_id.clone(),
1836                    libs: libs.unwrap_or_default(),
1837                    redirect_output,
1838                    append_output: false,
1839                    stream_id,
1840                    target_bounds: None, // Socket commands don't have direct access to bounds - will use defaults
1841                });
1842
1843            // Add ExecuteScript message to be sent via ThreadManager
1844            messages.push(execute_script_msg);
1845            log::info!(
1846                "PTY process spawn queued for execution via unified architecture: {}",
1847                box_id
1848            );
1849        }
1850        // F0139: Socket PTY Input - Send input to PTY processes remotely
1851        SocketFunction::SendPtyInput { box_id, input } => {
1852            if let Some(pty_manager) = &app_context.pty_manager {
1853                // Get the stream ID from the PTY process source object
1854                let stream_id = pty_manager
1855                    .get_stream_id(&box_id)
1856                    .unwrap_or_else(|| format!("error-no-pty-{}", box_id));
1857
1858                match pty_manager.send_input(&box_id, &input) {
1859                    Ok(_) => {
1860                        messages.push(Message::StreamUpdateMessage(
1861                            crate::model::common::StreamUpdate {
1862                                target_box_id: box_id.clone(),
1863                                stream_id,
1864                                content_update: format!(
1865                                    "Input sent successfully to PTY process for box {}",
1866                                    box_id
1867                                ),
1868                                source_state: crate::model::common::SourceState::Pty(
1869                                    crate::model::common::PtySourceState {
1870                                        process_id: 0,
1871                                        runtime: std::time::Duration::from_millis(0),
1872                                        exit_code: Some(0),
1873                                        status: crate::model::common::ExecutionPtyStatus::Completed,
1874                                    },
1875                                ),
1876                                execution_mode: crate::model::common::ExecutionMode::Pty,
1877                            },
1878                        ));
1879                    }
1880                    Err(err) => {
1881                        messages.push(Message::StreamUpdateMessage(
1882                            crate::model::common::StreamUpdate {
1883                                target_box_id: box_id.clone(),
1884                                stream_id,
1885                                content_update: format!(
1886                                    "Failed to send input to PTY process: {}",
1887                                    err
1888                                ),
1889                                source_state: crate::model::common::SourceState::Pty(
1890                                    crate::model::common::PtySourceState {
1891                                        process_id: 0,
1892                                        runtime: std::time::Duration::from_millis(0),
1893                                        exit_code: Some(1),
1894                                        status: crate::model::common::ExecutionPtyStatus::Failed(
1895                                            "Error".to_string(),
1896                                        ),
1897                                    },
1898                                ),
1899                                execution_mode: crate::model::common::ExecutionMode::Pty,
1900                            },
1901                        ));
1902                    }
1903                }
1904            } else {
1905                messages.push(Message::StreamUpdateMessage(
1906                    crate::model::common::StreamUpdate {
1907                        target_box_id: box_id.clone(),
1908                        stream_id: format!("error-no-pty-{}", box_id),
1909                        content_update: "PTY manager not available".to_string(),
1910                        source_state: crate::model::common::SourceState::Pty(
1911                            crate::model::common::PtySourceState {
1912                                process_id: 0,
1913                                runtime: std::time::Duration::from_millis(0),
1914                                exit_code: Some(1),
1915                                status: crate::model::common::ExecutionPtyStatus::Failed(
1916                                    "PTY manager not available".to_string(),
1917                                ),
1918                            },
1919                        ),
1920                        execution_mode: crate::model::common::ExecutionMode::Pty,
1921                    },
1922                ));
1923            }
1924        }
1925    }
1926    Ok((app_context, messages))
1927}
1928
1929#[derive(Clone, PartialEq, Debug)]
1930pub struct Cell {
1931    pub fg_color: String,
1932    pub bg_color: String,
1933    pub ch: char,
1934}
1935
1936#[derive(Debug, Clone)]
1937pub struct ScreenBuffer {
1938    pub width: usize,
1939    pub height: usize,
1940    pub buffer: Vec<Vec<Cell>>,
1941}
1942
1943impl Default for ScreenBuffer {
1944    fn default() -> Self {
1945        Self::new()
1946    }
1947}
1948
1949impl ScreenBuffer {
1950    pub fn new() -> Self {
1951        // Theme-aware default so any cell never touched by a draw (the root area
1952        // behind/around panels) shows the THEME background — white in light, black
1953        // in dark — instead of a hardcoded black that darkened the whole app in
1954        // light mode.
1955        let default_cell = Cell {
1956            fg_color: get_fg_color(crate::color_utils::default_fg_color(false)),
1957            bg_color: get_bg_color(crate::color_utils::default_bg_color(false)),
1958            ch: ' ',
1959        };
1960        let width = screen_width();
1961        let height = screen_height();
1962        let buffer = vec![vec![default_cell; width]; height];
1963        ScreenBuffer {
1964            width,
1965            height,
1966            buffer,
1967        }
1968    }
1969
1970    pub fn new_custom(width: usize, height: usize) -> Self {
1971        // Theme-aware default so any cell never touched by a draw (the root area
1972        // behind/around panels) shows the THEME background — white in light, black
1973        // in dark — instead of a hardcoded black that darkened the whole app in
1974        // light mode.
1975        let default_cell = Cell {
1976            fg_color: get_fg_color(crate::color_utils::default_fg_color(false)),
1977            bg_color: get_bg_color(crate::color_utils::default_bg_color(false)),
1978            ch: ' ',
1979        };
1980        let buffer = vec![vec![default_cell; width]; height];
1981        ScreenBuffer {
1982            width,
1983            height,
1984            buffer,
1985        }
1986    }
1987
1988    pub fn clear(&mut self) {
1989        // Theme-aware default so any cell never touched by a draw (the root area
1990        // behind/around panels) shows the THEME background — white in light, black
1991        // in dark — instead of a hardcoded black that darkened the whole app in
1992        // light mode.
1993        let default_cell = Cell {
1994            fg_color: get_fg_color(crate::color_utils::default_fg_color(false)),
1995            bg_color: get_bg_color(crate::color_utils::default_bg_color(false)),
1996            ch: ' ',
1997        };
1998        self.buffer = vec![vec![default_cell; self.width]; self.height];
1999    }
2000
2001    pub fn update(&mut self, x: usize, y: usize, cell: Cell) {
2002        if x < self.width && y < self.height {
2003            self.buffer[y][x] = cell;
2004        }
2005    }
2006
2007    pub fn get(&self, x: usize, y: usize) -> Option<&Cell> {
2008        if x < self.width && y < self.height {
2009            Some(&self.buffer[y][x])
2010        } else {
2011            None
2012        }
2013    }
2014
2015    pub fn resize(&mut self, width: usize, height: usize) {
2016        // First handle shrinking the buffer if necessary
2017        if height < self.height {
2018            self.buffer.truncate(height);
2019        }
2020        if width < self.width {
2021            for row in &mut self.buffer {
2022                row.truncate(width);
2023            }
2024        }
2025
2026        // Now handle expanding the buffer if necessary
2027        if height > self.height {
2028            let default_row = vec![
2029                Cell {
2030                    fg_color: get_fg_color("white"),
2031                    bg_color: get_bg_color("black"),
2032                    ch: ' ',
2033                };
2034                width
2035            ];
2036
2037            self.buffer.resize_with(height, || default_row.clone());
2038        }
2039        if width > self.width {
2040            for row in &mut self.buffer {
2041                row.resize_with(width, || Cell {
2042                    fg_color: get_fg_color("white"),
2043                    bg_color: get_bg_color("black"),
2044                    ch: ' ',
2045                });
2046            }
2047        }
2048
2049        // Update the dimensions
2050        self.width = width;
2051        self.height = height;
2052    }
2053}
2054
2055#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
2056pub struct InputBounds {
2057    pub x1: String,
2058    pub y1: String,
2059    pub x2: String,
2060    pub y2: String,
2061}
2062
2063impl InputBounds {
2064    pub fn to_bounds(&self, parent_bounds: &Bounds) -> Bounds {
2065        input_bounds_to_bounds(self, parent_bounds)
2066    }
2067}
2068
2069#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)]
2070pub struct Bounds {
2071    pub x1: usize,
2072    pub y1: usize,
2073    pub x2: usize,
2074    pub y2: usize,
2075}
2076
2077// PartialEq and Eq now derived automatically
2078
2079#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq, Default)]
2080pub enum Anchor {
2081    TopLeft,
2082    TopRight,
2083    BottomLeft,
2084    BottomRight,
2085    #[default]
2086    Center,
2087    CenterTop,
2088    CenterBottom,
2089    CenterLeft,
2090    CenterRight,
2091}
2092
2093impl Bounds {
2094    pub fn new(x1: usize, y1: usize, x2: usize, y2: usize) -> Self {
2095        Bounds { x1, y1, x2, y2 }
2096    }
2097
2098    pub fn validate(&self) {
2099        if self.x1 > self.x2 {
2100            panic!(
2101                "Validation error: x1 ({}) is greater than x2 ({})",
2102                self.x1, self.x2
2103            );
2104        }
2105        if self.y1 > self.y2 {
2106            panic!(
2107                "Validation error: y1 ({}) is greater than y2 ({})",
2108                self.y1, self.y2
2109            );
2110        }
2111    }
2112
2113    pub fn width(&self) -> usize {
2114        // For inclusive coordinate bounds, width is x2 - x1 + 1
2115        self.x2.saturating_sub(self.x1).saturating_add(1)
2116    }
2117
2118    pub fn height(&self) -> usize {
2119        // For inclusive coordinate bounds, height is y2 - y1 + 1
2120        self.y2.saturating_sub(self.y1).saturating_add(1)
2121    }
2122
2123    pub fn extend(&mut self, horizontal_amount: usize, vertical_amount: usize, anchor: Anchor) {
2124        match anchor {
2125            Anchor::TopLeft => {
2126                self.x1 = self.x1.saturating_sub(horizontal_amount);
2127                self.y1 = self.y1.saturating_sub(vertical_amount);
2128            }
2129            Anchor::TopRight => {
2130                self.x2 += horizontal_amount;
2131                self.y1 = self.y1.saturating_sub(vertical_amount);
2132            }
2133            Anchor::BottomLeft => {
2134                self.x1 = self.x1.saturating_sub(horizontal_amount);
2135                self.y2 += vertical_amount;
2136            }
2137            Anchor::BottomRight => {
2138                self.x2 += horizontal_amount;
2139                self.y2 += vertical_amount;
2140            }
2141            Anchor::Center => {
2142                let half_horizontal = horizontal_amount / 2;
2143                let half_vertical = vertical_amount / 2;
2144                self.x1 = self.x1.saturating_sub(half_horizontal);
2145                self.y1 = self.y1.saturating_sub(half_vertical);
2146                self.x2 += half_horizontal;
2147                self.y2 += half_vertical;
2148            }
2149            Anchor::CenterTop => {
2150                let half_horizontal = horizontal_amount / 2;
2151                self.x1 = self.x1.saturating_sub(half_horizontal);
2152                self.x2 += half_horizontal;
2153                self.y1 = self.y1.saturating_sub(vertical_amount);
2154            }
2155            Anchor::CenterBottom => {
2156                let half_horizontal = horizontal_amount / 2;
2157                self.x1 = self.x1.saturating_sub(half_horizontal);
2158                self.x2 += half_horizontal;
2159                self.y2 += vertical_amount;
2160            }
2161            Anchor::CenterLeft => {
2162                let half_vertical = vertical_amount / 2;
2163                self.x1 = self.x1.saturating_sub(horizontal_amount);
2164                self.y1 = self.y1.saturating_sub(half_vertical);
2165                self.y2 += half_vertical;
2166            }
2167            Anchor::CenterRight => {
2168                let half_vertical = vertical_amount / 2;
2169                self.x2 += horizontal_amount;
2170                self.y1 = self.y1.saturating_sub(half_vertical);
2171                self.y2 += half_vertical;
2172            }
2173        }
2174        self.validate();
2175    }
2176
2177    pub fn contract(&mut self, horizontal_amount: usize, vertical_amount: usize, anchor: Anchor) {
2178        match anchor {
2179            Anchor::TopLeft => {
2180                self.x1 += horizontal_amount;
2181                self.y1 += vertical_amount;
2182            }
2183            Anchor::TopRight => {
2184                self.x2 = self.x2.saturating_sub(horizontal_amount);
2185                self.y1 += vertical_amount;
2186            }
2187            Anchor::BottomLeft => {
2188                self.x1 += horizontal_amount;
2189                self.y2 = self.y2.saturating_sub(vertical_amount);
2190            }
2191            Anchor::BottomRight => {
2192                self.x2 = self.x2.saturating_sub(horizontal_amount);
2193                self.y2 = self.y2.saturating_sub(vertical_amount);
2194            }
2195            Anchor::Center => {
2196                let half_horizontal = horizontal_amount / 2;
2197                let half_vertical = vertical_amount / 2;
2198                self.x1 += half_horizontal;
2199                self.y1 += half_vertical;
2200                self.x2 = self.x2.saturating_sub(half_horizontal);
2201                self.y2 = self.y2.saturating_sub(half_vertical);
2202            }
2203            Anchor::CenterTop => {
2204                let half_horizontal = horizontal_amount / 2;
2205                self.x1 += half_horizontal;
2206                self.x2 = self.x2.saturating_sub(half_horizontal);
2207                self.y1 += vertical_amount;
2208            }
2209            Anchor::CenterBottom => {
2210                let half_horizontal = horizontal_amount / 2;
2211                self.x1 += half_horizontal;
2212                self.x2 = self.x2.saturating_sub(half_horizontal);
2213                self.y2 = self.y2.saturating_sub(vertical_amount);
2214            }
2215            Anchor::CenterLeft => {
2216                let half_vertical = vertical_amount / 2;
2217                self.x1 += horizontal_amount;
2218                self.y1 += half_vertical;
2219                self.y2 = self.y2.saturating_sub(half_vertical);
2220            }
2221            Anchor::CenterRight => {
2222                let half_vertical = vertical_amount / 2;
2223                self.x2 = self.x2.saturating_sub(horizontal_amount);
2224                self.y1 += half_vertical;
2225                self.y2 = self.y2.saturating_sub(half_vertical);
2226            }
2227        }
2228        self.validate();
2229    }
2230
2231    pub fn move_to(&mut self, x: usize, y: usize, anchor: Anchor) {
2232        match anchor {
2233            Anchor::TopLeft => {
2234                let width = self.width();
2235                let height = self.height();
2236                self.x1 = x;
2237                self.y1 = y;
2238                self.x2 = x + width - 1; // Inclusive bounds
2239                self.y2 = y + height - 1; // Inclusive bounds
2240            }
2241            Anchor::TopRight => {
2242                let width = self.width();
2243                let height = self.height();
2244                self.x2 = x;
2245                self.y1 = y;
2246                self.x1 = x - width + 1; // Inclusive bounds
2247                self.y2 = y + height - 1; // Inclusive bounds
2248            }
2249            Anchor::BottomLeft => {
2250                let width = self.width();
2251                let height = self.height();
2252                self.x1 = x;
2253                self.y2 = y;
2254                self.x2 = x + width - 1; // Inclusive bounds
2255                self.y1 = y - height + 1; // Inclusive bounds
2256            }
2257            Anchor::BottomRight => {
2258                let width = self.width();
2259                let height = self.height();
2260                self.x2 = x;
2261                self.y2 = y;
2262                self.x1 = x - width + 1; // Inclusive bounds
2263                self.y1 = y - height + 1; // Inclusive bounds
2264            }
2265            Anchor::Center => {
2266                let width = self.width();
2267                let height = self.height();
2268                let half_width = width / 2;
2269                let half_height = height / 2;
2270                self.x1 = x - half_width;
2271                self.y1 = y - half_height;
2272                self.x2 = x + width - half_width - 1; // Inclusive bounds
2273                self.y2 = y + height - half_height - 1; // Inclusive bounds
2274            }
2275            Anchor::CenterTop => {
2276                let width = self.width();
2277                let height = self.height();
2278                let half_width = width / 2;
2279                self.x1 = x - half_width;
2280                self.x2 = x + width - half_width - 1; // Inclusive bounds
2281                self.y1 = y;
2282                self.y2 = y + height - 1; // Inclusive bounds
2283            }
2284            Anchor::CenterBottom => {
2285                let width = self.width();
2286                let height = self.height();
2287                let half_width = width / 2;
2288                self.x1 = x - half_width;
2289                self.x2 = x + width - half_width - 1; // Inclusive bounds
2290                self.y2 = y;
2291                self.y1 = y - height + 1; // Inclusive bounds
2292            }
2293            Anchor::CenterLeft => {
2294                let width = self.width();
2295                let height = self.height();
2296                let half_height = height / 2;
2297                self.x1 = x;
2298                self.x2 = x + width - 1; // Inclusive bounds
2299                self.y1 = y - half_height;
2300                self.y2 = y + height - half_height - 1; // Inclusive bounds
2301            }
2302            Anchor::CenterRight => {
2303                let width = self.width();
2304                let height = self.height();
2305                let half_height = height / 2;
2306                self.x2 = x;
2307                self.x1 = x - width + 1; // Inclusive bounds
2308                self.y1 = y - half_height;
2309                self.y2 = y + height - half_height - 1; // Inclusive bounds
2310            }
2311        }
2312        self.validate();
2313    }
2314
2315    pub fn move_by(&mut self, dx: isize, dy: isize) {
2316        self.x1 = (self.x1 as isize + dx) as usize;
2317        self.y1 = (self.y1 as isize + dy) as usize;
2318        self.x2 = (self.x2 as isize + dx) as usize;
2319        self.y2 = (self.y2 as isize + dy) as usize;
2320        self.validate();
2321    }
2322
2323    pub fn contains(&self, x: usize, y: usize) -> bool {
2324        x >= self.x1 && x < self.x2 && y >= self.y1 && y < self.y2
2325    }
2326
2327    pub fn contains_bounds(&self, other: &Bounds) -> bool {
2328        self.contains(other.x1, other.y1) && self.contains(other.x2, other.y2)
2329    }
2330
2331    pub fn intersects(&self, other: &Bounds) -> bool {
2332        self.contains(other.x1, other.y1)
2333            || self.contains(other.x2, other.y2)
2334            || self.contains(other.x1, other.y2)
2335            || self.contains(other.x2, other.y1)
2336    }
2337
2338    pub fn intersection(&self, other: &Bounds) -> Option<Bounds> {
2339        if self.intersects(other) {
2340            Some(Bounds {
2341                x1: self.x1.max(other.x1),
2342                y1: self.y1.max(other.y1),
2343                x2: self.x2.min(other.x2),
2344                y2: self.y2.min(other.y2),
2345            })
2346        } else {
2347            None
2348        }
2349    }
2350
2351    pub fn union(&self, other: &Bounds) -> Bounds {
2352        Bounds {
2353            x1: self.x1.min(other.x1),
2354            y1: self.y1.min(other.y1),
2355            x2: self.x2.max(other.x2),
2356            y2: self.y2.max(other.y2),
2357        }
2358    }
2359
2360    pub fn translate(&self, dx: isize, dy: isize) -> Bounds {
2361        Bounds {
2362            x1: (self.x1 as isize + dx) as usize,
2363            y1: (self.y1 as isize + dy) as usize,
2364            x2: (self.x2 as isize + dx) as usize,
2365            y2: (self.y2 as isize + dy) as usize,
2366        }
2367    }
2368
2369    pub fn center(&self) -> (usize, usize) {
2370        ((self.x1 + self.x2) / 2, (self.y1 + self.y2) / 2)
2371    }
2372
2373    pub fn center_x(&self) -> usize {
2374        (self.x1 + self.x2) / 2
2375    }
2376
2377    pub fn center_y(&self) -> usize {
2378        (self.y1 + self.y2) / 2
2379    }
2380
2381    pub fn top_left(&self) -> (usize, usize) {
2382        (self.x1, self.y1)
2383    }
2384
2385    pub fn top_right(&self) -> (usize, usize) {
2386        (self.x2, self.y1)
2387    }
2388
2389    pub fn bottom_left(&self) -> (usize, usize) {
2390        (self.x1, self.y2)
2391    }
2392
2393    pub fn bottom_right(&self) -> (usize, usize) {
2394        (self.x2, self.y2)
2395    }
2396
2397    pub fn top(&self) -> usize {
2398        self.y1
2399    }
2400
2401    pub fn bottom(&self) -> usize {
2402        self.y2
2403    }
2404
2405    pub fn left(&self) -> usize {
2406        self.x1
2407    }
2408
2409    pub fn right(&self) -> usize {
2410        self.x2
2411    }
2412
2413    pub fn center_top(&self) -> (usize, usize) {
2414        ((self.x1 + self.x2) / 2, self.y1)
2415    }
2416
2417    pub fn center_bottom(&self) -> (usize, usize) {
2418        ((self.x1 + self.x2) / 2, self.y2)
2419    }
2420
2421    pub fn center_left(&self) -> (usize, usize) {
2422        (self.x1, (self.y1 + self.y2) / 2)
2423    }
2424
2425    pub fn center_right(&self) -> (usize, usize) {
2426        (self.x2, (self.y1 + self.y2) / 2)
2427    }
2428
2429    /// Check if a point (x, y) is contained within these bounds (inclusive)
2430    pub fn contains_point(&self, x: usize, y: usize) -> bool {
2431        x >= self.x1 && x <= self.x2 && y >= self.y1 && y <= self.y2
2432    }
2433}
2434
2435impl std::fmt::Display for Bounds {
2436    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2437        write!(f, "({}, {}), ({}, {})", self.x1, self.y1, self.x2, self.y2)
2438    }
2439}
2440
2441pub fn calculate_initial_bounds(app_graph: &AppGraph, layout: &Layout) -> HashMap<String, Bounds> {
2442    let mut bounds_map = HashMap::new();
2443
2444    fn dfs(
2445        _app_graph: &AppGraph,
2446        _layout_id: &str,
2447        muxbox: &MuxBox,
2448        parent_bounds: Bounds,
2449        bounds_map: &mut HashMap<String, Bounds>,
2450    ) {
2451        let bounds = muxbox.absolute_bounds(Some(&parent_bounds));
2452        bounds_map.insert(muxbox.id.clone(), bounds.clone());
2453
2454        if let Some(children) = &muxbox.children {
2455            for child in children {
2456                dfs(_app_graph, _layout_id, child, bounds.clone(), bounds_map);
2457            }
2458        }
2459    }
2460
2461    let root_bounds = screen_bounds();
2462    if let Some(children) = &layout.children {
2463        for muxbox in children {
2464            dfs(
2465                app_graph,
2466                &layout.id,
2467                muxbox,
2468                root_bounds.clone(),
2469                &mut bounds_map,
2470            );
2471        }
2472    }
2473
2474    bounds_map
2475}
2476
2477pub fn adjust_bounds_with_constraints(
2478    layout: &Layout,
2479    mut bounds_map: HashMap<String, Bounds>,
2480) -> HashMap<String, Bounds> {
2481    fn apply_constraints(muxbox: &MuxBox, bounds: &mut Bounds) {
2482        if let Some(min_width) = muxbox.min_width {
2483            if bounds.width() < min_width {
2484                bounds.extend(min_width - bounds.width(), 0, muxbox.anchor.clone());
2485            }
2486        }
2487        if let Some(min_height) = muxbox.min_height {
2488            if bounds.height() < min_height {
2489                bounds.extend(0, min_height - bounds.height(), muxbox.anchor.clone());
2490            }
2491        }
2492        if let Some(max_width) = muxbox.max_width {
2493            if bounds.width() > max_width {
2494                bounds.contract(bounds.width() - max_width, 0, muxbox.anchor.clone());
2495            }
2496        }
2497        if let Some(max_height) = muxbox.max_height {
2498            if bounds.height() > max_height {
2499                bounds.contract(0, bounds.height() - max_height, muxbox.anchor.clone());
2500            }
2501        }
2502    }
2503
2504    fn dfs(muxbox: &MuxBox, bounds_map: &mut HashMap<String, Bounds>) -> Bounds {
2505        let mut bounds = bounds_map.remove(&muxbox.id).unwrap();
2506        apply_constraints(muxbox, &mut bounds);
2507        bounds_map.insert(muxbox.id.clone(), bounds.clone());
2508
2509        if let Some(children) = &muxbox.children {
2510            for child in children {
2511                let child_bounds = dfs(child, bounds_map);
2512                bounds.x2 = bounds.x2.max(child_bounds.x2);
2513                bounds.y2 = bounds.y2.max(child_bounds.y2);
2514            }
2515        }
2516
2517        bounds
2518    }
2519
2520    fn revalidate_children(
2521        muxbox: &MuxBox,
2522        bounds_map: &mut HashMap<String, Bounds>,
2523        parent_bounds: &Bounds,
2524    ) {
2525        if let Some(children) = &muxbox.children {
2526            for child in children {
2527                if let Some(child_bounds) = bounds_map.get_mut(&child.id) {
2528                    // Ensure child bounds are within parent bounds
2529                    if child_bounds.x2 > parent_bounds.x2 {
2530                        child_bounds.x2 = parent_bounds.x2;
2531                    }
2532                    if child_bounds.y2 > parent_bounds.y2 {
2533                        child_bounds.y2 = parent_bounds.y2;
2534                    }
2535                    if child_bounds.x1 < parent_bounds.x1 {
2536                        child_bounds.x1 = parent_bounds.x1;
2537                    }
2538                    if child_bounds.y1 < parent_bounds.y1 {
2539                        child_bounds.y1 = parent_bounds.y1;
2540                    }
2541                }
2542                revalidate_children(child, bounds_map, parent_bounds);
2543            }
2544        }
2545    }
2546
2547    if let Some(children) = &layout.children {
2548        for muxbox in children {
2549            let parent_bounds = dfs(muxbox, &mut bounds_map);
2550            revalidate_children(muxbox, &mut bounds_map, &parent_bounds);
2551        }
2552    }
2553
2554    bounds_map
2555}
2556
2557pub fn calculate_bounds_map(app_graph: &AppGraph, layout: &Layout) -> HashMap<String, Bounds> {
2558    let bounds_map = calculate_initial_bounds(app_graph, layout);
2559    adjust_bounds_with_constraints(layout, bounds_map)
2560}
2561
2562/// Flexible deserializer for script fields that handles:
2563/// - Single string (split on newlines)
2564/// - Array of strings
2565/// - Mixed array with YAML literal blocks
2566pub fn deserialize_script<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
2567where
2568    D: Deserializer<'de>,
2569{
2570    #[derive(Deserialize)]
2571    #[serde(untagged)]
2572    enum ScriptFormat {
2573        Single(String),
2574        Multiple(Vec<String>),
2575        Mixed(Vec<serde_yaml::Value>),
2576    }
2577
2578    let script_format = Option::<ScriptFormat>::deserialize(deserializer)?;
2579
2580    match script_format {
2581        None => Ok(None),
2582        Some(ScriptFormat::Single(single)) => {
2583            // Split single string on newlines, filter empty lines
2584            let commands: Vec<String> = single
2585                .lines()
2586                .map(|line| line.trim().to_string())
2587                .filter(|line| !line.is_empty())
2588                .collect();
2589            Ok(Some(commands))
2590        }
2591        Some(ScriptFormat::Multiple(multiple)) => Ok(Some(multiple)),
2592        Some(ScriptFormat::Mixed(mixed)) => {
2593            // Handle mixed array with literal blocks and simple strings
2594            let mut commands = Vec::new();
2595            for value in mixed {
2596                match value {
2597                    serde_yaml::Value::String(s) => commands.push(s),
2598                    serde_yaml::Value::Mapping(_) | serde_yaml::Value::Sequence(_) => {
2599                        // Convert complex YAML structures to string representation
2600                        if let Ok(yaml_str) = serde_yaml::to_string(&value) {
2601                            // For literal blocks, extract the actual content
2602                            let clean_str = yaml_str.trim_start_matches("---\n").trim().to_string();
2603                            if !clean_str.is_empty() {
2604                                commands.push(clean_str);
2605                            }
2606                        }
2607                    }
2608                    _ => {
2609                        // For other types, convert to string
2610                        commands.push(format!("{:?}", value));
2611                    }
2612                }
2613            }
2614            Ok(Some(commands))
2615        }
2616    }
2617}
2618
2619use std::io::{Read, Write};
2620use std::os::unix::net::UnixStream;
2621
2622pub fn send_json_to_socket(socket_path: &str, json: &str) -> Result<String, Box<dyn Error>> {
2623    let mut stream = UnixStream::connect(socket_path)?;
2624    stream.write_all(json.as_bytes())?;
2625    let mut response = String::new();
2626    stream.read_to_string(&mut response)?;
2627    Ok(response)
2628}
2629
2630#[cfg(test)]
2631mod tests {
2632    use super::*;
2633
2634    // === Config Tests ===
2635
2636    /// Tests that Config::new() creates a valid configuration with the specified frame delay.
2637    /// This test demonstrates how to create a Config with proper validation.
2638    #[test]
2639    fn test_config_new_valid_frame_delay() {
2640        let config = Config::new(60);
2641        assert_eq!(config.frame_delay, 60);
2642    }
2643
2644    /// Tests that Config::new() panics when frame_delay is zero.
2645    /// This test demonstrates Config validation for invalid frame delays.
2646    #[test]
2647    #[should_panic(expected = "Validation error: frame_delay cannot be 0")]
2648    fn test_config_new_zero_frame_delay_panics() {
2649        Config::new(0);
2650    }
2651
2652    /// Tests that Config::default() creates a configuration with default values.
2653    /// This test demonstrates the default configuration settings.
2654    #[test]
2655    fn test_config_default() {
2656        let config = Config::default();
2657        assert_eq!(config.frame_delay, 30);
2658    }
2659
2660    /// Tests that Config::validate() correctly identifies invalid configurations.
2661    /// This test demonstrates Config validation behavior.
2662    #[test]
2663    #[should_panic(expected = "Validation error: frame_delay cannot be 0")]
2664    fn test_config_validate_zero_frame_delay() {
2665        let config = Config {
2666            frame_delay: 0,
2667            locked: false,
2668            calibrate: false,
2669        };
2670        config.validate();
2671    }
2672
2673    /// Tests that Config::validate() passes for valid configurations.
2674    /// This test demonstrates successful Config validation.
2675    #[test]
2676    fn test_config_validate_valid() {
2677        let config = Config {
2678            frame_delay: 16,
2679            locked: false,
2680            calibrate: false,
2681        };
2682        config.validate(); // Should not panic
2683    }
2684
2685    /// Tests that Config implements Hash consistently.
2686    /// This test demonstrates that Configs with the same values hash to the same value.
2687    #[test]
2688    fn test_config_hash_consistency() {
2689        let config1 = Config::new(30);
2690        let config2 = Config::new(30);
2691        let config3 = Config::new(60);
2692
2693        use std::collections::hash_map::DefaultHasher;
2694        use std::hash::{Hash, Hasher};
2695
2696        let mut hasher1 = DefaultHasher::new();
2697        let mut hasher2 = DefaultHasher::new();
2698        let mut hasher3 = DefaultHasher::new();
2699
2700        config1.hash(&mut hasher1);
2701        config2.hash(&mut hasher2);
2702        config3.hash(&mut hasher3);
2703
2704        assert_eq!(hasher1.finish(), hasher2.finish());
2705        assert_ne!(hasher1.finish(), hasher3.finish());
2706    }
2707
2708    // === Bounds Tests ===
2709
2710    /// Tests that Bounds::new() creates bounds with correct coordinates.
2711    /// This test demonstrates basic Bounds construction.
2712    #[test]
2713    fn test_bounds_new() {
2714        let bounds = Bounds::new(10, 20, 100, 200);
2715        assert_eq!(bounds.x1, 10);
2716        assert_eq!(bounds.y1, 20);
2717        assert_eq!(bounds.x2, 100);
2718        assert_eq!(bounds.y2, 200);
2719    }
2720
2721    /// Tests that Bounds::validate() panics for invalid x coordinates.
2722    /// This test demonstrates Bounds validation for x-coordinate ordering.
2723    #[test]
2724    #[should_panic(expected = "Validation error: x1 (100) is greater than x2 (50)")]
2725    fn test_bounds_validate_invalid_x_coordinates() {
2726        let bounds = Bounds::new(100, 20, 50, 200);
2727        bounds.validate();
2728    }
2729
2730    /// Tests that Bounds::validate() panics for invalid y coordinates.
2731    /// This test demonstrates Bounds validation for y-coordinate ordering.
2732    #[test]
2733    #[should_panic(expected = "Validation error: y1 (200) is greater than y2 (100)")]
2734    fn test_bounds_validate_invalid_y_coordinates() {
2735        let bounds = Bounds::new(10, 200, 100, 100);
2736        bounds.validate();
2737    }
2738
2739    /// Tests that Bounds::validate() passes for valid bounds.
2740    /// This test demonstrates successful Bounds validation.
2741    #[test]
2742    fn test_bounds_validate_valid() {
2743        let bounds = Bounds::new(10, 20, 100, 200);
2744        bounds.validate(); // Should not panic
2745    }
2746
2747    /// Tests that Bounds::width() calculates width correctly.
2748    /// This test demonstrates the width calculation feature.
2749    #[test]
2750    fn test_bounds_width() {
2751        let bounds = Bounds::new(10, 20, 100, 200);
2752        assert_eq!(bounds.width(), 91); // Inclusive bounds: 100-10+1 = 91
2753    }
2754
2755    /// Tests that Bounds::height() calculates height correctly.
2756    /// This test demonstrates the height calculation feature.
2757    #[test]
2758    fn test_bounds_height() {
2759        let bounds = Bounds::new(10, 20, 100, 200);
2760        assert_eq!(bounds.height(), 181); // Inclusive bounds: 200-20+1 = 181
2761    }
2762
2763    /// Tests that Bounds::width() handles edge case where x1 equals x2.
2764    /// This test demonstrates edge case handling in width calculation.
2765    #[test]
2766    fn test_bounds_width_zero() {
2767        let bounds = Bounds::new(50, 20, 50, 200);
2768        assert_eq!(bounds.width(), 1); // Inclusive bounds: 50-50+1 = 1
2769    }
2770
2771    /// Tests that Bounds::height() handles edge case where y1 equals y2.
2772    /// This test demonstrates edge case handling in height calculation.
2773    #[test]
2774    fn test_bounds_height_zero() {
2775        let bounds = Bounds::new(10, 50, 100, 50);
2776        assert_eq!(bounds.height(), 1); // Inclusive bounds: 50-50+1 = 1
2777    }
2778
2779    /// Tests that Bounds::contains() correctly identifies points within bounds.
2780    /// This test demonstrates the point containment feature.
2781    #[test]
2782    fn test_bounds_contains() {
2783        let bounds = Bounds::new(10, 20, 100, 200);
2784        assert!(bounds.contains(50, 100));
2785        assert!(bounds.contains(10, 20)); // Edge case: top-left corner
2786        assert!(!bounds.contains(100, 200)); // Edge case: bottom-right corner (exclusive)
2787        assert!(!bounds.contains(5, 100)); // Outside left
2788        assert!(!bounds.contains(150, 100)); // Outside right
2789        assert!(!bounds.contains(50, 10)); // Outside top
2790        assert!(!bounds.contains(50, 250)); // Outside bottom
2791    }
2792
2793    /// Tests that Bounds::contains_bounds() correctly identifies bounds containment.
2794    /// This test demonstrates the bounds containment feature.
2795    #[test]
2796    fn test_bounds_contains_bounds() {
2797        let outer = Bounds::new(10, 20, 100, 200);
2798        let inner = Bounds::new(30, 40, 80, 180);
2799        let overlapping = Bounds::new(5, 15, 50, 100);
2800
2801        assert!(outer.contains_bounds(&inner));
2802        assert!(!outer.contains_bounds(&overlapping));
2803    }
2804
2805    /// Tests that Bounds::intersects() correctly identifies intersecting bounds.
2806    /// This test demonstrates the bounds intersection detection feature.
2807    #[test]
2808    fn test_bounds_intersects() {
2809        let bounds1 = Bounds::new(10, 20, 100, 200);
2810        let bounds2 = Bounds::new(50, 100, 150, 250); // Overlapping
2811        let bounds3 = Bounds::new(200, 300, 250, 350); // Non-overlapping
2812
2813        assert!(bounds1.intersects(&bounds2));
2814        assert!(!bounds1.intersects(&bounds3));
2815    }
2816
2817    /// Tests that Bounds::intersection() returns correct intersection bounds.
2818    /// This test demonstrates the bounds intersection calculation feature.
2819    #[test]
2820    fn test_bounds_intersection() {
2821        let bounds1 = Bounds::new(10, 20, 100, 200);
2822        let bounds2 = Bounds::new(50, 100, 150, 250);
2823        let bounds3 = Bounds::new(200, 300, 250, 350);
2824
2825        let intersection = bounds1.intersection(&bounds2);
2826        assert!(intersection.is_some());
2827        let intersection = intersection.unwrap();
2828        assert_eq!(intersection.x1, 50);
2829        assert_eq!(intersection.y1, 100);
2830        assert_eq!(intersection.x2, 100);
2831        assert_eq!(intersection.y2, 200);
2832
2833        assert!(bounds1.intersection(&bounds3).is_none());
2834    }
2835
2836    /// Tests that Bounds::union() returns correct union bounds.
2837    /// This test demonstrates the bounds union calculation feature.
2838    #[test]
2839    fn test_bounds_union() {
2840        let bounds1 = Bounds::new(10, 20, 100, 200);
2841        let bounds2 = Bounds::new(50, 100, 150, 250);
2842
2843        let union = bounds1.union(&bounds2);
2844        assert_eq!(union.x1, 10);
2845        assert_eq!(union.y1, 20);
2846        assert_eq!(union.x2, 150);
2847        assert_eq!(union.y2, 250);
2848    }
2849
2850    /// Tests that Bounds::translate() correctly translates bounds.
2851    /// This test demonstrates the bounds translation feature.
2852    #[test]
2853    fn test_bounds_translate() {
2854        let bounds = Bounds::new(10, 20, 100, 200);
2855        let translated = bounds.translate(5, -10);
2856        assert_eq!(translated.x1, 15);
2857        assert_eq!(translated.y1, 10);
2858        assert_eq!(translated.x2, 105);
2859        assert_eq!(translated.y2, 190);
2860    }
2861
2862    /// Tests that Bounds::center() returns correct center point.
2863    /// This test demonstrates the center calculation feature.
2864    #[test]
2865    fn test_bounds_center() {
2866        let bounds = Bounds::new(10, 20, 100, 200);
2867        let center = bounds.center();
2868        assert_eq!(center, (55, 110));
2869    }
2870
2871    /// Tests that Bounds::center_x() returns correct x center.
2872    /// This test demonstrates the x-center calculation feature.
2873    #[test]
2874    fn test_bounds_center_x() {
2875        let bounds = Bounds::new(10, 20, 100, 200);
2876        assert_eq!(bounds.center_x(), 55);
2877    }
2878
2879    /// Tests that Bounds::center_y() returns correct y center.
2880    /// This test demonstrates the y-center calculation feature.
2881    #[test]
2882    fn test_bounds_center_y() {
2883        let bounds = Bounds::new(10, 20, 100, 200);
2884        assert_eq!(bounds.center_y(), 110);
2885    }
2886
2887    /// Tests that Bounds::extend() correctly extends bounds in all directions.
2888    /// This test demonstrates the bounds extension feature with Center anchor.
2889    #[test]
2890    fn test_bounds_extend_center() {
2891        let mut bounds = Bounds::new(50, 50, 100, 100);
2892        bounds.extend(20, 10, Anchor::Center);
2893        assert_eq!(bounds.x1, 40);
2894        assert_eq!(bounds.y1, 45);
2895        assert_eq!(bounds.x2, 110);
2896        assert_eq!(bounds.y2, 105);
2897    }
2898
2899    /// Tests that Bounds::extend() correctly extends bounds with TopLeft anchor.
2900    /// This test demonstrates the bounds extension feature with TopLeft anchor.
2901    #[test]
2902    fn test_bounds_extend_top_left() {
2903        let mut bounds = Bounds::new(50, 50, 100, 100);
2904        bounds.extend(20, 10, Anchor::TopLeft);
2905        assert_eq!(bounds.x1, 30);
2906        assert_eq!(bounds.y1, 40);
2907        assert_eq!(bounds.x2, 100);
2908        assert_eq!(bounds.y2, 100);
2909    }
2910
2911    /// Tests that Bounds::contract() correctly contracts bounds.
2912    /// This test demonstrates the bounds contraction feature.
2913    #[test]
2914    fn test_bounds_contract_center() {
2915        let mut bounds = Bounds::new(50, 50, 100, 100);
2916        bounds.contract(10, 20, Anchor::Center);
2917        assert_eq!(bounds.x1, 55);
2918        assert_eq!(bounds.y1, 60);
2919        assert_eq!(bounds.x2, 95);
2920        assert_eq!(bounds.y2, 90);
2921    }
2922
2923    /// Tests that Bounds::move_to() correctly moves bounds to new position.
2924    /// This test demonstrates the bounds movement feature.
2925    #[test]
2926    fn test_bounds_move_to() {
2927        let mut bounds = Bounds::new(10, 20, 60, 70);
2928        bounds.move_to(100, 150, Anchor::TopLeft);
2929        assert_eq!(bounds.x1, 100);
2930        assert_eq!(bounds.y1, 150);
2931        assert_eq!(bounds.x2, 150);
2932        assert_eq!(bounds.y2, 200);
2933    }
2934
2935    /// Tests that Bounds::move_by() correctly moves bounds by offset.
2936    /// This test demonstrates the bounds offset movement feature.
2937    #[test]
2938    fn test_bounds_move_by() {
2939        let mut bounds = Bounds::new(10, 20, 60, 70);
2940        bounds.move_by(5, -10);
2941        assert_eq!(bounds.x1, 15);
2942        assert_eq!(bounds.y1, 10);
2943        assert_eq!(bounds.x2, 65);
2944        assert_eq!(bounds.y2, 60);
2945    }
2946
2947    /// Tests various anchor point getters.
2948    /// This test demonstrates the anchor point calculation features.
2949    #[test]
2950    fn test_bounds_anchor_points() {
2951        let bounds = Bounds::new(10, 20, 100, 200);
2952
2953        assert_eq!(bounds.top_left(), (10, 20));
2954        assert_eq!(bounds.top_right(), (100, 20));
2955        assert_eq!(bounds.bottom_left(), (10, 200));
2956        assert_eq!(bounds.bottom_right(), (100, 200));
2957        assert_eq!(bounds.center_top(), (55, 20));
2958        assert_eq!(bounds.center_bottom(), (55, 200));
2959        assert_eq!(bounds.center_left(), (10, 110));
2960        assert_eq!(bounds.center_right(), (100, 110));
2961        assert_eq!(bounds.top(), 20);
2962        assert_eq!(bounds.bottom(), 200);
2963        assert_eq!(bounds.left(), 10);
2964        assert_eq!(bounds.right(), 100);
2965    }
2966
2967    /// Tests that Bounds::to_string() formats bounds correctly.
2968    /// This test demonstrates the bounds string formatting feature.
2969    #[test]
2970    fn test_bounds_to_string() {
2971        let bounds = Bounds::new(10, 20, 100, 200);
2972        assert_eq!(bounds.to_string(), "(10, 20), (100, 200)");
2973    }
2974
2975    // === InputBounds Tests ===
2976
2977    /// Tests that InputBounds::to_bounds() converts percentage strings to absolute bounds.
2978    /// This test demonstrates the InputBounds to Bounds conversion feature.
2979    #[test]
2980    fn test_input_bounds_to_bounds() {
2981        let input_bounds = InputBounds {
2982            x1: "25%".to_string(),
2983            y1: "50%".to_string(),
2984            x2: "75%".to_string(),
2985            y2: "100%".to_string(),
2986        };
2987        let parent_bounds = Bounds::new(0, 0, 100, 200);
2988        let bounds = input_bounds.to_bounds(&parent_bounds);
2989
2990        assert_eq!(bounds.x1, 25);
2991        assert_eq!(bounds.y1, 100);
2992        assert_eq!(bounds.x2, 75); // 75% of (101-1) range = 75
2993        assert_eq!(bounds.y2, 200); // 100% of (201-1) range = 200
2994    }
2995
2996    // === Anchor Tests ===
2997
2998    /// Tests that Anchor::default() returns Center.
2999    /// This test demonstrates the default anchor behavior.
3000    #[test]
3001    fn test_anchor_default() {
3002        let anchor = Anchor::default();
3003        assert_eq!(anchor, Anchor::Center);
3004    }
3005
3006    // === ScreenBuffer Tests ===
3007
3008    /// Tests that ScreenBuffer::new_custom() creates a buffer with specified dimensions.
3009    /// This test demonstrates how to create a custom-sized screen buffer.
3010    #[test]
3011    fn test_screenbuffer_new() {
3012        let screen_buffer = ScreenBuffer::new_custom(5, 5);
3013        assert_eq!(screen_buffer.width, 5);
3014        assert_eq!(screen_buffer.height, 5);
3015        assert_eq!(screen_buffer.buffer.len(), 5);
3016        assert_eq!(screen_buffer.buffer[0].len(), 5);
3017    }
3018
3019    /// Tests that ScreenBuffer::clear() resets all cells to default values.
3020    /// This test demonstrates the screen buffer clearing feature.
3021    #[test]
3022    fn test_screenbuffer_clear() {
3023        let mut screen_buffer = ScreenBuffer::new_custom(5, 5);
3024        let test_cell = Cell {
3025            fg_color: String::from("red"),
3026            bg_color: String::from("blue"),
3027            ch: 'X',
3028        };
3029        screen_buffer.update(2, 2, test_cell.clone());
3030        screen_buffer.clear();
3031        for row in screen_buffer.buffer.iter() {
3032            for cell in row.iter() {
3033                assert_eq!(cell.fg_color, get_fg_color("white"));
3034                assert_eq!(cell.bg_color, get_bg_color("black"));
3035                assert_eq!(cell.ch, ' ');
3036            }
3037        }
3038    }
3039
3040    /// Tests that ScreenBuffer::update() correctly updates a cell.
3041    /// This test demonstrates the screen buffer cell update feature.
3042    #[test]
3043    fn test_screenbuffer_update() {
3044        let mut screen_buffer = ScreenBuffer::new_custom(5, 5);
3045        let test_cell = Cell {
3046            fg_color: String::from("red"),
3047            bg_color: String::from("blue"),
3048            ch: 'X',
3049        };
3050        screen_buffer.update(2, 2, test_cell.clone());
3051        assert_eq!(screen_buffer.get(2, 2).unwrap(), &test_cell);
3052    }
3053
3054    /// Tests that ScreenBuffer::get() returns correct cell references.
3055    /// This test demonstrates the screen buffer cell retrieval feature.
3056    #[test]
3057    fn test_screenbuffer_get() {
3058        let screen_buffer = ScreenBuffer::new_custom(5, 5);
3059        assert!(screen_buffer.get(6, 6).is_none());
3060        assert!(screen_buffer.get(3, 3).is_some());
3061    }
3062
3063    /// Tests that ScreenBuffer::update() ignores out-of-bounds coordinates.
3064    /// This test demonstrates bounds checking in screen buffer updates.
3065    #[test]
3066    fn test_screenbuffer_update_out_of_bounds() {
3067        let mut screen_buffer = ScreenBuffer::new_custom(5, 5);
3068        let test_cell = Cell {
3069            fg_color: String::from("red"),
3070            bg_color: String::from("blue"),
3071            ch: 'X',
3072        };
3073        screen_buffer.update(10, 10, test_cell); // Should not panic
3074        assert!(screen_buffer.get(10, 10).is_none());
3075    }
3076
3077    /// Tests that ScreenBuffer::resize() correctly resizes the buffer.
3078    /// This test demonstrates the screen buffer resizing feature.
3079    #[test]
3080    fn test_screenbuffer_resize() {
3081        let mut screen_buffer = ScreenBuffer::new_custom(5, 5);
3082        screen_buffer.resize(10, 8);
3083        assert_eq!(screen_buffer.width, 10);
3084        assert_eq!(screen_buffer.height, 8);
3085        assert_eq!(screen_buffer.buffer.len(), 8);
3086        assert_eq!(screen_buffer.buffer[0].len(), 10);
3087    }
3088
3089    /// Tests that ScreenBuffer::resize() handles shrinking correctly.
3090    /// This test demonstrates the screen buffer shrinking feature.
3091    #[test]
3092    fn test_screenbuffer_resize_shrink() {
3093        let mut screen_buffer = ScreenBuffer::new_custom(10, 10);
3094        screen_buffer.resize(5, 5);
3095        assert_eq!(screen_buffer.width, 5);
3096        assert_eq!(screen_buffer.height, 5);
3097        assert_eq!(screen_buffer.buffer.len(), 5);
3098        assert_eq!(screen_buffer.buffer[0].len(), 5);
3099    }
3100
3101    // === Helper Functions ===
3102
3103    /// Creates a test app context with a valid layout for testing.
3104    /// This helper ensures tests have a valid app context with layouts.
3105    fn create_test_app_context() -> AppContext {
3106        let current_dir = std::env::current_dir().expect("Failed to get current directory");
3107        let dashboard_path = current_dir.join("layouts/tests.yaml");
3108        let app = crate::load_app_from_yaml(dashboard_path.to_str().unwrap())
3109            .expect("Failed to load app");
3110        AppContext::new(app, Config::default())
3111    }
3112
3113    // === SocketFunction Tests ===
3114
3115    /// Tests that run_socket_function() correctly handles ReplaceBoxContent.
3116    /// This test demonstrates socket function message processing.
3117    #[test]
3118    fn test_run_socket_function_replace_muxbox_content() {
3119        let app_context = create_test_app_context();
3120        let socket_function = SocketFunction::ReplaceBoxContent {
3121            box_id: "test_muxbox".to_string(),
3122            success: true,
3123            content: "Test content".to_string(),
3124        };
3125
3126        let result = run_socket_function(socket_function, &app_context);
3127        assert!(result.is_ok());
3128
3129        let (_, messages) = result.unwrap();
3130        assert_eq!(messages.len(), 1);
3131        match &messages[0] {
3132            crate::Message::StreamUpdateMessage(stream_update) => {
3133                assert_eq!(stream_update.stream_id, "socket-test_muxbox");
3134                assert_eq!(stream_update.content_update, "Test content");
3135                match &stream_update.source_state {
3136                    crate::model::common::SourceState::Batch(state) => {
3137                        assert!(matches!(
3138                            state.status,
3139                            crate::model::common::BatchStatus::Completed
3140                        ));
3141                    }
3142                    _ => panic!("Expected Batch source state"),
3143                }
3144            }
3145            _ => panic!("Expected StreamUpdateMessage"),
3146        }
3147    }
3148
3149    /// Tests that run_socket_function() correctly handles SwitchActiveLayout.
3150    /// This test demonstrates socket function layout switching.
3151    #[test]
3152    fn test_run_socket_function_switch_active_layout() {
3153        let app_context = create_test_app_context();
3154        let socket_function = SocketFunction::SwitchActiveLayout {
3155            layout_id: "new_layout".to_string(),
3156        };
3157
3158        let result = run_socket_function(socket_function, &app_context);
3159        assert!(result.is_ok());
3160
3161        let (_, messages) = result.unwrap();
3162        assert_eq!(messages.len(), 1);
3163        match &messages[0] {
3164            crate::Message::SwitchActiveLayout(layout_id) => {
3165                assert_eq!(layout_id, "new_layout");
3166            }
3167            _ => panic!("Expected SwitchActiveLayout message"),
3168        }
3169    }
3170
3171    // === Cell Tests ===
3172
3173    /// Tests that Cell implements Clone and PartialEq correctly.
3174    /// This test demonstrates Cell trait implementations.
3175    #[test]
3176    fn test_cell_clone_and_eq() {
3177        let cell1 = Cell {
3178            fg_color: "red".to_string(),
3179            bg_color: "blue".to_string(),
3180            ch: 'X',
3181        };
3182        let cell2 = cell1.clone();
3183        assert_eq!(cell1, cell2);
3184
3185        let cell3 = Cell {
3186            fg_color: "green".to_string(),
3187            bg_color: "blue".to_string(),
3188            ch: 'X',
3189        };
3190        assert_ne!(cell1, cell3);
3191    }
3192
3193    /// Test send_json_to_socket function
3194    #[test]
3195    fn test_send_json_to_socket_function() {
3196        use std::os::unix::net::UnixListener;
3197        use std::thread;
3198        use std::time::Duration;
3199
3200        let socket_path = "/tmp/test_send_json.sock";
3201        let _ = std::fs::remove_file(socket_path);
3202
3203        // Start a simple test server
3204        let server_socket_path = socket_path.to_string();
3205        let server_handle = thread::spawn(move || {
3206            match UnixListener::bind(&server_socket_path) {
3207                Ok(listener) => {
3208                    // Set a timeout to prevent hanging
3209                    if let Some(Ok(mut stream)) = listener.incoming().next() {
3210                        let mut buffer = Vec::new();
3211                        let mut temp_buffer = [0; 1024];
3212
3213                        // Read data in chunks to avoid hanging on read_to_string
3214                        match stream.read(&mut temp_buffer) {
3215                            Ok(n) => {
3216                                buffer.extend_from_slice(&temp_buffer[..n]);
3217                                let _ = stream.write_all(b"Test Response");
3218                                String::from_utf8_lossy(&buffer).to_string()
3219                            }
3220                            Err(_) => String::new(),
3221                        }
3222                    } else {
3223                        String::new()
3224                    }
3225                }
3226                Err(_) => String::new(),
3227            }
3228        });
3229
3230        // Give server time to start
3231        thread::sleep(Duration::from_millis(100));
3232
3233        // Test send_json_to_socket
3234        let test_json = r#"{"test": "message"}"#;
3235        let result = send_json_to_socket(socket_path, test_json);
3236
3237        // The test is successful if either:
3238        // 1. The connection succeeds and we get the expected response
3239        // 2. The connection fails (which can happen in CI environments)
3240        match result {
3241            Ok(response) => {
3242                assert_eq!(response, "Test Response");
3243
3244                // Verify server received the correct message
3245                let received_message = server_handle.join().unwrap();
3246                assert_eq!(received_message, test_json);
3247            }
3248            Err(_) => {
3249                // Connection failed - this can happen in CI environments
3250                // The important thing is that the function doesn't panic
3251                let _ = server_handle.join();
3252            }
3253        }
3254
3255        // Clean up
3256        let _ = std::fs::remove_file(socket_path);
3257    }
3258}
3259
3260// F0203: Multi-Stream Input Tabs - Tab system data structures (uses StreamType defined above)
3261
3262// Duplicate StreamSource removed - using new trait-based system above