This commit is contained in:
sxyazi 2023-10-08 18:34:36 +08:00
parent 5d1591f591
commit 71c86a50c9
No known key found for this signature in database
20 changed files with 336 additions and 205 deletions

View file

@ -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);
}

View file

@ -1,4 +1,3 @@
mod layout;
mod progress;
pub(super) use layout::*;

View file

@ -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);
}
}

View file

@ -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" }

View file

@ -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,

View file

@ -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<UnboundedSender<Event>> = RoCell::new();
@ -36,7 +36,7 @@ pub enum Event {
// Tasks
Open(Vec<(OsString, String)>, Option<Opener>),
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) => {

View file

@ -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) {

View file

@ -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<Precache>,
process: Arc<Process>,
todo: Sender<BoxFuture<'static, ()>>,
todo: async_channel::Sender<BoxFuture<'static, ()>>,
prog: mpsc::UnboundedSender<TaskOp>,
pub(super) running: Arc<RwLock<Running>>,
}
@ -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<BoxFuture<'static, ()>>) {
fn schedule_micro(&self, rx: async_channel::Receiver<BoxFuture<'static, ()>>) {
tokio::spawn(async move {
loop {
if let Ok(fut) = rx.recv().await {
@ -54,9 +54,11 @@ impl Scheduler {
});
}
fn schedule_macro(&self, rx: Receiver<BoxFuture<'static, ()>>) {
fn schedule_macro(&self, rx: async_channel::Receiver<BoxFuture<'static, ()>>) {
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));
}
}
});

View file

@ -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<mpsc::UnboundedSender<String>>,
}
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)]

View file

@ -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
}
}

View file

@ -10,8 +10,8 @@ use tracing::trace;
use crate::tasks::TaskOp;
pub(crate) struct File {
rx: async_channel::Receiver<FileOp>,
tx: async_channel::Sender<FileOp>,
rx: async_channel::Receiver<FileOp>,
sch: mpsc::UnboundedSender<TaskOp>,
}
@ -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::<PathBuf>());
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<Metadata> {
@ -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 {

View file

@ -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<PrecacheOp>,
tx: async_channel::Sender<PrecacheOp>,
rx: async_channel::Receiver<PrecacheOp>,
sch: mpsc::UnboundedSender<TaskOp>,
@ -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<Url>) -> 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<Url>) -> 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<Url>) -> 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))?) }
}

View file

@ -33,12 +33,6 @@ impl From<&mut ProcessOpOpen> for ShellOpt {
impl Process {
pub(crate) fn new(sch: mpsc::UnboundedSender<TaskOp>) -> 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))?) }
}

View file

@ -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"}

View file

@ -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

View file

@ -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<AnyUserData>, buf: &mut ratatui::prelude::Buffer) -> mlua::Result<()> {
@ -9,6 +9,8 @@ fn layout(values: Vec<AnyUserData>, buf: &mut ratatui::prelude::Buffer) -> mlua:
c.render(buf)
} else if let Ok(c) = value.take::<List>() {
c.render(buf)
} else if let Ok(c) = value.take::<Gauge>() {
c.render(buf)
}
}
Ok(())

102
plugin/src/layout/gauge.rs Normal file
View file

@ -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<ratatui::text::Span<'static>>,
style: Option<ratatui::style::Style>,
gauge_style: Option<ratatui::style::Style>,
}
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<Self> {
match value {
Value::UserData(ud) => Ok(ud.borrow::<Self>()?.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::<Self>()?.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::<Self>()?.ratio = ratio;
Ok(ud)
});
methods.add_function("label", |_, (ud, label): (AnyUserData, Span)| {
ud.borrow_mut::<Self>()?.label = Some(label.0);
Ok(ud)
});
methods.add_function("style", |_, (ud, value): (AnyUserData, Value)| {
ud.borrow_mut::<Self>()?.style = match value {
Value::Nil => None,
Value::Table(tbl) => Some(Style::from(tbl).0),
Value::UserData(ud) => Some(ud.borrow::<Style>()?.0),
_ => return Err(mlua::Error::external("expected a Style or Table or nil")),
};
Ok(ud)
});
methods.add_function("gauge_style", |_, (ud, value): (AnyUserData, Value)| {
ud.borrow_mut::<Self>()?.gauge_style = match value {
Value::Nil => None,
Value::Table(tbl) => Some(Style::from(tbl).0),
Value::UserData(ud) => Some(ud.borrow::<Style>()?.0),
_ => return Err(mlua::Error::external("expected a Style or Table or nil")),
};
Ok(ud)
});
}
}

View file

@ -50,6 +50,7 @@ impl<'lua> FromLua<'lua> for Line {
impl UserData for Line {
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("width", |_, ud: AnyUserData| Ok(ud.borrow_mut::<Self>()?.0.width()));
methods.add_function("style", |_, (ud, value): (AnyUserData, Value)| {
{
let mut me = ud.borrow_mut::<Self>()?;

View file

@ -1,6 +1,7 @@
#![allow(clippy::module_inception)]
mod constraint;
mod gauge;
mod layout;
mod line;
mod list;
@ -10,6 +11,7 @@ mod span;
mod style;
pub(super) use constraint::*;
pub(super) use gauge::*;
pub(super) use layout::*;
pub(super) use line::*;
pub(super) use list::*;

View file

@ -29,6 +29,7 @@ pub fn init() {
crate::Config.install()?;
layout::Constraint::install()?;
layout::Gauge::install()?;
layout::Layout::install()?;
layout::Line::install()?;
layout::List::install()?;