From 6ae94473e31c0f7d2c1ca79294f529419f1c536f Mon Sep 17 00:00:00 2001 From: Andy Wortman Date: Sat, 2 Dec 2017 21:40:41 -0800 Subject: track dirty bit to know if we should redraw display also clean up a bunch of TODOs --- src/commands/fav.rs | 4 +- src/commands/profile.rs | 3 -- src/commands/twete.rs | 4 +- src/display/mod.rs | 125 +++++++++++++++++++++++++++++++++++++----------- src/main.rs | 74 +++++++++++++++------------- src/tw/mod.rs | 56 ++++++++++------------ todo | 1 + 7 files changed, 167 insertions(+), 100 deletions(-) diff --git a/src/commands/fav.rs b/src/commands/fav.rs index 5e5f2a2..02ec7dd 100644 --- a/src/commands/fav.rs +++ b/src/commands/fav.rs @@ -21,7 +21,7 @@ fn unfav(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, di let maybe_id = TweetId::parse(line.to_owned()); match maybe_id { Ok(twid) => { - if let Some(twete) = tweeter.retrieve_tweet(&twid).map(|x| x.clone()) { // TODO: no clone when this stops taking &mut self + if let Some(twete) = tweeter.retrieve_tweet(&twid) { let result = match tweeter.current_profile() { Some(user_profile) => queryer.do_api_post(&format!("{}?id={}", UNFAV_TWEET_URL, twete.id), &tweeter.app_key, &user_profile.creds), None => Err("No logged in user to unfav from".to_owned()) @@ -53,7 +53,7 @@ fn fav(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, disp match maybe_id { Ok(twid) => { // tweeter.to_twitter_tweet_id(twid)... - if let Some(twete) = tweeter.retrieve_tweet(&twid).map(|x| x.clone()) { // TODO: no clone when this stops taking &mut self + if let Some(twete) = tweeter.retrieve_tweet(&twid) { let result = match tweeter.current_profile() { Some(user_profile) => queryer.do_api_post(&format!("{}?id={}", FAV_TWEET_URL, twete.id), &tweeter.app_key, &user_profile.creds), None => Err("No logged in user to fav from".to_owned()) diff --git a/src/commands/profile.rs b/src/commands/profile.rs index e20859b..f3bfd8d 100644 --- a/src/commands/profile.rs +++ b/src/commands/profile.rs @@ -1,8 +1,5 @@ use display::DisplayInfo; use tw; -use std; -use std::collections::HashMap; -use hyper; use ::Queryer; use commands::Command; diff --git a/src/commands/twete.rs b/src/commands/twete.rs index ebf3c3d..d727b9e 100644 --- a/src/commands/twete.rs +++ b/src/commands/twete.rs @@ -54,7 +54,7 @@ fn twete(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, di // if it's just "t", enter compose mode. let text = line.trim().to_owned(); if text.len() == 0 { - display_info.mode = Some(::display::DisplayMode::Compose(text)); + display_info.set_mode(Some(::display::DisplayMode::Compose(text))); } else { send_twete(text, tweeter, queryer, display_info); } @@ -178,7 +178,7 @@ fn rep(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, disp if reply.len() > 0 { send_reply(full_reply, twid, tweeter, queryer, user_profile.creds, display_info); } else { - display_info.mode = Some(::display::DisplayMode::Reply(twid, full_reply)); + display_info.set_mode(Some(::display::DisplayMode::Reply(twid, full_reply))); } } else { display_info.status(format!("No tweet for id: {:?}", twid)); diff --git a/src/display/mod.rs b/src/display/mod.rs index c3c4692..04b200f 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -35,14 +35,15 @@ pub enum Infos { const COMPOSE_HEIGHT: u16 = 5; pub struct DisplayInfo { - pub log_height: u16, - pub prompt_height: u16, - pub mode: Option, - pub log_seek: u32, - pub infos_seek: u32, - pub log: Vec, - pub infos: Vec, - pub input_buf: Vec + log_height: u16, + prompt_height: u16, + mode: Option, + log_seek: u32, + infos_seek: u32, + log: Vec, + infos: Vec, + input_buf: Vec, + dirty: bool } impl Default for DisplayInfo { @@ -55,18 +56,91 @@ impl Default for DisplayInfo { infos_seek: 0, log: Vec::new(), infos: Vec::new(), - input_buf: Vec::new() + input_buf: Vec::new(), + dirty: false } } } impl DisplayInfo { + pub fn set_mode(&mut self, new_mode: Option) { + self.mode = new_mode; + self.dirty = true; + } + + pub fn get_mode(&self) -> &Option { + &self.mode + } + + pub fn adjust_infos_seek(&mut self, seek_adjust: Option) { + self.infos_seek = match seek_adjust { + Some(adjust) => { + /* + * So this might be weird to read. + * + * I don't know how to add an i32 in a saturating manner to a u32. + * + * That's what this does: + * if adjust is negative, negate to positive and saturating_sub it + * if adjust is positive, we can just saturating_add it + */ + if adjust < 0 { + self.infos_seek.saturating_sub(-adjust as u32) + } else { + self.infos_seek.saturating_add(adjust as u32) + } + }, + None => 0 + }; + self.dirty = true; + } + + pub fn adjust_log_seek(&mut self, seek_adjust: Option) { + self.log_seek = match seek_adjust { + Some(adjust) => { + /* + * So this might be weird to read. + * + * I don't know how to add an i32 in a saturating manner to a u32. + * + * That's what this does: + * if adjust is negative, negate to positive and saturating_sub it + * if adjust is positive, we can just saturating_add it + */ + if adjust < 0 { + self.log_seek.saturating_sub(-adjust as u32) + } else { + self.log_seek.saturating_add(adjust as u32) + } + }, + None => 0 + }; + self.dirty = true; + } + pub fn status(&mut self, stat: String) { self.log.push(stat); + self.dirty = true; } pub fn recv(&mut self, info: Infos) { self.infos.push(info); + self.dirty = true; + } + + pub fn input_buf_push(&mut self, c: char) { + self.input_buf.push(c); + self.dirty = true; + } + + pub fn input_buf_pop(&mut self) { + self.input_buf.pop(); + self.dirty = true; + } + + pub fn input_buf_drain(&mut self) -> String { + self.dirty = true; + self.input_buf.drain(..).collect() } pub fn ui_height(&self) -> u16 { @@ -79,20 +153,6 @@ impl DisplayInfo { */ fn into_display_lines(x: Vec, width: u16) -> Vec { ansi_aware_into_display_lines(x, width) - /* - let split_on_newline: Vec = x.into_iter() - .flat_map(|x| x.split("\n") - .map(|x| x.to_owned()) - .collect::>() - ).collect(); - let wrapped: Vec = split_on_newline.iter() - .map(|x| x.chars().collect::>()) - .flat_map(|x| x.chunks(width as usize) - .map(|x| x.into_iter().collect::()) - .collect::>()) - .collect(); - wrapped - */ } #[derive(Clone)] @@ -211,7 +271,6 @@ fn ansi_aware_into_display_lines(x: Vec, width: u16) -> Vec { "".to_owned() }, Some(AnsiInfo::Esc) => { - // TODO: flush ansi_code = None; format!("{}{}", AnsiInfo::Esc, c) }, @@ -342,6 +401,9 @@ fn ansi_aware_into_display_lines(x: Vec, width: u16) -> Vec { pub fn paint(tweeter: &::tw::TwitterCache, display_info: &mut DisplayInfo) -> Result<(), std::io::Error> { match termion::terminal_size() { Ok((width, height)) => { + if !display_info.dirty { + return Ok(()); + } // draw input prompt let mut i = 0; let log_size = 4; @@ -405,7 +467,7 @@ pub fn paint(tweeter: &::tw::TwitterCache, display_info: &mut DisplayInfo) -> Re lines_drawn += 1; } h += lines_drawn - 3; - (cursor_idx as u16 + 3, height as u16 - 5) // TODO: panic on underflow + (cursor_idx as u16 + 3, height as u16 - 5) // TODO: this panics on underflow } Some(DisplayMode::Reply(twid, msg)) => { let mut lines: Vec = vec![]; @@ -432,7 +494,7 @@ pub fn paint(tweeter: &::tw::TwitterCache, display_info: &mut DisplayInfo) -> Re lines_drawn += 1; } h += lines_drawn - 3; - (cursor_idx as u16 + 3, height as u16 - 5) // TODO: panic on underflow + (cursor_idx as u16 + 3, height as u16 - 5) // TODO: this panics on underflow } }; @@ -522,6 +584,7 @@ pub fn paint(tweeter: &::tw::TwitterCache, display_info: &mut DisplayInfo) -> Re println!("Can't get term dimensions: {}", e); } } + display_info.dirty = false; Ok(()) } @@ -778,6 +841,14 @@ pub fn render_twete_no_recurse(twete_id: &TweetId, tweeter: &tw::TwitterCache, d } } } + /* + for elem in tweet.media.iter() { + if line.contains(elem.0) { + result = result.replace(elem.0, &format!("[{}]", urls_to_include.len())); + urls_to_include.push(elem.0); + } + } + */ result }) .collect(); @@ -792,8 +863,6 @@ pub fn render_twete_no_recurse(twete_id: &TweetId, tweeter: &tw::TwitterCache, d if expanded.len() < (width - 9) as usize { // "[XX]: " is 6 + some padding space? text.push(format!("[{}]: {}", i, expanded)); } else { - // TODO: try to just show domain, THEN fall back to just a link if the - // domain is too long text.push(format!("[{}]: {}", i, short_url)); } } diff --git a/src/main.rs b/src/main.rs index 7595ea0..e5c3910 100644 --- a/src/main.rs +++ b/src/main.rs @@ -260,60 +260,68 @@ fn main() { fn handle_input(event: termion::event::Event, tweeter: &mut tw::TwitterCache, queryer: &mut ::Queryer, display_info: &mut display::DisplayInfo) { match event { Event::Key(Key::Backspace) => { - match display_info.mode.clone() { - None => { display_info.input_buf.pop(); }, + let new_mode = match display_info.get_mode().clone() { + None => { display_info.input_buf_pop(); None }, Some(display::DisplayMode::Compose(msg)) => { let mut newstr = msg.clone(); newstr.pop(); - display_info.mode = Some(display::DisplayMode::Compose(newstr)); + Some(display::DisplayMode::Compose(newstr)) }, Some(display::DisplayMode::Reply(twid, msg)) => { let mut newstr = msg.clone(); newstr.pop(); - display_info.mode = Some(display::DisplayMode::Reply(twid, newstr)); + Some(display::DisplayMode::Reply(twid, newstr)) } - } + }; + display_info.set_mode(new_mode); } // would Shift('\n') but.. that doesn't exist. // would Ctrl('\n') but.. that doesn't work. Event::Key(Key::Ctrl('u')) => { - match display_info.mode.clone() { - None => display_info.input_buf = vec![], + let new_mode = match display_info.get_mode().clone() { + None => { display_info.input_buf_drain(); None}, Some(display::DisplayMode::Compose(msg)) => { - // TODO: clear only one line? - display_info.mode = Some(display::DisplayMode::Compose("".to_owned())); + Some(display::DisplayMode::Compose("".to_owned())) } Some(display::DisplayMode::Reply(twid, msg)) => { - display_info.mode = Some(display::DisplayMode::Reply(twid, "".to_owned())); + Some(display::DisplayMode::Reply(twid, "".to_owned())) } - } + }; + display_info.set_mode(new_mode); } Event::Key(Key::Ctrl('n')) => { - match display_info.mode.clone() { + let new_mode = match display_info.get_mode().clone() { Some(display::DisplayMode::Compose(msg)) => { - display_info.mode = Some(display::DisplayMode::Compose(format!("{}{}", msg, "\n"))); + Some(display::DisplayMode::Compose(format!("{}{}", msg, "\n"))) } - _ => {} - } + mode @ _ => mode + }; + display_info.set_mode(new_mode); } - // TODO: ctrl+u, ctrl+w + // TODO: ctrl+w Event::Key(Key::Char(x)) => { - match display_info.mode.clone() { + // Unlike other cases where we care about DisplayMode here, + // we can't just set the display mode in this function.. + // + // commands can change display mode, but might not, so just + // let them do their thing and only explicitly set display + // mode when we know we ought to + match display_info.get_mode().clone() { None => { if x == '\n' { - let line = display_info.input_buf.drain(..).collect::(); + let line = display_info.input_buf_drain(); tweeter.handle_user_input(line.into_bytes(), queryer, display_info); } else { - display_info.input_buf.push(x); + display_info.input_buf_push(x); } } Some(display::DisplayMode::Compose(msg)) => { if x == '\n' { // TODO: move this somewhere better. ::commands::twete::send_twete(msg, tweeter, queryer, display_info); - display_info.mode = None; + display_info.set_mode(None) } else { - display_info.mode = Some(display::DisplayMode::Compose(format!("{}{}", msg, x))); + display_info.set_mode(Some(display::DisplayMode::Compose(format!("{}{}", msg, x)))) } } Some(display::DisplayMode::Reply(twid, msg)) => { @@ -327,27 +335,27 @@ fn handle_input(event: termion::event::Event, tweeter: &mut tw::TwitterCache, qu display_info.status("Cannot reply when not logged in".to_owned()); } } - display_info.mode = None; + display_info.set_mode(None) } else { - display_info.mode = Some(display::DisplayMode::Reply(twid, format!("{}{}", msg, x))); + display_info.set_mode(Some(display::DisplayMode::Reply(twid, format!("{}{}", msg, x)))) } } - } + }; }, Event::Key(Key::PageUp) => { - display_info.infos_seek = display_info.infos_seek.saturating_add(1); + display_info.adjust_infos_seek(Some(1)); } Event::Key(Key::PageDown) => { - display_info.infos_seek = display_info.infos_seek.saturating_sub(1); + display_info.adjust_infos_seek(Some(-1)); } Event::Key(Key::Home) => { - display_info.log_seek = display_info.log_seek.saturating_add(1); + display_info.adjust_log_seek(Some(1)); } Event::Key(Key::End) => { - display_info.log_seek = display_info.log_seek.saturating_sub(1); + display_info.adjust_log_seek(Some(-1)); } Event::Key(Key::Esc) => { - display_info.mode = None; + display_info.set_mode(None); } Event::Key(_) => { } Event::Mouse(_) => { } @@ -356,9 +364,7 @@ fn handle_input(event: termion::event::Event, tweeter: &mut tw::TwitterCache, qu } fn handle_twitter_line(conn_id: u8, line: Vec, mut tweeter: &mut tw::TwitterCache, mut queryer: &mut ::Queryer, display_info: &mut display::DisplayInfo) { - let jsonstr = std::str::from_utf8(&line).unwrap().trim(); - /* TODO: replace from_str with from_slice? */ - match serde_json::from_str(&jsonstr) { + match serde_json::from_slice(&line) { Ok(json) => { tw::handle_message(conn_id, json, &mut tweeter, display_info, &mut queryer); if tweeter.needs_save && tweeter.caching_permitted { @@ -366,7 +372,7 @@ fn handle_twitter_line(conn_id: u8, line: Vec, mut tweeter: &mut tw::Twitter } }, Err(e) => - display_info.status(format!("Error reading twitter line: {}", jsonstr)) + display_info.status(format!("Error reading twitter line: {:?}", std::str::from_utf8(&line))) } } @@ -445,7 +451,7 @@ fn do_ui( for command in commands::COMMANDS { help_lines.push(format!("{}{: Result { - // TODO: figure out how to return a Result> if id_str.starts_with("twitter:") { Ok(TweetId::Twitter(id_str.chars().skip("twitter:".len()).collect())) } else if id_str.starts_with(":") { @@ -429,49 +428,37 @@ impl TwitterProfile { } /* * Returns: "did this change?" - * - * but currently pessimistically always returns true right now - * TODO: check if this should return false! (that's probably a cache bug though.) */ pub fn add_following(&mut self, user_id: &String) -> bool { - self.following.insert(user_id.to_owned()); - self.following_history.insert(user_id.to_owned(), ("following".to_string(), Utc::now().timestamp())); - true + let mut changed = self.following.insert(user_id.to_owned()); + changed |= self.following_history.insert(user_id.to_owned(), ("following".to_string(), Utc::now().timestamp())).is_none(); + changed } /* * Returns: "did this change?" - * - * but currently pessimistically always returns true right now - * TODO: check if this should return false! (that's probably a cache bug though.) */ pub fn remove_following(&mut self, user_id: &String) -> bool { - self.following.remove(user_id); - self.following_history.insert(user_id.to_owned(), ("unfollowing".to_string(), Utc::now().timestamp())); - true + let mut changed = self.following.remove(user_id); + changed |= self.following_history.insert(user_id.to_owned(), ("unfollowing".to_string(), Utc::now().timestamp())).is_some(); + changed } /* * Returns: "did this change?" - * - * but currently pessimistically always returns true right now - * TODO: check if this should return false! (that's probably a cache bug though.) */ pub fn add_follower(&mut self, user_id: &String) -> bool { - self.followers.insert(user_id.to_owned()); - self.lost_followers.remove(user_id); - self.follower_history.insert(user_id.to_owned(), ("follow".to_string(), Utc::now().timestamp())); - true + let mut changed = self.followers.insert(user_id.to_owned()); + changed |= self.lost_followers.remove(user_id); + changed |= self.follower_history.insert(user_id.to_owned(), ("follow".to_string(), Utc::now().timestamp())).is_none(); + changed } /* * Returns: "did this change?" - * - * but currently pessimistically always returns true right now - * TODO: check if this should return false! (that's probably a cache bug though.) */ pub fn remove_follower(&mut self, user_id: &String) -> bool { - self.followers.remove(user_id); - self.lost_followers.insert(user_id.to_owned()); - self.follower_history.insert(user_id.to_owned(), ("unfollow".to_string(), Utc::now().timestamp())); - true + let mut changed = self.followers.remove(user_id); + changed |= self.lost_followers.insert(user_id.to_owned()); + changed |= self.follower_history.insert(user_id.to_owned(), ("unfollow".to_string(), Utc::now().timestamp())).is_some(); + changed } } @@ -851,7 +838,9 @@ fn handle_twitter_event( tweeter.cache_api_event(conn_id, structure.clone(), queryer, display_info); match events::Event::from_json(structure) { Ok(event) => { - display_info.recv(display::Infos::Event(event)); + if !tweeter.event_muted(&event) { + display_info.recv(display::Infos::Event(event)); + } }, Err(e) => { display_info.status(format!("Unknown twitter json: {:?}", e)); @@ -882,10 +871,15 @@ fn handle_twitter_twete( display_info: &mut DisplayInfo, _queryer: &mut ::Queryer) { //display_info.recv(display::Infos::Text(vec![format!("{:?}", structure)])); - let twete_id = structure["id_str"].as_str().unwrap().to_string(); + let twete_id = TweetId::Twitter( + structure["id_str"].as_str().unwrap().to_string() + ); tweeter.cache_api_tweet(serde_json::Value::Object(structure)); - display_info.recv(display::Infos::Tweet(TweetId::Twitter(twete_id))); - // display::render_twete(&twete_id, tweeter); + if let Some(twete) = tweeter.retrieve_tweet(&twete_id) { + if !tweeter.tweet_muted(twete) { + display_info.recv(display::Infos::Tweet(twete_id)); + } + } } fn handle_twitter_dm( diff --git a/todo b/todo index 764b787..acc1f20 100644 --- a/todo +++ b/todo @@ -45,3 +45,4 @@ mute rt_fav/rt_rt [ ] do not duplicate notify when you fav or rt a retweet +handle ctrl+c gracefully (save cache and don't trash terminal state lol) -- cgit v1.1