diff --git a/src/main.rs b/src/main.rs index a88d376..3d51786 100644 --- a/src/main.rs +++ b/src/main.rs @@ -111,7 +111,6 @@ fn print_plain(url: &str, tls: bool, tor: bool) -> Result<(), Box> { let mut out = String::new(); let typ = gopher::type_for_url(url); let (_, response) = gopher::fetch_url(url, tls, tor)?; - let response = gopher::response_to_string(&response); match typ { gopher::Type::Menu => { let menu = menu::parse(url, response); @@ -120,7 +119,10 @@ fn print_plain(url: &str, tls: bool, tor: bool) -> Result<(), Box> { out.push('\n'); } } - gopher::Type::Text => println!("{}", response.trim_end_matches(".\r\n")), + gopher::Type::Text => println!( + "{}", + gopher::response_to_string(&response).trim_end_matches(".\r\n") + ), _ => { return Err(Box::new(io::Error::new( io::ErrorKind::Other, diff --git a/src/menu.rs b/src/menu.rs index d96cbef..d9ee195 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -8,11 +8,12 @@ use crate::{ config::SharedConfig as Config, + encoding::Encoding, gopher::{self, Type}, terminal, ui::{self, Action, Key, View, MAX_COLS, SCROLL_LINES}, }; -use std::fmt; +use std::{fmt, str}; /// The Menu holds our Gopher Lines, a list of links, and maintains /// both where the cursor is on screen and which lines need to be @@ -23,6 +24,8 @@ use std::fmt; pub struct Menu { /// Gopher URL pub url: String, + /// Ref to our global config + config: Config, /// Lines in the menu. Not all are links. Use the `lines()` iter /// or `line(N)` or `link(N)` to access one. spans: Vec, @@ -33,7 +36,9 @@ pub struct Menu { /// Size of the longest line, for wrapping purposes pub longest: usize, /// Actual Gopher response - pub raw: String, + raw: Vec, + /// Encoded Gopher response + encoded_response: String, /// User input on a prompt() line pub input: String, /// UI mode. Interactive (Run), Printing, Raw mode... @@ -48,6 +53,8 @@ pub struct Menu { tor: bool, /// Size of the screen currently, cols and rows pub size: (usize, usize), + /// Text Encoding of Response + encoding: Encoding, /// Wide mode? wide: bool, } @@ -217,8 +224,12 @@ impl View for Menu { self.tor } + fn encoding(&self) -> Encoding { + self.encoding + } + fn raw(&self) -> &str { - self.raw.as_ref() + str::from_utf8(&self.raw).unwrap_or_default() } fn render(&mut self) -> String { @@ -249,19 +260,34 @@ impl View for Menu { impl Menu { /// Create a representation of a Gopher Menu from a raw Gopher /// response and a few options. - pub fn from(url: &str, response: String, config: Config, tls: bool) -> Menu { - Menu { + pub fn from(url: &str, response: Vec, config: Config, tls: bool) -> Menu { + let encoding = config.read().unwrap().encoding; + let mut menu = Menu { + url: url.into(), + encoded_response: encoding.encode(&response).into(), + raw: response, tls, tor: config.read().unwrap().tor, wide: config.read().unwrap().wide, mode: config.read().unwrap().mode, - ..parse(url, response) - } + encoding, + config: config.clone(), + input: String::new(), + spans: vec![], + links: vec![], + link: 0, + longest: 0, + scroll: 0, + searching: false, + size: (0, 0), + }; + menu.parse(); + menu } /// Lines in this menu. Main iterator for getting Line with text. pub fn lines(&self) -> LinesIter { - LinesIter::new(&self.spans, &self.raw) + LinesIter::new(&self.spans, self.raw()) } /// Get a single Line in this menu by index. @@ -269,7 +295,7 @@ impl Menu { if idx >= self.spans.len() { None } else { - Some(Line::new(&self.spans[idx], &self.raw)) + Some(Line::new(&self.spans[idx], self.raw())) } } @@ -343,6 +369,59 @@ impl Menu { Some((x as u16, y as u16)) } + /// Parse our `encoded_response` and cache information, like the + /// number of links. + fn parse(&mut self) { + let mut spans = vec![]; + let mut links = vec![]; + let mut longest = 0; + let mut start = 0; + + for line in self.encoded_response.split_terminator('\n') { + // Check for Gopher's weird "end of response" message. + if line == ".\r" || line == "." { + break; + } + + if line == "" { + start += 1; + continue; + } + + if let Some(mut span) = parse_line(start, &self.encoded_response) { + if span.text_len() > longest { + longest = span.text_len(); + } + if span.typ.is_link() { + span.link = links.len(); + links.push(spans.len()); + } + spans.push(span); + } + + start += line.len() + 1; + } + + self.spans = spans; + self.links = links; + self.longest = longest; + self.link = 0; + self.scroll = 0; + } + + /// Toggle between our two encodings. + fn toggle_encoding(&mut self) -> Action { + if matches!(self.encoding, Encoding::UTF8) { + self.encoding = Encoding::CP437; + } else { + self.encoding = Encoding::UTF8; + } + self.config.write().unwrap().encoding = self.encoding; + self.encoded_response = self.encoding.encode(&self.raw).into(); + self.parse(); + Action::Redraw + } + fn render_lines(&mut self) -> String { let mut out = String::new(); let limit = if self.mode == ui::Mode::Run { @@ -849,6 +928,7 @@ impl Menu { match key { Key::Char('\n') => self.action_open(), + Key::Ctrl('e') => self.toggle_encoding(), Key::Up | Key::Ctrl('p') | Key::Char('p') | Key::Ctrl('k') | Key::Char('k') => { self.action_up() } @@ -924,53 +1004,8 @@ impl Menu { } /// Parse gopher response into a Menu object. -pub fn parse(url: &str, raw: String) -> Menu { - let mut spans = vec![]; - let mut links = vec![]; - let mut longest = 0; - let mut start = 0; - - for line in raw.split_terminator('\n') { - // Check for Gopher's weird "end of response" message. - if line == ".\r" || line == "." { - break; - } - - if line == "" { - start += 1; - continue; - } - - if let Some(mut span) = parse_line(start, &raw) { - if span.text_len() > longest { - longest = span.text_len(); - } - if span.typ.is_link() { - span.link = links.len(); - links.push(spans.len()); - } - spans.push(span); - } - - start += line.len() + 1; - } - - Menu { - url: url.into(), - spans, - links, - longest, - raw, - input: String::new(), - link: 0, - mode: Default::default(), - scroll: 0, - searching: false, - size: (0, 0), - tls: false, - tor: false, - wide: false, - } +pub fn parse(url: &str, raw: Vec) -> Menu { + Menu::from(url, raw, Config::default(), false) } /// Parses a single line from a Gopher menu into a `LineSpan` struct. @@ -1042,7 +1077,7 @@ mod tests { macro_rules! parse { ($s:expr) => { - parse("test", $s.to_string()); + parse("test", $s.as_bytes().to_vec()); }; } diff --git a/src/ui.rs b/src/ui.rs index 073055c..663084e 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -275,12 +275,9 @@ impl UI { }; let typ = gopher::type_for_url(&url); match typ { - Type::Menu | Type::Search => Ok(Box::new(Menu::from( - url, - gopher::response_to_string(&res), - self.config.clone(), - tls, - ))), + Type::Menu | Type::Search => { + Ok(Box::new(Menu::from(url, res, self.config.clone(), tls))) + } Type::Text | Type::HTML => Ok(Box::new(Text::from(url, res, self.config.clone(), tls))), _ => Err(error!("Unsupported Gopher Response: {:?}", typ)), } @@ -294,7 +291,7 @@ impl UI { ) { Ok(Box::new(Menu::from( url, - source, + source.as_bytes().to_vec(), self.config.clone(), false, )))