T39: a session's row shows its status, the most pressing first: waiting by its kind, an error, work with Ely's progress ring or a still loader, done, then a failed last command, then idle; hover lists each record's state, app, title and message, disarmed and cut; typing sees done and error, herdr's aside; a hidden session that starts waiting sets its title strong until shown
This commit is contained in:
12 files changed
+449
-22
No files matched your search
+265
-11
@@ -3,13 +3,14 @@ use std::path::{MAIN_SEPARATOR, Path, PathBuf};
|
||||
use ely_gpui_component::{
|
||||
buttons::{ButtonVariant, IconButton},
|
||||
lists::ListItem,
|
||||
primitives::{Icon, IconName},
|
||||
theme::{ActiveTheme, ControlSize},
|
||||
motion::ProgressRing,
|
||||
primitives::{Icon, IconName, Tooltip},
|
||||
theme::{ActiveTheme, ControlSize, IconSize},
|
||||
};
|
||||
use gpui::{App, Entity, Subscription};
|
||||
use gpui::{AnyElement, App, Entity, Hsla, SharedString, Subscription, div, prelude::*};
|
||||
|
||||
use crate::{
|
||||
term::TerminalView,
|
||||
term::{Kind, Record, Records, State, TerminalView},
|
||||
workspace::{TITLE, Workspace},
|
||||
};
|
||||
|
||||
@@ -24,11 +25,19 @@ pub struct Session {
|
||||
pub reports: bool,
|
||||
/// A bell rang while it was hidden.
|
||||
pub bell: bool,
|
||||
/// It started waiting on the user while hidden.
|
||||
pub waiting: bool,
|
||||
/// The last command's exit status, as its shell reported it; none before one, or when it gave none.
|
||||
pub status: Option<i32>,
|
||||
/// The state its mark shows, as its records last said.
|
||||
pub state: Option<State>,
|
||||
pub _events: Subscription,
|
||||
}
|
||||
|
||||
/// The most records hover lists, and the most characters it shows of each part.
|
||||
const LISTED: usize = 10;
|
||||
const SHORT: usize = 120;
|
||||
|
||||
/// `folder` as a row shows it: the home folder as `~`, on one line.
|
||||
pub fn shown(folder: &Path, home: Option<&Path>) -> String {
|
||||
let shown = match home.and_then(|home| folder.strip_prefix(home).ok()) {
|
||||
@@ -59,7 +68,7 @@ pub fn after_close(active: usize, closed: usize, left: usize) -> usize {
|
||||
}
|
||||
}
|
||||
|
||||
/// Session `index`'s row: its title and folder, a bell while one rang unseen, a button that closes it.
|
||||
/// Session `index`'s row: its title and folder, the mark of its status, a bell while one rang unseen, a button that closes it.
|
||||
pub fn row(
|
||||
workspace: &Entity<Workspace>,
|
||||
index: usize,
|
||||
@@ -68,7 +77,8 @@ pub fn row(
|
||||
home: Option<&Path>,
|
||||
cx: &App,
|
||||
) -> ListItem {
|
||||
let pid = session.terminal.read(cx).pid() as usize;
|
||||
let terminal = session.terminal.read(cx);
|
||||
let pid = terminal.pid() as usize;
|
||||
let title = if session.title.is_empty() {
|
||||
TITLE
|
||||
} else {
|
||||
@@ -78,7 +88,7 @@ pub fn row(
|
||||
let item = ListItem::new(("session", pid), title.to_string())
|
||||
.description(shown(&session.folder, home))
|
||||
.selected(active)
|
||||
.strong(session.bell)
|
||||
.strong(session.bell || session.waiting)
|
||||
.on_click(move |_, window, cx| select.update(cx, |this, cx| this.select(index, window, cx)))
|
||||
.trailing(
|
||||
IconButton::new(("close-session", pid), IconName::X)
|
||||
@@ -90,11 +100,178 @@ pub fn row(
|
||||
close.update(cx, |this, cx| this.close(index, window, cx))
|
||||
}),
|
||||
);
|
||||
if session.bell {
|
||||
item.leading(Icon::new(IconName::Bell).color(cx.theme().colors.warning))
|
||||
} else {
|
||||
item
|
||||
let mark = mark(pid, terminal.records(), session.status, cx);
|
||||
let bell = session
|
||||
.bell
|
||||
.then(|| Icon::new(IconName::Bell).color(cx.theme().colors.warning));
|
||||
if mark.is_none() && bell.is_none() {
|
||||
return item;
|
||||
}
|
||||
item.leading(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.children(mark)
|
||||
.children(bell),
|
||||
)
|
||||
}
|
||||
|
||||
/// The mark of what the session's records say, else of its last command's failure; hover lists them.
|
||||
fn mark(pid: usize, records: &Records, status: Option<i32>, cx: &App) -> Option<AnyElement> {
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let icon = |name: IconName, color: Hsla| Icon::new(name).color(color).into_any_element();
|
||||
let failed = status.filter(|&code| code != 0);
|
||||
let mark = match (records.shown(), failed) {
|
||||
(
|
||||
Some(Record {
|
||||
state: State::Blocked,
|
||||
kind,
|
||||
..
|
||||
}),
|
||||
_,
|
||||
) => {
|
||||
let name = match kind {
|
||||
Some(Kind::Permission) => IconName::Shield,
|
||||
Some(Kind::Question) => IconName::CircleHelp,
|
||||
Some(Kind::Auth) => IconName::Key,
|
||||
None => IconName::Hand,
|
||||
};
|
||||
icon(name, colors.warning)
|
||||
}
|
||||
(
|
||||
Some(Record {
|
||||
state: State::Error,
|
||||
..
|
||||
}),
|
||||
_,
|
||||
) => icon(IconName::CircleX, colors.danger),
|
||||
(
|
||||
Some(Record {
|
||||
state: State::Working,
|
||||
progress: Some(value),
|
||||
..
|
||||
}),
|
||||
_,
|
||||
) => ProgressRing::new(("session-progress", pid), f32::from(*value) / 100.0)
|
||||
.size(theme.icon_size(IconSize::Xl))
|
||||
.into_any_element(),
|
||||
// A turning spinner would draw the window at every frame while the program works.
|
||||
(
|
||||
Some(Record {
|
||||
state: State::Working,
|
||||
..
|
||||
}),
|
||||
_,
|
||||
) => icon(IconName::LoaderCircle, colors.info),
|
||||
(
|
||||
Some(Record {
|
||||
state: State::Done, ..
|
||||
}),
|
||||
_,
|
||||
) => icon(IconName::CircleCheck, colors.success),
|
||||
(_, Some(_)) => icon(IconName::CircleAlert, colors.danger),
|
||||
(
|
||||
Some(Record {
|
||||
state: State::Idle, ..
|
||||
}),
|
||||
None,
|
||||
) => icon(IconName::Circle, colors.fg_muted),
|
||||
(None, None) => return None,
|
||||
};
|
||||
let lines = hover(records, failed);
|
||||
let tip = Tooltip::rich(move |_, _| {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.children(lines.iter().cloned())
|
||||
.into_any_element()
|
||||
});
|
||||
// Every mark takes the ring's box, so the title stays put as the state changes.
|
||||
Some(
|
||||
div()
|
||||
.id(("session-status", pid))
|
||||
.size(theme.icon_size(IconSize::Xl))
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.tooltip(tip)
|
||||
.child(mark)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
/// What hover shows: a line for each record, ten at most, with its state, app, title and message, then a failed command's status.
|
||||
pub fn hover(records: &Records, failed: Option<i32>) -> Vec<SharedString> {
|
||||
let line = |(id, record): (&str, &Record)| {
|
||||
let parts = [
|
||||
records.app(id),
|
||||
record.title.as_deref(),
|
||||
record.msg.as_deref(),
|
||||
];
|
||||
let said: Vec<String> = parts.into_iter().flatten().map(plain).collect();
|
||||
let state = label(record);
|
||||
SharedString::from(if said.is_empty() {
|
||||
state
|
||||
} else {
|
||||
format!("{state}: {}", said.join(" · "))
|
||||
})
|
||||
};
|
||||
let mut lines: Vec<SharedString> = records.iter().take(LISTED).map(line).collect();
|
||||
let more = records.iter().count().saturating_sub(LISTED);
|
||||
if more > 0 {
|
||||
lines.push(format!("{more} more").into());
|
||||
}
|
||||
if let Some(code) = failed {
|
||||
lines.push(format!("Last command exited with {code}").into());
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
/// A record's state, as hover names it.
|
||||
fn label(record: &Record) -> String {
|
||||
match (record.state, record.kind, record.progress) {
|
||||
(State::Idle, ..) => "Idle".into(),
|
||||
(State::Working, _, Some(value)) => format!("Working, {value}%"),
|
||||
(State::Working, ..) => "Working".into(),
|
||||
(State::Blocked, Some(Kind::Permission), _) => "Waiting for permission".into(),
|
||||
(State::Blocked, Some(Kind::Question), _) => "Waiting for an answer".into(),
|
||||
(State::Blocked, Some(Kind::Auth), _) => "Waiting for a login".into(),
|
||||
(State::Blocked, None, _) => "Waiting".into(),
|
||||
(State::Done, ..) => "Done".into(),
|
||||
(State::Error, ..) => "Error".into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A program's text as hover shows it: on one line, 120 characters at most, without what could disguise it.
|
||||
pub fn plain(text: &str) -> String {
|
||||
let kept: String = text.chars().filter(|&c| !invisible(c)).collect();
|
||||
let line = one_line(&kept);
|
||||
match line.char_indices().nth(SHORT) {
|
||||
Some((at, _)) => format!("{}…", &line[..at]),
|
||||
None => line,
|
||||
}
|
||||
}
|
||||
|
||||
/// C1 controls, direction controls and invisible formatting; joiners stay, as scripts and emoji need them.
|
||||
fn invisible(c: char) -> bool {
|
||||
matches!(
|
||||
c,
|
||||
'\u{80}'..='\u{9f}'
|
||||
| '\u{ad}'
|
||||
| '\u{61c}'
|
||||
| '\u{180e}'
|
||||
| '\u{200b}'
|
||||
| '\u{200e}'
|
||||
| '\u{200f}'
|
||||
| '\u{202a}'..='\u{202e}'
|
||||
| '\u{2060}'..='\u{2064}'
|
||||
| '\u{2066}'..='\u{2069}'
|
||||
| '\u{feff}'
|
||||
| '\u{fff9}'..='\u{fffb}'
|
||||
| '\u{e0000}'..='\u{e007f}'
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -135,4 +312,81 @@ mod tests {
|
||||
assert_eq!(after_close(3, 3, 3), 2, "the last went: the one before");
|
||||
assert_eq!(after_close(1, 2, 3), 1, "one after it went");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_shows_a_program_s_text_as_text() {
|
||||
assert_eq!(plain("evil\u{202e}txt.exe"), "eviltxt.exe");
|
||||
assert_eq!(
|
||||
plain("a\u{200b}b\u{feff}c\u{e0041}d\u{85}e\u{2066}f"),
|
||||
"abcdef"
|
||||
);
|
||||
assert_eq!(
|
||||
plain("👨\u{200d}💻 two\nlines"),
|
||||
"👨\u{200d}💻 two␊lines",
|
||||
"joiners stay"
|
||||
);
|
||||
assert_eq!(plain(&"x".repeat(SHORT)), "x".repeat(SHORT));
|
||||
assert_eq!(
|
||||
plain(&"é".repeat(SHORT + 1)),
|
||||
format!("{}…", "é".repeat(SHORT))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_lists_each_record_with_its_app_then_a_failed_command() {
|
||||
let record =
|
||||
|state: State, kind: Option<Kind>, progress: Option<u8>, texts: [Option<&str>; 3]| {
|
||||
let [app, title, msg] = texts.map(|text| text.map(String::from));
|
||||
Record {
|
||||
state,
|
||||
kind,
|
||||
progress,
|
||||
app,
|
||||
title,
|
||||
msg,
|
||||
}
|
||||
};
|
||||
let mut records = Records::default();
|
||||
records.report(
|
||||
String::new(),
|
||||
record(
|
||||
State::Working,
|
||||
None,
|
||||
Some(40),
|
||||
[Some("deploy"), None, Some("Deploying")],
|
||||
),
|
||||
);
|
||||
records.report(
|
||||
"eu".into(),
|
||||
record(
|
||||
State::Blocked,
|
||||
Some(Kind::Permission),
|
||||
None,
|
||||
[None, Some("EU West"), Some("Approve?")],
|
||||
),
|
||||
);
|
||||
records.report(
|
||||
"eu/db".into(),
|
||||
record(State::Done, None, None, [Some("db"), None, None]),
|
||||
);
|
||||
let lines: Vec<String> = hover(&records, Some(2))
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
lines,
|
||||
[
|
||||
"Working, 40%: deploy · Deploying",
|
||||
"Waiting for permission: deploy · EU West · Approve?",
|
||||
"Done: db",
|
||||
"Last command exited with 2"
|
||||
]
|
||||
);
|
||||
for at in 0..LISTED {
|
||||
records.report(format!("r{at}"), record(State::Idle, None, None, [None; 3]));
|
||||
}
|
||||
let lines = hover(&records, None);
|
||||
assert_eq!(lines.len(), LISTED + 1);
|
||||
assert_eq!(lines[LISTED].to_string(), "3 more");
|
||||
}
|
||||
}
|
||||
+9
-2
@@ -76,7 +76,12 @@ pub fn follow(cx: &mut Context<TerminalView>) -> Task<()> {
|
||||
"terminal {pid}: herdr left the foreground, its agents' records with it"
|
||||
);
|
||||
states.clear();
|
||||
if view.update(cx, |view, _| view.records.herdr(&[])).is_err() {
|
||||
let gone = view.update(cx, |view, cx| {
|
||||
if view.records.herdr(&[]) {
|
||||
cx.emit(TerminalEvent::Records);
|
||||
}
|
||||
});
|
||||
if gone.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -122,7 +127,9 @@ pub fn follow(cx: &mut Context<TerminalView>) -> Task<()> {
|
||||
let Ok(still) = view.update(cx, |view, cx| {
|
||||
let still = view.reports == reports;
|
||||
if still {
|
||||
view.records.herdr(&agents);
|
||||
if view.records.herdr(&agents) {
|
||||
cx.emit(TerminalEvent::Records);
|
||||
}
|
||||
if let Some(folder) = report.clone() {
|
||||
cx.emit(TerminalEvent::Folder(folder));
|
||||
}
|
||||
|
||||
+5
-2
@@ -9,7 +9,7 @@ use gpui::{
|
||||
|
||||
use super::{
|
||||
keys::{self, Action, Key, Kitty},
|
||||
view::TerminalView,
|
||||
view::{TerminalEvent, TerminalView},
|
||||
};
|
||||
|
||||
actions!(elyterminal, [Find, FindNewer, PreviousPrompt, NextPrompt]);
|
||||
@@ -68,7 +68,7 @@ pub fn bind_keys(cx: &mut App) {
|
||||
}
|
||||
|
||||
impl TerminalView {
|
||||
/// Bytes from the keyboard: back to the newest line, no selection, then to the program.
|
||||
/// Bytes from the keyboard: back to the newest line, no selection, then to the program, which sees done and error records.
|
||||
pub(super) fn input(&mut self, bytes: Vec<u8>, cx: &mut Context<Self>) {
|
||||
let mut term = self.pty.term.lock();
|
||||
term.scroll_display(Scroll::Bottom);
|
||||
@@ -77,6 +77,9 @@ impl TerminalView {
|
||||
}
|
||||
drop(term);
|
||||
self.pty.write(bytes);
|
||||
if self.records.seen() {
|
||||
cx.emit(TerminalEvent::Records);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
||||
@@ -24,4 +24,5 @@ mod view;
|
||||
pub mod zoom;
|
||||
|
||||
pub use pty::{Events, Launch, Listener, Pty, PtyEvent, config, fit, spawn};
|
||||
pub use status::{Kind, Record, Records, State};
|
||||
pub use view::{BLINK, ICONS, MONO, TerminalEvent, TerminalView, load_font};
|
||||
+55
-3
@@ -232,19 +232,20 @@ impl Records {
|
||||
}
|
||||
}
|
||||
|
||||
/// herdr's agents in place of the last ones, unless they are the same.
|
||||
pub fn herdr(&mut self, agents: &[(String, Record)]) {
|
||||
/// herdr's agents in place of the last ones; true when they differ.
|
||||
pub fn herdr(&mut self, agents: &[(String, Record)]) -> bool {
|
||||
let held = self.list.iter().filter(|(id, ..)| id.starts_with(HERDR));
|
||||
if held
|
||||
.map(|(id, record, _)| (id, record))
|
||||
.eq(agents.iter().map(|(id, record)| (id, record)))
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
self.list.retain(|(id, ..)| !id.starts_with(HERDR));
|
||||
for (id, record) in agents {
|
||||
self.put(id.clone(), record.clone());
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// A full reset: every record goes, and OSC 9;4 maps again.
|
||||
@@ -287,6 +288,57 @@ impl Records {
|
||||
self.list
|
||||
.retain(|(_, record, _)| matches!(record.state, State::Done | State::Error));
|
||||
}
|
||||
|
||||
/// The user typed: done and error records are seen and go, but herdr's, which herdr marks seen; true when any went.
|
||||
pub fn seen(&mut self) -> bool {
|
||||
let before = self.list.len();
|
||||
self.list.retain(|(id, record, _)| {
|
||||
id.starts_with(HERDR) || !matches!(record.state, State::Done | State::Error)
|
||||
});
|
||||
self.list.len() != before
|
||||
}
|
||||
|
||||
/// The record a row shows: blocked, else error, working, done or idle, the one reported last in its state.
|
||||
pub fn shown(&self) -> Option<&Record> {
|
||||
[
|
||||
State::Blocked,
|
||||
State::Error,
|
||||
State::Working,
|
||||
State::Done,
|
||||
State::Idle,
|
||||
]
|
||||
.into_iter()
|
||||
.find_map(|state| {
|
||||
self.list
|
||||
.iter()
|
||||
.filter(|(_, record, _)| record.state == state)
|
||||
.max_by_key(|(.., at)| *at)
|
||||
.map(|(_, record, _)| record)
|
||||
})
|
||||
}
|
||||
|
||||
/// Each record with its id, in the order first reported.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&str, &Record)> {
|
||||
self.list
|
||||
.iter()
|
||||
.map(|(id, record, _)| (id.as_str(), record))
|
||||
}
|
||||
|
||||
/// The app of the record at `id`: its own, else its nearest ancestor's, the root's last; herdr's agents have none.
|
||||
pub fn app(&self, id: &str) -> Option<&str> {
|
||||
let mut at = id;
|
||||
loop {
|
||||
let app = self
|
||||
.list
|
||||
.iter()
|
||||
.find(|(name, ..)| name == at)
|
||||
.and_then(|(_, record, _)| record.app.as_deref());
|
||||
if app.is_some() || at.is_empty() || at.starts_with(HERDR) {
|
||||
return app;
|
||||
}
|
||||
at = at.rfind('/').map_or("", |slash| &at[..slash]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -298,3 +298,76 @@ fn osc_9_4_maps_to_the_root_record_until_a_program_reports() {
|
||||
"a full reset maps again"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_row_shows_the_most_pressing_record_the_last_reported_in_its_state() {
|
||||
let mut records = Records::default();
|
||||
assert_eq!(records.shown(), None);
|
||||
let shown = |records: &Records| records.shown().map(|record| (record.state, record.kind));
|
||||
records.report("a".into(), record(State::Idle, None));
|
||||
assert_eq!(shown(&records), Some((State::Idle, None)));
|
||||
records.report("b".into(), record(State::Done, None));
|
||||
assert_eq!(shown(&records), Some((State::Done, None)));
|
||||
records.report("c".into(), record(State::Working, None));
|
||||
assert_eq!(shown(&records), Some((State::Working, None)));
|
||||
records.report("d".into(), record(State::Error, None));
|
||||
assert_eq!(shown(&records), Some((State::Error, None)));
|
||||
let asks = |kind| Record {
|
||||
kind: Some(kind),
|
||||
..record(State::Blocked, None)
|
||||
};
|
||||
records.report("e".into(), asks(Kind::Auth));
|
||||
records.report("f".into(), asks(Kind::Question));
|
||||
assert_eq!(
|
||||
shown(&records),
|
||||
Some((State::Blocked, Some(Kind::Question)))
|
||||
);
|
||||
records.report("e".into(), asks(Kind::Auth));
|
||||
assert_eq!(
|
||||
shown(&records),
|
||||
Some((State::Blocked, Some(Kind::Auth))),
|
||||
"the last reported speaks"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_record_without_an_app_takes_its_nearest_ancestor_s() {
|
||||
let mut records = Records::default();
|
||||
records.report("us/east".into(), record(State::Working, None));
|
||||
assert_eq!(records.app("us/east"), None);
|
||||
records.report(String::new(), record(State::Working, Some("deploy")));
|
||||
records.report("us".into(), record(State::Working, Some("push")));
|
||||
records.report("us/east/a".into(), record(State::Working, None));
|
||||
records.report("eu/west".into(), record(State::Working, None));
|
||||
assert_eq!(
|
||||
records.app("us/east/a"),
|
||||
Some("push"),
|
||||
"past a parent without one"
|
||||
);
|
||||
assert_eq!(records.app("eu/west"), Some("deploy"), "the root's last");
|
||||
assert_eq!(records.app(""), Some("deploy"));
|
||||
assert_eq!(
|
||||
records.app("usa"),
|
||||
Some("deploy"),
|
||||
"a sibling's name is no parent"
|
||||
);
|
||||
records.herdr(&[("herdr:w1:p1".into(), record(State::Working, None))]);
|
||||
assert_eq!(
|
||||
records.app("herdr:w1:p1"),
|
||||
None,
|
||||
"herdr's agents are no program's children"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_sees_done_and_error_but_herdr_s() {
|
||||
let mut records = Records::default();
|
||||
records.report("a".into(), record(State::Done, None));
|
||||
records.report("b".into(), record(State::Error, None));
|
||||
records.report("c".into(), record(State::Working, None));
|
||||
assert!(records.herdr(&[("herdr:w1:p1".into(), record(State::Done, None))]));
|
||||
assert!(records.seen());
|
||||
let ids: Vec<&str> = records.iter().map(|(id, _)| id).collect();
|
||||
assert_eq!(ids, ["c", "herdr:w1:p1"]);
|
||||
assert!(!records.seen(), "nothing more to see");
|
||||
}
|
||||
@@ -55,6 +55,8 @@ pub enum TerminalEvent {
|
||||
Finished(Option<i32>),
|
||||
/// A notification to show or close, OSC 9, 777 or 99.
|
||||
Notice(Ask),
|
||||
/// Its records may have changed.
|
||||
Records,
|
||||
Exited(ExitStatus),
|
||||
}
|
||||
|
||||
@@ -148,6 +150,11 @@ impl TerminalView {
|
||||
self.pty.pid
|
||||
}
|
||||
|
||||
/// What programs in this terminal say they are doing.
|
||||
pub fn records(&self) -> &Records {
|
||||
&self.records
|
||||
}
|
||||
|
||||
fn pump(mut events: Events, cx: &mut Context<Self>) -> Task<()> {
|
||||
cx.spawn(async move |view, cx| {
|
||||
while let Some(first) = events.next().await {
|
||||
@@ -220,6 +227,15 @@ impl TerminalView {
|
||||
let drawn = events
|
||||
.iter()
|
||||
.any(|event| matches!(event, PtyEvent::Term(Event::Wakeup)));
|
||||
let restated = events.iter().any(|event| {
|
||||
matches!(
|
||||
event,
|
||||
PtyEvent::Mark(Mark::Prompt)
|
||||
| PtyEvent::Status(Status::Report(..) | Status::Clear(_))
|
||||
| PtyEvent::Ask(Ask::Progress(_))
|
||||
| PtyEvent::Reset
|
||||
)
|
||||
});
|
||||
for event in events {
|
||||
match event {
|
||||
PtyEvent::Folder(folder) => {
|
||||
@@ -241,6 +257,9 @@ impl TerminalView {
|
||||
if drawn && self.finder.is_some() {
|
||||
self.search(false, cx);
|
||||
}
|
||||
if restated {
|
||||
cx.emit(TerminalEvent::Records);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -154,7 +154,9 @@ impl Workspace {
|
||||
folder: launch.cwd,
|
||||
reports: launch.reports,
|
||||
bell: false,
|
||||
waiting: false,
|
||||
status: None,
|
||||
state: None,
|
||||
_events: events,
|
||||
});
|
||||
}
|
||||
@@ -184,7 +186,7 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows session `index`, clears its bell, and gives its terminal the keys.
|
||||
/// Shows session `index`, clears its bell and its waiting mark, and gives its terminal the keys.
|
||||
pub fn select(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if index >= self.sessions.len() {
|
||||
return;
|
||||
@@ -196,6 +198,7 @@ impl Workspace {
|
||||
}
|
||||
let session = &mut self.sessions[index];
|
||||
session.bell = false;
|
||||
session.waiting = false;
|
||||
window.focus(&session.terminal.focus_handle(cx), cx);
|
||||
let folder = session.folder.clone();
|
||||
self.files.update(cx, |files, cx| files.show(&folder, cx));
|
||||
|
||||
+14
-1
@@ -1,7 +1,7 @@
|
||||
use gpui::{Context, EntityId, Window};
|
||||
|
||||
use super::Workspace;
|
||||
use crate::term::TerminalEvent;
|
||||
use crate::term::{State, TerminalEvent};
|
||||
|
||||
impl Workspace {
|
||||
/// What session `id`'s terminal reported.
|
||||
@@ -46,6 +46,19 @@ impl Workspace {
|
||||
}
|
||||
session.status = *status;
|
||||
}
|
||||
TerminalEvent::Records => {
|
||||
let terminal = session.terminal.read(cx);
|
||||
let state = terminal.records().shown().map(|record| record.state);
|
||||
if state != session.state {
|
||||
log::info!("app: session {} shows {state:?}", terminal.pid());
|
||||
// A hidden session that starts waiting marks its row until it is shown.
|
||||
if state == Some(State::Blocked) && index != self.active {
|
||||
log::debug!("app: hidden session {} waits", index + 1);
|
||||
session.waiting = true;
|
||||
}
|
||||
session.state = state;
|
||||
}
|
||||
}
|
||||
TerminalEvent::Notice(ask) => {
|
||||
let pid = session.terminal.read(cx).pid();
|
||||
log::info!("app: session {pid} asks {ask:?}");
|
||||
|
||||
Reference in new issue
Block a user