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]]
|
||||
name = "tengri"
|
||||
version = "0.5.1"
|
||||
source = "git+https://codeberg.org/unspeaker/tengri?rev=829d35b#829d35b61fc4323273613695f02bb1e6bfde0cbd"
|
||||
version = "0.6.0"
|
||||
source = "git+https://codeberg.org/unspeaker/tengri?rev=6ca3a4a#6ca3a4a2cca1c9933c6f2e8a05b0aacc47bf782d"
|
||||
dependencies = [
|
||||
"tengri_input",
|
||||
"tengri_output",
|
||||
|
|
@ -1455,18 +1455,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tengri_input"
|
||||
version = "0.5.1"
|
||||
source = "git+https://codeberg.org/unspeaker/tengri?rev=829d35b#829d35b61fc4323273613695f02bb1e6bfde0cbd"
|
||||
version = "0.6.0"
|
||||
source = "git+https://codeberg.org/unspeaker/tengri?rev=6ca3a4a#6ca3a4a2cca1c9933c6f2e8a05b0aacc47bf782d"
|
||||
|
||||
[[package]]
|
||||
name = "tengri_output"
|
||||
version = "0.5.1"
|
||||
source = "git+https://codeberg.org/unspeaker/tengri?rev=829d35b#829d35b61fc4323273613695f02bb1e6bfde0cbd"
|
||||
version = "0.6.0"
|
||||
source = "git+https://codeberg.org/unspeaker/tengri?rev=6ca3a4a#6ca3a4a2cca1c9933c6f2e8a05b0aacc47bf782d"
|
||||
|
||||
[[package]]
|
||||
name = "tengri_tui"
|
||||
version = "0.5.1"
|
||||
source = "git+https://codeberg.org/unspeaker/tengri?rev=829d35b#829d35b61fc4323273613695f02bb1e6bfde0cbd"
|
||||
version = "0.6.0"
|
||||
source = "git+https://codeberg.org/unspeaker/tengri?rev=6ca3a4a#6ca3a4a2cca1c9933c6f2e8a05b0aacc47bf782d"
|
||||
dependencies = [
|
||||
"atomic_float",
|
||||
"better-panic",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ edition = "2024"
|
|||
|
||||
[dependencies.tengri]
|
||||
git = "https://codeberg.org/unspeaker/tengri"
|
||||
rev = "829d35b"
|
||||
rev = "6ca3a4a"
|
||||
|
||||
[dependencies]
|
||||
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!(Down) => { self.cursor = self.cursor + 1; },
|
||||
press!(PageUp) => { self.cursor = self.cursor.saturating_sub(PAGE_SIZE); },
|
||||
press!(PageDown) => { self.cursor += PAGE_SIZE; },
|
||||
press!(Left) => { self.column = self.column.saturating_sub(1); },
|
||||
press!(Right) => { self.column = self.column + 1; },
|
||||
press!(Char(' ')) => { open(self.entries[self.cursor].path.as_ref())?; }
|
||||
press!(Char(']')) => { self.columns.0[self.column].width += 1; }
|
||||
press!(Char('[')) => { self.columns.0[self.column].width =
|
||||
self.columns.0[self.column].width.saturating_sub(1).max(5); }
|
||||
press!(PageDown) => { self.cursor += PAGE_SIZE; },
|
||||
press!(Char(' ')) => { self.open_in_player()?; },
|
||||
press!(Left) => { self.column_prev(); },
|
||||
press!(Right) => { self.column_next(); },
|
||||
press!(Char('[')) => { self.column_resize(-1); },
|
||||
press!(Char(']')) => { self.column_resize( 1); },
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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::*;
|
||||
|
||||
mod column; pub use self::column::*;
|
||||
mod entry; pub use self::entry::*;
|
||||
mod metadata; pub use self::metadata::*;
|
||||
mod task; pub use self::task::*;
|
||||
mod column; pub use self::column::*;
|
||||
mod entry; pub use self::entry::*;
|
||||
mod task; pub use self::task::*;
|
||||
|
||||
/// The application state.
|
||||
pub struct Taggart {
|
||||
pub _root: PathBuf,
|
||||
pub entries: Vec<Entry>,
|
||||
pub cursor: usize,
|
||||
pub offset: usize,
|
||||
pub column: usize,
|
||||
pub columns: Columns<fn(&Entry)->Option<Arc<str>>, fn(&mut Self, usize, &str)>,
|
||||
pub display: Measure<TuiOut>,
|
||||
pub tasks: Vec<Task>,
|
||||
pub mode: Option<Mode>,
|
||||
pub _root: PathBuf,
|
||||
pub entries: Vec<Entry>,
|
||||
pub cursor: usize,
|
||||
pub offset: usize,
|
||||
pub column: usize,
|
||||
pub columns: Columns<fn(&Entry)->Option<Arc<str>>, fn(&mut Self, usize, &str)>,
|
||||
pub display: Measure<TuiOut>,
|
||||
pub tasks: Vec<Task>,
|
||||
/// State of modal dialog of editing field
|
||||
pub mode: Option<Mode>,
|
||||
/// Count of modified items
|
||||
pub modified: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -38,6 +40,7 @@ impl Taggart {
|
|||
columns: Columns::default(),
|
||||
tasks: vec![],
|
||||
entries,
|
||||
modified: 0
|
||||
})
|
||||
}
|
||||
/// Make sure cursor is always in view
|
||||
|
|
@ -58,4 +61,15 @@ impl Taggart {
|
|||
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::*;
|
||||
|
||||
pub struct Column<G, S> {
|
||||
pub title: Arc<str>,
|
||||
pub width: usize,
|
||||
pub getter: G,
|
||||
pub setter: Option<S>,
|
||||
//pub styler: Option<U>,
|
||||
pub title: Arc<str>,
|
||||
pub width: usize,
|
||||
pub collapsed: bool,
|
||||
pub getter: G,
|
||||
pub setter: Option<S>,
|
||||
//pub styler: Option<U>,
|
||||
}
|
||||
|
||||
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(),
|
||||
getter,
|
||||
setter: None,
|
||||
collapsed: false,
|
||||
}
|
||||
}
|
||||
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 (
|
||||
entries: &mut [Entry],
|
||||
index: usize
|
||||
|
|
@ -87,22 +63,52 @@ impl<G, S> Columns<G, S> {
|
|||
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)> {
|
||||
fn default () -> Self {
|
||||
Self(vec![
|
||||
// Computed file hash
|
||||
Column::new(&"Hash", 8, |entry: &Entry|entry.hash()),
|
||||
// File 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(&"Artist", 30, |entry: &Entry|entry.artist())
|
||||
.setter(setter!(set_artist)),
|
||||
Column::new(&"Year", 5, |entry: &Entry|entry.year())
|
||||
.setter(setter!(set_year)),
|
||||
Column::new(&"Release", 30, |entry: &Entry|entry.album())
|
||||
.setter(setter!(set_album)),
|
||||
Column::new(&"Track", 5, |entry: &Entry|entry.track())
|
||||
.setter(setter!(set_track)),
|
||||
Column::new(&"Title", 80, |entry: &Entry|entry.title())
|
||||
.setter(setter!(set_title)),
|
||||
// Modified tags?
|
||||
Column::new(&"~", 1, |entry: &Entry|if entry.is_modified() {
|
||||
Some("~".into())
|
||||
} else {
|
||||
None
|
||||
}),
|
||||
Column::new(&"Artist", 30, |entry: &Entry|entry.artist()).setter(setter!(set_artist)),
|
||||
Column::new(&"Year", 5, |entry: &Entry|entry.year()).setter(setter!(set_year)),
|
||||
Column::new(&"Release", 30, |entry: &Entry|entry.album()).setter(setter!(set_album)),
|
||||
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 std::fs::File;
|
||||
use std::io::{BufReader, Read};
|
||||
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 {
|
||||
/// How many levels deep is this from the working directory
|
||||
|
|
@ -11,6 +20,11 @@ pub struct 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> {
|
||||
let path = path.as_ref();
|
||||
if path.is_dir() {
|
||||
|
|
@ -49,11 +63,6 @@ impl Entry {
|
|||
pub fn is_image (&self) -> bool {
|
||||
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>> {
|
||||
let indent = "".pad_to_width((self.depth - 1) * 2);
|
||||
let icon = self.icon();
|
||||
|
|
@ -71,24 +80,269 @@ impl Entry {
|
|||
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 PartialEq for Entry {
|
||||
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 PartialEq for Entry { 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 {
|
||||
fn partial_cmp (&self, other: &Self) -> Option<Ordering> {
|
||||
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!(
|
||||
" {} 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) {
|
||||
let y = to.area().y();
|
||||
for (column_index, Column {
|
||||
width,
|
||||
getter,
|
||||
..
|
||||
}) in self.0.columns.0.iter().enumerate() {
|
||||
let cols = self.0.columns.0.iter().enumerate();
|
||||
for (column_index, Column { width, collapsed, getter, .. }) in cols {
|
||||
let width = if *collapsed { 1 } else { *width };
|
||||
to.area[0] = *x;
|
||||
if let Some(Mode::Edit { index: edit_index, value }) = self.0.mode.as_ref()
|
||||
&& self.0.column == column_index
|
||||
&& self.0.cursor == cursor
|
||||
{
|
||||
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_bg([*x, y, width as u16, 1], Self::BG_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);
|
||||
Content::render(&TrimStringRef(*width as u16, &value), to);
|
||||
Content::render(&TrimStringRef(width as u16, &value), to);
|
||||
} 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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -86,16 +84,18 @@ impl<'a> TreeTable<'a> {
|
|||
impl<G, S> Columns<G, S> {
|
||||
pub fn header (&self) -> Arc<str> {
|
||||
let mut output = String::new();
|
||||
for Column { width, title, .. } in self.0.iter() {
|
||||
let cell = title.pad_to_width(*width);
|
||||
output = format!("{output}{cell}│");
|
||||
for Column { width, collapsed, title, .. } in self.0.iter() {
|
||||
let width = if *collapsed { 1 } else { *width };
|
||||
let cell = trim_string(width, title).pad_to_width(width);
|
||||
output = format!("{output}{cell}│");
|
||||
}
|
||||
output.into()
|
||||
}
|
||||
pub fn xw (&self, column: usize) -> (u16, u16) {
|
||||
let mut x: u16 = 0;
|
||||
for (index, Column { width, .. }) in self.0.iter().enumerate() {
|
||||
let w = *width as u16 + 1;
|
||||
for (index, Column { width, collapsed, .. }) in self.0.iter().enumerate() {
|
||||
let width = if *collapsed { 1 } else { *width };
|
||||
let w = width as u16 + 1;
|
||||
if index == column {
|
||||
return (x, w)
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue