mirror of
https://github.com/itsjunetime/tdf.git
synced 2026-06-01 23:51:46 -04:00
Add formatter and format
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
hard_tabs = true
|
||||||
|
match_arm_blocks = false
|
||||||
|
imports_granularity = "Crate"
|
||||||
|
overflow_delimited_expr = true
|
||||||
|
group_imports = "StdExternalCrate"
|
||||||
|
trailing_comma = "Never"
|
||||||
|
use_field_init_shorthand = true
|
||||||
+15
-7
@@ -1,4 +1,7 @@
|
|||||||
use std::{pin::Pin, task::{Context, Poll}};
|
use std::{
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll}
|
||||||
|
};
|
||||||
|
|
||||||
use futures_util::Stream;
|
use futures_util::Stream;
|
||||||
use image::ImageFormat;
|
use image::ImageFormat;
|
||||||
@@ -79,15 +82,17 @@ impl Converter {
|
|||||||
.interleave(self.page..idx_end)
|
.interleave(self.page..idx_end)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.skip(self.iteration)
|
.skip(self.iteration)
|
||||||
.find_map(|(i_idx, p_idx)|
|
.find_map(|(i_idx, p_idx)| self.images[p_idx].take().map(|p| (p, i_idx)))?;
|
||||||
self.images[p_idx].take().map(|p| (p, i_idx))
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let img_area = page_info.img_data.area;
|
let img_area = page_info.img_data.area;
|
||||||
|
|
||||||
let dyn_img = match image::load_from_memory_with_format(&page_info.img_data.data, ImageFormat::Png) {
|
let dyn_img =
|
||||||
|
match image::load_from_memory_with_format(&page_info.img_data.data, ImageFormat::Png) {
|
||||||
Ok(dt) => dt,
|
Ok(dt) => dt,
|
||||||
Err(e) => return Some(Err(RenderError::Render(format!("Couldn't convert Vec<u8> to DynamicImage: {e}"))))
|
Err(e) =>
|
||||||
|
return Some(Err(RenderError::Render(format!(
|
||||||
|
"Couldn't convert Vec<u8> to DynamicImage: {e}"
|
||||||
|
)))),
|
||||||
};
|
};
|
||||||
|
|
||||||
// We don't actually want to Crop this image, but we've already
|
// We don't actually want to Crop this image, but we've already
|
||||||
@@ -96,7 +101,10 @@ impl Converter {
|
|||||||
// resize it, we tell them to crop it to fit.
|
// resize it, we tell them to crop it to fit.
|
||||||
let txt_img = match self.picker.new_protocol(dyn_img, img_area, Resize::Crop) {
|
let txt_img = match self.picker.new_protocol(dyn_img, img_area, Resize::Crop) {
|
||||||
Ok(img) => img,
|
Ok(img) => img,
|
||||||
Err(e) => return Some(Err(RenderError::Render(format!("Couldn't convert DynamicImage to ratatui image: {e}"))))
|
Err(e) =>
|
||||||
|
return Some(Err(RenderError::Render(format!(
|
||||||
|
"Couldn't convert DynamicImage to ratatui image: {e}"
|
||||||
|
)))),
|
||||||
};
|
};
|
||||||
|
|
||||||
// update the iteration to the iteration that we stole this image from
|
// update the iteration to the iteration that we stole this image from
|
||||||
|
|||||||
+18
-9
@@ -2,20 +2,26 @@
|
|||||||
|
|
||||||
use std::{io::stdout, path::PathBuf, str::FromStr};
|
use std::{io::stdout, path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
use converter::{Converter, ConvertedPage};
|
use converter::{ConvertedPage, Converter};
|
||||||
use crossterm::{execute, terminal::{disable_raw_mode, enable_raw_mode, EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen}};
|
use crossterm::{
|
||||||
|
execute,
|
||||||
|
terminal::{
|
||||||
|
disable_raw_mode, enable_raw_mode, EndSynchronizedUpdate, EnterAlternateScreen,
|
||||||
|
LeaveAlternateScreen
|
||||||
|
}
|
||||||
|
};
|
||||||
|
use futures_util::stream::StreamExt;
|
||||||
use glib::{LogField, LogLevel, LogWriterOutput};
|
use glib::{LogField, LogLevel, LogWriterOutput};
|
||||||
use notify::{RecursiveMode, Watcher};
|
use notify::{RecursiveMode, Watcher};
|
||||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||||
use ratatui_image::picker::Picker;
|
use ratatui_image::picker::Picker;
|
||||||
use tui::{InputAction, Tui};
|
|
||||||
use futures_util::stream::StreamExt;
|
|
||||||
use renderer::{RenderInfo, RenderNotif};
|
use renderer::{RenderInfo, RenderNotif};
|
||||||
|
use tui::{InputAction, Tui};
|
||||||
|
|
||||||
mod tui;
|
|
||||||
mod renderer;
|
|
||||||
mod converter;
|
mod converter;
|
||||||
|
mod renderer;
|
||||||
mod skip;
|
mod skip;
|
||||||
|
mod tui;
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
@@ -33,7 +39,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// This shouldn't fail to send unless the receiver gets disconnected. If that's happened,
|
// 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 if this panics
|
// then like the main thread has panicked or something, so it doesn't matter if this panics
|
||||||
// as well
|
// as well
|
||||||
watch_tx.blocking_send(renderer::RenderNotif::Reload).unwrap();
|
watch_tx
|
||||||
|
.blocking_send(renderer::RenderNotif::Reload)
|
||||||
|
.unwrap();
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// We're making this nonrecursive 'cause we're just watching a single file, so there's nothing
|
// We're making this nonrecursive 'cause we're just watching a single file, so there's nothing
|
||||||
@@ -51,11 +59,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// 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,
|
||||||
// since the methods we call in `start_rendering` will panic if called in an async context
|
// since the methods we call in `start_rendering` will panic if called in an async context
|
||||||
std::thread::spawn(move || { renderer::start_rendering(file_path, render_tx, render_rx) });
|
std::thread::spawn(move || renderer::start_rendering(file_path, render_tx, render_rx));
|
||||||
|
|
||||||
let mut ev_stream = crossterm::event::EventStream::new();
|
let mut ev_stream = crossterm::event::EventStream::new();
|
||||||
|
|
||||||
let file_name = path.file_name()
|
let file_name = path
|
||||||
|
.file_name()
|
||||||
.map(|n| n.to_string_lossy())
|
.map(|n| n.to_string_lossy())
|
||||||
.unwrap_or_else(|| "Unknown file".into())
|
.unwrap_or_else(|| "Unknown file".into())
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|||||||
+33
-24
@@ -1,6 +1,6 @@
|
|||||||
use cairo::{Antialias, Format};
|
use cairo::{Antialias, Format};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use poppler::{Color, Document, FindFlags, Page, SelectionStyle, Rectangle};
|
use poppler::{Color, Document, FindFlags, Page, Rectangle, SelectionStyle};
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use tokio::sync::mpsc::{error::TryRecvError, Receiver, Sender};
|
use tokio::sync::mpsc::{error::TryRecvError, Receiver, Sender};
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ pub enum RenderError {
|
|||||||
|
|
||||||
pub enum RenderInfo {
|
pub enum RenderInfo {
|
||||||
NumPages(usize),
|
NumPages(usize),
|
||||||
Page(PageInfo),
|
Page(PageInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PageInfo {
|
pub struct PageInfo {
|
||||||
@@ -71,7 +71,7 @@ pub fn start_rendering(
|
|||||||
area = r;
|
area = r;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// We want this outside of 'reload so that if the doc reloads, the search term that somebody
|
// We want this outside of 'reload so that if the doc reloads, the search term that somebody
|
||||||
// set will still get highlighted in the reloaded doc
|
// set will still get highlighted in the reloaded doc
|
||||||
@@ -82,12 +82,14 @@ pub fn start_rendering(
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
sender.blocking_send(Err(RenderError::Doc(e))).unwrap();
|
sender.blocking_send(Err(RenderError::Doc(e))).unwrap();
|
||||||
return;
|
return;
|
||||||
},
|
}
|
||||||
Ok(d) => d
|
Ok(d) => d
|
||||||
};
|
};
|
||||||
|
|
||||||
let n_pages = doc.n_pages() as usize;
|
let n_pages = doc.n_pages() as usize;
|
||||||
sender.blocking_send(Ok(RenderInfo::NumPages(n_pages))).unwrap();
|
sender
|
||||||
|
.blocking_send(Ok(RenderInfo::NumPages(n_pages)))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// We're using this vec of bools to indicate which page numbers have already been rendered,
|
// We're using this vec of bools to indicate which page numbers have already been rendered,
|
||||||
// to support people jumping to specific pages and having quick rendering results. We
|
// to support people jumping to specific pages and having quick rendering results. We
|
||||||
@@ -110,7 +112,8 @@ pub fn start_rendering(
|
|||||||
match $notif {
|
match $notif {
|
||||||
RenderNotif::Reload => continue 'reload,
|
RenderNotif::Reload => continue 'reload,
|
||||||
RenderNotif::Area(new_area) => {
|
RenderNotif::Area(new_area) => {
|
||||||
let bigger = new_area.width > area.width || new_area.height > area.height;
|
let bigger =
|
||||||
|
new_area.width > area.width || new_area.height > area.height;
|
||||||
area = new_area;
|
area = new_area;
|
||||||
// we only want to re-render pages if the new area is greater than the old
|
// we only want to re-render pages if the new area is greater than the old
|
||||||
// one, 'cause then we might need sharper images to make it all look good.
|
// one, 'cause then we might need sharper images to make it all look good.
|
||||||
@@ -120,11 +123,11 @@ pub fn start_rendering(
|
|||||||
fill_default(&mut rendered, n_pages);
|
fill_default(&mut rendered, n_pages);
|
||||||
continue 'render_pages;
|
continue 'render_pages;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
RenderNotif::JumpToPage(page) => {
|
RenderNotif::JumpToPage(page) => {
|
||||||
start_point = page;
|
start_point = page;
|
||||||
continue 'render_pages;
|
continue 'render_pages;
|
||||||
},
|
}
|
||||||
RenderNotif::Search(term) => {
|
RenderNotif::Search(term) => {
|
||||||
if term.is_empty() {
|
if term.is_empty() {
|
||||||
// If the term is set to nothing, then we don't need to re-render
|
// If the term is set to nothing, then we don't need to re-render
|
||||||
@@ -149,12 +152,13 @@ pub fn start_rendering(
|
|||||||
continue 'render_pages;
|
continue 'render_pages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let (left, right) = rendered.split_at_mut(start_point);
|
let (left, right) = rendered.split_at_mut(start_point);
|
||||||
|
|
||||||
let page_iter = right.iter_mut()
|
let page_iter = right
|
||||||
|
.iter_mut()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(idx, p)| (idx + start_point, p))
|
.map(|(idx, p)| (idx + start_point, p))
|
||||||
.interleave(
|
.interleave(
|
||||||
@@ -185,14 +189,17 @@ pub fn start_rendering(
|
|||||||
|
|
||||||
// We know this is in range 'cause we're iterating over it
|
// We know this is in range 'cause we're iterating over it
|
||||||
let Some(page) = doc.page(num as i32) else {
|
let Some(page) = doc.page(num as i32) else {
|
||||||
sender.blocking_send(
|
sender
|
||||||
Err(RenderError::Render(format!("Couldn't get page {num} ({}) of doc?", num as i32)))
|
.blocking_send(Err(RenderError::Render(format!(
|
||||||
)
|
"Couldn't get page {num} ({}) of doc?",
|
||||||
|
num as i32
|
||||||
|
))))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let rendered_with_no_results = rendered.successful && rendered.contained_term == Some(false);
|
let rendered_with_no_results =
|
||||||
|
rendered.successful && rendered.contained_term == Some(false);
|
||||||
|
|
||||||
// render the page
|
// render the page
|
||||||
match render_single_page(page, area, num, &search_term, rendered_with_no_results) {
|
match render_single_page(page, area, num, &search_term, rendered_with_no_results) {
|
||||||
@@ -205,14 +212,14 @@ pub fn start_rendering(
|
|||||||
// And if we got an error, then obviously we need to propagate that
|
// And if we got an error, then obviously we need to propagate that
|
||||||
Err(e) => sender.blocking_send(Err(RenderError::Render(e))).unwrap()
|
Err(e) => sender.blocking_send(Err(RenderError::Render(e))).unwrap()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
// Then once we've rendered all these pages, wait until we get another notification
|
// Then once we've rendered all these pages, wait until we get another notification
|
||||||
// that this doc needs to be reloaded
|
// that this doc needs to be reloaded
|
||||||
loop {
|
loop {
|
||||||
// This once returned None despite the main thing being still connected (I think, at
|
// This once returned None despite the main thing being still connected (I think, at
|
||||||
// last), so I'm just being safe here
|
// last), so I'm just being safe here
|
||||||
let Some(msg) = receiver.blocking_recv() else {
|
let Some(msg) = receiver.blocking_recv() else {
|
||||||
return
|
return;
|
||||||
};
|
};
|
||||||
handle_notif!(msg);
|
handle_notif!(msg);
|
||||||
}
|
}
|
||||||
@@ -235,13 +242,13 @@ fn render_single_page(
|
|||||||
// If there are no search terms on this page, and we've already rendered it with no search
|
// If there are no search terms on this page, and we've already rendered it with no search
|
||||||
// terms, then just return none to avoid this computation
|
// terms, then just return none to avoid this computation
|
||||||
if result_rects.is_empty() && already_rendered_no_results {
|
if result_rects.is_empty() && already_rendered_no_results {
|
||||||
return Ok(None)
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, get the font size; the number of pixels (width x height) per font character (I
|
// First, get the font size; the number of pixels (width x height) per font character (I
|
||||||
// think; it's at least something like that) on this terminal screen.
|
// think; it's at least something like that) on this terminal screen.
|
||||||
let size = crossterm::terminal::window_size()
|
let size =
|
||||||
.map_err(|e| format!("Couldn't get window size: {e}"))?;
|
crossterm::terminal::window_size().map_err(|e| format!("Couldn't get window size: {e}"))?;
|
||||||
let col_h = size.height / size.rows;
|
let col_h = size.height / size.rows;
|
||||||
let col_w = size.width / size.columns;
|
let col_w = size.width / size.columns;
|
||||||
|
|
||||||
@@ -285,9 +292,9 @@ fn render_single_page(
|
|||||||
// rendered at higher quality.
|
// rendered at higher quality.
|
||||||
surface_width as i32,
|
surface_width as i32,
|
||||||
surface_height as i32
|
surface_height as i32
|
||||||
).map_err(|e| format!("Couldn't create ImageSurface: {e}"))?;
|
)
|
||||||
let ctx = cairo::Context::new(surface)
|
.map_err(|e| format!("Couldn't create ImageSurface: {e}"))?;
|
||||||
.map_err(|e| format!("Couldn't create Context: {e}"))?;
|
let ctx = cairo::Context::new(surface).map_err(|e| format!("Couldn't create Context: {e}"))?;
|
||||||
|
|
||||||
ctx.scale(scale_factor, scale_factor);
|
ctx.scale(scale_factor, scale_factor);
|
||||||
|
|
||||||
@@ -296,7 +303,8 @@ fn render_single_page(
|
|||||||
ctx.set_source_rgba(1.0, 1.0, 1.0, 1.0);
|
ctx.set_source_rgba(1.0, 1.0, 1.0, 1.0);
|
||||||
|
|
||||||
ctx.set_antialias(Antialias::Best);
|
ctx.set_antialias(Antialias::Best);
|
||||||
ctx.paint().map_err(|e| format!("Couldn't paint Context: {e}"))?;
|
ctx.paint()
|
||||||
|
.map_err(|e| format!("Couldn't paint Context: {e}"))?;
|
||||||
page.render(&ctx);
|
page.render(&ctx);
|
||||||
|
|
||||||
let num_results = result_rects.len();
|
let num_results = result_rects.len();
|
||||||
@@ -325,7 +333,8 @@ fn render_single_page(
|
|||||||
ctx.scale(1. / scale_factor, 1. / scale_factor);
|
ctx.scale(1. / scale_factor, 1. / scale_factor);
|
||||||
|
|
||||||
let mut img_data = Vec::new();
|
let mut img_data = Vec::new();
|
||||||
ctx.target().write_to_png(&mut img_data)
|
ctx.target()
|
||||||
|
.write_to_png(&mut img_data)
|
||||||
.map_err(|e| format!("Couldn't write surface to png: {e}"))?;
|
.map_err(|e| format!("Couldn't write surface to png: {e}"))?;
|
||||||
|
|
||||||
Ok(Some(PageInfo {
|
Ok(Some(PageInfo {
|
||||||
|
|||||||
+116
-90
@@ -1,7 +1,17 @@
|
|||||||
use std::{io::stdout, rc::Rc};
|
use std::{io::stdout, rc::Rc};
|
||||||
|
|
||||||
use crossterm::{event::{Event, KeyCode, MouseEventKind}, execute, terminal::BeginSynchronizedUpdate};
|
use crossterm::{
|
||||||
use ratatui::{layout::{Constraint, Flex, Layout, Rect}, style::{Color, Style}, text::Span, widgets::{Block, Borders, Padding}, Frame};
|
event::{Event, KeyCode, MouseEventKind},
|
||||||
|
execute,
|
||||||
|
terminal::BeginSynchronizedUpdate
|
||||||
|
};
|
||||||
|
use ratatui::{
|
||||||
|
layout::{Constraint, Flex, Layout, Rect},
|
||||||
|
style::{Color, Style},
|
||||||
|
text::Span,
|
||||||
|
widgets::{Block, Borders, Padding},
|
||||||
|
Frame
|
||||||
|
};
|
||||||
use ratatui_image::{protocol::Protocol, Image};
|
use ratatui_image::{protocol::Protocol, Image};
|
||||||
|
|
||||||
use crate::{renderer::RenderError, skip::Skip};
|
use crate::{renderer::RenderError, skip::Skip};
|
||||||
@@ -14,7 +24,7 @@ pub struct Tui {
|
|||||||
// we use `prev_msg` to, for example, restore the 'search results' message on the bottom after
|
// we use `prev_msg` to, for example, restore the 'search results' message on the bottom after
|
||||||
// jumping to a specific page
|
// jumping to a specific page
|
||||||
prev_msg: Option<BottomMessage>,
|
prev_msg: Option<BottomMessage>,
|
||||||
rendered: Vec<RenderedInfo>,
|
rendered: Vec<RenderedInfo>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
@@ -61,7 +71,7 @@ impl Tui {
|
|||||||
prev_msg: None,
|
prev_msg: None,
|
||||||
bottom_msg: BottomMessage::Help,
|
bottom_msg: BottomMessage::Help,
|
||||||
last_render: LastRender::default(),
|
last_render: LastRender::default(),
|
||||||
rendered: vec![],
|
rendered: vec![]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,19 +103,12 @@ impl Tui {
|
|||||||
let top_layout = Layout::horizontal([
|
let top_layout = Layout::horizontal([
|
||||||
Constraint::Fill(1),
|
Constraint::Fill(1),
|
||||||
Constraint::Length(page_nums_text.len() as u16)
|
Constraint::Length(page_nums_text.len() as u16)
|
||||||
]).split(top_area);
|
])
|
||||||
|
.split(top_area);
|
||||||
|
|
||||||
let title = Span::styled(
|
let title = Span::styled(&self.name, Style::new().fg(Color::Cyan));
|
||||||
&self.name,
|
|
||||||
Style::new()
|
|
||||||
.fg(Color::Cyan)
|
|
||||||
);
|
|
||||||
|
|
||||||
let page_nums = Span::styled(
|
let page_nums = Span::styled(&page_nums_text, Style::new().fg(Color::Cyan));
|
||||||
&page_nums_text,
|
|
||||||
Style::new()
|
|
||||||
.fg(Color::Cyan)
|
|
||||||
);
|
|
||||||
|
|
||||||
frame.render_widget(top_block, main_area[0]);
|
frame.render_widget(top_block, main_area[0]);
|
||||||
frame.render_widget(title, top_layout[0]);
|
frame.render_widget(title, top_layout[0]);
|
||||||
@@ -126,7 +129,8 @@ impl Tui {
|
|||||||
let rendered_str = if !self.rendered.is_empty() {
|
let rendered_str = if !self.rendered.is_empty() {
|
||||||
format!(
|
format!(
|
||||||
"Rendered: {}%",
|
"Rendered: {}%",
|
||||||
(self.rendered.iter().filter(|i| i.img.is_some()).count() * 100) / self.rendered.len()
|
(self.rendered.iter().filter(|i| i.img.is_some()).count() * 100)
|
||||||
|
/ self.rendered.len()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
@@ -134,40 +138,45 @@ impl Tui {
|
|||||||
let bottom_layout = Layout::horizontal([
|
let bottom_layout = Layout::horizontal([
|
||||||
Constraint::Fill(1),
|
Constraint::Fill(1),
|
||||||
Constraint::Length(rendered_str.len() as u16)
|
Constraint::Length(rendered_str.len() as u16)
|
||||||
]).split(bottom_area);
|
])
|
||||||
|
.split(bottom_area);
|
||||||
|
|
||||||
|
let rendered_span = Span::styled(&rendered_str, Style::new().fg(Color::Cyan));
|
||||||
let rendered_span = Span::styled(
|
|
||||||
&rendered_str,
|
|
||||||
Style::new()
|
|
||||||
.fg(Color::Cyan)
|
|
||||||
);
|
|
||||||
frame.render_widget(rendered_span, bottom_layout[1]);
|
frame.render_widget(rendered_span, bottom_layout[1]);
|
||||||
|
|
||||||
let (msg_str, color) = match self.bottom_msg {
|
let (msg_str, color) = match self.bottom_msg {
|
||||||
BottomMessage::Help => (
|
BottomMessage::Help => (
|
||||||
"/: Search, g: Go To Page, n: Next Search Result, N: Previous Search Result".to_string(),
|
"/: Search, g: Go To Page, n: Next Search Result, N: Previous Search Result"
|
||||||
|
.to_string(),
|
||||||
Color::Blue
|
Color::Blue
|
||||||
),
|
),
|
||||||
BottomMessage::Error(ref e) => (
|
BottomMessage::Error(ref e) => (format!("Couldn't render a page: {e}"), Color::Red),
|
||||||
format!("Couldn't render a page: {e}"),
|
|
||||||
Color::Red
|
|
||||||
),
|
|
||||||
BottomMessage::Input(ref input_state) => (
|
BottomMessage::Input(ref input_state) => (
|
||||||
match input_state {
|
match input_state {
|
||||||
InputCommand::GoToPage(page) => format!("Go to: {page}"),
|
InputCommand::GoToPage(page) => format!("Go to: {page}"),
|
||||||
InputCommand::Search(s) => format!("Search: {s}"),
|
InputCommand::Search(s) => format!("Search: {s}")
|
||||||
},
|
},
|
||||||
Color::Blue
|
Color::Blue
|
||||||
),
|
),
|
||||||
BottomMessage::SearchResults(ref term) => {
|
BottomMessage::SearchResults(ref term) => {
|
||||||
let num_found = self.rendered.iter().filter_map(|r| r.num_results).sum::<usize>();
|
let num_found = self
|
||||||
let num_searched = self.rendered.iter().filter(|r| r.num_results.is_some()).count() * 100;
|
.rendered
|
||||||
|
.iter()
|
||||||
|
.filter_map(|r| r.num_results)
|
||||||
|
.sum::<usize>();
|
||||||
|
let num_searched = self
|
||||||
|
.rendered
|
||||||
|
.iter()
|
||||||
|
.filter(|r| r.num_results.is_some())
|
||||||
|
.count() * 100;
|
||||||
(
|
(
|
||||||
format!("Results for '{term}': {num_found} (searched: {}%)", num_searched / self.rendered.len()),
|
format!(
|
||||||
|
"Results for '{term}': {num_found} (searched: {}%)",
|
||||||
|
num_searched / self.rendered.len()
|
||||||
|
),
|
||||||
Color::Blue
|
Color::Blue
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let span = Span::styled(msg_str, Style::new().fg(color));
|
let span = Span::styled(msg_str, Style::new().fg(color));
|
||||||
@@ -186,28 +195,22 @@ impl Tui {
|
|||||||
// here we calculate how many pages can fit in the available area.
|
// here we calculate how many pages can fit in the available area.
|
||||||
let mut test_area_w = img_area.width;
|
let mut test_area_w = img_area.width;
|
||||||
// go through our pages, starting at the first one we want to view
|
// go through our pages, starting at the first one we want to view
|
||||||
let page_widths = self.rendered[self.page..].iter()
|
let page_widths = self.rendered[self.page..]
|
||||||
|
.iter()
|
||||||
// and get their indices (I know it's offset, we fix it down below when we actually
|
// and get their indices (I know it's offset, we fix it down below when we actually
|
||||||
// render each page)
|
// render each page)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
// and only take as many as are ready to be rendered
|
// and only take as many as are ready to be rendered
|
||||||
.take_while(|(_, page)| page.img.is_some())
|
.take_while(|(_, page)| page.img.is_some())
|
||||||
// and map it to their width (in cells on the terminal, not pixels)
|
// and map it to their width (in cells on the terminal, not pixels)
|
||||||
.flat_map(|(idx, page)|
|
.flat_map(|(idx, page)| page.img.as_ref().map(|img| (idx, img.rect().width)))
|
||||||
page.img.as_ref().map(|img| (
|
|
||||||
idx,
|
|
||||||
img.rect().width,
|
|
||||||
))
|
|
||||||
)
|
|
||||||
// and then take them as long as they won't overflow the available area.
|
// and then take them as long as they won't overflow the available area.
|
||||||
.take_while(|(_, width)| {
|
.take_while(|(_, width)| match test_area_w.checked_sub(*width) {
|
||||||
match test_area_w.checked_sub(*width) {
|
|
||||||
Some(new_val) => {
|
Some(new_val) => {
|
||||||
test_area_w = new_val;
|
test_area_w = new_val;
|
||||||
true
|
true
|
||||||
},
|
|
||||||
None => false
|
|
||||||
}
|
}
|
||||||
|
None => false
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
@@ -218,10 +221,7 @@ impl Tui {
|
|||||||
execute!(stdout(), BeginSynchronizedUpdate).unwrap();
|
execute!(stdout(), BeginSynchronizedUpdate).unwrap();
|
||||||
*end_update = true;
|
*end_update = true;
|
||||||
|
|
||||||
let total_width = page_widths
|
let total_width = page_widths.iter().map(|(_, w)| w).sum::<u16>();
|
||||||
.iter()
|
|
||||||
.map(|(_, w)| w)
|
|
||||||
.sum::<u16>();
|
|
||||||
|
|
||||||
self.last_render.pages_shown = page_widths.len();
|
self.last_render.pages_shown = page_widths.len();
|
||||||
|
|
||||||
@@ -236,7 +236,10 @@ impl Tui {
|
|||||||
// reference + an immutable reference (and also we need to potentially temporarily
|
// reference + an immutable reference (and also we need to potentially temporarily
|
||||||
// remove it from the array of rendered pages to replace it with a text-rendered
|
// remove it from the array of rendered pages to replace it with a text-rendered
|
||||||
// image)
|
// image)
|
||||||
self.render_single_page(frame, page_idx + self.page, Rect { width, ..img_area });
|
self.render_single_page(frame, page_idx + self.page, Rect {
|
||||||
|
width,
|
||||||
|
..img_area
|
||||||
|
});
|
||||||
img_area.x += width;
|
img_area.x += width;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,9 +259,8 @@ impl Tui {
|
|||||||
|
|
||||||
fn render_loading_in(frame: &mut Frame<'_>, area: Rect) {
|
fn render_loading_in(frame: &mut Frame<'_>, area: Rect) {
|
||||||
let loading_str = "Loading...";
|
let loading_str = "Loading...";
|
||||||
let inner_space = Layout::horizontal([
|
let inner_space = Layout::horizontal([Constraint::Length(loading_str.len() as u16)])
|
||||||
Constraint::Length(loading_str.len() as u16),
|
.flex(Flex::Center)
|
||||||
]).flex(Flex::Center)
|
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
let loading_span = Span::styled(loading_str, Style::new().fg(Color::Cyan));
|
let loading_span = Span::styled(loading_str, Style::new().fg(Color::Cyan));
|
||||||
@@ -275,7 +277,7 @@ impl Tui {
|
|||||||
let old = self.page;
|
let old = self.page;
|
||||||
match change {
|
match change {
|
||||||
PageChange::Next => self.set_page((self.page + diff).min(self.rendered.len() - 1)),
|
PageChange::Next => self.set_page((self.page + diff).min(self.rendered.len() - 1)),
|
||||||
PageChange::Prev => self.set_page(self.page.saturating_sub(diff)),
|
PageChange::Prev => self.set_page(self.page.saturating_sub(diff))
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.page as isize - old as isize {
|
match self.page as isize - old as isize {
|
||||||
@@ -312,7 +314,10 @@ impl Tui {
|
|||||||
// We always just set this here because we handle reloading in the `set_n_pages` function.
|
// We always just set this here because we handle reloading in the `set_n_pages` function.
|
||||||
// If the document was reloaded, then It'll have the `set_n_pages` called to set the new
|
// If the document was reloaded, then It'll have the `set_n_pages` called to set the new
|
||||||
// number of pages, so the vec will already be cleared
|
// number of pages, so the vec will already be cleared
|
||||||
self.rendered[page_num] = RenderedInfo { img: Some(img), num_results: Some(num_results) };
|
self.rendered[page_num] = RenderedInfo {
|
||||||
|
img: Some(img),
|
||||||
|
num_results: Some(num_results)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn got_num_results_on_page(&mut self, page_num: usize, num_results: usize) {
|
pub fn got_num_results_on_page(&mut self, page_num: usize, num_results: usize) {
|
||||||
@@ -320,7 +325,11 @@ impl Tui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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, new_page: Option<usize>) -> Option<InputAction> {
|
fn jump_to_page(
|
||||||
|
page: &mut usize,
|
||||||
|
rect: &mut Rect,
|
||||||
|
new_page: Option<usize>
|
||||||
|
) -> Option<InputAction> {
|
||||||
new_page.map(|new_page| {
|
new_page.map(|new_page| {
|
||||||
*page = new_page;
|
*page = new_page;
|
||||||
// Make sure we re-render
|
// Make sure we re-render
|
||||||
@@ -332,62 +341,73 @@ impl Tui {
|
|||||||
match ev {
|
match ev {
|
||||||
Event::Key(key) => {
|
Event::Key(key) => {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char(c) if let BottomMessage::Input(InputCommand::Search(ref mut term)) = self.bottom_msg => {
|
KeyCode::Char(c)
|
||||||
|
if let BottomMessage::Input(InputCommand::Search(ref mut term)) =
|
||||||
|
self.bottom_msg =>
|
||||||
|
{
|
||||||
term.push(c);
|
term.push(c);
|
||||||
Some(InputAction::Redraw)
|
Some(InputAction::Redraw)
|
||||||
},
|
}
|
||||||
KeyCode::Char(c) if let BottomMessage::Input(InputCommand::GoToPage(ref mut page)) = self.bottom_msg => {
|
KeyCode::Char(c)
|
||||||
c.to_digit(10)
|
if let BottomMessage::Input(InputCommand::GoToPage(ref mut page)) =
|
||||||
.map(|input_num| {
|
self.bottom_msg =>
|
||||||
|
c.to_digit(10).map(|input_num| {
|
||||||
*page = (*page * 10) + input_num as usize;
|
*page = (*page * 10) + input_num as usize;
|
||||||
InputAction::Redraw
|
InputAction::Redraw
|
||||||
})
|
}),
|
||||||
},
|
KeyCode::Right | KeyCode::Char('l') =>
|
||||||
KeyCode::Right | KeyCode::Char('l') => self.change_page(PageChange::Next, ChangeAmount::Single),
|
self.change_page(PageChange::Next, ChangeAmount::Single),
|
||||||
KeyCode::Down | KeyCode::Char('j') => self.change_page(PageChange::Next, ChangeAmount::WholeScreen),
|
KeyCode::Down | KeyCode::Char('j') =>
|
||||||
KeyCode::Left | KeyCode::Char('h') => self.change_page(PageChange::Prev, ChangeAmount::Single),
|
self.change_page(PageChange::Next, ChangeAmount::WholeScreen),
|
||||||
KeyCode::Up | KeyCode::Char('k') => self.change_page(PageChange::Prev, ChangeAmount::WholeScreen),
|
KeyCode::Left | KeyCode::Char('h') =>
|
||||||
|
self.change_page(PageChange::Prev, ChangeAmount::Single),
|
||||||
|
KeyCode::Up | KeyCode::Char('k') =>
|
||||||
|
self.change_page(PageChange::Prev, ChangeAmount::WholeScreen),
|
||||||
KeyCode::Esc => match self.bottom_msg {
|
KeyCode::Esc => match self.bottom_msg {
|
||||||
BottomMessage::Input(_) => {
|
BottomMessage::Input(_) => {
|
||||||
self.set_bottom_msg(None);
|
self.set_bottom_msg(None);
|
||||||
Some(InputAction::Redraw)
|
Some(InputAction::Redraw)
|
||||||
},
|
}
|
||||||
_ => Some(InputAction::QuitApp)
|
_ => Some(InputAction::QuitApp)
|
||||||
},
|
},
|
||||||
KeyCode::Char('q') => Some(InputAction::QuitApp),
|
KeyCode::Char('q') => Some(InputAction::QuitApp),
|
||||||
KeyCode::Char('g') => {
|
KeyCode::Char('g') => {
|
||||||
self.set_bottom_msg(Some(BottomMessage::Input(InputCommand::GoToPage(0))));
|
self.set_bottom_msg(Some(BottomMessage::Input(InputCommand::GoToPage(0))));
|
||||||
Some(InputAction::Redraw)
|
Some(InputAction::Redraw)
|
||||||
},
|
}
|
||||||
KeyCode::Char('/') => {
|
KeyCode::Char('/') => {
|
||||||
self.set_bottom_msg(Some(BottomMessage::Input(InputCommand::Search(String::new()))));
|
self.set_bottom_msg(Some(BottomMessage::Input(InputCommand::Search(
|
||||||
|
String::new()
|
||||||
|
))));
|
||||||
Some(InputAction::Redraw)
|
Some(InputAction::Redraw)
|
||||||
},
|
}
|
||||||
KeyCode::Char('n') if self.page < self.rendered.len() - 1 => {
|
KeyCode::Char('n') if self.page < self.rendered.len() - 1 => {
|
||||||
// TODO: If we can't find one, then maybe like block until we've verified
|
// TODO: If we can't find one, then maybe like block until we've verified
|
||||||
// all the pages have been checked?
|
// all the pages have been checked?
|
||||||
let next_page = self.rendered[(self.page + 1)..]
|
let next_page = self.rendered[(self.page + 1)..]
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.find_map(|(idx, p)| p.num_results
|
.find_map(|(idx, p)| {
|
||||||
|
p.num_results
|
||||||
.is_some_and(|num| num > 0)
|
.is_some_and(|num| num > 0)
|
||||||
.then_some(self.page + 1 + idx)
|
.then_some(self.page + 1 + idx)
|
||||||
);
|
});
|
||||||
|
|
||||||
jump_to_page(&mut self.page, &mut self.last_render.rect, next_page)
|
jump_to_page(&mut self.page, &mut self.last_render.rect, next_page)
|
||||||
},
|
}
|
||||||
KeyCode::Char('N') if self.page > 0 => {
|
KeyCode::Char('N') if self.page > 0 => {
|
||||||
let prev_page = self.rendered[..(self.page)]
|
let prev_page = self.rendered[..(self.page)]
|
||||||
.iter()
|
.iter()
|
||||||
.rev()
|
.rev()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.find_map(|(idx, p)| p.num_results
|
.find_map(|(idx, p)| {
|
||||||
|
p.num_results
|
||||||
.is_some_and(|num| num > 0)
|
.is_some_and(|num| num > 0)
|
||||||
.then_some(self.page - (idx + 1))
|
.then_some(self.page - (idx + 1))
|
||||||
);
|
});
|
||||||
|
|
||||||
jump_to_page(&mut self.page, &mut self.last_render.rect, prev_page)
|
jump_to_page(&mut self.page, &mut self.last_render.rect, prev_page)
|
||||||
},
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
let BottomMessage::Input(_) = self.bottom_msg else {
|
let BottomMessage::Input(_) = self.bottom_msg else {
|
||||||
return None;
|
return None;
|
||||||
@@ -409,14 +429,16 @@ impl Tui {
|
|||||||
self.set_page(page);
|
self.set_page(page);
|
||||||
InputAction::JumpingToPage(page)
|
InputAction::JumpingToPage(page)
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
InputCommand::Search(term) => {
|
InputCommand::Search(term) => {
|
||||||
let term = term.clone();
|
let term = term.clone();
|
||||||
|
|
||||||
// We only want to show search results if there would actually be
|
// We only want to show search results if there would actually be
|
||||||
// data to show
|
// data to show
|
||||||
if !term.is_empty() {
|
if !term.is_empty() {
|
||||||
self.set_bottom_msg(Some(BottomMessage::SearchResults(term.clone())));
|
self.set_bottom_msg(Some(BottomMessage::SearchResults(
|
||||||
|
term.clone()
|
||||||
|
)));
|
||||||
} else {
|
} else {
|
||||||
// else, if it's not empty, we just want to reset the bottom
|
// else, if it's not empty, we just want to reset the bottom
|
||||||
// area to show the default data; we don't want it to like show
|
// area to show the default data; we don't want it to like show
|
||||||
@@ -434,21 +456,25 @@ impl Tui {
|
|||||||
Some(InputAction::Search(term))
|
Some(InputAction::Search(term))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
_ => None,
|
|
||||||
}
|
}
|
||||||
},
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
Event::Mouse(mouse) => match mouse.kind {
|
Event::Mouse(mouse) => match mouse.kind {
|
||||||
MouseEventKind::ScrollRight => self.change_page(PageChange::Next, ChangeAmount::Single),
|
MouseEventKind::ScrollRight =>
|
||||||
MouseEventKind::ScrollDown => self.change_page(PageChange::Next, ChangeAmount::WholeScreen),
|
self.change_page(PageChange::Next, ChangeAmount::Single),
|
||||||
MouseEventKind::ScrollLeft => self.change_page(PageChange::Prev, ChangeAmount::Single),
|
MouseEventKind::ScrollDown =>
|
||||||
MouseEventKind::ScrollUp => self.change_page(PageChange::Prev, ChangeAmount::WholeScreen),
|
self.change_page(PageChange::Next, ChangeAmount::WholeScreen),
|
||||||
_ => None,
|
MouseEventKind::ScrollLeft =>
|
||||||
}
|
self.change_page(PageChange::Prev, ChangeAmount::Single),
|
||||||
|
MouseEventKind::ScrollUp =>
|
||||||
|
self.change_page(PageChange::Prev, ChangeAmount::WholeScreen),
|
||||||
|
_ => None
|
||||||
|
},
|
||||||
// One of these options is Event::Resize, and we don't care about that because
|
// One of these options is Event::Resize, and we don't care about that because
|
||||||
// we always check, regardless, if the available area for the images has
|
// we always check, regardless, if the available area for the images has
|
||||||
// changed.
|
// changed.
|
||||||
_ => None,
|
_ => None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,12 +500,12 @@ impl Tui {
|
|||||||
Some(mut msg) => {
|
Some(mut msg) => {
|
||||||
std::mem::swap(&mut self.bottom_msg, &mut msg);
|
std::mem::swap(&mut self.bottom_msg, &mut msg);
|
||||||
self.prev_msg = Some(msg);
|
self.prev_msg = Some(msg);
|
||||||
},
|
}
|
||||||
None => {
|
None => {
|
||||||
let mut new_bottom = self.prev_msg.take().unwrap_or_default();
|
let mut new_bottom = self.prev_msg.take().unwrap_or_default();
|
||||||
std::mem::swap(&mut self.bottom_msg, &mut new_bottom);
|
std::mem::swap(&mut self.bottom_msg, &mut new_bottom);
|
||||||
self.prev_msg = Some(new_bottom);
|
self.prev_msg = Some(new_bottom);
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user