use crate::pixmap::{Pixmap, SharedPixmap};
use crate::DaemonResult;
use anyhow::anyhow;
use itertools::Itertools;
use std::io::SeekFrom;
use std::mem;
use std::path::{Path, PathBuf};
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
use tokio::task::{AbortHandle, JoinSet};
use tokio::time::Interval;
const FILE_MAGIC: &[u8] = b"PIXELFLUT";
const HEADER_SIZE: usize = mem::size_of::<u64>() * 2;
const SEEK_MAGIC: SeekFrom = SeekFrom::Start(0);
const SEEK_HEADER: SeekFrom = SeekFrom::Start(FILE_MAGIC.len() as u64);
const SEEK_DATA: SeekFrom = SeekFrom::Start((FILE_MAGIC.len() + HEADER_SIZE) as u64);
#[derive(Debug)]
pub struct FileSinkOptions {
pub interval: Interval,
pub path: PathBuf,
}
#[derive(Debug)]
pub struct FileSink {
options: FileSinkOptions,
pixmap: SharedPixmap,
}
impl FileSink {
pub fn new(options: FileSinkOptions, pixmap: SharedPixmap) -> Self {
Self { options, pixmap }
}
pub async fn start(self, join_set: &mut JoinSet<DaemonResult>) -> anyhow::Result<AbortHandle> {
let mut file = self.open_file().await?;
self.write_header(&mut file).await?;
let handle = join_set
.build_task()
.name("file_sink")
.spawn(async move { self.run(file).await })?;
Ok(handle)
}
async fn open_file(&self) -> anyhow::Result<File> {
Ok(File::options()
.write(true)
.create(true)
.open(&self.options.path)
.await?)
}
async fn write_header(&self, file: &mut File) -> anyhow::Result<()> {
let (width, height) = self.pixmap.get_size();
file.set_len((FILE_MAGIC.len() + HEADER_SIZE + width * height * 3) as u64)
.await?;
file.seek(SEEK_MAGIC).await?;
file.write_all(FILE_MAGIC).await?;
file.seek(SEEK_HEADER).await?;
file.write_u64(width as u64).await?;
file.write_u64(height as u64).await?;
file.flush().await?;
file.sync_all().await?;
Ok(())
}
async fn write_data(&self, file: &mut File) -> anyhow::Result<()> {
file.seek(SEEK_DATA).await?;
let data = unsafe { self.pixmap.get_color_data() };
let data = data
.iter()
.flat_map(|c| Into::<[u8; 3]>::into(*c))
.collect::<Vec<_>>();
file.write_all(&data).await?;
file.flush().await?;
file.sync_all().await?;
Ok(())
}
async fn run(mut self, mut file: File) -> anyhow::Result<!> {
loop {
self.write_data(&mut file).await?;
self.options.interval.tick().await;
}
}
}
pub async fn load_pixmap_file(path: &Path) -> anyhow::Result<Pixmap> {
let mut file = File::open(path).await?;
let mut file_magic = [0u8; FILE_MAGIC.len()];
file.seek(SEEK_MAGIC).await?;
file.read_exact(&mut file_magic).await?;
if file_magic != FILE_MAGIC {
return Err(anyhow!(
"File at {} does not contain valid pixmap data",
path.display()
));
}
file.seek(SEEK_HEADER).await?;
let width = file.read_u64().await? as usize;
let height = file.read_u64().await? as usize;
let mut buf = vec![0u8; width * height * 3];
file.seek(SEEK_DATA).await?;
file.read_exact(&mut buf).await?;
let pixmap = Pixmap::new(width, height)?;
let pixmap_data = unsafe { pixmap.get_color_data() };
for (i, i_color) in buf.into_iter().tuples::<(_, _, _)>().enumerate() {
pixmap_data[i] = i_color.into()
}
Ok(pixmap)
}
#[cfg(test)]
mod test {
use super::*;
use crate::pixmap::Color;
use std::sync::Arc;
use std::time::Duration;
use tokio::time::interval;
#[tokio::test]
async fn test_store_and_load() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.into_path().join("test.pixmap");
let original_pixmap = Arc::new(Pixmap::new(5, 5).unwrap());
original_pixmap
.set_pixel(0, 0, Color::from((0xab, 0xab, 0xab)))
.unwrap();
original_pixmap
.set_pixel(2, 2, Color::from((0xab, 0xab, 0xab)))
.unwrap();
original_pixmap
.set_pixel(4, 4, Color::from((0xab, 0xab, 0xab)))
.unwrap();
{
let sink = FileSink::new(
FileSinkOptions {
path: file_path.clone(),
interval: interval(Duration::from_secs(1)),
},
original_pixmap.clone(),
);
let mut file = sink.open_file().await.unwrap();
sink.write_header(&mut file).await.unwrap();
sink.write_data(&mut file).await.unwrap();
}
let restored_pixmap = load_pixmap_file(&file_path).await.unwrap();
let original_data = unsafe { original_pixmap.get_color_data() };
let restored_data = unsafe { restored_pixmap.get_color_data() };
assert_eq!(original_data, restored_data);
}
}