mirror of
https://codeberg.org/unspeaker/perch.git
synced 2025-12-06 09:36:42 +01:00
better column resizing
This commit is contained in:
parent
44a2108585
commit
4de94beafb
9 changed files with 403 additions and 359 deletions
16
Cargo.lock
generated
16
Cargo.lock
generated
|
|
@ -1445,8 +1445,8 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tengri"
|
name = "tengri"
|
||||||
version = "0.5.1"
|
version = "0.6.0"
|
||||||
source = "git+https://codeberg.org/unspeaker/tengri?rev=829d35b#829d35b61fc4323273613695f02bb1e6bfde0cbd"
|
source = "git+https://codeberg.org/unspeaker/tengri?rev=6ca3a4a#6ca3a4a2cca1c9933c6f2e8a05b0aacc47bf782d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tengri_input",
|
"tengri_input",
|
||||||
"tengri_output",
|
"tengri_output",
|
||||||
|
|
@ -1455,18 +1455,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tengri_input"
|
name = "tengri_input"
|
||||||
version = "0.5.1"
|
version = "0.6.0"
|
||||||
source = "git+https://codeberg.org/unspeaker/tengri?rev=829d35b#829d35b61fc4323273613695f02bb1e6bfde0cbd"
|
source = "git+https://codeberg.org/unspeaker/tengri?rev=6ca3a4a#6ca3a4a2cca1c9933c6f2e8a05b0aacc47bf782d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tengri_output"
|
name = "tengri_output"
|
||||||
version = "0.5.1"
|
version = "0.6.0"
|
||||||
source = "git+https://codeberg.org/unspeaker/tengri?rev=829d35b#829d35b61fc4323273613695f02bb1e6bfde0cbd"
|
source = "git+https://codeberg.org/unspeaker/tengri?rev=6ca3a4a#6ca3a4a2cca1c9933c6f2e8a05b0aacc47bf782d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tengri_tui"
|
name = "tengri_tui"
|
||||||
version = "0.5.1"
|
version = "0.6.0"
|
||||||
source = "git+https://codeberg.org/unspeaker/tengri?rev=829d35b#829d35b61fc4323273613695f02bb1e6bfde0cbd"
|
source = "git+https://codeberg.org/unspeaker/tengri?rev=6ca3a4a#6ca3a4a2cca1c9933c6f2e8a05b0aacc47bf782d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic_float",
|
"atomic_float",
|
||||||
"better-panic",
|
"better-panic",
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ edition = "2024"
|
||||||
|
|
||||||
[dependencies.tengri]
|
[dependencies.tengri]
|
||||||
git = "https://codeberg.org/unspeaker/tengri"
|
git = "https://codeberg.org/unspeaker/tengri"
|
||||||
rev = "829d35b"
|
rev = "6ca3a4a"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "^4.5.4", features = [ "cargo" ] }
|
clap = { version = "^4.5.4", features = [ "cargo" ] }
|
||||||
|
|
|
||||||
37
src/keys.rs
37
src/keys.rs
|
|
@ -43,13 +43,14 @@ impl Handle<TuiIn> for Taggart {
|
||||||
press!(Up) => { self.cursor = self.cursor.saturating_sub(1); },
|
press!(Up) => { self.cursor = self.cursor.saturating_sub(1); },
|
||||||
press!(Down) => { self.cursor = self.cursor + 1; },
|
press!(Down) => { self.cursor = self.cursor + 1; },
|
||||||
press!(PageUp) => { self.cursor = self.cursor.saturating_sub(PAGE_SIZE); },
|
press!(PageUp) => { self.cursor = self.cursor.saturating_sub(PAGE_SIZE); },
|
||||||
press!(PageDown) => { self.cursor += PAGE_SIZE; },
|
press!(PageDown) => { self.cursor += PAGE_SIZE; },
|
||||||
press!(Left) => { self.column = self.column.saturating_sub(1); },
|
press!(Char(' ')) => { self.open_in_player()?; },
|
||||||
press!(Right) => { self.column = self.column + 1; },
|
press!(Left) => { self.column_prev(); },
|
||||||
press!(Char(' ')) => { open(self.entries[self.cursor].path.as_ref())?; }
|
press!(Right) => { self.column_next(); },
|
||||||
press!(Char(']')) => { self.columns.0[self.column].width += 1; }
|
press!(Char('[')) => { self.column_resize(-1); },
|
||||||
press!(Char('[')) => { self.columns.0[self.column].width =
|
press!(Char(']')) => { self.column_resize( 1); },
|
||||||
self.columns.0[self.column].width.saturating_sub(1).max(5); }
|
press!(Char('{')) => { self.column_collapse(true, 1); },
|
||||||
|
press!(Char('}')) => { self.column_collapse(false, -1); },
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -57,3 +58,25 @@ impl Handle<TuiIn> for Taggart {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Taggart {
|
||||||
|
fn open_in_player (&self) -> Usually<()> {
|
||||||
|
open(self.entries[self.cursor].path.as_ref())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn column_prev (&mut self) {
|
||||||
|
self.column = self.column.saturating_sub(1);
|
||||||
|
}
|
||||||
|
fn column_next (&mut self) {
|
||||||
|
self.column = self.column + 1;
|
||||||
|
}
|
||||||
|
fn column_resize (&mut self, amount: isize) {
|
||||||
|
let column = &mut self.columns.0[self.column];
|
||||||
|
column.width = ((column.width as isize) + amount).max(0) as usize;
|
||||||
|
}
|
||||||
|
fn column_collapse (&mut self, value: bool, next: isize) {
|
||||||
|
let column = &mut self.columns.0[self.column];
|
||||||
|
column.collapsed = value;
|
||||||
|
self.column = ((self.column as isize) + next).max(0) as usize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
40
src/model.rs
40
src/model.rs
|
|
@ -1,21 +1,23 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
mod column; pub use self::column::*;
|
mod column; pub use self::column::*;
|
||||||
mod entry; pub use self::entry::*;
|
mod entry; pub use self::entry::*;
|
||||||
mod metadata; pub use self::metadata::*;
|
mod task; pub use self::task::*;
|
||||||
mod task; pub use self::task::*;
|
|
||||||
|
|
||||||
/// The application state.
|
/// The application state.
|
||||||
pub struct Taggart {
|
pub struct Taggart {
|
||||||
pub _root: PathBuf,
|
pub _root: PathBuf,
|
||||||
pub entries: Vec<Entry>,
|
pub entries: Vec<Entry>,
|
||||||
pub cursor: usize,
|
pub cursor: usize,
|
||||||
pub offset: usize,
|
pub offset: usize,
|
||||||
pub column: usize,
|
pub column: usize,
|
||||||
pub columns: Columns<fn(&Entry)->Option<Arc<str>>, fn(&mut Self, usize, &str)>,
|
pub columns: Columns<fn(&Entry)->Option<Arc<str>>, fn(&mut Self, usize, &str)>,
|
||||||
pub display: Measure<TuiOut>,
|
pub display: Measure<TuiOut>,
|
||||||
pub tasks: Vec<Task>,
|
pub tasks: Vec<Task>,
|
||||||
pub mode: Option<Mode>,
|
/// State of modal dialog of editing field
|
||||||
|
pub mode: Option<Mode>,
|
||||||
|
/// Count of modified items
|
||||||
|
pub modified: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -38,6 +40,7 @@ impl Taggart {
|
||||||
columns: Columns::default(),
|
columns: Columns::default(),
|
||||||
tasks: vec![],
|
tasks: vec![],
|
||||||
entries,
|
entries,
|
||||||
|
modified: 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/// Make sure cursor is always in view
|
/// Make sure cursor is always in view
|
||||||
|
|
@ -58,4 +61,15 @@ impl Taggart {
|
||||||
self.column = self.columns.0.len().saturating_sub(1)
|
self.column = self.columns.0.len().saturating_sub(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// Count modified entries
|
||||||
|
pub(crate) fn count_modified (&mut self) -> usize {
|
||||||
|
let mut modified = 0;
|
||||||
|
for entry in self.entries.iter() {
|
||||||
|
if entry.is_modified() {
|
||||||
|
modified += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.modified = modified;
|
||||||
|
self.modified
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
pub struct Column<G, S> {
|
pub struct Column<G, S> {
|
||||||
pub title: Arc<str>,
|
pub title: Arc<str>,
|
||||||
pub width: usize,
|
pub width: usize,
|
||||||
pub getter: G,
|
pub collapsed: bool,
|
||||||
pub setter: Option<S>,
|
pub getter: G,
|
||||||
//pub styler: Option<U>,
|
pub setter: Option<S>,
|
||||||
|
//pub styler: Option<U>,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Getter<T> = fn(&T)->Option<Arc<str>>;
|
type Getter<T> = fn(&T)->Option<Arc<str>>;
|
||||||
|
|
@ -22,6 +23,7 @@ impl<G, S> Column<Getter<G>, Setter<S>> {
|
||||||
title: title.as_ref().into(),
|
title: title.as_ref().into(),
|
||||||
getter,
|
getter,
|
||||||
setter: None,
|
setter: None,
|
||||||
|
collapsed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn setter (mut self, setter: Setter<S>) -> Self {
|
fn setter (mut self, setter: Setter<S>) -> Self {
|
||||||
|
|
@ -30,32 +32,6 @@ impl<G, S> Column<Getter<G>, Setter<S>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! setter {
|
|
||||||
($name:ident) => {{
|
|
||||||
fn $name (
|
|
||||||
state: &mut Taggart,
|
|
||||||
index: usize,
|
|
||||||
value: &str
|
|
||||||
) {
|
|
||||||
if let Some(entries) = entries_under(&mut state.entries, index) {
|
|
||||||
for (p, entry) in entries.into_iter() {
|
|
||||||
if let Some(item) = entry.write().unwrap().$name(&value) {
|
|
||||||
state.tasks.retain(|t|{(t.path != p) || (t.item.key() != item.key())});
|
|
||||||
state.tasks.push(Task { path: p, item, });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else if let Some(entry) = state.entries.get_mut(index) {
|
|
||||||
let p = entry.path.clone();
|
|
||||||
if let Some(item) = entry.$name(&value) {
|
|
||||||
state.tasks.retain(|t|{(t.path != p) || (t.item.key() != item.key())});
|
|
||||||
state.tasks.push(Task { path: p, item, });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$name
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn entries_under (
|
pub(crate) fn entries_under (
|
||||||
entries: &mut [Entry],
|
entries: &mut [Entry],
|
||||||
index: usize
|
index: usize
|
||||||
|
|
@ -87,22 +63,52 @@ impl<G, S> Columns<G, S> {
|
||||||
const SCROLL_RIGHT: &'static str = "❯";
|
const SCROLL_RIGHT: &'static str = "❯";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! setter {
|
||||||
|
($set:ident) => {{
|
||||||
|
fn $set (state: &mut Taggart, index: usize, value: &str) {
|
||||||
|
if let Some(entries) = entries_under(&mut state.entries, index) {
|
||||||
|
for (_path, entry) in entries.into_iter() {
|
||||||
|
if entry.write().unwrap().$set(&value) {
|
||||||
|
state.count_modified();
|
||||||
|
//state.tasks.retain(|t|{(t.path != p) || (t.item.key() != item.key())});
|
||||||
|
//state.tasks.push(Task { path: p, item, });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if let Some(entry) = state.entries.get_mut(index) {
|
||||||
|
//let p = entry.path.clone();
|
||||||
|
if entry.$set(&value) {
|
||||||
|
state.count_modified();
|
||||||
|
//state.tasks.retain(|t|{(t.path != p) || (t.item.key() != item.key())});
|
||||||
|
//state.tasks.push(Task { path: p, item, });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$set
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for Columns<fn(&Entry)->Option<Arc<str>>, fn(&mut Taggart, usize, &str)> {
|
impl Default for Columns<fn(&Entry)->Option<Arc<str>>, fn(&mut Taggart, usize, &str)> {
|
||||||
fn default () -> Self {
|
fn default () -> Self {
|
||||||
Self(vec![
|
Self(vec![
|
||||||
|
// Computed file hash
|
||||||
Column::new(&"Hash", 8, |entry: &Entry|entry.hash()),
|
Column::new(&"Hash", 8, |entry: &Entry|entry.hash()),
|
||||||
|
// File size
|
||||||
Column::new(&"Size", 8, |entry: &Entry|entry.size()),
|
Column::new(&"Size", 8, |entry: &Entry|entry.size()),
|
||||||
|
// Selected?
|
||||||
|
Column::new(&"∗", 1, |entry: &Entry|Some(" ".into())),
|
||||||
|
// File name
|
||||||
Column::new(&"File", 80, |entry: &Entry|entry.name()),
|
Column::new(&"File", 80, |entry: &Entry|entry.name()),
|
||||||
Column::new(&"Artist", 30, |entry: &Entry|entry.artist())
|
// Modified tags?
|
||||||
.setter(setter!(set_artist)),
|
Column::new(&"~", 1, |entry: &Entry|if entry.is_modified() {
|
||||||
Column::new(&"Year", 5, |entry: &Entry|entry.year())
|
Some("~".into())
|
||||||
.setter(setter!(set_year)),
|
} else {
|
||||||
Column::new(&"Release", 30, |entry: &Entry|entry.album())
|
None
|
||||||
.setter(setter!(set_album)),
|
}),
|
||||||
Column::new(&"Track", 5, |entry: &Entry|entry.track())
|
Column::new(&"Artist", 30, |entry: &Entry|entry.artist()).setter(setter!(set_artist)),
|
||||||
.setter(setter!(set_track)),
|
Column::new(&"Year", 5, |entry: &Entry|entry.year()).setter(setter!(set_year)),
|
||||||
Column::new(&"Title", 80, |entry: &Entry|entry.title())
|
Column::new(&"Release", 30, |entry: &Entry|entry.album()).setter(setter!(set_album)),
|
||||||
.setter(setter!(set_title)),
|
Column::new(&"Track", 5, |entry: &Entry|entry.track()).setter(setter!(set_track)),
|
||||||
|
Column::new(&"Title", 80, |entry: &Entry|entry.title()).setter(setter!(set_title)),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,14 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{BufReader, Read};
|
||||||
use std::cmp::{Eq, PartialEq, Ord, PartialOrd, Ordering};
|
use std::cmp::{Eq, PartialEq, Ord, PartialOrd, Ordering};
|
||||||
|
use byte_unit::{Byte, Unit::MB};
|
||||||
|
use lofty::{
|
||||||
|
probe::Probe,
|
||||||
|
file::TaggedFileExt,
|
||||||
|
config::{ParseOptions, ParsingMode},
|
||||||
|
tag::{Accessor, Tag, TagType}
|
||||||
|
};
|
||||||
|
|
||||||
pub struct Entry {
|
pub struct Entry {
|
||||||
/// How many levels deep is this from the working directory
|
/// How many levels deep is this from the working directory
|
||||||
|
|
@ -11,6 +20,11 @@ pub struct Entry {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entry {
|
impl Entry {
|
||||||
|
pub const ICON_DIRECTORY: &'static str = "";
|
||||||
|
pub const ICON_IMAGE: &'static str = "";
|
||||||
|
pub const ICON_MUSIC: &'static str = "";
|
||||||
|
pub const ICON_MUSIC_NO_META: &'static str = "";
|
||||||
|
pub const ICON_UNKNOWN: &'static str = "";
|
||||||
pub fn new (root: &impl AsRef<Path>, path: &impl AsRef<Path>, depth: usize) -> Perhaps<Self> {
|
pub fn new (root: &impl AsRef<Path>, path: &impl AsRef<Path>, depth: usize) -> Perhaps<Self> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
|
|
@ -49,11 +63,6 @@ impl Entry {
|
||||||
pub fn is_image (&self) -> bool {
|
pub fn is_image (&self) -> bool {
|
||||||
matches!(&*self.info.read().unwrap(), Metadata::Image { .. })
|
matches!(&*self.info.read().unwrap(), Metadata::Image { .. })
|
||||||
}
|
}
|
||||||
pub const ICON_DIRECTORY: &'static str = "";
|
|
||||||
pub const ICON_IMAGE: &'static str = "";
|
|
||||||
pub const ICON_MUSIC: &'static str = "";
|
|
||||||
pub const ICON_MUSIC_NO_META: &'static str = "";
|
|
||||||
pub const ICON_UNKNOWN: &'static str = "";
|
|
||||||
pub fn name (&self) -> Option<Arc<str>> {
|
pub fn name (&self) -> Option<Arc<str>> {
|
||||||
let indent = "".pad_to_width((self.depth - 1) * 2);
|
let indent = "".pad_to_width((self.depth - 1) * 2);
|
||||||
let icon = self.icon();
|
let icon = self.icon();
|
||||||
|
|
@ -71,24 +80,269 @@ impl Entry {
|
||||||
Self::ICON_UNKNOWN
|
Self::ICON_UNKNOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn is_modified (&self) -> bool {
|
||||||
|
match &*self.info.read().unwrap() {
|
||||||
|
Metadata::Music { modified_tag, .. } => modified_tag.is_some(),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Eq for Entry {}
|
impl Eq for Entry {}
|
||||||
|
impl PartialEq for Entry { fn eq (&self, other: &Self) -> bool { self.path.eq(&other.path) } }
|
||||||
impl PartialEq for Entry {
|
impl Ord for Entry { fn cmp (&self, other: &Self) -> Ordering { self.path.cmp(&other.path) } }
|
||||||
fn eq (&self, other: &Self) -> bool {
|
|
||||||
self.path.eq(&other.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ord for Entry {
|
|
||||||
fn cmp (&self, other: &Self) -> Ordering {
|
|
||||||
self.path.cmp(&other.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialOrd for Entry {
|
impl PartialOrd for Entry {
|
||||||
fn partial_cmp (&self, other: &Self) -> Option<Ordering> {
|
fn partial_cmp (&self, other: &Self) -> Option<Ordering> {
|
||||||
self.path.partial_cmp(&other.path)
|
self.path.partial_cmp(&other.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum Metadata {
|
||||||
|
Directory {
|
||||||
|
hash_file: Option<()>,
|
||||||
|
catalog_file: Option<()>,
|
||||||
|
artist_file: Option<()>,
|
||||||
|
release_file: Option<()>,
|
||||||
|
},
|
||||||
|
Music {
|
||||||
|
invalid: bool,
|
||||||
|
hash: Arc<str>,
|
||||||
|
size: Arc<str>,
|
||||||
|
original_tag: Option<Arc<Tag>>,
|
||||||
|
modified_tag: Option<Arc<RwLock<Tag>>>,
|
||||||
|
},
|
||||||
|
Image {
|
||||||
|
invalid: bool,
|
||||||
|
hash: Arc<str>,
|
||||||
|
size: Arc<str>,
|
||||||
|
title: Option<String>,
|
||||||
|
author: Option<String>,
|
||||||
|
},
|
||||||
|
Unknown {
|
||||||
|
hash: Arc<str>,
|
||||||
|
size: Arc<str>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Metadata {
|
||||||
|
pub fn new (path: &Path, strict: bool) -> Usually<Self> {
|
||||||
|
let probe = Probe::new(BufReader::new(File::open(path)?))
|
||||||
|
.options(ParseOptions::new().parsing_mode(if strict {
|
||||||
|
ParsingMode::Strict
|
||||||
|
} else {
|
||||||
|
ParsingMode::BestAttempt
|
||||||
|
}))
|
||||||
|
.guess_file_type()?;
|
||||||
|
if probe.file_type().is_some() {
|
||||||
|
let file = lofty::read_from_path(path)?;
|
||||||
|
let tag = file.primary_tag();
|
||||||
|
let data = std::fs::read(path)?;
|
||||||
|
let hash = hex::encode(xxh3_64(data.as_slice()).to_be_bytes()).into();
|
||||||
|
let size = Byte::from_u64(data.len() as u64).get_adjusted_unit(MB);
|
||||||
|
Ok(Self::Music {
|
||||||
|
hash,
|
||||||
|
size: format!("{:#>8.2}", size).into(),
|
||||||
|
invalid: false,
|
||||||
|
original_tag: tag.map(|t|t.clone().into()),
|
||||||
|
modified_tag: None,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Self::new_fallback(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn new_fallback (path: &Path) -> Usually<Self> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let size = Byte::from_u64(file.metadata()?.len() as u64).get_adjusted_unit(MB);
|
||||||
|
let mut reader = BufReader::new(file);
|
||||||
|
let mut bytes = vec![0;16];
|
||||||
|
reader.read(&mut bytes)?;
|
||||||
|
// PNG
|
||||||
|
if bytes.starts_with(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]) {
|
||||||
|
let mut bytes = vec![];
|
||||||
|
BufReader::new(File::open(path)?).read(&mut bytes)?;
|
||||||
|
return Ok(Self::Image {
|
||||||
|
hash: hex::encode(xxh3_64(&bytes).to_be_bytes()).into(),
|
||||||
|
size: format!("{:#>8.2}", size).into(),
|
||||||
|
title: None,
|
||||||
|
author: None,
|
||||||
|
invalid: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// JPG
|
||||||
|
if bytes.starts_with(&[0xFF, 0xD8, 0xFF, 0xDB])
|
||||||
|
|| bytes.starts_with(&[0xFF, 0xD8, 0xFF, 0xE0,
|
||||||
|
0x00, 0x10, 0x4A, 0x46,
|
||||||
|
0x49, 0x46, 0x00, 0x01])
|
||||||
|
|| bytes.starts_with(&[0xFF, 0xD8, 0xFF, 0xEE])
|
||||||
|
|| (bytes.starts_with(&[0xFF, 0xD8, 0xFF, 0xE1]) &&
|
||||||
|
bytes.get(6) == Some(&0x45) && bytes.get(7) == Some(&0x78) &&
|
||||||
|
bytes.get(8) == Some(&0x69) && bytes.get(9) == Some(&0x66) &&
|
||||||
|
bytes.get(10) == Some(&0x00) && bytes.get(11) == Some(&0x00))
|
||||||
|
{
|
||||||
|
return Ok(Self::Image {
|
||||||
|
hash: hex::encode(xxh3_64(&bytes).to_be_bytes()).into(),
|
||||||
|
size: format!("{:#>8.2}", size).into(),
|
||||||
|
title: None,
|
||||||
|
author: None,
|
||||||
|
invalid: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Ok(Self::Unknown {
|
||||||
|
size: format!("{:#>8.2}", size).into(),
|
||||||
|
hash: hex::encode(xxh3_64(&bytes).to_be_bytes()).into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! generated_field {
|
||||||
|
($get:ident = |$self:ident| $expr:expr) => {
|
||||||
|
impl Entry {
|
||||||
|
pub fn $get (&$self) -> Option<Arc<str>> {
|
||||||
|
$self.info.read().unwrap().$get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Metadata {
|
||||||
|
pub fn $get (&$self) -> Option<Arc<str>> {
|
||||||
|
$expr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generated_field!(hash = |self|match self {
|
||||||
|
Metadata::Image { hash, .. } => Some(hash.clone()),
|
||||||
|
Metadata::Music { hash, .. } => Some(hash.clone()),
|
||||||
|
Metadata::Unknown { hash, .. } => Some(hash.clone()),
|
||||||
|
_ => None
|
||||||
|
});
|
||||||
|
|
||||||
|
generated_field!(size = |self|match self {
|
||||||
|
Metadata::Image { size, .. } => Some(size.clone()),
|
||||||
|
Metadata::Music { size, .. } => Some(size.clone()),
|
||||||
|
Metadata::Unknown { size, .. } => Some(size.clone()),
|
||||||
|
_ => None
|
||||||
|
});
|
||||||
|
|
||||||
|
macro_rules! music_tag_field {
|
||||||
|
(string: $get:ident $set:ident $del:ident $key:expr) => {
|
||||||
|
impl Entry {
|
||||||
|
pub fn $get (&self) -> Option<Arc<str>> {
|
||||||
|
self.info.read().unwrap().$get()
|
||||||
|
}
|
||||||
|
pub fn $set (&mut self, value: &impl AsRef<str>) -> bool {
|
||||||
|
self.info.write().unwrap().$set(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Metadata {
|
||||||
|
pub fn $get (&self) -> Option<Arc<str>> {
|
||||||
|
if let Metadata::Music { original_tag, modified_tag, .. } = self {
|
||||||
|
return modified_tag.as_ref()
|
||||||
|
.map(|tag|tag.read().unwrap().$get().map(|t|t.into()))
|
||||||
|
.flatten()
|
||||||
|
.or_else(||original_tag.as_ref()
|
||||||
|
.map(|tag|tag.$get().map(|t|t.into()))
|
||||||
|
.flatten())
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
pub fn $set (&mut self, value: &impl AsRef<str>) -> bool {
|
||||||
|
let value = value.as_ref().trim();
|
||||||
|
if let &mut Metadata::Music { ref mut modified_tag, .. } = self {
|
||||||
|
match (value.len(), &modified_tag) {
|
||||||
|
(0, Some(new_tag)) => {
|
||||||
|
if new_tag.read().unwrap().item_count() <= 1 {
|
||||||
|
// removing last entry removes modified_tag
|
||||||
|
*modified_tag = None;
|
||||||
|
} else {
|
||||||
|
// remove entry from modified_tag
|
||||||
|
new_tag.write().unwrap().$del();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
(_, Some(new_tag)) => {
|
||||||
|
// add entry to modified_tag
|
||||||
|
new_tag.write().unwrap().$set(value.into());
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
(0, None) => {
|
||||||
|
// leave modified_tag empty
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
(_, None) => {
|
||||||
|
// first entry creates modified_tag
|
||||||
|
let mut new_tag = Tag::new(TagType::Id3v2); // FIXME other types
|
||||||
|
new_tag.$set(value.into());
|
||||||
|
*modified_tag = Some(Arc::new(RwLock::new(new_tag)));
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(number: $get:ident $set:ident $del:ident $key:expr) => {
|
||||||
|
impl Entry {
|
||||||
|
pub fn $get (&self) -> Option<Arc<str>> {
|
||||||
|
self.info.read().unwrap().$get()
|
||||||
|
}
|
||||||
|
pub fn $set (&mut self, value: &impl AsRef<str>) -> bool {
|
||||||
|
self.info.write().unwrap().$set(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Metadata {
|
||||||
|
pub fn $get (&self) -> Option<Arc<str>> {
|
||||||
|
if let Metadata::Music { original_tag, modified_tag, .. } = self {
|
||||||
|
return modified_tag.as_ref()
|
||||||
|
.map(|tag|tag.read().unwrap().$get().map(|t|format!("{t}").into()))
|
||||||
|
.flatten()
|
||||||
|
.or_else(||original_tag.as_ref()
|
||||||
|
.map(|tag|tag.$get().map(|t|format!("{t}").into()))
|
||||||
|
.flatten())
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
pub fn $set (&mut self, value: &impl AsRef<str>) -> bool {
|
||||||
|
let value = value.as_ref().trim();
|
||||||
|
if let &mut Metadata::Music { ref mut modified_tag, .. } = self
|
||||||
|
&& let Ok(numeric_value) = value.parse::<u32>()
|
||||||
|
{
|
||||||
|
match (value.len(), &modified_tag) {
|
||||||
|
(0, Some(new_tag)) => {
|
||||||
|
if new_tag.read().unwrap().item_count() <= 1 {
|
||||||
|
// removing last entry removes modified_tag
|
||||||
|
*modified_tag = None;
|
||||||
|
} else {
|
||||||
|
// remove entry from modified_tag
|
||||||
|
new_tag.write().unwrap().$del();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
(_, Some(new_tag)) => {
|
||||||
|
// add entry to modified_tag
|
||||||
|
new_tag.write().unwrap().$set(numeric_value);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
(0, None) => {
|
||||||
|
// leave modified_tag empty
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
(_, None) => {
|
||||||
|
// first entry creates modified_tag
|
||||||
|
let mut new_tag = Tag::new(TagType::Id3v2); // FIXME other types
|
||||||
|
new_tag.$set(numeric_value);
|
||||||
|
*modified_tag = Some(Arc::new(RwLock::new(new_tag)));
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
music_tag_field!(string: artist set_artist remove_artist ItemKey::TrackArtist);
|
||||||
|
music_tag_field!(number: year set_year remove_year ItemKey::Year);
|
||||||
|
music_tag_field!(string: album set_album remove_album ItemKey::AlbumTitle);
|
||||||
|
music_tag_field!(number: track set_track remove_track ItemKey::TrackNumber);
|
||||||
|
music_tag_field!(string: title set_title remove_title ItemKey::TrackTitle);
|
||||||
|
|
|
||||||
|
|
@ -1,253 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::{BufReader, Read};
|
|
||||||
use byte_unit::{Byte, Unit::MB};
|
|
||||||
use lofty::{
|
|
||||||
probe::Probe,
|
|
||||||
file::TaggedFileExt,
|
|
||||||
config::{ParseOptions, ParsingMode},
|
|
||||||
tag::{Accessor, Tag, TagItem, TagType}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub enum Metadata {
|
|
||||||
Directory {
|
|
||||||
hash_file: Option<()>,
|
|
||||||
catalog_file: Option<()>,
|
|
||||||
artist_file: Option<()>,
|
|
||||||
release_file: Option<()>,
|
|
||||||
},
|
|
||||||
Music {
|
|
||||||
invalid: bool,
|
|
||||||
hash: Arc<str>,
|
|
||||||
size: Arc<str>,
|
|
||||||
original_tag: Option<Arc<Tag>>,
|
|
||||||
modified_tag: Option<Arc<RwLock<Tag>>>,
|
|
||||||
},
|
|
||||||
Image {
|
|
||||||
invalid: bool,
|
|
||||||
hash: Arc<str>,
|
|
||||||
size: Arc<str>,
|
|
||||||
title: Option<String>,
|
|
||||||
author: Option<String>,
|
|
||||||
},
|
|
||||||
Unknown {
|
|
||||||
hash: Arc<str>,
|
|
||||||
size: Arc<str>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Metadata {
|
|
||||||
pub fn new (path: &Path, strict: bool) -> Usually<Self> {
|
|
||||||
let probe = Probe::new(BufReader::new(File::open(path)?))
|
|
||||||
.options(ParseOptions::new().parsing_mode(if strict {
|
|
||||||
ParsingMode::Strict
|
|
||||||
} else {
|
|
||||||
ParsingMode::BestAttempt
|
|
||||||
}))
|
|
||||||
.guess_file_type()?;
|
|
||||||
if probe.file_type().is_some() {
|
|
||||||
let file = lofty::read_from_path(path)?;
|
|
||||||
let tag = file.primary_tag();
|
|
||||||
let data = std::fs::read(path)?;
|
|
||||||
let hash = hex::encode(xxh3_64(data.as_slice()).to_be_bytes()).into();
|
|
||||||
let size = Byte::from_u64(data.len() as u64).get_adjusted_unit(MB);
|
|
||||||
Ok(Self::Music {
|
|
||||||
hash,
|
|
||||||
size: format!("{:#>8.2}", size).into(),
|
|
||||||
invalid: false,
|
|
||||||
original_tag: tag.map(|t|t.clone().into()),
|
|
||||||
modified_tag: tag.map(|t|Arc::new(t.clone().into())),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Self::new_fallback(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn new_fallback (path: &Path) -> Usually<Self> {
|
|
||||||
let file = File::open(path)?;
|
|
||||||
let size = Byte::from_u64(file.metadata()?.len() as u64).get_adjusted_unit(MB);
|
|
||||||
let mut reader = BufReader::new(file);
|
|
||||||
let mut bytes = vec![0;16];
|
|
||||||
reader.read(&mut bytes)?;
|
|
||||||
// PNG
|
|
||||||
if bytes.starts_with(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]) {
|
|
||||||
let mut bytes = vec![];
|
|
||||||
BufReader::new(File::open(path)?).read(&mut bytes)?;
|
|
||||||
return Ok(Self::Image {
|
|
||||||
hash: hex::encode(xxh3_64(&bytes).to_be_bytes()).into(),
|
|
||||||
size: format!("{:#>8.2}", size).into(),
|
|
||||||
title: None,
|
|
||||||
author: None,
|
|
||||||
invalid: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// JPG
|
|
||||||
if bytes.starts_with(&[0xFF, 0xD8, 0xFF, 0xDB])
|
|
||||||
|| bytes.starts_with(&[0xFF, 0xD8, 0xFF, 0xE0,
|
|
||||||
0x00, 0x10, 0x4A, 0x46,
|
|
||||||
0x49, 0x46, 0x00, 0x01])
|
|
||||||
|| bytes.starts_with(&[0xFF, 0xD8, 0xFF, 0xEE])
|
|
||||||
|| (bytes.starts_with(&[0xFF, 0xD8, 0xFF, 0xE1]) &&
|
|
||||||
bytes.get(6) == Some(&0x45) && bytes.get(7) == Some(&0x78) &&
|
|
||||||
bytes.get(8) == Some(&0x69) && bytes.get(9) == Some(&0x66) &&
|
|
||||||
bytes.get(10) == Some(&0x00) && bytes.get(11) == Some(&0x00))
|
|
||||||
{
|
|
||||||
return Ok(Self::Image {
|
|
||||||
hash: hex::encode(xxh3_64(&bytes).to_be_bytes()).into(),
|
|
||||||
size: format!("{:#>8.2}", size).into(),
|
|
||||||
title: None,
|
|
||||||
author: None,
|
|
||||||
invalid: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Ok(Self::Unknown {
|
|
||||||
size: format!("{:#>8.2}", size).into(),
|
|
||||||
hash: hex::encode(xxh3_64(&bytes).to_be_bytes()).into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! generated_field {
|
|
||||||
($get:ident = |$self:ident| $expr:expr) => {
|
|
||||||
impl Entry {
|
|
||||||
pub fn $get (&$self) -> Option<Arc<str>> {
|
|
||||||
$self.info.read().unwrap().$get()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Metadata {
|
|
||||||
pub fn $get (&$self) -> Option<Arc<str>> {
|
|
||||||
$expr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generated_field!(hash = |self|match self {
|
|
||||||
Metadata::Image { hash, .. } => Some(hash.clone()),
|
|
||||||
Metadata::Music { hash, .. } => Some(hash.clone()),
|
|
||||||
Metadata::Unknown { hash, .. } => Some(hash.clone()),
|
|
||||||
_ => None
|
|
||||||
});
|
|
||||||
|
|
||||||
generated_field!(size = |self|match self {
|
|
||||||
Metadata::Image { size, .. } => Some(size.clone()),
|
|
||||||
Metadata::Music { size, .. } => Some(size.clone()),
|
|
||||||
Metadata::Unknown { size, .. } => Some(size.clone()),
|
|
||||||
_ => None
|
|
||||||
});
|
|
||||||
|
|
||||||
macro_rules! music_tag_field {
|
|
||||||
(string: $get:ident $set:ident $del:ident $key:expr) => {
|
|
||||||
impl Entry {
|
|
||||||
pub fn $get (&self) -> Option<Arc<str>> {
|
|
||||||
self.info.read().unwrap().$get()
|
|
||||||
}
|
|
||||||
pub fn $set (&mut self, value: &impl AsRef<str>) -> Option<TagItem> {
|
|
||||||
self.info.write().unwrap().$set(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Metadata {
|
|
||||||
pub fn $get (&self) -> Option<Arc<str>> {
|
|
||||||
if let Metadata::Music { original_tag, modified_tag, .. } = self {
|
|
||||||
return modified_tag.as_ref()
|
|
||||||
.map(|tag|tag.read().unwrap().$get().map(|t|t.into()))
|
|
||||||
.flatten()
|
|
||||||
.or_else(||original_tag.as_ref()
|
|
||||||
.map(|tag|tag.$get().map(|t|t.into()))
|
|
||||||
.flatten())
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
pub fn $set (&mut self, value: &impl AsRef<str>) -> Option<TagItem> {
|
|
||||||
let value = value.as_ref().trim();
|
|
||||||
if let &mut Metadata::Music { ref mut modified_tag, .. } = self {
|
|
||||||
match (value.len(), &modified_tag) {
|
|
||||||
(0, Some(new_tag)) => {
|
|
||||||
if new_tag.read().unwrap().item_count() <= 1 {
|
|
||||||
// removing last entry removes modified_tag
|
|
||||||
*modified_tag = None;
|
|
||||||
} else {
|
|
||||||
// remove entry from modified_tag
|
|
||||||
new_tag.write().unwrap().$del();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(_, Some(new_tag)) => {
|
|
||||||
// add entry to modified_tag
|
|
||||||
new_tag.write().unwrap().$set(value.into())
|
|
||||||
},
|
|
||||||
(0, None) => {
|
|
||||||
// leave modified_tag empty
|
|
||||||
},
|
|
||||||
(_, None) => {
|
|
||||||
// first entry creates modified_tag
|
|
||||||
let mut new_tag = Tag::new(TagType::Id3v2); // FIXME other types
|
|
||||||
new_tag.$set(value.into());
|
|
||||||
*modified_tag = Some(Arc::new(RwLock::new(new_tag)));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
(number: $get:ident $set:ident $del:ident $key:expr) => {
|
|
||||||
impl Entry {
|
|
||||||
pub fn $get (&self) -> Option<Arc<str>> {
|
|
||||||
self.info.read().unwrap().$get()
|
|
||||||
}
|
|
||||||
pub fn $set (&mut self, value: &impl AsRef<str>) -> Option<TagItem> {
|
|
||||||
self.info.write().unwrap().$set(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Metadata {
|
|
||||||
pub fn $get (&self) -> Option<Arc<str>> {
|
|
||||||
if let Metadata::Music { original_tag, modified_tag, .. } = self {
|
|
||||||
return modified_tag.as_ref()
|
|
||||||
.map(|tag|tag.read().unwrap().$get().map(|t|format!("{t}").into()))
|
|
||||||
.flatten()
|
|
||||||
.or_else(||original_tag.as_ref()
|
|
||||||
.map(|tag|tag.$get().map(|t|format!("{t}").into()))
|
|
||||||
.flatten())
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
pub fn $set (&mut self, value: &impl AsRef<str>) -> Option<TagItem> {
|
|
||||||
let value = value.as_ref().trim();
|
|
||||||
if let &mut Metadata::Music { ref mut modified_tag, .. } = self
|
|
||||||
&& let Ok(numeric_value) = value.parse::<u32>()
|
|
||||||
{
|
|
||||||
match (value.len(), &modified_tag) {
|
|
||||||
(0, Some(new_tag)) => {
|
|
||||||
if new_tag.read().unwrap().item_count() <= 1 {
|
|
||||||
// removing last entry removes modified_tag
|
|
||||||
*modified_tag = None;
|
|
||||||
} else {
|
|
||||||
// remove entry from modified_tag
|
|
||||||
new_tag.write().unwrap().$del();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(_, Some(new_tag)) => {
|
|
||||||
// add entry to modified_tag
|
|
||||||
new_tag.write().unwrap().$set(numeric_value)
|
|
||||||
},
|
|
||||||
(0, None) => {
|
|
||||||
// leave modified_tag empty
|
|
||||||
},
|
|
||||||
(_, None) => {
|
|
||||||
// first entry creates modified_tag
|
|
||||||
let mut new_tag = Tag::new(TagType::Id3v2); // FIXME other types
|
|
||||||
new_tag.$set(numeric_value);
|
|
||||||
*modified_tag = Some(Arc::new(RwLock::new(new_tag)));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
music_tag_field!(string: artist set_artist remove_artist ItemKey::TrackArtist);
|
|
||||||
music_tag_field!(number: year set_year remove_year ItemKey::Year);
|
|
||||||
music_tag_field!(string: album set_album remove_album ItemKey::AlbumTitle);
|
|
||||||
music_tag_field!(number: track set_track remove_track ItemKey::TrackNumber);
|
|
||||||
music_tag_field!(string: title set_title remove_title ItemKey::TrackTitle);
|
|
||||||
|
|
@ -32,7 +32,7 @@ impl Taggart {
|
||||||
))),
|
))),
|
||||||
Fill::x(Align::e(format!(
|
Fill::x(Align::e(format!(
|
||||||
" {} unsaved changes ",
|
" {} unsaved changes ",
|
||||||
self.tasks.len()
|
self.modified
|
||||||
)))
|
)))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -57,27 +57,25 @@ impl<'a> TreeTable<'a> {
|
||||||
}
|
}
|
||||||
fn row_data (&self, to: &mut TuiOut, entry: &Entry, cursor: usize, x: &mut u16) {
|
fn row_data (&self, to: &mut TuiOut, entry: &Entry, cursor: usize, x: &mut u16) {
|
||||||
let y = to.area().y();
|
let y = to.area().y();
|
||||||
for (column_index, Column {
|
let cols = self.0.columns.0.iter().enumerate();
|
||||||
width,
|
for (column_index, Column { width, collapsed, getter, .. }) in cols {
|
||||||
getter,
|
let width = if *collapsed { 1 } else { *width };
|
||||||
..
|
|
||||||
}) in self.0.columns.0.iter().enumerate() {
|
|
||||||
to.area[0] = *x;
|
to.area[0] = *x;
|
||||||
if let Some(Mode::Edit { index: edit_index, value }) = self.0.mode.as_ref()
|
if let Some(Mode::Edit { index: edit_index, value }) = self.0.mode.as_ref()
|
||||||
&& self.0.column == column_index
|
&& self.0.column == column_index
|
||||||
&& self.0.cursor == cursor
|
&& self.0.cursor == cursor
|
||||||
{
|
{
|
||||||
to.fill_bg([*x, y, *width as u16, 1], Self::BG_EDIT);
|
to.fill_bg([*x, y, width as u16, 1], Self::BG_EDIT);
|
||||||
to.fill_fg([*x, y, *width as u16, 1], Self::FG_EDIT);
|
to.fill_fg([*x, y, width as u16, 1], Self::FG_EDIT);
|
||||||
to.fill_reversed([*x + *edit_index as u16, y, 1, 1], true);
|
to.fill_reversed([*x + *edit_index as u16, y, 1, 1], true);
|
||||||
Content::render(&TrimStringRef(*width as u16, &value), to);
|
Content::render(&TrimStringRef(width as u16, &value), to);
|
||||||
} else if let Some(value) = getter(entry) {
|
} else if let Some(value) = getter(entry) {
|
||||||
Content::render(&TrimStringRef(*width as u16, &value), to);
|
Content::render(&TrimStringRef(width as u16, &value), to);
|
||||||
if self.0.cursor != cursor {
|
if self.0.cursor != cursor {
|
||||||
to.fill_fg([*x, y, *width as u16, 1], Self::FG_CELL);
|
to.fill_fg([*x, y, width as u16, 1], Self::FG_CELL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*x += *width as u16 + 1;
|
*x += width as u16 + 1;
|
||||||
to.blit(&"│", *x - 1, y, None);
|
to.blit(&"│", *x - 1, y, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -86,16 +84,18 @@ impl<'a> TreeTable<'a> {
|
||||||
impl<G, S> Columns<G, S> {
|
impl<G, S> Columns<G, S> {
|
||||||
pub fn header (&self) -> Arc<str> {
|
pub fn header (&self) -> Arc<str> {
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
for Column { width, title, .. } in self.0.iter() {
|
for Column { width, collapsed, title, .. } in self.0.iter() {
|
||||||
let cell = title.pad_to_width(*width);
|
let width = if *collapsed { 1 } else { *width };
|
||||||
output = format!("{output}{cell}│");
|
let cell = trim_string(width, title).pad_to_width(width);
|
||||||
|
output = format!("{output}{cell}│");
|
||||||
}
|
}
|
||||||
output.into()
|
output.into()
|
||||||
}
|
}
|
||||||
pub fn xw (&self, column: usize) -> (u16, u16) {
|
pub fn xw (&self, column: usize) -> (u16, u16) {
|
||||||
let mut x: u16 = 0;
|
let mut x: u16 = 0;
|
||||||
for (index, Column { width, .. }) in self.0.iter().enumerate() {
|
for (index, Column { width, collapsed, .. }) in self.0.iter().enumerate() {
|
||||||
let w = *width as u16 + 1;
|
let width = if *collapsed { 1 } else { *width };
|
||||||
|
let w = width as u16 + 1;
|
||||||
if index == column {
|
if index == column {
|
||||||
return (x, w)
|
return (x, w)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue