Skip to main content

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 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	/// Creates a new terminal instance, spawns a PTY, and manages its I/O.
121	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 on Ok(0) or Err
237					_ => 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			// Clean up the terminal from the state
274			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			// Dropping the PTY master's writer and reader handles will signal the
334			// underlying process to terminate.
335			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}