@@ -10,6 +10,9 @@ use tokio_util::sync::CancellationToken;
1010use vite_error:: Error ;
1111use vite_path:: { AbsolutePath , AbsolutePathBuf , RelativePathBuf } ;
1212
13+ #[ cfg( unix) ]
14+ use std:: os:: fd:: { BorrowedFd , RawFd } ;
15+
1316/// Result of running a command with fspy tracking.
1417#[ derive( Debug ) ]
1518pub struct FspyCommandResult {
@@ -59,6 +62,32 @@ pub fn build_command(bin_path: &AbsolutePath, cwd: &AbsolutePath) -> Command {
5962 cmd
6063}
6164
65+ /// Execute a command while preserving terminal state.
66+ /// This prevents escape sequences from appearing in the prompt when the child process
67+ /// is interrupted (e.g., via Ctrl+C) while the terminal is in a non-standard state.
68+ ///
69+ /// On Unix, saves the terminal state before spawning the child process and restores
70+ /// it after the child exits. On Windows, this is a simple pass-through.
71+ pub async fn execute_with_terminal_guard ( mut cmd : Command ) -> Result < ExitStatus , Error > {
72+ #[ cfg( unix) ]
73+ {
74+ use nix:: libc:: STDIN_FILENO ;
75+
76+ // Save terminal state before spawning child
77+ let _guard = TerminalStateGuard :: save ( STDIN_FILENO ) ;
78+
79+ // Spawn and wait for child - guard will restore terminal state on drop
80+ let mut child = cmd. spawn ( ) . map_err ( |e| Error :: Anyhow ( e. into ( ) ) ) ?;
81+ child. wait ( ) . await . map_err ( |e| Error :: Anyhow ( e. into ( ) ) )
82+ }
83+
84+ #[ cfg( not( unix) ) ]
85+ {
86+ let mut child = cmd. spawn ( ) . map_err ( |e| Error :: Anyhow ( e. into ( ) ) ) ?;
87+ child. wait ( ) . await . map_err ( |e| Error :: Anyhow ( e. into ( ) ) )
88+ }
89+ }
90+
6291/// Build a `tokio::process::Command` for shell execution.
6392/// Uses `/bin/sh -c` on Unix, `cmd.exe /C` on Windows.
6493pub fn build_shell_command ( shell_cmd : & str , cwd : & AbsolutePath ) -> Command {
@@ -230,6 +259,50 @@ pub fn fix_stdio_streams() {
230259 clear_cloexec ( unsafe { BorrowedFd :: borrow_raw ( STDERR_FILENO ) } ) ;
231260}
232261
262+ /// Guard that saves terminal state and restores it on drop.
263+ /// This prevents escape sequences from appearing in the prompt when a child process
264+ /// is interrupted (e.g., via Ctrl+C) while the terminal is in a non-standard state.
265+ #[ cfg( unix) ]
266+ struct TerminalStateGuard {
267+ fd : RawFd ,
268+ original : nix:: sys:: termios:: Termios ,
269+ }
270+
271+ #[ cfg( unix) ]
272+ impl TerminalStateGuard {
273+ /// Save the current terminal state for the given file descriptor.
274+ /// Returns None if the fd is not a terminal or if saving fails.
275+ fn save ( fd : RawFd ) -> Option < Self > {
276+ use nix:: sys:: termios:: tcgetattr;
277+
278+ // SAFETY: fd comes from a valid stdin/stdout/stderr file descriptor
279+ let borrowed_fd = unsafe { BorrowedFd :: borrow_raw ( fd) } ;
280+
281+ // Only save state if this is actually a terminal
282+ if !nix:: unistd:: isatty ( borrowed_fd) . unwrap_or ( false ) {
283+ return None ;
284+ }
285+
286+ match tcgetattr ( borrowed_fd) {
287+ Ok ( original) => Some ( Self { fd, original } ) ,
288+ Err ( _) => None ,
289+ }
290+ }
291+ }
292+
293+ #[ cfg( unix) ]
294+ impl Drop for TerminalStateGuard {
295+ fn drop ( & mut self ) {
296+ use nix:: sys:: termios:: { SetArg , tcsetattr} ;
297+
298+ // SAFETY: fd comes from stdin/stdout/stderr and the guard does not outlive the process
299+ let borrowed_fd = unsafe { BorrowedFd :: borrow_raw ( self . fd ) } ;
300+
301+ // Best effort: ignore errors during cleanup
302+ let _ = tcsetattr ( borrowed_fd, SetArg :: TCSANOW , & self . original ) ;
303+ }
304+ }
305+
233306#[ cfg( test) ]
234307mod tests {
235308 use tempfile:: { TempDir , tempdir} ;
0 commit comments