surfer/
main.rs

1#![cfg_attr(not(target_arch = "wasm32"), deny(unused_crate_dependencies))]
2#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3
4#[cfg(not(target_arch = "wasm32"))]
5mod main_impl {
6    use camino::Utf8PathBuf;
7    use clap::Parser;
8    use emath::Vec2;
9    use eyre::Context;
10    use eyre::Result;
11    use libsurfer::{
12        StartupParams, SystemState,
13        batch_commands::read_command_file,
14        file_watcher::FileWatcher,
15        logs,
16        message::Message,
17        run_egui,
18        wave_source::{WaveSource, string_to_wavesource},
19    };
20    use tracing::error;
21
22    #[derive(clap::Subcommand)]
23    enum Commands {
24        #[cfg(not(target_arch = "wasm32"))]
25        /// starts surfer in headless mode so that a user can connect to it
26        Server {
27            /// port on which server will listen
28            #[clap(long)]
29            port: Option<u16>,
30            /// IP address to bind the server to
31            #[clap(long)]
32            bind_address: Option<String>,
33            /// token used by the client to authenticate to the server
34            #[clap(long)]
35            token: Option<String>,
36            /// waveform file that we want to serve
37            #[arg(long)]
38            file: String,
39        },
40    }
41
42    #[derive(clap::Parser, Default)]
43    #[command(version = concat!(env!("CARGO_PKG_VERSION"), " (git: ", env!("VERGEN_GIT_DESCRIBE"), ")"), about)]
44    struct Args {
45        /// Waveform file in VCD, FST, or GHW format.
46        wave_file: Option<String>,
47        /// Path to a file containing 'commands' to run after a waveform has been loaded.
48        /// The commands are the same as those used in the command line interface inside the program.
49        /// Commands are separated by lines or ;. Empty lines are ignored. Line comments starting with
50        /// `#` are supported
51        /// NOTE: This feature is not permanent, it will be removed once a solid scripting system
52        /// is implemented.
53        #[clap(long, short, verbatim_doc_comment)]
54        command_file: Option<Utf8PathBuf>,
55        /// Alias for --command_file to support VUnit
56        #[clap(long)]
57        script: Option<Utf8PathBuf>,
58
59        #[clap(long, short)]
60        /// Load previously saved state file
61        state_file: Option<Utf8PathBuf>,
62
63        #[clap(long, action)]
64        /// Port for WCP to connect to
65        wcp_initiate: Option<u16>,
66
67        #[command(subcommand)]
68        command: Option<Commands>,
69    }
70
71    impl Args {
72        pub fn command_file(&self) -> Option<&Utf8PathBuf> {
73            match (&self.command_file, &self.script) {
74                (Some(_), Some(_)) => {
75                    error!("At most one of --command_file and --script can be used");
76                    None
77                }
78                (Some(cf), None) => Some(cf),
79                (None, Some(sc)) => Some(sc),
80                (None, None) => None,
81            }
82        }
83    }
84
85    #[cfg(test)]
86    mod tests {
87        use super::*;
88
89        #[test]
90        fn command_file_prefers_single_sources() {
91            // Only --command_file
92            let args = Args::parse_from(["surfer", "--command-file", "C:/tmp/cmds.sucl"]);
93            let cf = args.command_file().unwrap();
94            assert!(cf.ends_with("cmds.sucl"));
95
96            // Only --script
97            let args = Args::parse_from(["surfer", "--script", "C:/tmp/scr.sucl"]);
98            let cf = args.command_file().unwrap();
99            assert!(cf.ends_with("scr.sucl"));
100        }
101
102        #[test]
103        fn command_file_conflict_returns_none() {
104            let args = Args::parse_from([
105                "surfer",
106                "--command-file",
107                "C:/tmp/cmds.sucl",
108                "--script",
109                "C:/tmp/scr.sucl",
110            ]);
111            assert!(args.command_file().is_none());
112        }
113    }
114
115    #[allow(dead_code)] // NOTE: Only used in desktop version
116    fn startup_params_from_args(args: Args) -> StartupParams {
117        let startup_commands = if let Some(cmd_file) = args.command_file() {
118            read_command_file(cmd_file)
119        } else {
120            vec![]
121        };
122        StartupParams {
123            waves: args.wave_file.map(|s| string_to_wavesource(&s)),
124            wcp_initiate: args.wcp_initiate,
125            startup_commands,
126        }
127    }
128
129    #[cfg(not(target_arch = "wasm32"))]
130    pub(crate) fn main() -> Result<()> {
131        use libsurfer::state::UserState;
132        #[cfg(feature = "wasm_plugins")]
133        use libsurfer::translation::wasm_translator::discover_wasm_translators;
134        simple_eyre::install()?;
135
136        logs::start_logging()?;
137
138        // https://tokio.rs/tokio/topics/bridging
139        // We want to run the gui in the main thread, but some long running tasks like
140        // loading VCDs should be done asynchronously. We can't just use std::thread to
141        // do that due to wasm support, so we'll start a tokio runtime
142        let runtime = tokio::runtime::Builder::new_current_thread()
143            .worker_threads(1)
144            .enable_all()
145            .build()
146            .unwrap();
147
148        // parse arguments
149        let args = Args::parse();
150        #[cfg(not(target_arch = "wasm32"))]
151        if let Some(Commands::Server {
152            port,
153            bind_address,
154            token,
155            file,
156        }) = args.command
157        {
158            let config = SystemState::new()?.user.config;
159
160            // Use CLI override if provided, otherwise use config setting
161            let bind_addr = bind_address.unwrap_or(config.server.bind_address);
162            let port = port.unwrap_or(config.server.port);
163
164            let res = runtime.block_on(surver::server_main(port, bind_addr, token, file, None));
165            return res;
166        }
167
168        let _enter = runtime.enter();
169
170        std::thread::spawn(move || {
171            runtime.block_on(async {
172                loop {
173                    tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await;
174                }
175            });
176        });
177
178        let state_file = args.state_file.clone();
179        let startup_params = startup_params_from_args(args);
180        let waves = startup_params.waves.clone();
181
182        let state = match &state_file {
183            Some(file) => std::fs::read_to_string(file)
184                .with_context(|| format!("Failed to read state from {file}"))
185                .and_then(|content| {
186                    ron::from_str::<UserState>(&content)
187                        .with_context(|| format!("Failed to decode state from {file}"))
188                })
189                .map(SystemState::from)
190                .map(|mut s| {
191                    s.user.state_file = Some(file.into());
192                    s
193                })
194                .or_else(|e| {
195                    error!("Failed to read state file. Opening fresh session\n{e:#?}");
196                    SystemState::new()
197                })?,
198            None => SystemState::new()?,
199        }
200        .with_params(startup_params);
201
202        #[cfg(feature = "wasm_plugins")]
203        {
204            // Not using batch commands here as we want to start processing wasm plugins
205            // as soon as we start up, no need to wait for the waveform to load
206            let sender = state.channels.msg_sender.clone();
207            for message in discover_wasm_translators() {
208                if let Err(e) = sender.send(message) {
209                    error!("Failed to send message: {e}");
210                }
211            }
212        }
213        // install a file watcher that emits a `SuggestReloadWaveform` message
214        // whenever the user-provided file changes.
215        let _watcher = match waves {
216            Some(WaveSource::File(path)) => {
217                let sender = state.channels.msg_sender.clone();
218                FileWatcher::new(&path, move || {
219                    if let Err(e) = sender.send(Message::SuggestReloadWaveform) {
220                        error!("Message ReloadWaveform did not send:\n{e}")
221                    }
222                })
223                .inspect_err(|err| error!("Cannot set up the file watcher:\n{err}"))
224                .ok()
225            }
226            _ => None,
227        };
228        let icon = image::load_from_memory_with_format(
229            include_bytes!("../assets/com.gitlab.surferproject.surfer.png"),
230            image::ImageFormat::Png,
231        )
232        .expect("Failed to open icon path")
233        .to_rgba8();
234        let (icon_width, icon_height) = icon.dimensions();
235        let options = eframe::NativeOptions {
236            viewport: egui::ViewportBuilder::default()
237                .with_app_id("org.surfer-project.surfer")
238                .with_title("Surfer")
239                .with_icon(egui::viewport::IconData {
240                    rgba: icon.into_raw(),
241                    width: icon_width,
242                    height: icon_height,
243                })
244                .with_inner_size(Vec2::new(
245                    state.user.config.layout.window_width as f32,
246                    state.user.config.layout.window_height as f32,
247                )),
248            ..Default::default()
249        };
250
251        eframe::run_native("Surfer", options, Box::new(|cc| Ok(run_egui(cc, state)?))).unwrap();
252
253        Ok(())
254    }
255}
256
257#[cfg(target_arch = "wasm32")]
258mod main_impl {
259    use eframe::wasm_bindgen::JsCast;
260    use eframe::web_sys;
261    use libsurfer::wasm_api::WebHandle;
262
263    // Calling main is not the intended way to start surfer, instead, it should be
264    // started by `wasm_api::WebHandle`
265    pub(crate) fn main() -> eyre::Result<()> {
266        simple_eyre::install()?;
267        let document = web_sys::window()
268            .expect("No window")
269            .document()
270            .expect("No document");
271        let canvas = document
272            .get_element_by_id("the_canvas_id")
273            .expect("Failed to find the_canvas_id")
274            .dyn_into::<web_sys::HtmlCanvasElement>()
275            .expect("the_canvas_id was not a HtmlCanvasElement");
276
277        wasm_bindgen_futures::spawn_local(async {
278            let wh = WebHandle::new();
279            wh.start(canvas).await.expect("Failed to start surfer");
280        });
281
282        Ok(())
283    }
284}
285
286fn main() -> eyre::Result<()> {
287    main_impl::main()
288}