use std::path::PathBuf;
use day_spec::Rect;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Remembered {
pub frame: Rect,
pub maximized: bool,
}
fn app_id() -> Option<String> {
std::env::var("DAY_APP_ID")
.ok()
.filter(|s| !s.is_empty())
.or_else(|| option_env!("DAY_APP_ID").map(str::to_owned))
}
fn config_root() -> Option<PathBuf> {
if let Some(dir) = std::env::var_os("XDG_CONFIG_HOME").filter(|d| !d.is_empty()) {
return Some(PathBuf::from(dir));
}
if cfg!(target_os = "windows") {
return std::env::var_os("APPDATA").map(PathBuf::from);
}
let home = PathBuf::from(std::env::var_os("HOME")?);
if cfg!(target_os = "macos") {
Some(home.join("Library/Application Support"))
} else {
Some(home.join(".config"))
}
}
fn store() -> Option<PathBuf> {
Some(config_root()?.join(app_id()?).join("day-windows.txt"))
}
fn parse(line: &str) -> Option<(&str, Remembered)> {
let mut f = line.split('\t');
let key = f.next()?;
let mut num = || f.next()?.parse::<f64>().ok();
let (x, y, w, h) = (num()?, num()?, num()?, num()?);
let maximized = f.next()? == "1";
if w <= 0.0 || h <= 0.0 {
return None;
}
Some((
key,
Remembered {
frame: Rect::new(x, y, w, h),
maximized,
},
))
}
fn read_all() -> Vec<String> {
store()
.and_then(|p| std::fs::read_to_string(p).ok())
.map(|s| s.lines().map(str::to_owned).collect())
.unwrap_or_default()
}
pub fn load(key: &str) -> Option<Remembered> {
read_all()
.iter()
.find_map(|l| parse(l).filter(|(k, _)| *k == key).map(|(_, r)| r))
}
pub fn save(key: &str, r: Remembered) {
let Some(path) = store() else { return };
let mut lines: Vec<String> = read_all()
.into_iter()
.filter(|l| parse(l).is_none_or(|(k, _)| k != key))
.collect();
lines.push(format!(
"{key}\t{}\t{}\t{}\t{}\t{}",
r.frame.origin.x,
r.frame.origin.y,
r.frame.size.width,
r.frame.size.height,
u8::from(r.maximized)
));
if let Some(dir) = path.parent()
&& std::fs::create_dir_all(dir).is_err()
{
return;
}
if let Err(e) = std::fs::write(&path, lines.join("\n") + "\n") {
log::debug!("remembered window frame not saved: {e}");
}
}
pub fn on_screen(frame: Rect, work_areas: &[Rect]) -> bool {
work_areas.is_empty()
|| work_areas.iter().any(|a| {
let w = (frame.origin.x + frame.size.width).min(a.origin.x + a.size.width)
- frame.origin.x.max(a.origin.x);
let h = (frame.origin.y + frame.size.height).min(a.origin.y + a.size.height)
- frame.origin.y.max(a.origin.y);
w >= 64.0 && h >= 32.0
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_line_round_trips_and_a_bad_one_is_skipped() {
let (k, r) = parse("main\t10\t20\t800\t600\t1").unwrap();
assert_eq!(k, "main");
assert_eq!(r.frame.size, day_spec::Size::new(800.0, 600.0));
assert!(r.maximized);
assert!(parse("main\t10\t20\t0\t600\t0").is_none());
assert!(parse("main\tx").is_none());
}
#[test]
fn a_frame_off_every_display_is_not_on_screen() {
let display = [Rect::new(0.0, 0.0, 1440.0, 900.0)];
let inside = Rect::new(100.0, 100.0, 800.0, 600.0);
let gone = Rect::new(3000.0, 100.0, 800.0, 600.0);
let sliver = Rect::new(1420.0, 100.0, 800.0, 600.0);
assert!(on_screen(inside, &display));
assert!(!on_screen(gone, &display));
assert!(!on_screen(sliver, &display));
assert!(on_screen(gone, &[]));
}
}