diff options
-rw-r--r-- | src/epub.rs | 68 | ||||
-rw-r--r-- | src/main.rs | 101 |
2 files changed, 113 insertions, 56 deletions
diff --git a/src/epub.rs b/src/epub.rs index b947943..66c2eb1 100644 --- a/src/epub.rs +++ b/src/epub.rs @@ -10,6 +10,7 @@ pub struct Chapter { pub lines: Vec<(usize, usize)>, // crossterm gives us a bitset but doesn't let us diff it, so store the state transition pub attrs: Vec<(usize, Attribute, Attributes)>, + pub links: Vec<(usize, usize, String)>, state: Attributes, } @@ -17,6 +18,7 @@ pub struct Epub { container: zip::ZipArchive<File>, pub chapters: Vec<Chapter>, pub meta: String, + pub links: HashMap<String, (usize, usize)>, } impl Epub { @@ -26,8 +28,9 @@ impl Epub { container: zip::ZipArchive::new(file)?, chapters: Vec::new(), meta: String::new(), + links: HashMap::new(), }; - let chapters = epub.get_rootfile()?; + let chapters = epub.get_spine()?; if !meta { epub.get_chapters(chapters); } @@ -42,33 +45,31 @@ impl Epub { .unwrap(); text } - fn get_chapters(&mut self, chapters: Vec<(String, String)>) { - self.chapters = chapters - .into_iter() - .filter_map(|(title, path)| { - let xml = self.get_text(&path); - // https://github.com/RazrFalcon/roxmltree/issues/12 - // UnknownEntityReference for HTML entities - let doc = Document::parse(&xml).unwrap(); - let body = doc.root_element().last_element_child().unwrap(); - let state = Attributes::default(); - let mut c = Chapter { - title, - text: String::new(), - lines: Vec::new(), - attrs: vec![(0, Attribute::Reset, state)], - state, - }; - render(body, &mut c); - if c.text.is_empty() { - None - } else { - Some(c) - } - }) - .collect(); + fn get_chapters(&mut self, spine: Vec<(String, String)>) { + for (title, path) in spine { + let xml = self.get_text(&path); + // https://github.com/RazrFalcon/roxmltree/issues/12 + // UnknownEntityReference for HTML entities + let doc = Document::parse(&xml).unwrap(); + let body = doc.root_element().last_element_child().unwrap(); + let state = Attributes::default(); + let mut c = Chapter { + title, + text: String::new(), + lines: Vec::new(), + attrs: vec![(0, Attribute::Reset, state)], + state, + links: Vec::new(), + }; + render(body, &mut c); + if !c.text.is_empty() { + let key = path.rsplit('/').next().unwrap().to_string(); + self.links.insert(key, (self.chapters.len(), 0)); + self.chapters.push(c); + } + } } - fn get_rootfile(&mut self) -> Result<Vec<(String, String)>> { + fn get_spine(&mut self) -> Result<Vec<(String, String)>> { let xml = self.get_text("META-INF/container.xml"); let doc = Document::parse(&xml)?; let path = doc @@ -162,7 +163,18 @@ fn render(n: Node, c: &mut Chapter) { match n.tag_name().name() { "br" => c.text.push('\n'), "hr" => c.text.push_str("\n* * *\n"), - "a" => c.render(n, Attribute::Underlined, Attribute::NoUnderline), + "a" => { + if let Some(url) = n.attribute("href") { + let start = c.text.len(); + c.render(n, Attribute::Underlined, Attribute::NoUnderline); + let url = url.split('#').next().unwrap().to_string(); + c.links.push(( + start, + c.text.len(), + url, + )); + } + } "em" => c.render(n, Attribute::Italic, Attribute::NoItalic), "strong" => c.render(n, Attribute::Bold, Attribute::NoBold), "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => { diff --git a/src/main.rs b/src/main.rs index bf2be55..09eb5b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,14 @@ use anyhow::Result; use crossterm::{ cursor, - event::{self, Event, KeyCode}, + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseEvent}, queue, style::{Attribute, Print}, terminal, }; use serde::{Deserialize, Serialize}; use std::{ - cmp::min, + cmp::{Ordering, min}, collections::HashMap, env, fs, io::{stdout, Write}, @@ -87,14 +87,15 @@ enum Direction { } trait View { - fn run(&self, bk: &mut Bk, kc: KeyCode); fn render(&self, bk: &Bk) -> Vec<String>; + fn on_key(&self, bk: &mut Bk, kc: KeyCode); + fn on_mouse(&self, _: &mut Bk, _: MouseEvent) {} } // TODO render something useful? struct Mark; impl View for Mark { - fn run(&self, bk: &mut Bk, kc: KeyCode) { + fn on_key(&self, bk: &mut Bk, kc: KeyCode) { if let KeyCode::Char(c) = kc { bk.mark(c) } @@ -107,9 +108,11 @@ impl View for Mark { struct Jump; impl View for Jump { - fn run(&self, bk: &mut Bk, kc: KeyCode) { + fn on_key(&self, bk: &mut Bk, kc: KeyCode) { if let KeyCode::Char(c) = kc { - bk.jump(c) + if let Some(&pos) = bk.mark.get(&c) { + bk.jump(pos); + } } bk.view = Some(&Page); } @@ -120,7 +123,7 @@ impl View for Jump { struct Metadata; impl View for Metadata { - fn run(&self, bk: &mut Bk, _: KeyCode) { + fn on_key(&self, bk: &mut Bk, _: KeyCode) { bk.view = Some(&Page); } fn render(&self, bk: &Bk) -> Vec<String> { @@ -144,7 +147,7 @@ impl View for Metadata { struct Help; impl View for Help { - fn run(&self, bk: &mut Bk, _: KeyCode) { + fn on_key(&self, bk: &mut Bk, _: KeyCode) { bk.view = Some(&Page); } fn render(&self, _: &Bk) -> Vec<String> { @@ -179,7 +182,7 @@ PageDown Right Space f l Page Down struct Nav; impl View for Nav { - fn run(&self, bk: &mut Bk, kc: KeyCode) { + fn on_key(&self, bk: &mut Bk, kc: KeyCode) { match kc { KeyCode::Esc | KeyCode::Tab @@ -244,7 +247,40 @@ impl View for Nav { struct Page; impl View for Page { - fn run(&self, bk: &mut Bk, kc: KeyCode) { + fn on_mouse(&self, bk: &mut Bk, e: MouseEvent) { + match e { + MouseEvent::Down(_, col, row, _) => { + if col < bk.pad() { + return; + } + let c = bk.chap(); + let (start, end) = c.lines[bk.line + row as usize]; + // FIXME unicode width + let byte = start + (col - bk.pad()) as usize; + if byte > end { + return; + } + let r = c.links.binary_search_by(|&(start, end, _)| { + if start > byte { + Ordering::Greater + } else if end < byte { + Ordering::Less + } else { + Ordering::Equal + } + }); + if let Ok(i) = r { + let path = &c.links[i].2; + let &pos = bk.links.get(path).unwrap(); + bk.jump(pos); + } + } + MouseEvent::ScrollDown(_, _, _) => bk.scroll_down(3), + MouseEvent::ScrollUp(_, _, _) => bk.scroll_up(3), + _ => (), + } + } + fn on_key(&self, bk: &mut Bk, kc: KeyCode) { match kc { KeyCode::Esc | KeyCode::Char('q') => bk.view = None, KeyCode::Tab => { @@ -285,13 +321,13 @@ impl View for Page { bk.scroll_up(bk.rows / 2); } KeyCode::Up | KeyCode::Char('k') => { - bk.scroll_up(2); + bk.scroll_up(3); } KeyCode::Left | KeyCode::PageUp | KeyCode::Char('b') | KeyCode::Char('h') => { bk.scroll_up(bk.rows); } KeyCode::Down | KeyCode::Char('j') => { - bk.scroll_down(2); + bk.scroll_down(3); } KeyCode::Right | KeyCode::PageDown @@ -396,7 +432,7 @@ impl View for Page { struct Search; impl View for Search { - fn run(&self, bk: &mut Bk, kc: KeyCode) { + fn on_key(&self, bk: &mut Bk, kc: KeyCode) { match kc { KeyCode::Esc => { bk.jump_reset(); @@ -450,6 +486,7 @@ struct Bk<'a> { chapter: usize, line: usize, mark: HashMap<char, (usize, usize)>, + links: HashMap<String, (usize, usize)>, // terminal cols: u16, rows: usize, @@ -507,6 +544,7 @@ impl Bk<'_> { chapter: args.chapter, line, mark, + links: epub.links, cols, rows: rows as usize, max_width: args.width, @@ -517,26 +555,32 @@ impl Bk<'_> { query: String::new(), } } + fn pad(&self) -> u16 { + self.cols.saturating_sub(self.max_width) / 2 + } fn run(&mut self) -> crossterm::Result<()> { let mut stdout = stdout(); - queue!(stdout, terminal::EnterAlternateScreen, cursor::Hide)?; + queue!( + stdout, + terminal::EnterAlternateScreen, + cursor::Hide, + EnableMouseCapture + )?; terminal::enable_raw_mode()?; while let Some(view) = self.view { - let pad = self.cols.saturating_sub(self.max_width) / 2; - queue!( stdout, terminal::Clear(terminal::ClearType::All), Print(Attribute::Reset) )?; for (i, line) in view.render(self).iter().enumerate() { - queue!(stdout, cursor::MoveTo(pad, i as u16), Print(line))?; + queue!(stdout, cursor::MoveTo(self.pad(), i as u16), Print(line))?; } stdout.flush().unwrap(); match event::read()? { - Event::Key(e) => view.run(self, e.code), + Event::Key(e) => view.on_key(self, e.code), Event::Resize(cols, rows) => { self.rows = rows as usize; if cols != self.cols { @@ -547,24 +591,25 @@ impl Bk<'_> { } } } - // TODO - Event::Mouse(_) => (), + Event::Mouse(e) => view.on_mouse(self, e), } } - queue!(stdout, terminal::LeaveAlternateScreen, cursor::Show)?; + queue!( + stdout, + terminal::LeaveAlternateScreen, + cursor::Show, + DisableMouseCapture + )?; terminal::disable_raw_mode() } fn mark(&mut self, c: char) { self.mark.insert(c, (self.chapter, self.line)); } - fn jump(&mut self, c: char) { - if let Some(&(c, l)) = self.mark.get(&c) { - let jump = (self.chapter, self.line); - self.chapter = c; - self.line = l; - self.mark.insert('\'', jump); - } + fn jump(&mut self, (c, l): (usize, usize)) { + self.mark('\''); + self.chapter = c; + self.line = l; } fn jump_reset(&mut self) { let &(c, l) = self.mark.get(&'\'').unwrap(); |