Sort albums by month if two releases of the same artist happen in the same year #155

Merged
5 changed files with 314 additions and 116 deletions
Showing only changes of commit adddc5ba2f - Show all commits

View File

@ -3,7 +3,7 @@ use std::path::PathBuf;
use structopt::{clap::AppSettings, StructOpt};
use musichoard::{
collection::artist::ArtistId,
collection::{album::AlbumId, artist::ArtistId},
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
MusicHoard, MusicHoardBuilder, NoLibrary,
};
@ -22,56 +22,54 @@ struct Opt {
database_file_path: PathBuf,
#[structopt(subcommand)]
category: Category,
command: Command,
}
#[derive(StructOpt, Debug)]
enum Category {
#[structopt(about = "Edit artist information")]
Artist(ArtistCommand),
enum Command {
#[structopt(about = "Modify artist information")]
Artist(ArtistOpt),
}
impl Category {
fn handle(self, music_hoard: &mut MH) {
match self {
Category::Artist(artist_command) => artist_command.handle(music_hoard),
}
}
#[derive(StructOpt, Debug)]
struct ArtistOpt {
// For some reason, not specyfing the artist name with the `long` name makes StructOpt failed
// for inexplicable reason. For example, it won't recognise `Abadde` or `Abadden` as a name and
// will insteady try to process it as a command.
#[structopt(long, help = "The name of the artist")]
name: String,
#[structopt(subcommand)]
command: ArtistCommand,
}
#[derive(StructOpt, Debug)]
enum ArtistCommand {
#[structopt(about = "Add a new artist to the collection")]
Add(ArtistValue),
Add,
#[structopt(about = "Remove an artist from the collection")]
Remove(ArtistValue),
Remove,
#[structopt(about = "Edit the artist's sort name")]
Sort(SortCommand),
#[structopt(name = "musicbrainz", about = "Edit the MusicBrainz URL of an artist")]
MusicBrainz(MusicBrainzCommand),
#[structopt(name = "property", about = "Edit a property of an artist")]
#[structopt(about = "Edit a property of an artist")]
Property(PropertyCommand),
#[structopt(about = "Modify the artist's album information")]
Album(AlbumOpt),
}
#[derive(StructOpt, Debug)]
enum SortCommand {
#[structopt(about = "Set the provided name as the artist's sort name")]
Set(ArtistSortValue),
Set(SortValue),
#[structopt(about = "Clear the artist's sort name")]
Clear(ArtistValue),
Clear,
}
#[derive(StructOpt, Debug)]
struct ArtistValue {
#[structopt(help = "The name of the artist")]
artist: String,
}
#[derive(StructOpt, Debug)]
struct ArtistSortValue {
#[structopt(help = "The name of the artist")]
artist: String,
#[structopt(help = "The sort name of the artist")]
struct SortValue {
#[structopt(help = "The sort name")]
sort: String,
}
@ -80,13 +78,11 @@ enum MusicBrainzCommand {
#[structopt(about = "Set the MusicBrainz URL overwriting any existing value")]
Set(MusicBrainzValue),
#[structopt(about = "Clear the MusicBrainz URL)")]
Clear(ArtistValue),
Clear,
}
#[derive(StructOpt, Debug)]
struct MusicBrainzValue {
#[structopt(help = "The name of the artist")]
artist: String,
#[structopt(help = "The MusicBrainz URL")]
url: String,
}
@ -105,8 +101,6 @@ enum PropertyCommand {
#[derive(StructOpt, Debug)]
struct PropertyValue {
#[structopt(help = "The name of the artist")]
artist: String,
#[structopt(help = "The name of the property")]
property: String,
#[structopt(help = "The list of values")]
@ -115,101 +109,176 @@ struct PropertyValue {
#[derive(StructOpt, Debug)]
struct PropertyName {
#[structopt(help = "The name of the artist")]
artist: String,
#[structopt(help = "The name of the property")]
property: String,
}
impl ArtistCommand {
#[derive(StructOpt, Debug)]
struct AlbumOpt {
// Using `long` for consistency with `ArtistOpt`.
#[structopt(long, help = "The title of the album")]
title: String,
#[structopt(subcommand)]
command: AlbumCommand,
}
#[derive(StructOpt, Debug)]
enum AlbumCommand {
#[structopt(about = "Edit the album's sequence value")]
Seq(AlbumSeqCommand),
}
#[derive(StructOpt, Debug)]
enum AlbumSeqCommand {
#[structopt(about = "Set the sequence value overwriting any existing value")]
Set(AlbumSeqValue),
#[structopt(about = "Clear the sequence value")]
Clear,
}
#[derive(StructOpt, Debug)]
struct AlbumSeqValue {
#[structopt(help = "The new sequence value")]
value: u8,
}
impl Command {
fn handle(self, music_hoard: &mut MH) {
match self {
ArtistCommand::Add(artist_value) => {
Command::Artist(artist_opt) => artist_opt.handle(music_hoard),
}
}
}
impl ArtistOpt {
fn handle(self, music_hoard: &mut MH) {
self.command.handle(music_hoard, &self.name)
}
}
impl ArtistCommand {
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
match self {
ArtistCommand::Add => {
music_hoard
.add_artist(ArtistId::new(artist_value.artist))
.add_artist(ArtistId::new(artist_name))
.expect("failed to add artist");
}
ArtistCommand::Remove(artist_value) => {
ArtistCommand::Remove => {
music_hoard
.remove_artist(ArtistId::new(artist_value.artist))
.remove_artist(ArtistId::new(artist_name))
.expect("failed to remove artist");
}
ArtistCommand::Sort(sort_command) => {
sort_command.handle(music_hoard);
sort_command.handle(music_hoard, artist_name);
}
ArtistCommand::MusicBrainz(musicbrainz_command) => {
musicbrainz_command.handle(music_hoard)
musicbrainz_command.handle(music_hoard, artist_name)
}
ArtistCommand::Property(property_command) => {
property_command.handle(music_hoard);
property_command.handle(music_hoard, artist_name);
}
ArtistCommand::Album(album_opt) => {
album_opt.handle(music_hoard, artist_name);
}
}
}
}
impl SortCommand {
fn handle(self, music_hoard: &mut MH) {
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
match self {
SortCommand::Set(artist_sort_value) => music_hoard
.set_artist_sort(
ArtistId::new(artist_sort_value.artist),
ArtistId::new(artist_name),
ArtistId::new(artist_sort_value.sort),
)
.expect("faild to set artist sort name"),
SortCommand::Clear(artist_value) => music_hoard
.clear_artist_sort(ArtistId::new(artist_value.artist))
SortCommand::Clear => music_hoard
.clear_artist_sort(ArtistId::new(artist_name))
.expect("failed to clear artist sort name"),
}
}
}
impl MusicBrainzCommand {
fn handle(self, music_hoard: &mut MH) {
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
match self {
MusicBrainzCommand::Set(musicbrainz_value) => music_hoard
.set_musicbrainz_url(
ArtistId::new(musicbrainz_value.artist),
musicbrainz_value.url,
)
.set_artist_musicbrainz(ArtistId::new(artist_name), musicbrainz_value.url)
.expect("failed to set MusicBrainz URL"),
MusicBrainzCommand::Clear(artist_value) => music_hoard
.clear_musicbrainz_url(ArtistId::new(artist_value.artist))
MusicBrainzCommand::Clear => music_hoard
.clear_artist_musicbrainz(ArtistId::new(artist_name))
.expect("failed to clear MusicBrainz URL"),
}
}
}
impl PropertyCommand {
fn handle(self, music_hoard: &mut MH) {
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
match self {
PropertyCommand::Add(property_value) => music_hoard
.add_to_property(
ArtistId::new(property_value.artist),
.add_to_artist_property(
ArtistId::new(artist_name),
property_value.property,
property_value.values,
)
.expect("failed to add values to property"),
PropertyCommand::Remove(property_value) => music_hoard
.remove_from_property(
ArtistId::new(property_value.artist),
.remove_from_artist_property(
ArtistId::new(artist_name),
property_value.property,
property_value.values,
)
.expect("failed to remove values from property"),
PropertyCommand::Set(property_value) => music_hoard
.set_property(
ArtistId::new(property_value.artist),
.set_artist_property(
ArtistId::new(artist_name),
property_value.property,
property_value.values,
)
.expect("failed to set property"),
PropertyCommand::Clear(property_name) => music_hoard
.clear_property(ArtistId::new(property_name.artist), property_name.property)
.clear_artist_property(ArtistId::new(artist_name), property_name.property)
.expect("failed to clear property"),
}
}
}
impl AlbumOpt {
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
self.command.handle(music_hoard, artist_name, &self.title)
}
}
impl AlbumCommand {
fn handle(self, music_hoard: &mut MH, artist_name: &str, album_name: &str) {
match self {
AlbumCommand::Seq(seq_command) => {
seq_command.handle(music_hoard, artist_name, album_name);
}
}
}
}
impl AlbumSeqCommand {
fn handle(self, music_hoard: &mut MH, artist_name: &str, album_name: &str) {
match self {
AlbumSeqCommand::Set(seq_value) => music_hoard
.set_album_seq(
ArtistId::new(artist_name),
AlbumId::new(album_name),
seq_value.value,
)
.expect("failed to set sequence value"),
AlbumSeqCommand::Clear => music_hoard
.clear_album_seq(ArtistId::new(artist_name), AlbumId::new(album_name))
.expect("failed to clear sequence value"),
}
}
}
fn main() {
let opt = Opt::from_args();
@ -219,5 +288,5 @@ fn main() {
.set_database(db)
.build()
.expect("failed to initialise MusicHoard");
opt.category.handle(&mut music_hoard);
opt.command.handle(&mut music_hoard);
}

View File

@ -1,4 +1,7 @@
use std::mem;
use std::{
fmt::{self, Display},
mem,
};
use crate::core::collection::{
merge::{Merge, MergeSorted, WithId},
@ -30,25 +33,15 @@ pub struct AlbumId {
// There are crates for handling dates, but we don't need much complexity beyond year-month-day.
/// The album's release date.
#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq, Hash)]
#[derive(Clone, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)]
pub struct AlbumDate {
pub year: u32,
pub month: AlbumMonth,
pub day: u8,
}
impl Default for AlbumDate {
fn default() -> Self {
AlbumDate {
year: 0,
month: AlbumMonth::None,
day: 0,
}
}
}
/// The album's sequence to determine order when two or more albums have the same release date.
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq, Hash)]
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)]
pub struct AlbumSeq(pub u8);
#[repr(u8)]
@ -69,6 +62,12 @@ pub enum AlbumMonth {
December = 12,
}
impl Default for AlbumMonth {
fn default() -> Self {
AlbumMonth::None
}
}
impl From<u8> for AlbumMonth {
fn from(value: u8) -> Self {
match value {
@ -93,6 +92,14 @@ impl Album {
pub fn get_sort_key(&self) -> (&AlbumDate, &AlbumSeq, &AlbumId) {
(&self.date, &self.seq, &self.id)
}
pub fn set_seq(&mut self, seq: AlbumSeq) {
self.seq = seq;
}
pub fn clear_seq(&mut self) {
self.seq = AlbumSeq::default();
}
}
impl PartialOrd for Album {
@ -116,6 +123,24 @@ impl Merge for Album {
}
}
impl AsRef<AlbumId> for AlbumId {
fn as_ref(&self) -> &AlbumId {
self
}
}
impl AlbumId {
pub fn new<S: Into<String>>(name: S) -> AlbumId {
AlbumId { title: name.into() }
}
}
impl Display for AlbumId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.title)
}
}
#[cfg(test)]
mod tests {
use crate::core::testmod::FULL_COLLECTION;

View File

@ -171,6 +171,22 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
Error::CollectionError(format!("artist '{}' is not in the collection", artist_id))
})
}
fn get_album_mut<'a>(artist: &'a mut Artist, album_id: &AlbumId) -> Option<&'a mut Album> {
artist.albums.iter_mut().find(|a| &a.id == album_id)
}
fn get_album_mut_or_err<'a>(
artist: &'a mut Artist,
album_id: &AlbumId,
) -> Result<&'a mut Album, Error> {
Self::get_album_mut(artist, album_id).ok_or_else(|| {
Error::CollectionError(format!(
"album '{}' does not belong to the artist",
album_id
))
})
}
}
impl<LIB: ILibrary> MusicHoard<LIB, NoDatabase> {
@ -243,38 +259,62 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
Ok(())
}
fn update_collection<F>(&mut self, func: F) -> Result<(), Error>
fn update_collection<FN>(&mut self, func: FN) -> Result<(), Error>
where
F: FnOnce(&mut Collection),
FN: FnOnce(&mut Collection),
{
func(&mut self.pre_commit);
self.commit()
}
fn update_artist_and<ID: AsRef<ArtistId>, F1, F2>(
fn update_artist_and<ID: AsRef<ArtistId>, FNARTIST, FNCOLL>(
&mut self,
artist_id: ID,
f1: F1,
f2: F2,
fn_artist: FNARTIST,
fn_collection: FNCOLL,
) -> Result<(), Error>
where
F1: FnOnce(&mut Artist),
F2: FnOnce(&mut Collection),
FNARTIST: FnOnce(&mut Artist),
FNCOLL: FnOnce(&mut Collection),
{
f1(Self::get_artist_mut_or_err(
fn_artist(Self::get_artist_mut_or_err(
&mut self.pre_commit,
artist_id.as_ref(),
)?);
self.update_collection(f2)
self.update_collection(fn_collection)
}
fn update_artist<ID: AsRef<ArtistId>, F>(&mut self, artist_id: ID, func: F) -> Result<(), Error>
fn update_artist<ID: AsRef<ArtistId>, FN>(
&mut self,
artist_id: ID,
func: FN,
) -> Result<(), Error>
where
F: FnOnce(&mut Artist),
FN: FnOnce(&mut Artist),
{
self.update_artist_and(artist_id, func, |_| {})
}
fn update_album_and<ARTIST: AsRef<ArtistId>, ALBUM: AsRef<AlbumId>, FNALBUM, FNARTIST, FNCOLL>(
&mut self,
artist_id: ARTIST,
album_id: ALBUM,
fn_album: FNALBUM,
fn_artist: FNARTIST,
fn_collection: FNCOLL,
) -> Result<(), Error>
where
FNALBUM: FnOnce(&mut Album),
FNARTIST: FnOnce(&mut Artist),
FNCOLL: FnOnce(&mut Collection),
{
let artist = Self::get_artist_mut_or_err(&mut self.pre_commit, artist_id.as_ref())?;
let album = Self::get_album_mut_or_err(artist, album_id.as_ref())?;
fn_album(album);
fn_artist(artist);
self.update_collection(fn_collection)
}
pub fn add_artist<ID: Into<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> {
let artist_id: ArtistId = artist_id.into();
@ -315,7 +355,7 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
)
}
pub fn set_musicbrainz_url<ID: AsRef<ArtistId>, S: AsRef<str>>(
pub fn set_artist_musicbrainz<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: ID,
url: S,
@ -324,14 +364,14 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
self.update_artist(artist_id, |artist| artist.set_musicbrainz_url(url))
}
pub fn clear_musicbrainz_url<ID: AsRef<ArtistId>>(
pub fn clear_artist_musicbrainz<ID: AsRef<ArtistId>>(
&mut self,
artist_id: ID,
) -> Result<(), Error> {
self.update_artist(artist_id, |artist| artist.clear_musicbrainz_url())
}
pub fn add_to_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
pub fn add_to_artist_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
&mut self,
artist_id: ID,
property: S,
@ -340,7 +380,7 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
self.update_artist(artist_id, |artist| artist.add_to_property(property, values))
}
pub fn remove_from_property<ID: AsRef<ArtistId>, S: AsRef<str>>(
pub fn remove_from_artist_property<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: ID,
property: S,
@ -351,7 +391,7 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
})
}
pub fn set_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
pub fn set_artist_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
&mut self,
artist_id: ID,
property: S,
@ -360,13 +400,42 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
self.update_artist(artist_id, |artist| artist.set_property(property, values))
}
pub fn clear_property<ID: AsRef<ArtistId>, S: AsRef<str>>(
pub fn clear_artist_property<ID: AsRef<ArtistId>, S: AsRef<str>>(
&mut self,
artist_id: ID,
property: S,
) -> Result<(), Error> {
self.update_artist(artist_id, |artist| artist.clear_property(property))
}
pub fn set_album_seq<ARTIST: AsRef<ArtistId>, ALBUM: AsRef<AlbumId>>(
&mut self,
artist_id: ARTIST,
album_id: ALBUM,
seq: u8,
) -> Result<(), Error> {
self.update_album_and(
artist_id,
album_id,
|album| album.set_seq(AlbumSeq(seq)),
|artist| artist.albums.sort_unstable(),
|_| {},
)
}
pub fn clear_album_seq<ARTIST: AsRef<ArtistId>, ALBUM: AsRef<AlbumId>>(
&mut self,
artist_id: ARTIST,
album_id: ALBUM,
) -> Result<(), Error> {
self.update_album_and(
artist_id,
album_id,
|album| album.clear_seq(),
|artist| artist.albums.sort_unstable(),
|_| {},
)
}
}
impl<LIB: ILibrary, DB: IDatabase> MusicHoard<LIB, DB> {
@ -518,7 +587,7 @@ mod tests {
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
let actual_err = music_hoard
.set_musicbrainz_url(&artist_id, MUSICBUTLER)
.set_artist_musicbrainz(&artist_id, MUSICBUTLER)
.unwrap_err();
let expected_err = Error::CollectionError(format!(
"an error occurred when processing a URL: invalid MusicBrainz URL: {MUSICBUTLER}"
@ -544,23 +613,23 @@ mod tests {
// Setting a URL on an artist not in the collection is an error.
assert!(music_hoard
.set_musicbrainz_url(&artist_id_2, MUSICBRAINZ)
.set_artist_musicbrainz(&artist_id_2, MUSICBRAINZ)
.is_err());
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
// Setting a URL on an artist.
assert!(music_hoard
.set_musicbrainz_url(&artist_id, MUSICBRAINZ)
.set_artist_musicbrainz(&artist_id, MUSICBRAINZ)
.is_ok());
_ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap());
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
// Clearing URLs on an artist that does not exist is an error.
assert!(music_hoard.clear_musicbrainz_url(&artist_id_2).is_err());
assert!(music_hoard.clear_artist_musicbrainz(&artist_id_2).is_err());
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
// Clearing URLs.
assert!(music_hoard.clear_musicbrainz_url(&artist_id).is_ok());
assert!(music_hoard.clear_artist_musicbrainz(&artist_id).is_ok());
_ = expected.take();
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
}
@ -582,13 +651,13 @@ mod tests {
// Adding URLs to an artist not in the collection is an error.
assert!(music_hoard
.add_to_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.add_to_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.is_err());
assert!(music_hoard.collection[0].properties.is_empty());
// Adding mutliple URLs without clashes.
assert!(music_hoard
.add_to_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
.add_to_artist_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
.is_ok());
expected.push(MUSICBUTLER.to_owned());
expected.push(MUSICBUTLER_2.to_owned());
@ -599,7 +668,7 @@ mod tests {
// Removing URLs from an artist not in the collection is an error.
assert!(music_hoard
.remove_from_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.remove_from_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.is_err());
assert_eq!(
music_hoard.collection[0].properties.get("MusicButler"),
@ -608,7 +677,11 @@ mod tests {
// Removing multiple URLs without clashes.
assert!(music_hoard
.remove_from_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
.remove_from_artist_property(
&artist_id,
"MusicButler",
vec![MUSICBUTLER, MUSICBUTLER_2]
)
.is_ok());
expected.clear();
assert!(music_hoard.collection[0].properties.is_empty());
@ -631,13 +704,13 @@ mod tests {
// Seting URL on an artist not in the collection is an error.
assert!(music_hoard
.set_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.set_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.is_err());
assert!(music_hoard.collection[0].properties.is_empty());
// Set URLs.
assert!(music_hoard
.set_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
.set_artist_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
.is_ok());
expected.clear();
expected.push(MUSICBUTLER.to_owned());
@ -649,12 +722,12 @@ mod tests {
// Clearing URLs on an artist that does not exist is an error.
assert!(music_hoard
.clear_property(&artist_id_2, "MusicButler")
.clear_artist_property(&artist_id_2, "MusicButler")
.is_err());
// Clear URLs.
assert!(music_hoard
.clear_property(&artist_id, "MusicButler")
.clear_artist_property(&artist_id, "MusicButler")
.is_ok());
expected.clear();
assert!(music_hoard.collection[0].properties.is_empty());

View File

@ -75,7 +75,12 @@ fn with<LIB: ILibrary, DB: IDatabase>(builder: MusicHoardBuilder<LIB, DB>) {
let ui = Ui;
// Run the TUI application.
Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui");
let result = Tui::run(terminal, app, ui, handler, listener);
if let Err(tui::Error::ListenerPanic(err)) = result {
std::panic::resume_unwind(err);
} else {
result.expect("failed to run tui")
};
}
fn with_database<LIB: ILibrary>(db_opt: DbOpt, builder: MusicHoardBuilder<LIB, NoDatabase>) {

View File

@ -11,12 +11,16 @@ pub use handler::EventHandler;
pub use listener::EventListener;
pub use ui::Ui;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
use ratatui::backend::Backend;
use ratatui::Terminal;
use std::io;
use std::marker::PhantomData;
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture},
terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::Backend, Terminal};
use std::{
marker::PhantomData,
ptr,
{any::Any, io},
};
use crate::tui::{
app::{IAppAccess, IAppInteract},
@ -26,11 +30,32 @@ use crate::tui::{
ui::IUi,
};
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug)]
pub enum Error {
Io(String),
Event(String),
ListenerPanic,
ListenerPanic(Box<dyn Any + Send>),
}
impl Eq for Error {}
impl PartialEq for Error {
fn eq(&self, other: &Self) -> bool {
match self {
Error::Io(this) => match other {
Error::Io(other) => this == other,
_ => false,
},
Error::Event(this) => match other {
Error::Event(other) => this == other,
_ => false,
},
Error::ListenerPanic(this) => match other {
Error::ListenerPanic(other) => ptr::eq(this.as_ref(), other.as_ref()),
_ => false,
},
}
}
}
impl From<io::Error> for Error {
@ -114,7 +139,8 @@ impl<B: Backend, UI: IUi, APP: IAppInteract + IAppAccess> Tui<B, UI, APP> {
// Calling std::panic::resume_unwind(err) as recommended by the Rust docs
// will not produce an error message. The panic error message is printed at
// the location of the panic which at the time is hidden by the TUI.
Err(_) => return Err(Error::ListenerPanic),
// Therefore, propagate the error for the caller to resume unwinding.
Err(panic) => return Err(Error::ListenerPanic(panic)),
}
}
@ -301,14 +327,14 @@ mod tests {
let result = Tui::main(terminal, app, ui, handler, listener);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), Error::ListenerPanic);
assert!(matches!(result.unwrap_err(), Error::ListenerPanic(_)));
}
#[test]
fn errors() {
let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into();
let event_err: Error = EventError::Recv.into();
let listener_err = Error::ListenerPanic;
let listener_err = Error::ListenerPanic(Box::new("hello"));
assert!(!format!("{:?}", io_err).is_empty());
assert!(!format!("{:?}", event_err).is_empty());