mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
fix: popup components (Input, Select, etc.) being covered by previewed images (#360)
This commit is contained in:
parent
c643afd6e6
commit
63b7491ad7
33 changed files with 377 additions and 257 deletions
|
|
@ -7,7 +7,7 @@ use tracing::warn;
|
|||
use yazi_config::PREVIEW;
|
||||
use yazi_shared::{env_exists, RoCell};
|
||||
|
||||
use super::{Iterm2, Kitty};
|
||||
use super::{Iterm2, Kitty, KittyOld};
|
||||
use crate::{ueberzug::Ueberzug, Sixel, TMUX};
|
||||
|
||||
static IMAGE_SHOWN: AtomicBool = AtomicBool::new(false);
|
||||
|
|
@ -18,6 +18,7 @@ static UEBERZUG: RoCell<Option<UnboundedSender<Option<(PathBuf, Rect)>>>> = RoCe
|
|||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Adaptor {
|
||||
Kitty,
|
||||
KittyOld,
|
||||
Iterm2,
|
||||
Sixel,
|
||||
|
||||
|
|
@ -83,7 +84,7 @@ impl Adaptor {
|
|||
let mut protocols = match Self::emulator() {
|
||||
Emulator::Unknown => vec![],
|
||||
Emulator::Kitty => vec![Self::Kitty],
|
||||
Emulator::Konsole => vec![Self::Kitty, Self::Iterm2, Self::Sixel],
|
||||
Emulator::Konsole => vec![Self::KittyOld, Self::Iterm2, Self::Sixel],
|
||||
Emulator::Iterm2 => vec![Self::Iterm2, Self::Sixel],
|
||||
Emulator::WezTerm => vec![Self::Iterm2, Self::Sixel],
|
||||
Emulator::Foot => vec![Self::Sixel],
|
||||
|
|
@ -99,6 +100,9 @@ impl Adaptor {
|
|||
if env_exists("ZELLIJ_SESSION_NAME") {
|
||||
protocols.retain(|p| *p == Self::Sixel);
|
||||
}
|
||||
if *TMUX && protocols.len() > 1 {
|
||||
protocols.retain(|p| *p != Self::KittyOld);
|
||||
}
|
||||
if let Some(p) = protocols.first() {
|
||||
return *p;
|
||||
}
|
||||
|
|
@ -148,6 +152,7 @@ impl ToString for Adaptor {
|
|||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
Self::Kitty => "kitty",
|
||||
Self::KittyOld => "kitty",
|
||||
Self::Iterm2 => "iterm2",
|
||||
Self::Sixel => "sixel",
|
||||
Self::X11 => "x11",
|
||||
|
|
@ -174,6 +179,7 @@ impl Adaptor {
|
|||
|
||||
match self {
|
||||
Self::Kitty => Kitty::image_show(path, rect).await,
|
||||
Self::KittyOld => KittyOld::image_show(path, rect).await,
|
||||
Self::Iterm2 => Iterm2::image_show(path, rect).await,
|
||||
Self::Sixel => Sixel::image_show(path, rect).await,
|
||||
_ => Ok(if let Some(tx) = &*UEBERZUG {
|
||||
|
|
@ -190,6 +196,7 @@ impl Adaptor {
|
|||
match self {
|
||||
Self::Kitty => Kitty::image_hide(rect),
|
||||
Self::Iterm2 => Iterm2::image_hide(rect),
|
||||
Self::KittyOld => KittyOld::image_hide(),
|
||||
Self::Sixel => Sixel::image_hide(rect),
|
||||
_ => Ok(if let Some(tx) = &*UEBERZUG {
|
||||
tx.send(None)?;
|
||||
|
|
@ -199,6 +206,6 @@ impl Adaptor {
|
|||
|
||||
#[inline]
|
||||
pub(super) fn needs_ueberzug(self) -> bool {
|
||||
!matches!(self, Self::Kitty | Self::Iterm2 | Self::Sixel)
|
||||
!matches!(self, Self::Kitty | Self::KittyOld | Self::Iterm2 | Self::Sixel)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
76
yazi-adaptor/src/kitty_old.rs
Normal file
76
yazi-adaptor/src/kitty_old.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
use std::{io::{stdout, Write}, path::Path};
|
||||
|
||||
use anyhow::Result;
|
||||
use base64::{engine::general_purpose, Engine};
|
||||
use image::DynamicImage;
|
||||
use ratatui::prelude::Rect;
|
||||
use yazi_shared::Term;
|
||||
|
||||
use super::image::Image;
|
||||
use crate::{CLOSE, ESCAPE, START};
|
||||
|
||||
pub(super) struct KittyOld;
|
||||
|
||||
impl KittyOld {
|
||||
pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> {
|
||||
let img = Image::downscale(path, (rect.width, rect.height)).await?;
|
||||
let b = Self::encode(img).await?;
|
||||
|
||||
Self::image_hide()?;
|
||||
Term::move_lock(stdout().lock(), (rect.x, rect.y), |stdout| Ok(stdout.write_all(&b)?))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn image_hide() -> Result<()> {
|
||||
let mut stdout = stdout().lock();
|
||||
stdout.write_all(format!("{}_Gq=1,a=d,d=A{}\\{}", START, ESCAPE, CLOSE).as_bytes())?;
|
||||
stdout.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn encode(img: DynamicImage) -> Result<Vec<u8>> {
|
||||
fn output(raw: &[u8], format: u8, size: (u32, u32)) -> Result<Vec<u8>> {
|
||||
let b64 = general_purpose::STANDARD.encode(raw).chars().collect::<Vec<_>>();
|
||||
|
||||
let mut it = b64.chunks(4096).peekable();
|
||||
let mut buf = Vec::with_capacity(b64.len() + it.len() * 50);
|
||||
if let Some(first) = it.next() {
|
||||
write!(
|
||||
buf,
|
||||
"{}_Gq=1,a=T,z=-1,C=1,f={},s={},v={},m={};{}{}\\{}",
|
||||
START,
|
||||
format,
|
||||
size.0,
|
||||
size.1,
|
||||
it.peek().is_some() as u8,
|
||||
first.iter().collect::<String>(),
|
||||
ESCAPE,
|
||||
CLOSE
|
||||
)?;
|
||||
}
|
||||
|
||||
while let Some(chunk) = it.next() {
|
||||
write!(
|
||||
buf,
|
||||
"{}_Gm={};{}{}\\{}",
|
||||
START,
|
||||
it.peek().is_some() as u8,
|
||||
chunk.iter().collect::<String>(),
|
||||
ESCAPE,
|
||||
CLOSE
|
||||
)?;
|
||||
}
|
||||
|
||||
buf.write_all(CLOSE.as_bytes())?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
let size = (img.width(), img.height());
|
||||
tokio::task::spawn_blocking(move || match img {
|
||||
DynamicImage::ImageRgb8(v) => output(v.as_raw(), 24, size),
|
||||
DynamicImage::ImageRgba8(v) => output(v.as_raw(), 32, size),
|
||||
v => output(v.to_rgb8().as_raw(), 24, size),
|
||||
})
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
|
@ -4,12 +4,14 @@ mod adaptor;
|
|||
mod image;
|
||||
mod iterm2;
|
||||
mod kitty;
|
||||
mod kitty_old;
|
||||
mod sixel;
|
||||
mod ueberzug;
|
||||
|
||||
use adaptor::*;
|
||||
use iterm2::*;
|
||||
use kitty::*;
|
||||
use kitty_old::*;
|
||||
use sixel::*;
|
||||
use yazi_shared::{env_exists, RoCell};
|
||||
|
||||
|
|
|
|||
|
|
@ -41,9 +41,9 @@ impl<'de> Deserialize<'de> for Opener {
|
|||
#[serde(rename = "for")]
|
||||
for_: Option<String>,
|
||||
|
||||
// TODO: remove this when v1.0.5 is released --
|
||||
// TODO: remove this when v0.1.6 is released --
|
||||
display_name: Option<String>,
|
||||
// TODO: -- remove this when v1.0.5 is released
|
||||
// TODO: -- remove this when v0.1.6 is released
|
||||
}
|
||||
|
||||
let mut shadow = Shadow::deserialize(deserializer)?;
|
||||
|
|
|
|||
|
|
@ -51,9 +51,4 @@ impl Ctx {
|
|||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn image_layer(&self) -> bool {
|
||||
!self.which.visible && !self.help.visible && !self.tasks.visible
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ pub enum Event {
|
|||
Files(FilesOp),
|
||||
Pages(usize),
|
||||
Mimetype(BTreeMap<Url, String>),
|
||||
Peek(Option<(usize, Url)>),
|
||||
Preview(PreviewLock),
|
||||
|
||||
// Input
|
||||
|
|
@ -80,12 +79,6 @@ macro_rules! emit {
|
|||
(Mimetype($mimes:expr)) => {
|
||||
$crate::Event::Mimetype($mimes).emit();
|
||||
};
|
||||
(Peek) => {
|
||||
$crate::Event::Peek(None).emit();
|
||||
};
|
||||
(Peek($skip:expr, $url:expr)) => {
|
||||
$crate::Event::Peek(Some(($skip, $url))).emit();
|
||||
};
|
||||
(Preview($lock:expr)) => {
|
||||
$crate::Event::Preview($lock).emit();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use yazi_config::{keymap::{Control, Key, KeymapLayer}, KEYMAP};
|
|||
use yazi_shared::Term;
|
||||
|
||||
use super::HELP_MARGIN;
|
||||
use crate::{emit, input::Input};
|
||||
use crate::input::Input;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Help {
|
||||
|
|
@ -34,8 +34,6 @@ impl Help {
|
|||
|
||||
self.offset = 0;
|
||||
self.cursor = 0;
|
||||
|
||||
emit!(Peek); // Show/hide preview for images
|
||||
true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ impl Manager {
|
|||
if let Ok(f) = File::from(child.clone()).await {
|
||||
emit!(Files(FilesOp::Creating(cwd, f.into_map())));
|
||||
Manager::_hover(Some(child));
|
||||
Manager::_refresh();
|
||||
}
|
||||
Ok::<(), anyhow::Error>(())
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,25 +23,26 @@ impl Manager {
|
|||
}
|
||||
|
||||
pub fn hover(&mut self, opt: impl Into<Opt>) -> bool {
|
||||
// Hover on the file
|
||||
let opt = opt.into() as Opt;
|
||||
let b = self.current_mut().repos(opt.url);
|
||||
|
||||
// Re-peek
|
||||
self.peek(0);
|
||||
|
||||
// Refresh watcher
|
||||
let mut to_watch = BTreeSet::new();
|
||||
for tab in self.tabs.iter() {
|
||||
to_watch.insert(&tab.current.cwd);
|
||||
match tab.current.hovered() {
|
||||
Some(h) if h.is_dir() => _ = to_watch.insert(&h.url),
|
||||
_ => {}
|
||||
}
|
||||
if let Some(ref p) = tab.parent {
|
||||
to_watch.insert(&p.cwd);
|
||||
}
|
||||
if let Some(h) = tab.current.hovered().filter(|&h| h.is_dir()) {
|
||||
to_watch.insert(&h.url);
|
||||
}
|
||||
}
|
||||
self.watcher.watch(to_watch);
|
||||
|
||||
// Trigger peek
|
||||
emit!(Peek);
|
||||
|
||||
// Hover
|
||||
let opt = opt.into() as Opt;
|
||||
self.current_mut().repos(opt.url)
|
||||
b
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,86 @@
|
|||
use crate::manager::Manager;
|
||||
use yazi_config::{keymap::{Exec, KeymapLayer}, MANAGER};
|
||||
use yazi_shared::{Url, MIME_DIR};
|
||||
|
||||
use crate::{emit, manager::Manager};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Opt {
|
||||
step: isize,
|
||||
only_if: Option<Url>,
|
||||
upper_bound: bool,
|
||||
}
|
||||
|
||||
impl From<&Exec> for Opt {
|
||||
fn from(e: &Exec) -> Self {
|
||||
Self {
|
||||
step: e.args.first().and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||
only_if: e.named.get("only-if").map(Url::from),
|
||||
upper_bound: e.named.contains_key("upper-bound"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<isize> for Opt {
|
||||
fn from(step: isize) -> Self { Self { step, only_if: None, upper_bound: false } }
|
||||
}
|
||||
|
||||
impl Manager {
|
||||
pub fn peek(&mut self, sequent: bool, show_image: bool) -> bool {
|
||||
let Some(hovered) = self.hovered().cloned() else {
|
||||
return self.active_mut().preview.reset(|_| true);
|
||||
#[inline]
|
||||
pub fn _peek_upper_bound(bound: usize, only_if: &Url) {
|
||||
emit!(Call(
|
||||
Exec::call("peek", vec![bound.to_string()])
|
||||
.with("only-if", only_if.to_string())
|
||||
.with_bool("upper-bound", true)
|
||||
.vec(),
|
||||
KeymapLayer::Manager
|
||||
));
|
||||
}
|
||||
|
||||
pub fn peek(&mut self, opt: impl Into<Opt>) -> bool {
|
||||
let Some(hovered) = self.hovered() else {
|
||||
return self.active_mut().preview.reset();
|
||||
};
|
||||
|
||||
let url = &hovered.url;
|
||||
if !show_image {
|
||||
self.active_mut().preview.reset(|l| l.is_image());
|
||||
}
|
||||
|
||||
if hovered.is_dir() {
|
||||
let position = self.active().history(url).map(|f| (f.offset, f.files.len()));
|
||||
self.active_mut().preview.folder(url, position, sequent);
|
||||
let opt = opt.into() as Opt;
|
||||
if matches!(opt.only_if, Some(ref u) if *u != hovered.url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(mime) = self.mimetype.get(url).cloned() else {
|
||||
return self.active_mut().preview.reset(|_| true);
|
||||
if hovered.is_dir() {
|
||||
return self.peek_folder(opt, hovered.url.clone());
|
||||
}
|
||||
|
||||
let Some(mime) = self.mimetype.get(&hovered.url).cloned() else {
|
||||
return self.active_mut().preview.reset();
|
||||
};
|
||||
|
||||
if sequent {
|
||||
self.active_mut().preview.sequent(url, &mime, show_image);
|
||||
let (url, cha) = (hovered.url.clone(), hovered.cha);
|
||||
if self.active().preview.same_url(&url) {
|
||||
self.active_mut().preview.arrow(opt.step, &mime);
|
||||
} else if opt.upper_bound {
|
||||
self.active_mut().preview.apply_bound(opt.step as usize);
|
||||
} else {
|
||||
self.active_mut().preview.go(url, &mime, show_image);
|
||||
self.active_mut().preview.set_skip(0);
|
||||
}
|
||||
|
||||
self.active_mut().preview.go(&url, cha, &mime);
|
||||
false
|
||||
}
|
||||
|
||||
fn peek_folder(&mut self, opt: Opt, url: Url) -> bool {
|
||||
let folder = self.active().history.get(&url);
|
||||
let (skip, bound) = folder
|
||||
.map(|f| (f.offset, f.files.len().saturating_sub(MANAGER.layout.folder_height())))
|
||||
.unwrap_or_default();
|
||||
|
||||
if self.active().preview.same_url(&url) {
|
||||
self.active_mut().preview.arrow(opt.step, MIME_DIR);
|
||||
self.active_mut().preview.apply_bound(bound);
|
||||
return false;
|
||||
}
|
||||
|
||||
let in_chunks = folder.is_none();
|
||||
self.active_mut().preview.set_skip(skip);
|
||||
self.active_mut().preview.go_folder(url, in_chunks);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ impl Tabs {
|
|||
#[inline]
|
||||
pub(super) fn set_idx(&mut self, idx: usize) {
|
||||
self.idx = idx;
|
||||
self.active_mut().preview.reset(|l| l.is_image());
|
||||
self.active_mut().preview.reset_image();
|
||||
Manager::_refresh();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,3 +3,5 @@ mod provider;
|
|||
|
||||
pub use preview::*;
|
||||
use provider::*;
|
||||
|
||||
pub static COLLISION: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
use std::time::Duration;
|
||||
use std::{mem, time::Duration};
|
||||
|
||||
use tokio::{pin, task::JoinHandle};
|
||||
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
|
||||
use yazi_adaptor::ADAPTOR;
|
||||
use yazi_config::MANAGER;
|
||||
use yazi_shared::{MimeKind, PeekError, Url, MIME_DIR};
|
||||
use yazi_shared::{Cha, MimeKind, PeekError, Url};
|
||||
|
||||
use super::Provider;
|
||||
use crate::{emit, files::{Files, FilesOp}, Highlighter};
|
||||
use crate::{emit, files::{Files, FilesOp}, manager::Manager, Highlighter};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Preview {
|
||||
|
|
@ -19,7 +19,7 @@ pub struct Preview {
|
|||
|
||||
pub struct PreviewLock {
|
||||
pub url: Url,
|
||||
pub mime: String,
|
||||
pub cha: Option<Cha>,
|
||||
pub skip: usize,
|
||||
pub data: PreviewData,
|
||||
}
|
||||
|
|
@ -32,125 +32,59 @@ pub enum PreviewData {
|
|||
}
|
||||
|
||||
impl Preview {
|
||||
pub fn go(&mut self, url: &Url, mime: &str, show_image: bool) {
|
||||
let kind = MimeKind::new(mime);
|
||||
if !show_image && kind.show_as_image() {
|
||||
return;
|
||||
} else if self.same(url, mime) {
|
||||
pub fn go(&mut self, url: &Url, cha: Cha, mime: &str) {
|
||||
if self.content_unchanged(url, &cha) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.reset(|_| true);
|
||||
if !self.same_mime(url, mime) {
|
||||
self.skip = 0;
|
||||
}
|
||||
self.reset();
|
||||
let (url, kind, skip) = (url.clone(), MimeKind::new(mime), self.skip);
|
||||
|
||||
let (url, mime, skip) = (url.clone(), mime.to_owned(), self.skip);
|
||||
self.handle = Some(tokio::spawn(async move {
|
||||
match Provider::auto(kind, &url, skip).await {
|
||||
Ok(data) => {
|
||||
emit!(Preview(PreviewLock { url, mime, skip, data }));
|
||||
emit!(Preview(PreviewLock { url, cha: Some(cha), skip, data }));
|
||||
}
|
||||
Err(PeekError::Exceed(max)) => {
|
||||
emit!(Peek(max, url));
|
||||
Manager::_peek_upper_bound(max, &url);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn folder(&mut self, url: &Url, position: Option<(usize, usize)>, sequent: bool) {
|
||||
if let Some((_, len)) = position {
|
||||
self.skip = self.skip.min(len.saturating_sub(MANAGER.layout.preview_height()));
|
||||
}
|
||||
|
||||
if self.same(url, MIME_DIR) {
|
||||
return;
|
||||
} else if !self.same_mime(url, MIME_DIR) {
|
||||
self.skip = position.map(|(offset, _)| offset).unwrap_or(0);
|
||||
}
|
||||
|
||||
self.reset(|_| true);
|
||||
emit!(Preview(PreviewLock {
|
||||
pub fn go_folder(&mut self, url: Url, in_chunks: bool) {
|
||||
self.reset();
|
||||
self.lock = Some(PreviewLock {
|
||||
url: url.clone(),
|
||||
mime: MIME_DIR.to_owned(),
|
||||
cha: None,
|
||||
skip: self.skip,
|
||||
data: PreviewData::Folder,
|
||||
}));
|
||||
});
|
||||
|
||||
if sequent {
|
||||
return;
|
||||
}
|
||||
|
||||
let url = url.clone();
|
||||
self.handle = Some(tokio::spawn(async move {
|
||||
let Ok(rx) = Files::from_dir(&url).await else {
|
||||
emit!(Files(FilesOp::IOErr(url)));
|
||||
emit!(Files(FilesOp::IOErr(url.clone())));
|
||||
return;
|
||||
};
|
||||
|
||||
if position.is_some() {
|
||||
emit!(Files(FilesOp::Full(url, UnboundedReceiverStream::new(rx).collect().await)));
|
||||
if !in_chunks {
|
||||
emit!(Files(FilesOp::Full(url.clone(), UnboundedReceiverStream::new(rx).collect().await)));
|
||||
return;
|
||||
}
|
||||
|
||||
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(10000, Duration::from_millis(500));
|
||||
pin!(rx);
|
||||
let stream =
|
||||
UnboundedReceiverStream::new(rx).chunks_timeout(10000, Duration::from_millis(500));
|
||||
pin!(stream);
|
||||
|
||||
let ticket = FilesOp::prepare(&url);
|
||||
while let Some(chunk) = rx.next().await {
|
||||
while let Some(chunk) = stream.next().await {
|
||||
emit!(Files(FilesOp::Part(url.clone(), ticket, chunk)));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn sequent(&mut self, url: &Url, mime: &str, show_image: bool) {
|
||||
let kind = MimeKind::new(mime);
|
||||
if !show_image && kind.show_as_image() {
|
||||
return;
|
||||
} else if self.same(url, mime) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.handle.take().map(|h| h.abort());
|
||||
Highlighter::abort();
|
||||
|
||||
let (url, mime, skip) = (url.clone(), mime.to_owned(), self.skip);
|
||||
self.handle = Some(tokio::spawn(async move {
|
||||
match Provider::auto(kind, &url, skip).await {
|
||||
Ok(data) => {
|
||||
emit!(Preview(PreviewLock { url, mime, skip, data }));
|
||||
}
|
||||
Err(PeekError::Exceed(max)) => {
|
||||
emit!(Peek(max, url));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn arrow(&mut self, step: isize) -> bool {
|
||||
let Some(kind) = self.lock.as_ref().map(|l| MimeKind::new(&l.mime)) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let old = self.skip;
|
||||
let size = Provider::step_size(kind, step.unsigned_abs());
|
||||
|
||||
self.skip = if step < 0 { old.saturating_sub(size) } else { old + size };
|
||||
self.skip != old
|
||||
}
|
||||
|
||||
pub fn arrow_max(&mut self, max: usize) -> bool {
|
||||
if self.skip > max {
|
||||
self.skip = max;
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn reset<F: FnOnce(&PreviewLock) -> bool>(&mut self, f: F) -> bool {
|
||||
pub fn reset(&mut self) -> bool {
|
||||
self.handle.take().map(|h| h.abort());
|
||||
Highlighter::abort();
|
||||
ADAPTOR.image_hide(MANAGER.layout.image_rect()).ok();
|
||||
|
|
@ -159,39 +93,71 @@ impl Preview {
|
|||
return false;
|
||||
};
|
||||
|
||||
if !f(lock) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let b = !lock.is_image();
|
||||
self.lock = None;
|
||||
b
|
||||
}
|
||||
|
||||
pub fn reset_image(&mut self) -> bool {
|
||||
if !matches!(self.lock, Some(ref lock) if lock.is_image()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.reset();
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn same_url(&self, url: &Url) -> bool {
|
||||
matches!(self.lock, Some(ref lock) if lock.url == *url)
|
||||
}
|
||||
|
||||
fn content_unchanged(&self, url: &Url, cha: &Cha) -> bool {
|
||||
let Some(lock) = &self.lock else {
|
||||
return false;
|
||||
};
|
||||
let Some(cha_) = &lock.cha else {
|
||||
return false;
|
||||
};
|
||||
|
||||
*url == lock.url
|
||||
&& self.skip == lock.skip
|
||||
&& cha.len == cha_.len
|
||||
&& cha.modified == cha_.modified
|
||||
&& cha.meta == cha_.meta
|
||||
&& {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
cha.permissions == cha_.permissions
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Preview {
|
||||
// --- skip
|
||||
#[inline]
|
||||
pub fn same(&self, url: &Url, mime: &str) -> bool {
|
||||
if let Some(ref lock) = self.lock {
|
||||
return &lock.url == url && lock.mime == mime && lock.skip == self.skip;
|
||||
}
|
||||
false
|
||||
pub fn arrow(&mut self, step: isize, mime: &str) -> bool {
|
||||
let size = Provider::step_size(MimeKind::new(mime), step.unsigned_abs());
|
||||
let skip = if step < 0 { self.skip.saturating_sub(size) } else { self.skip + size };
|
||||
mem::replace(&mut self.skip, skip) != skip
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn same_mime(&self, url: &Url, mime: &str) -> bool {
|
||||
if let Some(ref lock) = self.lock {
|
||||
return &lock.url == url && lock.mime == mime;
|
||||
}
|
||||
false
|
||||
}
|
||||
pub fn set_skip(&mut self, skip: usize) -> bool { mem::replace(&mut self.skip, skip) != skip }
|
||||
|
||||
#[inline]
|
||||
pub fn same_path(&self, url: &Url) -> bool {
|
||||
if let Some(ref lock) = self.lock {
|
||||
return &lock.url == url;
|
||||
pub fn apply_bound(&mut self, upper: usize) -> bool {
|
||||
if self.skip <= upper {
|
||||
return false;
|
||||
}
|
||||
false
|
||||
|
||||
self.skip = upper;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use yazi_config::keymap::Exec;
|
||||
|
||||
use crate::{emit, tab::Tab};
|
||||
use crate::{manager::Manager, tab::Tab};
|
||||
|
||||
impl Tab {
|
||||
pub fn hidden(&mut self, e: &Exec) -> bool {
|
||||
|
|
@ -10,7 +10,7 @@ impl Tab {
|
|||
_ => !self.conf.show_hidden,
|
||||
};
|
||||
if self.apply_files_attrs(false) {
|
||||
emit!(Peek);
|
||||
Manager::_hover(None);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ impl Tab {
|
|||
handle.abort();
|
||||
}
|
||||
if self.current.cwd.is_search() {
|
||||
self.preview.reset(|l| l.is_image());
|
||||
self.preview.reset_image();
|
||||
|
||||
let rep = self.history_new(&self.current.cwd.to_regular());
|
||||
drop(mem::replace(&mut self.current, rep));
|
||||
|
|
|
|||
|
|
@ -47,21 +47,9 @@ impl From<&Url> for Tab {
|
|||
}
|
||||
|
||||
impl Tab {
|
||||
pub fn update_peek(&mut self, max: usize, url: Url) -> bool {
|
||||
let Some(hovered) = self.current.hovered() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if url != hovered.url {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.preview.arrow_max(max)
|
||||
}
|
||||
|
||||
pub fn update_preview(&mut self, lock: PreviewLock) -> bool {
|
||||
let Some(hovered) = self.current.hovered().map(|h| &h.url) else {
|
||||
return self.preview.reset(|_| true);
|
||||
return self.preview.reset();
|
||||
};
|
||||
|
||||
if lock.url != *hovered {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use yazi_config::keymap::Exec;
|
||||
|
||||
use crate::{emit, tasks::Tasks};
|
||||
use crate::tasks::Tasks;
|
||||
|
||||
pub struct Opt;
|
||||
|
||||
|
|
@ -14,7 +14,6 @@ impl From<()> for Opt {
|
|||
impl Tasks {
|
||||
pub fn toggle(&mut self, _: impl Into<Opt>) -> bool {
|
||||
self.visible = !self.visible;
|
||||
emit!(Peek); // Show/hide preview for images
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ impl Which {
|
|||
self.times = 1;
|
||||
self.cands =
|
||||
KEYMAP.get(layer).iter().filter(|s| s.on.len() > 1 && &s.on[0] == key).cloned().collect();
|
||||
self.switch(true);
|
||||
self.visible = true;
|
||||
true
|
||||
}
|
||||
|
||||
|
|
@ -35,22 +35,16 @@ impl Which {
|
|||
.collect();
|
||||
|
||||
if self.cands.is_empty() {
|
||||
self.switch(false);
|
||||
self.visible = false;
|
||||
} else if self.cands.len() == 1 {
|
||||
self.switch(false);
|
||||
self.visible = false;
|
||||
emit!(Call(self.cands[0].to_call(), self.layer));
|
||||
} else if let Some(i) = self.cands.iter().position(|c| c.on.len() == self.times + 1) {
|
||||
self.switch(false);
|
||||
self.visible = false;
|
||||
emit!(Call(self.cands[i].to_call(), self.layer));
|
||||
}
|
||||
|
||||
self.times += 1;
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn switch(&mut self, state: bool) {
|
||||
self.visible = state;
|
||||
emit!(Peek); // Show/hide preview for images
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
use std::ffi::OsString;
|
||||
use std::{ffi::OsString, sync::atomic::Ordering};
|
||||
|
||||
use anyhow::{Ok, Result};
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::prelude::Rect;
|
||||
use ratatui::{backend::Backend, prelude::Rect};
|
||||
use tokio::sync::oneshot;
|
||||
use yazi_config::{keymap::{Exec, Key, KeymapLayer}, BOOT};
|
||||
use yazi_core::{emit, files::FilesOp, input::InputMode, manager::Manager, Ctx, Event};
|
||||
use yazi_core::{emit, files::FilesOp, input::InputMode, manager::Manager, preview::COLLISION, Ctx, Event};
|
||||
use yazi_shared::Term;
|
||||
|
||||
use crate::{Executor, Logs, Panic, Root, Signals};
|
||||
|
|
@ -33,7 +33,7 @@ impl App {
|
|||
}
|
||||
Event::Key(key) => app.dispatch_key(key),
|
||||
Event::Paste(str) => app.dispatch_paste(str),
|
||||
Event::Render(_) => app.dispatch_render(),
|
||||
Event::Render(_) => app.dispatch_render()?,
|
||||
Event::Resize(cols, rows) => app.dispatch_resize(cols, rows),
|
||||
Event::Stop(state, tx) => app.dispatch_stop(state, tx),
|
||||
Event::Call(exec, layer) => app.dispatch_call(exec, layer),
|
||||
|
|
@ -76,18 +76,47 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
fn dispatch_render(&mut self) {
|
||||
if let Some(term) = &mut self.term {
|
||||
_ = term.draw(|f| {
|
||||
yazi_plugin::scope(&self.cx, |_| {
|
||||
f.render_widget(Root::new(&self.cx), f.size());
|
||||
});
|
||||
fn dispatch_render(&mut self) -> Result<()> {
|
||||
let Some(term) = &mut self.term else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Some((x, y)) = self.cx.cursor() {
|
||||
f.set_cursor(x, y);
|
||||
}
|
||||
let collision = COLLISION.swap(false, Ordering::Relaxed);
|
||||
let frame = term.draw(|f| {
|
||||
yazi_plugin::scope(&self.cx, |_| {
|
||||
f.render_widget(Root::new(&self.cx), f.size());
|
||||
});
|
||||
|
||||
if let Some((x, y)) = self.cx.cursor() {
|
||||
f.set_cursor(x, y);
|
||||
}
|
||||
})?;
|
||||
if !COLLISION.load(Ordering::Relaxed) {
|
||||
if collision {
|
||||
// Reload preview if collision is resolved
|
||||
self.cx.manager.active_mut().preview.reset();
|
||||
self.cx.manager.peek(0);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut patches = Vec::new();
|
||||
for x in frame.area.left()..frame.area.right() {
|
||||
for y in frame.area.top()..frame.area.bottom() {
|
||||
let cell = frame.buffer.get(x, y);
|
||||
if cell.skip {
|
||||
patches.push((x, y, cell.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
term.backend_mut().draw(patches.iter().map(|(x, y, cell)| (*x, *y, cell)))?;
|
||||
if let Some((x, y)) = self.cx.cursor() {
|
||||
term.show_cursor()?;
|
||||
term.set_cursor(x, y)?;
|
||||
}
|
||||
term.backend_mut().flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn dispatch_resize(&mut self, cols: u16, rows: u16) {
|
||||
|
|
@ -96,13 +125,13 @@ impl App {
|
|||
}
|
||||
|
||||
self.cx.manager.current_mut().set_page(true);
|
||||
self.cx.manager.active_mut().preview.reset(|_| true);
|
||||
self.cx.manager.peek(true, self.cx.image_layer());
|
||||
self.cx.manager.active_mut().preview.reset();
|
||||
self.cx.manager.peek(0);
|
||||
emit!(Render);
|
||||
}
|
||||
|
||||
fn dispatch_stop(&mut self, state: bool, tx: Option<oneshot::Sender<()>>) {
|
||||
self.cx.manager.active_mut().preview.reset(|l| l.is_image());
|
||||
self.cx.manager.active_mut().preview.reset_image();
|
||||
if state {
|
||||
self.signals.stop_term(true);
|
||||
self.term = None;
|
||||
|
|
@ -148,15 +177,7 @@ impl App {
|
|||
Event::Mimetype(mimes) => {
|
||||
if manager.update_mimetype(mimes, tasks) {
|
||||
emit!(Render);
|
||||
emit!(Peek);
|
||||
}
|
||||
}
|
||||
Event::Peek(sequent) => {
|
||||
if let Some((max, url)) = sequent {
|
||||
manager.active_mut().update_peek(max, url);
|
||||
self.cx.manager.peek(true, self.cx.image_layer());
|
||||
} else {
|
||||
self.cx.manager.peek(false, self.cx.image_layer());
|
||||
manager.peek(0);
|
||||
}
|
||||
}
|
||||
Event::Preview(lock) => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
use std::path::MAIN_SEPARATOR;
|
||||
|
||||
use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, Clear, List, ListItem, Widget}};
|
||||
use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, List, ListItem, Widget}};
|
||||
use yazi_config::{popup::{Offset, Position}, THEME};
|
||||
use yazi_core::Ctx;
|
||||
|
||||
use crate::widgets;
|
||||
|
||||
pub(crate) struct Completion<'a> {
|
||||
cx: &'a Ctx,
|
||||
}
|
||||
|
|
@ -53,7 +55,7 @@ impl<'a> Widget for Completion<'a> {
|
|||
area.height = rect.height.saturating_sub(area.y).min(area.height);
|
||||
}
|
||||
|
||||
Clear.render(area, buf);
|
||||
widgets::Clear.render(area, buf);
|
||||
List::new(items)
|
||||
.block(
|
||||
Block::new()
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ impl<'a> Executor<'a> {
|
|||
};
|
||||
}
|
||||
|
||||
on!(MANAGER, peek);
|
||||
on!(MANAGER, hover);
|
||||
on!(MANAGER, refresh);
|
||||
on!(MANAGER, quit, &self.cx.tasks);
|
||||
|
|
@ -139,11 +140,6 @@ impl<'a> Executor<'a> {
|
|||
on!(TABS, swap);
|
||||
|
||||
match exec.cmd.as_bytes() {
|
||||
b"peek" => {
|
||||
let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||
self.cx.manager.active_mut().preview.arrow(step);
|
||||
self.cx.manager.peek(true, self.cx.image_layer())
|
||||
}
|
||||
// Tasks
|
||||
b"tasks_show" => self.cx.tasks.toggle(()),
|
||||
// Help
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
use ratatui::{buffer::Buffer, layout::{self, Rect}, prelude::{Constraint, Direction}, widgets::{Clear, Paragraph, Widget}};
|
||||
use ratatui::{buffer::Buffer, layout::{self, Rect}, prelude::{Constraint, Direction}, widgets::{Paragraph, Widget}};
|
||||
use yazi_config::THEME;
|
||||
use yazi_core::Ctx;
|
||||
|
||||
use super::Bindings;
|
||||
use crate::widgets;
|
||||
|
||||
pub(crate) struct Layout<'a> {
|
||||
cx: &'a Ctx,
|
||||
|
|
@ -19,7 +20,7 @@ impl<'a> Widget for Layout<'a> {
|
|||
.constraints([Constraint::Min(0), Constraint::Length(1)])
|
||||
.split(area);
|
||||
|
||||
Clear.render(area, buf);
|
||||
widgets::Clear.render(area, buf);
|
||||
|
||||
let help = &self.cx.help;
|
||||
Paragraph::new(help.keyword().unwrap_or_else(|| format!("{}.help", help.layer)))
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
use std::ops::Range;
|
||||
|
||||
use ansi_to_tui::IntoText;
|
||||
use ratatui::{buffer::Buffer, layout::Rect, text::{Line, Text}, widgets::{Block, BorderType, Borders, Clear, Paragraph, Widget}};
|
||||
use ratatui::{buffer::Buffer, layout::Rect, text::{Line, Text}, widgets::{Block, BorderType, Borders, Paragraph, Widget}};
|
||||
use yazi_config::THEME;
|
||||
use yazi_core::{input::InputMode, Ctx};
|
||||
use yazi_shared::Term;
|
||||
|
||||
use crate::widgets;
|
||||
|
||||
pub(crate) struct Input<'a> {
|
||||
cx: &'a Ctx,
|
||||
}
|
||||
|
|
@ -25,7 +27,7 @@ impl<'a> Widget for Input<'a> {
|
|||
Text::from(input.value())
|
||||
};
|
||||
|
||||
Clear.render(area, buf);
|
||||
widgets::Clear.render(area, buf);
|
||||
Paragraph::new(value)
|
||||
.block(
|
||||
Block::new()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ mod select;
|
|||
mod signals;
|
||||
mod tasks;
|
||||
mod which;
|
||||
mod widgets;
|
||||
|
||||
use app::*;
|
||||
use executor::*;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, Clear, List, ListItem, Widget}};
|
||||
use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, List, ListItem, Widget}};
|
||||
use yazi_config::THEME;
|
||||
use yazi_core::Ctx;
|
||||
|
||||
use crate::widgets;
|
||||
|
||||
pub(crate) struct Select<'a> {
|
||||
cx: &'a Ctx,
|
||||
}
|
||||
|
|
@ -28,7 +30,7 @@ impl<'a> Widget for Select<'a> {
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Clear.render(area, buf);
|
||||
widgets::Clear.render(area, buf);
|
||||
List::new(items)
|
||||
.block(
|
||||
Block::new()
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
use ratatui::{buffer::Buffer, layout::Rect, widgets::{self, Widget}};
|
||||
|
||||
pub(super) struct Clear;
|
||||
|
||||
impl Widget for Clear {
|
||||
fn render(self, mut area: Rect, buf: &mut Buffer) {
|
||||
if area.x > 0 {
|
||||
area.x -= 1;
|
||||
area.width += 2;
|
||||
}
|
||||
|
||||
if area.y > 0 {
|
||||
area.y -= 1;
|
||||
area.height += 2;
|
||||
}
|
||||
|
||||
widgets::Clear.render(area, buf)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ use ratatui::{buffer::Buffer, layout::{self, Alignment, Constraint, Direction, R
|
|||
use yazi_config::THEME;
|
||||
use yazi_core::{tasks::TASKS_PERCENT, Ctx};
|
||||
|
||||
use super::Clear;
|
||||
use crate::widgets;
|
||||
|
||||
pub(crate) struct Layout<'a> {
|
||||
cx: &'a Ctx,
|
||||
|
|
@ -36,7 +36,7 @@ impl<'a> Widget for Layout<'a> {
|
|||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let area = Self::area(area);
|
||||
|
||||
Clear.render(area, buf);
|
||||
widgets::Clear.render(area, buf);
|
||||
let block = Block::new()
|
||||
.title({
|
||||
let mut line = Line::from("Tasks");
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
mod clear;
|
||||
mod layout;
|
||||
|
||||
use clear::*;
|
||||
pub(super) use layout::*;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
use ratatui::{layout, prelude::{Buffer, Constraint, Direction, Rect}, widgets::{Block, Clear, Widget}};
|
||||
use ratatui::{layout, prelude::{Buffer, Constraint, Direction, Rect}, widgets::{Block, Widget}};
|
||||
use yazi_config::THEME;
|
||||
use yazi_core::Ctx;
|
||||
|
||||
use super::Side;
|
||||
use crate::widgets;
|
||||
|
||||
pub(crate) struct Which<'a> {
|
||||
cx: &'a Ctx,
|
||||
|
|
@ -38,7 +39,7 @@ impl Widget for Which<'_> {
|
|||
.constraints([Constraint::Ratio(1, 3), Constraint::Ratio(1, 3), Constraint::Ratio(1, 3)])
|
||||
.split(area);
|
||||
|
||||
Clear.render(area, buf);
|
||||
widgets::Clear.render(area, buf);
|
||||
Block::new().style(THEME.which.mask.into()).render(area, buf);
|
||||
Side::new(which.times, cands.0).render(chunks[0], buf);
|
||||
Side::new(which.times, cands.1).render(chunks[1], buf);
|
||||
|
|
|
|||
43
yazi-fm/src/widgets/clear.rs
Normal file
43
yazi-fm/src/widgets/clear.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
use std::sync::atomic::Ordering;
|
||||
|
||||
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
|
||||
use yazi_adaptor::ADAPTOR;
|
||||
use yazi_config::MANAGER;
|
||||
use yazi_core::preview::COLLISION;
|
||||
|
||||
pub(crate) struct Clear;
|
||||
|
||||
#[inline]
|
||||
const fn is_overlapping(a: &Rect, b: &Rect) -> bool {
|
||||
a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y
|
||||
}
|
||||
|
||||
fn overlap(a: &Rect, b: &Rect) -> Option<Rect> {
|
||||
if !is_overlapping(a, b) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let x = a.x.max(b.x);
|
||||
let y = a.y.max(b.y);
|
||||
let width = (a.x + a.width).min(b.x + b.width) - x;
|
||||
let height = (a.y + a.height).min(b.y + b.height) - y;
|
||||
Some(Rect { x, y, width, height })
|
||||
}
|
||||
|
||||
impl Widget for Clear {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
ratatui::widgets::Clear.render(area, buf);
|
||||
|
||||
let Some(r) = overlap(&area, &MANAGER.layout.image_rect()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
ADAPTOR.image_hide(r).ok();
|
||||
COLLISION.store(true, Ordering::Relaxed);
|
||||
for x in r.left()..r.right() {
|
||||
for y in r.top()..r.bottom() {
|
||||
buf.get_mut(x, y).set_skip(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
yazi-fm/src/widgets/mod.rs
Normal file
3
yazi-fm/src/widgets/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
mod clear;
|
||||
|
||||
pub(super) use clear::*;
|
||||
|
|
@ -14,17 +14,11 @@ impl<'a> Preview<'a> {
|
|||
|
||||
impl<'a> Widget for Preview<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let manager = &self.cx.manager;
|
||||
let Some(hovered) = manager.hovered().map(|h| &h.url) else {
|
||||
let Some(ref lock) = self.cx.manager.active().preview.lock else {
|
||||
return;
|
||||
};
|
||||
|
||||
let preview = &manager.active().preview;
|
||||
if !preview.same_path(hovered) {
|
||||
return;
|
||||
}
|
||||
|
||||
match &preview.lock.as_ref().unwrap().data {
|
||||
match &lock.data {
|
||||
PreviewData::Folder => {
|
||||
Folder::preview(self.cx).render(area, buf);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::{fs::Metadata, time::SystemTime};
|
|||
use bitflags::bitflags;
|
||||
|
||||
bitflags! {
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct ChaMeta: u8 {
|
||||
const DIR = 0b00000001;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue