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 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 = if let Some(cmd_file) = args.command_file() {
88 read_command_file(cmd_file)
89 } else {
90 vec![]
91 };
92 StartupParams {
93 waves: args.wave_file.map(|s| string_to_wavesource(&s)),
94 wcp_initiate: args.wcp_initiate,
95 startup_commands,
96 }
97 }
98
99 #[cfg(not(target_arch = "wasm32"))]
100 pub(crate) fn main() -> Result<()> {
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_secs(3600)).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 })
195 .inspect_err(|err| error!("Cannot set up the file watcher:\n{err}"))
196 .ok()
197 }
198 _ => None,
199 };
200 let icon = image::load_from_memory_with_format(
201 include_bytes!("../assets/com.gitlab.surferproject.surfer.png"),
202 image::ImageFormat::Png,
203 )
204 .expect("Failed to open icon path")
205 .to_rgba8();
206 let (icon_width, icon_height) = icon.dimensions();
207 let options = eframe::NativeOptions {
208 viewport: egui::ViewportBuilder::default()
209 .with_app_id("org.surfer-project.surfer")
210 .with_title("Surfer")
211 .with_icon(egui::viewport::IconData {
212 rgba: icon.into_raw(),
213 width: icon_width,
214 height: icon_height,
215 })
216 .with_inner_size(Vec2::new(
217 state.user.config.layout.window_width as f32,
218 state.user.config.layout.window_height as f32,
219 )),
220 ..Default::default()
221 };
222
223 eframe::run_native("Surfer", options, Box::new(|cc| Ok(run_egui(cc, state)?))).unwrap();
224
225 Ok(())
226 }
227
228 fn panic_handler(info: &std::panic::PanicHookInfo) {
229 let backtrace = std::backtrace::Backtrace::force_capture();
230
231 eprintln!();
232 eprintln!("Surfer crashed due to a panic 😞");
233 eprintln!("Please report this issue at https://gitlab.com/surfer-project/surfer/-/issues");
234 eprintln!();
235 eprintln!("Some notes on reports:");
236 eprintln!(
237 "We are happy about any reports, but it makes it much easier for us to fix issues if you:",
238 );
239 eprintln!(" - Include the information below");
240 eprintln!(" - Try to reproduce the issue to give us steps on how to reproduce the issue");
241 eprintln!(" - Include (minimal) waveform file and state file you used");
242 eprintln!(" (you can upload those confidentially, for the surfer team only)");
243 eprintln!();
244
245 let location = info.location().unwrap();
246 let msg = if let Some(msg) = info.payload().downcast_ref::<&str>() {
247 msg.to_string()
248 } else if let Some(msg) = info.payload().downcast_ref::<String>() {
249 msg.clone()
250 } else {
251 "<panic message not a string>".to_owned()
252 };
253
254 eprintln!(
255 "Surfer version: {} (git: {})",
256 env!("CARGO_PKG_VERSION"),
257 env!("VERGEN_GIT_DESCRIBE"),
258 );
259 eprintln!(
260 "thread '{}' ({:?}) panicked at {}:{}:{:?}",
261 std::thread::current().name().unwrap_or("unknown"),
262 std::thread::current().id(),
263 location.file(),
264 location.line(),
265 location.column(),
266 );
267 eprintln!(" {}", msg);
268 eprintln!();
269 eprintln!("backtrace:");
270 eprintln!("{}", backtrace);
271 }
272
273 #[cfg(test)]
274 mod tests {
275 use super::*;
276
277 #[test]
278 fn command_file_prefers_single_sources() {
279 let args = Args::parse_from(["surfer", "--command-file", "C:/tmp/cmds.sucl"]);
281 let cf = args.command_file().unwrap();
282 assert!(cf.ends_with("cmds.sucl"));
283
284 let args = Args::parse_from(["surfer", "--script", "C:/tmp/scr.sucl"]);
286 let cf = args.command_file().unwrap();
287 assert!(cf.ends_with("scr.sucl"));
288 }
289
290 #[test]
291 fn command_file_conflict_returns_none() {
292 let args = Args::parse_from([
293 "surfer",
294 "--command-file",
295 "C:/tmp/cmds.sucl",
296 "--script",
297 "C:/tmp/scr.sucl",
298 ]);
299 assert!(args.command_file().is_none());
300 }
301 }
302}
303
304#[cfg(target_arch = "wasm32")]
305mod main_impl {
306 use eframe::wasm_bindgen::JsCast;
307 use eframe::web_sys;
308 use libsurfer::logs;
309 use libsurfer::wasm_api::WebHandle;
310
311 pub(crate) fn main() -> eyre::Result<()> {
314 simple_eyre::install()?;
315
316 logs::start_logging()?;
317
318 let document = web_sys::window()
319 .expect("No window")
320 .document()
321 .expect("No document");
322 let canvas = document
323 .get_element_by_id("the_canvas_id")
324 .expect("Failed to find the_canvas_id")
325 .dyn_into::<web_sys::HtmlCanvasElement>()
326 .expect("the_canvas_id was not a HtmlCanvasElement");
327
328 wasm_bindgen_futures::spawn_local(async {
329 let wh = WebHandle::new();
330 wh.start(canvas).await.expect("Failed to start surfer");
331 });
332
333 Ok(())
334 }
335}
336
337fn main() -> eyre::Result<()> {
338 main_impl::main()
339}