From 4de94beafbbfe4a165e5c1863789482cc42c4420 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 6 Apr 2025 19:09:44 +0300 Subject: [PATCH] better column resizing --- Cargo.lock | 16 +-- Cargo.toml | 2 +- src/keys.rs | 37 +++++- src/model.rs | 40 ++++-- src/model/column.rs | 88 +++++++------ src/model/entry.rs | 292 +++++++++++++++++++++++++++++++++++++++--- src/model/metadata.rs | 253 ------------------------------------ src/view/status.rs | 2 +- src/view/table.rs | 32 ++--- 9 files changed, 403 insertions(+), 359 deletions(-) delete mode 100644 src/model/metadata.rs diff --git a/Cargo.lock b/Cargo.lock index 27dc4df..f7b384a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index ccd002d..5a3ac19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" ] } diff --git a/src/keys.rs b/src/keys.rs index ccbee6c..4733618 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -43,13 +43,14 @@ impl Handle 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 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; + } +} diff --git a/src/model.rs b/src/model.rs index e1c695a..e9abd41 100644 --- a/src/model.rs +++ b/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, - pub cursor: usize, - pub offset: usize, - pub column: usize, - pub columns: ColumnsOption>, fn(&mut Self, usize, &str)>, - pub display: Measure, - pub tasks: Vec, - pub mode: Option, + pub _root: PathBuf, + pub entries: Vec, + pub cursor: usize, + pub offset: usize, + pub column: usize, + pub columns: ColumnsOption>, fn(&mut Self, usize, &str)>, + pub display: Measure, + pub tasks: Vec, + /// State of modal dialog of editing field + pub mode: Option, + /// 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 + } } diff --git a/src/model/column.rs b/src/model/column.rs index fb2e3a6..1cfc5fb 100644 --- a/src/model/column.rs +++ b/src/model/column.rs @@ -1,11 +1,12 @@ use crate::*; pub struct Column { - pub title: Arc, - pub width: usize, - pub getter: G, - pub setter: Option, - //pub styler: Option, + pub title: Arc, + pub width: usize, + pub collapsed: bool, + pub getter: G, + pub setter: Option, + //pub styler: Option, } type Getter = fn(&T)->Option>; @@ -22,6 +23,7 @@ impl Column, Setter> { title: title.as_ref().into(), getter, setter: None, + collapsed: false, } } fn setter (mut self, setter: Setter) -> Self { @@ -30,32 +32,6 @@ impl Column, Setter> { } } -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 Columns { 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 ColumnsOption>, 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)), ]) } } diff --git a/src/model/entry.rs b/src/model/entry.rs index dd1013f..b11a14a 100644 --- a/src/model/entry.rs +++ b/src/model/entry.rs @@ -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: &impl AsRef, depth: usize) -> Perhaps { 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> { 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 { 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, + size: Arc, + original_tag: Option>, + modified_tag: Option>>, + }, + Image { + invalid: bool, + hash: Arc, + size: Arc, + title: Option, + author: Option, + }, + Unknown { + hash: Arc, + size: Arc, + } +} + +impl Metadata { + pub fn new (path: &Path, strict: bool) -> Usually { + 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 { + 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> { + $self.info.read().unwrap().$get() + } + } + impl Metadata { + pub fn $get (&$self) -> Option> { + $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> { + self.info.read().unwrap().$get() + } + pub fn $set (&mut self, value: &impl AsRef) -> bool { + self.info.write().unwrap().$set(value) + } + } + impl Metadata { + pub fn $get (&self) -> Option> { + 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) -> 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> { + self.info.read().unwrap().$get() + } + pub fn $set (&mut self, value: &impl AsRef) -> bool { + self.info.write().unwrap().$set(value) + } + } + impl Metadata { + pub fn $get (&self) -> Option> { + 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) -> bool { + let value = value.as_ref().trim(); + if let &mut Metadata::Music { ref mut modified_tag, .. } = self + && let Ok(numeric_value) = value.parse::() + { + 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); diff --git a/src/model/metadata.rs b/src/model/metadata.rs deleted file mode 100644 index fbe3b67..0000000 --- a/src/model/metadata.rs +++ /dev/null @@ -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, - size: Arc, - original_tag: Option>, - modified_tag: Option>>, - }, - Image { - invalid: bool, - hash: Arc, - size: Arc, - title: Option, - author: Option, - }, - Unknown { - hash: Arc, - size: Arc, - } -} - -impl Metadata { - pub fn new (path: &Path, strict: bool) -> Usually { - 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 { - 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> { - $self.info.read().unwrap().$get() - } - } - impl Metadata { - pub fn $get (&$self) -> Option> { - $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> { - self.info.read().unwrap().$get() - } - pub fn $set (&mut self, value: &impl AsRef) -> Option { - self.info.write().unwrap().$set(value) - } - } - impl Metadata { - pub fn $get (&self) -> Option> { - 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) -> Option { - 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> { - self.info.read().unwrap().$get() - } - pub fn $set (&mut self, value: &impl AsRef) -> Option { - self.info.write().unwrap().$set(value) - } - } - impl Metadata { - pub fn $get (&self) -> Option> { - 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) -> Option { - let value = value.as_ref().trim(); - if let &mut Metadata::Music { ref mut modified_tag, .. } = self - && let Ok(numeric_value) = value.parse::() - { - 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); diff --git a/src/view/status.rs b/src/view/status.rs index b680c40..67cc834 100644 --- a/src/view/status.rs +++ b/src/view/status.rs @@ -32,7 +32,7 @@ impl Taggart { ))), Fill::x(Align::e(format!( " {} unsaved changes ", - self.tasks.len() + self.modified ))) ) ) diff --git a/src/view/table.rs b/src/view/table.rs index 882d5b0..1b477c9 100644 --- a/src/view/table.rs +++ b/src/view/table.rs @@ -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 Columns { pub fn header (&self) -> Arc { 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 {