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}