From 4008ca2e0483583e8d01b01d3983543305304b62 Mon Sep 17 00:00:00 2001 From: Chris Schade Date: Thu, 6 Jun 2024 07:55:31 +0200 Subject: [PATCH] feat: introduce confirm popup Confirm popup that takes a '\n' separated string message renders a popup requesting user for confirmation The message is displayed as a Paragraph with vertical scrollbar (if needed) - Lua binding: ya.confrm(title, message, position) - Position::try_from now parses "h" value (default 3) --- yazi-config/preset/keymap.toml | 21 ++++++++ yazi-config/preset/yazi.toml | 21 ++++---- yazi-config/src/keymap/keymap.rs | 6 +++ yazi-config/src/lib.rs | 2 + yazi-config/src/popup/confirm.rs | 33 ++++++++++++ yazi-config/src/popup/input.rs | 10 ---- yazi-config/src/popup/mod.rs | 2 + yazi-config/src/popup/options.rs | 49 ++++++++++-------- yazi-core/src/confirm/commands/arrow.rs | 38 ++++++++++++++ yazi-core/src/confirm/commands/close.rs | 27 ++++++++++ yazi-core/src/confirm/commands/mod.rs | 3 ++ yazi-core/src/confirm/commands/show.rs | 36 +++++++++++++ yazi-core/src/confirm/confirm.rs | 34 +++++++++++++ yazi-core/src/confirm/mod.rs | 4 ++ yazi-core/src/lib.rs | 1 + yazi-core/src/manager/commands/remove.rs | 16 +++--- yazi-fm/src/confirm/confirm.rs | 64 ++++++++++++++++++++++++ yazi-fm/src/confirm/mod.rs | 3 ++ yazi-fm/src/context.rs | 4 +- yazi-fm/src/executor.rs | 15 ++++++ yazi-fm/src/main.rs | 1 + yazi-fm/src/root.rs | 6 ++- yazi-fm/src/router.rs | 2 + yazi-plugin/src/bindings/confirm.rs | 37 ++++++++++++++ yazi-plugin/src/bindings/position.rs | 2 +- yazi-plugin/src/utils/layer.rs | 21 +++++++- yazi-proxy/src/confirm.rs | 14 ++++++ yazi-proxy/src/lib.rs | 2 + yazi-shared/src/layer.rs | 3 ++ 29 files changed, 423 insertions(+), 54 deletions(-) create mode 100644 yazi-config/src/popup/confirm.rs create mode 100644 yazi-core/src/confirm/commands/arrow.rs create mode 100644 yazi-core/src/confirm/commands/close.rs create mode 100644 yazi-core/src/confirm/commands/mod.rs create mode 100644 yazi-core/src/confirm/commands/show.rs create mode 100644 yazi-core/src/confirm/confirm.rs create mode 100644 yazi-core/src/confirm/mod.rs create mode 100644 yazi-fm/src/confirm/confirm.rs create mode 100644 yazi-fm/src/confirm/mod.rs create mode 100644 yazi-plugin/src/bindings/confirm.rs create mode 100644 yazi-proxy/src/confirm.rs diff --git a/yazi-config/preset/keymap.toml b/yazi-config/preset/keymap.toml index 9e4c0493..b257d19d 100644 --- a/yazi-config/preset/keymap.toml +++ b/yazi-config/preset/keymap.toml @@ -263,6 +263,27 @@ keymap = [ { on = [ "~" ], run = "help", desc = "Open help" } ] +[confirm] +keymap = [ + { on = [ "" ], run = "close", desc = "Cancel the confirm" }, + { on = [ "n" ], run = "close", desc = "Cancel the confirm" }, + { on = [ "y" ], run = "close --submit", desc = "Submit the confirm" }, + + { on = [ "k" ], run = "arrow -1", desc = "Move cursor up" }, + { on = [ "j" ], run = "arrow 1", desc = "Move cursor down" }, + + { on = [ "K" ], run = "arrow -5", desc = "Move cursor up 5 lines" }, + { on = [ "J" ], run = "arrow 5", desc = "Move cursor down 5 lines" }, + + { on = [ "" ], run = "arrow -1", desc = "Move cursor up" }, + { on = [ "" ], run = "arrow 1", desc = "Move cursor down" }, + + { on = [ "" ], run = "arrow -5", desc = "Move cursor up 5 lines" }, + { on = [ "" ], run = "arrow 5", desc = "Move cursor down 5 lines" }, + + { on = [ "~" ], run = "help", desc = "Open help" } +] + [completion] keymap = [ diff --git a/yazi-config/preset/yazi.toml b/yazi-config/preset/yazi.toml index db6f72df..f436cc50 100644 --- a/yazi-config/preset/yazi.toml +++ b/yazi-config/preset/yazi.toml @@ -139,16 +139,6 @@ rename_title = "Rename:" rename_origin = "hovered" rename_offset = [ 0, 1, 50, 3 ] -# trash -trash_title = "Move {n} selected file{s} to trash? (y/N)" -trash_origin = "top-center" -trash_offset = [ 0, 2, 50, 3 ] - -# delete -delete_title = "Delete {n} selected file{s} permanently? (y/N)" -delete_origin = "top-center" -delete_offset = [ 0, 2, 50, 3 ] - # filter filter_title = "Filter:" filter_origin = "top-center" @@ -179,6 +169,17 @@ quit_title = "{n} task{s} running, sure to quit? (y/N)" quit_origin = "top-center" quit_offset = [ 0, 2, 50, 3 ] +[confirm] +# trash +trash_title = "Move {n} selected file{s} to trash? (y/N)" +trash_origin = "top-center" +trash_offset = [ 0, 5, 70, 20 ] + +# delete +delete_title = "Delete {n} selected file{s} permanently? (y/N)" +delete_origin = "top-center" +delete_offset = [ 0, 5, 70, 20 ] + [select] open_title = "Open with:" open_origin = "hovered" diff --git a/yazi-config/src/keymap/keymap.rs b/yazi-config/src/keymap/keymap.rs index f5542281..c92ae526 100644 --- a/yazi-config/src/keymap/keymap.rs +++ b/yazi-config/src/keymap/keymap.rs @@ -12,6 +12,7 @@ pub struct Keymap { pub tasks: Vec, pub select: Vec, pub input: Vec, + pub confirm: Vec, pub help: Vec, pub completion: Vec, } @@ -25,6 +26,7 @@ impl Keymap { Layer::Tasks => &self.tasks, Layer::Select => &self.select, Layer::Input => &self.input, + Layer::Confirm => &self.confirm, Layer::Help => &self.help, Layer::Completion => &self.completion, Layer::Which => unreachable!(), @@ -49,6 +51,7 @@ impl<'de> Deserialize<'de> for Keymap { tasks: Inner, select: Inner, input: Inner, + confirm: Inner, help: Inner, completion: Inner, } @@ -72,6 +75,8 @@ impl<'de> Deserialize<'de> for Keymap { #[rustfmt::skip] Preset::mix(&mut shadow.input.keymap, shadow.input.prepend_keymap, shadow.input.append_keymap); #[rustfmt::skip] + Preset::mix(&mut shadow.confirm.keymap, shadow.confirm.prepend_keymap, shadow.confirm.append_keymap); + #[rustfmt::skip] Preset::mix(&mut shadow.help.keymap, shadow.help.prepend_keymap, shadow.help.append_keymap); #[rustfmt::skip] Preset::mix(&mut shadow.completion.keymap, shadow.completion.prepend_keymap, shadow.completion.append_keymap); @@ -81,6 +86,7 @@ impl<'de> Deserialize<'de> for Keymap { tasks: shadow.tasks.keymap, select: shadow.select.keymap, input: shadow.input.keymap, + confirm: shadow.confirm.keymap, help: shadow.help.keymap, completion: shadow.completion.keymap, }) diff --git a/yazi-config/src/lib.rs b/yazi-config/src/lib.rs index a715d969..facd6827 100644 --- a/yazi-config/src/lib.rs +++ b/yazi-config/src/lib.rs @@ -35,6 +35,7 @@ pub static PREVIEW: RoCell = RoCell::new(); pub static TASKS: RoCell = RoCell::new(); pub static THEME: RoCell = RoCell::new(); pub static INPUT: RoCell = RoCell::new(); +pub static CONFIRM: RoCell = RoCell::new(); pub static SELECT: RoCell = RoCell::new(); pub static WHICH: RoCell = RoCell::new(); @@ -55,6 +56,7 @@ pub fn init() -> anyhow::Result<()> { TASKS.init(<_>::from_str(yazi_toml)?); THEME.init(<_>::from_str(theme_toml)?); INPUT.init(<_>::from_str(yazi_toml)?); + CONFIRM.init(<>::from_str(yazi_toml)?); SELECT.init(<_>::from_str(yazi_toml)?); WHICH.init(<_>::from_str(yazi_toml)?); diff --git a/yazi-config/src/popup/confirm.rs b/yazi-config/src/popup/confirm.rs new file mode 100644 index 00000000..65e69820 --- /dev/null +++ b/yazi-config/src/popup/confirm.rs @@ -0,0 +1,33 @@ +use serde::Deserialize; + +use super::{Offset, Origin}; +use crate::MERGED_YAZI; + +#[derive(Deserialize)] +pub struct Confirm { + // trash + pub trash_title: String, + pub trash_origin: Origin, + pub trash_offset: Offset, + + // delete + pub delete_title: String, + pub delete_origin: Origin, + pub delete_offset: Offset, +} + +impl Default for Confirm { + fn default() -> Self { + #[derive(Deserialize)] + struct Outer { + confirm: Confirm, + } + + toml::from_str::(&MERGED_YAZI).unwrap().confirm + } +} + +impl Confirm { + #[inline] + pub const fn border(&self) -> u16 { 2 } +} diff --git a/yazi-config/src/popup/input.rs b/yazi-config/src/popup/input.rs index 9ec27cdc..29bbd57e 100644 --- a/yazi-config/src/popup/input.rs +++ b/yazi-config/src/popup/input.rs @@ -23,16 +23,6 @@ pub struct Input { pub rename_origin: Origin, pub rename_offset: Offset, - // trash - pub trash_title: String, - pub trash_origin: Origin, - pub trash_offset: Offset, - - // delete - pub delete_title: String, - pub delete_origin: Origin, - pub delete_offset: Offset, - // filter pub filter_title: String, pub filter_origin: Origin, diff --git a/yazi-config/src/popup/mod.rs b/yazi-config/src/popup/mod.rs index 8308b911..2bcca1b2 100644 --- a/yazi-config/src/popup/mod.rs +++ b/yazi-config/src/popup/mod.rs @@ -1,3 +1,4 @@ +mod confirm; mod input; mod offset; mod options; @@ -5,6 +6,7 @@ mod origin; mod position; mod select; +pub use confirm::*; pub use input::*; pub use offset::*; pub use options::*; diff --git a/yazi-config/src/popup/options.rs b/yazi-config/src/popup/options.rs index 2c7e72c6..d15e7bfa 100644 --- a/yazi-config/src/popup/options.rs +++ b/yazi-config/src/popup/options.rs @@ -1,5 +1,5 @@ use super::{Offset, Position}; -use crate::{INPUT, SELECT}; +use crate::{CONFIRM, INPUT, SELECT}; #[derive(Default)] pub struct InputCfg { @@ -19,6 +19,13 @@ pub struct SelectCfg { pub position: Position, } +#[derive(Default)] +pub struct ConfirmCfg { + pub title: String, + pub message: String, + pub position: Position, +} + impl InputCfg { #[inline] pub fn cd() -> Self { @@ -48,26 +55,6 @@ impl InputCfg { } } - #[inline] - pub fn trash(n: usize) -> Self { - let title = INPUT.trash_title.replace("{n}", &n.to_string()); - Self { - title: title.replace("{s}", if n > 1 { "s" } else { "" }), - position: Position::new(INPUT.trash_origin, INPUT.trash_offset), - ..Default::default() - } - } - - #[inline] - pub fn delete(n: usize) -> Self { - let title = INPUT.delete_title.replace("{n}", &n.to_string()); - Self { - title: title.replace("{s}", if n > 1 { "s" } else { "" }), - position: Position::new(INPUT.delete_origin, INPUT.delete_offset), - ..Default::default() - } - } - #[inline] pub fn filter() -> Self { Self { @@ -139,6 +126,26 @@ impl InputCfg { } } +impl ConfirmCfg { + #[inline] + pub fn delete(targets: &[yazi_shared::fs::Url]) -> Self { + Self { + title: CONFIRM.delete_title.replace("{n}", &targets.len().to_string()), + position: Position::new(CONFIRM.delete_origin, CONFIRM.delete_offset), + message: targets.iter().map(|t| t.to_string()).collect::>().join("\n"), + } + } + + #[inline] + pub fn trash(targets: &[yazi_shared::fs::Url]) -> Self { + Self { + title: CONFIRM.trash_title.replace("{n}", &targets.len().to_string()), + position: Position::new(CONFIRM.trash_origin, CONFIRM.trash_offset), + message: targets.iter().map(|t| t.to_string()).collect::>().join("\n"), + } + } +} + impl SelectCfg { #[inline] fn max_height(len: usize) -> u16 { diff --git a/yazi-core/src/confirm/commands/arrow.rs b/yazi-core/src/confirm/commands/arrow.rs new file mode 100644 index 00000000..dcc4d801 --- /dev/null +++ b/yazi-core/src/confirm/commands/arrow.rs @@ -0,0 +1,38 @@ +use yazi_shared::{event::{Cmd, Data}, render}; + +use crate::confirm::Confirm; + +pub struct Opt { + step: isize, +} + +impl From for Opt { + fn from(c: Cmd) -> Self { Self { step: c.first().and_then(Data::as_isize).unwrap_or(0) } } +} + +impl Confirm { + fn next(&mut self, step: usize) { + let len = self.message_num_lines(); + if len == 0 { + return; + } + + let old = self.vertical_scroll; + self.vertical_scroll = (self.vertical_scroll + step).min(len - 1); + + render!(old != self.vertical_scroll); + } + + fn prev(&mut self, step: usize) { + let old = self.vertical_scroll; + + self.vertical_scroll -= step.min(self.vertical_scroll); + + render!(old != self.vertical_scroll); + } + + pub fn arrow(&mut self, opt: impl Into) { + let opt = opt.into() as Opt; + if opt.step > 0 { self.next(opt.step as usize) } else { self.prev(opt.step.unsigned_abs()) } + } +} diff --git a/yazi-core/src/confirm/commands/close.rs b/yazi-core/src/confirm/commands/close.rs new file mode 100644 index 00000000..542ec4d9 --- /dev/null +++ b/yazi-core/src/confirm/commands/close.rs @@ -0,0 +1,27 @@ +use anyhow::anyhow; +use yazi_shared::{event::Cmd, render}; + +use crate::confirm::Confirm; + +pub struct Opt { + submit: bool, +} + +impl From for Opt { + fn from(c: Cmd) -> Self { Self { submit: c.bool("submit") } } +} +impl From for Opt { + fn from(submit: bool) -> Self { Self { submit } } +} + +impl Confirm { + pub fn close(&mut self, opt: impl Into) { + let opt = opt.into() as Opt; + if let Some(cb) = self.callback.take() { + _ = cb.send(if opt.submit { Ok(true) } else { Err(anyhow!("canceled")) }); + } + + self.visible = false; + render!(); + } +} diff --git a/yazi-core/src/confirm/commands/mod.rs b/yazi-core/src/confirm/commands/mod.rs new file mode 100644 index 00000000..d36c6ae8 --- /dev/null +++ b/yazi-core/src/confirm/commands/mod.rs @@ -0,0 +1,3 @@ +mod arrow; +mod close; +mod show; diff --git a/yazi-core/src/confirm/commands/show.rs b/yazi-core/src/confirm/commands/show.rs new file mode 100644 index 00000000..3f124271 --- /dev/null +++ b/yazi-core/src/confirm/commands/show.rs @@ -0,0 +1,36 @@ +use tokio::sync::oneshot; +use yazi_config::popup::ConfirmCfg; +use yazi_shared::{event::Cmd, render}; + +use crate::confirm::Confirm; + +pub struct Opt { + cfg: ConfirmCfg, + tx: oneshot::Sender>, +} + +impl TryFrom for Opt { + type Error = (); + + fn try_from(mut c: Cmd) -> Result { + Ok(Self { cfg: c.take_any("cfg").ok_or(())?, tx: c.take_any("tx").ok_or(())? }) + } +} + +impl Confirm { + pub fn show(&mut self, opt: impl TryInto) { + let Ok(opt) = opt.try_into() else { + return; + }; + + self.close(false); + self.title = opt.cfg.title; + self.set_message(&opt.cfg.message); + self.vertical_scroll = 0; + + self.position = opt.cfg.position; + self.callback = Some(opt.tx); + self.visible = true; + render!(); + } +} diff --git a/yazi-core/src/confirm/confirm.rs b/yazi-core/src/confirm/confirm.rs new file mode 100644 index 00000000..22b3ab11 --- /dev/null +++ b/yazi-core/src/confirm/confirm.rs @@ -0,0 +1,34 @@ +use anyhow::Result; +use tokio::sync::oneshot::Sender; +use yazi_config::popup::Position; + +#[derive(Default)] +pub struct Confirm { + pub(super) title: String, + message: String, + message_num_lines: usize, + + pub position: Position, + + pub vertical_scroll: usize, + pub(super) callback: Option>>, + + pub visible: bool, +} + +impl Confirm { + #[inline] + pub fn set_message(&mut self, message: &str) { + self.message = message.to_string(); + self.message_num_lines = self.message.split('\n').count(); + } + + #[inline] + pub fn message(&self) -> String { self.message.clone() } + + #[inline] + pub fn message_num_lines(&self) -> usize { self.message_num_lines } + + #[inline] + pub fn title(&self) -> String { self.title.clone() } +} diff --git a/yazi-core/src/confirm/mod.rs b/yazi-core/src/confirm/mod.rs new file mode 100644 index 00000000..cebfd9fa --- /dev/null +++ b/yazi-core/src/confirm/mod.rs @@ -0,0 +1,4 @@ +mod commands; +mod confirm; + +pub use confirm::*; diff --git a/yazi-core/src/lib.rs b/yazi-core/src/lib.rs index 9850ca81..b51c6aad 100644 --- a/yazi-core/src/lib.rs +++ b/yazi-core/src/lib.rs @@ -7,6 +7,7 @@ )] pub mod completion; +pub mod confirm; pub mod folder; pub mod help; pub mod input; diff --git a/yazi-core/src/manager/commands/remove.rs b/yazi-core/src/manager/commands/remove.rs index 8be5b536..177eab77 100644 --- a/yazi-core/src/manager/commands/remove.rs +++ b/yazi-core/src/manager/commands/remove.rs @@ -1,5 +1,5 @@ -use yazi_config::popup::InputCfg; -use yazi_proxy::{InputProxy, ManagerProxy}; +use yazi_config::popup::ConfirmCfg; +use yazi_proxy::{ConfirmProxy, ManagerProxy}; use yazi_shared::{event::Cmd, fs::Url}; use crate::{manager::Manager, tasks::Tasks}; @@ -27,21 +27,21 @@ impl Manager { } let mut opt = opt.into() as Opt; - opt.targets = self.selected_or_hovered(false).cloned().collect(); + opt.targets = self.selected_or_hovered(true).cloned().collect(); if opt.force { return self.remove_do(opt, tasks); } tokio::spawn(async move { - let mut result = InputProxy::show(if opt.permanently { - InputCfg::delete(opt.targets.len()) + let result = ConfirmProxy::show(if opt.permanently { + ConfirmCfg::delete(&opt.targets) } else { - InputCfg::trash(opt.targets.len()) + ConfirmCfg::trash(&opt.targets) }); - if let Some(Ok(choice)) = result.recv().await { - if choice != "y" && choice != "Y" { + if let Ok(choice) = result.await { + if !choice { return; } diff --git a/yazi-fm/src/confirm/confirm.rs b/yazi-fm/src/confirm/confirm.rs new file mode 100644 index 00000000..ecfdfdae --- /dev/null +++ b/yazi-fm/src/confirm/confirm.rs @@ -0,0 +1,64 @@ +use ratatui::{buffer::Buffer, layout::{Constraint, Layout, Margin, Rect}, text::Line, widgets::{Block, BorderType, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget, Wrap}}; +use yazi_config::THEME; + +use crate::Ctx; + +pub(crate) struct Confirm<'a> { + cx: &'a Ctx, +} + +impl<'a> Confirm<'a> { + pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } } +} +impl<'a> Widget for Confirm<'a> { + fn render(self, _win: Rect, buf: &mut Buffer) { + let confirm = &self.cx.confirm; + let area = self.cx.area(&confirm.position); + + yazi_plugin::elements::Clear::default().render(area, buf); + + Block::bordered() + .border_type(BorderType::Rounded) + .border_style(THEME.input.border) + .title(Line::styled(&confirm.title(), THEME.input.title)) + .render(area, buf); + + let popup_layout = + Layout::vertical(vec![Constraint::Percentage(70), Constraint::Percentage(30)]) + .vertical_margin(1) + .horizontal_margin(2) + .split(area); + + let button_layout = Layout::horizontal(vec![ + Constraint::Percentage(10), + Constraint::Percentage(30), + Constraint::Percentage(20), + Constraint::Percentage(30), + Constraint::Percentage(10), + ]) + .vertical_margin(1) + .split(popup_layout[1]); + + Paragraph::new(confirm.message().split('\n').map(Line::from).collect::>()) + .block(Block::bordered().border_type(BorderType::Rounded).border_style(THEME.input.border)) + .scroll((confirm.vertical_scroll as u16, 0)) + .wrap(Wrap { trim: false }) + .render(popup_layout[0], buf); + + const BORDER_SIZE: usize = 2; + if confirm.message_num_lines() > popup_layout[0].as_size().height as usize - BORDER_SIZE { + let mut scrollbar_state = + ScrollbarState::new(confirm.message().split('\n').collect::>().len()) + .position(confirm.vertical_scroll); + + Scrollbar::new(ScrollbarOrientation::VerticalRight).render( + popup_layout[0].inner(&Margin { vertical: 1, horizontal: 0 }), + buf, + &mut scrollbar_state, + ); + } + + Paragraph::new("[Y]es").block(Block::bordered()).centered().render(button_layout[1], buf); + Paragraph::new("(N)o").block(Block::bordered()).centered().render(button_layout[3], buf); + } +} diff --git a/yazi-fm/src/confirm/mod.rs b/yazi-fm/src/confirm/mod.rs new file mode 100644 index 00000000..f3ce5fd6 --- /dev/null +++ b/yazi-fm/src/confirm/mod.rs @@ -0,0 +1,3 @@ +mod confirm; + +pub(super) use confirm::*; diff --git a/yazi-fm/src/context.rs b/yazi-fm/src/context.rs index 0fec1dd8..2f2cf4d7 100644 --- a/yazi-fm/src/context.rs +++ b/yazi-fm/src/context.rs @@ -1,13 +1,14 @@ use ratatui::layout::Rect; use yazi_adapter::Dimension; use yazi_config::popup::{Origin, Position}; -use yazi_core::{completion::Completion, help::Help, input::Input, manager::Manager, notify::Notify, select::Select, tasks::Tasks, which::Which}; +use yazi_core::{completion::Completion, confirm::Confirm, help::Help, input::Input, manager::Manager, notify::Notify, select::Select, tasks::Tasks, which::Which}; pub struct Ctx { pub manager: Manager, pub tasks: Tasks, pub select: Select, pub input: Input, + pub confirm: Confirm, pub help: Help, pub completion: Completion, pub which: Which, @@ -21,6 +22,7 @@ impl Ctx { tasks: Tasks::serve(), select: Default::default(), input: Default::default(), + confirm: Default::default(), help: Default::default(), completion: Default::default(), which: Default::default(), diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index e34d4a71..a8a32722 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -19,6 +19,7 @@ impl<'a> Executor<'a> { Layer::Tasks => self.tasks(cmd), Layer::Select => self.select(cmd), Layer::Input => self.input(cmd), + Layer::Confirm => self.confirm(cmd), Layer::Help => self.help(cmd), Layer::Completion => self.completion(cmd), Layer::Which => self.which(cmd), @@ -246,6 +247,20 @@ impl<'a> Executor<'a> { } } + fn confirm(&mut self, cmd: Cmd) { + macro_rules! on { + ($name:ident) => { + if cmd.name == stringify!($name) { + return self.app.cx.confirm.$name(cmd); + } + }; + } + + on!(arrow); + on!(show); + on!(close); + } + fn help(&mut self, cmd: Cmd) { macro_rules! on { ($name:ident) => { diff --git a/yazi-fm/src/main.rs b/yazi-fm/src/main.rs index 93d67543..a3c96866 100644 --- a/yazi-fm/src/main.rs +++ b/yazi-fm/src/main.rs @@ -8,6 +8,7 @@ static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; mod app; mod completion; mod components; +mod confirm; mod context; mod executor; mod help; diff --git a/yazi-fm/src/root.rs b/yazi-fm/src/root.rs index 3ffeaaf8..145a3cb0 100644 --- a/yazi-fm/src/root.rs +++ b/yazi-fm/src/root.rs @@ -3,7 +3,7 @@ use mlua::{Table, TableExt}; use ratatui::{buffer::Buffer, layout::{Constraint, Layout, Rect}, widgets::Widget}; use yazi_plugin::{bindings::{Cast, MouseEvent}, LUA}; -use super::{completion, input, select, tasks, which}; +use super::{completion, confirm, input, select, tasks, which}; use crate::{components, help, Ctx}; pub(super) struct Root<'a> { @@ -37,6 +37,10 @@ impl<'a> Widget for Root<'a> { input::Input::new(self.cx).render(area, buf); } + if self.cx.confirm.visible { + confirm::Confirm::new(self.cx).render(area, buf); + } + if self.cx.help.visible { help::Layout::new(self.cx).render(area, buf); } diff --git a/yazi-fm/src/router.rs b/yazi-fm/src/router.rs index 84db76a4..f6079a69 100644 --- a/yazi-fm/src/router.rs +++ b/yazi-fm/src/router.rs @@ -31,6 +31,8 @@ impl<'a> Router<'a> { self.matches(Layer::Help, key) } else if cx.input.visible { self.matches(Layer::Input, key) + } else if cx.confirm.visible { + self.matches(Layer::Confirm, key) } else if cx.select.visible { self.matches(Layer::Select, key) } else if cx.tasks.visible { diff --git a/yazi-plugin/src/bindings/confirm.rs b/yazi-plugin/src/bindings/confirm.rs new file mode 100644 index 00000000..87a46700 --- /dev/null +++ b/yazi-plugin/src/bindings/confirm.rs @@ -0,0 +1,37 @@ +use std::pin::Pin; + +use mlua::{prelude::LuaUserDataMethods, UserData}; +use tokio::pin; +use tokio_stream::StreamExt; +use yazi_shared::InputError; + +pub struct ConfirmRx>> { + inner: T, +} + +impl>> InputRx { + pub fn new(inner: T) -> Self { Self { inner } } + + pub async fn consume(inner: T) -> (Option, u8) { + pin!(inner); + inner.next().await.map(Self::parse).unwrap_or((None, 0)) + } + + fn parse(res: Result) -> (Option, u8) { + match res { + Ok(s) => (Some(s), 1), + Err(InputError::Canceled(s)) => (Some(s), 2), + Err(InputError::Typed(s)) => (Some(s), 3), + _ => (None, 0), + } + } +} + +impl> + 'static> UserData for InputRx { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_async_method_mut("recv", |_, me, ()| async move { + let mut inner = unsafe { Pin::new_unchecked(&mut me.inner) }; + Ok(inner.next().await.map(Self::parse).unwrap_or((None, 0))) + }); + } +} diff --git a/yazi-plugin/src/bindings/position.rs b/yazi-plugin/src/bindings/position.rs index 935fbc28..e5151bda 100644 --- a/yazi-plugin/src/bindings/position.rs +++ b/yazi-plugin/src/bindings/position.rs @@ -26,7 +26,7 @@ impl<'a> TryFrom> for Position { x: t.raw_get("x").unwrap_or_default(), y: t.raw_get("y").unwrap_or_default(), width: t.raw_get("w")?, - height: 3, + height: t.raw_get("h").unwrap_or(3), }, })) } diff --git a/yazi-plugin/src/utils/layer.rs b/yazi-plugin/src/utils/layer.rs index 40c49d8a..9fb49f85 100644 --- a/yazi-plugin/src/utils/layer.rs +++ b/yazi-plugin/src/utils/layer.rs @@ -3,8 +3,8 @@ use std::{str::FromStr, time::Duration}; use mlua::{ExternalError, ExternalResult, IntoLuaMulti, Lua, Table, Value}; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; -use yazi_config::{keymap::{Control, Key}, popup::InputCfg}; -use yazi_proxy::{AppProxy, InputProxy}; +use yazi_config::{keymap::{Control, Key}, popup::{ConfirmCfg, InputCfg}}; +use yazi_proxy::{AppProxy, ConfirmProxy, InputProxy}; use yazi_shared::{emit, event::Cmd, Debounce, Layer}; use super::Utils; @@ -86,6 +86,23 @@ impl Utils { })?, )?; + ya.raw_set( + "confirm", + lua.create_async_function(|lua, t: Table| async move { + let result = ConfirmProxy::show(ConfirmCfg { + title: t.raw_get("title")?, + message: t.raw_get("message")?, + position: Position::try_from(t.raw_get::<_, Table>("position")?)?.into(), + }); + + if let Ok(_answer) = result.await { + true.into_lua_multi(lua) + } else { + false.into_lua_multi(lua) + } + })?, + )?; + ya.raw_set( "notify", lua.create_function(|_, t: Table| { diff --git a/yazi-proxy/src/confirm.rs b/yazi-proxy/src/confirm.rs new file mode 100644 index 00000000..eaf2925e --- /dev/null +++ b/yazi-proxy/src/confirm.rs @@ -0,0 +1,14 @@ +use tokio::sync::oneshot; +use yazi_config::popup::ConfirmCfg; +use yazi_shared::{emit, event::Cmd, Layer}; + +pub struct ConfirmProxy; + +impl ConfirmProxy { + #[inline] + pub async fn show(cfg: ConfirmCfg) -> anyhow::Result { + let (tx, rx) = oneshot::channel(); + emit!(Call(Cmd::new("show").with_any("tx", tx).with_any("cfg", cfg), Layer::Confirm)); + rx.await? + } +} diff --git a/yazi-proxy/src/lib.rs b/yazi-proxy/src/lib.rs index 6a0687fe..e87d6b60 100644 --- a/yazi-proxy/src/lib.rs +++ b/yazi-proxy/src/lib.rs @@ -1,5 +1,6 @@ mod app; mod completion; +mod confirm; mod input; mod manager; pub mod options; @@ -10,6 +11,7 @@ mod tasks; pub use app::*; pub use completion::*; +pub use confirm::*; pub use input::*; pub use manager::*; pub use select::*; diff --git a/yazi-shared/src/layer.rs b/yazi-shared/src/layer.rs index ea549571..525fc051 100644 --- a/yazi-shared/src/layer.rs +++ b/yazi-shared/src/layer.rs @@ -10,6 +10,7 @@ pub enum Layer { Tasks, Select, Input, + Confirm, Help, Completion, Which, @@ -23,6 +24,7 @@ impl Display for Layer { Self::Tasks => "tasks", Self::Select => "select", Self::Input => "input", + Self::Confirm => "confirm", Self::Help => "help", Self::Completion => "completion", Self::Which => "which", @@ -40,6 +42,7 @@ impl FromStr for Layer { "tasks" => Self::Tasks, "select" => Self::Select, "input" => Self::Input, + "confirm" => Self::Confirm, "help" => Self::Help, "completion" => Self::Completion, "which" => Self::Which,