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::Result;
10 use eyre::WrapErr as _;
11 use libsurfer::{
12 EGUI_CONTEXT, 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 Server {
27 #[clap(long)]
29 port: Option<u16>,
30 #[clap(long)]
32 bind_address: Option<String>,
33 #[clap(long)]
35 token: Option<String>,
36 #[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 wave_file: Option<String>,
47 #[clap(long, short, verbatim_doc_comment)]
54 command_file: Option<Utf8PathBuf>,
55 #[clap(long)]
57 script: Option<Utf8PathBuf>,
58 #[clap(long = "command", short = 'C', verbatim_doc_comment)]
62 command_string: Option<String>,
63 #[clap(long, short)]
64 state_file: Option<Utf8PathBuf>,
66
67 #[clap(long, action)]
68 wcp_initiate: Option<u16>,
70
71 #[command(subcommand)]
72 command: Option<Commands>,
73 }
74
75 impl Args {
76 pub fn command_file(&self) -> Option<&Utf8PathBuf> {
77 match (&self.command_file, &self.script) {
78 (Some(_), Some(_)) => {
79 error!("At most one of --command_file and --script can be used");
80 None
81 }
82 (Some(cf), None) => Some(cf),
83 (None, Some(sc)) => Some(sc),
84 (None, None) => None,
85 }
86 }
87 }
88
89 #[allow(dead_code)] fn startup_params_from_args(args: Args) -> StartupParams {
91 let mut startup_commands = Vec::new();
92 if let Some(command_string) = &args.command_string {
93 startup_commands.push(command_string.clone());
94 }
95 startup_commands.extend(
96 args.command_file()
97 .map(read_command_file)
98 .unwrap_or_default(),
99 );
100 StartupParams {
101 waves: args.wave_file.map(|s| string_to_wavesource(&s)),
102 wcp_initiate: args.wcp_initiate,
103 startup_commands,
104 }
105 }
106
107 #[cfg(not(target_arch = "wasm32"))]
108 pub(crate) fn main() -> Result<()> {
109 use egui::Pos2;
110 use libsurfer::state::UserState;
111 #[cfg(feature = "wasm_plugins")]
112 use libsurfer::translation::wasm_translator::discover_wasm_translators;
113 simple_eyre::install()?;
114
115 logs::start_logging()?;
116
117 std::panic::set_hook(Box::new(panic_handler));
118
119 let runtime = tokio::runtime::Builder::new_current_thread()
124 .worker_threads(1)
125 .enable_all()
126 .build()
127 .unwrap();
128
129 let args = Args::parse();
131 #[cfg(not(target_arch = "wasm32"))]
132 if let Some(Commands::Server {
133 port,
134 bind_address,
135 token,
136 file,
137 }) = args.command
138 {
139 let config = SystemState::new()?.user.config;
140
141 let bind_addr = bind_address.unwrap_or(config.server.bind_address);
143 let port = port.unwrap_or(config.server.port);
144
145 let res = runtime.block_on(surver::surver_main(port, bind_addr, token, &[file], None));
146 return res;
147 }
148
149 let _enter = runtime.enter();
150
151 std::thread::spawn(move || {
152 runtime.block_on(async {
153 loop {
154 tokio::time::sleep(tokio::time::Duration::from_hours(1)).await;
155 }
156 });
157 });
158
159 let state_file = args.state_file.clone();
160 let startup_params = startup_params_from_args(args);
161 let waves = startup_params.waves.clone();
162 let window_title = waves
163 .as_ref()
164 .map_or_else(|| "Surfer".to_string(), WaveSource::window_title);
165
166 let state = match &state_file {
167 Some(file) => std::fs::read_to_string(file)
168 .with_context(|| format!("Failed to read state from {file}"))
169 .and_then(|content| {
170 ron::from_str::<UserState>(&content)
171 .with_context(|| format!("Failed to decode state from {file}"))
172 })
173 .map(SystemState::from)
174 .map(|mut s| {
175 s.user.state_file = Some(file.into());
176 s
177 })
178 .or_else(|e| {
179 error!("Failed to read state file. Opening fresh session\n{e:#?}");
180 SystemState::new()
181 })?,
182 None => SystemState::new()?,
183 }
184 .with_params(startup_params);
185
186 #[cfg(feature = "wasm_plugins")]
187 {
188 let sender = state.channels.msg_sender.clone();
191 for message in discover_wasm_translators() {
192 if let Err(e) = sender.send(message) {
193 error!("Failed to send message: {e}");
194 }
195 }
196 }
197 let _watcher = match waves {
200 Some(WaveSource::File(path)) => {
201 let sender = state.channels.msg_sender.clone();
202 FileWatcher::new(&path, move || {
203 if let Err(e) = sender.send(Message::SuggestReloadWaveform) {
204 error!("Message ReloadWaveform did not send:\n{e}");
205 }
206 if let Some(ctx) = EGUI_CONTEXT.read().unwrap().as_ref() {
209 ctx.request_repaint();
210 }
211 })
212 .inspect_err(|err| error!("Cannot set up the file watcher:\n{err}"))
213 .ok()
214 }
215 _ => None,
216 };
217
218 let icon_bytes = include_bytes!("../assets/com.gitlab.surferproject.surfer.png");
220 let decoder = png::Decoder::new(std::io::Cursor::new(&icon_bytes[..]));
221 let mut reader = decoder.read_info().expect("Failed to read PNG info");
222 let mut icon_data = vec![
223 0;
224 reader
225 .output_buffer_size()
226 .expect("Failed to calculate PNG buffer size")
227 ];
228 let info = reader
229 .next_frame(&mut icon_data)
230 .expect("Failed to decode PNG");
231
232 let options = eframe::NativeOptions {
233 viewport: egui::ViewportBuilder::default()
234 .with_app_id("org.surfer-project.surfer")
235 .with_title(window_title)
236 .with_icon(egui::viewport::IconData {
237 rgba: icon_data,
238 width: info.width,
239 height: info.height,
240 })
241 .with_inner_size(Vec2::new(
242 state.user.config.layout.window_width as f32,
243 state.user.config.layout.window_height as f32,
244 ))
245 .with_position(Pos2::new(
246 state.user.config.layout.window_x_position as f32,
247 state.user.config.layout.window_y_position as f32,
248 )),
249 ..Default::default()
250 };
251
252 eframe::run_native("Surfer", options, Box::new(|cc| Ok(run_egui(cc, state)?))).unwrap();
253
254 Ok(())
255 }
256
257 fn panic_handler(info: &std::panic::PanicHookInfo) {
258 let backtrace = std::backtrace::Backtrace::force_capture();
259
260 eprintln!();
261 eprintln!("Surfer crashed due to a panic 😞");
262 eprintln!("Please report this issue at https://gitlab.com/surfer-project/surfer/-/issues");
263 eprintln!();
264 eprintln!("Some notes on reports:");
265 eprintln!(
266 "We are happy about any reports, but it makes it much easier for us to fix issues if you:",
267 );
268 eprintln!(" - Include the information below");
269 eprintln!(" - Try to reproduce the issue to give us steps on how to reproduce the issue");
270 eprintln!(" - Include (minimal) waveform file and state file you used");
271 eprintln!(" (you can upload those confidentially, for the surfer team only)");
272 eprintln!();
273
274 let location = info.location().unwrap();
275 let msg = if let Some(msg) = info.payload().downcast_ref::<&str>() {
276 (*msg).to_string()
277 } else if let Some(msg) = info.payload().downcast_ref::<String>() {
278 msg.clone()
279 } else {
280 "<panic message not a string>".to_owned()
281 };
282
283 eprintln!(
284 "Surfer version: {} (git: {})",
285 env!("CARGO_PKG_VERSION"),
286 env!("VERGEN_GIT_DESCRIBE"),
287 );
288 eprintln!(
289 "thread '{}' ({:?}) panicked at {}:{}:{:?}",
290 std::thread::current().name().unwrap_or("unknown"),
291 std::thread::current().id(),
292 location.file(),
293 location.line(),
294 location.column(),
295 );
296 eprintln!(" {msg}");
297 eprintln!();
298 eprintln!("backtrace:");
299 eprintln!("{backtrace}");
300 }
301
302 #[cfg(test)]
303 mod tests {
304 use super::*;
305
306 #[test]
307 fn command_file_prefers_single_sources() {
308 let args = Args::parse_from(["surfer", "--command-file", "C:/tmp/cmds.sucl"]);
310 let cf = args.command_file().unwrap();
311 assert!(cf.ends_with("cmds.sucl"));
312
313 let args = Args::parse_from(["surfer", "--script", "C:/tmp/scr.sucl"]);
315 let cf = args.command_file().unwrap();
316 assert!(cf.ends_with("scr.sucl"));
317 }
318
319 #[test]
320 fn command_file_conflict_returns_none() {
321 let args = Args::parse_from([
322 "surfer",
323 "--command-file",
324 "C:/tmp/cmds.sucl",
325 "--script",
326 "C:/tmp/scr.sucl",
327 ]);
328 assert!(args.command_file().is_none());
329 }
330 }
331}
332
333#[cfg(target_arch = "wasm32")]
334mod main_impl {
335 use libsurfer::logs;
336 use libsurfer::wasm_api::WebHandle;
337 use wasm_bindgen::JsCast;
338
339 pub(crate) fn main() -> eyre::Result<()> {
342 simple_eyre::install()?;
343
344 logs::start_logging()?;
345
346 let document = web_sys::window()
347 .expect("No window")
348 .document()
349 .expect("No document");
350 let canvas = document
351 .get_element_by_id("the_canvas_id")
352 .expect("Failed to find the_canvas_id")
353 .dyn_into::<web_sys::HtmlCanvasElement>()
354 .expect("the_canvas_id was not a HtmlCanvasElement");
355
356 wasm_bindgen_futures::spawn_local(async {
357 let wh = WebHandle::new();
358 wh.start(canvas).await.expect("Failed to start surfer");
359 });
360
361 Ok(())
362 }
363}
364
365fn main() -> eyre::Result<()> {
366 main_impl::main()
367}