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()
119 .with_inner_size([960.0, 720.0])
120 .with_min_inner_size([640.0, 480.0])
121 .with_title("studio-worker");
122 if let Some([x, y]) =
125 dev_window_position(std::env::var("STUDIO_WORKER_WINDOW_POS").ok().as_deref())
126 {
127 viewport = viewport.with_position([x, y]);
128 }
129 let native_options = eframe::NativeOptions {
130 viewport,
131 ..Default::default()
132 };
133
134 let initial_paused = paused.load(std::sync::atomic::Ordering::SeqCst);
138 let tokio_for_tray = handle.clone();
141
142 eframe::run_native(
143 "studio-worker",
144 native_options,
145 Box::new(move |cc| {
146 cc.egui_ctx.set_visuals(eframe::egui::Visuals::dark());
148 let app = app::App::with_notifier_and_registration(
149 app_state,
150 app::App::default_notifier_box(),
151 registration,
152 );
153 let quit_handle = app.quit_requested_handle();
154
155 let tray_handle = tray_host::install(
160 cc.egui_ctx.clone(),
161 paused.clone(),
162 quit_handle,
163 tokio_for_tray,
164 initial_paused,
165 );
166 let mut app = app;
169 if let Some(tray) = tray_handle {
170 app.attach_tray(tray);
171 }
172 Ok(Box::new(app))
173 }),
174 )
175 .map_err(|e| anyhow!("eframe: {e}"))?;
176
177 stop.store(true, std::sync::atomic::Ordering::SeqCst);
179 Ok(())
180}
181
182fn sync_autostart_on_launch(auto_start: bool) {
187 use crate::autostart::{self, AutostartSync};
188 match autostart::launch_sync_action(auto_start, autostart::is_enabled()) {
189 AutostartSync::Enable => match std::env::current_exe() {
190 Ok(exe) => {
191 if let Err(e) = autostart::enable(&exe) {
192 tracing::warn!(
193 target: "studio_worker::ui",
194 error = %e,
195 "could not enable autostart-on-login"
196 );
197 }
198 }
199 Err(e) => tracing::warn!(
200 target: "studio_worker::ui",
201 error = %e,
202 "could not resolve current exe to enable autostart-on-login"
203 ),
204 },
205 AutostartSync::Disable => {
206 if let Err(e) = autostart::disable() {
207 tracing::warn!(
208 target: "studio_worker::ui",
209 error = %e,
210 "could not disable stale autostart-on-login"
211 );
212 }
213 }
214 AutostartSync::Noop => {}
215 }
216}
217
218fn dev_window_position(env: Option<&str>) -> Option<[f32; 2]> {
225 if let Some(raw) = env {
226 let mut parts = raw.split(',').map(str::trim);
227 if let (Some(x), Some(y), None) = (parts.next(), parts.next(), parts.next()) {
228 if let (Ok(x), Ok(y)) = (x.parse::<f32>(), y.parse::<f32>()) {
229 return Some([x, y]);
230 }
231 }
232 return None;
233 }
234 #[cfg(debug_assertions)]
238 let default = Some([48.0, 48.0]);
239 #[cfg(not(debug_assertions))]
240 let default = None;
241 default
242}
243
244#[cfg(test)]
245mod tests {
246 use super::dev_window_position;
247
248 #[test]
249 fn parses_explicit_position_override() {
250 assert_eq!(dev_window_position(Some("100,200")), Some([100.0, 200.0]));
251 }
252
253 #[test]
254 fn trims_whitespace_around_coords() {
255 assert_eq!(dev_window_position(Some(" 10 , 20 ")), Some([10.0, 20.0]));
256 }
257
258 #[test]
259 fn rejects_malformed_override() {
260 assert_eq!(dev_window_position(Some("not-a-pos")), None);
261 assert_eq!(dev_window_position(Some("1,2,3")), None);
262 assert_eq!(dev_window_position(Some("1")), None);
263 }
264
265 #[cfg(debug_assertions)]
266 #[test]
267 fn defaults_to_left_screen_in_debug() {
268 assert_eq!(dev_window_position(None), Some([48.0, 48.0]));
269 }
270
271 #[cfg(not(debug_assertions))]
272 #[test]
273 fn defers_to_wm_in_release() {
274 assert_eq!(dev_window_position(None), None);
275 }
276}