@@ -5,6 +5,8 @@ use std::time::{Duration, Instant};
5
5
use core:: shell:: Verbosity ;
6
6
use util:: { CargoResult , Config } ;
7
7
8
+ use unicode_width:: UnicodeWidthChar ;
9
+
8
10
pub struct Progress < ' cfg > {
9
11
state : Option < State < ' cfg > > ,
10
12
}
@@ -16,15 +18,19 @@ pub enum ProgressStyle {
16
18
17
19
struct State < ' cfg > {
18
20
config : & ' cfg Config ,
19
- style : ProgressStyle ,
20
- max_width : usize ,
21
- width : usize ,
21
+ format : Format ,
22
22
first : bool ,
23
23
last_update : Instant ,
24
24
name : String ,
25
25
done : bool ,
26
26
}
27
27
28
+ struct Format {
29
+ style : ProgressStyle ,
30
+ max_width : usize ,
31
+ width : usize ,
32
+ }
33
+
28
34
impl < ' cfg > Progress < ' cfg > {
29
35
pub fn with_style ( name : & str , style : ProgressStyle , cfg : & ' cfg Config ) -> Progress < ' cfg > {
30
36
// report no progress when -q (for quiet) or TERM=dumb are set
@@ -39,9 +45,11 @@ impl<'cfg> Progress<'cfg> {
39
45
Progress {
40
46
state : cfg. shell ( ) . err_width ( ) . map ( |n| State {
41
47
config : cfg,
42
- style,
43
- max_width : n,
44
- width : cmp:: min ( n, 80 ) ,
48
+ format : Format {
49
+ style,
50
+ max_width : n,
51
+ width : cmp:: min ( n, 80 ) ,
52
+ } ,
45
53
first : true ,
46
54
last_update : Instant :: now ( ) ,
47
55
name : name. to_string ( ) ,
@@ -56,18 +64,18 @@ impl<'cfg> Progress<'cfg> {
56
64
57
65
pub fn tick ( & mut self , cur : usize , max : usize ) -> CargoResult < ( ) > {
58
66
match self . state {
59
- Some ( ref mut s) => s. tick ( cur, max, String :: new ( ) , true ) ,
67
+ Some ( ref mut s) => s. tick ( cur, max, "" , true ) ,
60
68
None => Ok ( ( ) ) ,
61
69
}
62
70
}
63
71
64
72
pub fn clear ( & mut self ) {
65
73
if let Some ( ref mut s) = self . state {
66
- clear ( s. max_width , s. config ) ;
74
+ clear ( s. format . max_width , s. config ) ;
67
75
}
68
76
}
69
77
70
- pub fn tick_now ( & mut self , cur : usize , max : usize , msg : String ) -> CargoResult < ( ) > {
78
+ pub fn tick_now ( & mut self , cur : usize , max : usize , msg : & str ) -> CargoResult < ( ) > {
71
79
match self . state {
72
80
Some ( ref mut s) => s. tick ( cur, max, msg, false ) ,
73
81
None => Ok ( ( ) ) ,
@@ -76,7 +84,7 @@ impl<'cfg> Progress<'cfg> {
76
84
}
77
85
78
86
impl < ' cfg > State < ' cfg > {
79
- fn tick ( & mut self , cur : usize , max : usize , msg : String , throttle : bool ) -> CargoResult < ( ) > {
87
+ fn tick ( & mut self , cur : usize , max : usize , msg : & str , throttle : bool ) -> CargoResult < ( ) > {
80
88
if self . done {
81
89
return Ok ( ( ) ) ;
82
90
}
@@ -109,6 +117,22 @@ impl<'cfg> State<'cfg> {
109
117
self . last_update = Instant :: now ( ) ;
110
118
}
111
119
120
+ if cur == max {
121
+ self . done = true ;
122
+ }
123
+
124
+ // Write out a pretty header, then the progress bar itself, and then
125
+ // return back to the beginning of the line for the next print.
126
+ if let Some ( string) = self . format . progress_status ( cur, max, msg) {
127
+ self . config . shell ( ) . status_header ( & self . name ) ?;
128
+ write ! ( self . config. shell( ) . err( ) , "{}\r " , string) ?;
129
+ }
130
+ Ok ( ( ) )
131
+ }
132
+ }
133
+
134
+ impl Format {
135
+ fn progress_status ( & self , cur : usize , max : usize , msg : & str ) -> Option < String > {
112
136
// Render the percentage at the far right and then figure how long the
113
137
// progress bar is
114
138
let pct = ( cur as f64 ) / ( max as f64 ) ;
@@ -120,9 +144,11 @@ impl<'cfg> State<'cfg> {
120
144
let extra_len = stats. len ( ) + 2 /* [ and ] */ + 15 /* status header */ ;
121
145
let display_width = match self . width . checked_sub ( extra_len) {
122
146
Some ( n) => n,
123
- None => return Ok ( ( ) ) ,
147
+ None => return None ,
124
148
} ;
125
- let mut string = String :: from ( "[" ) ;
149
+
150
+ let mut string = String :: with_capacity ( self . max_width ) ;
151
+ string. push ( '[' ) ;
126
152
let hashes = display_width as f64 * pct;
127
153
let hashes = hashes as usize ;
128
154
@@ -132,7 +158,6 @@ impl<'cfg> State<'cfg> {
132
158
string. push_str ( "=" ) ;
133
159
}
134
160
if cur == max {
135
- self . done = true ;
136
161
string. push_str ( "=" ) ;
137
162
} else {
138
163
string. push_str ( ">" ) ;
@@ -146,19 +171,26 @@ impl<'cfg> State<'cfg> {
146
171
string. push_str ( "]" ) ;
147
172
string. push_str ( & stats) ;
148
173
149
- let avail_msg_len = self . max_width - self . width ;
150
- if avail_msg_len >= msg. len ( ) + 3 {
151
- string. push_str ( & msg) ;
152
- } else if avail_msg_len >= 4 {
153
- string. push_str ( & msg[ ..( avail_msg_len - 3 ) ] ) ;
154
- string. push_str ( "..." ) ;
174
+ let mut avail_msg_len = self . max_width - self . width ;
175
+ let mut ellipsis_pos = 0 ;
176
+ if avail_msg_len > 3 {
177
+ for c in msg. chars ( ) {
178
+ let display_width = c. width ( ) . unwrap_or ( 0 ) ;
179
+ if avail_msg_len >= display_width {
180
+ avail_msg_len -= display_width;
181
+ string. push ( c) ;
182
+ if avail_msg_len >= 3 {
183
+ ellipsis_pos = string. len ( ) ;
184
+ }
185
+ } else {
186
+ string. truncate ( ellipsis_pos) ;
187
+ string. push_str ( "..." ) ;
188
+ break ;
189
+ }
190
+ }
155
191
}
156
192
157
- // Write out a pretty header, then the progress bar itself, and then
158
- // return back to the beginning of the line for the next print.
159
- self . config . shell ( ) . status_header ( & self . name ) ?;
160
- write ! ( self . config. shell( ) . err( ) , "{}\r " , string) ?;
161
- Ok ( ( ) )
193
+ Some ( string)
162
194
}
163
195
}
164
196
@@ -169,6 +201,122 @@ fn clear(width: usize, config: &Config) {
169
201
170
202
impl < ' cfg > Drop for State < ' cfg > {
171
203
fn drop ( & mut self ) {
172
- clear ( self . max_width , self . config ) ;
204
+ clear ( self . format . max_width , self . config ) ;
173
205
}
174
206
}
207
+
208
+ #[ test]
209
+ fn test_progress_status ( ) {
210
+ let format = Format {
211
+ style : ProgressStyle :: Ratio ,
212
+ width : 40 ,
213
+ max_width : 60 ,
214
+ } ;
215
+ assert_eq ! (
216
+ format. progress_status( 0 , 4 , "" ) ,
217
+ Some ( "[ ] 0/4" . to_string( ) )
218
+ ) ;
219
+ assert_eq ! (
220
+ format. progress_status( 1 , 4 , "" ) ,
221
+ Some ( "[===> ] 1/4" . to_string( ) )
222
+ ) ;
223
+ assert_eq ! (
224
+ format. progress_status( 2 , 4 , "" ) ,
225
+ Some ( "[========> ] 2/4" . to_string( ) )
226
+ ) ;
227
+ assert_eq ! (
228
+ format. progress_status( 3 , 4 , "" ) ,
229
+ Some ( "[=============> ] 3/4" . to_string( ) )
230
+ ) ;
231
+ assert_eq ! (
232
+ format. progress_status( 4 , 4 , "" ) ,
233
+ Some ( "[===================] 4/4" . to_string( ) )
234
+ ) ;
235
+
236
+ assert_eq ! (
237
+ format. progress_status( 3999 , 4000 , "" ) ,
238
+ Some ( "[===========> ] 3999/4000" . to_string( ) )
239
+ ) ;
240
+ assert_eq ! (
241
+ format. progress_status( 4000 , 4000 , "" ) ,
242
+ Some ( "[=============] 4000/4000" . to_string( ) )
243
+ ) ;
244
+
245
+ assert_eq ! (
246
+ format. progress_status( 3 , 4 , ": short message" ) ,
247
+ Some ( "[=============> ] 3/4: short message" . to_string( ) )
248
+ ) ;
249
+ assert_eq ! (
250
+ format. progress_status( 3 , 4 , ": msg thats just fit" ) ,
251
+ Some ( "[=============> ] 3/4: msg thats just fit" . to_string( ) )
252
+ ) ;
253
+ assert_eq ! (
254
+ format. progress_status( 3 , 4 , ": msg that's just fit" ) ,
255
+ Some ( "[=============> ] 3/4: msg that's just..." . to_string( ) )
256
+ ) ;
257
+
258
+ // combining diacritics have width zero and thus can fit max_width.
259
+ let zalgo_msg = "z̸̧̢̗͉̝̦͍̱ͧͦͨ̑̅̌ͥ́͢a̢ͬͨ̽ͯ̅̑ͥ͋̏̑ͫ̄͢͏̫̝̪̤͎̱̣͍̭̞̙̱͙͍̘̭͚l̶̡̛̥̝̰̭̹̯̯̞̪͇̱̦͙͔̘̼͇͓̈ͨ͗ͧ̓͒ͦ̀̇ͣ̈ͭ͊͛̃̑͒̿̕͜g̸̷̢̩̻̻͚̠͓̞̥͐ͩ͌̑ͥ̊̽͋͐̐͌͛̐̇̑ͨ́ͅo͙̳̣͔̰̠̜͕͕̞̦̙̭̜̯̹̬̻̓͑ͦ͋̈̉͌̃ͯ̀̂͠ͅ ̸̡͎̦̲̖̤̺̜̮̱̰̥͔̯̅̏ͬ̂ͨ̋̃̽̈́̾̔̇ͣ̚͜͜h̡ͫ̐̅̿̍̀͜҉̛͇̭̹̰̠͙̞ẽ̶̙̹̳̖͉͎̦͂̋̓ͮ̔ͬ̐̀͂̌͑̒͆̚͜͠ ͓͓̟͍̮̬̝̝̰͓͎̼̻ͦ͐̾̔͒̃̓͟͟c̮̦͍̺͈͚̯͕̄̒͐̂͊̊͗͊ͤͣ̀͘̕͝͞o̶͍͚͍̣̮͌ͦ̽̑ͩ̅ͮ̐̽̏͗́͂̅ͪ͠m̷̧͖̻͔̥̪̭͉͉̤̻͖̩̤͖̘ͦ̂͌̆̂ͦ̒͊ͯͬ͊̉̌ͬ͝͡e̵̹̣͍̜̺̤̤̯̫̹̠̮͎͙̯͚̰̼͗͐̀̒͂̉̀̚͝͞s̵̲͍͙͖̪͓͓̺̱̭̩̣͖̣ͤͤ͂̎̈͗͆ͨͪ̆̈͗͝͠" ;
260
+ assert_eq ! (
261
+ format. progress_status( 3 , 4 , zalgo_msg) ,
262
+ Some ( "[=============> ] 3/4" . to_string( ) + zalgo_msg)
263
+ ) ;
264
+
265
+ // some non-ASCII ellipsize test
266
+ assert_eq ! (
267
+ format. progress_status( 3 , 4 , "_123456789123456e\u{301} \u{301} 8\u{301} 90a" ) ,
268
+ Some ( "[=============> ] 3/4_123456789123456e\u{301} \u{301} ..." . to_string( ) )
269
+ ) ;
270
+ assert_eq ! (
271
+ format. progress_status( 3 , 4 , ":每個漢字佔據了兩個字元" ) ,
272
+ Some ( "[=============> ] 3/4:每個漢字佔據了..." . to_string( ) )
273
+ ) ;
274
+ }
275
+
276
+ #[ test]
277
+ fn test_progress_status_percentage ( ) {
278
+ let format = Format {
279
+ style : ProgressStyle :: Percentage ,
280
+ width : 40 ,
281
+ max_width : 60 ,
282
+ } ;
283
+ assert_eq ! (
284
+ format. progress_status( 0 , 77 , "" ) ,
285
+ Some ( "[ ] 0.00%" . to_string( ) )
286
+ ) ;
287
+ assert_eq ! (
288
+ format. progress_status( 1 , 77 , "" ) ,
289
+ Some ( "[ ] 1.30%" . to_string( ) )
290
+ ) ;
291
+ assert_eq ! (
292
+ format. progress_status( 76 , 77 , "" ) ,
293
+ Some ( "[=============> ] 98.70%" . to_string( ) )
294
+ ) ;
295
+ assert_eq ! (
296
+ format. progress_status( 77 , 77 , "" ) ,
297
+ Some ( "[===============] 100.00%" . to_string( ) )
298
+ ) ;
299
+ }
300
+
301
+ #[ test]
302
+ fn test_progress_status_too_short ( ) {
303
+ let format = Format {
304
+ style : ProgressStyle :: Percentage ,
305
+ width : 25 ,
306
+ max_width : 25 ,
307
+ } ;
308
+ assert_eq ! (
309
+ format. progress_status( 1 , 1 , "" ) ,
310
+ Some ( "[] 100.00%" . to_string( ) )
311
+ ) ;
312
+
313
+ let format = Format {
314
+ style : ProgressStyle :: Percentage ,
315
+ width : 24 ,
316
+ max_width : 24 ,
317
+ } ;
318
+ assert_eq ! (
319
+ format. progress_status( 1 , 1 , "" ) ,
320
+ None
321
+ ) ;
322
+ }
0 commit comments