mirror of
https://codeberg.org/unspeaker/perch.git
synced 2025-12-06 09:36:42 +01:00
dynamic columns; macro keydefs
This commit is contained in:
parent
b29511c23e
commit
8cc9418272
6 changed files with 113 additions and 147 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -991,6 +991,7 @@ dependencies = [
|
||||||
"moku",
|
"moku",
|
||||||
"pad",
|
"pad",
|
||||||
"tek_tui",
|
"tek_tui",
|
||||||
|
"unicode-width 0.2.0",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"xxhash-rust",
|
"xxhash-rust",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,4 @@ pad = "0.1"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
xxhash-rust = { version = "0.8.5", features = ["xxh3"] }
|
xxhash-rust = { version = "0.8.5", features = ["xxh3"] }
|
||||||
#base64 = "0.22"
|
#base64 = "0.22"
|
||||||
|
unicode-width = "0.2"
|
||||||
|
|
|
||||||
95
src/keys.rs
95
src/keys.rs
|
|
@ -1,82 +1,29 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
macro_rules! press {
|
||||||
|
($key:tt) => {
|
||||||
|
Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::$key,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
state: KeyEventState::NONE
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Handle<TuiIn> for Taggart {
|
impl Handle<TuiIn> for Taggart {
|
||||||
fn handle (&mut self, input: &TuiIn) -> Perhaps<bool> {
|
fn handle (&mut self, input: &TuiIn) -> Perhaps<bool> {
|
||||||
let x_min = self.offset;
|
let x_min = self.offset;
|
||||||
let x_max = self.offset + self.size.h().saturating_sub(1);
|
let x_max = self.offset + self.size.h().saturating_sub(1);
|
||||||
match &*input.event() {
|
match &*input.event() {
|
||||||
Event::Key(KeyEvent {
|
press!(Up) => { self.cursor = self.cursor.saturating_sub(1); },
|
||||||
code: KeyCode::Up,
|
press!(Down) => { self.cursor = self.cursor + 1; },
|
||||||
kind: KeyEventKind::Press,
|
press!(PageUp) => { self.cursor = self.cursor.saturating_sub(PAGE_SIZE); },
|
||||||
modifiers: KeyModifiers::NONE,
|
press!(PageDown) => { self.cursor += PAGE_SIZE; },
|
||||||
state: KeyEventState::NONE
|
press!(Left) => { self.column = self.column.saturating_sub(1); },
|
||||||
}) => {
|
press!(Right) => { self.column = self.column + 1; },
|
||||||
self.cursor = self.cursor.saturating_sub(1);
|
press!(Enter) => { self.editing = Some((self.cursor, self.column)); },
|
||||||
},
|
press!(Esc) => { self.editing = None; },
|
||||||
Event::Key(KeyEvent {
|
|
||||||
code: KeyCode::Down,
|
|
||||||
kind: KeyEventKind::Press,
|
|
||||||
modifiers: KeyModifiers::NONE,
|
|
||||||
state: KeyEventState::NONE
|
|
||||||
}) => {
|
|
||||||
self.cursor = self.cursor + 1;
|
|
||||||
},
|
|
||||||
Event::Key(KeyEvent {
|
|
||||||
code: KeyCode::PageUp,
|
|
||||||
kind: KeyEventKind::Press,
|
|
||||||
modifiers: KeyModifiers::NONE,
|
|
||||||
state: KeyEventState::NONE
|
|
||||||
}) => {
|
|
||||||
self.cursor = self.cursor.saturating_sub(PAGE_SIZE);
|
|
||||||
},
|
|
||||||
Event::Key(KeyEvent {
|
|
||||||
code: KeyCode::PageDown,
|
|
||||||
kind: KeyEventKind::Press,
|
|
||||||
modifiers: KeyModifiers::NONE,
|
|
||||||
state: KeyEventState::NONE
|
|
||||||
}) => {
|
|
||||||
self.cursor += PAGE_SIZE;
|
|
||||||
},
|
|
||||||
Event::Key(KeyEvent {
|
|
||||||
code: KeyCode::Left,
|
|
||||||
kind: KeyEventKind::Press,
|
|
||||||
modifiers: KeyModifiers::NONE,
|
|
||||||
state: KeyEventState::NONE
|
|
||||||
}) => {
|
|
||||||
self.column = self.column.saturating_sub(1);
|
|
||||||
},
|
|
||||||
Event::Key(KeyEvent {
|
|
||||||
code: KeyCode::Right,
|
|
||||||
kind: KeyEventKind::Press,
|
|
||||||
modifiers: KeyModifiers::NONE,
|
|
||||||
state: KeyEventState::NONE
|
|
||||||
}) => {
|
|
||||||
self.column = self.column + 1;
|
|
||||||
},
|
|
||||||
Event::Key(KeyEvent {
|
|
||||||
code: KeyCode::Enter,
|
|
||||||
kind: KeyEventKind::Press,
|
|
||||||
modifiers: KeyModifiers::NONE,
|
|
||||||
state: KeyEventState::NONE
|
|
||||||
}) => {
|
|
||||||
self.editing = Some((self.cursor, self.column));
|
|
||||||
},
|
|
||||||
Event::Key(KeyEvent {
|
|
||||||
code: KeyCode::Esc,
|
|
||||||
kind: KeyEventKind::Press,
|
|
||||||
modifiers: KeyModifiers::NONE,
|
|
||||||
state: KeyEventState::NONE
|
|
||||||
}) => {
|
|
||||||
self.editing = None;
|
|
||||||
},
|
|
||||||
Event::Key(KeyEvent {
|
|
||||||
code: KeyCode::Tab,
|
|
||||||
kind: KeyEventKind::Press,
|
|
||||||
modifiers: KeyModifiers::NONE,
|
|
||||||
state: KeyEventState::NONE
|
|
||||||
}) => {
|
|
||||||
self.show_hash = !self.show_hash;
|
|
||||||
},
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
if self.cursor < x_min {
|
if self.cursor < x_min {
|
||||||
|
|
@ -88,8 +35,8 @@ impl Handle<TuiIn> for Taggart {
|
||||||
if self.cursor >= self.paths.len() {
|
if self.cursor >= self.paths.len() {
|
||||||
self.cursor = self.paths.len().saturating_sub(1)
|
self.cursor = self.paths.len().saturating_sub(1)
|
||||||
}
|
}
|
||||||
if self.column + 1 > COLUMN_COUNT {
|
if self.column + 1 > self.columns.0.len() {
|
||||||
self.column = COLUMN_COUNT.saturating_sub(1)
|
self.column = self.columns.0.len().saturating_sub(1)
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
src/main.rs
20
src/main.rs
|
|
@ -16,15 +16,13 @@ use xxhash_rust::xxh3::xxh3_64;
|
||||||
use file_type::FileType;
|
use file_type::FileType;
|
||||||
|
|
||||||
mod keys;
|
mod keys;
|
||||||
mod view;
|
mod view; use self::view::*;
|
||||||
mod model; pub(crate) use self::model::*;
|
mod model; pub(crate) use self::model::*;
|
||||||
|
|
||||||
pub(crate) type Usually<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
pub(crate) type Usually<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||||
pub(crate) type Perhaps<T> = Usually<Option<T>>;
|
pub(crate) type Perhaps<T> = Usually<Option<T>>;
|
||||||
|
|
||||||
pub(crate) const PAGE_SIZE: usize = 10;
|
pub(crate) const PAGE_SIZE: usize = 10;
|
||||||
pub(crate) const COLUMN_COUNT: usize = 6;
|
|
||||||
pub(crate) const COLUMN_WIDTHS: [u16; COLUMN_COUNT] = [16, 60, 20, 20, 5, 20];
|
|
||||||
|
|
||||||
fn cli () -> clap::Command {
|
fn cli () -> clap::Command {
|
||||||
command!()
|
command!()
|
||||||
|
|
@ -38,7 +36,19 @@ fn main () -> Usually<()> {
|
||||||
Tui::new()?.run(&state)
|
Tui::new()?.run(&state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Taggart {
|
||||||
|
pub root: PathBuf,
|
||||||
|
pub paths: Vec<Entry>,
|
||||||
|
pub cursor: usize,
|
||||||
|
pub offset: usize,
|
||||||
|
pub column: usize,
|
||||||
|
pub columns: Columns<Entry>,
|
||||||
|
pub size: Measure<TuiOut>,
|
||||||
|
pub editing: Option<(usize, usize)>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Taggart {
|
impl Taggart {
|
||||||
|
|
||||||
fn new (root: Option<&impl AsRef<Path>>) -> Usually<Self> {
|
fn new (root: Option<&impl AsRef<Path>>) -> Usually<Self> {
|
||||||
let root = if let Some(root) = root {
|
let root = if let Some(root) = root {
|
||||||
root.as_ref().into()
|
root.as_ref().into()
|
||||||
|
|
@ -53,9 +63,10 @@ impl Taggart {
|
||||||
column: 0,
|
column: 0,
|
||||||
size: Measure::new(),
|
size: Measure::new(),
|
||||||
editing: None,
|
editing: None,
|
||||||
show_hash: false,
|
columns: Columns::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect (root: &impl AsRef<Path>) -> Usually<Vec<Entry>> {
|
fn collect (root: &impl AsRef<Path>) -> Usually<Vec<Entry>> {
|
||||||
let mut paths = vec![];
|
let mut paths = vec![];
|
||||||
for entry in WalkDir::new(&root).into_iter()
|
for entry in WalkDir::new(&root).into_iter()
|
||||||
|
|
@ -72,4 +83,5 @@ impl Taggart {
|
||||||
paths.sort();
|
paths.sort();
|
||||||
Ok(paths)
|
Ok(paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
29
src/model.rs
29
src/model.rs
|
|
@ -3,17 +3,6 @@ use walkdir::DirEntry;
|
||||||
use id3::{Tag, TagLike};
|
use id3::{Tag, TagLike};
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
pub struct Taggart {
|
|
||||||
pub root: PathBuf,
|
|
||||||
pub paths: Vec<Entry>,
|
|
||||||
pub cursor: usize,
|
|
||||||
pub offset: usize,
|
|
||||||
pub column: usize,
|
|
||||||
pub size: Measure<TuiOut>,
|
|
||||||
pub editing: Option<(usize, usize)>,
|
|
||||||
pub show_hash: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Ord, Eq, PartialEq, PartialOrd)]
|
#[derive(Ord, Eq, PartialEq, PartialOrd)]
|
||||||
pub struct Entry {
|
pub struct Entry {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
|
@ -54,7 +43,6 @@ pub enum EntryInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entry {
|
impl Entry {
|
||||||
|
|
||||||
pub fn new (root: &impl AsRef<Path>, entry: &DirEntry) -> Perhaps<Self> {
|
pub fn new (root: &impl AsRef<Path>, entry: &DirEntry) -> Perhaps<Self> {
|
||||||
if entry.path().is_dir() {
|
if entry.path().is_dir() {
|
||||||
Self::new_dir(root, entry)
|
Self::new_dir(root, entry)
|
||||||
|
|
@ -64,11 +52,10 @@ impl Entry {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_dir (root: &impl AsRef<Path>, entry: &DirEntry) -> Perhaps<Self> {
|
fn new_dir (root: &impl AsRef<Path>, entry: &DirEntry) -> Perhaps<Self> {
|
||||||
Ok(Some(Self {
|
Ok(Some(Self {
|
||||||
depth: entry.depth(),
|
depth: entry.depth(),
|
||||||
path: entry.path().into(),
|
path: entry.path().strip_prefix(root.as_ref())?.into(),
|
||||||
info: EntryInfo::Directory {
|
info: EntryInfo::Directory {
|
||||||
hash_file: None,
|
hash_file: None,
|
||||||
catalog_file: None,
|
catalog_file: None,
|
||||||
|
|
@ -77,7 +64,6 @@ impl Entry {
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_file (root: &impl AsRef<Path>, entry: &DirEntry) -> Perhaps<Self> {
|
fn new_file (root: &impl AsRef<Path>, entry: &DirEntry) -> Perhaps<Self> {
|
||||||
let bytes = read(entry.path())?;
|
let bytes = read(entry.path())?;
|
||||||
let hash = hex::encode(xxh3_64(&bytes).to_be_bytes());
|
let hash = hex::encode(xxh3_64(&bytes).to_be_bytes());
|
||||||
|
|
@ -85,7 +71,7 @@ impl Entry {
|
||||||
let mime_type = file_type.media_types().get(0);
|
let mime_type = file_type.media_types().get(0);
|
||||||
return Ok(Some(Self {
|
return Ok(Some(Self {
|
||||||
depth: entry.depth(),
|
depth: entry.depth(),
|
||||||
path: entry.path().into(),
|
path: entry.path().strip_prefix(root.as_ref())?.into(),
|
||||||
info: match mime_type {
|
info: match mime_type {
|
||||||
Some(&"audio/mpeg3") => {
|
Some(&"audio/mpeg3") => {
|
||||||
let id3 = Tag::read_from_path(entry.path())?;
|
let id3 = Tag::read_from_path(entry.path())?;
|
||||||
|
|
@ -123,23 +109,18 @@ impl Entry {
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn short_path (&self, root: &impl AsRef<Path>) -> Usually<&Path> {
|
pub fn short_path (&self, root: &impl AsRef<Path>) -> Usually<&Path> {
|
||||||
Ok(self.path.strip_prefix(root.as_ref())?)
|
Ok(self.path.strip_prefix(root.as_ref())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_dir (&self) -> bool {
|
pub fn is_dir (&self) -> bool {
|
||||||
matches!(self.info, EntryInfo::Directory { .. })
|
matches!(self.info, EntryInfo::Directory { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_mus (&self) -> bool {
|
pub fn is_mus (&self) -> bool {
|
||||||
matches!(self.info, EntryInfo::Music { .. })
|
matches!(self.info, EntryInfo::Music { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_img (&self) -> bool {
|
pub fn is_img (&self) -> bool {
|
||||||
matches!(self.info, EntryInfo::Image { .. })
|
matches!(self.info, EntryInfo::Image { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hash (&self) -> Option<Arc<str>> {
|
pub fn hash (&self) -> Option<Arc<str>> {
|
||||||
match self.info {
|
match self.info {
|
||||||
EntryInfo::Image { ref hash, .. } => Some(hash.clone()),
|
EntryInfo::Image { ref hash, .. } => Some(hash.clone()),
|
||||||
|
|
@ -165,4 +146,10 @@ impl Entry {
|
||||||
_ => None
|
_ => None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn track (&self) -> Option<Arc<str>> {
|
||||||
|
match self.info {
|
||||||
|
EntryInfo::Music { ref track, .. } => track.map(|t|format!("{t}").into()).clone(),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
106
src/view.rs
106
src/view.rs
|
|
@ -3,21 +3,63 @@ use tek_tui::ratatui::{style::{Color, Style}, prelude::Stylize};
|
||||||
use pad::PadStr;
|
use pad::PadStr;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
pub struct Column<T> {
|
||||||
|
title: Arc<str>,
|
||||||
|
width: usize,
|
||||||
|
value: Box<dyn Fn(&T)->Option<Arc<str>> + Send + Sync>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Column<T> {
|
||||||
|
pub fn new (
|
||||||
|
title: &impl AsRef<str>,
|
||||||
|
width: usize,
|
||||||
|
value: impl Fn(&T)->Option<Arc<str>> + Send + Sync + 'static
|
||||||
|
) -> Self {
|
||||||
|
Self { width, value: Box::new(value), title: title.as_ref().into() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Columns<T>(pub Vec<Column<T>>);
|
||||||
|
|
||||||
|
impl Default for Columns<Entry> {
|
||||||
|
fn default () -> Self {
|
||||||
|
Self(vec![
|
||||||
|
Column::new(&"HASH", 16, |entry: &Entry|entry.hash()),
|
||||||
|
Column::new(&"FILE", 80, |entry: &Entry|entry.name()),
|
||||||
|
Column::new(&"ARTIST", 30, |entry: &Entry|entry.artist()),
|
||||||
|
Column::new(&"RELEASE", 30, |entry: &Entry|entry.album()),
|
||||||
|
Column::new(&"TRACK", 5, |entry: &Entry|entry.track()),
|
||||||
|
Column::new(&"TITLE", 80, |entry: &Entry|entry.title()),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Columns<T> {
|
||||||
|
pub fn header (&self) -> Arc<str> {
|
||||||
|
let mut output = String::new();
|
||||||
|
for Column { width, value, title } in self.0.iter() {
|
||||||
|
let cell = title.pad_to_width(*width);
|
||||||
|
output = format!("{output}{cell}│");
|
||||||
|
}
|
||||||
|
output.into()
|
||||||
|
}
|
||||||
|
pub fn row (&self, entry: &T) -> Arc<str> {
|
||||||
|
let mut output = String::new();
|
||||||
|
for Column { width, value, .. } in self.0.iter() {
|
||||||
|
let cell = value(entry).unwrap_or_default().pad_to_width(*width);
|
||||||
|
output = format!("{output}{cell}│");
|
||||||
|
}
|
||||||
|
output.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Content<TuiOut> for Taggart {
|
impl Content<TuiOut> for Taggart {
|
||||||
fn content (&self) -> impl Render<TuiOut> {
|
fn content (&self) -> impl Render<TuiOut> {
|
||||||
let sizer = Fill::xy(&self.size);
|
let sizer = Fill::xy(&self.size);
|
||||||
let size = format!("{}x{}", self.size.w(), self.size.h());
|
let size = format!("{}x{}", self.size.w(), self.size.h());
|
||||||
let size_bar = status_bar(Align::e(size));
|
let size_bar = status_bar(Align::e(size));
|
||||||
let titlebar = status_bar(Align::w(table_row(
|
let titlebar = status_bar(Align::w(self.columns.header()));
|
||||||
Some("HASH".into()),
|
Bsp::n(size_bar, Bsp::s(titlebar, Bsp::b(sizer, Fill::xy(TreeTable(self)))))
|
||||||
"FILE",
|
|
||||||
Some("ARTIST".into()),
|
|
||||||
Some("RELEASE".into()),
|
|
||||||
"TRACK",
|
|
||||||
Some("TITLE".into())
|
|
||||||
)));
|
|
||||||
let table = Fill::xy(TreeTable(self));
|
|
||||||
Bsp::n(size_bar, Bsp::s(titlebar, Bsp::b(sizer, table)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,8 +70,8 @@ impl<'a> Content<TuiOut> for TreeTable<'a> {
|
||||||
let area = to.area();
|
let area = to.area();
|
||||||
let Taggart { offset, paths, cursor, column, .. } = self.0;
|
let Taggart { offset, paths, cursor, column, .. } = self.0;
|
||||||
let mut x = 0;
|
let mut x = 0;
|
||||||
for (index, _width) in COLUMN_WIDTHS.iter().enumerate() {
|
for (index, Column { width, .. }) in self.0.columns.0.iter().enumerate() {
|
||||||
let w = COLUMN_WIDTHS[index] + 1;
|
let w = *width as u16 + 1;
|
||||||
if index == *column {
|
if index == *column {
|
||||||
to.fill_bg([area.x() + x, area.y(), w, area.h()], Color::Rgb(0, 0, 0));
|
to.fill_bg([area.x() + x, area.y(), w, area.h()], Color::Rgb(0, 0, 0));
|
||||||
break
|
break
|
||||||
|
|
@ -44,10 +86,8 @@ impl<'a> Content<TuiOut> for TreeTable<'a> {
|
||||||
for (index, fragment) in entry.path.iter().enumerate() {
|
for (index, fragment) in entry.path.iter().enumerate() {
|
||||||
if index == entry.depth - 1 {
|
if index == entry.depth - 1 {
|
||||||
let cursor = if selected { ">" } else { " " };
|
let cursor = if selected { ">" } else { " " };
|
||||||
let icon = entry.icon();
|
let label = self.0.columns.row(&entry);
|
||||||
let style = entry.style();
|
to.blit(&label, area.x(), y, entry.style());
|
||||||
let label = entry.label(icon, &fragment.display());
|
|
||||||
to.blit(&label, area.x(), y, style);
|
|
||||||
if selected {
|
if selected {
|
||||||
let fill = [area.x(), y, area.w(), 1];
|
let fill = [area.x(), y, area.w(), 1];
|
||||||
to.fill_fg(fill, Color::Rgb(0, 0, 0));
|
to.fill_fg(fill, Color::Rgb(0, 0, 0));
|
||||||
|
|
@ -63,6 +103,12 @@ impl<'a> Content<TuiOut> for TreeTable<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entry {
|
impl Entry {
|
||||||
|
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 {
|
fn icon (&self) -> &'static str {
|
||||||
if self.is_dir() {
|
if self.is_dir() {
|
||||||
"" //"+"
|
"" //"+"
|
||||||
|
|
@ -81,34 +127,6 @@ impl Entry {
|
||||||
Some(Style::default().bold())
|
Some(Style::default().bold())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn label (&self, icon: &str, name: &impl Display) -> String {
|
|
||||||
let indent = "".pad_to_width((self.depth - 1) * 2);
|
|
||||||
table_row(
|
|
||||||
self.hash(),
|
|
||||||
&format!("{indent}{icon} {name}"),
|
|
||||||
self.artist(),
|
|
||||||
self.album(),
|
|
||||||
"",
|
|
||||||
self.title()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn table_row (
|
|
||||||
hash: Option<Arc<str>>,
|
|
||||||
label: &str,
|
|
||||||
artist: Option<Arc<str>>,
|
|
||||||
album: Option<Arc<str>>,
|
|
||||||
track: &str,
|
|
||||||
title: Option<Arc<str>>,
|
|
||||||
) -> String {
|
|
||||||
let hash = hash.unwrap_or_default().pad_to_width(COLUMN_WIDTHS[0] as usize);
|
|
||||||
let label = label.pad_to_width(COLUMN_WIDTHS[1] as usize);
|
|
||||||
let artist = artist.unwrap_or_default().pad_to_width(COLUMN_WIDTHS[2] as usize);
|
|
||||||
let album = album.unwrap_or_default().pad_to_width(COLUMN_WIDTHS[3] as usize);
|
|
||||||
let track = track.pad_to_width(COLUMN_WIDTHS[4] as usize);
|
|
||||||
let title = title.unwrap_or_default().pad_to_width(COLUMN_WIDTHS[5] as usize);
|
|
||||||
format!("{hash}│{label}│{artist}╎{album}╎{track}╎{title}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn status_bar (content: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
fn status_bar (content: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue