Mountain/Environment/
TerminalProvider.rs

1// File: Mountain/Source/Environment/TerminalProvider.rs
2// Role: Implements the `TerminalProvider` trait for the `MountainEnvironment`.
3// Responsibilities:
4//   - Core logic for managing integrated terminal instances.
5//   - Creating native pseudo-terminals (PTYs) and handling their I/O.
6//   - Spawning and managing the lifecycle of the underlying shell processes.
7
8//! # TerminalProvider Implementation
9//!
10//! Implements the `TerminalProvider` trait for the `MountainEnvironment`. This
11//! provider contains the core logic for managing integrated terminal instances,
12//! including creating native pseudo-terminals (PTYs) and handling their I/O.
13
14#![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	/// Creates a new terminal instance, spawns a PTY, and manages its I/O.
37	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 on Ok(0) or Err
141					_ => 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			// Clean up the terminal from the state
172			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			// Dropping the PTY master's writer and reader handles will signal the
226			// underlying process to terminate.
227			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}