owmods_core/progress.rs
1use anyhow::Result;
2use log::{info, warn};
3use serde::{Deserialize, Serialize};
4
5/// Represents a value in a progress bar
6pub type ProgressValue = u32;
7
8/// Type of progress bar
9#[derive(Clone, Serialize, Deserialize, Debug)]
10pub enum ProgressType {
11 /// We know an amount that's incrementing (ex: 10/90, 11/90, etc).
12 Definite,
13 /// We don't know an amount and are just waiting.
14 Indefinite,
15}
16
17/// The action this progress bar is reporting
18#[derive(Clone, Serialize, Deserialize, Debug)]
19pub enum ProgressAction {
20 /// We're downloading a file
21 Download,
22 /// We're extracting a ZIP archive
23 Extract,
24}
25
26/// Payload sent when a progress bar is started
27/// Contains all the information needed to create a progress bar
28#[derive(Debug, Clone, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase")]
30pub struct ProgressStartPayload {
31 /// The ID of the progress bar
32 pub id: String,
33 /// The unique name of the mod this progress bar is for, sometimes this will be None if the progress bar doesn't know what mod it's for
34 pub unique_name: Option<String>,
35 /// The length of the progress bar
36 pub len: ProgressValue,
37 /// The message of the progress bar
38 pub msg: String,
39 /// The type of progress bar
40 pub progress_type: ProgressType,
41 /// The action this progress bar is reporting
42 pub progress_action: ProgressAction,
43}
44
45/// Payload sent when a progress bar is incremented
46/// Note progress bars internally throttle the amount of times they can be incremented and may not report every increment
47/// This is done to prevent spamming small increments
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct ProgressIncrementPayload {
50 /// The ID of the progress bar
51 pub id: String,
52 /// The amount to increment the progress bar by
53 pub progress: ProgressValue,
54}
55
56/// Payload sent when a progress bar's message is updated
57/// This is usually used to show the current file being extracted
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct ProgressMessagePayload {
60 /// The ID of the progress bar
61 pub id: String,
62 /// The message of the progress bar
63 pub msg: String,
64}
65
66/// Payload sent when a progress bar has finished its task
67/// This is usually used to show the final message of the progress bar
68/// If the progress bar failed, the message will be the failure message
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct ProgressFinishPayload {
71 /// The ID of the progress bar
72 pub id: String,
73 /// Whether the progress bar succeeded or failed
74 pub success: bool,
75 /// The message of the progress bar
76 pub msg: String,
77}
78
79/// Payload sent when a progress bar is updated
80#[derive(Debug, Clone, Serialize, Deserialize)]
81#[serde(tag = "event", content = "payload")]
82pub enum ProgressPayload {
83 /// Payload sent when a progress bar is started
84 Start(ProgressStartPayload),
85 /// Payload sent when a progress bar is incremented
86 Increment(ProgressIncrementPayload),
87 /// Payload sent when a progress bar's message is updated
88 Msg(ProgressMessagePayload),
89 /// Payload sent when a progress bar has finished its task
90 Finish(ProgressFinishPayload),
91}
92
93impl ProgressPayload {
94 /// Parse a progress bar payload from a log line
95 ///
96 /// ## Returns
97 ///
98 /// The payload the log line contains.
99 ///
100 /// ## Panics
101 ///
102 /// If we cannot parse the line, this method should only be used when we know the line is valid
103 ///
104 pub fn parse(input: &str) -> Result<Self> {
105 let payload = serde_json::from_str::<Self>(input)?;
106 Ok(payload)
107 }
108}
109
110/// Represents a progress bar
111pub struct ProgressBar {
112 id: String,
113 len: ProgressValue,
114 progress: ProgressValue,
115 throttled_progress: ProgressValue,
116 failure_message: String,
117 complete: bool,
118}
119
120impl ProgressBar {
121 /// Create a new progress bar
122 /// This will begin reporting the progress bar to the log
123 ///
124 /// ## Arguments
125 ///
126 /// - `id` - The ID of the progress bar
127 /// - `unique_name` - The unique name of the mod this progress bar is for, pass None if the progress bar doesn't know what mod it's for
128 /// - `len` - The length of the progress bar
129 /// - `msg` - The message of the progress bar
130 /// - `failure_message` - The message to show if the progress bar fails
131 /// - `progress_type` - The type of progress bar
132 /// - `progress_action` - The action this progress bar is reporting
133 ///
134 /// ## Returns
135 ///
136 /// The new progress bar
137 /// Note that if this is dropped without calling finish, it will be considered a failure, so make sure to call finish!
138 pub fn new(
139 id: &str,
140 unique_name: Option<&str>,
141 len: ProgressValue,
142 msg: &str,
143 failure_message: &str,
144 progress_type: ProgressType,
145 progress_action: ProgressAction,
146 ) -> Self {
147 let new = Self {
148 id: id.to_string(),
149 len,
150 progress: 0,
151 throttled_progress: 0,
152 failure_message: failure_message.to_string(),
153 complete: false,
154 };
155 let payload = ProgressPayload::Start(ProgressStartPayload {
156 id: id.to_string(),
157 unique_name: unique_name.map(|s| s.to_string()),
158 len,
159 msg: msg.to_string(),
160 progress_type,
161 progress_action,
162 });
163 new.emit_event(payload);
164 new
165 }
166
167 fn emit_event(&self, payload: ProgressPayload) {
168 let json = serde_json::to_string(&payload);
169 match json {
170 Ok(json) => {
171 info!(target: "progress", "{json}");
172 }
173 Err(why) => {
174 warn!(target: "progress", "Failed to serialize progress bar event: {why:?}");
175 }
176 }
177 }
178
179 /// Increment the progress bar
180 /// This will throttle the amount of times the progress bar can be incremented, so an increment may not emit a log line
181 /// This is done to prevent spamming small increments
182 pub fn inc(&mut self, amount: ProgressValue) {
183 const THROTTLING_AMOUNT: ProgressValue = 30;
184
185 let new_progress = self.progress.saturating_add(amount);
186
187 self.progress = new_progress.min(self.len);
188
189 if self.progress - self.throttled_progress > self.len / THROTTLING_AMOUNT {
190 self.throttled_progress = self.progress;
191 let payload = ProgressPayload::Increment(ProgressIncrementPayload {
192 id: self.id.clone(),
193 progress: self.progress,
194 });
195 self.emit_event(payload);
196 }
197 }
198
199 /// Set the message of the progress bar
200 pub fn set_msg(&self, msg: &str) {
201 let payload = ProgressPayload::Msg(ProgressMessagePayload {
202 id: self.id.clone(),
203 msg: msg.to_string(),
204 });
205 self.emit_event(payload);
206 }
207
208 /// Finish the progress bar
209 ///
210 /// This will emit a log line with the final message of the progress bar
211 /// This function should always be called when the progress bar is done, as a drop will result in a failure
212 ///
213 /// ## Arguments
214 ///
215 /// - `success` - Whether the progress bar succeeded or failed
216 /// - `msg` - The message of the progress bar, **this will be ignored if the progress bar failed,
217 /// and will instead use the failure message passed initially**
218 pub fn finish(&mut self, success: bool, msg: &str) {
219 self.complete = true;
220 let msg = if success { msg } else { &self.failure_message };
221 let payload = ProgressPayload::Finish(ProgressFinishPayload {
222 id: self.id.clone(),
223 success,
224 msg: msg.to_string(),
225 });
226 self.emit_event(payload);
227 }
228}
229
230impl Drop for ProgressBar {
231 fn drop(&mut self) {
232 if !self.complete {
233 self.finish(false, "");
234 }
235 }
236}
237
238/// Generalized progress bar tools
239pub mod bars {
240 use std::collections::HashMap;
241
242 use typeshare::typeshare;
243
244 use super::*;
245
246 /// Represents a progress bar
247 #[typeshare]
248 #[derive(Serialize, Clone)]
249 #[serde(rename_all = "camelCase")]
250 pub struct ProgressBar {
251 /// The ID of the progress bar
252 pub id: String,
253 /// The unique name of the mod this progress bar is for, sometimes this will be None if the progress bar doesn't know what mod it's for
254 pub unique_name: Option<String>,
255 /// The message of the progress bar
256 pub message: String,
257 /// The current progress of the progress bar
258 pub progress: ProgressValue,
259 /// The type of progress bar
260 pub progress_type: ProgressType,
261 /// The action this progress bar is reporting
262 pub progress_action: ProgressAction,
263 /// The length of the progress bar
264 pub len: ProgressValue,
265 /// Whether the progress bar succeeded or failed, and None if it hasn't finished
266 pub success: Option<bool>,
267 /// The position of the progress bar, the higher the position the higher it is in the list
268 pub position: u32,
269 }
270
271 impl ProgressBar {
272 /// Create a new progress bar from a [ProgressStartPayload]
273 fn from_payload(value: ProgressStartPayload, position: u32) -> Self {
274 Self {
275 id: value.id,
276 unique_name: value.unique_name,
277 message: value.msg,
278 progress_type: value.progress_type,
279 progress_action: value.progress_action,
280 progress: 0,
281 len: value.len,
282 success: None,
283 position,
284 }
285 }
286 }
287
288 /// Represents a collection of progress bars
289 /// This is used as a generalized way to keep track of all the progress bars and their positions
290 /// This is also used to process progress payloads
291 ///
292 /// Note that this still needs to be setup in your logger implementation
293 ///
294 /// ```no_run
295 /// use owmods_core::progress::bars::ProgressBars;
296 /// use owmods_core::progress::ProgressPayload;
297 /// use std::sync::{Arc, Mutex};
298 ///
299 /// struct Logger {
300 /// progress_bars: Arc<Mutex<ProgressBars>>,
301 /// };
302 ///
303 /// impl log::Log for Logger {
304 /// # fn enabled(&self, metadata: &log::Metadata) -> bool {
305 /// # true
306 /// # }
307 /// # fn flush(&self) {}
308 ///
309 /// fn log(&self, record: &log::Record) {
310 /// if record.target() == "progress" {
311 /// // Get ProgressBars from your state somehow...
312 /// let mut progress_bars = self.progress_bars.lock().expect("Lock is tainted");
313 /// let payload = ProgressPayload::parse(&format!("{}", record.args())).unwrap();
314 /// let any_failed = progress_bars.process(payload);
315 /// // Then emit some sort of event to update your UI
316 /// // Also do stuff with any_failed, etc
317 /// }
318 /// }
319 /// }
320 /// ```
321 #[typeshare]
322 #[derive(Serialize, Clone)]
323 pub struct ProgressBars {
324 /// A map of progress bars by their ID
325 pub bars: HashMap<String, ProgressBar>,
326 counter: u32,
327 }
328
329 impl ProgressBars {
330 /// Create a new collection of progress bars
331 pub fn new() -> Self {
332 Self {
333 bars: HashMap::new(),
334 counter: 0,
335 }
336 }
337
338 /// Get a progress bar by its ID
339 ///
340 /// ## Examples
341 ///
342 /// ```no_run
343 /// use owmods_core::progress::bars::ProgressBars;
344 /// use owmods_core::progress::{ProgressPayload, ProgressStartPayload, ProgressType, ProgressAction};
345 ///
346 /// let mut bars = ProgressBars::new();
347 /// let payload = ProgressPayload::Start(ProgressStartPayload {
348 /// id: "test".to_string(),
349 /// unique_name: Some("Test.test".to_string()),
350 /// len: 50,
351 /// msg: "Test Download".to_string(),
352 /// progress_type: ProgressType::Definite,
353 /// progress_action: ProgressAction::Download,
354 /// });
355 /// bars.process(payload);
356 ///
357 /// let bar = bars.by_id("test").unwrap();
358 ///
359 /// assert_eq!(bar.id, "test");
360 /// assert_eq!(bar.len, 50);
361 /// assert_eq!(bar.unique_name.as_ref().unwrap(), "Test.test");
362 /// ```
363 ///
364 pub fn by_id(&self, id: &str) -> Option<&ProgressBar> {
365 self.bars.get(id)
366 }
367
368 /// Get a progress bar by the mod associated with it
369 ///
370 /// ## Examples
371 ///
372 /// ```no_run
373 /// use owmods_core::progress::bars::ProgressBars;
374 /// use owmods_core::progress::{ProgressPayload, ProgressStartPayload, ProgressType, ProgressAction};
375 ///
376 /// let mut bars = ProgressBars::new();
377 /// let payload = ProgressPayload::Start(ProgressStartPayload {
378 /// id: "test".to_string(),
379 /// unique_name: Some("Test.test".to_string()),
380 /// len: 50,
381 /// msg: "Test Download".to_string(),
382 /// progress_type: ProgressType::Definite,
383 /// progress_action: ProgressAction::Download,
384 /// });
385 /// bars.process(payload);
386 ///
387 /// let bar = bars.by_unique_name("Test.test").unwrap();
388 ///
389 /// assert_eq!(bar.id, "test");
390 /// ```
391 ///
392 pub fn by_unique_name(&self, unique_name: &str) -> Option<&ProgressBar> {
393 self.bars
394 .values()
395 .filter(|b| b.success.is_none())
396 .find(|b| match &b.unique_name {
397 Some(bar_name) => bar_name == unique_name,
398 _ => false,
399 })
400 }
401
402 /// Process a progress payload
403 /// This will update the progress bar associated with the payload accordingly
404 /// If the payload is a [ProgressPayload::Finish] payload and all progress bars are finished, this will return whether any of the progress bars failed
405 ///
406 /// ## Examples
407 ///
408 /// ```no_run
409 /// use owmods_core::progress::bars::ProgressBars;
410 /// use owmods_core::progress::*;
411 ///
412 /// let mut bars = ProgressBars::new();
413 /// let payload = ProgressPayload::Start(ProgressStartPayload {
414 /// id: "test".to_string(),
415 /// unique_name: Some("Test.test".to_string()),
416 /// len: 50,
417 /// msg: "Test Download".to_string(),
418 /// progress_type: ProgressType::Definite,
419 /// progress_action: ProgressAction::Download,
420 /// });
421 /// bars.process(payload);
422 /// let payload = ProgressPayload::Increment(ProgressIncrementPayload {
423 /// id: "test".to_string(),
424 /// progress: 30,
425 /// });
426 /// bars.process(payload);
427 ///
428 /// let bar = bars.by_id("test").unwrap();
429 ///
430 /// assert_eq!(bar.progress, 30);
431 ///
432 /// let payload = ProgressPayload::Finish(ProgressFinishPayload {
433 /// id: "test".to_string(),
434 /// success: true,
435 /// msg: "Finished".to_string(),
436 /// });
437 /// bars.process(payload);
438 ///
439 /// let bar = bars.by_id("test").unwrap();
440 ///
441 /// assert_eq!(bar.progress, 50);
442 /// ```
443 ///
444 /// ```no_run
445 /// use owmods_core::progress::bars::ProgressBars;
446 /// use owmods_core::progress::*;
447 ///
448 /// let mut bars = ProgressBars::new();
449 /// let payload = ProgressPayload::Start(ProgressStartPayload {
450 /// id: "test".to_string(),
451 /// unique_name: Some("Test.test".to_string()),
452 /// len: 50,
453 /// msg: "Test Download".to_string(),
454 /// progress_type: ProgressType::Definite,
455 /// progress_action: ProgressAction::Download,
456 /// });
457 /// bars.process(payload);
458 /// let payload = ProgressPayload::Increment(ProgressIncrementPayload {
459 /// id: "test".to_string(),
460 /// progress: 30,
461 /// });
462 /// bars.process(payload);
463 /// let payload = ProgressPayload::Finish(ProgressFinishPayload {
464 /// id: "test".to_string(),
465 /// success: true,
466 /// msg: "Finished".to_string(),
467 /// });
468 /// let any_failed = bars.process(payload);
469 ///
470 /// assert!(any_failed.is_none());
471 ///
472 /// let payload = ProgressPayload::Finish(ProgressFinishPayload {
473 /// id: "test2".to_string(),
474 /// success: false,
475 /// msg: "Failed".to_string(),
476 /// });
477 /// let any_failed = bars.process(payload);
478 ///
479 /// assert!(any_failed.unwrap());
480 /// ```
481 ///
482 pub fn process(&mut self, payload: ProgressPayload) -> Option<bool> {
483 match payload {
484 ProgressPayload::Start(start_payload) => {
485 self.bars.insert(
486 start_payload.id.clone(),
487 ProgressBar::from_payload(start_payload.clone(), self.counter),
488 );
489 self.counter += 1;
490 }
491 ProgressPayload::Increment(payload) => {
492 if let Some(bar) = self.bars.get_mut(&payload.id) {
493 bar.progress = payload.progress
494 }
495 }
496 ProgressPayload::Msg(payload) => {
497 if let Some(bar) = self.bars.get_mut(&payload.id) {
498 bar.message = payload.msg
499 }
500 }
501 ProgressPayload::Finish(payload) => {
502 if let Some(bar) = self.bars.get_mut(&payload.id) {
503 bar.message = payload.msg;
504 bar.progress = bar.len;
505 bar.success = Some(payload.success);
506 }
507 if self.bars.values().all(|b| {
508 b.success.is_some() && matches!(b.progress_action, ProgressAction::Extract)
509 }) {
510 return Some(self.bars.iter().any(|b| !b.1.success.unwrap_or(true)));
511 }
512 }
513 }
514 None
515 }
516 }
517
518 impl Default for ProgressBars {
519 fn default() -> Self {
520 Self::new()
521 }
522 }
523}
524
525#[cfg(test)]
526mod tests {
527
528 use super::*;
529
530 mod bar_tests {
531
532 use crate::progress::bars::ProgressBars;
533
534 use super::*;
535
536 fn get_start_payload() -> ProgressPayload {
537 ProgressPayload::Start(ProgressStartPayload {
538 id: "test".to_string(),
539 unique_name: Some("Test.test".to_string()),
540 len: 50,
541 msg: "Test Download".to_string(),
542 progress_type: ProgressType::Definite,
543 progress_action: ProgressAction::Download,
544 })
545 }
546
547 #[test]
548 fn test_bar_start() {
549 let mut bars = ProgressBars::new();
550 let start_payload = get_start_payload();
551 bars.process(start_payload);
552 let bar = bars.by_id("test").unwrap();
553 assert_eq!(bar.id, "test");
554 assert_eq!(bar.len, 50);
555 assert_eq!(bar.unique_name.as_ref().unwrap(), "Test.test");
556 assert!(matches!(bar.progress_type, ProgressType::Definite));
557 assert!(matches!(bar.progress_action, ProgressAction::Download));
558 assert_eq!(bar.message, "Test Download");
559 }
560
561 #[test]
562 fn test_bar_inc() {
563 let mut bars = ProgressBars::new();
564 let start_payload = get_start_payload();
565 bars.process(start_payload);
566 let inc_payload = ProgressPayload::Increment(ProgressIncrementPayload {
567 id: "test".to_string(),
568 progress: 30,
569 });
570 bars.process(inc_payload);
571 let bar = bars.by_id("test").unwrap();
572 assert_eq!(bar.progress, 30);
573 }
574
575 #[test]
576 fn test_bar_msg() {
577 let mut bars = ProgressBars::new();
578 let start_payload = get_start_payload();
579 bars.process(start_payload);
580 let msg_payload = ProgressPayload::Msg(ProgressMessagePayload {
581 id: "test".to_string(),
582 msg: "Test Msg".to_string(),
583 });
584 bars.process(msg_payload);
585 let bar = bars.by_id("test").unwrap();
586 assert_eq!(bar.message, "Test Msg");
587 }
588
589 #[test]
590 fn test_bar_finish() {
591 let mut bars = ProgressBars::new();
592 let start_payload = get_start_payload();
593 bars.process(start_payload);
594 let finish_payload = ProgressPayload::Finish(ProgressFinishPayload {
595 id: "test".to_string(),
596 success: true,
597 msg: "Finished".to_string(),
598 });
599 bars.process(finish_payload);
600 let bar = bars.by_id("test").unwrap();
601 assert_eq!(bar.message, "Finished");
602 assert!(bar.success.unwrap());
603 }
604
605 #[test]
606 fn test_bar_finish_fail() {
607 let mut bars = ProgressBars::new();
608 let start_payload = get_start_payload();
609 bars.process(start_payload);
610 let finish_payload = ProgressPayload::Finish(ProgressFinishPayload {
611 id: "test".to_string(),
612 success: false,
613 msg: "Failed".to_string(),
614 });
615 bars.process(finish_payload);
616 let bar = bars.by_id("test").unwrap();
617 assert_eq!(bar.message, "Failed");
618 assert!(!bar.success.unwrap());
619 }
620
621 #[test]
622 fn test_bar_finish_all() {
623 let mut bars = ProgressBars::new();
624 let start_payload = ProgressPayload::Start(ProgressStartPayload {
625 id: "test".to_string(),
626 unique_name: Some("Test.test".to_string()),
627 len: 50,
628 msg: "Test Download".to_string(),
629 progress_type: ProgressType::Definite,
630 progress_action: ProgressAction::Extract,
631 });
632 bars.process(start_payload);
633 let start_payload = ProgressPayload::Start(ProgressStartPayload {
634 id: "test2".to_string(),
635 unique_name: Some("Test.test".to_string()),
636 len: 50,
637 msg: "Test Download".to_string(),
638 progress_type: ProgressType::Definite,
639 progress_action: ProgressAction::Extract,
640 });
641 bars.process(start_payload);
642 let finish_payload = ProgressPayload::Finish(ProgressFinishPayload {
643 id: "test".to_string(),
644 success: true,
645 msg: "Finished".to_string(),
646 });
647 let all_done = bars.process(finish_payload);
648 assert!(all_done.is_none());
649 let finish_payload = ProgressPayload::Finish(ProgressFinishPayload {
650 id: "test2".to_string(),
651 success: true,
652 msg: "Finished".to_string(),
653 });
654 let failed = bars.process(finish_payload);
655 assert!(!failed.unwrap());
656 }
657
658 #[test]
659 fn test_bar_finish_all_fail() {
660 let mut bars = ProgressBars::new();
661 let start_payload = ProgressPayload::Start(ProgressStartPayload {
662 id: "test".to_string(),
663 unique_name: Some("Test.test".to_string()),
664 len: 50,
665 msg: "Test Download".to_string(),
666 progress_type: ProgressType::Definite,
667 progress_action: ProgressAction::Extract,
668 });
669 bars.process(start_payload);
670 let start_payload = ProgressPayload::Start(ProgressStartPayload {
671 id: "test2".to_string(),
672 unique_name: Some("Test.test".to_string()),
673 len: 50,
674 msg: "Test Download".to_string(),
675 progress_type: ProgressType::Definite,
676 progress_action: ProgressAction::Extract,
677 });
678 bars.process(start_payload);
679 let finish_payload = ProgressPayload::Finish(ProgressFinishPayload {
680 id: "test".to_string(),
681 success: true,
682 msg: "Finished".to_string(),
683 });
684 let all_done = bars.process(finish_payload);
685 assert!(all_done.is_none());
686 let finish_payload = ProgressPayload::Finish(ProgressFinishPayload {
687 id: "test2".to_string(),
688 success: false,
689 msg: "Failed".to_string(),
690 });
691 let failed = bars.process(finish_payload);
692 assert!(failed.unwrap());
693 }
694 }
695}