use super::*;
use crate::sync::Difference;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SyncAction {
ToRight,
ToLeft,
DeleteLeft,
DeleteRight,
Skip,
}
pub struct SyncEntry {
pub difference: Difference,
pub action: SyncAction,
}
impl SyncEntry {
fn new(difference: Difference) -> Self {
let action = default_action(&difference);
SyncEntry { difference, action }
}
pub fn allows(&self, action: SyncAction) -> bool {
let (left, right) = (self.difference.left, self.difference.right);
let mixed = matches!((left, right), (Some(l), Some(r)) if l.is_dir != r.is_dir);
match action {
SyncAction::Skip => true,
_ if mixed => false,
SyncAction::ToRight => left.is_some(),
SyncAction::ToLeft => right.is_some(),
SyncAction::DeleteLeft => left.is_some() && right.is_none(),
SyncAction::DeleteRight => right.is_some() && left.is_none(),
}
}
pub fn is_dir(&self) -> bool {
self.difference.left.or(self.difference.right).is_some_and(|side| side.is_dir)
}
}
fn default_action(difference: &Difference) -> SyncAction {
match (difference.left, difference.right) {
(Some(_), None) => SyncAction::ToRight,
(None, Some(_)) => SyncAction::ToLeft,
(Some(l), Some(r)) if l.is_dir == r.is_dir => match difference.left_newer() {
Some(true) => SyncAction::ToRight,
Some(false) => SyncAction::ToLeft,
None => SyncAction::Skip,
},
_ => SyncAction::Skip,
}
}
pub struct SyncPlan {
pub left: PathBuf,
pub right: PathBuf,
pub entries: Vec<SyncEntry>,
pub cursor: usize,
pub scroll: usize,
pub room: usize,
pub looking_in: Option<PathBuf>,
pub done: bool,
pub unreadable: usize,
pub armed: bool,
cancel: Arc<AtomicBool>,
updates: Receiver<crate::sync::Update>,
}
impl Drop for SyncPlan {
fn drop(&mut self) {
self.cancel.store(true, Ordering::Relaxed);
}
}
impl SyncPlan {
pub fn counts(&self) -> [usize; 4] {
let mut counts = [0; 4];
for entry in &self.entries {
let at = match entry.action {
SyncAction::ToRight => 0,
SyncAction::ToLeft => 1,
SyncAction::DeleteLeft | SyncAction::DeleteRight => 2,
SyncAction::Skip => 3,
};
counts[at] += 1;
}
counts
}
}
impl AppState {
pub fn open_sync(&mut self) {
if self.panel_busy(true) || self.panel_busy(false) {
return;
}
if self.panel_archive(true).is_some() || self.panel_archive(false).is_some() {
self.display_error(crate::app::read_only().to_string());
return;
}
let (left, right) = (self.panel_dir(true), self.panel_dir(false));
let real = |dir: &Path| dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf());
let (real_left, real_right) = (real(&left), real(&right));
if real_left.starts_with(&real_right) || real_right.starts_with(&real_left) {
let said = if real_left == real_right {
t!("Both panels show the same directory: nothing to synchronize")
} else {
t!("One panel's directory is inside the other's: nothing to synchronize")
};
self.display_error(said.to_string());
return;
}
let cancel = Arc::new(AtomicBool::new(false));
let (sender, updates) = mpsc::channel();
let (worker_cancel, worker_left, worker_right, show_hidden) = (Arc::clone(&cancel), left.clone(), right.clone(), self.options.show_hidden);
std::thread::spawn(move || {
crate::sync::compare(&worker_left, &worker_right, show_hidden, &worker_cancel, &mut |update| sender.send(update).is_ok());
});
self.dialog = Some(Dialog::Sync(Box::new(SyncPlan {
left,
right,
entries: Vec::new(),
cursor: 0,
scroll: 0,
room: 1,
looking_in: Some(PathBuf::new()),
done: false,
unreadable: 0,
armed: false,
cancel,
updates,
})));
}
fn sync_plan(&mut self) -> Option<&mut SyncPlan> {
match &mut self.dialog {
Some(Dialog::Sync(plan)) => Some(plan),
_ => None,
}
}
pub fn poll_sync(&mut self) {
let Some(plan) = self.sync_plan() else {
return;
};
loop {
match plan.updates.try_recv() {
Ok(crate::sync::Update::Looking(dir)) => plan.looking_in = Some(dir),
Ok(crate::sync::Update::Found(difference)) => plan.entries.push(SyncEntry::new(difference)),
Ok(crate::sync::Update::Finished { unreadable }) => {
plan.unreadable = unreadable;
plan.looking_in = None;
plan.done = true;
}
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => {
plan.looking_in = None;
plan.done = true;
break;
}
}
}
}
pub fn sync_move(&mut self, by: isize) {
if let Some(plan) = self.sync_plan() {
plan.armed = false;
plan.cursor = plan.cursor.saturating_add_signed(by).min(plan.entries.len().saturating_sub(1));
if plan.cursor < plan.scroll {
plan.scroll = plan.cursor;
} else if plan.cursor >= plan.scroll + plan.room {
plan.scroll = plan.cursor + 1 - plan.room;
}
}
}
pub fn sync_set(&mut self, action: SyncAction) {
let Some(plan) = self.sync_plan() else {
return;
};
let Some(entry) = plan.entries.get_mut(plan.cursor) else {
return;
};
let action = match action {
SyncAction::DeleteLeft | SyncAction::DeleteRight if entry.difference.left.is_none() => SyncAction::DeleteRight,
SyncAction::DeleteLeft | SyncAction::DeleteRight => SyncAction::DeleteLeft,
other => other,
};
if !entry.allows(action) {
return;
}
entry.action = action;
self.sync_move(1);
}
pub fn sync_mirror(&mut self, onto_right: Option<bool>) {
let Some(plan) = self.sync_plan() else {
return;
};
plan.armed = false;
for entry in &mut plan.entries {
let wanted = match onto_right {
None => default_action(&entry.difference),
Some(true) if entry.difference.left.is_none() => SyncAction::DeleteRight,
Some(true) => SyncAction::ToRight,
Some(false) if entry.difference.right.is_none() => SyncAction::DeleteLeft,
Some(false) => SyncAction::ToLeft,
};
entry.action = if entry.allows(wanted) { wanted } else { SyncAction::Skip };
}
}
pub fn run_sync(&mut self) {
let confirm = self.options.confirm_delete;
let to_trash = self.options.delete_to_trash;
let Some(plan) = self.sync_plan() else {
return;
};
if !plan.done {
return;
}
let [_, _, deleting, _] = plan.counts();
if deleting > 0 && confirm && !plan.armed {
plan.armed = true;
return;
}
let (left, right) = (plan.left.clone(), plan.right.clone());
let steps: Vec<SyncStep> = plan
.entries
.iter()
.filter_map(|entry| {
let (at_left, at_right, is_dir) = (left.join(&entry.difference.path), right.join(&entry.difference.path), entry.is_dir());
match entry.action {
SyncAction::ToRight => Some(SyncStep::Copy(at_left, at_right, is_dir)),
SyncAction::ToLeft => Some(SyncStep::Copy(at_right, at_left, is_dir)),
SyncAction::DeleteLeft => Some(SyncStep::Delete(at_left, is_dir)),
SyncAction::DeleteRight => Some(SyncStep::Delete(at_right, is_dir)),
SyncAction::Skip => None,
}
})
.collect();
self.dialog = None;
if steps.is_empty() {
self.find_note = Some(t!("Nothing to synchronize").to_string());
return;
}
self.start_sync(steps, to_trash);
}
}
pub enum SyncStep {
Copy(PathBuf, PathBuf, bool),
Delete(PathBuf, bool),
}