@@ -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;
215214use std:: fmt:: Display ;
216215use std:: io:: { self , Write } ;
217216use std:: sync:: Arc ;
@@ -220,6 +219,7 @@ use std::time::{Duration, Instant};
220219use parking_lot:: Mutex ;
221220
222221mod ansi;
222+ mod destination;
223223mod helpers;
224224pub mod models;
225225mod width;
@@ -233,6 +233,7 @@ pub mod _changelog {
233233}
234234
235235pub 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+ /// ```
339376pub 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.
609641struct 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 ) ]
629661enum 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
649683impl < 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