Improve errors returned from main

This commit is contained in:
itsjunetime
2025-08-01 21:28:46 -06:00
parent 5542daffb6
commit 035185a40f
+164 -55
View File
@@ -1,17 +1,21 @@
use core::error::Error;
use std::{ use std::{
borrow::Cow,
ffi::OsString, ffi::OsString,
io::{BufReader, Read, Write, stdout}, io::{BufReader, Read, Stdout, Write, stdout},
num::NonZeroUsize, num::NonZeroUsize,
path::PathBuf path::PathBuf
}; };
use crossterm::{ use crossterm::{
event::EventStream,
execute, execute,
terminal::{ terminal::{
EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode,
enable_raw_mode, window_size enable_raw_mode, window_size
} }
}; };
use flume::{Sender, r#async::RecvStream};
use futures_util::{FutureExt, stream::StreamExt}; use futures_util::{FutureExt, stream::StreamExt};
use notify::{Event, EventKind, RecursiveMode, Watcher}; use notify::{Event, EventKind, RecursiveMode, Watcher};
use ratatui::{Terminal, backend::CrosstermBackend}; use ratatui::{Terminal, backend::CrosstermBackend};
@@ -24,19 +28,24 @@ use tdf::{
}; };
// Dummy struct for easy errors in main // Dummy struct for easy errors in main
#[derive(Debug)] struct WrappedErr(Cow<'static, str>);
struct BadTermSizeStdin(String);
impl std::fmt::Display for BadTermSizeStdin { impl std::fmt::Display for WrappedErr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0) write!(f, "{}", self.0)
} }
} }
impl std::error::Error for BadTermSizeStdin {} impl std::fmt::Debug for WrappedErr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
impl std::error::Error for WrappedErr {}
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), WrappedErr> {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
console_subscriber::init(); console_subscriber::init();
@@ -59,18 +68,26 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
required file: PathBuf required file: PathBuf
}; };
let path = flags.file.canonicalize()?; let path = flags
let black = parse_color_to_i32(&flags.black_color.unwrap_or("000000".into())).map_err(|e| { .file
BadTermSizeStdin(format!( .canonicalize()
"Couldn't parse black color: {e} - is it formatted like a CSS color?" .map_err(|e| WrappedErr(format!("Cannot canonicalize provided file: {e}").into()))?;
))
})?;
let white = parse_color_to_i32(&flags.white_color.unwrap_or("FFFFFF".into())).map_err(|e| { let black =
BadTermSizeStdin(format!( parse_color_to_i32(flags.black_color.as_deref().unwrap_or("000000")).map_err(|e| {
"Couldn't parse while color: {e} - is it formatted like a CSS color?" WrappedErr(
)) format!("Couldn't parse black color: {e} - is it formatted like a CSS color?")
})?; .into()
)
})?;
let white =
parse_color_to_i32(flags.white_color.as_deref().unwrap_or("FFFFFF")).map_err(|e| {
WrappedErr(
format!("Couldn't parse white color: {e} - is it formatted like a CSS color?")
.into()
)
})?;
let (watch_to_render_tx, render_rx) = flume::unbounded(); let (watch_to_render_tx, render_rx) = flume::unbounded();
let tui_tx = watch_to_render_tx.clone(); let tui_tx = watch_to_render_tx.clone();
@@ -82,9 +99,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
watch_to_tui_tx, watch_to_tui_tx,
watch_to_render_tx, watch_to_render_tx,
path.file_name() path.file_name()
.ok_or("Path does not have a last component??")? .ok_or(WrappedErr("Path does not have a last component??".into()))?
.to_owned() .to_owned()
))?; ))
.map_err(|e| WrappedErr(format!("Couldn't start watching the provided file: {e}").into()))?;
// So we have to watch the parent directory of the file that we are interested in because the // So we have to watch the parent directory of the file that we are interested in because the
// `notify` library works on inodes, and if the file is deleted, that inode is gone as well, // `notify` library works on inodes, and if the file is deleted, that inode is gone as well,
@@ -94,25 +112,33 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// opinion on this clear // opinion on this clear
// (https://github.com/notify-rs/notify/issues/113#issuecomment-281836995) so whatever, guess // (https://github.com/notify-rs/notify/issues/113#issuecomment-281836995) so whatever, guess
// we have to do this annoying workaround. // we have to do this annoying workaround.
watcher.watch( watcher
path.parent().expect("The root directory is not a PDF"), .watch(
RecursiveMode::NonRecursive path.parent().expect("The root directory is not a PDF"),
)?; RecursiveMode::NonRecursive
)
.map_err(|e| WrappedErr(format!("Can't watch the provided file: {e}").into()))?;
// TODO: Handle non-utf8 file names? Maybe by constructing a CString and passing that in to the // TODO: Handle non-utf8 file names? Maybe by constructing a CString and passing that in to the
// mupdf stuff instead of a rust string? // mupdf stuff instead of a rust string?
let file_path = path.clone().into_os_string().to_string_lossy().to_string(); let file_path = path.clone().into_os_string().to_string_lossy().to_string();
let mut window_size = window_size()?; let mut window_size = window_size().map_err(|e| {
WrappedErr(format!("Can't get your current terminal window size: {e}").into())
})?;
if window_size.width == 0 || window_size.height == 0 { if window_size.width == 0 || window_size.height == 0 {
// send the command code to get the terminal window size // send the command code to get the terminal window size
print!("\x1b[14t"); print!("\x1b[14t");
std::io::stdout().flush()?; std::io::stdout().flush().unwrap();
// we need to enable raw mode here since this bit of output won't print a newline; it'll // we need to enable raw mode here since this bit of output won't print a newline; it'll
// just print the info it wants to tell us. So we want to get all characters as they come // just print the info it wants to tell us. So we want to get all characters as they come
enable_raw_mode()?; enable_raw_mode().map_err(|e| {
WrappedErr(
format!("Can't enable raw mode, which is necessary to receive input: {e}").into()
)
})?;
// read in the returned size until we hit a 't' (which indicates to us it's done) // read in the returned size until we hit a 't' (which indicates to us it's done)
let input_vec = BufReader::new(std::io::stdin()) let input_vec = BufReader::new(std::io::stdin())
@@ -122,9 +148,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// and then disable raw mode again in case we return an error in this next section // and then disable raw mode again in case we return an error in this next section
disable_raw_mode()?; disable_raw_mode().map_err(|e| {
WrappedErr(format!("Can't put the terminal back into a normal input state: {e}").into())
})?;
let input_line = String::from_utf8(input_vec)?; let input_line = String::from_utf8(input_vec).map_err(|e| {
WrappedErr(
format!(
"The terminal responded to our request for its font size by providing non-utf8 data: {e}"
)
.into()
)
})?;
let input_line = input_line let input_line = input_line
.trim_start_matches("\x1b[4") .trim_start_matches("\x1b[4")
.trim_start_matches(';'); .trim_start_matches(';');
@@ -134,19 +169,37 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut splits = input_line.split([';', 't']); let mut splits = input_line.split([';', 't']);
let (Some(h), Some(w)) = (splits.next(), splits.next()) else { let (Some(h), Some(w)) = (splits.next(), splits.next()) else {
return Err(BadTermSizeStdin(format!( return Err(WrappedErr(
"Terminal responded with unparseable size response '{input_line}'" format!("Terminal responded with unparseable size response '{input_line}'").into()
)) ));
.into());
}; };
window_size.height = h.parse::<u16>()?; window_size.height = h.parse::<u16>().map_err(|_| {
window_size.width = w.parse::<u16>()?; WrappedErr(
format!(
"Your terminal said its height is {h}, but that is not a 16-bit unsigned integer"
)
.into()
)
})?;
window_size.width = w.parse::<u16>().map_err(|_| {
WrappedErr(
format!(
"Your terminal said its width is {w}, but that is not a 16-bit unsigned integer"
)
.into()
)
})?;
} }
// We need to create `picker` on this thread because if we create it on the `renderer` thread, // We need to create `picker` on this thread because if we create it on the `renderer` thread,
// it messes up something with user input. Input never makes it to the crossterm thing // it messes up something with user input. Input never makes it to the crossterm thing
let picker = Picker::from_query_stdio()?; let picker = Picker::from_query_stdio()
.map_err(|e| WrappedErr(match e {
ratatui_image::errors::Errors::NoFontSize =>
"Unable to detect your terminal's font size; this is an issue with your terminal emulator.\nPlease use a different terminal emulator or report this bug to tdf.".into(),
e => format!("Couldn't get the necessary information to set up images: {e}").into()
}))?;
// then we want to spawn off the rendering task // then we want to spawn off the rendering task
// We need to use the thread::spawn API so that this exists in a thread not owned by tokio, // We need to use the thread::spawn API so that this exists in a thread not owned by tokio,
@@ -167,7 +220,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
) )
}); });
let mut ev_stream = crossterm::event::EventStream::new(); let ev_stream = crossterm::event::EventStream::new();
let (to_converter, from_main) = flume::unbounded(); let (to_converter, from_main) = flume::unbounded();
let (to_main, from_converter) = flume::unbounded(); let (to_main, from_converter) = flume::unbounded();
@@ -178,26 +231,91 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|| "Unknown file".into(), || "Unknown file".into(),
|n| n.to_string_lossy().to_string() |n| n.to_string_lossy().to_string()
); );
let mut tui = Tui::new(file_name, flags.max_wide, flags.r_to_l.unwrap_or_default()); let tui = Tui::new(file_name, flags.max_wide, flags.r_to_l.unwrap_or_default());
let backend = CrosstermBackend::new(std::io::stdout()); let backend = CrosstermBackend::new(std::io::stdout());
let mut term = Terminal::new(backend)?; let mut term = Terminal::new(backend).map_err(|e| {
WrappedErr(format!("Couldn't set up crossterm's terminal backend: {e}").into())
})?;
term.skip_diff(true); term.skip_diff(true);
execute!( execute!(
term.backend_mut(), term.backend_mut(),
EnterAlternateScreen, EnterAlternateScreen,
crossterm::cursor::Hide crossterm::cursor::Hide
)?; )
enable_raw_mode()?; .map_err(|e| {
WrappedErr(
format!(
"Couldn't enter the alternate screen and hide the cursor for proper presentation: {e}"
)
.into()
)
})?;
enable_raw_mode().map_err(|e| {
WrappedErr(
format!("Can't enable raw mode, which is necessary to receive input: {e}").into()
)
})?;
let mut fullscreen = flags.fullscreen.unwrap_or_default(); let fullscreen = flags.fullscreen.unwrap_or_default();
let mut main_area = Tui::main_layout(&term.get_frame(), fullscreen); let main_area = Tui::main_layout(&term.get_frame(), fullscreen);
tui_tx.send(RenderNotif::Area(main_area.page_area))?; tui_tx
.send(RenderNotif::Area(main_area.page_area))
.map_err(|e| {
WrappedErr(
format!("Couldn't inform the rendering thread of the available area: {e}").into()
)
})?;
let mut tui_rx = tui_rx.into_stream(); let tui_rx = tui_rx.into_stream();
let mut from_converter = from_converter.into_stream(); let from_converter = from_converter.into_stream();
enter_redraw_loop(
ev_stream,
tui_tx,
tui_rx,
to_converter,
from_converter,
fullscreen,
tui,
&mut term,
main_area
)
.await
.map_err(|e| {
WrappedErr(
format!(
"An unexpected error occurred while communicating between different parts of tdf: {e}"
)
.into()
)
})?;
execute!(
term.backend_mut(),
LeaveAlternateScreen,
crossterm::cursor::Show
)
.unwrap();
disable_raw_mode().unwrap();
Ok(())
}
// oh shut up clippy who cares
#[expect(clippy::too_many_arguments)]
async fn enter_redraw_loop(
mut ev_stream: EventStream,
tui_tx: Sender<RenderNotif>,
mut tui_rx: RecvStream<'_, Result<RenderInfo, RenderError>>,
to_converter: Sender<ConverterMsg>,
mut from_converter: RecvStream<'_, Result<ConvertedPage, RenderError>>,
mut fullscreen: bool,
mut tui: Tui,
term: &mut Terminal<CrosstermBackend<Stdout>>,
mut main_area: tdf::tui::RenderLayout
) -> Result<(), Box<dyn Error>> {
loop { loop {
let mut needs_redraw = true; let mut needs_redraw = true;
tokio::select! { tokio::select! {
@@ -210,7 +328,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
None => needs_redraw = false, None => needs_redraw = false,
Some(action) => match action { Some(action) => match action {
InputAction::Redraw => (), InputAction::Redraw => (),
InputAction::QuitApp => break, InputAction::QuitApp => return Ok(()),
InputAction::JumpingToPage(page) => { InputAction::JumpingToPage(page) => {
tui_tx.send(RenderNotif::JumpToPage(page))?; tui_tx.send(RenderNotif::JumpToPage(page))?;
to_converter.send(ConverterMsg::GoToPage(page))?; to_converter.send(ConverterMsg::GoToPage(page))?;
@@ -261,15 +379,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
execute!(stdout(), EndSynchronizedUpdate)?; execute!(stdout(), EndSynchronizedUpdate)?;
} }
} }
execute!(
term.backend_mut(),
LeaveAlternateScreen,
crossterm::cursor::Show
)?;
disable_raw_mode()?;
Ok(())
} }
fn on_notify_ev( fn on_notify_ev(