mirror of
https://github.com/itsjunetime/tdf.git
synced 2026-06-02 08:01:47 -04:00
Improve errors returned from main
This commit is contained in:
+160
-51
@@ -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,17 +68,25 @@ 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 black =
|
||||||
|
parse_color_to_i32(flags.black_color.as_deref().unwrap_or("000000")).map_err(|e| {
|
||||||
|
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.unwrap_or("FFFFFF".into())).map_err(|e| {
|
let white =
|
||||||
BadTermSizeStdin(format!(
|
parse_color_to_i32(flags.white_color.as_deref().unwrap_or("FFFFFF")).map_err(|e| {
|
||||||
"Couldn't parse while color: {e} - is it formatted like a CSS color?"
|
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();
|
||||||
@@ -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
|
||||||
|
.watch(
|
||||||
path.parent().expect("The root directory is not a PDF"),
|
path.parent().expect("The root directory is not a PDF"),
|
||||||
RecursiveMode::NonRecursive
|
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(
|
||||||
|
|||||||
Reference in New Issue
Block a user