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#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq, Default)]
17pub enum ExecutionMode {
18 #[default]
20 Immediate,
21 Thread,
23 Pty,
25}
26
27impl ExecutionMode {
28 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 pub fn creates_streams(&self) -> bool {
39 true }
41
42 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 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 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 pub fn is_pty(&self) -> bool {
71 matches!(self, ExecutionMode::Pty)
72 }
73
74 pub fn from_legacy(thread: bool, pty: bool) -> Self {
77 match (thread, pty) {
78 (_, true) => ExecutionMode::Pty, (true, false) => ExecutionMode::Thread,
80 (false, false) => ExecutionMode::Immediate,
81 }
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Hash, Eq)]
89pub struct ExecuteScript {
90 pub script: Vec<String>, pub source: ExecutionSource, pub execution_mode: ExecutionMode, pub target_box_id: String, pub libs: Vec<String>, pub redirect_output: Option<String>, pub append_output: bool, pub stream_id: String, pub target_bounds: Option<Bounds>, }
100
101#[derive(Debug, Clone, PartialEq, Hash, Eq)]
103pub struct ExecutionSource {
104 pub source_type: SourceType, pub source_id: String, pub source_reference: SourceReference, }
108
109#[derive(Debug, Clone, PartialEq, Hash, Eq)]
110pub enum SourceType {
111 Choice(String), StaticScript, PeriodicRefresh, SocketUpdate, RedirectedScript, HotkeyScript, ScheduledScript, }
119
120#[derive(Debug, Clone, PartialEq, Hash, Eq)]
121pub enum SourceReference {
122 Choice(Choice), StaticConfig(String), PeriodicConfig(String), SocketCommand(String), HotkeyBinding(String), Schedule(String), }
129
130#[derive(Debug, Clone, PartialEq, Hash, Eq)]
132pub struct StreamUpdate {
133 pub stream_id: String, pub target_box_id: String, pub content_update: String, pub source_state: SourceState, pub execution_mode: ExecutionMode, }
139
140#[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, pub queue_wait_time: Duration, pub execution_time: Duration, pub exit_code: Option<i32>, pub status: BatchStatus, }
156
157#[derive(Debug, Clone, PartialEq, Hash, Eq)]
158pub struct ThreadSourceState {
159 pub thread_id: String, pub execution_time: Duration, pub exit_code: Option<i32>, pub status: ExecutionThreadStatus, }
164
165#[derive(Debug, Clone, PartialEq, Hash, Eq)]
166pub struct PtySourceState {
167 pub process_id: u32, pub runtime: Duration, pub exit_code: Option<i32>, pub status: ExecutionPtyStatus, }
172
173#[derive(Debug, Clone, PartialEq, Hash, Eq)]
174pub enum BatchStatus {
175 Queued, Executing, Completed, Failed(String), }
180
181#[derive(Debug, Clone, PartialEq, Hash, Eq)]
182pub enum ExecutionThreadStatus {
183 Running, Completed, Failed(String), }
187
188#[derive(Debug, Clone, PartialEq, Hash, Eq)]
189pub enum ExecutionPtyStatus {
190 Starting, Running, Completed, Failed(String), Terminated, }
196
197#[derive(Debug, Clone, PartialEq, Hash, Eq)]
199pub struct SourceAction {
200 pub action: ActionType, pub source_id: String, pub execution_mode: ExecutionMode, }
204
205#[derive(Debug, Clone, PartialEq, Hash, Eq)]
206pub enum ActionType {
207 Kill, Query, Pause, Resume, }
212
213impl SourceState {
214 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), PTY,
243 Plugin(String),
244 ChoiceExecution(String), RedirectSource(String), ExternalSocket, PtySession(String), OwnScript, }
251
252#[derive(Debug, Clone, PartialEq, Hash, Eq)]
254pub struct UnifiedExecutionSource {
255 pub source_id: String, pub stream_id: String, pub target_box_id: String, 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), PeriodicScript(String), ChoiceExecution {
268 choice_id: String,
270 script: Vec<String>,
271 redirect_output: Option<String>,
272 },
273 PtyProcess {
274 process_id: Option<u32>,
276 command: Vec<String>,
277 },
278 SocketUpdate {
279 command_type: String,
281 },
282 HotkeyScript {
283 hotkey: String,
285 script: Vec<String>,
286 },
287}
288
289#[derive(Debug, Clone, PartialEq, Hash, Eq)]
290pub enum ExecutionSourceStatus {
291 Pending, Running, Completed, Failed(String), Terminated, }
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>>, pub source: Option<StreamSource>,
307 #[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
316fn default_content_hash() -> u64 {
318 0
319}
320fn default_system_time() -> std::time::SystemTime {
321 std::time::SystemTime::now()
322}
323
324pub 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
336pub 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
345pub trait ImmediateSource: StreamSourceTrait {
350 fn get_execution_result(&self) -> Option<Result<String, String>>;
352 fn is_complete(&self) -> bool;
354 fn get_execution_duration(&self) -> Option<std::time::Duration>;
356}
357
358pub trait ThreadPoolSource: StreamSourceTrait {
360 fn get_thread_id(&self) -> Option<String>;
362 fn is_thread_running(&self) -> bool;
364 fn cancel_thread(&self) -> Result<(), String>;
366 fn get_thread_status(&self) -> ThreadStatus;
368 fn set_timeout(&mut self, timeout_seconds: u32);
370}
371
372#[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
383pub trait PtySessionSource: StreamSourceTrait {
385 fn get_process_id(&self) -> Option<u32>;
387 fn is_process_running(&self) -> bool;
389 fn send_input(&self, input: &str) -> Result<(), String>;
391 fn kill_process(&self) -> Result<(), String>;
393 fn resize_terminal(&self, rows: u16, cols: u16) -> Result<(), String>;
395 fn get_terminal_size(&self) -> (u16, u16);
397 fn get_command(&self) -> String;
399 fn get_working_directory(&self) -> Option<String>;
401}
402
403#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
405pub struct StaticContentSource {
406 pub content_type: String, 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#[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>, pub process_id: Option<u32>, pub execution_type: String, pub started_at: std::time::SystemTime,
440 pub timeout_seconds: Option<u32>,
441}
442
443#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
445pub struct PeriodicRefreshSource {
446 pub source_id: String, pub box_id: String, pub script: Vec<String>, pub stream_id: String, pub execution_mode: ExecutionMode, pub refresh_interval: u64, pub last_execution: Option<std::time::SystemTime>, pub created_at: std::time::SystemTime, pub execution_count: u64, }
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 let _ = std::process::Command::new("kill")
473 .arg("-9")
474 .arg(pid.to_string())
475 .output();
476 }
477 Ok(())
478 }
479 "threaded" => {
480 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 }
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#[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 }
561 fn cleanup(&self) -> Result<(), String> {
562 Ok(()) }
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#[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 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 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#[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(()) }
730 } else {
731 Ok(()) }
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 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 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#[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), }
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 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#[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, pub created_at: std::time::SystemTime,
898 pub source_process_id: Option<u32>, }
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#[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 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#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
989pub enum StreamSource {
990 StaticContent(StaticContentSource),
991 ChoiceExecution(ChoiceExecutionSource), PeriodicRefresh(PeriodicRefreshSource), PTY(PTYSource),
994 Redirect(RedirectSource),
995 Socket(SocketSource),
996 ImmediateExecution(ImmediateExecutionSource),
998 ThreadPoolExecution(ThreadPoolExecutionSource),
999 PtySessionExecution(PtySessionExecutionSource),
1000}
1001
1002impl 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 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 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 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 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 StreamSource::ImmediateExecution(s) => s.get_metadata(),
1075 StreamSource::ThreadPoolExecution(s) => s.get_metadata(),
1076 StreamSource::PtySessionExecution(s) => s.get_metadata(),
1077 }
1078 }
1079}
1080
1081impl StreamSource {
1085 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 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 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 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 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 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
1246impl Stream {
1248 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 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 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 pub fn update_content(&mut self, new_content: Vec<String>) {
1303 self.content = new_content;
1304 self.update_content_hash();
1305 }
1306
1307 pub fn update_choices(&mut self, new_choices: Option<Vec<Choice>>) {
1309 self.choices = new_choices;
1310 self.update_content_hash();
1311 }
1312
1313 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 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
1332impl 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
1370impl 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#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
1414pub struct FieldUpdate {
1415 pub entity_type: EntityType, pub entity_id: Option<String>, pub field_name: String, pub new_value: Value, }
1420
1421pub trait Updatable {
1423 fn generate_diff(&self, other: &Self) -> Vec<FieldUpdate>;
1425
1426 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, #[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, 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, 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 KillPtyProcess {
1526 box_id: String,
1527 },
1528 RestartPtyProcess {
1529 box_id: String,
1530 },
1531 QueryPtyStatus {
1533 box_id: String,
1534 },
1535 SpawnPtyProcess {
1537 box_id: String,
1538 script: Vec<String>,
1539 libs: Option<Vec<String>>,
1540 redirect_output: Option<String>,
1541 },
1542 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 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 SocketFunction::KillPtyProcess { box_id } => {
1609 if let Some(pty_manager) = &app_context.pty_manager {
1610 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 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 SocketFunction::SpawnPtyProcess {
1810 box_id,
1811 script,
1812 libs,
1813 redirect_output,
1814 } => {
1815 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 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, });
1842
1843 messages.push(execute_script_msg);
1845 log::info!(
1846 "PTY process spawn queued for execution via unified architecture: {}",
1847 box_id
1848 );
1849 }
1850 SocketFunction::SendPtyInput { box_id, input } => {
1852 if let Some(pty_manager) = &app_context.pty_manager {
1853 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 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 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 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 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 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 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#[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 self.x2.saturating_sub(self.x1).saturating_add(1)
2116 }
2117
2118 pub fn height(&self) -> usize {
2119 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; self.y2 = y + height - 1; }
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; self.y2 = y + height - 1; }
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; self.y1 = y - height + 1; }
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; self.y1 = y - height + 1; }
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; self.y2 = y + height - half_height - 1; }
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; self.y1 = y;
2282 self.y2 = y + height - 1; }
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; self.y2 = y;
2291 self.y1 = y - height + 1; }
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; self.y1 = y - half_height;
2300 self.y2 = y + height - half_height - 1; }
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; self.y1 = y - half_height;
2309 self.y2 = y + height - half_height - 1; }
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 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 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
2562pub 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 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 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 if let Ok(yaml_str) = serde_yaml::to_string(&value) {
2601 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 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 #[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 #[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 #[test]
2655 fn test_config_default() {
2656 let config = Config::default();
2657 assert_eq!(config.frame_delay, 30);
2658 }
2659
2660 #[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 #[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(); }
2684
2685 #[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 #[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 #[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 #[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 #[test]
2742 fn test_bounds_validate_valid() {
2743 let bounds = Bounds::new(10, 20, 100, 200);
2744 bounds.validate(); }
2746
2747 #[test]
2750 fn test_bounds_width() {
2751 let bounds = Bounds::new(10, 20, 100, 200);
2752 assert_eq!(bounds.width(), 91); }
2754
2755 #[test]
2758 fn test_bounds_height() {
2759 let bounds = Bounds::new(10, 20, 100, 200);
2760 assert_eq!(bounds.height(), 181); }
2762
2763 #[test]
2766 fn test_bounds_width_zero() {
2767 let bounds = Bounds::new(50, 20, 50, 200);
2768 assert_eq!(bounds.width(), 1); }
2770
2771 #[test]
2774 fn test_bounds_height_zero() {
2775 let bounds = Bounds::new(10, 50, 100, 50);
2776 assert_eq!(bounds.height(), 1); }
2778
2779 #[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)); assert!(!bounds.contains(100, 200)); assert!(!bounds.contains(5, 100)); assert!(!bounds.contains(150, 100)); assert!(!bounds.contains(50, 10)); assert!(!bounds.contains(50, 250)); }
2792
2793 #[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 #[test]
2808 fn test_bounds_intersects() {
2809 let bounds1 = Bounds::new(10, 20, 100, 200);
2810 let bounds2 = Bounds::new(50, 100, 150, 250); let bounds3 = Bounds::new(200, 300, 250, 350); assert!(bounds1.intersects(&bounds2));
2814 assert!(!bounds1.intersects(&bounds3));
2815 }
2816
2817 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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); assert_eq!(bounds.y2, 200); }
2995
2996 #[test]
3001 fn test_anchor_default() {
3002 let anchor = Anchor::default();
3003 assert_eq!(anchor, Anchor::Center);
3004 }
3005
3006 #[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 #[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 #[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 #[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 #[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); assert!(screen_buffer.get(10, 10).is_none());
3075 }
3076
3077 #[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 #[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 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 #[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 #[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 #[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]
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 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 if let Some(Ok(mut stream)) = listener.incoming().next() {
3210 let mut buffer = Vec::new();
3211 let mut temp_buffer = [0; 1024];
3212
3213 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 thread::sleep(Duration::from_millis(100));
3232
3233 let test_json = r#"{"test": "message"}"#;
3235 let result = send_json_to_socket(socket_path, test_json);
3236
3237 match result {
3241 Ok(response) => {
3242 assert_eq!(response, "Test Response");
3243
3244 let received_message = server_handle.join().unwrap();
3246 assert_eq!(received_message, test_json);
3247 }
3248 Err(_) => {
3249 let _ = server_handle.join();
3252 }
3253 }
3254
3255 let _ = std::fs::remove_file(socket_path);
3257 }
3258}
3259
3260