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