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//!   - Handle terminal show/hide UI state.
8//!   - Send text input to terminal processes.
9//!   - Manage terminal environment variables.
10//!   - Handle terminal resizing and dimension management.
11//!   -Support terminal profiles and configuration.
12//!   - Handle terminal process exit detection.
13//!   - Manage terminal input/output channels.
14//!   - Support terminal color schemes and themes.
15//!   - Handle terminal bell/notification support.
16//!   - Implement terminal buffer management.
17//!   - Support terminal search and navigation.
18//!   - Handle terminal clipboard operations.
19//!   - Implement terminal tab support.
20//!   - Support custom shell integration.
21//!
22//! TODOs:
23//!   - Implement terminal profile management
24//!   - Add terminal environment variable management
25//!   - Implement terminal resize handling (PtySize updates)
26//!   - Support terminal color scheme configuration
27//!   - Add terminal bell handling and visual notifications
28//!   - Implement terminal buffer scrolling and history
29//!   - Support terminal search within output
30//!   - Add terminal reconnection for crashed processes
31//!   - Implement terminal tab management
32//!   - Support terminal split view
33//!   - Add terminal decoration support (e.g., cwd indicator)
34//!   - Implement terminal command history
35//!   - Support terminal shell integration (e.g., fish, zsh, bash)
36//!   - Add terminal ANSI escape sequence handling
37//!   - Implement terminal clipboard operations
38//!   - Support terminal link detection and navigation
39//!   - Add terminal performance optimizations for large output
40//!   - Implement terminal process tree (parent/child processes)
41//!   - Support terminal environment injection
42//!   - Add terminal keyboard mapping customization
43//!   - Implement terminal logging for debugging
44//!   - Support terminal font size and font family
45//!   - Add terminal UTF-8 and Unicode support
46//!   - Implement terminal timeout and idle detection
47//!   - Support terminal command execution automation
48//!   - Add terminal multi-instance management
49//!
50//! Inspired by VSCode's integrated terminal which:
51//! - Uses native PTY for process isolation
52//! - Streams I/O to avoid blocking the main thread
53//! - Supports multiple terminal instances
54//! - Handles terminal show/hide state
55//! - Manages terminal process lifecycle
56//! - Supports terminal profiles and custom shells
57//! - Provides shell integration features
58//! # TerminalProvider Implementation
59//!
60//! Implements the `TerminalProvider` trait for the `MountainEnvironment`. This
61//! provider contains the core logic for managing integrated terminal instances,
62//! including creating native pseudo-terminals (PTYs) and handling their I/O.
63//!
64//! ## Terminal Architecture
65//!
66//! The terminal implementation uses the following architecture:
67//!
68//! 1. **PTY Creation**: Use `portable-pty` to create native PTY pairs
69//! 2. **Process Spawning**: Spawn shell process as child of PTY slave
70//! 3. **I/O Streaming**: Spawn async tasks for input and output streaming
71//! 4. **IPC Communication**: Forward output to Cocoon sidecar via IPC
72//! 5. **State Management**: Track terminal state in ApplicationState
73//!
74//! ## Terminal Lifecycle
75//!
76//! 1. **Create**: Create PTY, spawn shell, start I/O tasks
77//! 2. **SendText**: Write user input to PTY master
78//! 3. **ReceiveData**: Read output from PTY and forward to sidecar
79//! 4. **Show/Hide**: Emit UI events to show/hide terminal
80//! 5. **ProcessExit**: Detect shell exit and notify sidecar
81//! 6. **Dispose**: Close PTY, kill process, cleanup state
82//!
83//! ## Shell Detection
84//!
85//! Default shell selection by platform:
86//! - **Windows**: `powershell.exe`
87//! - **macOS/Linux**: `$SHELL` environment variable, fallback to `sh`
88//!
89//! Custom shell paths can be provided via terminal options.
90//!
91//! ## I/O Streaming
92//!
93//! Terminal I/O is handled by background tokio tasks:
94//!
95//! - **Input Task**: Receives text from channel and writes to PTY master
96//! - **Output Task**: Reads from PTY master and forwards to sidecar
97//! - **Exit Task**: Waits for process exit and notifies sidecar
98//!
99//! Each terminal gets its own I/O tasks to prevent blocking each other.
100
101use 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	/// Creates a new terminal instance, spawns a PTY, and manages its I/O.
122	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 on Ok(0) or Err
229					_ => 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			// Clean up the terminal from the state
260			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			// Dropping the PTY master's writer and reader handles will signal the
320			// underlying process to terminate.
321			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}