From: Simon Tatham Date: Wed, 17 Jan 2024 07:41:53 +0000 (+0000) Subject: Run through rustfmt. X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ian/git?a=commitdiff_plain;h=266ac16b9cc9501f8d9d48ea2d72fe0be9d6339d;p=mastodonochrome.git Run through rustfmt. If this is a widespread standard, I might as well start getting used to it. --- diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..a1ffd27 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +max_width = 79 diff --git a/src/activity_stack.rs b/src/activity_stack.rs index 3ebdd12..34c5fbf 100644 --- a/src/activity_stack.rs +++ b/src/activity_stack.rs @@ -21,7 +21,7 @@ pub enum UtilityActivity { LogsMenu2, EgoLog, ExitMenu, - ExamineUser(String), // the account _id_, not its name + ExamineUser(String), // the account _id_, not its name ListUserFollowers(String), ListUserFollowees(String), InfoStatus(String), @@ -49,13 +49,19 @@ pub enum Activity { } impl From for Activity { - fn from(value: NonUtilityActivity) -> Self { Activity::NonUtil(value) } + fn from(value: NonUtilityActivity) -> Self { + Activity::NonUtil(value) + } } impl From for Activity { - fn from(value: UtilityActivity) -> Self { Activity::Util(value) } + fn from(value: UtilityActivity) -> Self { + Activity::Util(value) + } } impl From for Activity { - fn from(value: OverlayActivity) -> Self { Activity::Overlay(value) } + fn from(value: OverlayActivity) -> Self { + Activity::Overlay(value) + } } #[derive(PartialEq, Eq, Debug)] @@ -75,10 +81,10 @@ impl Activity { // gets reinitialised, because that's the simplest way to jump // you down to the new message. match self { - Activity::NonUtil(NonUtilityActivity::ComposeToplevel) | - Activity::NonUtil(NonUtilityActivity::PostComposeMenu) | - Activity::Util(UtilityActivity::ComposeReply(..)) | - Activity::Util(UtilityActivity::PostReplyMenu(..)) => false, + Activity::NonUtil(NonUtilityActivity::ComposeToplevel) + | Activity::NonUtil(NonUtilityActivity::PostComposeMenu) + | Activity::Util(UtilityActivity::ComposeReply(..)) + | Activity::Util(UtilityActivity::PostReplyMenu(..)) => false, _ => true, } } @@ -129,11 +135,11 @@ impl ActivityStack { match x { NonUtilityActivity::MainMenu => self.nonutil.clear(), y => { - let trunc = match self.nonutil.iter() - .position(|z| *z == y) { - Some(pos) => pos, - None => self.nonutil.len(), - }; + let trunc = + match self.nonutil.iter().position(|z| *z == y) { + Some(pos) => pos, + None => self.nonutil.len(), + }; self.nonutil.truncate(trunc); self.nonutil.push(y); } @@ -152,7 +158,7 @@ impl ActivityStack { _ => match self.nonutil.last() { Some(y) => Activity::NonUtil(y.clone()), _ => Activity::NonUtil(NonUtilityActivity::MainMenu), - } + }, } } @@ -161,8 +167,10 @@ impl ActivityStack { } pub fn chain_to(&mut self, act: Activity) { - assert!(self.overlay.is_none(), - "Don't expect to chain overlay actions"); + assert!( + self.overlay.is_none(), + "Don't expect to chain overlay actions" + ); self.pop(); self.goto(act); } @@ -172,125 +180,161 @@ impl ActivityStack { fn test() { let mut stk = ActivityStack::new(); - assert_eq!(stk, ActivityStack { - nonutil: vec! {}, - util: None, - initial_util: None, - overlay: None, - }); + assert_eq!( + stk, + ActivityStack { + nonutil: vec! {}, + util: None, + initial_util: None, + overlay: None, + } + ); stk.goto(NonUtilityActivity::HomeTimelineFile.into()); - assert_eq!(stk, ActivityStack { - nonutil: vec! { - NonUtilityActivity::HomeTimelineFile, - }, - util: None, - initial_util: None, - overlay: None, - }); + assert_eq!( + stk, + ActivityStack { + nonutil: vec! { + NonUtilityActivity::HomeTimelineFile, + }, + util: None, + initial_util: None, + overlay: None, + } + ); stk.goto(NonUtilityActivity::HomeTimelineFile.into()); - assert_eq!(stk, ActivityStack { - nonutil: vec! { - NonUtilityActivity::HomeTimelineFile, - }, - util: None, - initial_util: None, - overlay: None, - }); + assert_eq!( + stk, + ActivityStack { + nonutil: vec! { + NonUtilityActivity::HomeTimelineFile, + }, + util: None, + initial_util: None, + overlay: None, + } + ); stk.goto(NonUtilityActivity::MainMenu.into()); - assert_eq!(stk, ActivityStack { - nonutil: vec! {}, - util: None, - initial_util: None, - overlay: None, - }); + assert_eq!( + stk, + ActivityStack { + nonutil: vec! {}, + util: None, + initial_util: None, + overlay: None, + } + ); stk.goto(NonUtilityActivity::HomeTimelineFile.into()); - assert_eq!(stk, ActivityStack { - nonutil: vec! { - NonUtilityActivity::HomeTimelineFile, - }, - util: None, - initial_util: None, - overlay: None, - }); + assert_eq!( + stk, + ActivityStack { + nonutil: vec! { + NonUtilityActivity::HomeTimelineFile, + }, + util: None, + initial_util: None, + overlay: None, + } + ); stk.goto(UtilityActivity::UtilsMenu.into()); - assert_eq!(stk, ActivityStack { - nonutil: vec! { - NonUtilityActivity::HomeTimelineFile, - }, - util: Some(UtilityActivity::UtilsMenu), - initial_util: None, - overlay: None, - }); + assert_eq!( + stk, + ActivityStack { + nonutil: vec! { + NonUtilityActivity::HomeTimelineFile, + }, + util: Some(UtilityActivity::UtilsMenu), + initial_util: None, + overlay: None, + } + ); stk.goto(UtilityActivity::ReadMentions.into()); - assert_eq!(stk, ActivityStack { - nonutil: vec! { - NonUtilityActivity::HomeTimelineFile, - }, - util: Some(UtilityActivity::ReadMentions), - initial_util: None, - overlay: None, - }); + assert_eq!( + stk, + ActivityStack { + nonutil: vec! { + NonUtilityActivity::HomeTimelineFile, + }, + util: Some(UtilityActivity::ReadMentions), + initial_util: None, + overlay: None, + } + ); stk.pop(); - assert_eq!(stk, ActivityStack { - nonutil: vec! { - NonUtilityActivity::HomeTimelineFile, - }, - util: None, - initial_util: None, - overlay: None, - }); + assert_eq!( + stk, + ActivityStack { + nonutil: vec! { + NonUtilityActivity::HomeTimelineFile, + }, + util: None, + initial_util: None, + overlay: None, + } + ); stk.goto(UtilityActivity::ReadMentions.into()); - assert_eq!(stk, ActivityStack { - nonutil: vec! { - NonUtilityActivity::HomeTimelineFile, - }, - util: Some(UtilityActivity::ReadMentions), - initial_util: None, - overlay: None, - }); + assert_eq!( + stk, + ActivityStack { + nonutil: vec! { + NonUtilityActivity::HomeTimelineFile, + }, + util: Some(UtilityActivity::ReadMentions), + initial_util: None, + overlay: None, + } + ); stk.goto(NonUtilityActivity::HomeTimelineFile.into()); - assert_eq!(stk, ActivityStack { - nonutil: vec! { - NonUtilityActivity::HomeTimelineFile, - }, - util: None, - initial_util: None, - overlay: None, - }); + assert_eq!( + stk, + ActivityStack { + nonutil: vec! { + NonUtilityActivity::HomeTimelineFile, + }, + util: None, + initial_util: None, + overlay: None, + } + ); stk.pop(); - assert_eq!(stk, ActivityStack { - nonutil: vec! {}, - util: None, - initial_util: None, - overlay: None, - }); + assert_eq!( + stk, + ActivityStack { + nonutil: vec! {}, + util: None, + initial_util: None, + overlay: None, + } + ); stk.pop(); - assert_eq!(stk, ActivityStack { - nonutil: vec! {}, - util: None, - initial_util: None, - overlay: None, - }); + assert_eq!( + stk, + ActivityStack { + nonutil: vec! {}, + util: None, + initial_util: None, + overlay: None, + } + ); } diff --git a/src/auth.rs b/src/auth.rs index d65ad7f..8f1a009 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -11,9 +11,10 @@ pub enum AuthError { impl super::TopLevelErrorCandidate for AuthError {} impl std::fmt::Display for AuthError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> - Result<(), std::fmt::Error> - { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> Result<(), std::fmt::Error> { match self { AuthError::Nonexistent(_) => { write!(f, "not logged in") @@ -40,15 +41,19 @@ impl AuthConfig { pub fn load(cfgloc: &ConfigLocation) -> Result { let authfile = cfgloc.get_path("auth"); let authdata = match std::fs::read_to_string(&authfile) { - Err(e) => Err(AuthError::Nonexistent( - format!("unable to read config file '{}': {}", - authfile.display(), e))), + Err(e) => Err(AuthError::Nonexistent(format!( + "unable to read config file '{}': {}", + authfile.display(), + e + ))), Ok(d) => Ok(d), }?; let auth: Self = match serde_json::from_str(&authdata) { - Err(e) => Err(AuthError::Bad( - format!("unable to parse config file '{}': {}", - authfile.display(), e))), + Err(e) => Err(AuthError::Bad(format!( + "unable to parse config file '{}': {}", + authfile.display(), + e + ))), Ok(d) => Ok(d), }?; Ok(auth) diff --git a/src/client.rs b/src/client.rs index 4568a9a..00e382e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,18 +1,24 @@ use reqwest::Url; use std::collections::{HashMap, HashSet, VecDeque}; -use std::io::{Read, Write, IoSlice}; use std::fs::File; +use std::io::{IoSlice, Read, Write}; -use super::auth::{AuthConfig,AuthError}; +use super::auth::{AuthConfig, AuthError}; use super::config::ConfigLocation; -use super::types::*; use super::posting::Post; +use super::types::*; #[derive(Hash, Debug, PartialEq, Eq, Clone, Copy)] -pub enum Boosts { Show, Hide } +pub enum Boosts { + Show, + Hide, +} #[derive(Hash, Debug, PartialEq, Eq, Clone, Copy)] -pub enum Replies { Show, Hide } +pub enum Replies { + Show, + Hide, +} #[derive(Hash, Debug, PartialEq, Eq, Clone)] pub enum FeedId { @@ -62,7 +68,7 @@ pub enum Followness { Following { boosts: Boosts, languages: Vec, - } + }, } impl Followness { @@ -76,10 +82,7 @@ impl Followness { Boosts::Hide }; let languages = rel.languages.clone().unwrap_or(Vec::new()); - Followness::Following { - boosts, - languages, - } + Followness::Following { boosts, languages } } } } @@ -100,7 +103,10 @@ pub struct AccountDetails { } #[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum AccountFlag { Block, Mute } +pub enum AccountFlag { + Block, + Mute, +} pub struct Client { auth: AuthConfig, @@ -117,44 +123,54 @@ pub struct Client { #[derive(Debug, PartialEq, Eq, Clone)] pub enum ClientError { - Auth(AuthError), // message - InternalError(String), // message + Auth(AuthError), // message + InternalError(String), // message UrlParseError(String, String), // url, message - UrlError(String, String), // url, message + UrlError(String, String), // url, message NoAccountSource, } impl super::TopLevelErrorCandidate for ClientError {} impl From for ClientError { - fn from(err: AuthError) -> Self { ClientError::Auth(err) } + fn from(err: AuthError) -> Self { + ClientError::Auth(err) + } } impl From for ClientError { fn from(err: reqwest::Error) -> Self { match err.url() { - Some(url) => ClientError::UrlError( - url.to_string(), err.to_string()), + Some(url) => { + ClientError::UrlError(url.to_string(), err.to_string()) + } None => ClientError::InternalError(err.to_string()), } } } impl std::fmt::Display for ClientError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> - Result<(), std::fmt::Error> - { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> Result<(), std::fmt::Error> { match self { - ClientError::Auth(ref autherr) => - write!(f, "unable to read authentication: {}", autherr), - ClientError::InternalError(ref msg) => - write!(f, "internal failure: {}", msg), - ClientError::UrlParseError(ref url, ref msg) => - write!(f, "Parse failure {} (retrieving URL: {})", msg, url), - ClientError::UrlError(ref url, ref msg) => - write!(f, "{} (retrieving URL: {})", msg, url), - ClientError::NoAccountSource => - write!(f, "server did not send 'source' details for our account"), + ClientError::Auth(ref autherr) => { + write!(f, "unable to read authentication: {}", autherr) + } + ClientError::InternalError(ref msg) => { + write!(f, "internal failure: {}", msg) + } + ClientError::UrlParseError(ref url, ref msg) => { + write!(f, "Parse failure {} (retrieving URL: {})", msg, url) + } + ClientError::UrlError(ref url, ref msg) => { + write!(f, "{} (retrieving URL: {})", msg, url) + } + ClientError::NoAccountSource => write!( + f, + "server did not send 'source' details for our account" + ), } } } @@ -193,7 +209,8 @@ impl Req { } pub fn param(mut self, key: &str, value: T) -> Self - where T: ReqParam + where + T: ReqParam, { self.parameters.push((key.to_owned(), value.param_value())); self @@ -208,16 +225,19 @@ impl Req { }; let url = match parsed { Ok(url) => Ok(url), - Err(e) => Err(ClientError::UrlParseError( - urlstr.clone(), e.to_string())), + Err(e) => { + Err(ClientError::UrlParseError(urlstr.clone(), e.to_string())) + } }?; Ok((urlstr, url)) } - pub fn build(self, base_url: &str, client: &reqwest::blocking::Client, - bearer_token: Option<&str>) -> - Result<(String, reqwest::blocking::RequestBuilder), ClientError> - { + pub fn build( + self, + base_url: &str, + client: &reqwest::blocking::Client, + bearer_token: Option<&str>, + ) -> Result<(String, reqwest::blocking::RequestBuilder), ClientError> { let (urlstr, url) = self.url(base_url)?; let req = client.request(self.method, url); let req = match bearer_token { @@ -233,26 +253,37 @@ pub trait ReqParam { } impl ReqParam for &str { - fn param_value(self) -> String { self.to_owned() } + fn param_value(self) -> String { + self.to_owned() + } } impl ReqParam for String { - fn param_value(self) -> String { self } + fn param_value(self) -> String { + self + } } impl ReqParam for &String { - fn param_value(self) -> String { self.clone() } + fn param_value(self) -> String { + self.clone() + } } impl ReqParam for i32 { - fn param_value(self) -> String { self.to_string() } + fn param_value(self) -> String { + self.to_string() + } } impl ReqParam for usize { - fn param_value(self) -> String { self.to_string() } + fn param_value(self) -> String { + self.to_string() + } } impl ReqParam for bool { fn param_value(self) -> String { match self { false => "false", true => "true", - }.to_owned() + } + .to_owned() } } impl ReqParam for Visibility { @@ -260,15 +291,17 @@ impl ReqParam for Visibility { // A bit roundabout, but means we get to reuse the 'rename' // strings defined in types.rs let encoded = serde_json::to_string(&self).expect("can't fail"); - let decoded: String = serde_json::from_str(&encoded) - .expect("can't fail either"); + let decoded: String = + serde_json::from_str(&encoded).expect("can't fail either"); decoded } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum FeedExtend { - Initial, Past, Future + Initial, + Past, + Future, } pub fn reqwest_client() -> Result { @@ -294,8 +327,8 @@ pub fn reqwest_client() -> Result { attempt.error("too many redirects") } else if let Some(prev_url) = attempt.previous().last() { let next_url = attempt.url(); - if (prev_url.host(), prev_url.port()) != - (next_url.host(), next_url.port()) + if (prev_url.host(), prev_url.port()) + != (next_url.host(), next_url.port()) { // Stop and pass the 3xx response back to the // caller, rather than throwing a fatal error. @@ -325,10 +358,9 @@ pub struct TransactionLogEntry { } impl TransactionLogEntry { - fn translate_header(h: (&reqwest::header::HeaderName, - &reqwest::header::HeaderValue)) - -> (String, String) - { + fn translate_header( + h: (&reqwest::header::HeaderName, &reqwest::header::HeaderValue), + ) -> (String, String) { let (key, value) = h; let value = if key == reqwest::header::AUTHORIZATION { "[elided]".to_owned() @@ -346,20 +378,31 @@ impl TransactionLogEntry { } pub fn format_short(&self) -> String { - format!("{0} {1} -> {2} {3}", self.method.as_str(), self.url.as_str(), - self.status.as_str(), self.reason()) + format!( + "{0} {1} -> {2} {3}", + self.method.as_str(), + self.url.as_str(), + self.status.as_str(), + self.reason() + ) } pub fn format_full(&self) -> Vec { let mut lines = Vec::new(); - lines.push(format!("Request: {0} {1}", - self.method.as_str(), self.url.as_str())); + lines.push(format!( + "Request: {0} {1}", + self.method.as_str(), + self.url.as_str() + )); for (key, value) in &self.req_headers { lines.push(format!(" {0}: {1}", key, value)); } - lines.push(format!("Response: {0} {1}", - self.status.as_str(), self.reason())); + lines.push(format!( + "Response: {0} {1}", + self.status.as_str(), + self.reason() + )); for (key, value) in &self.rsp_headers { lines.push(format!(" {0}: {1}", key, value)); } @@ -375,17 +418,19 @@ impl TransactionLogEntry { // log file. If the disk fills up, it's a shame to // lose the logs, but we don't _also_ want to // terminate the client in a panic. - let _ = file.write_vectored(&[IoSlice::new(line.as_bytes()), - IoSlice::new(b"\n")]); + let _ = file.write_vectored(&[ + IoSlice::new(line.as_bytes()), + IoSlice::new(b"\n"), + ]); } } } } -pub fn execute_and_log_request(client: &reqwest::blocking::Client, - req: reqwest::blocking::Request) -> - Result<(reqwest::blocking::Response, TransactionLogEntry), ClientError> -{ +pub fn execute_and_log_request( + client: &reqwest::blocking::Client, + req: reqwest::blocking::Request, +) -> Result<(reqwest::blocking::Response, TransactionLogEntry), ClientError> { let method = req.method().clone(); let url = req.url().clone(); let mut req_headers = Vec::new(); @@ -400,8 +445,13 @@ pub fn execute_and_log_request(client: &reqwest::blocking::Client, rsp_headers.push(TransactionLogEntry::translate_header(h)); } - let log = TransactionLogEntry { method, url, req_headers, - status, rsp_headers }; + let log = TransactionLogEntry { + method, + url, + req_headers, + status, + rsp_headers, + }; Ok((rsp, log)) } @@ -426,7 +476,9 @@ impl Client { self.permit_write = permit; } - pub fn is_writable(&self) -> bool { self.permit_write } + pub fn is_writable(&self) -> bool { + self.permit_write + } pub fn set_logfile(&mut self, file: Option) { self.logfile = file; @@ -451,24 +503,27 @@ impl Client { log.write_to(&mut self.logfile) } - fn api_request_cl(&self, client: &reqwest::blocking::Client, req: Req) -> - Result<(String, reqwest::blocking::RequestBuilder), ClientError> - { + fn api_request_cl( + &self, + client: &reqwest::blocking::Client, + req: Req, + ) -> Result<(String, reqwest::blocking::RequestBuilder), ClientError> { if req.method != reqwest::Method::GET && !self.permit_write { return Err(ClientError::InternalError( - "Non-GET request attempted in readonly mode".to_string())); + "Non-GET request attempted in readonly mode".to_string(), + )); } let base_url = self.auth.instance_url.to_owned() + "/api/"; req.build(&base_url, client, Some(&self.auth.user_token)) } - fn api_request(&mut self, req: Req) -> - Result<(String, reqwest::blocking::Response), ClientError> - { + fn api_request( + &mut self, + req: Req, + ) -> Result<(String, reqwest::blocking::Response), ClientError> { let (urlstr, req) = self.api_request_cl(&self.client, req)?; - let (rsp, log) = execute_and_log_request( - &self.client, req.build()?)?; + let (rsp, log) = execute_and_log_request(&self.client, req.build()?)?; self.consume_transaction_log(log); Ok((urlstr, rsp)) } @@ -485,8 +540,7 @@ impl Client { } else { match serde_json::from_str(&rsp.text()?) { Ok(ac) => Ok(ac), - Err(e) => Err(ClientError::UrlError( - url, e.to_string())), + Err(e) => Err(ClientError::UrlError(url, e.to_string())), } }?; self.instance = Some(inst.clone()); @@ -498,8 +552,8 @@ impl Client { // Don't overwrite a cached account with a 'source' to one // without. if !ac.source.is_some() { - if let Some(source) = self.accounts.get(&ac.id) - .and_then(|ac| ac.source.as_ref()) + if let Some(source) = + self.accounts.get(&ac.id).and_then(|ac| ac.source.as_ref()) { ac.source = Some(source.clone()); } @@ -556,14 +610,16 @@ impl Client { } else { match serde_json::from_str(&rsp.text()?) { Ok(ac) => Ok(ac), - Err(e) => Err(ClientError::UrlError( - url.clone(), e.to_string())), + Err(e) => { + Err(ClientError::UrlError(url.clone(), e.to_string())) + } } }?; if ac.id != id { return Err(ClientError::UrlError( - url, format!("request returned wrong account id {}", - &ac.id))); + url, + format!("request returned wrong account id {}", &ac.id), + )); } self.cache_account(&ac); Ok(ac) @@ -574,39 +630,45 @@ impl Client { return Ok(st.clone()); } - let (url, rsp) = self.api_request(Req::get( - &("v1/polls/".to_owned() + id)))?; + let (url, rsp) = + self.api_request(Req::get(&("v1/polls/".to_owned() + id)))?; let rspstatus = rsp.status(); let poll: Poll = if !rspstatus.is_success() { Err(ClientError::UrlError(url.clone(), rspstatus.to_string())) } else { match serde_json::from_str(&rsp.text()?) { Ok(poll) => Ok(poll), - Err(e) => Err(ClientError::UrlError( - url.clone(), e.to_string())), + Err(e) => { + Err(ClientError::UrlError(url.clone(), e.to_string())) + } } }?; if poll.id != id { return Err(ClientError::UrlError( - url, format!("request returned wrong poll id {}", &poll.id))); + url, + format!("request returned wrong poll id {}", &poll.id), + )); } self.cache_poll(&poll); Ok(poll) } - pub fn account_relationship_by_id(&mut self, id: &str) -> - Result - { + pub fn account_relationship_by_id( + &mut self, + id: &str, + ) -> Result { let (url, rsp) = self.api_request( - Req::get("v1/accounts/relationships").param("id", id))?; + Req::get("v1/accounts/relationships").param("id", id), + )?; let rspstatus = rsp.status(); let rels: Vec = if !rspstatus.is_success() { Err(ClientError::UrlError(url.clone(), rspstatus.to_string())) } else { match serde_json::from_str(&rsp.text()?) { Ok(ac) => Ok(ac), - Err(e) => Err(ClientError::UrlError( - url.clone(), e.to_string())), + Err(e) => { + Err(ClientError::UrlError(url.clone(), e.to_string())) + } } }?; for rel in rels { @@ -615,8 +677,9 @@ impl Client { } } Err(ClientError::UrlError( - url, format!( - "request did not return expected account id {}", id))) + url, + format!("request did not return expected account id {}", id), + )) } pub fn status_by_id(&mut self, id: &str) -> Result { @@ -627,16 +690,17 @@ impl Client { // we had cached st.account = ac.clone(); } - if let Some(poll) = st.poll.as_ref().and_then( - |poll| self.polls.get(&poll.id)) { + if let Some(poll) = + st.poll.as_ref().and_then(|poll| self.polls.get(&poll.id)) + { // Ditto with the poll, if any st.poll = Some(poll.clone()); } return Ok(st); } - let (url, rsp) = self.api_request(Req::get( - &("v1/statuses/".to_owned() + id)))?; + let (url, rsp) = + self.api_request(Req::get(&("v1/statuses/".to_owned() + id)))?; let rspstatus = rsp.status(); let st: Status = if !rspstatus.is_success() { Err(ClientError::UrlError(url.clone(), rspstatus.to_string())) @@ -650,16 +714,18 @@ impl Client { }?; if st.id != id { return Err(ClientError::UrlError( - url, format!("request returned wrong status id {}", - &st.id))); + url, + format!("request returned wrong status id {}", &st.id), + )); } self.cache_status(&st); Ok(st) } - pub fn notification_by_id(&mut self, id: &str) -> - Result - { + pub fn notification_by_id( + &mut self, + id: &str, + ) -> Result { if let Some(not) = self.notifications.get(id) { let mut not = not.clone(); if let Some(ac) = self.accounts.get(¬.account.id) { @@ -676,8 +742,8 @@ impl Client { return Ok(not); } - let (url, rsp) = self.api_request(Req::get( - &("v1/notifications/".to_owned() + id)))?; + let (url, rsp) = self + .api_request(Req::get(&("v1/notifications/".to_owned() + id)))?; let rspstatus = rsp.status(); let not: Notification = if !rspstatus.is_success() { Err(ClientError::UrlError(url.clone(), rspstatus.to_string())) @@ -691,17 +757,20 @@ impl Client { }?; if not.id != id { return Err(ClientError::UrlError( - url, format!( - "request returned wrong notification id {}", ¬.id))); + url, + format!("request returned wrong notification id {}", ¬.id), + )); } self.cache_notification(¬); Ok(not) } // Ok(bool) tells you whether any new items were in fact retrieved - pub fn fetch_feed(&mut self, id: &FeedId, ext: FeedExtend) -> - Result - { + pub fn fetch_feed( + &mut self, + id: &FeedId, + ext: FeedExtend, + ) -> Result { if ext == FeedExtend::Initial { if self.feeds.contains_key(id) { // No need to fetch the initial contents - we already @@ -744,12 +813,10 @@ impl Client { FeedId::Mentions => { Req::get("v1/notifications").param("types[]", "mention") } - FeedId::Ego => { - Req::get("v1/notifications") - .param("types[]", "reblog") - .param("types[]", "follow") - .param("types[]", "favourite") - } + FeedId::Ego => Req::get("v1/notifications") + .param("types[]", "reblog") + .param("types[]", "follow") + .param("types[]", "favourite"), FeedId::Favouriters(id) => { Req::get(&format!("v1/statuses/{id}/favourited_by")) } @@ -766,44 +833,54 @@ impl Client { let req = match ext { FeedExtend::Initial => req.param("limit", 32), - FeedExtend::Past => if let Some(feed) = self.feeds.get(id) { - match feed.extend_past { - None => return Ok(false), - Some(ref params) => { - let mut req = req; - for (key, value) in params { - req = req.param(key, value); + FeedExtend::Past => { + if let Some(feed) = self.feeds.get(id) { + match feed.extend_past { + None => return Ok(false), + Some(ref params) => { + let mut req = req; + for (key, value) in params { + req = req.param(key, value); + } + req } - req } + } else { + req } - } else { req }, - FeedExtend::Future => if let Some(feed) = self.feeds.get(id) { - match feed.extend_future { - None => return Ok(false), - Some(ref params) => { - let mut req = req; - for (key, value) in params { - req = req.param(key, value); + } + FeedExtend::Future => { + if let Some(feed) = self.feeds.get(id) { + match feed.extend_future { + None => return Ok(false), + Some(ref params) => { + let mut req = req; + for (key, value) in params { + req = req.param(key, value); + } + req } - req } + } else { + req } - } else { req }, + } }; let (url, rsp) = self.api_request(req)?; let rspstatus = rsp.status(); if !rspstatus.is_success() { - return Err(ClientError::UrlError( - url, rspstatus.to_string())); + return Err(ClientError::UrlError(url, rspstatus.to_string())); } // Keep the Link: headers after we consume the response, for // use later once we've constructed a Feed - let link_headers: Vec<_> = rsp.headers() + let link_headers: Vec<_> = rsp + .headers() .get_all(reqwest::header::LINK) - .iter().cloned().collect(); + .iter() + .cloned() + .collect(); let body = rsp.text()?; @@ -811,8 +888,11 @@ impl Client { // depending on the feed. But in all cases we expect to end up // with a list of ids. let ids: VecDeque = match id { - FeedId::Home | FeedId::Local | FeedId::Public | - FeedId::Hashtag(..) | FeedId::User(..) => { + FeedId::Home + | FeedId::Local + | FeedId::Public + | FeedId::Hashtag(..) + | FeedId::User(..) => { let sts: Vec = match serde_json::from_str(&body) { Ok(sts) => Ok(sts), Err(e) => { @@ -825,13 +905,14 @@ impl Client { sts.iter().rev().map(|st| st.id.clone()).collect() } FeedId::Mentions | FeedId::Ego => { - let mut nots: Vec = match serde_json::from_str( - &body) { - Ok(nots) => Ok(nots), - Err(e) => { - Err(ClientError::UrlError(url.clone(), e.to_string())) - } - }?; + let mut nots: Vec = + match serde_json::from_str(&body) { + Ok(nots) => Ok(nots), + Err(e) => Err(ClientError::UrlError( + url.clone(), + e.to_string(), + )), + }?; match id { FeedId::Mentions => { @@ -841,15 +922,15 @@ impl Client { // code can safely .unwrap() or .expect() the status // from notifications they get via this feed. nots.retain(|not| { - not.ntype == NotificationType::Mention && - not.status.is_some() + not.ntype == NotificationType::Mention + && not.status.is_some() }); } FeedId::Ego => { nots.retain(|not| { - not.ntype == NotificationType::Reblog || - not.ntype == NotificationType::Follow || - not.ntype == NotificationType::Favourite + not.ntype == NotificationType::Reblog + || not.ntype == NotificationType::Follow + || not.ntype == NotificationType::Favourite }); } _ => panic!("outer match passed us {:?}", id), @@ -859,8 +940,10 @@ impl Client { } nots.iter().rev().map(|not| not.id.clone()).collect() } - FeedId::Favouriters(..) | FeedId::Boosters(..) | - FeedId::Followers(..) | FeedId::Followees(..) => { + FeedId::Favouriters(..) + | FeedId::Boosters(..) + | FeedId::Followers(..) + | FeedId::Followees(..) => { let acs: Vec = match serde_json::from_str(&body) { Ok(acs) => Ok(acs), Err(e) => { @@ -877,12 +960,15 @@ impl Client { match ext { FeedExtend::Initial => { - self.feeds.insert(id.clone(), Feed { - ids, - origin: 0, - extend_past: None, - extend_future: None, - }); + self.feeds.insert( + id.clone(), + Feed { + ids, + origin: 0, + extend_past: None, + extend_future: None, + }, + ); } FeedExtend::Future => { let feed = self.feeds.get_mut(id).unwrap(); @@ -903,13 +989,15 @@ impl Client { for linkhdr in link_headers { let linkhdr_str = match linkhdr.to_str() { Ok(s) => Ok(s), - Err(e) => Err(ClientError::UrlError( - url.clone(), e.to_string())), + Err(e) => { + Err(ClientError::UrlError(url.clone(), e.to_string())) + } }?; let links = match parse_link_header::parse(linkhdr_str) { Ok(links) => Ok(links), - Err(e) => Err(ClientError::UrlError( - url.clone(), e.to_string())), + Err(e) => { + Err(ClientError::UrlError(url.clone(), e.to_string())) + } }?; for (rel, link) in links { match rel.as_deref() { @@ -924,11 +1012,15 @@ impl Client { // past, then the future link we have already is // better than the new one (which will cause us to // re-fetch stuff we already had). And vice versa. - Some("next") => if ext != FeedExtend::Future { - feed.extend_past = Some(link.queries); + Some("next") => { + if ext != FeedExtend::Future { + feed.extend_past = Some(link.queries); + } } - Some("prev") => if ext != FeedExtend::Past { - feed.extend_future = Some(link.queries); + Some("prev") => { + if ext != FeedExtend::Past { + feed.extend_future = Some(link.queries); + } } _ => (), } @@ -939,14 +1031,16 @@ impl Client { } pub fn borrow_feed(&self, id: &FeedId) -> &Feed { - self.feeds.get(id).expect( - "should only ever borrow feeds that have been fetched") + self.feeds + .get(id) + .expect("should only ever borrow feeds that have been fetched") } pub fn start_streaming_thread( - &mut self, id: &StreamId, receiver: Box) -> - Result<(), ClientError> - { + &mut self, + id: &StreamId, + receiver: Box, + ) -> Result<(), ClientError> { let req = match id { StreamId::User => Req::get("v1/streaming/user"), }; @@ -954,8 +1048,7 @@ impl Client { let client = reqwest_client()?; let (url, mut req) = self.api_request_cl(&client, req)?; - let (rsp, log) = execute_and_log_request( - &self.client, req.build()?)?; + let (rsp, log) = execute_and_log_request(&self.client, req.build()?)?; self.consume_transaction_log(log); let mut rsp = rsp; if rsp.status().is_redirection() { @@ -980,42 +1073,54 @@ impl Client { return Err(ClientError::UrlError( url, "received redirection without a Location header" - .to_owned())); + .to_owned(), + )); } Some(hval) => { let bval = hval.as_bytes(); let sval = match std::str::from_utf8(bval) { Ok(s) => s, - Err(_) => return Err(ClientError::UrlError( - url, "HTTP redirect URL was invalid UTF-8" - .to_owned())), + Err(_) => { + return Err(ClientError::UrlError( + url, + "HTTP redirect URL was invalid UTF-8" + .to_owned(), + )) + } }; let newurl = match rsp.url().join(sval) { - Ok(u) => u, - Err(e) => return Err(ClientError::UrlError( - url, format!("processing redirection: {}", e))), + Ok(u) => u, + Err(e) => { + return Err(ClientError::UrlError( + url, + format!("processing redirection: {}", e), + )) + } }; let instance = self.instance()?; let ok = match &instance.configuration.urls.streaming { None => false, - Some(s) => if let Ok(goodurl) = Url::parse(s) { - (goodurl.host(), goodurl.port()) == - (newurl.host(), newurl.port()) - } else { - false + Some(s) => { + if let Ok(goodurl) = Url::parse(s) { + (goodurl.host(), goodurl.port()) + == (newurl.host(), newurl.port()) + } else { + false + } } }; if !ok { return Err(ClientError::UrlError( url, - format!("redirection to suspicious URL {}", - sval))); + format!("redirection to suspicious URL {}", sval), + )); } - req = client.request(method, newurl) + req = client + .request(method, newurl) .bearer_auth(&self.auth.user_token); - let (newrsp, log) = execute_and_log_request( - &self.client, req.build()?)?; + let (newrsp, log) = + execute_and_log_request(&self.client, req.build()?)?; self.consume_transaction_log(log); rsp = newrsp; } @@ -1024,8 +1129,7 @@ impl Client { let rspstatus = rsp.status(); if !rspstatus.is_success() { - return Err(ClientError::UrlError( - url, rspstatus.to_string())); + return Err(ClientError::UrlError(url, rspstatus.to_string())); } let id = id.clone(); @@ -1038,10 +1142,10 @@ impl Client { while let Ok(sz) = rsp.read(&mut buf) { let read = &buf[..sz]; vec.extend_from_slice(read); - let mut lines_iter = vec.split_inclusive(|c| *c == b'\n') - .peekable(); - while let Some(line) = lines_iter.next_if( - |line| line.ends_with(b"\n")) + let mut lines_iter = + vec.split_inclusive(|c| *c == b'\n').peekable(); + while let Some(line) = + lines_iter.next_if(|line| line.ends_with(b"\n")) { if line.starts_with(b":") { // Ignore lines starting with ':': in the @@ -1056,7 +1160,7 @@ impl Client { }; receiver(StreamUpdate { id: id.clone(), - response: rsp + response: rsp, }); } } @@ -1072,16 +1176,17 @@ impl Client { receiver(StreamUpdate { id: id.clone(), - response: StreamResponse::EOF + response: StreamResponse::EOF, }); }); Ok(()) } - pub fn process_stream_update(&mut self, up: StreamUpdate) -> - Result, ClientError> - { + pub fn process_stream_update( + &mut self, + up: StreamUpdate, + ) -> Result, ClientError> { let mut updates = HashSet::new(); match (up.id, up.response) { @@ -1107,26 +1212,25 @@ impl Client { // of a problem and let them decide. So probably this // function ends up returning a Result, and our owner // responds to an error by putting it in the Error Log. - _ => (), } Ok(updates) } - pub fn account_by_name(&mut self, name: &str) -> - Result - { - let (url, rsp) = self.api_request( - Req::get("v1/accounts/lookup").param("acct", name))?; + pub fn account_by_name( + &mut self, + name: &str, + ) -> Result { + let (url, rsp) = self + .api_request(Req::get("v1/accounts/lookup").param("acct", name))?; let rspstatus = rsp.status(); let ac: Account = if !rspstatus.is_success() { Err(ClientError::UrlError(url, rspstatus.to_string())) } else { match serde_json::from_str(&rsp.text()?) { Ok(ac) => Ok(ac), - Err(e) => Err(ClientError::UrlError( - url, e.to_string())), + Err(e) => Err(ClientError::UrlError(url, e.to_string())), } }?; self.cache_account(&ac); @@ -1141,14 +1245,13 @@ impl Client { .param("language", &post.m.language); let req = match &post.m.content_warning { None => req, - Some(text) => req - .param("sensitive", true) - .param("spoiler_text", text), + Some(text) => { + req.param("sensitive", true).param("spoiler_text", text) + } }; let req = match &post.m.in_reply_to_id { None => req, - Some(id) => req - .param("in_reply_to_id", id), + Some(id) => req.param("in_reply_to_id", id), }; let (url, rsp) = self.api_request(req)?; @@ -1160,11 +1263,13 @@ impl Client { } } - pub fn fave_boost_post(&mut self, id: &str, verb: &str) - -> Result<(), ClientError> - { - let (url, rsp) = self.api_request(Req::post( - &format!("v1/statuses/{id}/{verb}")))?; + pub fn fave_boost_post( + &mut self, + id: &str, + verb: &str, + ) -> Result<(), ClientError> { + let (url, rsp) = + self.api_request(Req::post(&format!("v1/statuses/{id}/{verb}")))?; let rspstatus = rsp.status(); // Cache the returned status so as to update its faved/boosted flags let st: Status = if !rspstatus.is_success() { @@ -1172,42 +1277,44 @@ impl Client { } else { match serde_json::from_str(&rsp.text()?) { Ok(st) => Ok(st), - Err(e) => { - Err(ClientError::UrlError(url, e.to_string())) - } + Err(e) => Err(ClientError::UrlError(url, e.to_string())), } }?; self.cache_status(&st); Ok(()) } - pub fn favourite_post(&mut self, id: &str, enable: bool) - -> Result<(), ClientError> - { - let verb = if enable {"favourite"} else {"unfavourite"}; + pub fn favourite_post( + &mut self, + id: &str, + enable: bool, + ) -> Result<(), ClientError> { + let verb = if enable { "favourite" } else { "unfavourite" }; self.fave_boost_post(id, verb) } - pub fn boost_post(&mut self, id: &str, enable: bool) - -> Result<(), ClientError> - { - let verb = if enable {"reblog"} else {"unreblog"}; + pub fn boost_post( + &mut self, + id: &str, + enable: bool, + ) -> Result<(), ClientError> { + let verb = if enable { "reblog" } else { "unreblog" }; self.fave_boost_post(id, verb) } - pub fn status_context(&mut self, id: &str) -> Result - { - let (url, rsp) = self.api_request(Req::get( - &format!("v1/statuses/{id}/context")))?; + pub fn status_context( + &mut self, + id: &str, + ) -> Result { + let (url, rsp) = + self.api_request(Req::get(&format!("v1/statuses/{id}/context")))?; let rspstatus = rsp.status(); let ctx: Context = if !rspstatus.is_success() { Err(ClientError::UrlError(url, rspstatus.to_string())) } else { match serde_json::from_str(&rsp.text()?) { Ok(st) => Ok(st), - Err(e) => { - Err(ClientError::UrlError(url, e.to_string())) - } + Err(e) => Err(ClientError::UrlError(url, e.to_string())), } }?; for st in &ctx.ancestors { @@ -1219,10 +1326,11 @@ impl Client { Ok(ctx) } - pub fn vote_in_poll(&mut self, id: &str, - choices: impl Iterator) - -> Result<(), ClientError> - { + pub fn vote_in_poll( + &mut self, + id: &str, + choices: impl Iterator, + ) -> Result<(), ClientError> { let choices: Vec<_> = choices.collect(); let mut req = Req::post(&format!("v1/polls/{id}/votes")); for choice in choices { @@ -1236,21 +1344,22 @@ impl Client { } else { match serde_json::from_str(&rsp.text()?) { Ok(poll) => Ok(poll), - Err(e) => { - Err(ClientError::UrlError(url, e.to_string())) - } + Err(e) => Err(ClientError::UrlError(url, e.to_string())), } }?; self.cache_poll(&poll); Ok(()) } - pub fn set_following(&mut self, id: &str, follow: Followness) - -> Result<(), ClientError> - { + pub fn set_following( + &mut self, + id: &str, + follow: Followness, + ) -> Result<(), ClientError> { let req = match follow { - Followness::NotFollowing => - Req::post(&format!("v1/accounts/{id}/unfollow")), + Followness::NotFollowing => { + Req::post(&format!("v1/accounts/{id}/unfollow")) + } Followness::Following { boosts, languages } => { let mut req = Req::post(&format!("v1/accounts/{id}/follow")) .param("reblogs", boosts == Boosts::Show); @@ -1269,18 +1378,25 @@ impl Client { } } - pub fn set_account_flag(&mut self, id: &str, flag: AccountFlag, - enable: bool) -> Result<(), ClientError> - { + pub fn set_account_flag( + &mut self, + id: &str, + flag: AccountFlag, + enable: bool, + ) -> Result<(), ClientError> { let req = match (flag, enable) { - (AccountFlag::Block, true) => - Req::post(&format!("v1/accounts/{id}/block")), - (AccountFlag::Block, false) => - Req::post(&format!("v1/accounts/{id}/unblock")), - (AccountFlag::Mute, true) => - Req::post(&format!("v1/accounts/{id}/mute")), - (AccountFlag::Mute, false) => - Req::post(&format!("v1/accounts/{id}/unmute")), + (AccountFlag::Block, true) => { + Req::post(&format!("v1/accounts/{id}/block")) + } + (AccountFlag::Block, false) => { + Req::post(&format!("v1/accounts/{id}/unblock")) + } + (AccountFlag::Mute, true) => { + Req::post(&format!("v1/accounts/{id}/mute")) + } + (AccountFlag::Mute, false) => { + Req::post(&format!("v1/accounts/{id}/unmute")) + } }; let (url, rsp) = self.api_request(req)?; let rspstatus = rsp.status(); @@ -1291,9 +1407,11 @@ impl Client { } } - pub fn set_account_details(&mut self, id: &str, details: AccountDetails) - -> Result<(), ClientError> - { + pub fn set_account_details( + &mut self, + id: &str, + details: AccountDetails, + ) -> Result<(), ClientError> { // TODO: add "note" with details.bio, and "fields_attributes" // for the variable info fields let req = Req::patch("v1/accounts/update_credentials") @@ -1318,14 +1436,16 @@ impl Client { } else { match serde_json::from_str(&rsp.text()?) { Ok(ac) => Ok(ac), - Err(e) => Err(ClientError::UrlError( - url.clone(), e.to_string())), + Err(e) => { + Err(ClientError::UrlError(url.clone(), e.to_string())) + } } }?; if ac.id != id { return Err(ClientError::UrlError( - url, format!("request returned wrong account id {}", - &ac.id))); + url, + format!("request returned wrong account id {}", &ac.id), + )); } self.cache_account(&ac); Ok(()) diff --git a/src/coloured_string.rs b/src/coloured_string.rs index eb3906c..e6a8b80 100644 --- a/src/coloured_string.rs +++ b/src/coloured_string.rs @@ -1,13 +1,19 @@ -use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthChar; +use unicode_width::UnicodeWidthStr; pub trait ColouredStringCommon { - fn text(&self) -> & str; - fn colours(&self) -> & str; + fn text(&self) -> &str; + fn colours(&self) -> &str; - fn is_empty(&self) -> bool { self.text().is_empty() } - fn nchars(&self) -> usize { self.text().chars().count() } - fn width(&self) -> usize { UnicodeWidthStr::width(self.text()) } + fn is_empty(&self) -> bool { + self.text().is_empty() + } + fn nchars(&self) -> usize { + self.text().chars().count() + } + fn width(&self) -> usize { + UnicodeWidthStr::width(self.text()) + } fn slice<'a>(&'a self) -> ColouredStringSlice<'a> { ColouredStringSlice { @@ -45,8 +51,10 @@ pub trait ColouredStringCommon { } fn repeat(&self, count: usize) -> ColouredString { - ColouredString::general(&self.text().repeat(count), - &self.colours().repeat(count)) + ColouredString::general( + &self.text().repeat(count), + &self.colours().repeat(count), + ) } fn recolour(&self, colour: char) -> ColouredString { @@ -61,13 +69,21 @@ pub struct ColouredString { } impl ColouredStringCommon for ColouredString { - fn text(&self) -> &str { &self.text } - fn colours(&self) -> &str { &self.colours } + fn text(&self) -> &str { + &self.text + } + fn colours(&self) -> &str { + &self.colours + } } impl<'a> ColouredStringCommon for &'a ColouredString { - fn text(&self) -> &str { &self.text } - fn colours(&self) -> &str { &self.colours } + fn text(&self) -> &str { + &self.text + } + fn colours(&self) -> &str { + &self.colours + } } // I'd have liked here to write @@ -106,17 +122,21 @@ impl ColouredString { } pub fn general(text: &str, colours: &str) -> Self { - assert_eq!(text.chars().count(), colours.chars().count(), - "Mismatched lengths in ColouredString::general"); + assert_eq!( + text.chars().count(), + colours.chars().count(), + "Mismatched lengths in ColouredString::general" + ); ColouredString { text: text.to_owned(), colours: colours.to_owned(), } } - fn concat - (lhs: T, rhs: U) -> ColouredString - { + fn concat( + lhs: T, + rhs: U, + ) -> ColouredString { ColouredString { text: lhs.text().to_owned() + rhs.text(), colours: lhs.colours().to_owned() + rhs.colours(), @@ -136,19 +156,30 @@ pub struct ColouredStringSlice<'a> { } impl<'a> ColouredStringCommon for ColouredStringSlice<'a> { - fn text(&self) -> &str { self.text } - fn colours(&self) -> &str { self.colours } + fn text(&self) -> &str { + self.text + } + fn colours(&self) -> &str { + self.colours + } } impl<'a> ColouredStringCommon for &ColouredStringSlice<'a> { - fn text(&self) -> &str { self.text } - fn colours(&self) -> &str { self.colours } + fn text(&self) -> &str { + self.text + } + fn colours(&self) -> &str { + self.colours + } } impl<'a> ColouredStringSlice<'a> { pub fn general(text: &'a str, colours: &'a str) -> Self { - assert_eq!(text.chars().count(), colours.chars().count(), - "Mismatched lengths in ColouredStringSlice::general"); + assert_eq!( + text.chars().count(), + colours.chars().count(), + "Mismatched lengths in ColouredStringSlice::general" + ); ColouredStringSlice { text, colours } } @@ -221,9 +252,9 @@ impl<'a> Iterator for ColouredStringCharIterator<'a> { self.textpos += textend; self.colourpos += colourend; Some(ColouredStringSlice { - text: &textslice[..textend], - colours: &colourslice[..colourend], - }) + text: &textslice[..textend], + colours: &colourslice[..colourend], + }) } else { None } @@ -307,9 +338,9 @@ impl<'a> Iterator for ColouredStringSplitIterator<'a> { self.textpos += textend; self.colourpos += colourend; Some(ColouredStringSlice { - text: &textslice[..textend], - colours: &colourslice[..colourend], - }) + text: &textslice[..textend], + colours: &colourslice[..colourend], + }) } _ => panic!("length mismatch in CSSI"), } @@ -318,23 +349,31 @@ impl<'a> Iterator for ColouredStringSplitIterator<'a> { #[test] fn test_constructors() { - assert_eq!(ColouredString::plain("hello"), - ColouredString::general("hello", " ")); - assert_eq!(ColouredString::uniform("hello", 'a'), - ColouredString::general("hello", "aaaaa")); + assert_eq!( + ColouredString::plain("hello"), + ColouredString::general("hello", " ") + ); + assert_eq!( + ColouredString::uniform("hello", 'a'), + ColouredString::general("hello", "aaaaa") + ); } #[test] fn test_repeat() { - assert_eq!(ColouredString::general("xyz", "pqr").repeat(3), - ColouredString::general("xyzxyzxyz", "pqrpqrpqr")); + assert_eq!( + ColouredString::general("xyz", "pqr").repeat(3), + ColouredString::general("xyzxyzxyz", "pqrpqrpqr") + ); } #[test] fn test_concat() { - assert_eq!(ColouredString::general("xyz", "pqr") + - ColouredString::general("abcde", "ijklm"), - ColouredString::general("xyzabcde", "pqrijklm")); + assert_eq!( + ColouredString::general("xyz", "pqr") + + ColouredString::general("abcde", "ijklm"), + ColouredString::general("xyzabcde", "pqrijklm") + ); } #[test] @@ -370,20 +409,38 @@ fn test_frags() { fn test_split() { let cs = ColouredString::general("abcdefgh", "mnopqrst"); let mut lines = cs.split(3); - assert_eq!(lines.next(), Some(ColouredStringSlice::general("abc", "mno"))); - assert_eq!(lines.next(), Some(ColouredStringSlice::general("def", "pqr"))); + assert_eq!( + lines.next(), + Some(ColouredStringSlice::general("abc", "mno")) + ); + assert_eq!( + lines.next(), + Some(ColouredStringSlice::general("def", "pqr")) + ); assert_eq!(lines.next(), Some(ColouredStringSlice::general("gh", "st"))); assert_eq!(lines.next(), None); let mut lines = cs.split(4); - assert_eq!(lines.next(), Some(ColouredStringSlice::general("abcd", "mnop"))); - assert_eq!(lines.next(), Some(ColouredStringSlice::general("efgh", "qrst"))); + assert_eq!( + lines.next(), + Some(ColouredStringSlice::general("abcd", "mnop")) + ); + assert_eq!( + lines.next(), + Some(ColouredStringSlice::general("efgh", "qrst")) + ); assert_eq!(lines.next(), None); let cs = ColouredStringSlice::general("ab\u{4567}defgh", "mnopqrst"); let mut lines = cs.split(3); assert_eq!(lines.next(), Some(ColouredStringSlice::general("ab", "mn"))); - assert_eq!(lines.next(), Some(ColouredStringSlice::general("\u{4567}d", "op"))); - assert_eq!(lines.next(), Some(ColouredStringSlice::general("efg", "qrs"))); + assert_eq!( + lines.next(), + Some(ColouredStringSlice::general("\u{4567}d", "op")) + ); + assert_eq!( + lines.next(), + Some(ColouredStringSlice::general("efg", "qrs")) + ); assert_eq!(lines.next(), Some(ColouredStringSlice::general("h", "t"))); assert_eq!(lines.next(), None); diff --git a/src/config.rs b/src/config.rs index d60ed4f..3ea64a0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -17,12 +17,13 @@ pub enum ConfigError { impl super::TopLevelErrorCandidate for ConfigError {} impl std::fmt::Display for ConfigError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> - Result<(), std::fmt::Error> - { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> Result<(), std::fmt::Error> { match self { #[cfg(unix)] - ConfigError::XDG(e) => { e.fmt(f) } + ConfigError::XDG(e) => e.fmt(f), #[cfg(windows)] ConfigError::Env(e) => { @@ -75,24 +76,22 @@ impl ConfigLocation { let mut dir = PathBuf::from_str(&appdata)?; dir.push("mastodonochrome"); - Ok(ConfigLocation { - dir, - }) + Ok(ConfigLocation { dir }) } pub fn from_pathbuf(dir: PathBuf) -> Self { - ConfigLocation { - dir, - } + ConfigLocation { dir } } pub fn get_path(&self, leaf: &str) -> PathBuf { self.dir.join(leaf) } - pub fn create_file(&self, leaf: &str, contents: &str) -> - Result<(), std::io::Error> - { + pub fn create_file( + &self, + leaf: &str, + contents: &str, + ) -> Result<(), std::io::Error> { std::fs::create_dir_all(&self.dir)?; // NamedTempFile creates the file with restricted permissions, diff --git a/src/editor.rs b/src/editor.rs index a8335b3..ef4b4e3 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -1,18 +1,17 @@ -use std::cmp::{min, max}; +use std::cmp::{max, min}; use unicode_width::UnicodeWidthChar; use super::activity_stack::{NonUtilityActivity, UtilityActivity}; use super::client::{Client, ClientError}; use super::coloured_string::*; use super::file::SearchDirection; +use super::posting::{Post, PostMetadata}; +use super::scan_re::Scan; use super::text::*; use super::tui::{ - ActivityState, CursorPosition, LogicalAction, - OurKey, OurKey::*, + ActivityState, CursorPosition, LogicalAction, OurKey, OurKey::*, }; use super::types::InstanceStatusConfig; -use super::scan_re::Scan; -use super::posting::{Post, PostMetadata}; struct EditorCore { text: String, @@ -31,8 +30,9 @@ impl EditorCore { None => true, // end of string // not just before a combining character - Some(c) => c == '\n' || - UnicodeWidthChar::width(c).unwrap_or(0) > 0, + Some(c) => { + c == '\n' || UnicodeWidthChar::width(c).unwrap_or(0) > 0 + } } } } @@ -98,31 +98,37 @@ impl EditorCore { fn forward(&mut self) -> bool { match self.next_position(self.point) { - Some(pos) => { self.point = pos; true } - None => false + Some(pos) => { + self.point = pos; + true + } + None => false, } } fn backward(&mut self) -> bool { match self.prev_position(self.point) { - Some(pos) => { self.point = pos; true } - None => false + Some(pos) => { + self.point = pos; + true + } + None => false, } } fn delete_backward(&mut self) { let prev_point = self.point; if self.backward() { - self.text = self.text[..self.point].to_owned() + - &self.text[prev_point..]; + self.text = + self.text[..self.point].to_owned() + &self.text[prev_point..]; } } fn delete_forward(&mut self) { let prev_point = self.point; if self.forward() { - self.text = self.text[..prev_point].to_owned() + - &self.text[self.point..]; + self.text = + self.text[..prev_point].to_owned() + &self.text[self.point..]; self.point = prev_point; } } @@ -153,8 +159,9 @@ impl EditorCore { } fn insert_after(&mut self, text: &str) { - self.text = self.text[..self.point].to_owned() + text + - &self.text[self.point..]; + self.text = self.text[..self.point].to_owned() + + text + + &self.text[self.point..]; } fn insert(&mut self, text: &str) { @@ -163,8 +170,9 @@ impl EditorCore { } fn paste(&mut self) { - self.text = self.text[..self.point].to_owned() + &self.paste_buffer + - &self.text[self.point..]; + self.text = self.text[..self.point].to_owned() + + &self.paste_buffer + + &self.text[self.point..]; self.point += self.paste_buffer.len(); } @@ -208,15 +216,33 @@ impl EditorCore { fn handle_keypress(&mut self, key: OurKey) { match key { - Left | Ctrl('B') => { self.backward(); }, - Right | Ctrl('F') => { self.forward(); }, - Backspace => { self.delete_backward(); }, - Del | Ctrl('D') => { self.delete_forward(); }, - Ctrl('W') => { self.backward_word(); }, - Ctrl('T') => { self.forward_word(); }, - Ctrl('Y') => { self.paste(); }, - Pr(c) => { self.insert(&c.to_string()); }, - Space => { self.insert(" "); }, + Left | Ctrl('B') => { + self.backward(); + } + Right | Ctrl('F') => { + self.forward(); + } + Backspace => { + self.delete_backward(); + } + Del | Ctrl('D') => { + self.delete_forward(); + } + Ctrl('W') => { + self.backward_word(); + } + Ctrl('T') => { + self.forward_word(); + } + Ctrl('Y') => { + self.paste(); + } + Pr(c) => { + self.insert(&c.to_string()); + } + Space => { + self.insert(" "); + } _ => (), } } @@ -271,27 +297,27 @@ fn test_forward_backward_word() { }; assert!(ec.forward_word()); - assert_eq!(ec.point, 6); // ipsum + assert_eq!(ec.point, 6); // ipsum assert!(ec.forward_word()); - assert_eq!(ec.point, 12); // dolor + assert_eq!(ec.point, 12); // dolor assert!(ec.forward_word()); - assert_eq!(ec.point, 18); // sit + assert_eq!(ec.point, 18); // sit assert!(ec.forward_word()); - assert_eq!(ec.point, 22); // amet + assert_eq!(ec.point, 22); // amet assert!(ec.forward_word()); - assert_eq!(ec.point, 26); // end of string + assert_eq!(ec.point, 26); // end of string assert!(!ec.forward_word()); assert!(ec.backward_word()); - assert_eq!(ec.point, 22); // amet + assert_eq!(ec.point, 22); // amet assert!(ec.backward_word()); - assert_eq!(ec.point, 18); // sit + assert_eq!(ec.point, 18); // sit assert!(ec.backward_word()); - assert_eq!(ec.point, 12); // dolor + assert_eq!(ec.point, 12); // dolor assert!(ec.backward_word()); - assert_eq!(ec.point, 6); // ipsum + assert_eq!(ec.point, 6); // ipsum assert!(ec.backward_word()); - assert_eq!(ec.point, 0); // lorem + assert_eq!(ec.point, 0); // lorem assert!(!ec.backward_word()); } @@ -397,8 +423,8 @@ impl SingleLineEditor { if self.first_visible > self.core.point { self.first_visible = self.core.point; } else { - let mut avail_width = self.width.saturating_sub( - self.promptwidth + 1); + let mut avail_width = + self.width.saturating_sub(self.promptwidth + 1); let mut counted_initial_trunc_marker = false; if self.first_visible > 0 { counted_initial_trunc_marker = true; @@ -449,17 +475,29 @@ impl SingleLineEditor { pub fn handle_keypress(&mut self, key: OurKey) -> bool { match key { - Ctrl('A') => { self.core.beginning_of_buffer(); }, - Ctrl('E') => { self.core.end_of_buffer(); }, - Ctrl('K') => { self.cut_to_end(); }, - Return => { return true; }, - _ => { self.core.handle_keypress(key); } + Ctrl('A') => { + self.core.beginning_of_buffer(); + } + Ctrl('E') => { + self.core.end_of_buffer(); + } + Ctrl('K') => { + self.cut_to_end(); + } + Return => { + return true; + } + _ => { + self.core.handle_keypress(key); + } } self.update_first_visible(); false } - pub fn borrow_text(&self) -> &str { &self.core.text } + pub fn borrow_text(&self) -> &str { + &self.core.text + } pub fn draw(&self, width: usize) -> (ColouredString, Option) { let mut s = self.prompt.clone(); @@ -476,15 +514,16 @@ impl SingleLineEditor { match self.core.char_width_and_bytes(pos) { None => break, Some((w, b)) => { - if width_so_far + w > width || - (width_so_far + w == width && - pos + b < self.core.text.len()) + if width_so_far + w > width + || (width_so_far + w == width + && pos + b < self.core.text.len()) { s.push_str(ColouredString::uniform(">", '>')); break; } else { s.push_str(ColouredString::plain( - &self.core.text[pos..pos+b])); + &self.core.text[pos..pos + b], + )); pos += b; } } @@ -496,7 +535,7 @@ impl SingleLineEditor { pub fn resize(&mut self, width: usize) { self.width = width; self.update_first_visible(); - } + } } #[test] @@ -543,16 +582,14 @@ fn test_single_line_visibility() { promptwidth: 0, }; - assert_eq!(sle.draw(sle.width), - (ColouredString::plain(""), Some(0))); + assert_eq!(sle.draw(sle.width), (ColouredString::plain(""), Some(0))); // Typing 'a' doesn't move first_visible away from the buffer start sle.core.insert("a"); assert_eq!(sle.core.point, 1); sle.update_first_visible(); assert_eq!(sle.first_visible, 0); - assert_eq!(sle.draw(sle.width), - (ColouredString::plain("a"), Some(1))); + assert_eq!(sle.draw(sle.width), (ColouredString::plain("a"), Some(1))); // Typing three more characters leaves the cursor in the last of // the 5 positions, so we're still good: we can print "abcd" @@ -561,8 +598,10 @@ fn test_single_line_visibility() { assert_eq!(sle.core.point, 4); sle.update_first_visible(); assert_eq!(sle.first_visible, 0); - assert_eq!(sle.draw(sle.width), - (ColouredString::plain("abcd"), Some(4))); + assert_eq!( + sle.draw(sle.width), + (ColouredString::plain("abcd"), Some(4)) + ); // One more character and we overflow. Now we must print " "), Some(4))); + assert_eq!( + sle.draw(sle.width), + (ColouredString::general(" "), Some(4)) + ); // And another two characters move that on in turn: " "), Some(4))); + assert_eq!( + sle.draw(sle.width), + (ColouredString::general(" "), Some(4)) + ); // Now start moving backwards. Three backwards movements leave the // cursor on the e, but nothing has changed. @@ -589,16 +632,20 @@ fn test_single_line_visibility() { assert_eq!(sle.core.point, 4); sle.update_first_visible(); assert_eq!(sle.first_visible, 4); - assert_eq!(sle.draw(sle.width), - (ColouredString::general(" "), Some(1))); + assert_eq!( + sle.draw(sle.width), + (ColouredString::general(" "), Some(1)) + ); // Move backwards one more, so that we must scroll to get the d in view. sle.core.backward(); assert_eq!(sle.core.point, 3); sle.update_first_visible(); assert_eq!(sle.first_visible, 3); - assert_eq!(sle.draw(sle.width), - (ColouredString::general(" "), Some(1))); + assert_eq!( + sle.draw(sle.width), + (ColouredString::general(" "), Some(1)) + ); // And on the _next_ backwards scroll, the end of the string also // becomes hidden. @@ -606,8 +653,10 @@ fn test_single_line_visibility() { assert_eq!(sle.core.point, 2); sle.update_first_visible(); assert_eq!(sle.first_visible, 2); - assert_eq!(sle.draw(sle.width), - (ColouredString::general("", "> >"), Some(1))); + assert_eq!( + sle.draw(sle.width), + (ColouredString::general("", "> >"), Some(1)) + ); // The one after that would naively leave us at "" with the // cursor on the b. But we can do better! In this case, the < @@ -617,8 +666,10 @@ fn test_single_line_visibility() { assert_eq!(sle.core.point, 1); sle.update_first_visible(); assert_eq!(sle.first_visible, 0); - assert_eq!(sle.draw(sle.width), - (ColouredString::general("abcd>", " >"), Some(1))); + assert_eq!( + sle.draw(sle.width), + (ColouredString::general("abcd>", " >"), Some(1)) + ); } struct BottomLineEditorOverlay { @@ -627,9 +678,10 @@ struct BottomLineEditorOverlay { } impl BottomLineEditorOverlay { - fn new(prompt: ColouredString, - result: Box LogicalAction>) -> Self - { + fn new( + prompt: ColouredString, + result: Box LogicalAction>, + ) -> Self { BottomLineEditorOverlay { ed: SingleLineEditor::new_with_prompt("".to_owned(), prompt), result, @@ -642,22 +694,26 @@ impl ActivityState for BottomLineEditorOverlay { self.ed.resize(w); } - fn draw(&self, w: usize, _h: usize) -> - (Vec, CursorPosition) - { + fn draw( + &self, + w: usize, + _h: usize, + ) -> (Vec, CursorPosition) { let (text, cursorpos) = self.ed.draw(w); let cursorpos = match cursorpos { Some(x) => CursorPosition::At(x, 0), None => CursorPosition::None, - }; + }; - (vec! { text }, cursorpos) + (vec![text], cursorpos) } - fn handle_keypress(&mut self, key: OurKey, client: &mut Client) -> - LogicalAction - { + fn handle_keypress( + &mut self, + key: OurKey, + client: &mut Client, + ) -> LogicalAction { if self.ed.handle_keypress(key) { (self.result)(&self.ed.core.text, client) } else { @@ -673,9 +729,15 @@ pub trait EditableMenuLineData { } impl EditableMenuLineData for String { - fn display(&self) -> ColouredString { ColouredString::plain(self) } - fn to_text(&self) -> String { self.clone() } - fn from_text(text: &str) -> Self { text.to_owned() } + fn display(&self) -> ColouredString { + ColouredString::plain(self) + } + fn to_text(&self) -> String { + self.clone() + } + fn from_text(text: &str) -> Self { + text.to_owned() + } } impl EditableMenuLineData for Option { @@ -712,8 +774,7 @@ pub struct EditableMenuLine { } impl EditableMenuLine { - pub fn new(key: OurKey, description: ColouredString, data: Data) -> Self - { + pub fn new(key: OurKey, description: ColouredString, data: Data) -> Self { let menuline = Self::make_menuline(key, &description, &data); let prompt = Self::make_prompt(key, &description); @@ -728,22 +789,28 @@ impl EditableMenuLine { } } - fn make_menuline(key: OurKey, description: &ColouredString, data: &Data) - -> MenuKeypressLine - { + fn make_menuline( + key: OurKey, + description: &ColouredString, + data: &Data, + ) -> MenuKeypressLine { let desc = description + data.display(); MenuKeypressLine::new(key, desc) } - fn make_prompt(key: OurKey, description: &ColouredString) - -> MenuKeypressLine - { + fn make_prompt( + key: OurKey, + description: &ColouredString, + ) -> MenuKeypressLine { MenuKeypressLine::new(key, description.to_owned()) } - pub fn render(&self, width: usize, cursorpos: &mut CursorPosition, - cy: usize) -> ColouredString - { + pub fn render( + &self, + width: usize, + cursorpos: &mut CursorPosition, + cy: usize, + ) -> ColouredString { if let Some(ref editor) = self.editor { let (text, cx) = editor.draw(width); if let Some(cx) = cx { @@ -751,7 +818,8 @@ impl EditableMenuLine { } text } else { - self.menuline.render_oneline(width, None, &DefaultDisplayStyle) + self.menuline + .render_oneline(width, None, &DefaultDisplayStyle) } } @@ -771,8 +839,8 @@ impl EditableMenuLine { pub fn refresh_editor_prompt(&mut self) { if let Some(ref mut editor) = self.editor { if let Some(w) = self.last_width { - let prompt = self.prompt - .render_oneline(w, None, &DefaultDisplayStyle); + let prompt = + self.prompt.render_oneline(w, None, &DefaultDisplayStyle); editor.resize(w); editor.set_prompt(prompt); } @@ -784,7 +852,10 @@ impl EditableMenuLine { if editor.handle_keypress(key) { self.data = Data::from_text(editor.borrow_text()); self.menuline = Self::make_menuline( - self.key, &self.description, &self.data); + self.key, + &self.description, + &self.data, + ); (true, true) } else { (true, false) @@ -800,12 +871,17 @@ impl EditableMenuLine { consumed } - pub fn get_data(&self) -> &Data { &self.data } - pub fn is_editing(&self) -> bool { self.editor.is_some() } + pub fn get_data(&self) -> &Data { + &self.data + } + pub fn is_editing(&self) -> bool { + self.editor.is_some() + } } impl MenuKeypressLineGeneral -for EditableMenuLine { + for EditableMenuLine +{ fn check_widths(&self, lmaxwid: &mut usize, rmaxwid: &mut usize) { self.menuline.check_widths(lmaxwid, rmaxwid); self.prompt.check_widths(lmaxwid, rmaxwid); @@ -831,7 +907,8 @@ pub fn get_user_to_examine() -> Box { } else { match client.account_by_name(s) { Ok(account) => LogicalAction::Goto( - UtilityActivity::ExamineUser(account.id).into()), + UtilityActivity::ExamineUser(account.id).into(), + ), // FIXME: it would be nice to discriminate errors // better here, and maybe return anything worse @@ -839,7 +916,7 @@ pub fn get_user_to_examine() -> Box { Err(_) => LogicalAction::PopOverlayBeep, } } - }) + }), )) } @@ -853,7 +930,8 @@ pub fn get_post_id_to_read() -> Box { } else { match client.status_by_id(s) { Ok(st) => LogicalAction::Goto( - UtilityActivity::InfoStatus(st.id).into()), + UtilityActivity::InfoStatus(st.id).into(), + ), // FIXME: it would be nice to discriminate errors // better here, and maybe return anything worse @@ -861,7 +939,7 @@ pub fn get_post_id_to_read() -> Box { Err(_) => LogicalAction::PopOverlayBeep, } } - }) + }), )) } @@ -875,10 +953,10 @@ pub fn get_hashtag_to_read() -> Box { LogicalAction::PopOverlaySilent } else { LogicalAction::Goto( - NonUtilityActivity::HashtagTimeline(s.to_owned()) - .into()) + NonUtilityActivity::HashtagTimeline(s.to_owned()).into(), + ) } - }) + }), )) } @@ -891,7 +969,7 @@ pub fn get_search_expression(dir: SearchDirection) -> Box { ColouredString::plain(title), Box::new(move |s, _client| { LogicalAction::GotSearchExpression(dir, s.to_owned()) - }) + }), )) } @@ -934,9 +1012,12 @@ struct Composer { } impl Composer { - fn new(conf: InstanceStatusConfig, header: FileHeader, - irt: Option, post: Post) -> Self - { + fn new( + conf: InstanceStatusConfig, + header: FileHeader, + irt: Option, + post: Post, + ) -> Self { let point = post.text.len(); Composer { core: EditorCore { @@ -963,8 +1044,12 @@ impl Composer { #[cfg(test)] fn test_new(conf: InstanceStatusConfig, text: &str) -> Self { - Self::new(conf, FileHeader::new(ColouredString::plain("dummy")), - None, Post::with_text(text)) + Self::new( + conf, + FileHeader::new(ColouredString::plain("dummy")), + None, + Post::with_text(text), + ) } fn is_line_boundary(c: char) -> bool { @@ -993,7 +1078,7 @@ impl Composer { // Special case: if the function below generates a // zero-length region, don't bother to add it at all. if end == start { - return + return; } // Check if there's a previous region with the same colour @@ -1020,10 +1105,10 @@ impl Composer { // Determine the total cost of the current region. let cost = match colour { 'u' => self.conf.characters_reserved_per_url, - '@' => match self.core.text[start+1..end].find('@') { + '@' => match self.core.text[start + 1..end].find('@') { Some(pos) => pos + 1, // just the part before the @domain - None => end - start, // otherwise the whole thing counts - } + None => end - start, // otherwise the whole thing counts + }, _ => region_chars, }; @@ -1044,8 +1129,9 @@ impl Composer { for _ in 0..nok_chars { char_iter.next(); } - let mut pos = char_iter.next() - .map_or_else(|| slice.len(), |(i,_)| i); + let mut pos = char_iter + .next() + .map_or_else(|| slice.len(), |(i, _)| i); // If we've landed on a Unicode char boundary but // not on an _editor_ char boundary (i.e. between @@ -1070,10 +1156,14 @@ impl Composer { // Try all three regex matchers and see which one picks up // something starting soonest (out of those that pick up // anything at all). - let next_match = scanners.iter().filter_map(|(colour, scanner)| { - scanner.get_span(&core.text, pos) - .map(|(start, end)| (start, end, colour)) - }).min(); + let next_match = scanners + .iter() + .filter_map(|(colour, scanner)| { + scanner + .get_span(&core.text, pos) + .map(|(start, end)| (start, end, colour)) + }) + .min(); match next_match { Some((start, end, colour)) => { @@ -1110,13 +1200,14 @@ impl Composer { let mut hard_wrap_pos = None; loop { - cells.push(ComposeLayoutCell{pos, x, y}); + cells.push(ComposeLayoutCell { pos, x, y }); match self.core.char_width_and_bytes(pos) { None => break, // we're done Some((w, b)) => { let mut chars_iter = self.core.text[pos..].chars(); - let c = chars_iter.next() - .expect("we just found out we're not at end of string"); + let c = chars_iter.next().expect( + "we just found out we're not at end of string", + ); if Self::is_line_boundary(c) { // End of paragraph. y += 1; @@ -1136,7 +1227,8 @@ impl Composer { let (wrap_pos, wrap_cell) = match soft_wrap_pos { Some(p) => p, None => hard_wrap_pos.expect( - "We can't break the line _anywhere_?!"), + "We can't break the line _anywhere_?!", + ), }; // Now rewind to the place we just broke @@ -1159,15 +1251,13 @@ impl Composer { fn get_coloured_line(&self, y: usize) -> Option { // Use self.layout to find the bounds of this line within the // buffer text. - let start_cell_index = self.layout - .partition_point(|cell| cell.y < y); + let start_cell_index = self.layout.partition_point(|cell| cell.y < y); if start_cell_index == self.layout.len() { - return None; // y is after the end of the buffer + return None; // y is after the end of the buffer } let start_pos = self.layout[start_cell_index].pos; - let end_cell_index = self.layout - .partition_point(|cell| cell.y <= y); + let end_cell_index = self.layout.partition_point(|cell| cell.y <= y); let end_pos = if end_cell_index == self.layout.len() { self.core.text.len() } else { @@ -1190,26 +1280,30 @@ impl Composer { // Now look up in self.regions to decide what colour to make // everything. - let start_region_index = self.regions + let start_region_index = self + .regions .partition_point(|region| region.end <= start_pos); let mut cs = ColouredString::plain(""); for region in self.regions[start_region_index..].iter() { if end_pos <= region.start { - break; // finished this line + break; // finished this line } let start = max(start_pos, region.start); let end = min(end_pos, region.end); cs.push_str(ColouredString::uniform( - &self.core.text[start..end], region.colour)); + &self.core.text[start..end], + region.colour, + )); } Some(cs) } fn determine_cursor_pos(&self) -> Option<(usize, usize)> { - let cursor_cell_index = self.layout + let cursor_cell_index = self + .layout .partition_point(|cell| cell.pos < self.core.point); if let Some(cell) = self.layout.get(cursor_cell_index) { if cell.pos == self.core.point { @@ -1242,9 +1336,11 @@ impl Composer { // that is >= (x,y), because that _is_ the one to its right. // Instead we must search for the last one that is <= (x,y). - let cell_index_after = self.layout + let cell_index_after = self + .layout .partition_point(|cell| (cell.y, cell.x) <= (y, x)); - let cell_index = cell_index_after.checked_sub(1) + let cell_index = cell_index_after + .checked_sub(1) .expect("Cell 0 should be at (0,0) and always count as <= (y,x)"); if let Some(cell) = self.layout.get(cell_index) { self.core.point = cell.pos; @@ -1260,7 +1356,7 @@ impl Composer { fn next_line(&mut self) { let (x, y) = self.cursor_pos.expect("post_update should have run"); let x = self.goal_column.unwrap_or(x); - if !self.goto_xy(x, y+1) { + if !self.goto_xy(x, y + 1) { self.goal_column = None; } } @@ -1345,9 +1441,10 @@ impl Composer { self.core.insert("\n"); let detect_magic_sequence = |seq: &str| { - self.core.text[..self.core.point].ends_with(seq) && - (self.core.point == seq.len() || - self.core.text[..self.core.point - seq.len()].ends_with('\n')) + self.core.text[..self.core.point].ends_with(seq) + && (self.core.point == seq.len() + || self.core.text[..self.core.point - seq.len()] + .ends_with('\n')) }; if detect_magic_sequence(".\n") { @@ -1384,149 +1481,200 @@ fn test_regions() { // Scan the sample text and ensure we're spotting the hashtag, // mention and URL. let composer = Composer::test_new(standard_conf.clone(), main_sample_text); - assert_eq!(composer.make_regions(&composer.core), vec! { - ComposeBufferRegion { start: 0, end: 7, colour: ' ' }, - ComposeBufferRegion { start: 7, end: 15, colour: '#' }, - ComposeBufferRegion { start: 15, end: 22, colour: ' ' }, - ComposeBufferRegion { start: 22, end: 44, colour: '@' }, - ComposeBufferRegion { start: 44, end: 51, colour: ' ' }, - ComposeBufferRegion { start: 51, end: 90, colour: 'u' }, - ComposeBufferRegion { start: 90, end: 91, colour: ' ' }, - }); + assert_eq!( + composer.make_regions(&composer.core), + vec! { + ComposeBufferRegion { start: 0, end: 7, colour: ' ' }, + ComposeBufferRegion { start: 7, end: 15, colour: '#' }, + ComposeBufferRegion { start: 15, end: 22, colour: ' ' }, + ComposeBufferRegion { start: 22, end: 44, colour: '@' }, + ComposeBufferRegion { start: 44, end: 51, colour: ' ' }, + ComposeBufferRegion { start: 51, end: 90, colour: 'u' }, + ComposeBufferRegion { start: 90, end: 91, colour: ' ' }, + } + ); // When a hashtag and a mention directly abut, it should // disqualify the hashtag. - let composer = Composer::test_new( - standard_conf.clone(), "#hashtag@mention"); - assert_eq!(composer.make_regions(&composer.core), vec! { - ComposeBufferRegion { start: 0, end: 8, colour: '#' }, - ComposeBufferRegion { start: 8, end: 16, colour: ' ' }, - }); + let composer = + Composer::test_new(standard_conf.clone(), "#hashtag@mention"); + assert_eq!( + composer.make_regions(&composer.core), + vec! { + ComposeBufferRegion { start: 0, end: 8, colour: '#' }, + ComposeBufferRegion { start: 8, end: 16, colour: ' ' }, + } + ); // But a space between them is enough to make them work. - let composer = Composer::test_new( - standard_conf.clone(), "#hashtag @mention"); - assert_eq!(composer.make_regions(&composer.core), vec! { - ComposeBufferRegion { start: 0, end: 8, colour: '#' }, - ComposeBufferRegion { start: 8, end: 9, colour: ' ' }, - ComposeBufferRegion { start: 9, end: 17, colour: '@' }, - }); + let composer = + Composer::test_new(standard_conf.clone(), "#hashtag @mention"); + assert_eq!( + composer.make_regions(&composer.core), + vec! { + ComposeBufferRegion { start: 0, end: 8, colour: '#' }, + ComposeBufferRegion { start: 8, end: 9, colour: ' ' }, + ComposeBufferRegion { start: 9, end: 17, colour: '@' }, + } + ); // The total cost of main_sample_text is 61 (counting the mention // and the URL for less than their full lengths). So setting // max=60 highlights the final character as overflow. - let composer = Composer::test_new(InstanceStatusConfig { - max_characters: 60, - max_media_attachments: 4, - characters_reserved_per_url: 23, - }, main_sample_text); - assert_eq!(composer.make_regions(&composer.core), vec! { - ComposeBufferRegion { start: 0, end: 7, colour: ' ' }, - ComposeBufferRegion { start: 7, end: 15, colour: '#' }, - ComposeBufferRegion { start: 15, end: 22, colour: ' ' }, - ComposeBufferRegion { start: 22, end: 44, colour: '@' }, - ComposeBufferRegion { start: 44, end: 51, colour: ' ' }, - ComposeBufferRegion { start: 51, end: 90, colour: 'u' }, - ComposeBufferRegion { start: 90, end: 91, colour: '!' }, - }); + let composer = Composer::test_new( + InstanceStatusConfig { + max_characters: 60, + max_media_attachments: 4, + characters_reserved_per_url: 23, + }, + main_sample_text, + ); + assert_eq!( + composer.make_regions(&composer.core), + vec! { + ComposeBufferRegion { start: 0, end: 7, colour: ' ' }, + ComposeBufferRegion { start: 7, end: 15, colour: '#' }, + ComposeBufferRegion { start: 15, end: 22, colour: ' ' }, + ComposeBufferRegion { start: 22, end: 44, colour: '@' }, + ComposeBufferRegion { start: 44, end: 51, colour: ' ' }, + ComposeBufferRegion { start: 51, end: 90, colour: 'u' }, + ComposeBufferRegion { start: 90, end: 91, colour: '!' }, + } + ); // Dropping the limit by another 1 highlights the last character // of the URL. // them.) - let composer = Composer::test_new(InstanceStatusConfig { - max_characters: 59, - max_media_attachments: 4, - characters_reserved_per_url: 23, - }, main_sample_text); - assert_eq!(composer.make_regions(&composer.core), vec! { - ComposeBufferRegion { start: 0, end: 7, colour: ' ' }, - ComposeBufferRegion { start: 7, end: 15, colour: '#' }, - ComposeBufferRegion { start: 15, end: 22, colour: ' ' }, - ComposeBufferRegion { start: 22, end: 44, colour: '@' }, - ComposeBufferRegion { start: 44, end: 51, colour: ' ' }, - ComposeBufferRegion { start: 51, end: 90-1, colour: 'u' }, - ComposeBufferRegion { start: 90-1, end: 91, colour: '!' }, - }); + let composer = Composer::test_new( + InstanceStatusConfig { + max_characters: 59, + max_media_attachments: 4, + characters_reserved_per_url: 23, + }, + main_sample_text, + ); + assert_eq!( + composer.make_regions(&composer.core), + vec! { + ComposeBufferRegion { start: 0, end: 7, colour: ' ' }, + ComposeBufferRegion { start: 7, end: 15, colour: '#' }, + ComposeBufferRegion { start: 15, end: 22, colour: ' ' }, + ComposeBufferRegion { start: 22, end: 44, colour: '@' }, + ComposeBufferRegion { start: 44, end: 51, colour: ' ' }, + ComposeBufferRegion { start: 51, end: 90-1, colour: 'u' }, + ComposeBufferRegion { start: 90-1, end: 91, colour: '!' }, + } + ); // and dropping it by another 21 highlights the last 22 characters // of the URL ... - let composer = Composer::test_new(InstanceStatusConfig { - max_characters: 38, - max_media_attachments: 4, - characters_reserved_per_url: 23, - }, main_sample_text); - assert_eq!(composer.make_regions(&composer.core), vec! { - ComposeBufferRegion { start: 0, end: 7, colour: ' ' }, - ComposeBufferRegion { start: 7, end: 15, colour: '#' }, - ComposeBufferRegion { start: 15, end: 22, colour: ' ' }, - ComposeBufferRegion { start: 22, end: 44, colour: '@' }, - ComposeBufferRegion { start: 44, end: 51, colour: ' ' }, - ComposeBufferRegion { start: 51, end: 90-22, colour: 'u' }, - ComposeBufferRegion { start: 90-22, end: 91, colour: '!' }, - }); + let composer = Composer::test_new( + InstanceStatusConfig { + max_characters: 38, + max_media_attachments: 4, + characters_reserved_per_url: 23, + }, + main_sample_text, + ); + assert_eq!( + composer.make_regions(&composer.core), + vec! { + ComposeBufferRegion { start: 0, end: 7, colour: ' ' }, + ComposeBufferRegion { start: 7, end: 15, colour: '#' }, + ComposeBufferRegion { start: 15, end: 22, colour: ' ' }, + ComposeBufferRegion { start: 22, end: 44, colour: '@' }, + ComposeBufferRegion { start: 44, end: 51, colour: ' ' }, + ComposeBufferRegion { start: 51, end: 90-22, colour: 'u' }, + ComposeBufferRegion { start: 90-22, end: 91, colour: '!' }, + } + ); // but dropping it by _another_ one means that the entire URL // (since it costs 23 chars no matter what its length) is beyond // the limit, so now it all gets highlighted. - let composer = Composer::test_new(InstanceStatusConfig { - max_characters: 37, - max_media_attachments: 4, - characters_reserved_per_url: 23, - }, main_sample_text); - assert_eq!(composer.make_regions(&composer.core), vec! { - ComposeBufferRegion { start: 0, end: 7, colour: ' ' }, - ComposeBufferRegion { start: 7, end: 15, colour: '#' }, - ComposeBufferRegion { start: 15, end: 22, colour: ' ' }, - ComposeBufferRegion { start: 22, end: 44, colour: '@' }, - ComposeBufferRegion { start: 44, end: 51, colour: ' ' }, - ComposeBufferRegion { start: 51, end: 91, colour: '!' }, - }); + let composer = Composer::test_new( + InstanceStatusConfig { + max_characters: 37, + max_media_attachments: 4, + characters_reserved_per_url: 23, + }, + main_sample_text, + ); + assert_eq!( + composer.make_regions(&composer.core), + vec! { + ComposeBufferRegion { start: 0, end: 7, colour: ' ' }, + ComposeBufferRegion { start: 7, end: 15, colour: '#' }, + ComposeBufferRegion { start: 15, end: 22, colour: ' ' }, + ComposeBufferRegion { start: 22, end: 44, colour: '@' }, + ComposeBufferRegion { start: 44, end: 51, colour: ' ' }, + ComposeBufferRegion { start: 51, end: 91, colour: '!' }, + } + ); // And just for good measure, drop the limit by one _more_, and show the ordinary character just before the URL being highlighted as well. - let composer = Composer::test_new(InstanceStatusConfig { - max_characters: 36, - max_media_attachments: 4, - characters_reserved_per_url: 23, - }, main_sample_text); - assert_eq!(composer.make_regions(&composer.core), vec! { - ComposeBufferRegion { start: 0, end: 7, colour: ' ' }, - ComposeBufferRegion { start: 7, end: 15, colour: '#' }, - ComposeBufferRegion { start: 15, end: 22, colour: ' ' }, - ComposeBufferRegion { start: 22, end: 44, colour: '@' }, - ComposeBufferRegion { start: 44, end: 51-1, colour: ' ' }, - ComposeBufferRegion { start: 51-1, end: 91, colour: '!' }, - }); + let composer = Composer::test_new( + InstanceStatusConfig { + max_characters: 36, + max_media_attachments: 4, + characters_reserved_per_url: 23, + }, + main_sample_text, + ); + assert_eq!( + composer.make_regions(&composer.core), + vec! { + ComposeBufferRegion { start: 0, end: 7, colour: ' ' }, + ComposeBufferRegion { start: 7, end: 15, colour: '#' }, + ComposeBufferRegion { start: 15, end: 22, colour: ' ' }, + ComposeBufferRegion { start: 22, end: 44, colour: '@' }, + ComposeBufferRegion { start: 44, end: 51-1, colour: ' ' }, + ComposeBufferRegion { start: 51-1, end: 91, colour: '!' }, + } + ); // Test handling of non-single-byte Unicode characters. Note here // that ² and ³ take two bytes each in UTF-8 (they live in the ISO // 8859-1 top half), but all the other superscript digits need // three bytes (U+2070 onwards). let unicode_sample_text = "⁰ⁱ²³⁴⁵⁶⁷⁸⁹⁰ⁱ²³⁴⁵⁶⁷⁸⁹⁰ⁱ²³⁴⁵⁶⁷⁸⁹"; - let composer = Composer::test_new(InstanceStatusConfig { - max_characters: 23, - max_media_attachments: 4, - characters_reserved_per_url: 23, - }, unicode_sample_text); - assert_eq!(composer.make_regions(&composer.core), vec! { - // 28 bytes for a full ⁰ⁱ²³⁴⁵⁶⁷⁸⁹, 3+3+2=8 bytes for ⁰ⁱ² - ComposeBufferRegion { start: 0, end: 2*28+8, colour: ' ' }, - ComposeBufferRegion { start: 2*28+8, end: 3*28, colour: '!' }, - }); + let composer = Composer::test_new( + InstanceStatusConfig { + max_characters: 23, + max_media_attachments: 4, + characters_reserved_per_url: 23, + }, + unicode_sample_text, + ); + assert_eq!( + composer.make_regions(&composer.core), + vec! { + // 28 bytes for a full ⁰ⁱ²³⁴⁵⁶⁷⁸⁹, 3+3+2=8 bytes for ⁰ⁱ² + ComposeBufferRegion { start: 0, end: 2*28+8, colour: ' ' }, + ComposeBufferRegion { start: 2*28+8, end: 3*28, colour: '!' }, + } + ); // An even more awkward case, where there's a combining mark at // the join. let unicode_sample_text = "Besźel"; - let composer = Composer::test_new(InstanceStatusConfig { - max_characters: 4, - max_media_attachments: 4, - characters_reserved_per_url: 23, - }, unicode_sample_text); - assert_eq!(composer.make_regions(&composer.core), vec! { - // We expect only the "Bes" to be marked as ok, even though - // the 'z' part of "ź" is within bounds in principle - ComposeBufferRegion { start: 0, end: 3, colour: ' ' }, - ComposeBufferRegion { start: 3, end: 8, colour: '!' }, - }); + let composer = Composer::test_new( + InstanceStatusConfig { + max_characters: 4, + max_media_attachments: 4, + characters_reserved_per_url: 23, + }, + unicode_sample_text, + ); + assert_eq!( + composer.make_regions(&composer.core), + vec! { + // We expect only the "Bes" to be marked as ok, even though + // the 'z' part of "ź" is within bounds in principle + ComposeBufferRegion { start: 0, end: 3, colour: ' ' }, + ComposeBufferRegion { start: 3, end: 8, colour: '!' }, + } + ); } #[test] @@ -1539,26 +1687,41 @@ fn test_layout() { // The empty string, because it would be embarrassing if that didn't work let composer = Composer::test_new(conf.clone(), ""); - assert_eq!(composer.layout(10), vec!{ - ComposeLayoutCell { pos: 0, x: 0, y: 0 }}); + assert_eq!( + composer.layout(10), + vec! { + ComposeLayoutCell { pos: 0, x: 0, y: 0 }} + ); // One line, just to check that we get a position assigned to // every character boundary let composer = Composer::test_new(conf.clone(), "abc"); - assert_eq!(composer.layout(10), - (0..=3).map(|i| ComposeLayoutCell { pos: i, x: i, y: 0 }) - .collect::>()); - assert_eq!(composer.layout(4), - (0..=3).map(|i| ComposeLayoutCell { pos: i, x: i, y: 0 }) - .collect::>()); + assert_eq!( + composer.layout(10), + (0..=3) + .map(|i| ComposeLayoutCell { pos: i, x: i, y: 0 }) + .collect::>() + ); + assert_eq!( + composer.layout(4), + (0..=3) + .map(|i| ComposeLayoutCell { pos: i, x: i, y: 0 }) + .collect::>() + ); // Two lines, which wrap so that 'g' is first on the new line let composer = Composer::test_new(conf.clone(), "abc def ghi jkl"); assert_eq!( composer.layout(10), - (0..=7).map(|i| ComposeLayoutCell { pos: i, x: i, y: 0 }).chain( - (0..=7).map(|i| ComposeLayoutCell { pos: i+8, x: i, y: 1 })) - .collect::>()); + (0..=7) + .map(|i| ComposeLayoutCell { pos: i, x: i, y: 0 }) + .chain((0..=7).map(|i| ComposeLayoutCell { + pos: i + 8, + x: i, + y: 1 + })) + .collect::>() + ); // An overlong line, which has to wrap via the fallback // hard_wrap_pos system, so we get the full 10 characters (as @@ -1566,22 +1729,34 @@ fn test_layout() { let composer = Composer::test_new(conf.clone(), "abcxdefxghixjkl"); assert_eq!( composer.layout(11), - (0..=9).map(|i| ComposeLayoutCell { pos: i, x: i, y: 0 }).chain( - (0..=5).map(|i| ComposeLayoutCell { pos: i+10, x: i, y: 1 })) - .collect::>()); + (0..=9) + .map(|i| ComposeLayoutCell { pos: i, x: i, y: 0 }) + .chain((0..=5).map(|i| ComposeLayoutCell { + pos: i + 10, + x: i, + y: 1 + })) + .collect::>() + ); // The most trivial case with a newline in: _just_ the newline let composer = Composer::test_new(conf.clone(), "\n"); - assert_eq!(composer.layout(10), vec!{ + assert_eq!( + composer.layout(10), + vec! { ComposeLayoutCell { pos: 0, x: 0, y: 0 }, - ComposeLayoutCell { pos: 1, x: 0, y: 1 }}); + ComposeLayoutCell { pos: 1, x: 0, y: 1 }} + ); // And now two newlines let composer = Composer::test_new(conf.clone(), "\n\n"); - assert_eq!(composer.layout(10), vec!{ + assert_eq!( + composer.layout(10), + vec! { ComposeLayoutCell { pos: 0, x: 0, y: 0 }, ComposeLayoutCell { pos: 1, x: 0, y: 1 }, - ComposeLayoutCell { pos: 2, x: 0, y: 2 }}); + ComposeLayoutCell { pos: 2, x: 0, y: 2 }} + ); // Watch what happens just as we type text across a wrap boundary. // At 8 characters, this should be fine as it is, since the wrap @@ -1591,16 +1766,24 @@ fn test_layout() { let composer = Composer::test_new(conf.clone(), "abc def "); assert_eq!( composer.layout(9), - (0..=8).map(|i| ComposeLayoutCell { pos: i, x: i, y: 0 }) - .collect::>()); + (0..=8) + .map(|i| ComposeLayoutCell { pos: i, x: i, y: 0 }) + .collect::>() + ); // Now we type the next character, and it should wrap on to the // next line. let composer = Composer::test_new(conf.clone(), "abc def g"); assert_eq!( composer.layout(9), - (0..=7).map(|i| ComposeLayoutCell { pos: i, x: i, y: 0 }).chain( - (0..=1).map(|i| ComposeLayoutCell { pos: i+8, x: i, y: 1 })) - .collect::>()); + (0..=7) + .map(|i| ComposeLayoutCell { pos: i, x: i, y: 0 }) + .chain((0..=1).map(|i| ComposeLayoutCell { + pos: i + 8, + x: i, + y: 1 + })) + .collect::>() + ); } impl ActivityState for Composer { @@ -1608,18 +1791,22 @@ impl ActivityState for Composer { if self.last_size != Some((w, h)) { self.last_size = Some((w, h)); self.page_len = h.saturating_sub( - self.header.render(w).len() + - match self.irt { + self.header.render(w).len() + + match self.irt { None => 0, Some(ref irt) => irt.render(w).len(), - } + self.headersep.render(w).len() + } + + self.headersep.render(w).len(), ); self.post_update(); } } - fn draw(&self, w: usize, h: usize) -> (Vec, CursorPosition) - { + fn draw( + &self, + w: usize, + h: usize, + ) -> (Vec, CursorPosition) { let mut lines = Vec::new(); lines.extend_from_slice(&self.header.render(w)); if let Some(irt) = &self.irt { @@ -1633,7 +1820,7 @@ impl ActivityState for Composer { let y = self.ytop + (lines.len() - ystart); match self.get_coloured_line(y) { Some(line) => lines.push(line), - None => break, // ran out of lines in the buffer + None => break, // ran out of lines in the buffer } } @@ -1650,17 +1837,21 @@ impl ActivityState for Composer { (lines, cursor_pos) } - fn handle_keypress(&mut self, key: OurKey, _client: &mut Client) -> - LogicalAction - { + fn handle_keypress( + &mut self, + key: OurKey, + _client: &mut Client, + ) -> LogicalAction { use ComposerKeyState::*; // Start by identifying whether a keystroke is an up/down one, // so that we can consistently clear the goal column for all // keystrokes that are not match (self.keystate, key) { - (Start, Ctrl('N')) | (Start, Down) | - (Start, Ctrl('P')) | (Start, Up) => { + (Start, Ctrl('N')) + | (Start, Down) + | (Start, Ctrl('P')) + | (Start, Up) => { if self.goal_column.is_none() { self.goal_column = self.cursor_pos.map(|(x, _y)| x); } @@ -1688,7 +1879,7 @@ impl ActivityState for Composer { Some(true) => return self.submit_post(), Some(false) => return LogicalAction::Pop, None => (), - } + }, // ^O is a prefix key that is followed by various less // common keystrokes @@ -1719,16 +1910,25 @@ impl ActivityState for Composer { } } -pub fn compose_post(client: &mut Client, post: Post) -> - Result, ClientError> -{ +pub fn compose_post( + client: &mut Client, + post: Post, +) -> Result, ClientError> { let inst = client.instance()?; let title = match post.m.in_reply_to_id { None => "Compose a post".to_owned(), Some(ref id) => format!("Reply to post {id}"), }; let header = FileHeader::new(ColouredString::uniform(&title, 'H')); - let irt = post.m.in_reply_to_id.as_ref() + let irt = post + .m + .in_reply_to_id + .as_ref() .map(|id| InReplyToLine::from_id(id, client)); - Ok(Box::new(Composer::new(inst.configuration.statuses, header, irt, post))) + Ok(Box::new(Composer::new( + inst.configuration.statuses, + header, + irt, + post, + ))) } diff --git a/src/file.rs b/src/file.rs index 72932ed..548048a 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,26 +1,27 @@ use itertools::Itertools; use regex::Regex; use std::cell::RefCell; -use std::cmp::{min, max}; -use std::collections::{HashMap, HashSet, hash_map}; +use std::cmp::{max, min}; +use std::collections::{hash_map, HashMap, HashSet}; use std::rc::Rc; use super::activity_stack::{ - NonUtilityActivity, UtilityActivity, OverlayActivity, + NonUtilityActivity, OverlayActivity, UtilityActivity, +}; +use super::client::{ + Boosts, Client, ClientError, FeedExtend, FeedId, Replies, }; -use super::client::{Client, ClientError, FeedId, FeedExtend, Boosts, Replies}; use super::coloured_string::*; use super::text::*; use super::tui::{ - ActivityState, CursorPosition, LogicalAction, - OurKey, OurKey::*, + ActivityState, CursorPosition, LogicalAction, OurKey, OurKey::*, SavedFilePos, }; #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct FilePosition { - item: isize, // The selected item in the file - line: usize, // The line number within that item + item: isize, // The selected item in the file + line: usize, // The line number within that item // 'line' only makes sense for a particular render width, because // when items are rewrapped to a different width, their line @@ -36,10 +37,18 @@ pub struct FilePosition { impl FilePosition { fn item_top(item: isize) -> Self { - FilePosition { item, line: 0, width: None } + FilePosition { + item, + line: 0, + width: None, + } } fn item_bottom(item: isize) -> Self { - FilePosition { item, line: 1, width: None } + FilePosition { + item, + line: 1, + width: None, + } } fn clip(self, first_index: isize, last_index: isize) -> Self { @@ -75,7 +84,9 @@ struct FeedSource { } impl FeedSource { - fn new(id: FeedId) -> Self { FeedSource { id } } + fn new(id: FeedId) -> Self { + FeedSource { id } + } } impl FileDataSource for FeedSource { @@ -98,7 +109,9 @@ impl FileDataSource for FeedSource { feeds_updated.contains(&self.id) } - fn extendable(&self) -> bool { true } + fn extendable(&self) -> bool { + true + } } struct StaticSource { @@ -106,22 +119,34 @@ struct StaticSource { } impl StaticSource { - fn singleton(id: String) -> Self { StaticSource { ids: vec! { id } } } - fn vector(ids: Vec) -> Self { StaticSource { ids } } + fn singleton(id: String) -> Self { + StaticSource { ids: vec![id] } + } + fn vector(ids: Vec) -> Self { + StaticSource { ids } + } } impl FileDataSource for StaticSource { fn get(&self, _client: &mut Client) -> (Vec, isize) { (self.ids.clone(), 0) } - fn init(&self, _client: &mut Client) -> Result<(), ClientError> { Ok(()) } + fn init(&self, _client: &mut Client) -> Result<(), ClientError> { + Ok(()) + } fn try_extend(&self, _client: &mut Client) -> Result { Ok(false) } - fn updated(&self, _feeds_updated: &HashSet) -> bool { false } - fn extendable(&self) -> bool { false } + fn updated(&self, _feeds_updated: &HashSet) -> bool { + false + } + fn extendable(&self) -> bool { + false + } fn single_id(&self) -> String { - self.ids.iter().exactly_one() + self.ids + .iter() + .exactly_one() .expect("Should only call this on singleton StaticSources") .to_owned() } @@ -129,7 +154,9 @@ impl FileDataSource for StaticSource { #[derive(Debug, PartialEq, Eq, Clone, Copy)] enum CanList { - Nothing, ForPost, ForUser, + Nothing, + ForPost, + ForUser, } trait FileType { @@ -138,79 +165,102 @@ trait FileType { const CAN_GET_POSTS: bool = false; const IS_EXAMINE_USER: bool = false; - fn get_from_client(id: &str, client: &mut Client) -> - Result; + fn get_from_client( + id: &str, + client: &mut Client, + ) -> Result; - fn feed_id(&self) -> Option<&FeedId> { None } + fn feed_id(&self) -> Option<&FeedId> { + None + } } struct StatusFeedType { id: Option, } impl StatusFeedType { - fn with_feed(id: FeedId) -> Self { Self { id: Some(id) } } - fn without_feed() -> Self { Self { id: None } } + fn with_feed(id: FeedId) -> Self { + Self { id: Some(id) } + } + fn without_feed() -> Self { + Self { id: None } + } } impl FileType for StatusFeedType { type Item = StatusDisplay; - fn get_from_client(id: &str, client: &mut Client) -> - Result - { + fn get_from_client( + id: &str, + client: &mut Client, + ) -> Result { let st = client.status_by_id(id)?; Ok(StatusDisplay::new(st, client)) } - fn feed_id(&self) -> Option<&FeedId> { self.id.as_ref() } + fn feed_id(&self) -> Option<&FeedId> { + self.id.as_ref() + } } struct NotificationStatusFeedType { id: FeedId, } impl NotificationStatusFeedType { - fn with_feed(id: FeedId) -> Self { Self { id } } + fn with_feed(id: FeedId) -> Self { + Self { id } + } } impl FileType for NotificationStatusFeedType { type Item = StatusDisplay; - fn get_from_client(id: &str, client: &mut Client) -> - Result - { + fn get_from_client( + id: &str, + client: &mut Client, + ) -> Result { let not = client.notification_by_id(id)?; let st = ¬.status.expect( - "expected all notifications in this feed would have statuses"); + "expected all notifications in this feed would have statuses", + ); Ok(StatusDisplay::new(st.clone(), client)) } - fn feed_id(&self) -> Option<&FeedId> { Some(&self.id) } + fn feed_id(&self) -> Option<&FeedId> { + Some(&self.id) + } } struct EgoNotificationFeedType { id: FeedId, } impl EgoNotificationFeedType { - fn with_feed(id: FeedId) -> Self { Self { id } } + fn with_feed(id: FeedId) -> Self { + Self { id } + } } impl FileType for EgoNotificationFeedType { type Item = NotificationLog; - fn get_from_client(id: &str, client: &mut Client) -> - Result - { + fn get_from_client( + id: &str, + client: &mut Client, + ) -> Result { let not = client.notification_by_id(id)?; Ok(NotificationLog::from_notification(¬, client)) } - fn feed_id(&self) -> Option<&FeedId> { Some(&self.id) } + fn feed_id(&self) -> Option<&FeedId> { + Some(&self.id) + } } struct UserListFeedType {} impl FileType for UserListFeedType { type Item = UserListEntry; - fn get_from_client(id: &str, client: &mut Client) -> - Result - { + fn get_from_client( + id: &str, + client: &mut Client, + ) -> Result { let ac = client.account_by_id(id)?; Ok(UserListEntry::from_account(&ac, client)) } @@ -224,7 +274,7 @@ struct FileContents { items: Vec<(String, Type::Item)>, } -impl FileContents { +impl FileContents { fn update_items(&mut self, client: &mut Client) { // FIXME: if the feed has been extended rather than created, // we should be able to make less effort than this. But we @@ -248,13 +298,20 @@ impl FileContents { self.origin - 1 - extcount } fn extender_index(&self) -> Option { - if self.extender.is_some() { Some(self.origin - 1) } else { None } + if self.extender.is_some() { + Some(self.origin - 1) + } else { + None + } } fn index_limit(&self) -> isize { - self.origin.checked_add_unsigned(self.items.len()) + self.origin + .checked_add_unsigned(self.items.len()) .expect("Out-of-range index") } - fn last_index(&self) -> isize { self.index_limit() -1 } + fn last_index(&self) -> isize { + self.index_limit() - 1 + } fn phys_index(&self, index: isize) -> usize { assert!(index >= self.origin, "Index before start"); @@ -276,7 +333,8 @@ impl FileContents { } fn id_at_index(&self, index: isize) -> Option<&str> { - index.checked_sub(self.origin) + index + .checked_sub(self.origin) .and_then(|i: isize| i.try_into().ok()) .and_then(|u: usize| self.items.get(u)) .map(|item: &(String, Type::Item)| &item.0 as &str) @@ -288,7 +346,9 @@ impl FileContents { // say, a list of users that took an action, the ids' natural // order would be user creation date, but here they'd be // ordered by when each user did the thing.) - self.items.iter().position(|item| item.0 == id) + self.items + .iter() + .position(|item| item.0 == id) .map(|u| (u as isize) + self.origin) } } @@ -314,7 +374,10 @@ enum UIMode { } #[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum SearchDirection { Up, Down } +pub enum SearchDirection { + Up, + Down, +} struct FileDisplayStyles { selected_poll_id: Option, @@ -334,17 +397,19 @@ impl FileDisplayStyles { impl DisplayStyleGetter for FileDisplayStyles { fn poll_options(&self, id: &str) -> Option> { - self.selected_poll_id.as_ref().and_then( - |s| if s == id { + self.selected_poll_id.as_ref().and_then(|s| { + if s == id { Some(self.selected_poll_options.clone()) } else { None } - ) + }) } fn unfolded(&self, id: &str) -> bool { - self.unfolded.as_ref().is_some_and(|set| set.borrow().contains(id)) + self.unfolded + .as_ref() + .is_some_and(|set| set.borrow().contains(id)) } } @@ -365,11 +430,15 @@ struct File { } impl File { - fn new(client: &mut Client, source: Source, desc: ColouredString, - file_desc: Type, saved_pos: Option<&SavedFilePos>, - unfolded: Option>>>, show_new: bool) -> - Result - { + fn new( + client: &mut Client, + source: Source, + desc: ColouredString, + file_desc: Type, + saved_pos: Option<&SavedFilePos>, + unfolded: Option>>>, + show_new: bool, + ) -> Result { source.init(client)?; let extender = if source.extendable() { @@ -396,8 +465,10 @@ impl File { // with it let mut latest_read_index = None; if let Some(saved_pos) = saved_pos { - latest_read_index = saved_pos.latest_read_id.as_ref().and_then( - |id| contents.index_of_id(id)); + latest_read_index = saved_pos + .latest_read_id + .as_ref() + .and_then(|id| contents.index_of_id(id)); if let Some(latest_read_index) = latest_read_index { initial_pos = if show_new { @@ -418,8 +489,8 @@ impl File { } // Now clip initial_pos at the top and bottom of the data we have - initial_pos = initial_pos.clip( - contents.first_index(), contents.last_index()); + initial_pos = + initial_pos.clip(contents.first_index(), contents.last_index()); let ff = File { contents, @@ -439,27 +510,33 @@ impl File { Ok(ff) } - fn ensure_item_rendered(&mut self, index: isize, w: usize) -> - &Vec - { + fn ensure_item_rendered( + &mut self, + index: isize, + w: usize, + ) -> &Vec { if let hash_map::Entry::Vacant(e) = self.rendered.entry(index) { let mut lines = Vec::new(); let highlight = match self.ui_mode { UIMode::Select(htype, _purpose) => match self.selection { None => None, - Some((item, sub)) => if item == index { - Some(Highlight(htype, sub)) - } else { - None + Some((item, sub)) => { + if item == index { + Some(Highlight(htype, sub)) + } else { + None + } } - } + }, _ => None, }; - for line in self.contents.get(index) - .render_highlighted(w, highlight, &self.display_styles) - { + for line in self.contents.get(index).render_highlighted( + w, + highlight, + &self.display_styles, + ) { for frag in line.split(w) { lines.push(frag.into()); } @@ -468,12 +545,15 @@ impl File { e.insert(lines); } - self.rendered.get(&index).expect("We just made sure this was present") + self.rendered + .get(&index) + .expect("We just made sure this was present") } fn ensure_enough_rendered(&mut self) -> Option { - let (w, h) = self.last_size.expect( - "ensure_enough_rendered before resize"); + let (w, h) = self + .last_size + .expect("ensure_enough_rendered before resize"); self.update_pos_for_size(w, h); @@ -511,8 +591,9 @@ impl File { } fn after_setting_pos(&mut self) { - let (w, _h) = self.last_size.expect( - "ensure_enough_rendered before setting pos"); + let (w, _h) = self + .last_size + .expect("ensure_enough_rendered before setting pos"); let at_top = self.at_top(); if let Some(ref mut ext) = &mut self.contents.extender { ext.set_primed(at_top); @@ -528,13 +609,18 @@ impl File { None => self.pos.line > 0, Some(pw) => match self.last_size { None => false, // if in doubt don't mark things as read - Some((sw, _sh)) => if pw == sw { - self.rendered.get(&self.pos.item) - .map_or(false, |lines| self.pos.line == lines.len()) - } else { - false // similarly, if in doubt + Some((sw, _sh)) => { + if pw == sw { + self.rendered + .get(&self.pos.item) + .map_or(false, |lines| { + self.pos.line == lines.len() + }) + } else { + false // similarly, if in doubt + } } - } + }, }; let latest_read_index = if pos_item_shown_in_full { @@ -543,8 +629,8 @@ impl File { self.pos.item - 1 }; - self.latest_read_index = max( - self.latest_read_index, Some(latest_read_index)); + self.latest_read_index = + max(self.latest_read_index, Some(latest_read_index)); } fn update_pos_for_size(&mut self, w: usize, h: usize) { @@ -560,8 +646,9 @@ impl File { } fn clip_pos_within_item(&mut self) { - let (w, _h) = self.last_size.expect( - "clip_pos_within_item before setting pos"); + let (w, _h) = self + .last_size + .expect("clip_pos_within_item before setting pos"); // If something has just changed the sizes of rendered items, // we need to make sure self.pos doesn't specify a line // position outside the current item. @@ -583,7 +670,9 @@ impl File { } } - fn at_top(&mut self) -> bool { self.ensure_enough_rendered().is_some() } + fn at_top(&mut self) -> bool { + self.ensure_enough_rendered().is_some() + } fn move_up(&mut self, distance: usize) { let (w, h) = self.last_size.expect("move_up before resize"); @@ -599,8 +688,8 @@ impl File { break; } self.pos.item -= 1; - self.pos.line = self.ensure_item_rendered(self.pos.item, w) - .len(); + self.pos.line = + self.ensure_item_rendered(self.pos.item, w).len(); } } self.fix_overshoot_at_top(); @@ -661,7 +750,8 @@ impl File { self.contents.extender = None; if self.pos.item < self.contents.first_index() { self.pos = FilePosition::item_top( - self.contents.first_index()); + self.contents.first_index(), + ); } } @@ -677,22 +767,26 @@ impl File { action } - fn last_selectable_above(&self, htype: HighlightType, index: isize) -> - Option<(isize, usize)> - { + fn last_selectable_above( + &self, + htype: HighlightType, + index: isize, + ) -> Option<(isize, usize)> { for i in (self.contents.origin..=index).rev() { let n = self.contents.get(i).count_highlightables(htype); if n > 0 { - return Some((i, n-1)); + return Some((i, n - 1)); } } None } - fn first_selectable_below(&self, htype: HighlightType, index: isize) -> - Option<(isize, usize)> - { + fn first_selectable_below( + &self, + htype: HighlightType, + index: isize, + ) -> Option<(isize, usize)> { for i in index..self.contents.index_limit() { let n = self.contents.get(i).count_highlightables(htype); if n > 0 { @@ -709,14 +803,16 @@ impl File { } } - fn start_selection(&mut self, htype: HighlightType, - purpose: SelectionPurpose, client: &mut Client) - -> LogicalAction - { + fn start_selection( + &mut self, + htype: HighlightType, + purpose: SelectionPurpose, + client: &mut Client, + ) -> LogicalAction { let item = self.pos.item; - let selection = - self.last_selectable_above(htype, item).or_else( - || self.first_selectable_below(htype, item + 1)); + let selection = self + .last_selectable_above(htype, item) + .or_else(|| self.first_selectable_below(htype, item + 1)); if selection.is_some() { self.ui_mode = UIMode::Select(htype, purpose); @@ -727,11 +823,15 @@ impl File { } } - fn change_selection_to(&mut self, new_selection: Option<(isize, usize)>, - none_ok: bool, client: &mut Client) -> LogicalAction - { - if self.selection_restrict_to_item.is_some_and(|restricted| - new_selection.is_some_and(|(item, _)| item != restricted)) { + fn change_selection_to( + &mut self, + new_selection: Option<(isize, usize)>, + none_ok: bool, + client: &mut Client, + ) -> LogicalAction { + if self.selection_restrict_to_item.is_some_and(|restricted| { + new_selection.is_some_and(|(item, _)| item != restricted) + }) { return LogicalAction::Beep; } @@ -748,34 +848,31 @@ impl File { }; self.display_styles.selected_poll_id = match self.ui_mode { - UIMode::Select(HighlightType::PollOption, _) => - self.selected_id(self.selection), + UIMode::Select(HighlightType::PollOption, _) => { + self.selected_id(self.selection) + } _ => None, }; self.select_aux = match self.ui_mode { UIMode::Select(_, SelectionPurpose::Favourite) => { match self.selected_id(self.selection) { - Some(id) => { - match client.status_by_id(&id) { - Ok(st) => Some(st.favourited == Some(true)), - Err(_) => Some(false), - } + Some(id) => match client.status_by_id(&id) { + Ok(st) => Some(st.favourited == Some(true)), + Err(_) => Some(false), }, None => Some(false), } - }, + } UIMode::Select(_, SelectionPurpose::Boost) => { match self.selected_id(self.selection) { - Some(id) => { - match client.status_by_id(&id) { - Ok(st) => Some(st.reblogged == Some(true)), - Err(_) => Some(false), - } + Some(id) => match client.status_by_id(&id) { + Ok(st) => Some(st.reblogged == Some(true)), + Err(_) => Some(false), }, None => Some(false), } - }, + } _ => None, }; @@ -790,10 +887,12 @@ impl File { let new_selection = match self.selection { None => None, - Some((item, sub)) => if sub > 0 { - Some((item, sub - 1)) - } else { - self.last_selectable_above(htype, item - 1) + Some((item, sub)) => { + if sub > 0 { + Some((item, sub - 1)) + } else { + self.last_selectable_above(htype, item - 1) + } } }; @@ -809,7 +908,8 @@ impl File { let new_selection = match self.selection { None => None, Some((item, sub)) => { - let count = self.contents.get(item).count_highlightables(htype); + let count = + self.contents.get(item).count_highlightables(htype); if sub + 1 < count { Some((item, sub + 1)) } else { @@ -822,8 +922,9 @@ impl File { } fn vote(&mut self) -> LogicalAction { - let (item, sub) = self.selection.expect( - "we should only call this if we have a selection"); + let (item, sub) = self + .selection + .expect("we should only call this if we have a selection"); self.selection_restrict_to_item = Some(item); if self.contents.get(item).is_multiple_choice_poll() { if self.display_styles.selected_poll_options.contains(&sub) { @@ -856,24 +957,29 @@ impl File { LogicalAction::Nothing } - fn selected_id(&self, selection: Option<(isize, usize)>) - -> Option - { + fn selected_id( + &self, + selection: Option<(isize, usize)>, + ) -> Option { let htype = match self.ui_mode { UIMode::Select(htype, _purpose) => htype, _ => return None, }; match selection { - Some((item, sub)) => self.contents.get(item) + Some((item, sub)) => self + .contents + .get(item) .highlighted_id(Some(Highlight(htype, sub))), None => None, } } - fn complete_selection(&mut self, client: &mut Client, alt: bool) - -> LogicalAction - { + fn complete_selection( + &mut self, + client: &mut Client, + alt: bool, + ) -> LogicalAction { let (_htype, purpose) = match self.ui_mode { UIMode::Select(htype, purpose) => (htype, purpose), _ => return LogicalAction::Beep, @@ -881,7 +987,7 @@ impl File { match purpose { SelectionPurpose::Favourite | SelectionPurpose::Boost => { - // alt means unfave; select_aux = Some(already faved?) + // alt means unfave; select_aux = Some(already faved?) if self.select_aux == Some(!alt) { return LogicalAction::Nothing; } @@ -892,9 +998,11 @@ impl File { let result = if let Some(id) = self.selected_id(self.selection) { match purpose { SelectionPurpose::ExamineUser => LogicalAction::Goto( - UtilityActivity::ExamineUser(id).into()), - SelectionPurpose::StatusInfo => LogicalAction::Goto( - UtilityActivity::InfoStatus(id).into()), + UtilityActivity::ExamineUser(id).into(), + ), + SelectionPurpose::StatusInfo => { + LogicalAction::Goto(UtilityActivity::InfoStatus(id).into()) + } SelectionPurpose::Favourite => { match client.favourite_post(&id, !alt) { Ok(_) => { @@ -914,17 +1022,16 @@ impl File { } } SelectionPurpose::Unfold => { - let did_something = if let Some(ref rc) = - self.display_styles.unfolded - { - let mut unfolded = rc.borrow_mut(); - if !unfolded.remove(&id) { - unfolded.insert(id); - } - true - } else { - false - }; + let did_something = + if let Some(ref rc) = self.display_styles.unfolded { + let mut unfolded = rc.borrow_mut(); + if !unfolded.remove(&id) { + unfolded.insert(id); + } + true + } else { + false + }; if did_something { self.rendered.clear(); @@ -933,12 +1040,19 @@ impl File { LogicalAction::Nothing } SelectionPurpose::Reply => LogicalAction::Goto( - UtilityActivity::ComposeReply(id).into()), + UtilityActivity::ComposeReply(id).into(), + ), SelectionPurpose::Thread => LogicalAction::Goto( - UtilityActivity::ThreadFile(id, alt).into()), + UtilityActivity::ThreadFile(id, alt).into(), + ), SelectionPurpose::Vote => { match client.vote_in_poll( - &id, self.display_styles.selected_poll_options.iter().copied()) { + &id, + self.display_styles + .selected_poll_options + .iter() + .copied(), + ) { Ok(_) => { self.contents.update_items(client); LogicalAction::Nothing @@ -973,16 +1087,21 @@ impl File { break LogicalAction::Beep; } - let rendered = self.rendered.get(&self.pos.item) + let rendered = self + .rendered + .get(&self.pos.item) .expect("we should have just rendered it"); // self.pos.line indicates the line number just off // the bottom of the screen, so it's never 0 unless // we're at the very top of the file if let Some(lineno) = self.pos.line.checked_sub(1) { if let Some(line) = rendered.get(lineno) { - if self.last_search.as_ref() + if self + .last_search + .as_ref() .expect("we just checked it above") - .find(line.text()).is_some() + .find(line.text()) + .is_some() { break LogicalAction::Nothing; } @@ -995,8 +1114,8 @@ impl File { } } -impl - ActivityState for File +impl ActivityState + for File { fn resize(&mut self, w: usize, h: usize) { if self.last_size != Some((w, h)) { @@ -1010,12 +1129,21 @@ impl self.after_setting_pos(); } - fn draw(&self, w: usize, h: usize) - -> (Vec, CursorPosition) { - assert_eq!(self.last_size, Some((w, h)), - "last resize() inconsistent with draw()"); - assert_eq!(self.pos.width, Some(w), - "file position inconsistent with draw()"); + fn draw( + &self, + w: usize, + h: usize, + ) -> (Vec, CursorPosition) { + assert_eq!( + self.last_size, + Some((w, h)), + "last resize() inconsistent with draw()" + ); + assert_eq!( + self.pos.width, + Some(w), + "file position inconsistent with draw()" + ); let (start_item, start_line) = (self.pos.item, self.pos.line); let mut item = start_item; @@ -1023,11 +1151,13 @@ impl let mut at_bottom = item == self.contents.last_index(); // Retrieve rendered lines from the bottom of the window upwards - 'outer: while item >= self.contents.first_index() && - item < self.contents.index_limit() && - lines.len() + 1 < h + 'outer: while item >= self.contents.first_index() + && item < self.contents.index_limit() + && lines.len() + 1 < h { - let rendered = self.rendered.get(&item) + let rendered = self + .rendered + .get(&item) .expect("unrendered item reached draw()"); let line_limit = if item == start_item { if start_line != rendered.len() { @@ -1060,13 +1190,12 @@ impl } else { fs.add(Space, "Down", 99) }; - let fs = if Type::Item::can_highlight( - HighlightType::WholeStatus) - { - fs.add(Pr('s'), "Reply", 42) - } else { - fs - }; + let fs = + if Type::Item::can_highlight(HighlightType::WholeStatus) { + fs.add(Pr('s'), "Reply", 42) + } else { + fs + }; let fs = if Type::Item::can_highlight(HighlightType::User) { fs.add(Pr('e'), "Examine", 40) } else { @@ -1092,28 +1221,25 @@ impl } else { fs }; - let fs = if Type::Item::can_highlight(HighlightType::Status) && - self.display_styles.unfolded.is_some() + let fs = if Type::Item::can_highlight(HighlightType::Status) + && self.display_styles.unfolded.is_some() { fs.add(Pr('u'), "Unfold", 39) } else { fs }; - let fs = if Type::Item::can_highlight( - HighlightType::WholeStatus) - { - fs.add(Pr('f'), "Fave", 41) - .add(Ctrl('B'), "Boost", 41) - } else { - fs - }; - let fs = if Type::Item::can_highlight( - HighlightType::PollOption) - { - fs.add(Ctrl('V'), "Vote", 10) - } else { - fs - }; + let fs = + if Type::Item::can_highlight(HighlightType::WholeStatus) { + fs.add(Pr('f'), "Fave", 41).add(Ctrl('B'), "Boost", 41) + } else { + fs + }; + let fs = + if Type::Item::can_highlight(HighlightType::PollOption) { + fs.add(Ctrl('V'), "Vote", 10) + } else { + fs + }; let fs = if Type::IS_EXAMINE_USER { fs.add(Pr('O'), "Options", 41) } else { @@ -1137,11 +1263,13 @@ impl // think that's a sensible tradeoff!) let base = self.contents.first_index(); let full_items = (start_item - base) as usize; - let total_items = (self.contents.index_limit() - base) as usize; + let total_items = + (self.contents.index_limit() - base) as usize; let mult = self.rendered.get(&start_item).unwrap().len(); fs.set_proportion( full_items * mult + start_line, - total_items * mult) + total_items * mult, + ) } UIMode::ListSubmenu => { let fs = match Type::CAN_LIST { @@ -1151,14 +1279,17 @@ impl CanList::ForPost => fs .add(Pr('F'), "List Favouriters", 99) .add(Pr('B'), "List Boosters", 99), - CanList::Nothing => - panic!("Then we shouldn't be in this submenu"), + CanList::Nothing => { + panic!("Then we shouldn't be in this submenu") + } }; fs.add(Pr('Q'), "Quit", 100) } UIMode::PostsSubmenu => { - assert!(Type::CAN_GET_POSTS, - "How did we get here if !CAN_GET_POSTS?"); + assert!( + Type::CAN_GET_POSTS, + "How did we get here if !CAN_GET_POSTS?" + ); fs.add(Pr('A'), "All", 99) .add(Pr('O'), "Original", 97) .add(Pr('T'), "Top-level", 98) @@ -1166,12 +1297,11 @@ impl } UIMode::Select(_htype, purpose) => { let fs = match purpose { - SelectionPurpose::ExamineUser => - fs.add(Space, "Examine", 98), - SelectionPurpose::StatusInfo => - fs.add(Space, "Info", 98), - SelectionPurpose::Reply => - fs.add(Space, "Reply", 98), + SelectionPurpose::ExamineUser => { + fs.add(Space, "Examine", 98) + } + SelectionPurpose::StatusInfo => fs.add(Space, "Info", 98), + SelectionPurpose::Reply => fs.add(Space, "Reply", 98), SelectionPurpose::Favourite => { if self.select_aux == Some(true) { fs.add(Pr('D'), "Unfave", 98) @@ -1186,42 +1316,58 @@ impl fs.add(Space, "Boost", 98) } } - SelectionPurpose::Thread => { - fs.add(Space, "Thread Context", 98) - .add(Pr('F'), "Full Thread", 97) - } + SelectionPurpose::Thread => fs + .add(Space, "Thread Context", 98) + .add(Pr('F'), "Full Thread", 97), SelectionPurpose::Vote => { // Different verb for selecting items // depending on whether the vote lets you // select more than one - let verb = if self.contents.get(item) + let verb = if self + .contents + .get(item) .is_multiple_choice_poll() - { "Toggle" } else { "Select" }; + { + "Toggle" + } else { + "Select" + }; // If you've selected nothing yet, prioritise // the keypress for selecting something. // Otherwise, prioritise the one for // submitting your answer. - if self.display_styles.selected_poll_options - .is_empty() + if self.display_styles.selected_poll_options.is_empty() { - fs.add(Space, verb, 98) - .add(Ctrl('V'), "Submit Vote", 97) + fs.add(Space, verb, 98).add( + Ctrl('V'), + "Submit Vote", + 97, + ) } else { - fs.add(Space, verb, 97) - .add(Ctrl('V'), "Submit Vote", 98) + fs.add(Space, verb, 97).add( + Ctrl('V'), + "Submit Vote", + 98, + ) } } SelectionPurpose::Unfold => { - let unfolded = self.contents.id_at_index(item) - .map_or(false, |id| self.display_styles.unfolded(id)); + let unfolded = self + .contents + .id_at_index(item) + .map_or(false, |id| { + self.display_styles.unfolded(id) + }); let verb = if unfolded { "Fold" } else { "Unfold" }; fs.add(Pr('U'), verb, 98) } }; - fs.add(Pr('+'), "Down", 99) - .add(Pr('-'), "Up", 99) - .add(Pr('Q'), "Quit", 100) + fs.add(Pr('+'), "Down", 99).add(Pr('-'), "Up", 99).add( + Pr('Q'), + "Quit", + 100, + ) } }; @@ -1232,9 +1378,11 @@ impl (lines, CursorPosition::End) } - fn handle_keypress(&mut self, key: OurKey, client: &mut Client) -> - LogicalAction - { + fn handle_keypress( + &mut self, + key: OurKey, + client: &mut Client, + ) -> LogicalAction { let (_w, h) = match self.last_size { Some(size) => size, None => panic!("handle_keypress before resize"), @@ -1293,9 +1441,11 @@ impl Pr('e') | Pr('E') => { if Type::Item::can_highlight(HighlightType::User) { - self.start_selection(HighlightType::User, - SelectionPurpose::ExamineUser, - client) + self.start_selection( + HighlightType::User, + SelectionPurpose::ExamineUser, + client, + ) } else { LogicalAction::Nothing } @@ -1303,9 +1453,11 @@ impl Pr('i') | Pr('I') => { if Type::Item::can_highlight(HighlightType::Status) { - self.start_selection(HighlightType::Status, - SelectionPurpose::StatusInfo, - client) + self.start_selection( + HighlightType::Status, + SelectionPurpose::StatusInfo, + client, + ) } else { LogicalAction::Nothing } @@ -1315,9 +1467,11 @@ impl if Type::Item::can_highlight(HighlightType::FoldableStatus) && self.display_styles.unfolded.is_some() { - self.start_selection(HighlightType::FoldableStatus, - SelectionPurpose::Unfold, - client) + self.start_selection( + HighlightType::FoldableStatus, + SelectionPurpose::Unfold, + client, + ) } else { LogicalAction::Nothing } @@ -1325,9 +1479,11 @@ impl Pr('f') | Pr('F') => { if Type::Item::can_highlight(HighlightType::WholeStatus) { - self.start_selection(HighlightType::WholeStatus, - SelectionPurpose::Favourite, - client) + self.start_selection( + HighlightType::WholeStatus, + SelectionPurpose::Favourite, + client, + ) } else { LogicalAction::Nothing } @@ -1335,9 +1491,11 @@ impl Ctrl('B') => { if Type::Item::can_highlight(HighlightType::WholeStatus) { - self.start_selection(HighlightType::WholeStatus, - SelectionPurpose::Boost, - client) + self.start_selection( + HighlightType::WholeStatus, + SelectionPurpose::Boost, + client, + ) } else { LogicalAction::Nothing } @@ -1345,9 +1503,11 @@ impl Pr('s') | Pr('S') => { if Type::Item::can_highlight(HighlightType::WholeStatus) { - self.start_selection(HighlightType::WholeStatus, - SelectionPurpose::Reply, - client) + self.start_selection( + HighlightType::WholeStatus, + SelectionPurpose::Reply, + client, + ) } else { LogicalAction::Nothing } @@ -1355,9 +1515,11 @@ impl Pr('t') | Pr('T') => { if Type::Item::can_highlight(HighlightType::Status) { - self.start_selection(HighlightType::Status, - SelectionPurpose::Thread, - client) + self.start_selection( + HighlightType::Status, + SelectionPurpose::Thread, + client, + ) } else { LogicalAction::Nothing } @@ -1365,9 +1527,11 @@ impl Ctrl('V') => { if Type::Item::can_highlight(HighlightType::PollOption) { - self.start_selection(HighlightType::PollOption, - SelectionPurpose::Vote, - client) + self.start_selection( + HighlightType::PollOption, + SelectionPurpose::Vote, + client, + ) } else { LogicalAction::Nothing } @@ -1384,16 +1548,16 @@ impl let search_direction = match key { Pr('/') => SearchDirection::Down, Pr('\\') => SearchDirection::Up, - _ => panic!("how are we in this arm anyway?") + _ => panic!("how are we in this arm anyway?"), }; self.search_direction = Some(search_direction); - LogicalAction::Goto(OverlayActivity::GetSearchExpression( - search_direction).into()) + LogicalAction::Goto( + OverlayActivity::GetSearchExpression(search_direction) + .into(), + ) } - Pr('n') | Pr('N') => { - self.search() - } + Pr('n') | Pr('N') => self.search(), Pr('p') | Pr('P') => { if Type::CAN_GET_POSTS { @@ -1404,42 +1568,70 @@ impl Pr('o') | Pr('O') => { if Type::IS_EXAMINE_USER { - LogicalAction::Goto(UtilityActivity::UserOptions( - self.contents.source.single_id()).into()) + LogicalAction::Goto( + UtilityActivity::UserOptions( + self.contents.source.single_id(), + ) + .into(), + ) } else { LogicalAction::Nothing } } _ => LogicalAction::Nothing, - } + }, UIMode::ListSubmenu => match key { - Pr('f') | Pr('F') => if Type::CAN_LIST == CanList::ForPost { - LogicalAction::Goto(UtilityActivity::ListStatusFavouriters( - self.contents.source.single_id()).into()) - } else { - LogicalAction::Nothing + Pr('f') | Pr('F') => { + if Type::CAN_LIST == CanList::ForPost { + LogicalAction::Goto( + UtilityActivity::ListStatusFavouriters( + self.contents.source.single_id(), + ) + .into(), + ) + } else { + LogicalAction::Nothing + } } - Pr('b') | Pr('B') => if Type::CAN_LIST == CanList::ForPost { - LogicalAction::Goto(UtilityActivity::ListStatusBoosters( - self.contents.source.single_id()).into()) - } else { - LogicalAction::Nothing + Pr('b') | Pr('B') => { + if Type::CAN_LIST == CanList::ForPost { + LogicalAction::Goto( + UtilityActivity::ListStatusBoosters( + self.contents.source.single_id(), + ) + .into(), + ) + } else { + LogicalAction::Nothing + } } - Pr('i') | Pr('I') => if Type::CAN_LIST == CanList::ForUser { - LogicalAction::Goto(UtilityActivity::ListUserFollowers( - self.contents.source.single_id()).into()) - } else { - LogicalAction::Nothing + Pr('i') | Pr('I') => { + if Type::CAN_LIST == CanList::ForUser { + LogicalAction::Goto( + UtilityActivity::ListUserFollowers( + self.contents.source.single_id(), + ) + .into(), + ) + } else { + LogicalAction::Nothing + } } - Pr('o') | Pr('O') => if Type::CAN_LIST == CanList::ForUser { - LogicalAction::Goto(UtilityActivity::ListUserFollowees( - self.contents.source.single_id()).into()) - } else { - LogicalAction::Nothing + Pr('o') | Pr('O') => { + if Type::CAN_LIST == CanList::ForUser { + LogicalAction::Goto( + UtilityActivity::ListUserFollowees( + self.contents.source.single_id(), + ) + .into(), + ) + } else { + LogicalAction::Nothing + } } Pr('q') | Pr('Q') => { @@ -1447,51 +1639,66 @@ impl LogicalAction::Nothing } _ => LogicalAction::Nothing, - } + }, UIMode::PostsSubmenu => match key { Pr('a') | Pr('A') => LogicalAction::Goto( NonUtilityActivity::UserPosts( self.contents.source.single_id(), - Boosts::Show, Replies::Show).into()), + Boosts::Show, + Replies::Show, + ) + .into(), + ), Pr('o') | Pr('O') => LogicalAction::Goto( NonUtilityActivity::UserPosts( self.contents.source.single_id(), - Boosts::Hide, Replies::Show).into()), + Boosts::Hide, + Replies::Show, + ) + .into(), + ), Pr('t') | Pr('T') => LogicalAction::Goto( NonUtilityActivity::UserPosts( self.contents.source.single_id(), - Boosts::Hide, Replies::Hide).into()), + Boosts::Hide, + Replies::Hide, + ) + .into(), + ), Pr('q') | Pr('Q') => { self.ui_mode = UIMode::Normal; LogicalAction::Nothing } _ => LogicalAction::Nothing, - } + }, UIMode::Select(_, purpose) => match key { Space => match purpose { SelectionPurpose::Vote => self.vote(), _ => self.complete_selection(client, false), - } + }, Pr('d') | Pr('D') => match purpose { - SelectionPurpose::Favourite | SelectionPurpose::Boost => - self.complete_selection(client, true), + SelectionPurpose::Favourite | SelectionPurpose::Boost => { + self.complete_selection(client, true) + } _ => LogicalAction::Nothing, - } + }, Pr('f') | Pr('F') => match purpose { - SelectionPurpose::Thread => - self.complete_selection(client, true), + SelectionPurpose::Thread => { + self.complete_selection(client, true) + } _ => LogicalAction::Nothing, - } + }, Ctrl('V') => match purpose { - SelectionPurpose::Vote => - self.complete_selection(client, false), + SelectionPurpose::Vote => { + self.complete_selection(client, false) + } _ => LogicalAction::Nothing, - } + }, Pr('-') | Up => self.selection_up(client), Pr('+') | Down => self.selection_down(client), Pr('q') | Pr('Q') => self.abort_selection(), _ => LogicalAction::Nothing, - } + }, })(); self.update_latest_read_index(); @@ -1499,8 +1706,11 @@ impl action } - fn handle_feed_updates(&mut self, feeds_updated: &HashSet, - client: &mut Client) { + fn handle_feed_updates( + &mut self, + feeds_updated: &HashSet, + client: &mut Client, + ) { if self.contents.source.updated(feeds_updated) { self.contents.update_items(client); self.ensure_enough_rendered(); @@ -1517,7 +1727,8 @@ impl self.file_desc.feed_id().map(|id| { let sfp = SavedFilePos { file_pos: Some(self.pos), - latest_read_id: self.latest_read_index + latest_read_id: self + .latest_read_index .and_then(|i| self.contents.id_at_index(i)) .map(|s| s.to_owned()), }; @@ -1525,9 +1736,11 @@ impl }) } - fn got_search_expression(&mut self, dir: SearchDirection, regex: String) - -> LogicalAction - { + fn got_search_expression( + &mut self, + dir: SearchDirection, + regex: String, + ) -> LogicalAction { match Regex::new(®ex) { Ok(re) => { self.search_direction = Some(dir); @@ -1539,86 +1752,123 @@ impl } } -pub fn home_timeline(file_positions: &HashMap, - unfolded: Rc>>, - client: &mut Client) -> - Result, ClientError> -{ +pub fn home_timeline( + file_positions: &HashMap, + unfolded: Rc>>, + client: &mut Client, +) -> Result, ClientError> { let feed = FeedId::Home; let pos = file_positions.get(&feed); let desc = StatusFeedType::with_feed(feed.clone()); let file = File::new( - client, FeedSource::new(feed), ColouredString::general( - "Home timeline ", - "HHHHHHHHHHHHHHHHHKH"), desc, pos, Some(unfolded), false)?; + client, + FeedSource::new(feed), + ColouredString::general("Home timeline ", "HHHHHHHHHHHHHHHHHKH"), + desc, + pos, + Some(unfolded), + false, + )?; Ok(Box::new(file)) } -pub fn local_timeline(file_positions: &HashMap, - unfolded: Rc>>, - client: &mut Client) -> - Result, ClientError> -{ +pub fn local_timeline( + file_positions: &HashMap, + unfolded: Rc>>, + client: &mut Client, +) -> Result, ClientError> { let feed = FeedId::Local; let pos = file_positions.get(&feed); let desc = StatusFeedType::with_feed(feed.clone()); let file = File::new( - client, FeedSource::new(feed), ColouredString::general( + client, + FeedSource::new(feed), + ColouredString::general( "Local public timeline ", - "HHHHHHHHHHHHHHHHHHHHHHHHHKH"), desc, pos, Some(unfolded), false)?; + "HHHHHHHHHHHHHHHHHHHHHHHHHKH", + ), + desc, + pos, + Some(unfolded), + false, + )?; Ok(Box::new(file)) } -pub fn public_timeline(file_positions: &HashMap, - unfolded: Rc>>, - client: &mut Client) -> - Result, ClientError> -{ +pub fn public_timeline( + file_positions: &HashMap, + unfolded: Rc>>, + client: &mut Client, +) -> Result, ClientError> { let feed = FeedId::Public; let pos = file_positions.get(&feed); let desc = StatusFeedType::with_feed(feed.clone()); let file = File::new( - client, FeedSource::new(feed), ColouredString::general( + client, + FeedSource::new(feed), + ColouredString::general( "Public timeline

", - "HHHHHHHHHHHHHHHHHHHKH"), desc, pos, Some(unfolded), false)?; + "HHHHHHHHHHHHHHHHHHHKH", + ), + desc, + pos, + Some(unfolded), + false, + )?; Ok(Box::new(file)) } -pub fn mentions(file_positions: &HashMap, - unfolded: Rc>>, - client: &mut Client, is_interrupt: bool) -> - Result, ClientError> -{ +pub fn mentions( + file_positions: &HashMap, + unfolded: Rc>>, + client: &mut Client, + is_interrupt: bool, +) -> Result, ClientError> { let feed = FeedId::Mentions; let pos = file_positions.get(&feed); let desc = NotificationStatusFeedType::with_feed(feed.clone()); let file = File::new( - client, FeedSource::new(feed), ColouredString::general( - "Mentions [ESC][R]", - "HHHHHHHHHHHHKKKHHKH"), desc, pos, Some(unfolded), is_interrupt)?; + client, + FeedSource::new(feed), + ColouredString::general("Mentions [ESC][R]", "HHHHHHHHHHHHKKKHHKH"), + desc, + pos, + Some(unfolded), + is_interrupt, + )?; Ok(Box::new(file)) } -pub fn ego_log(file_positions: &HashMap, - client: &mut Client) -> - Result, ClientError> -{ +pub fn ego_log( + file_positions: &HashMap, + client: &mut Client, +) -> Result, ClientError> { let feed = FeedId::Ego; let pos = file_positions.get(&feed); let desc = EgoNotificationFeedType::with_feed(feed.clone()); let file = File::new( - client, FeedSource::new(feed), ColouredString::general( + client, + FeedSource::new(feed), + ColouredString::general( "Ego Log [ESC][L][L][E]", - "HHHHHHHHHHHKKKHHKHHKHHKH"), desc, pos, None, false)?; + "HHHHHHHHHHHKKKHHKHHKHHKH", + ), + desc, + pos, + None, + false, + )?; Ok(Box::new(file)) } pub fn user_posts( file_positions: &HashMap, unfolded: Rc>>, - client: &mut Client, user: &str, boosts: Boosts, replies: Replies) - -> Result, ClientError> -{ + client: &mut Client, + user: &str, + boosts: Boosts, + replies: Replies, +) -> Result, ClientError> { let feed = FeedId::User(user.to_owned(), boosts, replies); let pos = file_positions.get(&feed); let desc = StatusFeedType::with_feed(feed.clone()); @@ -1627,86 +1877,126 @@ pub fn user_posts( let username = client.fq(&ac.acct); let title = match (boosts, replies) { - (Boosts::Hide, Replies::Hide) => - format!("Top-level posts by {username}"), - (Boosts::Hide, Replies::Show) => - format!("Original posts by {username}"), - (Boosts::Show, Replies::Show) => - format!("All posts by {username}"), + (Boosts::Hide, Replies::Hide) => { + format!("Top-level posts by {username}") + } + (Boosts::Hide, Replies::Show) => { + format!("Original posts by {username}") + } + (Boosts::Show, Replies::Show) => format!("All posts by {username}"), // We don't currently have a UI for asking for this // combination, but we can't leave it out of the match or Rust // will complain, so we might as well write a title in case we // ever decide to add it for some reason - (Boosts::Show, Replies::Hide) => - format!("Boosts and top-level posts by {username}"), + (Boosts::Show, Replies::Hide) => { + format!("Boosts and top-level posts by {username}") + } }; let file = File::new( - client, FeedSource::new(feed), ColouredString::uniform(&title, 'H'), - desc, pos, Some(unfolded), false)?; + client, + FeedSource::new(feed), + ColouredString::uniform(&title, 'H'), + desc, + pos, + Some(unfolded), + false, + )?; Ok(Box::new(file)) } -pub fn list_status_favouriters(client: &mut Client, id: &str) -> - Result, ClientError> -{ +pub fn list_status_favouriters( + client: &mut Client, + id: &str, +) -> Result, ClientError> { let file = File::new( - client, FeedSource::new(FeedId::Favouriters(id.to_owned())), + client, + FeedSource::new(FeedId::Favouriters(id.to_owned())), ColouredString::uniform( - &format!("Users who favourited post {id}"), 'H'), - UserListFeedType{}, None, None, false)?; + &format!("Users who favourited post {id}"), + 'H', + ), + UserListFeedType {}, + None, + None, + false, + )?; Ok(Box::new(file)) } -pub fn list_status_boosters(client: &mut Client, id: &str) -> - Result, ClientError> -{ +pub fn list_status_boosters( + client: &mut Client, + id: &str, +) -> Result, ClientError> { let file = File::new( - client, FeedSource::new(FeedId::Boosters(id.to_owned())), - ColouredString::uniform( - &format!("Users who boosted post {id}"), 'H'), - UserListFeedType{}, None, None, false)?; + client, + FeedSource::new(FeedId::Boosters(id.to_owned())), + ColouredString::uniform(&format!("Users who boosted post {id}"), 'H'), + UserListFeedType {}, + None, + None, + false, + )?; Ok(Box::new(file)) } -pub fn list_user_followers(client: &mut Client, id: &str) -> - Result, ClientError> -{ +pub fn list_user_followers( + client: &mut Client, + id: &str, +) -> Result, ClientError> { let ac = client.account_by_id(id)?; let name = client.fq(&ac.acct); let file = File::new( - client, FeedSource::new(FeedId::Followers(id.to_owned())), - ColouredString::uniform( - &format!("Users who follow {name}"), 'H'), - UserListFeedType{}, None, None, false)?; + client, + FeedSource::new(FeedId::Followers(id.to_owned())), + ColouredString::uniform(&format!("Users who follow {name}"), 'H'), + UserListFeedType {}, + None, + None, + false, + )?; Ok(Box::new(file)) } -pub fn list_user_followees(client: &mut Client, id: &str) -> - Result, ClientError> -{ +pub fn list_user_followees( + client: &mut Client, + id: &str, +) -> Result, ClientError> { let ac = client.account_by_id(id)?; let name = client.fq(&ac.acct); let file = File::new( - client, FeedSource::new(FeedId::Followees(id.to_owned())), - ColouredString::uniform( - &format!("Users who {name} follows"), 'H'), - UserListFeedType{}, None, None, false)?; + client, + FeedSource::new(FeedId::Followees(id.to_owned())), + ColouredString::uniform(&format!("Users who {name} follows"), 'H'), + UserListFeedType {}, + None, + None, + false, + )?; Ok(Box::new(file)) } -pub fn hashtag_timeline(unfolded: Rc>>, - client: &mut Client, tag: &str) -> - Result, ClientError> -{ +pub fn hashtag_timeline( + unfolded: Rc>>, + client: &mut Client, + tag: &str, +) -> Result, ClientError> { let title = ColouredString::uniform( - &format!("Posts mentioning hashtag #{tag}"), 'H'); + &format!("Posts mentioning hashtag #{tag}"), + 'H', + ); let feed = FeedId::Hashtag(tag.to_owned()); let desc = StatusFeedType::with_feed(feed); let file = File::new( - client, FeedSource::new(FeedId::Hashtag(tag.to_owned())), title, - desc, None, Some(unfolded), false)?; + client, + FeedSource::new(FeedId::Hashtag(tag.to_owned())), + title, + desc, + None, + Some(unfolded), + false, + )?; Ok(Box::new(file)) } @@ -1717,25 +2007,35 @@ impl FileType for ExamineUserFileType { const CAN_GET_POSTS: bool = true; const IS_EXAMINE_USER: bool = true; - fn get_from_client(id: &str, client: &mut Client) -> - Result - { + fn get_from_client( + id: &str, + client: &mut Client, + ) -> Result { let ac = client.account_by_id(id)?; ExamineUserDisplay::new(ac, client) } } -pub fn examine_user(client: &mut Client, account_id: &str) -> - Result, ClientError> -{ +pub fn examine_user( + client: &mut Client, + account_id: &str, +) -> Result, ClientError> { let ac = client.account_by_id(account_id)?; let username = client.fq(&ac.acct); let title = ColouredString::uniform( - &format!("Information about user {username}"), 'H'); + &format!("Information about user {username}"), + 'H', + ); let file = File::new( - client, StaticSource::singleton(ac.id), title, ExamineUserFileType{}, - Some(&FilePosition::item_top(isize::MIN).into()), None, false)?; + client, + StaticSource::singleton(ac.id), + title, + ExamineUserFileType {}, + Some(&FilePosition::item_top(isize::MIN).into()), + None, + false, + )?; Ok(Box::new(file)) } @@ -1744,34 +2044,44 @@ impl FileType for DetailedStatusFileType { type Item = DetailedStatusDisplay; const CAN_LIST: CanList = CanList::ForPost; - fn get_from_client(id: &str, client: &mut Client) -> - Result - { + fn get_from_client( + id: &str, + client: &mut Client, + ) -> Result { let st = client.status_by_id(id)?; Ok(DetailedStatusDisplay::new(st, client)) } } -pub fn view_single_post(unfolded: Rc>>, - client: &mut Client, status_id: &str) -> - Result, ClientError> -{ +pub fn view_single_post( + unfolded: Rc>>, + client: &mut Client, + status_id: &str, +) -> Result, ClientError> { let st = client.status_by_id(status_id)?; let title = ColouredString::uniform( - &format!("Information about post {}", st.id), 'H'); + &format!("Information about post {}", st.id), + 'H', + ); let file = File::new( - client, StaticSource::singleton(st.id), title, - DetailedStatusFileType{}, + client, + StaticSource::singleton(st.id), + title, + DetailedStatusFileType {}, Some(&FilePosition::item_top(isize::MIN).into()), - Some(unfolded), false)?; + Some(unfolded), + false, + )?; Ok(Box::new(file)) } -pub fn view_thread(unfolded: Rc>>, - client: &mut Client, start_id: &str, full: bool) -> - Result, ClientError> -{ +pub fn view_thread( + unfolded: Rc>>, + client: &mut Client, + start_id: &str, + full: bool, +) -> Result, ClientError> { let mut make_vec = |id: &str| -> Result, ClientError> { let ctx = client.status_context(id)?; let mut v = Vec::new(); @@ -1801,12 +2111,19 @@ pub fn view_thread(unfolded: Rc>>, let title = ColouredString::uniform(&title, 'H'); // Focus the id in question, assuming we can - let index = ids.iter().position(|x| x == start_id) + let index = ids + .iter() + .position(|x| x == start_id) .map_or(isize::MIN, |u| u as isize); let file = File::new( - client, StaticSource::vector(ids), title, + client, + StaticSource::vector(ids), + title, StatusFeedType::without_feed(), - Some(&FilePosition::item_top(index).into()), Some(unfolded), false)?; + Some(&FilePosition::item_top(index).into()), + Some(unfolded), + false, + )?; Ok(Box::new(file)) } diff --git a/src/html.rs b/src/html.rs index 8119c97..f600aff 100644 --- a/src/html.rs +++ b/src/html.rs @@ -1,8 +1,8 @@ -use html2text::{config, Colour}; -pub use html2text::RenderTree; use html2text::render::text_renderer::{ - TextDecorator, TaggedLine, TaggedLineElement + TaggedLine, TaggedLineElement, TextDecorator, }; +pub use html2text::RenderTree; +use html2text::{config, Colour}; use std::cell::RefCell; use super::coloured_string::*; @@ -36,8 +36,10 @@ impl<'a> TextDecorator for OurDecorator<'a> { type Annotation = char; /// Return an annotation and rendering prefix for a link. - fn decorate_link_start(&mut self, url: &str) - -> (String, Self::Annotation) { + fn decorate_link_start( + &mut self, + url: &str, + ) -> (String, Self::Annotation) { if self.colours_pushed == 0 && self.urls.is_some() { self.current_url = Some(url.to_owned()); } @@ -65,7 +67,9 @@ impl<'a> TextDecorator for OurDecorator<'a> { } /// Return a suffix for after an em. - fn decorate_em_end(&mut self) -> String { "".to_string() } + fn decorate_em_end(&mut self) -> String { + "".to_string() + } /// Return an annotation and rendering prefix for strong fn decorate_strong_start(&mut self) -> (String, Self::Annotation) { @@ -73,7 +77,9 @@ impl<'a> TextDecorator for OurDecorator<'a> { } /// Return a suffix for after a strong. - fn decorate_strong_end(&mut self) -> String { "".to_string() } + fn decorate_strong_end(&mut self) -> String { + "".to_string() + } /// Return an annotation and rendering prefix for strikeout fn decorate_strikeout_start(&mut self) -> (String, Self::Annotation) { @@ -81,7 +87,9 @@ impl<'a> TextDecorator for OurDecorator<'a> { } /// Return a suffix for after a strikeout. - fn decorate_strikeout_end(&mut self) -> String { "".to_string() } + fn decorate_strikeout_end(&mut self) -> String { + "".to_string() + } /// Return an annotation and rendering prefix for code fn decorate_code_start(&mut self) -> (String, Self::Annotation) { @@ -89,18 +97,27 @@ impl<'a> TextDecorator for OurDecorator<'a> { } /// Return a suffix for after a code. - fn decorate_code_end(&mut self) -> String { "".to_string() } + fn decorate_code_end(&mut self) -> String { + "".to_string() + } /// Return an annotation for the initial part of a preformatted line - fn decorate_preformat_first(&mut self) -> Self::Annotation { 'c' } + fn decorate_preformat_first(&mut self) -> Self::Annotation { + 'c' + } /// Return an annotation for a continuation line when a preformatted /// line doesn't fit. - fn decorate_preformat_cont(&mut self) -> Self::Annotation { 'c' } + fn decorate_preformat_cont(&mut self) -> Self::Annotation { + 'c' + } /// Return an annotation and rendering prefix for a link. - fn decorate_image(&mut self, _src: &str, _title: &str) - -> (String, Self::Annotation) { + fn decorate_image( + &mut self, + _src: &str, + _title: &str, + ) -> (String, Self::Annotation) { ("".to_string(), 'm') } @@ -110,10 +127,14 @@ impl<'a> TextDecorator for OurDecorator<'a> { } /// Return prefix string of quoted block. - fn quote_prefix(&mut self) -> String { "> ".to_string() } + fn quote_prefix(&mut self) -> String { + "> ".to_string() + } /// Return prefix string of unordered list item. - fn unordered_item_prefix(&mut self) -> String { " - ".to_string() } + fn unordered_item_prefix(&mut self) -> String { + " - ".to_string() + } /// Return prefix string of ith ordered list item. fn ordered_item_prefix(&mut self, i: i64) -> String { @@ -150,26 +171,32 @@ impl<'a> TextDecorator for OurDecorator<'a> { /// Finish with a document, and return extra lines (eg footnotes) /// to add to the rendered text. - fn finalise(&mut self, _links: Vec) - -> Vec> { + fn finalise( + &mut self, + _links: Vec, + ) -> Vec> { Vec::new() } } pub fn parse(html: &str) -> Result { - let cfg = config::plain().add_css(r##" + let cfg = config::plain().add_css( + r##" .mention { color: #010203; } .hashtag { color: #040506; } -"##)?; +"##, + )?; let dom = cfg.parse_html(html.as_bytes())?; cfg.dom_to_render_tree(&dom) } -fn try_render(rt: &RenderTree, wrapwidth: usize, fullwidth: usize) -> - Result>>, html2text::Error> -{ - let cfg = config::with_decorator(OurDecorator::new()) - .max_wrap_width(wrapwidth); +fn try_render( + rt: &RenderTree, + wrapwidth: usize, + fullwidth: usize, +) -> Result>>, html2text::Error> { + let cfg = + config::with_decorator(OurDecorator::new()).max_wrap_width(wrapwidth); cfg.render_to_lines(rt.clone(), fullwidth) } @@ -217,7 +244,10 @@ fn to_coloured_string(tl: &TaggedLine>) -> ColouredString { } pub fn render(rt: &RenderTree, width: usize) -> Vec { - render_tl(rt, width).iter().map(to_coloured_string).collect() + render_tl(rt, width) + .iter() + .map(to_coloured_string) + .collect() } pub fn list_urls(rt: &RenderTree) -> Vec { @@ -225,11 +255,13 @@ pub fn list_urls(rt: &RenderTree) -> Vec { loop { let urls = RefCell::new(Vec::new()); if config::with_decorator(OurDecorator::with_urls(&urls)) - .render_to_lines(rt.clone(), width).is_ok() + .render_to_lines(rt.clone(), width) + .is_ok() { break urls.into_inner(); } - width = width.checked_mul(2) + width = width + .checked_mul(2) .expect("Surely something else went wrong before we got this big"); } } diff --git a/src/lib.rs b/src/lib.rs index 5d44182..19e8b1d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,19 +1,19 @@ -pub mod types; -pub mod login; +pub mod activity_stack; pub mod auth; +pub mod client; +pub mod coloured_string; pub mod config; +pub mod editor; +pub mod file; pub mod html; +pub mod login; +pub mod menu; +pub mod options; +pub mod posting; pub mod scan_re; -pub mod coloured_string; pub mod text; -pub mod client; -pub mod activity_stack; pub mod tui; -pub mod menu; -pub mod file; -pub mod editor; -pub mod posting; -pub mod options; +pub mod types; #[derive(Debug)] pub struct TopLevelError { @@ -31,15 +31,18 @@ impl TopLevelError { } impl std::fmt::Display for TopLevelError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> - Result<(), std::fmt::Error> - { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> Result<(), std::fmt::Error> { write!(f, "mastodonochrome: {}{}", self.prefix, self.message) } } trait TopLevelErrorCandidate: std::fmt::Display { - fn get_prefix() -> String { "error: ".to_owned() } + fn get_prefix() -> String { + "error: ".to_owned() + } } impl From for TopLevelError { @@ -53,7 +56,9 @@ impl From for TopLevelError { impl TopLevelErrorCandidate for clap::error::Error { // clap prints its own "error: " - fn get_prefix() -> String { "".to_owned() } + fn get_prefix() -> String { + "".to_owned() + } } impl TopLevelErrorCandidate for std::io::Error {} diff --git a/src/login.rs b/src/login.rs index 8a63d03..b3017fc 100644 --- a/src/login.rs +++ b/src/login.rs @@ -1,12 +1,14 @@ use reqwest::Url; -use std::io::Write; use std::fs::File; +use std::io::Write; -use super::TopLevelError; use super::auth::{AuthConfig, AuthError}; +use super::client::{ + execute_and_log_request, reqwest_client, ClientError, Req, +}; use super::config::ConfigLocation; -use super::client::{reqwest_client, Req, ClientError, execute_and_log_request}; use super::types::{Account, Application, Instance, Token}; +use super::TopLevelError; struct Login { instance_url: String, @@ -23,9 +25,10 @@ use AppTokenType::*; const REDIRECT_MAGIC_STRING: &str = "urn:ietf:wg:oauth:2.0:oob"; impl Login { - fn new(instance_url: &str, logfile: Option) - -> Result - { + fn new( + instance_url: &str, + logfile: Option, + ) -> Result { Ok(Login { instance_url: instance_url.to_owned(), client: reqwest_client()?, @@ -33,11 +36,11 @@ impl Login { }) } - fn execute_request(&mut self, req: reqwest::blocking::RequestBuilder) - -> Result - { - let (rsp, log) = execute_and_log_request( - &self.client, req.build()?)?; + fn execute_request( + &mut self, + req: reqwest::blocking::RequestBuilder, + ) -> Result { + let (rsp, log) = execute_and_log_request(&self.client, req.build()?)?; log.write_to(&mut self.logfile); Ok(rsp) } @@ -62,16 +65,24 @@ impl Login { } } - fn get_token(&mut self, app: &Application, toktype: AppTokenType) -> - Result - { + fn get_token( + &mut self, + app: &Application, + toktype: AppTokenType, + ) -> Result { let client_id = match &app.client_id { Some(id) => Ok(id), - None => Err(ClientError::InternalError("registering application did not return a client id".to_owned())), + None => Err(ClientError::InternalError( + "registering application did not return a client id" + .to_owned(), + )), }?; let client_secret = match &app.client_secret { Some(id) => Ok(id), - None => Err(ClientError::InternalError("registering application did not return a client secret".to_owned())), + None => Err(ClientError::InternalError( + "registering application did not return a client secret" + .to_owned(), + )), }?; let req = Req::post("/oauth/token") @@ -79,8 +90,7 @@ impl Login { .param("client_id", client_id) .param("client_secret", client_secret); let req = match toktype { - ClientCredentials => req - .param("grant_type", "client_credentials"), + ClientCredentials => req.param("grant_type", "client_credentials"), AuthorizationCode(code) => req .param("grant_type", "authorization_code") .param("code", code) @@ -101,10 +111,15 @@ impl Login { } fn get serde::Deserialize<'a>>( - &mut self, path: &str, token: &str) -> Result - { - let (url, req) = Req::get(path) - .build(&self.instance_url, &self.client, Some(token))?; + &mut self, + path: &str, + token: &str, + ) -> Result { + let (url, req) = Req::get(path).build( + &self.instance_url, + &self.client, + Some(token), + )?; let rsp = self.execute_request(req)?; let rspstatus = rsp.status(); if !rspstatus.is_success() { @@ -121,7 +136,10 @@ impl Login { fn get_auth_url(&self, app: &Application) -> Result { let client_id = match &app.client_id { Some(id) => Ok(id), - None => Err(ClientError::InternalError("registering application did not return a client id".to_owned())), + None => Err(ClientError::InternalError( + "registering application did not return a client id" + .to_owned(), + )), }?; let (_urlstr, url) = Req::get("/oauth/authorize") @@ -134,10 +152,11 @@ impl Login { } } -pub fn login(cfgloc: &ConfigLocation, instance_url: &str, - logfile: Option) -> - Result<(), TopLevelError> -{ +pub fn login( + cfgloc: &ConfigLocation, + instance_url: &str, + logfile: Option, +) -> Result<(), TopLevelError> { // First, check we aren't logged in already, and give some // marginally useful advice on what to do if we are. match AuthConfig::load(cfgloc) { @@ -156,8 +175,9 @@ pub fn login(cfgloc: &ConfigLocation, instance_url: &str, }; let url = match Url::parse(&urlstr) { Ok(url) => Ok(url), - Err(e) => Err(ClientError::UrlParseError( - urlstr.clone(), e.to_string())), + Err(e) => { + Err(ClientError::UrlParseError(urlstr.clone(), e.to_string())) + } }?; let instance_url = url.as_str().trim_end_matches('/'); @@ -166,8 +186,8 @@ pub fn login(cfgloc: &ConfigLocation, instance_url: &str, // Register the client and get its details let app = login.register_client()?; let app_token = login.get_token(&app, ClientCredentials)?; - let _app: Application = login.get("/api/v1/apps/verify_credentials", - &app_token.access_token)?; + let _app: Application = login + .get("/api/v1/apps/verify_credentials", &app_token.access_token)?; // Get the URL the user will have to visit let url = login.get_auth_url(&app)?; @@ -192,13 +212,18 @@ pub fn login(cfgloc: &ConfigLocation, instance_url: &str, // Use that code to get the final user access token let user_token = login.get_token(&app, AuthorizationCode(code))?; - let account: Account = login.get("/api/v1/accounts/verify_credentials", - &user_token.access_token)?; - let instance: Instance = login.get("/api/v2/instance", - &user_token.access_token)?; + let account: Account = login.get( + "/api/v1/accounts/verify_credentials", + &user_token.access_token, + )?; + let instance: Instance = + login.get("/api/v2/instance", &user_token.access_token)?; println!(); - println!("Successfully logged in as {}@{}", account.username, instance.domain); + println!( + "Successfully logged in as {}@{}", + account.username, instance.domain + ); println!("Now run 'mastodonochrome' without arguments to read and post!"); // Save everything! diff --git a/src/main.rs b/src/main.rs index 00df08d..383cb30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,10 @@ use clap::Parser; use std::process::ExitCode; -use mastodonochrome::TopLevelError; use mastodonochrome::config::ConfigLocation; -use mastodonochrome::tui::Tui; use mastodonochrome::login::login; +use mastodonochrome::tui::Tui; +use mastodonochrome::TopLevelError; #[derive(Parser, Debug)] struct Args { diff --git a/src/menu.rs b/src/menu.rs index 2ca86d6..2e010e7 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -1,15 +1,14 @@ -use std::collections::HashMap; use itertools::Itertools; +use std::collections::HashMap; use super::activity_stack::{ - NonUtilityActivity, UtilityActivity, OverlayActivity + NonUtilityActivity, OverlayActivity, UtilityActivity, }; use super::client::Client; use super::coloured_string::ColouredString; use super::text::*; use super::tui::{ - ActivityState, CursorPosition, LogicalAction, - OurKey, OurKey::* + ActivityState, CursorPosition, LogicalAction, OurKey, OurKey::*, }; enum MenuLine { @@ -18,8 +17,10 @@ enum MenuLine { Info(CentredInfoLine), } -fn find_substring(haystack: &str, needle: &str) - -> Option<(usize, usize, usize)> { +fn find_substring( + haystack: &str, + needle: &str, +) -> Option<(usize, usize, usize)> { if let Some(pos) = haystack.find(needle) { let (pre, post) = haystack.split_at(pos); let pre_nchars = pre.chars().count(); @@ -31,8 +32,7 @@ fn find_substring(haystack: &str, needle: &str) } } -fn find_highlight_char(desc: &str, c: char) - -> Option<(usize, usize, usize)> { +fn find_highlight_char(desc: &str, c: char) -> Option<(usize, usize, usize)> { let found = find_substring(desc, &c.to_uppercase().to_string()); if found.is_some() { found @@ -73,20 +73,27 @@ impl Menu { menu } - fn add_action_coloured(&mut self, key: OurKey, desc: ColouredString, - action: LogicalAction) { - self.lines.push(MenuLine::Key(MenuKeypressLine::new(key, desc))); + fn add_action_coloured( + &mut self, + key: OurKey, + desc: ColouredString, + action: LogicalAction, + ) { + self.lines + .push(MenuLine::Key(MenuKeypressLine::new(key, desc))); if action != LogicalAction::Nothing { if let Pr(c) = key { - if let Ok(c) = c.to_lowercase(). - to_string().chars().exactly_one() { - self.actions.insert(Pr(c), action.clone()); - } - if let Ok(c) = c.to_uppercase(). - to_string().chars().exactly_one() { - self.actions.insert(Pr(c), action.clone()); - } + if let Ok(c) = + c.to_lowercase().to_string().chars().exactly_one() + { + self.actions.insert(Pr(c), action.clone()); + } + if let Ok(c) = + c.to_uppercase().to_string().chars().exactly_one() + { + self.actions.insert(Pr(c), action.clone()); + } } self.actions.insert(key, action); @@ -109,9 +116,10 @@ impl Menu { let desc_coloured = if let Some((before, during, after)) = highlight { ColouredString::general( desc, - &("H".repeat(before) + - &"K".repeat(during) + - &"H".repeat(after))) + &("H".repeat(before) + + &"K".repeat(during) + + &"H".repeat(after)), + ) } else { ColouredString::uniform(desc, 'H') }; @@ -124,7 +132,8 @@ impl Menu { } fn add_info_coloured(&mut self, desc: ColouredString) { - self.bottom_lines.push(MenuLine::Info(CentredInfoLine::new(desc))); + self.bottom_lines + .push(MenuLine::Info(CentredInfoLine::new(desc))); } fn add_info(&mut self, desc: &str) { self.add_info_coloured(ColouredString::uniform(desc, 'H')); @@ -148,8 +157,11 @@ impl Menu { } impl ActivityState for Menu { - fn draw(&self, w: usize, h: usize) - -> (Vec, CursorPosition) { + fn draw( + &self, + w: usize, + h: usize, + ) -> (Vec, CursorPosition) { let mut lines = Vec::new(); lines.extend_from_slice(&self.title.render(w)); lines.extend_from_slice(&BlankLine::render_static()); @@ -186,9 +198,11 @@ impl ActivityState for Menu { (lines, CursorPosition::End) } - fn handle_keypress(&mut self, key: OurKey, _client: &mut Client) -> - LogicalAction - { + fn handle_keypress( + &mut self, + key: OurKey, + _client: &mut Client, + ) -> LogicalAction { match self.actions.get(&key) { Some(action) => action.clone(), None => LogicalAction::Nothing, @@ -198,25 +212,43 @@ impl ActivityState for Menu { pub fn main_menu(client: &Client) -> Box { let mut menu = Menu::new( - ColouredString::uniform("Mastodonochrome Main Menu", 'H'), true); - - menu.add_action(Pr('H'), "Home timeline", LogicalAction::Goto( - NonUtilityActivity::HomeTimelineFile.into())); + ColouredString::uniform("Mastodonochrome Main Menu", 'H'), + true, + ); + + menu.add_action( + Pr('H'), + "Home timeline", + LogicalAction::Goto(NonUtilityActivity::HomeTimelineFile.into()), + ); menu.add_blank_line(); - menu.add_action(Pr('P'), "Public timeline (all servers)", - LogicalAction::Goto( - NonUtilityActivity::PublicTimelineFile.into())); - menu.add_action(Pr('L'), "Local public timeline (this server)", - LogicalAction::Goto( - NonUtilityActivity::LocalTimelineFile.into())); - menu.add_action(Pr('#'), "Timeline for a #hashtag", LogicalAction::Goto( - OverlayActivity::GetHashtagToRead.into())); + menu.add_action( + Pr('P'), + "Public timeline (all servers)", + LogicalAction::Goto(NonUtilityActivity::PublicTimelineFile.into()), + ); + menu.add_action( + Pr('L'), + "Local public timeline (this server)", + LogicalAction::Goto(NonUtilityActivity::LocalTimelineFile.into()), + ); + menu.add_action( + Pr('#'), + "Timeline for a #hashtag", + LogicalAction::Goto(OverlayActivity::GetHashtagToRead.into()), + ); menu.add_blank_line(); - menu.add_action(Pr('I'), "View a post by its ID", LogicalAction::Goto( - OverlayActivity::GetPostIdToRead.into())); + menu.add_action( + Pr('I'), + "View a post by its ID", + LogicalAction::Goto(OverlayActivity::GetPostIdToRead.into()), + ); menu.add_blank_line(); - menu.add_action(Pr('C'), "Compose a post", LogicalAction::Goto( - NonUtilityActivity::ComposeToplevel.into())); + menu.add_action( + Pr('C'), + "Compose a post", + LogicalAction::Goto(NonUtilityActivity::ComposeToplevel.into()), + ); menu.add_blank_line(); // We don't need to provide a LogicalAction for this keystroke, @@ -228,7 +260,9 @@ pub fn main_menu(client: &Client) -> Box { menu.add_info(&format!("Logged in as {}", &client.our_account_fq())); if !client.is_writable() { menu.add_info_coloured(ColouredString::uniform( - "Mastodonochrome was run in readonly mode", 'r')); + "Mastodonochrome was run in readonly mode", + 'r', + )); } Box::new(menu.finalise()) @@ -236,28 +270,48 @@ pub fn main_menu(client: &Client) -> Box { pub fn utils_menu(client: &Client) -> Box { let mut menu = Menu::new( - ColouredString::general( - "Utilities [ESC]", - "HHHHHHHHHHHKKKH"), false); + ColouredString::general("Utilities [ESC]", "HHHHHHHHHHHKKKH"), + false, + ); let our_account_id = client.our_account_id(); - menu.add_action(Pr('R'), "Read Mentions", LogicalAction::Goto( - UtilityActivity::ReadMentions.into())); + menu.add_action( + Pr('R'), + "Read Mentions", + LogicalAction::Goto(UtilityActivity::ReadMentions.into()), + ); menu.add_blank_line(); - menu.add_action(Pr('E'), "Examine User", LogicalAction::Goto( - OverlayActivity::GetUserToExamine.into())); - menu.add_action(Pr('Y'), "Examine Yourself", LogicalAction::Goto( - UtilityActivity::ExamineUser(our_account_id).into())); + menu.add_action( + Pr('E'), + "Examine User", + LogicalAction::Goto(OverlayActivity::GetUserToExamine.into()), + ); + menu.add_action( + Pr('Y'), + "Examine Yourself", + LogicalAction::Goto( + UtilityActivity::ExamineUser(our_account_id).into(), + ), + ); menu.add_blank_line(); - menu.add_action(Pr('L'), "Logs menu", LogicalAction::Goto( - UtilityActivity::LogsMenu1.into())); + menu.add_action( + Pr('L'), + "Logs menu", + LogicalAction::Goto(UtilityActivity::LogsMenu1.into()), + ); menu.add_blank_line(); - menu.add_action(Pr('G'), "Go to Main Menu", LogicalAction::Goto( - NonUtilityActivity::MainMenu.into())); + menu.add_action( + Pr('G'), + "Go to Main Menu", + LogicalAction::Goto(NonUtilityActivity::MainMenu.into()), + ); menu.add_blank_line(); - menu.add_action(Pr('X'), "Exit Mastodonochrome", LogicalAction::Goto( - UtilityActivity::ExitMenu.into())); + menu.add_action( + Pr('X'), + "Exit Mastodonochrome", + LogicalAction::Goto(UtilityActivity::ExitMenu.into()), + ); Box::new(menu.finalise()) } @@ -266,7 +320,10 @@ pub fn exit_menu() -> Box { let mut menu = Menu::new( ColouredString::general( "Exit Mastodonochrome [ESC][X]", - "HHHHHHHHHHHHHHHHHHHHHHKKKHHKH"), false); + "HHHHHHHHHHHHHHHHHHHHHHKKKHHKH", + ), + false, + ); menu.add_action(Pr('X'), "Confirm exit", LogicalAction::Exit); @@ -277,10 +334,16 @@ pub fn logs_menu_1() -> Box { let mut menu = Menu::new( ColouredString::general( "Client Logs [ESC][L]", - "HHHHHHHHHHHHHKKKHHKH"), false); + "HHHHHHHHHHHHHKKKHHKH", + ), + false, + ); - menu.add_action(Pr('L'), "Server Logs", LogicalAction::Goto( - UtilityActivity::LogsMenu2.into())); + menu.add_action( + Pr('L'), + "Server Logs", + LogicalAction::Goto(UtilityActivity::LogsMenu2.into()), + ); Box::new(menu.finalise()) } @@ -289,10 +352,16 @@ pub fn logs_menu_2() -> Box { let mut menu = Menu::new( ColouredString::general( "Server Logs [ESC][L][L]", - "HHHHHHHHHHHHHKKKHHKHHKH"), false); - - menu.add_action(Pr('E'), "Ego Log (Boosts, Follows and Faves)", - LogicalAction::Goto(UtilityActivity::EgoLog.into())); + "HHHHHHHHHHHHHKKKHHKHHKH", + ), + false, + ); + + menu.add_action( + Pr('E'), + "Ego Log (Boosts, Follows and Faves)", + LogicalAction::Goto(UtilityActivity::EgoLog.into()), + ); Box::new(menu.finalise()) } diff --git a/src/options.rs b/src/options.rs index 08d187f..fa44fba 100644 --- a/src/options.rs +++ b/src/options.rs @@ -1,13 +1,13 @@ use super::client::{ - Client, ClientError, Boosts, Followness, AccountFlag, AccountDetails + AccountDetails, AccountFlag, Boosts, Client, ClientError, Followness, }; use super::coloured_string::ColouredString; +use super::editor::{EditableMenuLine, EditableMenuLineData}; +use super::text::*; use super::tui::{ ActivityState, CursorPosition, LogicalAction, OurKey, OurKey::*, }; -use super::text::*; use super::types::Visibility; -use super::editor::{EditableMenuLine, EditableMenuLineData}; struct YourOptionsMenu { title: FileHeader, @@ -23,7 +23,6 @@ struct YourOptionsMenu { cl_discoverable: CyclingMenuLine, cl_hide_collections: CyclingMenuLine, cl_indexable: CyclingMenuLine, - // fields (harder because potentially open-ended number of them) // note (bio) (harder because flip to an editor) } @@ -38,55 +37,91 @@ impl YourOptionsMenu { let title = FileHeader::new(ColouredString::general( "Your user options [ESC][Y][O]", - "HHHHHHHHHHHHHHHHHHHKKKHHKHHKH")); + "HHHHHHHHHHHHHHHHHHHKKKHHKHHKH", + )); - let normal_status = FileStatusLine::new() - .add(Return, "Back", 10).finalise(); + let normal_status = + FileStatusLine::new().add(Return, "Back", 10).finalise(); let edit_status = FileStatusLine::new() - .message("Edit line and press Return").finalise(); + .message("Edit line and press Return") + .finalise(); let el_display_name = EditableMenuLine::new( - Pr('N'), ColouredString::plain("Display name: "), - ac.display_name.clone()); + Pr('N'), + ColouredString::plain("Display name: "), + ac.display_name.clone(), + ); let cl_default_vis = CyclingMenuLine::new( - Pr('V'), ColouredString::plain("Default post visibility: "), - &Visibility::long_descriptions(), source.privacy); + Pr('V'), + ColouredString::plain("Default post visibility: "), + &Visibility::long_descriptions(), + source.privacy, + ); let el_default_language = EditableMenuLine::new( - Pr('L'), ColouredString::plain("Default language: "), - source.language.clone()); + Pr('L'), + ColouredString::plain("Default language: "), + source.language.clone(), + ); let cl_default_sensitive = CyclingMenuLine::new( Pr('S'), ColouredString::plain("Posts marked sensitive by default: "), - &[(false, ColouredString::plain("no")), - (true, ColouredString::uniform("yes", 'r'))], source.sensitive); + &[ + (false, ColouredString::plain("no")), + (true, ColouredString::uniform("yes", 'r')), + ], + source.sensitive, + ); let cl_locked = CyclingMenuLine::new( Ctrl('K'), ColouredString::plain("Locked (you must approve followers): "), - &[(false, ColouredString::plain("no")), - (true, ColouredString::uniform("yes", 'r'))], ac.locked); + &[ + (false, ColouredString::plain("no")), + (true, ColouredString::uniform("yes", 'r')), + ], + ac.locked, + ); let cl_hide_collections = CyclingMenuLine::new( Ctrl('F'), - ColouredString::plain("Hide your lists of followers and followed users: "), - &[(false, ColouredString::plain("no")), - (true, ColouredString::uniform("yes", 'r'))], - source.hide_collections == Some(true)); + ColouredString::plain( + "Hide your lists of followers and followed users: ", + ), + &[ + (false, ColouredString::plain("no")), + (true, ColouredString::uniform("yes", 'r')), + ], + source.hide_collections == Some(true), + ); let cl_discoverable = CyclingMenuLine::new( Ctrl('D'), - ColouredString::plain("Discoverable (listed in profile directory): "), - &[(false, ColouredString::uniform("no", 'r')), - (true, ColouredString::uniform("yes", 'f'))], - source.discoverable == Some(true)); + ColouredString::plain( + "Discoverable (listed in profile directory): ", + ), + &[ + (false, ColouredString::uniform("no", 'r')), + (true, ColouredString::uniform("yes", 'f')), + ], + source.discoverable == Some(true), + ); let cl_indexable = CyclingMenuLine::new( Ctrl('X'), - ColouredString::plain("Indexable (people can search for your posts): "), - &[(false, ColouredString::uniform("no", 'r')), - (true, ColouredString::uniform("yes", 'f'))], - source.indexable == Some(true)); + ColouredString::plain( + "Indexable (people can search for your posts): ", + ), + &[ + (false, ColouredString::uniform("no", 'r')), + (true, ColouredString::uniform("yes", 'f')), + ], + source.indexable == Some(true), + ); let cl_bot = CyclingMenuLine::new( Ctrl('B'), ColouredString::plain("Bot (account identifies as automated): "), - &[(false, ColouredString::uniform("no", 'f')), - (true, ColouredString::uniform("yes", 'H'))], ac.bot); + &[ + (false, ColouredString::uniform("no", 'f')), + (true, ColouredString::uniform("yes", 'H')), + ], + ac.bot, + ); let mut menu = YourOptionsMenu { title, @@ -109,14 +144,19 @@ impl YourOptionsMenu { fn fix_widths(&mut self) -> (usize, usize) { let mut lmaxwid = 0; let mut rmaxwid = 0; - self.el_display_name.check_widths(&mut lmaxwid, &mut rmaxwid); + self.el_display_name + .check_widths(&mut lmaxwid, &mut rmaxwid); self.cl_default_vis.check_widths(&mut lmaxwid, &mut rmaxwid); - self.el_default_language.check_widths(&mut lmaxwid, &mut rmaxwid); - self.cl_default_sensitive.check_widths(&mut lmaxwid, &mut rmaxwid); + self.el_default_language + .check_widths(&mut lmaxwid, &mut rmaxwid); + self.cl_default_sensitive + .check_widths(&mut lmaxwid, &mut rmaxwid); self.cl_locked.check_widths(&mut lmaxwid, &mut rmaxwid); self.cl_bot.check_widths(&mut lmaxwid, &mut rmaxwid); - self.cl_discoverable.check_widths(&mut lmaxwid, &mut rmaxwid); - self.cl_hide_collections.check_widths(&mut lmaxwid, &mut rmaxwid); + self.cl_discoverable + .check_widths(&mut lmaxwid, &mut rmaxwid); + self.cl_hide_collections + .check_widths(&mut lmaxwid, &mut rmaxwid); self.cl_indexable.check_widths(&mut lmaxwid, &mut rmaxwid); self.el_display_name.reset_widths(); @@ -142,7 +182,6 @@ impl YourOptionsMenu { (lmaxwid, rmaxwid) } - fn submit(&self, client: &mut Client) -> LogicalAction { let details = AccountDetails { display_name: self.el_display_name.get_data().clone(), @@ -164,18 +203,27 @@ impl YourOptionsMenu { } impl ActivityState for YourOptionsMenu { - fn draw(&self, w: usize, h: usize) - -> (Vec, CursorPosition) { + fn draw( + &self, + w: usize, + h: usize, + ) -> (Vec, CursorPosition) { let mut lines = Vec::new(); let mut cursorpos = CursorPosition::End; lines.extend_from_slice(&self.title.render(w)); lines.extend_from_slice(&BlankLine::render_static()); lines.push(self.el_display_name.render( - w, &mut cursorpos, lines.len())); + w, + &mut cursorpos, + lines.len(), + )); lines.extend_from_slice(&BlankLine::render_static()); lines.extend_from_slice(&self.cl_default_vis.render(w)); lines.push(self.el_default_language.render( - w, &mut cursorpos, lines.len())); + w, + &mut cursorpos, + lines.len(), + )); lines.extend_from_slice(&self.cl_default_sensitive.render(w)); lines.extend_from_slice(&BlankLine::render_static()); lines.extend_from_slice(&self.cl_locked.render(w)); @@ -189,8 +237,9 @@ impl ActivityState for YourOptionsMenu { lines.extend_from_slice(&BlankLine::render_static()); } - if self.el_display_name.is_editing() || - self.el_default_language.is_editing(){ + if self.el_display_name.is_editing() + || self.el_default_language.is_editing() + { lines.extend_from_slice(&self.edit_status.render(w)); } else { lines.extend_from_slice(&self.normal_status.render(w)); @@ -199,12 +248,14 @@ impl ActivityState for YourOptionsMenu { (lines, cursorpos) } - fn handle_keypress(&mut self, key: OurKey, client: &mut Client) -> - LogicalAction - { + fn handle_keypress( + &mut self, + key: OurKey, + client: &mut Client, + ) -> LogicalAction { // Let editable menu lines have first crack at the keypress - if self.el_display_name.handle_keypress(key) || - self.el_default_language.handle_keypress(key) + if self.el_display_name.handle_keypress(key) + || self.el_default_language.handle_keypress(key) { self.fix_widths(); return LogicalAction::Nothing; @@ -249,12 +300,17 @@ impl EditableMenuLineData for LanguageVector { } } - fn to_text(&self) -> String { self.0.as_slice().join(",") } + fn to_text(&self) -> String { + self.0.as_slice().join(",") + } fn from_text(text: &str) -> Self { - Self(text.split(|c| c == ' ' || c == ',') - .filter(|s| !s.is_empty()) - .map(|s| s.to_owned()).collect()) + Self( + text.split(|c| c == ' ' || c == ',') + .filter(|s| !s.is_empty()) + .map(|s| s.to_owned()) + .collect(), + ) } } @@ -280,43 +336,70 @@ impl OtherUserOptionsMenu { let rel = client.account_relationship_by_id(id)?; let title = FileHeader::new(ColouredString::uniform( - &format!("Your options for user {name}"), 'H')); + &format!("Your options for user {name}"), + 'H', + )); - let normal_status = FileStatusLine::new() - .add(Return, "Back", 10).finalise(); + let normal_status = + FileStatusLine::new().add(Return, "Back", 10).finalise(); let edit_status = FileStatusLine::new() - .message("Edit line and press Return").finalise(); + .message("Edit line and press Return") + .finalise(); let cl_follow = CyclingMenuLine::new( - Pr('F'), ColouredString::plain("Follow this user: "), - &[(false, ColouredString::plain("no")), - (true, ColouredString::uniform("yes", 'f'))], - rel.following); + Pr('F'), + ColouredString::plain("Follow this user: "), + &[ + (false, ColouredString::plain("no")), + (true, ColouredString::uniform("yes", 'f')), + ], + rel.following, + ); let boosts = if rel.following { - if rel.showing_reblogs { Boosts::Show } else { Boosts::Hide } + if rel.showing_reblogs { + Boosts::Show + } else { + Boosts::Hide + } } else { // Default, if we start off not following the user Boosts::Show }; let cl_boosts = CyclingMenuLine::new( - Pr('B'), ColouredString::plain(" Include their boosts: "), - &[(Boosts::Hide, ColouredString::plain("no")), - (Boosts::Show, ColouredString::uniform("yes", 'f'))], boosts); + Pr('B'), + ColouredString::plain(" Include their boosts: "), + &[ + (Boosts::Hide, ColouredString::plain("no")), + (Boosts::Show, ColouredString::uniform("yes", 'f')), + ], + boosts, + ); let el_languages = EditableMenuLine::new( - Pr('L'), ColouredString::plain(" Include languages: "), - LanguageVector(rel.languages.clone() - .unwrap_or_else(|| Vec::new()))); + Pr('L'), + ColouredString::plain(" Include languages: "), + LanguageVector( + rel.languages.clone().unwrap_or_else(|| Vec::new()), + ), + ); let cl_block = CyclingMenuLine::new( - Ctrl('B'), ColouredString::plain("Block this user: "), - &[(false, ColouredString::plain("no")), - (true, ColouredString::uniform("yes", 'r'))], - rel.blocking); + Ctrl('B'), + ColouredString::plain("Block this user: "), + &[ + (false, ColouredString::plain("no")), + (true, ColouredString::uniform("yes", 'r')), + ], + rel.blocking, + ); // Can't use the obvious ^M because it's also Return, of course! let cl_mute = CyclingMenuLine::new( - Ctrl('U'), ColouredString::plain("Mute this user: "), - &[(false, ColouredString::plain("no")), - (true, ColouredString::uniform("yes", 'r'))], - rel.muting); + Ctrl('U'), + ColouredString::plain("Mute this user: "), + &[ + (false, ColouredString::plain("no")), + (true, ColouredString::uniform("yes", 'r')), + ], + rel.muting, + ); let prev_follow = Followness::from_rel(&rel); @@ -380,8 +463,9 @@ impl OtherUserOptionsMenu { let new_block = self.cl_block.get_value(); if new_block != self.prev_block { - if client.set_account_flag(&self.id, AccountFlag::Block, - new_block).is_err() + if client + .set_account_flag(&self.id, AccountFlag::Block, new_block) + .is_err() { return LogicalAction::Beep; // FIXME: report the error! } @@ -389,8 +473,9 @@ impl OtherUserOptionsMenu { let new_mute = self.cl_mute.get_value(); if new_mute != self.prev_mute { - if client.set_account_flag(&self.id, AccountFlag::Mute, - new_mute).is_err() + if client + .set_account_flag(&self.id, AccountFlag::Mute, new_mute) + .is_err() { return LogicalAction::Beep; // FIXME: report the error! } @@ -401,16 +486,18 @@ impl OtherUserOptionsMenu { } impl ActivityState for OtherUserOptionsMenu { - fn draw(&self, w: usize, h: usize) - -> (Vec, CursorPosition) { + fn draw( + &self, + w: usize, + h: usize, + ) -> (Vec, CursorPosition) { let mut lines = Vec::new(); let mut cursorpos = CursorPosition::End; lines.extend_from_slice(&self.title.render(w)); lines.extend_from_slice(&BlankLine::render_static()); lines.extend_from_slice(&self.cl_follow.render(w)); lines.extend_from_slice(&self.cl_boosts.render(w)); - lines.push(self.el_languages.render( - w, &mut cursorpos, lines.len())); + lines.push(self.el_languages.render(w, &mut cursorpos, lines.len())); lines.extend_from_slice(&BlankLine::render_static()); lines.extend_from_slice(&self.cl_block.render(w)); lines.extend_from_slice(&self.cl_mute.render(w)); @@ -428,12 +515,13 @@ impl ActivityState for OtherUserOptionsMenu { (lines, cursorpos) } - fn handle_keypress(&mut self, key: OurKey, client: &mut Client) -> - LogicalAction - { + fn handle_keypress( + &mut self, + key: OurKey, + client: &mut Client, + ) -> LogicalAction { // Let editable menu lines have first crack at the keypress - if self.el_languages.handle_keypress(key) - { + if self.el_languages.handle_keypress(key) { self.fix_widths(); return LogicalAction::Nothing; } @@ -441,9 +529,9 @@ impl ActivityState for OtherUserOptionsMenu { match key { Space => self.submit(client), Pr('q') | Pr('Q') => LogicalAction::Pop, - Pr('f') | Pr('F') => self.cl_follow.cycle(), - Pr('b') | Pr('B') => self.cl_boosts.cycle(), - Pr('l') | Pr('L') => self.el_languages.start_editing(), + Pr('f') | Pr('F') => self.cl_follow.cycle(), + Pr('b') | Pr('B') => self.cl_boosts.cycle(), + Pr('l') | Pr('L') => self.el_languages.start_editing(), Ctrl('B') => self.cl_block.cycle(), Ctrl('U') => self.cl_mute.cycle(), _ => LogicalAction::Nothing, @@ -455,9 +543,10 @@ impl ActivityState for OtherUserOptionsMenu { } } -pub fn user_options_menu(client: &mut Client, id: &str) - -> Result, ClientError> -{ +pub fn user_options_menu( + client: &mut Client, + id: &str, +) -> Result, ClientError> { if id == client.our_account_id() { Ok(Box::new(YourOptionsMenu::new(client)?)) } else { diff --git a/src/posting.rs b/src/posting.rs index a387938..79ae0d9 100644 --- a/src/posting.rs +++ b/src/posting.rs @@ -3,12 +3,12 @@ use sys_locale::get_locale; use super::client::{Client, ClientError}; use super::coloured_string::ColouredString; +use super::editor::EditableMenuLine; +use super::text::*; use super::tui::{ ActivityState, CursorPosition, LogicalAction, OurKey, OurKey::*, }; -use super::text::*; use super::types::{Account, Visibility}; -use super::editor::EditableMenuLine; #[derive(Debug, PartialEq, Eq, Clone)] pub struct PostMetadata { @@ -25,12 +25,17 @@ pub struct Post { } fn default_language(ac: &Account) -> String { - ac.source.as_ref().and_then(|s| s.language.clone()).unwrap_or_else( - || get_locale().as_deref() - .and_then(|s| s.split('-').next()) - .map(|s| if s.is_empty() { "en" } else { s }) - .unwrap_or("en") - .to_owned()) + ac.source + .as_ref() + .and_then(|s| s.language.clone()) + .unwrap_or_else(|| { + get_locale() + .as_deref() + .and_then(|s| s.split('-').next()) + .map(|s| if s.is_empty() { "en" } else { s }) + .unwrap_or("en") + .to_owned() + }) } impl Post { @@ -38,8 +43,8 @@ impl Post { let ac = client.account_by_id(&client.our_account_id())?; // Take the default visibility from your account settings - let visibility = ac.source.as_ref().map_or( - Visibility::Public, |s| s.privacy); + let visibility = + ac.source.as_ref().map_or(Visibility::Public, |s| s.privacy); // Set the 'sensitive' flag if the account is marked as // 'posts are sensitive by default'. @@ -54,7 +59,11 @@ impl Post { // Korean). So if the user has set that as their defaults, we // sigh, and go with 'sensitive but no message'. let content_warning = ac.source.as_ref().and_then(|s| { - if s.sensitive { Some("".to_owned()) } else { None } + if s.sensitive { + Some("".to_owned()) + } else { + None + } }); Ok(Post { @@ -81,16 +90,17 @@ impl Post { } } - pub fn reply_to(id: &str, client: &mut Client) -> - Result - { + pub fn reply_to( + id: &str, + client: &mut Client, + ) -> Result { let ac = client.account_by_id(&client.our_account_id())?; let st = client.status_by_id(id)?.strip_boost(); let ourself = client.our_account_fq(); let userids = once(client.fq(&st.account.acct)) - .chain(st.mentions.iter().map(|m| client.fq(&m.acct) )) + .chain(st.mentions.iter().map(|m| client.fq(&m.acct))) .filter(|acct| acct != &ourself); let text = userids.map(|acct| format!("@{} ", acct)).collect(); @@ -138,30 +148,47 @@ impl PostMenu { let title = FileHeader::new(ColouredString::uniform(&title, 'H')); let normal_status = FileStatusLine::new() - .message("Select a menu option").finalise(); + .message("Select a menu option") + .finalise(); let edit_status = FileStatusLine::new() - .message("Edit line and press Return").finalise(); + .message("Edit line and press Return") + .finalise(); - let ml_post = MenuKeypressLine::new( - Space, ColouredString::plain("Post")); + let ml_post = + MenuKeypressLine::new(Space, ColouredString::plain("Post")); let ml_cancel = MenuKeypressLine::new( - Pr('Q'), ColouredString::plain("Cancel post")); + Pr('Q'), + ColouredString::plain("Cancel post"), + ); let ml_edit = MenuKeypressLine::new( - Pr('A'), ColouredString::plain("Re-edit post")); + Pr('A'), + ColouredString::plain("Re-edit post"), + ); let cl_vis = CyclingMenuLine::new( - Pr('V'), ColouredString::plain("Visibility: "), - &Visibility::long_descriptions(), post.m.visibility); + Pr('V'), + ColouredString::plain("Visibility: "), + &Visibility::long_descriptions(), + post.m.visibility, + ); let cl_sensitive = CyclingMenuLine::new( - Pr('S'), ColouredString::plain("Mark post as sensitive: "), - &[(false, ColouredString::plain("no")), - (true, ColouredString::uniform("yes", 'r'))], - post.m.content_warning.is_some()); + Pr('S'), + ColouredString::plain("Mark post as sensitive: "), + &[ + (false, ColouredString::plain("no")), + (true, ColouredString::uniform("yes", 'r')), + ], + post.m.content_warning.is_some(), + ); let el_content_warning = EditableMenuLine::new( - Pr('W'), ColouredString::plain(" Content warning: "), - post.m.content_warning.clone()); + Pr('W'), + ColouredString::plain(" Content warning: "), + post.m.content_warning.clone(), + ); let el_language = EditableMenuLine::new( - Pr('L'), ColouredString::plain("Language: "), - post.m.language.clone()); + Pr('L'), + ColouredString::plain("Language: "), + post.m.language.clone(), + ); let mut pm = PostMenu { post, @@ -188,7 +215,8 @@ impl PostMenu { self.ml_edit.check_widths(&mut lmaxwid, &mut rmaxwid); self.cl_vis.check_widths(&mut lmaxwid, &mut rmaxwid); self.cl_sensitive.check_widths(&mut lmaxwid, &mut rmaxwid); - self.el_content_warning.check_widths(&mut lmaxwid, &mut rmaxwid); + self.el_content_warning + .check_widths(&mut lmaxwid, &mut rmaxwid); self.el_language.check_widths(&mut lmaxwid, &mut rmaxwid); self.ml_post.reset_widths(); @@ -213,7 +241,9 @@ impl PostMenu { fn post(&mut self, client: &mut Client) -> LogicalAction { self.post.m.visibility = self.cl_vis.get_value(); self.post.m.content_warning = if self.cl_sensitive.get_value() { - self.el_content_warning.get_data().clone() + self.el_content_warning + .get_data() + .clone() .or_else(|| Some("".to_owned())) } else { None @@ -228,8 +258,11 @@ impl PostMenu { } impl ActivityState for PostMenu { - fn draw(&self, w: usize, h: usize) - -> (Vec, CursorPosition) { + fn draw( + &self, + w: usize, + h: usize, + ) -> (Vec, CursorPosition) { let mut lines = Vec::new(); let mut cursorpos = CursorPosition::End; lines.extend_from_slice(&self.title.render(w)); @@ -241,16 +274,18 @@ impl ActivityState for PostMenu { lines.extend_from_slice(&self.cl_vis.render(w)); lines.extend_from_slice(&self.cl_sensitive.render(w)); lines.push(self.el_content_warning.render( - w, &mut cursorpos, lines.len())); - lines.push(self.el_language.render( - w, &mut cursorpos, lines.len())); + w, + &mut cursorpos, + lines.len(), + )); + lines.push(self.el_language.render(w, &mut cursorpos, lines.len())); while lines.len() + 1 < h { lines.extend_from_slice(&BlankLine::render_static()); } - if self.el_content_warning.is_editing() || - self.el_language.is_editing() + if self.el_content_warning.is_editing() + || self.el_language.is_editing() { lines.extend_from_slice(&self.edit_status.render(w)); } else { @@ -260,12 +295,14 @@ impl ActivityState for PostMenu { (lines, cursorpos) } - fn handle_keypress(&mut self, key: OurKey, client: &mut Client) -> - LogicalAction - { + fn handle_keypress( + &mut self, + key: OurKey, + client: &mut Client, + ) -> LogicalAction { // Let editable menu lines have first crack at the keypress - if self.el_content_warning.handle_keypress(key) || - self.el_language.handle_keypress(key) + if self.el_content_warning.handle_keypress(key) + || self.el_language.handle_keypress(key) { self.fix_widths(); return LogicalAction::Nothing; @@ -274,13 +311,12 @@ impl ActivityState for PostMenu { match key { Space => self.post(client), Pr('q') | Pr('Q') => LogicalAction::Pop, - Pr('a') | Pr('A') => LogicalAction::PostReEdit( - self.post.clone()), + Pr('a') | Pr('A') => LogicalAction::PostReEdit(self.post.clone()), Pr('v') | Pr('V') => self.cl_vis.cycle(), Pr('s') | Pr('S') => { let action = self.cl_sensitive.cycle(); - if self.cl_sensitive.get_value() && - self.el_content_warning.get_data().is_none() + if self.cl_sensitive.get_value() + && self.el_content_warning.get_data().is_none() { // Encourage the user to write a content warning, // by automatically focusing into the content diff --git a/src/scan_re.rs b/src/scan_re.rs index 2b16b14..476787b 100644 --- a/src/scan_re.rs +++ b/src/scan_re.rs @@ -8,7 +8,11 @@ pub struct Findable { } impl Findable { - pub fn get_span(&self, text: &str, start: usize) -> Option<(usize, usize)> { + pub fn get_span( + &self, + text: &str, + start: usize, + ) -> Option<(usize, usize)> { let mut start = start; loop { match self.text.find_at(text, start) { @@ -60,31 +64,60 @@ impl Scan { let username = "(?i:[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?)"; - let mention_bad_pre = Regex::new(&("[=/".to_owned() + word + "]$")) - .unwrap(); + let mention_bad_pre = + Regex::new(&("[=/".to_owned() + word + "]$")).unwrap(); let mention = Regex::new( - &("(?i:@((".to_owned() + username + ")(?:@[" + word + ".-]+[" + - word + "]+)?))")).unwrap(); + &("(?i:@((".to_owned() + + username + + ")(?:@[" + + word + + ".-]+[" + + word + + "]+)?))"), + ) + .unwrap(); let hashtag_separators = "_\u{B7}\u{30FB}\u{200C}"; let word_hash_sep = word.to_owned() + "#" + hashtag_separators; let alpha_hash_sep = alpha.to_owned() + "#" + hashtag_separators; - let hashtag_bad_pre = Regex::new(&("[=/\\)".to_owned() + word + "]$")) - .unwrap(); + let hashtag_bad_pre = + Regex::new(&("[=/\\)".to_owned() + word + "]$")).unwrap(); let hashtag = Regex::new( - &("(?i:#([".to_owned() + word + "_][" + - &word_hash_sep + "]*[" + &alpha_hash_sep + "][" + &word_hash_sep + - "]*[" + word + "_]|([" + word + "_]*[" + alpha + "][" + word + - "_]*)))")).unwrap(); - - let domain_invalid_middle_chars = directional.to_owned() + space + - ctrl + "!\"#$%&\\'()*+,./:;<=>?@\\[\\]^\\`{|}~"; + &("(?i:#([".to_owned() + + word + + "_][" + + &word_hash_sep + + "]*[" + + &alpha_hash_sep + + "][" + + &word_hash_sep + + "]*[" + + word + + "_]|([" + + word + + "_]*[" + + alpha + + "][" + + word + + "_]*)))"), + ) + .unwrap(); + + let domain_invalid_middle_chars = directional.to_owned() + + space + + ctrl + + "!\"#$%&\\'()*+,./:;<=>?@\\[\\]^\\`{|}~"; let domain_invalid_end_chars = domain_invalid_middle_chars.to_owned() + "_-"; - let domain_component = "[^".to_owned() + &domain_invalid_end_chars + - "](?:[^" + &domain_invalid_middle_chars + "]*" + - "[^" + &domain_invalid_end_chars + "])?"; + let domain_component = "[^".to_owned() + + &domain_invalid_end_chars + + "](?:[^" + + &domain_invalid_middle_chars + + "]*" + + "[^" + + &domain_invalid_end_chars + + "])?"; // This is not quite the way the server does it, because the // server has a huge list of valid TLDs! I can't face that. @@ -94,40 +127,62 @@ impl Scan { // composing a toot, to only enter URLs with sensible domains, // otherwise we'll mis-highlight them and get the character // counts wrong. - let domain = domain_component.to_owned() + "(?:\\." + - &domain_component + ")*"; - - let path_end_chars = "a-z".to_owned() + cyrillic + accented + - "0-9=_#/\\+\\-"; - let path_mid_chars = path_end_chars.to_owned() + pd + - "!\\*\\';:\\,\\.\\$\\%\\[\\]~&\\|@"; - - let path_bracketed_once = "\\([".to_owned() + - &path_mid_chars + "]*\\)"; - let path_char_or_bracketed_once = "(?:[".to_owned() + - &path_mid_chars + "]|" + &path_bracketed_once + ")"; - let path_bracketed = "\\(".to_owned() + - &path_char_or_bracketed_once + "*\\)"; - - let path = "(?:[".to_owned() + &path_mid_chars + "]|" + - &path_bracketed + ")*" + "(?:[" + &path_end_chars + "]|" + - &path_bracketed + ")"; + let domain = + domain_component.to_owned() + "(?:\\." + &domain_component + ")*"; + + let path_end_chars = + "a-z".to_owned() + cyrillic + accented + "0-9=_#/\\+\\-"; + let path_mid_chars = path_end_chars.to_owned() + + pd + + "!\\*\\';:\\,\\.\\$\\%\\[\\]~&\\|@"; + + let path_bracketed_once = + "\\([".to_owned() + &path_mid_chars + "]*\\)"; + let path_char_or_bracketed_once = "(?:[".to_owned() + + &path_mid_chars + + "]|" + + &path_bracketed_once + + ")"; + let path_bracketed = + "\\(".to_owned() + &path_char_or_bracketed_once + "*\\)"; + + let path = "(?:[".to_owned() + + &path_mid_chars + + "]|" + + &path_bracketed + + ")*" + + "(?:[" + + &path_end_chars + + "]|" + + &path_bracketed + + ")"; let query_end_chars = "a-z0-9_&=#/\\-"; - let query_mid_chars = query_end_chars.to_owned() + - "!?\\*\\'\\(\\);:\\+\\$%\\[\\]\\.,~|@"; + let query_mid_chars = query_end_chars.to_owned() + + "!?\\*\\'\\(\\);:\\+\\$%\\[\\]\\.,~|@"; let url_bad_pre = Regex::new( - &("[A-Z0-9@$#\u{FF20}\u{FF03}".to_owned() + directional + "]$")) - .unwrap(); + &("[A-Z0-9@$#\u{FF20}\u{FF03}".to_owned() + directional + "]$"), + ) + .unwrap(); let url = Regex::new( - &("(?i:".to_owned() + - "https?://" + - "(?:" + &domain + ")" + - "(?::[0-9]+)?" + - "(?:" + &path + ")*" + - "(?:\\?[" + &query_mid_chars + "]*[" + query_end_chars + "])?" + - ")")).unwrap(); + &("(?i:".to_owned() + + "https?://" + + "(?:" + + &domain + + ")" + + "(?::[0-9]+)?" + + "(?:" + + &path + + ")*" + + "(?:\\?[" + + &query_mid_chars + + "]*[" + + query_end_chars + + "])?" + + ")"), + ) + .unwrap(); #[cfg(test)] let domain = Regex::new(&domain).unwrap(); @@ -153,74 +208,74 @@ impl Scan { } } - pub fn get() -> &'static Self { &SCANNER } + pub fn get() -> &'static Self { + &SCANNER + } } #[test] fn test_mention() { let scan = Scan::get(); - assert_eq!(scan.mention.get_span("hello @user", 0), - Some((6, 11))); - assert_eq!(scan.mention.get_span("hello @user@domain.foo", 0), - Some((6, 22))); + assert_eq!(scan.mention.get_span("hello @user", 0), Some((6, 11))); + assert_eq!( + scan.mention.get_span("hello @user@domain.foo", 0), + Some((6, 22)) + ); assert_eq!(scan.mention.get_span("hello a@user", 0), None); assert_eq!(scan.mention.get_span("hello =@user", 0), None); assert_eq!(scan.mention.get_span("hello /@user", 0), None); - assert_eq!(scan.mention.get_span("hello )@user", 0), - Some((7, 12))); - - assert_eq!(scan.mention.get_span("hello @user.name", 0), - Some((6, 16))); - assert_eq!(scan.mention.get_span("hello @user.name.", 0), - Some((6, 16))); - assert_eq!(scan.mention.get_span("hello @user-name", 0), - Some((6, 16))); - assert_eq!(scan.mention.get_span("hello @user-name-", 0), - Some((6, 16))); + assert_eq!(scan.mention.get_span("hello )@user", 0), Some((7, 12))); + + assert_eq!(scan.mention.get_span("hello @user.name", 0), Some((6, 16))); + assert_eq!(scan.mention.get_span("hello @user.name.", 0), Some((6, 16))); + assert_eq!(scan.mention.get_span("hello @user-name", 0), Some((6, 16))); + assert_eq!(scan.mention.get_span("hello @user-name-", 0), Some((6, 16))); } #[test] fn test_hashtag() { let scan = Scan::get(); - assert_eq!(scan.hashtag.get_span("some #text here", 0), - Some((5, 10))); + assert_eq!(scan.hashtag.get_span("some #text here", 0), Some((5, 10))); assert_eq!(scan.hashtag.get_span("some # here", 0), None); - assert_eq!(scan.hashtag.get_span("some #__a__ here", 0), - Some((5, 11))); - assert_eq!(scan.hashtag.get_span("some #_____ here", 0), - Some((5, 11))); - assert_eq!(scan.hashtag.get_span("some #_0_0_ here", 0), - Some((5, 11))); + assert_eq!(scan.hashtag.get_span("some #__a__ here", 0), Some((5, 11))); + assert_eq!(scan.hashtag.get_span("some #_____ here", 0), Some((5, 11))); + assert_eq!(scan.hashtag.get_span("some #_0_0_ here", 0), Some((5, 11))); assert_eq!(scan.hashtag.get_span("some a#text here", 0), None); assert_eq!(scan.hashtag.get_span("some )#text here", 0), None); - assert_eq!(scan.hashtag.get_span("some (#text here", 0), - Some((6,11))); + assert_eq!(scan.hashtag.get_span("some (#text here", 0), Some((6, 11))); } #[test] fn test_domain() { let scan = Scan::get(); - assert_eq!(scan.domain.get_span("foo.bar.baz", 0), - Some((0, 11))); - assert_eq!(scan.domain.get_span("foo.bar.baz.", 0), - Some((0, 11))); - assert_eq!(scan.domain.get_span("foo.b-r.baz", 0), - Some((0, 11))); - assert_eq!(scan.domain.get_span("foo.-br.baz", 0), - Some((0, 3))); - assert_eq!(scan.domain.get_span("foo.br-.baz", 0), - Some((0, 6))); // matches foo.br + assert_eq!(scan.domain.get_span("foo.bar.baz", 0), Some((0, 11))); + assert_eq!(scan.domain.get_span("foo.bar.baz.", 0), Some((0, 11))); + assert_eq!(scan.domain.get_span("foo.b-r.baz", 0), Some((0, 11))); + assert_eq!(scan.domain.get_span("foo.-br.baz", 0), Some((0, 3))); + assert_eq!(scan.domain.get_span("foo.br-.baz", 0), Some((0, 6))); // matches foo.br } #[test] fn test_url() { let scan = Scan::get(); - assert_eq!(scan.url.get_span("Look at https://example.com.", 0), - Some((8, 27))); - assert_eq!(scan.url.get_span("Or https://en.wikipedia.org/wiki/Panda_(disambiguation).", 0), - Some((3, 55))); - assert_eq!(scan.url.get_span("Or https://example.com/music/Track_(Thing_(Edited)).", 0), - Some((3, 51))); + assert_eq!( + scan.url.get_span("Look at https://example.com.", 0), + Some((8, 27)) + ); + assert_eq!( + scan.url.get_span( + "Or https://en.wikipedia.org/wiki/Panda_(disambiguation).", + 0 + ), + Some((3, 55)) + ); + assert_eq!( + scan.url.get_span( + "Or https://example.com/music/Track_(Thing_(Edited)).", + 0 + ), + Some((3, 51)) + ); } diff --git a/src/text.rs b/src/text.rs index 4c1f8ba..44dd7b9 100644 --- a/src/text.rs +++ b/src/text.rs @@ -1,15 +1,15 @@ -use chrono::{DateTime, Local, Utc}; #[cfg(test)] use chrono::NaiveDateTime; -use core::cmp::{min, max}; +use chrono::{DateTime, Local, Utc}; +use core::cmp::{max, min}; use std::collections::{BTreeMap, HashSet}; use unicode_width::UnicodeWidthStr; -use super::html; use super::client::{Client, ClientError}; -use super::types::*; -use super::tui::{OurKey, LogicalAction}; use super::coloured_string::*; +use super::html; +use super::tui::{LogicalAction, OurKey}; +use super::types::*; #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum HighlightType { @@ -54,8 +54,12 @@ pub trait DisplayStyleGetter { } pub struct DefaultDisplayStyle; impl DisplayStyleGetter for DefaultDisplayStyle { - fn poll_options(&self, _id: &str) -> Option> { None } - fn unfolded(&self, _id: &str) -> bool { true } + fn poll_options(&self, _id: &str) -> Option> { + None + } + fn unfolded(&self, _id: &str) -> bool { + true + } } pub trait TextFragment { @@ -63,51 +67,63 @@ pub trait TextFragment { self.render_highlighted(width, None, &DefaultDisplayStyle) } - fn can_highlight(_htype: HighlightType) -> bool where Self : Sized { + fn can_highlight(_htype: HighlightType) -> bool + where + Self: Sized, + { false } - fn count_highlightables(&self, _htype: HighlightType) -> usize { 0 } + fn count_highlightables(&self, _htype: HighlightType) -> usize { + 0 + } fn highlighted_id(&self, _highlight: Option) -> Option { None } - fn render_highlighted(&self, width: usize, highlight: Option, - style: &dyn DisplayStyleGetter) - -> Vec; + fn render_highlighted( + &self, + width: usize, + highlight: Option, + style: &dyn DisplayStyleGetter, + ) -> Vec; - fn is_multiple_choice_poll(&self) -> bool { false } + fn is_multiple_choice_poll(&self) -> bool { + false + } fn render_highlighted_update( - &self, width: usize, highlight: &mut Option, - style: &dyn DisplayStyleGetter) -> Vec - { + &self, + width: usize, + highlight: &mut Option, + style: &dyn DisplayStyleGetter, + ) -> Vec { let (new_highlight, text) = match *highlight { Some(Highlight(htype, index)) => { let count = self.count_highlightables(htype); if index < count { (None, self.render_highlighted(width, *highlight, style)) } else { - (Some(Highlight(htype, index - count)), - self.render_highlighted(width, None, style)) + ( + Some(Highlight(htype, index - count)), + self.render_highlighted(width, None, style), + ) } } - None => { - (None, self.render_highlighted(width, None, style)) - } + None => (None, self.render_highlighted(width, None, style)), }; *highlight = new_highlight; text } fn highlighted_id_update( - &self, highlight: &mut Option) -> Option - { + &self, + highlight: &mut Option, + ) -> Option { let (answer, new_highlight) = match *highlight { Some(Highlight(htype, index)) => { let count = self.count_highlightables(htype); if index < count { - (self.highlighted_id(Some(Highlight(htype, index))), - None) + (self.highlighted_id(Some(Highlight(htype, index))), None) } else { (None, Some(Highlight(htype, index - count))) } @@ -121,12 +137,19 @@ pub trait TextFragment { pub trait TextFragmentOneLine { // A more specific trait for fragments always producing exactly one line - fn render_oneline(&self, width: usize, _highlight: Option, - _style: &dyn DisplayStyleGetter) -> ColouredString; + fn render_oneline( + &self, + width: usize, + _highlight: Option, + _style: &dyn DisplayStyleGetter, + ) -> ColouredString; } impl TextFragment for Option { - fn can_highlight(htype: HighlightType) -> bool where Self : Sized { + fn can_highlight(htype: HighlightType) -> bool + where + Self: Sized, + { T::can_highlight(htype) } @@ -136,12 +159,16 @@ impl TextFragment for Option { None => 0, } } - fn render_highlighted(&self, width: usize, highlight: Option, - style: &dyn DisplayStyleGetter) - -> Vec { + fn render_highlighted( + &self, + width: usize, + highlight: Option, + style: &dyn DisplayStyleGetter, + ) -> Vec { match self { - Some(ref inner) => inner.render_highlighted( - width, highlight, style), + Some(ref inner) => { + inner.render_highlighted(width, highlight, style) + } None => Vec::new(), } } @@ -154,25 +181,32 @@ impl TextFragment for Option { } impl TextFragment for Vec { - fn can_highlight(htype: HighlightType) -> bool where Self : Sized { + fn can_highlight(htype: HighlightType) -> bool + where + Self: Sized, + { T::can_highlight(htype) } fn count_highlightables(&self, htype: HighlightType) -> usize { self.iter().map(|x| x.count_highlightables(htype)).sum() } - fn render_highlighted(&self, width: usize, highlight: Option, - style: &dyn DisplayStyleGetter) - -> Vec { + fn render_highlighted( + &self, + width: usize, + highlight: Option, + style: &dyn DisplayStyleGetter, + ) -> Vec { let mut highlight = highlight; - itertools::concat(self.iter().map( - |x| x.render_highlighted_update(width, &mut highlight, style))) + itertools::concat(self.iter().map(|x| { + x.render_highlighted_update(width, &mut highlight, style) + })) } fn highlighted_id(&self, highlight: Option) -> Option { let mut highlight = highlight; for item in self { - if let result @ Some(..) = item.highlighted_id_update( - &mut highlight) + if let result @ Some(..) = + item.highlighted_id_update(&mut highlight) { return result; } @@ -185,30 +219,33 @@ pub struct BlankLine {} impl BlankLine { pub fn new() -> Self { - BlankLine{} + BlankLine {} } pub fn render_static() -> Vec { - vec! { - ColouredString::plain(""), - } + vec![ColouredString::plain("")] } } impl TextFragment for BlankLine { - fn render_highlighted(&self, _width: usize, _highlight: Option, - _style: &dyn DisplayStyleGetter) - -> Vec - { + fn render_highlighted( + &self, + _width: usize, + _highlight: Option, + _style: &dyn DisplayStyleGetter, + ) -> Vec { Self::render_static() } } #[test] fn test_blank() { - assert_eq!(BlankLine::new().render(40), vec! { + assert_eq!( + BlankLine::new().render(40), + vec! { ColouredString::plain("") - }); + } + ); } pub struct SeparatorLine { @@ -222,7 +259,7 @@ impl SeparatorLine { timestamp: Option>, favourited: bool, boosted: bool, - ) -> Self { + ) -> Self { SeparatorLine { timestamp, favourited, @@ -232,86 +269,105 @@ impl SeparatorLine { } fn format_date(date: DateTime) -> String { - date.with_timezone(&Local).format("%a %b %e %H:%M:%S %Y").to_string() + date.with_timezone(&Local) + .format("%a %b %e %H:%M:%S %Y") + .to_string() } impl TextFragment for SeparatorLine { - fn render_highlighted(&self, width: usize, _highlight: Option, - _style: &dyn DisplayStyleGetter) - -> Vec - { + fn render_highlighted( + &self, + width: usize, + _highlight: Option, + _style: &dyn DisplayStyleGetter, + ) -> Vec { let mut suffix = ColouredString::plain(""); let display_pre = ColouredString::uniform("[", 'S'); let display_post = ColouredString::uniform("]--", 'S'); if let Some(date) = self.timestamp { let datestr = format_date(date); - suffix = &display_pre + - ColouredString::uniform(&datestr, 'D') + - &display_post + suffix; + suffix = &display_pre + + ColouredString::uniform(&datestr, 'D') + + &display_post + + suffix; } if self.boosted { - suffix = &display_pre + ColouredString::uniform("B", 'D') + - &display_post + suffix; + suffix = &display_pre + + ColouredString::uniform("B", 'D') + + &display_post + + suffix; } if self.favourited { - suffix = &display_pre + ColouredString::uniform("F", 'D') + - &display_post + suffix; + suffix = &display_pre + + ColouredString::uniform("F", 'D') + + &display_post + + suffix; } let w = suffix.width(); if w < width - 1 { - suffix = ColouredString::uniform("-", 'S').repeat(width - 1 - w) + - suffix; - } - vec! { - suffix.truncate(width).into() + suffix = ColouredString::uniform("-", 'S').repeat(width - 1 - w) + + suffix; } + vec![suffix.truncate(width).into()] } } #[test] fn test_separator() { - let t = NaiveDateTime::parse_from_str("2001-08-03 04:05:06", - "%Y-%m-%d %H:%M:%S") - .unwrap().and_local_timezone(Local).unwrap().with_timezone(&Utc); - assert_eq!(SeparatorLine::new(Some(t), true, false) - .render(60), vec! { + let t = NaiveDateTime::parse_from_str( + "2001-08-03 04:05:06", + "%Y-%m-%d %H:%M:%S", + ) + .unwrap() + .and_local_timezone(Local) + .unwrap() + .with_timezone(&Utc); + assert_eq!( + SeparatorLine::new(Some(t), true, false).render(60), + vec! { ColouredString::general( "--------------------------[F]--[Fri Aug 3 04:05:06 2001]--", "SSSSSSSSSSSSSSSSSSSSSSSSSSSDSSSSDDDDDDDDDDDDDDDDDDDDDDDDSSS", ) - }); + } + ); } pub struct EditorHeaderSeparator {} impl EditorHeaderSeparator { pub fn new() -> Self { - EditorHeaderSeparator{} + EditorHeaderSeparator {} } } impl TextFragment for EditorHeaderSeparator { - fn render_highlighted(&self, width: usize, _highlight: Option, - _style: &dyn DisplayStyleGetter) - -> Vec - { - vec! { - ColouredString::uniform( - &("-".repeat(width - min(2, width)) + "|"), - '-', - ).truncate(width).into(), - } + fn render_highlighted( + &self, + width: usize, + _highlight: Option, + _style: &dyn DisplayStyleGetter, + ) -> Vec { + vec![ColouredString::uniform( + &("-".repeat(width - min(2, width)) + "|"), + '-', + ) + .truncate(width) + .into()] } } #[test] fn test_editorsep() { - assert_eq!(EditorHeaderSeparator::new().render(5), vec! { + assert_eq!( + EditorHeaderSeparator::new().render(5), + vec! { ColouredString::general( "---|", "----", ) - }); + } + ); } pub struct UsernameHeader { @@ -324,7 +380,7 @@ pub struct UsernameHeader { impl UsernameHeader { pub fn from(account: &str, nameline: &str, id: &str) -> Self { - UsernameHeader{ + UsernameHeader { header: "From".to_owned(), colour: 'F', account: account.to_owned(), @@ -334,7 +390,7 @@ impl UsernameHeader { } pub fn via(account: &str, nameline: &str, id: &str) -> Self { - UsernameHeader{ + UsernameHeader { header: "Via".to_owned(), colour: 'f', account: account.to_owned(), @@ -345,23 +401,28 @@ impl UsernameHeader { } impl TextFragment for UsernameHeader { - fn render_highlighted(&self, _width: usize, highlight: Option, - _style: &dyn DisplayStyleGetter) - -> Vec - { + fn render_highlighted( + &self, + _width: usize, + highlight: Option, + _style: &dyn DisplayStyleGetter, + ) -> Vec { let header = ColouredString::plain(&format!("{}: ", self.header)); let colour = match highlight { Some(Highlight(HighlightType::User, 0)) => '*', _ => self.colour, }; let body = ColouredString::uniform( - &format!("{} ({})", self.nameline, self.account), colour); - vec! { - header + body, - } + &format!("{} ({})", self.nameline, self.account), + colour, + ); + vec![header + body] } - fn can_highlight(htype: HighlightType) -> bool where Self : Sized { + fn can_highlight(htype: HighlightType) -> bool + where + Self: Sized, + { htype == HighlightType::User } @@ -382,20 +443,26 @@ impl TextFragment for UsernameHeader { #[test] fn test_userheader() { - assert_eq!(UsernameHeader::from("stoat@example.com", "Some Person", "123") - .render(80), vec! { + assert_eq!( + UsernameHeader::from("stoat@example.com", "Some Person", "123") + .render(80), + vec! { ColouredString::general( "From: Some Person (stoat@example.com)", " FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", ) - }); - assert_eq!(UsernameHeader::via("stoat@example.com", "Some Person", "123") - .render(80), vec! { + } + ); + assert_eq!( + UsernameHeader::via("stoat@example.com", "Some Person", "123") + .render(80), + vec! { ColouredString::general( "Via: Some Person (stoat@example.com)", " fffffffffffffffffffffffffffffff", ) - }); + } + ); } #[derive(PartialEq, Eq, Debug)] @@ -445,15 +512,21 @@ impl Paragraph { self } - pub fn set_indent(mut self, firstindent: usize, - laterindent: usize) -> Self { + pub fn set_indent( + mut self, + firstindent: usize, + laterindent: usize, + ) -> Self { self.firstindent = firstindent; self.laterindent = laterindent; self } - pub fn push_text(&mut self, text: impl ColouredStringCommon, - squash_spaces: bool) { + pub fn push_text( + &mut self, + text: impl ColouredStringCommon, + squash_spaces: bool, + ) { for ch in text.chars() { if let Some(curr_word) = self.words.last_mut() { let is_space = ch.is_space(); @@ -494,10 +567,12 @@ impl Paragraph { } pub fn delete_mention_words_from(&mut self, start: usize) { - let first_non_mention = start + self.words[start..].iter() - .position(|word| !(word.is_space() || word.is_colour('@'))) - .unwrap_or(self.words.len() - start); - self.words.splice(start..first_non_mention, vec!{}); + let first_non_mention = start + + self.words[start..] + .iter() + .position(|word| !(word.is_space() || word.is_colour('@'))) + .unwrap_or(self.words.len() - start); + self.words.splice(start..first_non_mention, vec![]); } pub fn is_empty(&self) -> bool { @@ -510,26 +585,36 @@ impl Paragraph { #[test] fn test_para_build() { - assert_eq!(Paragraph::new(), Paragraph { + assert_eq!( + Paragraph::new(), + Paragraph { words: Vec::new(), firstindent: 0, laterindent: 0, wrap: true, - }); - assert_eq!(Paragraph::new().set_wrap(false), Paragraph { + } + ); + assert_eq!( + Paragraph::new().set_wrap(false), + Paragraph { words: Vec::new(), firstindent: 0, laterindent: 0, wrap: false, - }); - assert_eq!(Paragraph::new().set_indent(3, 4), Paragraph { + } + ); + assert_eq!( + Paragraph::new().set_indent(3, 4), + Paragraph { words: Vec::new(), firstindent: 3, laterindent: 4, wrap: true, - }); - assert_eq!(Paragraph::new().add(ColouredString::plain("foo bar baz")), - Paragraph { + } + ); + assert_eq!( + Paragraph::new().add(ColouredString::plain("foo bar baz")), + Paragraph { words: vec! { ColouredString::plain("foo"), ColouredString::plain(" "), @@ -540,11 +625,13 @@ fn test_para_build() { firstindent: 0, laterindent: 0, wrap: true, - }); - assert_eq!(Paragraph::new() - .add(ColouredString::plain("foo ba")) - .add(ColouredString::plain("r baz")), - Paragraph { + } + ); + assert_eq!( + Paragraph::new() + .add(ColouredString::plain("foo ba")) + .add(ColouredString::plain("r baz")), + Paragraph { words: vec! { ColouredString::plain("foo"), ColouredString::plain(" "), @@ -555,10 +642,14 @@ fn test_para_build() { firstindent: 0, laterindent: 0, wrap: true, - }); - assert_eq!(Paragraph::new().add(ColouredString::general( - " foo bar baz ", "abcdefghijklmnopq")), - Paragraph { + } + ); + assert_eq!( + Paragraph::new().add(ColouredString::general( + " foo bar baz ", + "abcdefghijklmnopq" + )), + Paragraph { words: vec! { ColouredString::general(" ", "ab"), ColouredString::general("foo", "cde"), @@ -571,14 +662,17 @@ fn test_para_build() { firstindent: 0, laterindent: 0, wrap: true, - }); + } + ); } impl TextFragment for Paragraph { - fn render_highlighted(&self, width: usize, _highlight: Option, - _style: &dyn DisplayStyleGetter) - -> Vec - { + fn render_highlighted( + &self, + width: usize, + _highlight: Option, + _style: &dyn DisplayStyleGetter, + ) -> Vec { let mut lines = Vec::new(); let mut curr_width = 0; let mut curr_pos; @@ -594,10 +688,12 @@ impl TextFragment for Paragraph { curr_pos = i + 1; if !word.is_space() { - if self.wrap && break_pos > start_pos && - curr_width - start_width + curr_indent >= width { - let mut line = ColouredString::plain(" ") - .repeat(curr_indent); + if self.wrap + && break_pos > start_pos + && curr_width - start_width + curr_indent >= width + { + let mut line = + ColouredString::plain(" ").repeat(curr_indent); for i in start_pos..break_pos { line.push_str(&self.words[i]); } @@ -629,44 +725,66 @@ impl TextFragment for Paragraph { #[test] fn test_para_wrap() { let p = Paragraph::new().add(ColouredString::plain( - "the quick brown fox jumps over the lazy dog")); - assert_eq!(p.render(16), vec! { + "the quick brown fox jumps over the lazy dog", + )); + assert_eq!( + p.render(16), + vec! { ColouredString::plain("the quick brown"), ColouredString::plain("fox jumps over"), ColouredString::plain("the lazy dog"), - }); + } + ); let p = Paragraph::new().add(ColouredString::plain( - " one supercalifragilisticexpialidocious word")); - assert_eq!(p.render(15), vec! { + " one supercalifragilisticexpialidocious word", + )); + assert_eq!( + p.render(15), + vec! { ColouredString::plain(" one"), ColouredString::plain("supercalifragilisticexpialidocious"), ColouredString::plain("word"), - }); + } + ); let p = Paragraph::new().add(ColouredString::plain( - " supercalifragilisticexpialidocious word")); - assert_eq!(p.render(15), vec! { + " supercalifragilisticexpialidocious word", + )); + assert_eq!( + p.render(15), + vec! { ColouredString::plain(" supercalifragilisticexpialidocious"), ColouredString::plain("word"), - }); + } + ); - let p = Paragraph::new().add(ColouredString::plain( - "the quick brown fox jumps over the lazy dog")) + let p = Paragraph::new() + .add(ColouredString::plain( + "the quick brown fox jumps over the lazy dog", + )) .set_wrap(false); - assert_eq!(p.render(15), vec! { + assert_eq!( + p.render(15), + vec! { ColouredString::plain("the quick brown fox jumps over the lazy dog"), - }); + } + ); - let p = Paragraph::new().add(ColouredString::plain( - "the quick brown fox jumps over the lazy dog")) + let p = Paragraph::new() + .add(ColouredString::plain( + "the quick brown fox jumps over the lazy dog", + )) .set_indent(4, 2); - assert_eq!(p.render(15), vec! { + assert_eq!( + p.render(15), + vec! { ColouredString::plain(" the quick"), ColouredString::plain(" brown fox"), ColouredString::plain(" jumps over"), ColouredString::plain(" the lazy dog"), - }); + } + ); } pub struct FileHeader { @@ -675,35 +793,38 @@ pub struct FileHeader { impl FileHeader { pub fn new(text: ColouredString) -> Self { - FileHeader{ - text, - } + FileHeader { text } } } impl TextFragment for FileHeader { - fn render_highlighted(&self, width: usize, _highlight: Option, - _style: &dyn DisplayStyleGetter) - -> Vec - { + fn render_highlighted( + &self, + width: usize, + _highlight: Option, + _style: &dyn DisplayStyleGetter, + ) -> Vec { let elephants = width >= 16; let twidth = if elephants { width - 9 } else { width - 1 }; let title = self.text.truncate(twidth); let tspace = twidth - title.width(); let tleft = tspace / 2; let tright = tspace - tleft; - let titlepad = ColouredString::plain(" ").repeat(tleft) + - title + ColouredString::plain(" ").repeat(tright); + let titlepad = ColouredString::plain(" ").repeat(tleft) + + title + + ColouredString::plain(" ").repeat(tright); let underline = ColouredString::uniform("~", '~').repeat(twidth); if elephants { - vec! { - (ColouredString::general("(o) ", "JJJ ") + titlepad + - ColouredString::general(" (o)", " JJJ")), - (ColouredString::general("/J\\ ", "JJJ ") + underline + - ColouredString::general(" /J\\", " JJJ")), - } + vec![ + (ColouredString::general("(o) ", "JJJ ") + + titlepad + + ColouredString::general(" (o)", " JJJ")), + (ColouredString::general("/J\\ ", "JJJ ") + + underline + + ColouredString::general(" /J\\", " JJJ")), + ] } else { - vec! { titlepad, underline } + vec![titlepad, underline] } } } @@ -711,47 +832,57 @@ impl TextFragment for FileHeader { #[test] fn test_fileheader() { let fh = FileHeader::new(ColouredString::uniform("hello, world", 'H')); - assert_eq!(fh.render(40), - vec! { + assert_eq!( + fh.render(40), + vec! { ColouredString::general("(o) hello, world (o)", "JJJ HHHHHHHHHHHH JJJ"), ColouredString::general("/J\\ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /J\\", "JJJ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JJJ"), - }); + } + ); - assert_eq!(fh.render(21), - vec! { + assert_eq!( + fh.render(21), + vec! { ColouredString::general("(o) hello, world (o)", "JJJ HHHHHHHHHHHH JJJ"), ColouredString::general("/J\\ ~~~~~~~~~~~~ /J\\", "JJJ ~~~~~~~~~~~~ JJJ"), - }); + } + ); - assert_eq!(fh.render(20), - vec! { + assert_eq!( + fh.render(20), + vec! { ColouredString::general("(o) hello, worl (o)", "JJJ HHHHHHHHHHH JJJ"), ColouredString::general("/J\\ ~~~~~~~~~~~ /J\\", "JJJ ~~~~~~~~~~~ JJJ"), - }); + } + ); - assert_eq!(fh.render(10), - vec! { + assert_eq!( + fh.render(10), + vec! { ColouredString::general("hello, wo", "HHHHHHHHH"), ColouredString::general("~~~~~~~~~", "~~~~~~~~~"), - }); + } + ); } fn trim_para_list(paras: &mut Vec) { - let first_nonempty = paras.iter() - .position(|p| !p.is_empty()).unwrap_or(paras.len()); - paras.splice(..first_nonempty, vec!{}); + let first_nonempty = paras + .iter() + .position(|p| !p.is_empty()) + .unwrap_or(paras.len()); + paras.splice(..first_nonempty, vec![]); while match paras.last() { Some(p) => p.is_empty(), - None => false + None => false, } { paras.pop(); } @@ -784,9 +915,11 @@ impl Html { para } - pub fn render_indented(&self, width: usize, indent: usize) -> - Vec - { + pub fn render_indented( + &self, + width: usize, + indent: usize, + ) -> Vec { let prefix = ColouredString::plain(" ").repeat(indent); self.render(width.saturating_sub(indent)) .into_iter() @@ -803,15 +936,15 @@ impl Html { } impl TextFragment for Html { - fn render_highlighted(&self, width: usize, _highlight: Option, - _style: &dyn DisplayStyleGetter) - -> Vec - { + fn render_highlighted( + &self, + width: usize, + _highlight: Option, + _style: &dyn DisplayStyleGetter, + ) -> Vec { match self { Html::Rt(ref rt) => html::render(rt, width - min(width, 1)), - Html::Bad(e) => vec! { - ColouredString::uniform(e, '!'), - } + Html::Bad(e) => vec![ColouredString::uniform(e, '!')], } } } @@ -823,23 +956,29 @@ fn render_html(html: &str, width: usize) -> Vec { #[test] fn test_html() { - assert_eq!(render_html("

Testing, testing, 1, 2, 3

", 50), - vec! { + assert_eq!( + render_html("

Testing, testing, 1, 2, 3

", 50), + vec! { ColouredString::plain("Testing, testing, 1, 2, 3"), - }); + } + ); - assert_eq!(render_html("

First para

Second para

", 50), - vec! { + assert_eq!( + render_html("

First para

Second para

", 50), + vec! { ColouredString::plain("First para"), ColouredString::plain(""), ColouredString::plain("Second para"), - }); + } + ); - assert_eq!(render_html("

First line
Second line

", 50), - vec! { + assert_eq!( + render_html("

First line
Second line

", 50), + vec! { ColouredString::plain("First line"), ColouredString::plain("Second line"), - }); + } + ); assert_eq!(render_html("

Pease porridge hot, pease porridge cold, pease porridge in the pot, nine days old

", 50), vec! { @@ -847,17 +986,21 @@ fn test_html() { ColouredString::plain("porridge in the pot, nine days old"), }); - assert_eq!(render_html("

Test of some literal code

", 50), - vec! { + assert_eq!( + render_html("

Test of some literal code

", 50), + vec! { ColouredString::general("Test of some literal code", " cccccccccccc"), - }); + } + ); - assert_eq!(render_html("

Test of some strong text

", 50), - vec! { + assert_eq!( + render_html("

Test of some strong text

", 50), + vec! { ColouredString::general("Test of some strong text", " sssssssssss"), - }); + } + ); assert_eq!(render_html("

Test of a #hashtag

", 50), vec! { @@ -890,37 +1033,37 @@ pub struct ExtendableIndicator { impl ExtendableIndicator { pub fn new() -> Self { - ExtendableIndicator{ - primed: false - } + ExtendableIndicator { primed: false } } - pub fn set_primed(&mut self, primed: bool) { self.primed = primed; } + pub fn set_primed(&mut self, primed: bool) { + self.primed = primed; + } } impl TextFragment for ExtendableIndicator { - fn render_highlighted(&self, width: usize, _highlight: Option, - _style: &dyn DisplayStyleGetter) - -> Vec - { + fn render_highlighted( + &self, + width: usize, + _highlight: Option, + _style: &dyn DisplayStyleGetter, + ) -> Vec { let message = if self.primed { ColouredString::general( "Press [0] to extend", - "HHHHHHHKHHHHHHHHHHH") + "HHHHHHHKHHHHHHHHHHH", + ) } else { ColouredString::general( "Press [0] twice to extend", - "HHHHHHHKHHHHHHHHHHHHHHHHH") + "HHHHHHHKHHHHHHHHHHHHHHHHH", + ) }; let msgtrunc = message.truncate(width); let space = width - min(msgtrunc.width() + 1, width); let left = space / 2; let msgpad = ColouredString::plain(" ").repeat(left) + msgtrunc; - vec! { - ColouredString::plain(""), - msgpad, - ColouredString::plain(""), - } + vec![ColouredString::plain(""), msgpad, ColouredString::plain("")] } } @@ -928,23 +1071,29 @@ impl TextFragment for ExtendableIndicator { fn test_extendable() { let mut ei = ExtendableIndicator::new(); - assert_eq!(ei.render(40), vec! { + assert_eq!( + ei.render(40), + vec! { ColouredString::plain(""), ColouredString::general( " Press [0] twice to extend", " HHHHHHHKHHHHHHHHHHHHHHHHH"), ColouredString::plain(""), - }); + } + ); ei.set_primed(true); - assert_eq!(ei.render(40), vec! { + assert_eq!( + ei.render(40), + vec! { ColouredString::plain(""), ColouredString::general( " Press [0] to extend", " HHHHHHHKHHHHHHHHHHH"), ColouredString::plain(""), - }); + } + ); } pub struct InReplyToLine { @@ -979,18 +1128,19 @@ impl InReplyToLine { let st = client.status_by_id(id); let parent_text = match &st { Ok(st) => Html::new(&st.content).to_para(), - Err(e) => Paragraph::new().add(ColouredString::plain( - &format!("[unavailable: {}]", e) - )), + Err(e) => Paragraph::new() + .add(ColouredString::plain(&format!("[unavailable: {}]", e))), }; let warning = match &st { - Ok(st) => if st.sensitive { - Some(st.spoiler_text.clone()) - } else { - None + Ok(st) => { + if st.sensitive { + Some(st.spoiler_text.clone()) + } else { + None + } } - Err(..) => None + Err(..) => None, }; Self::new(parent_text, warning, id) @@ -998,18 +1148,22 @@ impl InReplyToLine { } impl TextFragment for InReplyToLine { - fn render_highlighted(&self, width: usize, highlight: Option, - style: &dyn DisplayStyleGetter) - -> Vec - { + fn render_highlighted( + &self, + width: usize, + highlight: Option, + style: &dyn DisplayStyleGetter, + ) -> Vec { let mut highlight = highlight; let highlighting = highlight.consume(HighlightType::FoldableStatus, 1) == Some(0); let which_para = match self.warning.as_ref() { - Some(folded) => if !style.unfolded(&self.id) { - folded - } else { - &self.para + Some(folded) => { + if !style.unfolded(&self.id) { + folded + } else { + &self.para + } } None => &self.para, }; @@ -1029,10 +1183,13 @@ impl TextFragment for InReplyToLine { } else { result.into() }; - vec! { result } + vec![result] } - fn can_highlight(htype: HighlightType) -> bool where Self : Sized { + fn can_highlight(htype: HighlightType) -> bool + where + Self: Sized, + { htype == HighlightType::FoldableStatus } @@ -1069,21 +1226,30 @@ fn test_in_reply_to() { "

@stoat @weasel take a look at this otter!

@badger might also like it

"); let irt = InReplyToLine::new(post.to_para(), None, "123"); - assert_eq!(irt.render(48), vec!{ - ColouredString::general( - "Re: take a look at this otter! @badger might...", - " @@@@@@@ "), - }); - assert_eq!(irt.render(47), vec!{ - ColouredString::general( - "Re: take a look at this otter! @badger...", - " @@@@@@@ "), - }); - assert_eq!(irt.render(80), vec!{ - ColouredString::general( - "Re: take a look at this otter! @badger might also like it", - " @@@@@@@ "), - }); + assert_eq!( + irt.render(48), + vec! { + ColouredString::general( + "Re: take a look at this otter! @badger might...", + " @@@@@@@ "), + } + ); + assert_eq!( + irt.render(47), + vec! { + ColouredString::general( + "Re: take a look at this otter! @badger...", + " @@@@@@@ "), + } + ); + assert_eq!( + irt.render(80), + vec! { + ColouredString::general( + "Re: take a look at this otter! @badger might also like it", + " @@@@@@@ "), + } + ); } pub struct VisibilityLine { @@ -1091,28 +1257,36 @@ pub struct VisibilityLine { } impl VisibilityLine { - pub fn new(vis: Visibility) -> Self { VisibilityLine { vis } } + pub fn new(vis: Visibility) -> Self { + VisibilityLine { vis } + } } impl TextFragment for VisibilityLine { - fn render_highlighted(&self, width: usize, _highlight: Option, - _style: &dyn DisplayStyleGetter) - -> Vec - { + fn render_highlighted( + &self, + width: usize, + _highlight: Option, + _style: &dyn DisplayStyleGetter, + ) -> Vec { let line = match self.vis { Visibility::Public => ColouredString::general( "Visibility: public", - " ffffff"), - Visibility::Unlisted => ColouredString::plain( - "Visibility: unlisted"), + " ffffff", + ), + Visibility::Unlisted => { + ColouredString::plain("Visibility: unlisted") + } Visibility::Private => ColouredString::general( "Visibility: private", - " rrrrrrr"), + " rrrrrrr", + ), Visibility::Direct => ColouredString::general( "Visibility: direct", - " rrrrrr"), + " rrrrrr", + ), }; - vec! { line.truncate(width).into() } + vec![line.truncate(width).into()] } } @@ -1126,11 +1300,14 @@ pub struct NotificationLog { impl NotificationLog { pub fn new( - timestamp: DateTime, account: &str, nameline: &str, - account_id: &str, ntype: NotificationType, post: Option<&Paragraph>, - status_id: Option<&str>) - -> Self - { + timestamp: DateTime, + account: &str, + nameline: &str, + account_id: &str, + ntype: NotificationType, + post: Option<&Paragraph>, + status_id: Option<&str>, + ) -> Self { let mut para = Paragraph::new(); let verb = match ntype { @@ -1159,7 +1336,9 @@ impl NotificationLog { } pub fn from_notification(not: &Notification, client: &mut Client) -> Self { - let para = not.status.as_ref() + let para = not + .status + .as_ref() .map(|st| Html::new(&st.content).to_para()); let status_id = not.status.as_ref().map(|st| &st.id as &str); Self::new( @@ -1175,10 +1354,12 @@ impl NotificationLog { } impl TextFragment for NotificationLog { - fn render_highlighted(&self, width: usize, highlight: Option, - _style: &dyn DisplayStyleGetter) - -> Vec - { + fn render_highlighted( + &self, + width: usize, + highlight: Option, + _style: &dyn DisplayStyleGetter, + ) -> Vec { let mut full_para = Paragraph::new().set_indent(0, 2); let datestr = format_date(self.timestamp); full_para.push_text(&ColouredString::uniform(&datestr, ' '), false); @@ -1189,44 +1370,58 @@ impl TextFragment for NotificationLog { _ => ' ', }; full_para.push_text( - &ColouredString::uniform(&self.account_desc, user_colour), false); + &ColouredString::uniform(&self.account_desc, user_colour), + false, + ); match (highlight, &self.status_id) { - (Some(Highlight(HighlightType::Status, 0)), Some(..)) => - full_para.push_para_recoloured(&self.para, '*'), + (Some(Highlight(HighlightType::Status, 0)), Some(..)) => { + full_para.push_para_recoloured(&self.para, '*') + } _ => full_para.push_para(&self.para), }; let rendered_para = full_para.render(width); if rendered_para.len() > 2 { - vec! { + vec![ rendered_para[0].clone(), - rendered_para[1].truncate(width-3) + - ColouredString::plain("..."), - } + rendered_para[1].truncate(width - 3) + + ColouredString::plain("..."), + ] } else { rendered_para } } - fn can_highlight(htype: HighlightType) -> bool where Self : Sized { + fn can_highlight(htype: HighlightType) -> bool + where + Self: Sized, + { htype == HighlightType::User || htype == HighlightType::Status } fn count_highlightables(&self, htype: HighlightType) -> usize { match htype { HighlightType::User => 1, - HighlightType::Status => if self.status_id.is_some() {1} else {0}, + HighlightType::Status => { + if self.status_id.is_some() { + 1 + } else { + 0 + } + } _ => 0, } } fn highlighted_id(&self, highlight: Option) -> Option { match highlight { - Some(Highlight(HighlightType::User, 0)) => - Some(self.account_id.clone()), - Some(Highlight(HighlightType::Status, 0)) => - self.status_id.clone(), + Some(Highlight(HighlightType::User, 0)) => { + Some(self.account_id.clone()) + } + Some(Highlight(HighlightType::Status, 0)) => { + self.status_id.clone() + } _ => None, } } @@ -1234,39 +1429,74 @@ impl TextFragment for NotificationLog { #[test] fn test_notification_log() { - let t = NaiveDateTime::parse_from_str("2001-08-03 04:05:06", - "%Y-%m-%d %H:%M:%S") - .unwrap().and_local_timezone(Local).unwrap().with_timezone(&Utc); + let t = NaiveDateTime::parse_from_str( + "2001-08-03 04:05:06", + "%Y-%m-%d %H:%M:%S", + ) + .unwrap() + .and_local_timezone(Local) + .unwrap() + .with_timezone(&Utc); let post = Paragraph::new().add(ColouredString::general( "@stoat @weasel take a look at this otter! @badger might also like it", "@@@@@@ @@@@@@@ @@@@@@@ ", )); - assert_eq!(NotificationLog::new( - t, "foo@example.com", "Foo Bar", "123", - NotificationType::Reblog, Some(&post), None).render(80), vec! { + assert_eq!( + NotificationLog::new( + t, + "foo@example.com", + "Foo Bar", + "123", + NotificationType::Reblog, + Some(&post), + None + ) + .render(80), + vec! { ColouredString::general("Fri Aug 3 04:05:06 2001 Foo Bar (foo@example.com) boosted: take a look at this", " "), ColouredString::general(" otter! @badger might also like it", " @@@@@@@ "), - }); + } + ); - assert_eq!(NotificationLog::new( - t, "foo@example.com", "Foo Bar", "123", - NotificationType::Favourite, Some(&post), None).render(51), vec! { + assert_eq!( + NotificationLog::new( + t, + "foo@example.com", + "Foo Bar", + "123", + NotificationType::Favourite, + Some(&post), + None + ) + .render(51), + vec! { ColouredString::general("Fri Aug 3 04:05:06 2001 Foo Bar (foo@example.com)", " "), ColouredString::general(" favourited: take a look at this otter! @badger...", " @@@@@@@ "), - }); + } + ); - assert_eq!(NotificationLog::new( - t, "foo@example.com", "Foo Bar", "123", - NotificationType::Follow, None, None).render(80), vec! { + assert_eq!( + NotificationLog::new( + t, + "foo@example.com", + "Foo Bar", + "123", + NotificationType::Follow, + None, + None + ) + .render(80), + vec! { ColouredString::general("Fri Aug 3 04:05:06 2001 Foo Bar (foo@example.com) followed you", " "), - }); + } + ); } pub struct UserListEntry { @@ -1288,21 +1518,28 @@ impl UserListEntry { } impl TextFragment for UserListEntry { - fn render_highlighted(&self, width: usize, highlight: Option, - _style: &dyn DisplayStyleGetter) - -> Vec - { + fn render_highlighted( + &self, + width: usize, + highlight: Option, + _style: &dyn DisplayStyleGetter, + ) -> Vec { let mut para = Paragraph::new().set_indent(0, 2); let colour = match highlight { Some(Highlight(HighlightType::User, 0)) => '*', _ => ' ', }; para.push_text( - &ColouredString::uniform(&self.account_desc, colour), false); + &ColouredString::uniform(&self.account_desc, colour), + false, + ); para.render(width) } - fn can_highlight(htype: HighlightType) -> bool where Self : Sized { + fn can_highlight(htype: HighlightType) -> bool + where + Self: Sized, + { htype == HighlightType::User } @@ -1315,8 +1552,9 @@ impl TextFragment for UserListEntry { fn highlighted_id(&self, highlight: Option) -> Option { match highlight { - Some(Highlight(HighlightType::User, 0)) => - Some(self.account_id.clone()), + Some(Highlight(HighlightType::User, 0)) => { + Some(self.account_id.clone()) + } _ => None, } } @@ -1324,11 +1562,13 @@ impl TextFragment for UserListEntry { #[test] fn test_user_list_entry() { - assert_eq!(UserListEntry::new("foo@example.com", "Foo Bar", "123") - .render(80), vec! { + assert_eq!( + UserListEntry::new("foo@example.com", "Foo Bar", "123").render(80), + vec! { ColouredString::general("Foo Bar (foo@example.com)", " "), - }); + } + ); } pub struct Media { @@ -1337,16 +1577,17 @@ pub struct Media { } impl Media { - pub fn new(url: &str, description: Option<&str>) - -> Self { + pub fn new(url: &str, description: Option<&str>) -> Self { let paras = match description { None => Vec::new(), Some(description) => { let mut paras = description .split('\n') - .map(|x| Paragraph::new() - .set_indent(2, 2) - .add(ColouredString::uniform(x, 'm'))) + .map(|x| { + Paragraph::new() + .set_indent(2, 2) + .add(ColouredString::uniform(x, 'm')) + }) .collect(); trim_para_list(&mut paras); paras @@ -1361,12 +1602,16 @@ impl Media { } impl TextFragment for Media { - fn render_highlighted(&self, width: usize, _highlight: Option, - _style: &dyn DisplayStyleGetter) - -> Vec - { + fn render_highlighted( + &self, + width: usize, + _highlight: Option, + _style: &dyn DisplayStyleGetter, + ) -> Vec { let mut lines: Vec<_> = ColouredString::uniform(&self.url, 'M') - .split(width.saturating_sub(1)).map(|x| x.into()).collect(); + .split(width.saturating_sub(1)) + .map(|x| x.into()) + .collect(); for para in &self.description { lines.extend_from_slice(¶.render(width)); } @@ -1452,12 +1697,19 @@ impl FileStatusLine { self } - pub fn add(mut self, key: OurKey, description: &str, - priority: usize) -> Self { - self.keypresses.push((Keypress { - key, - description: ColouredString::plain(description) - }, priority)); + pub fn add( + mut self, + key: OurKey, + description: &str, + priority: usize, + ) -> Self { + self.keypresses.push(( + Keypress { + key, + description: ColouredString::plain(description), + }, + priority, + )); self } @@ -1466,8 +1718,9 @@ impl FileStatusLine { for (kp, pri) in &self.keypresses { // [key]:desc let key = key_to_string(kp.key); - let width = UnicodeWidthStr::width(&key as &str) + - kp.description.width() + 3; + let width = UnicodeWidthStr::width(&key as &str) + + kp.description.width() + + 3; if let Some(priwidth) = bypri.get_mut(pri) { *priwidth += Self::SPACING + width; @@ -1479,21 +1732,25 @@ impl FileStatusLine { // If the keypresses are the only thing on this status line, // then we don't need an extra SPACING to separate them from // other stuff. - let initial = if self.proportion.is_some() || - self.message.is_some() { Self::SPACING } else { 0 }; + let initial = if self.proportion.is_some() || self.message.is_some() { + Self::SPACING + } else { + 0 + }; let mut cumulative = initial; let mut priwidth = Vec::new(); for (minpri, thiswidth) in bypri.iter().rev() { - cumulative += thiswidth + - if cumulative == initial { 0 } else { Self::SPACING }; + cumulative += thiswidth + + if cumulative == initial { + 0 + } else { + Self::SPACING + }; priwidth.push((**minpri, cumulative)); } - FileStatusLineFinal { - fs: self, - priwidth, - } + FileStatusLineFinal { fs: self, priwidth } } } @@ -1508,56 +1765,76 @@ fn test_filestatus_build() { let fsf = FileStatusLine::new() .add(OurKey::Pr('A'), "Annoy", 10) .finalise(); - assert_eq!(fsf.priwidth, vec! { + assert_eq!( + fsf.priwidth, + vec! { (10, 9), - }); + } + ); let fsf = FileStatusLine::new() .add(OurKey::Pr('A'), "Annoy", 10) .add(OurKey::Pr('B'), "Badger", 10) .finalise(); - assert_eq!(fsf.priwidth, vec! { + assert_eq!( + fsf.priwidth, + vec! { (10, 9 + 10 + FileStatusLine::SPACING), - }); + } + ); let fsf = FileStatusLine::new() .add(OurKey::Pr('A'), "Annoy", 10) .add(OurKey::Pr('B'), "Badger", 5) .finalise(); - assert_eq!(fsf.priwidth, vec! { + assert_eq!( + fsf.priwidth, + vec! { (10, 9), (5, 9 + 10 + FileStatusLine::SPACING), - }); + } + ); let fsf = FileStatusLine::new() .add(OurKey::Pr('A'), "Annoy", 10) .finalise(); - assert_eq!(fsf.priwidth, vec! { + assert_eq!( + fsf.priwidth, + vec! { (10, 9), - }); + } + ); let fsf = FileStatusLine::new() .set_proportion(2, 3) .add(OurKey::Pr('A'), "Annoy", 10) .finalise(); - assert_eq!(fsf.priwidth, vec! { + assert_eq!( + fsf.priwidth, + vec! { (10, 9 + FileStatusLine::SPACING), - }); + } + ); let fsf = FileStatusLine::new() .message("aha") .add(OurKey::Pr('A'), "Annoy", 10) .finalise(); - assert_eq!(fsf.priwidth, vec! { + assert_eq!( + fsf.priwidth, + vec! { (10, 9 + FileStatusLine::SPACING), - }); + } + ); } impl TextFragment for FileStatusLineFinal { - fn render_highlighted(&self, width: usize, _highlight: Option, - _style: &dyn DisplayStyleGetter) - -> Vec - { + fn render_highlighted( + &self, + width: usize, + _highlight: Option, + _style: &dyn DisplayStyleGetter, + ) -> Vec { let mut line = ColouredString::plain(""); let space = ColouredString::plain(" ").repeat(FileStatusLine::SPACING); let push = |line: &mut ColouredString, s: ColouredStringSlice<'_>| { @@ -1571,9 +1848,16 @@ impl TextFragment for FileStatusLineFinal { push(&mut line, ColouredString::plain(msg).slice()); } - let cprop = self.fs.proportion.as_ref() + let cprop = self + .fs + .proportion + .as_ref() .map(|prop| ColouredString::plain(&format!("({}%)", prop))); - let cpropwidth = if let Some(cprop) = &cprop { cprop.width() } else {0}; + let cpropwidth = if let Some(cprop) = &cprop { + cprop.width() + } else { + 0 + }; let extraspace = if !line.is_empty() && cpropwidth > 0 { FileStatusLine::SPACING } else if cprop.is_none() { @@ -1582,8 +1866,8 @@ impl TextFragment for FileStatusLineFinal { 0 }; - let avail = width - min( - width, line.width() + cpropwidth + extraspace + 1); + let avail = + width - min(width, line.width() + cpropwidth + extraspace + 1); let minindex = self.priwidth.partition_point(|(_, w)| *w <= avail); if minindex > 0 { let minpri = self.priwidth[minindex - 1].0; @@ -1591,14 +1875,13 @@ impl TextFragment for FileStatusLineFinal { for (kp, pri) in &self.fs.keypresses { if *pri >= minpri { let mut ckey = ColouredString::plain("["); - let ckp = ColouredString::uniform( - &key_to_string(kp.key), 'k'); + let ckp = + ColouredString::uniform(&key_to_string(kp.key), 'k'); ckey.push_str(&ckp); ckey.push_str(ColouredString::plain("]:")); ckey.push_str(&kp.description); push(&mut line, ckey.slice()); } - } } @@ -1613,7 +1896,7 @@ impl TextFragment for FileStatusLineFinal { let space = width - min(line.width() + 1, width); let left = space / 2; let linepad = ColouredString::plain(" ").repeat(left) + line; - vec! { linepad } + vec![linepad] } } @@ -1628,79 +1911,112 @@ fn test_filestatus_render() { .add(OurKey::Pr('d'), "Dull", 1) .finalise(); - assert_eq!(fs.render(80), vec! { + assert_eq!( + fs.render(80), + vec! { ColouredString::general( " stoat [A]:Annoy [B]:Badger [C]:Critical [D]:Dull (61%)", " k k k k "), - }); + } + ); - assert_eq!(fs.render(60), vec! { + assert_eq!( + fs.render(60), + vec! { ColouredString::general( "stoat [A]:Annoy [B]:Badger [C]:Critical [D]:Dull (61%)", " k k k k "), - }); + } + ); - assert_eq!(fs.render(59), vec! { + assert_eq!( + fs.render(59), + vec! { ColouredString::general( " stoat [A]:Annoy [B]:Badger [C]:Critical (61%)", " k k k "), - }); + } + ); - assert_eq!(fs.render(50), vec! { + assert_eq!( + fs.render(50), + vec! { ColouredString::general( "stoat [A]:Annoy [B]:Badger [C]:Critical (61%)", " k k k "), - }); + } + ); - assert_eq!(fs.render(49), vec! { + assert_eq!( + fs.render(49), + vec! { ColouredString::general( " stoat [C]:Critical (61%)", " k "), - }); + } + ); - assert_eq!(fs.render(27), vec! { + assert_eq!( + fs.render(27), + vec! { ColouredString::general( "stoat [C]:Critical (61%)", " k "), - }); + } + ); - assert_eq!(fs.render(26), vec! { + assert_eq!( + fs.render(26), + vec! { ColouredString::plain(" stoat (61%)"), - }); + } + ); let fs = FileStatusLine::new() .set_proportion(1, 11) .add(OurKey::Pr('K'), "Keypress", 10) .finalise(); - assert_eq!(fs.render(19), vec! { + assert_eq!( + fs.render(19), + vec! { ColouredString::general( "[K]:Keypress (9%)", " k "), - }); + } + ); - assert_eq!(fs.render(18), vec! { + assert_eq!( + fs.render(18), + vec! { ColouredString::general( " (9%)", " "), - }); + } + ); let fs = FileStatusLine::new() .message("weasel") .add(OurKey::Pr('K'), "Keypress", 10) .finalise(); - assert_eq!(fs.render(22), vec! { + assert_eq!( + fs.render(22), + vec! { ColouredString::general( "weasel [K]:Keypress.", " k "), - }); + } + ); - assert_eq!(fs.render(21), vec! { + assert_eq!( + fs.render(21), + vec! { ColouredString::general( " weasel.", " "), - }); + } + ); } pub struct MenuKeypressLine { @@ -1718,17 +2034,13 @@ pub trait MenuKeypressLineGeneral { } impl MenuKeypressLine { - pub fn new(key: OurKey, description: ColouredString) - -> Self { + pub fn new(key: OurKey, description: ColouredString) -> Self { // +2 for [] around the key name let lwid = UnicodeWidthStr::width(&key_to_string(key) as &str) + 2; let rwid = description.width(); MenuKeypressLine { - keypress: Keypress { - key, - description, - }, + keypress: Keypress { key, description }, lwid, rwid, lmaxwid: lwid, @@ -1753,9 +2065,12 @@ impl MenuKeypressLineGeneral for MenuKeypressLine { } impl TextFragmentOneLine for MenuKeypressLine { - fn render_oneline(&self, width: usize, _highlight: Option, - _style: &dyn DisplayStyleGetter) -> ColouredString - { + fn render_oneline( + &self, + width: usize, + _highlight: Option, + _style: &dyn DisplayStyleGetter, + ) -> ColouredString { let ourwidth = self.lmaxwid + self.rmaxwid + 3; // " = " in the middle let space = width - min(width, ourwidth + 1); let leftpad = min(space * 3 / 4, width - min(width, self.lmaxwid + 2)); @@ -1766,26 +2081,26 @@ impl TextFragmentOneLine for MenuKeypressLine { let lrspace = lspace - llspace; let keydesc = key_to_string(self.keypress.key); - let line = ColouredString::plain(" ").repeat(leftpad + llspace) + - ColouredString::plain("[") + - ColouredString::uniform(&keydesc, 'k') + - ColouredString::plain("]") + - ColouredString::plain(" ").repeat(lrspace) + - ColouredString::plain(" = ") + - &self.keypress.description; + let line = ColouredString::plain(" ").repeat(leftpad + llspace) + + ColouredString::plain("[") + + ColouredString::uniform(&keydesc, 'k') + + ColouredString::plain("]") + + ColouredString::plain(" ").repeat(lrspace) + + ColouredString::plain(" = ") + + &self.keypress.description; line.truncate(width).into() } } impl TextFragment for MenuKeypressLine { - fn render_highlighted(&self, width: usize, highlight: Option, - style: &dyn DisplayStyleGetter) - -> Vec - { - vec! { - self.render_oneline(width, highlight, style) - } + fn render_highlighted( + &self, + width: usize, + highlight: Option, + style: &dyn DisplayStyleGetter, + ) -> Vec { + vec![self.render_oneline(width, highlight, style)] } } @@ -1795,19 +2110,25 @@ pub struct CyclingMenuLine { } impl CyclingMenuLine { - pub fn new(key: OurKey, description: ColouredString, - values: &[(Value, ColouredString)], value: Value) -> Self { - let menulines = values.iter().map( |(val, desc)| { - (*val, MenuKeypressLine::new(key, &description + desc)) - }).collect(); - - let index = values.iter().position(|(val, _desc)| *val == value) + pub fn new( + key: OurKey, + description: ColouredString, + values: &[(Value, ColouredString)], + value: Value, + ) -> Self { + let menulines = values + .iter() + .map(|(val, desc)| { + (*val, MenuKeypressLine::new(key, &description + desc)) + }) + .collect(); + + let index = values + .iter() + .position(|(val, _desc)| *val == value) .expect("Input value must match one of the provided options"); - CyclingMenuLine { - menulines, - index, - } + CyclingMenuLine { menulines, index } } // Returns a LogicalAction just to make it more convenient to put @@ -1820,7 +2141,9 @@ impl CyclingMenuLine { LogicalAction::Nothing } - pub fn get_value(&self) -> Value { self.menulines[self.index].0 } + pub fn get_value(&self) -> Value { + self.menulines[self.index].0 + } } impl MenuKeypressLineGeneral for CyclingMenuLine { @@ -1842,55 +2165,73 @@ impl MenuKeypressLineGeneral for CyclingMenuLine { } impl TextFragmentOneLine for CyclingMenuLine { - fn render_oneline(&self, width: usize, highlight: Option, - style: &dyn DisplayStyleGetter) -> ColouredString - { - self.menulines[self.index].1.render_oneline(width, highlight, style) + fn render_oneline( + &self, + width: usize, + highlight: Option, + style: &dyn DisplayStyleGetter, + ) -> ColouredString { + self.menulines[self.index] + .1 + .render_oneline(width, highlight, style) } } impl TextFragment for CyclingMenuLine { - fn render_highlighted(&self, width: usize, highlight: Option, - style: &dyn DisplayStyleGetter) - -> Vec - { - vec! { - self.render_oneline(width, highlight, style) - } + fn render_highlighted( + &self, + width: usize, + highlight: Option, + style: &dyn DisplayStyleGetter, + ) -> Vec { + vec![self.render_oneline(width, highlight, style)] } } #[test] fn test_menu_keypress() { - let mut mk = MenuKeypressLine::new(OurKey::Pr('S'), ColouredString::general( - "Something or other", - "K ")); + let mut mk = MenuKeypressLine::new( + OurKey::Pr('S'), + ColouredString::general("Something or other", "K "), + ); - assert_eq!(mk.render(80), vec! { - ColouredString::general( - " [S] = Something or other", - " k K "), - }); + assert_eq!( + mk.render(80), + vec! { + ColouredString::general( + " [S] = Something or other", + " k K "), + } + ); - assert_eq!(mk.render(40), vec! { - ColouredString::general( - " [S] = Something or other", - " k K "), - }); + assert_eq!( + mk.render(40), + vec! { + ColouredString::general( + " [S] = Something or other", + " k K "), + } + ); - assert_eq!(mk.render(29), vec! { - ColouredString::general( - " [S] = Something or other", - " k K "), - }); + assert_eq!( + mk.render(29), + vec! { + ColouredString::general( + " [S] = Something or other", + " k K "), + } + ); mk.ensure_widths(5, 0); - assert_eq!(mk.render(40), vec! { - ColouredString::general( - " [S] = Something or other", - " k K "), - }); + assert_eq!( + mk.render(40), + vec! { + ColouredString::general( + " [S] = Something or other", + " k K "), + } + ); } pub struct CentredInfoLine { @@ -1899,23 +2240,23 @@ pub struct CentredInfoLine { impl CentredInfoLine { pub fn new(text: ColouredString) -> Self { - CentredInfoLine { - text, - } + CentredInfoLine { text } } } impl TextFragment for CentredInfoLine { - fn render_highlighted(&self, width: usize, _highlight: Option, - _style: &dyn DisplayStyleGetter) - -> Vec - { + fn render_highlighted( + &self, + width: usize, + _highlight: Option, + _style: &dyn DisplayStyleGetter, + ) -> Vec { let twidth = width.saturating_sub(1); let title = self.text.truncate(twidth); let tspace = twidth - title.width(); let tleft = tspace / 2; let textpad = ColouredString::plain(" ").repeat(tleft) + &self.text; - vec! { textpad } + vec![textpad] } } @@ -1951,20 +2292,27 @@ impl StatusDisplay { let sep = SeparatorLine::new( Some(st.created_at), st.favourited == Some(true), - st.reblogged == Some(true)); + st.reblogged == Some(true), + ); let from = UsernameHeader::from( - &client.fq(&st.account.acct), &st.account.display_name, - &st.account.id); + &client.fq(&st.account.acct), + &st.account.display_name, + &st.account.id, + ); let via = match via { None => None, Some(booster) => Some(UsernameHeader::via( - &client.fq(&booster.acct), &booster.display_name, - &booster.id)), + &client.fq(&booster.acct), + &booster.display_name, + &booster.id, + )), }; - let irt = st.in_reply_to_id.as_ref() + let irt = st + .in_reply_to_id + .as_ref() .map(|id| InReplyToLine::from_id(id, client)); let vis = match st.visibility { @@ -1980,10 +2328,14 @@ impl StatusDisplay { let content = Html::new(&st.content); - let media = st.media_attachments.iter().map(|m| { - let desc_ref = m.description.as_ref().map(|s| s as &str); - Media::new(&m.url, desc_ref) - }).collect(); + let media = st + .media_attachments + .iter() + .map(|m| { + let desc_ref = m.description.as_ref().map(|s| s as &str); + Media::new(&m.url, desc_ref) + }) + .collect(); let poll = st.poll.map(|poll| { let mut extras = Vec::new(); @@ -1994,16 +2346,22 @@ impl StatusDisplay { if poll.expired { extras.push(ColouredString::uniform("expired", 'H')); extras.push(ColouredString::uniform( - &format!("{} voters", voters), 'H')); + &format!("{} voters", voters), + 'H', + )); } else { if let Some(date) = poll.expires_at { extras.push(ColouredString::uniform( - &format!("expires {}", format_date(date)), 'H')); + &format!("expires {}", format_date(date)), + 'H', + )); } else { extras.push(ColouredString::uniform("no expiry", 'H')); } extras.push(ColouredString::uniform( - &format!("{} voters so far", voters), 'H')); + &format!("{} voters so far", voters), + 'H', + )); } let mut desc = ColouredString::uniform("Poll: ", 'H'); for (i, extra) in extras.iter().enumerate() { @@ -2024,7 +2382,9 @@ impl StatusDisplay { desc.push_str(ColouredString::plain(&opt.title)); if let Some(n) = opt.votes_count { desc.push_str(ColouredString::uniform( - &format!(" ({})", n), 'H')); + &format!(" ({})", n), + 'H', + )); } options.push((voted, desc)); } @@ -2053,40 +2413,56 @@ impl StatusDisplay { } } - pub fn list_urls(&self) -> Vec { self.content.list_urls() } + pub fn list_urls(&self) -> Vec { + self.content.list_urls() + } } fn push_fragment(lines: &mut Vec, frag: Vec) { lines.extend(frag.iter().map(|line| line.to_owned())); } -fn push_fragment_highlighted(lines: &mut Vec, - frag: Vec) { +fn push_fragment_highlighted( + lines: &mut Vec, + frag: Vec, +) { lines.extend(frag.iter().map(|line| line.recolour('*'))); } impl TextFragment for StatusDisplay { - fn render_highlighted(&self, width: usize, highlight: Option, - style: &dyn DisplayStyleGetter) - -> Vec - { + fn render_highlighted( + &self, + width: usize, + highlight: Option, + style: &dyn DisplayStyleGetter, + ) -> Vec { let mut lines = Vec::new(); let mut highlight = highlight; push_fragment(&mut lines, self.sep.render(width)); - push_fragment(&mut lines, self.from.render_highlighted_update( - width, &mut highlight, style)); - push_fragment(&mut lines, self.via.render_highlighted_update( - width, &mut highlight, style)); + push_fragment( + &mut lines, + self.from + .render_highlighted_update(width, &mut highlight, style), + ); + push_fragment( + &mut lines, + self.via + .render_highlighted_update(width, &mut highlight, style), + ); push_fragment(&mut lines, self.vis.render(width)); - push_fragment(&mut lines, self.irt.render_highlighted_update( - width, &mut highlight, style)); + push_fragment( + &mut lines, + self.irt + .render_highlighted_update(width, &mut highlight, style), + ); push_fragment(&mut lines, self.blank.render(width)); let highlighting_this_status = - highlight.consume(HighlightType::Status, 1) == Some(0) || - highlight.consume(HighlightType::WholeStatus, 1) == Some(0) || - (self.warning.is_some() && - highlight.consume(HighlightType::FoldableStatus, 1) == Some(0)); + highlight.consume(HighlightType::Status, 1) == Some(0) + || highlight.consume(HighlightType::WholeStatus, 1) == Some(0) + || (self.warning.is_some() + && highlight.consume(HighlightType::FoldableStatus, 1) + == Some(0)); let push_fragment_opt_highlight = if highlighting_this_status { push_fragment_highlighted } else { @@ -2097,7 +2473,9 @@ impl TextFragment for StatusDisplay { if let Some(warning_text) = &self.warning { let mut para = Paragraph::new(); para = para.add(&ColouredString::uniform( - if folded {"[-]"} else {"[+]"}, 'r')); + if folded { "[-]" } else { "[+]" }, + 'r', + )); para.end_word(); para = para.add(&ColouredString::plain(&warning_text)); push_fragment_opt_highlight(&mut lines, para.render(width)); @@ -2115,7 +2493,9 @@ impl TextFragment for StatusDisplay { for m in &self.media { push_fragment_opt_highlight(&mut lines, m.render(width)); push_fragment_opt_highlight( - &mut lines, self.blank.render(width)); + &mut lines, + self.blank.render(width), + ); } } @@ -2128,11 +2508,15 @@ impl TextFragment for StatusDisplay { for (i, (voted, desc)) in poll.options.iter().enumerate() { let highlighting_this_option = highlight.consume(HighlightType::PollOption, 1) == Some(0); - let voted = poll_options_selected.as_ref().map_or( - *voted, |opts| opts.contains(&i)); + let voted = poll_options_selected + .as_ref() + .map_or(*voted, |opts| opts.contains(&i)); let option = Paragraph::new().set_indent(0, 2).add( &ColouredString::uniform( - if voted {"[X]"} else {"[ ]"}, 'H')); + if voted { "[X]" } else { "[ ]" }, + 'H', + ), + ); let option = if folded { option } else { option.add(desc) }; let push_fragment_opt_highlight = if highlighting_this_option { push_fragment_highlighted @@ -2147,28 +2531,33 @@ impl TextFragment for StatusDisplay { lines } - fn can_highlight(htype: HighlightType) -> bool where Self : Sized { - htype == HighlightType::User || - htype == HighlightType::Status || - htype == HighlightType::WholeStatus || - htype == HighlightType::FoldableStatus || - htype == HighlightType::PollOption + fn can_highlight(htype: HighlightType) -> bool + where + Self: Sized, + { + htype == HighlightType::User + || htype == HighlightType::Status + || htype == HighlightType::WholeStatus + || htype == HighlightType::FoldableStatus + || htype == HighlightType::PollOption } fn count_highlightables(&self, htype: HighlightType) -> usize { match htype { HighlightType::User => { - self.from.count_highlightables(htype) + - self.via.count_highlightables(htype) + self.from.count_highlightables(htype) + + self.via.count_highlightables(htype) } HighlightType::Status => 1, HighlightType::WholeStatus => 1, HighlightType::FoldableStatus => { - self.irt.count_highlightables(htype) + - if self.warning.is_some() {1} else {0} + self.irt.count_highlightables(htype) + + if self.warning.is_some() { 1 } else { 0 } } - HighlightType::PollOption => self.poll.as_ref() + HighlightType::PollOption => self + .poll + .as_ref() .filter(|poll| poll.eligible) .map_or(0, |poll| poll.options.len()), } @@ -2178,32 +2567,34 @@ impl TextFragment for StatusDisplay { match highlight { Some(Highlight(HighlightType::User, _)) => { let mut highlight = highlight; - if let result @ Some(..) = self.from.highlighted_id_update( - &mut highlight) + if let result @ Some(..) = + self.from.highlighted_id_update(&mut highlight) { return result; } - if let result @ Some(..) = self.via.highlighted_id_update( - &mut highlight) + if let result @ Some(..) = + self.via.highlighted_id_update(&mut highlight) { return result; } None } - Some(Highlight(HighlightType::WholeStatus, 0)) | - Some(Highlight(HighlightType::Status, 0)) => Some(self.id.clone()), + Some(Highlight(HighlightType::WholeStatus, 0)) + | Some(Highlight(HighlightType::Status, 0)) => { + Some(self.id.clone()) + } Some(Highlight(HighlightType::FoldableStatus, _)) => { let mut highlight = highlight; - if let result @ Some(..) = self.irt.highlighted_id_update( - &mut highlight) + if let result @ Some(..) = + self.irt.highlighted_id_update(&mut highlight) { return result; } - if self.warning.is_some() && - highlight.consume(HighlightType::FoldableStatus, 1) == - Some(0) + if self.warning.is_some() + && highlight.consume(HighlightType::FoldableStatus, 1) + == Some(0) { return Some(self.id.clone()); } @@ -2211,7 +2602,9 @@ impl TextFragment for StatusDisplay { None } - Some(Highlight(HighlightType::PollOption, i)) => self.poll.as_ref() + Some(Highlight(HighlightType::PollOption, i)) => self + .poll + .as_ref() .filter(|poll| poll.eligible) .filter(|poll| i < poll.options.len()) .map(|poll| poll.id.clone()), @@ -2256,10 +2649,11 @@ impl DetailedStatusDisplay { let id = Paragraph::new() .add(ColouredString::plain("Post id: ")) .add(ColouredString::plain(&st.id)); - let webstatus = st.url.as_ref().map( - |s| Paragraph::new() + let webstatus = st.url.as_ref().map(|s| { + Paragraph::new() .add(ColouredString::plain("On the web: ")) - .add(ColouredString::uniform(&client.fq(s), 'u'))); + .add(ColouredString::uniform(&client.fq(s), 'u')) + }); let creation = Paragraph::new() .add(ColouredString::plain("Creation time: ")) @@ -2268,25 +2662,29 @@ impl DetailedStatusDisplay { .add(ColouredString::plain("Last edit time: ")) .add(&st.edited_at.map_or_else( || ColouredString::uniform("none", '0'), - |date| ColouredString::plain(&format_date(date)))); + |date| ColouredString::plain(&format_date(date)), + )); let reply_to = Paragraph::new() .add(ColouredString::plain("Reply to post: ")) .add(&st.in_reply_to_id.as_ref().map_or_else( || ColouredString::uniform("none", '0'), - |s| ColouredString::plain(s))); + |s| ColouredString::plain(s), + )); let reply_to_id = st.in_reply_to_id.clone(); let reply_to_user = Paragraph::new() .add(ColouredString::plain("Reply to account: ")) .add(&st.in_reply_to_account_id.as_ref().map_or_else( || ColouredString::uniform("none", '0'), - |s| ColouredString::plain(s))); + |s| ColouredString::plain(s), + )); let reply_to_user_id = st.in_reply_to_account_id.clone(); let language = Paragraph::new() .add(ColouredString::plain("Language: ")) .add(&st.language.as_ref().map_or_else( || ColouredString::uniform("none", '0'), - |s| ColouredString::plain(s))); + |s| ColouredString::plain(s), + )); let visibility = VisibilityLine::new(st.visibility); let sens_str = match st.sensitive { false => ColouredString::uniform("no", 'f'), @@ -2300,57 +2698,80 @@ impl DetailedStatusDisplay { } else { Some(&st.spoiler_text) }; - let spoiler = Paragraph::new().set_indent(0, 2) + let spoiler = Paragraph::new() + .set_indent(0, 2) .add(ColouredString::plain("Spoiler text: ")) .add(&opt_spoiler.as_ref().map_or_else( || ColouredString::uniform("none", '0'), - |s| ColouredString::plain(s))); - - let replies = Paragraph::new() - .add(ColouredString::plain( - &format!("Replies: {}", st.replies_count))); - let boosts = Paragraph::new() - .add(ColouredString::plain( - &format!("Boosts: {}", st.reblogs_count))); - let favourites = Paragraph::new() - .add(ColouredString::plain( - &format!("Favourites: {}", st.favourites_count))); - - let mentions: Vec<_> = st.mentions.iter().map(|m| { - let para = Paragraph::new().set_indent(2, 2) - .add(ColouredString::uniform(&client.fq(&m.acct), 'f')); - (para, m.id.to_owned()) - }).collect(); + |s| ColouredString::plain(s), + )); + + let replies = Paragraph::new().add(ColouredString::plain(&format!( + "Replies: {}", + st.replies_count + ))); + let boosts = Paragraph::new().add(ColouredString::plain(&format!( + "Boosts: {}", + st.reblogs_count + ))); + let favourites = Paragraph::new().add(ColouredString::plain( + &format!("Favourites: {}", st.favourites_count), + )); + + let mentions: Vec<_> = st + .mentions + .iter() + .map(|m| { + let para = Paragraph::new() + .set_indent(2, 2) + .add(ColouredString::uniform(&client.fq(&m.acct), 'f')); + (para, m.id.to_owned()) + }) + .collect(); let mentions_header = if mentions.is_empty() { None } else { - Some(Paragraph::new().add( - &ColouredString::plain("Mentioned users:"))) + Some( + Paragraph::new() + .add(&ColouredString::plain("Mentioned users:")), + ) }; let client_name = Paragraph::new() .add(ColouredString::plain("Client name: ")) .add(&st.application.as_ref().map_or_else( || ColouredString::uniform("none", '0'), - |app| ColouredString::plain(&app.name))); + |app| ColouredString::plain(&app.name), + )); let client_url = Paragraph::new() .add(ColouredString::plain("Client website: ")) - .add(&st.application.as_ref() - .and_then(|app| app.website.as_ref()) - .map_or_else( - || ColouredString::uniform("none", '0'), - |url| ColouredString::uniform(url, 'u'))); + .add( + &st.application + .as_ref() + .and_then(|app| app.website.as_ref()) + .map_or_else( + || ColouredString::uniform("none", '0'), + |url| ColouredString::uniform(url, 'u'), + ), + ); let sd = StatusDisplay::new(st, client); - let urls: Vec<_> = sd.list_urls().iter().map(|u| { - Paragraph::new().set_indent(2, 4) - .add(ColouredString::uniform(u, 'u')) - }).collect(); + let urls: Vec<_> = sd + .list_urls() + .iter() + .map(|u| { + Paragraph::new() + .set_indent(2, 4) + .add(ColouredString::uniform(u, 'u')) + }) + .collect(); let urls_header = if urls.is_empty() { None } else { - Some(Paragraph::new().add( - &ColouredString::plain("URLs in hyperlinks:"))) + Some( + Paragraph::new() + .add(&ColouredString::plain("URLs in hyperlinks:")), + ) }; DetailedStatusDisplay { @@ -2383,15 +2804,20 @@ impl DetailedStatusDisplay { } impl TextFragment for DetailedStatusDisplay { - fn render_highlighted(&self, width: usize, highlight: Option, - style: &dyn DisplayStyleGetter) - -> Vec - { + fn render_highlighted( + &self, + width: usize, + highlight: Option, + style: &dyn DisplayStyleGetter, + ) -> Vec { let mut lines = Vec::new(); let mut highlight = highlight; - push_fragment(&mut lines, self.sd.render_highlighted_update( - width, &mut highlight, style)); + push_fragment( + &mut lines, + self.sd + .render_highlighted_update(width, &mut highlight, style), + ); push_fragment(&mut lines, self.sep.render(width)); push_fragment(&mut lines, self.blank.render(width)); @@ -2403,20 +2829,19 @@ impl TextFragment for DetailedStatusDisplay { push_fragment(&mut lines, self.lastedit.render(width)); let mut reply_to = self.reply_to.render(width); - if self.reply_to_id.is_some() && - highlight.consume(HighlightType::Status, 1) == Some(0) + if self.reply_to_id.is_some() + && highlight.consume(HighlightType::Status, 1) == Some(0) { - reply_to = reply_to.iter().map(|s| s.recolour('*')) - .collect(); + reply_to = reply_to.iter().map(|s| s.recolour('*')).collect(); } push_fragment(&mut lines, reply_to); let mut reply_to_user = self.reply_to_user.render(width); - if self.reply_to_user_id.is_some() && - highlight.consume(HighlightType::User, 1) == Some(0) + if self.reply_to_user_id.is_some() + && highlight.consume(HighlightType::User, 1) == Some(0) { - reply_to_user = reply_to_user.iter().map(|s| s.recolour('*')) - .collect(); + reply_to_user = + reply_to_user.iter().map(|s| s.recolour('*')).collect(); } push_fragment(&mut lines, reply_to_user); @@ -2438,8 +2863,8 @@ impl TextFragment for DetailedStatusDisplay { for (para, _id) in &self.mentions { let mut rendered = para.render(width); if highlight.consume(HighlightType::User, 1) == Some(0) { - rendered = rendered.iter().map(|s| s.recolour('*')) - .collect(); + rendered = + rendered.iter().map(|s| s.recolour('*')).collect(); } push_fragment(&mut lines, rendered); } @@ -2459,32 +2884,36 @@ impl TextFragment for DetailedStatusDisplay { lines } - fn can_highlight(htype: HighlightType) -> bool where Self : Sized { - htype == HighlightType::User || htype == HighlightType::Status || - htype == HighlightType::PollOption + fn can_highlight(htype: HighlightType) -> bool + where + Self: Sized, + { + htype == HighlightType::User + || htype == HighlightType::Status + || htype == HighlightType::PollOption } fn count_highlightables(&self, htype: HighlightType) -> usize { let base = self.sd.count_highlightables(htype); match htype { HighlightType::User => { - base + - (if self.reply_to_user_id.is_some() {1} else {0}) + - self.mentions.len() + base + (if self.reply_to_user_id.is_some() { + 1 + } else { + 0 + }) + self.mentions.len() } HighlightType::Status => { - base + (if self.reply_to_id.is_some() {1} else {0}) + base + (if self.reply_to_id.is_some() { 1 } else { 0 }) } _ => base, } } - fn highlighted_id(&self, highlight: Option) - -> Option - { + fn highlighted_id(&self, highlight: Option) -> Option { let mut highlight = highlight; - if let result @ Some(..) = self.sd.highlighted_id_update( - &mut highlight) + if let result @ Some(..) = + self.sd.highlighted_id_update(&mut highlight) { return result; } @@ -2551,23 +2980,31 @@ impl ExamineUserDisplay { let dispname = Paragraph::new() .add(ColouredString::plain("Display name: ")) .add(ColouredString::plain(&ac.display_name)); - let bio_header = Paragraph::new() - .add(ColouredString::plain("Bio:")); + let bio_header = Paragraph::new().add(ColouredString::plain("Bio:")); let bio = Html::new(&ac.note); - let info_header = Paragraph::new() - .add(ColouredString::plain("Information:")); - let info_fields = ac.fields.iter().map(|field| { - let colour = if field.verified_at.is_some() { 'f' } else { ' ' }; - let title_text = field.name.trim(); - let title_text = title_text.strip_suffix(':').unwrap_or(title_text); - let title_text = title_text.to_owned() + ":"; - let title = Paragraph::new() - .add(ColouredString::uniform(&title_text, colour)) - .set_indent(2, 2); - let content = Html::new(&field.value); - (title, content) - }).collect(); + let info_header = + Paragraph::new().add(ColouredString::plain("Information:")); + let info_fields = ac + .fields + .iter() + .map(|field| { + let colour = if field.verified_at.is_some() { + 'f' + } else { + ' ' + }; + let title_text = field.name.trim(); + let title_text = + title_text.strip_suffix(':').unwrap_or(title_text); + let title_text = title_text.to_owned() + ":"; + let title = Paragraph::new() + .add(ColouredString::uniform(&title_text, colour)) + .set_indent(2, 2); + let content = Html::new(&field.value); + (title, content) + }) + .collect(); let id = Paragraph::new() .add(ColouredString::plain("Account id: ")) @@ -2578,121 +3015,166 @@ impl ExamineUserDisplay { let last_post = Paragraph::new() .add(ColouredString::plain("Latest post: ")) .add(&Self::format_option_approx_date(ac.last_status_at)); - let post_count = Paragraph::new() - .add(ColouredString::plain( - &format!("Number of posts: {}", ac.statuses_count))); - let followers_count = Paragraph::new() - .add(ColouredString::plain( - &format!("Number of followers: {}", ac.followers_count))); - let following_count = Paragraph::new() - .add(ColouredString::plain( - &format!("Number of users followed: {}", ac.following_count))); + let post_count = Paragraph::new().add(ColouredString::plain( + &format!("Number of posts: {}", ac.statuses_count), + )); + let followers_count = Paragraph::new().add(ColouredString::plain( + &format!("Number of followers: {}", ac.followers_count), + )); + let following_count = Paragraph::new().add(ColouredString::plain( + &format!("Number of users followed: {}", ac.following_count), + )); let mut flags = Vec::new(); if ac.locked { - flags.push(Paragraph::new() - .add(ColouredString::plain("This account is ")) - .add(ColouredString::uniform("locked", 'r')) - .add(ColouredString::plain( - " (you can't follow it without its permission)."))); + flags.push( + Paragraph::new() + .add(ColouredString::plain("This account is ")) + .add(ColouredString::uniform("locked", 'r')) + .add(ColouredString::plain( + " (you can't follow it without its permission).", + )), + ); } if ac.suspended.unwrap_or(false) { - flags.push(Paragraph::new().add( - &ColouredString::uniform( - "This account is suspended.", 'r'))); + flags.push(Paragraph::new().add(&ColouredString::uniform( + "This account is suspended.", + 'r', + ))); } if ac.limited.unwrap_or(false) { - flags.push(Paragraph::new().add( - &ColouredString::uniform( - "This account is silenced.", 'r'))); + flags.push(Paragraph::new().add(&ColouredString::uniform( + "This account is silenced.", + 'r', + ))); } if ac.bot { - flags.push(Paragraph::new().add( - &ColouredString::plain( - "This account identifies as a bot."))); + flags.push(Paragraph::new().add(&ColouredString::plain( + "This account identifies as a bot.", + ))); } if ac.group { - flags.push(Paragraph::new().add( - &ColouredString::plain( - "This account identifies as a group."))); + flags.push(Paragraph::new().add(&ColouredString::plain( + "This account identifies as a group.", + ))); } if let Some(moved_to) = ac.moved { - flags.push(Paragraph::new() - .add(ColouredString::uniform( - "This account has moved to:", 'r')) - .add(ColouredString::plain( - &format!(" {}", client.fq(&moved_to.acct))))); + flags.push( + Paragraph::new() + .add(ColouredString::uniform( + "This account has moved to:", + 'r', + )) + .add(ColouredString::plain(&format!( + " {}", + client.fq(&moved_to.acct) + ))), + ); } let mut relationships = Vec::new(); if ac.id == client.our_account_id() { relationships.push(Paragraph::new().set_indent(2, 2).add( - &ColouredString::general("You are this user!", - " ___ "))); + &ColouredString::general( + "You are this user!", + " ___ ", + ), + )); } match client.account_relationship_by_id(&ac.id) { Ok(rs) => { if rs.following && rs.showing_reblogs { relationships.push(Paragraph::new().set_indent(2, 2).add( - &ColouredString::uniform( - "You follow this user.", 'f'))); + &ColouredString::uniform("You follow this user.", 'f'), + )); } else if rs.following { relationships.push(Paragraph::new().set_indent(2, 2).add( &ColouredString::uniform( "You follow this user (but without boosts).", - 'f'))); + 'f', + ), + )); } if rs.followed_by { relationships.push(Paragraph::new().set_indent(2, 2).add( &ColouredString::uniform( - "This user follows you.", 'f'))); + "This user follows you.", + 'f', + ), + )); } if rs.requested { relationships.push(Paragraph::new().set_indent(2, 2).add( &ColouredString::uniform( - "This user has requested to follow you!", 'F'))); + "This user has requested to follow you!", + 'F', + ), + )); } if rs.notifying { relationships.push(Paragraph::new().set_indent(2, 2).add( &ColouredString::plain( - "You have enabled notifications for this user."))); + "You have enabled notifications for this user.", + ), + )); } if rs.blocking { relationships.push(Paragraph::new().set_indent(2, 2).add( &ColouredString::uniform( - "You have blocked this user.", 'r'))); + "You have blocked this user.", + 'r', + ), + )); } if rs.blocked_by { relationships.push(Paragraph::new().set_indent(2, 2).add( &ColouredString::uniform( - "This user has blocked you.", 'r'))); + "This user has blocked you.", + 'r', + ), + )); } if rs.muting { relationships.push(Paragraph::new().set_indent(2, 2).add( &ColouredString::uniform( - "You have muted this user.", 'r'))); + "You have muted this user.", + 'r', + ), + )); } if rs.muting_notifications { relationships.push(Paragraph::new().set_indent(2, 2).add( &ColouredString::uniform( "You have muted notifications from this user.", - 'r'))); + 'r', + ), + )); } if rs.domain_blocking { relationships.push(Paragraph::new().set_indent(2, 2).add( &ColouredString::uniform( "You have blocked this user's domain.", - 'r'))); + 'r', + ), + )); } } - Err(e) => relationships.push(Paragraph::new().set_indent(2, 2).add( - &ColouredString::uniform( - &format!("Unable to retrieve relationships: {}", e), - '!'))), + Err(e) => { + relationships.push(Paragraph::new().set_indent(2, 2).add( + &ColouredString::uniform( + &format!("Unable to retrieve relationships: {}", e), + '!', + ), + )) + } } if !relationships.is_empty() { - relationships.insert(0, Paragraph::new().add( - &ColouredString::plain("Relationships to this user:"))); + relationships.insert( + 0, + Paragraph::new().add(&ColouredString::plain( + "Relationships to this user:", + )), + ); } Ok(ExamineUserDisplay { @@ -2715,23 +3197,25 @@ impl ExamineUserDisplay { }) } - fn format_option_approx_date(date: Option) -> ColouredString - { + fn format_option_approx_date(date: Option) -> ColouredString { // Used for account creation dates and last-post times, which // don't seem to bother having precise timestamps match date { None => ColouredString::uniform("none", '0'), - Some(ApproxDate(date)) => ColouredString::plain( - &date.format("%a %b %e %Y").to_string()), + Some(ApproxDate(date)) => { + ColouredString::plain(&date.format("%a %b %e %Y").to_string()) + } } } } impl TextFragment for ExamineUserDisplay { - fn render_highlighted(&self, width: usize, _highlight: Option, - _style: &dyn DisplayStyleGetter) - -> Vec - { + fn render_highlighted( + &self, + width: usize, + _highlight: Option, + _style: &dyn DisplayStyleGetter, + ) -> Vec { let mut lines = Vec::new(); push_fragment(&mut lines, self.name.render(width)); @@ -2751,14 +3235,17 @@ impl TextFragment for ExamineUserDisplay { // indented further. let rkey = key.render(width); let rval = value.render_indented(width, 4); - if rkey.len() == 1 && rval.len() == 1 && - rkey[0].width() + rval[0].width() + 2 <= width + if rkey.len() == 1 + && rval.len() == 1 + && rkey[0].width() + rval[0].width() + 2 <= width { let rval = &rval[0]; assert!(rval.text().starts_with(" ")); // Trim 3 spaces off, leaving one after the colon - let rval = ColouredString::general(&rval.text()[3..], - &rval.colours()[3..]); + let rval = ColouredString::general( + &rval.text()[3..], + &rval.colours()[3..], + ); lines.push(&rkey[0] + &rval); } else { push_fragment(&mut lines, rkey); diff --git a/src/tui.rs b/src/tui.rs index 53e8946..4f83d05 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -1,5 +1,5 @@ use crossterm::{ - event::{self, Event, KeyEvent, KeyCode, KeyEventKind, KeyModifiers}, + event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}, terminal::{ disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, @@ -8,28 +8,28 @@ use crossterm::{ }; use ratatui::{ prelude::{Buffer, CrosstermBackend, Rect, Terminal}, - style::{Style, Color, Modifier}, + style::{Color, Modifier, Style}, }; use std::cell::RefCell; use std::cmp::min; -use std::collections::{BTreeMap, HashMap, HashSet, hash_map}; -use std::io::{Stdout, Write, stdout}; +use std::collections::{hash_map, BTreeMap, HashMap, HashSet}; use std::fs::File; +use std::io::{stdout, Stdout, Write}; use std::rc::Rc; use std::time::Duration; use unicode_width::UnicodeWidthStr; use super::activity_stack::*; use super::client::{ - Client, ClientError, FeedId, FeedExtend, StreamId, StreamUpdate, + Client, ClientError, FeedExtend, FeedId, StreamId, StreamUpdate, }; use super::coloured_string::*; use super::config::ConfigLocation; -use super::menu::*; -use super::file::*; use super::editor::*; -use super::posting::*; +use super::file::*; +use super::menu::*; use super::options::*; +use super::posting::*; fn ratatui_style_from_colour(colour: char) -> Style { match colour { @@ -37,15 +37,20 @@ fn ratatui_style_from_colour(colour: char) -> Style { ' ' => Style::default(), // message separator line, other than the date - 'S' => Style::default().fg(Color::Gray).bg(Color::Blue) + 'S' => Style::default() + .fg(Color::Gray) + .bg(Color::Blue) .add_modifier(Modifier::REVERSED | Modifier::BOLD), // date on a message separator line - 'D' => Style::default().fg(Color::Gray).bg(Color::Blue) + 'D' => Style::default() + .fg(Color::Gray) + .bg(Color::Blue) .add_modifier(Modifier::REVERSED), // username in a From line - 'F' => Style::default().fg(Color::Green) + 'F' => Style::default() + .fg(Color::Green) .add_modifier(Modifier::BOLD), // username in other headers like Via @@ -67,18 +72,22 @@ fn ratatui_style_from_colour(colour: char) -> Style { 's' => Style::default().add_modifier(Modifier::BOLD), // URL - 'u' => Style::default().fg(Color::Blue) + 'u' => Style::default() + .fg(Color::Blue) .add_modifier(Modifier::BOLD | Modifier::UNDERLINED), // media URL - 'M' => Style::default().fg(Color::Magenta) + 'M' => Style::default() + .fg(Color::Magenta) .add_modifier(Modifier::BOLD), // media description 'm' => Style::default().fg(Color::Magenta), // Mastodonochrome logo in file headers - 'J' => Style::default().fg(Color::Blue).bg(Color::Gray) + 'J' => Style::default() + .fg(Color::Blue) + .bg(Color::Gray) .add_modifier(Modifier::REVERSED | Modifier::BOLD), // ~~~~~ underline in file headers @@ -88,14 +97,17 @@ fn ratatui_style_from_colour(colour: char) -> Style { 'H' => Style::default().fg(Color::Cyan), // keypress / keypath names in file headers - 'K' => Style::default().fg(Color::Cyan) + 'K' => Style::default() + .fg(Color::Cyan) .add_modifier(Modifier::BOLD), // keypresses in file status lines 'k' => Style::default().add_modifier(Modifier::BOLD), // separator line between editor header and content - '-' => Style::default().fg(Color::Cyan).bg(Color::Black) + '-' => Style::default() + .fg(Color::Cyan) + .bg(Color::Black) .add_modifier(Modifier::REVERSED), // something really boring, like 'none' in place of data @@ -109,17 +121,25 @@ fn ratatui_style_from_colour(colour: char) -> Style { // a selected user or status you're about to operate on while // viewing a file - '*' => Style::default().fg(Color::Cyan).bg(Color::Black) + '*' => Style::default() + .fg(Color::Cyan) + .bg(Color::Black) .add_modifier(Modifier::REVERSED), // # error report, or by default any unrecognised colour character - '!' | _ => Style::default().fg(Color::Red).bg(Color::Yellow). - add_modifier(Modifier::REVERSED | Modifier::BOLD) + '!' | _ => Style::default() + .fg(Color::Red) + .bg(Color::Yellow) + .add_modifier(Modifier::REVERSED | Modifier::BOLD), } } -fn ratatui_set_string(buf: &mut Buffer, x: usize, y: usize, - text: impl ColouredStringCommon) { +fn ratatui_set_string( + buf: &mut Buffer, + x: usize, + y: usize, + text: impl ColouredStringCommon, +) { let mut x = x; if let Ok(y) = y.try_into() { for (frag, colour) in text.frags() { @@ -161,9 +181,20 @@ pub enum OurKey { Pr(char), Ctrl(char), FunKey(u8), - Backspace, Return, Escape, Space, - Up, Down, Left, Right, - PgUp, PgDn, Home, End, Ins, Del, + Backspace, + Return, + Escape, + Space, + Up, + Down, + Left, + Right, + PgUp, + PgDn, + Home, + End, + Ins, + Del, } #[derive(Debug)] @@ -211,18 +242,20 @@ impl From for TuiError { } impl std::fmt::Display for TuiError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> - Result<(), std::fmt::Error> - { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> Result<(), std::fmt::Error> { write!(f, "{}", self.message) } } impl Tui { - pub fn run(cfgloc: &ConfigLocation, readonly: bool, - logfile: Option) -> - Result<(), TuiError> - { + pub fn run( + cfgloc: &ConfigLocation, + readonly: bool, + logfile: Option, + ) -> Result<(), TuiError> { let (sender, receiver) = std::sync::mpsc::sync_channel(1); let input_sender = sender.clone(); @@ -284,12 +317,14 @@ impl Tui { KeyCode::Char(c) => { let initial = if ('\0'..' ').contains(&c) { Some(OurKey::Ctrl( - char::from_u32((c as u32) + 0x40).unwrap())) + char::from_u32((c as u32) + 0x40).unwrap(), + )) } else if ('\u{80}'..'\u{A0}').contains(&c) { None } else if ev.modifiers.contains(KeyModifiers::CONTROL) { Some(OurKey::Ctrl( - char::from_u32(((c as u32) & 0x1F) + 0x40).unwrap())) + char::from_u32(((c as u32) & 0x1F) + 0x40).unwrap(), + )) } else { Some(OurKey::Pr(c)) }; @@ -305,19 +340,21 @@ impl Tui { }; if let Some(main) = main { if ev.modifiers.contains(KeyModifiers::ALT) { - vec! { OurKey::Escape, main } + vec![OurKey::Escape, main] } else { - vec! { main } + vec![main] } } else { - vec! {} + vec![] } } fn run_inner(&mut self) -> Result<(), TuiError> { self.start_streaming_subthread(StreamId::User)?; - self.start_timing_subthread(Duration::from_secs(120), - SubthreadEvent::LDBCheckpointTimer)?; + self.start_timing_subthread( + Duration::from_secs(120), + SubthreadEvent::LDBCheckpointTimer, + )?; // Now fetch the two basic feeds - home and mentions. Most // importantly, getting the mentions feed started means that @@ -326,7 +363,8 @@ impl Tui { // We must do this _after_ starting the stream listener, so // that we won't miss a notification due to a race condition. self.client.fetch_feed(&FeedId::Home, FeedExtend::Initial)?; - self.client.fetch_feed(&FeedId::Mentions, FeedExtend::Initial)?; + self.client + .fetch_feed(&FeedId::Mentions, FeedExtend::Initial)?; // Now we've fetched the mentions feed, see if we need to beep // and throw the user into the mentions activity immediately @@ -338,28 +376,35 @@ impl Tui { self.main_loop() } - fn start_streaming_subthread(&mut self, id: StreamId) - -> Result<(), TuiError> - { + fn start_streaming_subthread( + &mut self, + id: StreamId, + ) -> Result<(), TuiError> { let sender = self.subthread_sender.clone(); self.client.start_streaming_thread( - &id, Box::new(move |update| { - if sender.send( - SubthreadEvent::StreamEv(update).clone()).is_err() { + &id, + Box::new(move |update| { + if sender + .send(SubthreadEvent::StreamEv(update).clone()) + .is_err() + { // It would be nice to do something about this // error, but what _can_ we do? We can hardly send // an error notification back to the main thread, // because that communication channel is just what // we've had a problem with. } - }))?; + }), + )?; Ok(()) } - fn start_timing_subthread(&mut self, dur: Duration, ev: SubthreadEvent) - -> Result<(), TuiError> - { + fn start_timing_subthread( + &mut self, + dur: Duration, + ev: SubthreadEvent, + ) -> Result<(), TuiError> { let sender = self.subthread_sender.clone(); let _joinhandle = std::thread::spawn(move || { loop { @@ -395,30 +440,32 @@ impl Tui { // Repeating the whole match on PhysicalAction branches in // the TermEv and StreamEv branches would be worse! - enum Todo { Keypress(OurKey), Stream(HashSet) } + enum Todo { + Keypress(OurKey), + Stream(HashSet), + } let todos = match self.subthread_receiver.recv() { Err(e) => break 'outer Err(e.into()), - Ok(SubthreadEvent::TermEv(ev)) => { - match ev { - Event::Key(key) => { - if key.kind == KeyEventKind::Press { - state.new_event(); - Self::translate_keypress(key).into_iter() - .map(|key| Todo::Keypress(key)) - .collect() - } else { - Vec::new() - } + Ok(SubthreadEvent::TermEv(ev)) => match ev { + Event::Key(key) => { + if key.kind == KeyEventKind::Press { + state.new_event(); + Self::translate_keypress(key) + .into_iter() + .map(|key| Todo::Keypress(key)) + .collect() + } else { + Vec::new() } - _ => Vec::new(), } - } + _ => Vec::new(), + }, Ok(SubthreadEvent::StreamEv(update)) => { match self.client.process_stream_update(update) { Ok(feeds_updated) => { - vec! { Todo::Stream(feeds_updated) } + vec![Todo::Stream(feeds_updated)] } // FIXME: errors here should go in the Error Log @@ -433,10 +480,11 @@ impl Tui { for todo in todos { let physact = match todo { - Todo::Keypress(ourkey) => state.handle_keypress( - ourkey, &mut self.client), - Todo::Stream(feeds_updated) => state.handle_feed_updates( - feeds_updated, &mut self.client), + Todo::Keypress(ourkey) => { + state.handle_keypress(ourkey, &mut self.client) + } + Todo::Stream(feeds_updated) => state + .handle_feed_updates(feeds_updated, &mut self.client), }; match physact { @@ -458,7 +506,7 @@ impl Tui { #[derive(Debug)] pub enum CursorPosition { - None, // cursor is hidden + None, // cursor is hidden End, // cursor at the end of the last drawn line (quite common in this UI) At(usize, usize), // (x,y) } @@ -491,22 +539,39 @@ pub struct SavedFilePos { impl From for SavedFilePos { fn from(file_pos: FilePosition) -> Self { - SavedFilePos { file_pos: Some(file_pos), latest_read_id: None } + SavedFilePos { + file_pos: Some(file_pos), + latest_read_id: None, + } } } pub trait ActivityState { fn resize(&mut self, _w: usize, _h: usize) {} - fn draw(&self, w: usize, h: usize) -> - (Vec, CursorPosition); - fn handle_keypress(&mut self, key: OurKey, client: &mut Client) -> - LogicalAction; - fn handle_feed_updates(&mut self, _feeds_updated: &HashSet, - _client: &mut Client) {} - fn save_file_position(&self) -> Option<(FeedId, SavedFilePos)> { None } - fn got_search_expression(&mut self, _dir: SearchDirection, _regex: String) - -> LogicalAction - { + fn draw( + &self, + w: usize, + h: usize, + ) -> (Vec, CursorPosition); + fn handle_keypress( + &mut self, + key: OurKey, + client: &mut Client, + ) -> LogicalAction; + fn handle_feed_updates( + &mut self, + _feeds_updated: &HashSet, + _client: &mut Client, + ) { + } + fn save_file_position(&self) -> Option<(FeedId, SavedFilePos)> { + None + } + fn got_search_expression( + &mut self, + _dir: SearchDirection, + _regex: String, + ) -> LogicalAction { panic!("a trait returning GetSearchExpression should fill this in"); } } @@ -537,8 +602,11 @@ impl TuiLogicalState { } } - fn draw_frame(&mut self, area: Rect, buf: &mut Buffer) - -> Option<(usize, usize)> { + fn draw_frame( + &mut self, + area: Rect, + buf: &mut Buffer, + ) -> Option<(usize, usize)> { let (w, h) = (area.width as usize, area.height as usize); if self.last_area != Some(area) { @@ -599,24 +667,29 @@ impl TuiLogicalState { self.activity_stack.new_event(); } - fn handle_keypress(&mut self, key: OurKey, client: &mut Client) -> - PhysicalAction - { + fn handle_keypress( + &mut self, + key: OurKey, + client: &mut Client, + ) -> PhysicalAction { let mut logact = match key { // Central handling of [ESC]: it _always_ goes to the // utilities menu, from any UI context at all. - OurKey::Escape => LogicalAction::Goto( - UtilityActivity::UtilsMenu.into()), + OurKey::Escape => { + LogicalAction::Goto(UtilityActivity::UtilsMenu.into()) + } // ^L forces a full screen redraw, in case your terminal // was corrupted by extraneous output. And again it should // do it absolutely anywhere. OurKey::Ctrl('L') => return PhysicalAction::Refresh, - _ => if let Some(ref mut state) = self.overlay_activity_state { - state.handle_keypress(key, client) - } else { - self.activity_state.handle_keypress(key, client) + _ => { + if let Some(ref mut state) = self.overlay_activity_state { + state.handle_keypress(key, client) + } else { + self.activity_state.handle_keypress(key, client) + } } }; @@ -628,63 +701,70 @@ impl TuiLogicalState { LogicalAction::Goto(activity) => { self.activity_stack.goto(activity); self.changed_activity(client, None, false); - break PhysicalAction::Nothing + break PhysicalAction::Nothing; } LogicalAction::Pop => { self.activity_stack.pop(); self.changed_activity(client, None, false); - break PhysicalAction::Nothing + break PhysicalAction::Nothing; } LogicalAction::PopOverlaySilent => { self.pop_overlay_activity(); - break PhysicalAction::Nothing + break PhysicalAction::Nothing; } LogicalAction::PopOverlayBeep => { self.pop_overlay_activity(); - break PhysicalAction::Beep + break PhysicalAction::Beep; } LogicalAction::GotSearchExpression(dir, regex) => { self.pop_overlay_activity(); self.activity_state.got_search_expression(dir, regex) } - LogicalAction::Error(_) => - break PhysicalAction::Beep, // FIXME: Error Log + LogicalAction::Error(_) => break PhysicalAction::Beep, // FIXME: Error Log LogicalAction::PostComposed(post) => { let newact = match self.activity_stack.top() { Activity::NonUtil( - NonUtilityActivity::ComposeToplevel) => - NonUtilityActivity::PostComposeMenu.into(), - Activity::Util(UtilityActivity::ComposeReply(id)) => - UtilityActivity::PostReplyMenu(id).into(), + NonUtilityActivity::ComposeToplevel, + ) => NonUtilityActivity::PostComposeMenu.into(), + Activity::Util(UtilityActivity::ComposeReply(id)) => { + UtilityActivity::PostReplyMenu(id).into() + } act => panic!("can't postcompose {act:?}"), }; self.activity_stack.chain_to(newact); self.changed_activity(client, Some(post), false); - break PhysicalAction::Nothing + break PhysicalAction::Nothing; } LogicalAction::PostReEdit(post) => { let newact = match self.activity_stack.top() { - Activity::NonUtil(NonUtilityActivity::PostComposeMenu) => - NonUtilityActivity::ComposeToplevel.into(), - Activity::Util(UtilityActivity::PostReplyMenu(id)) => - UtilityActivity::ComposeReply(id).into(), + Activity::NonUtil( + NonUtilityActivity::PostComposeMenu, + ) => NonUtilityActivity::ComposeToplevel.into(), + Activity::Util(UtilityActivity::PostReplyMenu(id)) => { + UtilityActivity::ComposeReply(id).into() + } act => panic!("can't reedit {act:?}"), }; self.activity_stack.chain_to(newact); self.changed_activity(client, Some(post), false); - break PhysicalAction::Nothing + break PhysicalAction::Nothing; } }; } } - fn handle_feed_updates(&mut self, feeds_updated: HashSet, - client: &mut Client) -> PhysicalAction { - self.activity_state.handle_feed_updates(&feeds_updated, client); + fn handle_feed_updates( + &mut self, + feeds_updated: HashSet, + client: &mut Client, + ) -> PhysicalAction { + self.activity_state + .handle_feed_updates(&feeds_updated, client); if feeds_updated.contains(&FeedId::Mentions) { if self.activity_stack.top().throw_into_mentions() { - self.activity_stack.goto(UtilityActivity::ReadMentions.into()); + self.activity_stack + .goto(UtilityActivity::ReadMentions.into()); self.changed_activity(client, None, true); } @@ -698,7 +778,9 @@ impl TuiLogicalState { fn check_startup_mentions(&mut self, client: &mut Client) -> bool { let feedid = FeedId::Mentions; let last_id = client.borrow_feed(&feedid).ids.back(); - let last_read_mention = self.file_positions.get(&feedid) + let last_read_mention = self + .file_positions + .get(&feedid) .and_then(|sfp| sfp.latest_read_id.clone()); if let Some(read) = last_read_mention { @@ -718,7 +800,8 @@ impl TuiLogicalState { return false; } - self.activity_stack.goto(UtilityActivity::ReadMentions.into()); + self.activity_stack + .goto(UtilityActivity::ReadMentions.into()); self.changed_activity(client, None, true); true } @@ -727,7 +810,8 @@ impl TuiLogicalState { if let Some((feed_id, saved_pos)) = self.activity_state.save_file_position() { - let changed = self.file_positions.get(&feed_id) != Some(&saved_pos); + let changed = + self.file_positions.get(&feed_id) != Some(&saved_pos); self.file_positions.insert(feed_id, saved_pos); if changed { // FIXME: maybe suddenly change our mind and go to the @@ -737,14 +821,23 @@ impl TuiLogicalState { } } - fn changed_activity(&mut self, client: &mut Client, post: Option, - is_interrupt: bool) { + fn changed_activity( + &mut self, + client: &mut Client, + post: Option, + is_interrupt: bool, + ) { self.checkpoint_ldb(); self.activity_state = self.new_activity_state( - self.activity_stack.top(), client, post, is_interrupt); + self.activity_stack.top(), + client, + post, + is_interrupt, + ); self.overlay_activity_state = match self.activity_stack.overlay() { - Some(activity) => - Some(self.new_activity_state(activity, client, None, false)), + Some(activity) => { + Some(self.new_activity_state(activity, client, None, false)) + } None => None, }; if let Some(area) = self.last_area { @@ -761,59 +854,85 @@ impl TuiLogicalState { self.overlay_activity_state = None; } - fn new_activity_state(&self, activity: Activity, client: &mut Client, - post: Option, is_interrupt: bool) - -> Box - { + fn new_activity_state( + &self, + activity: Activity, + client: &mut Client, + post: Option, + is_interrupt: bool, + ) -> Box { let result = match activity { - Activity::NonUtil(NonUtilityActivity::MainMenu) => - Ok(main_menu(client)), - Activity::Util(UtilityActivity::UtilsMenu) => - Ok(utils_menu(client)), - Activity::Util(UtilityActivity::ExitMenu) => - Ok(exit_menu()), - Activity::Util(UtilityActivity::LogsMenu1) => - Ok(logs_menu_1()), - Activity::Util(UtilityActivity::LogsMenu2) => - Ok(logs_menu_2()), - Activity::NonUtil(NonUtilityActivity::HomeTimelineFile) => - home_timeline(&self.file_positions, - self.unfolded_posts.clone(), client), - Activity::NonUtil(NonUtilityActivity::PublicTimelineFile) => - public_timeline(&self.file_positions, - self.unfolded_posts.clone(), client), - Activity::NonUtil(NonUtilityActivity::LocalTimelineFile) => - local_timeline(&self.file_positions, - self.unfolded_posts.clone(), client), - Activity::NonUtil(NonUtilityActivity::HashtagTimeline(ref id)) => - hashtag_timeline(self.unfolded_posts.clone(), client, id), - Activity::Util(UtilityActivity::ReadMentions) => - mentions(&self.file_positions, self.unfolded_posts.clone(), - client, is_interrupt), - Activity::Util(UtilityActivity::EgoLog) => - ego_log(&self.file_positions, client), - Activity::Overlay(OverlayActivity::GetUserToExamine) => - Ok(get_user_to_examine()), - Activity::Overlay(OverlayActivity::GetPostIdToRead) => - Ok(get_post_id_to_read()), - Activity::Overlay(OverlayActivity::GetHashtagToRead) => - Ok(get_hashtag_to_read()), - Activity::Overlay(OverlayActivity::GetSearchExpression(dir)) => - Ok(get_search_expression(dir)), - Activity::Util(UtilityActivity::ExamineUser(ref name)) => - examine_user(client, name), - Activity::Util(UtilityActivity::InfoStatus(ref id)) => - view_single_post(self.unfolded_posts.clone(), client, id), + Activity::NonUtil(NonUtilityActivity::MainMenu) => { + Ok(main_menu(client)) + } + Activity::Util(UtilityActivity::UtilsMenu) => { + Ok(utils_menu(client)) + } + Activity::Util(UtilityActivity::ExitMenu) => Ok(exit_menu()), + Activity::Util(UtilityActivity::LogsMenu1) => Ok(logs_menu_1()), + Activity::Util(UtilityActivity::LogsMenu2) => Ok(logs_menu_2()), + Activity::NonUtil(NonUtilityActivity::HomeTimelineFile) => { + home_timeline( + &self.file_positions, + self.unfolded_posts.clone(), + client, + ) + } + Activity::NonUtil(NonUtilityActivity::PublicTimelineFile) => { + public_timeline( + &self.file_positions, + self.unfolded_posts.clone(), + client, + ) + } + Activity::NonUtil(NonUtilityActivity::LocalTimelineFile) => { + local_timeline( + &self.file_positions, + self.unfolded_posts.clone(), + client, + ) + } + Activity::NonUtil(NonUtilityActivity::HashtagTimeline(ref id)) => { + hashtag_timeline(self.unfolded_posts.clone(), client, id) + } + Activity::Util(UtilityActivity::ReadMentions) => mentions( + &self.file_positions, + self.unfolded_posts.clone(), + client, + is_interrupt, + ), + Activity::Util(UtilityActivity::EgoLog) => { + ego_log(&self.file_positions, client) + } + Activity::Overlay(OverlayActivity::GetUserToExamine) => { + Ok(get_user_to_examine()) + } + Activity::Overlay(OverlayActivity::GetPostIdToRead) => { + Ok(get_post_id_to_read()) + } + Activity::Overlay(OverlayActivity::GetHashtagToRead) => { + Ok(get_hashtag_to_read()) + } + Activity::Overlay(OverlayActivity::GetSearchExpression(dir)) => { + Ok(get_search_expression(dir)) + } + Activity::Util(UtilityActivity::ExamineUser(ref name)) => { + examine_user(client, name) + } + Activity::Util(UtilityActivity::InfoStatus(ref id)) => { + view_single_post(self.unfolded_posts.clone(), client, id) + } Activity::NonUtil(NonUtilityActivity::ComposeToplevel) => (|| { let post = match post { Some(post) => post, None => Post::new(client)?, }; compose_post(client, post) - })(), - Activity::NonUtil(NonUtilityActivity::PostComposeMenu) => - Ok(post_menu(post.expect( - "how did we get here without a Post?"))), + })( + ), + Activity::NonUtil(NonUtilityActivity::PostComposeMenu) => Ok( + post_menu(post.expect("how did we get here without a Post?")), + ), Activity::Util(UtilityActivity::ComposeReply(ref id)) => { let post = match post { Some(post) => Ok(post), @@ -824,25 +943,39 @@ impl TuiLogicalState { Err(e) => Err(e), } } - Activity::Util(UtilityActivity::PostReplyMenu(_)) => - Ok(post_menu(post.expect( - "how did we get here without a Post?"))), - Activity::Util(UtilityActivity::ThreadFile(ref id, full)) => - view_thread(self.unfolded_posts.clone(), client, id, full), - Activity::Util(UtilityActivity::ListStatusFavouriters(ref id)) => - list_status_favouriters(client, id), - Activity::Util(UtilityActivity::ListStatusBoosters(ref id)) => - list_status_boosters(client, id), - Activity::Util(UtilityActivity::ListUserFollowers(ref id)) => - list_user_followers(client, id), - Activity::Util(UtilityActivity::ListUserFollowees(ref id)) => - list_user_followees(client, id), + Activity::Util(UtilityActivity::PostReplyMenu(_)) => Ok( + post_menu(post.expect("how did we get here without a Post?")), + ), + Activity::Util(UtilityActivity::ThreadFile(ref id, full)) => { + view_thread(self.unfolded_posts.clone(), client, id, full) + } + Activity::Util(UtilityActivity::ListStatusFavouriters(ref id)) => { + list_status_favouriters(client, id) + } + Activity::Util(UtilityActivity::ListStatusBoosters(ref id)) => { + list_status_boosters(client, id) + } + Activity::Util(UtilityActivity::ListUserFollowers(ref id)) => { + list_user_followers(client, id) + } + Activity::Util(UtilityActivity::ListUserFollowees(ref id)) => { + list_user_followees(client, id) + } Activity::NonUtil(NonUtilityActivity::UserPosts( - ref user, boosts, replies)) => - user_posts(&self.file_positions, self.unfolded_posts.clone(), - client, user, boosts, replies), - Activity::Util(UtilityActivity::UserOptions(ref id)) => - user_options_menu(client, id), + ref user, + boosts, + replies, + )) => user_posts( + &self.file_positions, + self.unfolded_posts.clone(), + client, + user, + boosts, + replies, + ), + Activity::Util(UtilityActivity::UserOptions(ref id)) => { + user_options_menu(client, id) + } }; result.expect("FIXME: need to implement the Error Log here") @@ -878,8 +1011,9 @@ impl TuiLogicalState { let filename = self.cfgloc.get_path("ldb"); let load_result = std::fs::read_to_string(&filename); - if load_result.as_ref().is_err_and( - |e| e.kind() == std::io::ErrorKind::NotFound) + if load_result + .as_ref() + .is_err_and(|e| e.kind() == std::io::ErrorKind::NotFound) { // Most errors are errors, but if the LDB file simply // doesn't exist, that's fine (we may be being run for the @@ -915,8 +1049,9 @@ impl TuiLogicalState { latest_read_id: Some(val), }); } - hash_map::Entry::Occupied(mut e) => - e.get_mut().latest_read_id = Some(val), + hash_map::Entry::Occupied(mut e) => { + e.get_mut().latest_read_id = Some(val) + } } } } diff --git a/src/types.rs b/src/types.rs index b1c6da1..20b8d40 100644 --- a/src/types.rs +++ b/src/types.rs @@ -28,18 +28,21 @@ pub struct ApproxDate(pub NaiveDate); impl<'de> Deserialize<'de> for ApproxDate { fn deserialize(deserializer: D) -> Result - where D: serde::Deserializer<'de> + where + D: serde::Deserializer<'de>, { struct StrVisitor; impl<'de> serde::de::Visitor<'de> for StrVisitor { type Value = ApproxDate; - fn expecting(&self, formatter: &mut std::fmt::Formatter) - -> std::fmt::Result - { + fn expecting( + &self, + formatter: &mut std::fmt::Formatter, + ) -> std::fmt::Result { formatter.write_str("a date in ISO 8601 format") } fn visit_str(self, orig_value: &str) -> Result - where E: serde::de::Error + where + E: serde::de::Error, { let value = match orig_value.split_once('T') { Some((before, _after)) => before, @@ -48,11 +51,12 @@ impl<'de> Deserialize<'de> for ApproxDate { match NaiveDate::parse_from_str(value, "%Y-%m-%d") { Ok(date) => Ok(ApproxDate(date)), Err(_) => Err(E::custom(format!( - "couldn't get an ISO 8601 date from '{orig_value}'"))), + "couldn't get an ISO 8601 date from '{orig_value}'" + ))), } } } - deserializer.deserialize_str(StrVisitor{}) + deserializer.deserialize_str(StrVisitor {}) } } @@ -60,26 +64,34 @@ impl<'de> Deserialize<'de> for ApproxDate { fn test_approx_date() { // Works with just the YYYY-MM-DD date string let date: ApproxDate = serde_json::from_str("\"2023-09-20\"").unwrap(); - assert_eq!(date, ApproxDate( - NaiveDate::from_ymd_opt(2023, 9, 20).unwrap())); + assert_eq!( + date, + ApproxDate(NaiveDate::from_ymd_opt(2023, 9, 20).unwrap()) + ); // Works with a T00:00:00 suffix - let date: ApproxDate = serde_json::from_str( - "\"2023-09-20T00:00:00\"").unwrap(); - assert_eq!(date, ApproxDate( - NaiveDate::from_ymd_opt(2023, 9, 20).unwrap())); + let date: ApproxDate = + serde_json::from_str("\"2023-09-20T00:00:00\"").unwrap(); + assert_eq!( + date, + ApproxDate(NaiveDate::from_ymd_opt(2023, 9, 20).unwrap()) + ); // Works with that _and_ the Z for timezone - let date: ApproxDate = serde_json::from_str( - "\"2023-09-20T00:00:00Z\"").unwrap(); - assert_eq!(date, ApproxDate( - NaiveDate::from_ymd_opt(2023, 9, 20).unwrap())); + let date: ApproxDate = + serde_json::from_str("\"2023-09-20T00:00:00Z\"").unwrap(); + assert_eq!( + date, + ApproxDate(NaiveDate::from_ymd_opt(2023, 9, 20).unwrap()) + ); // Deserializing as an Option works too - let date: Option = serde_json::from_str( - "\"2023-09-20T00:00:00\"").unwrap(); - assert_eq!(date, Some(ApproxDate( - NaiveDate::from_ymd_opt(2023, 9, 20).unwrap()))); + let date: Option = + serde_json::from_str("\"2023-09-20T00:00:00\"").unwrap(); + assert_eq!( + date, + Some(ApproxDate(NaiveDate::from_ymd_opt(2023, 9, 20).unwrap())) + ); // And if you give it JSON 'null' in place of a string it gives None let date: Option = serde_json::from_str("null").unwrap(); @@ -155,41 +167,63 @@ pub struct Token { #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] pub enum Visibility { - #[serde(rename = "public")] Public, - #[serde(rename = "unlisted")] Unlisted, - #[serde(rename = "private")] Private, - #[serde(rename = "direct")] Direct, + #[serde(rename = "public")] + Public, + #[serde(rename = "unlisted")] + Unlisted, + #[serde(rename = "private")] + Private, + #[serde(rename = "direct")] + Direct, } impl Visibility { pub fn long_descriptions() -> [(Visibility, ColouredString); 4] { [ (Visibility::Public, ColouredString::uniform("public", 'f')), - (Visibility::Unlisted, ColouredString::plain( - "unlisted (visible but not shown in feeds)")), - (Visibility::Private, ColouredString::general( - "private (to followees and @mentioned users)", - "rrrrrrr ")), - (Visibility::Direct, ColouredString::general( - "direct (only to @mentioned users)", - "rrrrrr ")), + ( + Visibility::Unlisted, + ColouredString::plain( + "unlisted (visible but not shown in feeds)", + ), + ), + ( + Visibility::Private, + ColouredString::general( + "private (to followees and @mentioned users)", + "rrrrrrr ", + ), + ), + ( + Visibility::Direct, + ColouredString::general( + "direct (only to @mentioned users)", + "rrrrrr ", + ), + ), ] } } #[derive(Deserialize, Debug, PartialEq, Eq, Clone, Copy)] pub enum MediaType { - #[serde(rename = "unknown")] Unknown, - #[serde(rename = "image")] Image, - #[serde(rename = "gifv")] GifV, - #[serde(rename = "video")] Video, - #[serde(rename = "audio")] Audio, + #[serde(rename = "unknown")] + Unknown, + #[serde(rename = "image")] + Image, + #[serde(rename = "gifv")] + GifV, + #[serde(rename = "video")] + Video, + #[serde(rename = "audio")] + Audio, } #[derive(Deserialize, Debug, Clone)] pub struct MediaAttachment { pub id: String, - #[serde(rename="type")] pub mediatype: MediaType, + #[serde(rename = "type")] + pub mediatype: MediaType, pub url: String, pub preview_url: String, pub remote_url: Option, @@ -270,22 +304,33 @@ impl Status { #[derive(Deserialize, Debug, PartialEq, Eq, Clone, Copy)] pub enum NotificationType { - #[serde(rename = "mention")] Mention, - #[serde(rename = "status")] Status, - #[serde(rename = "reblog")] Reblog, - #[serde(rename = "follow")] Follow, - #[serde(rename = "follow_request")] FollowRequest, - #[serde(rename = "favourite")] Favourite, - #[serde(rename = "poll")] Poll, - #[serde(rename = "update")] Update, - #[serde(rename = "admin.sign_up")] AdminSignUp, - #[serde(rename = "admin.report")] AdminReport, + #[serde(rename = "mention")] + Mention, + #[serde(rename = "status")] + Status, + #[serde(rename = "reblog")] + Reblog, + #[serde(rename = "follow")] + Follow, + #[serde(rename = "follow_request")] + FollowRequest, + #[serde(rename = "favourite")] + Favourite, + #[serde(rename = "poll")] + Poll, + #[serde(rename = "update")] + Update, + #[serde(rename = "admin.sign_up")] + AdminSignUp, + #[serde(rename = "admin.report")] + AdminReport, } #[derive(Deserialize, Debug, Clone)] pub struct Notification { pub id: String, - #[serde(rename="type")] pub ntype: NotificationType, + #[serde(rename = "type")] + pub ntype: NotificationType, pub created_at: DateTime, pub account: Account, pub status: Option, @@ -332,7 +377,6 @@ pub struct InstanceConfig { pub struct Instance { pub domain: String, pub configuration: InstanceConfig, - // FIXME: lots of things are missing from here! }