use serde::Deserializer;
use serde_json::Value;
use std::{collections::HashMap, error::Error, hash::Hash};
use crate::{
color_utils::{get_bg_color, get_fg_color},
model::choice::Choice,
screen_bounds, screen_height, screen_width,
utils::input_bounds_to_bounds,
AppContext, AppGraph, Layout, Message, MuxBox,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq, Default)]
pub enum ExecutionMode {
#[default]
Immediate,
Thread,
Pty,
}
impl ExecutionMode {
pub fn description(&self) -> &'static str {
match self {
ExecutionMode::Immediate => "Synchronous execution on UI thread",
ExecutionMode::Thread => "Background execution in thread pool",
ExecutionMode::Pty => "Real-time PTY execution with continuous output",
}
}
pub fn creates_streams(&self) -> bool {
true }
pub fn is_realtime(&self) -> bool {
match self {
ExecutionMode::Immediate => false,
ExecutionMode::Thread => false,
ExecutionMode::Pty => true,
}
}
pub fn is_background(&self) -> bool {
match self {
ExecutionMode::Immediate => false,
ExecutionMode::Thread => true,
ExecutionMode::Pty => true,
}
}
pub fn as_stream_suffix(&self) -> &'static str {
match self {
ExecutionMode::Immediate => "immediate",
ExecutionMode::Thread => "thread",
ExecutionMode::Pty => "pty",
}
}
pub fn is_pty(&self) -> bool {
matches!(self, ExecutionMode::Pty)
}
pub fn from_legacy(thread: bool, pty: bool) -> Self {
match (thread, pty) {
(_, true) => ExecutionMode::Pty, (true, false) => ExecutionMode::Thread,
(false, false) => ExecutionMode::Immediate,
}
}
}
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub struct ExecuteScript {
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>, }
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub struct ExecutionSource {
pub source_type: SourceType, pub source_id: String, pub source_reference: SourceReference, }
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub enum SourceType {
Choice(String), StaticScript, PeriodicRefresh, SocketUpdate, RedirectedScript, HotkeyScript, ScheduledScript, }
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub enum SourceReference {
Choice(Choice), StaticConfig(String), PeriodicConfig(String), SocketCommand(String), HotkeyBinding(String), Schedule(String), }
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub struct StreamUpdate {
pub stream_id: String, pub target_box_id: String, pub content_update: String, pub source_state: SourceState, pub execution_mode: ExecutionMode, }
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub enum SourceState {
Batch(BatchSourceState),
Thread(ThreadSourceState),
Pty(PtySourceState),
}
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub struct BatchSourceState {
pub task_id: String, pub queue_wait_time: Duration, pub execution_time: Duration, pub exit_code: Option<i32>, pub status: BatchStatus, }
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub struct ThreadSourceState {
pub thread_id: String, pub execution_time: Duration, pub exit_code: Option<i32>, pub status: ExecutionThreadStatus, }
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub struct PtySourceState {
pub process_id: u32, pub runtime: Duration, pub exit_code: Option<i32>, pub status: ExecutionPtyStatus, }
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub enum BatchStatus {
Queued, Executing, Completed, Failed(String), }
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub enum ExecutionThreadStatus {
Running, Completed, Failed(String), }
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub enum ExecutionPtyStatus {
Starting, Running, Completed, Failed(String), Terminated, }
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub struct SourceAction {
pub action: ActionType, pub source_id: String, pub execution_mode: ExecutionMode, }
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub enum ActionType {
Kill, Query, Pause, Resume, }
impl SourceState {
pub fn expects_more_updates(&self) -> bool {
match self {
SourceState::Batch(state) => {
matches!(state.status, BatchStatus::Queued | BatchStatus::Executing)
}
SourceState::Thread(state) => matches!(state.status, ExecutionThreadStatus::Running),
SourceState::Pty(state) => matches!(
state.status,
ExecutionPtyStatus::Starting | ExecutionPtyStatus::Running
),
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
pub enum EntityType {
AppContext,
App,
Layout,
MuxBox,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
pub enum StreamType {
Content,
Choices,
RedirectedOutput(String), PTY,
Plugin(String),
ChoiceExecution(String), RedirectSource(String), ExternalSocket, PtySession(String), OwnScript, }
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub struct UnifiedExecutionSource {
pub source_id: String, pub stream_id: String, pub target_box_id: String, pub source_type: ExecutionSourceType,
pub created_at: std::time::SystemTime,
pub status: ExecutionSourceStatus,
}
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub enum ExecutionSourceType {
StaticContent(String), PeriodicScript(String), ChoiceExecution {
choice_id: String,
script: Vec<String>,
redirect_output: Option<String>,
},
PtyProcess {
process_id: Option<u32>,
command: Vec<String>,
},
SocketUpdate {
command_type: String,
},
HotkeyScript {
hotkey: String,
script: Vec<String>,
},
}
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub enum ExecutionSourceStatus {
Pending, Running, Completed, Failed(String), Terminated, }
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
pub struct Stream {
pub id: String,
pub stream_type: StreamType,
pub label: String,
pub content: Vec<String>,
pub choices: Option<Vec<Choice>>, pub source: Option<StreamSource>,
#[serde(skip, default = "default_content_hash")]
pub content_hash: u64,
#[serde(skip, default = "default_system_time")]
pub last_updated: std::time::SystemTime,
#[serde(skip, default = "default_system_time")]
pub created_at: std::time::SystemTime,
}
fn default_content_hash() -> u64 {
0
}
fn default_system_time() -> std::time::SystemTime {
std::time::SystemTime::now()
}
pub trait ContentStreamTrait {
fn get_content_lines(&self) -> &Vec<String>;
fn set_content_lines(&mut self, content: Vec<String>);
}
pub trait ChoicesStreamTrait {
fn get_choices(&self) -> &Vec<Choice>;
fn get_choices_mut(&mut self) -> &mut Vec<Choice>;
fn set_choices(&mut self, choices: Vec<Choice>);
}
pub trait StreamSourceTrait {
fn source_type(&self) -> &'static str;
fn source_id(&self) -> String;
fn can_terminate(&self) -> bool;
fn cleanup(&self) -> Result<(), String>;
fn get_metadata(&self) -> std::collections::HashMap<String, String>;
}
pub trait ImmediateSource: StreamSourceTrait {
fn get_execution_result(&self) -> Option<Result<String, String>>;
fn is_complete(&self) -> bool;
fn get_execution_duration(&self) -> Option<std::time::Duration>;
}
pub trait ThreadPoolSource: StreamSourceTrait {
fn get_thread_id(&self) -> Option<String>;
fn is_thread_running(&self) -> bool;
fn cancel_thread(&self) -> Result<(), String>;
fn get_thread_status(&self) -> ThreadStatus;
fn set_timeout(&mut self, timeout_seconds: u32);
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash)]
pub enum ThreadStatus {
NotStarted,
Running,
Completed,
Failed,
Cancelled,
TimedOut,
}
pub trait PtySessionSource: StreamSourceTrait {
fn get_process_id(&self) -> Option<u32>;
fn is_process_running(&self) -> bool;
fn send_input(&self, input: &str) -> Result<(), String>;
fn kill_process(&self) -> Result<(), String>;
fn resize_terminal(&self, rows: u16, cols: u16) -> Result<(), String>;
fn get_terminal_size(&self) -> (u16, u16);
fn get_command(&self) -> String;
fn get_working_directory(&self) -> Option<String>;
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
pub struct StaticContentSource {
pub content_type: String, pub created_at: std::time::SystemTime,
}
impl StreamSourceTrait for StaticContentSource {
fn source_type(&self) -> &'static str {
"static_content"
}
fn source_id(&self) -> String {
format!("static_{}", self.content_type)
}
fn can_terminate(&self) -> bool {
false
}
fn cleanup(&self) -> Result<(), String> {
Ok(())
}
fn get_metadata(&self) -> std::collections::HashMap<String, String> {
let mut meta = std::collections::HashMap::new();
meta.insert("content_type".to_string(), self.content_type.clone());
meta.insert("created_at".to_string(), format!("{:?}", self.created_at));
meta
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
pub struct ChoiceExecutionSource {
pub choice_id: String,
pub muxbox_id: String,
pub thread_id: Option<String>, pub process_id: Option<u32>, pub execution_type: String, pub started_at: std::time::SystemTime,
pub timeout_seconds: Option<u32>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
pub struct PeriodicRefreshSource {
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, }
impl StreamSourceTrait for ChoiceExecutionSource {
fn source_type(&self) -> &'static str {
"choice_execution"
}
fn source_id(&self) -> String {
self.choice_id.clone()
}
fn can_terminate(&self) -> bool {
true
}
fn cleanup(&self) -> Result<(), String> {
match self.execution_type.as_str() {
"process" => {
if let Some(pid) = self.process_id {
let _ = std::process::Command::new("kill")
.arg("-9")
.arg(pid.to_string())
.output();
}
Ok(())
}
"threaded" => {
Ok(())
}
_ => Ok(()),
}
}
fn get_metadata(&self) -> std::collections::HashMap<String, String> {
let mut meta = std::collections::HashMap::new();
meta.insert("choice_id".to_string(), self.choice_id.clone());
meta.insert("muxbox_id".to_string(), self.muxbox_id.clone());
meta.insert("execution_type".to_string(), self.execution_type.clone());
if let Some(pid) = self.process_id {
meta.insert("process_id".to_string(), pid.to_string());
}
if let Some(thread_id) = &self.thread_id {
meta.insert("thread_id".to_string(), thread_id.clone());
}
meta
}
}
impl StreamSourceTrait for PeriodicRefreshSource {
fn source_type(&self) -> &'static str {
"periodic_refresh"
}
fn source_id(&self) -> String {
self.source_id.clone()
}
fn can_terminate(&self) -> bool {
true }
fn cleanup(&self) -> Result<(), String> {
log::info!("Cleaning up periodic refresh source: {}", self.source_id);
Ok(())
}
fn get_metadata(&self) -> std::collections::HashMap<String, String> {
let mut metadata = std::collections::HashMap::new();
metadata.insert("source_id".to_string(), self.source_id.clone());
metadata.insert("box_id".to_string(), self.box_id.clone());
metadata.insert("stream_id".to_string(), self.stream_id.clone());
metadata.insert(
"execution_mode".to_string(),
format!("{:?}", self.execution_mode),
);
metadata.insert(
"refresh_interval".to_string(),
self.refresh_interval.to_string(),
);
metadata.insert(
"execution_count".to_string(),
self.execution_count.to_string(),
);
metadata
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
pub struct ImmediateExecutionSource {
pub choice_id: String,
pub muxbox_id: String,
pub script: Vec<String>,
pub started_at: std::time::SystemTime,
pub completed_at: Option<std::time::SystemTime>,
pub execution_result: Option<Result<String, String>>,
pub execution_duration: Option<std::time::Duration>,
}
impl StreamSourceTrait for ImmediateExecutionSource {
fn source_type(&self) -> &'static str {
"immediate_execution"
}
fn source_id(&self) -> String {
format!("immediate_{}", self.choice_id)
}
fn can_terminate(&self) -> bool {
false }
fn cleanup(&self) -> Result<(), String> {
Ok(()) }
fn get_metadata(&self) -> std::collections::HashMap<String, String> {
let mut meta = std::collections::HashMap::new();
meta.insert("choice_id".to_string(), self.choice_id.clone());
meta.insert("muxbox_id".to_string(), self.muxbox_id.clone());
meta.insert("script_lines".to_string(), self.script.len().to_string());
meta.insert("started_at".to_string(), format!("{:?}", self.started_at));
if let Some(completed_at) = self.completed_at {
meta.insert("completed_at".to_string(), format!("{:?}", completed_at));
}
if let Some(duration) = self.execution_duration {
meta.insert("duration_ms".to_string(), duration.as_millis().to_string());
}
meta.insert("is_complete".to_string(), self.is_complete().to_string());
meta
}
}
impl ImmediateSource for ImmediateExecutionSource {
fn get_execution_result(&self) -> Option<Result<String, String>> {
self.execution_result.clone()
}
fn is_complete(&self) -> bool {
self.execution_result.is_some()
}
fn get_execution_duration(&self) -> Option<std::time::Duration> {
self.execution_duration
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
pub struct ThreadPoolExecutionSource {
pub choice_id: String,
pub muxbox_id: String,
pub script: Vec<String>,
pub thread_id: Option<String>,
pub started_at: std::time::SystemTime,
pub timeout_seconds: Option<u32>,
pub thread_status: ThreadStatus,
pub completion_result: Option<Result<String, String>>,
pub execution_duration: Option<std::time::Duration>,
}
impl StreamSourceTrait for ThreadPoolExecutionSource {
fn source_type(&self) -> &'static str {
"thread_pool_execution"
}
fn source_id(&self) -> String {
format!("thread_{}", self.choice_id)
}
fn can_terminate(&self) -> bool {
matches!(
self.thread_status,
ThreadStatus::Running | ThreadStatus::NotStarted
)
}
fn cleanup(&self) -> Result<(), String> {
if let Some(thread_id) = &self.thread_id {
log::info!("Cleanup requested for thread pool execution: {}", thread_id);
Ok(())
} else {
Ok(())
}
}
fn get_metadata(&self) -> std::collections::HashMap<String, String> {
let mut meta = std::collections::HashMap::new();
meta.insert("choice_id".to_string(), self.choice_id.clone());
meta.insert("muxbox_id".to_string(), self.muxbox_id.clone());
meta.insert("script_lines".to_string(), self.script.len().to_string());
meta.insert(
"thread_status".to_string(),
format!("{:?}", self.thread_status),
);
meta.insert("started_at".to_string(), format!("{:?}", self.started_at));
if let Some(thread_id) = &self.thread_id {
meta.insert("thread_id".to_string(), thread_id.clone());
}
if let Some(timeout) = self.timeout_seconds {
meta.insert("timeout_seconds".to_string(), timeout.to_string());
}
if let Some(duration) = self.execution_duration {
meta.insert("duration_ms".to_string(), duration.as_millis().to_string());
}
meta
}
}
impl ThreadPoolSource for ThreadPoolExecutionSource {
fn get_thread_id(&self) -> Option<String> {
self.thread_id.clone()
}
fn is_thread_running(&self) -> bool {
matches!(self.thread_status, ThreadStatus::Running)
}
fn cancel_thread(&self) -> Result<(), String> {
if matches!(
self.thread_status,
ThreadStatus::Running | ThreadStatus::NotStarted
) {
log::warn!(
"Thread cancellation requested for choice: {}",
self.choice_id
);
Ok(())
} else {
Err(format!(
"Cannot cancel thread in status: {:?}",
self.thread_status
))
}
}
fn get_thread_status(&self) -> ThreadStatus {
self.thread_status.clone()
}
fn set_timeout(&mut self, timeout_seconds: u32) {
self.timeout_seconds = Some(timeout_seconds);
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
pub struct PtySessionExecutionSource {
pub choice_id: String,
pub muxbox_id: String,
pub command: String,
pub args: Vec<String>,
pub working_dir: Option<String>,
pub process_id: Option<u32>,
pub terminal_size: (u16, u16),
pub started_at: std::time::SystemTime,
pub reader_thread_id: Option<String>,
pub is_process_running: bool,
}
impl StreamSourceTrait for PtySessionExecutionSource {
fn source_type(&self) -> &'static str {
"pty_session_execution"
}
fn source_id(&self) -> String {
format!("pty_{}", self.choice_id)
}
fn can_terminate(&self) -> bool {
self.process_id.is_some() && self.is_process_running
}
fn cleanup(&self) -> Result<(), String> {
if let Some(pid) = self.process_id {
if self.is_process_running {
let result = std::process::Command::new("kill")
.arg("-9")
.arg(pid.to_string())
.output();
match result {
Ok(_) => Ok(()),
Err(e) => Err(format!("Failed to terminate PTY process {}: {}", pid, e)),
}
} else {
Ok(()) }
} else {
Ok(()) }
}
fn get_metadata(&self) -> std::collections::HashMap<String, String> {
let mut meta = std::collections::HashMap::new();
meta.insert("choice_id".to_string(), self.choice_id.clone());
meta.insert("muxbox_id".to_string(), self.muxbox_id.clone());
meta.insert("command".to_string(), self.command.clone());
meta.insert("args".to_string(), self.args.join(" "));
meta.insert(
"is_running".to_string(),
self.is_process_running.to_string(),
);
meta.insert("started_at".to_string(), format!("{:?}", self.started_at));
meta.insert(
"terminal_size".to_string(),
format!("{}x{}", self.terminal_size.0, self.terminal_size.1),
);
if let Some(pid) = self.process_id {
meta.insert("process_id".to_string(), pid.to_string());
}
if let Some(thread_id) = &self.reader_thread_id {
meta.insert("reader_thread_id".to_string(), thread_id.clone());
}
if let Some(wd) = &self.working_dir {
meta.insert("working_dir".to_string(), wd.clone());
}
meta
}
}
impl PtySessionSource for PtySessionExecutionSource {
fn get_process_id(&self) -> Option<u32> {
self.process_id
}
fn is_process_running(&self) -> bool {
self.is_process_running
}
fn send_input(&self, input: &str) -> Result<(), String> {
if self.process_id.is_some() && self.is_process_running {
log::info!(
"PTY input requested for choice {}: {}",
self.choice_id,
input
);
Ok(())
} else {
Err("PTY process is not running".to_string())
}
}
fn kill_process(&self) -> Result<(), String> {
if let Some(pid) = self.process_id {
if self.is_process_running {
let result = std::process::Command::new("kill")
.arg("-TERM")
.arg(pid.to_string())
.output();
match result {
Ok(_) => Ok(()),
Err(e) => Err(format!("Failed to kill PTY process {}: {}", pid, e)),
}
} else {
Err("Process is not running".to_string())
}
} else {
Err("No process ID available".to_string())
}
}
fn resize_terminal(&self, rows: u16, cols: u16) -> Result<(), String> {
if self.process_id.is_some() && self.is_process_running {
log::info!(
"PTY resize requested for choice {}: {}x{}",
self.choice_id,
rows,
cols
);
Ok(())
} else {
Err("PTY process is not running".to_string())
}
}
fn get_terminal_size(&self) -> (u16, u16) {
self.terminal_size
}
fn get_command(&self) -> String {
if self.args.is_empty() {
self.command.clone()
} else {
format!("{} {}", self.command, self.args.join(" "))
}
}
fn get_working_directory(&self) -> Option<String> {
self.working_dir.clone()
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
pub struct PTYSource {
pub pty_id: String,
pub process_id: u32,
pub command: String,
pub args: Vec<String>,
pub working_dir: Option<String>,
pub reader_thread_id: Option<String>,
pub started_at: std::time::SystemTime,
pub terminal_size: (u16, u16), }
impl StreamSourceTrait for PTYSource {
fn source_type(&self) -> &'static str {
"pty"
}
fn source_id(&self) -> String {
self.pty_id.clone()
}
fn can_terminate(&self) -> bool {
true
}
fn cleanup(&self) -> Result<(), String> {
let result = std::process::Command::new("kill")
.arg("-9")
.arg(self.process_id.to_string())
.output();
match result {
Ok(_) => Ok(()),
Err(e) => Err(format!(
"Failed to terminate PTY process {}: {}",
self.process_id, e
)),
}
}
fn get_metadata(&self) -> std::collections::HashMap<String, String> {
let mut meta = std::collections::HashMap::new();
meta.insert("pty_id".to_string(), self.pty_id.clone());
meta.insert("process_id".to_string(), self.process_id.to_string());
meta.insert("command".to_string(), self.command.clone());
meta.insert("args".to_string(), self.args.join(" "));
meta.insert(
"terminal_size".to_string(),
format!("{}x{}", self.terminal_size.0, self.terminal_size.1),
);
if let Some(ref thread_id) = self.reader_thread_id {
meta.insert("reader_thread_id".to_string(), thread_id.clone());
}
meta
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
pub struct RedirectSource {
pub source_muxbox_id: String,
pub source_choice_id: Option<String>,
pub redirect_name: String,
pub redirect_type: String, pub created_at: std::time::SystemTime,
pub source_process_id: Option<u32>, }
impl StreamSourceTrait for RedirectSource {
fn source_type(&self) -> &'static str {
"redirect"
}
fn source_id(&self) -> String {
format!("{}_{}", self.source_muxbox_id, self.redirect_name)
}
fn can_terminate(&self) -> bool {
self.source_process_id.is_some()
}
fn cleanup(&self) -> Result<(), String> {
if let Some(pid) = self.source_process_id {
let result = std::process::Command::new("kill")
.arg("-9")
.arg(pid.to_string())
.output();
match result {
Ok(_) => Ok(()),
Err(e) => Err(format!(
"Failed to terminate redirect source process {}: {}",
pid, e
)),
}
} else {
Ok(())
}
}
fn get_metadata(&self) -> std::collections::HashMap<String, String> {
let mut meta = std::collections::HashMap::new();
meta.insert(
"source_muxbox_id".to_string(),
self.source_muxbox_id.clone(),
);
meta.insert("redirect_name".to_string(), self.redirect_name.clone());
meta.insert("redirect_type".to_string(), self.redirect_type.clone());
if let Some(ref choice_id) = self.source_choice_id {
meta.insert("source_choice_id".to_string(), choice_id.clone());
}
if let Some(pid) = self.source_process_id {
meta.insert("source_process_id".to_string(), pid.to_string());
}
meta
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
pub struct SocketSource {
pub connection_id: String,
pub socket_path: Option<String>,
pub client_info: String,
pub protocol_version: String,
pub connected_at: std::time::SystemTime,
pub last_activity: std::time::SystemTime,
}
impl StreamSourceTrait for SocketSource {
fn source_type(&self) -> &'static str {
"socket"
}
fn source_id(&self) -> String {
self.connection_id.clone()
}
fn can_terminate(&self) -> bool {
true
}
fn cleanup(&self) -> Result<(), String> {
Ok(())
}
fn get_metadata(&self) -> std::collections::HashMap<String, String> {
let mut meta = std::collections::HashMap::new();
meta.insert("connection_id".to_string(), self.connection_id.clone());
meta.insert("client_info".to_string(), self.client_info.clone());
meta.insert(
"protocol_version".to_string(),
self.protocol_version.clone(),
);
if let Some(ref socket_path) = self.socket_path {
meta.insert("socket_path".to_string(), socket_path.clone());
}
meta
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
pub enum StreamSource {
StaticContent(StaticContentSource),
ChoiceExecution(ChoiceExecutionSource), PeriodicRefresh(PeriodicRefreshSource), PTY(PTYSource),
Redirect(RedirectSource),
Socket(SocketSource),
ImmediateExecution(ImmediateExecutionSource),
ThreadPoolExecution(ThreadPoolExecutionSource),
PtySessionExecution(PtySessionExecutionSource),
}
impl StreamSourceTrait for StreamSource {
fn source_type(&self) -> &'static str {
match self {
StreamSource::StaticContent(s) => s.source_type(),
StreamSource::ChoiceExecution(s) => s.source_type(),
StreamSource::PeriodicRefresh(s) => s.source_type(),
StreamSource::PTY(s) => s.source_type(),
StreamSource::Redirect(s) => s.source_type(),
StreamSource::Socket(s) => s.source_type(),
StreamSource::ImmediateExecution(s) => s.source_type(),
StreamSource::ThreadPoolExecution(s) => s.source_type(),
StreamSource::PtySessionExecution(s) => s.source_type(),
}
}
fn source_id(&self) -> String {
match self {
StreamSource::StaticContent(s) => s.source_id(),
StreamSource::ChoiceExecution(s) => s.source_id(),
StreamSource::PeriodicRefresh(s) => s.source_id(),
StreamSource::PTY(s) => s.source_id(),
StreamSource::Redirect(s) => s.source_id(),
StreamSource::Socket(s) => s.source_id(),
StreamSource::ImmediateExecution(s) => s.source_id(),
StreamSource::ThreadPoolExecution(s) => s.source_id(),
StreamSource::PtySessionExecution(s) => s.source_id(),
}
}
fn can_terminate(&self) -> bool {
match self {
StreamSource::StaticContent(s) => s.can_terminate(),
StreamSource::ChoiceExecution(s) => s.can_terminate(),
StreamSource::PeriodicRefresh(s) => s.can_terminate(),
StreamSource::PTY(s) => s.can_terminate(),
StreamSource::Redirect(s) => s.can_terminate(),
StreamSource::Socket(s) => s.can_terminate(),
StreamSource::ImmediateExecution(s) => s.can_terminate(),
StreamSource::ThreadPoolExecution(s) => s.can_terminate(),
StreamSource::PtySessionExecution(s) => s.can_terminate(),
}
}
fn cleanup(&self) -> Result<(), String> {
match self {
StreamSource::StaticContent(s) => s.cleanup(),
StreamSource::ChoiceExecution(s) => s.cleanup(),
StreamSource::PeriodicRefresh(s) => s.cleanup(),
StreamSource::PTY(s) => s.cleanup(),
StreamSource::Redirect(s) => s.cleanup(),
StreamSource::Socket(s) => s.cleanup(),
StreamSource::ImmediateExecution(s) => s.cleanup(),
StreamSource::ThreadPoolExecution(s) => s.cleanup(),
StreamSource::PtySessionExecution(s) => s.cleanup(),
}
}
fn get_metadata(&self) -> std::collections::HashMap<String, String> {
match self {
StreamSource::StaticContent(s) => s.get_metadata(),
StreamSource::ChoiceExecution(s) => s.get_metadata(),
StreamSource::PeriodicRefresh(s) => s.get_metadata(),
StreamSource::PTY(s) => s.get_metadata(),
StreamSource::Redirect(s) => s.get_metadata(),
StreamSource::Socket(s) => s.get_metadata(),
StreamSource::ImmediateExecution(s) => s.get_metadata(),
StreamSource::ThreadPoolExecution(s) => s.get_metadata(),
StreamSource::PtySessionExecution(s) => s.get_metadata(),
}
}
}
impl StreamSource {
pub fn create_immediate_execution_source(
choice_id: String,
muxbox_id: String,
script: Vec<String>,
) -> Self {
StreamSource::ImmediateExecution(ImmediateExecutionSource {
choice_id,
muxbox_id,
script,
started_at: std::time::SystemTime::now(),
completed_at: None,
execution_result: None,
execution_duration: None,
})
}
pub fn create_thread_pool_execution_source(
choice_id: String,
muxbox_id: String,
script: Vec<String>,
thread_id: Option<String>,
timeout_seconds: Option<u32>,
) -> Self {
StreamSource::ThreadPoolExecution(ThreadPoolExecutionSource {
choice_id,
muxbox_id,
script,
thread_id,
started_at: std::time::SystemTime::now(),
timeout_seconds,
thread_status: ThreadStatus::NotStarted,
completion_result: None,
execution_duration: None,
})
}
pub fn create_pty_session_execution_source(
choice_id: String,
muxbox_id: String,
command: String,
args: Vec<String>,
working_dir: Option<String>,
terminal_size: (u16, u16),
) -> Self {
StreamSource::PtySessionExecution(PtySessionExecutionSource {
choice_id,
muxbox_id,
command,
args,
working_dir,
process_id: None,
terminal_size,
started_at: std::time::SystemTime::now(),
reader_thread_id: None,
is_process_running: false,
})
}
pub fn from_execution_mode(
execution_mode: &ExecutionMode,
choice_id: String,
muxbox_id: String,
script: Vec<String>,
additional_params: Option<std::collections::HashMap<String, String>>,
) -> Self {
match execution_mode {
ExecutionMode::Immediate => {
Self::create_immediate_execution_source(choice_id, muxbox_id, script)
}
ExecutionMode::Thread => {
let thread_id = additional_params
.as_ref()
.and_then(|p| p.get("thread_id"))
.cloned();
let timeout_seconds = additional_params
.as_ref()
.and_then(|p| p.get("timeout_seconds"))
.and_then(|s| s.parse().ok());
Self::create_thread_pool_execution_source(
choice_id,
muxbox_id,
script,
thread_id,
timeout_seconds,
)
}
ExecutionMode::Pty => {
let command = if script.is_empty() {
"sh".to_string()
} else {
script[0].clone()
};
let args = if script.len() > 1 {
script[1..].to_vec()
} else {
vec![]
};
let working_dir = additional_params
.as_ref()
.and_then(|p| p.get("working_dir"))
.cloned();
let terminal_size = additional_params
.as_ref()
.and_then(|p| {
let rows = p.get("terminal_rows")?.parse().ok()?;
let cols = p.get("terminal_cols")?.parse().ok()?;
Some((rows, cols))
})
.unwrap_or((24, 80));
Self::create_pty_session_execution_source(
choice_id,
muxbox_id,
command,
args,
working_dir,
terminal_size,
)
}
}
}
pub fn supports_immediate_source(&self) -> bool {
matches!(self, StreamSource::ImmediateExecution(_))
}
pub fn supports_thread_pool_source(&self) -> bool {
matches!(self, StreamSource::ThreadPoolExecution(_))
}
pub fn supports_pty_session_source(&self) -> bool {
matches!(self, StreamSource::PtySessionExecution(_))
}
pub fn as_immediate_source(&self) -> Option<&dyn ImmediateSource> {
match self {
StreamSource::ImmediateExecution(source) => Some(source),
_ => None,
}
}
pub fn as_thread_pool_source(&self) -> Option<&dyn ThreadPoolSource> {
match self {
StreamSource::ThreadPoolExecution(source) => Some(source),
_ => None,
}
}
pub fn as_pty_session_source(&self) -> Option<&dyn PtySessionSource> {
match self {
StreamSource::PtySessionExecution(source) => Some(source),
_ => None,
}
}
}
impl Stream {
pub fn new(
id: String,
stream_type: StreamType,
label: String,
content: Vec<String>,
choices: Option<Vec<Choice>>,
source: Option<StreamSource>,
) -> Self {
let now = std::time::SystemTime::now();
let mut stream = Self {
id,
stream_type,
label,
content,
choices,
source,
content_hash: 0,
last_updated: now,
created_at: now,
};
stream.update_content_hash();
stream
}
pub fn update_content_hash(&mut self) {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
self.content.hash(&mut hasher);
if let Some(ref choices) = self.choices {
choices.hash(&mut hasher);
}
self.content_hash = hasher.finish();
self.last_updated = std::time::SystemTime::now();
}
pub fn has_content_changed(&self) -> bool {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
self.content.hash(&mut hasher);
if let Some(ref choices) = self.choices {
choices.hash(&mut hasher);
}
let current_hash = hasher.finish();
current_hash != self.content_hash
}
pub fn update_content(&mut self, new_content: Vec<String>) {
self.content = new_content;
self.update_content_hash();
}
pub fn update_choices(&mut self, new_choices: Option<Vec<Choice>>) {
self.choices = new_choices;
self.update_content_hash();
}
pub fn time_since_last_update(
&self,
) -> Result<std::time::Duration, std::time::SystemTimeError> {
std::time::SystemTime::now().duration_since(self.last_updated)
}
pub fn is_closeable(&self) -> bool {
matches!(
&self.stream_type,
StreamType::RedirectedOutput(_)
| StreamType::ChoiceExecution(_)
| StreamType::PtySession(_)
| StreamType::ExternalSocket
)
}
}
impl ContentStreamTrait for Stream {
fn get_content_lines(&self) -> &Vec<String> {
match self.stream_type {
StreamType::Content
| StreamType::RedirectedOutput(_)
| StreamType::PTY
| StreamType::Plugin(_)
| StreamType::ChoiceExecution(_)
| StreamType::PtySession(_)
| StreamType::OwnScript => &self.content,
_ => panic!(
"ContentStreamTrait called on non-content stream: {:?}",
self.stream_type
),
}
}
fn set_content_lines(&mut self, content: Vec<String>) {
match self.stream_type {
StreamType::Content
| StreamType::RedirectedOutput(_)
| StreamType::PTY
| StreamType::Plugin(_)
| StreamType::ChoiceExecution(_)
| StreamType::PtySession(_)
| StreamType::OwnScript => {
self.content = content;
self.update_content_hash();
}
_ => panic!(
"ContentStreamTrait called on non-content stream: {:?}",
self.stream_type
),
}
}
}
impl ChoicesStreamTrait for Stream {
fn get_choices(&self) -> &Vec<Choice> {
match self.stream_type {
StreamType::Choices => self
.choices
.as_ref()
.expect("Choices stream must have choices"),
_ => panic!(
"ChoicesStreamTrait called on non-choices stream: {:?}",
self.stream_type
),
}
}
fn get_choices_mut(&mut self) -> &mut Vec<Choice> {
match self.stream_type {
StreamType::Choices => self
.choices
.as_mut()
.expect("Choices stream must have choices"),
_ => panic!(
"ChoicesStreamTrait called on non-choices stream: {:?}",
self.stream_type
),
}
}
fn set_choices(&mut self, choices: Vec<Choice>) {
match self.stream_type {
StreamType::Choices => {
self.choices = Some(choices);
self.update_content_hash();
}
_ => panic!(
"ChoicesStreamTrait called on non-choices stream: {:?}",
self.stream_type
),
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
pub struct FieldUpdate {
pub entity_type: EntityType, pub entity_id: Option<String>, pub field_name: String, pub new_value: Value, }
pub trait Updatable {
fn generate_diff(&self, other: &Self) -> Vec<FieldUpdate>;
fn apply_updates(&mut self, updates: Vec<FieldUpdate>);
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct Config {
pub frame_delay: u64,
pub locked: bool, #[serde(default)]
pub calibrate: bool,
}
impl Hash for Config {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.frame_delay.hash(state);
self.locked.hash(state);
self.calibrate.hash(state);
}
}
impl Default for Config {
fn default() -> Self {
Config {
frame_delay: 30,
locked: false, calibrate: false,
}
}
}
impl Config {
pub fn new(frame_delay: u64) -> Self {
let result = Config {
frame_delay,
locked: false, calibrate: false,
};
result.validate();
result
}
pub fn new_with_lock(frame_delay: u64, locked: bool) -> Self {
let result = Config {
frame_delay,
locked,
calibrate: false,
};
result.validate();
result
}
pub fn new_with_lock_and_calibration(frame_delay: u64, locked: bool, calibrate: bool) -> Self {
let result = Config {
frame_delay,
locked,
calibrate,
};
result.validate();
result
}
pub fn validate(&self) {
if self.frame_delay == 0 {
panic!("Validation error: frame_delay cannot be 0");
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
pub enum SocketFunction {
ReplaceBoxContent {
box_id: String,
success: bool,
content: String,
},
ReplaceBoxScript {
box_id: String,
script: Vec<String>,
},
StopBoxRefresh {
box_id: String,
},
StartBoxRefresh {
box_id: String,
},
ReplaceBox {
box_id: String,
new_box: MuxBox,
},
SwitchActiveLayout {
layout_id: String,
},
AddBox {
layout_id: String,
muxbox: MuxBox,
},
RemoveBox {
box_id: String,
},
KillPtyProcess {
box_id: String,
},
RestartPtyProcess {
box_id: String,
},
QueryPtyStatus {
box_id: String,
},
SpawnPtyProcess {
box_id: String,
script: Vec<String>,
libs: Option<Vec<String>>,
redirect_output: Option<String>,
},
SendPtyInput {
box_id: String,
input: String,
},
}
pub fn run_socket_function(
socket_function: SocketFunction,
app_context: &AppContext,
) -> Result<(AppContext, Vec<Message>), Box<dyn Error>> {
let mut app_context = app_context.clone();
let mut messages = Vec::new();
match socket_function {
SocketFunction::ReplaceBoxContent {
box_id,
success,
content,
} => {
messages.push(Message::StreamUpdateMessage(
crate::model::common::StreamUpdate {
stream_id: format!("socket-{}", box_id),
target_box_id: box_id.clone(),
content_update: content,
source_state: crate::model::common::SourceState::Batch(
crate::model::common::BatchSourceState {
task_id: format!("socket-{}", box_id),
queue_wait_time: std::time::Duration::from_millis(0),
execution_time: std::time::Duration::from_millis(0),
exit_code: if success { Some(0) } else { Some(1) },
status: if success {
crate::model::common::BatchStatus::Completed
} else {
crate::model::common::BatchStatus::Failed(
"Unknown error".to_string(),
)
},
},
),
execution_mode: crate::model::common::ExecutionMode::Immediate,
},
));
}
SocketFunction::ReplaceBoxScript { box_id, script } => {
messages.push(Message::MuxBoxScriptUpdate(box_id, script));
}
SocketFunction::StopBoxRefresh { box_id } => {
messages.push(Message::StopBoxRefresh(box_id));
}
SocketFunction::StartBoxRefresh { box_id } => {
messages.push(Message::StartBoxRefresh(box_id));
}
SocketFunction::ReplaceBox { box_id, new_box } => {
messages.push(Message::ReplaceMuxBox(box_id, new_box));
}
SocketFunction::SwitchActiveLayout { layout_id } => {
messages.push(Message::SwitchActiveLayout(layout_id));
}
SocketFunction::AddBox { layout_id, muxbox } => {
messages.push(Message::AddBox(layout_id, muxbox));
}
SocketFunction::RemoveBox { box_id } => {
messages.push(Message::RemoveBox(box_id));
}
SocketFunction::KillPtyProcess { box_id } => {
if let Some(pty_manager) = &app_context.pty_manager {
let stream_id = pty_manager
.get_stream_id(&box_id)
.unwrap_or_else(|| format!("error-no-pty-{}", box_id));
match pty_manager.kill_pty_process(&box_id) {
Ok(_) => {
messages.push(Message::StreamUpdateMessage(
crate::model::common::StreamUpdate {
target_box_id: box_id.clone(),
stream_id,
content_update: format!("PTY process killed for box {}", box_id),
source_state: crate::model::common::SourceState::Pty(
crate::model::common::PtySourceState {
process_id: 0,
runtime: std::time::Duration::from_millis(0),
exit_code: Some(0),
status: crate::model::common::ExecutionPtyStatus::Completed,
},
),
execution_mode: crate::model::common::ExecutionMode::Pty,
},
));
}
Err(err) => {
messages.push(Message::StreamUpdateMessage(
crate::model::common::StreamUpdate {
target_box_id: box_id.clone(),
stream_id,
content_update: format!("Failed to kill PTY process: {}", err),
source_state: crate::model::common::SourceState::Pty(
crate::model::common::PtySourceState {
process_id: 0,
runtime: std::time::Duration::from_millis(0),
exit_code: Some(1),
status: crate::model::common::ExecutionPtyStatus::Failed(
"Error".to_string(),
),
},
),
execution_mode: crate::model::common::ExecutionMode::Pty,
},
));
}
}
} else {
messages.push(Message::StreamUpdateMessage(
crate::model::common::StreamUpdate {
target_box_id: box_id.clone(),
stream_id: format!("error-no-pty-{}", box_id),
content_update: "PTY manager not available".to_string(),
source_state: crate::model::common::SourceState::Pty(
crate::model::common::PtySourceState {
process_id: 0,
runtime: std::time::Duration::from_millis(0),
exit_code: Some(1),
status: crate::model::common::ExecutionPtyStatus::Failed(
"PTY manager not available".to_string(),
),
},
),
execution_mode: crate::model::common::ExecutionMode::Pty,
},
));
}
}
SocketFunction::RestartPtyProcess { box_id } => {
if let Some(pty_manager) = &app_context.pty_manager {
match pty_manager.restart_pty_process(&box_id) {
Ok(_) => {
messages.push(Message::StreamUpdateMessage(
crate::model::common::StreamUpdate {
target_box_id: box_id.clone(),
stream_id: pty_manager
.get_stream_id(&box_id)
.unwrap_or_else(|| format!("error-no-pty-{}", box_id)),
content_update: format!("PTY process restarted for box {}", box_id),
source_state: crate::model::common::SourceState::Pty(
crate::model::common::PtySourceState {
process_id: 0,
runtime: std::time::Duration::from_millis(0),
exit_code: Some(0),
status: crate::model::common::ExecutionPtyStatus::Completed,
},
),
execution_mode: crate::model::common::ExecutionMode::Pty,
},
));
}
Err(err) => {
messages.push(Message::StreamUpdateMessage(
crate::model::common::StreamUpdate {
target_box_id: box_id.clone(),
stream_id: pty_manager
.get_stream_id(&box_id)
.unwrap_or_else(|| format!("error-no-pty-{}", box_id)),
content_update: format!("Failed to restart PTY process: {}", err),
source_state: crate::model::common::SourceState::Pty(
crate::model::common::PtySourceState {
process_id: 0,
runtime: std::time::Duration::from_millis(0),
exit_code: Some(1),
status: crate::model::common::ExecutionPtyStatus::Failed(
"Error".to_string(),
),
},
),
execution_mode: crate::model::common::ExecutionMode::Pty,
},
));
}
}
} else {
messages.push(Message::StreamUpdateMessage(
crate::model::common::StreamUpdate {
target_box_id: box_id.clone(),
stream_id: format!("error-no-pty-{}", box_id),
content_update: "PTY manager not available".to_string(),
source_state: crate::model::common::SourceState::Pty(
crate::model::common::PtySourceState {
process_id: 0,
runtime: std::time::Duration::from_millis(0),
exit_code: Some(1),
status: crate::model::common::ExecutionPtyStatus::Failed(
"PTY manager not available".to_string(),
),
},
),
execution_mode: crate::model::common::ExecutionMode::Pty,
},
));
}
}
SocketFunction::QueryPtyStatus { box_id } => {
if let Some(pty_manager) = &app_context.pty_manager {
if let Some(info) = pty_manager.get_detailed_process_info(&box_id) {
let status_info = format!(
"PTY Status - Box: {}, PID: {:?}, Status: {:?}, Running: {}, Buffer Lines: {}",
info.muxbox_id, info.process_id, info.status, info.is_running, info.buffer_lines
);
messages.push(Message::StreamUpdateMessage(
crate::model::common::StreamUpdate {
target_box_id: box_id.clone(),
stream_id: format!("error-no-manager-{}", box_id),
content_update: status_info,
source_state: crate::model::common::SourceState::Pty(
crate::model::common::PtySourceState {
process_id: 0,
runtime: std::time::Duration::from_millis(0),
exit_code: Some(0),
status: crate::model::common::ExecutionPtyStatus::Completed,
},
),
execution_mode: crate::model::common::ExecutionMode::Pty,
},
));
} else {
messages.push(Message::StreamUpdateMessage(
crate::model::common::StreamUpdate {
target_box_id: box_id.clone(),
stream_id: format!("error-no-manager-{}", box_id),
content_update: format!("No PTY process found for box {}", box_id),
source_state: crate::model::common::SourceState::Pty(
crate::model::common::PtySourceState {
process_id: 0,
runtime: std::time::Duration::from_millis(0),
exit_code: Some(1),
status: crate::model::common::ExecutionPtyStatus::Failed(
"Error".to_string(),
),
},
),
execution_mode: crate::model::common::ExecutionMode::Pty,
},
));
}
} else {
messages.push(Message::StreamUpdateMessage(
crate::model::common::StreamUpdate {
target_box_id: box_id.clone(),
stream_id: format!("error-no-pty-{}", box_id),
content_update: "PTY manager not available".to_string(),
source_state: crate::model::common::SourceState::Pty(
crate::model::common::PtySourceState {
process_id: 0,
runtime: std::time::Duration::from_millis(0),
exit_code: Some(1),
status: crate::model::common::ExecutionPtyStatus::Failed(
"PTY manager not available".to_string(),
),
},
),
execution_mode: crate::model::common::ExecutionMode::Pty,
},
));
}
}
SocketFunction::SpawnPtyProcess {
box_id,
script,
libs,
redirect_output,
} => {
let stream_id = app_context.app.register_execution_source(
ExecutionSourceType::SocketUpdate {
command_type: "spawn_pty_process".to_string(),
},
box_id.clone(),
);
let execute_script_msg =
crate::thread_manager::Message::ExecuteScriptMessage(ExecuteScript {
script,
source: ExecutionSource {
source_type: SourceType::SocketUpdate,
source_id: box_id.clone(),
source_reference: SourceReference::SocketCommand(
"spawn_pty_process".to_string(),
),
},
execution_mode: ExecutionMode::Pty,
target_box_id: box_id.clone(),
libs: libs.unwrap_or_default(),
redirect_output,
append_output: false,
stream_id,
target_bounds: None, });
messages.push(execute_script_msg);
log::info!(
"PTY process spawn queued for execution via unified architecture: {}",
box_id
);
}
SocketFunction::SendPtyInput { box_id, input } => {
if let Some(pty_manager) = &app_context.pty_manager {
let stream_id = pty_manager
.get_stream_id(&box_id)
.unwrap_or_else(|| format!("error-no-pty-{}", box_id));
match pty_manager.send_input(&box_id, &input) {
Ok(_) => {
messages.push(Message::StreamUpdateMessage(
crate::model::common::StreamUpdate {
target_box_id: box_id.clone(),
stream_id,
content_update: format!(
"Input sent successfully to PTY process for box {}",
box_id
),
source_state: crate::model::common::SourceState::Pty(
crate::model::common::PtySourceState {
process_id: 0,
runtime: std::time::Duration::from_millis(0),
exit_code: Some(0),
status: crate::model::common::ExecutionPtyStatus::Completed,
},
),
execution_mode: crate::model::common::ExecutionMode::Pty,
},
));
}
Err(err) => {
messages.push(Message::StreamUpdateMessage(
crate::model::common::StreamUpdate {
target_box_id: box_id.clone(),
stream_id,
content_update: format!(
"Failed to send input to PTY process: {}",
err
),
source_state: crate::model::common::SourceState::Pty(
crate::model::common::PtySourceState {
process_id: 0,
runtime: std::time::Duration::from_millis(0),
exit_code: Some(1),
status: crate::model::common::ExecutionPtyStatus::Failed(
"Error".to_string(),
),
},
),
execution_mode: crate::model::common::ExecutionMode::Pty,
},
));
}
}
} else {
messages.push(Message::StreamUpdateMessage(
crate::model::common::StreamUpdate {
target_box_id: box_id.clone(),
stream_id: format!("error-no-pty-{}", box_id),
content_update: "PTY manager not available".to_string(),
source_state: crate::model::common::SourceState::Pty(
crate::model::common::PtySourceState {
process_id: 0,
runtime: std::time::Duration::from_millis(0),
exit_code: Some(1),
status: crate::model::common::ExecutionPtyStatus::Failed(
"PTY manager not available".to_string(),
),
},
),
execution_mode: crate::model::common::ExecutionMode::Pty,
},
));
}
}
}
Ok((app_context, messages))
}
#[derive(Clone, PartialEq, Debug)]
pub struct Cell {
pub fg_color: String,
pub bg_color: String,
pub ch: char,
}
#[derive(Debug, Clone)]
pub struct ScreenBuffer {
pub width: usize,
pub height: usize,
pub buffer: Vec<Vec<Cell>>,
}
impl Default for ScreenBuffer {
fn default() -> Self {
Self::new()
}
}
impl ScreenBuffer {
pub fn new() -> Self {
let default_cell = Cell {
fg_color: get_fg_color(crate::color_utils::default_fg_color(false)),
bg_color: get_bg_color(crate::color_utils::default_bg_color(false)),
ch: ' ',
};
let width = screen_width();
let height = screen_height();
let buffer = vec![vec![default_cell; width]; height];
ScreenBuffer {
width,
height,
buffer,
}
}
pub fn new_custom(width: usize, height: usize) -> Self {
let default_cell = Cell {
fg_color: get_fg_color(crate::color_utils::default_fg_color(false)),
bg_color: get_bg_color(crate::color_utils::default_bg_color(false)),
ch: ' ',
};
let buffer = vec![vec![default_cell; width]; height];
ScreenBuffer {
width,
height,
buffer,
}
}
pub fn clear(&mut self) {
let default_cell = Cell {
fg_color: get_fg_color(crate::color_utils::default_fg_color(false)),
bg_color: get_bg_color(crate::color_utils::default_bg_color(false)),
ch: ' ',
};
self.buffer = vec![vec![default_cell; self.width]; self.height];
}
pub fn update(&mut self, x: usize, y: usize, cell: Cell) {
if x < self.width && y < self.height {
self.buffer[y][x] = cell;
}
}
pub fn get(&self, x: usize, y: usize) -> Option<&Cell> {
if x < self.width && y < self.height {
Some(&self.buffer[y][x])
} else {
None
}
}
pub fn resize(&mut self, width: usize, height: usize) {
if height < self.height {
self.buffer.truncate(height);
}
if width < self.width {
for row in &mut self.buffer {
row.truncate(width);
}
}
if height > self.height {
let default_row = vec![
Cell {
fg_color: get_fg_color("white"),
bg_color: get_bg_color("black"),
ch: ' ',
};
width
];
self.buffer.resize_with(height, || default_row.clone());
}
if width > self.width {
for row in &mut self.buffer {
row.resize_with(width, || Cell {
fg_color: get_fg_color("white"),
bg_color: get_bg_color("black"),
ch: ' ',
});
}
}
self.width = width;
self.height = height;
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq)]
pub struct InputBounds {
pub x1: String,
pub y1: String,
pub x2: String,
pub y2: String,
}
impl InputBounds {
pub fn to_bounds(&self, parent_bounds: &Bounds) -> Bounds {
input_bounds_to_bounds(self, parent_bounds)
}
}
#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Bounds {
pub x1: usize,
pub y1: usize,
pub x2: usize,
pub y2: usize,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Hash, Eq, Default)]
pub enum Anchor {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
#[default]
Center,
CenterTop,
CenterBottom,
CenterLeft,
CenterRight,
}
impl Bounds {
pub fn new(x1: usize, y1: usize, x2: usize, y2: usize) -> Self {
Bounds { x1, y1, x2, y2 }
}
pub fn validate(&self) {
if self.x1 > self.x2 {
panic!(
"Validation error: x1 ({}) is greater than x2 ({})",
self.x1, self.x2
);
}
if self.y1 > self.y2 {
panic!(
"Validation error: y1 ({}) is greater than y2 ({})",
self.y1, self.y2
);
}
}
pub fn width(&self) -> usize {
self.x2.saturating_sub(self.x1).saturating_add(1)
}
pub fn height(&self) -> usize {
self.y2.saturating_sub(self.y1).saturating_add(1)
}
pub fn extend(&mut self, horizontal_amount: usize, vertical_amount: usize, anchor: Anchor) {
match anchor {
Anchor::TopLeft => {
self.x1 = self.x1.saturating_sub(horizontal_amount);
self.y1 = self.y1.saturating_sub(vertical_amount);
}
Anchor::TopRight => {
self.x2 += horizontal_amount;
self.y1 = self.y1.saturating_sub(vertical_amount);
}
Anchor::BottomLeft => {
self.x1 = self.x1.saturating_sub(horizontal_amount);
self.y2 += vertical_amount;
}
Anchor::BottomRight => {
self.x2 += horizontal_amount;
self.y2 += vertical_amount;
}
Anchor::Center => {
let half_horizontal = horizontal_amount / 2;
let half_vertical = vertical_amount / 2;
self.x1 = self.x1.saturating_sub(half_horizontal);
self.y1 = self.y1.saturating_sub(half_vertical);
self.x2 += half_horizontal;
self.y2 += half_vertical;
}
Anchor::CenterTop => {
let half_horizontal = horizontal_amount / 2;
self.x1 = self.x1.saturating_sub(half_horizontal);
self.x2 += half_horizontal;
self.y1 = self.y1.saturating_sub(vertical_amount);
}
Anchor::CenterBottom => {
let half_horizontal = horizontal_amount / 2;
self.x1 = self.x1.saturating_sub(half_horizontal);
self.x2 += half_horizontal;
self.y2 += vertical_amount;
}
Anchor::CenterLeft => {
let half_vertical = vertical_amount / 2;
self.x1 = self.x1.saturating_sub(horizontal_amount);
self.y1 = self.y1.saturating_sub(half_vertical);
self.y2 += half_vertical;
}
Anchor::CenterRight => {
let half_vertical = vertical_amount / 2;
self.x2 += horizontal_amount;
self.y1 = self.y1.saturating_sub(half_vertical);
self.y2 += half_vertical;
}
}
self.validate();
}
pub fn contract(&mut self, horizontal_amount: usize, vertical_amount: usize, anchor: Anchor) {
match anchor {
Anchor::TopLeft => {
self.x1 += horizontal_amount;
self.y1 += vertical_amount;
}
Anchor::TopRight => {
self.x2 = self.x2.saturating_sub(horizontal_amount);
self.y1 += vertical_amount;
}
Anchor::BottomLeft => {
self.x1 += horizontal_amount;
self.y2 = self.y2.saturating_sub(vertical_amount);
}
Anchor::BottomRight => {
self.x2 = self.x2.saturating_sub(horizontal_amount);
self.y2 = self.y2.saturating_sub(vertical_amount);
}
Anchor::Center => {
let half_horizontal = horizontal_amount / 2;
let half_vertical = vertical_amount / 2;
self.x1 += half_horizontal;
self.y1 += half_vertical;
self.x2 = self.x2.saturating_sub(half_horizontal);
self.y2 = self.y2.saturating_sub(half_vertical);
}
Anchor::CenterTop => {
let half_horizontal = horizontal_amount / 2;
self.x1 += half_horizontal;
self.x2 = self.x2.saturating_sub(half_horizontal);
self.y1 += vertical_amount;
}
Anchor::CenterBottom => {
let half_horizontal = horizontal_amount / 2;
self.x1 += half_horizontal;
self.x2 = self.x2.saturating_sub(half_horizontal);
self.y2 = self.y2.saturating_sub(vertical_amount);
}
Anchor::CenterLeft => {
let half_vertical = vertical_amount / 2;
self.x1 += horizontal_amount;
self.y1 += half_vertical;
self.y2 = self.y2.saturating_sub(half_vertical);
}
Anchor::CenterRight => {
let half_vertical = vertical_amount / 2;
self.x2 = self.x2.saturating_sub(horizontal_amount);
self.y1 += half_vertical;
self.y2 = self.y2.saturating_sub(half_vertical);
}
}
self.validate();
}
pub fn move_to(&mut self, x: usize, y: usize, anchor: Anchor) {
match anchor {
Anchor::TopLeft => {
let width = self.width();
let height = self.height();
self.x1 = x;
self.y1 = y;
self.x2 = x + width - 1; self.y2 = y + height - 1; }
Anchor::TopRight => {
let width = self.width();
let height = self.height();
self.x2 = x;
self.y1 = y;
self.x1 = x - width + 1; self.y2 = y + height - 1; }
Anchor::BottomLeft => {
let width = self.width();
let height = self.height();
self.x1 = x;
self.y2 = y;
self.x2 = x + width - 1; self.y1 = y - height + 1; }
Anchor::BottomRight => {
let width = self.width();
let height = self.height();
self.x2 = x;
self.y2 = y;
self.x1 = x - width + 1; self.y1 = y - height + 1; }
Anchor::Center => {
let width = self.width();
let height = self.height();
let half_width = width / 2;
let half_height = height / 2;
self.x1 = x - half_width;
self.y1 = y - half_height;
self.x2 = x + width - half_width - 1; self.y2 = y + height - half_height - 1; }
Anchor::CenterTop => {
let width = self.width();
let height = self.height();
let half_width = width / 2;
self.x1 = x - half_width;
self.x2 = x + width - half_width - 1; self.y1 = y;
self.y2 = y + height - 1; }
Anchor::CenterBottom => {
let width = self.width();
let height = self.height();
let half_width = width / 2;
self.x1 = x - half_width;
self.x2 = x + width - half_width - 1; self.y2 = y;
self.y1 = y - height + 1; }
Anchor::CenterLeft => {
let width = self.width();
let height = self.height();
let half_height = height / 2;
self.x1 = x;
self.x2 = x + width - 1; self.y1 = y - half_height;
self.y2 = y + height - half_height - 1; }
Anchor::CenterRight => {
let width = self.width();
let height = self.height();
let half_height = height / 2;
self.x2 = x;
self.x1 = x - width + 1; self.y1 = y - half_height;
self.y2 = y + height - half_height - 1; }
}
self.validate();
}
pub fn move_by(&mut self, dx: isize, dy: isize) {
self.x1 = (self.x1 as isize + dx) as usize;
self.y1 = (self.y1 as isize + dy) as usize;
self.x2 = (self.x2 as isize + dx) as usize;
self.y2 = (self.y2 as isize + dy) as usize;
self.validate();
}
pub fn contains(&self, x: usize, y: usize) -> bool {
x >= self.x1 && x < self.x2 && y >= self.y1 && y < self.y2
}
pub fn contains_bounds(&self, other: &Bounds) -> bool {
self.contains(other.x1, other.y1) && self.contains(other.x2, other.y2)
}
pub fn intersects(&self, other: &Bounds) -> bool {
self.contains(other.x1, other.y1)
|| self.contains(other.x2, other.y2)
|| self.contains(other.x1, other.y2)
|| self.contains(other.x2, other.y1)
}
pub fn intersection(&self, other: &Bounds) -> Option<Bounds> {
if self.intersects(other) {
Some(Bounds {
x1: self.x1.max(other.x1),
y1: self.y1.max(other.y1),
x2: self.x2.min(other.x2),
y2: self.y2.min(other.y2),
})
} else {
None
}
}
pub fn union(&self, other: &Bounds) -> Bounds {
Bounds {
x1: self.x1.min(other.x1),
y1: self.y1.min(other.y1),
x2: self.x2.max(other.x2),
y2: self.y2.max(other.y2),
}
}
pub fn translate(&self, dx: isize, dy: isize) -> Bounds {
Bounds {
x1: (self.x1 as isize + dx) as usize,
y1: (self.y1 as isize + dy) as usize,
x2: (self.x2 as isize + dx) as usize,
y2: (self.y2 as isize + dy) as usize,
}
}
pub fn center(&self) -> (usize, usize) {
((self.x1 + self.x2) / 2, (self.y1 + self.y2) / 2)
}
pub fn center_x(&self) -> usize {
(self.x1 + self.x2) / 2
}
pub fn center_y(&self) -> usize {
(self.y1 + self.y2) / 2
}
pub fn top_left(&self) -> (usize, usize) {
(self.x1, self.y1)
}
pub fn top_right(&self) -> (usize, usize) {
(self.x2, self.y1)
}
pub fn bottom_left(&self) -> (usize, usize) {
(self.x1, self.y2)
}
pub fn bottom_right(&self) -> (usize, usize) {
(self.x2, self.y2)
}
pub fn top(&self) -> usize {
self.y1
}
pub fn bottom(&self) -> usize {
self.y2
}
pub fn left(&self) -> usize {
self.x1
}
pub fn right(&self) -> usize {
self.x2
}
pub fn center_top(&self) -> (usize, usize) {
((self.x1 + self.x2) / 2, self.y1)
}
pub fn center_bottom(&self) -> (usize, usize) {
((self.x1 + self.x2) / 2, self.y2)
}
pub fn center_left(&self) -> (usize, usize) {
(self.x1, (self.y1 + self.y2) / 2)
}
pub fn center_right(&self) -> (usize, usize) {
(self.x2, (self.y1 + self.y2) / 2)
}
pub fn contains_point(&self, x: usize, y: usize) -> bool {
x >= self.x1 && x <= self.x2 && y >= self.y1 && y <= self.y2
}
}
impl std::fmt::Display for Bounds {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "({}, {}), ({}, {})", self.x1, self.y1, self.x2, self.y2)
}
}
pub fn calculate_initial_bounds(app_graph: &AppGraph, layout: &Layout) -> HashMap<String, Bounds> {
let mut bounds_map = HashMap::new();
fn dfs(
_app_graph: &AppGraph,
_layout_id: &str,
muxbox: &MuxBox,
parent_bounds: Bounds,
bounds_map: &mut HashMap<String, Bounds>,
) {
let bounds = muxbox.absolute_bounds(Some(&parent_bounds));
bounds_map.insert(muxbox.id.clone(), bounds.clone());
if let Some(children) = &muxbox.children {
for child in children {
dfs(_app_graph, _layout_id, child, bounds.clone(), bounds_map);
}
}
}
let root_bounds = screen_bounds();
if let Some(children) = &layout.children {
for muxbox in children {
dfs(
app_graph,
&layout.id,
muxbox,
root_bounds.clone(),
&mut bounds_map,
);
}
}
bounds_map
}
pub fn adjust_bounds_with_constraints(
layout: &Layout,
mut bounds_map: HashMap<String, Bounds>,
) -> HashMap<String, Bounds> {
fn apply_constraints(muxbox: &MuxBox, bounds: &mut Bounds) {
if let Some(min_width) = muxbox.min_width {
if bounds.width() < min_width {
bounds.extend(min_width - bounds.width(), 0, muxbox.anchor.clone());
}
}
if let Some(min_height) = muxbox.min_height {
if bounds.height() < min_height {
bounds.extend(0, min_height - bounds.height(), muxbox.anchor.clone());
}
}
if let Some(max_width) = muxbox.max_width {
if bounds.width() > max_width {
bounds.contract(bounds.width() - max_width, 0, muxbox.anchor.clone());
}
}
if let Some(max_height) = muxbox.max_height {
if bounds.height() > max_height {
bounds.contract(0, bounds.height() - max_height, muxbox.anchor.clone());
}
}
}
fn dfs(muxbox: &MuxBox, bounds_map: &mut HashMap<String, Bounds>) -> Bounds {
let mut bounds = bounds_map.remove(&muxbox.id).unwrap();
apply_constraints(muxbox, &mut bounds);
bounds_map.insert(muxbox.id.clone(), bounds.clone());
if let Some(children) = &muxbox.children {
for child in children {
let child_bounds = dfs(child, bounds_map);
bounds.x2 = bounds.x2.max(child_bounds.x2);
bounds.y2 = bounds.y2.max(child_bounds.y2);
}
}
bounds
}
fn revalidate_children(
muxbox: &MuxBox,
bounds_map: &mut HashMap<String, Bounds>,
parent_bounds: &Bounds,
) {
if let Some(children) = &muxbox.children {
for child in children {
if let Some(child_bounds) = bounds_map.get_mut(&child.id) {
if child_bounds.x2 > parent_bounds.x2 {
child_bounds.x2 = parent_bounds.x2;
}
if child_bounds.y2 > parent_bounds.y2 {
child_bounds.y2 = parent_bounds.y2;
}
if child_bounds.x1 < parent_bounds.x1 {
child_bounds.x1 = parent_bounds.x1;
}
if child_bounds.y1 < parent_bounds.y1 {
child_bounds.y1 = parent_bounds.y1;
}
}
revalidate_children(child, bounds_map, parent_bounds);
}
}
}
if let Some(children) = &layout.children {
for muxbox in children {
let parent_bounds = dfs(muxbox, &mut bounds_map);
revalidate_children(muxbox, &mut bounds_map, &parent_bounds);
}
}
bounds_map
}
pub fn calculate_bounds_map(app_graph: &AppGraph, layout: &Layout) -> HashMap<String, Bounds> {
let bounds_map = calculate_initial_bounds(app_graph, layout);
adjust_bounds_with_constraints(layout, bounds_map)
}
pub fn deserialize_script<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum ScriptFormat {
Single(String),
Multiple(Vec<String>),
Mixed(Vec<serde_yaml::Value>),
}
let script_format = Option::<ScriptFormat>::deserialize(deserializer)?;
match script_format {
None => Ok(None),
Some(ScriptFormat::Single(single)) => {
let commands: Vec<String> = single
.lines()
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty())
.collect();
Ok(Some(commands))
}
Some(ScriptFormat::Multiple(multiple)) => Ok(Some(multiple)),
Some(ScriptFormat::Mixed(mixed)) => {
let mut commands = Vec::new();
for value in mixed {
match value {
serde_yaml::Value::String(s) => commands.push(s),
serde_yaml::Value::Mapping(_) | serde_yaml::Value::Sequence(_) => {
if let Ok(yaml_str) = serde_yaml::to_string(&value) {
let clean_str = yaml_str.trim_start_matches("---\n").trim().to_string();
if !clean_str.is_empty() {
commands.push(clean_str);
}
}
}
_ => {
commands.push(format!("{:?}", value));
}
}
}
Ok(Some(commands))
}
}
}
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
pub fn send_json_to_socket(socket_path: &str, json: &str) -> Result<String, Box<dyn Error>> {
let mut stream = UnixStream::connect(socket_path)?;
stream.write_all(json.as_bytes())?;
let mut response = String::new();
stream.read_to_string(&mut response)?;
Ok(response)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_new_valid_frame_delay() {
let config = Config::new(60);
assert_eq!(config.frame_delay, 60);
}
#[test]
#[should_panic(expected = "Validation error: frame_delay cannot be 0")]
fn test_config_new_zero_frame_delay_panics() {
Config::new(0);
}
#[test]
fn test_config_default() {
let config = Config::default();
assert_eq!(config.frame_delay, 30);
}
#[test]
#[should_panic(expected = "Validation error: frame_delay cannot be 0")]
fn test_config_validate_zero_frame_delay() {
let config = Config {
frame_delay: 0,
locked: false,
calibrate: false,
};
config.validate();
}
#[test]
fn test_config_validate_valid() {
let config = Config {
frame_delay: 16,
locked: false,
calibrate: false,
};
config.validate(); }
#[test]
fn test_config_hash_consistency() {
let config1 = Config::new(30);
let config2 = Config::new(30);
let config3 = Config::new(60);
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher1 = DefaultHasher::new();
let mut hasher2 = DefaultHasher::new();
let mut hasher3 = DefaultHasher::new();
config1.hash(&mut hasher1);
config2.hash(&mut hasher2);
config3.hash(&mut hasher3);
assert_eq!(hasher1.finish(), hasher2.finish());
assert_ne!(hasher1.finish(), hasher3.finish());
}
#[test]
fn test_bounds_new() {
let bounds = Bounds::new(10, 20, 100, 200);
assert_eq!(bounds.x1, 10);
assert_eq!(bounds.y1, 20);
assert_eq!(bounds.x2, 100);
assert_eq!(bounds.y2, 200);
}
#[test]
#[should_panic(expected = "Validation error: x1 (100) is greater than x2 (50)")]
fn test_bounds_validate_invalid_x_coordinates() {
let bounds = Bounds::new(100, 20, 50, 200);
bounds.validate();
}
#[test]
#[should_panic(expected = "Validation error: y1 (200) is greater than y2 (100)")]
fn test_bounds_validate_invalid_y_coordinates() {
let bounds = Bounds::new(10, 200, 100, 100);
bounds.validate();
}
#[test]
fn test_bounds_validate_valid() {
let bounds = Bounds::new(10, 20, 100, 200);
bounds.validate(); }
#[test]
fn test_bounds_width() {
let bounds = Bounds::new(10, 20, 100, 200);
assert_eq!(bounds.width(), 91); }
#[test]
fn test_bounds_height() {
let bounds = Bounds::new(10, 20, 100, 200);
assert_eq!(bounds.height(), 181); }
#[test]
fn test_bounds_width_zero() {
let bounds = Bounds::new(50, 20, 50, 200);
assert_eq!(bounds.width(), 1); }
#[test]
fn test_bounds_height_zero() {
let bounds = Bounds::new(10, 50, 100, 50);
assert_eq!(bounds.height(), 1); }
#[test]
fn test_bounds_contains() {
let bounds = Bounds::new(10, 20, 100, 200);
assert!(bounds.contains(50, 100));
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)); }
#[test]
fn test_bounds_contains_bounds() {
let outer = Bounds::new(10, 20, 100, 200);
let inner = Bounds::new(30, 40, 80, 180);
let overlapping = Bounds::new(5, 15, 50, 100);
assert!(outer.contains_bounds(&inner));
assert!(!outer.contains_bounds(&overlapping));
}
#[test]
fn test_bounds_intersects() {
let bounds1 = Bounds::new(10, 20, 100, 200);
let bounds2 = Bounds::new(50, 100, 150, 250); let bounds3 = Bounds::new(200, 300, 250, 350);
assert!(bounds1.intersects(&bounds2));
assert!(!bounds1.intersects(&bounds3));
}
#[test]
fn test_bounds_intersection() {
let bounds1 = Bounds::new(10, 20, 100, 200);
let bounds2 = Bounds::new(50, 100, 150, 250);
let bounds3 = Bounds::new(200, 300, 250, 350);
let intersection = bounds1.intersection(&bounds2);
assert!(intersection.is_some());
let intersection = intersection.unwrap();
assert_eq!(intersection.x1, 50);
assert_eq!(intersection.y1, 100);
assert_eq!(intersection.x2, 100);
assert_eq!(intersection.y2, 200);
assert!(bounds1.intersection(&bounds3).is_none());
}
#[test]
fn test_bounds_union() {
let bounds1 = Bounds::new(10, 20, 100, 200);
let bounds2 = Bounds::new(50, 100, 150, 250);
let union = bounds1.union(&bounds2);
assert_eq!(union.x1, 10);
assert_eq!(union.y1, 20);
assert_eq!(union.x2, 150);
assert_eq!(union.y2, 250);
}
#[test]
fn test_bounds_translate() {
let bounds = Bounds::new(10, 20, 100, 200);
let translated = bounds.translate(5, -10);
assert_eq!(translated.x1, 15);
assert_eq!(translated.y1, 10);
assert_eq!(translated.x2, 105);
assert_eq!(translated.y2, 190);
}
#[test]
fn test_bounds_center() {
let bounds = Bounds::new(10, 20, 100, 200);
let center = bounds.center();
assert_eq!(center, (55, 110));
}
#[test]
fn test_bounds_center_x() {
let bounds = Bounds::new(10, 20, 100, 200);
assert_eq!(bounds.center_x(), 55);
}
#[test]
fn test_bounds_center_y() {
let bounds = Bounds::new(10, 20, 100, 200);
assert_eq!(bounds.center_y(), 110);
}
#[test]
fn test_bounds_extend_center() {
let mut bounds = Bounds::new(50, 50, 100, 100);
bounds.extend(20, 10, Anchor::Center);
assert_eq!(bounds.x1, 40);
assert_eq!(bounds.y1, 45);
assert_eq!(bounds.x2, 110);
assert_eq!(bounds.y2, 105);
}
#[test]
fn test_bounds_extend_top_left() {
let mut bounds = Bounds::new(50, 50, 100, 100);
bounds.extend(20, 10, Anchor::TopLeft);
assert_eq!(bounds.x1, 30);
assert_eq!(bounds.y1, 40);
assert_eq!(bounds.x2, 100);
assert_eq!(bounds.y2, 100);
}
#[test]
fn test_bounds_contract_center() {
let mut bounds = Bounds::new(50, 50, 100, 100);
bounds.contract(10, 20, Anchor::Center);
assert_eq!(bounds.x1, 55);
assert_eq!(bounds.y1, 60);
assert_eq!(bounds.x2, 95);
assert_eq!(bounds.y2, 90);
}
#[test]
fn test_bounds_move_to() {
let mut bounds = Bounds::new(10, 20, 60, 70);
bounds.move_to(100, 150, Anchor::TopLeft);
assert_eq!(bounds.x1, 100);
assert_eq!(bounds.y1, 150);
assert_eq!(bounds.x2, 150);
assert_eq!(bounds.y2, 200);
}
#[test]
fn test_bounds_move_by() {
let mut bounds = Bounds::new(10, 20, 60, 70);
bounds.move_by(5, -10);
assert_eq!(bounds.x1, 15);
assert_eq!(bounds.y1, 10);
assert_eq!(bounds.x2, 65);
assert_eq!(bounds.y2, 60);
}
#[test]
fn test_bounds_anchor_points() {
let bounds = Bounds::new(10, 20, 100, 200);
assert_eq!(bounds.top_left(), (10, 20));
assert_eq!(bounds.top_right(), (100, 20));
assert_eq!(bounds.bottom_left(), (10, 200));
assert_eq!(bounds.bottom_right(), (100, 200));
assert_eq!(bounds.center_top(), (55, 20));
assert_eq!(bounds.center_bottom(), (55, 200));
assert_eq!(bounds.center_left(), (10, 110));
assert_eq!(bounds.center_right(), (100, 110));
assert_eq!(bounds.top(), 20);
assert_eq!(bounds.bottom(), 200);
assert_eq!(bounds.left(), 10);
assert_eq!(bounds.right(), 100);
}
#[test]
fn test_bounds_to_string() {
let bounds = Bounds::new(10, 20, 100, 200);
assert_eq!(bounds.to_string(), "(10, 20), (100, 200)");
}
#[test]
fn test_input_bounds_to_bounds() {
let input_bounds = InputBounds {
x1: "25%".to_string(),
y1: "50%".to_string(),
x2: "75%".to_string(),
y2: "100%".to_string(),
};
let parent_bounds = Bounds::new(0, 0, 100, 200);
let bounds = input_bounds.to_bounds(&parent_bounds);
assert_eq!(bounds.x1, 25);
assert_eq!(bounds.y1, 100);
assert_eq!(bounds.x2, 75); assert_eq!(bounds.y2, 200); }
#[test]
fn test_anchor_default() {
let anchor = Anchor::default();
assert_eq!(anchor, Anchor::Center);
}
#[test]
fn test_screenbuffer_new() {
let screen_buffer = ScreenBuffer::new_custom(5, 5);
assert_eq!(screen_buffer.width, 5);
assert_eq!(screen_buffer.height, 5);
assert_eq!(screen_buffer.buffer.len(), 5);
assert_eq!(screen_buffer.buffer[0].len(), 5);
}
#[test]
fn test_screenbuffer_clear() {
let mut screen_buffer = ScreenBuffer::new_custom(5, 5);
let test_cell = Cell {
fg_color: String::from("red"),
bg_color: String::from("blue"),
ch: 'X',
};
screen_buffer.update(2, 2, test_cell.clone());
screen_buffer.clear();
for row in screen_buffer.buffer.iter() {
for cell in row.iter() {
assert_eq!(cell.fg_color, get_fg_color("white"));
assert_eq!(cell.bg_color, get_bg_color("black"));
assert_eq!(cell.ch, ' ');
}
}
}
#[test]
fn test_screenbuffer_update() {
let mut screen_buffer = ScreenBuffer::new_custom(5, 5);
let test_cell = Cell {
fg_color: String::from("red"),
bg_color: String::from("blue"),
ch: 'X',
};
screen_buffer.update(2, 2, test_cell.clone());
assert_eq!(screen_buffer.get(2, 2).unwrap(), &test_cell);
}
#[test]
fn test_screenbuffer_get() {
let screen_buffer = ScreenBuffer::new_custom(5, 5);
assert!(screen_buffer.get(6, 6).is_none());
assert!(screen_buffer.get(3, 3).is_some());
}
#[test]
fn test_screenbuffer_update_out_of_bounds() {
let mut screen_buffer = ScreenBuffer::new_custom(5, 5);
let test_cell = Cell {
fg_color: String::from("red"),
bg_color: String::from("blue"),
ch: 'X',
};
screen_buffer.update(10, 10, test_cell); assert!(screen_buffer.get(10, 10).is_none());
}
#[test]
fn test_screenbuffer_resize() {
let mut screen_buffer = ScreenBuffer::new_custom(5, 5);
screen_buffer.resize(10, 8);
assert_eq!(screen_buffer.width, 10);
assert_eq!(screen_buffer.height, 8);
assert_eq!(screen_buffer.buffer.len(), 8);
assert_eq!(screen_buffer.buffer[0].len(), 10);
}
#[test]
fn test_screenbuffer_resize_shrink() {
let mut screen_buffer = ScreenBuffer::new_custom(10, 10);
screen_buffer.resize(5, 5);
assert_eq!(screen_buffer.width, 5);
assert_eq!(screen_buffer.height, 5);
assert_eq!(screen_buffer.buffer.len(), 5);
assert_eq!(screen_buffer.buffer[0].len(), 5);
}
fn create_test_app_context() -> AppContext {
let current_dir = std::env::current_dir().expect("Failed to get current directory");
let dashboard_path = current_dir.join("layouts/tests.yaml");
let app = crate::load_app_from_yaml(dashboard_path.to_str().unwrap())
.expect("Failed to load app");
AppContext::new(app, Config::default())
}
#[test]
fn test_run_socket_function_replace_muxbox_content() {
let app_context = create_test_app_context();
let socket_function = SocketFunction::ReplaceBoxContent {
box_id: "test_muxbox".to_string(),
success: true,
content: "Test content".to_string(),
};
let result = run_socket_function(socket_function, &app_context);
assert!(result.is_ok());
let (_, messages) = result.unwrap();
assert_eq!(messages.len(), 1);
match &messages[0] {
crate::Message::StreamUpdateMessage(stream_update) => {
assert_eq!(stream_update.stream_id, "socket-test_muxbox");
assert_eq!(stream_update.content_update, "Test content");
match &stream_update.source_state {
crate::model::common::SourceState::Batch(state) => {
assert!(matches!(
state.status,
crate::model::common::BatchStatus::Completed
));
}
_ => panic!("Expected Batch source state"),
}
}
_ => panic!("Expected StreamUpdateMessage"),
}
}
#[test]
fn test_run_socket_function_switch_active_layout() {
let app_context = create_test_app_context();
let socket_function = SocketFunction::SwitchActiveLayout {
layout_id: "new_layout".to_string(),
};
let result = run_socket_function(socket_function, &app_context);
assert!(result.is_ok());
let (_, messages) = result.unwrap();
assert_eq!(messages.len(), 1);
match &messages[0] {
crate::Message::SwitchActiveLayout(layout_id) => {
assert_eq!(layout_id, "new_layout");
}
_ => panic!("Expected SwitchActiveLayout message"),
}
}
#[test]
fn test_cell_clone_and_eq() {
let cell1 = Cell {
fg_color: "red".to_string(),
bg_color: "blue".to_string(),
ch: 'X',
};
let cell2 = cell1.clone();
assert_eq!(cell1, cell2);
let cell3 = Cell {
fg_color: "green".to_string(),
bg_color: "blue".to_string(),
ch: 'X',
};
assert_ne!(cell1, cell3);
}
#[test]
fn test_send_json_to_socket_function() {
use std::os::unix::net::UnixListener;
use std::thread;
use std::time::Duration;
let socket_path = "/tmp/test_send_json.sock";
let _ = std::fs::remove_file(socket_path);
let server_socket_path = socket_path.to_string();
let server_handle = thread::spawn(move || {
match UnixListener::bind(&server_socket_path) {
Ok(listener) => {
if let Some(Ok(mut stream)) = listener.incoming().next() {
let mut buffer = Vec::new();
let mut temp_buffer = [0; 1024];
match stream.read(&mut temp_buffer) {
Ok(n) => {
buffer.extend_from_slice(&temp_buffer[..n]);
let _ = stream.write_all(b"Test Response");
String::from_utf8_lossy(&buffer).to_string()
}
Err(_) => String::new(),
}
} else {
String::new()
}
}
Err(_) => String::new(),
}
});
thread::sleep(Duration::from_millis(100));
let test_json = r#"{"test": "message"}"#;
let result = send_json_to_socket(socket_path, test_json);
match result {
Ok(response) => {
assert_eq!(response, "Test Response");
let received_message = server_handle.join().unwrap();
assert_eq!(received_message, test_json);
}
Err(_) => {
let _ = server_handle.join();
}
}
let _ = std::fs::remove_file(socket_path);
}
}