1pub mod app;
9pub mod notifier;
10pub mod tab;
11pub mod tabs;
12pub mod tray;
13pub mod tray_host;
14
15use std::sync::{atomic::AtomicBool, Arc};
16use std::time::Duration;
17
18use anyhow::{anyhow, Result};
19use parking_lot::Mutex;
20
21use crate::{
22 auto_register::{self, RegistrationState},
23 config,
24 runtime::{self, LoopSchedule, WorkerObservers},
25 types::LogEntry,
26};
27
28pub fn run(config_path: Option<&str>) -> Result<()> {
32 let (cfg, path) = config::load(config_path)?;
33 runtime::log_startup_banner(&cfg, &path);
34
35 sync_autostart_on_launch(cfg.auto_start);
39
40 let cfg = config::shared(cfg);
41 let stop = Arc::new(AtomicBool::new(false));
42 let busy = Arc::new(AtomicBool::new(false));
43 let paused = Arc::new(AtomicBool::new(false));
46 let logs: Arc<Mutex<Vec<LogEntry>>> = Arc::new(Mutex::new(Vec::new()));
47 let observers = WorkerObservers::default();
48
49 let registration = auto_register::shared_initial();
50
51 let handle = tokio::runtime::Handle::current();
56
57 let cfg_autoreg = cfg.clone();
60 let path_autoreg = path.clone();
61 let registration_autoreg = registration.clone();
62 let stop_autoreg = stop.clone();
63 handle.spawn(async move {
64 loop {
65 if stop_autoreg.load(std::sync::atomic::Ordering::SeqCst) {
66 return;
67 }
68 let state =
69 auto_register::tick(&cfg_autoreg, &path_autoreg, ®istration_autoreg).await;
70 if matches!(
71 state,
72 RegistrationState::Approved | RegistrationState::Rejected { .. }
73 ) {
74 return;
75 }
76 for _ in 0..30 {
77 if stop_autoreg.load(std::sync::atomic::Ordering::SeqCst) {
78 return;
79 }
80 tokio::time::sleep(Duration::from_secs(1)).await;
81 }
82 }
83 });
84
85 let local_api = runtime::spawn_local_api(cfg.clone(), observers.clone(), stop.clone());
87
88 let cfg_loops = cfg.clone();
89 let stop_loops = stop.clone();
90 let logs_loops = logs.clone();
91 let busy_loops = busy.clone();
92 let paused_loops = paused.clone();
93 let observers_loops = observers.clone();
94 handle.spawn(async move {
95 if let Err(e) = runtime::run_loops(
96 cfg_loops,
97 stop_loops,
98 logs_loops,
99 busy_loops,
100 paused_loops,
101 observers_loops,
102 LoopSchedule::default(),
103 )
104 .await
105 {
106 tracing::error!(target: "studio_worker::ui", error = %e, "run_loops exited");
107 }
108 });
109
110 let app_state = app::AppDeps {
111 cfg: cfg.clone(),
112 logs: logs.clone(),
113 busy: busy.clone(),
114 paused: paused.clone(),
115 observers: observers.clone(),
116 stop: stop.clone(),
117 config_path: path,
118 tokio: handle.clone(),
119 };
120
121 let mut viewport = eframe::egui::ViewportBuilder::default()
125 .with_inner_size([960.0, 720.0])
126 .with_min_inner_size([640.0, 480.0])
127 .with_title("studio-worker");
128 if let Some([x, y]) =
131 dev_window_position(std::env::var("STUDIO_WORKER_WINDOW_POS").ok().as_deref())
132 {
133 viewport = viewport.with_position([x, y]);
134 }
135 let native_options = eframe::NativeOptions {
136 viewport,
137 ..Default::default()
138 };
139
140 let initial_paused = paused.load(std::sync::atomic::Ordering::SeqCst);
144 let tokio_for_tray = handle.clone();
147
148 eframe::run_native(
149 "studio-worker",
150 native_options,
151 Box::new(move |cc| {
152 cc.egui_ctx.set_visuals(eframe::egui::Visuals::dark());
154 let app = app::App::with_notifier_and_registration(
155 app_state,
156 app::App::default_notifier_box(),
157 registration,
158 );
159 let quit_handle = app.quit_requested_handle();
160
161 let tray_handle = tray_host::install(
166 cc.egui_ctx.clone(),
167 paused.clone(),
168 quit_handle,
169 tokio_for_tray,
170 initial_paused,
171 );
172 let mut app = app;
175 if let Some(tray) = tray_handle {
176 app.attach_tray(tray);
177 }
178 Ok(Box::new(app))
179 }),
180 )
181 .map_err(|e| anyhow!("eframe: {e}"))?;
182
183 stop.store(true, std::sync::atomic::Ordering::SeqCst);
185 if let Some(handle) = local_api {
186 let _ = handle.join();
187 }
188 Ok(())
189}
190
191fn sync_autostart_on_launch(auto_start: bool) {
196 use crate::autostart::{self, AutostartSync};
197 match autostart::launch_sync_action(auto_start, autostart::is_enabled()) {
198 AutostartSync::Enable => match std::env::current_exe() {
199 Ok(exe) => {
200 if let Err(e) = autostart::enable(&exe) {
201 tracing::warn!(
202 target: "studio_worker::ui",
203 error = %e,
204 "could not enable autostart-on-login"
205 );
206 }
207 }
208 Err(e) => tracing::warn!(
209 target: "studio_worker::ui",
210 error = %e,
211 "could not resolve current exe to enable autostart-on-login"
212 ),
213 },
214 AutostartSync::Disable => {
215 if let Err(e) = autostart::disable() {
216 tracing::warn!(
217 target: "studio_worker::ui",
218 error = %e,
219 "could not disable stale autostart-on-login"
220 );
221 }
222 }
223 AutostartSync::Noop => {}
224 }
225}
226
227fn dev_window_position(env: Option<&str>) -> Option<[f32; 2]> {
234 if let Some(raw) = env {
235 let mut parts = raw.split(',').map(str::trim);
236 if let (Some(x), Some(y), None) = (parts.next(), parts.next(), parts.next()) {
237 if let (Ok(x), Ok(y)) = (x.parse::<f32>(), y.parse::<f32>()) {
238 return Some([x, y]);
239 }
240 }
241 return None;
242 }
243 #[cfg(debug_assertions)]
247 let default = Some([48.0, 48.0]);
248 #[cfg(not(debug_assertions))]
249 let default = None;
250 default
251}
252
253#[cfg(test)]
254mod tests {
255 use super::dev_window_position;
256
257 #[test]
258 fn parses_explicit_position_override() {
259 assert_eq!(dev_window_position(Some("100,200")), Some([100.0, 200.0]));
260 }
261
262 #[test]
263 fn trims_whitespace_around_coords() {
264 assert_eq!(dev_window_position(Some(" 10 , 20 ")), Some([10.0, 20.0]));
265 }
266
267 #[test]
268 fn rejects_malformed_override() {
269 assert_eq!(dev_window_position(Some("not-a-pos")), None);
270 assert_eq!(dev_window_position(Some("1,2,3")), None);
271 assert_eq!(dev_window_position(Some("1")), None);
272 }
273
274 #[cfg(debug_assertions)]
275 #[test]
276 fn defaults_to_left_screen_in_debug() {
277 assert_eq!(dev_window_position(None), Some([48.0, 48.0]));
278 }
279
280 #[cfg(not(debug_assertions))]
281 #[test]
282 fn defers_to_wm_in_release() {
283 assert_eq!(dev_window_position(None), None);
284 }
285}