feat: cross-directory selection

This commit is contained in:
sxyazi 2024-02-18 19:03:23 +08:00
parent 6aae7683ba
commit 24243e6c60
No known key found for this signature in database
15 changed files with 189 additions and 128 deletions

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

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

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

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

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