1#![cfg_attr(not(target_arch = "wasm32"), deny(unused_crate_dependencies))]
2
3#[cfg(not(target_arch = "wasm32"))]
4mod main_impl {
5 use camino::Utf8PathBuf;
6 use clap::Parser;
7 use egui::Vec2;
8 use eyre::Context;
9 use eyre::Result;
10 use libsurfer::{
11 batch_commands::read_command_file,
12 file_watcher::FileWatcher,
13 logs,
14 message::Message,
15 run_egui,
16 wave_source::{string_to_wavesource, WaveSource},
17 StartupParams, SystemState,
18 };
19 use log::error;
20
21 #[derive(clap::Subcommand)]
22 enum Commands {
23 #[cfg(not(target_arch = "wasm32"))]
24 Server {
26 #[clap(long)]
28 port: Option<u16>,
29 #[clap(long)]
31 bind_address: Option<String>,
32 #[clap(long)]
34 token: Option<String>,
35 #[arg(long)]
37 file: String,
38 },
39 }
40
41 #[derive(clap::Parser, Default)]
42 #[command(version = concat!(env!("CARGO_PKG_VERSION"), " (git: ", env!("VERGEN_GIT_DESCRIBE"), ")"), about)]
43 struct Args {
44 wave_file: Option<String>,
46 #[clap(long, short, verbatim_doc_comment)]
53 command_file: Option<Utf8PathBuf>,
54 #[clap(long)]
56 script: Option<Utf8PathBuf>,
57
58 #[clap(long, short)]
59 state_file: Option<Utf8PathBuf>,
61
62 #[clap(long, action)]
63 wcp_initiate: Option<u16>,
65
66 #[command(subcommand)]
67 command: Option<Commands>,
68 }
69
70 impl Args {
71 pub fn command_file(&self) -> Option<&Utf8PathBuf> {
72 match (&self.command_file, &self.script) {
73 (Some(_), Some(_)) => {
74 error!("At most one of --command_file and --script can be used");
75 None
76 }
77 (Some(cf), None) => Some(cf),
78 (None, Some(sc)) => Some(sc),
79 (None, None) => None,
80 }
81 }
82 }
83
84 #[cfg(test)]
85 mod tests {
86 use super::*;
87
88 #[test]
89 fn command_file_prefers_single_sources() {
90 let args = Args::parse_from(["surfer", "--command-file", "C:/tmp/cmds.sucl"]);
92 let cf = args.command_file().unwrap();
93 assert!(cf.ends_with("cmds.sucl"));
94
95 let args = Args::parse_from(["surfer", "--script", "C:/tmp/scr.sucl"]);
97 let cf = args.command_file().unwrap();
98 assert!(cf.ends_with("scr.sucl"));
99 }
100
101 #[test]
102 fn command_file_conflict_returns_none() {
103 let args = Args::parse_from([
104 "surfer",
105 "--command-file",
106 "C:/tmp/cmds.sucl",
107 "--script",
108 "C:/tmp/scr.sucl",
109 ]);
110 assert!(args.command_file().is_none());
111 }
112 }
113
114 #[allow(dead_code)] fn startup_params_from_args(args: Args) -> StartupParams {
116 let startup_commands = if let Some(cmd_file) = args.command_file() {
117 read_command_file(cmd_file)
118 } else {
119 vec![]
120 };
121 StartupParams {
122 waves: args.wave_file.map(|s| string_to_wavesource(&s)),
123 wcp_initiate: args.wcp_initiate,
124 startup_commands,
125 }
126 }
127
128 #[cfg(not(target_arch = "wasm32"))]
129 pub(crate) fn main() -> Result<()> {
130 use libsurfer::state::UserState;
131 #[cfg(feature = "wasm_plugins")]
132 use libsurfer::translation::wasm_translator::discover_wasm_translators;
133 simple_eyre::install()?;
134
135 logs::start_logging()?;
136
137 let runtime = tokio::runtime::Builder::new_current_thread()
142 .worker_threads(1)
143 .enable_all()
144 .build()
145 .unwrap();
146
147 let args = Args::parse();
149 #[cfg(not(target_arch = "wasm32"))]
150 if let Some(Commands::Server {
151 port,
152 bind_address,
153 token,
154 file,
155 }) = args.command
156 {
157 let config = SystemState::new()?.user.config;
158
159 let bind_addr = bind_address.unwrap_or(config.server.bind_address);
161 let port = port.unwrap_or(config.server.port);
162
163 let res = runtime.block_on(surver::server_main(port, bind_addr, token, file, None));
164 return res;
165 }
166
167 let _enter = runtime.enter();
168
169 std::thread::spawn(move || {
170 runtime.block_on(async {
171 loop {
172 tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await;
173 }
174 });
175 });
176
177 let state_file = args.state_file.clone();
178 let startup_params = startup_params_from_args(args);
179 let waves = startup_params.waves.clone();
180
181 let state = match &state_file {
182 Some(file) => std::fs::read_to_string(file)
183 .with_context(|| format!("Failed to read state from {file}"))
184 .and_then(|content| {
185 ron::from_str::<UserState>(&content)
186 .with_context(|| format!("Failed to decode state from {file}"))
187 })
188 .map(SystemState::from)
189 .map(|mut s| {
190 s.user.state_file = Some(file.into());
191 s
192 })
193 .or_else(|e| {
194 error!("Failed to read state file. Opening fresh session\n{e:#?}");
195 SystemState::new()
196 })?,
197 None => SystemState::new()?,
198 }
199 .with_params(startup_params);
200
201 #[cfg(feature = "wasm_plugins")]
202 {
203 let sender = state.channels.msg_sender.clone();
206 for message in discover_wasm_translators() {
207 if let Err(e) = sender.send(message) {
208 error!("Failed to send message: {e}");
209 }
210 }
211 }
212 let _watcher = match waves {
215 Some(WaveSource::File(path)) => {
216 let sender = state.channels.msg_sender.clone();
217 FileWatcher::new(&path, move || {
218 if let Err(err) = sender.send(Message::SuggestReloadWaveform) {
219 error!("Message ReloadWaveform did not send:\n{err}")
220 }
221 })
222 .inspect_err(|err| error!("Cannot set up the file watcher:\n{err}"))
223 .ok()
224 }
225 _ => None,
226 };
227
228 let options = eframe::NativeOptions {
229 viewport: egui::ViewportBuilder::default()
230 .with_app_id("org.surfer-project.surfer")
231 .with_title("Surfer")
232 .with_inner_size(Vec2::new(
233 state.user.config.layout.window_width as f32,
234 state.user.config.layout.window_height as f32,
235 )),
236 ..Default::default()
237 };
238
239 eframe::run_native("Surfer", options, Box::new(|cc| Ok(run_egui(cc, state)?))).unwrap();
240
241 Ok(())
242 }
243}
244
245#[cfg(target_arch = "wasm32")]
246mod main_impl {
247 use eframe::wasm_bindgen::JsCast;
248 use eframe::web_sys;
249 use libsurfer::wasm_api::WebHandle;
250
251 pub(crate) fn main() -> eyre::Result<()> {
254 simple_eyre::install()?;
255 let document = web_sys::window()
256 .expect("No window")
257 .document()
258 .expect("No document");
259 let canvas = document
260 .get_element_by_id("the_canvas_id")
261 .expect("Failed to find the_canvas_id")
262 .dyn_into::<web_sys::HtmlCanvasElement>()
263 .expect("the_canvas_id was not a HtmlCanvasElement");
264
265 wasm_bindgen_futures::spawn_local(async {
266 let wh = WebHandle::new();
267 wh.start(canvas).await.expect("Failed to start surfer");
268 });
269
270 Ok(())
271 }
272}
273
274fn main() -> eyre::Result<()> {
275 main_impl::main()
276}