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
CI / platforms (ubuntu-24.04) (push) Successful in 1m23s
CI / platforms (windows-latest) (push) Successful in 1m35s
CI / macos (push) Successful in 12m25s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-17 14:43:56 -04:00
1 parent b504bfb9b8
commit 85bcbf466f
12 files changed
+449 -22

No files matched your search

+9 -2
View File
@@ -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
View File
@@ -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();
}
+1
View File
@@ -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
View File
@@ -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)]
+73
View File
@@ -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");
}
+19
View File
@@ -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();
}