aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.rs254
1 files changed, 254 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..e096cee
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,254 @@
+use std::fs::File;
+use std::io::{stdout, Read, Write};
+
+use crossterm::{
+ cursor,
+ event::{read, Event, KeyCode, KeyEvent},
+ queue,
+ style::Print,
+ terminal,
+};
+
+struct Bk {
+ cols: u16,
+ chapter: Vec<String>,
+ chapter_idx: usize,
+ container: zip::ZipArchive<File>,
+ pos: usize,
+ rows: usize,
+ toc: Vec<String>,
+}
+
+fn get_toc(container: &mut zip::ZipArchive<File>) -> Vec<String> {
+ // container.xml -> <rootfile> -> opf -> <manifest>
+ let mut container_xml = String::new();
+ container
+ .by_name("META-INF/container.xml")
+ .unwrap()
+ .read_to_string(&mut container_xml)
+ .unwrap();
+ let opf_doc = roxmltree::Document::parse(&container_xml).unwrap();
+ let opf_path = opf_doc
+ .descendants()
+ .find(|n| n.tag_name().name() == "rootfile")
+ .unwrap()
+ .attribute("full-path")
+ .unwrap();
+
+ let mut opf_xml = String::new();
+ container
+ .by_name(opf_path)
+ .unwrap()
+ .read_to_string(&mut opf_xml)
+ .unwrap();
+
+ let parent_path = std::path::Path::new(&opf_path).parent().unwrap();
+ roxmltree::Document::parse(&opf_xml)
+ .unwrap()
+ .descendants()
+ .find(|n| n.tag_name().name() == "manifest")
+ .unwrap()
+ .children()
+ .filter(|n| {
+ n.is_element()
+ && n.attribute("media-type").unwrap()
+ == "application/xhtml+xml"
+ })
+ .map(|n| {
+ parent_path
+ .join(n.attribute("href").unwrap())
+ .to_str()
+ .unwrap()
+ .to_string()
+ })
+ .collect()
+}
+
+fn wrap(text: Vec<String>, width: u16) -> Vec<String> {
+ // XXX assumes a char is 1 unit wide
+ let mut wrapped = Vec::with_capacity(text.len() * 2);
+
+ for chunk in text {
+ let mut start = 0;
+ let mut space = 0;
+ let mut line = 0;
+ let mut word = 0;
+
+ for (i, c) in chunk.char_indices() {
+ line += 1;
+ word += 1;
+ if c == ' ' {
+ space = i;
+ word = 0;
+ }
+ if line == width {
+ wrapped.push(String::from(&chunk[start..space]));
+ start = space + 1;
+ line = word;
+ }
+ }
+ wrapped.push(String::from(&chunk[start..]));
+ }
+ wrapped
+}
+
+impl Bk {
+ fn new(
+ path: &String,
+ cols: u16,
+ rows: usize,
+ chapter_idx: usize,
+ pos: usize,
+ ) -> Result<Self, std::io::Error> {
+ let file = File::open(path)?;
+ let mut container = zip::ZipArchive::new(file)?;
+ let toc = get_toc(&mut container);
+ let mut bk = Bk {
+ chapter: Vec::new(),
+ container,
+ toc,
+ cols,
+ rows,
+ chapter_idx,
+ pos,
+ };
+ bk.load_chapter();
+ Ok(bk)
+ }
+ fn load_chapter(&mut self) {
+ let mut text = String::new();
+ self.container
+ .by_name(&self.toc[self.chapter_idx])
+ .unwrap()
+ .read_to_string(&mut text)
+ .unwrap();
+ let doc = roxmltree::Document::parse(&text).unwrap();
+
+ let mut chapter = Vec::new();
+ for n in doc.descendants() {
+ match n.tag_name().name() {
+ "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => {
+ let text = n.descendants().find(|n| n.is_text()).unwrap();
+ chapter.push(format!("# {}", text.text().unwrap()));
+ }
+ "div" => chapter.push(String::from("")),
+ "p" => chapter.push(
+ n.descendants()
+ .filter(|n| n.is_text())
+ .map(|n| n.text().unwrap())
+ .collect(),
+ ),
+ _ => (),
+ }
+ }
+ self.chapter = wrap(chapter, self.cols);
+ }
+ fn run(&mut self, code: KeyCode) -> bool {
+ match code {
+ KeyCode::Char('q') => return true,
+ KeyCode::Char('p') => {
+ if self.chapter_idx > 0 {
+ self.pos = 0;
+ self.chapter_idx -= 1;
+ self.load_chapter();
+ }
+ }
+ KeyCode::Char('n') => {
+ self.pos = 0;
+ self.chapter_idx += 1;
+ self.load_chapter();
+ }
+ KeyCode::Left | KeyCode::Up | KeyCode::PageUp => {
+ if self.pos == 0 && self.chapter_idx > 0 {
+ self.chapter_idx -= 1;
+ self.load_chapter();
+ } else {
+ self.pos -= self.rows;
+ }
+ }
+ KeyCode::Right
+ | KeyCode::Down
+ | KeyCode::PageDown
+ | KeyCode::Char(' ') => {
+ self.pos += self.rows;
+ if self.pos > self.chapter.len() {
+ self.chapter_idx += 1;
+ self.load_chapter();
+ self.pos = 0;
+ }
+ }
+ _ => (),
+ }
+ false
+ }
+}
+
+fn save_path() -> String {
+ let home = std::env::var("HOME").unwrap();
+ format!("{}/.local/share/bk", home)
+}
+
+fn restore() -> (String, usize, usize) {
+ let save = std::fs::read_to_string(save_path()).unwrap();
+ let mut lines = save.lines();
+ let path = lines.next().unwrap().to_string();
+
+ if let Some(p) = std::env::args().nth(1) {
+ if p != path {
+ return (p, 0, 0);
+ }
+ }
+ (
+ path,
+ lines.next().unwrap().to_string().parse::<usize>().unwrap(),
+ lines.next().unwrap().to_string().parse::<usize>().unwrap(),
+ )
+}
+
+fn main() -> crossterm::Result<()> {
+ let (path, chapter, pos) = restore();
+ let (cols, rows) = terminal::size().unwrap();
+
+ let mut bk = Bk::new(&path, cols, rows as usize, chapter, pos)
+ .unwrap_or_else(|e| {
+ println!("error reading epub: {}", e);
+ std::process::exit(1);
+ });
+
+ let mut stdout = stdout();
+ queue!(stdout, terminal::EnterAlternateScreen, cursor::Hide)?;
+ terminal::enable_raw_mode()?;
+
+ loop {
+ queue!(
+ stdout,
+ terminal::Clear(terminal::ClearType::All),
+ cursor::MoveTo(0, 0),
+ )?;
+
+ let end = std::cmp::min(bk.pos + bk.rows, bk.chapter.len());
+ for line in bk.pos..end {
+ queue!(
+ stdout,
+ Print(&bk.chapter[line]),
+ cursor::MoveToNextLine(1)
+ )?;
+ }
+ stdout.flush()?;
+
+ if let Event::Key(KeyEvent { code, .. }) = read()? {
+ if bk.run(code) {
+ break;
+ }
+ }
+ }
+
+ std::fs::write(
+ save_path(),
+ format!("{}\n{}\n{}", path, bk.chapter_idx, bk.pos),
+ )
+ .unwrap();
+ queue!(stdout, terminal::LeaveAlternateScreen, cursor::Show)?;
+ stdout.flush()?;
+ terminal::disable_raw_mode()
+}