55// References:
66// * <https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797>
77
8- #![ allow( unused) ]
9-
108use std:: borrow:: Cow ;
119
10+ #[ allow( dead_code) ]
11+ pub ( crate ) const ESC : & str = "\x1b " ;
12+
1213pub ( crate ) const MOVE_TO_START_OF_LINE : & str = "\x1b [1G" ;
1314
1415// https://vt100.net/docs/vt510-rm/DECAWM
1516pub ( crate ) const DISABLE_LINE_WRAP : & str = "\x1b [?7l" ;
1617pub ( crate ) const ENABLE_LINE_WRAP : & str = "\x1b [?7h" ;
1718
1819pub ( crate ) const CLEAR_TO_END_OF_LINE : & str = "\x1b [0K" ;
20+ #[ allow( dead_code) ]
1921pub ( crate ) const CLEAR_CURRENT_LINE : & str = "\x1b [2K" ;
2022pub ( crate ) const CLEAR_TO_END_OF_SCREEN : & str = "\x1b [0J" ;
2123
@@ -37,14 +39,313 @@ pub(crate) fn enable_windows_ansi() -> bool {
3739 true
3840}
3941
40- pub ( crate ) fn insert_codes ( rendered : & str , up_lines : Option < usize > ) -> String {
42+ pub ( crate ) fn insert_codes ( rendered : & str , cursor_y : Option < usize > ) -> ( String , usize ) {
4143 let mut buf = String :: with_capacity ( rendered. len ( ) + 40 ) ;
42- if let Some ( up_lines) = up_lines {
43- buf. push_str ( & up_n_lines_and_home ( up_lines) ) ;
44- }
44+ buf. push_str ( & up_n_lines_and_home ( cursor_y. unwrap_or_default ( ) ) ) ;
4545 buf. push_str ( DISABLE_LINE_WRAP ) ;
46- buf. push_str ( CLEAR_TO_END_OF_SCREEN ) ;
47- buf. push_str ( rendered) ;
46+ // buf.push_str(CLEAR_TO_END_OF_SCREEN);
47+ let mut first = true ;
48+ let mut n_lines = 0 ;
49+ for line in rendered. lines ( ) {
50+ if !first {
51+ buf. push ( '\n' ) ;
52+ n_lines += 1 ;
53+ } else {
54+ first = false ;
55+ }
56+ buf. push_str ( line) ;
57+ buf. push_str ( CLEAR_TO_END_OF_LINE ) ;
58+ }
4859 buf. push_str ( ENABLE_LINE_WRAP ) ;
49- buf
60+ ( buf, n_lines)
61+ }
62+
63+ #[ cfg( test) ]
64+ mod test {
65+ use std:: {
66+ mem:: take,
67+ ops:: DerefMut ,
68+ thread:: sleep,
69+ time:: { Duration , Instant } ,
70+ } ;
71+
72+ use super :: * ;
73+ use crate :: { Destination , Model , Options , View } ;
74+
75+ struct MultiLineModel {
76+ i : usize ,
77+ }
78+
79+ impl Model for MultiLineModel {
80+ fn render ( & mut self , _width : usize ) -> String {
81+ format ! ( " count: {}\n bar: {}\n " , self . i, "*" . repeat( self . i) , )
82+ }
83+ }
84+
85+ #[ test]
86+ fn draw_progress_once ( ) {
87+ let model = MultiLineModel { i : 0 } ;
88+ let options = Options :: default ( ) . destination ( Destination :: Capture ) ;
89+ let view = View :: new ( model, options) ;
90+ let output = view. captured_output ( ) ;
91+
92+ view. update ( |model| model. i = 1 ) ;
93+
94+ let written = output. lock ( ) . unwrap ( ) . to_owned ( ) ;
95+ assert_eq ! (
96+ written,
97+ MOVE_TO_START_OF_LINE . to_string( )
98+ + DISABLE_LINE_WRAP
99+ + " count: 1"
100+ + CLEAR_TO_END_OF_LINE
101+ + "\n "
102+ + " bar: *"
103+ + CLEAR_TO_END_OF_LINE
104+ + ENABLE_LINE_WRAP
105+ ) ;
106+ output. lock ( ) . unwrap ( ) . clear ( ) ;
107+
108+ drop ( view) ;
109+ let written = output. lock ( ) . unwrap ( ) . to_owned ( ) ;
110+ assert_eq ! (
111+ written,
112+ ESC . to_owned( ) + "[1F" + CLEAR_TO_END_OF_SCREEN + ENABLE_LINE_WRAP
113+ )
114+ }
115+
116+ #[ test]
117+ fn abandoned_bar_is_not_erased ( ) {
118+ let model = MultiLineModel { i : 0 } ;
119+ let view = View :: new ( model, Options :: default ( ) . destination ( Destination :: Capture ) ) ;
120+ let output = view. captured_output ( ) ;
121+
122+ view. update ( |model| model. i = 1 ) ;
123+ view. abandon ( ) ;
124+
125+ // No erasure commands, just a newline after the last painted view.
126+ let written = output. lock ( ) . unwrap ( ) . to_owned ( ) ;
127+ assert_eq ! (
128+ written,
129+ MOVE_TO_START_OF_LINE . to_owned( )
130+ + DISABLE_LINE_WRAP
131+ + " count: 1"
132+ + CLEAR_TO_END_OF_LINE
133+ + "\n "
134+ + " bar: *"
135+ + CLEAR_TO_END_OF_LINE
136+ + ENABLE_LINE_WRAP
137+ + "\n "
138+ ) ;
139+ }
140+
141+ #[ test]
142+ fn rate_limiting_with_fake_clock ( ) {
143+ struct Model {
144+ draw_count : usize ,
145+ update_count : usize ,
146+ }
147+ impl crate :: Model for Model {
148+ fn render ( & mut self , _width : usize ) -> String {
149+ self . draw_count += 1 ;
150+ format ! ( "update:{} draw:{}" , self . update_count, self . draw_count)
151+ }
152+ }
153+ let model = Model {
154+ draw_count : 0 ,
155+ update_count : 0 ,
156+ } ;
157+ let options = Options :: default ( )
158+ . destination ( Destination :: Capture )
159+ . fake_clock ( true )
160+ . update_interval ( Duration :: from_millis ( 1 ) ) ;
161+ let mut fake_clock = Instant :: now ( ) ;
162+ let view = View :: new ( model, options) ;
163+ view. set_fake_clock ( fake_clock) ;
164+ let output = view. captured_output ( ) ;
165+
166+ // Any number of updates, but until the clock ticks only one will be drawn.
167+ for _i in 0 ..10 {
168+ view. update ( |model| model. update_count += 1 ) ;
169+ sleep ( Duration :: from_millis ( 10 ) ) ;
170+ }
171+ assert_eq ! ( view. inspect_model( |m| m. draw_count) , 1 ) ;
172+ assert_eq ! ( view. inspect_model( |m| m. update_count) , 10 ) ;
173+
174+ // Time passes...
175+ fake_clock += Duration :: from_secs ( 1 ) ;
176+ view. set_fake_clock ( fake_clock) ;
177+ // Another burst of updates, and just one of them will be drawn.
178+ for _i in 0 ..10 {
179+ view. update ( |model| model. update_count += 1 ) ;
180+ sleep ( Duration :: from_millis ( 10 ) ) ;
181+ }
182+ assert_eq ! ( view. inspect_model( |m| m. draw_count) , 2 ) ;
183+ assert_eq ! ( view. inspect_model( |m| m. update_count) , 20 ) ;
184+
185+ drop ( view) ;
186+ let written = output. lock ( ) . unwrap ( ) . to_owned ( ) ;
187+ assert_eq ! (
188+ written,
189+ MOVE_TO_START_OF_LINE . to_owned( )
190+ + DISABLE_LINE_WRAP
191+ + "update:1 draw:1"
192+ + CLEAR_TO_END_OF_LINE
193+ + ENABLE_LINE_WRAP
194+ + MOVE_TO_START_OF_LINE
195+ + DISABLE_LINE_WRAP
196+ + "update:11 draw:2"
197+ + CLEAR_TO_END_OF_LINE
198+ + ENABLE_LINE_WRAP
199+ + MOVE_TO_START_OF_LINE
200+ + CLEAR_TO_END_OF_SCREEN
201+ + ENABLE_LINE_WRAP
202+ ) ;
203+ }
204+
205+ /// If output is redirected, it should not be affected by the width of
206+ /// wherever stdout is pointing.
207+ #[ test]
208+ fn default_width_when_not_on_stdout ( ) {
209+ struct Model ( ) ;
210+ impl crate :: Model for Model {
211+ fn render ( & mut self , width : usize ) -> String {
212+ assert_eq ! ( width, 80 ) ;
213+ format ! ( "width={width}" )
214+ }
215+ }
216+ let model = Model ( ) ;
217+ let options = Options :: default ( ) . destination ( Destination :: Capture ) ;
218+ let view = View :: new ( model, options) ;
219+ let output = view. captured_output ( ) ;
220+
221+ view. update ( |_model| ( ) ) ;
222+ let written = output. lock ( ) . unwrap ( ) . to_owned ( ) ;
223+ assert_eq ! (
224+ written,
225+ MOVE_TO_START_OF_LINE . to_owned( )
226+ + DISABLE_LINE_WRAP
227+ + "width=80"
228+ + CLEAR_TO_END_OF_LINE
229+ + ENABLE_LINE_WRAP
230+ ) ;
231+
232+ drop ( view) ;
233+ }
234+
235+ #[ test]
236+ fn suspend_and_resume ( ) {
237+ struct Model ( usize ) ;
238+ impl crate :: Model for Model {
239+ fn render ( & mut self , _width : usize ) -> String {
240+ format ! ( "XX: {}" , self . 0 )
241+ }
242+ }
243+ let model = Model ( 0 ) ;
244+ let options = Options :: default ( )
245+ . destination ( Destination :: Capture )
246+ . update_interval ( Duration :: ZERO ) ;
247+ let view = View :: new ( model, options) ;
248+ let output = view. captured_output ( ) ;
249+
250+ // Paint 0 before it's suspended
251+ view. update ( |model| model. 0 = 0 ) ;
252+ let written = take ( output. lock ( ) . unwrap ( ) . deref_mut ( ) ) ;
253+ assert_eq ! (
254+ written,
255+ MOVE_TO_START_OF_LINE . to_owned( )
256+ + DISABLE_LINE_WRAP
257+ + "XX: 0"
258+ + CLEAR_TO_END_OF_LINE
259+ + ENABLE_LINE_WRAP
260+ ) ;
261+
262+ // Now suspend; this clears the bar from the screen.
263+ view. suspend ( ) ;
264+ view. update ( |model| model. 0 = 1 ) ;
265+ let written = take ( output. lock ( ) . unwrap ( ) . deref_mut ( ) ) ;
266+ assert_eq ! (
267+ written,
268+ MOVE_TO_START_OF_LINE . to_owned( ) + CLEAR_TO_END_OF_SCREEN + ENABLE_LINE_WRAP
269+ ) ;
270+
271+ // * 2 is also updated into the model while the bar is suspended, but then
272+ // it's resumed, so 2 is then painted.
273+ view. update ( |model| model. 0 = 2 ) ;
274+ let written = take ( output. lock ( ) . unwrap ( ) . deref_mut ( ) ) ;
275+ assert_eq ! ( written, "" ) ;
276+
277+ // Now 2 is painted when resumed.
278+ view. resume ( ) ;
279+ let written = take ( output. lock ( ) . unwrap ( ) . deref_mut ( ) ) ;
280+ assert_eq ! (
281+ written,
282+ MOVE_TO_START_OF_LINE . to_owned( )
283+ + DISABLE_LINE_WRAP
284+ + "XX: 2"
285+ + CLEAR_TO_END_OF_LINE
286+ + ENABLE_LINE_WRAP
287+ ) ;
288+
289+ // * 3 and 4 are painted in the usual way.
290+ view. update ( |model| model. 0 = 3 ) ;
291+ view. update ( |model| model. 0 = 4 ) ;
292+ let written = take ( output. lock ( ) . unwrap ( ) . deref_mut ( ) ) ;
293+ assert_eq ! (
294+ written,
295+ MOVE_TO_START_OF_LINE . to_owned( )
296+ + DISABLE_LINE_WRAP
297+ + "XX: 3"
298+ + CLEAR_TO_END_OF_LINE
299+ + ENABLE_LINE_WRAP
300+ + MOVE_TO_START_OF_LINE
301+ + DISABLE_LINE_WRAP
302+ + "XX: 4"
303+ + CLEAR_TO_END_OF_LINE
304+ + ENABLE_LINE_WRAP
305+ ) ;
306+
307+ view. abandon ( ) ;
308+ let written = take ( output. lock ( ) . unwrap ( ) . deref_mut ( ) ) ;
309+ assert_eq ! ( written, "\n " ) ;
310+ }
311+
312+ #[ test]
313+ fn identical_output_suppressed ( ) {
314+ struct Hundreds ( usize ) ;
315+
316+ impl Model for Hundreds {
317+ fn render ( & mut self , _width : usize ) -> String {
318+ format ! ( "hundreds={}" , self . 0 / 100 )
319+ }
320+ }
321+
322+ let options = Options :: default ( )
323+ . destination ( Destination :: Capture )
324+ . update_interval ( Duration :: ZERO ) ;
325+ let view = View :: new ( Hundreds ( 0 ) , options) ;
326+ let output = view. captured_output ( ) ;
327+
328+ for i in 0 ..200 {
329+ // We change the model, but not in a way that will change what's displayed.
330+ view. update ( |model| model. 0 = i) ;
331+ }
332+ view. abandon ( ) ;
333+
334+ // No erasure commands, just a newline after the last painted view.
335+ let written = output. lock ( ) . unwrap ( ) . to_owned ( ) ;
336+ assert_eq ! (
337+ written,
338+ MOVE_TO_START_OF_LINE . to_owned( )
339+ + DISABLE_LINE_WRAP
340+ + "hundreds=0"
341+ + CLEAR_TO_END_OF_LINE
342+ + ENABLE_LINE_WRAP
343+ + MOVE_TO_START_OF_LINE
344+ + DISABLE_LINE_WRAP
345+ + "hundreds=1"
346+ + CLEAR_TO_END_OF_LINE
347+ + ENABLE_LINE_WRAP
348+ + "\n " // bar abandoned
349+ ) ;
350+ }
50351}
0 commit comments