better column resizing

This commit is contained in:
🪞👃🪞 2025-04-06 19:09:44 +03:00
parent 44a2108585
commit 4de94beafb
9 changed files with 403 additions and 359 deletions

16
Cargo.lock generated
View file

@ -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",

View file

@ -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" ] }

View file

@ -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;
}
}

View file

@ -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
}
} }

View file

@ -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)),
]) ])
} }
} }

View file

@ -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);

View file

@ -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);

View file

@ -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
))) )))
) )
) )

View file

@ -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 {