@@ -21,19 +21,21 @@ use gpui::{
2121 DispatchPhase , Entity , EntityInputHandler , FocusHandle , Focusable , Hitbox , HitboxBehavior ,
2222 Hsla , InteractiveElement , IntoElement , KeyBinding , KeyDownEvent , MouseDownEvent ,
2323 MouseMoveEvent , MouseUpEvent , ParentElement , Pixels , Point , Refineable , Render , ScrollDelta ,
24- ScrollWheelEvent , Style , StyleRefinement , Styled , TextAlign , UTF16Selection , Window ,
25- WrappedLine , actions, canvas, div, point, px, quad, rgb, size, transparent_black,
24+ ScrollWheelEvent , Style , StyleRefinement , Styled , TextAlign , UTF16Selection , WeakEntity ,
25+ Window , WrappedLine , actions, canvas, div, point, px, quad, rgb, size, transparent_black,
2626} ;
2727use portable_pty:: { CommandBuilder , PtySize , native_pty_system} ;
2828use std:: cell:: RefCell ;
2929use std:: cmp:: Ordering ;
3030use std:: io:: { Read , Write } ;
3131use std:: ops:: { Range , Rem } ;
32+ use std:: path:: PathBuf ;
3233use std:: rc:: Rc ;
3334use std:: thread;
3435use std:: time:: Instant ;
3536use sysinfo:: { ProcessRefreshKind , RefreshKind , System } ;
3637use tracing:: { info, warn} ;
38+ use url:: Url ;
3739use vt100:: { Callbacks , Cell , Parser , Screen } ;
3840
3941actions ! ( terminal_screen, [ Backspace , Delete , Left , Right ] ) ;
@@ -56,6 +58,7 @@ pub struct TerminalScreen {
5658 color_scheme : ColorScheme ,
5759 keyboard : Keyboard ,
5860 title : String ,
61+ working_directory : Option < PathBuf > ,
5962 events : TerminalScreenEvents ,
6063 shell_pid : Option < u32 > ,
6164 timer : Instant ,
@@ -96,13 +99,16 @@ impl TerminalScreen {
9699 let ( tx_read, rx_read) = async_channel:: bounded ( 1 ) ;
97100 let ( tx_write, rx_write) = async_channel:: bounded ( 1 ) ;
98101
102+ let terminal_screen = cx. entity ( ) ;
103+
99104 let parser = cx. new ( |cx| {
100105 let screen_size = screen_size_entity. read ( cx) ;
101106 let mut parser = Parser :: new_with_callbacks (
102107 screen_size. lines ,
103108 screen_size. columns ,
104109 1000 ,
105110 TerminalScreenCallbacks {
111+ terminal_screen,
106112 tx_write,
107113 events : events. clone ( ) ,
108114 cx : cx. to_async ( ) ,
@@ -119,10 +125,9 @@ impl TerminalScreen {
119125 Ok ( pty_pair) => pty_pair,
120126 Err ( error) => {
121127 warn ! ( "Unable to open pty: {error}" ) ;
122- parser. process (
123- tr ! ( "PTY_OPEN_ERROR" , "Unable to open pty" )
124- . to_string ( )
125- . as_bytes ( ) ,
128+ print_system_message (
129+ & mut parser,
130+ tr ! ( "PTY_OPEN_ERROR" , "Unable to open pty" ) . to_string ( ) ,
126131 ) ;
127132 return parser;
128133 }
@@ -133,14 +138,44 @@ impl TerminalScreen {
133138 match pty_pair. slave . spawn_command ( cmd) {
134139 Err ( error) => {
135140 warn ! ( "Unable to spawn process: {error}" ) ;
136- parser. process (
137- tr ! ( "PTY_SPAWN_ERROR" , "Unable to spawn process" )
138- . to_string ( )
139- . as_bytes ( ) ,
141+ print_system_message (
142+ & mut parser,
143+ tr ! ( "PTY_SPAWN_ERROR" , "Unable to spawn process" ) . to_string ( ) ,
140144 ) ;
141145 return parser;
142146 }
143- Ok ( child) => shell_pid = child. process_id ( ) ,
147+ Ok ( mut child) => {
148+ shell_pid = child. process_id ( ) ;
149+
150+ let ( tx_dead, rx_dead) = async_channel:: bounded ( 1 ) ;
151+
152+ thread:: spawn ( move || {
153+ let exit_status = child. wait ( ) . unwrap ( ) ;
154+ smol:: block_on ( tx_dead. send ( exit_status) ) . unwrap ( ) ;
155+ } ) ;
156+ cx. spawn (
157+ async move |weak_parser : WeakEntity <
158+ Parser < TerminalScreenCallbacks > ,
159+ > ,
160+ cx : & mut AsyncApp | {
161+ let exit_status = rx_dead. recv ( ) . await . unwrap ( ) ;
162+ weak_parser
163+ . update ( cx, |parser, cx| {
164+ print_system_message (
165+ parser,
166+ tr ! (
167+ "PTY_EXITED" ,
168+ "Command exited with exit code {{exit_code}}" ,
169+ exit_code = exit_status. exit_code( )
170+ )
171+ . to_string ( ) ,
172+ ) ;
173+ } )
174+ . unwrap ( ) ;
175+ } ,
176+ )
177+ . detach ( ) ;
178+ }
144179 }
145180
146181 let mut reader = pty_pair. master . try_clone_reader ( ) . unwrap ( ) ;
@@ -210,6 +245,7 @@ impl TerminalScreen {
210245 color_scheme : ColorScheme :: default ( ) ,
211246 keyboard : Keyboard :: default ( ) ,
212247 title : tr ! ( "TERMINAL_DEFAULT_TITLE" , "Terminal" ) . to_string ( ) ,
248+ working_directory : None ,
213249 events,
214250 shell_pid,
215251 timer : Instant :: now ( ) ,
@@ -356,6 +392,10 @@ impl TerminalScreen {
356392 self . title . clone ( )
357393 }
358394
395+ pub fn working_directory ( & self ) -> Option < PathBuf > {
396+ self . working_directory . clone ( )
397+ }
398+
359399 pub fn request_close ( & mut self , window : & mut Window , cx : & mut Context < Self > ) {
360400 let system = System :: new_with_specifics (
361401 RefreshKind :: nothing ( ) . with_processes ( ProcessRefreshKind :: everything ( ) ) ,
@@ -820,6 +860,7 @@ fn paint_terminal_screen(
820860}
821861
822862struct TerminalScreenCallbacks {
863+ terminal_screen : Entity < TerminalScreen > ,
823864 tx_write : Sender < Vec < u8 > > ,
824865 events : TerminalScreenEvents ,
825866 cx : AsyncApp ,
@@ -835,6 +876,50 @@ impl Callbacks for TerminalScreenCallbacks {
835876 . detach ( )
836877 }
837878
879+ fn set_window_title ( & mut self , _: & mut Screen , title : & [ u8 ] ) {
880+ let title = String :: from_utf8_lossy ( title) . to_string ( ) ;
881+
882+ let terminal_screen = self . terminal_screen . clone ( ) ;
883+
884+ // Spawn in a background task to avoid a double borrow
885+ self . cx
886+ . spawn ( async move |cx : & mut AsyncApp | {
887+ cx. update_entity ( & terminal_screen, |terminal_screen, cx| {
888+ terminal_screen. title = title;
889+ cx. notify ( ) ;
890+ } )
891+ . unwrap ( ) ;
892+ } )
893+ . detach ( )
894+ }
895+
896+ fn set_working_directory ( & mut self , _: & mut Screen , working_directory : & [ u8 ] ) {
897+ let working_directory_string = String :: from_utf8_lossy ( working_directory) . to_string ( ) ;
898+ let Ok ( mut url) = Url :: parse ( working_directory_string. as_str ( ) ) else {
899+ return ;
900+ } ;
901+ let _ = url. set_host ( None ) ;
902+
903+ // Re-parse the URL because removing the host in a file: URL doesn't work correctly
904+ let url = Url :: parse ( url. as_str ( ) ) . unwrap ( ) ;
905+ let Ok ( path) = url. to_file_path ( ) else {
906+ return ;
907+ } ;
908+
909+ let terminal_screen = self . terminal_screen . clone ( ) ;
910+
911+ // Spawn in a background task to avoid a double borrow
912+ self . cx
913+ . spawn ( async move |cx : & mut AsyncApp | {
914+ cx. update_entity ( & terminal_screen, |terminal_screen, cx| {
915+ terminal_screen. working_directory = Some ( path) ;
916+ cx. notify ( ) ;
917+ } )
918+ . unwrap ( ) ;
919+ } )
920+ . detach ( )
921+ }
922+
838923 fn unhandled_control ( & mut self , _: & mut Screen , b : u8 ) {
839924 warn ! ( "Unhandled control: {b:?}" ) ;
840925 }
@@ -872,6 +957,12 @@ impl Callbacks for TerminalScreenCallbacks {
872957 }
873958}
874959
960+ fn print_system_message < T : Callbacks > ( parser : & mut Parser < T > , message : String ) {
961+ parser. process ( b"\n \x1B [7m[" ) ;
962+ parser. process ( message. as_bytes ( ) ) ;
963+ parser. process ( b"]" ) ;
964+ }
965+
875966#[ cfg( target_os = "windows" ) ]
876967fn default_shell ( ) -> String {
877968 "C:\\ Windows\\ System32\\ WindowsPowerShell\\ v1.0\\ powershell.exe" . to_string ( )
0 commit comments