use crate::core::{InputEvent, MouseButton, Point, px};
use crate::gpu::CapturedFrame;
use std::fs::File;
use std::io::{self, BufWriter};
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
pub fn pointer_event(spec: &str) -> Option<InputEvent> {
let mut parts = spec.split(':');
let kind = parts.next()?;
let position = Point::new(px(parts.next()?.parse().ok()?), px(parts.next()?.parse().ok()?));
match kind {
"move" => Some(InputEvent::MouseMove { position }),
"down" => Some(InputEvent::MouseDown { position, button: MouseButton::Left }),
"up" => Some(InputEvent::MouseUp { position, button: MouseButton::Left }),
"rdown" => Some(InputEvent::MouseDown { position, button: MouseButton::Right }),
"rup" => Some(InputEvent::MouseUp { position, button: MouseButton::Right }),
_ => None,
}
}
pub struct SnapshotRequest {
pub path: PathBuf,
pub keystrokes: Vec<String>,
started: Instant,
delay: Duration,
}
impl SnapshotRequest {
pub fn from_environment() -> Option<Self> {
let path = PathBuf::from(std::env::var_os("AURA_SNAPSHOT")?);
let keystrokes = std::env::var("AURA_SNAPSHOT_KEYS")
.map(|keys| keys.split(',').map(str::trim).filter(|key| !key.is_empty()).map(str::to_owned).collect())
.unwrap_or_default();
let delay = std::env::var("AURA_SNAPSHOT_DELAY_MS")
.ok()
.and_then(|value| value.parse().ok())
.map_or(Duration::ZERO, Duration::from_millis);
Some(Self { path, keystrokes, started: Instant::now(), delay })
}
pub fn deadline(&self) -> Instant {
self.started + self.delay
}
pub fn ready(&self) -> bool {
Instant::now() >= self.deadline()
}
}
pub fn save_png(path: &Path, frame: &CapturedFrame) -> io::Result<()> {
let mut encoder = png::Encoder::new(BufWriter::new(File::create(path)?), frame.width, frame.height);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header().map_err(io::Error::other)?;
writer.write_image_data(&frame.rgba).map_err(io::Error::other)?;
writer.finish().map_err(io::Error::other)
}
impl crate::app::surface::Surface {
pub fn press(&mut self, keystroke: &str) {
if let Some(milliseconds) = keystroke.strip_prefix("wait:").and_then(|value| value.parse::<u64>().ok()) {
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(milliseconds);
while std::time::Instant::now() < deadline {
self.ui.run_pending();
std::thread::sleep(std::time::Duration::from_millis(10));
}
self.ui.draw(None);
return;
}
let event = match (pointer_event(keystroke), keystroke.strip_prefix("text:")) {
(Some(event), _) => event,
(None, Some(text)) => InputEvent::ImeCommit(text.to_owned()),
(None, None) => {
let text = (keystroke.chars().count() == 1).then(|| keystroke.to_owned());
InputEvent::KeyDown { keystroke: keystroke.to_owned(), text }
}
};
self.ui.handle_event(&event);
self.ui.draw(None);
}
pub fn save_snapshot(&mut self, request: &SnapshotRequest) {
let clear = self.ui.theme.canvas;
let result = self
.renderer
.capture(self.ui.scene(), self.scale(), clear)
.map_err(|error| error.to_string())
.and_then(|frame| save_png(&request.path, &frame).map_err(|error| error.to_string()));
match result {
Ok(()) => crate::app::console::info(format_args!("instantánea guardada en {}", request.path.display())),
Err(error) => crate::app::console::warn(format_args!("no se pudo guardar la instantánea: {error}")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snapshot_delay_has_a_fixed_deadline_without_polling_frames() {
let started = Instant::now();
let request = SnapshotRequest {
path: PathBuf::from("unused.png"),
keystrokes: Vec::new(),
started,
delay: Duration::from_secs(3600),
};
assert_eq!(request.deadline(), started + Duration::from_secs(3600));
assert!(!request.ready());
let immediate = SnapshotRequest { delay: Duration::ZERO, ..request };
assert!(immediate.ready());
}
#[test]
fn reconoce_los_eventos_de_puntero_y_descarta_el_resto() {
assert!(matches!(pointer_event("down:12.5:40"), Some(InputEvent::MouseDown { .. })));
assert!(matches!(pointer_event("move:1:2"), Some(InputEvent::MouseMove { .. })));
assert!(matches!(pointer_event("up:1:2"), Some(InputEvent::MouseUp { .. })));
assert!(pointer_event("mod-shift-p").is_none() && pointer_event("down:x:2").is_none());
assert!(pointer_event("text:hola").is_none());
}
}