3030
3131use std:: io:: { self , IsTerminal , Stdout , stdout} ;
3232use std:: panic;
33+ use std:: sync:: Mutex ;
3334use std:: sync:: atomic:: { AtomicBool , Ordering } ;
3435
3536use anyhow:: Result ;
@@ -45,6 +46,9 @@ use crossterm::{
4546use ratatui:: Terminal ;
4647use ratatui:: backend:: CrosstermBackend ;
4748
49+ /// Global storage for the original terminal title to restore on exit.
50+ static ORIGINAL_TITLE : Mutex < Option < String > > = Mutex :: new ( None ) ;
51+
4852/// Track whether the panic hook has been installed to avoid installing it multiple times.
4953static PANIC_HOOK_INSTALLED : AtomicBool = AtomicBool :: new ( false ) ;
5054
@@ -59,7 +63,7 @@ static PANIC_HOOK_INSTALLED: AtomicBool = AtomicBool::new(false);
5963///
6064/// ```rust,ignore
6165/// // Guard is typically created internally by CortexTerminal
62- /// let guard = TerminalGuard::new(true, true, true);
66+ /// let guard = TerminalGuard::new(true, true, true, true );
6367/// // ... terminal operations ...
6468/// // Terminal is restored when guard is dropped
6569/// ```
@@ -70,6 +74,8 @@ pub struct TerminalGuard {
7074 mouse_capture : bool ,
7175 /// Whether bracketed paste is enabled
7276 bracketed_paste : bool ,
77+ /// Whether to restore the original title
78+ restore_title : bool ,
7379}
7480
7581impl TerminalGuard {
@@ -80,11 +86,18 @@ impl TerminalGuard {
8086 /// * `alternate_screen` - Whether alternate screen mode is enabled
8187 /// * `mouse_capture` - Whether mouse capture is enabled
8288 /// * `bracketed_paste` - Whether bracketed paste mode is enabled
83- fn new ( alternate_screen : bool , mouse_capture : bool , bracketed_paste : bool ) -> Self {
89+ /// * `restore_title` - Whether to restore the original title on cleanup
90+ fn new (
91+ alternate_screen : bool ,
92+ mouse_capture : bool ,
93+ bracketed_paste : bool ,
94+ restore_title : bool ,
95+ ) -> Self {
8496 Self {
8597 alternate_screen,
8698 mouse_capture,
8799 bracketed_paste,
100+ restore_title,
88101 }
89102 }
90103}
@@ -95,6 +108,7 @@ impl Drop for TerminalGuard {
95108 self . alternate_screen ,
96109 self . mouse_capture ,
97110 self . bracketed_paste ,
111+ self . restore_title ,
98112 ) ;
99113 }
100114}
@@ -281,10 +295,12 @@ impl CortexTerminal {
281295 let backend = CrosstermBackend :: new ( stdout ( ) ) ;
282296 let terminal = Terminal :: new ( backend) ?;
283297
298+ let restore_title = options. title . is_some ( ) ;
284299 let guard = TerminalGuard :: new (
285300 options. alternate_screen ,
286301 options. mouse_capture ,
287302 options. bracketed_paste ,
303+ restore_title,
288304 ) ;
289305
290306 Ok ( Self {
@@ -453,6 +469,24 @@ fn init_terminal(options: &TerminalOptions) -> Result<()> {
453469 // Install panic hook to restore terminal on panic
454470 install_panic_hook ( ) ;
455471
472+ // Save the original terminal title before we change it
473+ // We use the current working directory as a fallback since most terminals
474+ // set the title to something like "user@host: /path" which we can approximate
475+ if options. title . is_some ( ) {
476+ if let Ok ( mut guard) = ORIGINAL_TITLE . lock ( ) {
477+ if guard. is_none ( ) {
478+ // Try to get a reasonable default title to restore
479+ // Most terminals show something like the current directory or shell
480+ let fallback_title = std:: env:: current_dir ( )
481+ . map ( |p| p. display ( ) . to_string ( ) )
482+ . unwrap_or_else ( |_| String :: new ( ) ) ;
483+ if !fallback_title. is_empty ( ) {
484+ * guard = Some ( fallback_title) ;
485+ }
486+ }
487+ }
488+ }
489+
456490 // Enable raw mode
457491 enable_raw_mode ( ) ?;
458492
@@ -500,6 +534,7 @@ fn init_terminal(options: &TerminalOptions) -> Result<()> {
500534/// * `alternate_screen` - Whether alternate screen was enabled
501535/// * `mouse_capture` - Whether mouse capture was enabled
502536/// * `bracketed_paste` - Whether bracketed paste was enabled
537+ /// * `restore_title` - Whether to restore the original window title
503538///
504539/// # Errors
505540///
@@ -510,6 +545,7 @@ fn restore_terminal_impl(
510545 alternate_screen : bool ,
511546 mouse_capture : bool ,
512547 bracketed_paste : bool ,
548+ restore_title : bool ,
513549) -> Result < ( ) > {
514550 let mut stdout = stdout ( ) ;
515551
@@ -531,6 +567,15 @@ fn restore_terminal_impl(
531567 execute ! ( stdout, LeaveAlternateScreen ) ?;
532568 }
533569
570+ // Restore original terminal title if we saved one
571+ if restore_title {
572+ if let Ok ( guard) = ORIGINAL_TITLE . lock ( ) {
573+ if let Some ( ref title) = * guard {
574+ let _ = execute ! ( stdout, SetTitle ( title) ) ;
575+ }
576+ }
577+ }
578+
534579 // Disable raw mode
535580 disable_raw_mode ( ) ?;
536581
@@ -546,7 +591,7 @@ fn restore_terminal_impl(
546591///
547592/// Returns an error if any terminal operation fails.
548593pub fn restore_terminal ( ) -> Result < ( ) > {
549- restore_terminal_impl ( true , true , true )
594+ restore_terminal_impl ( true , true , true , true )
550595}
551596
552597/// Install a panic hook that restores the terminal.
@@ -688,6 +733,82 @@ pub fn supports_true_color() -> bool {
688733 . unwrap_or ( false )
689734}
690735
736+ /// Check if clipboard is available.
737+ ///
738+ /// This checks if the system clipboard can be accessed without relying on
739+ /// terminal escape sequences (OSC 52), which may leak as raw text in
740+ /// unsupported terminals like older PuTTY over SSH.
741+ ///
742+ /// Returns `true` if a native clipboard is available (X11, Wayland, macOS, Windows).
743+ /// Returns `false` if we're likely in an SSH session without forwarding or
744+ /// in a terminal that doesn't have clipboard access.
745+ ///
746+ /// Note: We never use OSC 52 escape sequences to avoid leaking raw escape
747+ /// sequences as visible text when the terminal doesn't support them.
748+ pub fn is_clipboard_available ( ) -> bool {
749+ // arboard will fail if no clipboard is available
750+ // This is a quick check - actual clipboard operations may still fail
751+ arboard:: Clipboard :: new ( ) . is_ok ( )
752+ }
753+
754+ /// Safely copy text to clipboard without OSC 52 escape sequences.
755+ ///
756+ /// This function uses native clipboard APIs (X11/Wayland on Linux, native on macOS/Windows)
757+ /// and never writes OSC 52 escape sequences that could leak as raw text in unsupported terminals.
758+ ///
759+ /// # Arguments
760+ ///
761+ /// * `text` - The text to copy to the clipboard
762+ ///
763+ /// # Returns
764+ ///
765+ /// Returns `true` if the text was successfully copied, `false` otherwise.
766+ /// Failures are logged as warnings but don't cause errors.
767+ pub fn safe_clipboard_copy ( text : & str ) -> bool {
768+ match arboard:: Clipboard :: new ( ) {
769+ Ok ( mut clipboard) => {
770+ #[ cfg( target_os = "linux" ) ]
771+ {
772+ use arboard:: SetExtLinux ;
773+ // On Linux, use wait() to ensure the clipboard manager receives the data
774+ match clipboard. set ( ) . wait ( ) . text ( text) {
775+ Ok ( _) => true ,
776+ Err ( e) => {
777+ tracing:: warn!( "Clipboard copy failed: {}" , e) ;
778+ false
779+ }
780+ }
781+ }
782+ #[ cfg( not( target_os = "linux" ) ) ]
783+ {
784+ match clipboard. set_text ( text) {
785+ Ok ( _) => true ,
786+ Err ( e) => {
787+ tracing:: warn!( "Clipboard copy failed: {}" , e) ;
788+ false
789+ }
790+ }
791+ }
792+ }
793+ Err ( e) => {
794+ tracing:: debug!( "Clipboard unavailable: {}" , e) ;
795+ false
796+ }
797+ }
798+ }
799+
800+ /// Safely read text from clipboard.
801+ ///
802+ /// # Returns
803+ ///
804+ /// Returns the clipboard text content if available, None otherwise.
805+ pub fn safe_clipboard_paste ( ) -> Option < String > {
806+ match arboard:: Clipboard :: new ( ) {
807+ Ok ( mut clipboard) => clipboard. get_text ( ) . ok ( ) ,
808+ Err ( _) => None ,
809+ }
810+ }
811+
691812#[ cfg( test) ]
692813mod tests {
693814 use super :: * ;
@@ -730,10 +851,11 @@ mod tests {
730851
731852 #[ test]
732853 fn test_terminal_guard_creation ( ) {
733- let guard = TerminalGuard :: new ( true , true , true ) ;
854+ let guard = TerminalGuard :: new ( true , true , true , true ) ;
734855 assert ! ( guard. alternate_screen) ;
735856 assert ! ( guard. mouse_capture) ;
736857 assert ! ( guard. bracketed_paste) ;
858+ assert ! ( guard. restore_title) ;
737859 }
738860
739861 // Note: Tests that actually create terminals are difficult to run
0 commit comments