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