chiark / gitweb /
Run through rustfmt.
authorSimon Tatham <anakin@pobox.com>
Wed, 17 Jan 2024 07:41:53 +0000 (07:41 +0000)
committerSimon Tatham <anakin@pobox.com>
Wed, 17 Jan 2024 07:41:53 +0000 (07:41 +0000)
If this is a widespread standard, I might as well start getting used
to it.

19 files changed:
.rustfmt.toml [new file with mode: 0644]
src/activity_stack.rs
src/auth.rs
src/client.rs
src/coloured_string.rs
src/config.rs
src/editor.rs
src/file.rs
src/html.rs
src/lib.rs
src/login.rs
src/main.rs
src/menu.rs
src/options.rs
src/posting.rs
src/scan_re.rs
src/text.rs
src/tui.rs
src/types.rs

diff --git a/.rustfmt.toml b/.rustfmt.toml
new file mode 100644 (file)
index 0000000..a1ffd27
--- /dev/null
@@ -0,0 +1 @@
+max_width = 79
index 3ebdd12420817e09ba1b450bdf582044c2c08a7d..34c5fbfefc931d1bfb105c5944d0e59e4bb91d99 100644 (file)
@@ -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<NonUtilityActivity> for Activity {
-    fn from(value: NonUtilityActivity) -> Self { Activity::NonUtil(value) }
+    fn from(value: NonUtilityActivity) -> Self {
+        Activity::NonUtil(value)
+    }
 }
 impl From<UtilityActivity> for Activity {
-    fn from(value: UtilityActivity) -> Self { Activity::Util(value) }
+    fn from(value: UtilityActivity) -> Self {
+        Activity::Util(value)
+    }
 }
 impl From<OverlayActivity> 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,
+        }
+    );
 }
index d65ad7f53e5fd63eece3115ea3989188b34d497d..8f1a009e0a09e6b7827fe78d68944fe5538b27c2 100644 (file)
@@ -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<Self, AuthError> {
         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)
index 4568a9adec7d231a88c6fb49461be156b25d3f27..00e382e4fa0a7bbed8930b408d716df2305c43aa 100644 (file)
@@ -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<String>,
-    }
+    },
 }
 
 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<AuthError> for ClientError {
-    fn from(err: AuthError) -> Self { ClientError::Auth(err) }
+    fn from(err: AuthError) -> Self {
+        ClientError::Auth(err)
+    }
 }
 
 impl From<reqwest::Error> 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<T>(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<reqwest::blocking::Client, ClientError> {
@@ -294,8 +327,8 @@ pub fn reqwest_client() -> Result<reqwest::blocking::Client, ClientError> {
             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<String> {
         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<File>) {
         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<Relationship, ClientError>
-    {
+    pub fn account_relationship_by_id(
+        &mut self,
+        id: &str,
+    ) -> Result<Relationship, ClientError> {
         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<Relationship> = 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<Status, ClientError> {
@@ -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<Notification, ClientError>
-    {
+    pub fn notification_by_id(
+        &mut self,
+        id: &str,
+    ) -> Result<Notification, ClientError> {
         if let Some(not) = self.notifications.get(id) {
             let mut not = not.clone();
             if let Some(ac) = self.accounts.get(&not.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 {}", &not.id)));
+                url,
+                format!("request returned wrong notification id {}", &not.id),
+            ));
         }
         self.cache_notification(&not);
         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<bool, ClientError>
-    {
+    pub fn fetch_feed(
+        &mut self,
+        id: &FeedId,
+        ext: FeedExtend,
+    ) -> Result<bool, ClientError> {
         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<String> = 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<Status> = 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<Notification> = match serde_json::from_str(
-                    &body) {
-                    Ok(nots) => Ok(nots),
-                    Err(e) => {
-                        Err(ClientError::UrlError(url.clone(), e.to_string()))
-                    }
-                }?;
+                let mut nots: Vec<Notification> =
+                    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<Account> = 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<Recv: Fn(StreamUpdate) + Send + 'static>(
-        &mut self, id: &StreamId, receiver: Box<Recv>) ->
-        Result<(), ClientError>
-    {
+        &mut self,
+        id: &StreamId,
+        receiver: Box<Recv>,
+    ) -> 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<HashSet<FeedId>, ClientError>
-    {
+    pub fn process_stream_update(
+        &mut self,
+        up: StreamUpdate,
+    ) -> Result<HashSet<FeedId>, 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<Account, ClientError>
-    {
-        let (url, rsp) = self.api_request(
-            Req::get("v1/accounts/lookup").param("acct", name))?;
+    pub fn account_by_name(
+        &mut self,
+        name: &str,
+    ) -> Result<Account, ClientError> {
+        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<Context, ClientError>
-    {
-        let (url, rsp) = self.api_request(Req::get(
-            &format!("v1/statuses/{id}/context")))?;
+    pub fn status_context(
+        &mut self,
+        id: &str,
+    ) -> Result<Context, ClientError> {
+        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<Item = usize>)
-                        -> Result<(), ClientError>
-    {
+    pub fn vote_in_poll(
+        &mut self,
+        id: &str,
+        choices: impl Iterator<Item = usize>,
+    ) -> 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(())
index eb3906ca4a2a4adfea7ab9aa6ce8435149ca3a76..e6a8b80ab61a44da4bf990e651d8074b24f0caf4 100644 (file)
@@ -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<T: ColouredStringCommon, U: ColouredStringCommon>
-        (lhs: T, rhs: U) -> ColouredString
-    {
+    fn concat<T: ColouredStringCommon, U: ColouredStringCommon>(
+        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);
 
index d60ed4f1a477e2f244849cc931797f7c14f1e9da..3ea64a0f7f7d46a440693101132770d512b465c4 100644 (file)
@@ -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,
index a8335b312ee4cfe718ed4d40e1f2f456b4175263..ef4b4e36534eb5b075930d86de1b10e09b66f3c1 100644 (file)
@@ -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<usize>) {
         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 "<cde"
     // followed by the cursor.
@@ -570,16 +609,20 @@ fn test_single_line_visibility() {
     assert_eq!(sle.core.point, 5);
     sle.update_first_visible();
     assert_eq!(sle.first_visible, 2);
-    assert_eq!(sle.draw(sle.width),
-               (ColouredString::general("<cde", ">   "), Some(4)));
+    assert_eq!(
+        sle.draw(sle.width),
+        (ColouredString::general("<cde", ">   "), Some(4))
+    );
 
     // And another two characters move that on in turn: "<efg" + cursor.
     sle.core.insert("fg");
     assert_eq!(sle.core.point, 7);
     sle.update_first_visible();
     assert_eq!(sle.first_visible, 4);
-    assert_eq!(sle.draw(sle.width),
-               (ColouredString::general("<efg", ">   "), Some(4)));
+    assert_eq!(
+        sle.draw(sle.width),
+        (ColouredString::general("<efg", ">   "), 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("<efg", ">   "), Some(1)));
+    assert_eq!(
+        sle.draw(sle.width),
+        (ColouredString::general("<efg", ">   "), 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("<defg", ">    "), Some(1)));
+    assert_eq!(
+        sle.draw(sle.width),
+        (ColouredString::general("<defg", ">    "), 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("<cde>", ">   >"), Some(1)));
+    assert_eq!(
+        sle.draw(sle.width),
+        (ColouredString::general("<cde>", ">   >"), Some(1))
+    );
 
     // The one after that would naively leave us at "<bcd>" 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<dyn Fn(&str, &mut Client) -> LogicalAction>) -> Self
-    {
+    fn new(
+        prompt: ColouredString,
+        result: Box<dyn Fn(&str, &mut Client) -> 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<ColouredString>, CursorPosition)
-    {
+    fn draw(
+        &self,
+        w: usize,
+        _h: usize,
+    ) -> (Vec<ColouredString>, 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<String> {
@@ -712,8 +774,7 @@ pub struct EditableMenuLine<Data: EditableMenuLineData> {
 }
 
 impl<Data: EditableMenuLineData> EditableMenuLine<Data> {
-    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<Data: EditableMenuLineData> EditableMenuLine<Data> {
         }
     }
 
-    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<Data: EditableMenuLineData> EditableMenuLine<Data> {
             }
             text
         } else {
-            self.menuline.render_oneline(width, None, &DefaultDisplayStyle)
+            self.menuline
+                .render_oneline(width, None, &DefaultDisplayStyle)
         }
     }
 
@@ -771,8 +839,8 @@ impl<Data: EditableMenuLineData> EditableMenuLine<Data> {
     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<Data: EditableMenuLineData> EditableMenuLine<Data> {
             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<Data: EditableMenuLineData> EditableMenuLine<Data> {
         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<Data: EditableMenuLineData> MenuKeypressLineGeneral
-for EditableMenuLine<Data> {
+    for EditableMenuLine<Data>
+{
     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<dyn ActivityState> {
             } 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<dyn ActivityState> {
                     Err(_) => LogicalAction::PopOverlayBeep,
                 }
             }
-        })
+        }),
     ))
 }
 
@@ -853,7 +930,8 @@ pub fn get_post_id_to_read() -> Box<dyn ActivityState> {
             } 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<dyn ActivityState> {
                     Err(_) => LogicalAction::PopOverlayBeep,
                 }
             }
-        })
+        }),
     ))
 }
 
@@ -875,10 +953,10 @@ pub fn get_hashtag_to_read() -> Box<dyn ActivityState> {
                 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<dyn ActivityState> {
         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<InReplyToLine>, post: Post) -> Self
-    {
+    fn new(
+        conf: InstanceStatusConfig,
+        header: FileHeader,
+        irt: Option<InReplyToLine>,
+        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<ColouredString> {
         // 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::<Vec<_>>());
-    assert_eq!(composer.layout(4),
-               (0..=3).map(|i| ComposeLayoutCell { pos: i, x: i, y: 0 })
-               .collect::<Vec<_>>());
+    assert_eq!(
+        composer.layout(10),
+        (0..=3)
+            .map(|i| ComposeLayoutCell { pos: i, x: i, y: 0 })
+            .collect::<Vec<_>>()
+    );
+    assert_eq!(
+        composer.layout(4),
+        (0..=3)
+            .map(|i| ComposeLayoutCell { pos: i, x: i, y: 0 })
+            .collect::<Vec<_>>()
+    );
 
     // 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::<Vec<_>>());
+        (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::<Vec<_>>()
+    );
 
     // 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::<Vec<_>>());
+        (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::<Vec<_>>()
+    );
 
     // 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::<Vec<_>>());
+        (0..=8)
+            .map(|i| ComposeLayoutCell { pos: i, x: i, y: 0 })
+            .collect::<Vec<_>>()
+    );
     // 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::<Vec<_>>());
+        (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::<Vec<_>>()
+    );
 }
 
 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<ColouredString>, CursorPosition)
-    {
+    fn draw(
+        &self,
+        w: usize,
+        h: usize,
+    ) -> (Vec<ColouredString>, 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<Box<dyn ActivityState>, ClientError>
-{
+pub fn compose_post(
+    client: &mut Client,
+    post: Post,
+) -> Result<Box<dyn ActivityState>, 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,
+    )))
 }
index 72932ed7eea33a5db0fad27a47826dd032338e22..548048a30240e776e637539b72a42f584e98b98d 100644 (file)
@@ -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<String>) -> Self { StaticSource { ids } }
+    fn singleton(id: String) -> Self {
+        StaticSource { ids: vec![id] }
+    }
+    fn vector(ids: Vec<String>) -> Self {
+        StaticSource { ids }
+    }
 }
 
 impl FileDataSource for StaticSource {
     fn get(&self, _client: &mut Client) -> (Vec<String>, 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<bool, ClientError> {
         Ok(false)
     }
-    fn updated(&self, _feeds_updated: &HashSet<FeedId>) -> bool { false }
-    fn extendable(&self) -> bool { false }
+    fn updated(&self, _feeds_updated: &HashSet<FeedId>) -> 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<Self::Item, ClientError>;
+    fn get_from_client(
+        id: &str,
+        client: &mut Client,
+    ) -> Result<Self::Item, ClientError>;
 
-    fn feed_id(&self) -> Option<&FeedId> { None }
+    fn feed_id(&self) -> Option<&FeedId> {
+        None
+    }
 }
 
 struct StatusFeedType {
     id: Option<FeedId>,
 }
 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<Self::Item, ClientError>
-    {
+    fn get_from_client(
+        id: &str,
+        client: &mut Client,
+    ) -> Result<Self::Item, ClientError> {
         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<Self::Item, ClientError>
-    {
+    fn get_from_client(
+        id: &str,
+        client: &mut Client,
+    ) -> Result<Self::Item, ClientError> {
         let not = client.notification_by_id(id)?;
         let st = &not.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<Self::Item, ClientError>
-    {
+    fn get_from_client(
+        id: &str,
+        client: &mut Client,
+    ) -> Result<Self::Item, ClientError> {
         let not = client.notification_by_id(id)?;
         Ok(NotificationLog::from_notification(&not, 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<Self::Item, ClientError>
-    {
+    fn get_from_client(
+        id: &str,
+        client: &mut Client,
+    ) -> Result<Self::Item, ClientError> {
         let ac = client.account_by_id(id)?;
         Ok(UserListEntry::from_account(&ac, client))
     }
@@ -224,7 +274,7 @@ struct FileContents<Type: FileType, Source: FileDataSource> {
     items: Vec<(String, Type::Item)>,
 }
 
-impl<Type: FileType, Source: FileDataSource> FileContents<Type,Source> {
+impl<Type: FileType, Source: FileDataSource> FileContents<Type, Source> {
     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<Type: FileType, Source: FileDataSource> FileContents<Type,Source> {
         self.origin - 1 - extcount
     }
     fn extender_index(&self) -> Option<isize> {
-        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<Type: FileType, Source: FileDataSource> FileContents<Type,Source> {
     }
 
     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<Type: FileType, Source: FileDataSource> FileContents<Type,Source> {
         // 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<String>,
@@ -334,17 +397,19 @@ impl FileDisplayStyles {
 
 impl DisplayStyleGetter for FileDisplayStyles {
     fn poll_options(&self, id: &str) -> Option<HashSet<usize>> {
-        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<Type: FileType, Source: FileDataSource> {
 }
 
 impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
-    fn new(client: &mut Client, source: Source, desc: ColouredString,
-           file_desc: Type, saved_pos: Option<&SavedFilePos>,
-           unfolded: Option<Rc<RefCell<HashSet<String>>>>, show_new: bool) ->
-        Result<Self, ClientError>
-    {
+    fn new(
+        client: &mut Client,
+        source: Source,
+        desc: ColouredString,
+        file_desc: Type,
+        saved_pos: Option<&SavedFilePos>,
+        unfolded: Option<Rc<RefCell<HashSet<String>>>>,
+        show_new: bool,
+    ) -> Result<Self, ClientError> {
         source.init(client)?;
 
         let extender = if source.extendable() {
@@ -396,8 +465,10 @@ impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
         // 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<Type: FileType, Source: FileDataSource> File<Type, Source> {
         }
 
         // 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<Type: FileType, Source: FileDataSource> File<Type, Source> {
         Ok(ff)
     }
 
-    fn ensure_item_rendered(&mut self, index: isize, w: usize) ->
-        &Vec<ColouredString>
-    {
+    fn ensure_item_rendered(
+        &mut self,
+        index: isize,
+        w: usize,
+    ) -> &Vec<ColouredString> {
         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<Type: FileType, Source: FileDataSource> File<Type, Source> {
             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<usize> {
-        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<Type: FileType, Source: FileDataSource> File<Type, Source> {
     }
 
     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<Type: FileType, Source: FileDataSource> File<Type, Source> {
             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<Type: FileType, Source: FileDataSource> File<Type, Source> {
             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<Type: FileType, Source: FileDataSource> File<Type, Source> {
     }
 
     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<Type: FileType, Source: FileDataSource> File<Type, Source> {
         }
     }
 
-    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<Type: FileType, Source: FileDataSource> File<Type, Source> {
                     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<Type: FileType, Source: FileDataSource> File<Type, Source> {
                     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<Type: FileType, Source: FileDataSource> File<Type, Source> {
         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<Type: FileType, Source: FileDataSource> File<Type, Source> {
         }
     }
 
-    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<Type: FileType, Source: FileDataSource> File<Type, Source> {
         }
     }
 
-    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<Type: FileType, Source: FileDataSource> File<Type, Source> {
         };
 
         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<Type: FileType, Source: FileDataSource> File<Type, Source> {
 
         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<Type: FileType, Source: FileDataSource> File<Type, Source> {
         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<Type: FileType, Source: FileDataSource> File<Type, Source> {
     }
 
     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<Type: FileType, Source: FileDataSource> File<Type, Source> {
         LogicalAction::Nothing
     }
 
-    fn selected_id(&self, selection: Option<(isize, usize)>)
-                   -> Option<String>
-    {
+    fn selected_id(
+        &self,
+        selection: Option<(isize, usize)>,
+    ) -> Option<String> {
         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<Type: FileType, Source: FileDataSource> File<Type, Source> {
 
         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<Type: FileType, Source: FileDataSource> File<Type, Source> {
         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<Type: FileType, Source: FileDataSource> File<Type, Source> {
                     }
                 }
                 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<Type: FileType, Source: FileDataSource> File<Type, Source> {
                     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<Type: FileType, Source: FileDataSource> File<Type, Source> {
                     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<Type: FileType, Source: FileDataSource> File<Type, Source> {
     }
 }
 
-impl<Type: FileType, Source: FileDataSource>
-    ActivityState for File<Type, Source>
+impl<Type: FileType, Source: FileDataSource> ActivityState
+    for File<Type, Source>
 {
     fn resize(&mut self, w: usize, h: usize) {
         if self.last_size != Some((w, h)) {
@@ -1010,12 +1129,21 @@ impl<Type: FileType, Source: FileDataSource>
         self.after_setting_pos();
     }
 
-    fn draw(&self, w: usize, h: usize)
-            -> (Vec<ColouredString>, 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<ColouredString>, 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<Type: FileType, Source: FileDataSource>
         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<Type: FileType, Source: FileDataSource>
                 } 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<Type: FileType, Source: FileDataSource>
                 } 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<Type: FileType, Source: FileDataSource>
                 // 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<Type: FileType, Source: FileDataSource>
                     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<Type: FileType, Source: FileDataSource>
             }
             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<Type: FileType, Source: FileDataSource>
                             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<Type: FileType, Source: FileDataSource>
         (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<Type: FileType, Source: FileDataSource>
 
                 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<Type: FileType, Source: FileDataSource>
 
                 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<Type: FileType, Source: FileDataSource>
                     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<Type: FileType, Source: FileDataSource>
 
                 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<Type: FileType, Source: FileDataSource>
 
                 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<Type: FileType, Source: FileDataSource>
 
                 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<Type: FileType, Source: FileDataSource>
 
                 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<Type: FileType, Source: FileDataSource>
 
                 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<Type: FileType, Source: FileDataSource>
                     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<Type: FileType, Source: FileDataSource>
 
                 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<Type: FileType, Source: FileDataSource>
                     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<Type: FileType, Source: FileDataSource>
         action
     }
 
-    fn handle_feed_updates(&mut self, feeds_updated: &HashSet<FeedId>,
-                           client: &mut Client) {
+    fn handle_feed_updates(
+        &mut self,
+        feeds_updated: &HashSet<FeedId>,
+        client: &mut Client,
+    ) {
         if self.contents.source.updated(feeds_updated) {
             self.contents.update_items(client);
             self.ensure_enough_rendered();
@@ -1517,7 +1727,8 @@ impl<Type: FileType, Source: FileDataSource>
         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<Type: FileType, Source: FileDataSource>
         })
     }
 
-    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(&regex) {
             Ok(re) => {
                 self.search_direction = Some(dir);
@@ -1539,86 +1752,123 @@ impl<Type: FileType, Source: FileDataSource>
     }
 }
 
-pub fn home_timeline(file_positions: &HashMap<FeedId, SavedFilePos>,
-                     unfolded: Rc<RefCell<HashSet<String>>>,
-                     client: &mut Client) ->
-    Result<Box<dyn ActivityState>, ClientError>
-{
+pub fn home_timeline(
+    file_positions: &HashMap<FeedId, SavedFilePos>,
+    unfolded: Rc<RefCell<HashSet<String>>>,
+    client: &mut Client,
+) -> Result<Box<dyn ActivityState>, 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   <H>",
-            "HHHHHHHHHHHHHHHHHKH"), desc, pos, Some(unfolded), false)?;
+        client,
+        FeedSource::new(feed),
+        ColouredString::general("Home timeline   <H>", "HHHHHHHHHHHHHHHHHKH"),
+        desc,
+        pos,
+        Some(unfolded),
+        false,
+    )?;
     Ok(Box::new(file))
 }
 
-pub fn local_timeline(file_positions: &HashMap<FeedId, SavedFilePos>,
-                      unfolded: Rc<RefCell<HashSet<String>>>,
-                      client: &mut Client) ->
-    Result<Box<dyn ActivityState>, ClientError>
-{
+pub fn local_timeline(
+    file_positions: &HashMap<FeedId, SavedFilePos>,
+    unfolded: Rc<RefCell<HashSet<String>>>,
+    client: &mut Client,
+) -> Result<Box<dyn ActivityState>, 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   <L>",
-            "HHHHHHHHHHHHHHHHHHHHHHHHHKH"), desc, pos, Some(unfolded), false)?;
+            "HHHHHHHHHHHHHHHHHHHHHHHHHKH",
+        ),
+        desc,
+        pos,
+        Some(unfolded),
+        false,
+    )?;
     Ok(Box::new(file))
 }
 
-pub fn public_timeline(file_positions: &HashMap<FeedId, SavedFilePos>,
-                       unfolded: Rc<RefCell<HashSet<String>>>,
-                       client: &mut Client) ->
-    Result<Box<dyn ActivityState>, ClientError>
-{
+pub fn public_timeline(
+    file_positions: &HashMap<FeedId, SavedFilePos>,
+    unfolded: Rc<RefCell<HashSet<String>>>,
+    client: &mut Client,
+) -> Result<Box<dyn ActivityState>, 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   <P>",
-            "HHHHHHHHHHHHHHHHHHHKH"), desc, pos, Some(unfolded), false)?;
+            "HHHHHHHHHHHHHHHHHHHKH",
+        ),
+        desc,
+        pos,
+        Some(unfolded),
+        false,
+    )?;
     Ok(Box::new(file))
 }
 
-pub fn mentions(file_positions: &HashMap<FeedId, SavedFilePos>,
-                unfolded: Rc<RefCell<HashSet<String>>>,
-                client: &mut Client, is_interrupt: bool) ->
-    Result<Box<dyn ActivityState>, ClientError>
-{
+pub fn mentions(
+    file_positions: &HashMap<FeedId, SavedFilePos>,
+    unfolded: Rc<RefCell<HashSet<String>>>,
+    client: &mut Client,
+    is_interrupt: bool,
+) -> Result<Box<dyn ActivityState>, 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<FeedId, SavedFilePos>,
-               client: &mut Client) ->
-    Result<Box<dyn ActivityState>, ClientError>
-{
+pub fn ego_log(
+    file_positions: &HashMap<FeedId, SavedFilePos>,
+    client: &mut Client,
+) -> Result<Box<dyn ActivityState>, 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<FeedId, SavedFilePos>,
     unfolded: Rc<RefCell<HashSet<String>>>,
-    client: &mut Client, user: &str, boosts: Boosts, replies: Replies)
-    -> Result<Box<dyn ActivityState>, ClientError>
-{
+    client: &mut Client,
+    user: &str,
+    boosts: Boosts,
+    replies: Replies,
+) -> Result<Box<dyn ActivityState>, 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<Box<dyn ActivityState>, ClientError>
-{
+pub fn list_status_favouriters(
+    client: &mut Client,
+    id: &str,
+) -> Result<Box<dyn ActivityState>, 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<Box<dyn ActivityState>, ClientError>
-{
+pub fn list_status_boosters(
+    client: &mut Client,
+    id: &str,
+) -> Result<Box<dyn ActivityState>, 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<Box<dyn ActivityState>, ClientError>
-{
+pub fn list_user_followers(
+    client: &mut Client,
+    id: &str,
+) -> Result<Box<dyn ActivityState>, 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<Box<dyn ActivityState>, ClientError>
-{
+pub fn list_user_followees(
+    client: &mut Client,
+    id: &str,
+) -> Result<Box<dyn ActivityState>, 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<RefCell<HashSet<String>>>,
-                        client: &mut Client, tag: &str) ->
-    Result<Box<dyn ActivityState>, ClientError>
-{
+pub fn hashtag_timeline(
+    unfolded: Rc<RefCell<HashSet<String>>>,
+    client: &mut Client,
+    tag: &str,
+) -> Result<Box<dyn ActivityState>, 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<Self::Item, ClientError>
-    {
+    fn get_from_client(
+        id: &str,
+        client: &mut Client,
+    ) -> Result<Self::Item, ClientError> {
         let ac = client.account_by_id(id)?;
         ExamineUserDisplay::new(ac, client)
     }
 }
 
-pub fn examine_user(client: &mut Client, account_id: &str) ->
-    Result<Box<dyn ActivityState>, ClientError>
-{
+pub fn examine_user(
+    client: &mut Client,
+    account_id: &str,
+) -> Result<Box<dyn ActivityState>, 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<Self::Item, ClientError>
-    {
+    fn get_from_client(
+        id: &str,
+        client: &mut Client,
+    ) -> Result<Self::Item, ClientError> {
         let st = client.status_by_id(id)?;
         Ok(DetailedStatusDisplay::new(st, client))
     }
 }
 
-pub fn view_single_post(unfolded: Rc<RefCell<HashSet<String>>>,
-                        client: &mut Client, status_id: &str) ->
-    Result<Box<dyn ActivityState>, ClientError>
-{
+pub fn view_single_post(
+    unfolded: Rc<RefCell<HashSet<String>>>,
+    client: &mut Client,
+    status_id: &str,
+) -> Result<Box<dyn ActivityState>, 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<RefCell<HashSet<String>>>,
-                   client: &mut Client, start_id: &str, full: bool) ->
-    Result<Box<dyn ActivityState>, ClientError>
-{
+pub fn view_thread(
+    unfolded: Rc<RefCell<HashSet<String>>>,
+    client: &mut Client,
+    start_id: &str,
+    full: bool,
+) -> Result<Box<dyn ActivityState>, ClientError> {
     let mut make_vec = |id: &str| -> Result<Vec<String>, ClientError> {
         let ctx = client.status_context(id)?;
         let mut v = Vec::new();
@@ -1801,12 +2111,19 @@ pub fn view_thread(unfolded: Rc<RefCell<HashSet<String>>>,
     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))
 }
index 8119c97cefaa68318330392f602aabf2ab72d32f..f600aff92adcdc0b01947c773f2897d5cd5889a1 100644 (file)
@@ -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<String>)
-                -> Vec<TaggedLine<Self::Annotation>> {
+    fn finalise(
+        &mut self,
+        _links: Vec<String>,
+    ) -> Vec<TaggedLine<Self::Annotation>> {
         Vec::new()
     }
 }
 
 pub fn parse(html: &str) -> Result<RenderTree, html2text::Error> {
-    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<Vec<TaggedLine<Vec<char>>>, html2text::Error>
-{
-    let cfg = config::with_decorator(OurDecorator::new())
-        .max_wrap_width(wrapwidth);
+fn try_render(
+    rt: &RenderTree,
+    wrapwidth: usize,
+    fullwidth: usize,
+) -> Result<Vec<TaggedLine<Vec<char>>>, 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<Vec<char>>) -> ColouredString {
 }
 
 pub fn render(rt: &RenderTree, width: usize) -> Vec<ColouredString> {
-    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<String> {
@@ -225,11 +255,13 @@ pub fn list_urls(rt: &RenderTree) -> Vec<String> {
     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");
     }
 }
index 5d44182e8e2733bf98315a4393b0884b6dceb7f7..19e8b1d8b0f5672ded96e4494953ee2960dd9cea 100644 (file)
@@ -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<E: TopLevelErrorCandidate> From<E> for TopLevelError {
@@ -53,7 +56,9 @@ impl<E: TopLevelErrorCandidate> From<E> 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 {}
index 8a63d0362371cc07f2e0760b1d4afd29b928b8e7..b3017fc54c3a746dd8ba7aa29378fc32131692ff 100644 (file)
@@ -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<File>)
-           -> Result<Self, ClientError>
-    {
+    fn new(
+        instance_url: &str,
+        logfile: Option<File>,
+    ) -> Result<Self, ClientError> {
         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<reqwest::blocking::Response, ClientError>
-    {
-        let (rsp, log) = execute_and_log_request(
-            &self.client, req.build()?)?;
+    fn execute_request(
+        &mut self,
+        req: reqwest::blocking::RequestBuilder,
+    ) -> Result<reqwest::blocking::Response, ClientError> {
+        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<Token, ClientError>
-    {
+    fn get_token(
+        &mut self,
+        app: &Application,
+        toktype: AppTokenType,
+    ) -> Result<Token, ClientError> {
         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<T: for<'a> serde::Deserialize<'a>>(
-        &mut self, path: &str, token: &str) -> Result<T, ClientError>
-    {
-        let (url, req) = Req::get(path)
-            .build(&self.instance_url, &self.client, Some(token))?;
+        &mut self,
+        path: &str,
+        token: &str,
+    ) -> Result<T, ClientError> {
+        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<String, ClientError> {
         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<File>) ->
-    Result<(), TopLevelError>
-{
+pub fn login(
+    cfgloc: &ConfigLocation,
+    instance_url: &str,
+    logfile: Option<File>,
+) -> 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!
index 00df08d833b6117fc88b2526e71ab9e63f7f1168..383cb30d2d8fed4a499c50384ba7cd65d8389450 100644 (file)
@@ -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 {
index 2ca86d6880e9cf7708a622391892e0d66b9bcc31..2e010e7475f1111e3f44f3506e2f52ff6d20c2eb 100644 (file)
@@ -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<ColouredString>, CursorPosition) {
+    fn draw(
+        &self,
+        w: usize,
+        h: usize,
+    ) -> (Vec<ColouredString>, 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<dyn ActivityState> {
     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<dyn ActivityState> {
     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<dyn ActivityState> {
 
 pub fn utils_menu(client: &Client) -> Box<dyn ActivityState> {
     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<dyn ActivityState> {
     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<dyn ActivityState> {
     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<dyn ActivityState> {
     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())
 }
index 08d187f67741bab9fd0c6d1312d71768b73d8b47..fa44fbac59623edb055bb3bf883b7e6f70fc58be 100644 (file)
@@ -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<bool>,
     cl_hide_collections: CyclingMenuLine<bool>,
     cl_indexable: CyclingMenuLine<bool>,
-
     // 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<ColouredString>, CursorPosition) {
+    fn draw(
+        &self,
+        w: usize,
+        h: usize,
+    ) -> (Vec<ColouredString>, 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<ColouredString>, CursorPosition) {
+    fn draw(
+        &self,
+        w: usize,
+        h: usize,
+    ) -> (Vec<ColouredString>, 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<Box<dyn ActivityState>, ClientError>
-{
+pub fn user_options_menu(
+    client: &mut Client,
+    id: &str,
+) -> Result<Box<dyn ActivityState>, ClientError> {
     if id == client.our_account_id() {
         Ok(Box::new(YourOptionsMenu::new(client)?))
     } else {
index a387938c63ac1b91e01ba6e1217b7e50672aec1b..79ae0d937600f4bf1d3b63c73046a107989b86d5 100644 (file)
@@ -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<Self, ClientError>
-    {
+    pub fn reply_to(
+        id: &str,
+        client: &mut Client,
+    ) -> Result<Self, ClientError> {
         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<ColouredString>, CursorPosition) {
+    fn draw(
+        &self,
+        w: usize,
+        h: usize,
+    ) -> (Vec<ColouredString>, 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
index 2b16b144b1c3537bf51eca13299e00a3b472ab41..476787b13539e59a6c70909bdc291b20849fa9c2 100644 (file)
@@ -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))
+    );
 }
index 4c1f8bac205b11f07a0379719c9cba8455e390e2..44dd7b9fc7dcad3a429562dca23832e7e588c911 100644 (file)
@@ -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<HashSet<usize>> { None }
-    fn unfolded(&self, _id: &str) -> bool { true }
+    fn poll_options(&self, _id: &str) -> Option<HashSet<usize>> {
+        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<Highlight>) -> Option<String> {
         None
     }
-    fn render_highlighted(&self, width: usize, highlight: Option<Highlight>,
-                          style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>;
+    fn render_highlighted(
+        &self,
+        width: usize,
+        highlight: Option<Highlight>,
+        style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString>;
 
-    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<Highlight>,
-        style: &dyn DisplayStyleGetter) -> Vec<ColouredString>
-    {
+        &self,
+        width: usize,
+        highlight: &mut Option<Highlight>,
+        style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
         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<Highlight>) -> Option<String>
-    {
+        &self,
+        highlight: &mut Option<Highlight>,
+    ) -> Option<String> {
         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<Highlight>,
-                      _style: &dyn DisplayStyleGetter) -> ColouredString;
+    fn render_oneline(
+        &self,
+        width: usize,
+        _highlight: Option<Highlight>,
+        _style: &dyn DisplayStyleGetter,
+    ) -> ColouredString;
 }
 
 impl<T: TextFragment> TextFragment for Option<T> {
-    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<T: TextFragment> TextFragment for Option<T> {
             None => 0,
         }
     }
-    fn render_highlighted(&self, width: usize, highlight: Option<Highlight>,
-                          style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString> {
+    fn render_highlighted(
+        &self,
+        width: usize,
+        highlight: Option<Highlight>,
+        style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
         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<T: TextFragment> TextFragment for Option<T> {
 }
 
 impl<T: TextFragment> TextFragment for Vec<T> {
-    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<Highlight>,
-                          style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString> {
+    fn render_highlighted(
+        &self,
+        width: usize,
+        highlight: Option<Highlight>,
+        style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
         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<Highlight>) -> Option<String> {
         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<ColouredString> {
-        vec! {
-            ColouredString::plain(""),
-        }
+        vec![ColouredString::plain("")]
     }
 }
 
 impl TextFragment for BlankLine {
-    fn render_highlighted(&self, _width: usize, _highlight: Option<Highlight>,
-                          _style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>
-    {
+    fn render_highlighted(
+        &self,
+        _width: usize,
+        _highlight: Option<Highlight>,
+        _style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
         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<DateTime<Utc>>,
         favourited: bool,
         boosted: bool,
-        ) -> Self {
+    ) -> Self {
         SeparatorLine {
             timestamp,
             favourited,
@@ -232,86 +269,105 @@ impl SeparatorLine {
 }
 
 fn format_date(date: DateTime<Utc>) -> 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<Highlight>,
-                          _style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>
-    {
+    fn render_highlighted(
+        &self,
+        width: usize,
+        _highlight: Option<Highlight>,
+        _style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
         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<Highlight>,
-                          _style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>
-    {
-        vec! {
-            ColouredString::uniform(
-                &("-".repeat(width - min(2, width)) + "|"),
-                '-',
-            ).truncate(width).into(),
-        }
+    fn render_highlighted(
+        &self,
+        width: usize,
+        _highlight: Option<Highlight>,
+        _style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
+        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<Highlight>,
-                          _style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>
-    {
+    fn render_highlighted(
+        &self,
+        _width: usize,
+        highlight: Option<Highlight>,
+        _style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
         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<Highlight>,
-                          _style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>
-    {
+    fn render_highlighted(
+        &self,
+        width: usize,
+        _highlight: Option<Highlight>,
+        _style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
         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<Highlight>,
-                          _style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>
-    {
+    fn render_highlighted(
+        &self,
+        width: usize,
+        _highlight: Option<Highlight>,
+        _style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
         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<Paragraph>) {
-    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<ColouredString>
-    {
+    pub fn render_indented(
+        &self,
+        width: usize,
+        indent: usize,
+    ) -> Vec<ColouredString> {
         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<Highlight>,
-                          _style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>
-    {
+    fn render_highlighted(
+        &self,
+        width: usize,
+        _highlight: Option<Highlight>,
+        _style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
         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<ColouredString> {
 
 #[test]
 fn test_html() {
-    assert_eq!(render_html("<p>Testing, testing, 1, 2, 3</p>", 50),
-               vec! {
+    assert_eq!(
+        render_html("<p>Testing, testing, 1, 2, 3</p>", 50),
+        vec! {
             ColouredString::plain("Testing, testing, 1, 2, 3"),
-        });
+        }
+    );
 
-    assert_eq!(render_html("<p>First para</p><p>Second para</p>", 50),
-               vec! {
+    assert_eq!(
+        render_html("<p>First para</p><p>Second para</p>", 50),
+        vec! {
             ColouredString::plain("First para"),
             ColouredString::plain(""),
             ColouredString::plain("Second para"),
-        });
+        }
+    );
 
-    assert_eq!(render_html("<p>First line<br>Second line</p>", 50),
-               vec! {
+    assert_eq!(
+        render_html("<p>First line<br>Second line</p>", 50),
+        vec! {
             ColouredString::plain("First line"),
             ColouredString::plain("Second line"),
-        });
+        }
+    );
 
     assert_eq!(render_html("<p>Pease porridge hot, pease porridge cold, pease porridge in the pot, nine days old</p>", 50),
                vec! {
@@ -847,17 +986,21 @@ fn test_html() {
             ColouredString::plain("porridge in the pot, nine days old"),
         });
 
-    assert_eq!(render_html("<p>Test of some <code>literal code</code></p>", 50),
-               vec! {
+    assert_eq!(
+        render_html("<p>Test of some <code>literal code</code></p>", 50),
+        vec! {
             ColouredString::general("Test of some literal code",
                                     "             cccccccccccc"),
-        });
+        }
+    );
 
-    assert_eq!(render_html("<p>Test of some <strong>strong text</strong></p>", 50),
-               vec! {
+    assert_eq!(
+        render_html("<p>Test of some <strong>strong text</strong></p>", 50),
+        vec! {
             ColouredString::general("Test of some strong text",
                                     "             sssssssssss"),
-        });
+        }
+    );
 
     assert_eq!(render_html("<p>Test of a <a href=\"https://some.instance/tags/hashtag\" class=\"mention hashtag\" rel=\"nofollow noopener noreferrer\" target=\"_blank\">#<span>hashtag</span></a></p>", 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<Highlight>,
-                          _style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>
-    {
+    fn render_highlighted(
+        &self,
+        width: usize,
+        _highlight: Option<Highlight>,
+        _style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
         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<Highlight>,
-                          style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>
-    {
+    fn render_highlighted(
+        &self,
+        width: usize,
+        highlight: Option<Highlight>,
+        style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
         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() {
         "<p><span class=\"h-card\" translate=\"no\"><a href=\"https://some.instance/@stoat\" class=\"u-url mention\" rel=\"nofollow noopener noreferrer\" target=\"_blank\">@<span>stoat</span></a></span> <span class=\"h-card\" translate=\"no\"><a href=\"https://some.instance/@weasel\" class=\"u-url mention\" rel=\"nofollow noopener noreferrer\" target=\"_blank\">@<span>weasel</span></a></span> take a look at this otter!</p><p><span class=\"h-card\" translate=\"no\"><a href=\"https://some.instance/@badger\" class=\"u-url mention\" rel=\"nofollow noopener noreferrer\" target=\"_blank\">@<span>badger</span></a></span> might also like it</p>");
 
     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<Highlight>,
-                          _style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>
-    {
+    fn render_highlighted(
+        &self,
+        width: usize,
+        _highlight: Option<Highlight>,
+        _style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
         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<Utc>, account: &str, nameline: &str,
-        account_id: &str, ntype: NotificationType, post: Option<&Paragraph>,
-        status_id: Option<&str>)
-        -> Self
-    {
+        timestamp: DateTime<Utc>,
+        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<Highlight>,
-                          _style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>
-    {
+    fn render_highlighted(
+        &self,
+        width: usize,
+        highlight: Option<Highlight>,
+        _style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
         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<Highlight>) -> Option<String> {
         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<Highlight>,
-                          _style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>
-    {
+    fn render_highlighted(
+        &self,
+        width: usize,
+        highlight: Option<Highlight>,
+        _style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
         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<Highlight>) -> Option<String> {
         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<Highlight>,
-                          _style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>
-    {
+    fn render_highlighted(
+        &self,
+        width: usize,
+        _highlight: Option<Highlight>,
+        _style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
         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(&para.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<Highlight>,
-                          _style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>
-    {
+    fn render_highlighted(
+        &self,
+        width: usize,
+        _highlight: Option<Highlight>,
+        _style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
         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<Highlight>,
-                      _style: &dyn DisplayStyleGetter) -> ColouredString
-    {
+    fn render_oneline(
+        &self,
+        width: usize,
+        _highlight: Option<Highlight>,
+        _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<Highlight>,
-                          style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>
-    {
-        vec! {
-            self.render_oneline(width, highlight, style)
-        }
+    fn render_highlighted(
+        &self,
+        width: usize,
+        highlight: Option<Highlight>,
+        style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
+        vec![self.render_oneline(width, highlight, style)]
     }
 }
 
@@ -1795,19 +2110,25 @@ pub struct CyclingMenuLine<Value: Eq + Copy> {
 }
 
 impl<Value: Eq + Copy> CyclingMenuLine<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)
+    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<Value: Eq + Copy> CyclingMenuLine<Value> {
         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<Value: Eq + Copy> MenuKeypressLineGeneral for CyclingMenuLine<Value> {
@@ -1842,55 +2165,73 @@ impl<Value: Eq + Copy> MenuKeypressLineGeneral for CyclingMenuLine<Value> {
 }
 
 impl<Value: Eq + Copy> TextFragmentOneLine for CyclingMenuLine<Value> {
-    fn render_oneline(&self, width: usize, highlight: Option<Highlight>,
-                      style: &dyn DisplayStyleGetter) -> ColouredString
-    {
-        self.menulines[self.index].1.render_oneline(width, highlight, style)
+    fn render_oneline(
+        &self,
+        width: usize,
+        highlight: Option<Highlight>,
+        style: &dyn DisplayStyleGetter,
+    ) -> ColouredString {
+        self.menulines[self.index]
+            .1
+            .render_oneline(width, highlight, style)
     }
 }
 
 impl<Value: Eq + Copy> TextFragment for CyclingMenuLine<Value> {
-    fn render_highlighted(&self, width: usize, highlight: Option<Highlight>,
-                          style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>
-    {
-        vec! {
-            self.render_oneline(width, highlight, style)
-        }
+    fn render_highlighted(
+        &self,
+        width: usize,
+        highlight: Option<Highlight>,
+        style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
+        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<Highlight>,
-                          _style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>
-    {
+    fn render_highlighted(
+        &self,
+        width: usize,
+        _highlight: Option<Highlight>,
+        _style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
         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<String> { self.content.list_urls() }
+    pub fn list_urls(&self) -> Vec<String> {
+        self.content.list_urls()
+    }
 }
 
 fn push_fragment(lines: &mut Vec<ColouredString>, frag: Vec<ColouredString>) {
     lines.extend(frag.iter().map(|line| line.to_owned()));
 }
-fn push_fragment_highlighted(lines: &mut Vec<ColouredString>,
-                             frag: Vec<ColouredString>) {
+fn push_fragment_highlighted(
+    lines: &mut Vec<ColouredString>,
+    frag: Vec<ColouredString>,
+) {
     lines.extend(frag.iter().map(|line| line.recolour('*')));
 }
 
 impl TextFragment for StatusDisplay {
-    fn render_highlighted(&self, width: usize, highlight: Option<Highlight>,
-                          style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>
-    {
+    fn render_highlighted(
+        &self,
+        width: usize,
+        highlight: Option<Highlight>,
+        style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
         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<Highlight>,
-                          style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>
-    {
+    fn render_highlighted(
+        &self,
+        width: usize,
+        highlight: Option<Highlight>,
+        style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
         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<Highlight>)
-                      -> Option<String>
-    {
+    fn highlighted_id(&self, highlight: Option<Highlight>) -> Option<String> {
         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<ApproxDate>) -> ColouredString
-    {
+    fn format_option_approx_date(date: Option<ApproxDate>) -> 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<Highlight>,
-                          _style: &dyn DisplayStyleGetter)
-                          -> Vec<ColouredString>
-    {
+    fn render_highlighted(
+        &self,
+        width: usize,
+        _highlight: Option<Highlight>,
+        _style: &dyn DisplayStyleGetter,
+    ) -> Vec<ColouredString> {
         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);
index 53e89464d07e55b4d8c28ce11dc874e7b6cb0805..4f83d05b111f8934c7d8fd3491bc8cf8071d965f 100644 (file)
@@ -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<serde_json::Error> 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<File>) ->
-        Result<(), TuiError>
-    {
+    pub fn run(
+        cfgloc: &ConfigLocation,
+        readonly: bool,
+        logfile: Option<File>,
+    ) -> 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<FeedId>) }
+            enum Todo {
+                Keypress(OurKey),
+                Stream(HashSet<FeedId>),
+            }
 
             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<FilePosition> 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<ColouredString>, CursorPosition);
-    fn handle_keypress(&mut self, key: OurKey, client: &mut Client) ->
-        LogicalAction;
-    fn handle_feed_updates(&mut self, _feeds_updated: &HashSet<FeedId>,
-                           _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<ColouredString>, CursorPosition);
+    fn handle_keypress(
+        &mut self,
+        key: OurKey,
+        client: &mut Client,
+    ) -> LogicalAction;
+    fn handle_feed_updates(
+        &mut self,
+        _feeds_updated: &HashSet<FeedId>,
+        _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<FeedId>,
-                           client: &mut Client)  -> PhysicalAction {
-        self.activity_state.handle_feed_updates(&feeds_updated, client);
+    fn handle_feed_updates(
+        &mut self,
+        feeds_updated: HashSet<FeedId>,
+        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<Post>,
-                        is_interrupt: bool) {
+    fn changed_activity(
+        &mut self,
+        client: &mut Client,
+        post: Option<Post>,
+        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<Post>, is_interrupt: bool)
-                          -> Box<dyn ActivityState>
-    {
+    fn new_activity_state(
+        &self,
+        activity: Activity,
+        client: &mut Client,
+        post: Option<Post>,
+        is_interrupt: bool,
+    ) -> Box<dyn ActivityState> {
         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)
+            }
         }
     }
 }
index b1c6da169d27ac3fb64ec76683cb737fe72ecc0c..20b8d40b762e80f53d02eee13f9678654a065b2b 100644 (file)
@@ -28,18 +28,21 @@ pub struct ApproxDate(pub NaiveDate);
 
 impl<'de> Deserialize<'de> for ApproxDate {
     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-        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<E>(self, orig_value: &str) -> Result<Self::Value, E>
-                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<ApproxDate> works too
-    let date: Option<ApproxDate> = 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<ApproxDate> =
+        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<ApproxDate> = 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<String>,
@@ -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<Utc>,
     pub account: Account,
     pub status: Option<Status>,
@@ -332,7 +377,6 @@ pub struct InstanceConfig {
 pub struct Instance {
     pub domain: String,
     pub configuration: InstanceConfig,
-
     // FIXME: lots of things are missing from here!
 }