use crate::{
config::SharedConfig as Config,
encoding::Encoding,
terminal,
ui::{self, Action, Key, View, MAX_COLS},
};
use std::{borrow::Cow, fmt, str};
pub struct Text {
config: Config,
url: String,
raw_response: Vec<u8>,
encoded_response: String,
offset: usize,
lines: usize,
longest: usize,
size: (usize, usize),
pub tls: bool,
pub tor: bool,
mode: ui::Mode,
encoding: Encoding,
pub wide: bool,
scroll: usize,
}
impl fmt::Display for Text {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.url())
}
}
impl View for Text {
fn is_tls(&self) -> bool {
self.tls
}
fn is_tor(&self) -> bool {
self.tor
}
fn url(&self) -> &str {
self.url.as_ref()
}
fn raw(&self) -> &str {
str::from_utf8(&self.raw_response).unwrap_or_default()
}
fn term_size(&mut self, cols: usize, rows: usize) {
self.size = (cols, rows);
}
fn set_wide(&mut self, wide: bool) {
self.wide = wide;
}
fn wide(&mut self) -> bool {
self.wide
}
fn encoding(&self) -> Encoding {
self.encoding
}
fn respond(&mut self, c: Key) -> Action {
match c {
Key::Home => {
self.offset = 0;
Action::Redraw
}
Key::End => {
self.offset = self.final_scroll();
Action::Redraw
}
Key::Ctrl('e') | Key::Char('e') => self.toggle_encoding(),
Key::Down | Key::Ctrl('n') | Key::Char('n') | Key::Ctrl('j') | Key::Char('j') => {
if self.offset < self.final_scroll() {
self.offset += 1;
Action::Redraw
} else {
Action::None
}
}
Key::Up | Key::Ctrl('p') | Key::Char('p') | Key::Ctrl('k') | Key::Char('k') => {
if self.offset > 0 {
self.offset -= 1;
Action::Redraw
} else {
Action::None
}
}
Key::PageUp | Key::Char('-') => {
if self.offset > 0 {
if self.offset >= self.scroll_by() {
self.offset -= self.scroll_by();
} else {
self.offset = 0;
}
Action::Redraw
} else {
Action::None
}
}
Key::PageDown | Key::Char(' ') => {
self.offset += self.scroll_by();
if self.offset > self.final_scroll() {
self.offset = self.final_scroll();
}
Action::Redraw
}
_ => Action::Keypress(c),
}
}
fn render(&mut self) -> String {
let (_cols, rows) = self.size;
let mut out = String::new();
let wrap = self.config.read().unwrap().wrap;
let indent = self.indent_str(wrap);
let limit = if self.mode == ui::Mode::Run {
rows - 1
} else {
self.lines
};
let iter = wrap_text(&self.encoded_response, wrap)
.into_iter()
.skip(self.offset)
.take(limit);
for line in iter {
if line == ".\r" || line == "." {
continue;
}
if !self.wide {
out.push_str(&indent);
}
let line = line.trim_end_matches('\r').replace('\t', " ");
out.push_str(&line);
out.push_str(&format!("{}", terminal::ClearUntilNewline));
out.push_str("\r\n");
}
out.push_str(&format!("{}", terminal::ClearAfterCursor));
out
}
}
impl Text {
pub fn from(url: &str, response: Vec<u8>, config: Config, tls: bool) -> Text {
let mode = config.read().unwrap().mode;
let tor = config.read().unwrap().tor;
let encoding = config.read().unwrap().encoding;
let wide = config.read().unwrap().wide;
let scroll = config.read().unwrap().scroll;
let mut new = Text {
config,
url: url.into(),
encoded_response: String::new(),
raw_response: response,
offset: 0,
lines: 0,
longest: 0,
size: (0, 0),
mode,
tls,
tor,
encoding,
wide,
scroll,
};
new.encode_response();
new
}
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.encode_response();
Action::Redraw
}
fn encode_response(&mut self) {
self.encoded_response = self.encoding.encode(&self.raw_response).into();
let wrapped = wrap_text(
self.encoded_response.as_ref(),
self.config.read().unwrap().wrap,
);
self.lines = wrapped.len();
self.longest = wrapped.iter().map(|line| line.len()).max().unwrap_or(0) as usize;
}
fn final_scroll(&self) -> usize {
let padding = (self.size.1 as f64 * 0.9) as usize;
if self.lines > padding {
self.lines - padding
} else {
0
}
}
fn scroll_by(&self) -> usize {
if self.scroll == 0 {
self.size.1 - 1
} else {
self.scroll
}
}
fn longest_line_with_wrap(&self, wrap: usize) -> usize {
let longest = if self.longest > MAX_COLS {
MAX_COLS
} else {
self.longest
};
if wrap > 0 && longest > wrap {
wrap
} else {
longest
}
}
fn indent_str(&self, wrap: usize) -> Cow<str> {
let (cols, _) = self.size;
let longest = self.longest_line_with_wrap(wrap);
if cols >= longest && cols - longest <= 6 {
Cow::from("")
} else if cols >= longest {
Cow::from(" ".repeat((cols - longest) / 2))
} else {
Cow::from("")
}
}
}
fn wrap_text(lines: &str, wrap: usize) -> Vec<&str> {
if wrap == 0 {
return lines.split('\n').collect();
}
let mut out = vec![];
for mut line in lines.lines() {
let mut len = line.chars().count();
if len > wrap {
while len > wrap {
let (end, _) = line.char_indices().take(wrap + 1).last().unwrap();
if !matches!(&line[end - 1..end], " " | "-" | "," | "." | ":") {
if let Some(&(end, _)) = line
.char_indices()
.take(wrap + 1)
.collect::<Vec<_>>()
.iter()
.rev()
.skip(1)
.find(|(_, c)| matches!(c, ' ' | '-' | ',' | '.' | ':'))
{
out.push(&line[..=end]);
if end + 1 < line.len() {
line = &line[end + 1..];
len -= end;
} else {
len = 0;
break;
}
continue;
}
}
out.push(&line[..end]);
line = &line[end..];
len -= wrap;
}
if len > 0 {
out.push(line);
}
} else {
out.push(line);
}
}
out
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_cp437() {
let body = include_bytes!("../tests/CP437.txt");
let mut text = Text::from("", body.to_vec(), Config::default(), false);
text.mode = ui::Mode::Print;
let res = text.render();
assert!(!res.contains("╟"));
assert!(!res.contains("≈"));
assert!(!res.contains("Ω"));
assert!(!res.contains("Θ"));
text.toggle_encoding();
let res = text.render();
assert!(res.contains("╟"));
assert!(res.contains("≈"));
assert!(res.contains("Ω"));
assert!(res.contains("Θ"));
}
#[test]
fn test_wrapping() {
let text = "regular line
really really really really really really really really really really long line
super duper extra scooper hoopa loopa double doopa maxi paxi giga baxi very very long line
Qua nova re oblata omnis administratio belliconsistit militesque aversi a proelio ad studium audiendi et cognoscendi feruntur ubi hostes ad legatosexercitumque pervenerunt universi se ad pedes proiciunt orant ut adventus Caesaris expectetur captamsuam urbem videre...
really really really really really really really really really kinda-but-not-really long line
another regular line
";
let lines = wrap_text(text, 70);
assert_eq!("regular line", lines[0]);
assert_eq!(
"really really really really really really really really really really",
lines[1].trim()
);
assert_eq!("long line", lines[2].trim());
assert_eq!(
"super duper extra scooper hoopa loopa double doopa maxi paxi giga",
lines[3].trim()
);
assert_eq!("baxi very very long line", lines[4].trim());
assert_eq!(
"Qua nova re oblata omnis administratio belliconsistit militesque",
lines[5].trim()
);
assert_eq!(
"aversi a proelio ad studium audiendi et cognoscendi feruntur ubi",
lines[6].trim()
);
assert_eq!(
"hostes ad legatosexercitumque pervenerunt universi se ad pedes",
lines[7].trim()
);
assert_eq!(
"really really really really really really really really really kinda-",
lines[10].trim()
);
assert_eq!("but-not-really long line", lines[11].trim());
assert_eq!(13, lines.len());
}
}