diff --git a/src/keys.rs b/src/keys.rs index 427944b..2f687e4 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -55,23 +55,3 @@ impl Handle for Taggart { Ok(None) } } - -impl Taggart { - fn clamp (&mut self, min: usize, max: usize) { - if self.cursor < min { - self.offset = self.cursor; - } - if self.cursor > max { - self.offset += self.cursor - max; - } - if self.offset > self.entries.len().saturating_sub(self.display.h()) { - self.offset = self.entries.len().saturating_sub(self.display.h()) - } - if self.cursor >= self.entries.len() { - self.cursor = self.entries.len().saturating_sub(1) - } - if self.column + 1 > self.columns.0.len() { - self.column = self.columns.0.len().saturating_sub(1) - } - } -} diff --git a/src/model.rs b/src/model.rs index 1ae9a73..cca1b67 100644 --- a/src/model.rs +++ b/src/model.rs @@ -47,4 +47,22 @@ impl Taggart { entries, }) } + /// Make sure cursor is always in view + pub(crate) fn clamp (&mut self, min: usize, max: usize) { + if self.cursor < min { + self.offset = self.cursor; + } + if self.cursor > max { + self.offset += self.cursor - max; + } + if self.offset > self.entries.len().saturating_sub(self.display.h()) { + self.offset = self.entries.len().saturating_sub(self.display.h()) + } + if self.cursor >= self.entries.len() { + self.cursor = self.entries.len().saturating_sub(1) + } + if self.column + 1 > self.columns.0.len() { + self.column = self.columns.0.len().saturating_sub(1) + } + } } diff --git a/src/model/entry.rs b/src/model/entry.rs index 4066426..61cdbbd 100644 --- a/src/model/entry.rs +++ b/src/model/entry.rs @@ -86,6 +86,28 @@ impl Entry { pub fn set_track (&self, value: &impl AsRef) -> Option { self.info.write().unwrap().set_track(value) } + 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(); + let name = self.path.iter().last().expect("empty path").display(); + Some(format!("{indent}{icon} {name}").into()) + } + fn icon (&self) -> &'static str { + if self.is_directory() { + Self::ICON_DIRECTORY + } else if self.is_image() { + Self::ICON_IMAGE + } else if self.is_music() { + Self::ICON_MUSIC + } else { + Self::ICON_UNKNOWN + } + } } impl Eq for Entry {} diff --git a/src/model/metadata.rs b/src/model/metadata.rs index d0bae1d..1980c80 100644 --- a/src/model/metadata.rs +++ b/src/model/metadata.rs @@ -54,7 +54,7 @@ impl Metadata { let size = Byte::from_u64(data.len() as u64).get_adjusted_unit(MB); Ok(Self::Music { hash, - size: format!("{:#>8.2}", size).into(), + 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())), diff --git a/src/view.rs b/src/view.rs index 7d39869..ecd37f7 100644 --- a/src/view.rs +++ b/src/view.rs @@ -3,6 +3,8 @@ pub(crate) use tengri::tui::ratatui::style::{Color, Modifier}; pub(crate) use pad::PadStr; mod table; pub use self::table::*; +mod dialog; pub use self::dialog::*; +mod status; pub use self::status::*; impl Content for Taggart { fn content (&self) -> impl Render { @@ -17,142 +19,6 @@ impl Content for Taggart { } fn render (&self, to: &mut TuiOut) { self.content().render(to); - match self.mode { - Some(Mode::Save { value }) => { - to.tint_all( - Color::Rgb(96,96,96), - Color::Rgb(48,48,48), - Modifier::DIM - ); - let options = [ - if value == 0 { "[ Clear changes ]" } else { " Clear changes " }, - if value == 1 { "[ Continue editing ]" } else { " Continue editing " }, - if value == 2 { "[ Write and continue ]" } else { " Write and continue " }, - ]; - let modal = Self::modal(Bsp::s( - format!("Save {} change(s)?", self.tasks.len()), - Bsp::s("", Bsp::e(options[0], Bsp::e(options[1], options[2]))))); - Content::render(&modal, to) - }, - Some(Mode::Quit { value }) => { - to.tint_all( - Color::Rgb(96,96,96), - Color::Rgb(48,48,48), - Modifier::DIM - ); - let options = [ - if value == 0 { "[ Exit without saving ]" } else { " Exit without saving " }, - if value == 1 { "[ Cancel ]" } else { " Cancel " }, - if value == 2 { "[ Write and exit ]" } else { " Write and exit " }, - ]; - let modal = Self::modal(Bsp::s( - format!("Save {} change(s) before exiting?", self.tasks.len()), - Bsp::s("", Bsp::e(options[0], Bsp::e(options[1], options[2]))))); - Content::render(&modal, to) - }, - _ => {}, - } - } -} - -impl Taggart { - pub(crate) const FG_BROWSE: Color = Color::Rgb(255, 192, 0); - pub(crate) const BG_BROWSE: Color = Color::Rgb(0, 0, 0); - pub(crate) const BG_EDIT: Color = Color::Rgb(48, 128, 0); - pub(crate) const FG_EDIT: Color = Color::Rgb(255, 255, 255); - pub(crate) const BG_SAVE: Color = Color::Rgb(192, 96, 0); - pub(crate) const FG_SAVE: Color = Color::Rgb(255, 255, 255); - pub(crate) const BG_QUIT: Color = Color::Rgb(128, 0, 0); - pub(crate) const FG_QUIT: Color = Color::Rgb(255, 255, 255); - pub(crate) const FG_MODAL: Color = Color::Rgb(255, 255, 255); - pub(crate) const BG_MODAL: Color = Color::Rgb(0, 0, 0); - fn modal (content: impl Content) -> impl Content { - let pos = |x|Fill::xy( Align::c(x)); - let style = |x|Tui::modify(false, Modifier::DIM, Tui::fg_bg(Self::FG_MODAL, Self::BG_MODAL, x)); - let border = |x|Margin::xy(1, 1, Bsp::b(Border(true, Lozenge(true, Default::default())), x)); - let bg = |x|Bsp::a(x, Repeat(" ")); - pos(style(border(bg(content)))) - } - fn title_bar (&self) -> impl Content { - status_bar( - Color::Rgb(0, 0, 0), - Color::Rgb(192, 192, 192), - Align::w(self.columns.header()) - ) - } - fn value_bar (&self) -> impl Content { - status_bar( - Color::Rgb(192, 192, 192), - Color::Rgb(0, 0, 0), - Fill::x( - Bsp::a( - Fill::x(Align::w(format!( - " {:>03}/{:>03} {}", - self.cursor + 1, - self.entries.len(), - (self.columns.0[self.column].getter)(&self.entries[self.cursor]) - .map(|value|format!("{}: {}", self.columns.0[self.column].title, value.trim())) - .unwrap_or(String::default()) - ))), - Fill::x(Align::e(format!( - " {} unsaved changes ", - self.tasks.len() - ))) - ) - ) - ) - } - fn mode_bar (&self, size: String) -> impl Content { - let mode = match self.mode { - Some(Mode::Save { .. }) => Tui::bg(Self::BG_SAVE, Tui::fg(Self::FG_SAVE, " SAVE ")), - Some(Mode::Quit { .. }) => Tui::bg(Self::BG_QUIT, Tui::fg(Self::FG_QUIT, " QUIT ")), - Some(Mode::Edit { .. }) => Tui::bg(Self::BG_EDIT, Tui::fg(Self::FG_EDIT, " EDIT ")), - _ => Tui::bg(Self::BG_BROWSE, Tui::fg(Self::FG_BROWSE, " BROWSE ")) - }; - let help = match self.mode { - Some(Mode::Save { .. }) => " Esc: cancel, Arrows: select, Enter: confirm", - Some(Mode::Quit { .. }) => " Esc: cancel, Arrows: select, Enter: confirm", - Some(Mode::Edit { .. }) => " Esc: cancel, Enter: set value", - _ => " Q: exit, W: save, Arrows: select, Space: open, Enter: edit", - }; - status_bar( - Color::Rgb(0, 0, 0), - Color::Rgb(192, 192, 192), - Fill::x(Bsp::a( - Fill::x(Align::w(Tui::bold(true, Bsp::e(mode, help)))), - Fill::x(Align::e(size)), - )) - ) - } -} - -pub fn status_bar ( - fg: Color, bg: Color, content: impl Content -) -> impl Content { - Fixed::y(1, Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, content)))) -} - -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 name (&self) -> Option> { - let indent = "".pad_to_width((self.depth - 1) * 2); - let icon = self.icon(); - let name = self.path.iter().last().expect("empty path").display(); - Some(format!("{indent}{icon} {name}").into()) - } - fn icon (&self) -> &'static str { - if self.is_directory() { - Self::ICON_DIRECTORY - } else if self.is_image() { - Self::ICON_IMAGE - } else if self.is_music() { - Self::ICON_MUSIC - } else { - Self::ICON_UNKNOWN - } + self.render_dialog(to) } } diff --git a/src/view/dialog.rs b/src/view/dialog.rs new file mode 100644 index 0000000..4ddb3be --- /dev/null +++ b/src/view/dialog.rs @@ -0,0 +1,50 @@ +use crate::*; + +impl Taggart { + pub(crate) const FG_MODAL: Color = Color::Rgb(255, 255, 255); + pub(crate) const BG_MODAL: Color = Color::Rgb(0, 0, 0); + fn dialog (content: impl Content) -> impl Content { + let pos = |x|Fill::xy( Align::c(x)); + let style = |x|Tui::modify(false, Modifier::DIM, Tui::fg_bg(Self::FG_MODAL, Self::BG_MODAL, x)); + let border = |x|Margin::xy(1, 1, Bsp::b(Border(true, Lozenge(true, Default::default())), x)); + let bg = |x|Bsp::a(x, Repeat(" ")); + pos(style(border(bg(content)))) + } + fn dialog_help (&self) { + } + fn dialog_save (&self, value: u8) { + let choices = [ + if value == 0 { "[ Clear changes ]" } else { " Clear changes " }, + if value == 1 { "[ Continue editing ]" } else { " Continue editing " }, + if value == 2 { "[ Write and continue ]" } else { " Write and continue " }, + ]; + Self::dialog(Bsp::s( + format!("Save {} change(s)?", self.tasks.len()), + Bsp::s("", Bsp::e(choices[0], Bsp::e(choices[1], choices[2]))))); + } + fn dialog_quit (&self, value: u8) { + let choices = [ + if value == 0 { "[ Exit without saving ]" } else { " Exit without saving " }, + if value == 1 { "[ Cancel ]" } else { " Cancel " }, + if value == 2 { "[ Write and exit ]" } else { " Write and exit " }, + ]; + Self::dialog(Bsp::s( + format!("Save {} change(s) before exiting?", self.tasks.len()), + Bsp::s("", Bsp::e(choices[0], Bsp::e(choices[1], choices[2]))))); + } + pub fn render_dialog (&self, to: &mut TuiOut) { + match self.mode { + Some(Mode::Save { value }) => { + to.tint_all(Color::Rgb(96,96,96), Color::Rgb(48,48,48), Modifier::DIM); + Content::render(&self.dialog_save(value), to) + }, + Some(Mode::Quit { value }) => { + to.tint_all(Color::Rgb(96,96,96), Color::Rgb(48,48,48), Modifier::DIM); + Content::render(&self.dialog_quit(value), to) + }, + Some(Mode::Help { .. }) => { + }, + _ => {}, + } + } +} diff --git a/src/view/status.rs b/src/view/status.rs new file mode 100644 index 0000000..b680c40 --- /dev/null +++ b/src/view/status.rs @@ -0,0 +1,69 @@ +use crate::*; + +impl Taggart { + pub(crate) const FG_BROWSE: Color = Color::Rgb(255, 192, 0); + pub(crate) const BG_BROWSE: Color = Color::Rgb(0, 0, 0); + pub(crate) const BG_EDIT: Color = Color::Rgb(48, 128, 0); + pub(crate) const FG_EDIT: Color = Color::Rgb(255, 255, 255); + pub(crate) const BG_SAVE: Color = Color::Rgb(192, 96, 0); + pub(crate) const FG_SAVE: Color = Color::Rgb(255, 255, 255); + pub(crate) const BG_QUIT: Color = Color::Rgb(128, 0, 0); + pub(crate) const FG_QUIT: Color = Color::Rgb(255, 255, 255); + pub(crate) fn title_bar (&self) -> impl Content { + status_bar( + Color::Rgb(0, 0, 0), + Color::Rgb(192, 192, 192), + Align::w(self.columns.header()) + ) + } + pub(crate) fn value_bar (&self) -> impl Content { + status_bar( + Color::Rgb(192, 192, 192), + Color::Rgb(0, 0, 0), + Fill::x( + Bsp::a( + Fill::x(Align::w(format!( + " {:>03}/{:>03} {}", + self.cursor + 1, + self.entries.len(), + (self.columns.0[self.column].getter)(&self.entries[self.cursor]) + .map(|value|format!("{}: {}", self.columns.0[self.column].title, value.trim())) + .unwrap_or(String::default()) + ))), + Fill::x(Align::e(format!( + " {} unsaved changes ", + self.tasks.len() + ))) + ) + ) + ) + } + pub(crate) fn mode_bar (&self, size: String) -> impl Content { + let mode = match self.mode { + Some(Mode::Save { .. }) => Tui::bg(Self::BG_SAVE, Tui::fg(Self::FG_SAVE, " SAVE ")), + Some(Mode::Quit { .. }) => Tui::bg(Self::BG_QUIT, Tui::fg(Self::FG_QUIT, " QUIT ")), + Some(Mode::Edit { .. }) => Tui::bg(Self::BG_EDIT, Tui::fg(Self::FG_EDIT, " EDIT ")), + _ => Tui::bg(Self::BG_BROWSE, Tui::fg(Self::FG_BROWSE, " BROWSE ")) + }; + let help = match self.mode { + Some(Mode::Save { .. }) => " Esc: cancel, Arrows: select, Enter: confirm", + Some(Mode::Quit { .. }) => " Esc: cancel, Arrows: select, Enter: confirm", + Some(Mode::Edit { .. }) => " Esc: cancel, Enter: set value", + _ => " Q: exit, W: save, Arrows: select, Space: open, Enter: edit", + }; + status_bar( + Color::Rgb(0, 0, 0), + Color::Rgb(192, 192, 192), + Fill::x(Bsp::a( + Fill::x(Align::w(Tui::bold(true, Bsp::e(mode, help)))), + Fill::x(Align::e(size)), + )) + ) + } +} + +pub fn status_bar ( + fg: Color, bg: Color, content: impl Content +) -> impl Content { + Fixed::y(1, Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, content)))) +}