Skip to content

Commit 7ec00bd

Browse files
committed
Handle shell exit
1 parent 8894949 commit 7ec00bd

File tree

6 files changed

+142
-24
lines changed

6 files changed

+142
-24
lines changed

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

theterminal/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ ini = { workspace = true }
1818
serde = { workspace = true }
1919
serde_json = { workspace = true }
2020
sysinfo = { workspace = true }
21+
url = "2.5.7"
2122

2223
[target.'cfg(target_os = "macos")'.dependencies]
2324
objc2-open-directory = { workspace = true }

theterminal/src/main_surface.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,18 +126,36 @@ impl Render for MainSurface {
126126
.content_stretch(),
127127
|david, (i, terminal_screen)| {
128128
let terminal_screen = terminal_screen.read(cx);
129+
130+
let tab_subtext = terminal_screen
131+
.working_directory()
132+
.and_then(|path| {
133+
path.iter()
134+
.next_back()
135+
.map(|str| str.to_string_lossy().to_string())
136+
})
137+
.unwrap_or_else(|| "".to_string());
138+
129139
david.child(
130140
button(i)
131141
.child(
132142
div()
133143
.flex()
134144
.flex_col()
135-
.child(terminal_screen.title())
136-
.child(
137-
div()
138-
.text_size(theme.system_font_size * 0.6)
139-
.child("Bottom Text"),
140-
),
145+
.child(if terminal_screen.title().is_empty() {
146+
tr!("TERMINAL_DEFAULT_TITLE").to_string()
147+
} else {
148+
terminal_screen.title()
149+
})
150+
.when(!tab_subtext.is_empty(), |david| {
151+
david.child(
152+
div()
153+
.text_size(
154+
theme.system_font_size * 0.6,
155+
)
156+
.child(tab_subtext),
157+
)
158+
}),
141159
)
142160
.on_click(cx.listener(move |this, _, _, cx| {
143161
this.current_terminal_screen = i;

theterminal/src/terminal_screen.rs

Lines changed: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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
};
2727
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
2828
use std::cell::RefCell;
2929
use std::cmp::Ordering;
3030
use std::io::{Read, Write};
3131
use std::ops::{Range, Rem};
32+
use std::path::PathBuf;
3233
use std::rc::Rc;
3334
use std::thread;
3435
use std::time::Instant;
3536
use sysinfo::{ProcessRefreshKind, RefreshKind, System};
3637
use tracing::{info, warn};
38+
use url::Url;
3739
use vt100::{Callbacks, Cell, Parser, Screen};
3840

3941
actions!(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

822862
struct 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")]
876967
fn default_shell() -> String {
877968
"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe".to_string()

theterminal/translations/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"FILE_NEW_TAB": "New Tab",
77
"MENU_EDIT": "Edit",
88
"MENU_FILE": "File",
9+
"PTY_EXITED": "Command exited with exit code {{exit_code}}",
910
"PTY_OPEN_ERROR": "Unable to open pty",
1011
"PTY_SPAWN_ERROR": "Unable to spawn process",
1112
"TERMINAL_CLOSE_WARNING_CLOSE": "Close and Terminate Processes",

theterminal/translations/meta.json

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,39 +41,45 @@
4141
"plural": false,
4242
"description": null
4343
},
44+
"PTY_EXITED": {
45+
"context": "terminal_screen.rs",
46+
"definedIn": "src/terminal_screen.rs:167",
47+
"plural": false,
48+
"description": null
49+
},
4450
"PTY_OPEN_ERROR": {
4551
"context": "terminal_screen.rs",
46-
"definedIn": "src/terminal_screen.rs:123",
52+
"definedIn": "src/terminal_screen.rs:130",
4753
"plural": false,
4854
"description": null
4955
},
5056
"PTY_SPAWN_ERROR": {
5157
"context": "terminal_screen.rs",
52-
"definedIn": "src/terminal_screen.rs:137",
58+
"definedIn": "src/terminal_screen.rs:143",
5359
"plural": false,
5460
"description": null
5561
},
5662
"TERMINAL_CLOSE_WARNING_CLOSE": {
5763
"context": "terminal_screen.rs",
58-
"definedIn": "src/terminal_screen.rs:470",
64+
"definedIn": "src/terminal_screen.rs:510",
5965
"plural": false,
6066
"description": null
6167
},
6268
"TERMINAL_CLOSE_WARNING_MESSAGE": {
6369
"context": "terminal_screen.rs",
64-
"definedIn": "src/terminal_screen.rs:447",
70+
"definedIn": "src/terminal_screen.rs:487",
6571
"plural": true,
6672
"description": null
6773
},
6874
"TERMINAL_CLOSE_WARNING_TITLE": {
6975
"context": "terminal_screen.rs",
70-
"definedIn": "src/terminal_screen.rs:442",
76+
"definedIn": "src/terminal_screen.rs:482",
7177
"plural": false,
7278
"description": null
7379
},
7480
"TERMINAL_DEFAULT_TITLE": {
7581
"context": "terminal_screen.rs",
76-
"definedIn": "src/terminal_screen.rs:212",
82+
"definedIn": "src/terminal_screen.rs:247",
7783
"plural": false,
7884
"description": null
7985
}

0 commit comments

Comments
 (0)