feat: cross-directory selection (#693)

This commit is contained in:
三咲雅 · Misaki Masa 2024-02-18 23:28:08 +08:00 committed by GitHub
parent 6aae7683ba
commit 3f27532c57
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 275 additions and 209 deletions

70
Cargo.lock generated
View file

@ -72,9 +72,9 @@ dependencies = [
[[package]]
name = "anstream"
version = "0.6.11"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5"
checksum = "96b09b5178381e0874812a9b157f7fe84982617e48f71f4e3235482775e5b540"
dependencies = [
"anstyle",
"anstyle-parse",
@ -224,9 +224,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.14.0"
version = "3.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
checksum = "d32a994c2b3ca201d9b263612a374263f05e7adde37c4707f693dcd375076d1f"
[[package]]
name = "bytemuck"
@ -290,9 +290,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.0"
version = "4.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f"
checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da"
dependencies = [
"clap_builder",
"clap_derive",
@ -300,9 +300,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.0"
version = "4.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99"
checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb"
dependencies = [
"anstream",
"anstyle",
@ -312,9 +312,9 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.5.0"
version = "4.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "299353be8209bd133b049bf1c63582d184a8b39fd9c04f15fe65f50f88bdfe6c"
checksum = "885e4d7d5af40bfb99ae6f9433e292feac98d452dcb3ec3d25dfe7552b77da8c"
dependencies = [
"clap",
]
@ -331,9 +331,9 @@ dependencies = [
[[package]]
name = "clap_complete_nushell"
version = "4.5.0"
version = "4.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92cd8ac7c321605310e4b3bdc047daf27c6ca1b50f6ae59b22aa0661ab45e295"
checksum = "80d0e48e026ce7df2040239117d25e4e79714907420c70294a5ce4b6bbe6a7b6"
dependencies = [
"clap",
"clap_complete",
@ -348,7 +348,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.49",
]
[[package]]
@ -749,7 +749,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.49",
]
[[package]]
@ -1221,7 +1221,7 @@ dependencies = [
"proc-macro2",
"quote",
"regex",
"syn 2.0.48",
"syn 2.0.49",
]
[[package]]
@ -1295,9 +1295,9 @@ dependencies = [
[[package]]
name = "num_threads"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
@ -1424,9 +1424,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.29"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "plist"
@ -1444,9 +1444,9 @@ dependencies = [
[[package]]
name = "png"
version = "0.17.11"
version = "0.17.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f6c3c3e617595665b8ea2ff95a86066be38fb121ff920a9c0eb282abcd1da5a"
checksum = "78c2378060fb13acff3ba0325b83442c1d2c44fbb76df481160ddc1687cce160"
dependencies = [
"bitflags 1.3.2",
"crc32fast",
@ -1695,7 +1695,7 @@ checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.49",
]
[[package]]
@ -1868,7 +1868,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.48",
"syn 2.0.49",
]
[[package]]
@ -1884,9 +1884,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.48"
version = "2.0.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496"
dependencies = [
"proc-macro2",
"quote",
@ -1931,7 +1931,7 @@ checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.49",
]
[[package]]
@ -2050,7 +2050,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.49",
]
[[package]]
@ -2101,9 +2101,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.22.5"
version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99e68c159e8f5ba8a28c4eb7b0c0c190d77bb479047ca713270048145a9ad28a"
checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6"
dependencies = [
"indexmap",
"serde",
@ -2143,7 +2143,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.49",
]
[[package]]
@ -2366,7 +2366,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.49",
"wasm-bindgen-shared",
]
@ -2388,7 +2388,7 @@ checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.49",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -2658,9 +2658,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
name = "winnow"
version = "0.6.0"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b1dbce9e90e5404c5a52ed82b1d13fc8cfbdad85033b6f57546ffd1265f8451"
checksum = "d90f4e0f530c4c69f62b80d839e9ef3855edc9cba471a160c4d692deed62b401"
dependencies = [
"memchr",
]
@ -2857,7 +2857,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.49",
]
[[package]]

View file

@ -37,7 +37,10 @@ impl Pattern {
}
#[inline]
pub fn is_wildcard(&self) -> bool { self.inner.as_str() == "*" || self.inner.as_str() == "*/" }
pub fn any_file(&self) -> bool { self.inner.as_str() == "*" }
#[inline]
pub fn any_dir(&self) -> bool { self.inner.as_str() == "*/" }
}
impl TryFrom<&str> for Pattern {

View file

@ -11,24 +11,6 @@ pub struct Plugin {
pub previewers: Vec<PluginRule>,
}
#[derive(Deserialize)]
pub struct PluginRule {
#[serde(default)]
pub id: u8,
pub cond: Option<Condition>,
pub name: Option<Pattern>,
pub mime: Option<Pattern>,
#[serde(rename = "exec")]
#[serde(deserialize_with = "super::exec_deserialize")]
pub cmd: Cmd,
#[serde(default)]
pub sync: bool,
#[serde(default)]
pub multi: bool,
#[serde(default)]
pub prio: Priority,
}
impl Default for Plugin {
fn default() -> Self {
#[derive(Deserialize)]
@ -52,11 +34,17 @@ impl Default for Plugin {
}
let mut shadow = toml::from_str::<Outer>(&MERGED_YAZI).unwrap().plugin;
if shadow.append_preloaders.iter().any(|r| r.name.as_ref().is_some_and(|p| p.is_wildcard())) {
shadow.preloaders.retain(|r| !r.name.as_ref().is_some_and(|p| p.is_wildcard()));
if shadow.append_preloaders.iter().any(|r| r.any_file()) {
shadow.preloaders.retain(|r| !r.any_file());
}
if shadow.append_previewers.iter().any(|r| r.name.as_ref().is_some_and(|p| p.is_wildcard())) {
shadow.previewers.retain(|r| !r.name.as_ref().is_some_and(|p| p.is_wildcard()));
if shadow.append_preloaders.iter().any(|r| r.any_dir()) {
shadow.preloaders.retain(|r| !r.any_dir());
}
if shadow.append_previewers.iter().any(|r| r.any_file()) {
shadow.previewers.retain(|r| !r.any_file());
}
if shadow.append_previewers.iter().any(|r| r.any_dir()) {
shadow.previewers.retain(|r| !r.any_dir());
}
Preset::mix(&mut shadow.preloaders, shadow.prepend_preloaders, shadow.append_preloaders);
@ -104,3 +92,29 @@ impl Plugin {
})
}
}
#[derive(Deserialize)]
pub struct PluginRule {
#[serde(default)]
pub id: u8,
pub cond: Option<Condition>,
pub name: Option<Pattern>,
pub mime: Option<Pattern>,
#[serde(rename = "exec")]
#[serde(deserialize_with = "super::exec_deserialize")]
pub cmd: Cmd,
#[serde(default)]
pub sync: bool,
#[serde(default)]
pub multi: bool,
#[serde(default)]
pub prio: Priority,
}
impl PluginRule {
#[inline]
fn any_file(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_file()) }
#[inline]
fn any_dir(&self) -> bool { self.name.as_ref().is_some_and(|p| p.any_dir()) }
}

View file

@ -31,6 +31,13 @@ impl Icon {
}
let mut outer = IconOuter::deserialize(deserializer)?;
if outer.append_rules.iter().any(|r| r.name.any_file()) {
outer.rules.retain(|r| !r.name.any_file());
}
if outer.append_rules.iter().any(|r| r.name.any_dir()) {
outer.rules.retain(|r| !r.name.any_dir());
}
Preset::mix(&mut outer.rules, outer.prepend_rules, outer.append_rules);
Ok(

View file

@ -71,12 +71,12 @@ impl Manager {
error!("preload in watcher failed: {e}");
}
Self::_open_do(opt.interactive, done);
Self::_open_do(done, opt.interactive);
});
}
#[inline]
pub fn _open_do(interactive: bool, targets: Vec<(Url, String)>) {
pub fn _open_do(targets: Vec<(Url, String)>, interactive: bool) {
emit!(Call(
Cmd::new("open_do").with_bool("interactive", interactive).with_data(targets),
Layer::Manager

View file

@ -1,17 +1,20 @@
use yazi_shared::event::Cmd;
use yazi_config::popup::InputCfg;
use yazi_shared::{emit, event::Cmd, fs::Url, Layer};
use crate::{manager::Manager, tasks::Tasks};
use crate::{input::Input, manager::Manager, tasks::Tasks};
pub struct Opt {
force: bool,
permanently: bool,
targets: Vec<Url>,
}
impl From<Cmd> for Opt {
fn from(c: Cmd) -> Self {
fn from(mut c: Cmd) -> Self {
Self {
force: c.named.contains_key("force"),
permanently: c.named.contains_key("permanently"),
targets: c.take_data().unwrap_or_default(),
}
}
}
@ -22,8 +25,44 @@ impl Manager {
return;
}
let mut opt = opt.into() as Opt;
opt.targets = self.selected_or_hovered().into_iter().cloned().collect();
if opt.force {
return self.remove_do(opt, tasks);
}
tokio::spawn(async move {
let mut result = Input::_show(if opt.permanently {
InputCfg::delete(opt.targets.len())
} else {
InputCfg::trash(opt.targets.len())
});
if let Some(Ok(choice)) = result.recv().await {
if choice != "y" && choice != "Y" {
return;
}
Self::_remove_do(opt.targets, opt.permanently);
}
});
}
#[inline]
pub fn _remove_do(targets: Vec<Url>, permanently: bool) {
emit!(Call(
Cmd::new("remove_do").with_bool("permanently", permanently).with_data(targets),
Layer::Manager
));
}
pub fn remove_do(&mut self, opt: impl Into<Opt>, tasks: &Tasks) {
let opt = opt.into() as Opt;
let targets = self.selected_or_hovered().into_iter().cloned().collect();
tasks.file_remove(targets, opt.force, opt.permanently);
for u in &opt.targets {
self.active_mut().selected.remove(u);
}
tasks.file_remove(opt.targets, opt.permanently);
}
}

View file

@ -5,6 +5,18 @@ use yazi_shared::{emit, event::Cmd, Layer};
use crate::notify::{Message, Notify};
impl Notify {
#[inline]
pub fn _push_warn(title: &str, content: &str) {
emit!(Call(
Cmd::new("notify")
.with("title", title)
.with("content", content)
.with("level", "warn")
.with("timeout", 5),
Layer::App
));
}
pub fn push(&mut self, msg: impl TryInto<Message>) {
let Ok(mut msg) = msg.try_into() else {
return;

View file

@ -1,5 +1,6 @@
use std::time::Duration;
use ratatui::layout::Rect;
use yazi_shared::{emit, event::Cmd, Layer};
use crate::notify::Notify;
@ -22,13 +23,13 @@ impl TryFrom<Cmd> for Opt {
}
impl Notify {
pub fn tick(&mut self, opt: impl TryInto<Opt>) {
pub fn tick(&mut self, opt: impl TryInto<Opt>, area: Rect) {
self.tick_handle.take().map(|h| h.abort());
let Ok(opt) = opt.try_into() else {
return;
};
let limit = self.limit();
let limit = self.limit(area);
if limit == 0 {
return;
}
@ -37,14 +38,14 @@ impl Notify {
if m.timeout.is_zero() {
m.percent = m.percent.saturating_sub(20);
} else if m.percent < 100 {
m.percent = m.percent.saturating_add(20);
m.percent += 20;
} else {
m.timeout = m.timeout.saturating_sub(opt.interval);
}
}
self.messages.retain(|m| m.percent > 0 || !m.timeout.is_zero());
let limit = self.limit();
let limit = self.limit(area);
let timeouts: Vec<_> = self.messages[..limit]
.iter()
.filter(|&m| m.percent == 100 && !m.timeout.is_zero())

View file

@ -1,5 +1,6 @@
use std::time::{Duration, Instant};
use unicode_width::UnicodeWidthStr;
use yazi_shared::event::Cmd;
use super::{Level, NOTIFY_BORDER};
@ -12,7 +13,6 @@ pub struct Message {
pub instant: Instant,
pub timeout: Duration,
pub lines: usize,
pub percent: u8,
}
@ -26,7 +26,6 @@ impl TryFrom<Cmd> for Message {
}
let content = c.take_name("content").ok_or(())?;
let lines = content.lines().count();
Ok(Self {
title: c.take_name("title").ok_or(())?,
content,
@ -35,7 +34,6 @@ impl TryFrom<Cmd> for Message {
instant: Instant::now(),
timeout: Duration::from_secs_f64(timeout),
lines,
percent: 0,
})
}
@ -43,5 +41,8 @@ impl TryFrom<Cmd> for Message {
impl Message {
#[inline]
pub fn height(&self) -> usize { self.lines + NOTIFY_BORDER as usize }
pub fn height(&self, width: u16) -> usize {
let lines = (self.content.width() as f64 / width as f64).ceil();
lines as usize + NOTIFY_BORDER as usize
}
}

View file

@ -1,7 +1,7 @@
use std::ops::ControlFlow;
use ratatui::layout::Rect;
use tokio::task::JoinHandle;
use yazi_shared::term::Term;
use super::{Message, NOTIFY_SPACING};
@ -12,14 +12,14 @@ pub struct Notify {
}
impl Notify {
pub fn limit(&self) -> usize {
pub fn limit(&self, area: Rect) -> usize {
if self.messages.is_empty() {
return 0;
}
let mut height = Term::size().height as usize;
let mut height = area.height as usize;
let flow = (0..self.messages.len().min(3)).try_fold(0, |acc, i| {
match height.checked_sub(self.messages[i].height() + NOTIFY_SPACING as usize) {
match height.checked_sub(self.messages[i].height(area.width) + NOTIFY_SPACING as usize) {
Some(h) => {
height = h;
ControlFlow::Continue(acc + 1)

View file

@ -1,7 +1,7 @@
use bitflags::bitflags;
use yazi_shared::{event::Cmd, render, render_and};
use crate::{manager::Manager, tab::{Mode, Tab}};
use crate::{manager::Manager, notify::Notify, tab::Tab};
bitflags! {
pub struct Opt: u8 {
@ -56,29 +56,17 @@ impl Tab {
}
}
#[inline]
pub fn escape_find(&mut self) -> bool { render_and!(self.finder.take().is_some()) }
#[inline]
pub fn escape_visual(&mut self) -> bool {
let Some((_, indices)) = self.mode.visual() else {
if !self.mode.is_visual() {
return false;
};
let state = self.mode.is_select();
for f in indices.iter().filter_map(|i| self.current.files.get(*i)) {
if state {
self.selected.add(&f.url);
} else {
self.selected.remove(&f.url);
}
}
self.mode = Mode::Normal;
render_and!(true)
self.try_escape_visual();
true
}
#[inline]
pub fn escape_select(&mut self) -> bool {
if self.selected.is_empty() {
return false;
@ -91,7 +79,6 @@ impl Tab {
render_and!(true)
}
#[inline]
pub fn escape_filter(&mut self) -> bool {
if self.current.files.filter().is_none() {
return false;
@ -101,7 +88,6 @@ impl Tab {
render_and!(true)
}
#[inline]
pub fn escape_search(&mut self) -> bool {
if !self.current.cwd.is_search() {
return false;
@ -111,9 +97,29 @@ impl Tab {
render_and!(true)
}
#[inline]
pub fn try_escape_visual(&mut self) -> bool {
self.escape_visual();
true
let state = self.mode.is_select();
let Some((_, indices)) = self.mode.take_visual() else {
return true;
};
let mut success = true;
for f in indices.iter().filter_map(|i| self.current.files.get(*i)) {
if state {
success &= self.selected.add(&f.url);
} else {
self.selected.remove(&f.url);
}
}
if !success {
Notify::_push_warn(
"Escape visual mode",
"Some files cannot be selected, due to path nesting conflict.",
);
}
render!();
success
}
}

View file

@ -1,4 +1,4 @@
use std::collections::BTreeSet;
use std::{collections::BTreeSet, mem};
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub enum Mode {
@ -9,8 +9,7 @@ pub enum Mode {
}
impl Mode {
#[inline]
pub fn visual(&self) -> Option<(usize, &BTreeSet<usize>)> {
pub fn visual_mut(&mut self) -> Option<(usize, &mut BTreeSet<usize>)> {
match self {
Mode::Normal => None,
Mode::Select(start, indices) => Some((*start, indices)),
@ -18,12 +17,11 @@ impl Mode {
}
}
#[inline]
pub fn visual_mut(&mut self) -> Option<(usize, &mut BTreeSet<usize>)> {
match self {
pub fn take_visual(&mut self) -> Option<(usize, BTreeSet<usize>)> {
match mem::take(self) {
Mode::Normal => None,
Mode::Select(start, indices) => Some((*start, indices)),
Mode::Unset(start, indices) => Some((*start, indices)),
Mode::Select(start, indices) => Some((start, indices)),
Mode::Unset(start, indices) => Some((start, indices)),
}
}
}

View file

@ -5,7 +5,7 @@ use tokio::task::JoinHandle;
use yazi_shared::{fs::Url, render};
use super::{Backstack, Config, Finder, Mode, Preview};
use crate::{folder::{Folder, FolderStage}, tab::selected::Selected};
use crate::{folder::{Folder, FolderStage}, tab::Selected};
pub struct Tab {
pub mode: Mode,

View file

@ -2,13 +2,13 @@ use std::{collections::{BTreeMap, HashMap, HashSet}, ffi::OsStr, mem, path::Path
use tokio::time::sleep;
use tracing::debug;
use yazi_config::{manager::SortBy, open::Opener, plugin::{PluginRule, MAX_PRELOADERS}, popup::InputCfg, OPEN, PLUGIN};
use yazi_config::{manager::SortBy, open::Opener, plugin::{PluginRule, MAX_PRELOADERS}, OPEN, PLUGIN};
use yazi_plugin::ValueSendable;
use yazi_scheduler::{Scheduler, TaskSummary};
use yazi_shared::{emit, event::Cmd, fs::{File, Url}, term::Term, Layer, MIME_DIR};
use super::{TasksProgress, TASKS_BORDER, TASKS_PADDING, TASKS_PERCENT};
use crate::{folder::Files, input::Input};
use crate::folder::Files;
pub struct Tasks {
pub(super) scheduler: Arc<Scheduler>,
@ -113,39 +113,14 @@ impl Tasks {
}
}
pub fn file_remove(&self, targets: Vec<Url>, force: bool, permanently: bool) {
if force {
for u in targets {
if permanently {
self.scheduler.file_delete(u);
} else {
self.scheduler.file_trash(u);
}
}
return;
}
let scheduler = self.scheduler.clone();
tokio::spawn(async move {
let mut result = Input::_show(if permanently {
InputCfg::delete(targets.len())
pub fn file_remove(&self, targets: Vec<Url>, permanently: bool) {
for u in targets {
if permanently {
self.scheduler.file_delete(u);
} else {
InputCfg::trash(targets.len())
});
if let Some(Ok(choice)) = result.recv().await {
if choice != "y" && choice != "Y" {
return;
}
for u in targets {
if permanently {
scheduler.file_delete(u);
} else {
scheduler.file_trash(u);
}
}
self.scheduler.file_trash(u);
}
});
}
}
#[inline]

View file

@ -2,7 +2,7 @@ use std::sync::atomic::Ordering;
use ratatui::{backend::{Backend, CrosstermBackend}, CompletedFrame};
use crate::{app::App, lives::Lives, notify::Notify, root::{Root, COLLISION}};
use crate::{app::App, lives::Lives, root::{Root, COLLISION}};
impl App {
pub(crate) fn render(&mut self) {
@ -45,7 +45,7 @@ impl App {
let frame = term
.draw_partial(|f| {
f.render_widget(Notify::new(&self.cx), f.size());
f.render_widget(crate::notify::Layout::new(&self.cx), f.size());
if let Some((x, y)) = self.cx.cursor() {
f.set_cursor(x, y);

View file

@ -1,10 +1,15 @@
use yazi_shared::event::Cmd;
use crossterm::terminal::WindowSize;
use ratatui::layout::Rect;
use yazi_shared::{event::Cmd, term::Term};
use crate::app::App;
impl App {
pub(crate) fn update_notify(&mut self, cmd: Cmd) {
self.cx.notify.tick(cmd);
let WindowSize { width, height, .. } = Term::size();
let area = crate::notify::Layout::available(Rect { x: 0, y: 0, width, height });
self.cx.notify.tick(cmd, area);
if self.cx.notify.messages.is_empty() {
self.render();

View file

@ -98,6 +98,7 @@ impl<'a> Executor<'a> {
on!(MANAGER, paste, &self.app.cx.tasks);
on!(MANAGER, link, &self.app.cx.tasks);
on!(MANAGER, remove, &self.app.cx.tasks);
on!(MANAGER, remove_do, &self.app.cx.tasks);
on!(MANAGER, create);
on!(MANAGER, rename);
on!(ACTIVE, copy);

View file

@ -0,0 +1,67 @@
use std::rc::Rc;
use ratatui::{buffer::Buffer, layout::{self, Constraint, Offset, Rect}, style::{Style, Stylize}, widgets::{Block, BorderType, Paragraph, Widget, Wrap}};
use yazi_core::notify::{Level, Message};
use crate::{widgets::Clear, Ctx};
pub(crate) struct Layout<'a> {
cx: &'a Ctx,
}
impl<'a> Layout<'a> {
pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } }
pub(crate) fn available(area: Rect) -> Rect {
let chunks =
layout::Layout::horizontal([Constraint::Fill(1), Constraint::Length(40), Constraint::Max(1)])
.split(area);
let chunks =
layout::Layout::vertical([Constraint::Max(1), Constraint::Min(1)]).split(chunks[1]);
chunks[1]
}
fn tile(area: Rect, messages: &[Message]) -> Rc<[Rect]> {
layout::Layout::vertical(
messages.iter().map(|m| Constraint::Length(m.height(area.width) as u16)),
)
.spacing(1)
.split(area)
}
}
impl<'a> Widget for Layout<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let notify = &self.cx.notify;
let available = Self::available(area);
let limit = notify.limit(available);
let tile = Self::tile(available, &notify.messages[..limit]);
for (i, m) in notify.messages.iter().enumerate().take(limit) {
let (icon, style) = match m.level {
Level::Info => ("", Style::default().green()),
Level::Warn => ("", Style::default().yellow()),
Level::Error => ("", Style::default().red()),
};
let mut rect =
tile[i].offset(Offset { x: (100 - m.percent) as i32 * tile[i].width as i32 / 100, y: 0 });
rect.width = area.width.saturating_sub(rect.x);
Clear.render(rect, buf);
Paragraph::new(m.content.as_str())
.wrap(Wrap { trim: false })
.block(
Block::bordered()
.border_type(BorderType::Rounded)
.title(format!("{} {}", icon, m.title))
.title_style(style)
.border_style(style),
)
.render(rect, buf);
}
}
}

View file

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

View file

@ -1,58 +0,0 @@
use std::rc::Rc;
use ratatui::{buffer::Buffer, layout::{Constraint, Layout, Offset, Rect}, style::{Style, Stylize}, widgets::{Block, BorderType, Paragraph, Widget}};
use yazi_core::notify::{Level, Message};
use crate::{widgets::Clear, Ctx};
pub(crate) struct Notify<'a> {
cx: &'a Ctx,
}
impl<'a> Notify<'a> {
pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } }
fn chunks(area: Rect, messages: &[Message]) -> Rc<[Rect]> {
let chunks =
Layout::horizontal([Constraint::Fill(1), Constraint::Length(40), Constraint::Max(1)])
.split(area);
let chunks = Layout::vertical([Constraint::Max(1), Constraint::Min(1)]).split(chunks[1]);
Layout::vertical(messages.iter().map(|m| Constraint::Length(m.height() as u16)))
.spacing(1)
.split(chunks[1])
}
}
impl<'a> Widget for Notify<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let notify = &self.cx.notify;
let limit = notify.limit();
let chunks = Self::chunks(area, &notify.messages[..limit]);
for (i, m) in notify.messages.iter().enumerate().take(limit) {
let (icon, style) = match m.level {
Level::Info => ("", Style::default().green()),
Level::Warn => ("", Style::default().yellow()),
Level::Error => ("", Style::default().red()),
};
let mut rect = chunks[i]
.offset(Offset { x: (100 - m.percent) as i32 * chunks[i].width as i32 / 100, y: 0 });
rect.width = area.width.saturating_sub(rect.x);
Clear.render(rect, buf);
Paragraph::new(m.content.as_str())
.block(
Block::bordered()
.border_type(BorderType::Rounded)
.title(format!("{} {}", icon, m.title))
.title_style(style)
.border_style(style),
)
.render(rect, buf);
}
}
}

View file

@ -7,14 +7,9 @@ mod icon;
mod range;
mod window;
#[allow(unused_imports)]
pub use bindings::*;
pub use cha::*;
pub use file::*;
pub use icon::*;
pub use range::*;
pub use window::*;
pub trait Cast<T> {
fn cast(lua: &mlua::Lua, data: T) -> mlua::Result<mlua::AnyUserData>;
}