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