mirror of
https://codeberg.org/unspeaker/perch.git
synced 2025-12-06 01:26:43 +01:00
factor out modules; modal -> dialog
This commit is contained in:
parent
c075871e50
commit
b157a87647
7 changed files with 163 additions and 158 deletions
20
src/keys.rs
20
src/keys.rs
|
|
@ -55,23 +55,3 @@ impl Handle<TuiIn> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
src/model.rs
18
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,6 +86,28 @@ impl Entry {
|
|||
pub fn set_track (&self, value: &impl AsRef<str>) -> Option<TagItem> {
|
||||
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<Arc<str>> {
|
||||
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 {}
|
||||
|
|
|
|||
|
|
@ -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())),
|
||||
|
|
|
|||
140
src/view.rs
140
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<TuiOut> for Taggart {
|
||||
fn content (&self) -> impl Render<TuiOut> {
|
||||
|
|
@ -17,142 +19,6 @@ impl Content<TuiOut> 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<TuiOut>) -> impl Content<TuiOut> {
|
||||
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<TuiOut> {
|
||||
status_bar(
|
||||
Color::Rgb(0, 0, 0),
|
||||
Color::Rgb(192, 192, 192),
|
||||
Align::w(self.columns.header())
|
||||
)
|
||||
}
|
||||
fn value_bar (&self) -> impl Content<TuiOut> {
|
||||
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<TuiOut> {
|
||||
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<TuiOut>
|
||||
) -> impl Content<TuiOut> {
|
||||
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<Arc<str>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
50
src/view/dialog.rs
Normal file
50
src/view/dialog.rs
Normal file
|
|
@ -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<TuiOut>) -> impl Content<TuiOut> {
|
||||
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 { .. }) => {
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
69
src/view/status.rs
Normal file
69
src/view/status.rs
Normal file
|
|
@ -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<TuiOut> {
|
||||
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<TuiOut> {
|
||||
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<TuiOut> {
|
||||
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<TuiOut>
|
||||
) -> impl Content<TuiOut> {
|
||||
Fixed::y(1, Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, content))))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue