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 as _, 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        if !filename.exists() {
265            let msg = Message::Error(anyhow!("Waveform file is missing: {filename}"));
266            checked_send(&sender, msg);
267            return Ok(());
268        }
269        perform_work(move || {
270            let header_result = wellen::viewers::read_header_from_file(
271                filename.as_str(),
272                &WELLEN_SURFER_DEFAULT_OPTIONS,
273            )
274            .map_err(|e| anyhow!("{e:?}"))
275            .with_context(|| format!("Failed to parse wave file: {source}"));
276
277            let msg = match header_result {
278                Ok(header) => Message::WaveHeaderLoaded(
279                    start,
280                    source,
281                    load_options,
282                    HeaderResult::LocalFile(Box::new(header)),
283                ),
284                Err(e) => Message::Error(e),
285            };
286            checked_send(&sender, msg);
287        });
288
289        self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::ReadingHeader(
290            source_copy,
291        )));
292        Ok(())
293    }
294
295    pub fn load_from_data(&mut self, data: Vec<u8>, load_options: LoadOptions) -> Result<()> {
296        self.load_from_bytes(WaveSource::Data, data, load_options);
297        Ok(())
298    }
299
300    pub fn load_from_dropped(&mut self, file: egui::DroppedFile) -> Result<()> {
301        info!("Got a dropped file");
302
303        let path = file.path.and_then(|x| Utf8PathBuf::try_from(x).ok());
304
305        if let Some(bytes) = file.bytes {
306            if bytes.is_empty() {
307                Err(anyhow!("Dropped an empty file"))
308            } else {
309                if let Some(path) = path.clone() {
310                    if get_multi_extension(&path) == Some(STATE_FILE_EXTENSION.to_string()) {
311                        let sender = self.channels.msg_sender.clone();
312                        perform_async_work(async move {
313                            let new_state = match ron::de::from_bytes(&bytes)
314                                .context(format!("Failed loading {path}"))
315                            {
316                                Ok(s) => s,
317                                Err(e) => {
318                                    error!("Failed to load state: {e:#?}");
319                                    return;
320                                }
321                            };
322
323                            checked_send(
324                                &sender,
325                                Message::LoadState(new_state, Some(path.into_std_path_buf())),
326                            );
327                        });
328                    } else {
329                        self.load_from_bytes(
330                            WaveSource::DragAndDrop(Some(path)),
331                            bytes.to_vec(),
332                            LoadOptions::Clear,
333                        );
334                    }
335                } else {
336                    self.load_from_bytes(
337                        WaveSource::DragAndDrop(path),
338                        bytes.to_vec(),
339                        LoadOptions::Clear,
340                    );
341                }
342                Ok(())
343            }
344        } else if let Some(path) = path {
345            self.load_from_file(path, LoadOptions::Clear)
346        } else {
347            Err(anyhow!(
348                "Unknown how to load dropped file w/o path or bytes"
349            ))
350        }
351    }
352
353    pub fn load_wave_from_url(
354        &mut self,
355        url: String,
356        load_options: LoadOptions,
357        force_switch: bool,
358        file_index: Option<usize>,
359    ) {
360        if file_index.is_some() {
361            self.user.selected_server_file_index = file_index;
362            *self.surver_selected_file.borrow_mut() = file_index;
363        }
364
365        match url_to_wavesource(&url) {
366            // We want to support opening cxxrtl urls using open url and friends,
367            // so we'll special case
368            #[cfg(not(target_arch = "wasm32"))]
369            Some(WaveSource::Cxxrtl(kind)) => {
370                self.connect_to_cxxrtl(kind, load_options != LoadOptions::Clear);
371            }
372            // However, if we don't get a cxxrtl url, we want to continue loading this as
373            // a url even if it isn't auto detected as a url.
374            _ => {
375                let sender = self.channels.msg_sender.clone();
376                let url_ = url.clone();
377                info!("Loading wave from url: {url}");
378                perform_async_work(async move {
379                    let maybe_response = reqwest::get(&url)
380                        .map(|e| e.with_context(|| format!("Failed fetch download {url}")))
381                        .await;
382                    let response: reqwest::Response = match maybe_response {
383                        Ok(r) => r,
384                        Err(e) => {
385                            checked_send(&sender, Message::Error(e));
386                            return;
387                        }
388                    };
389
390                    // check to see if the response came from a Surfer running in server mode
391                    if let Some(value) = response.headers().get(HTTP_SERVER_KEY)
392                        && matches!(value.to_str(), Ok(HTTP_SERVER_VALUE_SURFER))
393                    {
394                        match load_options {
395                            LoadOptions::Clear => {
396                                info!("Connecting to a surfer server at: {url}");
397                                // Request status
398                                get_server_status(sender.clone(), url.clone(), 0);
399                                // Request hierarchy
400                                if let Some(file_index) = file_index {
401                                    get_hierarchy_from_server(
402                                        sender.clone(),
403                                        url,
404                                        load_options,
405                                        file_index,
406                                    );
407                                }
408                            }
409                            LoadOptions::KeepAvailable | LoadOptions::KeepAll => {
410                                // Request a reload (will also get status and request hierarchy if needed)
411                                if let Some(file_index) = file_index {
412                                    if force_switch {
413                                        get_hierarchy_from_server(
414                                            sender.clone(),
415                                            url,
416                                            load_options,
417                                            file_index,
418                                        );
419                                    } else {
420                                        info!("Reloading from surver instance at: {url}");
421                                        server_reload(
422                                            sender.clone(),
423                                            url,
424                                            load_options,
425                                            file_index,
426                                        );
427                                    }
428                                } else if force_switch {
429                                    // We started Surfer with a Surver URL as argument, so request status
430                                    get_server_status(sender.clone(), url.clone(), 0);
431                                } else {
432                                    warn!(
433                                        "Cannot reload from surver instance without a selected file index"
434                                    );
435                                }
436                            }
437                        }
438                        return;
439                    }
440
441                    // otherwise we load the body to get at the file
442                    let bytes = response
443                        .bytes()
444                        .map(|e| e.with_context(|| format!("Failed to download {url}")))
445                        .await;
446
447                    let msg = match bytes {
448                        Ok(b) => Message::FileDownloaded(url, b, load_options),
449                        Err(e) => Message::Error(e),
450                    };
451                    checked_send(&sender, msg);
452                });
453
454                self.progress_tracker =
455                    Some(LoadProgress::new(LoadProgressStatus::Downloading(url_)));
456            }
457        }
458    }
459
460    pub fn load_transactions_from_file(
461        &mut self,
462        filename: camino::Utf8PathBuf,
463        load_options: LoadOptions,
464    ) -> Result<()> {
465        info!("Loading a transaction file: {filename}");
466        let sender = self.channels.msg_sender.clone();
467        let source = WaveSource::File(filename.clone());
468        let format = WaveFormat::Ftr;
469
470        let result = ftr_parser::parse::parse_ftr(filename.into_std_path_buf());
471
472        info!("Done with loading ftr file");
473
474        let msg = match result {
475            Ok(ftr) => Message::TransactionStreamsLoaded(
476                source,
477                format,
478                TransactionContainer { inner: ftr },
479                load_options,
480            ),
481            Err(e) => Message::Error(Report::msg(e)),
482        };
483        checked_send(&sender, msg);
484        Ok(())
485    }
486    pub fn load_transactions_from_bytes(
487        &mut self,
488        source: WaveSource,
489        bytes: Vec<u8>,
490        load_options: LoadOptions,
491    ) {
492        let sender = self.channels.msg_sender.clone();
493
494        let result = parse::parse_ftr_from_bytes(bytes);
495
496        info!("Done with loading ftr file");
497
498        let msg = match result {
499            Ok(ftr) => Message::TransactionStreamsLoaded(
500                source,
501                WaveFormat::Ftr,
502                TransactionContainer { inner: ftr },
503                load_options,
504            ),
505            Err(e) => Message::Error(Report::msg(e)),
506        };
507        checked_send(&sender, msg);
508    }
509
510    /// uses the server status in order to display a loading bar
511    pub fn server_status_to_progress(&mut self, server: &str, file_info: &SurverFileInfo) {
512        // once the body is loaded, we are no longer interested in the status
513        let body_loaded = self
514            .user
515            .waves
516            .as_ref()
517            .is_some_and(|w| w.inner.body_loaded());
518        if !body_loaded {
519            // the progress tracker will be cleared once the hierarchy is returned from the server
520            let source = WaveSource::Url(server.to_string());
521            let sender = self.channels.msg_sender.clone();
522            self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::ReadingBody(
523                source,
524                file_info.bytes,
525                Arc::new(AtomicU64::new(file_info.bytes_loaded)),
526            )));
527            // get another status update
528            get_server_status(sender, server.to_string(), 250);
529        }
530    }
531
532    pub fn connect_to_cxxrtl(&mut self, kind: CxxrtlKind, keep_variables: bool) {
533        let sender = self.channels.msg_sender.clone();
534
535        self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::Connecting(format!(
536            "{kind}"
537        ))));
538
539        let task = async move {
540            let container = match &kind {
541                #[cfg(not(target_arch = "wasm32"))]
542                CxxrtlKind::Tcp { url } => {
543                    CxxrtlContainer::new_tcp(url, self.channels.msg_sender.clone()).await
544                }
545                #[cfg(target_arch = "wasm32")]
546                CxxrtlKind::Tcp { .. } => {
547                    error!("Cxxrtl tcp is not supported om wasm");
548                    return;
549                }
550                #[cfg(not(target_arch = "wasm32"))]
551                CxxrtlKind::Mailbox => {
552                    error!("CXXRTL mailboxes are only supported on wasm for now");
553                    return;
554                }
555                #[cfg(target_arch = "wasm32")]
556                CxxrtlKind::Mailbox => CxxrtlContainer::new_wasm_mailbox(sender.clone()).await,
557            };
558
559            let load_options = if keep_variables {
560                LoadOptions::KeepAvailable
561            } else {
562                LoadOptions::Clear
563            };
564            let msg = match container {
565                Ok(c) => Message::WavesLoaded(
566                    WaveSource::Cxxrtl(kind),
567                    WaveFormat::CxxRtl,
568                    Box::new(WaveContainer::Cxxrtl(Box::new(Mutex::new(c)))),
569                    load_options,
570                ),
571                Err(e) => Message::Error(e),
572            };
573            checked_send(&sender, msg);
574        };
575        #[cfg(not(target_arch = "wasm32"))]
576        futures::executor::block_on(task);
577        #[cfg(target_arch = "wasm32")]
578        wasm_bindgen_futures::spawn_local(task);
579    }
580
581    pub fn load_wave_from_bytes(
582        &mut self,
583        source: WaveSource,
584        bytes: Vec<u8>,
585        load_options: LoadOptions,
586    ) {
587        let start = web_time::Instant::now();
588        let sender = self.channels.msg_sender.clone();
589        let source_copy = source.clone();
590        perform_work(move || {
591            let header_result =
592                wellen::viewers::read_header(Cursor::new(bytes), &WELLEN_SURFER_DEFAULT_OPTIONS)
593                    .map_err(|e| anyhow!("{e:?}"))
594                    .with_context(|| format!("Failed to parse wave file: {source}"));
595
596            let msg = match header_result {
597                Ok(header) => Message::WaveHeaderLoaded(
598                    start,
599                    source,
600                    load_options,
601                    HeaderResult::LocalBytes(Box::new(header)),
602                ),
603                Err(e) => Message::Error(e),
604            };
605            checked_send(&sender, msg);
606        });
607
608        self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::ReadingHeader(
609            source_copy,
610        )));
611    }
612
613    fn get_thread_pool() -> Option<rayon::ThreadPool> {
614        // try to create a new rayon thread pool so that we do not block drawing functionality
615        // which might be blocked by the waveform reader using up all the threads in the global pool
616        match rayon::ThreadPoolBuilder::new().build() {
617            Ok(pool) => Some(pool),
618            Err(e) => {
619                // on wasm this will always fail
620                warn!("failed to create thread pool: {e:?}");
621                None
622            }
623        }
624    }
625
626    pub fn load_wave_body<R: std::io::BufRead + std::io::Seek + Sync + Send + 'static>(
627        &mut self,
628        source: WaveSource,
629        cont: wellen::viewers::ReadBodyContinuation<R>,
630        body_len: u64,
631        hierarchy: Arc<wellen::Hierarchy>,
632    ) {
633        let start = web_time::Instant::now();
634        let sender = self.channels.msg_sender.clone();
635        let source_copy = source.clone();
636        let progress = Arc::new(AtomicU64::new(0));
637        let progress_copy = progress.clone();
638        let pool = Self::get_thread_pool();
639
640        perform_work(move || {
641            let action = || {
642                let p = Some(progress_copy);
643                let body_result = wellen::viewers::read_body(cont, &hierarchy, p)
644                    .map_err(|e| anyhow!("{e:?}"))
645                    .with_context(|| format!("Failed to parse body of wave file: {source}"));
646
647                let msg = match body_result {
648                    Ok(body) => Message::WaveBodyLoaded(start, source, BodyResult::Local(body)),
649                    Err(e) => Message::Error(e),
650                };
651                checked_send(&sender, msg);
652            };
653            if let Some(pool) = pool {
654                pool.install(action);
655            } else {
656                action();
657            }
658        });
659
660        self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::ReadingBody(
661            source_copy,
662            body_len,
663            progress,
664        )));
665    }
666
667    pub fn load_variables(&mut self, cmd: LoadSignalsCmd) {
668        let (signals, from_unique_id, payload) = cmd.destruct();
669        if signals.is_empty() {
670            return;
671        }
672        let num_signals = signals.len() as u64;
673        let start = web_time::Instant::now();
674        let sender = self.channels.msg_sender.clone();
675        let max_url_length = self.user.config.max_url_length;
676        match payload {
677            LoadSignalPayload::Local(mut source, hierarchy) => {
678                let pool = Self::get_thread_pool();
679
680                perform_work(move || {
681                    let action = || {
682                        let loaded = source.load_signals(&signals, &hierarchy, true);
683                        let res = LoadSignalsResult::local(source, loaded, from_unique_id);
684                        checked_send(&sender, Message::SignalsLoaded(start, res));
685                    };
686                    if let Some(pool) = pool {
687                        pool.install(action);
688                    } else {
689                        action();
690                    }
691                });
692            }
693            LoadSignalPayload::Remote(server, file_index) => {
694                perform_async_work(async move {
695                    let res =
696                        crate::remote::get_signals(
697                            server.clone(),
698                            &signals,
699                            max_url_length,
700                            file_index,
701                        )
702                        .await
703                        .map_err(|e| anyhow!("{e:?}"))
704                        .with_context(|| {
705                            format!(
706                                "Failed to retrieve signals from remote server {server} file index {file_index}"
707                            )
708                        });
709
710                    let msg = match res {
711                        Ok(loaded) => {
712                            let res = LoadSignalsResult::remote(server, loaded, from_unique_id);
713                            Message::SignalsLoaded(start, res)
714                        }
715                        Err(e) => Message::Error(e),
716                    };
717                    checked_send(&sender, msg);
718                });
719            }
720        }
721
722        self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::LoadingVariables(
723            num_signals,
724        )));
725    }
726}
727
728pub fn draw_progress_information(ui: &mut egui::Ui, progress_data: &LoadProgress) {
729    match &progress_data.progress {
730        LoadProgressStatus::Connecting(url) => {
731            ui.horizontal(|ui| {
732                ui.spinner();
733                ui.monospace(format!("Connecting {url}"));
734            });
735        }
736        LoadProgressStatus::Downloading(url) => {
737            ui.horizontal(|ui| {
738                ui.spinner();
739                ui.monospace(format!("Downloading {url}"));
740            });
741        }
742        LoadProgressStatus::ReadingHeader(source) => {
743            ui.spinner();
744            ui.monospace(format!("Loading variable names from {source}"));
745        }
746        LoadProgressStatus::ReadingBody(source, 0, _) => {
747            ui.spinner();
748            ui.monospace(format!("Loading variable change data from {source}"));
749        }
750        LoadProgressStatus::LoadingVariables(num) => {
751            ui.spinner();
752            ui.monospace(format!("Loading {num} variables"));
753        }
754        LoadProgressStatus::ReadingBody(source, total, bytes_done) => {
755            let num_bytes = bytes_done.load(std::sync::atomic::Ordering::SeqCst);
756            let progress = num_bytes as f32 / *total as f32;
757            ui.monospace(format!(
758                "Loading variable change data from {source}. {} / {}",
759                bytesize::ByteSize::b(num_bytes),
760                bytesize::ByteSize::b(*total),
761            ));
762            let progress_bar = egui::ProgressBar::new(progress)
763                .show_percentage()
764                .desired_width(300.);
765            ui.add(progress_bar);
766        }
767    }
768}