T42: a session's status mark moves, as the owner asked after aicss.dev's orbs: 3 by 3 dots in the state's color; work ripples from the centre, progress fills the outer ring clockwise from the top, waiting stays lit and dips twice, error, done and idle draw an X, a check and a ring, then hold; the window draws at the marks' 80 ms steps only while one moves; no review, as the owner asked
This commit is contained in:
9 files changed
+350
-81
No files matched your search
+91
-75
@@ -1,19 +1,25 @@
|
||||
use std::path::{MAIN_SEPARATOR, Path, PathBuf};
|
||||
use std::{
|
||||
path::{MAIN_SEPARATOR, Path, PathBuf},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use ely_gpui_component::{
|
||||
buttons::{ButtonVariant, IconButton},
|
||||
lists::ListItem,
|
||||
motion::ProgressRing,
|
||||
primitives::{Icon, IconName, Tooltip},
|
||||
theme::{ActiveTheme, ControlSize, IconSize},
|
||||
};
|
||||
use gpui::{AnyElement, App, Entity, Hsla, SharedString, Subscription, div, prelude::*, rems};
|
||||
use gpui::{AnyElement, App, Entity, SharedString, Subscription, div, prelude::*, rems};
|
||||
|
||||
use crate::{
|
||||
term::{Kind, Record, Records, State, TerminalView},
|
||||
workspace::{TITLE, Workspace},
|
||||
};
|
||||
|
||||
pub mod orb;
|
||||
|
||||
use orb::Look;
|
||||
|
||||
/// A program on its own terminal, as the sessions column lists it.
|
||||
pub struct Session {
|
||||
pub terminal: Entity<TerminalView>,
|
||||
@@ -27,9 +33,25 @@ pub struct Session {
|
||||
pub bell: bool,
|
||||
/// It started waiting on the user while hidden.
|
||||
pub waiting: bool,
|
||||
/// The look its mark shows, and since when.
|
||||
pub look: Option<(Look, Instant)>,
|
||||
pub _events: Subscription,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// Notes the look its mark takes `now`; true while the mark moves.
|
||||
pub fn note(&mut self, now: Instant, cx: &App) -> bool {
|
||||
let view = self.terminal.read(cx);
|
||||
let failed = view.ended().is_some_and(|code| code != 0);
|
||||
let shows = look(view.records().shown().map(|(_, record)| record), failed);
|
||||
if self.look.map(|(shown, _)| shown) != shows {
|
||||
self.look = shows.map(|shows| (shows, now));
|
||||
}
|
||||
self.look
|
||||
.is_some_and(|(look, since)| orb::moves(look, now.saturating_duration_since(since)))
|
||||
}
|
||||
}
|
||||
|
||||
/// The most records hover lists, and the most characters it shows of each part.
|
||||
const LISTED: usize = 10;
|
||||
const SHORT: usize = 120;
|
||||
@@ -71,6 +93,7 @@ pub fn row(
|
||||
session: &Session,
|
||||
active: bool,
|
||||
home: Option<&Path>,
|
||||
now: Instant,
|
||||
cx: &App,
|
||||
) -> ListItem {
|
||||
let terminal = session.terminal.read(cx);
|
||||
@@ -96,7 +119,7 @@ pub fn row(
|
||||
close.update(cx, |this, cx| this.close(index, window, cx))
|
||||
}),
|
||||
);
|
||||
let mark = mark(&session.terminal, cx);
|
||||
let mark = mark(session, now, cx);
|
||||
let bell = session
|
||||
.bell
|
||||
.then(|| Icon::new(IconName::Bell).color(cx.theme().colors.warning));
|
||||
@@ -113,79 +136,36 @@ pub fn row(
|
||||
)
|
||||
}
|
||||
|
||||
/// The mark of what the session's records say, else of its last command's failure; hover lists them.
|
||||
fn mark(terminal: &Entity<TerminalView>, cx: &App) -> Option<AnyElement> {
|
||||
/// What a mark shows: the most pressing record's state, else a failed last command.
|
||||
pub fn look(record: Option<&Record>, failed: bool) -> Option<Look> {
|
||||
Some(
|
||||
match (record.map(|record| (record.state, record.progress)), failed) {
|
||||
(Some((State::Blocked, _)), _) => Look::Waiting,
|
||||
(Some((State::Error, _)), _) => Look::Error,
|
||||
(Some((State::Working, progress)), _) => Look::Working(progress),
|
||||
(Some((State::Done, _)), _) => Look::Done,
|
||||
(_, true) => Look::Error,
|
||||
(Some((State::Idle, _)), false) => Look::Idle,
|
||||
(None, false) => return None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// The session's mark: its look's dots in its state's color; hover lists the records.
|
||||
fn mark(session: &Session, now: Instant, cx: &App) -> Option<AnyElement> {
|
||||
let (look, since) = session.look?;
|
||||
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 = view.ended().filter(|&code| code != 0);
|
||||
let mark = match (records.shown().map(|(_, record)| record), 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))
|
||||
// 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.
|
||||
(
|
||||
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 color = match look {
|
||||
Look::Working(_) => colors.info,
|
||||
Look::Waiting => colors.warning,
|
||||
Look::Error => colors.danger,
|
||||
Look::Done => colors.success,
|
||||
Look::Idle => colors.fg_muted,
|
||||
};
|
||||
let pid = session.terminal.read(cx).pid() as usize;
|
||||
// Hover reads the records as they stand, so an open one follows them, and scrolls within the window.
|
||||
let watched = terminal.downgrade();
|
||||
let watched = session.terminal.downgrade();
|
||||
let tip = Tooltip::rich(move |window, cx| {
|
||||
let lines = watched.upgrade().map_or_else(Vec::new, |terminal| {
|
||||
let view = terminal.read(cx);
|
||||
@@ -201,7 +181,7 @@ fn mark(terminal: &Entity<TerminalView>, cx: &App) -> Option<AnyElement> {
|
||||
.children(lines)
|
||||
.into_any_element()
|
||||
});
|
||||
// Every mark takes the ring's box, so the title stays put as the state changes.
|
||||
// Every mark takes a 24-pixel box, so the title stays put as the state changes.
|
||||
Some(
|
||||
div()
|
||||
.id(("session-status", pid))
|
||||
@@ -210,7 +190,7 @@ fn mark(terminal: &Entity<TerminalView>, cx: &App) -> Option<AnyElement> {
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.hoverable_tooltip(tip)
|
||||
.child(mark)
|
||||
.child(orb::lattice(look, color, now, since))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
@@ -332,6 +312,42 @@ mod tests {
|
||||
assert_eq!(after_close(1, 2, 3), 1, "one after it went");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_mark_shows_the_most_pressing_look() {
|
||||
let record = |state, progress| Record {
|
||||
state,
|
||||
kind: None,
|
||||
progress,
|
||||
app: None,
|
||||
title: None,
|
||||
msg: None,
|
||||
};
|
||||
let waiting = look(Some(&record(State::Blocked, None)), true);
|
||||
assert_eq!(
|
||||
waiting,
|
||||
Some(Look::Waiting),
|
||||
"waiting outranks a failed command"
|
||||
);
|
||||
let working = look(Some(&record(State::Working, Some(40))), false);
|
||||
assert_eq!(working, Some(Look::Working(Some(40))));
|
||||
let idle = record(State::Idle, None);
|
||||
assert_eq!(
|
||||
look(Some(&idle), true),
|
||||
Some(Look::Error),
|
||||
"a failed command outranks idle"
|
||||
);
|
||||
assert_eq!(look(Some(&idle), false), Some(Look::Idle));
|
||||
assert_eq!(
|
||||
look(Some(&record(State::Done, None)), true),
|
||||
Some(Look::Done)
|
||||
);
|
||||
assert_eq!(
|
||||
look(Some(&record(State::Error, None)), false),
|
||||
Some(Look::Error)
|
||||
);
|
||||
assert_eq!(look(None, false), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_shows_a_program_s_text_as_text() {
|
||||
assert_eq!(plain("evil\u{202e}txt.exe"), "eviltxt.exe");
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
//! A session's status mark: 3 by 3 dots in its state's color, every mark stepping on one clock.
|
||||
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
f64::consts::TAU,
|
||||
sync::OnceLock,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use gpui::{Div, Hsla, ParentElement, Styled, div};
|
||||
|
||||
/// The dots' beat: while a mark moves, the window draws at each step, not at every frame.
|
||||
pub const STEP: Duration = Duration::from_millis(80);
|
||||
|
||||
/// What a mark shows.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Look {
|
||||
/// A ripple from the centre; with progress, the outer ring fills clockwise from the top.
|
||||
Working(Option<u8>),
|
||||
/// Lit, with two quick dips.
|
||||
Waiting,
|
||||
/// A program's error, or a failed last command.
|
||||
Error,
|
||||
Done,
|
||||
Idle,
|
||||
}
|
||||
|
||||
/// One ripple, in seconds, and how far a dot one dot out from the centre lags it.
|
||||
const RIPPLE: f64 = 1.2;
|
||||
const LAG: f64 = 0.3;
|
||||
/// Steps in one round of a waiting mark's dips.
|
||||
const BLINK: u64 = 20;
|
||||
/// A working dot at its lowest, a waiting mark in a dip, and a dot outside a settled shape.
|
||||
const DIM: f32 = 0.35;
|
||||
const DIP: f32 = 0.25;
|
||||
const FAINT: f32 = 0.12;
|
||||
/// Steps a settled shape's dot takes to light, and between one dot and the next.
|
||||
const FADE: u64 = 2;
|
||||
const STAGGER: u64 = 1;
|
||||
/// The outer ring clockwise from the top, then the centre: the order progress fills.
|
||||
const FILL: [usize; 9] = [1, 2, 5, 8, 7, 6, 3, 0, 4];
|
||||
|
||||
/// A settled look's dots in the order they light, and how bright they end; none for a look that keeps moving.
|
||||
fn shape(look: Look) -> Option<(&'static [usize], f32)> {
|
||||
match look {
|
||||
Look::Working(_) | Look::Waiting => None,
|
||||
Look::Error => Some((&[0, 4, 8, 2, 6], 1.0)),
|
||||
Look::Done => Some((&[3, 7, 5, 2], 1.0)),
|
||||
Look::Idle => Some((&FILL[..8], 0.6)),
|
||||
}
|
||||
}
|
||||
|
||||
fn steps(time: Duration) -> u64 {
|
||||
(time.as_millis() / STEP.as_millis()) as u64
|
||||
}
|
||||
|
||||
/// A dot's brightness `t` seconds into its ripple.
|
||||
fn crest(t: f64) -> f32 {
|
||||
let wave = 0.5 + 0.5 * (TAU * t / RIPPLE).cos();
|
||||
DIM + (1.0 - DIM) * wave as f32
|
||||
}
|
||||
|
||||
/// Each dot's opacity, row by row, `clock` on the marks' clock and `held` after the look began.
|
||||
pub fn dots(look: Look, clock: Duration, held: Duration) -> [f32; 9] {
|
||||
let t = steps(clock) as f64 * STEP.as_secs_f64();
|
||||
let mut levels = [DIM; 9];
|
||||
match look {
|
||||
Look::Working(None) => {
|
||||
for (dot, level) in levels.iter_mut().enumerate() {
|
||||
let (x, y) = ((dot % 3) as f64 - 1.0, (dot / 3) as f64 - 1.0);
|
||||
*level = crest(t - x.hypot(y) * LAG);
|
||||
}
|
||||
}
|
||||
Look::Working(Some(progress)) => {
|
||||
let filled = (usize::from(progress) * FILL.len() / 100).min(FILL.len() - 1);
|
||||
for (rank, &dot) in FILL.iter().enumerate() {
|
||||
levels[dot] = match rank.cmp(&filled) {
|
||||
Ordering::Less => 1.0,
|
||||
Ordering::Equal => crest(t),
|
||||
Ordering::Greater => DIM,
|
||||
};
|
||||
}
|
||||
}
|
||||
Look::Waiting => {
|
||||
let dip = matches!(steps(clock) % BLINK, 0 | 1 | 4 | 5);
|
||||
levels = [if dip { DIP } else { 1.0 }; 9];
|
||||
}
|
||||
settled => {
|
||||
let (shape, bright) = shape(settled).expect("a settled look has a shape");
|
||||
levels = [FAINT; 9];
|
||||
let held = steps(held);
|
||||
for (rank, &dot) in shape.iter().enumerate() {
|
||||
let lit = held.saturating_sub(rank as u64 * STAGGER).min(FADE) as f32 / FADE as f32;
|
||||
levels[dot] = bright * lit + FAINT * (1.0 - lit);
|
||||
}
|
||||
}
|
||||
}
|
||||
levels
|
||||
}
|
||||
|
||||
/// Whether a mark still changes `held` after its look began: work and waiting always, a settled shape until it is drawn.
|
||||
pub fn moves(look: Look, held: Duration) -> bool {
|
||||
shape(look).is_none_or(|(shape, _)| steps(held) < (shape.len() as u64 - 1) * STAGGER + FADE)
|
||||
}
|
||||
|
||||
/// Time on the clock every mark steps to, so marks move together.
|
||||
fn clock(now: Instant) -> Duration {
|
||||
static START: OnceLock<Instant> = OnceLock::new();
|
||||
now.saturating_duration_since(*START.get_or_init(|| now))
|
||||
}
|
||||
|
||||
/// How long until the clock's next step, a little past it.
|
||||
pub fn wait(now: Instant) -> Duration {
|
||||
let into = clock(now).as_nanos() % STEP.as_nanos();
|
||||
STEP - Duration::from_nanos(into as u64) + Duration::from_millis(2)
|
||||
}
|
||||
|
||||
/// The mark's dots in `color`, `now`, its look shown since `since`.
|
||||
pub fn lattice(look: Look, color: Hsla, now: Instant, since: Instant) -> Div {
|
||||
let levels = dots(look, clock(now), now.saturating_duration_since(since));
|
||||
div()
|
||||
.grid()
|
||||
.grid_cols(3)
|
||||
.gap_0p5()
|
||||
.children(levels.map(|level| div().size_1().rounded_full().bg(color.opacity(level))))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn at(steps: u32) -> Duration {
|
||||
STEP * steps
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn work_ripples_from_the_centre_to_the_edges_then_the_corners() {
|
||||
let levels = |steps| dots(Look::Working(None), at(steps), Duration::ZERO);
|
||||
let peak = |dot: usize| {
|
||||
(0..15)
|
||||
.max_by(|&a, &b| levels(a)[dot].total_cmp(&levels(b)[dot]))
|
||||
.expect("steps")
|
||||
};
|
||||
let (centre, edge, corner) = (peak(4), peak(1), peak(0));
|
||||
assert_eq!(centre, 0);
|
||||
assert!(centre < edge && edge < corner, "{centre} {edge} {corner}");
|
||||
assert_eq!(peak(3), edge, "every edge together");
|
||||
assert_eq!(peak(8), corner, "every corner together");
|
||||
let again = levels(15);
|
||||
assert!(
|
||||
levels(0)
|
||||
.iter()
|
||||
.zip(again)
|
||||
.all(|(a, b)| (a - b).abs() < 1e-4),
|
||||
"one ripple every 15 steps"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn progress_fills_the_ring_clockwise_from_the_top_then_the_centre() {
|
||||
let lit = |progress, steps| dots(Look::Working(Some(progress)), at(steps), Duration::ZERO);
|
||||
let half = lit(50, 9);
|
||||
assert!([1, 2, 5, 8].iter().all(|&dot| half[dot] == 1.0), "{half:?}");
|
||||
assert!([6, 3, 0, 4].iter().all(|&dot| half[dot] == DIM), "{half:?}");
|
||||
assert!(lit(50, 0)[7] > half[7], "the next dot pulses");
|
||||
let full = lit(100, 9);
|
||||
assert!(FILL[..8].iter().all(|&dot| full[dot] == 1.0), "{full:?}");
|
||||
assert!(lit(100, 0)[4] > full[4], "then the centre pulses");
|
||||
assert!(lit(0, 0)[1] > lit(0, 9)[1], "at 0% the top pulses");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn waiting_stays_lit_and_dips_twice() {
|
||||
let blink = |steps| dots(Look::Waiting, at(steps), Duration::ZERO);
|
||||
let dips: Vec<u32> = (0..20).filter(|&steps| blink(steps)[0] == DIP).collect();
|
||||
assert_eq!(dips, [0, 1, 4, 5]);
|
||||
assert!((0..20).all(|steps| blink(steps).iter().all(|&level| level == blink(steps)[0])));
|
||||
assert_eq!(blink(2), [1.0; 9]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_settled_look_draws_its_shape_dot_by_dot_then_holds() {
|
||||
let done = |steps| dots(Look::Done, at(1000), at(steps));
|
||||
assert_eq!(done(0), [FAINT; 9], "nothing lit at first");
|
||||
assert!(
|
||||
done(1)[3] > FAINT && done(1)[2] == FAINT,
|
||||
"the check draws from its left end"
|
||||
);
|
||||
let check = [3, 7, 5, 2];
|
||||
let drawn = done(5);
|
||||
assert!((0..9).all(|dot| drawn[dot] == if check.contains(&dot) { 1.0 } else { FAINT }));
|
||||
assert_eq!(done(500), drawn, "then it holds still");
|
||||
assert!(moves(Look::Done, at(4)) && !moves(Look::Done, at(5)));
|
||||
let idle = dots(Look::Idle, Duration::ZERO, at(9));
|
||||
assert_eq!(idle[4], FAINT, "idle is a ring");
|
||||
assert!(FILL[..8].iter().all(|&dot| idle[dot] == 0.6));
|
||||
assert!(!moves(Look::Idle, at(9)) && moves(Look::Idle, at(8)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn work_and_waiting_never_stop() {
|
||||
for look in [Look::Working(None), Look::Working(Some(40)), Look::Waiting] {
|
||||
assert!(moves(look, at(1_000_000)), "{look:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
-3
@@ -1,3 +1,5 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use ely_gpui_component::{
|
||||
buttons::{ButtonVariant, IconButton, SegmentedControl},
|
||||
lists::List,
|
||||
@@ -9,7 +11,7 @@ use ely_gpui_component::{
|
||||
use gpui::{
|
||||
Action, App, AppContext, Context, Entity, FocusHandle, Focusable, InteractiveElement,
|
||||
IntoElement, KeyBinding, ParentElement, Render, SharedString, StatefulInteractiveElement,
|
||||
StyleRefinement, Styled, Subscription, Window, actions, div, prelude::FluentBuilder,
|
||||
StyleRefinement, Styled, Subscription, Task, Window, actions, div, prelude::FluentBuilder,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -17,7 +19,7 @@ use crate::{
|
||||
files::Files,
|
||||
git::panel::{Acted, GitPanel},
|
||||
menus,
|
||||
sessions::{self, Session},
|
||||
sessions::{self, Session, orb},
|
||||
settings::{PanelTab, Settings, SettingsStore, StartIn, window::OpenSettings},
|
||||
term::{
|
||||
Events, Launch, Pty, TerminalView, config,
|
||||
@@ -95,6 +97,8 @@ pub struct Workspace {
|
||||
background: Blur,
|
||||
/// Column widths a drag shows before the settings hold them.
|
||||
dragged: Dragged,
|
||||
/// Draws the window at each step of the status marks while one shown moves.
|
||||
ticking: Option<Task<()>>,
|
||||
_settings: Subscription,
|
||||
_acted: Subscription,
|
||||
}
|
||||
@@ -122,6 +126,7 @@ impl Workspace {
|
||||
git,
|
||||
background: Blur::new(window, SettingsStore::get(cx)),
|
||||
dragged: Dragged::default(),
|
||||
ticking: None,
|
||||
_settings: cx.observe_global_in::<SettingsStore>(window, |this, window, cx| {
|
||||
this.background.follow(window, SettingsStore::get(cx));
|
||||
cx.notify();
|
||||
@@ -155,10 +160,36 @@ impl Workspace {
|
||||
reports: launch.reports,
|
||||
bell: false,
|
||||
waiting: false,
|
||||
look: None,
|
||||
_events: events,
|
||||
});
|
||||
}
|
||||
|
||||
/// Notes each session's mark `now`, and draws the window at each step while one shown moves.
|
||||
fn tick(&mut self, now: Instant, cx: &mut Context<Self>) {
|
||||
let mut moving = false;
|
||||
for session in &mut self.sessions {
|
||||
moving |= session.note(now, cx);
|
||||
}
|
||||
let moving = moving && SettingsStore::get(cx).show_sessions;
|
||||
if moving == self.ticking.is_some() {
|
||||
return;
|
||||
}
|
||||
log::debug!("app: status marks {}", if moving { "move" } else { "rest" });
|
||||
self.ticking = moving.then(|| {
|
||||
cx.spawn(async move |this, cx| {
|
||||
loop {
|
||||
cx.background_executor()
|
||||
.timer(orb::wait(Instant::now()))
|
||||
.await;
|
||||
if this.update(cx, |_, cx| cx.notify()).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/// A session of the shell the settings name, in home or the shown session's folder.
|
||||
fn new_session(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(home) = dirs::home_dir() else {
|
||||
@@ -307,6 +338,8 @@ fn column_toggle(
|
||||
impl Render for Workspace {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.background.fit(window);
|
||||
let now = Instant::now();
|
||||
self.tick(now, cx);
|
||||
let settings = SettingsStore::get(cx);
|
||||
let workspace = cx.entity();
|
||||
let tip =
|
||||
@@ -350,7 +383,7 @@ impl Render for Workspace {
|
||||
let home = dirs::home_dir();
|
||||
let rows = self.sessions.iter().enumerate().map(|(index, session)| {
|
||||
let active = index == self.active;
|
||||
sessions::row(&workspace, index, session, active, home.as_deref(), cx)
|
||||
sessions::row(&workspace, index, session, active, home.as_deref(), now, cx)
|
||||
});
|
||||
let list = List::new().children(rows);
|
||||
let new = IconButton::new("new-session", IconName::Plus)
|
||||
|
||||
Reference in new issue
Block a user