diff --git a/yazi-config/src/pattern.rs b/yazi-config/src/pattern.rs index 3a428dbf..652cafee 100644 --- a/yazi-config/src/pattern.rs +++ b/yazi-config/src/pattern.rs @@ -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 { diff --git a/yazi-config/src/plugin/plugin.rs b/yazi-config/src/plugin/plugin.rs index 91ae8d73..00d93d00 100644 --- a/yazi-config/src/plugin/plugin.rs +++ b/yazi-config/src/plugin/plugin.rs @@ -11,24 +11,6 @@ pub struct Plugin { pub previewers: Vec, } -#[derive(Deserialize)] -pub struct PluginRule { - #[serde(default)] - pub id: u8, - pub cond: Option, - pub name: Option, - pub mime: Option, - #[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::(&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, + pub name: Option, + pub mime: Option, + #[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()) } +} diff --git a/yazi-config/src/theme/icon.rs b/yazi-config/src/theme/icon.rs index 3b2bd3f2..c0f78430 100644 --- a/yazi-config/src/theme/icon.rs +++ b/yazi-config/src/theme/icon.rs @@ -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( diff --git a/yazi-core/src/notify/commands/push.rs b/yazi-core/src/notify/commands/push.rs index f3f2572b..778241ea 100644 --- a/yazi-core/src/notify/commands/push.rs +++ b/yazi-core/src/notify/commands/push.rs @@ -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) { let Ok(mut msg) = msg.try_into() else { return; diff --git a/yazi-core/src/notify/commands/tick.rs b/yazi-core/src/notify/commands/tick.rs index 7b78d814..247c351c 100644 --- a/yazi-core/src/notify/commands/tick.rs +++ b/yazi-core/src/notify/commands/tick.rs @@ -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 for Opt { } impl Notify { - pub fn tick(&mut self, opt: impl TryInto) { + pub fn tick(&mut self, opt: impl TryInto, 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; } @@ -44,7 +45,7 @@ impl Notify { } 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()) diff --git a/yazi-core/src/notify/message.rs b/yazi-core/src/notify/message.rs index 13bb39d2..0f1d0f3e 100644 --- a/yazi-core/src/notify/message.rs +++ b/yazi-core/src/notify/message.rs @@ -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 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 for Message { instant: Instant::now(), timeout: Duration::from_secs_f64(timeout), - lines, percent: 0, }) } @@ -43,5 +41,8 @@ impl TryFrom 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 + } } diff --git a/yazi-core/src/notify/notify.rs b/yazi-core/src/notify/notify.rs index a0adea4c..27b7a227 100644 --- a/yazi-core/src/notify/notify.rs +++ b/yazi-core/src/notify/notify.rs @@ -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) diff --git a/yazi-core/src/tab/commands/escape.rs b/yazi-core/src/tab/commands/escape.rs index fb7d139d..b259b711 100644 --- a/yazi-core/src/tab/commands/escape.rs +++ b/yazi-core/src/tab/commands/escape.rs @@ -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,34 @@ 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 results: Vec<_> = indices + .iter() + .filter_map(|i| self.current.files.get(*i)) + .map(|f| { + if state { + self.selected.add(&f.url) + } else { + self.selected.remove(&f.url); + true + } + }) + .collect(); + + render!(!results.is_empty()); + if results.into_iter().all(|b| b) { + return true; + } + + Notify::_push_warn( + "Escape visual mode", + "Some files cannot be selected due to path nesting conflict.", + ); + false } } diff --git a/yazi-core/src/tab/mode.rs b/yazi-core/src/tab/mode.rs index af882893..389a5daa 100644 --- a/yazi-core/src/tab/mode.rs +++ b/yazi-core/src/tab/mode.rs @@ -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)> { + pub fn visual_mut(&mut self) -> Option<(usize, &mut BTreeSet)> { 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)> { - match self { + pub fn take_visual(&mut self) -> Option<(usize, BTreeSet)> { + 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)), } } } diff --git a/yazi-core/src/tab/tab.rs b/yazi-core/src/tab/tab.rs index eef9ab47..d354ed0b 100644 --- a/yazi-core/src/tab/tab.rs +++ b/yazi-core/src/tab/tab.rs @@ -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, diff --git a/yazi-fm/src/app/commands/render.rs b/yazi-fm/src/app/commands/render.rs index c77e1242..e9106f4c 100644 --- a/yazi-fm/src/app/commands/render.rs +++ b/yazi-fm/src/app/commands/render.rs @@ -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); diff --git a/yazi-fm/src/app/commands/update_notify.rs b/yazi-fm/src/app/commands/update_notify.rs index a9bb76a6..a408e1f3 100644 --- a/yazi-fm/src/app/commands/update_notify.rs +++ b/yazi-fm/src/app/commands/update_notify.rs @@ -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(); diff --git a/yazi-fm/src/notify/layout.rs b/yazi-fm/src/notify/layout.rs new file mode 100644 index 00000000..b2b8acd9 --- /dev/null +++ b/yazi-fm/src/notify/layout.rs @@ -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, ¬ify.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); + } + } +} diff --git a/yazi-fm/src/notify/mod.rs b/yazi-fm/src/notify/mod.rs index 2e5703d1..03520470 100644 --- a/yazi-fm/src/notify/mod.rs +++ b/yazi-fm/src/notify/mod.rs @@ -1,3 +1,3 @@ -mod notify; +mod layout; -pub(super) use notify::*; +pub(super) use layout::*; diff --git a/yazi-fm/src/notify/notify.rs b/yazi-fm/src/notify/notify.rs deleted file mode 100644 index 97e040f5..00000000 --- a/yazi-fm/src/notify/notify.rs +++ /dev/null @@ -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, ¬ify.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); - } - } -}