diff --git a/README.md b/README.md index 586ba0f6..aa75975e 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Yazi ("duck" in Chinese) is a terminal file manager written in Rust, based on no Before getting started, ensure that the following dependencies are installed on your system: +- nerd-fonts (required, for icons) - jq (optional, for JSON preview) - ffmpegthumbnailer (optional, for video thumbnails) - fd (optional, for file searching) diff --git a/src/config/theme/filetype.rs b/src/config/theme/filetype.rs index 3a40e054..b31bc95c 100644 --- a/src/config/theme/filetype.rs +++ b/src/config/theme/filetype.rs @@ -1,17 +1,26 @@ +use std::path::Path; + use serde::{Deserialize, Deserializer}; -use crate::config::Pattern; +use super::Style; +use crate::config::{theme::Color, Pattern}; -#[derive(Debug, Deserialize)] pub struct Filetype { - pub name: Option, - pub mime: Option, - #[serde(default)] - pub bg: String, - #[serde(default)] - pub fg: String, - #[serde(default)] - pub bold: bool, + pub name: Option, + pub mime: Option, + pub style: Style, +} + +impl Filetype { + pub fn matches(&self, path: &Path, mime: Option, is_dir: bool) -> bool { + if self.name.as_ref().map_or(false, |e| e.match_path(path, Some(is_dir))) { + return true; + } + if let Some(mime) = mime { + return self.mime.as_ref().map_or(false, |m| m.matches(&mime)); + } + false + } } impl Filetype { @@ -21,9 +30,33 @@ impl Filetype { { #[derive(Deserialize)] struct FiletypeOuter { - rules: Vec, + rules: Vec, + } + #[derive(Deserialize)] + struct FiletypeOuterStyle { + name: Option, + mime: Option, + fg: Option, + bg: Option, + bold: Option, + underline: Option, } - Ok(FiletypeOuter::deserialize(deserializer)?.rules) + Ok( + FiletypeOuter::deserialize(deserializer)? + .rules + .into_iter() + .map(|r| Filetype { + name: r.name, + mime: r.mime, + style: Style { + fg: r.fg, + bg: r.bg, + bold: r.bold, + underline: r.underline, + }, + }) + .collect::>(), + ) } } diff --git a/src/config/theme/theme.rs b/src/config/theme/theme.rs index 1b256f10..a0a3b5a4 100644 --- a/src/config/theme/theme.rs +++ b/src/config/theme/theme.rs @@ -16,14 +16,21 @@ pub struct Tab { pub struct Status { pub primary: ColorGroup, pub secondary: ColorGroup, - pub emphasis: ColorGroup, + pub tertiary: ColorGroup, pub body: ColorGroup, + pub emphasis: ColorGroup, pub info: ColorGroup, pub success: ColorGroup, pub warning: ColorGroup, pub danger: ColorGroup, } +#[derive(Deserialize)] +pub struct Progress { + pub gauge: Style, + pub label: Style, +} + #[derive(Deserialize)] pub struct Selection { pub hovered: Style, @@ -36,21 +43,23 @@ pub struct Marker { } #[derive(Deserialize)] -pub struct Syntect { - pub theme: PathBuf, +pub struct Preview { + pub hovered: Style, + pub syntect_theme: PathBuf, } #[derive(Deserialize)] pub struct Theme { pub tab: Tab, pub status: Status, + pub progress: Progress, pub selection: Selection, pub marker: Marker, + pub preview: Preview, #[serde(deserialize_with = "Filetype::deserialize")] pub filetypes: Vec, #[serde(deserialize_with = "Icon::deserialize")] pub icons: Vec, - pub syntect: Syntect, } impl Theme { @@ -58,7 +67,7 @@ impl Theme { let path = BaseDirectories::new().unwrap().get_config_file("yazi/theme.toml"); let mut parsed: Self = toml::from_str(&fs::read_to_string(path).unwrap()).unwrap(); - parsed.syntect.theme = absolute_path(&parsed.syntect.theme); + parsed.preview.syntect_theme = absolute_path(&parsed.preview.syntect_theme); parsed } } diff --git a/src/core/files/file.rs b/src/core/files/file.rs index f610bee6..4a7ff8f8 100644 --- a/src/core/files/file.rs +++ b/src/core/files/file.rs @@ -41,4 +41,9 @@ impl File { self.path = path.to_path_buf(); self } + + #[inline] + pub fn name(&self) -> Option { + self.path.file_name().map(|s| s.to_string_lossy().to_string()) + } } diff --git a/src/core/manager/manager.rs b/src/core/manager/manager.rs index 1604f545..3e6b835c 100644 --- a/src/core/manager/manager.rs +++ b/src/core/manager/manager.rs @@ -1,4 +1,4 @@ -use std::{collections::{BTreeSet, HashMap, HashSet}, mem, path::PathBuf}; +use std::{collections::{BTreeSet, HashMap, HashSet}, mem, path::{Path, PathBuf}}; use tokio::fs; @@ -169,7 +169,10 @@ impl Manager { self.current().selected().or_else(|| self.hovered().map(|h| vec![h.path()])).unwrap_or_default() } - pub async fn mimetype(&mut self, files: &Vec) -> Vec> { + #[inline] + pub fn mimetype(&self, file: &Path) -> Option { self.mimetype.get(file).cloned() } + + pub async fn mimetypes(&mut self, files: &Vec) -> Vec> { let todo = files.iter().filter(|&p| !self.mimetype.contains_key(p)).cloned().collect::>(); if let Ok(mime) = Precache::mimetype(&todo).await { diff --git a/src/core/manager/preview.rs b/src/core/manager/preview.rs index c3e56ec5..5c212bd4 100644 --- a/src/core/manager/preview.rs +++ b/src/core/manager/preview.rs @@ -130,7 +130,7 @@ impl Preview { let theme = SYNTECT_THEME.get_or_init(|| { let from_file = || -> Result { - let file = File::open(&THEME.syntect.theme)?; + let file = File::open(&THEME.preview.syntect_theme)?; Ok(ThemeSet::load_from_reader(&mut BufReader::new(file))?) }; from_file().unwrap_or_else(|_| ThemeSet::load_defaults().themes["base16-ocean.dark"].clone()) diff --git a/src/core/manager/tabs.rs b/src/core/manager/tabs.rs index aefc4bd6..dc88b10c 100644 --- a/src/core/manager/tabs.rs +++ b/src/core/manager/tabs.rs @@ -52,12 +52,12 @@ impl Tabs { pub fn close(&mut self, idx: usize) -> bool { let len = self.items.len(); - if len <= 1 || idx as usize >= len { + if len < 2 || idx as usize >= len { return false; } self.items.remove(idx); - if idx == self.idx { + if idx <= self.idx { self.set_idx(self.absolute(1)); } diff --git a/src/misc/fns.rs b/src/misc/fns.rs index fdae4e7c..01fd722b 100644 --- a/src/misc/fns.rs +++ b/src/misc/fns.rs @@ -1,23 +1,6 @@ -use std::{collections::VecDeque, env, path::{Path, PathBuf}}; +use std::{env, path::{Path, PathBuf}}; -use anyhow::Result; -use libc::{ioctl, winsize, STDOUT_FILENO, TIOCGWINSZ}; -use tokio::{fs::{self, File}, io::{self, AsyncBufReadExt, BufReader}, select, sync::{mpsc, oneshot}, time}; - -#[inline] -pub fn tty_size() -> winsize { - unsafe { - let s: winsize = std::mem::zeroed(); - ioctl(STDOUT_FILENO, TIOCGWINSZ, &s); - s - } -} - -#[inline] -pub fn tty_ratio() -> (f64, f64) { - let s = tty_size(); - (f64::from(s.ws_xpixel) / f64::from(s.ws_col), f64::from(s.ws_ypixel) / f64::from(s.ws_row)) -} +use tokio::fs; pub fn absolute_path(p: &Path) -> PathBuf { if p.starts_with("~") { @@ -47,6 +30,17 @@ pub fn readable_home(p: &Path) -> String { p.display().to_string() } +pub fn readable_size(size: u64) -> String { + let units = ["B", "KB", "MB", "GB", "TB", "PB", "EB"]; + let mut size = size as f64; + let mut i = 0; + while size > 1024.0 && i < units.len() - 1 { + size /= 1024.0; + i += 1; + } + format!("{:.1} {}", size, units[i]) +} + pub async fn unique_path(mut p: PathBuf) -> PathBuf { let name = if let Some(name) = p.file_name() { name.to_os_string() @@ -75,113 +69,6 @@ pub fn optinal_bool(s: &str) -> Option { } } -pub async fn first_n_lines(path: &Path, n: usize) -> Result> { - let mut lines = Vec::new(); - let mut it = BufReader::new(File::open(path).await?).lines(); - for _ in 0..n { - if let Some(line) = it.next_line().await? { - lines.push(line); - } else { - break; - } - } - Ok(lines) -} - -pub async fn calculate_size(path: &Path) -> u64 { - let mut total = 0; - let mut stack = VecDeque::from([path.to_path_buf()]); - while let Some(path) = stack.pop_front() { - let meta = if let Ok(meta) = fs::symlink_metadata(&path).await { - meta - } else { - continue; - }; - - if !meta.is_dir() { - total += meta.len(); - continue; - } - - let mut it = if let Ok(it) = fs::read_dir(path).await { - it - } else { - continue; - }; - - while let Ok(Some(entry)) = it.next_entry().await { - let meta = if let Ok(m) = entry.metadata().await { - m - } else { - continue; - }; - - if meta.is_dir() { - stack.push_back(entry.path()); - } else { - total += meta.len(); - } - } - } - total -} - -pub fn copy_with_progress(from: &Path, to: &Path) -> mpsc::Receiver> { - let (tx, rx) = mpsc::channel(1); - let (tick_tx, mut tick_rx) = oneshot::channel(); - - tokio::spawn({ - let (from, to) = (from.to_path_buf(), to.to_path_buf()); - - async move { - let _ = match fs::copy(from, to).await { - Ok(len) => tick_tx.send(Ok(len)), - Err(e) => tick_tx.send(Err(e)), - }; - } - }); - - tokio::spawn({ - let tx = tx.clone(); - let to = to.to_path_buf(); - - async move { - let mut last = 0; - let mut exit = None; - loop { - select! { - res = &mut tick_rx => exit = Some(res.unwrap()), - _ = tx.closed() => break, - _ = time::sleep(time::Duration::from_secs(1)) => (), - } - - match exit { - Some(Ok(len)) => { - if len > last { - tx.send(Ok(len - last)).await.ok(); - } - tx.send(Ok(0)).await.ok(); - break; - } - Some(Err(e)) => { - tx.send(Err(e)).await.ok(); - break; - } - None => {} - } - - let len = fs::symlink_metadata(&to).await.map(|m| m.len()).unwrap_or(0); - if len > last { - tx.send(Ok(len - last)).await.ok(); - last = len; - } - } - } - }); - - rx -} - pub fn valid_mimetype(str: &str) -> bool { let parts = str.split('/').collect::>(); if parts.len() != 2 { diff --git a/src/misc/fs.rs b/src/misc/fs.rs new file mode 100644 index 00000000..de6f0a8d --- /dev/null +++ b/src/misc/fs.rs @@ -0,0 +1,159 @@ +use std::{collections::VecDeque, path::Path}; + +use anyhow::Result; +use tokio::{fs::{self, File}, io::{self, AsyncBufReadExt, BufReader}, select, sync::{mpsc, oneshot}, time}; + +pub async fn first_n_lines(path: &Path, n: usize) -> Result> { + let mut lines = Vec::new(); + let mut it = BufReader::new(File::open(path).await?).lines(); + for _ in 0..n { + if let Some(line) = it.next_line().await? { + lines.push(line); + } else { + break; + } + } + Ok(lines) +} + +pub async fn calculate_size(path: &Path) -> u64 { + let mut total = 0; + let mut stack = VecDeque::from([path.to_path_buf()]); + while let Some(path) = stack.pop_front() { + let meta = if let Ok(meta) = fs::symlink_metadata(&path).await { + meta + } else { + continue; + }; + + if !meta.is_dir() { + total += meta.len(); + continue; + } + + let mut it = if let Ok(it) = fs::read_dir(path).await { + it + } else { + continue; + }; + + while let Ok(Some(entry)) = it.next_entry().await { + let meta = if let Ok(m) = entry.metadata().await { + m + } else { + continue; + }; + + if meta.is_dir() { + stack.push_back(entry.path()); + } else { + total += meta.len(); + } + } + } + total +} + +pub fn copy_with_progress(from: &Path, to: &Path) -> mpsc::Receiver> { + let (tx, rx) = mpsc::channel(1); + let (tick_tx, mut tick_rx) = oneshot::channel(); + + tokio::spawn({ + let (from, to) = (from.to_path_buf(), to.to_path_buf()); + + async move { + let _ = match fs::copy(from, to).await { + Ok(len) => tick_tx.send(Ok(len)), + Err(e) => tick_tx.send(Err(e)), + }; + } + }); + + tokio::spawn({ + let tx = tx.clone(); + let to = to.to_path_buf(); + + async move { + let mut last = 0; + let mut exit = None; + loop { + select! { + res = &mut tick_rx => exit = Some(res.unwrap()), + _ = tx.closed() => break, + _ = time::sleep(time::Duration::from_secs(1)) => (), + } + + match exit { + Some(Ok(len)) => { + if len > last { + tx.send(Ok(len - last)).await.ok(); + } + tx.send(Ok(0)).await.ok(); + break; + } + Some(Err(e)) => { + tx.send(Err(e)).await.ok(); + break; + } + None => {} + } + + let len = fs::symlink_metadata(&to).await.map(|m| m.len()).unwrap_or(0); + if len > last { + tx.send(Ok(len - last)).await.ok(); + last = len; + } + } + } + }); + + rx +} + +// Convert a file mode to a string representation +pub fn file_mode(mode: u32) -> String { + use libc::{S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFSOCK, S_IRGRP, S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR}; + + let m = mode as u16; + let mut s = String::with_capacity(10); + + // File type + s.push(match m & S_IFMT { + S_IFBLK => 'b', + S_IFCHR => 'c', + S_IFDIR => 'd', + S_IFIFO => 'p', + S_IFLNK => 'l', + S_IFSOCK => 's', + _ => '-', + }); + + // Owner + s.push(if m & S_IRUSR != 0 { 'r' } else { '-' }); + s.push(if m & S_IWUSR != 0 { 'w' } else { '-' }); + s.push(if m & S_IXUSR != 0 { + if m & S_ISUID != 0 { 's' } else { 'x' } + } else { + if m & S_ISUID != 0 { 'S' } else { '-' } + }); + + // Group + s.push(if m & S_IRGRP != 0 { 'r' } else { '-' }); + s.push(if m & S_IWGRP != 0 { 'w' } else { '-' }); + s.push(if m & S_IXGRP != 0 { + if m & S_ISGID != 0 { 's' } else { 'x' } + } else { + if m & S_ISGID != 0 { 'S' } else { '-' } + }); + + // Other + s.push(if m & S_IROTH != 0 { 'r' } else { '-' }); + s.push(if m & S_IWOTH != 0 { 'w' } else { '-' }); + s.push(if m & S_IXOTH != 0 { + if m & S_ISVTX != 0 { 't' } else { 'x' } + } else { + if m & S_ISVTX != 0 { 'T' } else { '-' } + }); + + s +} diff --git a/src/misc/mod.rs b/src/misc/mod.rs index b8b66626..8f37bc15 100644 --- a/src/misc/mod.rs +++ b/src/misc/mod.rs @@ -2,8 +2,12 @@ mod buffer; mod chars; mod defer; mod fns; +mod fs; +mod tty; pub use buffer::*; pub use chars::*; pub use defer::*; pub use fns::*; +pub use fs::*; +pub use tty::*; diff --git a/src/misc/tty.rs b/src/misc/tty.rs new file mode 100644 index 00000000..035f09ec --- /dev/null +++ b/src/misc/tty.rs @@ -0,0 +1,16 @@ +use libc::{ioctl, winsize, STDOUT_FILENO, TIOCGWINSZ}; + +#[inline] +pub fn tty_size() -> winsize { + unsafe { + let s: winsize = std::mem::zeroed(); + ioctl(STDOUT_FILENO, TIOCGWINSZ, &s); + s + } +} + +#[inline] +pub fn tty_ratio() -> (f64, f64) { + let s = tty_size(); + (f64::from(s.ws_xpixel) / f64::from(s.ws_col), f64::from(s.ws_ypixel) / f64::from(s.ws_row)) +} diff --git a/src/ui/app.rs b/src/ui/app.rs index 01387597..b1fdd79d 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -120,7 +120,7 @@ impl App { } Event::Open(files) => { - let mime = manager.mimetype(&files).await; + let mime = manager.mimetypes(&files).await; let targets = files.into_iter().zip(mime).map_while(|(f, m)| m.map(|m| (f, m))).collect(); self.cx.tasks.file_open(targets); } diff --git a/src/ui/header/tabs.rs b/src/ui/header/tabs.rs index ece1bb1e..89c29bfe 100644 --- a/src/ui/header/tabs.rs +++ b/src/ui/header/tabs.rs @@ -1,6 +1,6 @@ -use ratatui::{buffer::Buffer, layout::{Alignment, Rect}, style::{Color, Style}, text::{Line, Span}, widgets::{Paragraph, Widget}}; +use ratatui::{buffer::Buffer, layout::{Alignment, Rect}, text::{Line, Span}, widgets::{Paragraph, Widget}}; -use crate::ui::Ctx; +use crate::{config::THEME, ui::Ctx}; pub struct Tabs<'a> { cx: &'a Ctx, @@ -14,20 +14,20 @@ impl<'a> Widget for Tabs<'a> { fn render(self, area: Rect, buf: &mut Buffer) { let tabs = self.cx.manager.tabs(); - let spans = Line::from( + let line = Line::from( tabs .iter() .enumerate() .map(|(i, _)| { if i == tabs.idx() { - Span::styled(format!(" {} ", i + 1), Style::default().fg(Color::Black).bg(Color::Blue)) + Span::styled(format!(" {} ", i + 1), THEME.tab.active.get()) } else { - Span::styled(format!(" {} ", i + 1), Style::default().fg(Color::Gray).bg(Color::Black)) + Span::styled(format!(" {} ", i + 1), THEME.tab.inactive.get()) } }) .collect::>(), ); - Paragraph::new(spans).alignment(Alignment::Right).render(area, buf); + Paragraph::new(line).alignment(Alignment::Right).render(area, buf); } } diff --git a/src/ui/manager/folder.rs b/src/ui/manager/folder.rs index 2b1dc0b8..7f5efaaa 100644 --- a/src/ui/manager/folder.rs +++ b/src/ui/manager/folder.rs @@ -1,16 +1,17 @@ -use ratatui::{buffer::Buffer, layout::Rect, style::{Color, Modifier, Style}, widgets::{List, ListItem, Widget}}; +use ratatui::{buffer::Buffer, layout::Rect, style::Style, widgets::{List, ListItem, Widget}}; -use crate::{config::THEME, core, misc::readable_path}; +use crate::{config::THEME, core::{self, files::File}, misc::readable_path, ui::Ctx}; pub struct Folder<'a> { + cx: &'a Ctx, folder: &'a core::manager::Folder, is_preview: bool, is_selection: bool, } impl<'a> Folder<'a> { - pub fn new(folder: &'a core::manager::Folder) -> Self { - Self { folder, is_preview: false, is_selection: true } + pub fn new(cx: &'a Ctx, folder: &'a core::manager::Folder) -> Self { + Self { cx, folder, is_preview: false, is_selection: true } } #[inline] @@ -24,6 +25,16 @@ impl<'a> Folder<'a> { self.is_selection = state; self } + + #[inline] + fn file_style(&self, file: &File) -> Style { + THEME + .filetypes + .iter() + .find(|x| x.matches(&file.path, self.cx.manager.mimetype(&file.path), file.meta.is_dir())) + .map(|x| x.style.get()) + .unwrap_or_else(|| Style::new()) + } } impl<'a> Widget for Folder<'a> { @@ -44,29 +55,24 @@ impl<'a> Widget for Folder<'a> { if v.is_selected { buf.set_style( Rect { x: area.x.saturating_sub(1), y: i as u16 + 1, width: 1, height: 1 }, - Style::default().fg(Color::Red).bg(Color::Red), + if self.is_selection { + THEME.marker.selecting.get() + } else { + THEME.marker.selected.get() + }, ); } - let name = if self.is_selection && v.is_selected { - format!(" {} {}", icon, readable_path(k, &self.folder.cwd)) + let hovered = matches!(self.folder.hovered, Some(ref h) if h.path == *k); + let style = if self.is_preview && hovered { + THEME.preview.hovered.get() + } else if hovered { + THEME.selection.hovered.get() } else { - format!(" {} {}", icon, readable_path(k, &self.folder.cwd)) + self.file_style(v) }; - let hovered = matches!(self.folder.hovered, Some(ref h) if h.path == *k); - let mut style = Style::default(); - if self.is_preview { - if hovered { - style = style.add_modifier(Modifier::UNDERLINED) - } - } else { - if hovered { - style = style.fg(Color::Black).bg(Color::Yellow); - } - } - - ListItem::new(name).style(style) + ListItem::new(format!(" {} {}", icon, readable_path(k, &self.folder.cwd))).style(style) }) .collect::>(); diff --git a/src/ui/manager/layout.rs b/src/ui/manager/layout.rs index c1e86c75..7d15dc53 100644 --- a/src/ui/manager/layout.rs +++ b/src/ui/manager/layout.rs @@ -30,12 +30,12 @@ impl<'a> Widget for Layout<'a> { // Parent let block = Block::default().borders(Borders::RIGHT).padding(Padding::new(1, 0, 0, 0)); if let Some(ref parent) = manager.parent() { - Folder::new(parent).render(block.inner(chunks[0]), buf); + Folder::new(self.cx, parent).render(block.inner(chunks[0]), buf); } block.render(chunks[0], buf); // Current - Folder::new(&manager.current()) + Folder::new(self.cx, manager.current()) .with_selection(matches!(manager.active().mode(), Mode::Select(_))) .render(chunks[1], buf); diff --git a/src/ui/manager/preview.rs b/src/ui/manager/preview.rs index 9a729dd8..135918fc 100644 --- a/src/ui/manager/preview.rs +++ b/src/ui/manager/preview.rs @@ -42,7 +42,7 @@ impl<'a> Widget for Preview<'a> { PreviewData::None => {} PreviewData::Folder => { if let Some(folder) = manager.active().history(&hovered.path) { - Folder::new(folder).with_preview(true).render(area, buf); + Folder::new(self.cx, folder).with_preview(true).render(area, buf); } } PreviewData::Text(s) => { diff --git a/src/ui/status/layout.rs b/src/ui/status/layout.rs index 4212aa44..593a55b2 100644 --- a/src/ui/status/layout.rs +++ b/src/ui/status/layout.rs @@ -1,7 +1,7 @@ -use ratatui::{buffer::Buffer, layout::{self, Constraint, Direction, Rect}, style::Modifier, widgets::{Paragraph, Widget}}; +use ratatui::{buffer::Buffer, layout::{self, Constraint, Direction, Rect}, widgets::Widget}; -use super::Progress; -use crate::{config::THEME, ui::Ctx}; +use super::{Left, Right}; +use crate::ui::Ctx; pub struct Layout<'a> { cx: &'a Ctx, @@ -13,36 +13,12 @@ impl<'a> Layout<'a> { impl<'a> Widget for Layout<'a> { fn render(self, area: Rect, buf: &mut Buffer) { - let mode = self.cx.manager.active().mode(); - let chunks = layout::Layout::default() .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Length(1), - Constraint::Length(8), - Constraint::Length(8), - Constraint::Length(1), - Constraint::Min(0), - ] - .as_ref(), - ) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .split(area); - let primary = mode.color(&THEME.status.primary); - let secondary = mode.color(&THEME.status.secondary); - let body = mode.color(&THEME.status.body); - - Paragraph::new("").style(primary.fg()).render(chunks[0], buf); - - Paragraph::new(format!(" {} ", mode)) - .style(primary.bg().fg(**secondary).add_modifier(Modifier::BOLD)) - .render(chunks[1], buf); - - Paragraph::new(" master ").style(body.bg().fg(**primary)).render(chunks[2], buf); - - Paragraph::new("").style(body.fg()).render(chunks[3], buf); - - Progress::new(self.cx).render(chunks[4], buf); + Left::new(self.cx).render(chunks[0], buf); + Right::new(self.cx).render(chunks[1], buf); } } diff --git a/src/ui/status/left.rs b/src/ui/status/left.rs new file mode 100644 index 00000000..8f36574a --- /dev/null +++ b/src/ui/status/left.rs @@ -0,0 +1,45 @@ +use ratatui::{buffer::Buffer, layout::Rect, style::Modifier, text::{Line, Span}, widgets::{Paragraph, Widget}}; + +use crate::{config::THEME, misc::readable_size, ui::Ctx}; + +pub struct Left<'a> { + cx: &'a Ctx, +} + +impl<'a> Left<'a> { + pub fn new(cx: &'a Ctx) -> Self { Self { cx } } +} + +impl<'a> Widget for Left<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + let manager = self.cx.manager.current(); + let mode = self.cx.manager.active().mode(); + + // Colors + let primary = mode.color(&THEME.status.primary); + let secondary = mode.color(&THEME.status.secondary); + let body = mode.color(&THEME.status.body); + + let mut spans = vec![]; + + // Mode + spans.push(Span::styled("", primary.fg())); + spans.push(Span::styled( + format!(" {} ", mode), + primary.bg().fg(**secondary).add_modifier(Modifier::BOLD), + )); + + if let Some(h) = &manager.hovered { + // Length + if let Some(len) = h.length { + spans.push(Span::styled(format!(" {} ", readable_size(len)), body.bg().fg(**primary))); + spans.push(Span::styled("", body.fg())); + } + + // Filename + spans.push(Span::raw(format!(" {} ", h.name().unwrap()))); + } + + Paragraph::new(Line::from(spans)).render(area, buf); + } +} diff --git a/src/ui/status/mod.rs b/src/ui/status/mod.rs index 7b68b8e5..dbed3bca 100644 --- a/src/ui/status/mod.rs +++ b/src/ui/status/mod.rs @@ -1,5 +1,9 @@ mod layout; +mod left; mod progress; +mod right; pub use layout::*; +pub use left::*; pub use progress::*; +pub use right::*; diff --git a/src/ui/status/progress.rs b/src/ui/status/progress.rs index 8573c7ba..cfab6984 100644 --- a/src/ui/status/progress.rs +++ b/src/ui/status/progress.rs @@ -1,6 +1,6 @@ -use ratatui::{style::{Color, Style}, widgets::{Gauge, Widget}}; +use ratatui::{buffer::Buffer, layout::Rect, text::Span, widgets::{Gauge, Widget}}; -use crate::ui::Ctx; +use crate::{config::THEME, ui::Ctx}; pub struct Progress<'a> { cx: &'a Ctx, @@ -11,16 +11,19 @@ impl<'a> Progress<'a> { } impl<'a> Widget for Progress<'a> { - fn render(self, area: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) { + fn render(self, area: Rect, buf: &mut Buffer) { let progress = &self.cx.tasks.progress; if progress.0 >= 100 { return; } Gauge::default() - .gauge_style(Style::default().fg(Color::Yellow)) + .gauge_style(THEME.progress.gauge.get()) .percent(progress.0 as u16) - .label(format!("{}%, {} left", progress.0, progress.1)) + .label(Span::styled( + format!("{:>3}%, {} left", progress.0, progress.1), + THEME.progress.label.get(), + )) .use_unicode(true) .render(area, buf); } diff --git a/src/ui/status/right.rs b/src/ui/status/right.rs new file mode 100644 index 00000000..6aa2411e --- /dev/null +++ b/src/ui/status/right.rs @@ -0,0 +1,88 @@ +use std::os::unix::prelude::PermissionsExt; + +use ratatui::{buffer::Buffer, layout::{Alignment, Rect}, text::{Line, Span}, widgets::{Paragraph, Widget}}; + +use super::Progress; +use crate::{config::THEME, misc::file_mode, ui::Ctx}; + +pub struct Right<'a> { + cx: &'a Ctx, +} + +impl<'a> Right<'a> { + pub fn new(cx: &'a Ctx) -> Self { Self { cx } } + + fn permissions(&self, s: &str) -> Vec { + // Colors + let mode = self.cx.manager.active().mode(); + let tertiary = mode.color(&THEME.status.tertiary); + let info = mode.color(&THEME.status.info); + let success = mode.color(&THEME.status.success); + let warning = mode.color(&THEME.status.warning); + let danger = mode.color(&THEME.status.danger); + + s.chars() + .map(|c| match c { + '-' => Span::styled("-", tertiary.fg()), + 'r' => Span::styled("r", warning.fg()), + 'w' => Span::styled("w", danger.fg()), + 'x' | 's' | 'S' | 't' | 'T' => Span::styled(c.to_string(), info.fg()), + _ => Span::styled(c.to_string(), success.fg()), + }) + .collect() + } + + fn position<'b>(&self) -> Vec { + // Colors + let mode = self.cx.manager.active().mode(); + let primary = mode.color(&THEME.status.primary); + let secondary = mode.color(&THEME.status.secondary); + let body = mode.color(&THEME.status.body); + + let cursor = self.cx.manager.current().cursor(); + let length = self.cx.manager.current().files.len(); + let percent = if cursor == 0 || length == 0 { 0 } else { (cursor + 1) * 100 / length }; + + vec![ + Span::styled(" ", body.fg()), + Span::styled( + if percent == 0 { " Top ".to_string() } else { format!(" {:>3}% ", percent) }, + body.bg().fg(**primary), + ), + Span::styled( + format!(" {:>2}/{:<2} ", (cursor + 1).min(length), length), + primary.bg().fg(**secondary), + ), + Span::styled("", primary.fg()), + ] + } +} + +impl Widget for Right<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + let manager = self.cx.manager.current(); + let mut spans = vec![]; + + // Permissions + if let Some(h) = &manager.hovered { + spans.extend(self.permissions(&file_mode(h.meta.permissions().mode()))) + } + + // Position + spans.extend(self.position()); + + // Progress + let line = Line::from(spans); + Progress::new(self.cx).render( + Rect { + x: area.x + area.width.saturating_sub(21 + line.width() as u16), + y: area.y, + width: 20.min(area.width), + height: 1, + }, + buf, + ); + + Paragraph::new(line).alignment(Alignment::Right).render(area, buf); + } +}