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