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/menu.rs

464 lines
14 KiB
Rust

5 years ago
use gopher;
use gopher::Type;
5 years ago
use std::io::stdout;
use std::io::Write;
5 years ago
use ui::{Action, Key, View, MAX_COLS, SCROLL_LINES};
5 years ago
pub struct MenuView {
pub input: String, // user's inputted value
pub menu: Menu, // data
pub link: usize, // selected link
pub scroll: usize, // scrolling offset
pub size: (usize, usize), // cols, rows
5 years ago
}
pub struct Menu {
5 years ago
url: String, // gopher url
lines: Vec<Line>, // lines
links: Vec<usize>, // links (index of line in lines vec)
longest: usize, // size of the longest line
5 years ago
}
5 years ago
5 years ago
#[derive(Debug)]
5 years ago
pub struct Line {
5 years ago
name: String,
5 years ago
url: String,
5 years ago
typ: Type,
5 years ago
link: usize, // link #, if any
5 years ago
}
// direction of a given link relative to the visible screen
#[derive(PartialEq)]
enum LinkDir {
Above,
Below,
Visible,
}
impl View for MenuView {
fn render(&self) -> String {
self.render_lines()
5 years ago
}
5 years ago
fn process_input(&mut self, key: Key) -> Action {
5 years ago
self.process_key(key)
5 years ago
}
5 years ago
fn url(&self) -> String {
self.menu.url.to_string()
}
fn set_size(&mut self, cols: usize, rows: usize) {
self.size = (cols, rows);
}
5 years ago
}
impl MenuView {
pub fn from(url: String, response: String) -> MenuView {
MenuView {
menu: Menu::from(url, response),
input: String::new(),
5 years ago
link: 0,
5 years ago
scroll: 0,
size: (0, 0),
5 years ago
}
}
5 years ago
fn lines(&self) -> &Vec<Line> {
&self.menu.lines
}
5 years ago
fn links(&self) -> &Vec<usize> {
&self.menu.links
}
fn link(&self, i: usize) -> Option<&Line> {
if let Some(line) = self.menu.links.get(i) {
self.menu.lines.get(*line)
} else {
None
}
5 years ago
}
// is the given link visible on the screen right now?
fn visible_link(&self, i: usize) -> Option<LinkDir> {
if let Some(pos) = self.links().get(i) {
Some(if *pos < self.scroll {
LinkDir::Above
} else if *pos >= self.scroll + self.size.1 - 1 {
LinkDir::Below
} else {
LinkDir::Visible
})
} else {
None
}
}
fn render_lines(&self) -> String {
5 years ago
let mut out = String::new();
let (cols, rows) = self.size;
5 years ago
macro_rules! push {
($c:expr, $e:expr) => {{
5 years ago
out.push_str("\x1b[");
out.push_str($c);
out.push_str("m");
5 years ago
out.push_str(&$e);
5 years ago
out.push_str("\x1b[0m");
5 years ago
}};
}
5 years ago
let iter = self.lines().iter().skip(self.scroll).take(rows - 1);
5 years ago
let longest = if self.menu.longest > MAX_COLS {
MAX_COLS
} else {
self.menu.longest
};
let indent = if longest > cols {
String::from("")
} else {
5 years ago
let left = (cols - longest) / 2;
5 years ago
if left > 6 {
" ".repeat(left - 6)
} else {
String::from("")
}
};
5 years ago
for line in iter {
out.push_str(&indent);
5 years ago
if line.typ == Type::Info {
5 years ago
out.push_str(" ");
5 years ago
} else {
5 years ago
if line.link - 1 == self.link {
out.push_str("\x1b[97;1m*\x1b[0m")
5 years ago
} else {
out.push(' ');
}
5 years ago
out.push(' ');
out.push_str("\x1b[95m");
5 years ago
if line.link < 10 {
5 years ago
out.push(' ');
}
5 years ago
out.push_str(&line.link.to_string());
5 years ago
out.push_str(".\x1b[0m ");
}
5 years ago
// truncate long lines, instead of wrapping
5 years ago
let name = if line.name.len() + 6 > cols {
5 years ago
line.name.chars().take(cols - 6).collect::<String>()
5 years ago
} else {
5 years ago
line.name.to_string()
5 years ago
};
5 years ago
match line.typ {
5 years ago
Type::Text => push!("96", name),
Type::Menu => push!("94", name),
Type::Info => push!("93", name),
Type::HTML => push!("92", name),
Type::Error => push!("91", name),
typ if typ.is_download() => push!("4;97", name),
_ => push!("0", name),
5 years ago
}
out.push('\n');
5 years ago
}
5 years ago
if self.lines().len() < rows {
// fill in empty space
out.push_str(&format!(
"{}",
" \r\n".repeat(rows - 1 - self.lines().len())
));
5 years ago
}
5 years ago
out.push_str(&self.input);
5 years ago
out
5 years ago
}
5 years ago
fn redraw_input(&self) -> Action {
5 years ago
print!("\r\x1b[K{}", self.input);
stdout().flush();
5 years ago
Action::None
}
5 years ago
fn action_page_down(&mut self) -> Action {
let lines = self.lines().len();
if lines > SCROLL_LINES && self.scroll < lines - SCROLL_LINES {
self.scroll += SCROLL_LINES;
5 years ago
Action::Redraw
} else {
Action::None
}
}
fn action_page_up(&mut self) -> Action {
if self.scroll > 0 {
if self.scroll > SCROLL_LINES {
self.scroll -= SCROLL_LINES;
} else {
5 years ago
self.scroll = 0;
}
Action::Redraw
} else {
Action::None
}
}
5 years ago
fn action_up(&mut self) -> Action {
if self.link > 0 {
self.link -= 1;
Action::Redraw
} else {
Action::None
}
}
fn action_down(&mut self) -> Action {
5 years ago
let count = self.links().len();
5 years ago
if count > 0 && self.link < count - 1 {
if let Some(dir) = self.visible_link(self.link + 1) {
match dir {
LinkDir::Above => {
// jump to link....
if let Some(pos) = self.links().get(self.link + 1) {
self.scroll = *pos;
self.link = self.link + 1;
}
}
LinkDir::Below => {
// scroll down by 1
self.scroll += 1;
// select it if it's visible now
if let Some(dir) = self.visible_link(self.link + 1) {
if dir == LinkDir::Visible {
self.link += 1;
}
}
}
LinkDir::Visible => {
// select next link down
self.link += 1;
}
}
Action::Redraw
} else {
Action::None
}
5 years ago
} else {
Action::None
}
}
5 years ago
5 years ago
fn action_select_link(&mut self, line: usize) -> Action {
5 years ago
if line < self.links().len() {
5 years ago
self.link = line;
Action::Redraw
} else {
Action::None
}
}
5 years ago
fn action_follow_link(&mut self, link: usize) -> Action {
self.input.clear();
5 years ago
if let Some(line) = self.link(link) {
5 years ago
let url = line.url.to_string();
Action::Open(url)
5 years ago
} else {
Action::None
}
}
5 years ago
fn process_key(&mut self, key: Key) -> Action {
5 years ago
match key {
5 years ago
Key::Char('\n') => {
5 years ago
self.input.clear();
5 years ago
if let Some(line) = self.link(self.link) {
5 years ago
let url = line.url.to_string();
Action::Open(url)
5 years ago
} else {
Action::None
}
}
5 years ago
Key::Up | Key::Ctrl('p') => self.action_up(),
Key::Down | Key::Ctrl('n') => self.action_down(),
5 years ago
Key::Backspace | Key::Delete => {
5 years ago
if self.input.is_empty() {
Action::Back
} else {
5 years ago
self.input.pop();
self.redraw_input()
5 years ago
}
}
5 years ago
Key::Esc => {
if self.input.len() > 0 {
self.input.clear();
self.redraw_input()
} else {
Action::None
}
}
5 years ago
Key::Ctrl('c') => {
if self.input.len() > 0 {
self.input.clear();
5 years ago
self.redraw_input()
5 years ago
} else {
Action::Quit
}
}
5 years ago
Key::Char('-') => {
5 years ago
if self.input.is_empty() {
5 years ago
self.action_page_up()
5 years ago
} else {
self.input.push('-');
5 years ago
self.redraw_input()
5 years ago
}
}
5 years ago
Key::PageUp => self.action_page_up(),
Key::PageDown => self.action_page_down(),
Key::Char(' ') => {
5 years ago
if self.input.is_empty() {
5 years ago
self.action_page_down()
5 years ago
} else {
self.input.push(' ');
5 years ago
self.redraw_input()
5 years ago
}
}
Key::Char(c) => {
self.input.push(c);
5 years ago
let count = self.links().len();
5 years ago
let input = &self.input;
5 years ago
// jump to <10 number
if input.len() == 1 {
if let Some(c) = input.chars().nth(0) {
if c.is_digit(10) {
let i = c.to_digit(10).unwrap() as usize;
if i < count {
if count < (i * 10) {
return self.action_follow_link(i - 1);
} else {
return self.action_select_link(i - 1);
}
5 years ago
}
5 years ago
}
}
5 years ago
} else if input.len() == 2 {
// jump to >=10 number
5 years ago
let s = input.chars().take(2).collect::<String>();
if let Ok(num) = s.parse::<usize>() {
if num <= count {
if count < (num * 10) {
return self.action_follow_link(num - 1);
} else {
return self.action_select_link(num - 1);
}
}
}
} else if input.len() == 3 {
// jump to >=100 number
let s = input.chars().take(3).collect::<String>();
if let Ok(num) = s.parse::<usize>() {
if num <= count {
if count < (num * 10) {
return self.action_follow_link(num - 1);
} else {
return self.action_select_link(num - 1);
}
5 years ago
}
}
}
for i in 0..count {
// check for name match
5 years ago
let name = if let Some(link) = self.link(i) {
5 years ago
link.name.to_ascii_lowercase()
5 years ago
} else {
5 years ago
"".to_string()
};
5 years ago
5 years ago
if name.contains(&self.input.to_ascii_lowercase()) {
return self.action_select_link(i);
5 years ago
}
}
5 years ago
self.link = 0;
Action::Redraw
5 years ago
}
5 years ago
_ => Action::Unknown,
5 years ago
}
}
}
impl Menu {
pub fn from(url: String, gopher_response: String) -> Menu {
Self::parse(url, gopher_response)
5 years ago
}
fn parse(url: String, raw: String) -> Menu {
5 years ago
let mut lines = vec![];
5 years ago
let mut links = vec![];
5 years ago
let mut link = 0;
let mut longest = 0;
for line in raw.split_terminator("\n") {
if let Some(c) = line.chars().nth(0) {
5 years ago
let typ = match gopher::type_for_char(c) {
Some(t) => t,
None => continue,
};
5 years ago
// build string URL
let parts: Vec<&str> = line.split_terminator("\t").collect();
5 years ago
let mut url = String::from("gopher://");
if parts.len() > 2 {
url.push_str(parts[2]); // host
}
if parts.len() > 3 {
// port
let port = parts[3].trim_end_matches('\r');
if port != "70" {
url.push(':');
url.push_str(parts[3].trim_end_matches('\r'));
}
5 years ago
}
5 years ago
5 years ago
// auto-prepend gopher type to selector
5 years ago
if let Some(first_char) = parts[0].chars().nth(0) {
5 years ago
url.push_str("/");
url.push(first_char);
5 years ago
}
5 years ago
if parts.len() > 1 {
url.push_str(parts[1]); // selector
}
5 years ago
let mut name = String::from("");
if parts[0].len() > 0 {
name.push_str(&parts[0][1..]);
}
5 years ago
if typ != Type::Info {
link += 1;
}
5 years ago
let link = if typ == Type::Info { 0 } else { link };
5 years ago
if name.len() > longest {
longest = name.len();
}
5 years ago
if link > 0 {
links.push(lines.len());
}
5 years ago
lines.push(Line {
name,
url,
typ,
5 years ago
link,
5 years ago
});
5 years ago
}
}
Menu {
url,
lines,
5 years ago
links,
longest,
}
5 years ago
}
5 years ago
}