aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndy Wortman <ixineeringeverywhere@gmail.com>2017-11-26 22:06:02 -0800
committerAndy Wortman <ixineeringeverywhere@gmail.com>2017-11-26 22:06:02 -0800
commitd193f2bc3dd883851f9149f1564a6d1904525914 (patch)
tree3cbeb2196dd01290469365bc89686b40960796e1
parente4925f0311574cd954909695bb587902179f8680 (diff)
wrap lines intelligently with respect to ANSI sequences
-rw-r--r--Cargo.toml4
-rw-r--r--src/display/mod.rs250
-rw-r--r--src/tw/mod.rs24
3 files changed, 265 insertions, 13 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 78a38c5..b37cf52 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,8 +12,8 @@ a nice twitter
[[bin]]
name = "twidder"
# path = "main.rs"
-test = false
-bench = false
+#test = false
+#bench = false
[dependencies]
"termios" = "0.2.2"
diff --git a/src/display/mod.rs b/src/display/mod.rs
index 35efa33..0cc1050 100644
--- a/src/display/mod.rs
+++ b/src/display/mod.rs
@@ -3,6 +3,9 @@ extern crate termion;
use std::io::Write;
use std::io::stdout;
+use std::iter::Iterator;
+use std::fmt;
+
use self::termion::color;
use self::termion::{clear, cursor};
@@ -75,6 +78,8 @@ impl DisplayInfo {
* wraps x so each line is width or fewer characters, after splitting by \n.
*/
fn into_display_lines(x: Vec<String>, width: u16) -> Vec<String> {
+ ansi_aware_into_display_lines(x, width)
+ /*
let split_on_newline: Vec<String> = x.into_iter()
.flat_map(|x| x.split("\n")
.map(|x| x.to_owned())
@@ -87,6 +92,251 @@ fn into_display_lines(x: Vec<String>, width: u16) -> Vec<String> {
.collect::<Vec<String>>())
.collect();
wrapped
+ */
+}
+
+#[derive(Clone)]
+enum AnsiInfo {
+ Esc,
+ EscBracket,
+ CSI(String), // CSI <n_string> with no tailing character ... yet?
+ FullSequence(String, char) // CSI n_string param
+}
+
+impl fmt::Display for AnsiInfo {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> fmt::Result {
+ match self {
+ &AnsiInfo::Esc => {
+ write!(f, "\x1b")
+ },
+ &AnsiInfo::EscBracket => {
+ write!(f, "\x1b[")
+ },
+ &AnsiInfo::CSI(ref n) => {
+ write!(f, "\x1b[{}", n)
+ },
+ &AnsiInfo::FullSequence(ref n, ref c) => {
+ write!(f, "\x1b[{}{}", n, c)
+ }
+ }
+ }
+}
+
+#[derive(Clone)]
+struct TextState {
+ color: Option<String>, // Box<termion::color::Color>>,
+ underline: bool,
+ italic: bool
+}
+
+/*
+ * wraps x so each line is width for fewer displayed characters
+ * (this probably doesn't work for zero width unicode symbols)
+ *
+ * preserves coloration of the string across splits:
+ * | <-- wrap here
+ * "hello talking to \x1b[5m@som\x1b[0m"
+ * "\x1b[5mename\x1b[0m"
+ */
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn ansi_display_lines_test() {
+ let initial = "hello talking to \x1b[5m@somename\x1b[0m".to_owned();
+ let split = ::display::ansi_aware_into_display_lines(vec![initial], 22);
+ assert_eq!(split.len(), 2);
+ assert_eq!(split[0], "hello talking to \x1b[5m@some\x1b[0m");
+ assert_eq!(split[1], "\x1b[5mname\x1b[0m");
+ }
+}
+fn ansi_aware_into_display_lines(x: Vec<String>, width: u16) -> Vec<String> {
+ let mut current_color: Option<u8> = None;
+ let mut ansi_code: Option<AnsiInfo> = None;
+ let mut text_state: Option<TextState> = None;
+ let mut display_len: u16 = 0;
+ let mut split_lines = Vec::new();
+ if x.len() == 0 {
+ return split_lines;
+ } else {
+ split_lines.push(String::new());
+ }
+ for (i, line) in x.iter().enumerate() {
+ for chr in line.chars() {
+ let addend = match chr {
+ '\x1b' => {
+ match ansi_code.clone() {
+ None => {
+ ansi_code = Some(AnsiInfo::Esc);
+ "".to_owned()
+ }
+ Some(ansi) => {
+ ansi_code = Some(AnsiInfo::Esc);
+ format!("{}", ansi)
+ }
+ }
+ },
+ '[' => {
+ match ansi_code.clone() {
+ Some(AnsiInfo::Esc) => {
+ ansi_code = Some(AnsiInfo::EscBracket);
+ "".to_owned()
+ },
+ Some(info @ AnsiInfo::EscBracket) => {
+ format!("{}[", info)
+ },
+ Some(info @ AnsiInfo::CSI(_)) => {
+ format!("{}[", info)
+ },
+ Some(info @ AnsiInfo::FullSequence(_, _)) => {
+ format!("{}[", info)
+ },
+ None => {
+ "[".to_owned()
+ }
+ }
+ },
+ c @ '0'...'9' => {
+ match ansi_code.clone() {
+ Some(AnsiInfo::EscBracket) => {
+ ansi_code = Some(AnsiInfo::CSI(c.to_string()));
+ "".to_owned()
+ },
+ Some(info @ AnsiInfo::FullSequence(_, _)) => {
+ ansi_code = None;
+ format!("{}{}", info, c)
+ }
+ Some(AnsiInfo::CSI(mut n)) => {
+ n.push(c);
+ ansi_code = Some(AnsiInfo::CSI(n));
+ "".to_owned()
+ },
+ Some(AnsiInfo::Esc) => {
+ // TODO: flush
+ ansi_code = None;
+ format!("{}{}", AnsiInfo::Esc, c)
+ },
+ None => {
+ c.to_string()
+ }
+ }
+ },
+ ';' => {
+ match ansi_code.clone() {
+ Some(info @ AnsiInfo::FullSequence(_, _)) => {
+ ansi_code = None;
+ format!("{};", info)
+ }
+ Some(AnsiInfo::EscBracket) => {
+ ansi_code = None;
+ format!("{};", AnsiInfo::EscBracket)
+ },
+ Some(AnsiInfo::CSI(n)) => {
+ ansi_code = Some(AnsiInfo::CSI(format!("{};", n)));
+ "".to_string()
+ },
+ Some(AnsiInfo::Esc) => {
+ ansi_code = None;
+ format!("{};", AnsiInfo::Esc)
+ },
+ None => {
+ ';'.to_string()
+ }
+ }
+ },
+ c => {
+ match ansi_code.clone() {
+ Some(info @ AnsiInfo::FullSequence(_, _)) => {
+ panic!("This should not be reachable - a FullSequence should be flushed immediately after construction.");
+ }
+ Some(AnsiInfo::EscBracket) => {
+ ansi_code = Some(AnsiInfo::FullSequence("".to_owned(), c));
+ "".to_string()
+ },
+ Some(AnsiInfo::CSI(n)) => {
+ ansi_code = Some(AnsiInfo::FullSequence(n, c));
+ "".to_string()
+ },
+ Some(AnsiInfo::Esc) => {
+ ansi_code = None;
+ format!("{}{}", AnsiInfo::Esc, c)
+ },
+ None => {
+ c.to_string()
+ }
+ }
+ }
+ };
+
+ // if we've produced a full sequence, dump that to the string and set that as the
+ // curret info
+ //
+ // TODO: support ansi sequences other than m aka colors.
+
+ if let Some(AnsiInfo::FullSequence(n, c)) = ansi_code.clone() {
+ // this is not printable so we don't advance the printable text counter
+ split_lines.last_mut().unwrap().push_str(&format!("\x1b[{}{}", n, c));
+ text_state = match text_state {
+ None => {
+ if n != "0" && n != "" {
+ Some(TextState {
+ color: Some(n),
+ underline: false,
+ italic: false
+ })
+ } else {
+ None
+ }
+ },
+ Some(mut state) => {
+ if n == "0" || n == "" {
+ state.color = None;
+ } else {
+ state.color = Some(n);
+ };
+ Some(state)
+ }
+ };
+ ansi_code = None;
+ }
+
+ for chr in addend.chars() {
+ // If we're adding a new character, see if we have to add a new line
+ if display_len == width || chr == '\n' {
+ match &text_state {
+ &Some(ref state) => {
+ split_lines.last_mut().unwrap().push_str("\x1b[0m");
+ split_lines.push(String::new());
+ split_lines.last_mut().unwrap().push_str(&format!("\x1b[{}m", state.color.clone().unwrap_or("".to_owned())));
+ display_len = 0;
+ }
+ &None => {
+ split_lines.push(String::new());
+ display_len = 0;
+ }
+ }
+ }
+ // whatever happened, we're now ready to add a character
+ split_lines.last_mut().unwrap().push(chr);
+ display_len += 1;
+ }
+ }
+
+ if i < x.len() - 1 {
+ match &text_state {
+ &Some(ref state) => {
+ split_lines.last_mut().unwrap().push_str("\x1b[0m");
+ split_lines.push(String::new());
+ split_lines.last_mut().unwrap().push_str(&format!("\x1b[{}m", state.color.clone().unwrap_or("".to_owned())));
+ display_len = 0;
+ }
+ &None => {
+ split_lines.push(String::new());
+ display_len = 0;
+ }
+ }
+ }
+ }
+ split_lines
}
pub fn paint(tweeter: &::tw::TwitterCache, display_info: &mut DisplayInfo) -> Result<(), std::io::Error> {
diff --git a/src/tw/mod.rs b/src/tw/mod.rs
index ee3b73e..629a6c7 100644
--- a/src/tw/mod.rs
+++ b/src/tw/mod.rs
@@ -178,17 +178,19 @@ mod tests {
use super::*;
#[test]
fn tweet_id_parse_test() {
- assert_eq!(TweetId::parse("12345".to_string()), Some(TweetId::Today(12345)));
- assert_eq!(TweetId::parse("20170403:12345".to_string()), Some(TweetId::Dated("20170403".to_string(), 12345)));
- assert_eq!(TweetId::parse(":12345".to_string()), Some(TweetId::Bare(12345)));
- assert_eq!(TweetId::parse("twitter:12345".to_string()), Some(TweetId::Twitter("12345".to_string())));
- assert_eq!(TweetId::parse("twitter:asdf".to_string()), Some(TweetId::Twitter("asdf".to_string())));
- assert_eq!(TweetId::parse("a2345".to_string()), None);
- assert_eq!(TweetId::parse(":".to_string()), None);
- assert_eq!(TweetId::parse("::".to_string()), None);
- assert_eq!(TweetId::parse("a:13234".to_string()), None);
- assert_eq!(TweetId::parse(":a34".to_string()), None);
- assert_eq!(TweetId::parse("asdf:34".to_string()), None);
+ assert_eq!(TweetId::parse("12345".to_string()), Ok(TweetId::Today(12345)));
+ assert_eq!(TweetId::parse("20170403:12345".to_string()), Ok(TweetId::Dated("20170403".to_string(), 12345)));
+ assert_eq!(TweetId::parse(":12345".to_string()), Ok(TweetId::Bare(12345)));
+ assert_eq!(TweetId::parse("twitter:12345".to_string()), Ok(TweetId::Twitter("12345".to_string())));
+ assert_eq!(TweetId::parse("twitter:asdf".to_string()), Ok(TweetId::Twitter("asdf".to_string())));
+ assert_eq!(TweetId::parse("a2345".to_string()), Err("Unrecognized id string: a2345".to_owned()));
+ // TODO: clarify
+ assert_eq!(TweetId::parse(":".to_string()), Err("cannot parse integer from empty string".to_owned()));
+ // TODO: clarify
+ assert_eq!(TweetId::parse("::".to_string()), Err("invalid digit found in string".to_owned()));
+ assert_eq!(TweetId::parse("a:13234".to_string()), Err("Unrecognized id string: a:13234".to_owned()));
+ assert_eq!(TweetId::parse(":a34".to_string()), Err("invalid digit found in string".to_owned()));
+ assert_eq!(TweetId::parse("asdf:34".to_string()), Err("Unrecognized id string: asdf:34".to_owned()));
}
}