Implement pause for operations

This commit is contained in:
Jeremy Soller 2024-11-15 09:47:03 -07:00
parent 8068688f48
commit 2109c8c3d6
No known key found for this signature in database
GPG key ID: D02FD439211AF56F
5 changed files with 212 additions and 115 deletions

View file

@ -30,6 +30,8 @@ size = Size
details = Details
dismiss = Dismiss message
operations-in-progress = {$count} actions in progress ({$percent}%)...
pause = Pause
resume = Resume
# Dialogs

View file

@ -48,10 +48,7 @@ use std::{
num::NonZeroU16,
path::PathBuf,
process,
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
sync::{Arc, Mutex},
time::{self, Instant},
};
use tokio::sync::mpsc;
@ -67,7 +64,7 @@ use crate::{
localize::LANGUAGE_SORTER,
menu, mime_app, mime_icon,
mounter::{MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage, MOUNTERS},
operation::{Operation, ReplaceResult},
operation::{Controller, Operation, ReplaceResult},
spawn_detached::spawn_detached,
tab::{self, HeadingOptions, ItemMetadata, Location, Tab, HOVER_DURATION},
};
@ -314,6 +311,8 @@ pub enum Message {
PendingComplete(u64),
PendingDismiss,
PendingError(u64, String),
PendingPause(u64, bool),
PendingPauseAll(bool),
PendingProgress(u64, f32),
Preview(Option<Entity>),
RescanTrash,
@ -522,7 +521,7 @@ pub struct App {
#[cfg(feature = "notify")]
notification_opt: Option<Arc<Mutex<notify_rust::NotificationHandle>>>,
pending_operation_id: u64,
pending_operations: BTreeMap<u64, (Operation, f32, Arc<AtomicBool>)>,
pending_operations: BTreeMap<u64, (Operation, f32, Controller)>,
progress_operations: BTreeSet<u64>,
complete_operations: BTreeMap<u64, Operation>,
failed_operations: BTreeMap<u64, (Operation, f32, String)>,
@ -705,7 +704,7 @@ impl App {
self.progress_operations.insert(id);
}
self.pending_operations
.insert(id, (operation, 0.0, Arc::new(AtomicBool::new(false))));
.insert(id, (operation, 0.0, Controller::new()));
}
fn remove_window(&mut self, id: &window::Id) {
@ -1226,12 +1225,35 @@ impl App {
if !self.pending_operations.is_empty() {
let mut section = widget::settings::section().title(fl!("pending"));
for (id, (op, progress, _)) in self.pending_operations.iter().rev() {
for (id, (op, progress, controller)) in self.pending_operations.iter().rev() {
section = section.add(widget::column::with_children(vec![
widget::row::with_children(vec![
widget::progress_bar(0.0..=100.0, *progress)
.height(progress_bar_height)
.into(),
if controller.is_paused() {
widget::tooltip(
widget::button::icon(widget::icon::from_name(
"media-playback-start-symbolic",
))
.on_press(Message::PendingPause(*id, false))
.padding(8),
widget::text::body(fl!("resume")),
widget::tooltip::Position::Top,
)
.into()
} else {
widget::tooltip(
widget::button::icon(widget::icon::from_name(
"media-playback-pause-symbolic",
))
.on_press(Message::PendingPause(*id, true))
.padding(8),
widget::text::body(fl!("pause")),
widget::tooltip::Position::Top,
)
.into()
},
widget::tooltip(
widget::button::icon(widget::icon::from_name("window-close-symbolic"))
.on_press(Message::PendingCancel(*id))
@ -2344,14 +2366,14 @@ impl Application for App {
}
}
Message::PendingCancel(id) => {
if let Some((_, _, cancelled)) = self.pending_operations.get(&id) {
cancelled.store(true, Ordering::SeqCst);
if let Some((_, _, controller)) = self.pending_operations.get(&id) {
controller.cancel();
self.progress_operations.remove(&id);
}
}
Message::PendingCancelAll => {
for (id, (_, _, cancelled)) in self.pending_operations.iter() {
cancelled.store(true, Ordering::SeqCst);
for (id, (_, _, controller)) in self.pending_operations.iter() {
controller.cancel();
self.progress_operations.remove(&id);
}
}
@ -2396,17 +2418,43 @@ impl Application for App {
self.progress_operations.clear();
}
Message::PendingError(id, err) => {
if let Some((op, progress, cancelled)) = self.pending_operations.remove(&id) {
if let Some((op, progress, controller)) = self.pending_operations.remove(&id) {
self.failed_operations.insert(id, (op, progress, err));
// Only show dialog if not cancelled
if !cancelled.load(Ordering::SeqCst) {
if !controller.is_cancelled() {
self.dialog_pages.push_back(DialogPage::FailedOperation(id));
}
self.progress_operations.remove(&id);
}
// Close progress notification if all relavent operations are finished
if !self
.pending_operations
.iter()
.any(|(_id, (op, _, _))| op.show_progress_notification())
{
self.progress_operations.clear();
}
// Manually rescan any trash tabs after any operation is completed
return self.rescan_trash();
}
Message::PendingPause(id, pause) => {
if let Some((_, _, controller)) = self.pending_operations.get(&id) {
if pause {
controller.pause();
} else {
controller.unpause();
}
}
}
Message::PendingPauseAll(pause) => {
for (id, (_, _, controller)) in self.pending_operations.iter() {
if pause {
controller.pause();
} else {
controller.unpause();
}
}
}
Message::PendingProgress(id, new_progress) => {
if let Some((_, progress, _)) = self.pending_operations.get_mut(&id) {
*progress = new_progress;
@ -3819,7 +3867,11 @@ impl Application for App {
let mut title = String::new();
let mut total_progress = 0.0;
let mut count = 0;
for (_id, (op, progress, _)) in self.pending_operations.iter() {
let mut all_paused = true;
for (_id, (op, progress, controller)) in self.pending_operations.iter() {
if !controller.is_paused() {
all_paused = false;
}
if op.show_progress_notification() {
if title.is_empty() {
title = op.pending_text(*progress as i32);
@ -3851,6 +3903,29 @@ impl Application for App {
let container = widget::layer_container(widget::column::with_children(vec![
widget::row::with_children(vec![
progress_bar.into(),
if all_paused {
widget::tooltip(
widget::button::icon(widget::icon::from_name(
"media-playback-start-symbolic",
))
.on_press(Message::PendingPauseAll(false))
.padding(8),
widget::text::body(fl!("resume")),
widget::tooltip::Position::Top,
)
.into()
} else {
widget::tooltip(
widget::button::icon(widget::icon::from_name(
"media-playback-pause-symbolic",
))
.on_press(Message::PendingPauseAll(true))
.padding(8),
widget::text::body(fl!("pause")),
widget::tooltip::Position::Top,
)
.into()
},
widget::tooltip(
widget::button::icon(widget::icon::from_name("window-close-symbolic"))
.on_press(Message::PendingCancelAll)

View file

@ -4,12 +4,9 @@ use std::{
fs,
io::{self, Read, Write},
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
sync::{Arc, Condvar, Mutex},
};
use tokio::sync::{mpsc, Mutex};
use tokio::sync::{mpsc, Mutex as TokioMutex};
use walkdir::WalkDir;
use self::reader::OpReader;
@ -27,7 +24,7 @@ pub mod reader;
pub mod recursive;
fn handle_replace(
msg_tx: &Arc<Mutex<Sender<Message>>>,
msg_tx: &Arc<TokioMutex<Sender<Message>>>,
file_from: PathBuf,
file_to: PathBuf,
multiple: bool,
@ -81,8 +78,8 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
archive: &mut zip::ZipArchive<R>,
directory: P,
id: u64,
msg_tx: Arc<Mutex<Sender<Message>>>,
cancelled: Arc<AtomicBool>,
msg_tx: Arc<TokioMutex<Sender<Message>>>,
controller: Controller,
) -> zip::result::ZipResult<()> {
use std::{ffi::OsString, fs};
use zip::result::ZipError;
@ -108,9 +105,9 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
let mut buffer = vec![0; 4 * 1024 * 1024];
let total_files = archive.len();
for i in 0..total_files {
if cancelled.load(Ordering::SeqCst) {
return Err(io::Error::new(io::ErrorKind::Other, fl!("cancelled")).into());
}
controller
.check()
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
executor::block_on(async {
let total_progress = (i as f32) / total_files as f32;
@ -179,9 +176,9 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
let mut outfile = fs::File::create(&outpath)?;
let mut current = 0;
loop {
if cancelled.load(Ordering::SeqCst) {
return Err(io::Error::new(io::ErrorKind::Other, fl!("cancelled")).into());
}
controller
.check()
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
let count = file.read(&mut buffer)?;
if count == 0 {
@ -227,6 +224,72 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
Ok(())
}
struct ControllerInner {
state: Mutex<u32>,
condvar: Condvar,
}
#[derive(Clone)]
pub struct Controller {
inner: Arc<ControllerInner>,
}
impl Controller {
const RUNNING: u32 = 0;
const PAUSED: u32 = 1;
const CANCELLED: u32 = 2;
pub fn new() -> Self {
Self {
inner: Arc::new(ControllerInner {
state: Mutex::new(Self::RUNNING),
condvar: Condvar::new(),
}),
}
}
pub fn check(&self) -> Result<(), String> {
let mut state = self.inner.state.lock().unwrap();
loop {
if *state == Self::CANCELLED {
return Err(fl!("cancelled"));
} else if *state == Self::PAUSED {
state = self.inner.condvar.wait(state).unwrap();
} else {
return Ok(());
}
}
}
pub fn is_cancelled(&self) -> bool {
let state = self.inner.state.lock().unwrap();
*state == Self::CANCELLED
}
pub fn cancel(&self) {
let mut state = self.inner.state.lock().unwrap();
*state = Self::CANCELLED;
self.inner.condvar.notify_all();
}
pub fn is_paused(&self) -> bool {
let state = self.inner.state.lock().unwrap();
*state == Self::PAUSED
}
pub fn pause(&self) {
let mut state = self.inner.state.lock().unwrap();
*state = Self::PAUSED;
self.inner.condvar.notify_all();
}
pub fn unpause(&self) {
let mut state = self.inner.state.lock().unwrap();
*state = Self::RUNNING;
self.inner.condvar.notify_all();
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ReplaceResult {
Replace(bool),
@ -289,8 +352,8 @@ async fn copy_or_move(
to: PathBuf,
moving: bool,
id: u64,
msg_tx: &Arc<Mutex<Sender<Message>>>,
cancelled: Arc<AtomicBool>,
msg_tx: &Arc<TokioMutex<Sender<Message>>>,
controller: Controller,
) -> Result<(), String> {
let msg_tx = msg_tx.clone();
tokio::task::spawn_blocking(move || -> Result<(), String> {
@ -321,7 +384,7 @@ async fn copy_or_move(
})
.collect();
let mut context = Context::new(cancelled);
let mut context = Context::new(controller);
{
let msg_tx = msg_tx.clone();
@ -605,8 +668,8 @@ impl Operation {
pub async fn perform(
self,
id: u64,
msg_tx: &Arc<Mutex<Sender<Message>>>,
cancelled: Arc<AtomicBool>,
msg_tx: &Arc<TokioMutex<Sender<Message>>>,
controller: Controller,
) -> Result<(), String> {
let _ = msg_tx
.lock()
@ -650,9 +713,7 @@ impl Operation {
let total_paths = paths.len();
for (i, path) in paths.iter().enumerate() {
if cancelled.load(Ordering::SeqCst) {
return Err(fl!("cancelled"));
}
controller.check()?;
executor::block_on(async {
let total_progress = (i as f32) / total_paths as f32;
@ -683,9 +744,7 @@ impl Operation {
let total_paths = paths.len();
let mut buffer = vec![0; 4 * 1024 * 1024];
for (i, path) in paths.iter().enumerate() {
if cancelled.load(Ordering::SeqCst) {
return Err(fl!("cancelled"));
}
controller.check()?;
executor::block_on(async {
let total_progress = (i as f32) / total_paths as f32;
@ -719,9 +778,7 @@ impl Operation {
.map_err(err_str)?;
let mut current = 0;
loop {
if cancelled.load(Ordering::SeqCst) {
return Err(fl!("cancelled"));
}
controller.check()?;
let count = file.read(&mut buffer).map_err(err_str)?;
if count == 0 {
@ -763,14 +820,12 @@ impl Operation {
.map_err(err_str)?;
}
Self::Copy { paths, to } => {
copy_or_move(paths, to, false, id, msg_tx, cancelled).await?;
copy_or_move(paths, to, false, id, msg_tx, controller).await?;
}
Self::Delete { paths } => {
let total = paths.len();
for (i, path) in paths.into_iter().enumerate() {
if cancelled.load(Ordering::SeqCst) {
return Err(fl!("cancelled"));
}
controller.check()?;
let _ = msg_tx
.lock()
@ -804,9 +859,7 @@ impl Operation {
let items = trash::os_limited::list().map_err(err_str)?;
let count = items.len();
for (i, item) in items.into_iter().enumerate() {
if cancelled.load(Ordering::SeqCst) {
return Err(fl!("cancelled"));
}
controller.check()?;
executor::block_on(async {
let total_progress = i as f32 / count as f32;
@ -830,9 +883,7 @@ impl Operation {
tokio::task::spawn_blocking(move || -> Result<(), String> {
let total_paths = paths.len();
for (i, path) in paths.iter().enumerate() {
if cancelled.load(Ordering::SeqCst) {
return Err(fl!("cancelled"));
}
controller.check()?;
executor::block_on(async {
let total_progress = (i as f32) / total_paths as f32;
@ -856,18 +907,18 @@ impl Operation {
}
let msg_tx = msg_tx.clone();
let cancelled = cancelled.clone();
let controller = controller.clone();
let mime = mime_for_path(&path);
match mime.essence_str() {
"application/gzip" | "application/x-compressed-tar" => {
OpReader::new(path, id, msg_tx, cancelled)
OpReader::new(path, id, msg_tx, controller)
.map(io::BufReader::new)
.map(flate2::read::GzDecoder::new)
.map(tar::Archive::new)
.and_then(|mut archive| archive.unpack(&new_dir))
.map_err(err_str)?
}
"application/x-tar" => OpReader::new(path, id, msg_tx, cancelled)
"application/x-tar" => OpReader::new(path, id, msg_tx, controller)
.map(io::BufReader::new)
.map(tar::Archive::new)
.and_then(|mut archive| archive.unpack(&new_dir))
@ -877,12 +928,12 @@ impl Operation {
.map(zip::ZipArchive::new)
.map_err(err_str)?
.and_then(move |mut archive| {
zip_extract(&mut archive, &new_dir, id, msg_tx, cancelled)
zip_extract(&mut archive, &new_dir, id, msg_tx, controller)
})
.map_err(err_str)?,
#[cfg(feature = "bzip2")]
"application/x-bzip" | "application/x-bzip-compressed-tar" => {
OpReader::new(path, id, msg_tx, cancelled)
OpReader::new(path, id, msg_tx, controller)
.map(io::BufReader::new)
.map(bzip2::read::BzDecoder::new)
.map(tar::Archive::new)
@ -891,7 +942,7 @@ impl Operation {
}
#[cfg(feature = "liblzma")]
"application/x-xz" | "application/x-xz-compressed-tar" => {
OpReader::new(path, id, msg_tx, cancelled)
OpReader::new(path, id, msg_tx, controller)
.map(io::BufReader::new)
.map(liblzma::read::XzDecoder::new)
.map(tar::Archive::new)
@ -910,12 +961,10 @@ impl Operation {
.map_err(err_str)?;
}
Self::Move { paths, to } => {
copy_or_move(paths, to, true, id, msg_tx, cancelled).await?;
copy_or_move(paths, to, true, id, msg_tx, controller).await?;
}
Self::NewFolder { path } => {
if cancelled.load(Ordering::SeqCst) {
return Err(fl!("cancelled"));
}
controller.check()?;
tokio::task::spawn_blocking(|| fs::create_dir(path))
.await
@ -923,9 +972,7 @@ impl Operation {
.map_err(err_str)?;
}
Self::NewFile { path } => {
if cancelled.load(Ordering::SeqCst) {
return Err(fl!("cancelled"));
}
controller.check()?;
tokio::task::spawn_blocking(|| fs::File::create(path))
.await
@ -933,9 +980,7 @@ impl Operation {
.map_err(err_str)?;
}
Self::Rename { from, to } => {
if cancelled.load(Ordering::SeqCst) {
return Err(fl!("cancelled"));
}
controller.check()?;
tokio::task::spawn_blocking(|| fs::rename(from, to))
.await
@ -951,9 +996,7 @@ impl Operation {
Self::Restore { paths } => {
let total = paths.len();
for (i, path) in paths.into_iter().enumerate() {
if cancelled.load(Ordering::SeqCst) {
return Err(fl!("cancelled"));
}
controller.check()?;
let _ = msg_tx
.lock()
@ -977,9 +1020,7 @@ impl Operation {
{
use std::os::unix::fs::PermissionsExt;
if cancelled.load(Ordering::SeqCst) {
return Err(fl!("cancelled"));
}
controller.check()?;
let mut perms = fs::metadata(&path).map_err(err_str)?.permissions();
let current_mode = perms.mode();
@ -988,9 +1029,7 @@ impl Operation {
fs::set_permissions(&path, perms).map_err(err_str)?;
}
if cancelled.load(Ordering::SeqCst) {
return Err(fl!("cancelled"));
}
controller.check()?;
let mut command = std::process::Command::new(path);
spawn_detached(&mut command).map_err(err_str)?;
@ -1052,7 +1091,7 @@ mod tests {
paths: paths_clone,
to: to_clone,
}
.perform(id, &sync::Mutex::new(tx).into())
.perform(id, &Mutex::new(tx).into())
.await
});

View file

@ -1,15 +1,9 @@
use cosmic::iced::futures::{channel::mpsc::Sender, executor, SinkExt};
use std::{
fs, io,
path::Path,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use std::{fs, io, path::Path, sync::Arc};
use tokio::sync::Mutex;
use crate::{app::Message, fl};
use super::Controller;
use crate::app::Message;
// Special reader just for operations, handling cancel and progress
pub struct OpReader {
@ -18,7 +12,7 @@ pub struct OpReader {
current: u64,
id: u64,
msg_tx: Arc<Mutex<Sender<Message>>>,
cancelled: Arc<AtomicBool>,
controller: Controller,
}
impl OpReader {
@ -26,7 +20,7 @@ impl OpReader {
path: P,
id: u64,
msg_tx: Arc<Mutex<Sender<Message>>>,
cancelled: Arc<AtomicBool>,
controller: Controller,
) -> io::Result<Self> {
let file = fs::File::open(&path)?;
let metadata = file.metadata()?;
@ -36,16 +30,16 @@ impl OpReader {
current: 0,
id,
msg_tx,
cancelled,
controller,
})
}
}
impl io::Read for OpReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.cancelled.load(Ordering::SeqCst) {
return Err(io::Error::new(io::ErrorKind::Other, fl!("cancelled")));
}
self.controller
.check()
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
let count = self.file.read(buf)?;
self.current += count as u64;

View file

@ -4,29 +4,24 @@ use std::{
io::{Read, Write},
ops::ControlFlow,
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use walkdir::WalkDir;
use super::{copy_unique_path, ReplaceResult};
use crate::fl;
use super::{copy_unique_path, Controller, ReplaceResult};
pub struct Context {
buf: Vec<u8>,
cancelled: Arc<AtomicBool>,
controller: Controller,
on_progress: Box<dyn Fn(&Op, &Progress) + 'static>,
on_replace: Box<dyn Fn(&Op) -> ReplaceResult + 'static>,
replace_result_opt: Option<ReplaceResult>,
}
impl Context {
pub fn new(cancelled: Arc<AtomicBool>) -> Self {
pub fn new(controller: Controller) -> Self {
Self {
buf: vec![0; 4 * 1024 * 1024],
cancelled,
controller,
on_progress: Box::new(|_op, _progress| {}),
on_replace: Box::new(|_op| ReplaceResult::Cancel),
replace_result_opt: None,
@ -41,9 +36,7 @@ impl Context {
let mut ops = Vec::new();
let mut cleanup_ops = Vec::new();
for (from_parent, to_parent) in from_to_pairs {
if self.cancelled.load(Ordering::SeqCst) {
return Err(fl!("cancelled"));
}
self.controller.check()?;
if from_parent == to_parent {
// Skip matching source and destination
@ -51,9 +44,7 @@ impl Context {
}
for entry in WalkDir::new(&from_parent).into_iter() {
if self.cancelled.load(Ordering::SeqCst) {
return Err(fl!("cancelled"));
}
self.controller.check()?;
let entry = entry.map_err(|err| {
format!("failed to walk directory {:?}: {}", from_parent, err)
@ -106,9 +97,7 @@ impl Context {
let total_ops = ops.len();
for (current_ops, mut op) in ops.into_iter().enumerate() {
if self.cancelled.load(Ordering::SeqCst) {
return Err(fl!("cancelled"));
}
self.controller.check()?;
let progress = Progress {
current_ops,
@ -234,9 +223,7 @@ impl Op {
.open(&self.to)?;
to_file.set_permissions(metadata.permissions())?;
loop {
if ctx.cancelled.load(Ordering::SeqCst) {
return Err(fl!("cancelled").into());
}
ctx.controller.check()?;
let count = from_file.read(&mut ctx.buf)?;
if count == 0 {