You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
phetch/src/ui.rs

191 lines
5.4 KiB
Rust

5 years ago
use std::io::{stdin, stdout, Write};
5 years ago
use std::process::{Command, Stdio};
5 years ago
use termion::input::TermRead;
use termion::raw::IntoRawMode;
use gopher;
use gopher::Type;
5 years ago
use menu::MenuView;
5 years ago
use text::TextView;
5 years ago
pub type Key = termion::event::Key;
5 years ago
pub struct UI {
pages: Vec<Box<dyn View>>,
5 years ago
page: usize, // currently focused page
dirty: bool, // redraw?
running: bool, // main ui loop running?
5 years ago
}
5 years ago
#[derive(Debug)]
pub enum Action {
5 years ago
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
5 years ago
}
pub trait View {
fn process_input(&mut self, c: Key) -> Action;
5 years ago
fn render(&self, width: usize, height: usize) -> String;
5 years ago
fn url(&self) -> String;
5 years ago
}
5 years ago
impl UI {
pub fn new() -> UI {
UI {
pages: vec![],
page: 0,
5 years ago
dirty: true,
running: true,
5 years ago
}
}
5 years ago
pub fn run(&mut self) {
while self.running {
5 years ago
self.draw();
self.update();
5 years ago
}
}
5 years ago
pub fn draw(&mut self) {
if self.dirty {
// let prefix = ""; // debug
let prefix = "\x1b[2J\x1b[H\x1b[?25l"; // clear screen + hide cursor
5 years ago
print!("{}{}", prefix, self.render());
5 years ago
self.dirty = false;
}
5 years ago
}
pub fn update(&mut self) {
match self.process_input() {
Action::Quit => self.running = false,
5 years ago
_ => {}
}
5 years ago
}
pub fn render(&self) -> String {
let (cols, rows) = termion::terminal_size().expect("can't get terminal size"); // TODO
5 years ago
if self.pages.len() > 0 && self.page < self.pages.len() {
if let Some(page) = self.pages.get(self.page) {
5 years ago
return page.render(cols as usize, rows as usize);
5 years ago
}
}
String::from("N/A")
5 years ago
}
5 years ago
pub fn open(&mut self, url: &str) {
5 years ago
print!("\r\x1b[90mLoading...\x1b[0m\x1b[K");
stdout().flush();
5 years ago
self.dirty = true;
5 years ago
let (typ, host, port, sel) = gopher::parse_url(url);
5 years ago
let response = gopher::fetch(host, port, sel)
.map_err(|e| {
eprintln!("\x1B[91merror loading \x1b[93m{}: \x1B[0m{}[?25h", url, e); // TODO
5 years ago
std::process::exit(1);
})
.unwrap();
match typ {
5 years ago
Type::Menu => self.add_page(MenuView::from(url.to_string(), response)),
5 years ago
Type::Text => self.add_page(TextView::from(url.to_string(), response)),
5 years ago
Type::HTML => self.add_page(TextView::from(url.to_string(), response)),
5 years ago
_ => panic!("unknown type: {:?}", typ),
5 years ago
}
}
5 years ago
fn add_page<T: View + 'static>(&mut self, view: T) {
5 years ago
if self.pages.len() > 0 && self.page < self.pages.len() - 1 {
self.pages.truncate(self.page + 1);
}
self.pages.push(Box::from(view));
5 years ago
if self.pages.len() > 1 {
self.page += 1;
5 years ago
}
}
5 years ago
fn process_input(&mut self) -> Action {
let mut stdout = stdout().into_raw_mode().unwrap();
stdout.flush().unwrap();
5 years ago
match self.process_page_input() {
5 years ago
Action::Redraw => {
self.dirty = true;
Action::None
}
5 years ago
Action::Open(url) => {
self.open(&url);
Action::None
}
5 years ago
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
}
5 years ago
Action::Clipboard(url) => {
copy_to_clipboard(&url);
Action::None
}
5 years ago
a => a,
}
}
fn process_page_input(&mut self) -> Action {
let stdin = stdin();
5 years ago
let page = self.pages.get_mut(self.page).expect("expected Page"); // TODO
5 years ago
for c in stdin.keys() {
5 years ago
let key = c.expect("UI error on stdin.keys"); // TODO
5 years ago
match page.process_input(key) {
Action::Unknown => match key {
5 years ago
Key::Ctrl('q') | Key::Ctrl('c') => return Action::Quit,
5 years ago
Key::Left | Key::Backspace => return Action::Back,
5 years ago
Key::Right => return Action::Forward,
5 years ago
Key::Char('\n') => return Action::Redraw,
5 years ago
Key::Ctrl('y') => return Action::Clipboard(page.url()),
5 years ago
_ => {}
},
action => return action,
}
}
Action::None
}
5 years ago
}
5 years ago
impl Drop for UI {
fn drop(&mut self) {
print!("\x1b[?25h"); // show cursor
}
}
5 years ago
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()
}
5 years ago
.unwrap() // TODO
5 years ago
}