mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
feat: theme support
This commit is contained in:
parent
ca08a8447a
commit
4d002424a9
21 changed files with 453 additions and 214 deletions
|
|
@ -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:
|
Before getting started, ensure that the following dependencies are installed on your system:
|
||||||
|
|
||||||
|
- nerd-fonts (required, for icons)
|
||||||
- jq (optional, for JSON preview)
|
- jq (optional, for JSON preview)
|
||||||
- ffmpegthumbnailer (optional, for video thumbnails)
|
- ffmpegthumbnailer (optional, for video thumbnails)
|
||||||
- fd (optional, for file searching)
|
- fd (optional, for file searching)
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,26 @@
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
use serde::{Deserialize, Deserializer};
|
use serde::{Deserialize, Deserializer};
|
||||||
|
|
||||||
use crate::config::Pattern;
|
use super::Style;
|
||||||
|
use crate::config::{theme::Color, Pattern};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct Filetype {
|
pub struct Filetype {
|
||||||
pub name: Option<Pattern>,
|
pub name: Option<Pattern>,
|
||||||
pub mime: Option<Pattern>,
|
pub mime: Option<Pattern>,
|
||||||
#[serde(default)]
|
pub style: Style,
|
||||||
pub bg: String,
|
}
|
||||||
#[serde(default)]
|
|
||||||
pub fg: String,
|
impl Filetype {
|
||||||
#[serde(default)]
|
pub fn matches(&self, path: &Path, mime: Option<String>, is_dir: bool) -> bool {
|
||||||
pub bold: 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 {
|
impl Filetype {
|
||||||
|
|
@ -21,9 +30,33 @@ impl Filetype {
|
||||||
{
|
{
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct FiletypeOuter {
|
struct FiletypeOuter {
|
||||||
rules: Vec<Filetype>,
|
rules: Vec<FiletypeOuterStyle>,
|
||||||
|
}
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct FiletypeOuterStyle {
|
||||||
|
name: Option<Pattern>,
|
||||||
|
mime: Option<Pattern>,
|
||||||
|
fg: Option<Color>,
|
||||||
|
bg: Option<Color>,
|
||||||
|
bold: Option<bool>,
|
||||||
|
underline: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
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::<Vec<_>>(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,21 @@ pub struct Tab {
|
||||||
pub struct Status {
|
pub struct Status {
|
||||||
pub primary: ColorGroup,
|
pub primary: ColorGroup,
|
||||||
pub secondary: ColorGroup,
|
pub secondary: ColorGroup,
|
||||||
pub emphasis: ColorGroup,
|
pub tertiary: ColorGroup,
|
||||||
pub body: ColorGroup,
|
pub body: ColorGroup,
|
||||||
|
pub emphasis: ColorGroup,
|
||||||
pub info: ColorGroup,
|
pub info: ColorGroup,
|
||||||
pub success: ColorGroup,
|
pub success: ColorGroup,
|
||||||
pub warning: ColorGroup,
|
pub warning: ColorGroup,
|
||||||
pub danger: ColorGroup,
|
pub danger: ColorGroup,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Progress {
|
||||||
|
pub gauge: Style,
|
||||||
|
pub label: Style,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct Selection {
|
pub struct Selection {
|
||||||
pub hovered: Style,
|
pub hovered: Style,
|
||||||
|
|
@ -36,21 +43,23 @@ pub struct Marker {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct Syntect {
|
pub struct Preview {
|
||||||
pub theme: PathBuf,
|
pub hovered: Style,
|
||||||
|
pub syntect_theme: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct Theme {
|
pub struct Theme {
|
||||||
pub tab: Tab,
|
pub tab: Tab,
|
||||||
pub status: Status,
|
pub status: Status,
|
||||||
|
pub progress: Progress,
|
||||||
pub selection: Selection,
|
pub selection: Selection,
|
||||||
pub marker: Marker,
|
pub marker: Marker,
|
||||||
|
pub preview: Preview,
|
||||||
#[serde(deserialize_with = "Filetype::deserialize")]
|
#[serde(deserialize_with = "Filetype::deserialize")]
|
||||||
pub filetypes: Vec<Filetype>,
|
pub filetypes: Vec<Filetype>,
|
||||||
#[serde(deserialize_with = "Icon::deserialize")]
|
#[serde(deserialize_with = "Icon::deserialize")]
|
||||||
pub icons: Vec<Icon>,
|
pub icons: Vec<Icon>,
|
||||||
pub syntect: Syntect,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Theme {
|
impl Theme {
|
||||||
|
|
@ -58,7 +67,7 @@ impl Theme {
|
||||||
let path = BaseDirectories::new().unwrap().get_config_file("yazi/theme.toml");
|
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();
|
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
|
parsed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,4 +41,9 @@ impl File {
|
||||||
self.path = path.to_path_buf();
|
self.path = path.to_path_buf();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn name(&self) -> Option<String> {
|
||||||
|
self.path.file_name().map(|s| s.to_string_lossy().to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
use tokio::fs;
|
||||||
|
|
||||||
|
|
@ -169,7 +169,10 @@ impl Manager {
|
||||||
self.current().selected().or_else(|| self.hovered().map(|h| vec![h.path()])).unwrap_or_default()
|
self.current().selected().or_else(|| self.hovered().map(|h| vec![h.path()])).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn mimetype(&mut self, files: &Vec<PathBuf>) -> Vec<Option<String>> {
|
#[inline]
|
||||||
|
pub fn mimetype(&self, file: &Path) -> Option<String> { self.mimetype.get(file).cloned() }
|
||||||
|
|
||||||
|
pub async fn mimetypes(&mut self, files: &Vec<PathBuf>) -> Vec<Option<String>> {
|
||||||
let todo =
|
let todo =
|
||||||
files.iter().filter(|&p| !self.mimetype.contains_key(p)).cloned().collect::<Vec<_>>();
|
files.iter().filter(|&p| !self.mimetype.contains_key(p)).cloned().collect::<Vec<_>>();
|
||||||
if let Ok(mime) = Precache::mimetype(&todo).await {
|
if let Ok(mime) = Precache::mimetype(&todo).await {
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ impl Preview {
|
||||||
|
|
||||||
let theme = SYNTECT_THEME.get_or_init(|| {
|
let theme = SYNTECT_THEME.get_or_init(|| {
|
||||||
let from_file = || -> Result<Theme> {
|
let from_file = || -> Result<Theme> {
|
||||||
let file = File::open(&THEME.syntect.theme)?;
|
let file = File::open(&THEME.preview.syntect_theme)?;
|
||||||
Ok(ThemeSet::load_from_reader(&mut BufReader::new(file))?)
|
Ok(ThemeSet::load_from_reader(&mut BufReader::new(file))?)
|
||||||
};
|
};
|
||||||
from_file().unwrap_or_else(|_| ThemeSet::load_defaults().themes["base16-ocean.dark"].clone())
|
from_file().unwrap_or_else(|_| ThemeSet::load_defaults().themes["base16-ocean.dark"].clone())
|
||||||
|
|
|
||||||
|
|
@ -52,12 +52,12 @@ impl Tabs {
|
||||||
|
|
||||||
pub fn close(&mut self, idx: usize) -> bool {
|
pub fn close(&mut self, idx: usize) -> bool {
|
||||||
let len = self.items.len();
|
let len = self.items.len();
|
||||||
if len <= 1 || idx as usize >= len {
|
if len < 2 || idx as usize >= len {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.items.remove(idx);
|
self.items.remove(idx);
|
||||||
if idx == self.idx {
|
if idx <= self.idx {
|
||||||
self.set_idx(self.absolute(1));
|
self.set_idx(self.absolute(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
139
src/misc/fns.rs
139
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 tokio::fs;
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn absolute_path(p: &Path) -> PathBuf {
|
pub fn absolute_path(p: &Path) -> PathBuf {
|
||||||
if p.starts_with("~") {
|
if p.starts_with("~") {
|
||||||
|
|
@ -47,6 +30,17 @@ pub fn readable_home(p: &Path) -> String {
|
||||||
p.display().to_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 {
|
pub async fn unique_path(mut p: PathBuf) -> PathBuf {
|
||||||
let name = if let Some(name) = p.file_name() {
|
let name = if let Some(name) = p.file_name() {
|
||||||
name.to_os_string()
|
name.to_os_string()
|
||||||
|
|
@ -75,113 +69,6 @@ pub fn optinal_bool(s: &str) -> Option<bool> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn first_n_lines(path: &Path, n: usize) -> Result<Vec<String>> {
|
|
||||||
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<Result<u64, io::Error>> {
|
|
||||||
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 {
|
pub fn valid_mimetype(str: &str) -> bool {
|
||||||
let parts = str.split('/').collect::<Vec<_>>();
|
let parts = str.split('/').collect::<Vec<_>>();
|
||||||
if parts.len() != 2 {
|
if parts.len() != 2 {
|
||||||
|
|
|
||||||
159
src/misc/fs.rs
Normal file
159
src/misc/fs.rs
Normal file
|
|
@ -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<Vec<String>> {
|
||||||
|
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<Result<u64, io::Error>> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,12 @@ mod buffer;
|
||||||
mod chars;
|
mod chars;
|
||||||
mod defer;
|
mod defer;
|
||||||
mod fns;
|
mod fns;
|
||||||
|
mod fs;
|
||||||
|
mod tty;
|
||||||
|
|
||||||
pub use buffer::*;
|
pub use buffer::*;
|
||||||
pub use chars::*;
|
pub use chars::*;
|
||||||
pub use defer::*;
|
pub use defer::*;
|
||||||
pub use fns::*;
|
pub use fns::*;
|
||||||
|
pub use fs::*;
|
||||||
|
pub use tty::*;
|
||||||
|
|
|
||||||
16
src/misc/tty.rs
Normal file
16
src/misc/tty.rs
Normal file
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
@ -120,7 +120,7 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
Event::Open(files) => {
|
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();
|
let targets = files.into_iter().zip(mime).map_while(|(f, m)| m.map(|m| (f, m))).collect();
|
||||||
self.cx.tasks.file_open(targets);
|
self.cx.tasks.file_open(targets);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
pub struct Tabs<'a> {
|
||||||
cx: &'a Ctx,
|
cx: &'a Ctx,
|
||||||
|
|
@ -14,20 +14,20 @@ impl<'a> Widget for Tabs<'a> {
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
let tabs = self.cx.manager.tabs();
|
let tabs = self.cx.manager.tabs();
|
||||||
|
|
||||||
let spans = Line::from(
|
let line = Line::from(
|
||||||
tabs
|
tabs
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, _)| {
|
.map(|(i, _)| {
|
||||||
if i == tabs.idx() {
|
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 {
|
} else {
|
||||||
Span::styled(format!(" {} ", i + 1), Style::default().fg(Color::Gray).bg(Color::Black))
|
Span::styled(format!(" {} ", i + 1), THEME.tab.inactive.get())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Paragraph::new(spans).alignment(Alignment::Right).render(area, buf);
|
Paragraph::new(line).alignment(Alignment::Right).render(area, buf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
pub struct Folder<'a> {
|
||||||
|
cx: &'a Ctx,
|
||||||
folder: &'a core::manager::Folder,
|
folder: &'a core::manager::Folder,
|
||||||
is_preview: bool,
|
is_preview: bool,
|
||||||
is_selection: bool,
|
is_selection: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Folder<'a> {
|
impl<'a> Folder<'a> {
|
||||||
pub fn new(folder: &'a core::manager::Folder) -> Self {
|
pub fn new(cx: &'a Ctx, folder: &'a core::manager::Folder) -> Self {
|
||||||
Self { folder, is_preview: false, is_selection: true }
|
Self { cx, folder, is_preview: false, is_selection: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
@ -24,6 +25,16 @@ impl<'a> Folder<'a> {
|
||||||
self.is_selection = state;
|
self.is_selection = state;
|
||||||
self
|
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> {
|
impl<'a> Widget for Folder<'a> {
|
||||||
|
|
@ -44,29 +55,24 @@ impl<'a> Widget for Folder<'a> {
|
||||||
if v.is_selected {
|
if v.is_selected {
|
||||||
buf.set_style(
|
buf.set_style(
|
||||||
Rect { x: area.x.saturating_sub(1), y: i as u16 + 1, width: 1, height: 1 },
|
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 {
|
let hovered = matches!(self.folder.hovered, Some(ref h) if h.path == *k);
|
||||||
format!(" {} {}", icon, readable_path(k, &self.folder.cwd))
|
let style = if self.is_preview && hovered {
|
||||||
|
THEME.preview.hovered.get()
|
||||||
|
} else if hovered {
|
||||||
|
THEME.selection.hovered.get()
|
||||||
} else {
|
} 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);
|
ListItem::new(format!(" {} {}", icon, readable_path(k, &self.folder.cwd))).style(style)
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,12 @@ impl<'a> Widget for Layout<'a> {
|
||||||
// Parent
|
// Parent
|
||||||
let block = Block::default().borders(Borders::RIGHT).padding(Padding::new(1, 0, 0, 0));
|
let block = Block::default().borders(Borders::RIGHT).padding(Padding::new(1, 0, 0, 0));
|
||||||
if let Some(ref parent) = manager.parent() {
|
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);
|
block.render(chunks[0], buf);
|
||||||
|
|
||||||
// Current
|
// Current
|
||||||
Folder::new(&manager.current())
|
Folder::new(self.cx, manager.current())
|
||||||
.with_selection(matches!(manager.active().mode(), Mode::Select(_)))
|
.with_selection(matches!(manager.active().mode(), Mode::Select(_)))
|
||||||
.render(chunks[1], buf);
|
.render(chunks[1], buf);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ impl<'a> Widget for Preview<'a> {
|
||||||
PreviewData::None => {}
|
PreviewData::None => {}
|
||||||
PreviewData::Folder => {
|
PreviewData::Folder => {
|
||||||
if let Some(folder) = manager.active().history(&hovered.path) {
|
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) => {
|
PreviewData::Text(s) => {
|
||||||
|
|
|
||||||
|
|
@ -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 super::{Left, Right};
|
||||||
use crate::{config::THEME, ui::Ctx};
|
use crate::ui::Ctx;
|
||||||
|
|
||||||
pub struct Layout<'a> {
|
pub struct Layout<'a> {
|
||||||
cx: &'a Ctx,
|
cx: &'a Ctx,
|
||||||
|
|
@ -13,36 +13,12 @@ impl<'a> Layout<'a> {
|
||||||
|
|
||||||
impl<'a> Widget for Layout<'a> {
|
impl<'a> Widget for Layout<'a> {
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
let mode = self.cx.manager.active().mode();
|
|
||||||
|
|
||||||
let chunks = layout::Layout::default()
|
let chunks = layout::Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints(
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||||
[
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Length(8),
|
|
||||||
Constraint::Length(8),
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Min(0),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
let primary = mode.color(&THEME.status.primary);
|
Left::new(self.cx).render(chunks[0], buf);
|
||||||
let secondary = mode.color(&THEME.status.secondary);
|
Right::new(self.cx).render(chunks[1], buf);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
45
src/ui/status/left.rs
Normal file
45
src/ui/status/left.rs
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
mod layout;
|
mod layout;
|
||||||
|
mod left;
|
||||||
mod progress;
|
mod progress;
|
||||||
|
mod right;
|
||||||
|
|
||||||
pub use layout::*;
|
pub use layout::*;
|
||||||
|
pub use left::*;
|
||||||
pub use progress::*;
|
pub use progress::*;
|
||||||
|
pub use right::*;
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
pub struct Progress<'a> {
|
||||||
cx: &'a Ctx,
|
cx: &'a Ctx,
|
||||||
|
|
@ -11,16 +11,19 @@ impl<'a> Progress<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Widget for 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;
|
let progress = &self.cx.tasks.progress;
|
||||||
if progress.0 >= 100 {
|
if progress.0 >= 100 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Gauge::default()
|
Gauge::default()
|
||||||
.gauge_style(Style::default().fg(Color::Yellow))
|
.gauge_style(THEME.progress.gauge.get())
|
||||||
.percent(progress.0 as u16)
|
.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)
|
.use_unicode(true)
|
||||||
.render(area, buf);
|
.render(area, buf);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
88
src/ui/status/right.rs
Normal file
88
src/ui/status/right.rs
Normal file
|
|
@ -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<Span> {
|
||||||
|
// 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<Span> {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue