T34: prompts and commands carry OSC 133 marks: the tap reads every OSC body and hands 133's marks to the session, which keeps its last command's status; zsh, bash, fish and PowerShell mark what the shell leaves unmarked, and the user's prompt still sees the status
This commit is contained in:
18 files changed
+359
-88
No files matched your search
@@ -24,6 +24,8 @@ pub struct Session {
|
||||
pub reports: bool,
|
||||
/// A bell rang while it was hidden.
|
||||
pub bell: bool,
|
||||
/// The last command's exit status, as its shell reported it; none before one, or when it gave none.
|
||||
pub status: Option<i32>,
|
||||
pub _events: Subscription,
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/// A semantic prompt mark a program sends, OSC 133.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Mark {
|
||||
/// A prompt starts: A, or N.
|
||||
Prompt,
|
||||
/// The prompt ends and input starts: B, or I.
|
||||
Input,
|
||||
/// Input ends and the command's output starts: C.
|
||||
Output,
|
||||
/// The command ended, with the status it gave: D.
|
||||
Done(Option<i32>),
|
||||
}
|
||||
|
||||
/// The mark in an OSC 133 body past `133;`; none for one the terminal takes no action on.
|
||||
pub(crate) fn read(body: &[u8]) -> Option<Mark> {
|
||||
let shown = || String::from_utf8_lossy(body);
|
||||
let Some((&letter, rest)) = body.split_first() else {
|
||||
log::warn!("pty: OSC 133 without a mark");
|
||||
return None;
|
||||
};
|
||||
let options = match rest {
|
||||
[] => rest,
|
||||
[b';', options @ ..] => options,
|
||||
_ => {
|
||||
log::warn!("pty: OSC 133;{} is no mark", shown());
|
||||
return None;
|
||||
}
|
||||
};
|
||||
match letter {
|
||||
b'A' | b'N' => Some(Mark::Prompt),
|
||||
b'B' | b'I' => Some(Mark::Input),
|
||||
b'C' => Some(Mark::Output),
|
||||
b'D' => Some(Mark::Done(status(options))),
|
||||
// L's fresh line needs the cursor, unseen before the parser; P adds nothing to A.
|
||||
b'L' | b'P' => {
|
||||
log::debug!("pty: OSC 133;{} takes no action here", shown());
|
||||
None
|
||||
}
|
||||
_ => {
|
||||
log::warn!("pty: OSC 133;{} is no mark", shown());
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// D's status, its first option; none when the program gave none or no number.
|
||||
fn status(options: &[u8]) -> Option<i32> {
|
||||
let first = options.split(|&byte| byte == b';').next()?;
|
||||
if first.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let status = std::str::from_utf8(first)
|
||||
.ok()
|
||||
.and_then(|text| text.parse().ok());
|
||||
if status.is_none() {
|
||||
log::warn!(
|
||||
"pty: OSC 133;D with a status that is no number ({})",
|
||||
String::from_utf8_lossy(first)
|
||||
);
|
||||
}
|
||||
status
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn each_mark_reads_with_its_options_as_ghostty_reads_it() {
|
||||
assert_eq!(read(b"A"), Some(Mark::Prompt));
|
||||
assert_eq!(read(b"A;click_events=1"), Some(Mark::Prompt));
|
||||
assert_eq!(read(b"N;aid=7"), Some(Mark::Prompt));
|
||||
assert_eq!(read(b"B"), Some(Mark::Input));
|
||||
assert_eq!(read(b"I"), Some(Mark::Input));
|
||||
assert_eq!(read(b"C;cmdline_url=false"), Some(Mark::Output));
|
||||
assert_eq!(read(b"D;1"), Some(Mark::Done(Some(1))));
|
||||
assert_eq!(read(b"D;127;aid=foo"), Some(Mark::Done(Some(127))));
|
||||
assert_eq!(read(b"D;-1"), Some(Mark::Done(Some(-1))));
|
||||
assert_eq!(read(b"D"), Some(Mark::Done(None)));
|
||||
assert_eq!(read(b"D;"), Some(Mark::Done(None)));
|
||||
assert_eq!(read(b"D;abc"), Some(Mark::Done(None)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mark_the_terminal_takes_no_action_on_reads_as_none() {
|
||||
for body in [&b""[..], b"L", b"P;k=s", b"AB", b"Q", b"a"] {
|
||||
assert_eq!(read(body), None, "{}", String::from_utf8_lossy(body));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ pub mod keys;
|
||||
mod links;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod mac;
|
||||
mod marks;
|
||||
pub mod mouse;
|
||||
mod paint;
|
||||
mod pointer;
|
||||
|
||||
+19
-4
@@ -11,7 +11,7 @@ use alacritty_terminal::{
|
||||
};
|
||||
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded};
|
||||
|
||||
use super::{mouse::MouseMode, tap::Tapped};
|
||||
use super::{marks::Mark, mouse::MouseMode, tap::Tapped};
|
||||
use crate::settings::{CursorStyle, Settings};
|
||||
|
||||
/// What a session hears from its pty.
|
||||
@@ -20,6 +20,7 @@ pub enum PtyEvent {
|
||||
Term(Event),
|
||||
Folder(PathBuf),
|
||||
Mouse(MouseMode),
|
||||
Mark(Mark),
|
||||
}
|
||||
|
||||
/// Hands the grid's events to the session; `pid` names the program in logs.
|
||||
@@ -256,6 +257,7 @@ mod tests {
|
||||
|
||||
struct Ran {
|
||||
folders: Vec<PathBuf>,
|
||||
marks: Vec<Mark>,
|
||||
text: String,
|
||||
success: bool,
|
||||
pid: u32,
|
||||
@@ -272,23 +274,25 @@ mod tests {
|
||||
};
|
||||
let (pty, mut events) =
|
||||
spawn(&launch, size, (8, 16), config(&Settings::default())).expect("sh starts");
|
||||
let (folders, success) = futures::executor::block_on(async {
|
||||
let (mut folders, mut success) = (Vec::new(), false);
|
||||
let (folders, marks, success) = futures::executor::block_on(async {
|
||||
let (mut folders, mut marks, mut success) = (Vec::new(), Vec::new(), false);
|
||||
while let Some(event) = events.next().await {
|
||||
match event {
|
||||
PtyEvent::Folder(folder) => folders.push(folder),
|
||||
PtyEvent::Mark(mark) => marks.push(mark),
|
||||
PtyEvent::Term(Event::ChildExit(status)) => success = status.success(),
|
||||
PtyEvent::Term(Event::Exit) => break,
|
||||
PtyEvent::Term(_) | PtyEvent::Mouse(_) => {}
|
||||
}
|
||||
}
|
||||
(folders, success)
|
||||
(folders, marks, success)
|
||||
});
|
||||
let term = pty.term.lock();
|
||||
let row = &term.grid()[Line(0)];
|
||||
let text: String = (0..24).map(|column| row[Column(column)].c).collect();
|
||||
Ran {
|
||||
folders,
|
||||
marks,
|
||||
text: text.trim_end().to_string(),
|
||||
success,
|
||||
pid: pty.pid,
|
||||
@@ -314,6 +318,17 @@ mod tests {
|
||||
assert_eq!(ran.text, "x");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osc_133_reaches_the_session_as_marks() {
|
||||
let ran = run(
|
||||
"printf '\\033]133;A\\007>\\033]133;B\\007\\033]133;C\\033\\\\x\\033]133;D;3\\007'",
|
||||
(40, 4),
|
||||
);
|
||||
let marks = [Mark::Prompt, Mark::Input, Mark::Output, Mark::Done(Some(3))];
|
||||
assert_eq!(ran.marks, marks);
|
||||
assert_eq!(ran.text, ">x");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_foreground_is_the_program_s_own_group() {
|
||||
let launch = Launch {
|
||||
|
||||
+29
-5
@@ -6,7 +6,7 @@ use futures::channel::mpsc::TryRecvError;
|
||||
use super::*;
|
||||
use crate::{
|
||||
settings::Settings,
|
||||
term::{Events, Pty, PtyEvent, config, spawn},
|
||||
term::{Events, Pty, PtyEvent, config, marks::Mark, spawn},
|
||||
};
|
||||
|
||||
/// A folder name with a space, a percent, a bell and a newline.
|
||||
@@ -43,6 +43,7 @@ struct Run {
|
||||
start: PathBuf,
|
||||
folders: Vec<PathBuf>,
|
||||
titles: Vec<String>,
|
||||
marks: Vec<Mark>,
|
||||
deadline: Instant,
|
||||
}
|
||||
|
||||
@@ -79,6 +80,7 @@ impl Run {
|
||||
start,
|
||||
folders: Vec::new(),
|
||||
titles: Vec::new(),
|
||||
marks: Vec::new(),
|
||||
deadline: Instant::now() + Duration::from_secs(30),
|
||||
}
|
||||
}
|
||||
@@ -88,15 +90,17 @@ impl Run {
|
||||
while !done(self) {
|
||||
assert!(
|
||||
Instant::now() < self.deadline,
|
||||
"{}: no {what}; folders {:?}, titles {:?}, screen\n{}",
|
||||
"{}: no {what}; folders {:?}, titles {:?}, marks {:?}, screen\n{}",
|
||||
self.shell,
|
||||
self.folders,
|
||||
self.titles,
|
||||
self.marks,
|
||||
self.screen()
|
||||
);
|
||||
match self.events.try_recv() {
|
||||
Ok(PtyEvent::Folder(folder)) => self.folders.push(folder),
|
||||
Ok(PtyEvent::Term(Event::Title(title))) => self.titles.push(title),
|
||||
Ok(PtyEvent::Mark(mark)) => self.marks.push(mark),
|
||||
Ok(PtyEvent::Term(Event::PtyWrite(reply))) => self.pty.write(reply.into_bytes()),
|
||||
Ok(PtyEvent::Term(Event::ChildExit(status))) => {
|
||||
panic!("{} ended, {status}:\n{}", self.shell, self.screen())
|
||||
@@ -142,6 +146,13 @@ impl Run {
|
||||
self.until("inner folder", |run| run.folders.contains(&inner));
|
||||
}
|
||||
|
||||
/// Types `line`, the last command sent, which fails with 1; its marks follow its earlier ones.
|
||||
fn fails(&mut self, line: &str) {
|
||||
self.send(line);
|
||||
let after = [Mark::Output, Mark::Done(Some(1)), Mark::Prompt, Mark::Input];
|
||||
self.until("marks of the failure", |run| run.marks.ends_with(&after));
|
||||
}
|
||||
|
||||
fn finish(mut self) {
|
||||
self.send("exit");
|
||||
loop {
|
||||
@@ -193,7 +204,10 @@ fn a_script_it_cannot_read_fails_the_install() {
|
||||
|
||||
#[test]
|
||||
fn zsh_reports_its_folder_byte_for_byte_and_its_titles() {
|
||||
let mut run = Run::start(&installed("zsh"), &[]);
|
||||
// A theme that sets PS1 in its own hook, which runs after ours.
|
||||
let theme =
|
||||
"autoload -Uz add-zsh-hook\n_theme() { PS1='THEME %? > ' }\nadd-zsh-hook precmd _theme";
|
||||
let mut run = Run::start(&installed("zsh"), &[(".zshrc", theme)]);
|
||||
run.walk("(( $+functions[_ely_precmd] )) && echo ELY-LOAD''ED");
|
||||
run.until("title", |run| {
|
||||
run.titles.iter().any(|title| title == "in ner%xy")
|
||||
@@ -203,6 +217,10 @@ fn zsh_reports_its_folder_byte_for_byte_and_its_titles() {
|
||||
"{:?}",
|
||||
run.titles
|
||||
);
|
||||
run.fails("false");
|
||||
run.until("the theme's prompt", |run| {
|
||||
run.shows("THEME 0 > false") && run.shows("THEME 1 >")
|
||||
});
|
||||
run.finish();
|
||||
}
|
||||
|
||||
@@ -215,8 +233,9 @@ fn bash_reports_its_folder_byte_for_byte_and_keeps_the_status() {
|
||||
run.until("title", |run| {
|
||||
run.titles.iter().any(|title| title == "in ner%xy")
|
||||
});
|
||||
run.send("false");
|
||||
run.fails("false");
|
||||
run.until("kept status", |run| run.shows("USER_STATUS=1"));
|
||||
assert!(run.shows("$ false"), "{}", run.screen());
|
||||
run.finish();
|
||||
}
|
||||
|
||||
@@ -228,6 +247,10 @@ fn fish_loads_its_script_and_restores_its_data_dirs() {
|
||||
run.send(r#"echo "DIRS[$ELY_FISH_XDG_DATA_DIRS][$XDG_DATA_DIRS]""#);
|
||||
let restored = format!("DIRS[][{previous}]");
|
||||
run.until("restored dirs", |run| run.shows(&restored));
|
||||
// The prompt hears each command's own status, none failed so far.
|
||||
assert!(!run.shows("[1]"), "{}", run.screen());
|
||||
run.fails("false");
|
||||
run.until("the failed status", |run| run.shows("[1]>"));
|
||||
run.finish();
|
||||
}
|
||||
|
||||
@@ -244,8 +267,9 @@ fn powershell_reports_its_folder_byte_for_byte_and_keeps_the_status() {
|
||||
run.until("title", |run| {
|
||||
run.titles.iter().any(|title| title == "in ner%xy")
|
||||
});
|
||||
run.send("/usr/bin/false");
|
||||
run.fails("/usr/bin/false");
|
||||
run.until("kept status", |run| run.shows("USER_SUCCESS=False"));
|
||||
assert!(run.shows("> /usr/bin/false"), "{}", run.screen());
|
||||
run.finish();
|
||||
}
|
||||
|
||||
|
||||
+16
-9
@@ -7,7 +7,7 @@ use alacritty_terminal::{
|
||||
use futures::channel::mpsc::UnboundedSender;
|
||||
use polling::{Event, PollMode, Poller};
|
||||
|
||||
use super::{mouse::MouseMode, pty::PtyEvent};
|
||||
use super::{marks, mouse::MouseMode, pty::PtyEvent};
|
||||
|
||||
/// The longest OSC body kept; longer ones are dropped whole.
|
||||
const MOST: usize = 4096;
|
||||
@@ -15,7 +15,7 @@ const MOST: usize = 4096;
|
||||
/// A pty whose reader shows each byte to its scanners before the parser.
|
||||
pub(crate) struct Tapped<P> {
|
||||
pub inner: P,
|
||||
scanner: Osc7,
|
||||
scanner: Osc,
|
||||
mouse: MouseModes,
|
||||
events: UnboundedSender<PtyEvent>,
|
||||
pid: u32,
|
||||
@@ -26,7 +26,7 @@ impl<P> Tapped<P> {
|
||||
pub fn new(inner: P, events: UnboundedSender<PtyEvent>, pid: u32) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
scanner: Osc7::default(),
|
||||
scanner: Osc::default(),
|
||||
mouse: MouseModes::default(),
|
||||
events,
|
||||
pid,
|
||||
@@ -40,7 +40,14 @@ impl<P: EventedReadWrite> io::Read for Tapped<P> {
|
||||
let count = self.inner.reader().read(buf)?;
|
||||
let (events, pid, last) = (&self.events, self.pid, &mut self.last);
|
||||
self.scanner.feed(&buf[..count], &mut |body| {
|
||||
let Some(folder) = folder(body) else {
|
||||
if let Some(mark) = body.strip_prefix(b"133;").and_then(marks::read) {
|
||||
log::debug!("pty {pid}: {mark:?}");
|
||||
if events.unbounded_send(PtyEvent::Mark(mark)).is_err() {
|
||||
log::debug!("pty {pid}: a mark after its session closed");
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(folder) = body.strip_prefix(b"7;").and_then(folder) else {
|
||||
return;
|
||||
};
|
||||
if last.as_ref() == Some(&folder) {
|
||||
@@ -114,15 +121,15 @@ enum State {
|
||||
OscEscape,
|
||||
}
|
||||
|
||||
/// Finds `ESC ] 7 ; body` ended by BEL or `ESC \`, across reads.
|
||||
/// Finds each OSC body, `ESC ] body` ended by BEL or `ESC \`, across reads.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Osc7 {
|
||||
pub(crate) struct Osc {
|
||||
state: State,
|
||||
body: Vec<u8>,
|
||||
overflow: bool,
|
||||
}
|
||||
|
||||
impl Osc7 {
|
||||
impl Osc {
|
||||
pub fn feed(&mut self, bytes: &[u8], found: &mut impl FnMut(&[u8])) {
|
||||
let mut rest = bytes;
|
||||
while !rest.is_empty() {
|
||||
@@ -175,8 +182,8 @@ impl Osc7 {
|
||||
fn finish(&mut self, found: &mut impl FnMut(&[u8])) {
|
||||
if self.overflow {
|
||||
log::warn!("pty: an OSC over {MOST} bytes dropped");
|
||||
} else if let Some(body) = self.body.strip_prefix(b"7;") {
|
||||
found(body);
|
||||
} else {
|
||||
found(&self.body);
|
||||
}
|
||||
self.body.clear();
|
||||
}
|
||||
|
||||
+14
-8
@@ -1,7 +1,7 @@
|
||||
use super::*;
|
||||
|
||||
fn scan(chunks: &[&[u8]]) -> Vec<String> {
|
||||
let mut scanner = Osc7::default();
|
||||
let mut scanner = Osc::default();
|
||||
let mut found = Vec::new();
|
||||
for chunk in chunks {
|
||||
scanner.feed(chunk, &mut |body| {
|
||||
@@ -12,17 +12,23 @@ fn scan(chunks: &[&[u8]]) -> Vec<String> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_osc_7_ended_by_bel_or_st_across_reads() {
|
||||
assert_eq!(scan(&[b"ab\x1b]7;file:///a\x07cd"]), ["file:///a"]);
|
||||
assert_eq!(scan(&[b"\x1b]7;file:", b"///b\x1b", b"\\"]), ["file:///b"]);
|
||||
assert_eq!(scan(&[b"\x1b", b"]7;x\x07\x1b]7;y\x1b\\"]), ["x", "y"]);
|
||||
fn finds_each_osc_body_ended_by_bel_or_st_across_reads() {
|
||||
assert_eq!(scan(&[b"ab\x1b]7;file:///a\x07cd"]), ["7;file:///a"]);
|
||||
assert_eq!(
|
||||
scan(&[b"\x1b]7;file:", b"///b\x1b", b"\\"]),
|
||||
["7;file:///b"]
|
||||
);
|
||||
assert_eq!(
|
||||
scan(&[b"\x1b", b"]133;A\x07\x1b]0;t\x1b\\"]),
|
||||
["133;A", "0;t"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_sequences_and_cancels_pass() {
|
||||
assert!(scan(&[b"\x1b]0;title\x07\x1b[31m"]).is_empty());
|
||||
assert!(scan(&[b"\x1b[31m\x1bc"]).is_empty());
|
||||
assert!(scan(&[b"\x1b]7;file:///a\x18\x07"]).is_empty());
|
||||
assert_eq!(scan(&[b"\x1b]7;lost\x1b]7;kept\x07"]), ["kept"]);
|
||||
assert_eq!(scan(&[b"\x1b]7;lost\x1b]133;D;1\x07"]), ["133;D;1"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -31,7 +37,7 @@ fn an_overlong_body_is_dropped() {
|
||||
long.extend(std::iter::repeat_n(b'a', MOST + 1));
|
||||
long.push(0x07);
|
||||
assert!(scan(&[&long]).is_empty());
|
||||
assert_eq!(scan(&[&long, b"\x1b]7;b\x07"]), ["b"]);
|
||||
assert_eq!(scan(&[&long, b"\x1b]7;b\x07"]), ["7;b"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -22,6 +22,7 @@ use super::{
|
||||
frame::frame,
|
||||
ink::Ink,
|
||||
input::{self, Find},
|
||||
marks::Mark,
|
||||
mouse::MouseMode,
|
||||
paint::{self, Look},
|
||||
pointer::Pointer,
|
||||
@@ -48,6 +49,8 @@ pub enum TerminalEvent {
|
||||
Title(String),
|
||||
Folder(PathBuf),
|
||||
Bell,
|
||||
/// A command the shell ran ended, with the status it gave.
|
||||
Finished(Option<i32>),
|
||||
Exited(ExitStatus),
|
||||
}
|
||||
|
||||
@@ -218,6 +221,8 @@ impl TerminalView {
|
||||
}
|
||||
PtyEvent::Term(event) => self.term_event(event, cx),
|
||||
PtyEvent::Mouse(mode) => self.mouse = mode,
|
||||
PtyEvent::Mark(Mark::Done(status)) => cx.emit(TerminalEvent::Finished(status)),
|
||||
PtyEvent::Mark(_) => {}
|
||||
}
|
||||
}
|
||||
// ponytail: a full search per output batch while find is open; search only new lines if it lags.
|
||||
|
||||
+4
-47
@@ -7,7 +7,7 @@ use ely_gpui_component::{
|
||||
typography::Caption,
|
||||
};
|
||||
use gpui::{
|
||||
Action, App, AppContext, Context, Entity, EntityId, FocusHandle, Focusable, InteractiveElement,
|
||||
Action, App, AppContext, Context, Entity, FocusHandle, Focusable, InteractiveElement,
|
||||
IntoElement, KeyBinding, ParentElement, Render, SharedString, StatefulInteractiveElement,
|
||||
StyleRefinement, Styled, Subscription, Window, actions, div, prelude::FluentBuilder,
|
||||
};
|
||||
@@ -20,12 +20,13 @@ use crate::{
|
||||
sessions::{self, Session},
|
||||
settings::{PanelTab, Settings, SettingsStore, StartIn, window::OpenSettings},
|
||||
term::{
|
||||
Events, Launch, Pty, TerminalEvent, TerminalView, config,
|
||||
Events, Launch, Pty, TerminalView, config,
|
||||
shell::{ShellIntegration, launch},
|
||||
spawn,
|
||||
},
|
||||
};
|
||||
|
||||
mod events;
|
||||
mod widths;
|
||||
|
||||
use widths::{Dragged, Side};
|
||||
@@ -153,6 +154,7 @@ impl Workspace {
|
||||
folder: launch.cwd,
|
||||
reports: launch.reports,
|
||||
bell: false,
|
||||
status: None,
|
||||
_events: events,
|
||||
});
|
||||
}
|
||||
@@ -242,51 +244,6 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
fn session_event(
|
||||
&mut self,
|
||||
id: EntityId,
|
||||
event: &TerminalEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(index) = self
|
||||
.sessions
|
||||
.iter()
|
||||
.position(|session| session.terminal.entity_id() == id)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let session = &mut self.sessions[index];
|
||||
match event {
|
||||
TerminalEvent::Title(title) => session.title.clone_from(title),
|
||||
TerminalEvent::Folder(folder) => {
|
||||
session.folder.clone_from(folder);
|
||||
session.reports = true;
|
||||
if index == self.active {
|
||||
self.files.update(cx, |files, cx| files.show(folder, cx));
|
||||
self.git.update(cx, |git, cx| git.show(folder, cx));
|
||||
}
|
||||
}
|
||||
TerminalEvent::Bell if index != self.active => {
|
||||
if !session.bell {
|
||||
log::debug!("app: a bell in hidden session {}", index + 1);
|
||||
}
|
||||
session.bell = true;
|
||||
}
|
||||
TerminalEvent::Bell => {}
|
||||
TerminalEvent::Exited(status) => {
|
||||
let pid = session.terminal.read(cx).pid();
|
||||
log::info!("app: session {pid}'s program ended, {status}");
|
||||
self.close(index, window, cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if index == self.active {
|
||||
self.retitle(window);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Shows or hides a side column, as the settings keep it.
|
||||
fn toggle(&mut self, flip: fn(&mut Settings), cx: &mut Context<Self>) {
|
||||
match SettingsStore::update(cx, flip) {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
use gpui::{Context, EntityId, Window};
|
||||
|
||||
use super::Workspace;
|
||||
use crate::term::TerminalEvent;
|
||||
|
||||
impl Workspace {
|
||||
/// What session `id`'s terminal reported.
|
||||
pub(super) fn session_event(
|
||||
&mut self,
|
||||
id: EntityId,
|
||||
event: &TerminalEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(index) = self
|
||||
.sessions
|
||||
.iter()
|
||||
.position(|session| session.terminal.entity_id() == id)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let session = &mut self.sessions[index];
|
||||
match event {
|
||||
TerminalEvent::Title(title) => session.title.clone_from(title),
|
||||
TerminalEvent::Folder(folder) => {
|
||||
session.folder.clone_from(folder);
|
||||
session.reports = true;
|
||||
if index == self.active {
|
||||
self.files.update(cx, |files, cx| files.show(folder, cx));
|
||||
self.git.update(cx, |git, cx| git.show(folder, cx));
|
||||
}
|
||||
}
|
||||
TerminalEvent::Bell if index != self.active => {
|
||||
if !session.bell {
|
||||
log::debug!("app: a bell in hidden session {}", index + 1);
|
||||
}
|
||||
session.bell = true;
|
||||
}
|
||||
TerminalEvent::Bell => {}
|
||||
TerminalEvent::Finished(status) => {
|
||||
if session.status != *status {
|
||||
let pid = session.terminal.read(cx).pid();
|
||||
let ended =
|
||||
status.map_or("with no status".into(), |code| format!("with {code}"));
|
||||
log::info!("app: session {pid}'s last command ended {ended}");
|
||||
}
|
||||
session.status = *status;
|
||||
}
|
||||
TerminalEvent::Exited(status) => {
|
||||
let pid = session.terminal.read(cx).pid();
|
||||
log::info!("app: session {pid}'s program ended, {status}");
|
||||
self.close(index, window, cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if index == self.active {
|
||||
self.retitle(window);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user