--- /dev/null
+max_width = 79
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),
}
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)]
// 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,
}
}
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);
}
_ => match self.nonutil.last() {
Some(y) => Activity::NonUtil(y.clone()),
_ => Activity::NonUtil(NonUtilityActivity::MainMenu),
- }
+ },
}
}
}
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);
}
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,
+ }
+ );
}
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")
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)
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 {
Following {
boosts: Boosts,
languages: Vec<String>,
- }
+ },
}
impl Followness {
Boosts::Hide
};
let languages = rel.languages.clone().unwrap_or(Vec::new());
- Followness::Following {
- boosts,
- languages,
- }
+ Followness::Following { boosts, languages }
}
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum AccountFlag { Block, Mute }
+pub enum AccountFlag {
+ Block,
+ Mute,
+}
pub struct Client {
auth: AuthConfig,
#[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"
+ ),
}
}
}
}
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
};
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 {
}
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 {
// 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> {
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.
}
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()
}
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));
}
// 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();
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))
}
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;
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))
}
} 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());
// 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());
}
} 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)
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 {
}
}
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> {
// 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()))
}?;
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(¬.account.id) {
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()))
}?;
if not.id != id {
return Err(ClientError::UrlError(
- url, format!(
- "request returned wrong notification id {}", ¬.id)));
+ url,
+ format!("request returned wrong notification id {}", ¬.id),
+ ));
}
self.cache_notification(¬);
Ok(not)
}
// Ok(bool) tells you whether any new items were in fact retrieved
- pub fn fetch_feed(&mut self, id: &FeedId, ext: FeedExtend) ->
- Result<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
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"))
}
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()?;
// 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) => {
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 => {
// 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),
}
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) => {
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();
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() {
// 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);
+ }
}
_ => (),
}
}
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"),
};
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() {
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;
}
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();
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
};
receiver(StreamUpdate {
id: id.clone(),
- response: rsp
+ response: rsp,
});
}
}
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) {
// 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);
.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)?;
}
}
- 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() {
} 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 {
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 {
} 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);
}
}
- 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();
}
}
- 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")
} 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(())
-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 {
}
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 {
}
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
}
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(),
}
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 }
}
self.textpos += textend;
self.colourpos += colourend;
Some(ColouredStringSlice {
- text: &textslice[..textend],
- colours: &colourslice[..colourend],
- })
+ text: &textslice[..textend],
+ colours: &colourslice[..colourend],
+ })
} else {
None
}
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"),
}
#[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]
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);
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) => {
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,
-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,
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
+ }
}
}
}
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;
}
}
}
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) {
}
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();
}
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(" ");
+ }
_ => (),
}
}
};
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());
}
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;
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();
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;
}
}
pub fn resize(&mut self, width: usize) {
self.width = width;
self.update_first_visible();
- }
+ }
}
#[test]
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"
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.
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.
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.
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 <
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 {
}
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,
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 {
}
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> {
}
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);
}
}
- 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 {
}
text
} else {
- self.menuline.render_oneline(width, None, &DefaultDisplayStyle)
+ self.menuline
+ .render_oneline(width, None, &DefaultDisplayStyle)
}
}
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);
}
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)
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);
} 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
Err(_) => LogicalAction::PopOverlayBeep,
}
}
- })
+ }),
))
}
} 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
Err(_) => LogicalAction::PopOverlayBeep,
}
}
- })
+ }),
))
}
LogicalAction::PopOverlaySilent
} else {
LogicalAction::Goto(
- NonUtilityActivity::HashtagTimeline(s.to_owned())
- .into())
+ NonUtilityActivity::HashtagTimeline(s.to_owned()).into(),
+ )
}
- })
+ }),
))
}
ColouredString::plain(title),
Box::new(move |s, _client| {
LogicalAction::GotSearchExpression(dir, s.to_owned())
- })
+ }),
))
}
}
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 {
#[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 {
// 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
// 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,
};
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
// 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)) => {
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;
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
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 {
// 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 {
// 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;
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;
}
}
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") {
// 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]
// 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
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
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 {
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 {
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
}
}
(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);
}
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
}
}
-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,
+ )))
}
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
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 {
}
impl FeedSource {
- fn new(id: FeedId) -> Self { FeedSource { id } }
+ fn new(id: FeedId) -> Self {
+ FeedSource { id }
+ }
}
impl FileDataSource for FeedSource {
feeds_updated.contains(&self.id)
}
- fn extendable(&self) -> bool { true }
+ fn extendable(&self) -> bool {
+ true
+ }
}
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()
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum CanList {
- Nothing, ForPost, ForUser,
+ Nothing,
+ ForPost,
+ ForUser,
}
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 = ¬.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(¬, 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))
}
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
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");
}
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)
// 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)
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum SearchDirection { Up, Down }
+pub enum SearchDirection {
+ Up,
+ Down,
+}
struct FileDisplayStyles {
selected_poll_id: Option<String>,
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))
}
}
}
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() {
// 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 {
}
// 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,
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());
}
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);
}
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);
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 {
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) {
}
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.
}
}
- 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");
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();
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(),
+ );
}
}
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 {
}
}
- 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);
}
}
- 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;
}
};
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,
};
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)
+ }
}
};
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 {
}
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) {
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,
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;
}
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(_) => {
}
}
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();
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
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;
}
}
}
-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)) {
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;
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() {
} 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 {
} 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 {
// 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 {
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)
}
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)
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,
+ )
}
};
(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"),
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
}
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
}
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
}
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
}
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
}
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
}
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
}
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
}
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 {
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') => {
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();
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();
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()),
};
})
}
- fn got_search_expression(&mut self, dir: SearchDirection, regex: String)
- -> LogicalAction
- {
+ fn got_search_expression(
+ &mut self,
+ dir: SearchDirection,
+ regex: String,
+ ) -> LogicalAction {
match Regex::new(®ex) {
Ok(re) => {
self.search_direction = Some(dir);
}
}
-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());
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))
}
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))
}
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();
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))
}
-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::*;
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());
}
}
/// 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) {
}
/// 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) {
}
/// 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) {
}
/// 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')
}
}
/// 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 {
/// 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)
}
}
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> {
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");
}
}
-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 {
}
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 {
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 {}
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,
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()?,
})
}
- 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)
}
}
}
- 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")
.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)
}
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() {
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")
}
}
-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) {
};
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('/');
// 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)?;
// 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!
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 {
-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 {
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();
}
}
-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
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);
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')
};
}
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'));
}
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());
(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,
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,
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())
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())
}
let mut menu = Menu::new(
ColouredString::general(
"Exit Mastodonochrome [ESC][X]",
- "HHHHHHHHHHHHHHHHHHHHHHKKKHHKH"), false);
+ "HHHHHHHHHHHHHHHHHHHHHHKKKHHKH",
+ ),
+ false,
+ );
menu.add_action(Pr('X'), "Confirm exit", LogicalAction::Exit);
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())
}
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())
}
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,
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)
}
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,
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();
(lmaxwid, rmaxwid)
}
-
fn submit(&self, client: &mut Client) -> LogicalAction {
let details = AccountDetails {
display_name: self.el_display_name.get_data().clone(),
}
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));
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));
(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;
}
}
- 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(),
+ )
}
}
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);
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!
}
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!
}
}
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));
(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;
}
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,
}
}
-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 {
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 {
}
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 {
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'.
// 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 {
}
}
- 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();
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,
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();
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
}
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));
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 {
(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;
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
}
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) {
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.
// 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();
}
}
- 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))
+ );
}
-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 {
}
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 {
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)))
}
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)
}
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(),
}
}
}
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;
}
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 {
timestamp: Option<DateTime<Utc>>,
favourited: bool,
boosted: bool,
- ) -> Self {
+ ) -> Self {
SeparatorLine {
timestamp,
favourited,
}
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 {
impl UsernameHeader {
pub fn from(account: &str, nameline: &str, id: &str) -> Self {
- UsernameHeader{
+ UsernameHeader {
header: "From".to_owned(),
colour: 'F',
account: account.to_owned(),
}
pub fn via(account: &str, nameline: &str, id: &str) -> Self {
- UsernameHeader{
+ UsernameHeader {
header: "Via".to_owned(),
colour: 'f',
account: account.to_owned(),
}
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
}
#[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)]
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();
}
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 {
#[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(" "),
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(" "),
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"),
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;
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]);
}
#[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 {
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]
}
}
}
#[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();
}
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()
}
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, '!')],
}
}
}
#[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! {
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! {
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("")]
}
}
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 {
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)
}
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,
};
} 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
}
"<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 {
}
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()]
}
}
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 {
}
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(
}
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);
_ => ' ',
};
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,
}
}
#[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 {
}
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
}
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,
}
}
#[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 {
}
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
}
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(¶.render(width));
}
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
}
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;
// 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 }
}
}
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<'_>| {
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() {
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;
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());
}
-
}
}
let space = width - min(line.width() + 1, width);
let left = space / 2;
let linepad = ColouredString::plain(" ").repeat(left) + line;
- vec! { linepad }
+ vec![linepad]
}
}
.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 {
}
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,
}
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));
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)]
}
}
}
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
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> {
}
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 {
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]
}
}
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 {
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();
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() {
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));
}
}
}
- 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 {
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));
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),
+ );
}
}
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
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()),
}
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());
}
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()),
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: "))
.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'),
} 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 {
}
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));
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);
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);
}
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;
}
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: "))
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 {
})
}
- 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));
// 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);
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,
};
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 {
' ' => 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
'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
'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
// 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() {
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)]
}
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();
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))
};
};
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
// 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
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 {
// 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
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 {
#[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)
}
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");
}
}
}
}
- 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) {
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)
+ }
}
};
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);
}
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 {
return false;
}
- self.activity_stack.goto(UtilityActivity::ReadMentions.into());
+ self.activity_stack
+ .goto(UtilityActivity::ReadMentions.into());
self.changed_activity(client, None, true);
true
}
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
}
}
- 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 {
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),
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")
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
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)
+ }
}
}
}
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,
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 {})
}
}
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();
#[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>,
#[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>,
pub struct Instance {
pub domain: String,
pub configuration: InstanceConfig,
-
// FIXME: lots of things are missing from here!
}