use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
pub fn check_screen_permission() -> bool {
#[cfg(target_os = "macos")]
{
let test_path = std::env::temp_dir().join("minutes-screen-test.png");
let result = std::process::Command::new("screencapture")
.args(["-x", "-R", "0,0,1,1", "-t", "png"])
.arg(&test_path)
.output();
let _ = std::fs::remove_file(&test_path);
match result {
Ok(output) => {
if output.status.success() {
true
} else {
tracing::warn!("screen capture permission check failed — grant Screen Recording permission in System Settings > Privacy & Security");
false
}
}
Err(_) => {
tracing::warn!("screencapture command not found");
false
}
}
}
#[cfg(not(target_os = "macos"))]
{
true
}
}
pub fn start_capture(
output_dir: &Path,
interval: Duration,
stop_flag: Arc<AtomicBool>,
) -> std::io::Result<ScreenCaptureHandle> {
std::fs::create_dir_all(output_dir)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(output_dir, std::fs::Permissions::from_mode(0o700))?;
}
let dir = output_dir.to_path_buf();
let thread_stop = stop_flag.clone();
let handle = std::thread::spawn(move || {
let mut index: u32 = 0;
let start = std::time::Instant::now();
tracing::info!(
dir = %dir.display(),
interval_secs = interval.as_secs(),
"screen capture started"
);
let first_sleep_end = std::time::Instant::now() + interval;
while std::time::Instant::now() < first_sleep_end {
if thread_stop.load(Ordering::Relaxed) {
tracing::info!(
captures = 0,
"screen capture stopped (before first capture)"
);
return;
}
std::thread::sleep(Duration::from_millis(250));
}
while !thread_stop.load(Ordering::Relaxed) && index < MAX_SCREENSHOTS {
let elapsed = start.elapsed().as_secs();
let filename = format!("screen-{:04}-{:04}s.png", index, elapsed);
let path = dir.join(&filename);
if let Err(e) = capture_screenshot(&path) {
tracing::warn!("screen capture failed: {}", e);
} else {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).ok();
}
tracing::debug!(file = %filename, "screen captured");
index += 1;
}
let sleep_end = std::time::Instant::now() + interval;
while std::time::Instant::now() < sleep_end {
if thread_stop.load(Ordering::Relaxed) {
break;
}
std::thread::sleep(Duration::from_millis(250));
}
}
tracing::info!(captures = index, "screen capture stopped");
});
Ok(ScreenCaptureHandle {
thread: Some(handle),
})
}
const MAX_SCREENSHOTS: u32 = 60;
const TARGET_WIDTH: u32 = 1280;
fn capture_screenshot(path: &Path) -> std::io::Result<()> {
#[cfg(target_os = "macos")]
{
let output = std::process::Command::new("screencapture")
.args(["-x", "-C", "-t", "png"])
.arg(path)
.output()?;
if !output.status.success() {
return Err(std::io::Error::other(format!(
"screencapture failed: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
let _ = std::process::Command::new("sips")
.args([
"--resampleWidth",
&TARGET_WIDTH.to_string(),
"-s",
"format",
"png",
])
.arg(path)
.output(); }
#[cfg(target_os = "linux")]
{
let result = std::process::Command::new("scrot").arg(path).output();
match result {
Ok(output) if output.status.success() => {}
_ => {
let output = std::process::Command::new("gnome-screenshot")
.args(["--file"])
.arg(path)
.output()?;
if !output.status.success() {
return Err(std::io::Error::other("no screenshot tool available"));
}
}
}
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
return Err(std::io::Error::other(
"screen capture not supported on this platform",
));
}
Ok(())
}
pub fn screens_dir_for(audio_path: &Path) -> PathBuf {
let stem = audio_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join(".minutes")
.join("screens")
.join(stem)
}
pub fn list_screenshots(dir: &Path) -> Vec<PathBuf> {
let mut files: Vec<PathBuf> = std::fs::read_dir(dir)
.into_iter()
.flatten()
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("png"))
.collect();
files.sort();
files
}
pub struct ScreenCaptureHandle {
thread: Option<std::thread::JoinHandle<()>>,
}
impl Drop for ScreenCaptureHandle {
fn drop(&mut self) {
if let Some(handle) = self.thread.take() {
handle.join().ok();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn list_screenshots_returns_sorted_pngs() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("screen-0002-0060s.png"), "fake").unwrap();
std::fs::write(dir.path().join("screen-0000-0000s.png"), "fake").unwrap();
std::fs::write(dir.path().join("screen-0001-0030s.png"), "fake").unwrap();
std::fs::write(dir.path().join("not-a-screenshot.txt"), "fake").unwrap();
let files = list_screenshots(dir.path());
assert_eq!(files.len(), 3);
assert!(files[0].to_str().unwrap().contains("0000"));
assert!(files[1].to_str().unwrap().contains("0001"));
assert!(files[2].to_str().unwrap().contains("0002"));
}
}