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