diff --git a/src/bin/musichoard-edit.rs b/src/bin/musichoard-edit.rs index 00de904..94f7f0c 100644 --- a/src/bin/musichoard-edit.rs +++ b/src/bin/musichoard-edit.rs @@ -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); } diff --git a/src/core/collection/album.rs b/src/core/collection/album.rs index 03e2acb..9a692e1 100644 --- a/src/core/collection/album.rs +++ b/src/core/collection/album.rs @@ -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 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 for AlbumId { + fn as_ref(&self) -> &AlbumId { + self + } +} + +impl AlbumId { + pub fn new>(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; diff --git a/src/core/musichoard/musichoard.rs b/src/core/musichoard/musichoard.rs index 8995e86..b82c8ea 100644 --- a/src/core/musichoard/musichoard.rs +++ b/src/core/musichoard/musichoard.rs @@ -171,6 +171,22 @@ impl MusicHoard { 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 MusicHoard { @@ -243,38 +259,62 @@ impl MusicHoard { Ok(()) } - fn update_collection(&mut self, func: F) -> Result<(), Error> + fn update_collection(&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, F1, F2>( + fn update_artist_and, 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, F>(&mut self, artist_id: ID, func: F) -> Result<(), Error> + fn update_artist, 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, ALBUM: AsRef, 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>(&mut self, artist_id: ID) -> Result<(), Error> { let artist_id: ArtistId = artist_id.into(); @@ -315,7 +355,7 @@ impl MusicHoard { ) } - pub fn set_musicbrainz_url, S: AsRef>( + pub fn set_artist_musicbrainz, S: AsRef>( &mut self, artist_id: ID, url: S, @@ -324,14 +364,14 @@ impl MusicHoard { self.update_artist(artist_id, |artist| artist.set_musicbrainz_url(url)) } - pub fn clear_musicbrainz_url>( + pub fn clear_artist_musicbrainz>( &mut self, artist_id: ID, ) -> Result<(), Error> { self.update_artist(artist_id, |artist| artist.clear_musicbrainz_url()) } - pub fn add_to_property, S: AsRef + Into>( + pub fn add_to_artist_property, S: AsRef + Into>( &mut self, artist_id: ID, property: S, @@ -340,7 +380,7 @@ impl MusicHoard { self.update_artist(artist_id, |artist| artist.add_to_property(property, values)) } - pub fn remove_from_property, S: AsRef>( + pub fn remove_from_artist_property, S: AsRef>( &mut self, artist_id: ID, property: S, @@ -351,7 +391,7 @@ impl MusicHoard { }) } - pub fn set_property, S: AsRef + Into>( + pub fn set_artist_property, S: AsRef + Into>( &mut self, artist_id: ID, property: S, @@ -360,13 +400,42 @@ impl MusicHoard { self.update_artist(artist_id, |artist| artist.set_property(property, values)) } - pub fn clear_property, S: AsRef>( + pub fn clear_artist_property, S: AsRef>( &mut self, artist_id: ID, property: S, ) -> Result<(), Error> { self.update_artist(artist_id, |artist| artist.clear_property(property)) } + + pub fn set_album_seq, ALBUM: AsRef>( + &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, ALBUM: AsRef>( + &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 MusicHoard { @@ -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()); diff --git a/src/main.rs b/src/main.rs index 5c3dfa5..9449066 100644 --- a/src/main.rs +++ b/src/main.rs @@ -75,7 +75,12 @@ fn with(builder: MusicHoardBuilder) { 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(db_opt: DbOpt, builder: MusicHoardBuilder) { diff --git a/src/tui/mod.rs b/src/tui/mod.rs index f442ce6..c43e01a 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -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), +} + +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 for Error { @@ -114,7 +139,8 @@ impl Tui { // 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());