mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
feat: Vim-like notification (#659)
This commit is contained in:
parent
a764e42098
commit
22ecad47ab
22 changed files with 387 additions and 31 deletions
|
|
@ -25,8 +25,8 @@ ueberzug_offset = [ 0, 0, 0, 0 ]
|
||||||
|
|
||||||
[opener]
|
[opener]
|
||||||
edit = [
|
edit = [
|
||||||
{ exec = '$EDITOR "$@"', block = true, for = "unix" },
|
{ exec = '[ -n "$EDITOR" ] && $EDITOR "$@"', desc = "$EDITOR", block = true, for = "unix" },
|
||||||
{ exec = 'code "%*"', orphan = true, for = "windows" },
|
{ exec = 'code "%*"', orphan = true, for = "windows" },
|
||||||
]
|
]
|
||||||
open = [
|
open = [
|
||||||
{ exec = 'xdg-open "$@"', desc = "Open", for = "linux" },
|
{ exec = 'xdg-open "$@"', desc = "Open", for = "linux" },
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ pub mod folder;
|
||||||
pub mod help;
|
pub mod help;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod manager;
|
pub mod manager;
|
||||||
|
pub mod notify;
|
||||||
pub mod select;
|
pub mod select;
|
||||||
mod step;
|
mod step;
|
||||||
pub mod tab;
|
pub mod tab;
|
||||||
|
|
|
||||||
2
yazi-core/src/notify/commands/mod.rs
Normal file
2
yazi-core/src/notify/commands/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
mod push;
|
||||||
|
mod tick;
|
||||||
19
yazi-core/src/notify/commands/push.rs
Normal file
19
yazi-core/src/notify/commands/push.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use yazi_shared::{emit, event::Cmd, Layer};
|
||||||
|
|
||||||
|
use crate::notify::{Message, Notify};
|
||||||
|
|
||||||
|
impl Notify {
|
||||||
|
pub fn push(&mut self, msg: impl TryInto<Message>) {
|
||||||
|
let Ok(mut msg) = msg.try_into() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let instant = Instant::now();
|
||||||
|
msg.timeout += instant - self.messages.first().map_or(instant, |m| m.instant);
|
||||||
|
self.messages.push(msg);
|
||||||
|
|
||||||
|
emit!(Call(Cmd::args("update_notify", vec![0.to_string()]), Layer::App));
|
||||||
|
}
|
||||||
|
}
|
||||||
67
yazi-core/src/notify/commands/tick.rs
Normal file
67
yazi-core/src/notify/commands/tick.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use yazi_shared::{emit, event::Cmd, Layer};
|
||||||
|
|
||||||
|
use crate::notify::Notify;
|
||||||
|
|
||||||
|
pub struct Opt {
|
||||||
|
interval: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Cmd> for Opt {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(mut c: Cmd) -> Result<Self, Self::Error> {
|
||||||
|
let interval = c.take_first().and_then(|s| s.parse::<f64>().ok()).ok_or(())?;
|
||||||
|
if interval < 0.0 {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { interval: Duration::from_secs_f64(interval) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Notify {
|
||||||
|
pub fn tick(&mut self, opt: impl TryInto<Opt>) {
|
||||||
|
self.tick_handle.take().map(|h| h.abort());
|
||||||
|
let Ok(opt) = opt.try_into() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let limit = self.limit();
|
||||||
|
if limit == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for m in &mut self.messages[..limit] {
|
||||||
|
if m.timeout.is_zero() {
|
||||||
|
m.percent = m.percent.saturating_sub(20);
|
||||||
|
} else if m.percent < 100 {
|
||||||
|
m.percent = m.percent.saturating_add(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 timeouts: Vec<_> = self.messages[..limit]
|
||||||
|
.iter()
|
||||||
|
.filter(|&m| m.percent == 100 && !m.timeout.is_zero())
|
||||||
|
.map(|m| m.timeout)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let interval = if timeouts.len() != limit {
|
||||||
|
Duration::from_millis(50)
|
||||||
|
} else if let Some(min) = timeouts.iter().min() {
|
||||||
|
*min
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.tick_handle = Some(tokio::spawn(async move {
|
||||||
|
tokio::time::sleep(interval).await;
|
||||||
|
emit!(Call(Cmd::args("update_notify", vec![interval.as_secs_f64().to_string()]), Layer::App));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
20
yazi-core/src/notify/level.rs
Normal file
20
yazi-core/src/notify/level.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
pub enum Level {
|
||||||
|
Info,
|
||||||
|
Warn,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Level {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(match s {
|
||||||
|
"info" => Self::Info,
|
||||||
|
"warn" => Self::Warn,
|
||||||
|
"error" => Self::Error,
|
||||||
|
_ => return Err(()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
47
yazi-core/src/notify/message.rs
Normal file
47
yazi-core/src/notify/message.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use yazi_shared::event::Cmd;
|
||||||
|
|
||||||
|
use super::{Level, NOTIFY_BORDER};
|
||||||
|
|
||||||
|
pub struct Message {
|
||||||
|
pub title: String,
|
||||||
|
pub content: String,
|
||||||
|
pub level: Level,
|
||||||
|
|
||||||
|
pub instant: Instant,
|
||||||
|
pub timeout: Duration,
|
||||||
|
|
||||||
|
pub lines: usize,
|
||||||
|
pub percent: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Cmd> for Message {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(mut c: Cmd) -> Result<Self, Self::Error> {
|
||||||
|
let timeout = c.take_name("timeout").and_then(|s| s.parse::<f64>().ok()).ok_or(())?;
|
||||||
|
if timeout < 0.0 {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = c.take_name("content").ok_or(())?;
|
||||||
|
let lines = content.lines().count();
|
||||||
|
Ok(Self {
|
||||||
|
title: c.take_name("title").ok_or(())?,
|
||||||
|
content,
|
||||||
|
level: c.take_name("level").ok_or(())?.parse()?,
|
||||||
|
|
||||||
|
instant: Instant::now(),
|
||||||
|
timeout: Duration::from_secs_f64(timeout),
|
||||||
|
|
||||||
|
lines,
|
||||||
|
percent: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message {
|
||||||
|
#[inline]
|
||||||
|
pub fn height(&self) -> usize { self.lines + NOTIFY_BORDER as usize }
|
||||||
|
}
|
||||||
11
yazi-core/src/notify/mod.rs
Normal file
11
yazi-core/src/notify/mod.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
mod commands;
|
||||||
|
mod level;
|
||||||
|
mod message;
|
||||||
|
mod notify;
|
||||||
|
|
||||||
|
pub use level::*;
|
||||||
|
pub use message::*;
|
||||||
|
pub use notify::*;
|
||||||
|
|
||||||
|
pub const NOTIFY_BORDER: u16 = 2;
|
||||||
|
pub const NOTIFY_SPACING: u16 = 1;
|
||||||
36
yazi-core/src/notify/notify.rs
Normal file
36
yazi-core/src/notify/notify.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
use std::ops::ControlFlow;
|
||||||
|
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
use yazi_shared::term::Term;
|
||||||
|
|
||||||
|
use super::{Message, NOTIFY_SPACING};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Notify {
|
||||||
|
pub(super) tick_handle: Option<JoinHandle<()>>,
|
||||||
|
pub messages: Vec<Message>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Notify {
|
||||||
|
pub fn limit(&self) -> usize {
|
||||||
|
if self.messages.is_empty() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut height = Term::size().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) {
|
||||||
|
Some(h) => {
|
||||||
|
height = h;
|
||||||
|
ControlFlow::Continue(acc + 1)
|
||||||
|
}
|
||||||
|
None => ControlFlow::Break(acc),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
1.max(match flow {
|
||||||
|
ControlFlow::Continue(i) => i,
|
||||||
|
ControlFlow::Break(i) => i,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
|
mod notify;
|
||||||
mod plugin;
|
mod plugin;
|
||||||
mod quit;
|
mod quit;
|
||||||
mod render;
|
mod render;
|
||||||
mod resize;
|
mod resize;
|
||||||
mod resume;
|
mod resume;
|
||||||
mod stop;
|
mod stop;
|
||||||
|
mod update_notify;
|
||||||
mod update_progress;
|
mod update_progress;
|
||||||
|
|
|
||||||
7
yazi-fm/src/app/commands/notify.rs
Normal file
7
yazi-fm/src/app/commands/notify.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
use yazi_core::notify::Message;
|
||||||
|
|
||||||
|
use crate::app::App;
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub(crate) fn notify(&mut self, msg: impl TryInto<Message>) { self.cx.notify.push(msg); }
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
use ratatui::backend::Backend;
|
use ratatui::{backend::{Backend, CrosstermBackend}, CompletedFrame};
|
||||||
|
|
||||||
use crate::{app::App, lives::Lives, root::{Root, COLLISION}};
|
use crate::{app::App, lives::Lives, notify::Notify, root::{Root, COLLISION}};
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub(crate) fn render(&mut self) {
|
pub(crate) fn render(&mut self) {
|
||||||
|
|
@ -14,35 +14,68 @@ impl App {
|
||||||
let frame = term
|
let frame = term
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
_ = Lives::scope(&self.cx, |_| Ok(f.render_widget(Root::new(&self.cx), f.size())));
|
_ = Lives::scope(&self.cx, |_| Ok(f.render_widget(Root::new(&self.cx), f.size())));
|
||||||
|
|
||||||
if let Some((x, y)) = self.cx.cursor() {
|
if let Some((x, y)) = self.cx.cursor() {
|
||||||
f.set_cursor(x, y);
|
f.set_cursor(x, y);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
if !COLLISION.load(Ordering::Relaxed) {
|
if COLLISION.load(Ordering::Relaxed) {
|
||||||
if collision {
|
Self::patch(frame, self.cx.cursor());
|
||||||
// Reload preview if collision is resolved
|
}
|
||||||
self.cx.manager.peek(true);
|
if !self.cx.notify.messages.is_empty() {
|
||||||
}
|
self.render_notify();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut patch = vec![];
|
// Reload preview if collision is resolved
|
||||||
for x in frame.area.left()..frame.area.right() {
|
if collision && !COLLISION.load(Ordering::Relaxed) {
|
||||||
for y in frame.area.top()..frame.area.bottom() {
|
self.cx.manager.peek(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn render_notify(&mut self) {
|
||||||
|
let Some(term) = &mut self.term else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !term.can_partial() {
|
||||||
|
return self.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
let frame = term
|
||||||
|
.draw_partial(|f| {
|
||||||
|
f.render_widget(Notify::new(&self.cx), f.size());
|
||||||
|
|
||||||
|
if let Some((x, y)) = self.cx.cursor() {
|
||||||
|
f.set_cursor(x, y);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if COLLISION.load(Ordering::Relaxed) {
|
||||||
|
Self::patch(frame, self.cx.cursor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn patch(frame: CompletedFrame, cursor: Option<(u16, u16)>) {
|
||||||
|
let mut patches = vec![];
|
||||||
|
for y in frame.area.top()..frame.area.bottom() {
|
||||||
|
for x in frame.area.left()..frame.area.right() {
|
||||||
let cell = frame.buffer.get(x, y);
|
let cell = frame.buffer.get(x, y);
|
||||||
if cell.skip {
|
if cell.skip {
|
||||||
patch.push((x, y, cell.clone()));
|
patches.push((x, y, cell));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
term.backend_mut().draw(patch.iter().map(|(x, y, cell)| (*x, *y, cell))).ok();
|
let mut backend = CrosstermBackend::new(std::io::stdout().lock());
|
||||||
if let Some((x, y)) = self.cx.cursor() {
|
backend.draw(patches.into_iter()).ok();
|
||||||
term.show_cursor().ok();
|
if let Some((x, y)) = cursor {
|
||||||
term.set_cursor(x, y).ok();
|
backend.show_cursor().ok();
|
||||||
|
backend.set_cursor(x, y).ok();
|
||||||
}
|
}
|
||||||
term.backend_mut().flush().ok();
|
backend.flush().ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
yazi-fm/src/app/commands/update_notify.rs
Normal file
15
yazi-fm/src/app/commands/update_notify.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
use yazi_shared::event::Cmd;
|
||||||
|
|
||||||
|
use crate::app::App;
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub(crate) fn update_notify(&mut self, cmd: Cmd) {
|
||||||
|
self.cx.notify.tick(cmd);
|
||||||
|
|
||||||
|
if self.cx.notify.messages.is_empty() {
|
||||||
|
self.render();
|
||||||
|
} else {
|
||||||
|
self.render_notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,8 +22,8 @@ impl Progress {
|
||||||
w.render(buf);
|
w.render(buf);
|
||||||
|
|
||||||
let mut patch = Vec::with_capacity(area.width as usize * area.height as usize);
|
let mut patch = Vec::with_capacity(area.width as usize * area.height as usize);
|
||||||
for x in area.left()..area.right() {
|
for y in area.top()..area.bottom() {
|
||||||
for y in area.top()..area.bottom() {
|
for x in area.left()..area.right() {
|
||||||
patch.push((x, y, mem::take(buf.get_mut(x, y))));
|
patch.push((x, y, mem::take(buf.get_mut(x, y))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use yazi_config::popup::{Origin, Position};
|
use yazi_config::popup::{Origin, Position};
|
||||||
use yazi_core::{completion::Completion, help::Help, input::Input, manager::Manager, select::Select, tasks::Tasks, which::Which};
|
use yazi_core::{completion::Completion, help::Help, input::Input, manager::Manager, notify::Notify, select::Select, tasks::Tasks, which::Which};
|
||||||
|
|
||||||
pub struct Ctx {
|
pub struct Ctx {
|
||||||
pub manager: Manager,
|
pub manager: Manager,
|
||||||
|
|
@ -10,6 +10,7 @@ pub struct Ctx {
|
||||||
pub help: Help,
|
pub help: Help,
|
||||||
pub completion: Completion,
|
pub completion: Completion,
|
||||||
pub which: Which,
|
pub which: Which,
|
||||||
|
pub notify: Notify,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ctx {
|
impl Ctx {
|
||||||
|
|
@ -22,6 +23,7 @@ impl Ctx {
|
||||||
help: Default::default(),
|
help: Default::default(),
|
||||||
completion: Default::default(),
|
completion: Default::default(),
|
||||||
which: Default::default(),
|
which: Default::default(),
|
||||||
|
notify: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,10 @@ impl<'a> Executor<'a> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
on!(notify);
|
||||||
on!(plugin);
|
on!(plugin);
|
||||||
on!(plugin_do);
|
on!(plugin_do);
|
||||||
|
on!(update_notify);
|
||||||
on!(update_progress);
|
on!(update_progress);
|
||||||
on!(resize);
|
on!(resize);
|
||||||
on!(stop);
|
on!(stop);
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ mod help;
|
||||||
mod input;
|
mod input;
|
||||||
mod lives;
|
mod lives;
|
||||||
mod logs;
|
mod logs;
|
||||||
|
mod notify;
|
||||||
mod panic;
|
mod panic;
|
||||||
mod root;
|
mod root;
|
||||||
mod router;
|
mod router;
|
||||||
|
|
|
||||||
3
yazi-fm/src/notify/mod.rs
Normal file
3
yazi-fm/src/notify/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
mod notify;
|
||||||
|
|
||||||
|
pub(super) use notify::*;
|
||||||
54
yazi-fm/src/notify/notify.rs
Normal file
54
yazi-fm/src/notify/notify.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
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::Percentage(100), Constraint::Min(40)]).split(area);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,8 +34,8 @@ impl Widget for Clear {
|
||||||
|
|
||||||
ADAPTOR.image_erase(r).ok();
|
ADAPTOR.image_erase(r).ok();
|
||||||
COLLISION.store(true, Ordering::Relaxed);
|
COLLISION.store(true, Ordering::Relaxed);
|
||||||
for x in area.left()..area.right() {
|
for y in area.top()..area.bottom() {
|
||||||
for y in area.top()..area.bottom() {
|
for x in area.left()..area.right() {
|
||||||
buf.get_mut(x, y).set_skip(true);
|
buf.get_mut(x, y).set_skip(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ pub fn permissions(m: libc::mode_t) -> String {
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the max common root of a list of files
|
// Find the max common root in a list of files
|
||||||
// e.g. /a/b/c, /a/b/d -> /a/b
|
// e.g. /a/b/c, /a/b/d -> /a/b
|
||||||
// /aa/bb/cc, /aa/dd/ee -> /aa
|
// /aa/bb/cc, /aa/dd/ee -> /aa
|
||||||
pub fn max_common_root(files: &[impl AsRef<Path>]) -> PathBuf {
|
pub fn max_common_root(files: &[impl AsRef<Path>]) -> PathBuf {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,24 @@
|
||||||
use std::{io::{stdout, Stdout, Write}, mem, ops::{Deref, DerefMut}, sync::atomic::{AtomicBool, Ordering}};
|
use std::{io::{self, stdout, Stdout, Write}, mem, ops::{Deref, DerefMut}, sync::atomic::{AtomicBool, Ordering}};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::{event::{DisableBracketedPaste, DisableFocusChange, EnableBracketedPaste, EnableFocusChange, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, execute, queue, terminal::{disable_raw_mode, enable_raw_mode, supports_keyboard_enhancement, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, WindowSize}};
|
use crossterm::{event::{DisableBracketedPaste, DisableFocusChange, EnableBracketedPaste, EnableFocusChange, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, execute, queue, terminal::{disable_raw_mode, enable_raw_mode, supports_keyboard_enhancement, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, WindowSize}};
|
||||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
use ratatui::{backend::CrosstermBackend, buffer::Buffer, layout::Rect, CompletedFrame, Frame, Terminal};
|
||||||
|
|
||||||
static CSI_U: AtomicBool = AtomicBool::new(false);
|
static CSI_U: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
pub struct Term(Terminal<CrosstermBackend<Stdout>>);
|
pub struct Term {
|
||||||
|
inner: Terminal<CrosstermBackend<Stdout>>,
|
||||||
|
last_area: Rect,
|
||||||
|
last_buffer: Buffer,
|
||||||
|
}
|
||||||
|
|
||||||
impl Term {
|
impl Term {
|
||||||
pub fn start() -> Result<Self> {
|
pub fn start() -> Result<Self> {
|
||||||
let mut term = Self(Terminal::new(CrosstermBackend::new(stdout()))?);
|
let mut term = Self {
|
||||||
|
inner: Terminal::new(CrosstermBackend::new(stdout()))?,
|
||||||
|
last_area: Default::default(),
|
||||||
|
last_buffer: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
queue!(stdout(), EnterAlternateScreen, EnableBracketedPaste, EnableFocusChange)?;
|
queue!(stdout(), EnterAlternateScreen, EnableBracketedPaste, EnableFocusChange)?;
|
||||||
|
|
@ -69,6 +77,32 @@ impl Term {
|
||||||
std::process::exit(f() as i32);
|
std::process::exit(f() as i32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn draw(&mut self, f: impl FnOnce(&mut Frame)) -> io::Result<CompletedFrame> {
|
||||||
|
let last = self.inner.draw(f)?;
|
||||||
|
|
||||||
|
self.last_area = last.area;
|
||||||
|
self.last_buffer = last.buffer.clone();
|
||||||
|
Ok(last)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_partial(&mut self, f: impl FnOnce(&mut Frame)) -> io::Result<CompletedFrame> {
|
||||||
|
self.inner.draw(|frame| {
|
||||||
|
let buffer = frame.buffer_mut();
|
||||||
|
for y in self.last_area.top()..self.last_area.bottom() {
|
||||||
|
for x in self.last_area.left()..self.last_area.right() {
|
||||||
|
let mut cell = self.last_buffer.get(x, y).clone();
|
||||||
|
cell.skip = false;
|
||||||
|
*buffer.get_mut(x, y) = cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f(frame);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn can_partial(&mut self) -> bool { self.last_area == self.inner.get_frame().size() }
|
||||||
|
|
||||||
pub fn size() -> WindowSize {
|
pub fn size() -> WindowSize {
|
||||||
let mut size = WindowSize { rows: 0, columns: 0, width: 0, height: 0 };
|
let mut size = WindowSize { rows: 0, columns: 0, width: 0, height: 0 };
|
||||||
if let Ok(s) = crossterm::terminal::window_size() {
|
if let Ok(s) = crossterm::terminal::window_size() {
|
||||||
|
|
@ -112,9 +146,9 @@ impl Drop for Term {
|
||||||
impl Deref for Term {
|
impl Deref for Term {
|
||||||
type Target = Terminal<CrosstermBackend<Stdout>>;
|
type Target = Terminal<CrosstermBackend<Stdout>>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target { &self.0 }
|
fn deref(&self) -> &Self::Target { &self.inner }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DerefMut for Term {
|
impl DerefMut for Term {
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
|
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue