#[cfg(unix)] extern crate argparse; #[cfg(unix)] extern crate unicode_width; #[cfg(unix)] mod top { use argparse::{ArgumentParser, Store}; use html2text::render::{RichAnnotation, TaggedLine, TaggedLineElement}; use std::collections::HashMap; use std::io::{self, Write}; use termion::cursor::Goto; use termion::event::Key; use termion::input::TermRead; use termion::raw::IntoRawMode; use termion::screen::IntoAlternateScreen; use unicode_width::UnicodeWidthStr; fn to_style(tag: &[RichAnnotation]) -> String { let mut style = String::new(); for ann in tag { match *ann { RichAnnotation::Default => (), RichAnnotation::Link(_) => { style.push_str(&format!("{}", termion::style::Underline)); } RichAnnotation::Image(_) => { style.push_str(&format!( "{}", termion::color::Fg(termion::color::LightBlue) )); } RichAnnotation::Emphasis => { style.push_str(&format!( "{}", termion::color::Fg(termion::color::LightGreen) )); } RichAnnotation::Strong => { style.push_str(&format!( "{}", termion::color::Fg(termion::color::LightGreen) )); } RichAnnotation::Strikeout => (), RichAnnotation::Code => { style.push_str(&format!( "{}", termion::color::Fg(termion::color::LightYellow) )); } RichAnnotation::Preformat(is_cont) => { if is_cont { style.push_str(&format!( "{}", termion::color::Fg(termion::color::LightMagenta) )); } else { style.push_str(&format!("{}", termion::color::Fg(termion::color::Magenta))); } } // Ignore unhandled annotations _ => {} } } style } struct LinkMap { lines: Vec>>, // lines[y][x] => Some(URL) or None } impl LinkMap { pub fn link_at(&self, x: usize, y: usize) -> Option<&str> { if let Some(linevec) = self.lines.get(y) { if let Some(Some(text)) = linevec.get(x) { return Some(text); } } None } } fn link_from_tag(tag: &[RichAnnotation]) -> Option { let mut link = None; for annotation in tag { if let RichAnnotation::Link(text) = annotation { link = Some(text.clone()); } } link } fn find_links(lines: &[TaggedLine>]) -> LinkMap { let mut map = Vec::new(); for line in lines { let mut linevec = Vec::new(); for ts in line.tagged_strings() { let link = link_from_tag(&ts.tag); for _ in 0..UnicodeWidthStr::width(ts.s.as_str()) { linevec.push(link.clone()); } } map.push(linevec); } LinkMap { lines: map } } struct FragMap { start_xy: HashMap, } fn find_frags(lines: &[TaggedLine>]) -> FragMap { use self::TaggedLineElement::*; let mut map = HashMap::new(); for (y, line) in lines.iter().enumerate() { let mut x = 0; for tli in line.iter() { match tli { FragmentStart(fragname) => { map.insert(fragname.to_string(), (x, y)); } Str(ts) => { x += UnicodeWidthStr::width(ts.s.as_str()); } } } } FragMap { start_xy: map } } pub fn main() { let mut filename = String::new(); { let mut ap = ArgumentParser::new(); ap.refer(&mut filename) .add_argument("filename", Store, "Set HTML filename"); ap.parse_args_or_exit(); } let (width, height) = termion::terminal_size().unwrap(); let (width, height) = (width as usize, height as usize); let mut file = std::fs::File::open(filename).expect("Tried to open file"); let annotated = html2text::from_read_rich(&mut file, width).expect("Failed to convert from HTML"); let link_map = find_links(&annotated); let frag_map = find_frags(&annotated); let mut keys = io::stdin().keys(); // max_y is the largest (0-based) index of a real document line. let max_y = annotated.len() - 1; // top_y is the (0-based) index of the document line shown at // the top of the visible screen. let mut top_y = 0; // doc_x and doc_y are the logical (0-based) x and y of the // cursor position within the document. let mut doc_x = 0; let mut doc_y = 0; let mut screen = io::stdout() .into_raw_mode() .unwrap() .into_alternate_screen() .unwrap(); loop { // Sanity-check the current screen position. max_y should // be small enough that no blank lines beyond the end of // the document are visible on screen (except when the // document is shorter than a screenful); large enough // that the cursor isn't off the bottom of the visible // screen; and small enough that the cursor isn't off the // top. if max_y >= height - 1 { top_y = std::cmp::min(top_y, max_y - (height - 1)); } if doc_y >= height - 1 { top_y = std::cmp::max(top_y, doc_y - (height - 1)); } top_y = std::cmp::min(top_y, doc_y); let opt_url = link_map.link_at(doc_x, doc_y); let vis_y_limit = std::cmp::min(top_y + height, max_y + 1); write!(screen, "{}", termion::clear::All).unwrap(); for (i, line) in annotated[top_y..vis_y_limit].iter().enumerate() { write!(screen, "{}", Goto(1, i as u16 + 1)).unwrap(); for ts in line.tagged_strings() { let style = to_style(&ts.tag); let link = link_from_tag(&ts.tag); match (opt_url, link) { (Some(ref t1), Some(ref t2)) if t1 == t2 => { write!(screen, "{}", termion::style::Invert).unwrap(); } _ => (), } write!(screen, "{}{}{}", style, ts.s, termion::style::Reset).unwrap(); } } // 1-based screen coordinates let cursor_x = (doc_x + 1) as u16; let cursor_y = (doc_y - top_y + 1) as u16; write!(screen, "{}", Goto(cursor_x, cursor_y)).unwrap(); screen.flush().unwrap(); if let Some(Ok(k)) = keys.next() { match k { Key::Char('q') => break, Key::Char('j') | Key::Down => { if doc_y < max_y { doc_y += 1; } } Key::Char('k') | Key::Up => { doc_y = doc_y.saturating_sub(1); } Key::Char('h') | Key::Left => { doc_x = doc_x.saturating_sub(1); } Key::Char('l') | Key::Right => { if doc_x + 1 < width { doc_x += 1; } } Key::Char(' ') | Key::PageDown => { // Ideally, move both the cursor and the top // visible line down by a whole page doc_y += height; top_y += height; // But bound the cursor within the document doc_y = std::cmp::min(doc_y, max_y); // And the standard bounds checking for top_y // will take care of the rest of the special // cases. } Key::PageUp => { // Ideally, move both the cursor and the top // visible line up by a whole page. But bound // both at zero. doc_y = std::cmp::max(doc_y, height) - height; top_y = std::cmp::max(top_y, height) - height; } Key::Home => { doc_y = 0; } Key::End => { doc_y = max_y; } Key::Char('\t') => {} Key::Char('\r') | Key::Char('\n') => { if let Some(url) = opt_url { if let Some(u) = url.strip_prefix('#') { let start = frag_map.start_xy.get(u); if let Some((x, y)) = start { doc_x = *x; doc_y = *y; } } } } _ => {} } } } } } #[cfg(not(unix))] mod top { pub fn main() {} } fn main() { top::main() }