Skip to content

Commit bdf963d

Browse files
committed
View::new is now const
So you can hold a view in a static global variable.
1 parent 8e4d18b commit bdf963d

File tree

4 files changed

+172
-71
lines changed

4 files changed

+172
-71
lines changed

NEWS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
- New: Support and documentation for sending `tracing` updates through a Nutmeg view.
66

7+
- New: `View::new` is now `const`, so that a `static` view can be constructed in
8+
a global variable: you no longer need to wrap them in an `Arc`. See `examples/static_view`.
9+
710
## 0.1.3
811

912
Released 2023-05-24

examples/static_view.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//! Demonstrates that you can have a View in a static global variable.
2+
//!
3+
//! This works even when the View is accessed by multiple threads, because
4+
//! it synchronizes internally.
5+
6+
use std::sync::atomic::AtomicUsize;
7+
use std::sync::atomic::Ordering::Relaxed;
8+
use std::thread::{self, sleep};
9+
use std::time::Duration;
10+
11+
// Note: The initial model must also be `const`, so you cannot call `Default::default()`.
12+
// Note: Similarly, you can call `nutmeg::Options::new()` but not `Options::default()`.
13+
static VIEW: nutmeg::View<Model> = nutmeg::View::new(
14+
Model {
15+
i: AtomicUsize::new(0),
16+
},
17+
nutmeg::Options::new(),
18+
);
19+
20+
#[derive(Default)]
21+
struct Model {
22+
i: AtomicUsize,
23+
}
24+
25+
impl nutmeg::Model for Model {
26+
fn render(&mut self, _width: usize) -> String {
27+
format!("i={}", self.i.load(Relaxed))
28+
}
29+
}
30+
31+
fn main() -> std::io::Result<()> {
32+
thread::scope(|scope| {
33+
for tid in 0..3 {
34+
scope.spawn(move || {
35+
VIEW.message(format!("thread {} starting\n", tid));
36+
for _i in 0..20 {
37+
VIEW.update(|model| model.i.fetch_add(1, Relaxed));
38+
sleep(Duration::from_millis(rand::random::<u64>() % 200));
39+
}
40+
VIEW.message(format!("thread {} done\n", tid));
41+
});
42+
}
43+
});
44+
Ok(())
45+
}

src/destination.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2022-2023 Martin Pool.
2+
3+
use std::env;
4+
use std::result::Result;
5+
6+
#[allow(unused)] // for docstrings
7+
use crate::View;
8+
use crate::{ansi, width};
9+
10+
/// Destinations for progress bar output.
11+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12+
pub enum Destination {
13+
/// Draw to stdout.
14+
Stdout,
15+
/// Draw to stderr.
16+
Stderr,
17+
/// Draw to an internal capture buffer, which can be retrieved with [View::captured_output].
18+
///
19+
/// This is intended for testing.
20+
///
21+
/// A width of 80 columns is used.
22+
Capture,
23+
}
24+
25+
impl Destination {
26+
/// Determine if this destination is possible, and, if necessary, enable Windows ANSI support.
27+
pub(crate) fn initalize(&self) -> Result<(), ()> {
28+
if match self {
29+
Destination::Stdout => {
30+
atty::is(atty::Stream::Stdout) && !is_dumb_term() && ansi::enable_windows_ansi()
31+
}
32+
Destination::Stderr => {
33+
atty::is(atty::Stream::Stderr) && !is_dumb_term() && ansi::enable_windows_ansi()
34+
}
35+
Destination::Capture => true,
36+
} {
37+
Ok(())
38+
} else {
39+
Err(())
40+
}
41+
}
42+
43+
pub(crate) fn width(&self) -> Option<usize> {
44+
match self {
45+
Destination::Stdout => width::stdout_width(),
46+
Destination::Stderr => width::stderr_width(),
47+
Destination::Capture => Some(80),
48+
}
49+
}
50+
}
51+
52+
fn is_dumb_term() -> bool {
53+
env::var("TERM").map_or(false, |s| s.eq_ignore_ascii_case("dumb"))
54+
}

src/lib.rs

Lines changed: 70 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,9 @@ fn main() -> std::io::Result<()> {
106106
model.i += 1;
107107
model.last_file_name = format!("file{}.txt", i);
108108
});
109-
// 5. Interleave text output lines by writing to the view.
109+
// 5. Interleave text output lines by writing messages to the view.
110110
if i % 10 == 3 {
111-
writeln!(view, "reached {}", i)?;
111+
view.message(format!("reached {}", i));
112112
}
113113
}
114114
@@ -211,7 +211,6 @@ is welcome.
211211

212212
#![warn(missing_docs)]
213213

214-
use std::env;
215214
use std::fmt::Display;
216215
use std::io::{self, Write};
217216
use std::sync::Arc;
@@ -220,6 +219,7 @@ use std::time::{Duration, Instant};
220219
use parking_lot::Mutex;
221220

222221
mod ansi;
222+
mod destination;
223223
mod helpers;
224224
pub mod models;
225225
mod width;
@@ -233,6 +233,7 @@ pub mod _changelog {
233233
}
234234

235235
pub use crate::helpers::*;
236+
pub use destination::Destination;
236237

237238
/// An application-defined type that holds whatever state is relevant to the
238239
/// progress bar, and that can render it into one or more lines of text.
@@ -336,6 +337,42 @@ where
336337
/// It is OK to print incomplete lines, i.e. without a final `\n`
337338
/// character. In this case the progress bar remains suspended
338339
/// until the line is completed.
340+
///
341+
/// ## Static views
342+
///
343+
/// Views can be constructed as static variables, and used from multiple threads.
344+
///
345+
/// Note that `Default::default()` is not `const` so cannot be used to construct
346+
/// either your model or the `Options`.
347+
///
348+
/// For example:
349+
/// ```
350+
/// static VIEW: nutmeg::View<Model> = nutmeg::View::new(Model { i: 0 }, nutmeg::Options::new());
351+
///
352+
/// struct Model {
353+
/// i: usize,
354+
/// }
355+
///
356+
/// impl nutmeg::Model for Model {
357+
/// fn render(&mut self, _width: usize) -> String {
358+
/// format!("i={}", self.i)
359+
/// }
360+
/// }
361+
///
362+
/// fn main() -> std::io::Result<()> {
363+
/// for i in 0..20 {
364+
/// VIEW.update(|model| model.i = i);
365+
/// if i % 5 == 0 {
366+
/// // Note: You cannot use writeln!() here, because its argument must be
367+
/// // `&mut`, but you can send messages.
368+
/// VIEW.message(&format!("message: i={i}\n"));
369+
/// }
370+
/// std::thread::sleep(std::time::Duration::from_millis(20));
371+
/// }
372+
/// Ok(())
373+
/// }
374+
///
375+
/// ```
339376
pub struct View<M: Model> {
340377
/// The real state of the view.
341378
///
@@ -364,10 +401,7 @@ impl<M: Model> View<M> {
364401
/// This constructor arranges that output from the progress view will be
365402
/// captured by the Rust test framework and not leak to stdout, but
366403
/// detection of whether to show progress bars may not work correctly.
367-
pub fn new(model: M, mut options: Options) -> View<M> {
368-
if !options.destination.is_possible() {
369-
options.progress_enabled = false;
370-
}
404+
pub const fn new(model: M, options: Options) -> View<M> {
371405
View {
372406
inner: Mutex::new(Some(InnerView::new(model, options))),
373407
}
@@ -539,7 +573,9 @@ impl<M: Model> View<M> {
539573
}
540574

541575
/// If the view's destination is [Destination::Capture], returns the buffer
542-
/// of captured output. Panics if the destination is not [Destination::Capture].
576+
/// of captured output.
577+
///
578+
/// Panics if the destination is not [Destination::Capture].
543579
///
544580
/// The buffer is returned in an Arc so that it remains valid after the View
545581
/// is dropped.
@@ -601,10 +637,6 @@ impl<M: Model> Drop for View<M> {
601637
}
602638
}
603639

604-
fn is_dumb_term() -> bool {
605-
env::var("TERM").map_or(false, |s| s.eq_ignore_ascii_case("dumb"))
606-
}
607-
608640
/// The real contents of a View, inside a mutex.
609641
struct InnerView<M: Model> {
610642
/// Current application model.
@@ -619,14 +651,16 @@ struct InnerView<M: Model> {
619651
options: Options,
620652

621653
/// The current time on the fake clock, if it is enabled.
622-
fake_clock: Instant,
654+
fake_clock: Option<Instant>,
623655

624656
/// Captured output, if active.
625657
capture_buffer: Option<Arc<Mutex<String>>>,
626658
}
627659

628660
#[derive(Debug, PartialEq, Eq, Clone)]
629661
enum State {
662+
/// Nothing has ever been painted, and the screen has not yet been initialized.
663+
New,
630664
/// Progress is not visible and nothing was recently printed.
631665
None,
632666
/// Progress bar is currently displayed.
@@ -647,17 +681,13 @@ enum State {
647681
}
648682

649683
impl<M: Model> InnerView<M> {
650-
fn new(model: M, options: Options) -> InnerView<M> {
651-
let capture_buffer = match options.destination {
652-
Destination::Capture => Some(Arc::new(Mutex::new(String::new()))),
653-
_ => None,
654-
};
684+
const fn new(model: M, options: Options) -> InnerView<M> {
655685
InnerView {
656-
capture_buffer,
657-
fake_clock: Instant::now(),
686+
capture_buffer: None,
687+
fake_clock: None,
658688
model,
659689
options,
660-
state: State::None,
690+
state: State::New,
661691
suspended: false,
662692
}
663693
}
@@ -676,29 +706,36 @@ impl<M: Model> InnerView<M> {
676706
State::ProgressDrawn { .. } => {
677707
self.write_output("\n");
678708
}
679-
State::IncompleteLine | State::None | State::Printed { .. } => (),
709+
State::New | State::IncompleteLine | State::None | State::Printed { .. } => (),
680710
}
681711
self.state = State::None; // so that drop does not attempt to erase
682712
Ok(self.model)
683713
}
684714

685715
/// Return the real or fake clock.
686716
fn clock(&self) -> Instant {
687-
if self.options.fake_clock {
688-
self.fake_clock
689-
} else {
690-
Instant::now()
717+
self.fake_clock.unwrap_or_else(Instant::now)
718+
}
719+
720+
fn init_destination(&mut self) {
721+
if self.state == State::New {
722+
if self.options.destination.initalize().is_err() {
723+
// This destination doesn't want to draw progress bars, so stay off forever.
724+
self.options.progress_enabled = false;
725+
}
726+
self.state = State::None;
691727
}
692728
}
693729

694730
fn paint_progress(&mut self) -> io::Result<()> {
731+
self.init_destination();
695732
if !self.options.progress_enabled || self.suspended {
696733
return Ok(());
697734
}
698735
let now = self.clock();
699736
match self.state {
700737
State::IncompleteLine => return Ok(()),
701-
State::None => (),
738+
State::New | State::None => (),
702739
State::Printed { last_printed } => {
703740
if now - last_printed < self.options.print_holdoff {
704741
return Ok(());
@@ -770,7 +807,7 @@ impl<M: Model> InnerView<M> {
770807
));
771808
self.state = State::None;
772809
}
773-
State::None | State::IncompleteLine | State::Printed { .. } => {}
810+
State::None | State::New | State::IncompleteLine | State::Printed { .. } => {}
774811
}
775812
Ok(())
776813
}
@@ -788,6 +825,7 @@ impl<M: Model> InnerView<M> {
788825
if buf.is_empty() {
789826
return Ok(0);
790827
}
828+
self.init_destination();
791829
self.clear()?;
792830
self.state = if buf.ends_with(b"\n") {
793831
State::Printed {
@@ -802,8 +840,8 @@ impl<M: Model> InnerView<M> {
802840

803841
/// Set the value of the fake clock, for testing.
804842
fn set_fake_clock(&mut self, fake_clock: Instant) {
805-
assert!(self.options.fake_clock, "fake clock is not enabled");
806-
self.fake_clock = fake_clock;
843+
assert!(self.options.fake_clock, "Options.fake_clock is not enabled");
844+
self.fake_clock = Some(fake_clock);
807845
}
808846

809847
fn write_output(&mut self, buf: &str) {
@@ -818,8 +856,7 @@ impl<M: Model> InnerView<M> {
818856
}
819857
Destination::Capture => {
820858
self.capture_buffer
821-
.as_mut()
822-
.expect("capture buffer is not allocated")
859+
.get_or_insert_with(|| Arc::new(Mutex::new(String::new())))
823860
.lock()
824861
.push_str(buf);
825862
}
@@ -828,8 +865,7 @@ impl<M: Model> InnerView<M> {
828865

829866
fn captured_output(&mut self) -> Arc<Mutex<String>> {
830867
self.capture_buffer
831-
.as_ref()
832-
.expect("capture buffer allocated")
868+
.get_or_insert_with(|| Arc::new(Mutex::new(String::new())))
833869
.clone()
834870
}
835871
}
@@ -963,40 +999,3 @@ impl Default for Options {
963999
Options::new()
9641000
}
9651001
}
966-
967-
/// Destinations for progress bar output.
968-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
969-
pub enum Destination {
970-
/// Draw to stdout.
971-
Stdout,
972-
/// Draw to stderr.
973-
Stderr,
974-
/// Draw to an internal capture buffer, which can be retrieved with [View::captured_output].
975-
///
976-
/// This is intended for testing.
977-
///
978-
/// A width of 80 columns is used.
979-
Capture,
980-
}
981-
982-
impl Destination {
983-
fn is_possible(&self) -> bool {
984-
match self {
985-
Destination::Stdout => {
986-
atty::is(atty::Stream::Stdout) && !is_dumb_term() && ansi::enable_windows_ansi()
987-
}
988-
Destination::Stderr => {
989-
atty::is(atty::Stream::Stderr) && !is_dumb_term() && ansi::enable_windows_ansi()
990-
}
991-
Destination::Capture => true,
992-
}
993-
}
994-
995-
fn width(&self) -> Option<usize> {
996-
match self {
997-
Destination::Stdout => width::stdout_width(),
998-
Destination::Stderr => width::stderr_width(),
999-
Destination::Capture => Some(80),
1000-
}
1001-
}
1002-
}

0 commit comments

Comments
 (0)