Mountain/Environment/
TerminalProvider.rs1#![allow(non_snake_case, non_camel_case_types)]
15
16use std::{env, io::Write, sync::Arc};
17
18use Common::{
19 Environment::Requires::Requires,
20 Error::CommonError::CommonError,
21 IPC::IPCProvider::IPCProvider,
22 Terminal::TerminalProvider::TerminalProvider,
23};
24use async_trait::async_trait;
25use log::{error, info, trace, warn};
26use portable_pty::{CommandBuilder, NativePtySystem, PtySize, PtySystem};
27use serde_json::{Value, json};
28use tauri::Emitter;
29use tokio::sync::mpsc as TokioMPSC;
30
31use super::{MountainEnvironment::MountainEnvironment, Utility};
32use crate::ApplicationState::DTO::TerminalStateDTO::TerminalStateDTO;
33
34#[async_trait]
35impl TerminalProvider for MountainEnvironment {
36 async fn CreateTerminal(&self, OptionsValue:Value) -> Result<Value, CommonError> {
38 let TerminalIdentifier = self.ApplicationState.GetNextTerminalIdentifier();
39
40 let DefaultShell = if cfg!(windows) {
41 "powershell.exe".to_string()
42 } else {
43 env::var("SHELL").unwrap_or_else(|_| "sh".to_string())
44 };
45
46 let Name = OptionsValue
47 .get("name")
48 .and_then(Value::as_str)
49 .unwrap_or("terminal")
50 .to_string();
51
52 info!(
53 "[TerminalProvider] Creating terminal ID: {}, Name: '{}'",
54 TerminalIdentifier, Name
55 );
56
57 let mut TerminalState = TerminalStateDTO::Create(TerminalIdentifier, Name.clone(), &OptionsValue, DefaultShell);
58
59 let PtySystem = NativePtySystem::default();
60
61 let PtyPair = PtySystem
62 .openpty(PtySize::default())
63 .map_err(|Error| CommonError::IPCError { Description:format!("Failed to open PTY: {}", Error) })?;
64
65 let mut Command = CommandBuilder::new(&TerminalState.ShellPath);
66
67 Command.args(&TerminalState.ShellArguments);
68
69 if let Some(CWD) = &TerminalState.CurrentWorkingDirectory {
70 Command.cwd(CWD);
71 }
72
73 let mut ChildProcess = PtyPair.slave.spawn_command(Command).map_err(|Error| {
74 CommonError::IPCError { Description:format!("Failed to spawn shell process: {}", Error) }
75 })?;
76
77 TerminalState.OSProcessIdentifier = ChildProcess.process_id();
78
79 let mut PTYWriter = PtyPair.master.take_writer().map_err(|Error| {
80 CommonError::FileSystemIO {
81 Path:"pty master".into(),
82
83 Description:format!("Failed to take PTY writer: {}", Error),
84 }
85 })?;
86
87 let (InputTransmitter, mut InputReceiver) = TokioMPSC::channel::<String>(32);
88
89 TerminalState.PTYInputTransmitter = Some(InputTransmitter);
90
91 let TermIDForInput = TerminalIdentifier;
92
93 tokio::spawn(async move {
94 while let Some(Data) = InputReceiver.recv().await {
95 if let Err(Error) = PTYWriter.write_all(Data.as_bytes()) {
96 error!("[TerminalProvider] PTY write failed for ID {}: {}", TermIDForInput, Error);
97
98 break;
99 }
100 }
101 });
102
103 let mut PTYReader = PtyPair.master.try_clone_reader().map_err(|Error| {
104 CommonError::FileSystemIO {
105 Path:"pty master".into(),
106
107 Description:format!("Failed to clone PTY reader: {}", Error),
108 }
109 })?;
110
111 let IPCProvider:Arc<dyn IPCProvider> = self.Require();
112
113 let TermIDForOutput = TerminalIdentifier;
114
115 tokio::spawn(async move {
116 let mut Buffer = [0u8; 8192];
117
118 loop {
119 match PTYReader.read(&mut Buffer) {
120 Ok(count) if count > 0 => {
121 let DataString = String::from_utf8_lossy(&Buffer[..count]);
122
123 let Payload = json!([TermIDForOutput, DataString.to_string()]);
124
125 if let Err(Error) = IPCProvider
126 .SendNotificationToSideCar(
127 "cocoon-main".into(),
128 "$acceptTerminalProcessData".into(),
129 Payload,
130 )
131 .await
132 {
133 warn!(
134 "[TerminalProvider] Failed to send process data for ID {}: {}",
135 TermIDForOutput, Error
136 );
137 }
138 },
139
140 _ => break,
142 }
143 }
144 });
145
146 let TermIDForExit = TerminalIdentifier;
147
148 let EnvironmentClone = self.clone();
149
150 tokio::spawn(async move {
151 let _exit_status = ChildProcess.wait();
152
153 info!("[TerminalProvider] Process for terminal ID {} has exited.", TermIDForExit);
154
155 let IPCProvider:Arc<dyn IPCProvider> = EnvironmentClone.Require();
156
157 if let Err(Error) = IPCProvider
158 .SendNotificationToSideCar(
159 "cocoon-main".into(),
160 "$acceptTerminalProcessExit".into(),
161 json!([TermIDForExit]),
162 )
163 .await
164 {
165 warn!(
166 "[TerminalProvider] Failed to send process exit notification for ID {}: {}",
167 TermIDForExit, Error
168 );
169 }
170
171 if let Ok(mut Guard) = EnvironmentClone.ApplicationState.ActiveTerminals.lock() {
173 Guard.remove(&TermIDForExit);
174 }
175 });
176
177 self.ApplicationState
178 .ActiveTerminals
179 .lock()
180 .map_err(Utility::MapApplicationStateLockErrorToCommonError)?
181 .insert(TerminalIdentifier, Arc::new(std::sync::Mutex::new(TerminalState.clone())));
182
183 Ok(json!({ "id": TerminalIdentifier, "name": Name, "pid": TerminalState.OSProcessIdentifier }))
184 }
185
186 async fn SendTextToTerminal(&self, TerminalId:u64, Text:String) -> Result<(), CommonError> {
187 trace!("[TerminalProvider] Sending text to terminal ID: {}", TerminalId);
188
189 let SenderOption = {
190 let TerminalsGuard = self
191 .ApplicationState
192 .ActiveTerminals
193 .lock()
194 .map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
195
196 TerminalsGuard
197 .get(&TerminalId)
198 .and_then(|TerminalArc| TerminalArc.lock().ok())
199 .and_then(|TerminalStateGuard| TerminalStateGuard.PTYInputTransmitter.clone())
200 };
201
202 if let Some(Sender) = SenderOption {
203 Sender
204 .send(Text)
205 .await
206 .map_err(|Error| CommonError::IPCError { Description:Error.to_string() })
207 } else {
208 Err(CommonError::IPCError {
209 Description:format!("Terminal with ID {} not found or has no input channel.", TerminalId),
210 })
211 }
212 }
213
214 async fn DisposeTerminal(&self, TerminalId:u64) -> Result<(), CommonError> {
215 info!("[TerminalProvider] Disposing terminal ID: {}", TerminalId);
216
217 let TerminalArc = self
218 .ApplicationState
219 .ActiveTerminals
220 .lock()
221 .map_err(Utility::MapApplicationStateLockErrorToCommonError)?
222 .remove(&TerminalId);
223
224 if let Some(TerminalArc) = TerminalArc {
225 drop(TerminalArc);
228 }
229
230 Ok(())
231 }
232
233 async fn ShowTerminal(&self, TerminalId:u64, PreserveFocus:bool) -> Result<(), CommonError> {
234 info!("[TerminalProvider] Showing terminal ID: {}", TerminalId);
235
236 self.ApplicationHandle
237 .emit(
238 "sky://terminal/show",
239 json!({ "id": TerminalId, "preserveFocus": PreserveFocus }),
240 )
241 .map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })
242 }
243
244 async fn HideTerminal(&self, TerminalId:u64) -> Result<(), CommonError> {
245 info!("[TerminalProvider] Hiding terminal ID: {}", TerminalId);
246
247 self.ApplicationHandle
248 .emit("sky://terminal/hide", json!({ "id": TerminalId }))
249 .map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })
250 }
251
252 async fn GetTerminalProcessId(&self, TerminalId:u64) -> Result<Option<u32>, CommonError> {
253 let TerminalsGuard = self
254 .ApplicationState
255 .ActiveTerminals
256 .lock()
257 .map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
258
259 Ok(TerminalsGuard
260 .get(&TerminalId)
261 .and_then(|t| t.lock().ok().and_then(|g| g.OSProcessIdentifier)))
262 }
263}