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 cfg_loops = cfg.clone();
86 let stop_loops = stop.clone();
87 let logs_loops = logs.clone();
88 let busy_loops = busy.clone();
89 let paused_loops = paused.clone();
90 let observers_loops = observers.clone();
91 handle.spawn(async move {
92 if let Err(e) = runtime::run_loops(
93 cfg_loops,
94 stop_loops,
95 logs_loops,
96 busy_loops,
97 paused_loops,
98 observers_loops,
99 LoopSchedule::default(),
100 )
101 .await
102 {
103 tracing::error!(target: "studio_worker::ui", error = %e, "run_loops exited");
104 }
105 });
106
107 let app_state = app::AppDeps {
108 cfg: cfg.clone(),
109 logs: logs.clone(),
110 busy: busy.clone(),
111 paused: paused.clone(),
112 observers: observers.clone(),
113 stop: stop.clone(),
114 config_path: path,
115 tokio: handle.clone(),
116 };
117
118 let mut viewport = eframe::egui::ViewportBuilder::default()
122 .with_inner_size([960.0, 720.0])
123 .with_min_inner_size([640.0, 480.0])
124 .with_title("studio-worker");
125 if let Some([x, y]) =
128 dev_window_position(std::env::var("STUDIO_WORKER_WINDOW_POS").ok().as_deref())
129 {
130 viewport = viewport.with_position([x, y]);
131 }
132 let native_options = eframe::NativeOptions {
133 viewport,
134 ..Default::default()
135 };
136
137 let initial_paused = paused.load(std::sync::atomic::Ordering::SeqCst);
141 let tokio_for_tray = handle.clone();
144
145 eframe::run_native(
146 "studio-worker",
147 native_options,
148 Box::new(move |cc| {
149 cc.egui_ctx.set_visuals(eframe::egui::Visuals::dark());
151 let app = app::App::with_notifier_and_registration(
152 app_state,
153 app::App::default_notifier_box(),
154 registration,
155 );
156 let quit_handle = app.quit_requested_handle();
157
158 let tray_handle = tray_host::install(
163 cc.egui_ctx.clone(),
164 paused.clone(),
165 quit_handle,
166 tokio_for_tray,
167 initial_paused,
168 );
169 let mut app = app;
172 if let Some(tray) = tray_handle {
173 app.attach_tray(tray);
174 }
175 Ok(Box::new(app))
176 }),
177 )
178 .map_err(|e| anyhow!("eframe: {e}"))?;
179
180 stop.store(true, std::sync::atomic::Ordering::SeqCst);
182 Ok(())
183}
184
185fn sync_autostart_on_launch(auto_start: bool) {
190 use crate::autostart::{self, AutostartSync};
191 match autostart::launch_sync_action(auto_start, autostart::is_enabled()) {
192 AutostartSync::Enable => match std::env::current_exe() {
193 Ok(exe) => {
194 if let Err(e) = autostart::enable(&exe) {
195 tracing::warn!(
196 target: "studio_worker::ui",
197 error = %e,
198 "could not enable autostart-on-login"
199 );
200 }
201 }
202 Err(e) => tracing::warn!(
203 target: "studio_worker::ui",
204 error = %e,
205 "could not resolve current exe to enable autostart-on-login"
206 ),
207 },
208 AutostartSync::Disable => {
209 if let Err(e) = autostart::disable() {
210 tracing::warn!(
211 target: "studio_worker::ui",
212 error = %e,
213 "could not disable stale autostart-on-login"
214 );
215 }
216 }
217 AutostartSync::Noop => {}
218 }
219}
220
221fn dev_window_position(env: Option<&str>) -> Option<[f32; 2]> {
228 if let Some(raw) = env {
229 let mut parts = raw.split(',').map(str::trim);
230 if let (Some(x), Some(y), None) = (parts.next(), parts.next(), parts.next()) {
231 if let (Ok(x), Ok(y)) = (x.parse::<f32>(), y.parse::<f32>()) {
232 return Some([x, y]);
233 }
234 }
235 return None;
236 }
237 #[cfg(debug_assertions)]
241 let default = Some([48.0, 48.0]);
242 #[cfg(not(debug_assertions))]
243 let default = None;
244 default
245}
246
247#[cfg(test)]
248mod tests {
249 use super::dev_window_position;
250
251 #[test]
252 fn parses_explicit_position_override() {
253 assert_eq!(dev_window_position(Some("100,200")), Some([100.0, 200.0]));
254 }
255
256 #[test]
257 fn trims_whitespace_around_coords() {
258 assert_eq!(dev_window_position(Some(" 10 , 20 ")), Some([10.0, 20.0]));
259 }
260
261 #[test]
262 fn rejects_malformed_override() {
263 assert_eq!(dev_window_position(Some("not-a-pos")), None);
264 assert_eq!(dev_window_position(Some("1,2,3")), None);
265 assert_eq!(dev_window_position(Some("1")), None);
266 }
267
268 #[cfg(debug_assertions)]
269 #[test]
270 fn defaults_to_left_screen_in_debug() {
271 assert_eq!(dev_window_position(None), Some([48.0, 48.0]));
272 }
273
274 #[cfg(not(debug_assertions))]
275 #[test]
276 fn defers_to_wm_in_release() {
277 assert_eq!(dev_window_position(None), None);
278 }
279}