use std::io::{stdin, stdout, Write}; use std::process::{Command, Stdio}; use termion::input::TermRead; use termion::raw::IntoRawMode; use gopher; use gopher::Type; use menu::MenuView; use text::TextView; pub type Key = termion::event::Key; pub const SCROLL_LINES: usize = 15; pub const MAX_COLS: usize = 72; pub struct UI { pages: Vec>, page: usize, // currently focused page dirty: bool, // redraw? running: bool, // main ui loop running? } #[derive(Debug)] pub enum Action { None, // do nothing Back, // back in history Forward, // also history Open(String), // url Redraw, // redraw everything Quit, // yup Clipboard(String), // copy to clipboard Unknown, // handler doesn't know what to do } pub trait View { fn process_input(&mut self, c: Key) -> Action; fn render(&self) -> String; fn url(&self) -> String; fn set_size(&mut self, cols: usize, rows: usize); } impl UI { pub fn new() -> UI { UI { pages: vec![], page: 0, dirty: true, running: true, } } pub fn run(&mut self) { while self.running { self.draw(); self.update(); } } pub fn draw(&mut self) { if self.dirty { print!( "{}{}{}{}", termion::clear::All, termion::cursor::Goto(1, 1), termion::cursor::Hide, self.render() ); self.dirty = false; } } pub fn update(&mut self) { if let Action::Quit = self.process_input() { self.running = false; } } pub fn render(&mut self) -> String { let (cols, rows) = termion::terminal_size().expect("can't get terminal size"); // TODO if !self.pages.is_empty() && self.page < self.pages.len() { if let Some(page) = self.pages.get_mut(self.page) { page.set_size(cols as usize, rows as usize); return page.render(); } } String::from("N/A") } pub fn open(&mut self, url: &str) { print!("\r\x1b[90mLoading...\x1b[0m\x1b[K"); stdout().flush(); self.dirty = true; let (typ, host, port, sel) = gopher::parse_url(url); let response = gopher::fetch(host, port, sel) .map_err(|e| { eprintln!("\x1B[91merror loading \x1b[93m{}: \x1B[0m{}[?25h", url, e); // TODO std::process::exit(1); }) .unwrap(); match typ { Type::Menu => self.add_page(MenuView::from(url.to_string(), response)), Type::Text => self.add_page(TextView::from(url.to_string(), response)), Type::HTML => self.add_page(TextView::from(url.to_string(), response)), _ => panic!("unknown type: {:?}", typ), } } fn add_page(&mut self, view: T) { if !self.pages.is_empty() && self.page < self.pages.len() - 1 { self.pages.truncate(self.page + 1); } self.pages.push(Box::from(view)); if self.pages.len() > 1 { self.page += 1; } } fn process_input(&mut self) -> Action { let mut stdout = stdout().into_raw_mode().unwrap(); stdout.flush().unwrap(); match self.process_page_input() { Action::Redraw => { self.dirty = true; Action::None } Action::Open(url) => { self.open(&url); Action::None } Action::Back => { if self.page > 0 { self.dirty = true; self.page -= 1; } Action::None } Action::Forward => { if self.page < self.pages.len() - 1 { self.dirty = true; self.page += 1; } Action::None } Action::Clipboard(url) => { copy_to_clipboard(&url); Action::None } a => a, } } fn process_page_input(&mut self) -> Action { let stdin = stdin(); let page = self.pages.get_mut(self.page).expect("expected Page"); // TODO for c in stdin.keys() { let key = c.expect("UI error on stdin.keys"); // TODO match page.process_input(key) { Action::Unknown => match key { Key::Ctrl('q') | Key::Ctrl('c') => return Action::Quit, Key::Left | Key::Backspace => return Action::Back, Key::Right => return Action::Forward, Key::Char('\n') => return Action::Redraw, Key::Ctrl('y') => return Action::Clipboard(page.url()), _ => {} }, action => return action, } } Action::None } } impl Drop for UI { fn drop(&mut self) { print!("\x1b[?25h"); // show cursor } } fn copy_to_clipboard(data: &str) { let mut child = spawn_os_clipboard(); let child_stdin = child.stdin.as_mut().unwrap(); child_stdin.write_all(data.as_bytes()); } fn spawn_os_clipboard() -> std::process::Child { if cfg!(target_os = "macos") { Command::new("pbcopy").stdin(Stdio::piped()).spawn() } else { Command::new("xclip") .args(&["-sel", "clip"]) .stdin(Stdio::piped()) .spawn() } .unwrap() // TODO }