pub mod app;
pub mod autostart;
pub mod notifier;
pub mod tab;
pub mod tabs;
pub mod tray;
use std::sync::{atomic::AtomicBool, Arc};
use std::time::Duration;
use anyhow::{anyhow, Result};
use parking_lot::Mutex;
use crate::{
auto_register::{self, RegistrationState},
config,
runtime::{self, LoopSchedule, WorkerObservers},
types::LogEntry,
};
pub fn run(config_path: Option<&str>) -> Result<()> {
let (cfg, path) = config::load(config_path)?;
runtime::log_startup_banner(&cfg, &path);
let cfg = config::shared(cfg);
let stop = Arc::new(AtomicBool::new(false));
let busy = Arc::new(AtomicBool::new(false));
let logs: Arc<Mutex<Vec<LogEntry>>> = Arc::new(Mutex::new(Vec::new()));
let observers = WorkerObservers::default();
let registration = auto_register::shared_initial();
let handle = tokio::runtime::Handle::current();
let cfg_autoreg = cfg.clone();
let path_autoreg = path.clone();
let registration_autoreg = registration.clone();
let stop_autoreg = stop.clone();
handle.spawn(async move {
loop {
if stop_autoreg.load(std::sync::atomic::Ordering::SeqCst) {
return;
}
let state =
auto_register::tick(&cfg_autoreg, &path_autoreg, ®istration_autoreg).await;
if matches!(
state,
RegistrationState::Approved | RegistrationState::Rejected { .. }
) {
return;
}
for _ in 0..30 {
if stop_autoreg.load(std::sync::atomic::Ordering::SeqCst) {
return;
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
});
let cfg_loops = cfg.clone();
let stop_loops = stop.clone();
let logs_loops = logs.clone();
let busy_loops = busy.clone();
let observers_loops = observers.clone();
handle.spawn(async move {
if let Err(e) = runtime::run_loops(
cfg_loops,
stop_loops,
logs_loops,
busy_loops,
observers_loops,
LoopSchedule::default(),
)
.await
{
tracing::error!(target: "studio_worker::ui", error = %e, "run_loops exited");
}
});
let app_state = app::AppDeps {
cfg: cfg.clone(),
logs: logs.clone(),
busy: busy.clone(),
observers: observers.clone(),
stop: stop.clone(),
config_path: path,
tokio: handle,
};
let native_options = eframe::NativeOptions {
viewport: eframe::egui::ViewportBuilder::default()
.with_inner_size([960.0, 720.0])
.with_min_inner_size([640.0, 480.0])
.with_title("studio-worker"),
..Default::default()
};
let auto_enabled = { cfg.lock().auto_enabled };
eframe::run_native(
"studio-worker",
native_options,
Box::new(move |cc| {
cc.egui_ctx.set_visuals(eframe::egui::Visuals::dark());
let app = app::App::with_notifier_and_registration(
app_state,
app::App::default_notifier_box(),
registration,
);
let quit_handle = app.quit_requested_handle();
let tray_state =
install_tray(cc.egui_ctx.clone(), cfg.clone(), quit_handle, auto_enabled);
let mut app = app;
app.attach_tray(tray_state);
Ok(Box::new(app))
}),
)
.map_err(|e| anyhow!("eframe: {e}"))?;
stop.store(true, std::sync::atomic::Ordering::SeqCst);
Ok(())
}
pub struct TrayState {
pub icon: Option<tray_icon::TrayIcon>,
pub current_variant: tray::TrayVariant,
pub menu_ids: TrayMenuIds,
}
#[derive(Debug, Clone)]
pub struct TrayMenuIds {
pub open_window: tray_icon::menu::MenuId,
pub toggle_auto: tray_icon::menu::MenuId,
pub quit: tray_icon::menu::MenuId,
}
fn install_tray(
ctx: eframe::egui::Context,
cfg: crate::config::SharedConfig,
quit_requested: std::sync::Arc<std::sync::atomic::AtomicBool>,
auto_enabled: bool,
) -> TrayState {
use tray_icon::menu::{MenuEvent, MenuId};
let open_id = MenuId::new(tray::menu_ids::OPEN_WINDOW);
let toggle_id = MenuId::new(tray::menu_ids::TOGGLE_AUTO);
let quit_id = MenuId::new(tray::menu_ids::QUIT);
let labels = tray::menu_labels(auto_enabled);
let open_label = labels.open_window;
let toggle_label = labels.toggle_auto.clone();
let quit_label = labels.quit;
let tray_holder = spawn_tray_thread(
open_id.clone(),
toggle_id.clone(),
quit_id.clone(),
open_label,
toggle_label,
quit_label,
);
let ctx_clone = ctx.clone();
let cfg_clone = cfg.clone();
let open_for_thread = open_id.clone();
let toggle_for_thread = toggle_id.clone();
let quit_for_thread = quit_id.clone();
std::thread::spawn(move || {
let rx = MenuEvent::receiver();
while let Ok(event) = rx.recv() {
if event.id == open_for_thread {
ctx_clone.send_viewport_cmd(eframe::egui::ViewportCommand::Visible(true));
ctx_clone.send_viewport_cmd(eframe::egui::ViewportCommand::Focus);
} else if event.id == toggle_for_thread {
let mut guard = cfg_clone.lock();
guard.auto_enabled = !guard.auto_enabled;
let snapshot = guard.clone();
drop(guard);
if let Ok(p) = crate::config::resolve_path(None) {
let _ = crate::config::save(&snapshot, &p);
}
} else if event.id == quit_for_thread {
quit_requested.store(true, std::sync::atomic::Ordering::SeqCst);
ctx_clone.request_repaint();
}
ctx_clone.request_repaint();
}
});
TrayState {
icon: tray_holder,
current_variant: tray::TrayVariant::Disconnected,
menu_ids: TrayMenuIds {
open_window: open_id,
toggle_auto: toggle_id,
quit: quit_id,
},
}
}
#[cfg(target_os = "linux")]
fn spawn_tray_thread(
open_id: tray_icon::menu::MenuId,
toggle_id: tray_icon::menu::MenuId,
quit_id: tray_icon::menu::MenuId,
open_label: &'static str,
toggle_label: String,
quit_label: &'static str,
) -> Option<tray_icon::TrayIcon> {
use tray_icon::menu::{Menu, MenuItem};
use tray_icon::{Icon, TrayIconBuilder};
std::thread::spawn(move || {
if let Err(e) = gtk::init() {
tracing::warn!(
target: "studio_worker::ui::tray",
"gtk init failed: {e}"
);
return;
}
let menu = Menu::new();
let open_item = MenuItem::with_id(open_id, open_label, true, None);
let toggle_item = MenuItem::with_id(toggle_id, &toggle_label, true, None);
let quit_item = MenuItem::with_id(quit_id, quit_label, true, None);
let _ = menu.append(&open_item);
let _ = menu.append(&toggle_item);
let _ = menu.append(&quit_item);
let variant = tray::TrayVariant::Disconnected;
let icon = Icon::from_rgba(variant.rgba_16(), 16, 16).ok();
let mut builder = TrayIconBuilder::new()
.with_tooltip(variant.tooltip())
.with_menu(Box::new(menu));
if let Some(i) = icon {
builder = builder.with_icon(i);
}
let _tray = match builder.build() {
Ok(t) => t,
Err(e) => {
tracing::warn!(
target: "studio_worker::ui::tray",
"tray build failed: {e}"
);
return;
}
};
gtk::main();
});
None
}
#[cfg(not(target_os = "linux"))]
fn spawn_tray_thread(
open_id: tray_icon::menu::MenuId,
toggle_id: tray_icon::menu::MenuId,
quit_id: tray_icon::menu::MenuId,
open_label: &'static str,
toggle_label: String,
quit_label: &'static str,
) -> Option<tray_icon::TrayIcon> {
use tray_icon::menu::{Menu, MenuItem};
use tray_icon::{Icon, TrayIconBuilder};
let menu = Menu::new();
let open_item = MenuItem::with_id(open_id, open_label, true, None);
let toggle_item = MenuItem::with_id(toggle_id, &toggle_label, true, None);
let quit_item = MenuItem::with_id(quit_id, quit_label, true, None);
let _ = menu.append(&open_item);
let _ = menu.append(&toggle_item);
let _ = menu.append(&quit_item);
let variant = tray::TrayVariant::Disconnected;
let icon = Icon::from_rgba(variant.rgba_16(), 16, 16).ok();
let mut builder = TrayIconBuilder::new()
.with_tooltip(variant.tooltip())
.with_menu(Box::new(menu));
if let Some(i) = icon {
builder = builder.with_icon(i);
}
builder.build().ok()
}