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 #[cfg(test)]
86 mod tests {
87 use super::*;
88
89 #[test]
90 fn command_file_prefers_single_sources() {
91 let args = Args::parse_from(["surfer", "--command-file", "C:/tmp/cmds.sucl"]);
93 let cf = args.command_file().unwrap();
94 assert!(cf.ends_with("cmds.sucl"));
95
96 let args = Args::parse_from(["surfer", "--script", "C:/tmp/scr.sucl"]);
98 let cf = args.command_file().unwrap();
99 assert!(cf.ends_with("scr.sucl"));
100 }
101
102 #[test]
103 fn command_file_conflict_returns_none() {
104 let args = Args::parse_from([
105 "surfer",
106 "--command-file",
107 "C:/tmp/cmds.sucl",
108 "--script",
109 "C:/tmp/scr.sucl",
110 ]);
111 assert!(args.command_file().is_none());
112 }
113 }
114
115 #[allow(dead_code)] fn startup_params_from_args(args: Args) -> StartupParams {
117 let startup_commands = if let Some(cmd_file) = args.command_file() {
118 read_command_file(cmd_file)
119 } else {
120 vec![]
121 };
122 StartupParams {
123 waves: args.wave_file.map(|s| string_to_wavesource(&s)),
124 wcp_initiate: args.wcp_initiate,
125 startup_commands,
126 }
127 }
128
129 #[cfg(not(target_arch = "wasm32"))]
130 pub(crate) fn main() -> Result<()> {
131 use libsurfer::state::UserState;
132 #[cfg(feature = "wasm_plugins")]
133 use libsurfer::translation::wasm_translator::discover_wasm_translators;
134 simple_eyre::install()?;
135
136 logs::start_logging()?;
137
138 let runtime = tokio::runtime::Builder::new_current_thread()
143 .worker_threads(1)
144 .enable_all()
145 .build()
146 .unwrap();
147
148 let args = Args::parse();
150 #[cfg(not(target_arch = "wasm32"))]
151 if let Some(Commands::Server {
152 port,
153 bind_address,
154 token,
155 file,
156 }) = args.command
157 {
158 let config = SystemState::new()?.user.config;
159
160 let bind_addr = bind_address.unwrap_or(config.server.bind_address);
162 let port = port.unwrap_or(config.server.port);
163
164 let res = runtime.block_on(surver::server_main(port, bind_addr, token, file, None));
165 return res;
166 }
167
168 let _enter = runtime.enter();
169
170 std::thread::spawn(move || {
171 runtime.block_on(async {
172 loop {
173 tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await;
174 }
175 });
176 });
177
178 let state_file = args.state_file.clone();
179 let startup_params = startup_params_from_args(args);
180 let waves = startup_params.waves.clone();
181
182 let state = match &state_file {
183 Some(file) => std::fs::read_to_string(file)
184 .with_context(|| format!("Failed to read state from {file}"))
185 .and_then(|content| {
186 ron::from_str::<UserState>(&content)
187 .with_context(|| format!("Failed to decode state from {file}"))
188 })
189 .map(SystemState::from)
190 .map(|mut s| {
191 s.user.state_file = Some(file.into());
192 s
193 })
194 .or_else(|e| {
195 error!("Failed to read state file. Opening fresh session\n{e:#?}");
196 SystemState::new()
197 })?,
198 None => SystemState::new()?,
199 }
200 .with_params(startup_params);
201
202 #[cfg(feature = "wasm_plugins")]
203 {
204 let sender = state.channels.msg_sender.clone();
207 for message in discover_wasm_translators() {
208 if let Err(e) = sender.send(message) {
209 error!("Failed to send message: {e}");
210 }
211 }
212 }
213 let _watcher = match waves {
216 Some(WaveSource::File(path)) => {
217 let sender = state.channels.msg_sender.clone();
218 FileWatcher::new(&path, move || {
219 if let Err(e) = sender.send(Message::SuggestReloadWaveform) {
220 error!("Message ReloadWaveform did not send:\n{e}")
221 }
222 })
223 .inspect_err(|err| error!("Cannot set up the file watcher:\n{err}"))
224 .ok()
225 }
226 _ => None,
227 };
228 let icon = image::load_from_memory_with_format(
229 include_bytes!("../assets/com.gitlab.surferproject.surfer.png"),
230 image::ImageFormat::Png,
231 )
232 .expect("Failed to open icon path")
233 .to_rgba8();
234 let (icon_width, icon_height) = icon.dimensions();
235 let options = eframe::NativeOptions {
236 viewport: egui::ViewportBuilder::default()
237 .with_app_id("org.surfer-project.surfer")
238 .with_title("Surfer")
239 .with_icon(egui::viewport::IconData {
240 rgba: icon.into_raw(),
241 width: icon_width,
242 height: icon_height,
243 })
244 .with_inner_size(Vec2::new(
245 state.user.config.layout.window_width as f32,
246 state.user.config.layout.window_height as f32,
247 )),
248 ..Default::default()
249 };
250
251 eframe::run_native("Surfer", options, Box::new(|cc| Ok(run_egui(cc, state)?))).unwrap();
252
253 Ok(())
254 }
255}
256
257#[cfg(target_arch = "wasm32")]
258mod main_impl {
259 use eframe::wasm_bindgen::JsCast;
260 use eframe::web_sys;
261 use libsurfer::wasm_api::WebHandle;
262
263 pub(crate) fn main() -> eyre::Result<()> {
266 simple_eyre::install()?;
267 let document = web_sys::window()
268 .expect("No window")
269 .document()
270 .expect("No document");
271 let canvas = document
272 .get_element_by_id("the_canvas_id")
273 .expect("Failed to find the_canvas_id")
274 .dyn_into::<web_sys::HtmlCanvasElement>()
275 .expect("the_canvas_id was not a HtmlCanvasElement");
276
277 wasm_bindgen_futures::spawn_local(async {
278 let wh = WebHandle::new();
279 wh.start(canvas).await.expect("Failed to start surfer");
280 });
281
282 Ok(())
283 }
284}
285
286fn main() -> eyre::Result<()> {
287 main_impl::main()
288}