Mountain/Environment/
TerminalProvider.rs1use std::{env, io::Write, sync::Arc};
102
103use CommonLibrary::{
104 Environment::Requires::Requires,
105 Error::CommonError::CommonError,
106 IPC::IPCProvider::IPCProvider,
107 Terminal::TerminalProvider::TerminalProvider,
108};
109use async_trait::async_trait;
110use log::{error, info, trace, warn};
111use portable_pty::{CommandBuilder, NativePtySystem, PtySize, PtySystem};
112use serde_json::{Value, json};
113use tauri::Emitter;
114use tokio::sync::mpsc as TokioMPSC;
115
116use super::{MountainEnvironment::MountainEnvironment, Utility};
117use crate::ApplicationState::DTO::TerminalStateDTO::TerminalStateDTO;
118
119#[async_trait]
120impl TerminalProvider for MountainEnvironment {
121 async fn CreateTerminal(&self, OptionsValue:Value) -> Result<Value, CommonError> {
123 let TerminalIdentifier = self.ApplicationState.GetNextTerminalIdentifier();
124
125 let DefaultShell = if cfg!(windows) {
126 "powershell.exe".to_string()
127 } else {
128 env::var("SHELL").unwrap_or_else(|_| "sh".to_string())
129 };
130
131 let Name = OptionsValue
132 .get("name")
133 .and_then(Value::as_str)
134 .unwrap_or("terminal")
135 .to_string();
136
137 info!(
138 "[TerminalProvider] Creating terminal ID: {}, Name: '{}'",
139 TerminalIdentifier, Name
140 );
141
142 let mut TerminalState = TerminalStateDTO::Create(TerminalIdentifier, Name.clone(), &OptionsValue, DefaultShell)
143 .map_err(|e| {
144 CommonError::ConfigurationLoad { Description:format!("Failed to create terminal state: {}", e) }
145 })?;
146
147 let PtySystem = NativePtySystem::default();
148
149 let PtyPair = PtySystem
150 .openpty(PtySize::default())
151 .map_err(|Error| CommonError::IPCError { Description:format!("Failed to open PTY: {}", Error) })?;
152
153 let mut Command = CommandBuilder::new(&TerminalState.ShellPath);
154
155 Command.args(&TerminalState.ShellArguments);
156
157 if let Some(CWD) = &TerminalState.CurrentWorkingDirectory {
158 Command.cwd(CWD);
159 }
160
161 let mut ChildProcess = PtyPair.slave.spawn_command(Command).map_err(|Error| {
162 CommonError::IPCError { Description:format!("Failed to spawn shell process: {}", Error) }
163 })?;
164
165 TerminalState.OSProcessIdentifier = ChildProcess.process_id();
166
167 let mut PTYWriter = PtyPair.master.take_writer().map_err(|Error| {
168 CommonError::FileSystemIO {
169 Path:"pty master".into(),
170
171 Description:format!("Failed to take PTY writer: {}", Error),
172 }
173 })?;
174
175 let (InputTransmitter, mut InputReceiver) = TokioMPSC::channel::<String>(32);
176
177 TerminalState.PTYInputTransmitter = Some(InputTransmitter);
178
179 let TermIDForInput = TerminalIdentifier;
180
181 tokio::spawn(async move {
182 while let Some(Data) = InputReceiver.recv().await {
183 if let Err(Error) = PTYWriter.write_all(Data.as_bytes()) {
184 error!("[TerminalProvider] PTY write failed for ID {}: {}", TermIDForInput, Error);
185
186 break;
187 }
188 }
189 });
190
191 let mut PTYReader = PtyPair.master.try_clone_reader().map_err(|Error| {
192 CommonError::FileSystemIO {
193 Path:"pty master".into(),
194
195 Description:format!("Failed to clone PTY reader: {}", Error),
196 }
197 })?;
198
199 let IPCProvider:Arc<dyn IPCProvider> = self.Require();
200
201 let TermIDForOutput = TerminalIdentifier;
202
203 tokio::spawn(async move {
204 let mut Buffer = [0u8; 8192];
205
206 loop {
207 match PTYReader.read(&mut Buffer) {
208 Ok(count) if count > 0 => {
209 let DataString = String::from_utf8_lossy(&Buffer[..count]);
210
211 let Payload = json!([TermIDForOutput, DataString.to_string()]);
212
213 if let Err(Error) = IPCProvider
214 .SendNotificationToSideCar(
215 "cocoon-main".into(),
216 "$acceptTerminalProcessData".into(),
217 Payload,
218 )
219 .await
220 {
221 warn!(
222 "[TerminalProvider] Failed to send process data for ID {}: {}",
223 TermIDForOutput, Error
224 );
225 }
226 },
227
228 _ => break,
230 }
231 }
232 });
233
234 let TermIDForExit = TerminalIdentifier;
235
236 let EnvironmentClone = self.clone();
237
238 tokio::spawn(async move {
239 let _exit_status = ChildProcess.wait();
240
241 info!("[TerminalProvider] Process for terminal ID {} has exited.", TermIDForExit);
242
243 let IPCProvider:Arc<dyn IPCProvider> = EnvironmentClone.Require();
244
245 if let Err(Error) = IPCProvider
246 .SendNotificationToSideCar(
247 "cocoon-main".into(),
248 "$acceptTerminalProcessExit".into(),
249 json!([TermIDForExit]),
250 )
251 .await
252 {
253 warn!(
254 "[TerminalProvider] Failed to send process exit notification for ID {}: {}",
255 TermIDForExit, Error
256 );
257 }
258
259 if let Ok(mut Guard) = EnvironmentClone.ApplicationState.Feature.Terminals.ActiveTerminals.lock() {
261 Guard.remove(&TermIDForExit);
262 }
263 });
264
265 self.ApplicationState
266 .Feature
267 .Terminals
268 .ActiveTerminals
269 .lock()
270 .map_err(Utility::MapApplicationStateLockErrorToCommonError)?
271 .insert(TerminalIdentifier, Arc::new(std::sync::Mutex::new(TerminalState.clone())));
272
273 Ok(json!({ "id": TerminalIdentifier, "name": Name, "pid": TerminalState.OSProcessIdentifier }))
274 }
275
276 async fn SendTextToTerminal(&self, TerminalId:u64, Text:String) -> Result<(), CommonError> {
277 trace!("[TerminalProvider] Sending text to terminal ID: {}", TerminalId);
278
279 let SenderOption = {
280 let TerminalsGuard = self
281 .ApplicationState
282 .Feature
283 .Terminals
284 .ActiveTerminals
285 .lock()
286 .map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
287
288 TerminalsGuard
289 .get(&TerminalId)
290 .and_then(|TerminalArc| TerminalArc.lock().ok())
291 .and_then(|TerminalStateGuard| TerminalStateGuard.PTYInputTransmitter.clone())
292 };
293
294 if let Some(Sender) = SenderOption {
295 Sender
296 .send(Text)
297 .await
298 .map_err(|Error| CommonError::IPCError { Description:Error.to_string() })
299 } else {
300 Err(CommonError::IPCError {
301 Description:format!("Terminal with ID {} not found or has no input channel.", TerminalId),
302 })
303 }
304 }
305
306 async fn DisposeTerminal(&self, TerminalId:u64) -> Result<(), CommonError> {
307 info!("[TerminalProvider] Disposing terminal ID: {}", TerminalId);
308
309 let TerminalArc = self
310 .ApplicationState
311 .Feature
312 .Terminals
313 .ActiveTerminals
314 .lock()
315 .map_err(Utility::MapApplicationStateLockErrorToCommonError)?
316 .remove(&TerminalId);
317
318 if let Some(TerminalArc) = TerminalArc {
319 drop(TerminalArc);
322 }
323
324 Ok(())
325 }
326
327 async fn ShowTerminal(&self, TerminalId:u64, PreserveFocus:bool) -> Result<(), CommonError> {
328 info!("[TerminalProvider] Showing terminal ID: {}", TerminalId);
329
330 self.ApplicationHandle
331 .emit(
332 "sky://terminal/show",
333 json!({ "id": TerminalId, "preserveFocus": PreserveFocus }),
334 )
335 .map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })
336 }
337
338 async fn HideTerminal(&self, TerminalId:u64) -> Result<(), CommonError> {
339 info!("[TerminalProvider] Hiding terminal ID: {}", TerminalId);
340
341 self.ApplicationHandle
342 .emit("sky://terminal/hide", json!({ "id": TerminalId }))
343 .map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })
344 }
345
346 async fn GetTerminalProcessId(&self, TerminalId:u64) -> Result<Option<u32>, CommonError> {
347 let TerminalsGuard = self
348 .ApplicationState
349 .Feature
350 .Terminals
351 .ActiveTerminals
352 .lock()
353 .map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
354
355 Ok(TerminalsGuard
356 .get(&TerminalId)
357 .and_then(|t| t.lock().ok().and_then(|g| g.OSProcessIdentifier)))
358 }
359}