Skip to main content

libsurfer/
wave_source.rs

1use std::fmt::{Display, Formatter};
2use std::fs;
3use std::io::Cursor;
4use std::sync::Arc;
5use std::sync::Mutex;
6use std::sync::atomic::AtomicU64;
7
8use crate::async_util::{perform_async_work, perform_work};
9use crate::channels::checked_send;
10use crate::cxxrtl_container::CxxrtlContainer;
11use crate::file_dialog::OpenMode;
12use crate::remote::{get_hierarchy_from_server, get_server_status, server_reload};
13use crate::transactions::TRANSACTIONS_FILE_EXTENSION;
14use crate::util::get_multi_extension;
15use camino::{Utf8Path, Utf8PathBuf};
16use eyre::Report;
17use eyre::Result;
18use eyre::{WrapErr, anyhow};
19use ftr_parser::parse;
20use futures_util::FutureExt;
21use serde::{Deserialize, Serialize};
22use tracing::{error, info, warn};
23use web_time::Instant;
24
25use crate::transaction_container::TransactionContainer;
26use crate::wave_container::WaveContainer;
27use crate::wellen::{
28    BodyResult, HeaderResult, LoadSignalPayload, LoadSignalsCmd, LoadSignalsResult,
29};
30use crate::{SystemState, message::Message};
31use surver::{
32    HTTP_SERVER_KEY, HTTP_SERVER_VALUE_SURFER, SurverFileInfo, WELLEN_SURFER_DEFAULT_OPTIONS,
33};
34
35#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
36pub enum CxxrtlKind {
37    Tcp { url: String },
38    Mailbox,
39}
40impl std::fmt::Display for CxxrtlKind {
41    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
42        match self {
43            CxxrtlKind::Tcp { url } => write!(f, "cxxrtl+tcp://{url}"),
44            CxxrtlKind::Mailbox => write!(f, "cxxrtl mailbox"),
45        }
46    }
47}
48
49#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
50pub enum WaveSource {
51    File(Utf8PathBuf),
52    Data,
53    DragAndDrop(Option<Utf8PathBuf>),
54    Url(String),
55    Cxxrtl(CxxrtlKind),
56}
57
58pub const STATE_FILE_EXTENSION: &str = "surf.ron";
59
60impl WaveSource {
61    #[must_use]
62    pub fn as_file(&self) -> Option<&Utf8Path> {
63        match self {
64            WaveSource::File(path) => Some(path.as_path()),
65            _ => None,
66        }
67    }
68
69    #[must_use]
70    pub fn path(&self) -> Option<&Utf8PathBuf> {
71        match self {
72            WaveSource::File(path) => Some(path),
73            WaveSource::DragAndDrop(Some(path)) => Some(path),
74            _ => None,
75        }
76    }
77
78    #[must_use]
79    pub fn sibling_state_file(&self) -> Option<Utf8PathBuf> {
80        let path = self.path()?;
81        let directory = path.parent()?;
82        let paths = fs::read_dir(directory).ok()?;
83
84        for entry in paths {
85            let Ok(entry) = entry else { continue };
86            if let Ok(path) = Utf8PathBuf::from_path_buf(entry.path()) {
87                let Some(ext) = get_multi_extension(&path) else {
88                    continue;
89                };
90                if ext.as_str() == STATE_FILE_EXTENSION {
91                    return Some(path);
92                }
93            }
94        }
95
96        None
97    }
98
99    #[must_use]
100    pub fn into_translation_type(&self) -> surfer_translation_types::WaveSource {
101        use surfer_translation_types::WaveSource as Ws;
102        match self {
103            WaveSource::File(file) => Ws::File(file.to_string()),
104            WaveSource::Data => Ws::Data,
105            WaveSource::DragAndDrop(file) => {
106                Ws::DragAndDrop(file.as_ref().map(ToString::to_string))
107            }
108            WaveSource::Url(u) => Ws::Url(u.clone()),
109            WaveSource::Cxxrtl(_) => Ws::Cxxrtl,
110        }
111    }
112}
113
114pub fn url_to_wavesource(url: &str) -> Option<WaveSource> {
115    if url.starts_with("https://") || url.starts_with("http://") {
116        info!("Wave source is url");
117        Some(WaveSource::Url(url.to_string()))
118    } else if url.starts_with("cxxrtl+tcp://") {
119        #[cfg(not(target_arch = "wasm32"))]
120        {
121            info!("Wave source is cxxrtl tcp");
122            Some(WaveSource::Cxxrtl(CxxrtlKind::Tcp {
123                url: url.replace("cxxrtl+tcp://", ""),
124            }))
125        }
126        #[cfg(target_arch = "wasm32")]
127        {
128            warn!("Loading waves from cxxrtl via tcp is unsupported in WASM builds.");
129            None
130        }
131    } else {
132        None
133    }
134}
135
136pub fn string_to_wavesource(path: &str) -> WaveSource {
137    if let Some(source) = url_to_wavesource(path) {
138        source
139    } else {
140        info!("Wave source is file");
141        WaveSource::File(path.into())
142    }
143}
144
145impl Display for WaveSource {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        match self {
148            WaveSource::File(file) => write!(f, "{file}"),
149            WaveSource::Data => write!(f, "File data"),
150            WaveSource::DragAndDrop(None) => write!(f, "Dropped file"),
151            WaveSource::DragAndDrop(Some(filename)) => write!(f, "Dropped file ({filename})"),
152            WaveSource::Url(url) => write!(f, "{url}"),
153            WaveSource::Cxxrtl(CxxrtlKind::Tcp { url }) => write!(f, "cxxrtl+tcp://{url}"),
154            WaveSource::Cxxrtl(CxxrtlKind::Mailbox) => write!(f, "cxxrtl mailbox"),
155        }
156    }
157}
158
159#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
160pub enum WaveFormat {
161    Vcd,
162    Fst,
163    Ghw,
164    CxxRtl,
165    Ftr,
166}
167
168impl Display for WaveFormat {
169    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
170        match self {
171            WaveFormat::Vcd => write!(f, "VCD"),
172            WaveFormat::Fst => write!(f, "FST"),
173            WaveFormat::Ghw => write!(f, "GHW"),
174            WaveFormat::CxxRtl => write!(f, "Cxxrtl"),
175            WaveFormat::Ftr => write!(f, "FTR"),
176        }
177    }
178}
179
180#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy)]
181pub enum LoadOptions {
182    Clear,
183    KeepAvailable,
184    KeepAll,
185}
186
187impl From<(OpenMode, bool)> for LoadOptions {
188    fn from(val: (OpenMode, bool)) -> Self {
189        match val {
190            (OpenMode::Open, _) => LoadOptions::Clear,
191            (OpenMode::Switch, false) => LoadOptions::KeepAvailable,
192            (OpenMode::Switch, true) => LoadOptions::KeepAll,
193        }
194    }
195}
196
197pub struct LoadProgress {
198    pub started: Instant,
199    pub progress: LoadProgressStatus,
200}
201
202impl LoadProgress {
203    #[must_use]
204    pub fn new(progress: LoadProgressStatus) -> Self {
205        LoadProgress {
206            started: Instant::now(),
207            progress,
208        }
209    }
210}
211
212pub enum LoadProgressStatus {
213    Downloading(String),
214    Connecting(String),
215    ReadingHeader(WaveSource),
216    ReadingBody(WaveSource, u64, Arc<AtomicU64>),
217    LoadingVariables(u64),
218}
219
220impl SystemState {
221    pub fn load_from_file(
222        &mut self,
223        filename: Utf8PathBuf,
224        load_options: LoadOptions,
225    ) -> Result<()> {
226        match get_multi_extension(&filename) {
227            Some(ext) => match ext.as_str() {
228                STATE_FILE_EXTENSION => {
229                    self.load_state_file(Some(filename.into_std_path_buf()));
230                    Ok(())
231                }
232                TRANSACTIONS_FILE_EXTENSION => {
233                    self.load_transactions_from_file(filename, load_options)
234                }
235                _ => self.load_wave_from_file(filename, load_options),
236            },
237            _ => self.load_wave_from_file(filename, load_options),
238        }
239    }
240
241    pub fn load_from_bytes(
242        &mut self,
243        source: WaveSource,
244        bytes: Vec<u8>,
245        load_options: LoadOptions,
246    ) {
247        if parse::is_ftr(&mut Cursor::new(&bytes)).is_ok_and(|is_ftr| is_ftr) {
248            self.load_transactions_from_bytes(source, bytes, load_options);
249        } else {
250            self.load_wave_from_bytes(source, bytes, load_options);
251        }
252    }
253
254    pub fn load_wave_from_file(
255        &mut self,
256        filename: Utf8PathBuf,
257        load_options: LoadOptions,
258    ) -> Result<()> {
259        info!("Loading a waveform file: {filename}");
260        let start = web_time::Instant::now();
261        let source = WaveSource::File(filename.clone());
262        let source_copy = source.clone();
263        let sender = self.channels.msg_sender.clone();
264
265        perform_work(move || {
266            let header_result = wellen::viewers::read_header_from_file(
267                filename.as_str(),
268                &WELLEN_SURFER_DEFAULT_OPTIONS,
269            )
270            .map_err(|e| anyhow!("{e:?}"))
271            .with_context(|| format!("Failed to parse wave file: {source}"));
272
273            let msg = match header_result {
274                Ok(header) => Message::WaveHeaderLoaded(
275                    start,
276                    source,
277                    load_options,
278                    HeaderResult::LocalFile(Box::new(header)),
279                ),
280                Err(e) => Message::Error(e),
281            };
282            checked_send(&sender, msg);
283        });
284
285        self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::ReadingHeader(
286            source_copy,
287        )));
288        Ok(())
289    }
290
291    pub fn load_from_data(&mut self, data: Vec<u8>, load_options: LoadOptions) -> Result<()> {
292        self.load_from_bytes(WaveSource::Data, data, load_options);
293        Ok(())
294    }
295
296    pub fn load_from_dropped(&mut self, file: egui::DroppedFile) -> Result<()> {
297        info!("Got a dropped file");
298
299        let path = file.path.and_then(|x| Utf8PathBuf::try_from(x).ok());
300
301        if let Some(bytes) = file.bytes {
302            if bytes.is_empty() {
303                Err(anyhow!("Dropped an empty file"))
304            } else {
305                if let Some(path) = path.clone() {
306                    if get_multi_extension(&path) == Some(STATE_FILE_EXTENSION.to_string()) {
307                        let sender = self.channels.msg_sender.clone();
308                        perform_async_work(async move {
309                            let new_state = match ron::de::from_bytes(&bytes)
310                                .context(format!("Failed loading {path}"))
311                            {
312                                Ok(s) => s,
313                                Err(e) => {
314                                    error!("Failed to load state: {e:#?}");
315                                    return;
316                                }
317                            };
318
319                            checked_send(
320                                &sender,
321                                Message::LoadState(new_state, Some(path.into_std_path_buf())),
322                            );
323                        });
324                    } else {
325                        self.load_from_bytes(
326                            WaveSource::DragAndDrop(Some(path)),
327                            bytes.to_vec(),
328                            LoadOptions::Clear,
329                        );
330                    }
331                } else {
332                    self.load_from_bytes(
333                        WaveSource::DragAndDrop(path),
334                        bytes.to_vec(),
335                        LoadOptions::Clear,
336                    );
337                }
338                Ok(())
339            }
340        } else if let Some(path) = path {
341            self.load_from_file(path, LoadOptions::Clear)
342        } else {
343            Err(anyhow!(
344                "Unknown how to load dropped file w/o path or bytes"
345            ))
346        }
347    }
348
349    pub fn load_wave_from_url(
350        &mut self,
351        url: String,
352        load_options: LoadOptions,
353        force_switch: bool,
354    ) {
355        match url_to_wavesource(&url) {
356            // We want to support opening cxxrtl urls using open url and friends,
357            // so we'll special case
358            #[cfg(not(target_arch = "wasm32"))]
359            Some(WaveSource::Cxxrtl(kind)) => {
360                self.connect_to_cxxrtl(kind, load_options != LoadOptions::Clear);
361            }
362            // However, if we don't get a cxxrtl url, we want to continue loading this as
363            // a url even if it isn't auto detected as a url.
364            _ => {
365                let sender = self.channels.msg_sender.clone();
366                let url_ = url.clone();
367                let file_index = self.user.selected_server_file_index;
368                info!("Loading wave from url: {url}");
369                perform_async_work(async move {
370                    let maybe_response = reqwest::get(&url)
371                        .map(|e| e.with_context(|| format!("Failed fetch download {url}")))
372                        .await;
373                    let response: reqwest::Response = match maybe_response {
374                        Ok(r) => r,
375                        Err(e) => {
376                            checked_send(&sender, Message::Error(e));
377                            return;
378                        }
379                    };
380
381                    // check to see if the response came from a Surfer running in server mode
382                    if let Some(value) = response.headers().get(HTTP_SERVER_KEY)
383                        && matches!(value.to_str(), Ok(HTTP_SERVER_VALUE_SURFER))
384                    {
385                        match load_options {
386                            LoadOptions::Clear => {
387                                info!("Connecting to a surfer server at: {url}");
388                                // Request status
389                                get_server_status(sender.clone(), url.clone(), 0);
390                                // Request hierarchy
391                                if let Some(file_index) = file_index {
392                                    get_hierarchy_from_server(
393                                        sender.clone(),
394                                        url,
395                                        load_options,
396                                        file_index,
397                                    );
398                                }
399                            }
400                            LoadOptions::KeepAvailable | LoadOptions::KeepAll => {
401                                // Request a reload (will also get status and request hierarchy if needed)
402                                if let Some(file_index) = file_index {
403                                    if force_switch {
404                                        get_hierarchy_from_server(
405                                            sender.clone(),
406                                            url,
407                                            load_options,
408                                            file_index,
409                                        );
410                                    } else {
411                                        info!("Reloading from surver instance at: {url}");
412                                        server_reload(
413                                            sender.clone(),
414                                            url,
415                                            load_options,
416                                            file_index,
417                                        );
418                                    }
419                                } else if force_switch {
420                                    // We started Surfer with a Surver URL as argument, so request status
421                                    get_server_status(sender.clone(), url.clone(), 0);
422                                } else {
423                                    warn!(
424                                        "Cannot reload from surver instance without a selected file index"
425                                    );
426                                }
427                            }
428                        }
429                        return;
430                    }
431
432                    // otherwise we load the body to get at the file
433                    let bytes = response
434                        .bytes()
435                        .map(|e| e.with_context(|| format!("Failed to download {url}")))
436                        .await;
437
438                    let msg = match bytes {
439                        Ok(b) => Message::FileDownloaded(url, b, load_options),
440                        Err(e) => Message::Error(e),
441                    };
442                    checked_send(&sender, msg);
443                });
444
445                self.progress_tracker =
446                    Some(LoadProgress::new(LoadProgressStatus::Downloading(url_)));
447            }
448        }
449    }
450
451    pub fn load_transactions_from_file(
452        &mut self,
453        filename: camino::Utf8PathBuf,
454        load_options: LoadOptions,
455    ) -> Result<()> {
456        info!("Loading a transaction file: {filename}");
457        let sender = self.channels.msg_sender.clone();
458        let source = WaveSource::File(filename.clone());
459        let format = WaveFormat::Ftr;
460
461        let result = ftr_parser::parse::parse_ftr(filename.into_std_path_buf());
462
463        info!("Done with loading ftr file");
464
465        let msg = match result {
466            Ok(ftr) => Message::TransactionStreamsLoaded(
467                source,
468                format,
469                TransactionContainer { inner: ftr },
470                load_options,
471            ),
472            Err(e) => Message::Error(Report::msg(e)),
473        };
474        checked_send(&sender, msg);
475        Ok(())
476    }
477    pub fn load_transactions_from_bytes(
478        &mut self,
479        source: WaveSource,
480        bytes: Vec<u8>,
481        load_options: LoadOptions,
482    ) {
483        let sender = self.channels.msg_sender.clone();
484
485        let result = parse::parse_ftr_from_bytes(bytes);
486
487        info!("Done with loading ftr file");
488
489        let msg = match result {
490            Ok(ftr) => Message::TransactionStreamsLoaded(
491                source,
492                WaveFormat::Ftr,
493                TransactionContainer { inner: ftr },
494                load_options,
495            ),
496            Err(e) => Message::Error(Report::msg(e)),
497        };
498        checked_send(&sender, msg);
499    }
500
501    /// uses the server status in order to display a loading bar
502    pub fn server_status_to_progress(&mut self, server: &str, file_info: &SurverFileInfo) {
503        // once the body is loaded, we are no longer interested in the status
504        let body_loaded = self
505            .user
506            .waves
507            .as_ref()
508            .is_some_and(|w| w.inner.body_loaded());
509        if !body_loaded {
510            // the progress tracker will be cleared once the hierarchy is returned from the server
511            let source = WaveSource::Url(server.to_string());
512            let sender = self.channels.msg_sender.clone();
513            self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::ReadingBody(
514                source,
515                file_info.bytes,
516                Arc::new(AtomicU64::new(file_info.bytes_loaded)),
517            )));
518            // get another status update
519            get_server_status(sender, server.to_string(), 250);
520        }
521    }
522
523    pub fn connect_to_cxxrtl(&mut self, kind: CxxrtlKind, keep_variables: bool) {
524        let sender = self.channels.msg_sender.clone();
525
526        self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::Connecting(format!(
527            "{kind}"
528        ))));
529
530        let task = async move {
531            let container = match &kind {
532                #[cfg(not(target_arch = "wasm32"))]
533                CxxrtlKind::Tcp { url } => {
534                    CxxrtlContainer::new_tcp(url, self.channels.msg_sender.clone()).await
535                }
536                #[cfg(target_arch = "wasm32")]
537                CxxrtlKind::Tcp { .. } => {
538                    error!("Cxxrtl tcp is not supported om wasm");
539                    return;
540                }
541                #[cfg(not(target_arch = "wasm32"))]
542                CxxrtlKind::Mailbox => {
543                    error!("CXXRTL mailboxes are only supported on wasm for now");
544                    return;
545                }
546                #[cfg(target_arch = "wasm32")]
547                CxxrtlKind::Mailbox => CxxrtlContainer::new_wasm_mailbox(sender.clone()).await,
548            };
549
550            let load_options = if keep_variables {
551                LoadOptions::KeepAvailable
552            } else {
553                LoadOptions::Clear
554            };
555            let msg = match container {
556                Ok(c) => Message::WavesLoaded(
557                    WaveSource::Cxxrtl(kind),
558                    WaveFormat::CxxRtl,
559                    Box::new(WaveContainer::Cxxrtl(Box::new(Mutex::new(c)))),
560                    load_options,
561                ),
562                Err(e) => Message::Error(e),
563            };
564            checked_send(&sender, msg);
565        };
566        #[cfg(not(target_arch = "wasm32"))]
567        futures::executor::block_on(task);
568        #[cfg(target_arch = "wasm32")]
569        wasm_bindgen_futures::spawn_local(task);
570    }
571
572    pub fn load_wave_from_bytes(
573        &mut self,
574        source: WaveSource,
575        bytes: Vec<u8>,
576        load_options: LoadOptions,
577    ) {
578        let start = web_time::Instant::now();
579        let sender = self.channels.msg_sender.clone();
580        let source_copy = source.clone();
581        perform_work(move || {
582            let header_result =
583                wellen::viewers::read_header(Cursor::new(bytes), &WELLEN_SURFER_DEFAULT_OPTIONS)
584                    .map_err(|e| anyhow!("{e:?}"))
585                    .with_context(|| format!("Failed to parse wave file: {source}"));
586
587            let msg = match header_result {
588                Ok(header) => Message::WaveHeaderLoaded(
589                    start,
590                    source,
591                    load_options,
592                    HeaderResult::LocalBytes(Box::new(header)),
593                ),
594                Err(e) => Message::Error(e),
595            };
596            checked_send(&sender, msg);
597        });
598
599        self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::ReadingHeader(
600            source_copy,
601        )));
602    }
603
604    fn get_thread_pool() -> Option<rayon::ThreadPool> {
605        // try to create a new rayon thread pool so that we do not block drawing functionality
606        // which might be blocked by the waveform reader using up all the threads in the global pool
607        match rayon::ThreadPoolBuilder::new().build() {
608            Ok(pool) => Some(pool),
609            Err(e) => {
610                // on wasm this will always fail
611                warn!("failed to create thread pool: {e:?}");
612                None
613            }
614        }
615    }
616
617    pub fn load_wave_body<R: std::io::BufRead + std::io::Seek + Sync + Send + 'static>(
618        &mut self,
619        source: WaveSource,
620        cont: wellen::viewers::ReadBodyContinuation<R>,
621        body_len: u64,
622        hierarchy: Arc<wellen::Hierarchy>,
623    ) {
624        let start = web_time::Instant::now();
625        let sender = self.channels.msg_sender.clone();
626        let source_copy = source.clone();
627        let progress = Arc::new(AtomicU64::new(0));
628        let progress_copy = progress.clone();
629        let pool = Self::get_thread_pool();
630
631        perform_work(move || {
632            let action = || {
633                let p = Some(progress_copy);
634                let body_result = wellen::viewers::read_body(cont, &hierarchy, p)
635                    .map_err(|e| anyhow!("{e:?}"))
636                    .with_context(|| format!("Failed to parse body of wave file: {source}"));
637
638                let msg = match body_result {
639                    Ok(body) => Message::WaveBodyLoaded(start, source, BodyResult::Local(body)),
640                    Err(e) => Message::Error(e),
641                };
642                checked_send(&sender, msg);
643            };
644            if let Some(pool) = pool {
645                pool.install(action);
646            } else {
647                action();
648            }
649        });
650
651        self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::ReadingBody(
652            source_copy,
653            body_len,
654            progress,
655        )));
656    }
657
658    pub fn load_variables(&mut self, cmd: LoadSignalsCmd) {
659        let (signals, from_unique_id, payload) = cmd.destruct();
660        if signals.is_empty() {
661            return;
662        }
663        let num_signals = signals.len() as u64;
664        let start = web_time::Instant::now();
665        let sender = self.channels.msg_sender.clone();
666        let max_url_length = self.user.config.max_url_length;
667        match payload {
668            LoadSignalPayload::Local(mut source, hierarchy) => {
669                let pool = Self::get_thread_pool();
670
671                perform_work(move || {
672                    let action = || {
673                        let loaded = source.load_signals(&signals, &hierarchy, true);
674                        let res = LoadSignalsResult::local(source, loaded, from_unique_id);
675                        checked_send(&sender, Message::SignalsLoaded(start, res));
676                    };
677                    if let Some(pool) = pool {
678                        pool.install(action);
679                    } else {
680                        action();
681                    }
682                });
683            }
684            LoadSignalPayload::Remote(server) => {
685                perform_async_work(async move {
686                    let res =
687                        crate::remote::get_signals(server.clone(), &signals, max_url_length, 0)
688                            .await
689                            .map_err(|e| anyhow!("{e:?}"))
690                            .with_context(|| {
691                                format!("Failed to retrieve signals from remote server {server}")
692                            });
693
694                    let msg = match res {
695                        Ok(loaded) => {
696                            let res = LoadSignalsResult::remote(server, loaded, from_unique_id);
697                            Message::SignalsLoaded(start, res)
698                        }
699                        Err(e) => Message::Error(e),
700                    };
701                    checked_send(&sender, msg);
702                });
703            }
704        }
705
706        self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::LoadingVariables(
707            num_signals,
708        )));
709    }
710}
711
712pub fn draw_progress_information(ui: &mut egui::Ui, progress_data: &LoadProgress) {
713    match &progress_data.progress {
714        LoadProgressStatus::Connecting(url) => {
715            ui.horizontal(|ui| {
716                ui.spinner();
717                ui.monospace(format!("Connecting {url}"));
718            });
719        }
720        LoadProgressStatus::Downloading(url) => {
721            ui.horizontal(|ui| {
722                ui.spinner();
723                ui.monospace(format!("Downloading {url}"));
724            });
725        }
726        LoadProgressStatus::ReadingHeader(source) => {
727            ui.spinner();
728            ui.monospace(format!("Loading variable names from {source}"));
729        }
730        LoadProgressStatus::ReadingBody(source, 0, _) => {
731            ui.spinner();
732            ui.monospace(format!("Loading variable change data from {source}"));
733        }
734        LoadProgressStatus::LoadingVariables(num) => {
735            ui.spinner();
736            ui.monospace(format!("Loading {num} variables"));
737        }
738        LoadProgressStatus::ReadingBody(source, total, bytes_done) => {
739            let num_bytes = bytes_done.load(std::sync::atomic::Ordering::SeqCst);
740            let progress = num_bytes as f32 / *total as f32;
741            ui.monospace(format!(
742                "Loading variable change data from {source}. {} / {}",
743                bytesize::ByteSize::b(num_bytes),
744                bytesize::ByteSize::b(*total),
745            ));
746            let progress_bar = egui::ProgressBar::new(progress)
747                .show_percentage()
748                .desired_width(300.);
749            ui.add(progress_bar);
750        }
751    }
752}