1use std::{env, io::Write, sync::Arc};
51
52use CommonLibrary::{
53 Environment::Requires::Requires,
54 Error::CommonError::CommonError,
55 IPC::{IPCProvider::IPCProvider, SkyEvent::SkyEvent},
56 Terminal::TerminalProvider::TerminalProvider,
57};
58use async_trait::async_trait;
59use portable_pty::{CommandBuilder, MasterPty, NativePtySystem, PtySize, PtySystem};
60use serde_json::{Value, json};
61use tauri::Emitter;
62use tokio::sync::mpsc as TokioMPSC;
63
64use super::{MountainEnvironment::MountainEnvironment, Utility};
65use crate::{ApplicationState::DTO::TerminalStateDTO::TerminalStateDTO, IPC::SkyEmit::LogSkyEmit, dev_log};
66
67const MAX_BUFFERED_BYTES:usize = 64 * 1024;
81
82static TERMINAL_OUTPUT_BUFFER:std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<u64, Vec<u8>>>> =
83 std::sync::OnceLock::new();
84
85fn TerminalOutputBuffer() -> &'static std::sync::Mutex<std::collections::HashMap<u64, Vec<u8>>> {
86 TERMINAL_OUTPUT_BUFFER.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
87}
88
89pub(crate) fn AppendTerminalOutput(TerminalId:u64, Bytes:&[u8]) {
90 if let Ok(mut Map) = TerminalOutputBuffer().lock() {
91 let Entry = Map.entry(TerminalId).or_insert_with(Vec::new);
92
93 Entry.extend_from_slice(Bytes);
94
95 if Entry.len() > MAX_BUFFERED_BYTES {
98 let DropCount = Entry.len() - MAX_BUFFERED_BYTES;
99
100 Entry.drain(..DropCount);
101 }
102 }
103}
104
105pub fn Fn() -> Vec<(u64, Vec<u8>)> {
106 if let Ok(Map) = TerminalOutputBuffer().lock() {
107 Map.iter().map(|(K, V)| (*K, V.clone())).collect()
108 } else {
109 Vec::new()
110 }
111}
112
113pub(crate) fn RemoveTerminalOutputBuffer(TerminalId:u64) {
114 if let Ok(mut Map) = TerminalOutputBuffer().lock() {
115 Map.remove(&TerminalId);
116 }
117}
118
119#[async_trait]
126impl TerminalProvider for MountainEnvironment {
127 async fn CreateTerminal(&self, OptionsValue:Value) -> Result<Value, CommonError> {
129 let TerminalIdentifier = self.ApplicationState.GetNextTerminalIdentifier();
130
131 let DefaultShell = if cfg!(windows) {
132 "powershell.exe".to_string()
133 } else {
134 env::var("SHELL").unwrap_or_else(|_| "sh".to_string())
135 };
136
137 let Name = OptionsValue
138 .get("name")
139 .and_then(Value::as_str)
140 .unwrap_or("terminal")
141 .to_string();
142
143 dev_log!(
144 "terminal",
145 "[TerminalProvider] Creating terminal ID: {}, Name: '{}'",
146 TerminalIdentifier,
147 Name
148 );
149
150 let mut TerminalState = TerminalStateDTO::Create(TerminalIdentifier, Name.clone(), &OptionsValue, DefaultShell)
151 .map_err(|e| {
152 CommonError::ConfigurationLoad { Description:format!("Failed to create terminal state: {}", e) }
153 })?;
154
155 let PtySystem = NativePtySystem::default();
156
157 let PtyPair = PtySystem
158 .openpty(PtySize::default())
159 .map_err(|Error| CommonError::IPCError { Description:format!("Failed to open PTY: {}", Error) })?;
160
161 let mut Command = CommandBuilder::new(&TerminalState.ShellPath);
162
163 let mut MergedEnv:std::collections::HashMap<String, String> = std::env::vars().collect();
170
171 super::TerminalEnvCollection::ApplyToEnv(&mut MergedEnv);
176
177 if let Some(Injection) =
181 super::Terminal::ShellIntegration::Compute(&self.ApplicationHandle, &TerminalState.ShellPath)
182 {
183 for (Key, Val) in Injection.EnvVars {
184 MergedEnv.insert(Key, Val);
185 }
186
187 let mut AllArgs = Injection.PrependArgs;
190
191 AllArgs.extend(TerminalState.ShellArguments.iter().cloned());
192
193 AllArgs.extend(Injection.AppendArgs);
194
195 Command.args(&AllArgs);
196 } else {
197 Command.args(&TerminalState.ShellArguments);
198 }
199
200 for (Key, Val) in &MergedEnv {
203 Command.env(Key, Val);
204 }
205
206 if let Some(CWD) = &TerminalState.CurrentWorkingDirectory {
207 Command.cwd(CWD);
208 }
209
210 let mut ChildProcess = PtyPair.slave.spawn_command(Command).map_err(|Error| {
211 CommonError::IPCError { Description:format!("Failed to spawn shell process: {}", Error) }
212 })?;
213
214 TerminalState.OSProcessIdentifier = ChildProcess.process_id();
215
216 let mut PTYWriter = PtyPair.master.take_writer().map_err(|Error| {
217 CommonError::FileSystemIO {
218 Path:"pty master".into(),
219
220 Description:format!("Failed to take PTY writer: {}", Error),
221 }
222 })?;
223
224 let (InputTransmitter, mut InputReceiver) = TokioMPSC::channel::<String>(32);
225
226 TerminalState.PTYInputTransmitter = Some(InputTransmitter);
227
228 let TermIDForInput = TerminalIdentifier;
229
230 tokio::spawn(async move {
231 while let Some(Data) = InputReceiver.recv().await {
232 if let Err(Error) = PTYWriter.write_all(Data.as_bytes()) {
233 dev_log!(
234 "terminal",
235 "error: [TerminalProvider] PTY write failed for ID {}: {}",
236 TermIDForInput,
237 Error
238 );
239
240 break;
241 }
242 }
243 });
244
245 let mut PTYReader = PtyPair.master.try_clone_reader().map_err(|Error| {
246 CommonError::FileSystemIO {
247 Path:"pty master".into(),
248
249 Description:format!("Failed to clone PTY reader: {}", Error),
250 }
251 })?;
252
253 let PTYMasterHandle:crate::ApplicationState::DTO::TerminalStateDTO::PtyMasterHandle =
257 Arc::new(std::sync::Mutex::new(PtyPair.master));
258
259 TerminalState.PTYMaster = Some(PTYMasterHandle);
260
261 let IPCProvider:Arc<dyn IPCProvider> = self.Require();
262
263 let TermIDForOutput = TerminalIdentifier;
264
265 let AppHandleForOutput = self.ApplicationHandle.clone();
266
267 tokio::spawn(async move {
268 let mut Buffer = [0u8; 8192];
269
270 loop {
271 match PTYReader.read(&mut Buffer) {
272 Ok(count) if count > 0 => {
273 AppendTerminalOutput(TermIDForOutput, &Buffer[..count]);
280
281 let DataString = String::from_utf8_lossy(&Buffer[..count]).to_string();
282
283 let Payload = json!([TermIDForOutput, DataString.clone()]);
296
297 if let Err(Error) = IPCProvider
298 .SendNotificationToSideCar(
299 "cocoon-main".into(),
300 "$acceptTerminalProcessData".into(),
301 Payload,
302 )
303 .await
304 {
305 dev_log!(
306 "terminal",
307 "warn: [TerminalProvider] Failed to send process data for ID {}: {}",
308 TermIDForOutput,
309 Error
310 );
311 }
312
313 if let Err(Error) = AppHandleForOutput.emit(
314 SkyEvent::TerminalData.AsStr(),
315 json!({
316 "id": TermIDForOutput,
317 "data": DataString,
318 }),
319 ) {
320 dev_log!(
321 "terminal",
322 "warn: [TerminalProvider] sky://terminal/data emit failed for ID {}: {}",
323 TermIDForOutput,
324 Error
325 );
326 }
327 },
328
329 _ => break,
331 }
332 }
333 });
334
335 let TermIDForExit = TerminalIdentifier;
336
337 let PidForExit = ChildProcess.process_id();
346
347 let EnvironmentClone = self.clone();
348
349 tokio::spawn(async move {
350 let ExitStatus = ChildProcess.wait();
351
352 let StatusSummary = match &ExitStatus {
357 Ok(Code) => format!("exited {:?}", Code),
358 Err(Error) => format!("wait failed: {}", Error),
359 };
360
361 dev_log!(
362 "terminal",
363 "[TerminalProvider] Process for terminal ID {} pid={:?} {}",
364 TermIDForExit,
365 PidForExit,
366 StatusSummary
367 );
368
369 let IPCProvider:Arc<dyn IPCProvider> = EnvironmentClone.Require();
370
371 if let Err(Error) = IPCProvider
372 .SendNotificationToSideCar(
373 "cocoon-main".into(),
374 "$acceptTerminalProcessExit".into(),
375 json!([TermIDForExit]),
376 )
377 .await
378 {
379 dev_log!(
380 "terminal",
381 "warn: [TerminalProvider] Failed to send process exit notification for ID {}: {}",
382 TermIDForExit,
383 Error
384 );
385 }
386
387 if let Ok(mut Guard) = EnvironmentClone.ApplicationState.Feature.Terminals.ActiveTerminals.lock() {
389 Guard.remove(&TermIDForExit);
390 }
391
392 RemoveTerminalOutputBuffer(TermIDForExit);
395
396 if let Err(Error) = LogSkyEmit(
401 &EnvironmentClone.ApplicationHandle,
402 SkyEvent::TerminalExit.AsStr(),
403 json!({ "id": TermIDForExit }),
404 ) {
405 dev_log!(
406 "terminal",
407 "warn: [TerminalProvider] sky://terminal/exit emit failed for ID {}: {}",
408 TermIDForExit,
409 Error
410 );
411 }
412
413 let _ = ::Vine::Client::SendNotification::Fn(
417 "cocoon-main".to_string(),
418 "$acceptTerminalClosed".to_string(),
419 serde_json::json!({ "id": TermIDForExit }),
420 )
421 .await;
422 });
423
424 self.ApplicationState
425 .Feature
426 .Terminals
427 .ActiveTerminals
428 .lock()
429 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
430 .insert(TerminalIdentifier, Arc::new(std::sync::Mutex::new(TerminalState.clone())));
431
432 let CreateAppHandle = self.ApplicationHandle.clone();
460
461 let CreateTermId = TerminalIdentifier;
462
463 let CreateName = Name.clone();
464
465 let CreatePid = TerminalState.OSProcessIdentifier;
466
467 tokio::spawn(async move {
468 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
473
474 let CreatePayload = json!({
475 "id": CreateTermId,
476 "name": CreateName.clone(),
477 "pid": CreatePid,
478 });
479
480 if let Err(Error) = LogSkyEmit(&CreateAppHandle, SkyEvent::TerminalCreate.AsStr(), CreatePayload.clone()) {
486 dev_log!(
487 "terminal",
488 "warn: [TerminalProvider] sky://terminal/create emit failed for ID {}: {}",
489 CreateTermId,
490 Error
491 );
492 }
493
494 if let Err(E) = ::Vine::Client::SendNotification::Fn(
499 "cocoon-main".to_string(),
500 "$acceptTerminalOpened".to_string(),
501 serde_json::json!({ "id": CreateTermId, "name": CreateName, "pid": CreatePid }),
502 )
503 .await
504 {
505 dev_log!(
506 "terminal",
507 "warn: [TerminalProvider] $acceptTerminalOpened notify failed ID={}: {}",
508 CreateTermId,
509 E
510 );
511 }
512 });
513
514 dev_log!(
515 "terminal",
516 "[TerminalProvider] localPty:spawn OK id={} pid={:?}",
517 TerminalIdentifier,
518 TerminalState.OSProcessIdentifier
519 );
520
521 Ok(json!({ "id": TerminalIdentifier, "name": Name, "pid": TerminalState.OSProcessIdentifier }))
522 }
523
524 async fn SendTextToTerminal(&self, TerminalId:u64, Text:String) -> Result<(), CommonError> {
525 dev_log!("terminal", "[TerminalProvider] Sending text to terminal ID: {}", TerminalId);
526
527 let SenderOption = {
528 let TerminalsGuard = self
529 .ApplicationState
530 .Feature
531 .Terminals
532 .ActiveTerminals
533 .lock()
534 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
535
536 TerminalsGuard
537 .get(&TerminalId)
538 .and_then(|TerminalArc| TerminalArc.lock().ok())
539 .and_then(|TerminalStateGuard| TerminalStateGuard.PTYInputTransmitter.clone())
540 };
541
542 if let Some(Sender) = SenderOption {
543 Sender
544 .send(Text)
545 .await
546 .map_err(|Error| CommonError::IPCError { Description:Error.to_string() })
547 } else {
548 Err(CommonError::IPCError {
549 Description:format!("Terminal with ID {} not found or has no input channel.", TerminalId),
550 })
551 }
552 }
553
554 async fn DisposeTerminal(&self, TerminalId:u64) -> Result<(), CommonError> {
555 dev_log!("terminal", "[TerminalProvider] Disposing terminal ID: {}", TerminalId);
556
557 let TerminalArc = self
558 .ApplicationState
559 .Feature
560 .Terminals
561 .ActiveTerminals
562 .lock()
563 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
564 .remove(&TerminalId);
565
566 if let Some(TerminalArc) = TerminalArc {
567 drop(TerminalArc);
570 }
571
572 Ok(())
573 }
574
575 async fn ShowTerminal(&self, TerminalId:u64, PreserveFocus:bool) -> Result<(), CommonError> {
576 dev_log!("terminal", "[TerminalProvider] Showing terminal ID: {}", TerminalId);
577
578 self.ApplicationHandle
579 .emit(
580 SkyEvent::TerminalShow.AsStr(),
581 json!({ "id": TerminalId, "preserveFocus": PreserveFocus }),
582 )
583 .map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })
584 }
585
586 async fn HideTerminal(&self, TerminalId:u64) -> Result<(), CommonError> {
587 dev_log!("terminal", "[TerminalProvider] Hiding terminal ID: {}", TerminalId);
588
589 LogSkyEmit(
592 &self.ApplicationHandle,
593 SkyEvent::TerminalHide.AsStr(),
594 json!({ "id": TerminalId }),
595 )
596 .map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })
597 }
598
599 async fn GetTerminalProcessId(&self, TerminalId:u64) -> Result<Option<u32>, CommonError> {
600 let TerminalsGuard = self
601 .ApplicationState
602 .Feature
603 .Terminals
604 .ActiveTerminals
605 .lock()
606 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
607
608 Ok(TerminalsGuard
609 .get(&TerminalId)
610 .and_then(|t| t.lock().ok().and_then(|g| g.OSProcessIdentifier)))
611 }
612
613 async fn ResizeTerminal(&self, TerminalId:u64, Columns:u16, Rows:u16) -> Result<(), CommonError> {
614 if Columns == 0 || Rows == 0 {
615 return Err(CommonError::InvalidArgument {
616 ArgumentName:"Columns/Rows".to_string(),
617 Reason:format!("Columns and Rows must be ≥ 1 (got {}×{})", Columns, Rows),
618 });
619 }
620
621 let MasterOption = {
624 let TerminalsGuard = self
625 .ApplicationState
626 .Feature
627 .Terminals
628 .ActiveTerminals
629 .lock()
630 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
631
632 TerminalsGuard
633 .get(&TerminalId)
634 .and_then(|TerminalArc| TerminalArc.lock().ok())
635 .and_then(|TerminalStateGuard| TerminalStateGuard.PTYMaster.clone())
636 };
637
638 let Master = MasterOption.ok_or_else(|| {
639 CommonError::IPCError {
640 Description:format!("Terminal with ID {} not found or has no PTY master handle.", TerminalId),
641 }
642 })?;
643
644 let Size = PtySize { rows:Rows, cols:Columns, pixel_width:0, pixel_height:0 };
645
646 tokio::task::spawn_blocking(move || {
652 let Guard = Master.lock().map_err(|_| "PTY master mutex poisoned".to_string())?;
653
654 Guard.resize(Size).map_err(|Error| Error.to_string())
655 })
656 .await
657 .map_err(|Error| CommonError::IPCError { Description:format!("resize join error: {}", Error) })?
658 .map_err(|Error| CommonError::IPCError { Description:format!("PTY resize failed: {}", Error) })?;
659
660 dev_log!(
661 "terminal",
662 "[TerminalProvider] Resized terminal ID {} to {}×{}",
663 TerminalId,
664 Columns,
665 Rows
666 );
667
668 Ok(())
669 }
670}