mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
feat: cross-directory selection
This commit is contained in:
parent
6aae7683ba
commit
24243e6c60
15 changed files with 189 additions and 128 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
67
yazi-fm/src/notify/layout.rs
Normal file
67
yazi-fm/src/notify/layout.rs
Normal 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, ¬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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
mod notify;
|
||||
mod layout;
|
||||
|
||||
pub(super) use notify::*;
|
||||
pub(super) use layout::*;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue