surfer/
main.rs

1#![cfg_attr(not(target_arch = "wasm32"), deny(unused_crate_dependencies))]
2
3#[cfg(not(target_arch = "wasm32"))]
4mod main_impl {
5    use camino::Utf8PathBuf;
6    use clap::Parser;
7    use color_eyre::eyre::Context;
8    use color_eyre::Result;
9    use egui::Vec2;
10    use libsurfer::{
11        file_watcher::FileWatcher, logs, message::Message, run_egui,
12        wave_source::string_to_wavesource, wave_source::WaveSource, StartupParams, SystemState,
13    };
14    use log::error;
15
16    #[derive(clap::Subcommand)]
17    enum Commands {
18        #[cfg(not(target_arch = "wasm32"))]
19        /// starts surfer in headless mode so that a user can connect to it
20        Server {
21            /// port on which server will listen
22            #[clap(long)]
23            port: Option<u16>,
24            /// token used by the client to authenticate to the server
25            #[clap(long)]
26            token: Option<String>,
27            /// waveform file that we want to serve
28            #[arg(long)]
29            file: String,
30        },
31    }
32
33    #[derive(clap::Parser, Default)]
34    #[command(version, about)]
35    struct Args {
36        /// Waveform file in VCD, FST, or GHW format.
37        wave_file: Option<String>,
38        #[cfg(feature = "spade")]
39        #[clap(long)]
40        /// Load Spade state file
41        spade_state: Option<Utf8PathBuf>,
42        #[cfg(feature = "spade")]
43        #[clap(long)]
44        /// Specify Spade top-level entity
45        spade_top: Option<String>,
46        /// Path to a file containing 'commands' to run after a waveform has been loaded.
47        /// The commands are the same as those used in the command line interface inside the program.
48        /// Commands are separated by lines or ;. Empty lines are ignored. Line comments starting with
49        /// `#` are supported
50        /// NOTE: This feature is not permanent, it will be removed once a solid scripting system
51        /// is implemented.
52        #[clap(long, short, verbatim_doc_comment)]
53        command_file: Option<Utf8PathBuf>,
54        /// Alias for --command_file to support VUnit
55        #[clap(long)]
56        script: Option<Utf8PathBuf>,
57
58        #[clap(long, short)]
59        /// Load previously saved state file
60        state_file: Option<Utf8PathBuf>,
61
62        #[clap(long, action)]
63        /// Port for WCP to connect to
64        wcp_initiate: Option<u16>,
65
66        #[command(subcommand)]
67        command: Option<Commands>,
68    }
69
70    impl Args {
71        pub fn command_file(&self) -> &Option<Utf8PathBuf> {
72            if self.script.is_some() && self.command_file.is_some() {
73                error!("At most one of --command_file and --script can be used");
74                return &None;
75            }
76            if self.command_file.is_some() {
77                &self.command_file
78            } else {
79                &self.script
80            }
81        }
82    }
83
84    #[allow(dead_code)] // NOTE: Only used in desktop version
85    fn startup_params_from_args(args: Args) -> StartupParams {
86        let startup_commands = if let Some(cmd_file) = args.command_file() {
87            std::fs::read_to_string(cmd_file)
88                .map_err(|e| error!("Failed to read commands from {cmd_file}. {e:#?}"))
89                .ok()
90                .map(|file_content| {
91                    file_content
92                        .lines()
93                        .map(std::string::ToString::to_string)
94                        .collect()
95                })
96                .unwrap_or_default()
97        } else {
98            vec![]
99        };
100        StartupParams {
101            #[cfg(feature = "spade")]
102            spade_state: args.spade_state,
103            #[cfg(feature = "spade")]
104            spade_top: args.spade_top,
105            #[cfg(not(feature = "spade"))]
106            spade_state: None,
107            #[cfg(not(feature = "spade"))]
108            spade_top: None,
109            waves: args.wave_file.map(|s| string_to_wavesource(&s)),
110            wcp_initiate: args.wcp_initiate,
111            startup_commands,
112        }
113    }
114
115    #[cfg(not(target_arch = "wasm32"))]
116    pub(crate) fn main() -> Result<()> {
117        use libsurfer::state::UserState;
118
119        logs::start_logging()?;
120
121        // https://tokio.rs/tokio/topics/bridging
122        // We want to run the gui in the main thread, but some long running tasks like
123        // loading VCDs should be done asynchronously. We can't just use std::thread to
124        // do that due to wasm support, so we'll start a tokio runtime
125        let runtime = tokio::runtime::Builder::new_current_thread()
126            .worker_threads(1)
127            .enable_all()
128            .build()
129            .unwrap();
130
131        // parse arguments
132        let args = Args::parse();
133        #[cfg(not(target_arch = "wasm32"))]
134        if let Some(Commands::Server { port, token, file }) = args.command {
135            let default_port = 8911; // FIXME: make this more configurable
136            let res = runtime.block_on(surver::server_main(
137                port.unwrap_or(default_port),
138                token,
139                file,
140                None,
141            ));
142            return res;
143        }
144
145        let _enter = runtime.enter();
146
147        std::thread::spawn(move || {
148            runtime.block_on(async {
149                loop {
150                    tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await;
151                }
152            });
153        });
154
155        let state_file = args.state_file.clone();
156        let startup_params = startup_params_from_args(args);
157        let waves = startup_params.waves.clone();
158
159        let state = match &state_file {
160            Some(file) => std::fs::read_to_string(file)
161                .with_context(|| format!("Failed to read state from {file}"))
162                .and_then(|content| {
163                    ron::from_str::<UserState>(&content)
164                        .with_context(|| format!("Failed to decode state from {file}"))
165                })
166                .map(SystemState::from)
167                .map(|mut s| {
168                    s.user.state_file = Some(file.into());
169                    s
170                })
171                .or_else(|e| {
172                    error!("Failed to read state file. Opening fresh session\n{e:#?}");
173                    SystemState::new()
174                })?,
175            None => SystemState::new()?,
176        }
177        .with_params(startup_params);
178
179        // install a file watcher that emits a `SuggestReloadWaveform` message
180        // whenever the user-provided file changes.
181        let _watcher = match waves {
182            Some(WaveSource::File(path)) => {
183                let sender = state.channels.msg_sender.clone();
184                FileWatcher::new(&path, move || {
185                    match sender.send(Message::SuggestReloadWaveform) {
186                        Ok(_) => {}
187                        Err(err) => {
188                            error!("Message ReloadWaveform did not send:\n{err}")
189                        }
190                    }
191                })
192                .inspect_err(|err| error!("Cannot set up the file watcher:\n{err}"))
193                .ok()
194            }
195            _ => None,
196        };
197
198        let options = eframe::NativeOptions {
199            viewport: egui::ViewportBuilder::default()
200                .with_title("Surfer")
201                .with_inner_size(Vec2::new(
202                    state.user.config.layout.window_width as f32,
203                    state.user.config.layout.window_height as f32,
204                )),
205            ..Default::default()
206        };
207
208        eframe::run_native("Surfer", options, Box::new(|cc| Ok(run_egui(cc, state)?))).unwrap();
209
210        Ok(())
211    }
212}
213
214#[cfg(target_arch = "wasm32")]
215mod main_impl {
216    use eframe::wasm_bindgen::JsCast;
217    use eframe::web_sys;
218    use libsurfer::wasm_api::WebHandle;
219
220    // Calling main is not the intended way to start surfer, instead, it should be
221    // started by `wasm_api::WebHandle`
222    pub(crate) fn main() -> color_eyre::Result<()> {
223        let document = web_sys::window()
224            .expect("No window")
225            .document()
226            .expect("No document");
227        let canvas = document
228            .get_element_by_id("the_canvas_id")
229            .expect("Failed to find the_canvas_id")
230            .dyn_into::<web_sys::HtmlCanvasElement>()
231            .expect("the_canvas_id was not a HtmlCanvasElement");
232
233        wasm_bindgen_futures::spawn_local(async {
234            let wh = WebHandle::new();
235            wh.start(canvas).await.expect("Failed to start surfer");
236        });
237
238        Ok(())
239    }
240}
241
242fn main() -> color_eyre::Result<()> {
243    main_impl::main()
244}