use super::*;
use crate::fs_ops::PackFormat;
pub(super) fn open_rename(app_state: &mut AppState) {
if app_state.refuse_in_archive(app_state.is_left_active) {
return;
}
let selected = if app_state.is_left_active { &app_state.selected_left } else { &app_state.selected_right };
if !selected.is_empty() {
let items = chosen_items(app_state).into_iter().map(|(path, _)| path).collect();
app_state.open_multi_rename(items);
return;
}
let children = if app_state.is_left_active { &app_state.children_left } else { &app_state.children_right };
let state = if app_state.is_left_active { &app_state.state_left } else { &app_state.state_right };
let Some(item) = state.selected().and_then(|index| children.get(index)) else {
return;
};
if item.is_parent() {
return;
}
let mut input = TextInput::new();
input.set(item.leaf().to_string());
app_state.dialog = Some(Dialog::Rename(input));
}
pub(super) fn toggle_create(app_state: &mut AppState, is_dir: bool) {
if matches!(app_state.dialog, Some(Dialog::Create { is_dir: making, .. }) if making == is_dir) {
app_state.dialog = None;
return;
}
if app_state.refuse_in_archive(app_state.is_left_active) {
return;
}
app_state.dialog = Some(Dialog::Create { is_dir, input: TextInput::new() });
}
pub(super) fn handle_rename(app_state: &mut AppState) {
let Some(Dialog::Rename(input)) = app_state.dialog.take() else {
return;
};
let parent_path = if app_state.is_left_active { &app_state.dir_left } else { &app_state.dir_right };
let children = if app_state.is_left_active { &app_state.children_left } else { &app_state.children_right };
let state = if app_state.is_left_active { &app_state.state_left } else { &app_state.state_right };
let Some(item) = state.selected().and_then(|index| children.get(index)) else {
return;
};
if item.is_parent() {
return;
}
let new_name = input.text;
if new_name == item.leaf() || new_name.is_empty() {
return;
}
if !is_plain_name(&new_name) {
app_state.display_error(format!("Not a plain file name: {}", new_name));
return;
}
let original_path = item.path_in(parent_path);
let new_path = original_path.with_file_name(&new_name);
let landing = Path::new(&item.name_os).with_file_name(&new_name);
if new_path != original_path && path_exists(&new_path) && !is_same_entry(&original_path, &new_path) {
app_state.display_error(format!("Already exists: {}", new_name));
return;
}
match rename_path(original_path.clone(), new_path.clone()) {
Ok(_) => {
if let Some(size) = app_state.dir_sizes.remove(&original_path) {
app_state.dir_sizes.insert(new_path, size);
}
app_state.reload_panel(app_state.is_left_active, Some(landing.as_os_str()));
}
Err(e) => app_state.display_error(e.to_string()),
}
}
pub(super) fn open_delete(app_state: &mut AppState, permanently: bool) {
let items = chosen_items(app_state);
if items.is_empty() || app_state.refuse_in_archive(app_state.is_left_active) {
return;
}
let to_trash = app_state.options.delete_to_trash && !permanently;
app_state.dialog = Some(Dialog::Delete { items, to_trash });
if !app_state.options.confirm_delete && !permanently {
handle_delete_confirm(app_state);
}
}
pub(super) fn handle_delete_confirm(app_state: &mut AppState) {
if let Some(Dialog::Delete { items, to_trash }) = app_state.dialog.take() {
if to_trash {
app_state.start_trash(items);
} else {
app_state.start_delete(items);
}
}
}
pub(super) fn handle_create_confirm(app_state: &mut AppState) {
let Some(Dialog::Create { is_dir, input }) = app_state.dialog.take() else {
return;
};
let name = input.text;
if name.is_empty() {
return;
}
if !is_plain_name(&name) {
app_state.display_error(format!("Not a plain file name: {}", name));
return;
}
let parent_path = if app_state.is_left_active { &app_state.dir_left } else { &app_state.dir_right };
let path = parent_path.join(&name);
let result = if is_dir { create_directory(path) } else { create_file(path) };
match result {
Ok(_) => app_state.reload_panel(app_state.is_left_active, Some(name.as_ref())),
Err(e) => app_state.display_error(e.to_string()),
}
}
pub(super) fn open_transfer(app_state: &mut AppState, is_copy: bool) {
let sources = chosen_items(app_state);
if sources.is_empty() {
return;
}
if !is_copy && app_state.refuse_in_archive(app_state.is_left_active) {
return;
}
let other = app_state.panel_dir(!app_state.is_left_active);
if !app_state.options.confirm_copy_move {
go_transfer(app_state, &sources, &other, true, is_copy);
return;
}
let (default, into_dir) = match &sources[..] {
[(file, false)] => (other.join(file.file_name().unwrap_or_default()), false),
_ => (other, true),
};
let mut opened_with = default.to_string_lossy().into_owned();
if into_dir && !opened_with.ends_with(std::path::is_separator) {
opened_with.push(std::path::MAIN_SEPARATOR);
}
let mut input = TextInput::new();
input.set(opened_with.clone());
app_state.dialog = Some(Dialog::Transfer { sources, is_copy, input, opened_with, default });
}
pub(super) fn handle_transfer_confirm(app_state: &mut AppState) {
let Some(Dialog::Transfer { sources, is_copy, input, opened_with, default }) = app_state.dialog.take() else {
return;
};
if input.text == opened_with {
match &sources[..] {
[(file, false)] if !opened_with.ends_with(std::path::is_separator) => submit_transfer(app_state, vec![(file.clone(), default, false)], is_copy),
_ => go_transfer(app_state, &sources, &default, true, is_copy),
}
return;
}
if input.text.trim().is_empty() {
return;
}
let base = if app_state.is_left_active { &app_state.dir_left } else { &app_state.dir_right };
let destination = typed_path(&input.text, base);
go_transfer(app_state, &sources, &destination, input.text.ends_with(std::path::is_separator), is_copy);
}
fn go_transfer(app_state: &mut AppState, sources: &[(PathBuf, bool)], destination: &Path, names_dir: bool, is_copy: bool) {
if crate::archive::locate(destination).is_some() {
app_state.display_error(READ_ONLY.to_string());
return;
}
match transfer_targets(sources, destination, names_dir) {
Ok(items) => submit_transfer(app_state, items, is_copy),
Err(message) => app_state.display_error(message),
}
}
fn submit_transfer(app_state: &mut AppState, items: Vec<(PathBuf, PathBuf, bool)>, is_copy: bool) {
if items.iter().any(|(_, dest, _)| dest.parent().is_some_and(|dir| crate::archive::locate(dir).is_some())) {
return app_state.display_error(READ_ONLY.to_string());
}
if let Some(archive) = app_state.panel_archive(app_state.is_left_active).cloned() {
return submit_extract(app_state, archive, items);
}
let taken = match check_destinations(&items) {
Ok(taken) => taken,
Err(message) => {
app_state.display_error(message);
return;
}
};
match app_state.options.on_existing {
OnExisting::Refuse if !taken.is_empty() => app_state.display_error(format!("Destination already exists: {}", taken[0].display())),
OnExisting::Overwrite => app_state.start_transfer(items, is_copy, Clashes::ReplaceAll),
OnExisting::KeepBoth => app_state.start_transfer(items, is_copy, Clashes::KeepBothAll),
OnExisting::Ask | OnExisting::Refuse => app_state.start_transfer(items, is_copy, Clashes::Ask),
}
}
fn submit_extract(app_state: &mut AppState, archive: PathBuf, items: Vec<(PathBuf, PathBuf, bool)>) {
let mut taken = Vec::new();
let mut extracting = Vec::new();
for (source, dest, is_dir) in items {
if let Ok(metadata) = dest.symlink_metadata() {
match (is_dir, metadata.is_dir()) {
(false, true) => return app_state.display_error(format!("Cannot replace directory {} with a file", dest.display())),
(true, false) => return app_state.display_error(format!("Cannot replace file {} with a directory", dest.display())),
_ => taken.push(dest.clone()),
}
}
let Ok(inner) = source.strip_prefix(&archive) else {
continue;
};
extracting.push((inner.to_path_buf(), dest));
}
match app_state.options.on_existing {
OnExisting::Refuse if !taken.is_empty() => app_state.display_error(format!("Destination already exists: {}", taken[0].display())),
OnExisting::Overwrite => app_state.start_extract(archive, extracting, Clashes::ReplaceAll, None),
OnExisting::KeepBoth => app_state.start_extract(archive, extracting, Clashes::KeepBothAll, None),
OnExisting::Ask | OnExisting::Refuse => app_state.start_extract(archive, extracting, Clashes::Ask, None),
}
}
pub(super) fn open_extract_all(app_state: &mut AppState) {
let is_left = app_state.is_left_active;
let archive = match app_state.panel_archive(is_left) {
Some(archive) => archive.clone(),
None => {
let (children, state, dir) = if is_left {
(&app_state.children_left, &app_state.state_left, &app_state.dir_left)
} else {
(&app_state.children_right, &app_state.state_right, &app_state.dir_right)
};
let item = state.selected().and_then(|index| children.get(index));
match item.map(|item| item.path_in(dir)).filter(|path| item.is_some_and(|item| !item.is_dir) && crate::archive::kind_of(path).is_some()) {
Some(path) => path,
None => return app_state.display_error("Put the cursor on an archive to extract it".to_string()),
}
}
};
let other = app_state.panel_dir(!is_left);
if !app_state.options.confirm_copy_move {
return go_extract_all(app_state, archive, &other);
}
let mut text = other.to_string_lossy().into_owned();
if !text.ends_with(std::path::is_separator) {
text.push(std::path::MAIN_SEPARATOR);
}
let mut input = TextInput::new();
input.set(text);
app_state.dialog = Some(Dialog::Extract { archive, input });
}
pub(super) fn handle_extract_confirm(app_state: &mut AppState) {
let Some(Dialog::Extract { archive, input }) = app_state.dialog.take() else {
return;
};
if input.text.trim().is_empty() {
return;
}
let base = app_state.panel_real_dir(app_state.is_left_active);
let destination = typed_path(&input.text, &base);
go_extract_all(app_state, archive, &destination);
}
fn go_extract_all(app_state: &mut AppState, archive: PathBuf, destination: &Path) {
if crate::archive::locate(destination).is_some() {
return app_state.display_error(READ_ONLY.to_string());
}
if !destination.is_dir() {
return app_state.display_error(format!("No such directory: {}", destination.display()));
}
let clashes = match app_state.options.on_existing {
OnExisting::Overwrite => Clashes::ReplaceAll,
OnExisting::KeepBoth => Clashes::KeepBothAll,
OnExisting::Ask | OnExisting::Refuse => Clashes::Ask,
};
app_state.start_extract(archive, vec![(PathBuf::new(), destination.to_path_buf())], clashes, None);
}
pub(super) fn open_pack(app_state: &mut AppState) {
let is_left = app_state.is_left_active;
if app_state.panel_archive(is_left).is_some() {
return app_state.display_error("Copy it out of the archive first: what is packed has to be on the disk".to_string());
}
let sources = chosen_items(app_state);
if sources.is_empty() {
return;
}
let base = match &sources[..] {
[(path, true)] => path.file_name().map(std::ffi::OsStr::to_os_string),
[(path, false)] => path.file_stem().map(std::ffi::OsStr::to_os_string),
_ => app_state.panel_dir(is_left).file_name().map(std::ffi::OsStr::to_os_string),
};
let base = base.map_or_else(|| "archive".to_string(), |name| name.to_string_lossy().into_owned());
let name = format!("{base}{}", PackFormat::Zip.extension());
let mut input = TextInput::new();
input.set(app_state.panel_dir(!is_left).join(name).to_string_lossy().into_owned());
app_state.dialog = Some(Dialog::Pack { sources, input });
}
pub(super) fn cycle_pack_format(app_state: &mut AppState) {
let Some(Dialog::Pack { input, .. }) = &mut app_state.dialog else {
return;
};
input.set(next_pack_format(&input.text));
}
pub(super) fn next_pack_format(name: &str) -> String {
let without = |ending: &str| {
let at = name.len().checked_sub(ending.len())?;
(name.is_char_boundary(at) && name[at..].eq_ignore_ascii_case(ending)).then(|| &name[..at])
};
let mut known: Vec<(usize, PackFormat)> = PackFormat::ALL.iter().copied().enumerate().collect();
known.sort_by_key(|(_, format)| std::cmp::Reverse(format.extension().len()));
match known.into_iter().find_map(|(at, format)| without(format.extension()).map(|front| (at, front))) {
Some((at, front)) => format!("{front}{}", PackFormat::ALL[(at + 1) % PackFormat::ALL.len()].extension()),
None => format!("{name}{}", PackFormat::Zip.extension()),
}
}
pub(super) fn handle_pack_confirm(app_state: &mut AppState) {
let Some(Dialog::Pack { sources, input }) = app_state.dialog.take() else {
return;
};
if input.text.trim().is_empty() {
return;
}
let dest = typed_path(&input.text, &app_state.panel_dir(app_state.is_left_active));
let Some(format) = PackFormat::of(&dest) else {
let endings: Vec<&str> = PackFormat::ALL.iter().map(|format| format.extension()).collect();
return app_state.display_error(format!("Name it as an archive, ending in {}", endings.join(", ")));
};
match dest.parent() {
Some(dir) if crate::archive::locate(dir).is_some() => return app_state.display_error(READ_ONLY.to_string()),
Some(dir) if dir.is_dir() => {}
Some(dir) => return app_state.display_error(format!("No such directory: {}", dir.display())),
None => return app_state.display_error(format!("Cannot pack into {}", dest.display())),
}
let (dest, replacing) = match dest.symlink_metadata() {
Ok(metadata) if metadata.is_dir() => return app_state.display_error(format!("{} is a directory", dest.display())),
Ok(_) if app_state.options.on_existing == OnExisting::Overwrite => (dest, true),
Ok(_) if app_state.options.on_existing == OnExisting::KeepBoth => (crate::fs_ops::free_name(&dest), false),
Ok(_) => return app_state.display_error(format!("Already exists: {} - another name, or Overwrite or Keep both under F11", dest.display())),
Err(_) => (dest, false),
};
app_state.start_pack(sources, dest, format, replacing);
}
pub(super) fn open_checksums(app_state: &mut AppState) {
let is_left = app_state.is_left_active;
if app_state.panel_archive(is_left).is_some() {
return app_state.display_error("Copy it out of the archive first: checksums are worked out of files on the disk".to_string());
}
let sources = chosen_items(app_state);
if let [(path, false)] = &sources[..]
&& crate::checksum::is_sums_file(path)
{
return app_state.start_check(path.clone());
}
if !sources.is_empty() {
let base = app_state.panel_dir(is_left);
app_state.start_checksums(sources, base);
}
}
pub(super) fn checksums_key(app_state: &mut AppState, code: KeyCode) {
let Some(Dialog::Checksums(view)) = &mut app_state.dialog else {
return;
};
match code {
KeyCode::Up => view.scroll = view.scroll.saturating_sub(1),
KeyCode::Down => view.scroll += 1,
KeyCode::PageUp => view.scroll = view.scroll.saturating_sub(view.room),
KeyCode::PageDown => view.scroll += view.room,
KeyCode::Home => view.scroll = 0,
KeyCode::End => view.scroll = usize::MAX,
KeyCode::Char('c' | 'C') => {
let text = view.report.to_copy();
crate::utils::set_system_clipboard(&text);
app_state.find_note = Some(if text.contains('\n') { "Copied the report".to_string() } else { format!("Copied: {text}") });
app_state.clipboard = text;
}
KeyCode::Tab | KeyCode::Right => view.report.step(true),
KeyCode::BackTab | KeyCode::Left => view.report.step(false),
KeyCode::Enter if view.report.shown().is_some() => write_checksums(app_state),
KeyCode::Esc | KeyCode::Enter => app_state.dialog = None,
_ => {}
}
}
fn write_checksums(app_state: &mut AppState) {
let Some(Dialog::Checksums(view)) = &app_state.dialog else {
return;
};
let Some((dest, text)) = view.report.sums_file() else {
return app_state.display_error("Nothing to write: no checksum could be worked out".to_string());
};
let (dest, replacing) = match dest.symlink_metadata() {
Err(_) => (dest, false),
Ok(_) if app_state.options.on_existing == OnExisting::Overwrite => (dest, true),
Ok(_) if app_state.options.on_existing == OnExisting::KeepBoth => (crate::fs_ops::free_name(&dest), false),
Ok(_) => return app_state.display_error(format!("Already exists: {} - Overwrite or Keep both under F11", dest.display())),
};
if let Err(e) = crate::fs_ops::write_whole(&dest, &text, replacing) {
return app_state.display_error(format!("Cannot write {}: {e}", dest.display()));
}
app_state.dialog = None;
for is_left in [true, false] {
if dest.parent() == Some(app_state.panel_dir(is_left).as_path()) {
app_state.reload_panel(is_left, dest.file_name());
}
}
app_state.find_note = Some(format!("Written: {}", dest.file_name().unwrap_or_default().to_string_lossy()));
}
pub(super) fn open_compare(app_state: &mut AppState) {
let is_left = app_state.is_left_active;
let file_at_cursor = |app_state: &AppState, is_left: bool| {
let (children, state, dir) = if is_left {
(&app_state.children_left, &app_state.state_left, &app_state.dir_left)
} else {
(&app_state.children_right, &app_state.state_right, &app_state.dir_right)
};
state.selected().and_then(|index| children.get(index)).filter(|item| !item.is_dir).map(|item| (item.name_os.clone(), item.path_in(dir)))
};
let (children, selected, dir) = if is_left {
(&app_state.children_left, &app_state.selected_left, &app_state.dir_left)
} else {
(&app_state.children_right, &app_state.selected_right, &app_state.dir_right)
};
let selected_files: Vec<PathBuf> = children.iter().filter(|item| !item.is_dir && selected.contains(&item.name_os)).map(|item| item.path_in(dir)).collect();
if let [first, second] = &selected_files[..] {
app_state.compare_anywhere(first.clone(), second.clone());
return;
}
let Some((name, here)) = file_at_cursor(app_state, is_left) else {
app_state.display_error("Put the cursor on a file to compare, or select two".to_string());
return;
};
let (other_children, other_dir) = if is_left { (&app_state.children_right, &app_state.dir_right) } else { (&app_state.children_left, &app_state.dir_left) };
let namesake = other_children.iter().find(|item| !item.is_dir && item.name_os == name).map(|item| item.path_in(other_dir));
let Some(there) = namesake.or_else(|| file_at_cursor(app_state, !is_left).map(|(_, path)| path)) else {
app_state.display_error("Nothing to compare with: the other panel has no file of that name, nor one under its cursor".to_string());
return;
};
if is_left {
app_state.compare_anywhere(here, there);
} else {
app_state.compare_anywhere(there, here);
}
}
pub(super) fn open_symlink(app_state: &mut AppState) {
let sources = chosen_items(app_state);
if sources.is_empty() || app_state.refuse_in_archive(app_state.is_left_active) {
return;
}
let other = app_state.panel_dir(!app_state.is_left_active);
let (default, into_dir) = match &sources[..] {
[(source, _)] => (other.join(source.file_name().unwrap_or_default()), false),
_ => (other, true),
};
let mut opened_with = default.to_string_lossy().into_owned();
if into_dir && !opened_with.ends_with(std::path::is_separator) {
opened_with.push(std::path::MAIN_SEPARATOR);
}
let mut input = TextInput::new();
input.set(opened_with.clone());
app_state.dialog = Some(Dialog::Symlink {
sources,
input,
opened_with,
default,
hard: false,
});
}
pub(super) fn handle_symlink_confirm(app_state: &mut AppState) {
let Some(Dialog::Symlink { sources, input, opened_with, default, hard }) = app_state.dialog.take() else {
return;
};
let links = if input.text == opened_with {
match &sources[..] {
[(source, is_dir)] => Ok(vec![(source.clone(), default, *is_dir)]),
_ => transfer_targets(&sources, &default, true),
}
} else if input.text.trim().is_empty() {
return;
} else {
let base = if app_state.is_left_active { &app_state.dir_left } else { &app_state.dir_right };
transfer_targets(&sources, &typed_path(&input.text, base), input.text.ends_with(std::path::is_separator))
};
if let Ok(links) = &links
&& links.iter().any(|(_, link, _)| link.parent().is_some_and(|dir| crate::archive::locate(dir).is_some()))
{
app_state.display_error(READ_ONLY.to_string());
return;
}
let links = match links {
Ok(links) => links,
Err(message) => {
app_state.display_error(message);
return;
}
};
let mut failed = Vec::new();
for (target, link, is_dir) in &links {
let made = if hard { create_hard_link(target, link, *is_dir) } else { create_symlink(target, link, *is_dir) };
if let Err(e) = made {
failed.push(format!("{}: {}", link.display(), e));
}
}
let landed = match &links[..] {
[(_, link, _)] if failed.is_empty() => link.parent().zip(link.file_name()),
_ => None,
};
for is_left in [true, false] {
let name = landed.filter(|(parent, _)| app_state.panel_dir(is_left) == *parent).map(|(_, name)| name);
app_state.reload_panel(is_left, name);
}
match failed.len() {
0 => {}
1 if links.len() == 1 => app_state.display_error(format!("Cannot link {}", failed[0])),
count => app_state.display_error(format!("Could not link {count} of {}. {}", links.len(), failed[0])),
}
}