diff --git a/app/src/app.rs b/app/src/app.rs index 4188d6ab..74870fd1 100644 --- a/app/src/app.rs +++ b/app/src/app.rs @@ -211,8 +211,8 @@ impl App { tasks.file_open(&targets); } } - Event::Progress(percent, left) => { - tasks.progress = (percent, left); + Event::Progress(progress) => { + tasks.progress = progress; emit!(Render); } diff --git a/app/src/status/mod.rs b/app/src/status/mod.rs index e74211c4..03520470 100644 --- a/app/src/status/mod.rs +++ b/app/src/status/mod.rs @@ -1,4 +1,3 @@ mod layout; -mod progress; pub(super) use layout::*; diff --git a/app/src/status/progress.rs b/app/src/status/progress.rs deleted file mode 100644 index 953287ed..00000000 --- a/app/src/status/progress.rs +++ /dev/null @@ -1,30 +0,0 @@ -use core::Ctx; - -use config::THEME; -use ratatui::{buffer::Buffer, layout::Rect, text::Span, widgets::{Gauge, Widget}}; - -pub(super) struct Progress<'a> { - cx: &'a Ctx, -} - -impl<'a> Progress<'a> { - pub(super) fn new(cx: &'a Ctx) -> Self { Self { cx } } -} - -impl<'a> Widget for Progress<'a> { - fn render(self, area: Rect, buf: &mut Buffer) { - let progress = &self.cx.tasks.progress; - if progress.0 >= 100 { - return; - } - - Gauge::default() - .gauge_style(THEME.status.progress_gauge.into()) - .percent(progress.0 as u16) - .label(Span::styled( - format!("{:>3}%, {} left", progress.0, progress.1), - THEME.status.progress_label.into(), - )) - .render(area, buf); - } -} diff --git a/config/preset/theme.toml b/config/preset/theme.toml index 25fcfa7f..999243fd 100644 --- a/config/preset/theme.toml +++ b/config/preset/theme.toml @@ -14,8 +14,9 @@ mode_select = { fg = "#1E1E30", bg = "#D2A4FE", bold = true } mode_unset = { fg = "#1E1E30", bg = "#FFAF80", bold = true } # Progress -progress_label = { fg = "#FFFFFF", bold = true } -progress_gauge = { fg = "#FFA577", bg = "#484D66" } +progress_label = { fg = "#FFFFFF", bold = true } +progress_normal = { fg = "#FFA577", bg = "#484D66" } +progress_error = { fg = "#FF84A9", bg = "#484D66" } # Permissions permissions_t = { fg = "#6D738F" } diff --git a/config/src/theme/status.rs b/config/src/theme/status.rs index 37959d2b..8198b488 100644 --- a/config/src/theme/status.rs +++ b/config/src/theme/status.rs @@ -14,8 +14,9 @@ pub struct Status { pub mode_unset: Style, // Progress - pub progress_label: Style, - pub progress_gauge: Style, + pub progress_label: Style, + pub progress_normal: Style, + pub progress_error: Style, // Permissions pub permissions_t: Style, diff --git a/core/src/event.rs b/core/src/event.rs index 31b7d825..6c4f0cc9 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -7,7 +7,7 @@ use shared::{InputError, RoCell, Url}; use tokio::sync::{mpsc::{self, UnboundedSender}, oneshot}; use super::{files::{File, FilesOp}, input::InputOpt, select::SelectOpt}; -use crate::manager::PreviewLock; +use crate::{manager::PreviewLock, tasks::TasksProgress}; static TX: RoCell> = RoCell::new(); @@ -36,7 +36,7 @@ pub enum Event { // Tasks Open(Vec<(OsString, String)>, Option), - Progress(u8, u32), + Progress(TasksProgress), } impl Event { @@ -115,8 +115,8 @@ macro_rules! emit { (Open($targets:expr, $opener:expr)) => { $crate::Event::Open($targets, $opener).emit(); }; - (Progress($percent:expr, $tasks:expr)) => { - $crate::Event::Progress($percent, $tasks).emit(); + (Progress($progress:expr)) => { + $crate::Event::Progress($progress).emit(); }; ($event:ident) => { diff --git a/core/src/tasks/running.rs b/core/src/tasks/running.rs index 1f38f2b0..882aa31d 100644 --- a/core/src/tasks/running.rs +++ b/core/src/tasks/running.rs @@ -54,7 +54,7 @@ impl Running { match task.stage { TaskStage::Pending => return None, TaskStage::Dispatched => { - if task.processed < task.found { + if task.succ < task.total { return None; } if let Some(hook) = self.hooks.remove(&id) { diff --git a/core/src/tasks/scheduler.rs b/core/src/tasks/scheduler.rs index 3c4a0259..64d39fa1 100644 --- a/core/src/tasks/scheduler.rs +++ b/core/src/tasks/scheduler.rs @@ -1,14 +1,12 @@ use std::{ffi::OsStr, sync::Arc, time::Duration}; -use async_channel::{Receiver, Sender}; use config::{open::Opener, TASKS}; use futures::{future::BoxFuture, FutureExt}; use parking_lot::RwLock; use shared::{unique_path, Throttle, Url}; use tokio::{fs, select, sync::{mpsc::{self, UnboundedReceiver}, oneshot}, time::sleep}; -use tracing::{info, trace}; -use super::{workers::{File, FileOpDelete, FileOpLink, FileOpPaste, FileOpTrash, Precache, PrecacheOpMime, PrecacheOpSize, Process, ProcessOpOpen}, Running, TaskOp, TaskStage}; +use super::{workers::{File, FileOpDelete, FileOpLink, FileOpPaste, FileOpTrash, Precache, PrecacheOpMime, PrecacheOpSize, Process, ProcessOpOpen}, Running, TaskOp, TaskStage, TasksProgress}; use crate::emit; pub struct Scheduler { @@ -16,7 +14,8 @@ pub struct Scheduler { precache: Arc, process: Arc, - todo: Sender>, + todo: async_channel::Sender>, + prog: mpsc::UnboundedSender, pub(super) running: Arc>, } @@ -28,9 +27,10 @@ impl Scheduler { let scheduler = Self { file: Arc::new(File::new(prog_tx.clone())), precache: Arc::new(Precache::new(prog_tx.clone())), - process: Arc::new(Process::new(prog_tx)), + process: Arc::new(Process::new(prog_tx.clone())), todo: todo_tx, + prog: prog_tx, running: Default::default(), }; @@ -44,7 +44,7 @@ impl Scheduler { scheduler } - fn schedule_micro(&self, rx: Receiver>) { + fn schedule_micro(&self, rx: async_channel::Receiver>) { tokio::spawn(async move { loop { if let Ok(fut) = rx.recv().await { @@ -54,9 +54,11 @@ impl Scheduler { }); } - fn schedule_macro(&self, rx: Receiver>) { + fn schedule_macro(&self, rx: async_channel::Receiver>) { let file = self.file.clone(); let precache = self.precache.clone(); + + let prog = self.prog.clone(); let running = self.running.clone(); tokio::spawn(async move { @@ -72,20 +74,18 @@ impl Scheduler { } Ok((id, mut op)) = file.recv() => { if !running.read().exists(id) { - trace!("Skipping task {:?} as it was removed", op); continue; } if let Err(e) = file.work(&mut op).await { - info!("Failed to work on task {:?}: {e}", op); + prog.send(TaskOp::Fail(id, format!("Failed to work on this task: {:?}", e))).ok(); } } Ok((id, mut op)) = precache.recv() => { if !running.read().exists(id) { - trace!("Skipping task {:?} as it was removed", op); continue; } if let Err(e) = precache.work(&mut op).await { - info!("Failed to work on task {:?}: {e}", op); + prog.send(TaskOp::Fail(id, format!("Failed to work on this task: {:?}", e))).ok(); } } } @@ -102,8 +102,36 @@ impl Scheduler { match op { TaskOp::New(id, size) => { if let Some(task) = running.write().get_mut(id) { - task.found += 1; - task.todo += size; + task.total += 1; + task.found += size; + } + } + TaskOp::Adv(id, succ, processed) => { + let mut running = running.write(); + if let Some(task) = running.get_mut(id) { + task.succ += succ; + task.processed += processed; + } + if succ > 0 { + if let Some(fut) = running.try_remove(id, TaskStage::Pending) { + todo.send_blocking(fut).ok(); + } + } + } + TaskOp::Succ(id) => { + if let Some(fut) = running.write().try_remove(id, TaskStage::Dispatched) { + todo.send_blocking(fut).ok(); + } + } + TaskOp::Fail(id, reason) => { + if let Some(task) = running.write().get_mut(id) { + task.fail += 1; + task.logs.push_str(&reason); + task.logs.push('\n'); + + if let Some(logger) = &task.logger { + logger.send(reason).ok(); + } } } TaskOp::Log(id, line) => { @@ -116,62 +144,20 @@ impl Scheduler { } } } - TaskOp::Adv(id, processed, size) => { - let mut running = running.write(); - if let Some(task) = running.get_mut(id) { - task.processed += processed; - task.done += size; - } - if processed > 0 { - if let Some(fut) = running.try_remove(id, TaskStage::Pending) { - todo.send_blocking(fut).ok(); - } - } - } - TaskOp::Done(id) => { - if let Some(fut) = running.write().try_remove(id, TaskStage::Dispatched) { - todo.send_blocking(fut).ok(); - } - } } } }); let running = self.running.clone(); - let mut last = (100, 0); tokio::spawn(async move { + let mut last = TasksProgress::default(); loop { - sleep(Duration::from_secs(1)).await; - if running.read().is_empty() { - if last != (100, 0) { - last = (100, 0); - emit!(Progress(last.0, last.1)); - } - continue; - } + sleep(Duration::from_millis(500)).await; - let mut tasks = 0u32; - let mut left = 0; - let mut progress = (0, 0); - for task in running.read().values() { - tasks += 1; - left += task.found.saturating_sub(task.processed); - progress = (progress.0 + task.done, progress.1 + task.todo); - } - - let mut percent = match progress.1 { - 0 => 100u8, - _ => 100.min(progress.0 * 100 / progress.1) as u8, - }; - - if tasks != 0 { - percent = percent.min(99); - left = left.max(1); - } - - if last != (percent, left) { - last = (percent, left); - emit!(Progress(last.0, last.1)); + let new = TasksProgress::from(&*running.read()); + if last != new { + last = new; + emit!(Progress(new)); } } }); diff --git a/core/src/tasks/task.rs b/core/src/tasks/task.rs index c6010475..ffb7dd0f 100644 --- a/core/src/tasks/task.rs +++ b/core/src/tasks/task.rs @@ -1,49 +1,36 @@ use tokio::sync::mpsc; -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Task { pub id: usize, pub name: String, pub stage: TaskStage, - pub found: u32, - pub processed: u32, + pub total: u32, + pub succ: u32, + pub fail: u32, - pub todo: u64, - pub done: u64, + pub found: u64, + pub processed: u64, pub logs: String, pub logger: Option>, } +impl Task { + pub fn new(id: usize, name: String) -> Self { Self { id, name, ..Default::default() } } +} + #[derive(Debug)] pub struct TaskSummary { pub name: String, - pub found: u32, - pub processed: u32, + pub total: u32, + pub succ: u32, + pub fail: u32, - pub todo: u64, - pub done: u64, -} - -impl Task { - pub fn new(id: usize, name: String) -> Self { - Self { - id, - name, - stage: Default::default(), - - found: 0, - processed: 0, - - todo: 0, - done: 0, - - logs: Default::default(), - logger: Default::default(), - } - } + pub found: u64, + pub processed: u64, } impl From<&Task> for TaskSummary { @@ -51,25 +38,28 @@ impl From<&Task> for TaskSummary { TaskSummary { name: task.name.clone(), + total: task.total, + succ: task.succ, + fail: task.fail, + found: task.found, processed: task.processed, - - todo: task.todo, - done: task.done, } } } #[derive(Debug)] pub enum TaskOp { - // task_id, size + // id, size New(usize, u64), - // task_id, line - Log(usize, String), - // task_id, processed, size + // id, processed, size Adv(usize, u32, u64), - // task_id - Done(usize), + // id + Succ(usize), + // id + Fail(usize, String), + // id, line + Log(usize, String), } #[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] diff --git a/core/src/tasks/tasks.rs b/core/src/tasks/tasks.rs index 158fd9b1..3bddd0d7 100644 --- a/core/src/tasks/tasks.rs +++ b/core/src/tasks/tasks.rs @@ -2,11 +2,12 @@ use std::{collections::{BTreeMap, HashMap, HashSet}, ffi::OsStr, io::{stdout, Wr use config::{manager::SortBy, open::Opener, OPEN}; use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; +use serde::Serialize; use shared::{Defer, MimeKind, Term, Url}; use tokio::{io::{stdin, AsyncReadExt}, select, sync::mpsc, time}; use tracing::trace; -use super::{task::TaskSummary, Scheduler, TASKS_PADDING, TASKS_PERCENT}; +use super::{running::Running, task::TaskSummary, Scheduler, TASKS_PADDING, TASKS_PERCENT}; use crate::{emit, files::{File, Files}, input::InputOpt, Event, BLOCKER}; pub struct Tasks { @@ -14,7 +15,7 @@ pub struct Tasks { pub visible: bool, pub cursor: usize, - pub progress: (u8, u32), + pub progress: TasksProgress, } impl Tasks { @@ -23,7 +24,7 @@ impl Tasks { scheduler: Arc::new(Scheduler::start()), visible: false, cursor: 0, - progress: (100, 0), + progress: Default::default(), } } @@ -305,3 +306,32 @@ impl Tasks { #[inline] pub fn len(&self) -> usize { self.scheduler.running.read().len() } } + +#[derive(Clone, Copy, Default, Eq, PartialEq, Serialize)] +pub struct TasksProgress { + pub total: u32, + pub succ: u32, + pub fail: u32, + + pub found: u64, + pub processed: u64, +} + +impl From<&Running> for TasksProgress { + fn from(running: &Running) -> Self { + let mut progress = Self::default(); + if running.is_empty() { + return progress; + } + + for task in running.values() { + progress.total += task.total; + progress.succ += task.succ; + progress.fail += task.fail; + + progress.found += task.found; + progress.processed += task.processed; + } + progress + } +} diff --git a/core/src/tasks/workers/file.rs b/core/src/tasks/workers/file.rs index f25950ae..86982a9a 100644 --- a/core/src/tasks/workers/file.rs +++ b/core/src/tasks/workers/file.rs @@ -10,8 +10,8 @@ use tracing::trace; use crate::tasks::TaskOp; pub(crate) struct File { - rx: async_channel::Receiver, tx: async_channel::Sender, + rx: async_channel::Receiver, sch: mpsc::UnboundedSender, } @@ -92,10 +92,7 @@ impl File { } break; } - Ok(n) => { - self.log(task.id, format!("Paste task advanced {n}: {:?}", task))?; - self.sch.send(TaskOp::Adv(task.id, 0, n))? - } + Ok(n) => self.sch.send(TaskOp::Adv(task.id, 0, n))?, Err(e) if e.kind() == NotFound => { trace!("Paste task partially done: {:?}", task); break; @@ -163,7 +160,7 @@ impl File { FileOp::Delete(task) => { if let Err(e) = fs::remove_file(&task.target).await { if e.kind() != NotFound && fs::symlink_metadata(&task.target).await.is_ok() { - self.log(task.id, format!("Delete task failed: {:?}, {e}", task))?; + self.fail(task.id, format!("Delete task failed: {:?}, {e}", task))?; Err(e)? } } @@ -187,17 +184,11 @@ impl File { Ok(()) } - #[inline] - fn log(&self, id: usize, line: String) -> Result<()> { Ok(self.sch.send(TaskOp::Log(id, line))?) } - - #[inline] - fn done(&self, id: usize) -> Result<()> { Ok(self.sch.send(TaskOp::Done(id))?) } - pub(crate) async fn paste(&self, mut task: FileOpPaste) -> Result<()> { if task.cut { match fs::rename(&task.from, &task.to).await { - Ok(_) => return self.done(task.id), - Err(e) if e.kind() == NotFound => return self.done(task.id), + Ok(_) => return self.succ(task.id), + Err(e) if e.kind() == NotFound => return self.succ(task.id), _ => {} } } @@ -212,7 +203,20 @@ impl File { } else if meta.is_symlink() { self.tx.send(FileOp::Link(task.to_link(meta))).await?; } - return self.done(id); + return self.succ(id); + } + + macro_rules! continue_unless_ok { + ($result:expr) => { + match $result { + Ok(v) => v, + Err(e) => { + self.sch.send(TaskOp::New(task.id, 0))?; + self.fail(task.id, format!("An error occurred while pasting: {e}"))?; + continue; + } + } + }; } let root = task.to.clone(); @@ -221,27 +225,15 @@ impl File { while let Some(src) = dirs.pop_front() { let dest = root.join(src.components().skip(skip).collect::()); - match fs::create_dir(&dest).await { - Err(e) if e.kind() != AlreadyExists => { - self.log(task.id, format!("Create dir failed: {dest:?}, {e}"))?; - continue; - } - _ => {} - } - - let mut it = match fs::read_dir(&src).await { - Ok(it) => it, - Err(e) => { - self.log(task.id, format!("Read dir failed: {src:?}, {e}"))?; - continue; - } - }; + continue_unless_ok!(match fs::create_dir(&dest).await { + Err(e) if e.kind() != AlreadyExists => Err(e), + _ => Ok(()), + }); + let mut it = continue_unless_ok!(fs::read_dir(&src).await); while let Ok(Some(entry)) = it.next_entry().await { let src = Url::from(entry.path()); - let Ok(meta) = Self::metadata(&src, task.follow).await else { - continue; - }; + let meta = continue_unless_ok!(Self::metadata(&src, task.follow).await); if meta.is_dir() { dirs.push_back(src); @@ -259,7 +251,7 @@ impl File { } } } - self.done(task.id) + self.succ(task.id) } pub(crate) async fn link(&self, mut task: FileOpLink) -> Result<()> { @@ -270,7 +262,7 @@ impl File { self.sch.send(TaskOp::New(id, task.meta.as_ref().unwrap().len()))?; self.tx.send(FileOp::Link(task)).await?; - self.done(id) + self.succ(id) } pub(crate) async fn delete(&self, mut task: FileOpDelete) -> Result<()> { @@ -280,7 +272,7 @@ impl File { task.length = meta.len(); self.sch.send(TaskOp::New(id, meta.len()))?; self.tx.send(FileOp::Delete(task)).await?; - return self.done(id); + return self.succ(id); } let mut dirs = VecDeque::from([task.target]); @@ -307,7 +299,7 @@ impl File { self.tx.send(FileOp::Delete(task.clone())).await?; } } - self.done(task.id) + self.succ(task.id) } pub(crate) async fn trash(&self, mut task: FileOpTrash) -> Result<()> { @@ -316,7 +308,7 @@ impl File { self.sch.send(TaskOp::New(id, task.length))?; self.tx.send(FileOp::Trash(task)).await?; - self.done(id) + self.succ(id) } async fn metadata(path: &Path, follow: bool) -> io::Result { @@ -349,6 +341,19 @@ impl File { } } +impl File { + #[inline] + fn succ(&self, id: usize) -> Result<()> { Ok(self.sch.send(TaskOp::Succ(id))?) } + + #[inline] + fn fail(&self, id: usize, reason: String) -> Result<()> { + Ok(self.sch.send(TaskOp::Fail(id, reason))?) + } + + #[inline] + fn log(&self, id: usize, line: String) -> Result<()> { Ok(self.sch.send(TaskOp::Log(id, line))?) } +} + impl FileOpPaste { fn to_link(&self, meta: Metadata) -> FileOpLink { FileOpLink { diff --git a/core/src/tasks/workers/precache.rs b/core/src/tasks/workers/precache.rs index 244c985c..c63a9abc 100644 --- a/core/src/tasks/workers/precache.rs +++ b/core/src/tasks/workers/precache.rs @@ -10,8 +10,8 @@ use tokio::{fs, sync::mpsc}; use crate::{emit, external, files::FilesOp, tasks::TaskOp}; pub(crate) struct Precache { - rx: async_channel::Receiver, tx: async_channel::Sender, + rx: async_channel::Receiver, sch: mpsc::UnboundedSender, @@ -105,9 +105,6 @@ impl Precache { Ok(()) } - #[inline] - fn done(&self, id: usize) -> Result<()> { Ok(self.sch.send(TaskOp::Done(id))?) } - pub(crate) async fn mime(&self, task: PrecacheOpMime) -> Result<()> { self.sch.send(TaskOp::New(task.id, 0))?; if let Ok(mimes) = external::file(&task.targets).await { @@ -115,7 +112,7 @@ impl Precache { } self.sch.send(TaskOp::Adv(task.id, 1, 0))?; - self.done(task.id) + self.succ(task.id) } pub(crate) async fn size(&self, task: PrecacheOpSize) -> Result<()> { @@ -133,7 +130,7 @@ impl Precache { }); self.sch.send(TaskOp::Adv(task.id, 1, 0))?; - self.done(task.id) + self.succ(task.id) } pub(crate) fn image(&self, id: usize, targets: Vec) -> Result<()> { @@ -141,7 +138,7 @@ impl Precache { self.sch.send(TaskOp::New(id, 0))?; self.tx.send_blocking(PrecacheOp::Image(PrecacheOpImage { id, target }))?; } - self.done(id) + self.succ(id) } pub(crate) fn video(&self, id: usize, targets: Vec) -> Result<()> { @@ -149,7 +146,7 @@ impl Precache { self.sch.send(TaskOp::New(id, 0))?; self.tx.send_blocking(PrecacheOp::Video(PrecacheOpVideo { id, target }))?; } - self.done(id) + self.succ(id) } pub(crate) fn pdf(&self, id: usize, targets: Vec) -> Result<()> { @@ -157,6 +154,11 @@ impl Precache { self.sch.send(TaskOp::New(id, 0))?; self.tx.send_blocking(PrecacheOp::Pdf(PrecacheOpPDF { id, target }))?; } - self.done(id) + self.succ(id) } } + +impl Precache { + #[inline] + fn succ(&self, id: usize) -> Result<()> { Ok(self.sch.send(TaskOp::Succ(id))?) } +} diff --git a/core/src/tasks/workers/process.rs b/core/src/tasks/workers/process.rs index b45c86f7..885aea36 100644 --- a/core/src/tasks/workers/process.rs +++ b/core/src/tasks/workers/process.rs @@ -33,12 +33,6 @@ impl From<&mut ProcessOpOpen> for ShellOpt { impl Process { pub(crate) fn new(sch: mpsc::UnboundedSender) -> Self { Self { sch } } - #[inline] - fn log(&self, id: usize, line: String) -> Result<()> { Ok(self.sch.send(TaskOp::Log(id, line))?) } - - #[inline] - fn done(&self, id: usize) -> Result<()> { Ok(self.sch.send(TaskOp::Done(id))?) } - pub(crate) async fn open(&self, mut task: ProcessOpOpen) -> Result<()> { let opt = ShellOpt::from(&mut task); if task.block { @@ -48,11 +42,11 @@ impl Process { match external::shell(opt) { Ok(mut child) => { child.wait().await.ok(); - self.done(task.id)?; + self.succ(task.id)?; } Err(e) => { self.sch.send(TaskOp::New(task.id, 0))?; - self.log(task.id, format!("Failed to spawn process: {e}"))?; + self.fail(task.id, format!("Failed to spawn process: {e}"))?; } } return Ok(emit!(Stop(false)).await); @@ -60,10 +54,10 @@ impl Process { if task.orphan { match external::shell(opt) { - Ok(_) => self.done(task.id)?, + Ok(_) => self.succ(task.id)?, Err(e) => { self.sch.send(TaskOp::New(task.id, 0))?; - self.log(task.id, format!("Failed to spawn process: {e}"))?; + self.fail(task.id, format!("Failed to spawn process: {e}"))?; } } return Ok(()); @@ -92,7 +86,7 @@ impl Process { None => "Process terminated by signal".to_string(), })?; if !status.success() { - return Ok(()); + return self.fail(task.id, "Process failed".to_string()); } break; } @@ -100,6 +94,19 @@ impl Process { } self.sch.send(TaskOp::Adv(task.id, 1, 0))?; - self.done(task.id) + self.succ(task.id) } } + +impl Process { + #[inline] + fn succ(&self, id: usize) -> Result<()> { Ok(self.sch.send(TaskOp::Succ(id))?) } + + #[inline] + fn fail(&self, id: usize, reason: String) -> Result<()> { + Ok(self.sch.send(TaskOp::Fail(id, reason))?) + } + + #[inline] + fn log(&self, id: usize, line: String) -> Result<()> { Ok(self.sch.send(TaskOp::Log(id, line))?) } +} diff --git a/cspell.json b/cspell.json index 4c8f6379..b1209d6d 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit"],"language":"en","version":"0.2"} +{"language":"en","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ"],"version":"0.2"} diff --git a/plugin/preset/components/status.lua b/plugin/preset/components/status.lua index b4de3af3..3bb5faac 100644 --- a/plugin/preset/components/status.lua +++ b/plugin/preset/components/status.lua @@ -99,6 +99,33 @@ function Status:position() } end +function Status:progress(area, offset) + local progress = cx.tasks.progress + local left = progress.total - progress.succ + if left == 0 then + return {} + end + + local gauge = ui.Gauge(ui.Rect { + x = area.x + math.max(0, area.w - offset - 21), + y = area.y, + w = math.min(20, area.w), + h = 1, + }) + + local percent = 0 + if progress.processed ~= 0 then + percent = math.floor(progress.processed * 100 / progress.found) + end + + return { + gauge + :gauge_style(THEME.status.progress_normal) + :percent(percent) + :label(ui.Span(string.format("%3d%%, %d left", percent, left)):style(THEME.status.progress_label)), + } +end + function Status:render(area) local chunks = ui.Layout() :direction(ui.Direction.HORIZONTAL) @@ -107,5 +134,10 @@ function Status:render(area) local left = ui.Line { self:mode(), self:size(), self:name() } local right = ui.Line { self:permissions(), self:percentage(), self:position() } - return { ui.Paragraph(chunks[1], { left }), ui.Paragraph(chunks[2], { right }):align(ui.Alignment.RIGHT) } + local progress = self:progress(chunks[2], right:width()) + return { + ui.Paragraph(chunks[1], { left }), + ui.Paragraph(chunks[2], { right }):align(ui.Alignment.RIGHT), + table.unpack(progress), + } end diff --git a/plugin/src/components.rs b/plugin/src/components.rs index 0d9c25c1..cbe73459 100644 --- a/plugin/src/components.rs +++ b/plugin/src/components.rs @@ -1,6 +1,6 @@ use mlua::{AnyUserData, Table, TableExt}; -use crate::{layout::{List, Paragraph, Rect}, GLOBALS, LUA}; +use crate::{layout::{Gauge, List, Paragraph, Rect}, GLOBALS, LUA}; #[inline] fn layout(values: Vec, buf: &mut ratatui::prelude::Buffer) -> mlua::Result<()> { @@ -9,6 +9,8 @@ fn layout(values: Vec, buf: &mut ratatui::prelude::Buffer) -> mlua: c.render(buf) } else if let Ok(c) = value.take::() { c.render(buf) + } else if let Ok(c) = value.take::() { + c.render(buf) } } Ok(()) diff --git a/plugin/src/layout/gauge.rs b/plugin/src/layout/gauge.rs new file mode 100644 index 00000000..ce7baf1a --- /dev/null +++ b/plugin/src/layout/gauge.rs @@ -0,0 +1,102 @@ +use mlua::{AnyUserData, FromLua, Lua, Table, UserData, UserDataMethods, Value}; +use ratatui::widgets::Widget; + +use super::{Rect, Span, Style}; +use crate::{GLOBALS, LUA}; + +#[derive(Clone, Default)] +pub(crate) struct Gauge { + area: ratatui::layout::Rect, + + ratio: f64, + label: Option>, + style: Option, + gauge_style: Option, +} + +impl Gauge { + pub(crate) fn install() -> mlua::Result<()> { + let ui: Table = GLOBALS.get("ui")?; + ui.set( + "Gauge", + LUA.create_function(|_, area: Rect| Ok(Gauge { area: area.0, ..Default::default() }))?, + ) + } + + pub(crate) fn render(self, buf: &mut ratatui::buffer::Buffer) { + let mut gauge = ratatui::widgets::Gauge::default(); + + gauge = gauge.ratio(self.ratio); + if let Some(label) = self.label { + gauge = gauge.label(label); + } + if let Some(style) = self.style { + gauge = gauge.style(style); + } + if let Some(gauge_style) = self.gauge_style { + gauge = gauge.gauge_style(gauge_style); + } + + gauge.render(self.area, buf) + } +} + +impl<'lua> FromLua<'lua> for Gauge { + fn from_lua(value: Value<'lua>, _: &'lua Lua) -> mlua::Result { + match value { + Value::UserData(ud) => Ok(ud.borrow::()?.clone()), + _ => Err(mlua::Error::FromLuaConversionError { + from: value.type_name(), + to: "Gauge", + message: Some("expected a Gauge".to_string()), + }), + } + } +} + +impl UserData for Gauge { + fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_function("percent", |_, (ud, percent): (AnyUserData, u8)| { + if percent > 100 { + return Err(mlua::Error::RuntimeError("percent must be between 0 and 100".to_string())); + } + + ud.borrow_mut::()?.ratio = percent as f64 / 100.0; + Ok(ud) + }); + + methods.add_function("ratio", |_, (ud, ratio): (AnyUserData, f64)| { + if !(0.0..1.0).contains(&ratio) { + return Err(mlua::Error::RuntimeError("ratio must be between 0 and 1".to_string())); + } + + ud.borrow_mut::()?.ratio = ratio; + Ok(ud) + }); + + methods.add_function("label", |_, (ud, label): (AnyUserData, Span)| { + ud.borrow_mut::()?.label = Some(label.0); + Ok(ud) + }); + + methods.add_function("style", |_, (ud, value): (AnyUserData, Value)| { + ud.borrow_mut::()?.style = match value { + Value::Nil => None, + Value::Table(tbl) => Some(Style::from(tbl).0), + Value::UserData(ud) => Some(ud.borrow::