use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::env::Env;
use crate::i18n::I18n;
use crate::icons::PillarStyle;
use crate::storage::{Ecosystem, FolderChange, FolderWatch, Preferences, Settings};
#[derive(Debug, Clone)]
pub(crate) struct Member {
ecosystem: Ecosystem,
app: String,
folder: Option<PathBuf>,
}
impl Member {
pub(crate) fn new(ecosystem: Ecosystem, app: &str, folder: Option<PathBuf>) -> Self {
Self { ecosystem, app: app.to_owned(), folder }
}
fn folder(&self) -> Option<PathBuf> {
self.folder.clone().or_else(|| self.ecosystem.config_dir())
}
pub(crate) fn settings(&self) -> Settings {
match &self.folder {
Some(folder) => Settings::open(own_file(folder, &self.app)).member_of(&self.ecosystem),
None => Settings::load_member(&self.ecosystem, &self.app),
}
}
pub(crate) fn preferences(&self, i18n: &I18n) -> Preferences {
match &self.folder {
Some(folder) => self.ecosystem.preferences_in(folder, &self.app, i18n),
None => self.ecosystem.preferences(&self.app, i18n),
}
}
pub(crate) fn follow(self, settings: &Settings, preferences: Preferences, watch: bool) -> Follow {
let folder = self.folder();
let watch = match (&folder, watch) {
(Some(folder), true) => Watch::start(folder, [shared_name(&self.ecosystem), file_name(&self.app)]),
_ => None,
};
Follow {
member: self,
folder,
heard: Heard { preferences, pillar: settings.pillar_style() },
told: false,
watch,
}
}
}
pub(crate) struct Start<'a> {
pub(crate) theme: Option<&'a str>,
pub(crate) settings: Option<Settings>,
pub(crate) preferences: Option<Preferences>,
}
impl Start<'_> {
pub(crate) fn apply(self, env: &mut Env, member: Option<Member>, watch: bool) -> Option<Follow> {
let (settings, preferences) = match &member {
Some(member) => (
Some(self.settings.unwrap_or_else(|| member.settings())),
Some(self.preferences.unwrap_or_else(|| member.preferences(env.i18n()))),
),
None => (self.settings, self.preferences),
};
if let Some(theme) = self.theme {
env.set_theme(theme);
}
if let Some(settings) = &settings {
env.apply_settings(settings);
}
if let Some(preferences) = &preferences {
env.apply_preferences(preferences);
}
let (member, settings, preferences) = (member?, settings?, preferences?);
Some(member.follow(&settings, preferences, watch))
}
}
fn file_name(id: &str) -> OsString {
OsString::from(crate::storage::ecosystem_file_name(id))
}
fn shared_name(ecosystem: &Ecosystem) -> OsString {
file_name(ecosystem.id())
}
fn own_file(folder: &Path, app: &str) -> PathBuf {
folder.join(file_name(app))
}
#[derive(Debug, Clone)]
pub(crate) struct Heard {
pub(crate) preferences: Preferences,
pub(crate) pillar: Option<PillarStyle>,
}
#[derive(Debug)]
pub(crate) struct Follow {
member: Member,
folder: Option<PathBuf>,
pub(crate) heard: Heard,
pub(crate) told: bool,
watch: Option<Watch>,
}
impl Follow {
pub(crate) fn take_change(&self) -> bool {
self.watch.as_ref().is_some_and(Watch::take_change)
}
pub(crate) fn read(&self, i18n: &I18n) -> Option<Heard> {
let folder = self.folder.as_ref()?;
let ecosystem = &self.member.ecosystem;
let preferences = ecosystem.preferences_without_saving_in(folder, &self.member.app, i18n);
let own = Settings::open(own_file(folder, &self.member.app)).member_of(ecosystem);
Some(Heard { preferences, pillar: own.pillar_style() })
}
}
#[derive(Debug)]
struct Watch {
_watch: FolderWatch,
changed: Arc<AtomicBool>,
}
impl Watch {
fn start(folder: &Path, names: [OsString; 2]) -> Option<Self> {
let mut watch = FolderWatch::new().ok()?;
watch.watch(folder).ok()?;
let changes = watch.changes();
let changed = Arc::new(AtomicBool::new(false));
let raised = Arc::clone(&changed);
std::thread::Builder::new()
.name("quvyta-preferences".to_owned())
.spawn(move || {
loop {
let batch = changes.next();
if batch.is_empty() {
return;
}
if batch.iter().any(|change| concerns(change, &names)) {
raised.store(true, Ordering::SeqCst);
super::signals::wake();
}
}
})
.ok()?;
Some(Self { _watch: watch, changed })
}
fn take_change(&self) -> bool {
self.changed.swap(false, Ordering::SeqCst)
}
}
fn concerns(change: &FolderChange, names: &[OsString; 2]) -> bool {
use crate::storage::FolderChangeKind;
match change.kind {
FolderChangeKind::Overflow => true,
_ => change.name.as_ref().is_some_and(|name| names.contains(name)),
}
}
#[cfg(test)]
mod tests {
use std::fs;
use std::time::{Duration, Instant};
use super::*;
use crate::storage::{FolderChangeKind, Scope, Shared};
fn folder(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("quvyta-follow-{name}-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("test folder");
dir
}
fn heard_within(watch: &Watch, bound: Duration) -> bool {
let until = Instant::now() + bound;
while Instant::now() < until {
if watch.take_change() {
return true;
}
std::thread::sleep(Duration::from_millis(10));
}
false
}
fn names() -> [OsString; 2] {
[OsString::from("quvyta.conf"), OsString::from("code.conf")]
}
#[test]
fn only_the_two_files_and_dropped_events_concern_the_follow() {
let change = |name: Option<&str>, kind| FolderChange {
folder: PathBuf::from("/c"),
name: name.map(OsString::from),
kind,
};
let names = names();
assert!(concerns(&change(Some("quvyta.conf"), FolderChangeKind::Modified), &names));
assert!(concerns(
&change(Some("code.conf"), FolderChangeKind::Renamed { from: ".code.conf.tmp".into() }),
&names
));
assert!(!concerns(&change(Some("desktop.conf"), FolderChangeKind::Modified), &names));
assert!(!concerns(&change(None, FolderChangeKind::Gone), &names));
assert!(concerns(&change(None, FolderChangeKind::Overflow), &names));
}
#[cfg(target_os = "linux")]
#[test]
fn a_change_to_the_shared_file_raises_the_flag_and_one_elsewhere_does_not() {
let dir = folder("flag");
let watch = Watch::start(&dir, names()).expect("a watch on a folder that is there");
fs::write(dir.join("notes.txt"), "not a settings file").expect("write");
assert!(!heard_within(&watch, Duration::from_millis(600)), "another file is not a change");
Ecosystem::QUVYTA.set_in(&dir, "code", Shared::Theme, "amber", Scope::Ecosystem).expect("set");
assert!(heard_within(&watch, Duration::from_secs(10)), "the shared file changed");
drop(watch);
fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_missing_folder_is_not_watched_and_nothing_fails() {
let dir = std::env::temp_dir().join(format!("quvyta-follow-missing-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
assert!(Watch::start(&dir, names()).is_none());
}
}