mirror of
https://github.com/itsjunetime/tdf.git
synced 2026-06-01 23:51:46 -04:00
Add --r-to-l and --max-wide flags to cli args
This commit is contained in:
+55
-45
@@ -2,8 +2,8 @@
|
||||
|
||||
use std::{
|
||||
io::{stdout, Read, Write},
|
||||
path::PathBuf,
|
||||
str::FromStr
|
||||
num::NonZeroUsize,
|
||||
path::PathBuf
|
||||
};
|
||||
|
||||
use converter::{run_conversion_loop, ConvertedPage, ConverterMsg};
|
||||
@@ -44,42 +44,33 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[cfg(feature = "tracing")]
|
||||
console_subscriber::init();
|
||||
|
||||
let file = std::env::args()
|
||||
.nth(1)
|
||||
.ok_or("Program requires a file to process")?;
|
||||
let path = PathBuf::from_str(&file)?.canonicalize()?;
|
||||
let flags = xflags::parse_or_exit! {
|
||||
/// Display the pdf with the pages starting at the right hand size and moving left and
|
||||
/// adjust input keys to match
|
||||
optional -r,--r-to-l r_to_l: bool
|
||||
/// The maximum number of pages to display together, horizontally, at a time
|
||||
optional -m,--max-wide max_wide: NonZeroUsize
|
||||
/// PDF file to read
|
||||
required file: PathBuf
|
||||
};
|
||||
|
||||
let (watch_tx, render_rx) = flume::unbounded();
|
||||
let tui_tx = watch_tx.clone();
|
||||
let path = flags.file.canonicalize()?;
|
||||
|
||||
let (watch_to_render_tx, render_rx) = flume::unbounded();
|
||||
let tui_tx = watch_to_render_tx.clone();
|
||||
|
||||
let (render_tx, tui_rx) = flume::unbounded();
|
||||
let watch_to_tui_tx = render_tx.clone();
|
||||
|
||||
// we need to call this outside the recommended_watcher call because if we call it inside, that
|
||||
// will be calling it from a thread not owned by the tokio runtime (since it's created by
|
||||
// calling thread::spawn) and that will cause a panic
|
||||
let mut watcher = notify::recommended_watcher(move |res: notify::Result<Event>| match res {
|
||||
// If we get an error here, and then an error sending, everything's going wrong. Just give
|
||||
// up lol.
|
||||
Err(e) => watch_to_tui_tx.send(Err(RenderError::Notify(e))).unwrap(),
|
||||
// TODO: Should we match EventKind::Rename and propogate that so that the other parts of the
|
||||
// process know that too? Or should that be
|
||||
Ok(ev) => match ev.kind {
|
||||
EventKind::Access(_) => (),
|
||||
EventKind::Remove(_) =>
|
||||
drop(watch_to_tui_tx.send(Err(RenderError::Render("File was deleted".into())))),
|
||||
// This shouldn't fail to send unless the receiver gets disconnected. If that's
|
||||
// happened, then like the main thread has panicked or something, so it doesn't matter
|
||||
// we don't handle the error here.
|
||||
EventKind::Other | EventKind::Any | EventKind::Create(_) | EventKind::Modify(_) =>
|
||||
drop(watch_tx.send(renderer::RenderNotif::Reload)),
|
||||
}
|
||||
})?;
|
||||
let mut watcher =
|
||||
notify::recommended_watcher(on_notify_ev(watch_to_tui_tx, watch_to_render_tx))?;
|
||||
|
||||
// We're making this nonrecursive 'cause we're just watching a single file, so there's nothing
|
||||
// to recurse into
|
||||
watcher.watch(&path, RecursiveMode::NonRecursive)?;
|
||||
|
||||
// TODO: Handle non-utf8 file names? Maybe by constructing a CString and passing that in to the
|
||||
// poppler stuff instead of a rust string?
|
||||
let file_path = format!("file://{}", path.clone().into_os_string().to_string_lossy());
|
||||
|
||||
let mut window_size = window_size()?;
|
||||
@@ -158,7 +149,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|| "Unknown file".into(),
|
||||
|n| n.to_string_lossy().to_string()
|
||||
);
|
||||
let mut tui = tui::Tui::new(file_name);
|
||||
let mut tui = tui::Tui::new(file_name, flags.max_wide, flags.r_to_l.unwrap_or_default());
|
||||
|
||||
let backend = CrosstermBackend::new(std::io::stdout());
|
||||
let mut term = Terminal::new(backend)?;
|
||||
@@ -183,25 +174,23 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut from_converter = from_converter.into_stream();
|
||||
|
||||
loop {
|
||||
let mut needs_redraw = tokio::select! {
|
||||
let mut needs_redraw = true;
|
||||
tokio::select! {
|
||||
// First we check if we have any keystrokes
|
||||
Some(ev) = ev_stream.next().fuse() => {
|
||||
// If we can't get user input, just crash.
|
||||
let ev = ev.expect("Couldn't get any user input");
|
||||
|
||||
match tui.handle_event(ev) {
|
||||
None => false,
|
||||
Some(action) => {
|
||||
match action {
|
||||
InputAction::Redraw => (),
|
||||
InputAction::QuitApp => break,
|
||||
InputAction::JumpingToPage(page) => {
|
||||
tui_tx.send(RenderNotif::JumpToPage(page))?;
|
||||
to_converter.send(ConverterMsg::GoToPage(page))?;
|
||||
},
|
||||
InputAction::Search(term) => tui_tx.send(RenderNotif::Search(term))?,
|
||||
};
|
||||
true
|
||||
match tui.handle_event(&ev) {
|
||||
None => needs_redraw = false,
|
||||
Some(action) => match action {
|
||||
InputAction::Redraw => (),
|
||||
InputAction::QuitApp => break,
|
||||
InputAction::JumpingToPage(page) => {
|
||||
tui_tx.send(RenderNotif::JumpToPage(page))?;
|
||||
to_converter.send(ConverterMsg::GoToPage(page))?;
|
||||
},
|
||||
InputAction::Search(term) => tui_tx.send(RenderNotif::Search(term))?,
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -224,14 +213,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
},
|
||||
Err(e) => tui.show_error(e),
|
||||
}
|
||||
true
|
||||
}
|
||||
Some(img_res) = from_converter.next() => {
|
||||
match img_res {
|
||||
Ok(ConvertedPage { page, num, num_results }) => tui.page_ready(page, num, num_results),
|
||||
Err(e) => tui.show_error(e),
|
||||
}
|
||||
true
|
||||
},
|
||||
};
|
||||
|
||||
@@ -260,6 +247,29 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_notify_ev(
|
||||
to_tui_tx: flume::Sender<Result<RenderInfo, RenderError>>,
|
||||
to_render_tx: flume::Sender<RenderNotif>
|
||||
) -> impl Fn(notify::Result<Event>) {
|
||||
move |res| match res {
|
||||
// If we get an error here, and then an error sending, everything's going wrong. Just give
|
||||
// up lol.
|
||||
Err(e) => to_tui_tx.send(Err(RenderError::Notify(e))).unwrap(),
|
||||
// TODO: Should we match EventKind::Rename and propogate that so that the other parts of the
|
||||
// process know that too? Or should that be
|
||||
Ok(ev) => match ev.kind {
|
||||
EventKind::Access(_) => (),
|
||||
EventKind::Remove(_) =>
|
||||
drop(to_tui_tx.send(Err(RenderError::Render("File was deleted".into())))),
|
||||
// This shouldn't fail to send unless the receiver gets disconnected. If that's
|
||||
// happened, then like the main thread has panicked or something, so it doesn't matter
|
||||
// we don't handle the error here.
|
||||
EventKind::Other | EventKind::Any | EventKind::Create(_) | EventKind::Modify(_) =>
|
||||
drop(to_render_tx.send(renderer::RenderNotif::Reload)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn noop(_: LogLevel, _: &[LogField<'_>]) -> LogWriterOutput {
|
||||
LogWriterOutput::Handled
|
||||
}
|
||||
|
||||
+12
-8
@@ -64,6 +64,10 @@ pub fn fill_default<T: Default>(vec: &mut Vec<T>, size: usize) {
|
||||
// Also we just kinda 'unwrap' all of the send/recv calls here 'cause if they return an error, that
|
||||
// means the other side's disconnected, which means that the main thread has panicked, which means
|
||||
// we're done.
|
||||
// We're allowing passing by value here because this is only called once, at the beginning of the
|
||||
// program, and the arguments that 'should' be passed by value (`receiver` and `size`) would
|
||||
// probably be more performant if accessed by-value instead of through a reference. Probably.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn start_rendering(
|
||||
path: &str,
|
||||
mut sender: Sender<Result<RenderInfo, RenderError>>,
|
||||
@@ -227,8 +231,8 @@ pub fn start_rendering(
|
||||
|
||||
// render the page
|
||||
match render_single_page_to_ctx(
|
||||
page,
|
||||
&search_term,
|
||||
&page,
|
||||
search_term.as_deref(),
|
||||
rendered_with_no_results,
|
||||
(area_w, area_h)
|
||||
) {
|
||||
@@ -251,11 +255,11 @@ pub fn start_rendering(
|
||||
// since the effects of parallelizing that will be noticeable if the user
|
||||
// tries to move through pages more quickly
|
||||
if num == start_point {
|
||||
render_ctx_to_png(ctx, &mut sender, (col_w, col_h), num)?;
|
||||
render_ctx_to_png(&ctx, &mut sender, (col_w, col_h), num)?;
|
||||
} else {
|
||||
let mut sender = sender.clone();
|
||||
thread::spawn(move || {
|
||||
render_ctx_to_png(ctx, &mut sender, (col_w, col_h), num)
|
||||
render_ctx_to_png(&ctx, &mut sender, (col_w, col_h), num)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -298,8 +302,8 @@ struct RenderedContext {
|
||||
unsafe impl Send for RenderedContext {}
|
||||
|
||||
fn render_single_page_to_ctx(
|
||||
page: Page,
|
||||
search_term: &Option<String>,
|
||||
page: &Page,
|
||||
search_term: Option<&str>,
|
||||
already_rendered_no_results: bool,
|
||||
(area_w, area_h): (f64, f64)
|
||||
) -> Result<Option<RenderedContext>, String> {
|
||||
@@ -375,7 +379,7 @@ fn render_single_page_to_ctx(
|
||||
highlight_color.set_green((u16::MAX / 5) * 4);
|
||||
|
||||
let mut old_rect = Rectangle::new();
|
||||
for rect in result_rects.iter_mut() {
|
||||
for rect in &mut result_rects {
|
||||
// According to https://gitlab.freedesktop.org/poppler/poppler/-/issues/763, these rects
|
||||
// need to be corrected since they use different references as the y-coordinate base
|
||||
rect.set_y1(p_height - rect.y1());
|
||||
@@ -401,7 +405,7 @@ fn render_single_page_to_ctx(
|
||||
}
|
||||
|
||||
fn render_ctx_to_png(
|
||||
ctx: RenderedContext,
|
||||
ctx: &RenderedContext,
|
||||
sender: &mut Sender<Result<RenderInfo, RenderError>>,
|
||||
(col_w, col_h): (u16, u16),
|
||||
page: usize
|
||||
|
||||
+38
-8
@@ -1,4 +1,4 @@
|
||||
use std::{io::stdout, rc::Rc};
|
||||
use std::{io::stdout, num::NonZeroUsize, rc::Rc};
|
||||
|
||||
use crossterm::{
|
||||
event::{Event, KeyCode, MouseEventKind},
|
||||
@@ -24,7 +24,8 @@ pub struct Tui {
|
||||
// we use `prev_msg` to, for example, restore the 'search results' message on the bottom after
|
||||
// jumping to a specific page
|
||||
prev_msg: Option<BottomMessage>,
|
||||
rendered: Vec<RenderedInfo>
|
||||
rendered: Vec<RenderedInfo>,
|
||||
page_constraints: PageConstraints
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
@@ -50,6 +51,11 @@ pub enum InputCommand {
|
||||
Search(String)
|
||||
}
|
||||
|
||||
struct PageConstraints {
|
||||
max_wide: Option<NonZeroUsize>,
|
||||
r_to_l: bool
|
||||
}
|
||||
|
||||
// This seems like a kinda weird struct because it holds two optionals but any representation
|
||||
// within it is valid; I think it's the best way to represent it
|
||||
#[derive(Default)]
|
||||
@@ -64,14 +70,15 @@ struct RenderedInfo {
|
||||
}
|
||||
|
||||
impl Tui {
|
||||
pub fn new(name: String) -> Tui {
|
||||
pub fn new(name: String, max_wide: Option<NonZeroUsize>, r_to_l: bool) -> Tui {
|
||||
Self {
|
||||
name,
|
||||
page: 0,
|
||||
prev_msg: None,
|
||||
bottom_msg: BottomMessage::Help,
|
||||
last_render: LastRender::default(),
|
||||
rendered: vec![]
|
||||
rendered: vec![],
|
||||
page_constraints: PageConstraints { max_wide, r_to_l }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,13 +203,19 @@ impl Tui {
|
||||
// here we calculate how many pages can fit in the available area.
|
||||
let mut test_area_w = img_area.width;
|
||||
// go through our pages, starting at the first one we want to view
|
||||
let page_widths = self.rendered[self.page..]
|
||||
let mut page_widths = self.rendered[self.page..]
|
||||
.iter()
|
||||
// and get their indices (I know it's offset, we fix it down below when we actually
|
||||
// render each page)
|
||||
.enumerate()
|
||||
// and only take as many as are ready to be rendered
|
||||
.take_while(|(_, page)| page.img.is_some())
|
||||
.take_while(|(idx, page)| {
|
||||
let mut take = page.img.is_some();
|
||||
if let Some(max) = self.page_constraints.max_wide {
|
||||
take &= *idx < max.get();
|
||||
}
|
||||
take
|
||||
})
|
||||
// and map it to their width (in cells on the terminal, not pixels)
|
||||
.filter_map(|(idx, page)| page.img.as_ref().map(|img| (idx, img.rect().width)))
|
||||
// and then take them as long as they won't overflow the available area.
|
||||
@@ -215,6 +228,10 @@ impl Tui {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if self.page_constraints.r_to_l {
|
||||
page_widths.reverse();
|
||||
}
|
||||
|
||||
if page_widths.is_empty() {
|
||||
// If none are ready to render, just show the loading thing
|
||||
Self::render_loading_in(frame, img_area);
|
||||
@@ -268,12 +285,23 @@ impl Tui {
|
||||
frame.render_widget(loading_span, inner_space[0]);
|
||||
}
|
||||
|
||||
fn change_page(&mut self, change: PageChange, amt: ChangeAmount) -> Option<InputAction> {
|
||||
fn change_page(&mut self, mut change: PageChange, amt: ChangeAmount) -> Option<InputAction> {
|
||||
let diff = match amt {
|
||||
ChangeAmount::Single => 1,
|
||||
ChangeAmount::WholeScreen => self.last_render.pages_shown
|
||||
};
|
||||
|
||||
// This is a kinda weird way to switch around the controls for this sort of thing but it
|
||||
// allows it to be pretty centralized and avoids annoyingly duplicated match arms (since
|
||||
// we'd have to do `match key { 'h' if r_to_l | 'l' => {}}` and that doesn't play well with
|
||||
// `if` guards on match arms)
|
||||
if self.page_constraints.r_to_l {
|
||||
change = match change {
|
||||
PageChange::Next => PageChange::Prev,
|
||||
PageChange::Prev => PageChange::Next
|
||||
};
|
||||
}
|
||||
|
||||
let old = self.page;
|
||||
match change {
|
||||
PageChange::Next => self.set_page((self.page + diff).min(self.rendered.len() - 1)),
|
||||
@@ -323,7 +351,7 @@ impl Tui {
|
||||
self.rendered[page_num].num_results = Some(num_results);
|
||||
}
|
||||
|
||||
pub fn handle_event(&mut self, ev: Event) -> Option<InputAction> {
|
||||
pub fn handle_event(&mut self, ev: &Event) -> Option<InputAction> {
|
||||
fn jump_to_page(
|
||||
page: &mut usize,
|
||||
rect: &mut Rect,
|
||||
@@ -522,11 +550,13 @@ pub enum InputAction {
|
||||
QuitApp
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum PageChange {
|
||||
Prev,
|
||||
Next
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum ChangeAmount {
|
||||
WholeScreen,
|
||||
Single
|
||||
|
||||
Reference in New Issue
Block a user