T39: review round 1: the view notes the state its row shows after each change it hears and tells the workspace of each, so a hidden session that waits, works and waits again in one output starts waiting again; hover reads the records and the last command's status, which the view now keeps, at each frame, stays under the pointer and scrolls within the window; the progress ring holds Ely's loader, since its track can match a selected row's ground
This commit is contained in:
10 files changed
+119
-61
No files matched your search
+25
-12
@@ -7,7 +7,7 @@ use ely_gpui_component::{
|
||||
primitives::{Icon, IconName, Tooltip},
|
||||
theme::{ActiveTheme, ControlSize, IconSize},
|
||||
};
|
||||
use gpui::{AnyElement, App, Entity, Hsla, SharedString, Subscription, div, prelude::*};
|
||||
use gpui::{AnyElement, App, Entity, Hsla, SharedString, Subscription, div, prelude::*, rems};
|
||||
|
||||
use crate::{
|
||||
term::{Kind, Record, Records, State, TerminalView},
|
||||
@@ -27,10 +27,6 @@ pub struct Session {
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -100,7 +96,7 @@ pub fn row(
|
||||
close.update(cx, |this, cx| this.close(index, window, cx))
|
||||
}),
|
||||
);
|
||||
let mark = mark(pid, terminal.records(), session.status, cx);
|
||||
let mark = mark(&session.terminal, cx);
|
||||
let bell = session
|
||||
.bell
|
||||
.then(|| Icon::new(IconName::Bell).color(cx.theme().colors.warning));
|
||||
@@ -118,11 +114,13 @@ pub fn row(
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
fn mark(terminal: &Entity<TerminalView>, cx: &App) -> Option<AnyElement> {
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let view = terminal.read(cx);
|
||||
let (pid, records) = (view.pid() as usize, view.records());
|
||||
let icon = |name: IconName, color: Hsla| Icon::new(name).color(color).into_any_element();
|
||||
let failed = status.filter(|&code| code != 0);
|
||||
let failed = view.ended().filter(|&code| code != 0);
|
||||
let mark = match (records.shown(), failed) {
|
||||
(
|
||||
Some(Record {
|
||||
@@ -156,6 +154,12 @@ fn mark(pid: usize, records: &Records, status: Option<i32>, cx: &App) -> Option<
|
||||
_,
|
||||
) => ProgressRing::new(("session-progress", pid), f32::from(*value) / 100.0)
|
||||
.size(theme.icon_size(IconSize::Xl))
|
||||
// The track takes the border's color, which a selected row's ground may match.
|
||||
.inside(
|
||||
Icon::new(IconName::LoaderCircle)
|
||||
.size(IconSize::Xs)
|
||||
.color(colors.info),
|
||||
)
|
||||
.into_any_element(),
|
||||
// A turning spinner would draw the window at every frame while the program works.
|
||||
(
|
||||
@@ -180,12 +184,21 @@ fn mark(pid: usize, records: &Records, status: Option<i32>, cx: &App) -> Option<
|
||||
) => icon(IconName::Circle, colors.fg_muted),
|
||||
(None, None) => return None,
|
||||
};
|
||||
let lines = hover(records, failed);
|
||||
let tip = Tooltip::rich(move |_, _| {
|
||||
// Hover reads the records as they stand, so an open one follows them, and scrolls within the window.
|
||||
let watched = terminal.downgrade();
|
||||
let tip = Tooltip::rich(move |window, cx| {
|
||||
let lines = watched.upgrade().map_or_else(Vec::new, |terminal| {
|
||||
let view = terminal.read(cx);
|
||||
hover(view.records(), view.ended().filter(|&code| code != 0))
|
||||
});
|
||||
let room = window.viewport_size().height - rems(3.).to_pixels(window.rem_size());
|
||||
div()
|
||||
.id("session-hover")
|
||||
.max_h(room)
|
||||
.overflow_y_scroll()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.children(lines.iter().cloned())
|
||||
.children(lines)
|
||||
.into_any_element()
|
||||
});
|
||||
// Every mark takes the ring's box, so the title stays put as the state changes.
|
||||
@@ -196,7 +209,7 @@ fn mark(pid: usize, records: &Records, status: Option<i32>, cx: &App) -> Option<
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.tooltip(tip)
|
||||
.hoverable_tooltip(tip)
|
||||
.child(mark)
|
||||
.into_any_element(),
|
||||
)
|
||||
|
||||
+2
-2
@@ -78,7 +78,7 @@ pub fn follow(cx: &mut Context<TerminalView>) -> Task<()> {
|
||||
states.clear();
|
||||
let gone = view.update(cx, |view, cx| {
|
||||
if view.records.herdr(&[]) {
|
||||
cx.emit(TerminalEvent::Records);
|
||||
view.restate(cx);
|
||||
}
|
||||
});
|
||||
if gone.is_err() {
|
||||
@@ -128,7 +128,7 @@ pub fn follow(cx: &mut Context<TerminalView>) -> Task<()> {
|
||||
let still = view.reports == reports;
|
||||
if still {
|
||||
if view.records.herdr(&agents) {
|
||||
cx.emit(TerminalEvent::Records);
|
||||
view.restate(cx);
|
||||
}
|
||||
if let Some(folder) = report.clone() {
|
||||
cx.emit(TerminalEvent::Folder(folder));
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ use gpui::{
|
||||
|
||||
use super::{
|
||||
keys::{self, Action, Key, Kitty},
|
||||
view::{TerminalEvent, TerminalView},
|
||||
view::TerminalView,
|
||||
};
|
||||
|
||||
actions!(elyterminal, [Find, FindNewer, PreviousPrompt, NextPrompt]);
|
||||
@@ -78,7 +78,7 @@ impl TerminalView {
|
||||
drop(term);
|
||||
self.pty.write(bytes);
|
||||
if self.records.seen() {
|
||||
cx.emit(TerminalEvent::Records);
|
||||
self.restate(cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@ pub struct Records {
|
||||
clock: u64,
|
||||
/// A program has reported through OSC 7501 since the last full reset, so OSC 9;4 maps no more.
|
||||
reported: bool,
|
||||
/// The state a row showed at the last note.
|
||||
shows: Option<State>,
|
||||
}
|
||||
|
||||
impl Records {
|
||||
@@ -317,6 +319,19 @@ impl Records {
|
||||
})
|
||||
}
|
||||
|
||||
/// Notes the state a row shows now; true when it changed since the last note.
|
||||
pub fn restate(&mut self) -> bool {
|
||||
let now = self.shown().map(|record| record.state);
|
||||
let changed = now != self.shows;
|
||||
self.shows = now;
|
||||
changed
|
||||
}
|
||||
|
||||
/// The state a row showed at the last note.
|
||||
pub fn shows(&self) -> Option<State> {
|
||||
self.shows
|
||||
}
|
||||
|
||||
/// Each record with its id, in the order first reported.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&str, &Record)> {
|
||||
self.list
|
||||
|
||||
@@ -371,3 +371,21 @@ fn typing_sees_done_and_error_but_herdr_s() {
|
||||
assert_eq!(ids, ["c", "herdr:w1:p1"]);
|
||||
assert!(!records.seen(), "nothing more to see");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_change_of_the_state_a_row_shows_is_noted() {
|
||||
let mut records = Records::default();
|
||||
assert!(!records.restate(), "none shows yet");
|
||||
records.report(String::new(), record(State::Blocked, None));
|
||||
assert!(records.restate());
|
||||
records.report(String::new(), record(State::Working, None));
|
||||
assert!(records.restate());
|
||||
records.report(String::new(), record(State::Blocked, None));
|
||||
assert!(records.restate(), "waiting again after work in one output");
|
||||
assert_eq!(records.shows(), Some(State::Blocked));
|
||||
records.report("b".into(), record(State::Working, None));
|
||||
assert!(!records.restate(), "the row shows what it showed");
|
||||
records.reset();
|
||||
assert!(records.restate());
|
||||
assert_eq!(records.shows(), None);
|
||||
}
|
||||
+47
-20
@@ -28,7 +28,7 @@ use super::{
|
||||
paint::{self, Look},
|
||||
pointer::Pointer,
|
||||
pty::{Events, Pty, PtyEvent, config, fit},
|
||||
status::{self, Records, Status},
|
||||
status::{self, Records, State, Status},
|
||||
zoom,
|
||||
};
|
||||
use crate::{
|
||||
@@ -55,8 +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,
|
||||
/// The state its row shows changed, to this one.
|
||||
Shows(Option<State>),
|
||||
Exited(ExitStatus),
|
||||
}
|
||||
|
||||
@@ -79,6 +79,8 @@ pub struct TerminalView {
|
||||
pub(super) reports: u64,
|
||||
/// What programs say they are doing, OSC 7501.
|
||||
pub(super) records: Records,
|
||||
/// The status its shell's last command ended with; none before one, or when it gave none.
|
||||
ended: Option<i32>,
|
||||
cell: (u16, u16),
|
||||
/// Focused in an active window, as the program last heard.
|
||||
focused: bool,
|
||||
@@ -127,6 +129,7 @@ impl TerminalView {
|
||||
size,
|
||||
reports: 0,
|
||||
records: Records::default(),
|
||||
ended: None,
|
||||
cell: (8, 16),
|
||||
focused: false,
|
||||
blinking: false,
|
||||
@@ -155,6 +158,30 @@ impl TerminalView {
|
||||
&self.records
|
||||
}
|
||||
|
||||
/// The status its shell's last command ended with.
|
||||
pub fn ended(&self) -> Option<i32> {
|
||||
self.ended
|
||||
}
|
||||
|
||||
/// Notes the state its row shows, and tells the owner of each change.
|
||||
pub(super) fn restate(&mut self, cx: &mut Context<Self>) {
|
||||
if self.records.restate() {
|
||||
let state = self.records.shows();
|
||||
log::info!("terminal {}: its row shows {state:?}", self.pty.pid);
|
||||
cx.emit(TerminalEvent::Shows(state));
|
||||
}
|
||||
}
|
||||
|
||||
/// A command the shell ran ended with `status`.
|
||||
fn finished(&mut self, status: Option<i32>, cx: &mut Context<Self>) {
|
||||
if self.ended != status {
|
||||
let ended = status.map_or("with no status".into(), |code| format!("with {code}"));
|
||||
log::info!("terminal {}: its last command ended {ended}", self.pty.pid);
|
||||
}
|
||||
self.ended = status;
|
||||
cx.emit(TerminalEvent::Finished(status));
|
||||
}
|
||||
|
||||
fn pump(mut events: Events, cx: &mut Context<Self>) -> Task<()> {
|
||||
cx.spawn(async move |view, cx| {
|
||||
while let Some(first) = events.next().await {
|
||||
@@ -227,15 +254,6 @@ 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) => {
|
||||
@@ -244,22 +262,31 @@ 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(Mark::Done(status)) => self.finished(status, cx),
|
||||
// A new prompt ends what was working, waiting or idle; done and error stay.
|
||||
PtyEvent::Mark(Mark::Prompt) => self.records.prompt(),
|
||||
PtyEvent::Mark(Mark::Prompt) => {
|
||||
self.records.prompt();
|
||||
self.restate(cx);
|
||||
}
|
||||
PtyEvent::Mark(_) => {}
|
||||
PtyEvent::Status(status) => self.status(status),
|
||||
PtyEvent::Ask(ask) => self.ask(ask, cx),
|
||||
PtyEvent::Reset => self.records.reset(),
|
||||
PtyEvent::Status(status) => {
|
||||
self.status(status);
|
||||
self.restate(cx);
|
||||
}
|
||||
PtyEvent::Ask(ask) => {
|
||||
self.ask(ask, cx);
|
||||
self.restate(cx);
|
||||
}
|
||||
PtyEvent::Reset => {
|
||||
self.records.reset();
|
||||
self.restate(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
// ponytail: a full search per output batch while find is open; search only new lines if it lags.
|
||||
if drawn && self.finder.is_some() {
|
||||
self.search(false, cx);
|
||||
}
|
||||
if restated {
|
||||
cx.emit(TerminalEvent::Records);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
||||
@@ -155,8 +155,6 @@ impl Workspace {
|
||||
reports: launch.reports,
|
||||
bell: false,
|
||||
waiting: false,
|
||||
status: None,
|
||||
state: None,
|
||||
_events: events,
|
||||
});
|
||||
}
|
||||
|
||||
+7
-20
@@ -37,26 +37,13 @@ impl Workspace {
|
||||
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::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::Finished(_) => {}
|
||||
TerminalEvent::Shows(state) => {
|
||||
let now = session.terminal.read(cx).records().shows();
|
||||
// A hidden session that starts waiting, and still waits, marks its row until it is shown.
|
||||
if *state == Some(State::Blocked) && now == *state && index != self.active {
|
||||
log::debug!("app: hidden session {} waits", index + 1);
|
||||
session.waiting = true;
|
||||
}
|
||||
}
|
||||
TerminalEvent::Notice(ask) => {
|
||||
|
||||
Reference in new issue
Block a user