diff --git a/Cargo.lock b/Cargo.lock index bc9d902..fc8645f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,7 +61,7 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "bk" -version = "0.3.0" +version = "0.4.0" dependencies = [ "anyhow", "argh", diff --git a/Cargo.toml b/Cargo.toml index e02f8d9..85eb4f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bk" -version = "0.3.0" +version = "0.4.0" authors = ["James Campos "] edition = "2018" license = "MIT" diff --git a/README.md b/README.md index eb614d7..b718527 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ # bk -bk is a WIP terminal Epub reader, written in Rust. +bk is a WIP terminal EPUB reader, written in Rust. # Features - Cross platform - Linux, macOS and Windows support - Single binary, instant startup -- Epub 2/3 support +- EPUB 2/3 support - Vim bindings - Incremental search - Bookmarks +- Inline styles (bold/italic) # Install Install from crates.io: @@ -31,22 +32,31 @@ or from github: -w, --width characters per line --help display usage information -Running `bk` without a path will load the most recent Epub. +Running `bk` without a path will load the most recent EPUB. Type any function key (eg F1) to see the keybinds. -# Configuration alternatives - -- Theming: theme your terminal -- Config file: create an alias with cli options - -# TODO -- more configuration -- better html support -- test unicode -- github actions / ci -- css? -- mobi? +Check if your terminal supports italics: + + echo -e "\e[3mitalic\e[0m" + +# Comparison +| | bk | epr/epy | +| - | - | - | +| language | rust | python | +| runtime deps | :x: | python, curses | +| inline styles | :heavy_check_mark: | :x: | +| incremental search | :heavy_check_mark: | :x: | +| multi line search | :heavy_check_mark: | :x: | +| regex search | :x: | :heavy_check_mark: | +| links | :x: | :x: | +| images | :x: | :heavy_check_mark: | +| themes | :x: | :heavy_check_mark: | +| choose file from history | :x: | :heavy_check_mark: | +| additional formats | :x: | FictionBook | +| external integration | see 1 | dictionary | + +1: you can use the `--meta` switch to use `bk` as a file previewer with eg [nnn](https://github.com/jarun/nnn/) # Inspiration diff --git a/src/epub.rs b/src/epub.rs index b523c06..68c834d 100644 --- a/src/epub.rs +++ b/src/epub.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use crossterm::style::Attribute; use roxmltree::{Document, Node}; use std::{collections::HashMap, fs::File, io::Read}; @@ -11,14 +12,14 @@ pub struct Epub { } impl Epub { - pub fn new(path: &str, meta: bool) -> std::io::Result { + pub fn new(path: &str, meta: bool) -> Result { let file = File::open(path)?; let mut epub = Epub { container: zip::ZipArchive::new(file)?, chapters: Vec::new(), meta: String::new(), }; - let chapters = epub.get_rootfile(); + let chapters = epub.get_rootfile()?; if !meta { epub.get_chapters(chapters); } @@ -53,9 +54,9 @@ impl Epub { }) .collect(); } - fn get_rootfile(&mut self) -> Vec<(String, String)> { + fn get_rootfile(&mut self) -> Result> { let xml = self.get_text("META-INF/container.xml"); - let doc = Document::parse(&xml).unwrap(); + let doc = Document::parse(&xml)?; let path = doc .descendants() .find(|n| n.has_tag_name("rootfile")) @@ -63,7 +64,7 @@ impl Epub { .attribute("full-path") .unwrap(); let xml = self.get_text(path); - let doc = Document::parse(&xml).unwrap(); + let doc = Document::parse(&xml)?; // zip expects unix path even on windows let rootdir = match path.rfind('/') { @@ -79,11 +80,13 @@ impl Epub { meta_node .children() - .filter(|n| n.is_element() && n.tag_name().name() != "meta") + .filter(Node::is_element) .for_each(|n| { let name = n.tag_name().name(); - let text = n.text().unwrap(); - self.meta.push_str(&format!("{}: {}\n", name, text)); + let text = n.text(); + if text.is_some() && name != "meta" { + self.meta.push_str(&format!("{}: {}\n", name, text.unwrap())); + } }); manifest_node .children() @@ -99,16 +102,16 @@ impl Epub { .attribute("href") .unwrap(); let xml = self.get_text(&format!("{}{}", rootdir, path)); - let doc = Document::parse(&xml).unwrap(); + let doc = Document::parse(&xml)?; epub3(doc, &mut nav); } else { let toc = spine_node.attribute("toc").unwrap_or("ncx"); let path = manifest.get(toc).unwrap(); let xml = self.get_text(&format!("{}{}", rootdir, path)); - let doc = Document::parse(&xml).unwrap(); + let doc = Document::parse(&xml)?; epub2(doc, &mut nav); } - spine_node + Ok(spine_node .children() .filter(Node::is_element) .enumerate() @@ -119,7 +122,7 @@ impl Epub { let path = format!("{}{}", rootdir, path); (label, path) }) - .collect() + .collect()) } } @@ -139,9 +142,23 @@ fn render(n: Node, buf: &mut String, attrs: &mut Attrs) { for c in n.children() { render(c, buf, attrs); } - attrs.push((buf.len(), Attribute::Reset)); + attrs.push((buf.len(), Attribute::NoBold)); buf.push('\n'); } + "em" => { + attrs.push((buf.len(), Attribute::Italic)); + for c in n.children() { + render(c, buf, attrs); + } + attrs.push((buf.len(), Attribute::NoItalic)); + } + "strong" => { + attrs.push((buf.len(), Attribute::Bold)); + for c in n.children() { + render(c, buf, attrs); + } + attrs.push((buf.len(), Attribute::NoBold)); + } "blockquote" | "p" | "tr" => { buf.push('\n'); for c in n.children() { @@ -157,6 +174,7 @@ fn render(n: Node, buf: &mut String, attrs: &mut Attrs) { buf.push('\n'); } "br" => buf.push('\n'), + "hr" => buf.push_str("\n* * *\n"), _ => { for c in n.children() { render(c, buf, attrs); diff --git a/src/main.rs b/src/main.rs index d977b12..30abdec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -301,99 +301,85 @@ impl View for Page { } } fn render(&self, bk: &Bk) -> Vec { - render_page(bk, 0) - } -} - -fn render_page(bk: &Bk, offset: usize) -> Vec { - let c = bk.chap(); - let line_end = min(bk.line + bk.rows - offset, bk.chap().lines.len()); - - // TODO support inline tags (strong) - let attrs = { - let text_start = c.lines[bk.line].0; - let text_end = c.lines[line_end - 1].1; - - let mut search = Vec::new(); - let qlen = bk.query.len(); - if qlen > 0 { - for (pos, _) in c.text[text_start..text_end].match_indices(&bk.query) { - search.push(text_start + pos); + let c = bk.chap(); + let line_end = min(bk.line + bk.rows, c.lines.len()); + + let attrs = { + let text_start = c.lines[bk.line].0; + let text_end = c.lines[line_end - 1].1; + + let qlen = bk.query.len(); + let mut search = Vec::new(); + if qlen > 0 { + for (pos, _) in c.text[text_start..text_end].match_indices(&bk.query) { + search.push((text_start + pos, Attribute::Reverse)); + search.push((text_start + pos + qlen, Attribute::NoReverse)); + } } - } - let mut search_iter = search.into_iter(); + let mut search_iter = search.into_iter().peekable(); - let attr_end = match c.attrs.binary_search_by_key(&text_end, |&(pos, _)| pos) { - Ok(n) => n, - Err(n) => n, - }; - let attr_start = c.attrs[..attr_end] - .iter() - .rposition(|&(pos, _)| pos <= text_start) - .unwrap(); - // keep attr pos >= line start - let (pos, attr) = c.attrs[attr_start]; - let head = (max(pos, text_start), attr); - let tail = &c.attrs[attr_start + 1..]; - let mut attrs_iter = iter::once(&head).chain(tail.iter()); - - let mut merged = Vec::new(); - let mut sn = search_iter.next(); - let mut an = attrs_iter.next(); - loop { - match (sn, an) { - (None, None) => panic!("does this happen?"), - (Some(s), None) => { - merged.push((s, Attribute::Reverse)); - merged.push((s + qlen, Attribute::Reset)); - for s in search_iter { - merged.push((s, Attribute::Reverse)); - merged.push((s + qlen, Attribute::Reset)); + let attr_end = match c.attrs.binary_search_by_key(&text_end, |&(pos, _)| pos) { + Ok(n) => n, + Err(n) => n, + }; + let attr_start = c.attrs[..attr_end] + .iter() + .rposition(|&(pos, _)| pos <= text_start) + .unwrap(); + // keep attr pos >= line start + let (pos, attr) = c.attrs[attr_start]; + let head = (max(pos, text_start), attr); + let tail = &c.attrs[attr_start + 1..]; + let mut attrs_iter = iter::once(&head).chain(tail.iter()).peekable(); + + // seems like this should be simpler. use itertools? + let mut merged = Vec::new(); + loop { + match (search_iter.peek(), attrs_iter.peek()) { + (None, None) => panic!("double none wtf"), + (Some(_), None) => { + merged.extend(search_iter); + break; } - break; - } - (None, Some(&a)) => { - merged.push(a); - merged.extend(attrs_iter); - break; - } - (Some(s), Some(&a)) => { - if s < a.0 { - merged.push((s, Attribute::Reverse)); - merged.push((s + qlen, Attribute::Reset)); - // this match arm is inside a header tag - merged.push((s + qlen, Attribute::Bold)); - sn = search_iter.next(); - } else { - merged.push(a); - an = attrs_iter.next(); + (None, Some(_)) => { + merged.extend(attrs_iter); + break; + } + (Some(&s), Some(&&a)) => { + if s.0 < a.0 { + merged.push(s); + search_iter.next(); + } else { + merged.push(a); + attrs_iter.next(); + } } } } - } - merged - }; + merged + }; - let mut buf = Vec::new(); - let mut iter = attrs.into_iter().peekable(); - for &(mut start, end) in &c.lines[bk.line..line_end] { - let mut s = String::new(); - while let Some(a) = iter.peek() { - if a.0 <= end { - s.push_str(&c.text[start..a.0]); - s.push_str(&a.1.to_string()); - start = a.0; + let mut buf = Vec::new(); + let mut iter = attrs.into_iter().peekable(); + for &(mut start, end) in &c.lines[bk.line..line_end] { + let mut s = String::new(); + while let Some(&(pos, attr)) = iter.peek() { + if pos > end { + break; + } + s.push_str(&c.text[start..pos]); + s.push_str(&attr.to_string()); + start = pos; iter.next(); - } else { - break; } + s.push_str(&c.text[start..end]); + buf.push(s); } - s.push_str(&c.text[start..end]); - buf.push(s); + buf } - buf } + struct Search; impl View for Search { fn run(&self, bk: &mut Bk, kc: KeyCode) { @@ -427,10 +413,13 @@ impl View for Search { } } fn render(&self, bk: &Bk) -> Vec { - let mut buf = render_page(bk, 1); - - for _ in buf.len()..bk.rows - 1 { - buf.push(String::new()); + let mut buf = Page::render(&Page, bk); + if buf.len() == bk.rows { + buf.pop(); + } else { + for _ in buf.len()..bk.rows - 1 { + buf.push(String::new()); + } } let prefix = match bk.dir { Direction::Next => '/', @@ -769,7 +758,10 @@ fn main() { exit(0); } let mut bk = Bk::new(epub, state.bk); - bk.run().unwrap(); + bk.run().unwrap_or_else(|e| { + println!("run error: {}", e); + exit(1); + }); let byte = bk.chap().lines[bk.line].0; state