use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::{Duration, Instant};
const SETTLE_TIMEOUT: Duration = Duration::from_secs(180);
const START_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Default)]
pub struct Script {
pub attach: Vec<PathBuf>,
pub send: Vec<String>,
pub queue: Vec<String>,
pub stop_after: Option<Duration>,
pub shoot_queued: bool,
pub screenshot: Option<PathBuf>,
pub after: Duration,
pub then: Vec<Command>,
pub test_connection: Option<(String, String)>,
pub open_panel: Option<String>,
pub draft: Option<String>,
pub new_workspace: Option<(String, byob_types::config::EnvKind)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Command {
OpenAll,
TogglePane,
CollapseAll,
ExpandAll,
Maximize,
Wait(u64),
}
impl Command {
pub const NAMES: &'static str = "open-all, toggle-pane, collapse-all, expand-all, maximize, wait:SECONDS";
pub fn parse(s: &str) -> Option<Self> {
if let Some(n) = s.strip_prefix("wait:") {
return n.parse().ok().map(Self::Wait);
}
Some(match s {
"open-all" => Self::OpenAll,
"toggle-pane" => Self::TogglePane,
"collapse-all" => Self::CollapseAll,
"expand-all" => Self::ExpandAll,
"maximize" => Self::Maximize,
_ => return None,
})
}
fn run(self, app: &mut byob_ui_egui::ByobApp) {
let a = app.artifacts_mut();
match self {
Self::OpenAll => a.open_all(),
Self::TogglePane => a.toggle_pane(),
Self::CollapseAll => a.minimize_all(true),
Self::ExpandAll => a.minimize_all(false),
Self::Maximize => a.toggle_maximized(),
Self::Wait(_) => {}
}
}
}
pub struct Runner {
script: Script,
started: Instant,
waiting: Option<(Instant, bool)>,
requested: bool,
created: bool,
queued_at: Option<Instant>,
stopped: bool,
then_at: Option<Instant>,
next_at: Option<Instant>,
done: Arc<AtomicBool>,
result: Arc<Mutex<Option<String>>>,
}
impl Runner {
pub fn new(mut script: Script) -> Self {
script.send.reverse();
Self {
script,
started: Instant::now(),
waiting: None,
requested: false,
created: false,
queued_at: None,
stopped: false,
then_at: None,
next_at: None,
done: Arc::default(),
result: Arc::default(),
}
}
pub fn frame(&mut self, app: &mut byob_ui_egui::ByobApp, ctx: &egui::Context) {
if app.settled()
&& let Some((url, model)) = self.script.test_connection.take()
{
app.test_connection(&url, &model);
}
if let Some((name, env)) = self.script.new_workspace.clone() {
if !self.created && app.settled() {
app.create_workspace(&name, env);
self.created = true;
}
if !app.in_workspace(&name) {
ctx.request_repaint_after(Duration::from_millis(100));
return;
}
self.script.new_workspace = None;
}
for path in std::mem::take(&mut self.script.attach) {
match byob_ui_egui::upload::read_path(&path) {
Ok((name, bytes)) => app.attach(&name, &bytes),
Err(e) => eprintln!("byob: --attach {}: {e}", path.display()),
}
}
if let Some((at, was_busy)) = self.waiting {
let busy = was_busy || app.busy();
let finished = at.elapsed() >= SETTLE_TIMEOUT || if busy { !app.busy() && app.settled() } else { at.elapsed() >= START_TIMEOUT };
self.waiting = (!finished).then_some((at, busy));
}
if self.waiting.is_none()
&& !app.uploading()
&& let Some(text) = self.script.send.pop()
{
app.submit(text);
self.waiting = Some((Instant::now(), false));
}
if !self.script.queue.is_empty() && app.busy() {
for text in std::mem::take(&mut self.script.queue) {
app.submit(text);
}
self.queued_at = Some(Instant::now());
}
if let (Some(after), Some(at)) = (self.script.stop_after, self.queued_at)
&& !self.stopped
&& at.elapsed() >= after
{
app.stop();
self.stopped = true;
}
if self.script.shoot_queued && self.queued_at.is_some() && app.queued() > 0 {
self.shoot(ctx);
return;
}
if self.waiting.is_some() || !self.script.send.is_empty() || !self.script.queue.is_empty() {
ctx.request_repaint_after(Duration::from_millis(100));
return;
}
if let Some(name) = self.script.open_panel.clone()
&& app.settled()
&& app.open_panel(&name)
{
self.script.open_panel = None;
}
if let Some(text) = self.script.draft.take_if(|_| app.settled()) {
app.set_draft(&text);
}
if self.script.screenshot.is_none() {
return;
}
ctx.request_repaint_after(Duration::from_millis(100));
if self.close_when_shot(ctx) {
return;
}
let elapsed = self.started.elapsed();
let settled = app.settled() || elapsed >= SETTLE_TIMEOUT;
if !self.script.then.is_empty() {
if !settled || elapsed < Duration::from_secs(2) || self.next_at.is_some_and(|t| Instant::now() < t) {
return;
}
let c = self.script.then.remove(0);
eprintln!("byob: {:.1}s: {c:?}", elapsed.as_secs_f64());
c.run(app);
let now = Instant::now();
self.next_at = if let Command::Wait(s) = c { Some(now + Duration::from_secs(s)) } else { None };
self.then_at = Some(now);
return;
}
let waited = self.then_at.map_or(elapsed, |t| t.elapsed());
let ready = waited >= self.script.after && settled;
if ready {
self.shoot(ctx);
}
}
fn shoot(&mut self, ctx: &egui::Context) {
ctx.request_repaint_after(Duration::from_millis(100));
if self.close_when_shot(ctx) || self.requested {
return;
}
let Some(path) = self.script.screenshot.clone() else { return };
self.requested = true;
let (done, result) = (Arc::clone(&self.done), Arc::clone(&self.result));
ctx.request_screenshot(move |image| {
let msg = match save_png(&path, &image) {
Ok(()) => format!("screenshot saved to {}", path.display()),
Err(e) => format!("screenshot failed: {e}"),
};
*result.lock().unwrap_or_else(PoisonError::into_inner) = Some(msg);
done.store(true, Ordering::Release);
});
}
fn close_when_shot(&self, ctx: &egui::Context) -> bool {
if !self.done.load(Ordering::Acquire) {
return false;
}
if let Some(msg) = self.result.lock().unwrap_or_else(PoisonError::into_inner).take() {
eprintln!("byob: {msg}");
}
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
true
}
}
fn save_png(path: &Path, image: &egui::ColorImage) -> Result<(), String> {
let [w, h] = image.size;
let (w, h) = (u32::try_from(w).map_err(|e| e.to_string())?, u32::try_from(h).map_err(|e| e.to_string())?);
let file = std::fs::File::create(path).map_err(|e| format!("{}: {e}", path.display()))?;
let mut encoder = png::Encoder::new(std::io::BufWriter::new(file), w, h);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header().map_err(|e| e.to_string())?;
writer.write_image_data(image.as_raw()).map_err(|e| e.to_string())
}