Skip to main content

progress/
progress.rs

1//! A `flow` surface: a running card with a spinner and an elapsed timer,
2//! log lines streamed into it, a progress bar, then done. Ctrl+C cancels.
3//! Outside Tern the log prints as plain lines.
4
5use std::time::{Duration, Instant};
6
7use tern_sdk::{
8	Input, Options, Session, SurfaceOptions, nodes,
9	ui::{self, Bound, Extent, Status, View},
10};
11
12/// The steps of the pretend build.
13const STEPS: [&str; 8] = [
14	"Resolving dependencies",
15	"Fetching 214 crates",
16	"Compiling serde v1.0.228",
17	"Compiling tern-sdk v0.1.0",
18	"Linking target/debug/demo",
19	"Running 42 tests",
20	"Packaging demo-0.1.0.tar.gz",
21	"Uploading to the registry",
22];
23
24/// How long each step takes.
25const STEP: Duration = Duration::from_millis(350);
26
27/// Where the build is.
28struct Build {
29	/// Log lines so far.
30	log:    String,
31	/// Steps done.
32	done:   usize,
33	/// When it finished, in ms since the start.
34	took:   Option<u64>,
35	/// The final status.
36	status: Status,
37}
38
39/// The card for `build`.
40fn view(build: &Build) -> View {
41	let fraction = build.done as f64 / STEPS.len() as f64;
42	let running = build.took.is_none();
43	let activity: ui::Node = if running {
44		ui::spinner()
45			.style("dots")
46			.label(STEPS[build.done.min(STEPS.len() - 1)])
47			.into()
48	} else {
49		ui::icon(if build.status == Status::Done {
50			"check"
51		} else {
52			"x"
53		})
54		.tone(if build.status == Status::Done {
55			"success"
56		} else {
57			"error"
58		})
59		.into()
60	};
61	let mut timer = ui::elapsed().age(0);
62	if let Some(took) = build.took {
63		timer = timer.stopped(took);
64	}
65	View::new().main([ui::card()
66		.key("build")
67		.head("Building demo")
68		.status(build.status.clone())
69		.child(
70			ui::row()
71				.key("head")
72				.gap("sm")
73				.children(nodes![activity.key("a"), timer.key("t")]),
74		)
75		.child(
76			ui::ansi(&build.log)
77				.key("log")
78				.follow(true)
79				.max(Bound::h(Extent::Lines(8.0))),
80		)
81		.child(ui::progress().key("bar").value(fraction).label(format!(
82			"{}/{}",
83			build.done,
84			STEPS.len()
85		)))])
86}
87
88/// Waits `time` while handling input: whether Ctrl+C asked to stop.
89fn wait(session: &mut Session, time: Duration) -> Result<bool, tern_sdk::Error> {
90	let until = Instant::now() + time;
91	loop {
92		let left = until.saturating_duration_since(Instant::now());
93		match session.next(Some(left))? {
94			None => return Ok(false),
95			Some(Input::Key(key)) if key.is("ctrl+c") => return Ok(true),
96			Some(_) => {},
97		}
98	}
99}
100
101fn main() -> Result<(), tern_sdk::Error> {
102	let Some(mut session) = Session::connect(Options::new())? else {
103		for (i, step) in STEPS.iter().enumerate() {
104			println!("[{}/{}] {step}", i + 1, STEPS.len());
105			std::thread::sleep(STEP);
106		}
107		println!("Built demo.");
108		return Ok(());
109	};
110	let start = Instant::now();
111	let sf = session.open(SurfaceOptions::flow().title("progress"))?;
112	let mut build =
113		Build { log: String::new(), done: 0, took: None, status: Status::Running };
114	session.render(sf, view(&build))?;
115	for step in STEPS {
116		if wait(&mut session, STEP)? {
117			build.status = Status::Cancelled;
118			break;
119		}
120		build.log.push_str("\x1b[32m✓\x1b[0m ");
121		build.log.push_str(step);
122		build.log.push_str("\r\n");
123		build.done += 1;
124		session.render(sf, view(&build))?;
125	}
126	if build.status == Status::Running {
127		build.status = Status::Done;
128	}
129	let took = start.elapsed();
130	build.took = Some(took.as_millis() as u64);
131	session.render(sf, view(&build))?;
132	session.close()?;
133	match build.status {
134		Status::Done => println!("Built demo in {:.1}s.", took.as_secs_f64()),
135		_ => println!("Build cancelled."),
136	}
137	Ok(())
138}