diff --git a/adaptor/src/image.rs b/adaptor/src/image.rs index 871c1248..5ebb636a 100644 --- a/adaptor/src/image.rs +++ b/adaptor/src/image.rs @@ -9,19 +9,22 @@ use tokio::fs; pub struct Image; impl Image { - pub(super) async fn crop(path: &Path, size: (u16, u16)) -> Result { - let (w, h) = Term::ratio() - .map(|(w, h)| { - let (w, h) = ((size.0 as f64 * w) as u32, (size.1 as f64 * h) as u32); - (w.min(PREVIEW.max_width), h.min(PREVIEW.max_height)) + pub(super) async fn crop( + path: &Path, + (target_width, target_height): (u16, u16), + ) -> Result { + let (max_width, max_height) = Term::ratio() + .map(|(a, b)| { + let (width, height) = ((target_width as f64 * a) as u32, (target_height as f64 * b) as u32); + (width.min(PREVIEW.max_width), height.min(PREVIEW.max_height)) }) .unwrap_or((PREVIEW.max_width, PREVIEW.max_height)); let img = fs::read(path).await?; let img = tokio::task::spawn_blocking(move || -> Result { let img = image::load_from_memory(&img)?; - Ok(if img.width() > w || img.height() > h { - img.resize(w, h, FilterType::Triangle) + Ok(if img.width() > max_width || img.height() > max_height { + img.resize(max_width, max_height, FilterType::Triangle) } else { img }) @@ -34,13 +37,15 @@ impl Image { let cache = cache.as_ref().to_owned(); let result = tokio::task::spawn_blocking(move || { let img = image::load_from_memory(&img)?; - let (w, h) = (PREVIEW.max_width, PREVIEW.max_height); + let (max_width, max_height) = (PREVIEW.max_width, PREVIEW.max_height); - if img.width() <= w && img.height() <= h { + if img.width() <= max_width && img.height() <= max_height { return Ok(false); } - img.resize(w, h, FilterType::Triangle).save_with_format(cache, ImageFormat::Jpeg)?; + img + .resize(max_width, max_height, FilterType::Triangle) + .save_with_format(cache, ImageFormat::Jpeg)?; Ok(true) }); diff --git a/adaptor/src/iterm2.rs b/adaptor/src/iterm2.rs index fc042e28..08c7f20d 100644 --- a/adaptor/src/iterm2.rs +++ b/adaptor/src/iterm2.rs @@ -1,4 +1,7 @@ -use std::{io::{stdout, BufWriter, Write}, path::Path}; +use std::{ + io::{stdout, BufWriter, Write}, + path::Path, +}; use anyhow::Result; use base64::{engine::general_purpose, Engine}; @@ -33,8 +36,6 @@ impl Iterm2 { async fn encode(img: DynamicImage) -> Result> { tokio::task::spawn_blocking(move || { - let size = (img.width(), img.height()); - let mut jpg = vec![]; JpegEncoder::new_with_quality(&mut jpg, 75).encode_image(&img)?; @@ -44,8 +45,8 @@ impl Iterm2 { "{}]1337;File=inline=1;size={};width={}px;height={}px;doNotMoveCursor=1:{}\x07{}", START, jpg.len(), - size.0, - size.1, + img.width(), + img.height(), general_purpose::STANDARD.encode(&jpg), CLOSE )?; diff --git a/adaptor/src/kitty.rs b/adaptor/src/kitty.rs index fbd2980f..497a9ac0 100644 --- a/adaptor/src/kitty.rs +++ b/adaptor/src/kitty.rs @@ -1,4 +1,7 @@ -use std::{io::{stdout, Write}, path::Path}; +use std::{ + io::{stdout, Write}, + path::Path, +}; use anyhow::Result; use base64::{engine::general_purpose, Engine}; @@ -29,7 +32,7 @@ impl Kitty { } async fn encode(img: DynamicImage) -> Result> { - fn output(raw: &[u8], format: u8, size: (u32, u32)) -> Result> { + fn output(raw: &[u8], format: u8, (width, height): (u32, u32)) -> Result> { let b64 = general_purpose::STANDARD.encode(raw).chars().collect::>(); let mut it = b64.chunks(4096).peekable(); @@ -40,8 +43,8 @@ impl Kitty { "{}_Ga=T,f={},s={},v={},m={};{}{}\\{}", START, format, - size.0, - size.1, + width, + height, it.peek().is_some() as u8, first.iter().collect::(), ESCAPE, diff --git a/app/src/help/bindings.rs b/app/src/help/bindings.rs index f3fb2733..b75fdf71 100644 --- a/app/src/help/bindings.rs +++ b/app/src/help/bindings.rs @@ -1,4 +1,9 @@ -use ratatui::{layout::{self, Constraint}, prelude::{Buffer, Direction, Rect}, style::{Color, Style, Stylize}, widgets::{List, ListItem, Widget}}; +use ratatui::{ + layout::{self, Constraint}, + prelude::{Buffer, Direction, Rect}, + style::{Color, Style, Stylize}, + widgets::{List, ListItem, Widget}, +}; use crate::context::Ctx; @@ -7,7 +12,9 @@ pub(super) struct Bindings<'a> { } impl<'a> Bindings<'a> { - pub(super) fn new(cx: &'a Ctx) -> Self { Self { cx } } + pub(super) fn new(cx: &'a Ctx) -> Self { + Self { cx } + } } impl Widget for Bindings<'_> { @@ -17,17 +24,20 @@ impl Widget for Bindings<'_> { return; } - let col1 = bindings + // The First Column + let keys = bindings .iter() .map(|c| ListItem::new(c.on()).style(Style::new().fg(Color::Yellow))) .collect::>(); - let col2 = bindings + // The Second Column + let commands = bindings .iter() .map(|c| ListItem::new(c.exec()).style(Style::new().fg(Color::Cyan))) .collect::>(); - let col3 = bindings + // The Third Column + let desc = bindings .iter() .map(|c| ListItem::new(if let Some(ref desc) = c.desc { desc } else { "-" })) .collect::>(); @@ -43,8 +53,8 @@ impl Widget for Bindings<'_> { Style::new().bg(Color::Black).bold(), ); - List::new(col1).render(chunks[0], buf); - List::new(col2).render(chunks[1], buf); - List::new(col3).render(chunks[2], buf); + for (i, col) in [keys, commands, desc].into_iter().enumerate() { + List::new(col).render(chunks[i], buf); + } } } diff --git a/app/src/manager/#folder.rs# b/app/src/manager/#folder.rs# new file mode 100644 index 00000000..f49a5840 --- /dev/null +++ b/app/src/manager/#folder.rs# @@ -0,0 +1,141 @@ +use core::files::File; + +use config::{MANAGER, THEME}; +use ratatui::{ + buffer::Buffer, + layout::Rect, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{List, ListItem, Widget}, +}; +use shared::short_path; + +use crate::Ctx; + +pub(super) struct Folder<'a> { + cx: &'a Ctx, + folder: &'a core::manager::Folder, + is_preview: bool, + is_selection: bool, + is_find: bool, +} + +impl<'a> Folder<'a> { + pub(super) fn new(cx: &'a Ctx, folder: &'a core::manager::Folder) -> Self { + Self { cx, folder, is_preview: false, is_selection: false, is_find: false } + } + + #[inline] + pub(super) fn with_preview(mut self, state: bool) -> Self { + self.is_preview = state; + self + } + + #[inline] + pub(super) fn with_selection(mut self, state: bool) -> Self { + self.is_selection = state; + self + } + + #[inline] + pub(super) fn with_find(mut self, state: bool) -> Self { + self.is_find = state; + self + } +} + +impl<'a> Folder<'a> { + #[inline] + fn icon(file: &File) -> &'static str { + THEME + .icons + .iter() + .find(|x| x.name.match_path(file.url(), Some(file.is_dir()))) + .map(|x| x.display.as_ref()) + .unwrap_or("") + } + + #[inline] + fn file_style(&self, file: &File) -> Style { + let mimetype = &self.cx.manager.mimetype; + THEME + .filetypes + .iter() + .find(|x| x.matches(file.url(), mimetype.get(file.url()), file.is_dir())) + .map(|x| x.style.get()) + .unwrap_or_else(Style::new) + } +} + +impl<'a> Widget for Folder<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + let active = self.cx.manager.active(); + let mode = active.mode(); + + let window = if self.is_preview { + self.folder.window_for(active.preview().skip()) + } else { + self.folder.window() + }; + + let items: Vec<_> = window + .iter() + .enumerate() + .map(|(i, file)| { + let is_selected = self.folder.files.is_selected(file.url()); + if (!self.is_selection && is_selected) + || (self.is_selection && mode.pending(self.folder.offset() + i, is_selected)) + { + buf.set_style( + Rect { x: area.x.saturating_sub(1), y: i as u16 + 1, width: 1, height: 1 }, + if self.is_selection { + THEME.marker.selecting.get() + } else { + THEME.marker.selected.get() + }, + ); + } + + let hovered = matches!(self.folder.hovered, Some(ref hover) if hover.url() == file.url()); + + let style = match (self.is_preview, hovered) { + (true, true) => THEME.preview.hovered.get(), + (_, true) => THEME.selection.hovered.get(), + _ => self.file_style(file), + }; + + let mut spans = Vec::with_capacity(10); + + spans.push(Span::raw(format!(" {} ", Self::icon(file)))); + spans.push(Span::raw(short_path(file.url(), &self.folder.cwd))); + + if let Some(link_to) = file.link_to() { + if MANAGER.show_symlink { + spans.push(Span::raw(format!(" -> {}", link_to.display()))); + } + } + + if let Some(idx) = active + .finder() + .filter(|&finder| hovered && self.is_find && finder.has_matched()) + .and_then(|finder| finder.matched_idx(file.url())) + { + let len = active.finder().unwrap().matched().len(); + let style = Style::new().fg(Color::Rgb(255, 255, 50)).add_modifier(Modifier::ITALIC); + spans.push(Span::styled( + format!( + " [{}/{}]", + if idx > 99 { ">99".to_string() } else { (idx + 1).to_string() }, + if len > 99 { ">99".to_string() } else { len.to_string() } + ), + style, + )); + } + + ListItem::new(Line::from(spans)).style(style) + }) + .collect(); + + List::new(items).render(area, buf); + } +} diff --git a/app/src/manager/folder.rs b/app/src/manager/folder.rs index 68bafe86..110cb5b9 100644 --- a/app/src/manager/folder.rs +++ b/app/src/manager/folder.rs @@ -1,17 +1,23 @@ use core::files::File; use config::{MANAGER, THEME}; -use ratatui::{buffer::Buffer, layout::Rect, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{List, ListItem, Widget}}; +use ratatui::{ + buffer::Buffer, + layout::Rect, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{List, ListItem, Widget}, +}; use shared::short_path; use crate::Ctx; pub(super) struct Folder<'a> { - cx: &'a Ctx, - folder: &'a core::manager::Folder, - is_preview: bool, + cx: &'a Ctx, + folder: &'a core::manager::Folder, + is_preview: bool, is_selection: bool, - is_find: bool, + is_find: bool, } impl<'a> Folder<'a> { @@ -75,8 +81,8 @@ impl<'a> Widget for Folder<'a> { let items: Vec<_> = window .iter() .enumerate() - .map(|(i, f)| { - let is_selected = self.folder.files.is_selected(f.url()); + .map(|(i, file)| { + let is_selected = self.folder.files.is_selected(file.url()); if (!self.is_selection && is_selected) || (self.is_selection && mode.pending(self.folder.offset() + i, is_selected)) { @@ -90,21 +96,21 @@ impl<'a> Widget for Folder<'a> { ); } - let hovered = matches!(self.folder.hovered, Some(ref h) if h.url() == f.url()); - let style = if self.is_preview && hovered { - THEME.preview.hovered.get() - } else if hovered { - THEME.selection.hovered.get() - } else { - self.file_style(f) + let hovered = + matches!(self.folder.hovered, Some(ref hover) if hover.url() == file.url()); + + let style = match (self.is_preview, hovered) { + (true, true) => THEME.preview.hovered.get(), + (_, true) => THEME.selection.hovered.get(), + _ => self.file_style(file), }; let mut spans = Vec::with_capacity(10); - spans.push(Span::raw(format!(" {} ", Self::icon(f)))); - spans.push(Span::raw(short_path(f.url(), &self.folder.cwd))); + spans.push(Span::raw(format!(" {} ", Self::icon(file)))); + spans.push(Span::raw(short_path(file.url(), &self.folder.cwd))); - if let Some(link_to) = f.link_to() { + if let Some(link_to) = file.link_to() { if MANAGER.show_symlink { spans.push(Span::raw(format!(" -> {}", link_to.display()))); } @@ -112,8 +118,8 @@ impl<'a> Widget for Folder<'a> { if let Some(idx) = active .finder() - .filter(|&f| hovered && self.is_find && f.has_matched()) - .and_then(|finder| finder.matched_idx(f.url())) + .filter(|&finder| hovered && self.is_find && finder.has_matched()) + .and_then(|finder| finder.matched_idx(file.url())) { let len = active.finder().unwrap().matched().len(); let style = Style::new().fg(Color::Rgb(255, 255, 50)).add_modifier(Modifier::ITALIC); diff --git a/app/src/manager/preview.rs b/app/src/manager/preview.rs index c75df174..1943c5fa 100644 --- a/app/src/manager/preview.rs +++ b/app/src/manager/preview.rs @@ -17,7 +17,7 @@ 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(hovered) = manager.hovered().map(|hovered| hovered.url()) else { return; };