From b4dc1709d1ed05418a770b8d46b9bd9c4e43e3ad Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Wed, 28 Aug 2024 13:50:35 +0200 Subject: [PATCH 01/89] add tests to config mod.rs --- src/config/mod.rs | 51 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 8945397..e68ffdb 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -198,28 +198,61 @@ impl Config { #[cfg(test)] mod tests { - // use super::*; + use super::*; + + // The ordering of these tests is important since we set the static CONFIG object! #[test] #[should_panic(expected = "config not initialized")] fn get_config_before_init() { - super::get(); + get(); } #[test] #[should_panic( expected = "Could not Create Config dir: Os { code: 13, kind: PermissionDenied, message: \"Permission denied\" }" )] fn init_with_faulty_path() { - super::init("/test"); + init("/test"); } + #[test] - fn init_with_no_path() { - super::init(""); + fn default_values() { + init("./tests/"); + assert!(get().get_data_dir().ends_with(".local/share/sechat-rs")); + assert!(get() + .get_server_data_dir() + .ends_with(".local/share/sechat-rs/MyNCInstance")); + assert!(get() + .get_http_dump_dir() + .expect("Not Https Dump Dir found") + .ends_with(".local/share/sechat-rs")); + assert!(get().get_enable_mouse()); + assert!(get().get_enable_paste()); } #[test] - #[should_panic(expected = "Could not set global config!: failed to set config Config")] - fn init_config_twice() { - super::init(""); - super::init(""); + fn init_logging() { + let conf = Config::default(); + conf.config_logging(); + } + + #[test] + fn update_data() { + let mut conf = Config::default(); + conf.set_config_data(Data::default()); + conf.set_strategy( + choose_app_strategy(AppStrategyArgs { + top_level_domain: "org".to_string(), + author: "emlix".to_string(), + app_name: "sechat-rs".to_string(), + }) + .unwrap(), + ); + assert!(conf.get_data_dir().ends_with(".local/share/sechat-rs")); + assert!(conf + .get_server_data_dir() + .ends_with(".local/share/sechat-rs")); + assert!(conf.get_http_dump_dir().is_none()); + assert!(!conf.get_enable_mouse()); + assert!(!conf.get_enable_paste()); } } From 19b7b84af3db9557cf141bfbc134f5c35ab49865 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Wed, 28 Aug 2024 13:54:44 +0200 Subject: [PATCH 02/89] remove outdated lines from gitignore --- .gitignore | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.gitignore b/.gitignore index e524876..e81bf31 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,4 @@ -config.toml Cargo.lock target -dev.log -logs -devlogs -config_*.toml -CONFIG_*.toml .vscode/settings.json .pre-commit-config.yaml From 4d1c68f7e38049d175a3cee543c1aca31b8ce77f Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Wed, 28 Aug 2024 13:54:57 +0200 Subject: [PATCH 03/89] update tests to use test folder --- src/config/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index e68ffdb..2e1dede 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -212,12 +212,12 @@ mod tests { expected = "Could not Create Config dir: Os { code: 13, kind: PermissionDenied, message: \"Permission denied\" }" )] fn init_with_faulty_path() { - init("/test"); + init("/bogus_test/path"); } #[test] fn default_values() { - init("./tests/"); + init("./test/"); assert!(get().get_data_dir().ends_with(".local/share/sechat-rs")); assert!(get() .get_server_data_dir() From fc569623ad69b4eaf6c8b5cbd7ddadb379e53669 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Wed, 28 Aug 2024 13:57:37 +0200 Subject: [PATCH 04/89] add dummy config --- test/config.toml | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 test/config.toml diff --git a/test/config.toml b/test/config.toml new file mode 100644 index 0000000..ed5cea4 --- /dev/null +++ b/test/config.toml @@ -0,0 +1,43 @@ +[general] +# `General.chat_server_name` is the name used for storage and displaying +# UPDATE THIS FIELD +chat_server_name = "MyNCInstance" + +# `General.url` is the base url of the NC instance. Do not append any further parts. +# UPDATE THIS FIELD +url = "https://butz.com/" + +# `General.user` is the username. Usually not a email address. +# UPDATE THIS FIELD +user = "dummy_user" + +# `General.app_pw` generated by NC. See +# UPDATE THIS FIELD +app_pw = "foobar-asdasd-asdsf" + +# `General.log_to_file` should a log file be written into the apps data dir? +log_to_file = true + +# `General.dump_failed_requests_to_file` should a log file be written into the apps data dir? +dump_failed_requests_to_file = true + +[notifications] +# `Notifications.timeout_ms` how long a notification shall be displayed. +timeout_ms = 5000 + +persistent = false + +silent = false + +[ui] +# The default room you want to see on startup. +# UPDATE THIS FIELD +default_room = "General" + +categories = [ "", ] + +categories_separator = "" + +use_mouse = true + +use_paste = true From 71b581250ae614f67c7d02796de57142c68de1b2 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Wed, 28 Aug 2024 16:15:38 +0200 Subject: [PATCH 05/89] make NcMessage a tuple struct --- src/backend/nc_message.rs | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/backend/nc_message.rs b/src/backend/nc_message.rs index b7ef6a5..8fc99f7 100644 --- a/src/backend/nc_message.rs +++ b/src/backend/nc_message.rs @@ -2,66 +2,60 @@ use crate::backend::nc_request::NCReqDataMessage; use chrono::prelude::*; #[derive(Debug)] -pub struct NCMessage { - data: NCReqDataMessage, - message: String, -} +pub struct NCMessage(NCReqDataMessage); impl From for NCMessage { fn from(data: NCReqDataMessage) -> Self { - NCMessage { - message: data.message.clone(), - data, - } + NCMessage(data) } } impl NCMessage { pub fn get_time_str(&self) -> String { let time: DateTime = - DateTime::from(DateTime::::from_timestamp(self.data.timestamp, 0).unwrap()); + DateTime::from(DateTime::::from_timestamp(self.0.timestamp, 0).unwrap()); time.format("%H:%M").to_string() } pub fn get_name(&self) -> String { - self.data.actorDisplayName.clone() + self.0.actorDisplayName.clone() } - pub fn get_message(&self) -> String { - self.message.clone() + pub fn get_message(&self) -> &str { + &self.0.message } pub fn get_reactions_str(&self) -> String { let mut reactions = String::new(); - for (icon, number) in &self.data.reactions { + for (icon, number) in &self.0.reactions { reactions = reactions + "('" + icon + "' times " + &number.to_string() + "), "; } reactions } pub fn get_id(&self) -> i32 { - self.data.id + self.0.id } pub fn to_data(&self) -> NCReqDataMessage { - self.data.clone() + self.0.clone() } pub fn is_comment(&self) -> bool { - self.data.messageType == "comment" + self.0.messageType == "comment" } pub fn is_comment_deleted(&self) -> bool { - self.data.messageType == "comment_deleted" + self.0.messageType == "comment_deleted" } pub fn is_system(&self) -> bool { - self.data.messageType == "system" + self.0.messageType == "system" } pub fn is_edit_note(&self) -> bool { if self.is_system() { - self.data.systemMessage == "message_edited" + self.0.systemMessage == "message_edited" } else { false } @@ -69,17 +63,17 @@ impl NCMessage { pub fn is_reaction(&self) -> bool { if self.is_system() { - self.data.systemMessage == "reaction" + self.0.systemMessage == "reaction" } else { false } } pub fn is_command(&self) -> bool { - self.data.messageType == "command" + self.0.messageType == "command" } pub fn has_reactions(&self) -> bool { - !self.data.reactions.is_empty() + !self.0.reactions.is_empty() } } From 528dde9b27b77d8c1cdf7f6803277600783fed68 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Wed, 28 Aug 2024 16:20:36 +0200 Subject: [PATCH 06/89] remove mut and for --- src/backend/nc_message.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/backend/nc_message.rs b/src/backend/nc_message.rs index 8fc99f7..b06d2ea 100644 --- a/src/backend/nc_message.rs +++ b/src/backend/nc_message.rs @@ -26,11 +26,14 @@ impl NCMessage { } pub fn get_reactions_str(&self) -> String { - let mut reactions = String::new(); - for (icon, number) in &self.0.reactions { - reactions = reactions + "('" + icon + "' times " + &number.to_string() + "), "; - } - reactions + self.0 + .reactions + .iter() + .map(|(icon, number)| format!("('{icon}' times {}), ", &number.to_string())) + .collect::>() + .join(", ") + // TODO: this was in previous code! is this comma at the end needed? + + ", " } pub fn get_id(&self) -> i32 { From 2029decabd589c0aec08f5909beca6f1c73c856f Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Wed, 28 Aug 2024 16:22:11 +0200 Subject: [PATCH 07/89] remove some redundant ifs --- src/backend/nc_message.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/backend/nc_message.rs b/src/backend/nc_message.rs index b06d2ea..b449968 100644 --- a/src/backend/nc_message.rs +++ b/src/backend/nc_message.rs @@ -57,19 +57,11 @@ impl NCMessage { } pub fn is_edit_note(&self) -> bool { - if self.is_system() { - self.0.systemMessage == "message_edited" - } else { - false - } + self.is_system() && self.0.systemMessage == "message_edited" } pub fn is_reaction(&self) -> bool { - if self.is_system() { - self.0.systemMessage == "reaction" - } else { - false - } + self.is_system() && self.0.systemMessage == "reaction" } pub fn is_command(&self) -> bool { From bc3b7bed72e79ab84d3d327bb4fe9949021e44ad Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Wed, 28 Aug 2024 16:23:49 +0200 Subject: [PATCH 08/89] remove unwrap with expect to give a hint in case --- src/backend/nc_message.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/backend/nc_message.rs b/src/backend/nc_message.rs index b449968..fb3db91 100644 --- a/src/backend/nc_message.rs +++ b/src/backend/nc_message.rs @@ -12,8 +12,10 @@ impl From for NCMessage { impl NCMessage { pub fn get_time_str(&self) -> String { - let time: DateTime = - DateTime::from(DateTime::::from_timestamp(self.0.timestamp, 0).unwrap()); + let time: DateTime = DateTime::from( + DateTime::::from_timestamp(self.0.timestamp, 0) + .expect("cannot convert UTC time stamp"), + ); time.format("%H:%M").to_string() } From 9bdd7e92b3bb992caa05f8aff7edc8d6f6f2e7c5 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Wed, 28 Aug 2024 16:35:10 +0200 Subject: [PATCH 09/89] remove mut and for and rename to_data() --- src/backend/nc_message.rs | 2 +- src/backend/nc_room.rs | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/backend/nc_message.rs b/src/backend/nc_message.rs index fb3db91..27e46a2 100644 --- a/src/backend/nc_message.rs +++ b/src/backend/nc_message.rs @@ -42,7 +42,7 @@ impl NCMessage { self.0.id } - pub fn to_data(&self) -> NCReqDataMessage { + pub fn clone_data(&self) -> NCReqDataMessage { self.0.clone() } diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index a781e7b..438185f 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -230,11 +230,8 @@ impl NCRoom { } pub fn write_to_log(&mut self) -> Result<(), std::io::Error> { - let mut data = Vec::::new(); + let data: Vec<_> = self.messages.iter().map(NCMessage::clone_data).collect(); let path = self.path_to_log.as_path(); - for message in &mut self.messages { - data.push(message.to_data()); - } // Open a file in write-only mode, returns `io::Result` let mut file = match File::create(path) { Err(why) => { From 0ab02c5105a11168c81feee1a044342be0a9d83e Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Wed, 28 Aug 2024 16:37:10 +0200 Subject: [PATCH 10/89] return reference --- src/backend/nc_message.rs | 4 ++-- src/ui/chat_box.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/nc_message.rs b/src/backend/nc_message.rs index 27e46a2..a0d89c7 100644 --- a/src/backend/nc_message.rs +++ b/src/backend/nc_message.rs @@ -19,8 +19,8 @@ impl NCMessage { time.format("%H:%M").to_string() } - pub fn get_name(&self) -> String { - self.0.actorDisplayName.clone() + pub fn get_name(&self) -> &str { + &self.0.actorDisplayName } pub fn get_message(&self) -> &str { diff --git a/src/ui/chat_box.rs b/src/ui/chat_box.rs index 45250c1..d95ea33 100644 --- a/src/ui/chat_box.rs +++ b/src/ui/chat_box.rs @@ -46,7 +46,7 @@ impl<'a> ChatBox<'a> { }) { let name = textwrap::wrap( - &message_data.get_name(), + message_data.get_name(), Options::new(NAME_WIDTH.into()).break_words(true), ) .into_iter() From eafce25bfc8391859f46fe80987ccbacc5d73310 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Wed, 28 Aug 2024 16:38:18 +0200 Subject: [PATCH 11/89] use derive Default --- src/backend/nc_notify.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/backend/nc_notify.rs b/src/backend/nc_notify.rs index e8c878f..29611d4 100644 --- a/src/backend/nc_notify.rs +++ b/src/backend/nc_notify.rs @@ -1,7 +1,7 @@ use crate::config::{self}; use notify_rust::{Hint, Notification, Timeout}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct NCNotify { app_name: String, timeout_ms: u32, @@ -64,9 +64,3 @@ impl NCNotify { Ok(()) } } - -impl Default for NCNotify { - fn default() -> Self { - Self::new() - } -} From 37333ab95941d341fd61b293b29d0fc1e4d6d0e1 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Wed, 28 Aug 2024 16:43:48 +0200 Subject: [PATCH 12/89] improve readability --- src/backend/nc_request.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/backend/nc_request.rs b/src/backend/nc_request.rs index 73a2861..a0e77c7 100644 --- a/src/backend/nc_request.rs +++ b/src/backend/nc_request.rs @@ -245,10 +245,14 @@ pub struct NCReqOCS { impl NCRequest { pub fn new() -> Result> { - let username = config::get().data.general.user.clone(); - let password = Some(config::get().data.general.app_pw.clone()); - let base_url = config::get().data.general.url.clone(); - let json_dump_path = config::get().get_http_dump_dir(); + let config = &config::get(); + let general = &config.data.general; + + let username = general.user.clone(); + let password = Some(general.app_pw.clone()); + let base_url = general.url.clone(); + + let json_dump_path = config.get_http_dump_dir(); let mut headers = header::HeaderMap::new(); headers.insert("OCS-APIRequest", header::HeaderValue::from_static("true")); headers.insert( @@ -259,9 +263,9 @@ impl NCRequest { let mut buf = b"Basic ".to_vec(); { let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD); - let _ = write!(encoder, "{username}:"); + write!(encoder, "{username}:").expect("i/o error"); if let Some(password) = password { - let _ = write!(encoder, "{password}"); + write!(encoder, "{password}").expect("i/o error"); } } let mut auth_value = From 11fda13b565decd037fa74f6d2c704758cfe8927 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Wed, 28 Aug 2024 16:56:04 +0200 Subject: [PATCH 13/89] unify reqwest usage --- src/backend/nc_request.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/backend/nc_request.rs b/src/backend/nc_request.rs index a0e77c7..96548c7 100644 --- a/src/backend/nc_request.rs +++ b/src/backend/nc_request.rs @@ -4,8 +4,10 @@ use base64::{prelude::BASE64_STANDARD, write::EncoderWriter}; use jzon; -use reqwest::header::HeaderMap; -use reqwest::{header, Client, Response, Url}; +use reqwest::{ + header::{HeaderMap, HeaderValue, AUTHORIZATION}, + Client, Response, Url, +}; use serde::{Deserialize, Deserializer, Serialize}; use std::path::PathBuf; use std::{collections::HashMap, error::Error, fs::File, io::Write}; @@ -253,12 +255,9 @@ impl NCRequest { let base_url = general.url.clone(); let json_dump_path = config.get_http_dump_dir(); - let mut headers = header::HeaderMap::new(); - headers.insert("OCS-APIRequest", header::HeaderValue::from_static("true")); - headers.insert( - "Accept", - header::HeaderValue::from_static("application/json"), - ); + let mut headers = HeaderMap::new(); + headers.insert("OCS-APIRequest", HeaderValue::from_static("true")); + headers.insert("Accept", HeaderValue::from_static("application/json")); let mut buf = b"Basic ".to_vec(); { @@ -269,9 +268,9 @@ impl NCRequest { } } let mut auth_value = - header::HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue"); + HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue"); auth_value.set_sensitive(true); - headers.insert(header::AUTHORIZATION, auth_value); + headers.insert(AUTHORIZATION, auth_value); // get a client builder let client = reqwest::Client::builder() From 69d92237ba9b7814b2e029b15f3fc87f0e7da039 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Wed, 28 Aug 2024 17:13:39 +0200 Subject: [PATCH 14/89] remove further mutables --- src/backend/nc_request.rs | 53 +++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/src/backend/nc_request.rs b/src/backend/nc_request.rs index 96548c7..d7611de 100644 --- a/src/backend/nc_request.rs +++ b/src/backend/nc_request.rs @@ -291,8 +291,7 @@ impl NCRequest { token: &str, ) -> Result> { let url_string = self.base_url.clone() + "/ocs/v2.php/apps/spreed/api/v1/chat/" + token; - let mut params = HashMap::new(); - params.insert("message".to_owned(), message.clone()); + let params = HashMap::from([("message", message)]); let url = Url::parse_with_params(&url_string, params)?; let response = self.request_post(url).await?; @@ -316,13 +315,10 @@ impl NCRequest { name: &str, ) -> Result, Box> { let url_string = self.base_url.clone() + "/ocs/v2.php/core/autocomplete/get"; - let mut params = HashMap::new(); - params.insert("limit".to_owned(), "200".to_string()); - params.insert("search".to_owned(), name.to_string()); - + let params = HashMap::from([("limit", "200"), ("search", name)]); let url = Url::parse_with_params(&url_string, params)?; - let response = self.request(url).await?; + match response.status() { reqwest::StatusCode::OK => { let text = response.text().await?; @@ -352,8 +348,7 @@ impl NCRequest { + "/ocs/v2.php/apps/spreed/api/v4/room/" + token + "/participants"; - let mut params = HashMap::new(); - params.insert("includeStatus".to_owned(), "true".to_string()); + let params = HashMap::from([("includeStatus", "true")]); let url = Url::parse_with_params(&url_string, params)?; let response = self.request(url).await?; @@ -394,10 +389,11 @@ impl NCRequest { last_timestamp: Option, ) -> Result<(Vec, i64), Box> { let url_string = self.base_url.clone() + "/ocs/v2.php/apps/spreed/api/v4/room"; - let mut params = HashMap::new(); - if let Some(timestamp) = last_timestamp { - params.insert("modifiedSince".to_owned(), timestamp.to_string()); - } + let params = if let Some(timestamp) = last_timestamp { + HashMap::from([("modifiedSince", timestamp.to_string())]) + } else { + HashMap::new() + }; let url = Url::parse_with_params(&url_string, ¶ms)?; let response = self.request(url).await?; match response.status() { @@ -466,18 +462,23 @@ impl NCRequest { last_message: Option, ) -> Result>, Box> { let url_string = self.base_url.clone() + "/ocs/v2.php/apps/spreed/api/v1/chat/" + token; - let mut params = HashMap::new(); - params.insert("limit".to_owned(), maxMessage.to_string()); - params.insert("setReadMarker".to_owned(), "0".to_owned()); - if let Some(lastId) = last_message { + let params = if let Some(lastId) = last_message { log::debug!("Last MessageID {}", lastId); - params.insert("lastKnownMessageId".to_owned(), lastId.to_string()); - params.insert("lookIntoFuture".to_owned(), "1".to_owned()); - params.insert("timeout".to_owned(), "0".to_owned()); - params.insert("includeLastKnown".to_owned(), "0".to_owned()); + HashMap::from([ + ("limit", maxMessage.to_string()), + ("setReadMarker", "0".into()), + ("lookIntoFuture", "1".to_owned()), + ("lastKnownMessageId", lastId.to_string()), + ("timeout", "0".to_owned()), + ("includeLastKnown", "0".to_owned()), + ]) } else { - params.insert("lookIntoFuture".to_owned(), "0".to_owned()); - } + HashMap::from([ + ("limit", maxMessage.to_string()), + ("setReadMarker", "0".into()), + ("lookIntoFuture", "0".into()), + ]) + }; let url = Url::parse_with_params(&url_string, ¶ms)?; let response = self.request(url).await?; match response.status() { @@ -542,8 +543,10 @@ impl NCRequest { fn dump_json_to_log(&self, url: &str, text: &str) -> Result<(), Box> { if let Some(path) = &self.json_dump_path { - let mut name = path.clone(); - name.push(url.replace('/', "_")); + let name: String = url + .chars() + .map(|ch| if ch == '/' { '_' } else { ch }) + .collect(); let mut file = File::create(name)?; let pretty_text = jzon::stringify_pretty(jzon::parse(text)?, 2); file.write_all(pretty_text.as_bytes())?; From de37fb0f17d11ec0c15a95f7ac6836c333905822 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Wed, 28 Aug 2024 17:32:11 +0200 Subject: [PATCH 15/89] remove for loop --- src/backend/nc_talk.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index 299cd99..8d9ace9 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -35,15 +35,14 @@ impl NCTalk { rooms: &mut HashMap, chat_log_path: PathBuf, ) { - let mut v = Vec::new(); - for child in response { - v.push(tokio::spawn(NCTalk::new_room( + let v = response.into_iter().map(|child| { + tokio::spawn(NCTalk::new_room( child, requester.clone(), notifier.clone(), chat_log_path.clone(), - ))); - } + )) + }); for jh in v { let (name, room_option) = jh.await.unwrap(); if let Some(room) = room_option { From 818a1c80e1708f7af31b567fe5a9ea44fff70e9c Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Wed, 28 Aug 2024 17:45:09 +0200 Subject: [PATCH 16/89] remove mut and for --- src/backend/nc_room.rs | 2 +- src/backend/nc_talk.rs | 30 ++++++++++++++++-------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index 438185f..ca04542 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -14,7 +14,7 @@ use std::{ path::PathBuf, }; -#[derive(Debug, FromPrimitive)] +#[derive(Debug, FromPrimitive, PartialEq)] pub enum NCRoomTypes { OneToOne = 1, Group, diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index 8d9ace9..c2e4886 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -250,20 +250,22 @@ impl NCTalk { } pub fn get_dm_keys_display_name_mapping(&self) -> Vec<(String, String)> { - let mut mapping: Vec<(String, String)> = Vec::new(); - for (key, room) in &self.rooms { - match room.room_type { - NCRoomTypes::OneToOne | NCRoomTypes::NoteToSelf | NCRoomTypes::ChangeLog => { - mapping.push((key.clone(), self.rooms[key].to_string())); - } - _ => {} - } - } - mapping.sort_by(|(token_a, _), (token_b, _)| { - self.get_room_by_token(token_a) - .cmp(self.get_room_by_token(token_b)) - }); - mapping + self.rooms + .iter() + .filter(|(_, room)| { + [ + NCRoomTypes::OneToOne, + NCRoomTypes::NoteToSelf, + NCRoomTypes::ChangeLog, + ] + .contains(&room.room_type) + }) + .map(|(key, _)| (key.clone(), self.rooms[key].to_string())) + .sorted_by(|(token_a, _), (token_b, _)| { + self.get_room_by_token(token_a) + .cmp(self.get_room_by_token(token_b)) + }) + .collect_vec() } pub fn get_group_keys_display_name_mapping(&self) -> Vec<(String, String)> { From 248a90c262832c289e4d390e18db1119b017d99f Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Wed, 28 Aug 2024 17:52:12 +0200 Subject: [PATCH 17/89] replace for loop --- src/backend/nc_room.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index ca04542..0307fb1 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -66,9 +66,7 @@ impl NCRoom { if let Ok(data) = serde_json::from_str::>( read_to_string(path).unwrap().as_str(), ) { - for message in data { - messages.push(message.into()); - } + messages.extend(data.into_iter().map(Into::into)); } else { log::debug!( "Failed to parse json for {}, falling back to fetching", From 638fdb1e9392ee04b2fba1423a8822ac18e6eb51 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz Date: Wed, 28 Aug 2024 21:27:37 +0200 Subject: [PATCH 18/89] replace use of seshat with sechat --- Cargo.toml | 2 +- src/main.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7a8a413..66b0910 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "seshat-rs" +name = "sechat-rs" description = "A TUI based chat client for Nextcloud Talk" readme = "README.md" license-file = "LICENSE" diff --git a/src/main.rs b/src/main.rs index 2cf0461..5239ed9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,7 +23,7 @@ async fn main() { // check if crate has alpha suffix in version let pre = env!("CARGO_PKG_VERSION_PRE"); if !pre.is_empty() { - log::warn!("Entering Seshat-rs, please be aware this is {pre} SW!"); + log::warn!("Entering Sechat-rs, please be aware this is {pre} SW!"); } let requester = backend::nc_request::NCRequest::new().expect("cannot create NCRequest"); From 3d22674087df2a5b623544d23af7c536ae61bbb1 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz Date: Wed, 28 Aug 2024 21:35:59 +0200 Subject: [PATCH 19/89] add comment on app name in readme file --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 8d043ef..def5f89 100644 --- a/README.md +++ b/README.md @@ -46,5 +46,9 @@ Use "?" to get to the help screen. Please open issues in the issue tracker. A list of planned and requested freatures is also kept there. +## The Name +Originally intended to be called seshat, after the egyptian goddess of writing, a typo became sechat. +Thank to Sebastian for sugesting the name in the first place. + ## Sponsors Thanks to [emlix gmbh](https://github.com/emlix) for allowing [@tofu](https://github.com/tofubert) and other so spend some of their work time to tinker with this. From a8a75dd44546fe8a86107263aceca52da2a89d3f Mon Sep 17 00:00:00 2001 From: Michel von Czettritz Date: Wed, 28 Aug 2024 21:57:47 +0200 Subject: [PATCH 20/89] extend make file --- Cargo.toml | 3 --- Makefile.toml | 13 ++++++++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7a8a413..1d12973 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,9 +20,6 @@ exclude = [ "tags", ] -[build] -target = ["x86_64-unknown-linux-gnu", "i686-unknown-linux-gnu"] - [dependencies] reqwest = { version = "0.12", features = ["json"] } tokio = { version = "1", features = ["full"] } diff --git a/Makefile.toml b/Makefile.toml index 95f38a4..e83e25b 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -10,6 +10,10 @@ alias = "ci" description = "Run continuous integration tasks" dependencies = ["lint", "clippy", "check", "test"] +[tasks.all] +description = "Run all tasks" +dependencies = ["lint", "clippy", "check", "audit", "build", "coverage-text"] + [tasks.clippy] description = "Run Clippy for linting" @@ -29,7 +33,7 @@ args = [ [tasks.lint] description = "Lint code style (formatting, typos, docs, markdown)" -dependencies = ["lint-format"] +dependencies = ["lint-format", "lint-typos"] [tasks.lint-format] description = "Lint code formatting" @@ -67,6 +71,13 @@ args = [ "target/lcov.info", ] +[tasks.coverage-text] +description = "Generate code coverage report" +command = "cargo" +args = [ + "llvm-cov", +] + [tasks.build] command = "cargo" args = ["build"] From 1b4118078b09a47b41b380e4fb1ccf66ea20abfc Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Thu, 29 Aug 2024 00:06:13 +0200 Subject: [PATCH 21/89] remove following comma --- src/backend/nc_message.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/backend/nc_message.rs b/src/backend/nc_message.rs index a0d89c7..e02195b 100644 --- a/src/backend/nc_message.rs +++ b/src/backend/nc_message.rs @@ -34,8 +34,6 @@ impl NCMessage { .map(|(icon, number)| format!("('{icon}' times {}), ", &number.to_string())) .collect::>() .join(", ") - // TODO: this was in previous code! is this comma at the end needed? - + ", " } pub fn get_id(&self) -> i32 { From 090fd6febc5b981e58b84354de25b3cf1a610f0c Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Thu, 29 Aug 2024 00:13:19 +0200 Subject: [PATCH 22/89] simplified conversions --- src/backend/nc_request.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/nc_request.rs b/src/backend/nc_request.rs index d7611de..c23dc2b 100644 --- a/src/backend/nc_request.rs +++ b/src/backend/nc_request.rs @@ -467,10 +467,10 @@ impl NCRequest { HashMap::from([ ("limit", maxMessage.to_string()), ("setReadMarker", "0".into()), - ("lookIntoFuture", "1".to_owned()), + ("lookIntoFuture", "1".into()), ("lastKnownMessageId", lastId.to_string()), - ("timeout", "0".to_owned()), - ("includeLastKnown", "0".to_owned()), + ("timeout", "0".into()), + ("includeLastKnown", "0".into()), ]) } else { HashMap::from([ From 0c75e92b26d3cbb6258b59522f030b6ad3a3711c Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Thu, 29 Aug 2024 00:18:22 +0200 Subject: [PATCH 23/89] remove mut --- src/ui/chat_box.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ui/chat_box.rs b/src/ui/chat_box.rs index d95ea33..b849144 100644 --- a/src/ui/chat_box.rs +++ b/src/ui/chat_box.rs @@ -53,7 +53,6 @@ impl<'a> ChatBox<'a> { .map(std::borrow::Cow::into_owned) .map(Line::from) .collect_vec(); - let mut row_height: u16 = name.len().try_into().unwrap(); let message_string = message_data .get_message() @@ -66,8 +65,11 @@ impl<'a> ChatBox<'a> { .collect_vec() }) .collect_vec(); - if message_string.len() > row_height as usize { - row_height = message_string.len().try_into().unwrap(); + + let row_height: u16 = if message_string.len() > name.len() { + message_string.len().try_into().expect("message too long") + } else { + name.len().try_into().expect("name too long") }; let message: Vec = vec![ message_data.get_time_str().into(), From 0e5c0c335e2bcc98d7441b5af2ded8d31d069893 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Thu, 29 Aug 2024 00:20:58 +0200 Subject: [PATCH 24/89] typo in UI --- src/ui/help_box.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/help_box.rs b/src/ui/help_box.rs index c73fd76..43143ab 100644 --- a/src/ui/help_box.rs +++ b/src/ui/help_box.rs @@ -49,7 +49,7 @@ impl Widget for &HelpBox { ) .column_spacing(1) .style(Style::new().white().on_black()) - .header(Row::new(vec!["Key", "Name", "Behaviour"]).style(Style::new().bold().blue())) + .header(Row::new(vec!["Key", "Name", "Behavior"]).style(Style::new().bold().blue())) .block(Block::default()) .highlight_style(Style::new().green()) .highlight_spacing(HighlightSpacing::Never), From 118c152118d1aedb710798630f527340b0bc6561 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Thu, 29 Aug 2024 00:29:21 +0200 Subject: [PATCH 25/89] cleaned usages --- src/ui/mod.rs | 58 +++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index d2ebb97..263c56e 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -7,32 +7,26 @@ pub mod title_bar; use super::{ backend::nc_talk::NCTalk, - config::{self}, + config, ui::app::{App, CurrentScreen}, }; use cfg_if::cfg_if; use color_eyre::{ config::{EyreHook, HookBuilder, PanicHook}, - eyre::{self, Result}, + eyre, }; use crossterm::{ event::{ - self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, - Event, KeyCode, KeyEventKind, KeyModifiers, MouseEventKind, + poll, read, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, + EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEventKind, }, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use ratatui::prelude::*; -use std::{ - error::Error, - io::{stdout, Stdout}, - panic, - time::Duration, -}; use tracing::error; -pub fn install_hooks() -> Result<()> { +pub fn install_hooks() -> eyre::Result<()> { let (panic_hook, eyre_hook) = HookBuilder::default() .panic_section(format!( "This is a bug. Consider reporting it at {}", @@ -68,7 +62,7 @@ fn install_color_eyre_panic_hook(panic_hook: PanicHook) { // convert from a `color_eyre::config::PanicHook`` to a `Box` let panic_hook = panic_hook.into_panic_hook(); - panic::set_hook(Box::new(move |panic_info| { + std::panic::set_hook(Box::new(move |panic_info| { if let Err(err) = restore() { error!("Unable to restore terminal: {err:?}"); } @@ -89,9 +83,11 @@ fn install_eyre_hook(eyre_hook: EyreHook) -> color_eyre::Result<()> { Ok(()) } -pub type Tui = Terminal>; +pub type Tui = Terminal>; + +pub fn init() -> eyre::Result { + use std::io::stdout; -pub fn init() -> Result { enable_raw_mode()?; execute!(stdout(), EnterAlternateScreen)?; if config::get().get_enable_mouse() { @@ -106,7 +102,9 @@ pub fn init() -> Result { Ok(terminal) } -pub fn restore() -> Result<()> { +pub fn restore() -> eyre::Result<()> { + use std::io::stdout; + if config::get().get_enable_paste() { execute!(stdout(), DisableBracketedPaste)?; } @@ -123,7 +121,7 @@ enum ProcessEventResult { Exit, } -pub async fn run(nc_backend: NCTalk) -> Result<(), Box> { +pub async fn run(nc_backend: NCTalk) -> Result<(), Box> { install_hooks()?; // create app and run it @@ -140,15 +138,15 @@ pub async fn run(nc_backend: NCTalk) -> Result<(), Box> { async fn run_app( mut terminal: Terminal, mut app: App<'_>, -) -> Result<(), Box> { +) -> Result<(), Box> { app.select_room().await?; log::debug!("Entering Main Loop"); loop { terminal.draw(|f| app.ui(f))?; // Event within timeout? - if event::poll(Duration::from_millis(3000))? { - match process_event(&mut app, event::read()?).await { + if poll(std::time::Duration::from_millis(3000))? { + match process_event(&mut app, read()?).await { Ok(ProcessEventResult::Continue) => (), Ok(ProcessEventResult::Exit) => return Ok(()), Err(why) => return Err(why), @@ -164,11 +162,11 @@ async fn run_app( async fn process_event( app: &mut App<'_>, event: Event, -) -> Result> { +) -> Result> { // It's guaranteed that `read` won't block, because `poll` returned // `Ok(true)`. match event { - Event::Key(key) if key.kind != event::KeyEventKind::Release => { + Event::Key(key) if key.kind != KeyEventKind::Release => { log::debug!("Processing key event {:?}", key.code); match app.current_screen { CurrentScreen::Helping => handle_key_in_help(key, app), @@ -196,9 +194,9 @@ async fn process_event( } async fn handle_key_in_opening( - key: event::KeyEvent, + key: KeyEvent, app: &mut App<'_>, -) -> Result<(), Box> { +) -> Result<(), Box> { match key.code { KeyCode::Esc => app.current_screen = CurrentScreen::Reading, KeyCode::Char('h') | KeyCode::Left => _ = app.selector.state.key_left(), @@ -219,9 +217,9 @@ async fn handle_key_in_opening( } async fn handle_key_in_editing( - key: event::KeyEvent, + key: KeyEvent, app: &mut App<'_>, -) -> Result<(), Box> { +) -> Result<(), Box> { if key.kind == KeyEventKind::Press { match key.code { KeyCode::Enter => { @@ -239,7 +237,7 @@ async fn handle_key_in_editing( Ok(()) } -fn handle_key_in_help(key: event::KeyEvent, app: &mut App) { +fn handle_key_in_help(key: KeyEvent, app: &mut App) { match key.code { KeyCode::Char('q') => app.current_screen = CurrentScreen::Exiting, KeyCode::Esc => app.current_screen = CurrentScreen::Reading, @@ -249,9 +247,9 @@ fn handle_key_in_help(key: event::KeyEvent, app: &mut App) { } fn handle_key_in_exit( - key: event::KeyEvent, + key: KeyEvent, app: &mut App, -) -> Option>> { +) -> Option>> { match key.code { KeyCode::Char('?') => app.current_screen = CurrentScreen::Helping, KeyCode::Char('y') => { @@ -270,9 +268,9 @@ fn handle_key_in_exit( } async fn handle_key_in_reading( - key: event::KeyEvent, + key: KeyEvent, app: &mut App<'_>, -) -> Result<(), Box> { +) -> Result<(), Box> { match key.code { KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { app.current_screen = CurrentScreen::Exiting; From ed8ad0fe5887ce1d500435efe8a938e854c7e44a Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Thu, 29 Aug 2024 00:41:04 +0200 Subject: [PATCH 26/89] remove high level std usages --- src/ui/app.rs | 13 ++++++------- src/ui/chat_box.rs | 4 ++-- src/ui/title_bar.rs | 1 - 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index 904e2fc..7550008 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -6,7 +6,6 @@ use crate::{ }, }; use ratatui::{prelude::*, widgets::Paragraph}; -use std::error::Error; use strum_macros::Display; #[derive(PartialEq, Clone, Copy, Display)] @@ -82,21 +81,21 @@ impl<'a> App<'a> { self.title.render_area(f, base_layout[0]); } - pub async fn mark_current_as_read(&mut self) -> Result<(), Box> { + pub async fn mark_current_as_read(&mut self) -> Result<(), Box> { self.backend.get_current_room().mark_as_read().await?; self.backend.update_rooms(true).await?; self.update_ui()?; Ok(()) } - fn update_ui(&mut self) -> Result<(), Box> { + fn update_ui(&mut self) -> Result<(), Box> { self.title.update(self.current_screen, &self.backend); self.selector.update(&self.backend)?; self.chat.update_messages(&self.backend); Ok(()) } - pub async fn send_message(&mut self) -> Result<(), Box> { + pub async fn send_message(&mut self) -> Result<(), Box> { self.backend.send_message(self.input.to_string()).await?; self.input.clear(); self.update_ui()?; @@ -104,7 +103,7 @@ impl<'a> App<'a> { Ok(()) } - pub async fn select_room(&mut self) -> Result<(), Box> { + pub async fn select_room(&mut self) -> Result<(), Box> { if self.selector.state.selected().len() == 2 { self.backend .select_room( @@ -125,7 +124,7 @@ impl<'a> App<'a> { Ok(()) } - pub async fn fetch_updates(&mut self) -> Result<(), Box> { + pub async fn fetch_updates(&mut self) -> Result<(), Box> { self.backend.update_rooms(false).await?; self.update_ui()?; Ok(()) @@ -147,7 +146,7 @@ impl<'a> App<'a> { self.chat.select_down(); } - pub fn click_at(&mut self, position: Position) -> Result<(), Box> { + pub fn click_at(&mut self, position: Position) -> Result<(), Box> { match self.current_screen { CurrentScreen::Reading => self.chat.select_line(position)?, CurrentScreen::Opening => { diff --git a/src/ui/chat_box.rs b/src/ui/chat_box.rs index b849144..15c7151 100644 --- a/src/ui/chat_box.rs +++ b/src/ui/chat_box.rs @@ -3,7 +3,6 @@ use ratatui::{ prelude::*, widgets::{Block, Cell, HighlightSpacing, Row, Table, TableState}, }; -use std::{convert::TryInto, error::Error}; use textwrap::Options; // this fits my name, so 20 it is :D @@ -38,6 +37,7 @@ impl<'a> ChatBox<'a> { pub fn update_messages(&mut self, backend: &NCTalk) { use itertools::Itertools; + use std::convert::TryInto; self.messages.clear(); for message_data in @@ -128,7 +128,7 @@ impl<'a> ChatBox<'a> { .clamp(0, self.messages.len() - 1); self.state.select(Some(self.current_index)); } - pub fn select_line(&mut self, position: Position) -> Result<(), Box> { + pub fn select_line(&mut self, position: Position) -> Result<(), Box> { log::debug!( "Got Position {:?} and selected {:?}", position, diff --git a/src/ui/title_bar.rs b/src/ui/title_bar.rs index 6bd5d3e..5e88668 100644 --- a/src/ui/title_bar.rs +++ b/src/ui/title_bar.rs @@ -4,7 +4,6 @@ use ratatui::{ prelude::*, widgets::{Block, Borders, Paragraph}, }; -use std::string::ToString; use style::Styled; pub struct TitleBar<'a> { From 0b4dc3efb41895e8c4a8729daabca07e08f37d06 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Thu, 29 Aug 2024 00:50:47 +0200 Subject: [PATCH 27/89] lock config once --- src/backend/nc_notify.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/backend/nc_notify.rs b/src/backend/nc_notify.rs index 29611d4..934a2c3 100644 --- a/src/backend/nc_notify.rs +++ b/src/backend/nc_notify.rs @@ -11,11 +11,12 @@ pub struct NCNotify { impl NCNotify { pub fn new() -> Self { + let data = &config::get().data; NCNotify { - app_name: config::get().data.general.chat_server_name.clone(), - timeout_ms: config::get().data.notifications.timeout_ms, - persistent: config::get().data.notifications.persistent, - silent: config::get().data.notifications.silent, + app_name: data.general.chat_server_name.clone(), + timeout_ms: data.notifications.timeout_ms, + persistent: data.notifications.persistent, + silent: data.notifications.silent, } } From 32b68c1e1a111ebdee78404962a5fd54a3335537 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Thu, 29 Aug 2024 00:52:16 +0200 Subject: [PATCH 28/89] shorten as_str() --- src/backend/nc_notify.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/backend/nc_notify.rs b/src/backend/nc_notify.rs index 934a2c3..3711345 100644 --- a/src/backend/nc_notify.rs +++ b/src/backend/nc_notify.rs @@ -26,10 +26,12 @@ impl NCNotify { number_of_unread: usize, ) -> Result<(), Box> { let mut notification = Notification::new() - .summary(format!("Unread: {room_name}").as_str()) - .body(format!("You have {number_of_unread} new Messages in {room_name}").as_str()) + .summary(&format!("Unread: {room_name}")) + .body(&format!( + "You have {number_of_unread} new Messages in {room_name}" + )) .icon("dialog-information") - .appname(self.app_name.as_str()) + .appname(&self.app_name) .to_owned(); if self.persistent { log::debug!("Persistent Message!"); @@ -47,10 +49,10 @@ impl NCNotify { pub fn new_room(&self, room_name: &String) -> Result<(), Box> { let mut notification = Notification::new() - .summary(format!("New Room: {room_name}").as_str()) - .body(format!("You have been added to a new Room {room_name}").as_str()) + .summary(&format!("New Room: {room_name}")) + .body(&format!("You have been added to a new Room {room_name}")) .icon("dialog-information") - .appname(self.app_name.as_str()) + .appname(&self.app_name) .to_owned(); if self.persistent { notification From c5f80ebce081fcb858b5d1b794a28b4b99054e21 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Thu, 29 Aug 2024 01:04:47 +0200 Subject: [PATCH 29/89] remove clone --- src/backend/nc_message.rs | 5 +++-- src/backend/nc_room.rs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/backend/nc_message.rs b/src/backend/nc_message.rs index e02195b..d5db4fe 100644 --- a/src/backend/nc_message.rs +++ b/src/backend/nc_message.rs @@ -40,8 +40,9 @@ impl NCMessage { self.0.id } - pub fn clone_data(&self) -> NCReqDataMessage { - self.0.clone() + /// return inner data message + pub fn data(&self) -> &NCReqDataMessage { + &self.0 } pub fn is_comment(&self) -> bool { diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index 0307fb1..edc5cc0 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -228,7 +228,7 @@ impl NCRoom { } pub fn write_to_log(&mut self) -> Result<(), std::io::Error> { - let data: Vec<_> = self.messages.iter().map(NCMessage::clone_data).collect(); + let data: Vec<_> = self.messages.iter().map(NCMessage::data).collect(); let path = self.path_to_log.as_path(); // Open a file in write-only mode, returns `io::Result` let mut file = match File::create(path) { From 14d8c181e40a4f1a04b595996ade7b7941e745b0 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Thu, 29 Aug 2024 01:24:01 +0200 Subject: [PATCH 30/89] remove redundant flag `persistent` --- src/backend/nc_notify.rs | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/backend/nc_notify.rs b/src/backend/nc_notify.rs index 3711345..8e4396c 100644 --- a/src/backend/nc_notify.rs +++ b/src/backend/nc_notify.rs @@ -4,8 +4,7 @@ use notify_rust::{Hint, Notification, Timeout}; #[derive(Debug, Clone, Default)] pub struct NCNotify { app_name: String, - timeout_ms: u32, - persistent: bool, + timeout: Timeout, silent: bool, } @@ -14,8 +13,11 @@ impl NCNotify { let data = &config::get().data; NCNotify { app_name: data.general.chat_server_name.clone(), - timeout_ms: data.notifications.timeout_ms, - persistent: data.notifications.persistent, + timeout: if data.notifications.persistent { + Timeout::Never + } else { + Timeout::Milliseconds(data.notifications.timeout_ms) + }, silent: data.notifications.silent, } } @@ -33,14 +35,12 @@ impl NCNotify { .icon("dialog-information") .appname(&self.app_name) .to_owned(); - if self.persistent { + if self.is_persistent() { log::debug!("Persistent Message!"); - notification - .hint(Hint::Resident(true)) // this is not supported by all implementations - .timeout(Timeout::Never); // this however is - } else { - notification.timeout(Timeout::Milliseconds(self.timeout_ms)); } + notification + .hint(Hint::Resident(self.is_persistent())) // this is not supported by all implementations + .timeout(self.timeout); notification.hint(Hint::SuppressSound(self.silent)); notification.show()?; @@ -54,16 +54,17 @@ impl NCNotify { .icon("dialog-information") .appname(&self.app_name) .to_owned(); - if self.persistent { - notification - .hint(Hint::Resident(true)) // this is not supported by all implementations - .timeout(Timeout::Never); // this however is - } else { - notification.timeout(Timeout::Milliseconds(self.timeout_ms)); - } + notification + .hint(Hint::Resident(self.is_persistent())) // this is not supported by all implementations + .timeout(self.timeout); // this however is notification.hint(Hint::SuppressSound(self.silent)); notification.show()?; Ok(()) } + + /// return `true` if notification is persistent (has infinite display timeout) + pub fn is_persistent(&self) -> bool { + self.timeout == Timeout::Never + } } From e7e4d84773de0d1e0bd8bd1f098e6361238d3c06 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Thu, 29 Aug 2024 01:24:13 +0200 Subject: [PATCH 31/89] add doc comments --- src/backend/nc_message.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/backend/nc_message.rs b/src/backend/nc_message.rs index d5db4fe..a4c2225 100644 --- a/src/backend/nc_message.rs +++ b/src/backend/nc_message.rs @@ -1,6 +1,7 @@ use crate::backend::nc_request::NCReqDataMessage; use chrono::prelude::*; +/// `NextCloud` message interface #[derive(Debug)] pub struct NCMessage(NCReqDataMessage); @@ -11,6 +12,7 @@ impl From for NCMessage { } impl NCMessage { + /// return message time stamp as string pub fn get_time_str(&self) -> String { let time: DateTime = DateTime::from( DateTime::::from_timestamp(self.0.timestamp, 0) @@ -19,14 +21,17 @@ impl NCMessage { time.format("%H:%M").to_string() } + /// return opponent display name pub fn get_name(&self) -> &str { &self.0.actorDisplayName } + /// return the message itself pub fn get_message(&self) -> &str { &self.0.message } + /// get list of reactions as comma separated string pub fn get_reactions_str(&self) -> String { self.0 .reactions @@ -36,6 +41,7 @@ impl NCMessage { .join(", ") } + /// get message identifier pub fn get_id(&self) -> i32 { self.0.id } @@ -45,30 +51,37 @@ impl NCMessage { &self.0 } + /// return `true` if message is a comment pub fn is_comment(&self) -> bool { self.0.messageType == "comment" } + /// return `true` if message is a deleted comment pub fn is_comment_deleted(&self) -> bool { self.0.messageType == "comment_deleted" } + /// return `true` if message is a system message pub fn is_system(&self) -> bool { self.0.messageType == "system" } + /// return `true` if message is an edited message pub fn is_edit_note(&self) -> bool { self.is_system() && self.0.systemMessage == "message_edited" } + /// return `true` if message is a reaction pub fn is_reaction(&self) -> bool { self.is_system() && self.0.systemMessage == "reaction" } + /// return `true` if message is a command pub fn is_command(&self) -> bool { self.0.messageType == "command" } + /// return `true` if message has any reactions pub fn has_reactions(&self) -> bool { !self.0.reactions.is_empty() } From 394e7a264f47d3a82b17da417d7478aacc4b0687 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Thu, 29 Aug 2024 01:37:38 +0200 Subject: [PATCH 32/89] best practice naming --- src/backend/nc_request.rs | 2 +- src/backend/nc_room.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/backend/nc_request.rs b/src/backend/nc_request.rs index c23dc2b..b2f8da6 100644 --- a/src/backend/nc_request.rs +++ b/src/backend/nc_request.rs @@ -32,7 +32,7 @@ pub struct NCReqMeta { #[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct NCReqDataMessageParameter { #[serde(rename = "type")] - paramtype: String, + param_type: String, id: String, name: String, } diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index edc5cc0..9bea148 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -191,17 +191,17 @@ impl NCRoom { pub async fn update_if_id_is_newer( &mut self, - messageid: i32, + message_id: i32, data_option: Option<&NCReqDataRoom>, ) -> Result<(), Box> { if let Some(last_internal_id) = self.get_last_room_level_message_id() { - match messageid.cmp(&last_internal_id) { + match message_id.cmp(&last_internal_id) { Ordering::Greater => { log::info!( "New Messages for '{}' was {} now {}", self.to_string(), last_internal_id, - messageid + message_id ); self.update(data_option).await?; } @@ -210,7 +210,7 @@ impl NCRoom { "Message Id was older than message stored '{}'! Stored {} Upstream {}", self.to_string(), last_internal_id, - messageid + message_id ); } Ordering::Equal => (), From fd677a12a309c0bf3913c9e9ee8c553056126418 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Thu, 29 Aug 2024 01:46:46 +0200 Subject: [PATCH 33/89] qualify lonely std usages --- src/backend/nc_request.rs | 14 ++++++++------ src/backend/nc_room.rs | 33 +++++++++++++++------------------ src/backend/nc_talk.rs | 17 ++++++----------- 3 files changed, 29 insertions(+), 35 deletions(-) diff --git a/src/backend/nc_request.rs b/src/backend/nc_request.rs index b2f8da6..9769b0a 100644 --- a/src/backend/nc_request.rs +++ b/src/backend/nc_request.rs @@ -2,6 +2,7 @@ #![allow(unused_variables)] #![allow(dead_code)] +use crate::config; use base64::{prelude::BASE64_STANDARD, write::EncoderWriter}; use jzon; use reqwest::{ @@ -9,17 +10,14 @@ use reqwest::{ Client, Response, Url, }; use serde::{Deserialize, Deserializer, Serialize}; -use std::path::PathBuf; -use std::{collections::HashMap, error::Error, fs::File, io::Write}; - -use crate::config; +use std::{collections::HashMap, error::Error}; #[derive(Debug, Clone)] pub struct NCRequest { base_url: String, client: Client, base_headers: HeaderMap, - json_dump_path: Option, + json_dump_path: Option, } #[derive(Serialize, Deserialize, Debug, Default)] @@ -247,6 +245,8 @@ pub struct NCReqOCS { impl NCRequest { pub fn new() -> Result> { + use std::io::Write; + let config = &config::get(); let general = &config.data.general; @@ -542,12 +542,14 @@ impl NCRequest { } fn dump_json_to_log(&self, url: &str, text: &str) -> Result<(), Box> { + use std::io::Write; + if let Some(path) = &self.json_dump_path { let name: String = url .chars() .map(|ch| if ch == '/' { '_' } else { ch }) .collect(); - let mut file = File::create(name)?; + let mut file = std::fs::File::create(name)?; let pretty_text = jzon::stringify_pretty(jzon::parse(text)?, 2); file.write_all(pretty_text.as_bytes())?; } diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index 9bea148..af1024a 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -6,13 +6,6 @@ use super::{ use log; use num_derive::FromPrimitive; use num_traits::{AsPrimitive, FromPrimitive}; -use std::{ - cmp::Ordering, - error::Error, - fs::{read_to_string, File}, - io::prelude::*, - path::PathBuf, -}; #[derive(Debug, FromPrimitive, PartialEq)] pub enum NCRoomTypes { @@ -30,7 +23,7 @@ pub struct NCRoom { notifier: NCNotify, pub messages: Vec, room_data: NCReqDataRoom, - path_to_log: PathBuf, + path_to_log: std::path::PathBuf, pub room_type: NCRoomTypes, participants: Vec, } @@ -40,7 +33,7 @@ impl NCRoom { requester: NCRequest, token: String, messages: &mut Vec, - ) -> Result<(), Box> { + ) -> Result<(), Box> { let response = requester .fetch_chat_initial(token.clone().as_str(), 200) .await?; @@ -54,7 +47,7 @@ impl NCRoom { room_data: NCReqDataRoom, requester: NCRequest, notifier: NCNotify, - path_to_log: PathBuf, + path_to_log: std::path::PathBuf, ) -> Option { let mut tmp_path_buf = path_to_log.clone(); tmp_path_buf.push(room_data.token.as_str()); @@ -64,7 +57,7 @@ impl NCRoom { if path.exists() { if let Ok(data) = serde_json::from_str::>( - read_to_string(path).unwrap().as_str(), + std::fs::read_to_string(path).unwrap().as_str(), ) { messages.extend(data.into_iter().map(Into::into)); } else { @@ -100,7 +93,7 @@ impl NCRoom { }) } - pub async fn send(&self, message: String) -> Result> { + pub async fn send(&self, message: String) -> Result> { log::debug!("Send Message {}", &message); let response = self .requester @@ -115,7 +108,7 @@ impl NCRoom { pub async fn update( &mut self, data_option: Option<&NCReqDataRoom>, - ) -> Result<(), Box> { + ) -> Result<(), Box> { if let Some(data) = data_option { self.room_data = data.clone(); } @@ -161,7 +154,7 @@ impl NCRoom { .map(|message| message.get_id()) } - pub async fn mark_as_read(&self) -> Result<(), Box> { + pub async fn mark_as_read(&self) -> Result<(), Box> { if !self.messages.is_empty() { self.requester .mark_chat_read( @@ -193,7 +186,9 @@ impl NCRoom { &mut self, message_id: i32, data_option: Option<&NCReqDataRoom>, - ) -> Result<(), Box> { + ) -> Result<(), Box> { + use std::cmp::Ordering; + if let Some(last_internal_id) = self.get_last_room_level_message_id() { match message_id.cmp(&last_internal_id) { Ordering::Greater => { @@ -228,10 +223,12 @@ impl NCRoom { } pub fn write_to_log(&mut self) -> Result<(), std::io::Error> { + use std::io::Write; + let data: Vec<_> = self.messages.iter().map(NCMessage::data).collect(); let path = self.path_to_log.as_path(); // Open a file in write-only mode, returns `io::Result` - let mut file = match File::create(path) { + let mut file = match std::fs::File::create(path) { Err(why) => { log::warn!( "Couldn't create log file {} for {}: {}", @@ -266,13 +263,13 @@ impl NCRoom { } impl Ord for NCRoom { - fn cmp(&self, other: &Self) -> Ordering { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.to_string().cmp(other) } } impl PartialOrd for NCRoom { - fn partial_cmp(&self, other: &Self) -> Option { + fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index c2e4886..b41104c 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -1,5 +1,3 @@ -use itertools::Itertools; - use crate::{ backend::{ nc_notify::NCNotify, @@ -9,13 +7,8 @@ use crate::{ config::{self}, }; use core::panic; -use std::{ - collections::HashMap, - error::Error, - fs::{read_to_string, File}, - io::prelude::*, - path::PathBuf, -}; +use itertools::Itertools; +use std::{collections::HashMap, error::Error, path::PathBuf}; #[derive(Debug)] pub struct NCTalk { @@ -84,7 +77,7 @@ impl NCTalk { if path.exists() { if let Ok(mut data) = serde_json::from_str::>( - read_to_string(path).unwrap().as_str(), + std::fs::read_to_string(path).unwrap().as_str(), ) { let mut handles = HashMap::new(); for (token, room) in &mut data { @@ -177,6 +170,8 @@ impl NCTalk { } pub fn write_to_log(&mut self) -> Result<(), std::io::Error> { + use std::io::Write; + let mut data = HashMap::::new(); let mut tmp_path_buf = self.chat_data_path.clone(); tmp_path_buf.push("Talk.json"); @@ -186,7 +181,7 @@ impl NCTalk { room.write_to_log()?; } // Open a file in write-only mode, returns `io::Result` - let mut file = match File::create(path) { + let mut file = match std::fs::File::create(path) { Err(why) => { log::warn!( "couldn't create top level log file {}: {}", From ca75e55d94e252c7a53eeacba0ee5a4d8a1dae9a Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Thu, 29 Aug 2024 01:52:59 +0200 Subject: [PATCH 34/89] fix member initialization order --- src/backend/nc_room.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index af1024a..611f709 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -76,7 +76,6 @@ impl NCRoom { .ok(); } - let type_num = room_data.roomtype; let participants = requester .fetch_participants(&room_data.token) .await @@ -85,11 +84,11 @@ impl NCRoom { Some(NCRoom { requester, notifier, - room_data, messages, path_to_log: tmp_path_buf, - room_type: FromPrimitive::from_i32(type_num).unwrap(), + room_type: FromPrimitive::from_i32(room_data.roomtype).unwrap(), participants, + room_data, }) } From 103163cfa847c3d5e9fdb7ea4da6f3c1bdb8a266 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Thu, 29 Aug 2024 02:04:34 +0200 Subject: [PATCH 35/89] remove some clone() --- src/backend/nc_room.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index 611f709..a54bd04 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -30,13 +30,11 @@ pub struct NCRoom { impl NCRoom { async fn fetch_messages( - requester: NCRequest, - token: String, + requester: &NCRequest, + token: &str, messages: &mut Vec, ) -> Result<(), Box> { - let response = requester - .fetch_chat_initial(token.clone().as_str(), 200) - .await?; + let response = requester.fetch_chat_initial(token, 200).await?; for message in response { messages.push(message.into()); } @@ -65,13 +63,13 @@ impl NCRoom { "Failed to parse json for {}, falling back to fetching", room_data.displayName ); - NCRoom::fetch_messages(requester.clone(), room_data.token.clone(), &mut messages) + NCRoom::fetch_messages(&requester, &room_data.token, &mut messages) .await .ok(); } } else { log::debug!("No Log File found for room {}", room_data.displayName); - NCRoom::fetch_messages(requester.clone(), room_data.token.clone(), &mut messages) + NCRoom::fetch_messages(&requester, &room_data.token, &mut messages) .await .ok(); } @@ -114,7 +112,7 @@ impl NCRoom { let response = self .requester .fetch_chat_update( - self.room_data.token.clone().as_str(), + self.room_data.token.as_str(), 200, self.messages.last().unwrap().get_id(), ) From b04042dc63afd3e7209f794ac21b8596c478c093 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Thu, 29 Aug 2024 02:43:50 +0200 Subject: [PATCH 36/89] remove mut --- src/config/mod.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 8945397..743acf7 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -31,14 +31,13 @@ pub fn init(path_arg: &str) { ); path_arg.into() }; - let mut config_path = config_path_base.clone(); - config_path.push("config.toml"); + let config_path = config_path_base.join("config.toml"); println!("Config Path: {:?}", config_path.as_os_str()); if !config_path.exists() { println!( - "Config files doesnt exist creating default now at {}.", + "Config files doesn't exist creating default now at {}.", config_path .as_os_str() .to_str() @@ -60,8 +59,7 @@ pub fn init(path_arg: &str) { Ok(good_data) => good_data, Err(why) => { println!("Please Update your config {why} "); - let mut example_config_path = config_path_base.clone(); - example_config_path.push("config.toml_new"); + let example_config_path = config_path_base.join("config.toml_new"); println!( "Writing example config to {}", example_config_path @@ -127,8 +125,10 @@ impl Config { self.strategy.data_dir() } pub fn get_server_data_dir(&self) -> PathBuf { - let mut path = self.strategy.data_dir(); - path.push(self.data.general.chat_server_name.clone()); + let path = self + .strategy + .data_dir() + .join(self.data.general.chat_server_name.clone()); if !path.exists() { std::fs::create_dir_all(path.clone()).expect("Failed to create server data path"); } @@ -154,8 +154,7 @@ impl Config { filter::threshold::ThresholdFilter, }; - let mut log_path = self.strategy.data_dir().clone(); - log_path.push("app.log"); + let log_path = self.strategy.data_dir().join("app.log"); // Build a stderr logger. let stderr = ConsoleAppender::builder() @@ -164,7 +163,7 @@ impl Config { .build(); // Logging to log file. - let logfile = FileAppender::builder() + let log_file = FileAppender::builder() // Pattern: https://docs.rs/log4rs/*/log4rs/encode/pattern/index.html .encoder(Box::new(PatternEncoder::new( "{d(%H:%M:%S)} {l} {M}: {m}{n}", @@ -185,7 +184,7 @@ impl Config { let mut root = Root::builder().appender("stderr"); if self.data.general.log_to_file { config_builder = - config_builder.appender(Appender::builder().build("logfile", Box::new(logfile))); + config_builder.appender(Appender::builder().build("logfile", Box::new(log_file))); root = root.appender("logfile"); } let config = config_builder From 3377acb435da91a26f4600c151aeab7a429903ed Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Wed, 28 Aug 2024 20:17:09 +0200 Subject: [PATCH 37/89] use tui_textarea for input box --- Cargo.toml | 1 + src/ui/app.rs | 31 +++++++++++++++----------- src/ui/input_box.rs | 47 +++++++++++++-------------------------- src/ui/mod.rs | 54 ++++++++++++++++++++++++++++++--------------- 4 files changed, 70 insertions(+), 63 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 66b0910..5886dff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ libc = "0.2.158" strip-ansi-escapes = "0.2.0" tracing = "0.1.40" cfg-if = "1.0.0" +tui-textarea = "0.6.1" [lints.clippy] pedantic = "warn" diff --git a/src/ui/app.rs b/src/ui/app.rs index 7550008..2f5e7af 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -7,6 +7,7 @@ use crate::{ }; use ratatui::{prelude::*, widgets::Paragraph}; use strum_macros::Display; +use tui_textarea::Input; #[derive(PartialEq, Clone, Copy, Display)] pub enum CurrentScreen { @@ -23,7 +24,7 @@ pub struct App<'a> { title: TitleBar<'a>, chat: ChatBox<'a>, pub selector: ChatSelector<'a>, - input: InputBox, + input: InputBox<'a>, help: HelpBox, } @@ -38,7 +39,7 @@ impl<'a> App<'a> { .to_string(), ), selector: ChatSelector::new(&backend), - input: InputBox::default(), + input: InputBox::new(""), chat: { let mut chat = ChatBox::new(); chat.update_messages(&backend); @@ -96,11 +97,19 @@ impl<'a> App<'a> { } pub async fn send_message(&mut self) -> Result<(), Box> { - self.backend.send_message(self.input.to_string()).await?; - self.input.clear(); - self.update_ui()?; - self.chat.select_last_message(); - Ok(()) + if self.input.is_empty() { + Ok(()) + } else { + self.backend + .send_message(self.input.lines().join("\n")) + .await?; + self.input.select_all(); + self.input.cut(); + self.input.select_all(); + self.update_ui()?; + self.chat.select_last_message(); + Ok(()) + } } pub async fn select_room(&mut self) -> Result<(), Box> { @@ -130,12 +139,8 @@ impl<'a> App<'a> { Ok(()) } - pub fn pop_input(&mut self) { - self.input.pop(); - } - - pub fn append_input(&mut self, new_input: char) { - self.input.push(new_input); + pub fn new_input_key(&mut self, key: Input) { + self.input.input(key); } pub fn scroll_up(&mut self) { diff --git a/src/ui/input_box.rs b/src/ui/input_box.rs index e174a73..bca940b 100644 --- a/src/ui/input_box.rs +++ b/src/ui/input_box.rs @@ -1,53 +1,36 @@ use ratatui::{ prelude::*, - widgets::{Block, Borders, Paragraph}, + widgets::{Block, Borders}, }; +use tui_textarea::TextArea; #[derive(Default)] -pub struct InputBox { - current_text: String, +pub struct InputBox<'a> { + textarea: TextArea<'a>, } -impl InputBox { - pub fn new(initial_message: &str) -> InputBox { - InputBox { - current_text: initial_message.to_string(), - } +impl<'a> InputBox<'a> { + pub fn new(initial_message: &str) -> InputBox<'a> { + let mut textarea = TextArea::new(vec![initial_message.into()]); + textarea.set_block(Block::default().borders(Borders::TOP)); + InputBox { textarea } } pub fn render_area(&self, frame: &mut Frame, area: Rect) { - frame.render_widget(self, area); + frame.render_widget(&self.textarea, area); } } -impl Widget for &InputBox { - fn render(self, area: Rect, buf: &mut Buffer) { - let text: Vec = textwrap::wrap( - ("> ".to_string() + &self.current_text).as_str(), - area.width as usize, - ) - .into_iter() - .map(std::borrow::Cow::into_owned) - .map(Line::from) - .collect(); - Paragraph::new(text) - .block(Block::default().borders(Borders::TOP)) - .style(Style::new().white().on_black()) - .alignment(Alignment::Left) - .render(area, buf); - } -} - -impl std::ops::Deref for InputBox { - type Target = String; +impl<'a> std::ops::Deref for InputBox<'a> { + type Target = TextArea<'a>; fn deref(&self) -> &Self::Target { - &self.current_text + &self.textarea } } -impl std::ops::DerefMut for InputBox { +impl<'a> std::ops::DerefMut for InputBox<'a> { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.current_text + &mut self.textarea } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 263c56e..448c2ce 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -18,13 +18,16 @@ use color_eyre::{ use crossterm::{ event::{ poll, read, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, - EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEventKind, + EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, + KeyboardEnhancementFlags, MouseEventKind, PopKeyboardEnhancementFlags, + PushKeyboardEnhancementFlags, }, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use ratatui::prelude::*; use tracing::error; +use tui_textarea::{Input, Key}; pub fn install_hooks() -> eyre::Result<()> { let (panic_hook, eyre_hook) = HookBuilder::default() @@ -90,6 +93,17 @@ pub fn init() -> eyre::Result { enable_raw_mode()?; execute!(stdout(), EnterAlternateScreen)?; + if execute!( + stdout(), + PushKeyboardEnhancementFlags( + KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES + | KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES + ) + ) + .is_err() + { + log::warn!("Consider using a Terminal that supports KeyboardEnhancementFlags."); + } if config::get().get_enable_mouse() { execute!(stdout(), EnableMouseCapture)?; } @@ -111,6 +125,8 @@ pub fn restore() -> eyre::Result<()> { if config::get().get_enable_mouse() { execute!(stdout(), DisableMouseCapture)?; } + //proceed here regardless of error, since this will fail if the terminal doesnt support this. + let _ = execute!(stdout(), PopKeyboardEnhancementFlags); execute!(stdout(), LeaveAlternateScreen)?; disable_raw_mode()?; Ok(()) @@ -166,8 +182,8 @@ async fn process_event( // It's guaranteed that `read` won't block, because `poll` returned // `Ok(true)`. match event { - Event::Key(key) if key.kind != KeyEventKind::Release => { - log::debug!("Processing key event {:?}", key.code); + Event::Key(key) => { + log::debug!("Processing key event {:?}", key); match app.current_screen { CurrentScreen::Helping => handle_key_in_help(key, app), CurrentScreen::Reading => handle_key_in_reading(key, app).await?, @@ -176,7 +192,9 @@ async fn process_event( return value; } } - CurrentScreen::Editing => handle_key_in_editing(key, app).await?, + CurrentScreen::Editing => { + handle_key_in_editing(Input::from(event.clone()), app).await?; + } CurrentScreen::Opening => handle_key_in_opening(key, app).await?, } } @@ -217,22 +235,22 @@ async fn handle_key_in_opening( } async fn handle_key_in_editing( - key: KeyEvent, + key: Input, app: &mut App<'_>, ) -> Result<(), Box> { - if key.kind == KeyEventKind::Press { - match key.code { - KeyCode::Enter => { - // SEND MEssage - app.current_screen = CurrentScreen::Reading; - app.send_message().await?; - } - KeyCode::Backspace => app.pop_input(), - KeyCode::Esc => app.current_screen = CurrentScreen::Reading, - KeyCode::Char(value) => app.append_input(value), - _ => (), - }; - } + match key { + Input { key: Key::Esc, .. } => app.current_screen = CurrentScreen::Reading, + Input { + key: Key::Enter, + shift: false, + .. + } => { + // SEND MEssage + app.current_screen = CurrentScreen::Reading; + app.send_message().await?; + } + _ => app.new_input_key(key), + }; Ok(()) } From b5dd15370ca2594f3b5d2602562d75d86c98da6a Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Thu, 29 Aug 2024 08:44:12 +0200 Subject: [PATCH 38/89] update codecov to be less noisy --- codecov.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/codecov.yml b/codecov.yml index 81aca0c..905c964 100644 --- a/codecov.yml +++ b/codecov.yml @@ -6,7 +6,13 @@ coverage: # https://docs.codecov.com/docs/codecovyml-reference#coverage project: default: threshold: 10% # Avoid false negatives, while numbers are so low. + patch: + default: + threshold: 2% comment: # https://docs.codecov.com/docs/pull-request-comments # make the comments less noisy require_changes: true + +github_checks: + annotations: false #Make commit readable again. From c469909370f1d195076fcb62ccfb834eab46e3a2 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Thu, 29 Aug 2024 09:12:34 +0200 Subject: [PATCH 39/89] make config init not call exit --- src/config/mod.rs | 9 +++++---- src/main.rs | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 2e1dede..6acf709 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -15,7 +15,7 @@ pub struct Config { strategy: Xdg, } -pub fn init(path_arg: &str) { +pub fn init(path_arg: &str) -> Result<(), Box> { let strategy = choose_app_strategy(AppStrategyArgs { top_level_domain: "org".to_string(), author: "emlix".to_string(), @@ -70,7 +70,7 @@ pub fn init(path_arg: &str) { .expect("Failed to make config path into string") ); Data::to_toml_example(example_config_path.as_os_str().to_str().unwrap()).unwrap(); - exit(-1); + return Err(Box::new(why)); } }; @@ -81,6 +81,7 @@ pub fn init(path_arg: &str) { .set(config) .map_err(|config| eyre!("failed to set config {config:?}")) .expect("Could not set global config!"); + Ok(()) } /// Get the application configuration. @@ -212,12 +213,12 @@ mod tests { expected = "Could not Create Config dir: Os { code: 13, kind: PermissionDenied, message: \"Permission denied\" }" )] fn init_with_faulty_path() { - init("/bogus_test/path"); + assert!(init("/bogus_test/path").is_err()); } #[test] fn default_values() { - init("./test/"); + assert!(init("./test/").is_ok()); assert!(get().get_data_dir().ends_with(".local/share/sechat-rs")); assert!(get() .get_server_data_dir() diff --git a/src/main.rs b/src/main.rs index 5239ed9..3ab0f78 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,7 @@ struct Args { async fn main() { let args = Args::parse(); - config::init(&args.config_path); + config::init(&args.config_path).expect("Config init aborted."); config::get().config_logging(); // check if crate has alpha suffix in version From 2494cd7ad2f38387b978e0428cce1558d8d103a7 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Thu, 29 Aug 2024 17:44:48 +0200 Subject: [PATCH 40/89] split nc_request into several files --- src/backend/nc_message.rs | 2 +- .../{nc_request.rs => nc_request/mod.rs} | 227 +----------------- src/backend/nc_request/nc_req_data_message.rs | 70 ++++++ src/backend/nc_request/nc_req_data_room.rs | 75 ++++++ src/backend/nc_request/nc_req_data_user.rs | 58 +++++ .../nc_request/nc_request_ocs_wrapper.rs | 19 ++ 6 files changed, 234 insertions(+), 217 deletions(-) rename src/backend/{nc_request.rs => nc_request/mod.rs} (67%) create mode 100644 src/backend/nc_request/nc_req_data_message.rs create mode 100644 src/backend/nc_request/nc_req_data_room.rs create mode 100644 src/backend/nc_request/nc_req_data_user.rs create mode 100644 src/backend/nc_request/nc_request_ocs_wrapper.rs diff --git a/src/backend/nc_message.rs b/src/backend/nc_message.rs index a4c2225..7eaedc1 100644 --- a/src/backend/nc_message.rs +++ b/src/backend/nc_message.rs @@ -1,4 +1,4 @@ -use crate::backend::nc_request::NCReqDataMessage; +use super::nc_request::NCReqDataMessage; use chrono::prelude::*; /// `NextCloud` message interface diff --git a/src/backend/nc_request.rs b/src/backend/nc_request/mod.rs similarity index 67% rename from src/backend/nc_request.rs rename to src/backend/nc_request/mod.rs index 9769b0a..feb5f80 100644 --- a/src/backend/nc_request.rs +++ b/src/backend/nc_request/mod.rs @@ -2,6 +2,16 @@ #![allow(unused_variables)] #![allow(dead_code)] +mod nc_req_data_message; +mod nc_req_data_room; +mod nc_req_data_user; +mod nc_request_ocs_wrapper; + +pub use nc_req_data_message::*; +pub use nc_req_data_room::*; +pub use nc_req_data_user::*; +pub use nc_request_ocs_wrapper::*; + use crate::config; use base64::{prelude::BASE64_STANDARD, write::EncoderWriter}; use jzon; @@ -9,7 +19,7 @@ use reqwest::{ header::{HeaderMap, HeaderValue, AUTHORIZATION}, Client, Response, Url, }; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; use std::{collections::HashMap, error::Error}; #[derive(Debug, Clone)] @@ -20,13 +30,6 @@ pub struct NCRequest { json_dump_path: Option, } -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct NCReqMeta { - status: String, - statuscode: i32, - message: String, -} - #[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct NCReqDataMessageParameter { #[serde(rename = "type")] @@ -35,214 +38,6 @@ pub struct NCReqDataMessageParameter { name: String, } -fn str_or_status<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - #[derive(Deserialize)] - #[serde(untagged)] - enum NCReqDataMessageVec { - ParamMap(Box), - String(String), - } - - Ok(match NCReqDataMessageVec::deserialize(deserializer)? { - NCReqDataMessageVec::ParamMap(v) => *v, // Ignoring parsing errors - NCReqDataMessageVec::String(_) => NCReqDataUserStatus::default(), - }) -} - -fn arr_or_message<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - #[derive(Deserialize)] - #[serde(untagged)] - enum NCReqDataMessageVec { - ParamMap(Box), - Vec(Vec), - } - - Ok(match NCReqDataMessageVec::deserialize(deserializer)? { - NCReqDataMessageVec::ParamMap(v) => *v, // Ignoring parsing errors - NCReqDataMessageVec::Vec(_) => NCReqDataMessage::default(), - }) -} - -fn arr_or_messageParam<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - #[derive(Deserialize)] - #[serde(untagged)] - enum NCReqDataMessageParameterMap { - ParamMap(HashMap), - Vec(Vec), - } - - Ok( - match NCReqDataMessageParameterMap::deserialize(deserializer)? { - NCReqDataMessageParameterMap::ParamMap(v) => v, // Ignoring parsing errors - NCReqDataMessageParameterMap::Vec(_) => HashMap::new(), - }, - ) -} - -#[derive(Serialize, Deserialize, Debug, Default, Clone)] -pub struct NCReqDataMessageParent { - pub id: i32, - pub token: String, - pub actorType: String, - pub actorId: String, - pub actorDisplayName: String, - pub timestamp: i32, - pub systemMessage: String, - pub messageType: String, - pub isReplyable: bool, - pub referenceId: String, - pub message: String, - #[serde(deserialize_with = "arr_or_messageParam")] - pub messageParameters: HashMap, - pub expirationTimestamp: i32, - pub reactions: HashMap, - #[serde(default)] - pub reactionsSelf: Vec, - pub markdown: bool, -} - -#[derive(Serialize, Deserialize, Debug, Default, Clone)] -pub struct NCReqDataMessage { - pub id: i32, - pub token: String, - pub actorType: String, - pub actorId: String, - pub actorDisplayName: String, - pub timestamp: i64, - pub systemMessage: String, - pub messageType: String, - pub isReplyable: bool, - pub referenceId: String, - pub message: String, - #[serde(deserialize_with = "arr_or_messageParam")] - pub messageParameters: HashMap, - pub expirationTimestamp: i32, - #[serde(default)] - pub parent: NCReqDataMessageParent, - pub reactions: HashMap, - #[serde(default)] - pub reactionsSelf: Vec, - pub markdown: bool, -} - -#[derive(Serialize, Deserialize, Debug, Default, Clone)] -#[allow(clippy::struct_excessive_bools)] -pub struct NCReqDataRoom { - pub id: i32, - pub token: String, - #[serde(rename = "type")] - pub roomtype: i32, - pub name: String, - pub displayName: String, - pub description: String, - pub participantType: i32, - pub attendeeId: i32, - pub attendeePin: String, - pub actorType: String, - pub actorId: String, - pub permissions: i32, - pub attendeePermissions: i32, - pub callPermissions: i32, - pub defaultPermissions: i32, - pub participantFlags: i32, - pub readOnly: i32, - pub listable: i32, - pub messageExpiration: i32, - lastPing: i32, - sessionId: String, - hasPassword: bool, - hasCall: bool, - callFlag: i32, - canStartCall: bool, - canDeleteConversation: bool, - canLeaveConversation: bool, - lastActivity: i32, - isFavorite: bool, - notificationLevel: i32, - lobbyState: i32, - lobbyTimer: i32, - sipEnabled: i32, - canEnableSIP: bool, - pub unreadMessages: i32, - unreadMention: bool, - unreadMentionDirect: bool, - pub lastReadMessage: i32, - lastCommonReadMessage: i32, - #[serde(deserialize_with = "arr_or_message")] - pub lastMessage: NCReqDataMessage, - objectType: String, - objectId: String, - breakoutRoomMode: i32, - breakoutRoomStatus: i32, - avatarVersion: String, - isCustomAvatar: bool, - callStartTime: i32, - callRecording: i32, - recordingConsent: i32, -} - -#[derive(Serialize, Deserialize, Debug, Default, Clone)] -pub struct NCReqDataParticipants { - attendeeId: i32, - actorType: String, - actorId: String, - displayName: String, - participantType: i32, - lastPing: i32, - inCall: i32, - permissions: i32, - attendeePermissions: i32, - sessionIds: Vec, - status: Option, - statusIcon: Option, - statusMessage: Option, - statusClearAt: Option, - roomToken: Option, - phoneNumber: Option, -} - -#[derive(Serialize, Deserialize, Debug, Default, Clone)] -pub struct NCReqDataUserStatus { - status: String, - message: Option, - icon: Option, - clearAt: Option, -} - -#[derive(Serialize, Deserialize, Debug, Default, Clone)] -pub struct NCReqDataUser { - id: String, - label: String, - icon: String, - source: String, - #[serde(deserialize_with = "str_or_status")] - status: NCReqDataUserStatus, - subline: String, - shareWithDisplayNameUnique: String, -} - -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct NCReqOCSWrapper { - ocs: NCReqOCS, -} - -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct NCReqOCS { - meta: NCReqMeta, - data: T, -} - impl NCRequest { pub fn new() -> Result> { use std::io::Write; diff --git a/src/backend/nc_request/nc_req_data_message.rs b/src/backend/nc_request/nc_req_data_message.rs new file mode 100644 index 0000000..1b95248 --- /dev/null +++ b/src/backend/nc_request/nc_req_data_message.rs @@ -0,0 +1,70 @@ +use super::NCReqDataMessageParameter; +use serde::{Deserialize, Deserializer, Serialize}; +use std::collections::HashMap; + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct NCReqDataMessage { + pub id: i32, + pub token: String, + pub actorType: String, + pub actorId: String, + pub actorDisplayName: String, + pub timestamp: i64, + pub systemMessage: String, + pub messageType: String, + pub isReplyable: bool, + pub referenceId: String, + pub message: String, + #[serde(deserialize_with = "arr_or_messageParam")] + pub messageParameters: HashMap, + pub expirationTimestamp: i32, + #[serde(default)] + pub parent: NCReqDataMessageParent, + pub reactions: HashMap, + #[serde(default)] + pub reactionsSelf: Vec, + pub markdown: bool, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct NCReqDataMessageParent { + pub id: i32, + pub token: String, + pub actorType: String, + pub actorId: String, + pub actorDisplayName: String, + pub timestamp: i32, + pub systemMessage: String, + pub messageType: String, + pub isReplyable: bool, + pub referenceId: String, + pub message: String, + #[serde(deserialize_with = "arr_or_messageParam")] + pub messageParameters: HashMap, + pub expirationTimestamp: i32, + pub reactions: HashMap, + #[serde(default)] + pub reactionsSelf: Vec, + pub markdown: bool, +} + +fn arr_or_messageParam<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum NCReqDataMessageParameterMap { + ParamMap(HashMap), + Vec(Vec), + } + + Ok( + match NCReqDataMessageParameterMap::deserialize(deserializer)? { + NCReqDataMessageParameterMap::ParamMap(v) => v, // Ignoring parsing errors + NCReqDataMessageParameterMap::Vec(_) => HashMap::new(), + }, + ) +} diff --git a/src/backend/nc_request/nc_req_data_room.rs b/src/backend/nc_request/nc_req_data_room.rs new file mode 100644 index 0000000..4994665 --- /dev/null +++ b/src/backend/nc_request/nc_req_data_room.rs @@ -0,0 +1,75 @@ +use super::NCReqDataMessage; +use serde::{Deserialize, Deserializer, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[allow(clippy::struct_excessive_bools)] +pub struct NCReqDataRoom { + pub id: i32, + pub token: String, + #[serde(rename = "type")] + pub roomtype: i32, + pub name: String, + pub displayName: String, + pub description: String, + pub participantType: i32, + pub attendeeId: i32, + pub attendeePin: String, + pub actorType: String, + pub actorId: String, + pub permissions: i32, + pub attendeePermissions: i32, + pub callPermissions: i32, + pub defaultPermissions: i32, + pub participantFlags: i32, + pub readOnly: i32, + pub listable: i32, + pub messageExpiration: i32, + lastPing: i32, + sessionId: String, + hasPassword: bool, + hasCall: bool, + callFlag: i32, + canStartCall: bool, + canDeleteConversation: bool, + canLeaveConversation: bool, + lastActivity: i32, + isFavorite: bool, + notificationLevel: i32, + lobbyState: i32, + lobbyTimer: i32, + sipEnabled: i32, + canEnableSIP: bool, + pub unreadMessages: i32, + unreadMention: bool, + unreadMentionDirect: bool, + pub lastReadMessage: i32, + lastCommonReadMessage: i32, + #[serde(deserialize_with = "arr_or_message")] + pub lastMessage: NCReqDataMessage, + objectType: String, + objectId: String, + breakoutRoomMode: i32, + breakoutRoomStatus: i32, + avatarVersion: String, + isCustomAvatar: bool, + callStartTime: i32, + callRecording: i32, + recordingConsent: i32, +} + +fn arr_or_message<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum NCReqDataMessageVec { + ParamMap(Box), + Vec(Vec), + } + + Ok(match NCReqDataMessageVec::deserialize(deserializer)? { + NCReqDataMessageVec::ParamMap(v) => *v, // Ignoring parsing errors + NCReqDataMessageVec::Vec(_) => NCReqDataMessage::default(), + }) +} diff --git a/src/backend/nc_request/nc_req_data_user.rs b/src/backend/nc_request/nc_req_data_user.rs new file mode 100644 index 0000000..47c2fdd --- /dev/null +++ b/src/backend/nc_request/nc_req_data_user.rs @@ -0,0 +1,58 @@ +use serde::{Deserialize, Deserializer, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct NCReqDataParticipants { + attendeeId: i32, + actorType: String, + actorId: String, + displayName: String, + participantType: i32, + lastPing: i32, + inCall: i32, + permissions: i32, + attendeePermissions: i32, + sessionIds: Vec, + status: Option, + statusIcon: Option, + statusMessage: Option, + statusClearAt: Option, + roomToken: Option, + phoneNumber: Option, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct NCReqDataUserStatus { + status: String, + message: Option, + icon: Option, + clearAt: Option, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct NCReqDataUser { + id: String, + label: String, + icon: String, + source: String, + #[serde(deserialize_with = "str_or_status")] + status: NCReqDataUserStatus, + subline: String, + shareWithDisplayNameUnique: String, +} + +fn str_or_status<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum NCReqDataMessageVec { + ParamMap(Box), + String(String), + } + + Ok(match NCReqDataMessageVec::deserialize(deserializer)? { + NCReqDataMessageVec::ParamMap(v) => *v, // Ignoring parsing errors + NCReqDataMessageVec::String(_) => NCReqDataUserStatus::default(), + }) +} diff --git a/src/backend/nc_request/nc_request_ocs_wrapper.rs b/src/backend/nc_request/nc_request_ocs_wrapper.rs new file mode 100644 index 0000000..373e9a1 --- /dev/null +++ b/src/backend/nc_request/nc_request_ocs_wrapper.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct NCReqOCSWrapper { + pub ocs: NCReqOCS, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct NCReqOCS { + pub meta: NCReqMeta, + pub data: T, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct NCReqMeta { + status: String, + statuscode: i32, + message: String, +} From 12a6745667f8ccaec6bad14fd5b9e9c0812e67d4 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Fri, 30 Aug 2024 09:11:27 +0200 Subject: [PATCH 41/89] add first simple requester test --- src/backend/nc_request.rs | 13 +++++++++++++ src/config/mod.rs | 13 +++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/backend/nc_request.rs b/src/backend/nc_request.rs index 9769b0a..6934126 100644 --- a/src/backend/nc_request.rs +++ b/src/backend/nc_request.rs @@ -556,3 +556,16 @@ impl NCRequest { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn new_requester() { + let _ = config::init("./test/"); + let result = NCRequest::new(); + assert!(result.is_ok()); + let requester = result.unwrap(); + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index 6acf709..8a32673 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -15,7 +15,7 @@ pub struct Config { strategy: Xdg, } -pub fn init(path_arg: &str) -> Result<(), Box> { +pub fn init(path_arg: &str) -> Result<(), color_eyre::eyre::Report> { let strategy = choose_app_strategy(AppStrategyArgs { top_level_domain: "org".to_string(), author: "emlix".to_string(), @@ -70,7 +70,7 @@ pub fn init(path_arg: &str) -> Result<(), Box> { .expect("Failed to make config path into string") ); Data::to_toml_example(example_config_path.as_os_str().to_str().unwrap()).unwrap(); - return Err(Box::new(why)); + return Err(eyre!(why)); } }; @@ -79,9 +79,7 @@ pub fn init(path_arg: &str) -> Result<(), Box> { config.set_strategy(strategy); CONFIG .set(config) - .map_err(|config| eyre!("failed to set config {config:?}")) - .expect("Could not set global config!"); - Ok(()) + .map_err(|config| eyre!("Failed to set config {config:?}")) } /// Get the application configuration. @@ -201,8 +199,6 @@ impl Config { mod tests { use super::*; - // The ordering of these tests is important since we set the static CONFIG object! - #[test] #[should_panic(expected = "config not initialized")] fn get_config_before_init() { @@ -218,7 +214,8 @@ mod tests { #[test] fn default_values() { - assert!(init("./test/").is_ok()); + // since we cant control the order of the tests we cannot be sure that this returns suchess. + let _ = init("./test/"); assert!(get().get_data_dir().ends_with(".local/share/sechat-rs")); assert!(get() .get_server_data_dir() From 609622f291817e5bba0512a9255f184f7044bffe Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Thu, 29 Aug 2024 02:43:50 +0200 Subject: [PATCH 42/89] remove mut --- src/config/mod.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 8a32673..b8087b5 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -31,14 +31,13 @@ pub fn init(path_arg: &str) -> Result<(), color_eyre::eyre::Report> { ); path_arg.into() }; - let mut config_path = config_path_base.clone(); - config_path.push("config.toml"); + let config_path = config_path_base.join("config.toml"); println!("Config Path: {:?}", config_path.as_os_str()); if !config_path.exists() { println!( - "Config files doesnt exist creating default now at {}.", + "Config files doesn't exist creating default now at {}.", config_path .as_os_str() .to_str() @@ -60,8 +59,7 @@ pub fn init(path_arg: &str) -> Result<(), color_eyre::eyre::Report> { Ok(good_data) => good_data, Err(why) => { println!("Please Update your config {why} "); - let mut example_config_path = config_path_base.clone(); - example_config_path.push("config.toml_new"); + let example_config_path = config_path_base.join("config.toml_new"); println!( "Writing example config to {}", example_config_path @@ -126,8 +124,10 @@ impl Config { self.strategy.data_dir() } pub fn get_server_data_dir(&self) -> PathBuf { - let mut path = self.strategy.data_dir(); - path.push(self.data.general.chat_server_name.clone()); + let path = self + .strategy + .data_dir() + .join(self.data.general.chat_server_name.clone()); if !path.exists() { std::fs::create_dir_all(path.clone()).expect("Failed to create server data path"); } @@ -153,8 +153,7 @@ impl Config { filter::threshold::ThresholdFilter, }; - let mut log_path = self.strategy.data_dir().clone(); - log_path.push("app.log"); + let log_path = self.strategy.data_dir().join("app.log"); // Build a stderr logger. let stderr = ConsoleAppender::builder() @@ -163,7 +162,7 @@ impl Config { .build(); // Logging to log file. - let logfile = FileAppender::builder() + let log_file = FileAppender::builder() // Pattern: https://docs.rs/log4rs/*/log4rs/encode/pattern/index.html .encoder(Box::new(PatternEncoder::new( "{d(%H:%M:%S)} {l} {M}: {m}{n}", @@ -184,7 +183,7 @@ impl Config { let mut root = Root::builder().appender("stderr"); if self.data.general.log_to_file { config_builder = - config_builder.appender(Appender::builder().build("logfile", Box::new(logfile))); + config_builder.appender(Appender::builder().build("logfile", Box::new(log_file))); root = root.appender("logfile"); } let config = config_builder From f33bc8ee294013a55577970745e030410148c4f4 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Thu, 29 Aug 2024 17:44:48 +0200 Subject: [PATCH 43/89] split nc_request into several files --- src/backend/nc_message.rs | 2 +- .../{nc_request.rs => nc_request/mod.rs} | 227 +----------------- src/backend/nc_request/nc_req_data_message.rs | 70 ++++++ src/backend/nc_request/nc_req_data_room.rs | 75 ++++++ src/backend/nc_request/nc_req_data_user.rs | 58 +++++ .../nc_request/nc_request_ocs_wrapper.rs | 19 ++ 6 files changed, 234 insertions(+), 217 deletions(-) rename src/backend/{nc_request.rs => nc_request/mod.rs} (67%) create mode 100644 src/backend/nc_request/nc_req_data_message.rs create mode 100644 src/backend/nc_request/nc_req_data_room.rs create mode 100644 src/backend/nc_request/nc_req_data_user.rs create mode 100644 src/backend/nc_request/nc_request_ocs_wrapper.rs diff --git a/src/backend/nc_message.rs b/src/backend/nc_message.rs index a4c2225..7eaedc1 100644 --- a/src/backend/nc_message.rs +++ b/src/backend/nc_message.rs @@ -1,4 +1,4 @@ -use crate::backend::nc_request::NCReqDataMessage; +use super::nc_request::NCReqDataMessage; use chrono::prelude::*; /// `NextCloud` message interface diff --git a/src/backend/nc_request.rs b/src/backend/nc_request/mod.rs similarity index 67% rename from src/backend/nc_request.rs rename to src/backend/nc_request/mod.rs index 6934126..828b01e 100644 --- a/src/backend/nc_request.rs +++ b/src/backend/nc_request/mod.rs @@ -2,6 +2,16 @@ #![allow(unused_variables)] #![allow(dead_code)] +mod nc_req_data_message; +mod nc_req_data_room; +mod nc_req_data_user; +mod nc_request_ocs_wrapper; + +pub use nc_req_data_message::*; +pub use nc_req_data_room::*; +pub use nc_req_data_user::*; +pub use nc_request_ocs_wrapper::*; + use crate::config; use base64::{prelude::BASE64_STANDARD, write::EncoderWriter}; use jzon; @@ -9,7 +19,7 @@ use reqwest::{ header::{HeaderMap, HeaderValue, AUTHORIZATION}, Client, Response, Url, }; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; use std::{collections::HashMap, error::Error}; #[derive(Debug, Clone)] @@ -20,13 +30,6 @@ pub struct NCRequest { json_dump_path: Option, } -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct NCReqMeta { - status: String, - statuscode: i32, - message: String, -} - #[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct NCReqDataMessageParameter { #[serde(rename = "type")] @@ -35,214 +38,6 @@ pub struct NCReqDataMessageParameter { name: String, } -fn str_or_status<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - #[derive(Deserialize)] - #[serde(untagged)] - enum NCReqDataMessageVec { - ParamMap(Box), - String(String), - } - - Ok(match NCReqDataMessageVec::deserialize(deserializer)? { - NCReqDataMessageVec::ParamMap(v) => *v, // Ignoring parsing errors - NCReqDataMessageVec::String(_) => NCReqDataUserStatus::default(), - }) -} - -fn arr_or_message<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - #[derive(Deserialize)] - #[serde(untagged)] - enum NCReqDataMessageVec { - ParamMap(Box), - Vec(Vec), - } - - Ok(match NCReqDataMessageVec::deserialize(deserializer)? { - NCReqDataMessageVec::ParamMap(v) => *v, // Ignoring parsing errors - NCReqDataMessageVec::Vec(_) => NCReqDataMessage::default(), - }) -} - -fn arr_or_messageParam<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - #[derive(Deserialize)] - #[serde(untagged)] - enum NCReqDataMessageParameterMap { - ParamMap(HashMap), - Vec(Vec), - } - - Ok( - match NCReqDataMessageParameterMap::deserialize(deserializer)? { - NCReqDataMessageParameterMap::ParamMap(v) => v, // Ignoring parsing errors - NCReqDataMessageParameterMap::Vec(_) => HashMap::new(), - }, - ) -} - -#[derive(Serialize, Deserialize, Debug, Default, Clone)] -pub struct NCReqDataMessageParent { - pub id: i32, - pub token: String, - pub actorType: String, - pub actorId: String, - pub actorDisplayName: String, - pub timestamp: i32, - pub systemMessage: String, - pub messageType: String, - pub isReplyable: bool, - pub referenceId: String, - pub message: String, - #[serde(deserialize_with = "arr_or_messageParam")] - pub messageParameters: HashMap, - pub expirationTimestamp: i32, - pub reactions: HashMap, - #[serde(default)] - pub reactionsSelf: Vec, - pub markdown: bool, -} - -#[derive(Serialize, Deserialize, Debug, Default, Clone)] -pub struct NCReqDataMessage { - pub id: i32, - pub token: String, - pub actorType: String, - pub actorId: String, - pub actorDisplayName: String, - pub timestamp: i64, - pub systemMessage: String, - pub messageType: String, - pub isReplyable: bool, - pub referenceId: String, - pub message: String, - #[serde(deserialize_with = "arr_or_messageParam")] - pub messageParameters: HashMap, - pub expirationTimestamp: i32, - #[serde(default)] - pub parent: NCReqDataMessageParent, - pub reactions: HashMap, - #[serde(default)] - pub reactionsSelf: Vec, - pub markdown: bool, -} - -#[derive(Serialize, Deserialize, Debug, Default, Clone)] -#[allow(clippy::struct_excessive_bools)] -pub struct NCReqDataRoom { - pub id: i32, - pub token: String, - #[serde(rename = "type")] - pub roomtype: i32, - pub name: String, - pub displayName: String, - pub description: String, - pub participantType: i32, - pub attendeeId: i32, - pub attendeePin: String, - pub actorType: String, - pub actorId: String, - pub permissions: i32, - pub attendeePermissions: i32, - pub callPermissions: i32, - pub defaultPermissions: i32, - pub participantFlags: i32, - pub readOnly: i32, - pub listable: i32, - pub messageExpiration: i32, - lastPing: i32, - sessionId: String, - hasPassword: bool, - hasCall: bool, - callFlag: i32, - canStartCall: bool, - canDeleteConversation: bool, - canLeaveConversation: bool, - lastActivity: i32, - isFavorite: bool, - notificationLevel: i32, - lobbyState: i32, - lobbyTimer: i32, - sipEnabled: i32, - canEnableSIP: bool, - pub unreadMessages: i32, - unreadMention: bool, - unreadMentionDirect: bool, - pub lastReadMessage: i32, - lastCommonReadMessage: i32, - #[serde(deserialize_with = "arr_or_message")] - pub lastMessage: NCReqDataMessage, - objectType: String, - objectId: String, - breakoutRoomMode: i32, - breakoutRoomStatus: i32, - avatarVersion: String, - isCustomAvatar: bool, - callStartTime: i32, - callRecording: i32, - recordingConsent: i32, -} - -#[derive(Serialize, Deserialize, Debug, Default, Clone)] -pub struct NCReqDataParticipants { - attendeeId: i32, - actorType: String, - actorId: String, - displayName: String, - participantType: i32, - lastPing: i32, - inCall: i32, - permissions: i32, - attendeePermissions: i32, - sessionIds: Vec, - status: Option, - statusIcon: Option, - statusMessage: Option, - statusClearAt: Option, - roomToken: Option, - phoneNumber: Option, -} - -#[derive(Serialize, Deserialize, Debug, Default, Clone)] -pub struct NCReqDataUserStatus { - status: String, - message: Option, - icon: Option, - clearAt: Option, -} - -#[derive(Serialize, Deserialize, Debug, Default, Clone)] -pub struct NCReqDataUser { - id: String, - label: String, - icon: String, - source: String, - #[serde(deserialize_with = "str_or_status")] - status: NCReqDataUserStatus, - subline: String, - shareWithDisplayNameUnique: String, -} - -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct NCReqOCSWrapper { - ocs: NCReqOCS, -} - -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct NCReqOCS { - meta: NCReqMeta, - data: T, -} - impl NCRequest { pub fn new() -> Result> { use std::io::Write; diff --git a/src/backend/nc_request/nc_req_data_message.rs b/src/backend/nc_request/nc_req_data_message.rs new file mode 100644 index 0000000..1b95248 --- /dev/null +++ b/src/backend/nc_request/nc_req_data_message.rs @@ -0,0 +1,70 @@ +use super::NCReqDataMessageParameter; +use serde::{Deserialize, Deserializer, Serialize}; +use std::collections::HashMap; + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct NCReqDataMessage { + pub id: i32, + pub token: String, + pub actorType: String, + pub actorId: String, + pub actorDisplayName: String, + pub timestamp: i64, + pub systemMessage: String, + pub messageType: String, + pub isReplyable: bool, + pub referenceId: String, + pub message: String, + #[serde(deserialize_with = "arr_or_messageParam")] + pub messageParameters: HashMap, + pub expirationTimestamp: i32, + #[serde(default)] + pub parent: NCReqDataMessageParent, + pub reactions: HashMap, + #[serde(default)] + pub reactionsSelf: Vec, + pub markdown: bool, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct NCReqDataMessageParent { + pub id: i32, + pub token: String, + pub actorType: String, + pub actorId: String, + pub actorDisplayName: String, + pub timestamp: i32, + pub systemMessage: String, + pub messageType: String, + pub isReplyable: bool, + pub referenceId: String, + pub message: String, + #[serde(deserialize_with = "arr_or_messageParam")] + pub messageParameters: HashMap, + pub expirationTimestamp: i32, + pub reactions: HashMap, + #[serde(default)] + pub reactionsSelf: Vec, + pub markdown: bool, +} + +fn arr_or_messageParam<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum NCReqDataMessageParameterMap { + ParamMap(HashMap), + Vec(Vec), + } + + Ok( + match NCReqDataMessageParameterMap::deserialize(deserializer)? { + NCReqDataMessageParameterMap::ParamMap(v) => v, // Ignoring parsing errors + NCReqDataMessageParameterMap::Vec(_) => HashMap::new(), + }, + ) +} diff --git a/src/backend/nc_request/nc_req_data_room.rs b/src/backend/nc_request/nc_req_data_room.rs new file mode 100644 index 0000000..4994665 --- /dev/null +++ b/src/backend/nc_request/nc_req_data_room.rs @@ -0,0 +1,75 @@ +use super::NCReqDataMessage; +use serde::{Deserialize, Deserializer, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[allow(clippy::struct_excessive_bools)] +pub struct NCReqDataRoom { + pub id: i32, + pub token: String, + #[serde(rename = "type")] + pub roomtype: i32, + pub name: String, + pub displayName: String, + pub description: String, + pub participantType: i32, + pub attendeeId: i32, + pub attendeePin: String, + pub actorType: String, + pub actorId: String, + pub permissions: i32, + pub attendeePermissions: i32, + pub callPermissions: i32, + pub defaultPermissions: i32, + pub participantFlags: i32, + pub readOnly: i32, + pub listable: i32, + pub messageExpiration: i32, + lastPing: i32, + sessionId: String, + hasPassword: bool, + hasCall: bool, + callFlag: i32, + canStartCall: bool, + canDeleteConversation: bool, + canLeaveConversation: bool, + lastActivity: i32, + isFavorite: bool, + notificationLevel: i32, + lobbyState: i32, + lobbyTimer: i32, + sipEnabled: i32, + canEnableSIP: bool, + pub unreadMessages: i32, + unreadMention: bool, + unreadMentionDirect: bool, + pub lastReadMessage: i32, + lastCommonReadMessage: i32, + #[serde(deserialize_with = "arr_or_message")] + pub lastMessage: NCReqDataMessage, + objectType: String, + objectId: String, + breakoutRoomMode: i32, + breakoutRoomStatus: i32, + avatarVersion: String, + isCustomAvatar: bool, + callStartTime: i32, + callRecording: i32, + recordingConsent: i32, +} + +fn arr_or_message<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum NCReqDataMessageVec { + ParamMap(Box), + Vec(Vec), + } + + Ok(match NCReqDataMessageVec::deserialize(deserializer)? { + NCReqDataMessageVec::ParamMap(v) => *v, // Ignoring parsing errors + NCReqDataMessageVec::Vec(_) => NCReqDataMessage::default(), + }) +} diff --git a/src/backend/nc_request/nc_req_data_user.rs b/src/backend/nc_request/nc_req_data_user.rs new file mode 100644 index 0000000..47c2fdd --- /dev/null +++ b/src/backend/nc_request/nc_req_data_user.rs @@ -0,0 +1,58 @@ +use serde::{Deserialize, Deserializer, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct NCReqDataParticipants { + attendeeId: i32, + actorType: String, + actorId: String, + displayName: String, + participantType: i32, + lastPing: i32, + inCall: i32, + permissions: i32, + attendeePermissions: i32, + sessionIds: Vec, + status: Option, + statusIcon: Option, + statusMessage: Option, + statusClearAt: Option, + roomToken: Option, + phoneNumber: Option, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct NCReqDataUserStatus { + status: String, + message: Option, + icon: Option, + clearAt: Option, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct NCReqDataUser { + id: String, + label: String, + icon: String, + source: String, + #[serde(deserialize_with = "str_or_status")] + status: NCReqDataUserStatus, + subline: String, + shareWithDisplayNameUnique: String, +} + +fn str_or_status<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum NCReqDataMessageVec { + ParamMap(Box), + String(String), + } + + Ok(match NCReqDataMessageVec::deserialize(deserializer)? { + NCReqDataMessageVec::ParamMap(v) => *v, // Ignoring parsing errors + NCReqDataMessageVec::String(_) => NCReqDataUserStatus::default(), + }) +} diff --git a/src/backend/nc_request/nc_request_ocs_wrapper.rs b/src/backend/nc_request/nc_request_ocs_wrapper.rs new file mode 100644 index 0000000..373e9a1 --- /dev/null +++ b/src/backend/nc_request/nc_request_ocs_wrapper.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct NCReqOCSWrapper { + pub ocs: NCReqOCS, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct NCReqOCS { + pub meta: NCReqMeta, + pub data: T, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct NCReqMeta { + status: String, + statuscode: i32, + message: String, +} From d55217023ae72ceb6b35d850e018f9421be7a6f2 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz Date: Sat, 31 Aug 2024 20:08:35 +0200 Subject: [PATCH 44/89] format --- src/backend/nc_request/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backend/nc_request/mod.rs b/src/backend/nc_request/mod.rs index f9c5339..828b01e 100644 --- a/src/backend/nc_request/mod.rs +++ b/src/backend/nc_request/mod.rs @@ -364,4 +364,3 @@ mod tests { let requester = result.unwrap(); } } - From 1457a5635ad4249333daa4e7a4991768a7613f5f Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Fri, 9 Aug 2024 15:42:48 +0200 Subject: [PATCH 45/89] add user sidebar with toggle key and showing of status --- src/backend/nc_request/mod.rs | 1 + src/backend/nc_request/nc_req_data_user.rs | 5 +- src/backend/nc_room.rs | 20 +++++++ src/config/data/ui.rs | 3 + src/ui/app.rs | 35 +++++++++-- src/ui/chat_box.rs | 2 +- src/ui/help_box.rs | 1 + src/ui/mod.rs | 2 + src/ui/users.rs | 70 ++++++++++++++++++++++ 9 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 src/ui/users.rs diff --git a/src/backend/nc_request/mod.rs b/src/backend/nc_request/mod.rs index 828b01e..bef033d 100644 --- a/src/backend/nc_request/mod.rs +++ b/src/backend/nc_request/mod.rs @@ -38,6 +38,7 @@ pub struct NCReqDataMessageParameter { name: String, } + impl NCRequest { pub fn new() -> Result> { use std::io::Write; diff --git a/src/backend/nc_request/nc_req_data_user.rs b/src/backend/nc_request/nc_req_data_user.rs index 47c2fdd..93af43b 100644 --- a/src/backend/nc_request/nc_req_data_user.rs +++ b/src/backend/nc_request/nc_req_data_user.rs @@ -5,14 +5,14 @@ pub struct NCReqDataParticipants { attendeeId: i32, actorType: String, actorId: String, - displayName: String, + pub displayName: String, participantType: i32, lastPing: i32, inCall: i32, permissions: i32, attendeePermissions: i32, sessionIds: Vec, - status: Option, + pub status: Option, statusIcon: Option, statusMessage: Option, statusClearAt: Option, @@ -40,6 +40,7 @@ pub struct NCReqDataUser { shareWithDisplayNameUnique: String, } + fn str_or_status<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index a54bd04..5cea1a1 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -167,6 +167,23 @@ impl NCRoom { self.room_data.unreadMessages > 0 } + pub fn is_dm(&self) -> bool { + match self.room_type { + NCRoomTypes::OneToOne | NCRoomTypes::NoteToSelf | NCRoomTypes::ChangeLog => true, + NCRoomTypes::Deprecated | NCRoomTypes::Group | NCRoomTypes::Public => false, + } + } + + pub fn is_group(&self) -> bool { + match self.room_type { + NCRoomTypes::Deprecated + | NCRoomTypes::OneToOne + | NCRoomTypes::NoteToSelf + | NCRoomTypes::ChangeLog => false, + NCRoomTypes::Group | NCRoomTypes::Public => true, + } + } + pub fn get_unread(&self) -> usize { self.room_data.unreadMessages.as_() } @@ -178,6 +195,9 @@ impl NCRoom { pub fn get_last_read(&self) -> i32 { self.room_data.lastReadMessage } + pub fn get_users(&self) -> &Vec { + &self.participants + } pub async fn update_if_id_is_newer( &mut self, diff --git a/src/config/data/ui.rs b/src/config/data/ui.rs index 2c07279..42f8677 100644 --- a/src/config/data/ui.rs +++ b/src/config/data/ui.rs @@ -9,6 +9,9 @@ pub struct Ui { pub default_room: String, pub categories: Vec, pub categories_separator: String, + /// Should the userlist be shown in rooms by default? + #[toml_example(default = true)] + pub user_sidebar_default: bool, #[toml_example(default = true)] pub use_mouse: bool, #[toml_example(default = true)] diff --git a/src/ui/app.rs b/src/ui/app.rs index 2f5e7af..fe575af 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -1,8 +1,9 @@ use crate::{ backend::nc_talk::NCTalk, + config, ui::{ chat_box::ChatBox, chat_selector::ChatSelector, help_box::HelpBox, input_box::InputBox, - title_bar::TitleBar, + title_bar::TitleBar, users::Users, }, }; use ratatui::{prelude::*, widgets::Paragraph}; @@ -26,6 +27,8 @@ pub struct App<'a> { pub selector: ChatSelector<'a>, input: InputBox<'a>, help: HelpBox, + users: Users<'a>, + user_sidebar_visible: bool, } impl<'a> App<'a> { @@ -46,8 +49,14 @@ impl<'a> App<'a> { chat.select_last_message(); chat }, + users: { + let mut users = Users::new(); + users.update(&backend); + users + }, backend, help: HelpBox::default(), + user_sidebar_visible: config::get().data.ui.user_sidebar_default, } } @@ -73,9 +82,22 @@ impl<'a> App<'a> { .direction(Direction::Vertical) .constraints([Constraint::Min(4), Constraint::Length(3)]) .split(base_layout[1]); - self.chat - .set_width_and_update_if_change(main_layout[0].width, &self.backend); - self.chat.render_area(f, main_layout[0]); + + if self.user_sidebar_visible && self.backend.get_current_room().is_group() { + let chat_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(80), Constraint::Percentage(20)]) + .split(main_layout[0]); + self.chat + .set_width_and_update_if_change(chat_layout[0].width, &self.backend); + self.chat.render_area(f, chat_layout[0]); + self.users.render_area(f, chat_layout[1]); + } else { + self.chat + .set_width_and_update_if_change(main_layout[0].width, &self.backend); + self.chat.render_area(f, main_layout[0]); + }; + self.input.render_area(f, main_layout[1]); } self.title.update(self.current_screen, &self.backend); @@ -93,6 +115,7 @@ impl<'a> App<'a> { self.title.update(self.current_screen, &self.backend); self.selector.update(&self.backend)?; self.chat.update_messages(&self.backend); + self.users.update(&self.backend); Ok(()) } @@ -151,6 +174,10 @@ impl<'a> App<'a> { self.chat.select_down(); } + pub fn toggle_user_sidebar(&mut self) { + self.user_sidebar_visible = !self.user_sidebar_visible; + } + pub fn click_at(&mut self, position: Position) -> Result<(), Box> { match self.current_screen { CurrentScreen::Reading => self.chat.select_line(position)?, diff --git a/src/ui/chat_box.rs b/src/ui/chat_box.rs index 15c7151..f238bf9 100644 --- a/src/ui/chat_box.rs +++ b/src/ui/chat_box.rs @@ -28,7 +28,7 @@ impl<'a> ChatBox<'a> { } pub fn set_width_and_update_if_change(&mut self, width: u16, backend: &NCTalk) { - let new_width = (width - TIME_WIDTH - 2 - NAME_WIDTH).max(10) / 5 * 4; + let new_width = (width - TIME_WIDTH - 2 - NAME_WIDTH).max(10); if self.width != new_width { self.width = new_width; self.update_messages(backend); diff --git a/src/ui/help_box.rs b/src/ui/help_box.rs index 43143ab..df37a8e 100644 --- a/src/ui/help_box.rs +++ b/src/ui/help_box.rs @@ -19,6 +19,7 @@ impl Widget for &HelpBox { vec![ Row::new(["q", "quit", "enter the quit screen."]), Row::new(["o", "open", "enter the chat selection screen."]), + Row::new(["u", "users sidebar", "Toggle wether the users are shown in a chat sidebar. Avaliable in reading mode."]), Row::new(["?", "help", "enter this help screen."]), Row::new([ "m", diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 448c2ce..ae18b1b 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -4,6 +4,7 @@ pub mod chat_selector; pub mod help_box; pub mod input_box; pub mod title_bar; +pub mod users; use super::{ backend::nc_talk::NCTalk, @@ -300,6 +301,7 @@ async fn handle_key_in_reading( KeyCode::Char('o') => app.current_screen = CurrentScreen::Opening, KeyCode::Char('q') => app.current_screen = CurrentScreen::Exiting, KeyCode::Char('?') => app.current_screen = CurrentScreen::Helping, + KeyCode::Char('u') => app.toggle_user_sidebar(), _ => (), }; Ok(()) diff --git a/src/ui/users.rs b/src/ui/users.rs new file mode 100644 index 0000000..fc32317 --- /dev/null +++ b/src/ui/users.rs @@ -0,0 +1,70 @@ +use ratatui::{ + prelude::*, + widgets::{Block, Borders, Cell, HighlightSpacing, Row, Table, TableState}, +}; +use style::Styled; + +use crate::backend::nc_talk::NCTalk; + +pub struct Users<'a> { + user_list: Vec>, + state: TableState, +} + +impl<'a> Default for Users<'a> { + fn default() -> Self { + Self::new() + } +} + +impl<'a> Users<'a> { + pub fn new() -> Users<'a> { + Users { + user_list: vec![], + state: TableState::default().with_offset(0).with_selected(0), + } + } + pub fn render_area(&self, frame: &mut Frame, area: Rect) { + frame.render_stateful_widget(self, area, &mut self.state.clone()); + } + pub fn update(&mut self, backend: &NCTalk) { + self.user_list.clear(); + let mut new_users = backend.get_current_room().get_users().clone(); + new_users.sort_by(|user1, user2| user1.displayName.cmp(&user2.displayName)); + for user in new_users { + let mut cell = Cell::new(user.displayName.to_string()); + if let Some(status) = user.status { + cell = match status.as_str() { + "away" => cell.set_style(Style::new().blue()), + "offline" => cell.set_style(Style::new().gray()), + "dnd" => cell.set_style(Style::new().red()), + "online" => cell.set_style(Style::new().green()), + _ => cell.set_style(Style::new()), + } + }; + self.user_list.push(Row::new([cell])); + } + + self.state = TableState::default().with_offset(0).with_selected(0); + } +} + +impl<'a> StatefulWidget for &Users<'a> { + type State = TableState; + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + StatefulWidget::render( + Table::new(self.user_list.clone(), [Constraint::Percentage(100)]) + .column_spacing(1) + .style(Style::new().white().on_black()) + .header(Row::new(vec!["Users"]).style(Style::new().bold().green())) + .block(Block::default()) + .highlight_style(Style::new().bold()) + .highlight_spacing(HighlightSpacing::Never) + .highlight_symbol("") + .block(Block::new().borders(Borders::LEFT)), + area, + buf, + state, + ); + } +} From 865e3aec9413778a6ea5a12abd0d672185a13b14 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Fri, 9 Aug 2024 15:07:04 +0200 Subject: [PATCH 46/89] log unknown statuses to the log --- src/ui/users.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ui/users.rs b/src/ui/users.rs index fc32317..39b37d7 100644 --- a/src/ui/users.rs +++ b/src/ui/users.rs @@ -39,7 +39,10 @@ impl<'a> Users<'a> { "offline" => cell.set_style(Style::new().gray()), "dnd" => cell.set_style(Style::new().red()), "online" => cell.set_style(Style::new().green()), - _ => cell.set_style(Style::new()), + unknown => { + log::debug!("Unknown Status {unknown}"); + cell.set_style(Style::new()) + } } }; self.user_list.push(Row::new([cell])); From 06983054e508a7db9d42d39fd86011a317c0471b Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Wed, 28 Aug 2024 18:20:48 +0200 Subject: [PATCH 47/89] fix typos --- src/ui/help_box.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/help_box.rs b/src/ui/help_box.rs index df37a8e..75808c1 100644 --- a/src/ui/help_box.rs +++ b/src/ui/help_box.rs @@ -19,7 +19,7 @@ impl Widget for &HelpBox { vec![ Row::new(["q", "quit", "enter the quit screen."]), Row::new(["o", "open", "enter the chat selection screen."]), - Row::new(["u", "users sidebar", "Toggle wether the users are shown in a chat sidebar. Avaliable in reading mode."]), + Row::new(["u", "users sidebar", "Toggle whether the users are shown in a chat sidebar. Available in reading mode."]), Row::new(["?", "help", "enter this help screen."]), Row::new([ "m", From 19c831b920b46a02dc0e8a0b206436394d7d4f49 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Thu, 29 Aug 2024 09:18:10 +0200 Subject: [PATCH 48/89] update test config --- test/config.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/config.toml b/test/config.toml index ed5cea4..8b4e63c 100644 --- a/test/config.toml +++ b/test/config.toml @@ -38,6 +38,9 @@ categories = [ "", ] categories_separator = "" +# Should the userlist be shown in rooms by default? +user_sidebar_default = true + use_mouse = true use_paste = true From e761509ab1dd9159f6c2383afbc9ee5ce0a7342b Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Fri, 30 Aug 2024 09:27:57 +0200 Subject: [PATCH 49/89] add test to ui/users --- src/ui/users.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/ui/users.rs b/src/ui/users.rs index 39b37d7..5c2041e 100644 --- a/src/ui/users.rs +++ b/src/ui/users.rs @@ -71,3 +71,39 @@ impl<'a> StatefulWidget for &Users<'a> { ); } } + +#[cfg(test)] +mod tests { + use backend::TestBackend; + + use super::*; + + #[test] + fn render_users() { + let backend = TestBackend::new(10, 10); + let mut terminal = Terminal::new(backend).unwrap(); + let users = Users::new(); + terminal + .draw(|frame| users.render_area(frame, Rect::new(0, 0, 8, 8))) + .unwrap(); + let mut expected = Buffer::with_lines([ + "│Users ", + "│ ", + "│ ", + "│ ", + "│ ", + "│ ", + "│ ", + "│ ", + " ", + " ", + ]); + expected.set_style(Rect::new(0, 0, 8, 8), Style::new().white().on_black()); + + for x in 1..=7 { + expected[(x, 0)].set_style(Style::new().green().on_black().bold()); + } + + terminal.backend().assert_buffer(&expected); + } +} From e65d12363bc6069bca16d79b6aadbf95c05bbe6c Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Fri, 30 Aug 2024 10:34:15 +0200 Subject: [PATCH 50/89] move NCBackend into a trait to allow for a mock --- src/backend/nc_talk.rs | 43 +++++++++++++++++++++++++++-------------- src/ui/app.rs | 2 +- src/ui/chat_box.rs | 2 +- src/ui/chat_selector.rs | 2 +- src/ui/title_bar.rs | 5 ++++- src/ui/users.rs | 2 +- 6 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index b41104c..f09a521 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -10,6 +10,21 @@ use core::panic; use itertools::Itertools; use std::{collections::HashMap, error::Error, path::PathBuf}; +pub trait NCBackend { + fn write_to_log(&mut self) -> Result<(), std::io::Error>; + fn get_current_room(&self) -> &NCRoom; + fn get_room_by_token(&self, token: &String) -> &NCRoom; + fn get_unread_rooms(&self) -> Vec; + fn get_room_by_displayname(&self, name: &String) -> &NCRoom; + fn add_room(&mut self, room_option: Option); + fn get_dm_keys_display_name_mapping(&self) -> Vec<(String, String)>; + fn get_group_keys_display_name_mapping(&self) -> Vec<(String, String)>; + fn get_room_keys(&self) -> Vec<&String>; + async fn send_message(&mut self, message: String) -> Result<(), Box>; + async fn select_room(&mut self, token: String) -> Result<(), Box>; + async fn update_rooms(&mut self, force_update: bool) -> Result<(), Box>; +} + #[derive(Debug)] pub struct NCTalk { rooms: HashMap, @@ -57,7 +72,6 @@ impl NCTalk { NCRoom::new(packaged_child, requester_box, notifier, chat_log_path).await, ) } - pub async fn new(requester: NCRequest) -> Result> { let notify = NCNotify::new(); @@ -168,8 +182,9 @@ impl NCTalk { Ok(talk) } - - pub fn write_to_log(&mut self) -> Result<(), std::io::Error> { +} +impl NCBackend for NCTalk { + fn write_to_log(&mut self) -> Result<(), std::io::Error> { use std::io::Write; let mut data = HashMap::::new(); @@ -212,15 +227,15 @@ impl NCTalk { } } - pub fn get_current_room(&self) -> &NCRoom { + fn get_current_room(&self) -> &NCRoom { &self.rooms[&self.current_room_token] } - pub fn get_room_by_token(&self, token: &String) -> &NCRoom { + fn get_room_by_token(&self, token: &String) -> &NCRoom { &self.rooms[token] } - pub fn get_unread_rooms(&self) -> Vec { + fn get_unread_rooms(&self) -> Vec { self.rooms .values() .filter(|room| room.has_unread() && self.current_room_token != room.to_token()) @@ -229,7 +244,7 @@ impl NCTalk { .collect::>() } - pub fn get_room_by_displayname(&self, name: &String) -> &NCRoom { + fn get_room_by_displayname(&self, name: &String) -> &NCRoom { for room in self.rooms.values() { if room.to_string() == *name { return room; @@ -238,13 +253,13 @@ impl NCTalk { panic!("room doesnt exist {}", name); } - pub fn add_room(&mut self, room_option: Option) { + fn add_room(&mut self, room_option: Option) { if let Some(room) = room_option { self.rooms.insert(room.to_token(), room); } } - pub fn get_dm_keys_display_name_mapping(&self) -> Vec<(String, String)> { + fn get_dm_keys_display_name_mapping(&self) -> Vec<(String, String)> { self.rooms .iter() .filter(|(_, room)| { @@ -263,7 +278,7 @@ impl NCTalk { .collect_vec() } - pub fn get_group_keys_display_name_mapping(&self) -> Vec<(String, String)> { + fn get_group_keys_display_name_mapping(&self) -> Vec<(String, String)> { let mut mapping: Vec<(String, String)> = Vec::new(); for (key, room) in &self.rooms { match room.room_type { @@ -280,11 +295,11 @@ impl NCTalk { mapping } - pub fn get_room_keys(&self) -> Vec<&String> { + fn get_room_keys(&self) -> Vec<&String> { self.rooms.keys().collect::>() } - pub async fn send_message(&mut self, message: String) -> Result<(), Box> { + async fn send_message(&mut self, message: String) -> Result<(), Box> { self.rooms .get(&self.current_room_token) .ok_or("Room not found when it should be there")? @@ -297,7 +312,7 @@ impl NCTalk { .await } - pub async fn select_room(&mut self, token: String) -> Result<(), Box> { + async fn select_room(&mut self, token: String) -> Result<(), Box> { self.current_room_token.clone_from(&token); self.rooms .get_mut(&self.current_room_token) @@ -306,7 +321,7 @@ impl NCTalk { .await } - pub async fn update_rooms(&mut self, force_update: bool) -> Result<(), Box> { + async fn update_rooms(&mut self, force_update: bool) -> Result<(), Box> { let (response, timestamp) = if force_update { self.requester .fetch_rooms_update(self.last_requested) diff --git a/src/ui/app.rs b/src/ui/app.rs index fe575af..823200a 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -1,5 +1,5 @@ use crate::{ - backend::nc_talk::NCTalk, + backend::nc_talk::{NCBackend, NCTalk}, config, ui::{ chat_box::ChatBox, chat_selector::ChatSelector, help_box::HelpBox, input_box::InputBox, diff --git a/src/ui/chat_box.rs b/src/ui/chat_box.rs index f238bf9..a44b7de 100644 --- a/src/ui/chat_box.rs +++ b/src/ui/chat_box.rs @@ -1,4 +1,4 @@ -use crate::backend::nc_talk::NCTalk; +use crate::backend::nc_talk::{NCBackend, NCTalk}; use ratatui::{ prelude::*, widgets::{Block, Cell, HighlightSpacing, Row, Table, TableState}, diff --git a/src/ui/chat_selector.rs b/src/ui/chat_selector.rs index f674e5d..3f5fd92 100644 --- a/src/ui/chat_selector.rs +++ b/src/ui/chat_selector.rs @@ -10,7 +10,7 @@ use ratatui::{ use tui_tree_widget::{Tree, TreeItem, TreeState}; -use crate::backend::nc_talk::NCTalk; +use crate::backend::nc_talk::{NCBackend, NCTalk}; pub struct ChatSelector<'a> { pub state: TreeState, diff --git a/src/ui/title_bar.rs b/src/ui/title_bar.rs index 5e88668..e09ac31 100644 --- a/src/ui/title_bar.rs +++ b/src/ui/title_bar.rs @@ -1,4 +1,7 @@ -use crate::{backend::nc_talk::NCTalk, ui::app::CurrentScreen}; +use crate::{ + backend::nc_talk::{NCBackend, NCTalk}, + ui::app::CurrentScreen, +}; use num_traits::AsPrimitive; use ratatui::{ prelude::*, diff --git a/src/ui/users.rs b/src/ui/users.rs index 5c2041e..b22aeef 100644 --- a/src/ui/users.rs +++ b/src/ui/users.rs @@ -4,7 +4,7 @@ use ratatui::{ }; use style::Styled; -use crate::backend::nc_talk::NCTalk; +use crate::backend::nc_talk::{NCBackend, NCTalk}; pub struct Users<'a> { user_list: Vec>, From c6607095be1cd78a35ba2e905ac949fbb1eaf035 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Fri, 30 Aug 2024 14:45:37 +0200 Subject: [PATCH 51/89] make the backend a trait to be able to mock it --- Cargo.toml | 1 + src/backend/nc_talk.rs | 62 +++++++++++++++++++++++++++++++++++++---- src/main.rs | 2 +- src/ui/app.rs | 30 ++++++++++---------- src/ui/chat_box.rs | 6 ++-- src/ui/chat_selector.rs | 6 ++-- src/ui/mod.rs | 4 +-- src/ui/title_bar.rs | 7 ++--- src/ui/users.rs | 4 +-- 9 files changed, 86 insertions(+), 36 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7594322..6015927 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ strip-ansi-escapes = "0.2.0" tracing = "0.1.40" cfg-if = "1.0.0" tui-textarea = "0.6.1" +async-trait = "0.1.81" [lints.clippy] pedantic = "warn" diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index f09a521..327fcc9 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -6,16 +6,19 @@ use crate::{ }, config::{self}, }; +use async_trait::async_trait; use core::panic; use itertools::Itertools; -use std::{collections::HashMap, error::Error, path::PathBuf}; +use std::{collections::HashMap, error::Error, fmt::Debug, path::PathBuf}; -pub trait NCBackend { +#[async_trait] +pub trait NCBackend: Debug + Send { fn write_to_log(&mut self) -> Result<(), std::io::Error>; fn get_current_room(&self) -> &NCRoom; - fn get_room_by_token(&self, token: &String) -> &NCRoom; + fn get_current_room_token(&self) -> &str; + fn get_room_by_token(&self, token: &str) -> &NCRoom; fn get_unread_rooms(&self) -> Vec; - fn get_room_by_displayname(&self, name: &String) -> &NCRoom; + fn get_room_by_displayname(&self, name: &str) -> &NCRoom; fn add_room(&mut self, room_option: Option); fn get_dm_keys_display_name_mapping(&self) -> Vec<(String, String)>; fn get_group_keys_display_name_mapping(&self) -> Vec<(String, String)>; @@ -183,6 +186,8 @@ impl NCTalk { Ok(talk) } } + +#[async_trait] impl NCBackend for NCTalk { fn write_to_log(&mut self) -> Result<(), std::io::Error> { use std::io::Write; @@ -231,7 +236,11 @@ impl NCBackend for NCTalk { &self.rooms[&self.current_room_token] } - fn get_room_by_token(&self, token: &String) -> &NCRoom { + fn get_current_room_token(&self) -> &str { + self.current_room_token.as_str() + } + + fn get_room_by_token(&self, token: &str) -> &NCRoom { &self.rooms[token] } @@ -244,7 +253,7 @@ impl NCBackend for NCTalk { .collect::>() } - fn get_room_by_displayname(&self, name: &String) -> &NCRoom { + fn get_room_by_displayname(&self, name: &str) -> &NCRoom { for room in self.rooms.values() { if room.to_string() == *name { return room; @@ -359,3 +368,44 @@ impl NCBackend for NCTalk { Ok(()) } } +#[cfg(test)] +mod tests { + + use super::*; + + #[derive(Debug)] + pub struct MockBackend {} + + // #[async_trait] + // impl NCBackend for MockBackend { + // fn write_to_log(&mut self) -> Result<(), std::io::Error> { + // Ok(()) + // } + // fn get_current_room(&self) -> &NCRoom {} + // fn get_room_by_token(&self, token: &String) -> &NCRoom {} + // fn get_current_room_token(&self) -> &str {} + // fn get_unread_rooms(&self) -> Vec { + // vec![] + // } + // fn get_room_by_displayname(&self, name: &String) -> &NCRoom {} + // fn add_room(&mut self, room_option: Option) {} + // fn get_dm_keys_display_name_mapping(&self) -> Vec<(String, String)> { + // vec![] + // } + // fn get_group_keys_display_name_mapping(&self) -> Vec<(String, String)> { + // vec![] + // } + // fn get_room_keys(&self) -> Vec<&String> { + // vec![] + // } + // async fn send_message(&mut self, _message: String) -> Result<(), Box> { + // Ok(()) + // } + // async fn select_room(&mut self, _token: String) -> Result<(), Box> { + // Ok(()) + // } + // async fn update_rooms(&mut self, _force_update: bool) -> Result<(), Box> { + // Ok(()) + // } + // } +} diff --git a/src/main.rs b/src/main.rs index 3ab0f78..7d0a5ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,7 +29,7 @@ async fn main() { let requester = backend::nc_request::NCRequest::new().expect("cannot create NCRequest"); match backend::nc_talk::NCTalk::new(requester).await { - Ok(backend) => ui::run(backend).await.expect("crashed"), + Ok(backend) => ui::run(Box::new(backend)).await.expect("crashed"), Err(why) => { log::error!("Failed to create backend because: {why}"); } diff --git a/src/ui/app.rs b/src/ui/app.rs index 823200a..f8f1a5c 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -1,5 +1,5 @@ use crate::{ - backend::nc_talk::{NCBackend, NCTalk}, + backend::nc_talk::NCBackend, config, ui::{ chat_box::ChatBox, chat_selector::ChatSelector, help_box::HelpBox, input_box::InputBox, @@ -21,7 +21,7 @@ pub enum CurrentScreen { pub struct App<'a> { pub current_screen: CurrentScreen, // the current screen the user is looking at, and will later determine what is rendered. - backend: NCTalk, + backend: Box, title: TitleBar<'a>, chat: ChatBox<'a>, pub selector: ChatSelector<'a>, @@ -32,26 +32,26 @@ pub struct App<'a> { } impl<'a> App<'a> { - pub fn new(backend: NCTalk) -> Self { + pub fn new(backend: Box) -> Self { Self { current_screen: CurrentScreen::Reading, title: TitleBar::new( CurrentScreen::Reading, backend - .get_room_by_token(&backend.current_room_token) + .get_room_by_token(backend.get_current_room_token()) .to_string(), ), - selector: ChatSelector::new(&backend), + selector: ChatSelector::new(backend.as_ref()), input: InputBox::new(""), chat: { let mut chat = ChatBox::new(); - chat.update_messages(&backend); + chat.update_messages(backend.as_ref()); chat.select_last_message(); chat }, users: { let mut users = Users::new(); - users.update(&backend); + users.update(backend.as_ref()); users }, backend, @@ -89,18 +89,19 @@ impl<'a> App<'a> { .constraints([Constraint::Percentage(80), Constraint::Percentage(20)]) .split(main_layout[0]); self.chat - .set_width_and_update_if_change(chat_layout[0].width, &self.backend); + .set_width_and_update_if_change(chat_layout[0].width, self.backend.as_ref()); self.chat.render_area(f, chat_layout[0]); self.users.render_area(f, chat_layout[1]); } else { self.chat - .set_width_and_update_if_change(main_layout[0].width, &self.backend); + .set_width_and_update_if_change(main_layout[0].width, self.backend.as_ref()); self.chat.render_area(f, main_layout[0]); }; self.input.render_area(f, main_layout[1]); } - self.title.update(self.current_screen, &self.backend); + self.title + .update(self.current_screen, self.backend.as_ref()); self.title.render_area(f, base_layout[0]); } @@ -112,10 +113,11 @@ impl<'a> App<'a> { } fn update_ui(&mut self) -> Result<(), Box> { - self.title.update(self.current_screen, &self.backend); - self.selector.update(&self.backend)?; - self.chat.update_messages(&self.backend); - self.users.update(&self.backend); + self.title + .update(self.current_screen, self.backend.as_ref()); + self.selector.update(self.backend.as_ref())?; + self.chat.update_messages(self.backend.as_ref()); + self.users.update(self.backend.as_ref()); Ok(()) } diff --git a/src/ui/chat_box.rs b/src/ui/chat_box.rs index a44b7de..c8b8241 100644 --- a/src/ui/chat_box.rs +++ b/src/ui/chat_box.rs @@ -1,4 +1,4 @@ -use crate::backend::nc_talk::{NCBackend, NCTalk}; +use crate::backend::nc_talk::NCBackend; use ratatui::{ prelude::*, widgets::{Block, Cell, HighlightSpacing, Row, Table, TableState}, @@ -27,7 +27,7 @@ impl<'a> ChatBox<'a> { } } - pub fn set_width_and_update_if_change(&mut self, width: u16, backend: &NCTalk) { + pub fn set_width_and_update_if_change(&mut self, width: u16, backend: &dyn NCBackend) { let new_width = (width - TIME_WIDTH - 2 - NAME_WIDTH).max(10); if self.width != new_width { self.width = new_width; @@ -35,7 +35,7 @@ impl<'a> ChatBox<'a> { } } - pub fn update_messages(&mut self, backend: &NCTalk) { + pub fn update_messages(&mut self, backend: &dyn NCBackend) { use itertools::Itertools; use std::convert::TryInto; diff --git a/src/ui/chat_selector.rs b/src/ui/chat_selector.rs index 3f5fd92..0157833 100644 --- a/src/ui/chat_selector.rs +++ b/src/ui/chat_selector.rs @@ -10,7 +10,7 @@ use ratatui::{ use tui_tree_widget::{Tree, TreeItem, TreeState}; -use crate::backend::nc_talk::{NCBackend, NCTalk}; +use crate::backend::nc_talk::NCBackend; pub struct ChatSelector<'a> { pub state: TreeState, @@ -18,7 +18,7 @@ pub struct ChatSelector<'a> { } impl<'a> ChatSelector<'a> { - pub fn new(backend: &NCTalk) -> Self { + pub fn new(backend: &dyn NCBackend) -> Self { Self { state: TreeState::default(), items: vec![ @@ -65,7 +65,7 @@ impl<'a> ChatSelector<'a> { } } - pub fn update(&mut self, backend: &NCTalk) -> Result<(), Box> { + pub fn update(&mut self, backend: &dyn NCBackend) -> Result<(), Box> { self.items = vec![ TreeItem::new::( "unread".to_string(), diff --git a/src/ui/mod.rs b/src/ui/mod.rs index ae18b1b..f715e88 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -7,7 +7,7 @@ pub mod title_bar; pub mod users; use super::{ - backend::nc_talk::NCTalk, + backend::nc_talk::NCBackend, config, ui::app::{App, CurrentScreen}, }; @@ -138,7 +138,7 @@ enum ProcessEventResult { Exit, } -pub async fn run(nc_backend: NCTalk) -> Result<(), Box> { +pub async fn run(nc_backend: Box) -> Result<(), Box> { install_hooks()?; // create app and run it diff --git a/src/ui/title_bar.rs b/src/ui/title_bar.rs index e09ac31..2d53a79 100644 --- a/src/ui/title_bar.rs +++ b/src/ui/title_bar.rs @@ -1,7 +1,4 @@ -use crate::{ - backend::nc_talk::{NCBackend, NCTalk}, - ui::app::CurrentScreen, -}; +use crate::{backend::nc_talk::NCBackend, ui::app::CurrentScreen}; use num_traits::AsPrimitive; use ratatui::{ prelude::*, @@ -26,7 +23,7 @@ impl<'a> TitleBar<'a> { } } - pub fn update(&mut self, screen: CurrentScreen, backend: &NCTalk) { + pub fn update(&mut self, screen: CurrentScreen, backend: &dyn NCBackend) { self.mode = screen.to_string(); self.room = backend.get_current_room().to_string(); self.unread = backend.get_current_room().get_unread(); diff --git a/src/ui/users.rs b/src/ui/users.rs index b22aeef..66a32e2 100644 --- a/src/ui/users.rs +++ b/src/ui/users.rs @@ -4,7 +4,7 @@ use ratatui::{ }; use style::Styled; -use crate::backend::nc_talk::{NCBackend, NCTalk}; +use crate::backend::nc_talk::NCBackend; pub struct Users<'a> { user_list: Vec>, @@ -27,7 +27,7 @@ impl<'a> Users<'a> { pub fn render_area(&self, frame: &mut Frame, area: Rect) { frame.render_stateful_widget(self, area, &mut self.state.clone()); } - pub fn update(&mut self, backend: &NCTalk) { + pub fn update(&mut self, backend: &dyn NCBackend) { self.user_list.clear(); let mut new_users = backend.get_current_room().get_users().clone(); new_users.sort_by(|user1, user2| user1.displayName.cmp(&user2.displayName)); From ae7bc90c5cc3f4e683ba0f3d3544c65054ee07d5 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Fri, 30 Aug 2024 21:26:34 +0200 Subject: [PATCH 52/89] commit current state --- src/backend/nc_room.rs | 247 ++++++++++++++++++++++++---------------- src/backend/nc_talk.rs | 139 ++++++++++++---------- src/main.rs | 5 +- src/ui/app.rs | 36 +++--- src/ui/chat_box.rs | 15 +-- src/ui/chat_selector.rs | 9 +- src/ui/mod.rs | 36 +++--- src/ui/title_bar.rs | 6 +- src/ui/users.rs | 4 +- 9 files changed, 287 insertions(+), 210 deletions(-) diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index 5cea1a1..e4c743b 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -3,9 +3,11 @@ use super::{ nc_notify::NCNotify, nc_request::{NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, NCRequest}, }; +use async_trait::async_trait; use log; use num_derive::FromPrimitive; use num_traits::{AsPrimitive, FromPrimitive}; +use std::fmt::{Debug, Display}; #[derive(Debug, FromPrimitive, PartialEq)] pub enum NCRoomTypes { @@ -17,6 +19,42 @@ pub enum NCRoomTypes { NoteToSelf, } +#[async_trait] +pub trait NCRoomInterface: Debug + Send + Display + Ord { + async fn new( + room_data: NCReqDataRoom, + requester: NCRequest, + notifier: NCNotify, + path_to_log: std::path::PathBuf, + ) -> Option; + fn get_last_room_level_message_id(&self) -> Option; + fn has_unread(&self) -> bool; + fn is_dm(&self) -> bool; + fn is_group(&self) -> bool; + fn get_messages(&self) -> &Vec; + fn get_unread(&self) -> usize; + fn get_display_name(&self) -> &str; + fn get_last_read(&self) -> i32; + fn get_users(&self) -> &Vec; + fn get_room_type(&self) -> NCRoomTypes; + + fn to_json(&self) -> String; + fn to_data(&self) -> NCReqDataRoom; + fn write_to_log(&mut self) -> Result<(), std::io::Error>; + fn to_token(&self) -> String; + async fn update_if_id_is_newer( + &mut self, + message_id: i32, + data_option: Option<&NCReqDataRoom>, + ) -> Result<(), Box>; + async fn send(&self, message: String) -> Result>; + async fn update( + &mut self, + data_option: Option<&NCReqDataRoom>, + ) -> Result<(), Box>; + async fn mark_as_read(&self) -> Result<(), Box>; +} + #[derive(Debug)] pub struct NCRoom { requester: NCRequest, @@ -40,8 +78,11 @@ impl NCRoom { } Ok(()) } +} - pub async fn new( +#[async_trait] +impl NCRoomInterface for NCRoom { + async fn new( room_data: NCReqDataRoom, requester: NCRequest, notifier: NCNotify, @@ -73,7 +114,6 @@ impl NCRoom { .await .ok(); } - let participants = requester .fetch_participants(&room_data.token) .await @@ -89,8 +129,108 @@ impl NCRoom { room_data, }) } + // the room endpoint doesnt tell you about reactions... + fn get_last_room_level_message_id(&self) -> Option { + self.messages + .iter() + .filter(|&message| !message.is_reaction() && !message.is_edit_note()) + .collect::>() + .last() + .map(|message| message.get_id()) + } + + fn has_unread(&self) -> bool { + self.room_data.unreadMessages > 0 + } + + fn is_dm(&self) -> bool { + match self.room_type { + NCRoomTypes::OneToOne | NCRoomTypes::NoteToSelf | NCRoomTypes::ChangeLog => true, + NCRoomTypes::Deprecated | NCRoomTypes::Group | NCRoomTypes::Public => false, + } + } + + fn is_group(&self) -> bool { + match self.room_type { + NCRoomTypes::Deprecated + | NCRoomTypes::OneToOne + | NCRoomTypes::NoteToSelf + | NCRoomTypes::ChangeLog => false, + NCRoomTypes::Group | NCRoomTypes::Public => true, + } + } + + fn get_room_type(&self) -> NCRoomTypes { + self.room_type + } + + fn get_messages(&self) -> &Vec { + &self.messages + } + + fn get_unread(&self) -> usize { + self.room_data.unreadMessages.as_() + } + + fn get_display_name(&self) -> &str { + &self.room_data.displayName + } + + fn get_last_read(&self) -> i32 { + self.room_data.lastReadMessage + } + fn get_users(&self) -> &Vec { + &self.participants + } + + fn to_json(&self) -> String { + serde_json::to_string(&self.room_data).unwrap() + } + + fn to_data(&self) -> NCReqDataRoom { + self.room_data.clone() + } + + fn write_to_log(&mut self) -> Result<(), std::io::Error> { + use std::io::Write; + + let data: Vec<_> = self.messages.iter().map(NCMessage::data).collect(); + let path = self.path_to_log.as_path(); + // Open a file in write-only mode, returns `io::Result` + let mut file = match std::fs::File::create(path) { + Err(why) => { + log::warn!( + "Couldn't create log file {} for {}: {}", + path.to_str().unwrap(), + self.room_data.displayName, + why + ); + return Err(why); + } + Ok(file) => file, + }; + + match file.write_all(serde_json::to_string(&data).unwrap().as_bytes()) { + Err(why) => { + log::warn!( + "couldn't write log file to {} for {}: {}", + path.as_os_str() + .to_str() + .expect("Could not convert log path to string"), + self.room_data.displayName, + why + ); + Err(why) + } + Ok(()) => Ok(()), + } + } + + fn to_token(&self) -> String { + self.room_data.token.clone() + } - pub async fn send(&self, message: String) -> Result> { + async fn send(&self, message: String) -> Result> { log::debug!("Send Message {}", &message); let response = self .requester @@ -102,7 +242,7 @@ impl NCRoom { } } - pub async fn update( + async fn update( &mut self, data_option: Option<&NCReqDataRoom>, ) -> Result<(), Box> { @@ -140,18 +280,7 @@ impl NCRoom { Ok(()) } - - // the room endpoint doesnt tell you about reactions... - pub fn get_last_room_level_message_id(&self) -> Option { - self.messages - .iter() - .filter(|&message| !message.is_reaction() && !message.is_edit_note()) - .collect::>() - .last() - .map(|message| message.get_id()) - } - - pub async fn mark_as_read(&self) -> Result<(), Box> { + async fn mark_as_read(&self) -> Result<(), Box> { if !self.messages.is_empty() { self.requester .mark_chat_read( @@ -162,44 +291,7 @@ impl NCRoom { } Ok(()) } - - pub fn has_unread(&self) -> bool { - self.room_data.unreadMessages > 0 - } - - pub fn is_dm(&self) -> bool { - match self.room_type { - NCRoomTypes::OneToOne | NCRoomTypes::NoteToSelf | NCRoomTypes::ChangeLog => true, - NCRoomTypes::Deprecated | NCRoomTypes::Group | NCRoomTypes::Public => false, - } - } - - pub fn is_group(&self) -> bool { - match self.room_type { - NCRoomTypes::Deprecated - | NCRoomTypes::OneToOne - | NCRoomTypes::NoteToSelf - | NCRoomTypes::ChangeLog => false, - NCRoomTypes::Group | NCRoomTypes::Public => true, - } - } - - pub fn get_unread(&self) -> usize { - self.room_data.unreadMessages.as_() - } - - pub fn get_display_name(&self) -> &str { - &self.room_data.displayName - } - - pub fn get_last_read(&self) -> i32 { - self.room_data.lastReadMessage - } - pub fn get_users(&self) -> &Vec { - &self.participants - } - - pub async fn update_if_id_is_newer( + async fn update_if_id_is_newer( &mut self, message_id: i32, data_option: Option<&NCReqDataRoom>, @@ -230,53 +322,6 @@ impl NCRoom { } Ok(()) } - - pub fn to_json(&self) -> String { - serde_json::to_string(&self.room_data).unwrap() - } - - pub fn to_data(&self) -> NCReqDataRoom { - self.room_data.clone() - } - - pub fn write_to_log(&mut self) -> Result<(), std::io::Error> { - use std::io::Write; - - let data: Vec<_> = self.messages.iter().map(NCMessage::data).collect(); - let path = self.path_to_log.as_path(); - // Open a file in write-only mode, returns `io::Result` - let mut file = match std::fs::File::create(path) { - Err(why) => { - log::warn!( - "Couldn't create log file {} for {}: {}", - path.to_str().unwrap(), - self.room_data.displayName, - why - ); - return Err(why); - } - Ok(file) => file, - }; - - match file.write_all(serde_json::to_string(&data).unwrap().as_bytes()) { - Err(why) => { - log::warn!( - "couldn't write log file to {} for {}: {}", - path.as_os_str() - .to_str() - .expect("Could not convert log path to string"), - self.room_data.displayName, - why - ); - Err(why) - } - Ok(()) => Ok(()), - } - } - - pub fn to_token(&self) -> String { - self.room_data.token.clone() - } } impl Ord for NCRoom { diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index 327fcc9..8909434 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -2,7 +2,7 @@ use crate::{ backend::{ nc_notify::NCNotify, nc_request::{NCReqDataRoom, NCRequest}, - nc_room::{NCRoom, NCRoomTypes}, + nc_room::NCRoomInterface, }, config::{self}, }; @@ -11,26 +11,29 @@ use core::panic; use itertools::Itertools; use std::{collections::HashMap, error::Error, fmt::Debug, path::PathBuf}; +use super::nc_room::NCRoomTypes; + #[async_trait] pub trait NCBackend: Debug + Send { + type Room: NCRoomInterface; fn write_to_log(&mut self) -> Result<(), std::io::Error>; - fn get_current_room(&self) -> &NCRoom; fn get_current_room_token(&self) -> &str; - fn get_room_by_token(&self, token: &str) -> &NCRoom; + fn get_room(&self, token: &str) -> &Self::Room; + fn get_current_room(&self) -> &Self::Room; fn get_unread_rooms(&self) -> Vec; - fn get_room_by_displayname(&self, name: &str) -> &NCRoom; - fn add_room(&mut self, room_option: Option); + fn get_room_by_displayname(&self, name: &str) -> &str; fn get_dm_keys_display_name_mapping(&self) -> Vec<(String, String)>; fn get_group_keys_display_name_mapping(&self) -> Vec<(String, String)>; fn get_room_keys(&self) -> Vec<&String>; async fn send_message(&mut self, message: String) -> Result<(), Box>; async fn select_room(&mut self, token: String) -> Result<(), Box>; async fn update_rooms(&mut self, force_update: bool) -> Result<(), Box>; + fn add_room(&mut self, room_option: Option); } #[derive(Debug)] -pub struct NCTalk { - rooms: HashMap, +pub struct NCTalk { + rooms: HashMap, chat_data_path: PathBuf, last_requested: i64, requester: NCRequest, @@ -38,14 +41,29 @@ pub struct NCTalk { pub current_room_token: String, } -impl NCTalk { +impl NCTalk { async fn parse_response( response: Vec, requester: NCRequest, notifier: NCNotify, - rooms: &mut HashMap, + rooms: &mut HashMap, chat_log_path: PathBuf, ) { + // for res in response { + // let room_tuple = NCTalk::::new_room( + // res, + // requester.clone(), + // notifier.clone(), + // chat_log_path.clone(), + // ); + // let (name, room_option) = room_tuple.await; + // if let Some(room) = room_option { + // rooms.insert(name, room); + // } else { + // log::info!("Encountered a room that cannot be added {} ", name); + // } + // } + let v = response.into_iter().map(|child| { tokio::spawn(NCTalk::new_room( child, @@ -69,13 +87,15 @@ impl NCTalk { requester_box: NCRequest, notifier: NCNotify, chat_log_path: PathBuf, - ) -> (String, Option) { + ) -> (String, Option) { ( packaged_child.token.clone(), - NCRoom::new(packaged_child, requester_box, notifier, chat_log_path).await, + Room::new(packaged_child, requester_box, notifier, chat_log_path).await, ) } - pub async fn new(requester: NCRequest) -> Result> { + pub async fn new( + requester: NCRequest, + ) -> Result, Box> { let notify = NCNotify::new(); let chat_log_path = config::get().get_server_data_dir(); @@ -90,7 +110,7 @@ impl NCTalk { .map(|room| (room.token.clone(), room)) .collect::>(); - let mut rooms = HashMap::::new(); + let mut rooms = HashMap::::new(); if path.exists() { if let Ok(mut data) = serde_json::from_str::>( @@ -100,7 +120,7 @@ impl NCTalk { for (token, room) in &mut data { handles.insert( token.clone(), - tokio::spawn(NCRoom::new( + tokio::spawn(Room::new( room.clone(), requester.clone(), notify.clone(), @@ -111,7 +131,7 @@ impl NCTalk { for (token, room_future) in &mut handles { //we can safely unwrap here bc the json file on disk shall never be this broken. let mut json_room = room_future.await?.unwrap(); - if initial_message_ids.contains_key(token) { + if initial_message_ids.contains_key::(token) { let message_id = initial_message_ids.get(token).unwrap().lastMessage.id; json_room .update_if_id_is_newer( @@ -132,7 +152,7 @@ impl NCTalk { .filter(|data| initial_message_ids.contains_key(&data.token)) .cloned() .collect::>(); - NCTalk::parse_response( + NCTalk::::parse_response( remaining_room_data, requester.clone(), notify.clone(), @@ -148,7 +168,7 @@ impl NCTalk { log::info!("Loaded Rooms from log files"); } else { log::debug!("Failed to parse top level json, falling back to fetching"); - NCTalk::parse_response( + NCTalk::::parse_response( response, requester.clone(), notify.clone(), @@ -159,7 +179,7 @@ impl NCTalk { } } else { log::debug!("No Log files found in Path, fetching logs from server."); - NCTalk::parse_response( + NCTalk::::parse_response( response, requester.clone(), notify.clone(), @@ -177,9 +197,15 @@ impl NCTalk { requester, notifier: notify, }; - let current_token = talk.get_room_by_displayname(&config::get().data.ui.default_room); - log::info!("Entering default room {}", current_token.to_string()); - talk.select_room(current_token.to_token()).await?; + log::info!( + "Entering default room {}", + &config::get().data.ui.default_room + ); + talk.select_room( + talk.get_room_by_displayname(&config::get().data.ui.default_room) + .to_string(), + ) + .await?; log::debug!("Found {} Rooms", talk.rooms.len()); @@ -188,7 +214,8 @@ impl NCTalk { } #[async_trait] -impl NCBackend for NCTalk { +impl<'a, TalkRoom: NCRoomInterface + 'static> NCBackend for NCTalk { + type Room = TalkRoom; fn write_to_log(&mut self) -> Result<(), std::io::Error> { use std::io::Write; @@ -232,15 +259,15 @@ impl NCBackend for NCTalk { } } - fn get_current_room(&self) -> &NCRoom { - &self.rooms[&self.current_room_token] - } - fn get_current_room_token(&self) -> &str { self.current_room_token.as_str() } - fn get_room_by_token(&self, token: &str) -> &NCRoom { + fn get_current_room(&self) -> &Self::Room { + &self.rooms[&self.current_room_token] + } + + fn get_room(&self, token: &str) -> &Self::Room { &self.rooms[token] } @@ -249,25 +276,19 @@ impl NCBackend for NCTalk { .values() .filter(|room| room.has_unread() && self.current_room_token != room.to_token()) .sorted_by(std::cmp::Ord::cmp) - .map(NCRoom::to_token) + .map(NCRoomInterface::to_token) .collect::>() } - fn get_room_by_displayname(&self, name: &str) -> &NCRoom { + fn get_room_by_displayname(&self, name: &str) -> &str { for room in self.rooms.values() { if room.to_string() == *name { - return room; + return room.to_token().as_str(); } } panic!("room doesnt exist {}", name); } - fn add_room(&mut self, room_option: Option) { - if let Some(room) = room_option { - self.rooms.insert(room.to_token(), room); - } - } - fn get_dm_keys_display_name_mapping(&self) -> Vec<(String, String)> { self.rooms .iter() @@ -277,30 +298,24 @@ impl NCBackend for NCTalk { NCRoomTypes::NoteToSelf, NCRoomTypes::ChangeLog, ] - .contains(&room.room_type) + .contains(&room.get_room_type()) }) .map(|(key, _)| (key.clone(), self.rooms[key].to_string())) - .sorted_by(|(token_a, _), (token_b, _)| { - self.get_room_by_token(token_a) - .cmp(self.get_room_by_token(token_b)) - }) + .sorted_by(|(token_a, _), (token_b, _)| self.rooms[token_a].cmp(&self.rooms[token_b])) .collect_vec() } fn get_group_keys_display_name_mapping(&self) -> Vec<(String, String)> { let mut mapping: Vec<(String, String)> = Vec::new(); for (key, room) in &self.rooms { - match room.room_type { + match room.get_room_type() { NCRoomTypes::Group | NCRoomTypes::Public => { mapping.push((key.clone(), self.rooms[key].to_string())); } _ => {} } } - mapping.sort_by(|(token_a, _), (token_b, _)| { - self.get_room_by_token(token_a) - .cmp(self.get_room_by_token(token_b)) - }); + mapping.sort_by(|(token_a, _), (token_b, _)| self.rooms[token_a].cmp(&self.rooms[token_b])); mapping } @@ -355,7 +370,7 @@ impl NCBackend for NCTalk { } else { self.notifier.new_room(&room.displayName)?; self.add_room( - NCRoom::new( + Self::Room::new( room, self.requester.clone(), self.notifier.clone(), @@ -367,28 +382,35 @@ impl NCBackend for NCTalk { } Ok(()) } + + fn add_room(&mut self, room_option: Option) { + if let Some(room) = room_option { + self.rooms.insert(room.to_token(), room); + } + } } #[cfg(test)] mod tests { - use super::*; + // use super::*; - #[derive(Debug)] - pub struct MockBackend {} + // #[derive(Debug, Default)] + // pub struct MockBackend { + // pub current_token: String, + // pub room: HashMap>, + // } - // #[async_trait] - // impl NCBackend for MockBackend { + // impl<'a, Room> NCBackend<'a, Room> for MockBackend<'a, Room> { + // fn get_room(&self, token: &str) -> &Room {} + // fn get_current_room(&self, token: &str) -> &Room {} // fn write_to_log(&mut self) -> Result<(), std::io::Error> { // Ok(()) // } - // fn get_current_room(&self) -> &NCRoom {} - // fn get_room_by_token(&self, token: &String) -> &NCRoom {} // fn get_current_room_token(&self) -> &str {} // fn get_unread_rooms(&self) -> Vec { // vec![] // } - // fn get_room_by_displayname(&self, name: &String) -> &NCRoom {} - // fn add_room(&mut self, room_option: Option) {} + // fn get_room_by_displayname(&self, name: &str) -> &str {} // fn get_dm_keys_display_name_mapping(&self) -> Vec<(String, String)> { // vec![] // } @@ -396,8 +418,11 @@ mod tests { // vec![] // } // fn get_room_keys(&self) -> Vec<&String> { - // vec![] + // self.rooms.keys().collect::>() // } + // } + // #[async_trait] + // impl NCBackendAsync for MockBackend { // async fn send_message(&mut self, _message: String) -> Result<(), Box> { // Ok(()) // } diff --git a/src/main.rs b/src/main.rs index 7d0a5ac..09708ab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod backend; mod config; mod ui; +use backend::nc_room::NCRoom; use clap::Parser; #[derive(Parser, Debug)] @@ -28,8 +29,8 @@ async fn main() { let requester = backend::nc_request::NCRequest::new().expect("cannot create NCRequest"); - match backend::nc_talk::NCTalk::new(requester).await { - Ok(backend) => ui::run(Box::new(backend)).await.expect("crashed"), + match backend::nc_talk::NCTalk::::new(requester).await { + Ok(backend) => ui::run(backend).await.expect("crashed"), Err(why) => { log::error!("Failed to create backend because: {why}"); } diff --git a/src/ui/app.rs b/src/ui/app.rs index f8f1a5c..96b1224 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -1,5 +1,5 @@ use crate::{ - backend::nc_talk::NCBackend, + backend::{nc_room::NCRoomInterface, nc_talk::NCBackend}, config, ui::{ chat_box::ChatBox, chat_selector::ChatSelector, help_box::HelpBox, input_box::InputBox, @@ -19,9 +19,9 @@ pub enum CurrentScreen { Helping, } -pub struct App<'a> { +pub struct App<'a, Backend: NCBackend> { pub current_screen: CurrentScreen, // the current screen the user is looking at, and will later determine what is rendered. - backend: Box, + backend: Backend, title: TitleBar<'a>, chat: ChatBox<'a>, pub selector: ChatSelector<'a>, @@ -31,27 +31,25 @@ pub struct App<'a> { user_sidebar_visible: bool, } -impl<'a> App<'a> { - pub fn new(backend: Box) -> Self { +impl<'a, Backend: NCBackend> App<'a, Backend> { + pub fn new(backend: Backend) -> Self { Self { current_screen: CurrentScreen::Reading, title: TitleBar::new( CurrentScreen::Reading, - backend - .get_room_by_token(backend.get_current_room_token()) - .to_string(), + backend.get_current_room().to_string(), ), - selector: ChatSelector::new(backend.as_ref()), + selector: ChatSelector::new(&backend), input: InputBox::new(""), chat: { let mut chat = ChatBox::new(); - chat.update_messages(backend.as_ref()); + chat.update_messages(&backend); chat.select_last_message(); chat }, users: { let mut users = Users::new(); - users.update(backend.as_ref()); + users.update(&backend); users }, backend, @@ -89,19 +87,18 @@ impl<'a> App<'a> { .constraints([Constraint::Percentage(80), Constraint::Percentage(20)]) .split(main_layout[0]); self.chat - .set_width_and_update_if_change(chat_layout[0].width, self.backend.as_ref()); + .set_width_and_update_if_change(chat_layout[0].width, &self.backend); self.chat.render_area(f, chat_layout[0]); self.users.render_area(f, chat_layout[1]); } else { self.chat - .set_width_and_update_if_change(main_layout[0].width, self.backend.as_ref()); + .set_width_and_update_if_change(main_layout[0].width, &self.backend); self.chat.render_area(f, main_layout[0]); }; self.input.render_area(f, main_layout[1]); } - self.title - .update(self.current_screen, self.backend.as_ref()); + self.title.update(self.current_screen, &self.backend); self.title.render_area(f, base_layout[0]); } @@ -113,11 +110,10 @@ impl<'a> App<'a> { } fn update_ui(&mut self) -> Result<(), Box> { - self.title - .update(self.current_screen, self.backend.as_ref()); - self.selector.update(self.backend.as_ref())?; - self.chat.update_messages(self.backend.as_ref()); - self.users.update(self.backend.as_ref()); + self.title.update(self.current_screen, &self.backend); + self.selector.update(&self.backend)?; + self.chat.update_messages(&self.backend); + self.users.update(&self.backend); Ok(()) } diff --git a/src/ui/chat_box.rs b/src/ui/chat_box.rs index c8b8241..377d9c3 100644 --- a/src/ui/chat_box.rs +++ b/src/ui/chat_box.rs @@ -1,4 +1,4 @@ -use crate::backend::nc_talk::NCBackend; +use crate::backend::{nc_room::NCRoomInterface, nc_talk::NCBackend}; use ratatui::{ prelude::*, widgets::{Block, Cell, HighlightSpacing, Row, Table, TableState}, @@ -27,7 +27,7 @@ impl<'a> ChatBox<'a> { } } - pub fn set_width_and_update_if_change(&mut self, width: u16, backend: &dyn NCBackend) { + pub fn set_width_and_update_if_change(&mut self, width: u16, backend: &impl NCBackend) { let new_width = (width - TIME_WIDTH - 2 - NAME_WIDTH).max(10); if self.width != new_width { self.width = new_width; @@ -35,15 +35,16 @@ impl<'a> ChatBox<'a> { } } - pub fn update_messages(&mut self, backend: &dyn NCBackend) { + pub fn update_messages(&mut self, backend: &impl NCBackend) { use itertools::Itertools; use std::convert::TryInto; self.messages.clear(); - for message_data in - backend.get_current_room().messages.iter().filter(|mes| { - !mes.is_reaction() && !mes.is_edit_note() && !mes.is_comment_deleted() - }) + for message_data in backend + .get_current_room() + .get_messages() + .iter() + .filter(|mes| !mes.is_reaction() && !mes.is_edit_note() && !mes.is_comment_deleted()) { let name = textwrap::wrap( message_data.get_name(), diff --git a/src/ui/chat_selector.rs b/src/ui/chat_selector.rs index 0157833..8c58a8f 100644 --- a/src/ui/chat_selector.rs +++ b/src/ui/chat_selector.rs @@ -10,6 +10,7 @@ use ratatui::{ use tui_tree_widget::{Tree, TreeItem, TreeState}; +use crate::backend::nc_room::NCRoomInterface; use crate::backend::nc_talk::NCBackend; pub struct ChatSelector<'a> { @@ -18,7 +19,7 @@ pub struct ChatSelector<'a> { } impl<'a> ChatSelector<'a> { - pub fn new(backend: &dyn NCBackend) -> Self { + pub fn new(backend: &impl NCBackend) -> Self { Self { state: TreeState::default(), items: vec![ @@ -31,7 +32,7 @@ impl<'a> ChatSelector<'a> { .map(|token| { TreeItem::new_leaf::( token.clone(), - backend.get_room_by_token(token).get_display_name().into(), + backend.get_room(token).get_display_name().into(), ) }) .collect_vec(), @@ -65,7 +66,7 @@ impl<'a> ChatSelector<'a> { } } - pub fn update(&mut self, backend: &dyn NCBackend) -> Result<(), Box> { + pub fn update(&mut self, backend: &impl NCBackend) -> Result<(), Box> { self.items = vec![ TreeItem::new::( "unread".to_string(), @@ -76,7 +77,7 @@ impl<'a> ChatSelector<'a> { .map(|token| { TreeItem::new_leaf::( token.clone(), - backend.get_room_by_token(token).get_display_name().into(), + backend.get_room(token).get_display_name().into(), ) }) .collect_vec(), diff --git a/src/ui/mod.rs b/src/ui/mod.rs index f715e88..0c2d1f1 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -6,8 +6,9 @@ pub mod input_box; pub mod title_bar; pub mod users; +use crate::backend::{nc_room::NCRoomInterface, nc_talk::NCTalk}; + use super::{ - backend::nc_talk::NCBackend, config, ui::app::{App, CurrentScreen}, }; @@ -138,7 +139,9 @@ enum ProcessEventResult { Exit, } -pub async fn run(nc_backend: Box) -> Result<(), Box> { +pub async fn run( + nc_backend: NCTalk, +) -> Result<(), Box> { install_hooks()?; // create app and run it @@ -152,9 +155,9 @@ pub async fn run(nc_backend: Box) -> Result<(), Box( +async fn run_app<'a, B: Backend>( mut terminal: Terminal, - mut app: App<'_>, + mut app: App<'a, NCTalk>, ) -> Result<(), Box> { app.select_room().await?; log::debug!("Entering Main Loop"); @@ -176,8 +179,8 @@ async fn run_app( } } -async fn process_event( - app: &mut App<'_>, +async fn process_event<'a>( + app: &mut App<'a, NCTalk>, event: Event, ) -> Result> { // It's guaranteed that `read` won't block, because `poll` returned @@ -212,9 +215,9 @@ async fn process_event( Ok(ProcessEventResult::Continue) } -async fn handle_key_in_opening( +async fn handle_key_in_opening<'a>( key: KeyEvent, - app: &mut App<'_>, + app: &mut App<'a, NCTalk>, ) -> Result<(), Box> { match key.code { KeyCode::Esc => app.current_screen = CurrentScreen::Reading, @@ -235,9 +238,9 @@ async fn handle_key_in_opening( Ok(()) } -async fn handle_key_in_editing( +async fn handle_key_in_editing<'a>( key: Input, - app: &mut App<'_>, + app: &mut App<'a, NCTalk>, ) -> Result<(), Box> { match key { Input { key: Key::Esc, .. } => app.current_screen = CurrentScreen::Reading, @@ -256,7 +259,10 @@ async fn handle_key_in_editing( Ok(()) } -fn handle_key_in_help(key: KeyEvent, app: &mut App) { +fn handle_key_in_help<'a>( + key: KeyEvent, + app: &mut App<'a, NCTalk>, +) { match key.code { KeyCode::Char('q') => app.current_screen = CurrentScreen::Exiting, KeyCode::Esc => app.current_screen = CurrentScreen::Reading, @@ -265,9 +271,9 @@ fn handle_key_in_help(key: KeyEvent, app: &mut App) { } } -fn handle_key_in_exit( +fn handle_key_in_exit<'a>( key: KeyEvent, - app: &mut App, + app: &mut App<'a, NCTalk>, ) -> Option>> { match key.code { KeyCode::Char('?') => app.current_screen = CurrentScreen::Helping, @@ -286,9 +292,9 @@ fn handle_key_in_exit( None } -async fn handle_key_in_reading( +async fn handle_key_in_reading<'a>( key: KeyEvent, - app: &mut App<'_>, + app: &mut App<'a, NCTalk>, ) -> Result<(), Box> { match key.code { KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { diff --git a/src/ui/title_bar.rs b/src/ui/title_bar.rs index 2d53a79..4c41e18 100644 --- a/src/ui/title_bar.rs +++ b/src/ui/title_bar.rs @@ -1,4 +1,6 @@ +use crate::backend::nc_room::NCRoomInterface; use crate::{backend::nc_talk::NCBackend, ui::app::CurrentScreen}; + use num_traits::AsPrimitive; use ratatui::{ prelude::*, @@ -23,7 +25,7 @@ impl<'a> TitleBar<'a> { } } - pub fn update(&mut self, screen: CurrentScreen, backend: &dyn NCBackend) { + pub fn update(&mut self, screen: CurrentScreen, backend: &impl NCBackend) { self.mode = screen.to_string(); self.room = backend.get_current_room().to_string(); self.unread = backend.get_current_room().get_unread(); @@ -31,7 +33,7 @@ impl<'a> TitleBar<'a> { .get_unread_rooms() .iter() .map(|token| { - let room = backend.get_room_by_token(token); + let room = backend.get_room(token); format!("{room}: {}", room.get_unread()) }) .collect(); diff --git a/src/ui/users.rs b/src/ui/users.rs index 66a32e2..d5e5637 100644 --- a/src/ui/users.rs +++ b/src/ui/users.rs @@ -4,7 +4,7 @@ use ratatui::{ }; use style::Styled; -use crate::backend::nc_talk::NCBackend; +use crate::backend::{nc_room::NCRoomInterface, nc_talk::NCBackend}; pub struct Users<'a> { user_list: Vec>, @@ -27,7 +27,7 @@ impl<'a> Users<'a> { pub fn render_area(&self, frame: &mut Frame, area: Rect) { frame.render_stateful_widget(self, area, &mut self.state.clone()); } - pub fn update(&mut self, backend: &dyn NCBackend) { + pub fn update(&mut self, backend: &impl NCBackend) { self.user_list.clear(); let mut new_users = backend.get_current_room().get_users().clone(); new_users.sort_by(|user1, user2| user1.displayName.cmp(&user2.displayName)); From 5d487124019d1605f39658c682216099943fbfd4 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz Date: Fri, 30 Aug 2024 22:06:08 +0200 Subject: [PATCH 53/89] fix NCTalk --- src/backend/nc_room.rs | 45 +++++++++++++++++++----------------------- src/backend/nc_talk.rs | 34 +++++++++++++++---------------- src/main.rs | 3 +-- src/ui/mod.rs | 18 ++++++++--------- 4 files changed, 47 insertions(+), 53 deletions(-) diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index e4c743b..cc11e36 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -21,12 +21,6 @@ pub enum NCRoomTypes { #[async_trait] pub trait NCRoomInterface: Debug + Send + Display + Ord { - async fn new( - room_data: NCReqDataRoom, - requester: NCRequest, - notifier: NCNotify, - path_to_log: std::path::PathBuf, - ) -> Option; fn get_last_room_level_message_id(&self) -> Option; fn has_unread(&self) -> bool; fn is_dm(&self) -> bool; @@ -36,7 +30,7 @@ pub trait NCRoomInterface: Debug + Send + Display + Ord { fn get_display_name(&self) -> &str; fn get_last_read(&self) -> i32; fn get_users(&self) -> &Vec; - fn get_room_type(&self) -> NCRoomTypes; + fn get_room_type(&self) -> &NCRoomTypes; fn to_json(&self) -> String; fn to_data(&self) -> NCReqDataRoom; @@ -67,22 +61,7 @@ pub struct NCRoom { } impl NCRoom { - async fn fetch_messages( - requester: &NCRequest, - token: &str, - messages: &mut Vec, - ) -> Result<(), Box> { - let response = requester.fetch_chat_initial(token, 200).await?; - for message in response { - messages.push(message.into()); - } - Ok(()) - } -} - -#[async_trait] -impl NCRoomInterface for NCRoom { - async fn new( + pub async fn new( room_data: NCReqDataRoom, requester: NCRequest, notifier: NCNotify, @@ -129,6 +108,22 @@ impl NCRoomInterface for NCRoom { room_data, }) } + async fn fetch_messages( + requester: &NCRequest, + token: &str, + messages: &mut Vec, + ) -> Result<(), Box> { + let response = requester.fetch_chat_initial(token, 200).await?; + for message in response { + messages.push(message.into()); + } + Ok(()) + } +} + +#[async_trait] +impl NCRoomInterface for NCRoom { + // the room endpoint doesnt tell you about reactions... fn get_last_room_level_message_id(&self) -> Option { self.messages @@ -160,8 +155,8 @@ impl NCRoomInterface for NCRoom { } } - fn get_room_type(&self) -> NCRoomTypes { - self.room_type + fn get_room_type(&self) -> &NCRoomTypes { + &self.room_type } fn get_messages(&self) -> &Vec { diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index 8909434..965c38c 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -11,7 +11,7 @@ use core::panic; use itertools::Itertools; use std::{collections::HashMap, error::Error, fmt::Debug, path::PathBuf}; -use super::nc_room::NCRoomTypes; +use super::nc_room::{NCRoom, NCRoomTypes}; #[async_trait] pub trait NCBackend: Debug + Send { @@ -32,8 +32,8 @@ pub trait NCBackend: Debug + Send { } #[derive(Debug)] -pub struct NCTalk { - rooms: HashMap, +pub struct NCTalk { + rooms: HashMap, chat_data_path: PathBuf, last_requested: i64, requester: NCRequest, @@ -41,12 +41,12 @@ pub struct NCTalk { pub current_room_token: String, } -impl NCTalk { +impl NCTalk { async fn parse_response( response: Vec, requester: NCRequest, notifier: NCNotify, - rooms: &mut HashMap, + rooms: &mut HashMap, chat_log_path: PathBuf, ) { // for res in response { @@ -87,15 +87,15 @@ impl NCTalk { requester_box: NCRequest, notifier: NCNotify, chat_log_path: PathBuf, - ) -> (String, Option) { + ) -> (String, Option) { ( packaged_child.token.clone(), - Room::new(packaged_child, requester_box, notifier, chat_log_path).await, + NCRoom::new(packaged_child, requester_box, notifier, chat_log_path).await, ) } pub async fn new( requester: NCRequest, - ) -> Result, Box> { + ) -> Result> { let notify = NCNotify::new(); let chat_log_path = config::get().get_server_data_dir(); @@ -110,7 +110,7 @@ impl NCTalk { .map(|room| (room.token.clone(), room)) .collect::>(); - let mut rooms = HashMap::::new(); + let mut rooms = HashMap::::new(); if path.exists() { if let Ok(mut data) = serde_json::from_str::>( @@ -120,7 +120,7 @@ impl NCTalk { for (token, room) in &mut data { handles.insert( token.clone(), - tokio::spawn(Room::new( + tokio::spawn(NCRoom::new( room.clone(), requester.clone(), notify.clone(), @@ -152,7 +152,7 @@ impl NCTalk { .filter(|data| initial_message_ids.contains_key(&data.token)) .cloned() .collect::>(); - NCTalk::::parse_response( + NCTalk::parse_response( remaining_room_data, requester.clone(), notify.clone(), @@ -168,7 +168,7 @@ impl NCTalk { log::info!("Loaded Rooms from log files"); } else { log::debug!("Failed to parse top level json, falling back to fetching"); - NCTalk::::parse_response( + NCTalk::parse_response( response, requester.clone(), notify.clone(), @@ -179,7 +179,7 @@ impl NCTalk { } } else { log::debug!("No Log files found in Path, fetching logs from server."); - NCTalk::::parse_response( + NCTalk::parse_response( response, requester.clone(), notify.clone(), @@ -214,8 +214,8 @@ impl NCTalk { } #[async_trait] -impl<'a, TalkRoom: NCRoomInterface + 'static> NCBackend for NCTalk { - type Room = TalkRoom; +impl NCBackend for NCTalk { + type Room = NCRoom; fn write_to_log(&mut self) -> Result<(), std::io::Error> { use std::io::Write; @@ -283,7 +283,7 @@ impl<'a, TalkRoom: NCRoomInterface + 'static> NCBackend for NCTalk { fn get_room_by_displayname(&self, name: &str) -> &str { for room in self.rooms.values() { if room.to_string() == *name { - return room.to_token().as_str(); + return self.rooms[&room.to_token()].as_str(); } } panic!("room doesnt exist {}", name); @@ -370,7 +370,7 @@ impl<'a, TalkRoom: NCRoomInterface + 'static> NCBackend for NCTalk { } else { self.notifier.new_room(&room.displayName)?; self.add_room( - Self::Room::new( + NCRoom::new( room, self.requester.clone(), self.notifier.clone(), diff --git a/src/main.rs b/src/main.rs index 09708ab..3ab0f78 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,6 @@ mod backend; mod config; mod ui; -use backend::nc_room::NCRoom; use clap::Parser; #[derive(Parser, Debug)] @@ -29,7 +28,7 @@ async fn main() { let requester = backend::nc_request::NCRequest::new().expect("cannot create NCRequest"); - match backend::nc_talk::NCTalk::::new(requester).await { + match backend::nc_talk::NCTalk::new(requester).await { Ok(backend) => ui::run(backend).await.expect("crashed"), Err(why) => { log::error!("Failed to create backend because: {why}"); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 0c2d1f1..96a90e4 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -6,7 +6,7 @@ pub mod input_box; pub mod title_bar; pub mod users; -use crate::backend::{nc_room::NCRoomInterface, nc_talk::NCTalk}; +use crate::backend::nc_talk::NCTalk; use super::{ config, @@ -140,7 +140,7 @@ enum ProcessEventResult { } pub async fn run( - nc_backend: NCTalk, + nc_backend: NCTalk, ) -> Result<(), Box> { install_hooks()?; @@ -157,7 +157,7 @@ pub async fn run( async fn run_app<'a, B: Backend>( mut terminal: Terminal, - mut app: App<'a, NCTalk>, + mut app: App<'a, NCTalk>, ) -> Result<(), Box> { app.select_room().await?; log::debug!("Entering Main Loop"); @@ -180,7 +180,7 @@ async fn run_app<'a, B: Backend>( } async fn process_event<'a>( - app: &mut App<'a, NCTalk>, + app: &mut App<'a, NCTalk>, event: Event, ) -> Result> { // It's guaranteed that `read` won't block, because `poll` returned @@ -217,7 +217,7 @@ async fn process_event<'a>( async fn handle_key_in_opening<'a>( key: KeyEvent, - app: &mut App<'a, NCTalk>, + app: &mut App<'a, NCTalk>, ) -> Result<(), Box> { match key.code { KeyCode::Esc => app.current_screen = CurrentScreen::Reading, @@ -240,7 +240,7 @@ async fn handle_key_in_opening<'a>( async fn handle_key_in_editing<'a>( key: Input, - app: &mut App<'a, NCTalk>, + app: &mut App<'a, NCTalk>, ) -> Result<(), Box> { match key { Input { key: Key::Esc, .. } => app.current_screen = CurrentScreen::Reading, @@ -261,7 +261,7 @@ async fn handle_key_in_editing<'a>( fn handle_key_in_help<'a>( key: KeyEvent, - app: &mut App<'a, NCTalk>, + app: &mut App<'a, NCTalk>, ) { match key.code { KeyCode::Char('q') => app.current_screen = CurrentScreen::Exiting, @@ -273,7 +273,7 @@ fn handle_key_in_help<'a>( fn handle_key_in_exit<'a>( key: KeyEvent, - app: &mut App<'a, NCTalk>, + app: &mut App<'a, NCTalk>, ) -> Option>> { match key.code { KeyCode::Char('?') => app.current_screen = CurrentScreen::Helping, @@ -294,7 +294,7 @@ fn handle_key_in_exit<'a>( async fn handle_key_in_reading<'a>( key: KeyEvent, - app: &mut App<'a, NCTalk>, + app: &mut App<'a, NCTalk>, ) -> Result<(), Box> { match key.code { KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { From 289a79981784126b21b273ec82f9c37ab62cf329 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz Date: Sat, 31 Aug 2024 12:49:10 +0200 Subject: [PATCH 54/89] mock NCTalk and add test to users with Mock --- Cargo.toml | 4 +++ src/backend/nc_message.rs | 2 +- src/backend/nc_request/mod.rs | 2 +- src/backend/nc_room.rs | 58 ++++++++++++++++++++++++++++++----- src/backend/nc_talk.rs | 53 ++++++++++++++++++++++++++------ src/ui/mod.rs | 9 ++---- src/ui/users.rs | 23 ++++++++++++-- 7 files changed, 123 insertions(+), 28 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6015927..3c7005b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,10 @@ exclude = [ "tags", ] +[dev-dependencies] +mockall = { version = "0.13.0" } +mockall_derive = { version = "0.13.0"} + [dependencies] reqwest = { version = "0.12", features = ["json"] } tokio = { version = "1", features = ["full"] } diff --git a/src/backend/nc_message.rs b/src/backend/nc_message.rs index 7eaedc1..ecb9774 100644 --- a/src/backend/nc_message.rs +++ b/src/backend/nc_message.rs @@ -2,7 +2,7 @@ use super::nc_request::NCReqDataMessage; use chrono::prelude::*; /// `NextCloud` message interface -#[derive(Debug)] +#[derive(Debug, Default)] pub struct NCMessage(NCReqDataMessage); impl From for NCMessage { diff --git a/src/backend/nc_request/mod.rs b/src/backend/nc_request/mod.rs index bef033d..43d14bc 100644 --- a/src/backend/nc_request/mod.rs +++ b/src/backend/nc_request/mod.rs @@ -22,7 +22,7 @@ use reqwest::{ use serde::{Deserialize, Serialize}; use std::{collections::HashMap, error::Error}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct NCRequest { base_url: String, client: Client, diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index cc11e36..0f04f12 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -19,8 +19,17 @@ pub enum NCRoomTypes { NoteToSelf, } +impl Default for NCRoomTypes { + fn default() -> Self { + NCRoomTypes::OneToOne + } +} +#[cfg(test)] +use mockall::{automock, predicate::*}; + +#[cfg_attr(test, automock)] #[async_trait] -pub trait NCRoomInterface: Debug + Send + Display + Ord { +pub trait NCRoomInterface: Debug + Send + Display + Ord + Default { fn get_last_room_level_message_id(&self) -> Option; fn has_unread(&self) -> bool; fn is_dm(&self) -> bool; @@ -39,17 +48,17 @@ pub trait NCRoomInterface: Debug + Send + Display + Ord { async fn update_if_id_is_newer( &mut self, message_id: i32, - data_option: Option<&NCReqDataRoom>, + data_option: Option, ) -> Result<(), Box>; async fn send(&self, message: String) -> Result>; async fn update( &mut self, - data_option: Option<&NCReqDataRoom>, + data_option: Option, ) -> Result<(), Box>; async fn mark_as_read(&self) -> Result<(), Box>; } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct NCRoom { requester: NCRequest, notifier: NCNotify, @@ -123,7 +132,6 @@ impl NCRoom { #[async_trait] impl NCRoomInterface for NCRoom { - // the room endpoint doesnt tell you about reactions... fn get_last_room_level_message_id(&self) -> Option { self.messages @@ -239,7 +247,7 @@ impl NCRoomInterface for NCRoom { async fn update( &mut self, - data_option: Option<&NCReqDataRoom>, + data_option: Option, ) -> Result<(), Box> { if let Some(data) = data_option { self.room_data = data.clone(); @@ -289,7 +297,7 @@ impl NCRoomInterface for NCRoom { async fn update_if_id_is_newer( &mut self, message_id: i32, - data_option: Option<&NCReqDataRoom>, + data_option: Option, ) -> Result<(), Box> { use std::cmp::Ordering; @@ -352,3 +360,39 @@ impl std::ops::Deref for NCRoom { &self.room_data.displayName } } + +#[cfg(test)] +mod tests { + use super::*; + static BUTZ: &str = "Butz"; + impl std::ops::Deref for MockNCRoomInterface { + type Target = str; + fn deref(&self) -> &Self::Target { + BUTZ + } + } + impl Ord for MockNCRoomInterface { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.to_string().cmp(&other.to_string()) + } + } + + impl PartialOrd for MockNCRoomInterface { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + impl PartialEq for MockNCRoomInterface { + fn eq(&self, other: &Self) -> bool { + self == other + } + } + + impl Eq for MockNCRoomInterface {} + impl std::fmt::Display for MockNCRoomInterface { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self) + } + } +} diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index 965c38c..bb5da41 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -7,14 +7,13 @@ use crate::{ config::{self}, }; use async_trait::async_trait; -use core::panic; use itertools::Itertools; use std::{collections::HashMap, error::Error, fmt::Debug, path::PathBuf}; use super::nc_room::{NCRoom, NCRoomTypes}; #[async_trait] -pub trait NCBackend: Debug + Send { +pub trait NCBackend: Debug + Send + Default { type Room: NCRoomInterface; fn write_to_log(&mut self) -> Result<(), std::io::Error>; fn get_current_room_token(&self) -> &str; @@ -24,14 +23,14 @@ pub trait NCBackend: Debug + Send { fn get_room_by_displayname(&self, name: &str) -> &str; fn get_dm_keys_display_name_mapping(&self) -> Vec<(String, String)>; fn get_group_keys_display_name_mapping(&self) -> Vec<(String, String)>; - fn get_room_keys(&self) -> Vec<&String>; + fn get_room_keys(&self) -> Vec<&'_ String>; async fn send_message(&mut self, message: String) -> Result<(), Box>; async fn select_room(&mut self, token: String) -> Result<(), Box>; async fn update_rooms(&mut self, force_update: bool) -> Result<(), Box>; fn add_room(&mut self, room_option: Option); } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct NCTalk { rooms: HashMap, chat_data_path: PathBuf, @@ -93,9 +92,7 @@ impl NCTalk { NCRoom::new(packaged_child, requester_box, notifier, chat_log_path).await, ) } - pub async fn new( - requester: NCRequest, - ) -> Result> { + pub async fn new(requester: NCRequest) -> Result> { let notify = NCNotify::new(); let chat_log_path = config::get().get_server_data_dir(); @@ -136,7 +133,7 @@ impl NCTalk { json_room .update_if_id_is_newer( message_id, - Some(initial_message_ids.get(token).unwrap()), + Some((*initial_message_ids.get(token).unwrap()).clone()), ) .await?; rooms.insert(token.clone(), json_room); @@ -361,10 +358,10 @@ impl NCBackend for NCTalk { .get_mut(room.token.as_str()) .ok_or("Failed to get Room ref.")?; if force_update { - room_ref.update(Some(&room)).await?; + room_ref.update(Some(room)).await?; } else { room_ref - .update_if_id_is_newer(room.lastMessage.id, Some(&room)) + .update_if_id_is_newer(room.lastMessage.id, Some(room)) .await?; } } else { @@ -389,8 +386,44 @@ impl NCBackend for NCTalk { } } } + +#[cfg(test)] +use crate::backend::nc_room::MockNCRoomInterface; +#[cfg(test)] +use mockall::{mock, predicate::*}; + +#[cfg(test)] +mock! { + #[derive(Debug)] + pub NCTalk{} + #[async_trait] + impl NCBackend for NCTalk{ + type Room = MockNCRoomInterface; + fn write_to_log(&mut self) -> Result<(), std::io::Error>; + fn get_current_room_token(&self) -> &str; + fn get_room(&self, token: &str) -> &::Room; + fn get_current_room(&self) -> &::Room; + fn get_unread_rooms(&self) -> Vec; + fn get_room_by_displayname(&self, name: &str) -> &str; + fn get_dm_keys_display_name_mapping(&self) -> Vec<(String, String)>; + fn get_group_keys_display_name_mapping(&self) -> Vec<(String, String)>; + fn get_room_keys<'a>(&'a self) -> Vec<&'a String>; + async fn send_message(& mut self, message: String) -> Result<(), Box>; + async fn select_room(&mut self, token: String) -> Result<(), Box>; + async fn update_rooms(& mut self, force_update: bool) -> Result<(), Box>; + fn add_room(&mut self, room_option: Option<::Room>); + } +} +#[cfg(test)] +impl std::fmt::Display for MockNCTalk { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self) + } +} + #[cfg(test)] mod tests { + // use crate::backend::nc_room::MockNCRoomInterface; // use super::*; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 96a90e4..0606c49 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -139,9 +139,7 @@ enum ProcessEventResult { Exit, } -pub async fn run( - nc_backend: NCTalk, -) -> Result<(), Box> { +pub async fn run(nc_backend: NCTalk) -> Result<(), Box> { install_hooks()?; // create app and run it @@ -259,10 +257,7 @@ async fn handle_key_in_editing<'a>( Ok(()) } -fn handle_key_in_help<'a>( - key: KeyEvent, - app: &mut App<'a, NCTalk>, -) { +fn handle_key_in_help<'a>(key: KeyEvent, app: &mut App<'a, NCTalk>) { match key.code { KeyCode::Char('q') => app.current_screen = CurrentScreen::Exiting, KeyCode::Esc => app.current_screen = CurrentScreen::Reading, diff --git a/src/ui/users.rs b/src/ui/users.rs index d5e5637..8efd007 100644 --- a/src/ui/users.rs +++ b/src/ui/users.rs @@ -74,21 +74,37 @@ impl<'a> StatefulWidget for &Users<'a> { #[cfg(test)] mod tests { + use crate::backend::{ + nc_request::NCReqDataParticipants, nc_room::MockNCRoomInterface, nc_talk::MockNCTalk, + }; use backend::TestBackend; use super::*; #[test] fn render_users() { + let mut mock_nc_backend = MockNCTalk::new(); let backend = TestBackend::new(10, 10); let mut terminal = Terminal::new(backend).unwrap(); - let users = Users::new(); + let mut users = Users::new(); + + let mut mock_room = MockNCRoomInterface::new(); + let mut dummy_user = NCReqDataParticipants::default(); + dummy_user.displayName = "Butz".to_string(); + mock_room.expect_get_users().return_const(vec![dummy_user]); + mock_nc_backend + .expect_get_current_room() + .once() + .return_const(mock_room); + users.update(&mock_nc_backend); + terminal .draw(|frame| users.render_area(frame, Rect::new(0, 0, 8, 8))) .unwrap(); + let mut expected = Buffer::with_lines([ "│Users ", - "│ ", + "│Butz ", "│ ", "│ ", "│ ", @@ -103,6 +119,9 @@ mod tests { for x in 1..=7 { expected[(x, 0)].set_style(Style::new().green().on_black().bold()); } + for x in 1..=7 { + expected[(x, 1)].set_style(Style::new().white().on_black().bold()); + } terminal.backend().assert_buffer(&expected); } From 169c9e3909e6358e2150d8c7248aec4cbe1bea73 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz Date: Sat, 31 Aug 2024 13:12:29 +0200 Subject: [PATCH 55/89] fix clippy and fmt --- src/backend/nc_room.rs | 8 +--- src/backend/nc_talk.rs | 105 ++++++++++++++++++++++------------------- src/ui/mod.rs | 26 +++++----- 3 files changed, 72 insertions(+), 67 deletions(-) diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index 0f04f12..66cc564 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -9,8 +9,9 @@ use num_derive::FromPrimitive; use num_traits::{AsPrimitive, FromPrimitive}; use std::fmt::{Debug, Display}; -#[derive(Debug, FromPrimitive, PartialEq)] +#[derive(Debug, FromPrimitive, PartialEq, Default)] pub enum NCRoomTypes { + #[default] OneToOne = 1, Group, Public, @@ -19,11 +20,6 @@ pub enum NCRoomTypes { NoteToSelf, } -impl Default for NCRoomTypes { - fn default() -> Self { - NCRoomTypes::OneToOne - } -} #[cfg(test)] use mockall::{automock, predicate::*}; diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index bb5da41..52f31b2 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -8,7 +8,12 @@ use crate::{ }; use async_trait::async_trait; use itertools::Itertools; -use std::{collections::HashMap, error::Error, fmt::Debug, path::PathBuf}; +use std::{ + collections::HashMap, + error::Error, + fmt::Debug, + path::{Path, PathBuf}, +}; use super::nc_room::{NCRoom, NCRoomTypes}; @@ -48,21 +53,6 @@ impl NCTalk { rooms: &mut HashMap, chat_log_path: PathBuf, ) { - // for res in response { - // let room_tuple = NCTalk::::new_room( - // res, - // requester.clone(), - // notifier.clone(), - // chat_log_path.clone(), - // ); - // let (name, room_option) = room_tuple.await; - // if let Some(room) = room_option { - // rooms.insert(name, room); - // } else { - // log::info!("Encountered a room that cannot be added {} ", name); - // } - // } - let v = response.into_iter().map(|child| { tokio::spawn(NCTalk::new_room( child, @@ -80,6 +70,46 @@ impl NCTalk { } } } + async fn parse_files( + mut data: HashMap, + requester: &NCRequest, + notify: &NCNotify, + chat_log_path: &Path, + initial_message_ids: &mut HashMap, + rooms: &mut HashMap, + ) -> Result<(), Box> { + let mut handles = HashMap::new(); + for (token, room) in &mut data { + handles.insert( + token.clone(), + tokio::spawn(NCRoom::new( + room.clone(), + requester.clone(), + notify.clone(), + chat_log_path.to_path_buf(), + )), + ); + } + for (token, room_future) in &mut handles { + //we can safely unwrap here bc the json file on disk shall never be this broken. + let mut json_room = room_future.await?.unwrap(); + if initial_message_ids.contains_key::(token) { + let message_id = initial_message_ids.get(token).unwrap().lastMessage.id; + json_room + .update_if_id_is_newer( + message_id, + Some((*initial_message_ids.get(token).unwrap()).clone()), + ) + .await?; + rooms.insert(token.clone(), json_room); + initial_message_ids.remove(token); + } else { + log::warn!("Room was deleted upstream, failed to locate!"); + //TODO: remove old chat log!! + } + } + Ok(()) + } async fn new_room( packaged_child: NCReqDataRoom, @@ -110,39 +140,18 @@ impl NCTalk { let mut rooms = HashMap::::new(); if path.exists() { - if let Ok(mut data) = serde_json::from_str::>( + if let Ok(data) = serde_json::from_str::>( std::fs::read_to_string(path).unwrap().as_str(), ) { - let mut handles = HashMap::new(); - for (token, room) in &mut data { - handles.insert( - token.clone(), - tokio::spawn(NCRoom::new( - room.clone(), - requester.clone(), - notify.clone(), - chat_log_path.clone(), - )), - ); - } - for (token, room_future) in &mut handles { - //we can safely unwrap here bc the json file on disk shall never be this broken. - let mut json_room = room_future.await?.unwrap(); - if initial_message_ids.contains_key::(token) { - let message_id = initial_message_ids.get(token).unwrap().lastMessage.id; - json_room - .update_if_id_is_newer( - message_id, - Some((*initial_message_ids.get(token).unwrap()).clone()), - ) - .await?; - rooms.insert(token.clone(), json_room); - initial_message_ids.remove(token); - } else { - log::warn!("Room was deleted upstream, failed to locate!"); - //TODO: remove old chat log!! - } - } + NCTalk::parse_files( + data, + &requester, + ¬ify, + chat_log_path.as_path(), + &mut initial_message_ids, + &mut rooms, + ) + .await?; if !initial_message_ids.is_empty() { let remaining_room_data = response .iter() @@ -295,7 +304,7 @@ impl NCBackend for NCTalk { NCRoomTypes::NoteToSelf, NCRoomTypes::ChangeLog, ] - .contains(&room.get_room_type()) + .contains(room.get_room_type()) }) .map(|(key, _)| (key.clone(), self.rooms[key].to_string())) .sorted_by(|(token_a, _), (token_b, _)| self.rooms[token_a].cmp(&self.rooms[token_b])) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 0606c49..aef3f76 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -153,9 +153,9 @@ pub async fn run(nc_backend: NCTalk) -> Result<(), Box> { Ok(()) } -async fn run_app<'a, B: Backend>( +async fn run_app( mut terminal: Terminal, - mut app: App<'a, NCTalk>, + mut app: App<'_, NCTalk>, ) -> Result<(), Box> { app.select_room().await?; log::debug!("Entering Main Loop"); @@ -177,8 +177,8 @@ async fn run_app<'a, B: Backend>( } } -async fn process_event<'a>( - app: &mut App<'a, NCTalk>, +async fn process_event( + app: &mut App<'_, NCTalk>, event: Event, ) -> Result> { // It's guaranteed that `read` won't block, because `poll` returned @@ -213,9 +213,9 @@ async fn process_event<'a>( Ok(ProcessEventResult::Continue) } -async fn handle_key_in_opening<'a>( +async fn handle_key_in_opening( key: KeyEvent, - app: &mut App<'a, NCTalk>, + app: &mut App<'_, NCTalk>, ) -> Result<(), Box> { match key.code { KeyCode::Esc => app.current_screen = CurrentScreen::Reading, @@ -236,9 +236,9 @@ async fn handle_key_in_opening<'a>( Ok(()) } -async fn handle_key_in_editing<'a>( +async fn handle_key_in_editing( key: Input, - app: &mut App<'a, NCTalk>, + app: &mut App<'_, NCTalk>, ) -> Result<(), Box> { match key { Input { key: Key::Esc, .. } => app.current_screen = CurrentScreen::Reading, @@ -257,7 +257,7 @@ async fn handle_key_in_editing<'a>( Ok(()) } -fn handle_key_in_help<'a>(key: KeyEvent, app: &mut App<'a, NCTalk>) { +fn handle_key_in_help(key: KeyEvent, app: &mut App<'_, NCTalk>) { match key.code { KeyCode::Char('q') => app.current_screen = CurrentScreen::Exiting, KeyCode::Esc => app.current_screen = CurrentScreen::Reading, @@ -266,9 +266,9 @@ fn handle_key_in_help<'a>(key: KeyEvent, app: &mut App<'a, NCTalk>) { } } -fn handle_key_in_exit<'a>( +fn handle_key_in_exit( key: KeyEvent, - app: &mut App<'a, NCTalk>, + app: &mut App<'_, NCTalk>, ) -> Option>> { match key.code { KeyCode::Char('?') => app.current_screen = CurrentScreen::Helping, @@ -287,9 +287,9 @@ fn handle_key_in_exit<'a>( None } -async fn handle_key_in_reading<'a>( +async fn handle_key_in_reading( key: KeyEvent, - app: &mut App<'a, NCTalk>, + app: &mut App<'_, NCTalk>, ) -> Result<(), Box> { match key.code { KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { From 6bbfe31620eadfddc21a19a97331130b15668286 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz Date: Sat, 31 Aug 2024 14:36:25 +0200 Subject: [PATCH 56/89] fix clippy --- src/backend/nc_message.rs | 2 ++ src/backend/nc_room.rs | 9 ++++++--- src/backend/nc_talk.rs | 10 ++++++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/backend/nc_message.rs b/src/backend/nc_message.rs index ecb9774..b829afc 100644 --- a/src/backend/nc_message.rs +++ b/src/backend/nc_message.rs @@ -51,6 +51,7 @@ impl NCMessage { &self.0 } + #[allow(dead_code)] /// return `true` if message is a comment pub fn is_comment(&self) -> bool { self.0.messageType == "comment" @@ -76,6 +77,7 @@ impl NCMessage { self.is_system() && self.0.systemMessage == "reaction" } + #[allow(dead_code)] /// return `true` if message is a command pub fn is_command(&self) -> bool { self.0.messageType == "command" diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index 66cc564..27c3dec 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -28,6 +28,7 @@ use mockall::{automock, predicate::*}; pub trait NCRoomInterface: Debug + Send + Display + Ord + Default { fn get_last_room_level_message_id(&self) -> Option; fn has_unread(&self) -> bool; + #[allow(dead_code)] fn is_dm(&self) -> bool; fn is_group(&self) -> bool; fn get_messages(&self) -> &Vec; @@ -37,6 +38,7 @@ pub trait NCRoomInterface: Debug + Send + Display + Ord + Default { fn get_users(&self) -> &Vec; fn get_room_type(&self) -> &NCRoomTypes; + #[allow(dead_code)] fn to_json(&self) -> String; fn to_data(&self) -> NCReqDataRoom; fn write_to_log(&mut self) -> Result<(), std::io::Error>; @@ -375,20 +377,21 @@ mod tests { impl PartialOrd for MockNCRoomInterface { fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) + Some(self.to_string().cmp(&other.to_string())) } } impl PartialEq for MockNCRoomInterface { fn eq(&self, other: &Self) -> bool { - self == other + self.to_string() == other.to_string() } } impl Eq for MockNCRoomInterface {} impl std::fmt::Display for MockNCRoomInterface { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self) + let self_name = BUTZ.to_string(); + write!(f, "{self_name}") } } } diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index 52f31b2..b2bf63a 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -404,7 +404,8 @@ use mockall::{mock, predicate::*}; #[cfg(test)] mock! { #[derive(Debug)] - pub NCTalk{} + pub NCTalk{ + } #[async_trait] impl NCBackend for NCTalk{ type Room = MockNCRoomInterface; @@ -423,10 +424,15 @@ mock! { fn add_room(&mut self, room_option: Option<::Room>); } } + +#[cfg(test)] +static BUTZ: &str = "Butz"; + #[cfg(test)] impl std::fmt::Display for MockNCTalk { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self) + let self_name = BUTZ.to_string(); + write!(f, "{self_name}") } } From ae6bde2d63f31897f5f23c7bdd2bfff8b3b6a063 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz Date: Sat, 31 Aug 2024 18:47:59 +0200 Subject: [PATCH 57/89] add mock and trait to request --- src/backend/nc_request/mod.rs | 336 +++++++++++++-------- src/backend/nc_request/nc_req_data_user.rs | 1 - src/backend/nc_room.rs | 6 +- src/backend/nc_talk.rs | 60 +--- 4 files changed, 224 insertions(+), 179 deletions(-) diff --git a/src/backend/nc_request/mod.rs b/src/backend/nc_request/mod.rs index 43d14bc..e67c2eb 100644 --- a/src/backend/nc_request/mod.rs +++ b/src/backend/nc_request/mod.rs @@ -13,6 +13,7 @@ pub use nc_req_data_user::*; pub use nc_request_ocs_wrapper::*; use crate::config; +use async_trait::async_trait; use base64::{prelude::BASE64_STANDARD, write::EncoderWriter}; use jzon; use reqwest::{ @@ -20,8 +21,46 @@ use reqwest::{ Client, Response, Url, }; use serde::{Deserialize, Serialize}; +use std::fmt::Debug; use std::{collections::HashMap, error::Error}; +#[cfg(test)] +use mockall::{mock, predicate::*}; + +#[async_trait] +pub trait NCRequestInterface: Debug + Send + Clone + Default { + async fn send_message( + &self, + message: String, + token: &str, + ) -> Result>; + async fn fetch_autocomplete_users( + &self, + name: &str, + ) -> Result, Box>; + async fn fetch_participants( + &self, + token: &str, + ) -> Result, Box>; + async fn fetch_rooms_initial(&self) -> Result<(Vec, i64), Box>; + async fn fetch_rooms_update( + &self, + last_timestamp: i64, + ) -> Result<(Vec, i64), Box>; + async fn fetch_chat_initial( + &self, + token: &str, + maxMessage: i32, + ) -> Result, Box>; + async fn fetch_chat_update( + &self, + token: &str, + maxMessage: i32, + last_message: i32, + ) -> Result, Box>; + async fn mark_chat_read(&self, token: &str, last_message: i32) -> Result<(), Box>; +} + #[derive(Debug, Clone, Default)] pub struct NCRequest { base_url: String, @@ -38,7 +77,6 @@ pub struct NCReqDataMessageParameter { name: String, } - impl NCRequest { pub fn new() -> Result> { use std::io::Write; @@ -81,7 +119,128 @@ impl NCRequest { }) } - pub async fn send_message( + async fn request_rooms( + &self, + last_timestamp: Option, + ) -> Result<(Vec, i64), Box> { + let url_string = self.base_url.clone() + "/ocs/v2.php/apps/spreed/api/v4/room"; + let params = if let Some(timestamp) = last_timestamp { + HashMap::from([("modifiedSince", timestamp.to_string())]) + } else { + HashMap::new() + }; + let url = Url::parse_with_params(&url_string, ¶ms)?; + let response = self.request(url).await?; + match response.status() { + reqwest::StatusCode::OK => { + let timestamp = response + .headers() + .get("X-Nextcloud-Talk-Modified-Before") + .ok_or("Failed to get header")? + .to_str()? + .parse::()?; + let text = response.text().await?; + match serde_json::from_str::>>(&text) { + Ok(parser_response) => Ok((parser_response.ocs.data, timestamp)), + Err(why) => { + self.dump_json_to_log(&url_string, &text)?; + Err(Box::new(why)) + } + } + } + _ => Err(Box::new( + response + .error_for_status() + .err() + .ok_or("Failed to convert Err in reqwest")?, + )), + } + } + + async fn request_chat( + &self, + token: &str, + maxMessage: i32, + last_message: Option, + ) -> Result>, Box> { + let url_string = self.base_url.clone() + "/ocs/v2.php/apps/spreed/api/v1/chat/" + token; + let params = if let Some(lastId) = last_message { + log::debug!("Last MessageID {}", lastId); + HashMap::from([ + ("limit", maxMessage.to_string()), + ("setReadMarker", "0".into()), + ("lookIntoFuture", "1".into()), + ("lastKnownMessageId", lastId.to_string()), + ("timeout", "0".into()), + ("includeLastKnown", "0".into()), + ]) + } else { + HashMap::from([ + ("limit", maxMessage.to_string()), + ("setReadMarker", "0".into()), + ("lookIntoFuture", "0".into()), + ]) + }; + let url = Url::parse_with_params(&url_string, ¶ms)?; + let response = self.request(url).await?; + match response.status() { + reqwest::StatusCode::OK => { + log::debug!("Got new Messages."); + let text = response.text().await?; + match serde_json::from_str::>>(&text) { + Ok(parser_response) => Ok(Some(parser_response.ocs.data)), + Err(why) => { + self.dump_json_to_log(&url_string, &text)?; + Err(Box::new(why)) + } + } + } + reqwest::StatusCode::NOT_MODIFIED => { + log::debug!("No new Messages."); + Ok(Some(Vec::new())) + } + reqwest::StatusCode::PRECONDITION_FAILED => Ok(None), + _ => { + log::debug!("{} got Err {:?}", token, response); + Err(Box::new( + response + .error_for_status() + .err() + .ok_or("Failed to convert Error")?, + )) + } + } + } + + async fn request_post(&self, url: Url) -> Result { + let builder = self.client.post(url); + builder.send().await + } + + async fn request(&self, url: Url) -> Result { + let builder = self.client.get(url); + builder.send().await + } + + fn dump_json_to_log(&self, url: &str, text: &str) -> Result<(), Box> { + use std::io::Write; + + if let Some(path) = &self.json_dump_path { + let name: String = url + .chars() + .map(|ch| if ch == '/' { '_' } else { ch }) + .collect(); + let mut file = std::fs::File::create(name)?; + let pretty_text = jzon::stringify_pretty(jzon::parse(text)?, 2); + file.write_all(pretty_text.as_bytes())?; + } + Ok(()) + } +} + +#[async_trait] +impl NCRequestInterface for NCRequest { + async fn send_message( &self, message: String, token: &str, @@ -106,7 +265,7 @@ impl NCRequest { } } - pub async fn fetch_autocomplete_users( + async fn fetch_autocomplete_users( &self, name: &str, ) -> Result, Box> { @@ -136,7 +295,7 @@ impl NCRequest { } } - pub async fn fetch_participants( + async fn fetch_participants( &self, token: &str, ) -> Result, Box> { @@ -169,56 +328,18 @@ impl NCRequest { } } - pub async fn fetch_rooms_initial(&self) -> Result<(Vec, i64), Box> { + async fn fetch_rooms_initial(&self) -> Result<(Vec, i64), Box> { self.request_rooms(None).await } - pub async fn fetch_rooms_update( + async fn fetch_rooms_update( &self, last_timestamp: i64, ) -> Result<(Vec, i64), Box> { self.request_rooms(Some(last_timestamp)).await } - async fn request_rooms( - &self, - last_timestamp: Option, - ) -> Result<(Vec, i64), Box> { - let url_string = self.base_url.clone() + "/ocs/v2.php/apps/spreed/api/v4/room"; - let params = if let Some(timestamp) = last_timestamp { - HashMap::from([("modifiedSince", timestamp.to_string())]) - } else { - HashMap::new() - }; - let url = Url::parse_with_params(&url_string, ¶ms)?; - let response = self.request(url).await?; - match response.status() { - reqwest::StatusCode::OK => { - let timestamp = response - .headers() - .get("X-Nextcloud-Talk-Modified-Before") - .ok_or("Failed to get header")? - .to_str()? - .parse::()?; - let text = response.text().await?; - match serde_json::from_str::>>(&text) { - Ok(parser_response) => Ok((parser_response.ocs.data, timestamp)), - Err(why) => { - self.dump_json_to_log(&url_string, &text)?; - Err(Box::new(why)) - } - } - } - _ => Err(Box::new( - response - .error_for_status() - .err() - .ok_or("Failed to convert Err in reqwest")?, - )), - } - } - - pub async fn fetch_chat_initial( + async fn fetch_chat_initial( &self, token: &str, maxMessage: i32, @@ -235,7 +356,7 @@ impl NCRequest { } } - pub async fn fetch_chat_update( + async fn fetch_chat_update( &self, token: &str, maxMessage: i32, @@ -251,66 +372,7 @@ impl NCRequest { } } - async fn request_chat( - &self, - token: &str, - maxMessage: i32, - last_message: Option, - ) -> Result>, Box> { - let url_string = self.base_url.clone() + "/ocs/v2.php/apps/spreed/api/v1/chat/" + token; - let params = if let Some(lastId) = last_message { - log::debug!("Last MessageID {}", lastId); - HashMap::from([ - ("limit", maxMessage.to_string()), - ("setReadMarker", "0".into()), - ("lookIntoFuture", "1".into()), - ("lastKnownMessageId", lastId.to_string()), - ("timeout", "0".into()), - ("includeLastKnown", "0".into()), - ]) - } else { - HashMap::from([ - ("limit", maxMessage.to_string()), - ("setReadMarker", "0".into()), - ("lookIntoFuture", "0".into()), - ]) - }; - let url = Url::parse_with_params(&url_string, ¶ms)?; - let response = self.request(url).await?; - match response.status() { - reqwest::StatusCode::OK => { - log::debug!("Got new Messages."); - let text = response.text().await?; - match serde_json::from_str::>>(&text) { - Ok(parser_response) => Ok(Some(parser_response.ocs.data)), - Err(why) => { - self.dump_json_to_log(&url_string, &text)?; - Err(Box::new(why)) - } - } - } - reqwest::StatusCode::NOT_MODIFIED => { - log::debug!("No new Messages."); - Ok(Some(Vec::new())) - } - reqwest::StatusCode::PRECONDITION_FAILED => Ok(None), - _ => { - log::debug!("{} got Err {:?}", token, response); - Err(Box::new( - response - .error_for_status() - .err() - .ok_or("Failed to convert Error")?, - )) - } - } - } - - pub async fn mark_chat_read( - &self, - token: &str, - last_message: i32, - ) -> Result<(), Box> { + async fn mark_chat_read(&self, token: &str, last_message: i32) -> Result<(), Box> { let url_string = self.base_url.clone() + "/ocs/v2.php/apps/spreed/api/v1/chat/" + token + "/read"; let url = Url::parse(&url_string)?; @@ -326,30 +388,48 @@ impl NCRequest { )), } } +} - async fn request_post(&self, url: Url) -> Result { - let builder = self.client.post(url); - builder.send().await - } - - async fn request(&self, url: Url) -> Result { - let builder = self.client.get(url); - builder.send().await +#[cfg(test)] +mock! { + #[derive(Debug, Default, Clone)] + pub NCRequest {} // Name of the mock struct, less the "Mock" prefix + + #[async_trait] + impl NCRequestInterface for NCRequest { + async fn send_message( + &self, + message: String, + token: &str, + ) -> Result>; + async fn fetch_autocomplete_users( + &self, + name: &str, + ) -> Result, Box>; + async fn fetch_participants( + &self, + token: &str, + ) -> Result, Box>; + async fn fetch_rooms_initial(&self) -> Result<(Vec, i64), Box>; + async fn fetch_rooms_update( + &self, + last_timestamp: i64, + ) -> Result<(Vec, i64), Box>; + async fn fetch_chat_initial( + &self, + token: &str, + maxMessage: i32, + ) -> Result, Box>; + async fn fetch_chat_update( + &self, + token: &str, + maxMessage: i32, + last_message: i32, + ) -> Result, Box>; + async fn mark_chat_read(&self, token: &str, last_message: i32) -> Result<(), Box>; } - - fn dump_json_to_log(&self, url: &str, text: &str) -> Result<(), Box> { - use std::io::Write; - - if let Some(path) = &self.json_dump_path { - let name: String = url - .chars() - .map(|ch| if ch == '/' { '_' } else { ch }) - .collect(); - let mut file = std::fs::File::create(name)?; - let pretty_text = jzon::stringify_pretty(jzon::parse(text)?, 2); - file.write_all(pretty_text.as_bytes())?; - } - Ok(()) + impl Clone for NCRequest { // specification of the trait to mock + fn clone(&self) -> Self; } } diff --git a/src/backend/nc_request/nc_req_data_user.rs b/src/backend/nc_request/nc_req_data_user.rs index 93af43b..ec791e8 100644 --- a/src/backend/nc_request/nc_req_data_user.rs +++ b/src/backend/nc_request/nc_req_data_user.rs @@ -40,7 +40,6 @@ pub struct NCReqDataUser { shareWithDisplayNameUnique: String, } - fn str_or_status<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index 27c3dec..c45be95 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -1,7 +1,9 @@ use super::{ nc_message::NCMessage, nc_notify::NCNotify, - nc_request::{NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, NCRequest}, + nc_request::{ + NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, NCRequest, NCRequestInterface, + }, }; use async_trait::async_trait; use log; @@ -70,7 +72,7 @@ pub struct NCRoom { impl NCRoom { pub async fn new( room_data: NCReqDataRoom, - requester: NCRequest, + requester: impl NCRequestInterface, notifier: NCNotify, path_to_log: std::path::PathBuf, ) -> Option { diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index b2bf63a..73d204a 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -1,7 +1,7 @@ use crate::{ backend::{ nc_notify::NCNotify, - nc_request::{NCReqDataRoom, NCRequest}, + nc_request::{NCReqDataRoom, NCRequest, NCRequestInterface}, nc_room::NCRoomInterface, }, config::{self}, @@ -48,7 +48,7 @@ pub struct NCTalk { impl NCTalk { async fn parse_response( response: Vec, - requester: NCRequest, + requester: impl NCRequestInterface, notifier: NCNotify, rooms: &mut HashMap, chat_log_path: PathBuf, @@ -72,7 +72,7 @@ impl NCTalk { } async fn parse_files( mut data: HashMap, - requester: &NCRequest, + requester: &impl NCRequestInterface, notify: &NCNotify, chat_log_path: &Path, initial_message_ids: &mut HashMap, @@ -113,7 +113,7 @@ impl NCTalk { async fn new_room( packaged_child: NCReqDataRoom, - requester_box: NCRequest, + requester_box: impl NCRequestInterface, notifier: NCNotify, chat_log_path: PathBuf, ) -> (String, Option) { @@ -122,7 +122,7 @@ impl NCTalk { NCRoom::new(packaged_child, requester_box, notifier, chat_log_path).await, ) } - pub async fn new(requester: NCRequest) -> Result> { + pub async fn new(requester: impl NCRequestInterface) -> Result> { let notify = NCNotify::new(); let chat_log_path = config::get().get_server_data_dir(); @@ -438,47 +438,11 @@ impl std::fmt::Display for MockNCTalk { #[cfg(test)] mod tests { - // use crate::backend::nc_room::MockNCRoomInterface; - - // use super::*; - - // #[derive(Debug, Default)] - // pub struct MockBackend { - // pub current_token: String, - // pub room: HashMap>, - // } - - // impl<'a, Room> NCBackend<'a, Room> for MockBackend<'a, Room> { - // fn get_room(&self, token: &str) -> &Room {} - // fn get_current_room(&self, token: &str) -> &Room {} - // fn write_to_log(&mut self) -> Result<(), std::io::Error> { - // Ok(()) - // } - // fn get_current_room_token(&self) -> &str {} - // fn get_unread_rooms(&self) -> Vec { - // vec![] - // } - // fn get_room_by_displayname(&self, name: &str) -> &str {} - // fn get_dm_keys_display_name_mapping(&self) -> Vec<(String, String)> { - // vec![] - // } - // fn get_group_keys_display_name_mapping(&self) -> Vec<(String, String)> { - // vec![] - // } - // fn get_room_keys(&self) -> Vec<&String> { - // self.rooms.keys().collect::>() - // } - // } - // #[async_trait] - // impl NCBackendAsync for MockBackend { - // async fn send_message(&mut self, _message: String) -> Result<(), Box> { - // Ok(()) - // } - // async fn select_room(&mut self, _token: String) -> Result<(), Box> { - // Ok(()) - // } - // async fn update_rooms(&mut self, _force_update: bool) -> Result<(), Box> { - // Ok(()) - // } - // } + use super::NCTalk; + + #[test] + fn create_backend() { + let mock_requester = crate::backend::nc_request::MockNCRequestInterface::new(); + let backend = NCTalk::new(mock_requester); + } } From cfd907339d960c70d6d9639f042c88791c9f610b Mon Sep 17 00:00:00 2001 From: Michel von Czettritz Date: Sat, 31 Aug 2024 20:05:45 +0200 Subject: [PATCH 58/89] add test for backend --- src/backend/nc_request/mod.rs | 2 +- src/backend/nc_room.rs | 32 ++++++++++---------- src/backend/nc_talk.rs | 56 +++++++++++++++++++---------------- src/ui/mod.rs | 18 +++++------ 4 files changed, 57 insertions(+), 51 deletions(-) diff --git a/src/backend/nc_request/mod.rs b/src/backend/nc_request/mod.rs index e67c2eb..4bedc0d 100644 --- a/src/backend/nc_request/mod.rs +++ b/src/backend/nc_request/mod.rs @@ -28,7 +28,7 @@ use std::{collections::HashMap, error::Error}; use mockall::{mock, predicate::*}; #[async_trait] -pub trait NCRequestInterface: Debug + Send + Clone + Default { +pub trait NCRequestInterface: Debug + Send + Clone + Default + Send + Sync{ async fn send_message( &self, message: String, diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index c45be95..46f885f 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -2,7 +2,7 @@ use super::{ nc_message::NCMessage, nc_notify::NCNotify, nc_request::{ - NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, NCRequest, NCRequestInterface, + NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, NCRequestInterface, }, }; use async_trait::async_trait; @@ -59,8 +59,8 @@ pub trait NCRoomInterface: Debug + Send + Display + Ord + Default { } #[derive(Debug, Default)] -pub struct NCRoom { - requester: NCRequest, +pub struct NCRoom { + requester: Requester, notifier: NCNotify, pub messages: Vec, room_data: NCReqDataRoom, @@ -69,13 +69,13 @@ pub struct NCRoom { participants: Vec, } -impl NCRoom { +impl NCRoom { pub async fn new( room_data: NCReqDataRoom, - requester: impl NCRequestInterface, + requester: Requester, notifier: NCNotify, path_to_log: std::path::PathBuf, - ) -> Option { + ) -> Option> { let mut tmp_path_buf = path_to_log.clone(); tmp_path_buf.push(room_data.token.as_str()); let path = tmp_path_buf.as_path(); @@ -92,13 +92,13 @@ impl NCRoom { "Failed to parse json for {}, falling back to fetching", room_data.displayName ); - NCRoom::fetch_messages(&requester, &room_data.token, &mut messages) + NCRoom::::fetch_messages(&requester, &room_data.token, &mut messages) .await .ok(); } } else { log::debug!("No Log File found for room {}", room_data.displayName); - NCRoom::fetch_messages(&requester, &room_data.token, &mut messages) + NCRoom::::fetch_messages(&requester, &room_data.token, &mut messages) .await .ok(); } @@ -118,7 +118,7 @@ impl NCRoom { }) } async fn fetch_messages( - requester: &NCRequest, + requester: &Requester, token: &str, messages: &mut Vec, ) -> Result<(), Box> { @@ -131,7 +131,7 @@ impl NCRoom { } #[async_trait] -impl NCRoomInterface for NCRoom { +impl NCRoomInterface for NCRoom { // the room endpoint doesnt tell you about reactions... fn get_last_room_level_message_id(&self) -> Option { self.messages @@ -327,33 +327,33 @@ impl NCRoomInterface for NCRoom { } } -impl Ord for NCRoom { +impl Ord for NCRoom { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.to_string().cmp(other) } } -impl PartialOrd for NCRoom { +impl PartialOrd for NCRoom { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl PartialEq for NCRoom { +impl PartialEq for NCRoom { fn eq(&self, other: &Self) -> bool { self.as_str() == other.as_str() } } -impl Eq for NCRoom {} +impl Eq for NCRoom{} -impl std::fmt::Display for NCRoom { +impl std::fmt::Display for NCRoom { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_str()) } } -impl std::ops::Deref for NCRoom { +impl std::ops::Deref for NCRoom { type Target = String; fn deref(&self) -> &Self::Target { diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index 73d204a..747b344 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -1,7 +1,7 @@ use crate::{ backend::{ nc_notify::NCNotify, - nc_request::{NCReqDataRoom, NCRequest, NCRequestInterface}, + nc_request::{NCReqDataRoom, NCRequestInterface}, nc_room::NCRoomInterface, }, config::{self}, @@ -36,25 +36,25 @@ pub trait NCBackend: Debug + Send + Default { } #[derive(Debug, Default)] -pub struct NCTalk { - rooms: HashMap, +pub struct NCTalk { + rooms: HashMap>, chat_data_path: PathBuf, last_requested: i64, - requester: NCRequest, + requester: Requester, notifier: NCNotify, pub current_room_token: String, } -impl NCTalk { +impl NCTalk { async fn parse_response( response: Vec, - requester: impl NCRequestInterface, + requester: Requester, notifier: NCNotify, - rooms: &mut HashMap, + rooms: &mut HashMap>, chat_log_path: PathBuf, ) { let v = response.into_iter().map(|child| { - tokio::spawn(NCTalk::new_room( + tokio::spawn(NCTalk::::new_room( child, requester.clone(), notifier.clone(), @@ -72,17 +72,17 @@ impl NCTalk { } async fn parse_files( mut data: HashMap, - requester: &impl NCRequestInterface, + requester: &Requester, notify: &NCNotify, chat_log_path: &Path, initial_message_ids: &mut HashMap, - rooms: &mut HashMap, + rooms: &mut HashMap>, ) -> Result<(), Box> { let mut handles = HashMap::new(); for (token, room) in &mut data { handles.insert( token.clone(), - tokio::spawn(NCRoom::new( + tokio::spawn(NCRoom::::new( room.clone(), requester.clone(), notify.clone(), @@ -113,16 +113,16 @@ impl NCTalk { async fn new_room( packaged_child: NCReqDataRoom, - requester_box: impl NCRequestInterface, + requester_box: Requester, notifier: NCNotify, chat_log_path: PathBuf, - ) -> (String, Option) { + ) -> (String, Option>) { ( packaged_child.token.clone(), - NCRoom::new(packaged_child, requester_box, notifier, chat_log_path).await, + NCRoom::::new(packaged_child, requester_box, notifier, chat_log_path).await, ) } - pub async fn new(requester: impl NCRequestInterface) -> Result> { + pub async fn new(requester: Requester) -> Result, Box> { let notify = NCNotify::new(); let chat_log_path = config::get().get_server_data_dir(); @@ -137,7 +137,7 @@ impl NCTalk { .map(|room| (room.token.clone(), room)) .collect::>(); - let mut rooms = HashMap::::new(); + let mut rooms = HashMap::>::new(); if path.exists() { if let Ok(data) = serde_json::from_str::>( @@ -158,7 +158,7 @@ impl NCTalk { .filter(|data| initial_message_ids.contains_key(&data.token)) .cloned() .collect::>(); - NCTalk::parse_response( + NCTalk::::parse_response( remaining_room_data, requester.clone(), notify.clone(), @@ -174,7 +174,7 @@ impl NCTalk { log::info!("Loaded Rooms from log files"); } else { log::debug!("Failed to parse top level json, falling back to fetching"); - NCTalk::parse_response( + NCTalk::::parse_response( response, requester.clone(), notify.clone(), @@ -185,7 +185,7 @@ impl NCTalk { } } else { log::debug!("No Log files found in Path, fetching logs from server."); - NCTalk::parse_response( + NCTalk::::parse_response( response, requester.clone(), notify.clone(), @@ -220,8 +220,8 @@ impl NCTalk { } #[async_trait] -impl NCBackend for NCTalk { - type Room = NCRoom; +impl NCBackend for NCTalk { + type Room = NCRoom::; fn write_to_log(&mut self) -> Result<(), std::io::Error> { use std::io::Write; @@ -439,10 +439,16 @@ impl std::fmt::Display for MockNCTalk { #[cfg(test)] mod tests { use super::NCTalk; + use crate::config::init; - #[test] - fn create_backend() { - let mock_requester = crate::backend::nc_request::MockNCRequestInterface::new(); - let backend = NCTalk::new(mock_requester); + #[tokio::test] + async fn create_backend() { + let _ = init("./test/"); + + let mock_requester = crate::backend::nc_request::MockNCRequest::new(); + let backend = NCTalk::new(mock_requester).await.expect("Failed to create Backend"); + assert_eq!(backend.rooms.len(), 0); + + } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index aef3f76..e3ec73b 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -6,7 +6,7 @@ pub mod input_box; pub mod title_bar; pub mod users; -use crate::backend::nc_talk::NCTalk; +use crate::backend::{nc_request::NCRequest, nc_talk::NCTalk}; use super::{ config, @@ -139,7 +139,7 @@ enum ProcessEventResult { Exit, } -pub async fn run(nc_backend: NCTalk) -> Result<(), Box> { +pub async fn run(nc_backend: NCTalk::) -> Result<(), Box> { install_hooks()?; // create app and run it @@ -155,7 +155,7 @@ pub async fn run(nc_backend: NCTalk) -> Result<(), Box> { async fn run_app( mut terminal: Terminal, - mut app: App<'_, NCTalk>, + mut app: App<'_, NCTalk::>, ) -> Result<(), Box> { app.select_room().await?; log::debug!("Entering Main Loop"); @@ -178,7 +178,7 @@ async fn run_app( } async fn process_event( - app: &mut App<'_, NCTalk>, + app: &mut App<'_, NCTalk::>, event: Event, ) -> Result> { // It's guaranteed that `read` won't block, because `poll` returned @@ -215,7 +215,7 @@ async fn process_event( async fn handle_key_in_opening( key: KeyEvent, - app: &mut App<'_, NCTalk>, + app: &mut App<'_, NCTalk::>, ) -> Result<(), Box> { match key.code { KeyCode::Esc => app.current_screen = CurrentScreen::Reading, @@ -238,7 +238,7 @@ async fn handle_key_in_opening( async fn handle_key_in_editing( key: Input, - app: &mut App<'_, NCTalk>, + app: &mut App<'_, NCTalk::>, ) -> Result<(), Box> { match key { Input { key: Key::Esc, .. } => app.current_screen = CurrentScreen::Reading, @@ -257,7 +257,7 @@ async fn handle_key_in_editing( Ok(()) } -fn handle_key_in_help(key: KeyEvent, app: &mut App<'_, NCTalk>) { +fn handle_key_in_help(key: KeyEvent, app: &mut App<'_, NCTalk::>) { match key.code { KeyCode::Char('q') => app.current_screen = CurrentScreen::Exiting, KeyCode::Esc => app.current_screen = CurrentScreen::Reading, @@ -268,7 +268,7 @@ fn handle_key_in_help(key: KeyEvent, app: &mut App<'_, NCTalk>) { fn handle_key_in_exit( key: KeyEvent, - app: &mut App<'_, NCTalk>, + app: &mut App<'_, NCTalk::>, ) -> Option>> { match key.code { KeyCode::Char('?') => app.current_screen = CurrentScreen::Helping, @@ -289,7 +289,7 @@ fn handle_key_in_exit( async fn handle_key_in_reading( key: KeyEvent, - app: &mut App<'_, NCTalk>, + app: &mut App<'_, NCTalk::>, ) -> Result<(), Box> { match key.code { KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { From f3cf5e700d28cdc1dc4e1c69683b5259a60ab3ce Mon Sep 17 00:00:00 2001 From: Michel von Czettritz Date: Sat, 31 Aug 2024 20:48:50 +0200 Subject: [PATCH 59/89] add first test on NCTalk --- Makefile.toml | 4 +-- src/backend/nc_request/mod.rs | 2 +- src/backend/nc_room.rs | 30 +++++++++++--------- src/backend/nc_talk.rs | 52 +++++++++++++++++++++++++---------- src/ui/mod.rs | 16 +++++------ 5 files changed, 65 insertions(+), 39 deletions(-) diff --git a/Makefile.toml b/Makefile.toml index e83e25b..60506d4 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -12,7 +12,7 @@ dependencies = ["lint", "clippy", "check", "test"] [tasks.all] description = "Run all tasks" -dependencies = ["lint", "clippy", "check", "audit", "build", "coverage-text"] +dependencies = ["format", "lint-typos", "clippy", "check", "audit", "build", "coverage-text"] [tasks.clippy] @@ -81,7 +81,7 @@ args = [ [tasks.build] command = "cargo" args = ["build"] -dependencies = ["format", "clippy", "audit"] +dependencies = ["lint-format", "clippy", "audit"] [tasks.test] command = "cargo" diff --git a/src/backend/nc_request/mod.rs b/src/backend/nc_request/mod.rs index 4bedc0d..78cf3de 100644 --- a/src/backend/nc_request/mod.rs +++ b/src/backend/nc_request/mod.rs @@ -28,7 +28,7 @@ use std::{collections::HashMap, error::Error}; use mockall::{mock, predicate::*}; #[async_trait] -pub trait NCRequestInterface: Debug + Send + Clone + Default + Send + Sync{ +pub trait NCRequestInterface: Debug + Send + Clone + Default + Send + Sync { async fn send_message( &self, message: String, diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index 46f885f..f11ad54 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -1,9 +1,7 @@ use super::{ nc_message::NCMessage, nc_notify::NCNotify, - nc_request::{ - NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, NCRequestInterface, - }, + nc_request::{NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, NCRequestInterface}, }; use async_trait::async_trait; use log; @@ -59,7 +57,7 @@ pub trait NCRoomInterface: Debug + Send + Display + Ord + Default { } #[derive(Debug, Default)] -pub struct NCRoom { +pub struct NCRoom { requester: Requester, notifier: NCNotify, pub messages: Vec, @@ -69,13 +67,13 @@ pub struct NCRoom { participants: Vec, } -impl NCRoom { +impl NCRoom { pub async fn new( room_data: NCReqDataRoom, requester: Requester, notifier: NCNotify, path_to_log: std::path::PathBuf, - ) -> Option> { + ) -> Option> { let mut tmp_path_buf = path_to_log.clone(); tmp_path_buf.push(room_data.token.as_str()); let path = tmp_path_buf.as_path(); @@ -131,7 +129,9 @@ impl NCRoom NCRoomInterface for NCRoom { +impl NCRoomInterface + for NCRoom +{ // the room endpoint doesnt tell you about reactions... fn get_last_room_level_message_id(&self) -> Option { self.messages @@ -327,33 +327,37 @@ impl NCRoomInterface } } -impl Ord for NCRoom { +impl Ord for NCRoom { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.to_string().cmp(other) } } -impl PartialOrd for NCRoom { +impl PartialOrd for NCRoom { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl PartialEq for NCRoom { +impl PartialEq for NCRoom { fn eq(&self, other: &Self) -> bool { self.as_str() == other.as_str() } } -impl Eq for NCRoom{} +impl Eq for NCRoom {} -impl std::fmt::Display for NCRoom { +impl std::fmt::Display + for NCRoom +{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_str()) } } -impl std::ops::Deref for NCRoom { +impl std::ops::Deref + for NCRoom +{ type Target = String; fn deref(&self) -> &Self::Target { diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index 747b344..1112503 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -36,8 +36,8 @@ pub trait NCBackend: Debug + Send + Default { } #[derive(Debug, Default)] -pub struct NCTalk { - rooms: HashMap>, +pub struct NCTalk { + rooms: HashMap>, chat_data_path: PathBuf, last_requested: i64, requester: Requester, @@ -45,12 +45,12 @@ pub struct NCTalk { pub current_room_token: String, } -impl NCTalk { +impl NCTalk { async fn parse_response( response: Vec, requester: Requester, notifier: NCNotify, - rooms: &mut HashMap>, + rooms: &mut HashMap>, chat_log_path: PathBuf, ) { let v = response.into_iter().map(|child| { @@ -76,7 +76,7 @@ impl NCTalk, - rooms: &mut HashMap>, + rooms: &mut HashMap>, ) -> Result<(), Box> { let mut handles = HashMap::new(); for (token, room) in &mut data { @@ -116,13 +116,13 @@ impl NCTalk (String, Option>) { + ) -> (String, Option>) { ( packaged_child.token.clone(), NCRoom::::new(packaged_child, requester_box, notifier, chat_log_path).await, ) } - pub async fn new(requester: Requester) -> Result, Box> { + pub async fn new(requester: Requester) -> Result, Box> { let notify = NCNotify::new(); let chat_log_path = config::get().get_server_data_dir(); @@ -137,7 +137,7 @@ impl NCTalk>(); - let mut rooms = HashMap::>::new(); + let mut rooms = HashMap::>::new(); if path.exists() { if let Ok(data) = serde_json::from_str::>( @@ -220,8 +220,8 @@ impl NCTalk NCBackend for NCTalk { - type Room = NCRoom::; +impl NCBackend for NCTalk { + type Room = NCRoom; fn write_to_log(&mut self) -> Result<(), std::io::Error> { use std::io::Write; @@ -439,16 +439,38 @@ impl std::fmt::Display for MockNCTalk { #[cfg(test)] mod tests { use super::NCTalk; - use crate::config::init; + use crate::{backend::nc_request::NCReqDataRoom, config::init}; #[tokio::test] async fn create_backend() { let _ = init("./test/"); - let mock_requester = crate::backend::nc_request::MockNCRequest::new(); - let backend = NCTalk::new(mock_requester).await.expect("Failed to create Backend"); + let mut mock_requester = crate::backend::nc_request::MockNCRequest::new(); + let mut mock_requester_file = crate::backend::nc_request::MockNCRequest::new(); + let mock_requester_fetch = crate::backend::nc_request::MockNCRequest::new(); + let mock_requester_room = crate::backend::nc_request::MockNCRequest::new(); + + mock_requester + .expect_fetch_rooms_initial() + .once() + .returning_st(move || { + let mut default_room = NCReqDataRoom::default(); + default_room.displayName = "General".to_string(); + Ok((vec![default_room], 0)) + }); + mock_requester_file + .expect_clone() + .return_once_st(|| mock_requester_fetch); + mock_requester + .expect_clone() + .return_once_st(|| mock_requester_file); + + mock_requester + .expect_clone() + .return_once_st(|| mock_requester_room); + let backend = NCTalk::new(mock_requester) + .await + .expect("Failed to create Backend"); assert_eq!(backend.rooms.len(), 0); - - } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index e3ec73b..7d1d1f0 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -139,7 +139,7 @@ enum ProcessEventResult { Exit, } -pub async fn run(nc_backend: NCTalk::) -> Result<(), Box> { +pub async fn run(nc_backend: NCTalk) -> Result<(), Box> { install_hooks()?; // create app and run it @@ -155,7 +155,7 @@ pub async fn run(nc_backend: NCTalk::) -> Result<(), Box( mut terminal: Terminal, - mut app: App<'_, NCTalk::>, + mut app: App<'_, NCTalk>, ) -> Result<(), Box> { app.select_room().await?; log::debug!("Entering Main Loop"); @@ -178,7 +178,7 @@ async fn run_app( } async fn process_event( - app: &mut App<'_, NCTalk::>, + app: &mut App<'_, NCTalk>, event: Event, ) -> Result> { // It's guaranteed that `read` won't block, because `poll` returned @@ -215,7 +215,7 @@ async fn process_event( async fn handle_key_in_opening( key: KeyEvent, - app: &mut App<'_, NCTalk::>, + app: &mut App<'_, NCTalk>, ) -> Result<(), Box> { match key.code { KeyCode::Esc => app.current_screen = CurrentScreen::Reading, @@ -238,7 +238,7 @@ async fn handle_key_in_opening( async fn handle_key_in_editing( key: Input, - app: &mut App<'_, NCTalk::>, + app: &mut App<'_, NCTalk>, ) -> Result<(), Box> { match key { Input { key: Key::Esc, .. } => app.current_screen = CurrentScreen::Reading, @@ -257,7 +257,7 @@ async fn handle_key_in_editing( Ok(()) } -fn handle_key_in_help(key: KeyEvent, app: &mut App<'_, NCTalk::>) { +fn handle_key_in_help(key: KeyEvent, app: &mut App<'_, NCTalk>) { match key.code { KeyCode::Char('q') => app.current_screen = CurrentScreen::Exiting, KeyCode::Esc => app.current_screen = CurrentScreen::Reading, @@ -268,7 +268,7 @@ fn handle_key_in_help(key: KeyEvent, app: &mut App<'_, NCTalk::>) { fn handle_key_in_exit( key: KeyEvent, - app: &mut App<'_, NCTalk::>, + app: &mut App<'_, NCTalk>, ) -> Option>> { match key.code { KeyCode::Char('?') => app.current_screen = CurrentScreen::Helping, @@ -289,7 +289,7 @@ fn handle_key_in_exit( async fn handle_key_in_reading( key: KeyEvent, - app: &mut App<'_, NCTalk::>, + app: &mut App<'_, NCTalk>, ) -> Result<(), Box> { match key.code { KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { From 9795727e82cda4200ccfee6d8646836a2760e32e Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Mon, 2 Sep 2024 09:42:33 +0200 Subject: [PATCH 60/89] mark commented code with todo!() --- src/ui/chat_box.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ui/chat_box.rs b/src/ui/chat_box.rs index 377d9c3..fc4452c 100644 --- a/src/ui/chat_box.rs +++ b/src/ui/chat_box.rs @@ -135,11 +135,14 @@ impl<'a> ChatBox<'a> { position, self.state.selected().ok_or("nothing selected")? ); + // let new_selection = state.selected().ok_or("nothing selected")?; // self.current_index = position // .y // .clamp(0, (self.messages.len() - 1).try_into()?) // .try_into()?; + todo!("commented code missing?"); + Ok(()) } } From 329ce522b77c155d1ac8ed9c87dd81bb1c9481b5 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Mon, 2 Sep 2024 09:43:55 +0200 Subject: [PATCH 61/89] mark commented code with TODO --- src/ui/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 7d1d1f0..d9507ce 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -72,7 +72,7 @@ fn install_color_eyre_panic_hook(panic_hook: PanicHook) { error!("Unable to restore terminal: {err:?}"); } - // not sure about this + // TODO not sure about this // let msg = format!("{}", panic_hook.panic_report(panic_info)); // error!("Error: {}", strip_ansi_escapes::strip_str(msg)); panic_hook(panic_info); From 27ec919b096da6b7b0d575797e202545145908b4 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Mon, 2 Sep 2024 09:44:32 +0200 Subject: [PATCH 62/89] fix typo --- src/ui/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index d9507ce..f124ae1 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -127,10 +127,12 @@ pub fn restore() -> eyre::Result<()> { if config::get().get_enable_mouse() { execute!(stdout(), DisableMouseCapture)?; } - //proceed here regardless of error, since this will fail if the terminal doesnt support this. + + //proceed here regardless of error, since this will fail if the terminal doesn't support this. let _ = execute!(stdout(), PopKeyboardEnhancementFlags); execute!(stdout(), LeaveAlternateScreen)?; disable_raw_mode()?; + Ok(()) } From 16a1e2dea7257687a121370bcf1d25a6a5dc1cb7 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Mon, 2 Sep 2024 10:01:51 +0200 Subject: [PATCH 63/89] avoid mut by using iter().sorted_by().map() with builder pattern --- src/ui/users.rs | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/ui/users.rs b/src/ui/users.rs index 8efd007..b7b59da 100644 --- a/src/ui/users.rs +++ b/src/ui/users.rs @@ -1,3 +1,4 @@ +use itertools::Itertools; use ratatui::{ prelude::*, widgets::{Block, Borders, Cell, HighlightSpacing, Row, Table, TableState}, @@ -28,25 +29,30 @@ impl<'a> Users<'a> { frame.render_stateful_widget(self, area, &mut self.state.clone()); } pub fn update(&mut self, backend: &impl NCBackend) { - self.user_list.clear(); - let mut new_users = backend.get_current_room().get_users().clone(); - new_users.sort_by(|user1, user2| user1.displayName.cmp(&user2.displayName)); - for user in new_users { - let mut cell = Cell::new(user.displayName.to_string()); - if let Some(status) = user.status { - cell = match status.as_str() { - "away" => cell.set_style(Style::new().blue()), - "offline" => cell.set_style(Style::new().gray()), - "dnd" => cell.set_style(Style::new().red()), - "online" => cell.set_style(Style::new().green()), - unknown => { - log::debug!("Unknown Status {unknown}"); - cell.set_style(Style::new()) + self.user_list = backend + .get_current_room() + .get_users() + .iter() + .sorted_by(|user1, user2| user1.displayName.cmp(&user2.displayName)) + .map(|user| { + Row::new([{ + if let Some(status) = &user.status { + Cell::new(user.displayName.to_string()).set_style(match status.as_str() { + "away" => Style::new().blue(), + "offline" => Style::new().gray(), + "dnd" => Style::new().red(), + "online" => Style::new().green(), + unknown => { + log::debug!("Unknown Status {unknown}"); + Style::new() + } + }) + } else { + Cell::new(user.displayName.to_string()) } - } - }; - self.user_list.push(Row::new([cell])); - } + }]) + }) + .collect(); self.state = TableState::default().with_offset(0).with_selected(0); } From 9c00e7ca9b96b9b470757e4ed7ebb502344b5e89 Mon Sep 17 00:00:00 2001 From: Patrick Hoffmann Date: Mon, 2 Sep 2024 10:02:16 +0200 Subject: [PATCH 64/89] remove #[allow(dead_code)] at pub functions --- src/backend/nc_message.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/backend/nc_message.rs b/src/backend/nc_message.rs index b829afc..ecb9774 100644 --- a/src/backend/nc_message.rs +++ b/src/backend/nc_message.rs @@ -51,7 +51,6 @@ impl NCMessage { &self.0 } - #[allow(dead_code)] /// return `true` if message is a comment pub fn is_comment(&self) -> bool { self.0.messageType == "comment" @@ -77,7 +76,6 @@ impl NCMessage { self.is_system() && self.0.systemMessage == "reaction" } - #[allow(dead_code)] /// return `true` if message is a command pub fn is_command(&self) -> bool { self.0.messageType == "command" From db2fabe2afed485253e9efb4f028165eb7b2d2d5 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Tue, 22 Oct 2024 17:32:25 +0200 Subject: [PATCH 65/89] update deps --- Cargo.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3c7005b..9bf4aba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ serde_json = "1.0" jzon = "*" base64 = "*" toml = "*" -toml-example = "0.11.1" +toml-example = "0.12.0" ratatui = {version = "0.28", features = ["serde"] } crossterm = "*" tui-tree-widget = "0.22" @@ -44,20 +44,20 @@ log = "*" log4rs = "1.3" notify-rust = "4.11" itertools = "*" -clap = { version = "4.5.16", features = ["derive"] } +clap = { version = "4.5.20", features = ["derive"] } strum = "0.26" strum_macros = "0.26" textwrap = "0.16.1" etcetera = "0.8.0" better-panic = "0.3.0" color-eyre = "0.6.3" -human-panic = "2.0.1" -libc = "0.2.158" +human-panic = "2.0.2" +libc = "0.2.161" strip-ansi-escapes = "0.2.0" tracing = "0.1.40" cfg-if = "1.0.0" tui-textarea = "0.6.1" -async-trait = "0.1.81" +async-trait = "0.1.83" [lints.clippy] pedantic = "warn" From d7254187c3b622cd7af10341f2f89b963b36e77c Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Tue, 22 Oct 2024 17:32:53 +0200 Subject: [PATCH 66/89] fix clippy and small issues --- src/backend/nc_message.rs | 7 ++- src/backend/nc_room.rs | 2 +- src/backend/nc_talk.rs | 100 +++++++++++++++++++++----------------- src/ui/chat_box.rs | 3 +- 4 files changed, 64 insertions(+), 48 deletions(-) diff --git a/src/backend/nc_message.rs b/src/backend/nc_message.rs index ecb9774..3e4c415 100644 --- a/src/backend/nc_message.rs +++ b/src/backend/nc_message.rs @@ -23,7 +23,12 @@ impl NCMessage { /// return opponent display name pub fn get_name(&self) -> &str { - &self.0.actorDisplayName + if !self.is_comment() || self.is_system() || self.is_comment_deleted() || self.is_command() + { + "System" + } else { + &self.0.actorDisplayName + } } /// return the message itself diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index f11ad54..518c2c5 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -80,7 +80,7 @@ impl NCRoom::new(); - if path.exists() { + if path.exists() && path.is_file() { if let Ok(data) = serde_json::from_str::>( std::fs::read_to_string(path).unwrap().as_str(), ) { diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index 1112503..de7cb8b 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -25,7 +25,7 @@ pub trait NCBackend: Debug + Send + Default { fn get_room(&self, token: &str) -> &Self::Room; fn get_current_room(&self) -> &Self::Room; fn get_unread_rooms(&self) -> Vec; - fn get_room_by_displayname(&self, name: &str) -> &str; + fn get_room_by_displayname(&self, name: &str) -> String; fn get_dm_keys_display_name_mapping(&self) -> Vec<(String, String)>; fn get_group_keys_display_name_mapping(&self) -> Vec<(String, String)>; fn get_room_keys(&self) -> Vec<&'_ String>; @@ -286,10 +286,10 @@ impl NCBackend for .collect::>() } - fn get_room_by_displayname(&self, name: &str) -> &str { + fn get_room_by_displayname(&self, name: &str) -> String { for room in self.rooms.values() { if room.to_string() == *name { - return self.rooms[&room.to_token()].as_str(); + return room.to_token(); } } panic!("room doesnt exist {}", name); @@ -344,9 +344,10 @@ impl NCBackend for async fn select_room(&mut self, token: String) -> Result<(), Box> { self.current_room_token.clone_from(&token); + log::debug!("key {}", token); self.rooms .get_mut(&self.current_room_token) - .ok_or("Failed to get Room ref")? + .ok_or_else(|| format!("Failed to get Room ref for room selection: {token}."))? .update(None) .await } @@ -365,7 +366,7 @@ impl NCBackend for let room_ref = self .rooms .get_mut(room.token.as_str()) - .ok_or("Failed to get Room ref.")?; + .ok_or("Failed to get Room ref for update.")?; if force_update { room_ref.update(Some(room)).await?; } else { @@ -414,7 +415,7 @@ mock! { fn get_room(&self, token: &str) -> &::Room; fn get_current_room(&self) -> &::Room; fn get_unread_rooms(&self) -> Vec; - fn get_room_by_displayname(&self, name: &str) -> &str; + fn get_room_by_displayname(&self, name: &str) -> String; fn get_dm_keys_display_name_mapping(&self) -> Vec<(String, String)>; fn get_group_keys_display_name_mapping(&self) -> Vec<(String, String)>; fn get_room_keys<'a>(&'a self) -> Vec<&'a String>; @@ -436,41 +437,52 @@ impl std::fmt::Display for MockNCTalk { } } -#[cfg(test)] -mod tests { - use super::NCTalk; - use crate::{backend::nc_request::NCReqDataRoom, config::init}; - - #[tokio::test] - async fn create_backend() { - let _ = init("./test/"); - - let mut mock_requester = crate::backend::nc_request::MockNCRequest::new(); - let mut mock_requester_file = crate::backend::nc_request::MockNCRequest::new(); - let mock_requester_fetch = crate::backend::nc_request::MockNCRequest::new(); - let mock_requester_room = crate::backend::nc_request::MockNCRequest::new(); - - mock_requester - .expect_fetch_rooms_initial() - .once() - .returning_st(move || { - let mut default_room = NCReqDataRoom::default(); - default_room.displayName = "General".to_string(); - Ok((vec![default_room], 0)) - }); - mock_requester_file - .expect_clone() - .return_once_st(|| mock_requester_fetch); - mock_requester - .expect_clone() - .return_once_st(|| mock_requester_file); - - mock_requester - .expect_clone() - .return_once_st(|| mock_requester_room); - let backend = NCTalk::new(mock_requester) - .await - .expect("Failed to create Backend"); - assert_eq!(backend.rooms.len(), 0); - } -} +// #[cfg(test)] +// mod tests { +// use super::NCTalk; +// use crate::{ +// backend::nc_request::{NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom}, +// config::init, +// }; + +// #[tokio::test] +// async fn create_backend() { +// let _ = init("./test/"); + +// let mut mock_requester = crate::backend::nc_request::MockNCRequest::new(); +// let mut mock_requester_file = crate::backend::nc_request::MockNCRequest::new(); +// let mut mock_requester_fetch = crate::backend::nc_request::MockNCRequest::new(); +// let mock_requester_room = crate::backend::nc_request::MockNCRequest::new(); + +// mock_requester +// .expect_fetch_rooms_initial() +// .once() +// .returning_st(move || { +// let mut default_room = NCReqDataRoom::default(); +// default_room.displayName = "General".to_string(); +// Ok((vec![default_room], 0)) +// }); +// mock_requester_fetch +// .expect_fetch_chat_initial() +// .return_once_st(|_, _| Ok(vec![NCReqDataMessage::default()])); +// mock_requester_fetch +// .expect_fetch_participants() +// .return_once_st(|_| Ok(vec![NCReqDataParticipants::default()])); +// mock_requester_file +// .expect_clone() +// .return_once_st(|| mock_requester_fetch); + +// mock_requester +// .expect_clone() +// .return_once_st(|| mock_requester_file); + +// mock_requester +// .expect_clone() +// .return_once_st(|| mock_requester_room); + +// let backend = NCTalk::new(mock_requester) +// .await +// .expect("Failed to create Backend"); +// assert_eq!(backend.rooms.len(), 0); +// } +// } diff --git a/src/ui/chat_box.rs b/src/ui/chat_box.rs index fc4452c..8a256ac 100644 --- a/src/ui/chat_box.rs +++ b/src/ui/chat_box.rs @@ -141,9 +141,8 @@ impl<'a> ChatBox<'a> { // .y // .clamp(0, (self.messages.len() - 1).try_into()?) // .try_into()?; + // Ok(()) todo!("commented code missing?"); - - Ok(()) } } From 9b28878c70a2e7720b8da0a95fa841e75e284df9 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Wed, 23 Oct 2024 09:00:44 +0200 Subject: [PATCH 67/89] fix inital backend test --- src/backend/nc_request/nc_req_data_room.rs | 54 +++++----- src/backend/nc_talk.rs | 116 ++++++++++++--------- 2 files changed, 94 insertions(+), 76 deletions(-) diff --git a/src/backend/nc_request/nc_req_data_room.rs b/src/backend/nc_request/nc_req_data_room.rs index 4994665..9b1c0ca 100644 --- a/src/backend/nc_request/nc_req_data_room.rs +++ b/src/backend/nc_request/nc_req_data_room.rs @@ -24,37 +24,37 @@ pub struct NCReqDataRoom { pub readOnly: i32, pub listable: i32, pub messageExpiration: i32, - lastPing: i32, - sessionId: String, - hasPassword: bool, - hasCall: bool, - callFlag: i32, - canStartCall: bool, - canDeleteConversation: bool, - canLeaveConversation: bool, - lastActivity: i32, - isFavorite: bool, - notificationLevel: i32, - lobbyState: i32, - lobbyTimer: i32, - sipEnabled: i32, - canEnableSIP: bool, + pub lastPing: i32, + pub sessionId: String, + pub hasPassword: bool, + pub hasCall: bool, + pub callFlag: i32, + pub canStartCall: bool, + pub canDeleteConversation: bool, + pub canLeaveConversation: bool, + pub lastActivity: i32, + pub isFavorite: bool, + pub notificationLevel: i32, + pub lobbyState: i32, + pub lobbyTimer: i32, + pub sipEnabled: i32, + pub canEnableSIP: bool, pub unreadMessages: i32, - unreadMention: bool, - unreadMentionDirect: bool, + pub unreadMention: bool, + pub unreadMentionDirect: bool, pub lastReadMessage: i32, - lastCommonReadMessage: i32, + pub lastCommonReadMessage: i32, #[serde(deserialize_with = "arr_or_message")] pub lastMessage: NCReqDataMessage, - objectType: String, - objectId: String, - breakoutRoomMode: i32, - breakoutRoomStatus: i32, - avatarVersion: String, - isCustomAvatar: bool, - callStartTime: i32, - callRecording: i32, - recordingConsent: i32, + pub objectType: String, + pub objectId: String, + pub breakoutRoomMode: i32, + pub breakoutRoomStatus: i32, + pub avatarVersion: String, + pub isCustomAvatar: bool, + pub callStartTime: i32, + pub callRecording: i32, + pub recordingConsent: i32, } fn arr_or_message<'de, D>(deserializer: D) -> Result diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index de7cb8b..d08fccf 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -437,52 +437,70 @@ impl std::fmt::Display for MockNCTalk { } } -// #[cfg(test)] -// mod tests { -// use super::NCTalk; -// use crate::{ -// backend::nc_request::{NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom}, -// config::init, -// }; - -// #[tokio::test] -// async fn create_backend() { -// let _ = init("./test/"); - -// let mut mock_requester = crate::backend::nc_request::MockNCRequest::new(); -// let mut mock_requester_file = crate::backend::nc_request::MockNCRequest::new(); -// let mut mock_requester_fetch = crate::backend::nc_request::MockNCRequest::new(); -// let mock_requester_room = crate::backend::nc_request::MockNCRequest::new(); - -// mock_requester -// .expect_fetch_rooms_initial() -// .once() -// .returning_st(move || { -// let mut default_room = NCReqDataRoom::default(); -// default_room.displayName = "General".to_string(); -// Ok((vec![default_room], 0)) -// }); -// mock_requester_fetch -// .expect_fetch_chat_initial() -// .return_once_st(|_, _| Ok(vec![NCReqDataMessage::default()])); -// mock_requester_fetch -// .expect_fetch_participants() -// .return_once_st(|_| Ok(vec![NCReqDataParticipants::default()])); -// mock_requester_file -// .expect_clone() -// .return_once_st(|| mock_requester_fetch); - -// mock_requester -// .expect_clone() -// .return_once_st(|| mock_requester_file); - -// mock_requester -// .expect_clone() -// .return_once_st(|| mock_requester_room); - -// let backend = NCTalk::new(mock_requester) -// .await -// .expect("Failed to create Backend"); -// assert_eq!(backend.rooms.len(), 0); -// } -// } +#[cfg(test)] +mod tests { + use super::NCTalk; + use crate::{ + backend::nc_request::{NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom}, + config::init, + }; + + #[tokio::test] + async fn create_backend() { + let _ = init("./test/"); + + let mut mock_requester = crate::backend::nc_request::MockNCRequest::new(); + let mut mock_requester_file = crate::backend::nc_request::MockNCRequest::new(); + let mut mock_requester_fetch = crate::backend::nc_request::MockNCRequest::new(); + let mock_requester_room = crate::backend::nc_request::MockNCRequest::new(); + + let default_room = NCReqDataRoom { + displayName: "General".to_string(), + roomtype: 2, // Group Chat + ..Default::default() + }; + + let default_message = NCReqDataMessage { + messageType: "comment".to_string(), + id: 1, + ..Default::default() + }; + let update_message = NCReqDataMessage { + messageType: "comment".to_string(), + id: 2, + ..Default::default() + }; + + mock_requester + .expect_fetch_rooms_initial() + .once() + .returning_st(move || Ok((vec![default_room.clone()], 0))); + mock_requester_fetch + .expect_fetch_chat_initial() + .return_once_st(move |_, _| Ok(vec![default_message.clone()])); + mock_requester_fetch + .expect_fetch_participants() + .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); + + mock_requester_fetch + .expect_fetch_chat_update() + .return_once_st(move |_, _, _| Ok(vec![update_message.clone()])); + + mock_requester_file + .expect_clone() + .return_once_st(|| mock_requester_fetch); + + mock_requester + .expect_clone() + .return_once_st(|| mock_requester_file); + + mock_requester + .expect_clone() + .return_once_st(|| mock_requester_room); + + let backend = NCTalk::new(mock_requester) + .await + .expect("Failed to create Backend"); + assert_eq!(backend.rooms.len(), 1); + } +} From 35fe58e4ea37faa54a60be00f95a5a7492aae712 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Wed, 23 Oct 2024 10:20:31 +0200 Subject: [PATCH 68/89] fix cargo deny --- .github/workflows/rust.yml | 6 +++++- Makefile.toml | 12 +++++++++++- deny.toml | 3 +++ src/backend/nc_talk.rs | 1 + 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 27b422a..e35f660 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -41,7 +41,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: EmbarkStudios/cargo-deny-action@v2 + - uses: taiki-e/install-action@cargo-make + - uses: Swatinem/rust-cache@v2 + - run: cargo make audit + - run: cargo make deny + clippy: runs-on: ubuntu-latest diff --git a/Makefile.toml b/Makefile.toml index 60506d4..ad1e54b 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -12,7 +12,7 @@ dependencies = ["lint", "clippy", "check", "test"] [tasks.all] description = "Run all tasks" -dependencies = ["format", "lint-typos", "clippy", "check", "audit", "build", "coverage-text"] +dependencies = ["format", "lint-typos", "clippy", "check", "dependencies", "build", "coverage-text"] [tasks.clippy] @@ -50,12 +50,22 @@ description = "Run typo checks" install_crate = { crate_name = "typos-cli", binary = "typos", test_arg = "--version" } command = "typos" +[tasks.dependencies] +description = "Lint code style (formatting, typos, docs, markdown)" +dependencies = ["audit", "deny"] + [tasks.audit] command = "cargo" install_crate = "cargo-audit" args = ["audit", "-D", "warnings"] +[tasks.deny] +command = "cargo" +install_crate = "cargo-deny" +args = ["deny", "check", "bans", "licenses"] + + [tasks.check] description = "Check code for errors and warnings" command = "cargo" diff --git a/deny.toml b/deny.toml index fefd89d..bc1db99 100644 --- a/deny.toml +++ b/deny.toml @@ -4,13 +4,16 @@ confidence-threshold = 0.8 allow = [ "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", "BSD-2-Clause", "BSD-3-Clause", "ISC", "MIT", "Unicode-DFS-2016", "GPL-3.0", + "Zlib" ] + [bans] multiple-versions = "allow" diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index d08fccf..c38e7aa 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -480,6 +480,7 @@ mod tests { .return_once_st(move |_, _| Ok(vec![default_message.clone()])); mock_requester_fetch .expect_fetch_participants() + .times(2) .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); mock_requester_fetch From 44a4b483a69d2941ea46cd3a1b34a6fad4ddb2d1 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Fri, 9 Aug 2024 15:42:48 +0200 Subject: [PATCH 69/89] add user sidebar with toggle key and showing of status --- src/ui/help_box.rs | 1 + src/ui/users.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/ui/help_box.rs b/src/ui/help_box.rs index 75808c1..67a14ee 100644 --- a/src/ui/help_box.rs +++ b/src/ui/help_box.rs @@ -20,6 +20,7 @@ impl Widget for &HelpBox { Row::new(["q", "quit", "enter the quit screen."]), Row::new(["o", "open", "enter the chat selection screen."]), Row::new(["u", "users sidebar", "Toggle whether the users are shown in a chat sidebar. Available in reading mode."]), + Row::new(["?", "help", "enter this help screen."]), Row::new([ "m", diff --git a/src/ui/users.rs b/src/ui/users.rs index b7b59da..a69b698 100644 --- a/src/ui/users.rs +++ b/src/ui/users.rs @@ -1,4 +1,5 @@ use itertools::Itertools; + use ratatui::{ prelude::*, widgets::{Block, Borders, Cell, HighlightSpacing, Row, Table, TableState}, From 33d3560e1f5d7e8bddd3114b62bbf2cda78c27b8 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Wed, 14 Aug 2024 16:54:47 +0200 Subject: [PATCH 70/89] Add theme support --- Cargo.toml | 1 + src/config/mod.rs | 4 ++++ src/config/theme.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++ src/ui/title_bar.rs | 10 +++++--- src/ui/users.rs | 15 ++++++++---- 5 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 src/config/theme.rs diff --git a/Cargo.toml b/Cargo.toml index 9bf4aba..ce968ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ tracing = "0.1.40" cfg-if = "1.0.0" tui-textarea = "0.6.1" async-trait = "0.1.83" +serde_with = "3.9.0" [lints.clippy] pedantic = "warn" diff --git a/src/config/mod.rs b/src/config/mod.rs index b8087b5..3bcff78 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,10 +1,12 @@ mod data; +mod theme; use color_eyre::eyre::eyre; use data::Data; use etcetera::{app_strategy::Xdg, choose_app_strategy, AppStrategy, AppStrategyArgs}; use log::LevelFilter; use std::{path::PathBuf, process::exit, sync::OnceLock}; +use theme::Theme; use toml_example::TomlExample; static CONFIG: OnceLock = OnceLock::new(); @@ -12,6 +14,7 @@ static CONFIG: OnceLock = OnceLock::new(); #[derive(Debug)] pub struct Config { pub data: Data, + pub theme: Theme, strategy: Xdg, } @@ -95,6 +98,7 @@ impl Default for Config { fn default() -> Self { Self { data: Data::default(), + theme: Theme::default(), strategy: choose_app_strategy(AppStrategyArgs { top_level_domain: "org".to_string(), author: "emlix".to_string(), diff --git a/src/config/theme.rs b/src/config/theme.rs new file mode 100644 index 0000000..d0f75fb --- /dev/null +++ b/src/config/theme.rs @@ -0,0 +1,56 @@ +use ratatui::style::Color; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; +use std::str::FromStr; + +#[serde_as] +#[derive(Serialize, Deserialize, Debug)] +pub struct Theme { + /// Default Backround + #[serde_as(as = "DisplayFromStr")] + pub backround: Color, + /// Default Text Colour + #[serde_as(as = "DisplayFromStr")] + pub foreground: Color, + + /// Backround for unread message highlight + #[serde_as(as = "DisplayFromStr")] + pub backround_unread_message: Color, + /// Foreground for unread message highlight + #[serde_as(as = "DisplayFromStr")] + pub foreground_unread_message: Color, + + /// Text Colour for Chat and User table Headers + #[serde_as(as = "DisplayFromStr")] + pub table_header: Color, + + /// Text Colour for titlebar contents + #[serde_as(as = "DisplayFromStr")] + pub foreground_titlebar: Color, + + #[serde_as(as = "DisplayFromStr")] + pub user_away: Color, + #[serde_as(as = "DisplayFromStr")] + pub user_dnd: Color, + #[serde_as(as = "DisplayFromStr")] + pub user_offline: Color, + #[serde_as(as = "DisplayFromStr")] + pub user_online: Color, +} + +impl Default for Theme { + fn default() -> Self { + Self { + backround: Color::Black, + foreground: Color::White, + user_away: Color::Blue, + user_dnd: Color::Red, + user_offline: Color::Gray, + user_online: Color::Green, + backround_unread_message: Color::from_str("#6e6a86").unwrap(), + foreground_unread_message: Color::from_str("#e0def4").unwrap(), + table_header: Color::from_str("#e0def4").unwrap(), + foreground_titlebar: Color::White, + } + } +} diff --git a/src/ui/title_bar.rs b/src/ui/title_bar.rs index 4c41e18..720ca4c 100644 --- a/src/ui/title_bar.rs +++ b/src/ui/title_bar.rs @@ -1,5 +1,5 @@ use crate::backend::nc_room::NCRoomInterface; -use crate::{backend::nc_talk::NCBackend, ui::app::CurrentScreen}; +use crate::{backend::nc_talk::NCBackend, config, ui::app::CurrentScreen}; use num_traits::AsPrimitive; use ratatui::{ @@ -55,12 +55,16 @@ impl<'a> Widget for &TitleBar<'a> { let (room_title, room_title_style) = if self.unread > 0 { ( format!("Current: {}: {}", self.room, self.unread), - Style::default().fg(Color::White).bg(Color::Red), + Style::default() + .fg(config::get().theme.foreground_unread_message) + .bg(config::get().theme.backround_unread_message), ) } else { ( format!("Current: {}", self.room), - Style::default().fg(Color::Green), + Style::default() + .fg(config::get().theme.foreground_titlebar) + .bg(config::get().theme.backround), ) }; diff --git a/src/ui/users.rs b/src/ui/users.rs index a69b698..014f087 100644 --- a/src/ui/users.rs +++ b/src/ui/users.rs @@ -7,6 +7,7 @@ use ratatui::{ use style::Styled; use crate::backend::{nc_room::NCRoomInterface, nc_talk::NCBackend}; +use crate::config; pub struct Users<'a> { user_list: Vec>, @@ -39,10 +40,16 @@ impl<'a> Users<'a> { Row::new([{ if let Some(status) = &user.status { Cell::new(user.displayName.to_string()).set_style(match status.as_str() { - "away" => Style::new().blue(), - "offline" => Style::new().gray(), - "dnd" => Style::new().red(), - "online" => Style::new().green(), + "away" => { + cell.set_style(Style::new().fg(config::get().theme.user_away)) + } + "offline" => { + cell.set_style(Style::new().fg(config::get().theme.user_offline)) + } + "dnd" => cell.set_style(Style::new().fg(config::get().theme.user_dnd)), + "online" => { + cell.set_style(Style::new().fg(config::get().theme.user_online)) + } unknown => { log::debug!("Unknown Status {unknown}"); Style::new() From dcb0beb6873162ef6a5b2599256fc914c9ba9f66 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Tue, 29 Oct 2024 08:53:04 +0100 Subject: [PATCH 71/89] fixup for config usage in users view --- src/ui/users.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/ui/users.rs b/src/ui/users.rs index 014f087..9574fdd 100644 --- a/src/ui/users.rs +++ b/src/ui/users.rs @@ -40,16 +40,10 @@ impl<'a> Users<'a> { Row::new([{ if let Some(status) = &user.status { Cell::new(user.displayName.to_string()).set_style(match status.as_str() { - "away" => { - cell.set_style(Style::new().fg(config::get().theme.user_away)) - } - "offline" => { - cell.set_style(Style::new().fg(config::get().theme.user_offline)) - } - "dnd" => cell.set_style(Style::new().fg(config::get().theme.user_dnd)), - "online" => { - cell.set_style(Style::new().fg(config::get().theme.user_online)) - } + "away" => Style::new().fg(config::get().theme.user_away), + "offline" => Style::new().fg(config::get().theme.user_offline), + "dnd" => Style::new().fg(config::get().theme.user_dnd), + "online" => Style::new().fg(config::get().theme.user_online), unknown => { log::debug!("Unknown Status {unknown}"); Style::new() From 4946b6f0e149e8b6d7b1396a3d116c6f08acb9b0 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Fri, 1 Nov 2024 15:11:28 +0100 Subject: [PATCH 72/89] add theme getter and create style methods in theme --- src/config/mod.rs | 4 ++++ src/config/theme.rs | 24 ++++++++++++++++++++++++ src/ui/users.rs | 10 +++++----- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 3bcff78..362feb0 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -94,6 +94,10 @@ pub fn get() -> &'static Config { CONFIG.get().expect("config not initialized") } +pub fn get_theme() -> &'static Theme { + &CONFIG.get().expect("config not initialized").theme +} + impl Default for Config { fn default() -> Self { Self { diff --git a/src/config/theme.rs b/src/config/theme.rs index d0f75fb..8a942a0 100644 --- a/src/config/theme.rs +++ b/src/config/theme.rs @@ -1,4 +1,5 @@ use ratatui::style::Color; +use ratatui::style::Style; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; use std::str::FromStr; @@ -38,6 +39,29 @@ pub struct Theme { pub user_online: Color, } +impl Theme { + pub fn default_style(&self) -> Style { + Style::new().fg(self.foreground).bg(self.backround) + } + pub fn user_away_style(&self) -> Style { + Style::new().fg(self.user_away).bg(self.backround) + } + pub fn user_dnd_style(&self) -> Style { + Style::new().fg(self.user_dnd).bg(self.backround) + } + pub fn user_offline_style(&self) -> Style { + Style::new().fg(self.user_offline).bg(self.backround) + } + pub fn user_online_style(&self) -> Style { + Style::new().fg(self.user_online).bg(self.backround) + } + pub fn unread_message_style(&self) -> Style { + Style::new() + .fg(self.foreground_unread_message) + .bg(self.backround_unread_message) + } +} + impl Default for Theme { fn default() -> Self { Self { diff --git a/src/ui/users.rs b/src/ui/users.rs index 9574fdd..b70ef16 100644 --- a/src/ui/users.rs +++ b/src/ui/users.rs @@ -40,13 +40,13 @@ impl<'a> Users<'a> { Row::new([{ if let Some(status) = &user.status { Cell::new(user.displayName.to_string()).set_style(match status.as_str() { - "away" => Style::new().fg(config::get().theme.user_away), - "offline" => Style::new().fg(config::get().theme.user_offline), - "dnd" => Style::new().fg(config::get().theme.user_dnd), - "online" => Style::new().fg(config::get().theme.user_online), + "away" => config::get_theme().user_away_style(), + "offline" => config::get_theme().user_offline_style(), + "dnd" => config::get_theme().user_dnd_style(), + "online" => config::get_theme().user_online_style(), unknown => { log::debug!("Unknown Status {unknown}"); - Style::new() + config::get_theme().default_style() } }) } else { From 87c38682edeb8fcb279382a1d4c23344db4ba7d2 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Fri, 1 Nov 2024 15:31:28 +0100 Subject: [PATCH 73/89] move user tab over to theme usage --- src/config/theme.rs | 5 +++++ src/ui/users.rs | 18 +++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/config/theme.rs b/src/config/theme.rs index 8a942a0..f1f4e64 100644 --- a/src/config/theme.rs +++ b/src/config/theme.rs @@ -1,5 +1,6 @@ use ratatui::style::Color; use ratatui::style::Style; +use ratatui::style::Stylize; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; use std::str::FromStr; @@ -60,6 +61,10 @@ impl Theme { .fg(self.foreground_unread_message) .bg(self.backround_unread_message) } + + pub fn table_header_style(&self) -> Style { + Style::new().bold().fg(self.table_header).bg(self.backround) + } } impl Default for Theme { diff --git a/src/ui/users.rs b/src/ui/users.rs index b70ef16..8095022 100644 --- a/src/ui/users.rs +++ b/src/ui/users.rs @@ -51,6 +51,7 @@ impl<'a> Users<'a> { }) } else { Cell::new(user.displayName.to_string()) + .style(config::get_theme().default_style()) } }]) }) @@ -66,8 +67,8 @@ impl<'a> StatefulWidget for &Users<'a> { StatefulWidget::render( Table::new(self.user_list.clone(), [Constraint::Percentage(100)]) .column_spacing(1) - .style(Style::new().white().on_black()) - .header(Row::new(vec!["Users"]).style(Style::new().bold().green())) + .style(config::get_theme().default_style()) + .header(Row::new(vec!["Users"]).style(config::get_theme().table_header_style())) .block(Block::default()) .highlight_style(Style::new().bold()) .highlight_spacing(HighlightSpacing::Never) @@ -82,15 +83,19 @@ impl<'a> StatefulWidget for &Users<'a> { #[cfg(test)] mod tests { + use crate::backend::{ nc_request::NCReqDataParticipants, nc_room::MockNCRoomInterface, nc_talk::MockNCTalk, }; use backend::TestBackend; + use config::init; use super::*; #[test] fn render_users() { + let _ = init("./test/"); + let mut mock_nc_backend = MockNCTalk::new(); let backend = TestBackend::new(10, 10); let mut terminal = Terminal::new(backend).unwrap(); @@ -122,13 +127,16 @@ mod tests { " ", " ", ]); - expected.set_style(Rect::new(0, 0, 8, 8), Style::new().white().on_black()); + expected.set_style(Rect::new(0, 0, 8, 8), config::get_theme().default_style()); + // header for x in 1..=7 { - expected[(x, 0)].set_style(Style::new().green().on_black().bold()); + expected[(x, 0)].set_style(config::get_theme().table_header_style()); } + + // selected user for x in 1..=7 { - expected[(x, 1)].set_style(Style::new().white().on_black().bold()); + expected[(x, 1)].set_style(config::get_theme().default_style().bold()); } terminal.backend().assert_buffer(&expected); From d21305625fb1daad39f6ab64f0674de6f6a2a0de Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Fri, 1 Nov 2024 15:49:48 +0100 Subject: [PATCH 74/89] move titlebar over to theme --- src/config/theme.rs | 21 ++++++++++++++++++++- src/ui/title_bar.rs | 21 +++++++++------------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/config/theme.rs b/src/config/theme.rs index f1f4e64..418ddf1 100644 --- a/src/config/theme.rs +++ b/src/config/theme.rs @@ -29,6 +29,12 @@ pub struct Theme { /// Text Colour for titlebar contents #[serde_as(as = "DisplayFromStr")] pub foreground_titlebar: Color, + /// Text Colour for titlebar contents + #[serde_as(as = "DisplayFromStr")] + pub backround_important_titlebar: Color, + /// Text Colour for titlebar contents + #[serde_as(as = "DisplayFromStr")] + pub foreground_important_titlebar: Color, #[serde_as(as = "DisplayFromStr")] pub user_away: Color, @@ -65,12 +71,23 @@ impl Theme { pub fn table_header_style(&self) -> Style { Style::new().bold().fg(self.table_header).bg(self.backround) } + + pub fn title_status_style(&self) -> Style { + Style::new().bg(self.backround).fg(self.foreground_titlebar) + } + + pub fn title_important_style(&self) -> Style { + Style::new() + .bold() + .bg(self.backround_important_titlebar) + .fg(self.foreground_important_titlebar) + } } impl Default for Theme { fn default() -> Self { Self { - backround: Color::Black, + backround: Color::DarkGray, foreground: Color::White, user_away: Color::Blue, user_dnd: Color::Red, @@ -80,6 +97,8 @@ impl Default for Theme { foreground_unread_message: Color::from_str("#e0def4").unwrap(), table_header: Color::from_str("#e0def4").unwrap(), foreground_titlebar: Color::White, + backround_important_titlebar: Color::Red, + foreground_important_titlebar: Color::White, } } } diff --git a/src/ui/title_bar.rs b/src/ui/title_bar.rs index 720ca4c..4e87cfc 100644 --- a/src/ui/title_bar.rs +++ b/src/ui/title_bar.rs @@ -1,5 +1,6 @@ use crate::backend::nc_room::NCRoomInterface; -use crate::{backend::nc_talk::NCBackend, config, ui::app::CurrentScreen}; +use crate::config::get_theme; +use crate::{backend::nc_talk::NCBackend, ui::app::CurrentScreen}; use num_traits::AsPrimitive; use ratatui::{ @@ -41,7 +42,7 @@ impl<'a> TitleBar<'a> { Text::raw("") } else { Text::raw("UNREAD: ".to_owned() + unread_array.join(", ").as_str()) - .set_style(Style::default().white().on_red().rapid_blink().bold()) + .set_style(get_theme().title_important_style().rapid_blink()) }; } @@ -55,16 +56,12 @@ impl<'a> Widget for &TitleBar<'a> { let (room_title, room_title_style) = if self.unread > 0 { ( format!("Current: {}: {}", self.room, self.unread), - Style::default() - .fg(config::get().theme.foreground_unread_message) - .bg(config::get().theme.backround_unread_message), + get_theme().title_status_style(), ) } else { ( format!("Current: {}", self.room), - Style::default() - .fg(config::get().theme.foreground_titlebar) - .bg(config::get().theme.backround), + get_theme().title_status_style(), ) }; @@ -79,7 +76,7 @@ impl<'a> Widget for &TitleBar<'a> { let title_block = Block::default() .borders(Borders::BOTTOM) - .style(Style::default()); + .style(get_theme().default_style()); Paragraph::new(Text::styled(room_title, room_title_style)) .block(title_block) @@ -87,7 +84,7 @@ impl<'a> Widget for &TitleBar<'a> { let unread_block = Block::default() .borders(Borders::BOTTOM) - .style(Style::default()); + .style(get_theme().default_style()); Paragraph::new(self.unread_rooms.clone()) .block(unread_block) @@ -95,11 +92,11 @@ impl<'a> Widget for &TitleBar<'a> { let mode_block = Block::default() .borders(Borders::BOTTOM) - .style(Style::default()); + .style(get_theme().default_style()); Paragraph::new(Text::styled( self.mode.clone(), - Style::default().fg(Color::Green), + get_theme().title_status_style(), )) .block(mode_block) .alignment(Alignment::Right) From 4dab9df7fcb794035bd6bbf7c8d203d96e1b8277 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Fri, 1 Nov 2024 15:57:27 +0100 Subject: [PATCH 75/89] move chat_box to theme --- src/config/theme.rs | 11 +++++++++++ src/ui/chat_box.rs | 14 ++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/config/theme.rs b/src/config/theme.rs index 418ddf1..0c24e74 100644 --- a/src/config/theme.rs +++ b/src/config/theme.rs @@ -14,6 +14,10 @@ pub struct Theme { /// Default Text Colour #[serde_as(as = "DisplayFromStr")] pub foreground: Color, + #[serde_as(as = "DisplayFromStr")] + pub backround_highlight: Color, + #[serde_as(as = "DisplayFromStr")] + pub foreground_highlight: Color, /// Backround for unread message highlight #[serde_as(as = "DisplayFromStr")] @@ -50,6 +54,11 @@ impl Theme { pub fn default_style(&self) -> Style { Style::new().fg(self.foreground).bg(self.backround) } + pub fn default_highlight_style(&self) -> Style { + Style::new() + .fg(self.foreground_highlight) + .bg(self.backround_highlight) + } pub fn user_away_style(&self) -> Style { Style::new().fg(self.user_away).bg(self.backround) } @@ -89,6 +98,8 @@ impl Default for Theme { Self { backround: Color::DarkGray, foreground: Color::White, + backround_highlight: Color::Gray, + foreground_highlight: Color::White, user_away: Color::Blue, user_dnd: Color::Red, user_offline: Color::Gray, diff --git a/src/ui/chat_box.rs b/src/ui/chat_box.rs index 8a256ac..f47b212 100644 --- a/src/ui/chat_box.rs +++ b/src/ui/chat_box.rs @@ -1,4 +1,5 @@ use crate::backend::{nc_room::NCRoomInterface, nc_talk::NCBackend}; +use crate::config::get_theme; use ratatui::{ prelude::*, widgets::{Block, Cell, HighlightSpacing, Row, Table, TableState}, @@ -96,7 +97,9 @@ impl<'a> ChatBox<'a> { "".into(), Span::styled( "+++ LAST READ +++", - Style::default().add_modifier(Modifier::BOLD), + get_theme() + .unread_message_style() + .add_modifier(Modifier::BOLD), ) .into(), ]; @@ -158,10 +161,13 @@ impl<'a> StatefulWidget for &ChatBox<'a> { StatefulWidget::render( Table::new(self.messages.clone(), widths) .column_spacing(1) - .style(Style::new().white().on_black()) - .header(Row::new(vec!["Time", "Name", "Message"]).style(Style::new().bold().blue())) + .style(get_theme().default_style()) + .header( + Row::new(vec!["Time", "Name", "Message"]) + .style(get_theme().table_header_style()), + ) .block(Block::default()) - .highlight_style(Style::new().green()) + .highlight_style(get_theme().default_highlight_style()) .highlight_spacing(HighlightSpacing::Never), area, buf, From 01df2b9aa4ded3379f27fe4e83fef6e831eb73e2 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Fri, 1 Nov 2024 16:07:04 +0100 Subject: [PATCH 76/89] move input box over --- src/config/theme.rs | 2 +- src/ui/input_box.rs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/config/theme.rs b/src/config/theme.rs index 0c24e74..10cf7fc 100644 --- a/src/config/theme.rs +++ b/src/config/theme.rs @@ -106,7 +106,7 @@ impl Default for Theme { user_online: Color::Green, backround_unread_message: Color::from_str("#6e6a86").unwrap(), foreground_unread_message: Color::from_str("#e0def4").unwrap(), - table_header: Color::from_str("#e0def4").unwrap(), + table_header: Color::Blue, foreground_titlebar: Color::White, backround_important_titlebar: Color::Red, foreground_important_titlebar: Color::White, diff --git a/src/ui/input_box.rs b/src/ui/input_box.rs index bca940b..a1aa785 100644 --- a/src/ui/input_box.rs +++ b/src/ui/input_box.rs @@ -1,3 +1,4 @@ +use crate::config::get_theme; use ratatui::{ prelude::*, widgets::{Block, Borders}, @@ -12,7 +13,11 @@ pub struct InputBox<'a> { impl<'a> InputBox<'a> { pub fn new(initial_message: &str) -> InputBox<'a> { let mut textarea = TextArea::new(vec![initial_message.into()]); - textarea.set_block(Block::default().borders(Borders::TOP)); + textarea.set_block( + Block::default() + .borders(Borders::TOP) + .style(get_theme().default_style()), + ); InputBox { textarea } } From 88cd8f0e5d79880ef649049160d06e86ee80f1b8 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Tue, 10 Dec 2024 09:55:32 +0100 Subject: [PATCH 77/89] fix help screen by breaking new lines in the insert field crossterm seems to not be able to send us uper case keys while also capturing shit enter. since '?' is more important that adding new lines I will switch this over for now. --- src/ui/mod.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index f124ae1..18a3940 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -97,10 +97,7 @@ pub fn init() -> eyre::Result { execute!(stdout(), EnterAlternateScreen)?; if execute!( stdout(), - PushKeyboardEnhancementFlags( - KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES - | KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES - ) + PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES) ) .is_err() { @@ -210,7 +207,9 @@ async fn process_event( } _ => (), }, - _ => (), + _ => { + log::debug!("Unknown Event {:?}", event); + } } Ok(ProcessEventResult::Continue) } From 6c2e7194426ec6ae0d00f47ad45c05bf36ae43d5 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Tue, 10 Dec 2024 10:01:59 +0100 Subject: [PATCH 78/89] move over chat_selector --- src/ui/chat_selector.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/ui/chat_selector.rs b/src/ui/chat_selector.rs index 8c58a8f..71dc78c 100644 --- a/src/ui/chat_selector.rs +++ b/src/ui/chat_selector.rs @@ -3,7 +3,6 @@ use std::error::Error; use itertools::Itertools; use ratatui::{ prelude::*, - style::{Color, Modifier, Style}, widgets::{Block, Scrollbar, ScrollbarOrientation}, Frame, }; @@ -12,6 +11,7 @@ use tui_tree_widget::{Tree, TreeItem, TreeState}; use crate::backend::nc_room::NCRoomInterface; use crate::backend::nc_talk::NCBackend; +use crate::config::get_theme; pub struct ChatSelector<'a> { pub state: TreeState, @@ -118,12 +118,8 @@ impl<'a> ChatSelector<'a> { .track_symbol(None) .end_symbol(None), )) - .highlight_style( - Style::new() - .fg(Color::Black) - .bg(Color::LightGreen) - .add_modifier(Modifier::BOLD), - ) + .style(get_theme().default_style()) + .highlight_style(get_theme().default_highlight_style().bold()) .highlight_symbol(">> "); frame.render_stateful_widget(widget, area, &mut self.state); } From 053d6baeca84599aa6117c14a07114acf2116c52 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Tue, 10 Dec 2024 10:04:05 +0100 Subject: [PATCH 79/89] fix typo in theme --- src/config/theme.rs | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/config/theme.rs b/src/config/theme.rs index 10cf7fc..fc64f85 100644 --- a/src/config/theme.rs +++ b/src/config/theme.rs @@ -8,20 +8,20 @@ use std::str::FromStr; #[serde_as] #[derive(Serialize, Deserialize, Debug)] pub struct Theme { - /// Default Backround + /// Default Background #[serde_as(as = "DisplayFromStr")] - pub backround: Color, + pub background: Color, /// Default Text Colour #[serde_as(as = "DisplayFromStr")] pub foreground: Color, #[serde_as(as = "DisplayFromStr")] - pub backround_highlight: Color, + pub background_highlight: Color, #[serde_as(as = "DisplayFromStr")] pub foreground_highlight: Color, - /// Backround for unread message highlight + /// background for unread message highlight #[serde_as(as = "DisplayFromStr")] - pub backround_unread_message: Color, + pub background_unread_message: Color, /// Foreground for unread message highlight #[serde_as(as = "DisplayFromStr")] pub foreground_unread_message: Color, @@ -35,7 +35,7 @@ pub struct Theme { pub foreground_titlebar: Color, /// Text Colour for titlebar contents #[serde_as(as = "DisplayFromStr")] - pub backround_important_titlebar: Color, + pub background_important_titlebar: Color, /// Text Colour for titlebar contents #[serde_as(as = "DisplayFromStr")] pub foreground_important_titlebar: Color, @@ -52,43 +52,48 @@ pub struct Theme { impl Theme { pub fn default_style(&self) -> Style { - Style::new().fg(self.foreground).bg(self.backround) + Style::new().fg(self.foreground).bg(self.background) } pub fn default_highlight_style(&self) -> Style { Style::new() .fg(self.foreground_highlight) - .bg(self.backround_highlight) + .bg(self.background_highlight) } pub fn user_away_style(&self) -> Style { - Style::new().fg(self.user_away).bg(self.backround) + Style::new().fg(self.user_away).bg(self.background) } pub fn user_dnd_style(&self) -> Style { - Style::new().fg(self.user_dnd).bg(self.backround) + Style::new().fg(self.user_dnd).bg(self.background) } pub fn user_offline_style(&self) -> Style { - Style::new().fg(self.user_offline).bg(self.backround) + Style::new().fg(self.user_offline).bg(self.background) } pub fn user_online_style(&self) -> Style { - Style::new().fg(self.user_online).bg(self.backround) + Style::new().fg(self.user_online).bg(self.background) } pub fn unread_message_style(&self) -> Style { Style::new() .fg(self.foreground_unread_message) - .bg(self.backround_unread_message) + .bg(self.background_unread_message) } pub fn table_header_style(&self) -> Style { - Style::new().bold().fg(self.table_header).bg(self.backround) + Style::new() + .bold() + .fg(self.table_header) + .bg(self.background) } pub fn title_status_style(&self) -> Style { - Style::new().bg(self.backround).fg(self.foreground_titlebar) + Style::new() + .bg(self.background) + .fg(self.foreground_titlebar) } pub fn title_important_style(&self) -> Style { Style::new() .bold() - .bg(self.backround_important_titlebar) + .bg(self.background_important_titlebar) .fg(self.foreground_important_titlebar) } } @@ -96,19 +101,19 @@ impl Theme { impl Default for Theme { fn default() -> Self { Self { - backround: Color::DarkGray, + background: Color::DarkGray, foreground: Color::White, - backround_highlight: Color::Gray, + background_highlight: Color::Gray, foreground_highlight: Color::White, user_away: Color::Blue, user_dnd: Color::Red, user_offline: Color::Gray, user_online: Color::Green, - backround_unread_message: Color::from_str("#6e6a86").unwrap(), + background_unread_message: Color::from_str("#6e6a86").unwrap(), foreground_unread_message: Color::from_str("#e0def4").unwrap(), table_header: Color::Blue, foreground_titlebar: Color::White, - backround_important_titlebar: Color::Red, + background_important_titlebar: Color::Red, foreground_important_titlebar: Color::White, } } From 8c38f17ea711a763dc7b80061aa6e031a992cf50 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Tue, 10 Dec 2024 10:13:34 +0100 Subject: [PATCH 80/89] move over exti screen and help_box --- src/ui/app.rs | 4 ++-- src/ui/help_box.rs | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index 96b1224..2f33796 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -1,6 +1,6 @@ use crate::{ backend::{nc_room::NCRoomInterface, nc_talk::NCBackend}, - config, + config::{self, get_theme}, ui::{ chat_box::ChatBox, chat_selector::ChatSelector, help_box::HelpBox, input_box::InputBox, title_bar::TitleBar, users::Users, @@ -70,7 +70,7 @@ impl<'a, Backend: NCBackend> App<'a, Backend> { f.render_widget( Paragraph::new("To Quit Press 'y', to stay 'n'") .alignment(Alignment::Center) - .style(Style::default().bold().light_magenta()), + .style(get_theme().default_style().bold()), base_layout[1], ); } else if self.current_screen == CurrentScreen::Helping { diff --git a/src/ui/help_box.rs b/src/ui/help_box.rs index 67a14ee..68c3bf3 100644 --- a/src/ui/help_box.rs +++ b/src/ui/help_box.rs @@ -1,3 +1,4 @@ +use crate::config::get_theme; use ratatui::{ prelude::*, widgets::{Block, HighlightSpacing, Row, Table}, @@ -50,10 +51,10 @@ impl Widget for &HelpBox { ], ) .column_spacing(1) - .style(Style::new().white().on_black()) - .header(Row::new(vec!["Key", "Name", "Behavior"]).style(Style::new().bold().blue())) + .style(get_theme().default_style()) + .header(Row::new(vec!["Key", "Name", "Behavior"]).style(get_theme().table_header_style())) .block(Block::default()) - .highlight_style(Style::new().green()) + .highlight_style(get_theme().default_highlight_style()) .highlight_spacing(HighlightSpacing::Never), area, buf, From e89cf939a82a9ba838dde9627808d430ba240de9 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Wed, 11 Dec 2024 10:24:02 +0100 Subject: [PATCH 81/89] update deps --- Cargo.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ce968ff..becf325 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,8 +21,8 @@ exclude = [ ] [dev-dependencies] -mockall = { version = "0.13.0" } -mockall_derive = { version = "0.13.0"} +mockall = { version = "0.13.1" } +mockall_derive = { version = "0.13.1"} [dependencies] reqwest = { version = "0.12", features = ["json"] } @@ -44,7 +44,7 @@ log = "*" log4rs = "1.3" notify-rust = "4.11" itertools = "*" -clap = { version = "4.5.20", features = ["derive"] } +clap = { version = "4.5.23", features = ["derive"] } strum = "0.26" strum_macros = "0.26" textwrap = "0.16.1" @@ -52,13 +52,13 @@ etcetera = "0.8.0" better-panic = "0.3.0" color-eyre = "0.6.3" human-panic = "2.0.2" -libc = "0.2.161" +libc = "0.2.168" strip-ansi-escapes = "0.2.0" -tracing = "0.1.40" +tracing = "0.1.41" cfg-if = "1.0.0" tui-textarea = "0.6.1" async-trait = "0.1.83" -serde_with = "3.9.0" +serde_with = "3.11.0" [lints.clippy] pedantic = "warn" From 08e7bc992e311cdd5e7d8f48db5f8e9503bf48c1 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Wed, 11 Dec 2024 10:28:16 +0100 Subject: [PATCH 82/89] fix clippy --- src/ui/chat_box.rs | 4 ++-- src/ui/chat_selector.rs | 2 +- src/ui/input_box.rs | 4 ++-- src/ui/title_bar.rs | 4 ++-- src/ui/users.rs | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ui/chat_box.rs b/src/ui/chat_box.rs index f47b212..26634dd 100644 --- a/src/ui/chat_box.rs +++ b/src/ui/chat_box.rs @@ -18,8 +18,8 @@ pub struct ChatBox<'a> { state: TableState, } -impl<'a> ChatBox<'a> { - pub fn new() -> ChatBox<'a> { +impl ChatBox<'_> { + pub fn new() -> Self { ChatBox { messages: Vec::new(), current_index: 0, diff --git a/src/ui/chat_selector.rs b/src/ui/chat_selector.rs index 71dc78c..d732e6c 100644 --- a/src/ui/chat_selector.rs +++ b/src/ui/chat_selector.rs @@ -18,7 +18,7 @@ pub struct ChatSelector<'a> { items: Vec>, } -impl<'a> ChatSelector<'a> { +impl ChatSelector<'_> { pub fn new(backend: &impl NCBackend) -> Self { Self { state: TreeState::default(), diff --git a/src/ui/input_box.rs b/src/ui/input_box.rs index a1aa785..61052c6 100644 --- a/src/ui/input_box.rs +++ b/src/ui/input_box.rs @@ -10,8 +10,8 @@ pub struct InputBox<'a> { textarea: TextArea<'a>, } -impl<'a> InputBox<'a> { - pub fn new(initial_message: &str) -> InputBox<'a> { +impl InputBox<'_> { + pub fn new(initial_message: &str) -> Self { let mut textarea = TextArea::new(vec![initial_message.into()]); textarea.set_block( Block::default() diff --git a/src/ui/title_bar.rs b/src/ui/title_bar.rs index 4e87cfc..976c06a 100644 --- a/src/ui/title_bar.rs +++ b/src/ui/title_bar.rs @@ -16,8 +16,8 @@ pub struct TitleBar<'a> { unread_rooms: Text<'a>, } -impl<'a> TitleBar<'a> { - pub fn new(initial_state: CurrentScreen, room: String) -> TitleBar<'a> { +impl TitleBar<'_> { + pub fn new(initial_state: CurrentScreen, room: String) -> Self { TitleBar { room, mode: initial_state.to_string(), diff --git a/src/ui/users.rs b/src/ui/users.rs index 8095022..c8d7ad5 100644 --- a/src/ui/users.rs +++ b/src/ui/users.rs @@ -14,7 +14,7 @@ pub struct Users<'a> { state: TableState, } -impl<'a> Default for Users<'a> { +impl Default for Users<'_> { fn default() -> Self { Self::new() } From 97daaa9281552a80327c7346dc0fae5faa5695da Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Wed, 11 Dec 2024 10:31:03 +0100 Subject: [PATCH 83/89] TEMP: ignore unmaintained upstream lib for log4rs --- Makefile.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.toml b/Makefile.toml index ad1e54b..7f8b09d 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -57,7 +57,7 @@ dependencies = ["audit", "deny"] [tasks.audit] command = "cargo" install_crate = "cargo-audit" -args = ["audit", "-D", "warnings"] +args = ["audit", "-D", "warnings", "--ignore", "RUSTSEC-2024-0388"] [tasks.deny] From c12b7da6802bee566e000ba7087bfc26a85a9c68 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Wed, 11 Dec 2024 10:34:45 +0100 Subject: [PATCH 84/89] allow most recent license changes --- deny.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deny.toml b/deny.toml index bc1db99..597f4d5 100644 --- a/deny.toml +++ b/deny.toml @@ -9,7 +9,7 @@ allow = [ "BSD-3-Clause", "ISC", "MIT", - "Unicode-DFS-2016", + "Unicode-3.0", "GPL-3.0", "Zlib" ] From a7b1fff1e84be3e6d14f6a046ba416e608ccee0c Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Wed, 11 Dec 2024 10:49:31 +0100 Subject: [PATCH 85/89] fix more clippy findings --- src/ui/chat_box.rs | 2 +- src/ui/input_box.rs | 2 +- src/ui/title_bar.rs | 2 +- src/ui/users.rs | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ui/chat_box.rs b/src/ui/chat_box.rs index 26634dd..d316f2c 100644 --- a/src/ui/chat_box.rs +++ b/src/ui/chat_box.rs @@ -149,7 +149,7 @@ impl ChatBox<'_> { } } -impl<'a> StatefulWidget for &ChatBox<'a> { +impl StatefulWidget for &ChatBox<'_> { type State = TableState; fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { // Columns widths are constrained in the same way as Layout... diff --git a/src/ui/input_box.rs b/src/ui/input_box.rs index 61052c6..f3b2857 100644 --- a/src/ui/input_box.rs +++ b/src/ui/input_box.rs @@ -34,7 +34,7 @@ impl<'a> std::ops::Deref for InputBox<'a> { } } -impl<'a> std::ops::DerefMut for InputBox<'a> { +impl std::ops::DerefMut for InputBox<'_> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.textarea } diff --git a/src/ui/title_bar.rs b/src/ui/title_bar.rs index 976c06a..936e8b4 100644 --- a/src/ui/title_bar.rs +++ b/src/ui/title_bar.rs @@ -51,7 +51,7 @@ impl TitleBar<'_> { } } -impl<'a> Widget for &TitleBar<'a> { +impl Widget for &TitleBar<'_> { fn render(self, area: Rect, buf: &mut Buffer) { let (room_title, room_title_style) = if self.unread > 0 { ( diff --git a/src/ui/users.rs b/src/ui/users.rs index c8d7ad5..e48674b 100644 --- a/src/ui/users.rs +++ b/src/ui/users.rs @@ -20,8 +20,8 @@ impl Default for Users<'_> { } } -impl<'a> Users<'a> { - pub fn new() -> Users<'a> { +impl Users<'_> { + pub fn new() -> Self { Users { user_list: vec![], state: TableState::default().with_offset(0).with_selected(0), @@ -61,7 +61,7 @@ impl<'a> Users<'a> { } } -impl<'a> StatefulWidget for &Users<'a> { +impl StatefulWidget for &Users<'_> { type State = TableState; fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { StatefulWidget::render( From 359901813e91d232e355e0788e92479f69c96dd9 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz Date: Wed, 11 Dec 2024 16:04:14 +0100 Subject: [PATCH 86/89] make theme be a toml file and loadable from disk --- src/config/mod.rs | 68 +++++++++++++++++++++++++--------------- src/config/theme.rs | 75 ++++++++++++++++++++++----------------------- test/theme.toml | 47 ++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 64 deletions(-) create mode 100644 test/theme.toml diff --git a/src/config/mod.rs b/src/config/mod.rs index 362feb0..db47b63 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -5,7 +5,8 @@ use color_eyre::eyre::eyre; use data::Data; use etcetera::{app_strategy::Xdg, choose_app_strategy, AppStrategy, AppStrategyArgs}; use log::LevelFilter; -use std::{path::PathBuf, process::exit, sync::OnceLock}; +use serde::de::DeserializeOwned; +use std::{path::Path, path::PathBuf, process::exit, sync::OnceLock}; use theme::Theme; use toml_example::TomlExample; @@ -18,26 +19,7 @@ pub struct Config { strategy: Xdg, } -pub fn init(path_arg: &str) -> Result<(), color_eyre::eyre::Report> { - let strategy = choose_app_strategy(AppStrategyArgs { - top_level_domain: "org".to_string(), - author: "emlix".to_string(), - app_name: "sechat-rs".to_string(), - }) - .unwrap(); - let config_path_base = if path_arg.is_empty() { - strategy.config_dir() - } else { - println!( - "Please consider using the default config file location. {}", - strategy.config_dir().as_os_str().to_str().unwrap() - ); - path_arg.into() - }; - let config_path = config_path_base.join("config.toml"); - - println!("Config Path: {:?}", config_path.as_os_str()); - +pub fn check_config_exists_else_create_new(config_path: &Path) { if !config_path.exists() { println!( "Config files doesn't exist creating default now at {}.", @@ -54,15 +36,18 @@ pub fn init(path_arg: &str) -> Result<(), color_eyre::eyre::Report> { std::fs::create_dir_all(config_path.parent().expect("Config File has no Parent")) .expect("Could not Create Config dir"); } - Data::to_toml_example(config_path.as_os_str().to_str().unwrap()).unwrap(); + T::to_toml_example(config_path.as_os_str().to_str().unwrap()).unwrap(); println!("Please Update the config with sensible values!"); exit(0); } +} + +pub fn read_config_file(config_path: &PathBuf) -> T { let data = match toml::from_str(&std::fs::read_to_string(config_path).unwrap()) { Ok(good_data) => good_data, Err(why) => { println!("Please Update your config {why} "); - let example_config_path = config_path_base.join("config.toml_new"); + let example_config_path = config_path.join("_new"); println!( "Writing example config to {}", example_config_path @@ -70,13 +55,43 @@ pub fn init(path_arg: &str) -> Result<(), color_eyre::eyre::Report> { .to_str() .expect("Failed to make config path into string") ); - Data::to_toml_example(example_config_path.as_os_str().to_str().unwrap()).unwrap(); - return Err(eyre!(why)); + T::to_toml_example(example_config_path.as_os_str().to_str().unwrap()).unwrap(); + exit(-1); } }; + data +} + +pub fn init(path_arg: &str) -> Result<(), color_eyre::eyre::Report> { + let strategy = choose_app_strategy(AppStrategyArgs { + top_level_domain: "org".to_string(), + author: "emlix".to_string(), + app_name: "sechat-rs".to_string(), + }) + .unwrap(); + let config_path_base = if path_arg.is_empty() { + strategy.config_dir() + } else { + println!( + "Please consider using the default config file location. {}", + strategy.config_dir().as_os_str().to_str().unwrap() + ); + path_arg.into() + }; + let config_path = config_path_base.join("config.toml"); + let theme_path = config_path_base.join("theme.toml"); + + println!("Config Path: {:?}", config_path.as_os_str()); + + check_config_exists_else_create_new::(&config_path); + check_config_exists_else_create_new::(&theme_path); + + let data = read_config_file::(&config_path); + let theme_data = read_config_file::(&theme_path); let mut config = Config::default(); config.set_config_data(data); + config.set_theme(theme_data); config.set_strategy(strategy); CONFIG .set(config) @@ -117,6 +132,9 @@ impl Config { pub fn set_config_data(&mut self, data: Data) { self.data = data; } + pub fn set_theme(&mut self, data: Theme) { + self.theme = data; + } pub fn set_strategy(&mut self, strategy: Xdg) { self.strategy = strategy; } diff --git a/src/config/theme.rs b/src/config/theme.rs index fc64f85..31d7783 100644 --- a/src/config/theme.rs +++ b/src/config/theme.rs @@ -2,51 +2,69 @@ use ratatui::style::Color; use ratatui::style::Style; use ratatui::style::Stylize; use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, DisplayFromStr}; -use std::str::FromStr; +use toml_example::TomlExample; -#[serde_as] -#[derive(Serialize, Deserialize, Debug)] +/// Valid Color Values can be: +/// String, e.g. "white", see +/// indexed, e.g. "10", see +/// hex, e.g. "#a03f49", see +/// `toml_example` seems to not handle index and hex well, so the default is pure strings +#[derive(Serialize, Deserialize, Debug, Default, TomlExample)] pub struct Theme { /// Default Background - #[serde_as(as = "DisplayFromStr")] + #[toml_example(default = "black")] pub background: Color, + /// Default Text Colour - #[serde_as(as = "DisplayFromStr")] + #[toml_example(default = "white")] pub foreground: Color, - #[serde_as(as = "DisplayFromStr")] + + /// Background for highlighted lines + #[toml_example(default = "darkgrey")] pub background_highlight: Color, - #[serde_as(as = "DisplayFromStr")] + + /// Foreground for highlighted lines + #[toml_example(default = "white")] pub foreground_highlight: Color, /// background for unread message highlight - #[serde_as(as = "DisplayFromStr")] + #[toml_example(default = "lightgray")] pub background_unread_message: Color, + /// Foreground for unread message highlight - #[serde_as(as = "DisplayFromStr")] + #[toml_example(default = "darkgray")] pub foreground_unread_message: Color, /// Text Colour for Chat and User table Headers - #[serde_as(as = "DisplayFromStr")] + #[toml_example(default = "blue")] pub table_header: Color, /// Text Colour for titlebar contents - #[serde_as(as = "DisplayFromStr")] + #[toml_example(default = "darkgray")] pub foreground_titlebar: Color, + /// Text Colour for titlebar contents - #[serde_as(as = "DisplayFromStr")] + #[toml_example(default = "blue")] pub background_important_titlebar: Color, + /// Text Colour for titlebar contents - #[serde_as(as = "DisplayFromStr")] + #[toml_example(default = "white")] pub foreground_important_titlebar: Color, - #[serde_as(as = "DisplayFromStr")] + /// Foreground for Away Users + #[toml_example(default = "blue")] pub user_away: Color, - #[serde_as(as = "DisplayFromStr")] + + /// Foreground for DND Users + #[toml_example(default = "red")] pub user_dnd: Color, - #[serde_as(as = "DisplayFromStr")] + + /// Foreground for Offline Users + #[toml_example(default = "gray")] pub user_offline: Color, - #[serde_as(as = "DisplayFromStr")] + + /// Foreground for Online Users + #[toml_example(default = "green")] pub user_online: Color, } @@ -97,24 +115,3 @@ impl Theme { .fg(self.foreground_important_titlebar) } } - -impl Default for Theme { - fn default() -> Self { - Self { - background: Color::DarkGray, - foreground: Color::White, - background_highlight: Color::Gray, - foreground_highlight: Color::White, - user_away: Color::Blue, - user_dnd: Color::Red, - user_offline: Color::Gray, - user_online: Color::Green, - background_unread_message: Color::from_str("#6e6a86").unwrap(), - foreground_unread_message: Color::from_str("#e0def4").unwrap(), - table_header: Color::Blue, - foreground_titlebar: Color::White, - background_important_titlebar: Color::Red, - foreground_important_titlebar: Color::White, - } - } -} diff --git a/test/theme.toml b/test/theme.toml new file mode 100644 index 0000000..2d4dddd --- /dev/null +++ b/test/theme.toml @@ -0,0 +1,47 @@ +# Valid Color Values can be: +# String, e.g. "white", see +# indexed, e.g. "10", see +# hex, e.g. "#a03f49", see +# `toml_example` seems to not handle index and hex well, so the default is pure strings +# Default Background +background = "black" + +# Default Text Colour +foreground = "white" + +# Background for highlighted lines +background_highlight = "darkgrey" + +# Foreground for highlighted lines +foreground_highlight = "white" + +# background for unread message highlight +background_unread_message = "lightgray" + +# Foreground for unread message highlight +foreground_unread_message = "darkgray" + +# Text Colour for Chat and User table Headers +table_header = "blue" + +# Text Colour for titlebar contents +foreground_titlebar = "darkgray" + +# Text Colour for titlebar contents +background_important_titlebar = "blue" + +# Text Colour for titlebar contents +foreground_important_titlebar = "white" + +# Foreground for Away Users +user_away = "blue" + +# Foreground for DND Users +user_dnd = "red" + +# Foreground for Offline Users +user_offline = "gray" + +# Foreground for Online Users +user_online = "green" + From a65620c3260563605666f9616666b0dc30fc3cb9 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz Date: Wed, 11 Dec 2024 16:33:00 +0100 Subject: [PATCH 87/89] refactor theme files to follow general structure of repo --- src/config/data/mod.rs | 2 +- src/config/mod.rs | 24 ++++---- src/config/theme/mod.rs | 68 +++++++++++++++++++++++ src/config/{theme.rs => theme/options.rs} | 52 +---------------- 4 files changed, 82 insertions(+), 64 deletions(-) create mode 100644 src/config/theme/mod.rs rename src/config/{theme.rs => theme/options.rs} (58%) diff --git a/src/config/data/mod.rs b/src/config/data/mod.rs index 84349b2..c35523a 100644 --- a/src/config/data/mod.rs +++ b/src/config/data/mod.rs @@ -9,7 +9,7 @@ use toml_example::TomlExample; use ui::Ui; #[derive(Serialize, Deserialize, Debug, Default, TomlExample)] -pub struct Data { +pub struct ConfigOptions { #[toml_example(nesting)] pub general: General, #[toml_example(nesting)] diff --git a/src/config/mod.rs b/src/config/mod.rs index db47b63..c69b3cd 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -2,19 +2,19 @@ mod data; mod theme; use color_eyre::eyre::eyre; -use data::Data; +use data::ConfigOptions; use etcetera::{app_strategy::Xdg, choose_app_strategy, AppStrategy, AppStrategyArgs}; use log::LevelFilter; use serde::de::DeserializeOwned; use std::{path::Path, path::PathBuf, process::exit, sync::OnceLock}; -use theme::Theme; +use theme::{options::ColorPalette, Theme}; use toml_example::TomlExample; static CONFIG: OnceLock = OnceLock::new(); #[derive(Debug)] pub struct Config { - pub data: Data, + pub data: ConfigOptions, pub theme: Theme, strategy: Xdg, } @@ -83,11 +83,11 @@ pub fn init(path_arg: &str) -> Result<(), color_eyre::eyre::Report> { println!("Config Path: {:?}", config_path.as_os_str()); - check_config_exists_else_create_new::(&config_path); - check_config_exists_else_create_new::(&theme_path); + check_config_exists_else_create_new::(&config_path); + check_config_exists_else_create_new::(&theme_path); - let data = read_config_file::(&config_path); - let theme_data = read_config_file::(&theme_path); + let data = read_config_file::(&config_path); + let theme_data = read_config_file::(&theme_path); let mut config = Config::default(); config.set_config_data(data); @@ -116,7 +116,7 @@ pub fn get_theme() -> &'static Theme { impl Default for Config { fn default() -> Self { Self { - data: Data::default(), + data: ConfigOptions::default(), theme: Theme::default(), strategy: choose_app_strategy(AppStrategyArgs { top_level_domain: "org".to_string(), @@ -129,11 +129,11 @@ impl Default for Config { } impl Config { - pub fn set_config_data(&mut self, data: Data) { + pub fn set_config_data(&mut self, data: ConfigOptions) { self.data = data; } - pub fn set_theme(&mut self, data: Theme) { - self.theme = data; + pub fn set_theme(&mut self, data: ColorPalette) { + self.theme.set_theme(data); } pub fn set_strategy(&mut self, strategy: Xdg) { self.strategy = strategy; @@ -261,7 +261,7 @@ mod tests { #[test] fn update_data() { let mut conf = Config::default(); - conf.set_config_data(Data::default()); + conf.set_config_data(ConfigOptions::default()); conf.set_strategy( choose_app_strategy(AppStrategyArgs { top_level_domain: "org".to_string(), diff --git a/src/config/theme/mod.rs b/src/config/theme/mod.rs new file mode 100644 index 0000000..236f101 --- /dev/null +++ b/src/config/theme/mod.rs @@ -0,0 +1,68 @@ +use ratatui::style::Style; +use ratatui::style::Stylize; + +pub mod options; + +#[derive(Debug, Default)] +pub struct Theme { + data: options::ColorPalette, +} + +impl Theme { + pub fn set_theme(&mut self, data: options::ColorPalette) { + self.data = data; + } + pub fn default_style(&self) -> Style { + Style::new() + .fg(self.data.foreground) + .bg(self.data.background) + } + pub fn default_highlight_style(&self) -> Style { + Style::new() + .fg(self.data.foreground_highlight) + .bg(self.data.background_highlight) + } + pub fn user_away_style(&self) -> Style { + Style::new() + .fg(self.data.user_away) + .bg(self.data.background) + } + pub fn user_dnd_style(&self) -> Style { + Style::new().fg(self.data.user_dnd).bg(self.data.background) + } + pub fn user_offline_style(&self) -> Style { + Style::new() + .fg(self.data.user_offline) + .bg(self.data.background) + } + pub fn user_online_style(&self) -> Style { + Style::new() + .fg(self.data.user_online) + .bg(self.data.background) + } + pub fn unread_message_style(&self) -> Style { + Style::new() + .fg(self.data.foreground_unread_message) + .bg(self.data.background_unread_message) + } + + pub fn table_header_style(&self) -> Style { + Style::new() + .bold() + .fg(self.data.table_header) + .bg(self.data.background) + } + + pub fn title_status_style(&self) -> Style { + Style::new() + .bg(self.data.background) + .fg(self.data.foreground_titlebar) + } + + pub fn title_important_style(&self) -> Style { + Style::new() + .bold() + .bg(self.data.background_important_titlebar) + .fg(self.data.foreground_important_titlebar) + } +} diff --git a/src/config/theme.rs b/src/config/theme/options.rs similarity index 58% rename from src/config/theme.rs rename to src/config/theme/options.rs index 31d7783..0e2bb2d 100644 --- a/src/config/theme.rs +++ b/src/config/theme/options.rs @@ -1,6 +1,4 @@ use ratatui::style::Color; -use ratatui::style::Style; -use ratatui::style::Stylize; use serde::{Deserialize, Serialize}; use toml_example::TomlExample; @@ -10,7 +8,7 @@ use toml_example::TomlExample; /// hex, e.g. "#a03f49", see /// `toml_example` seems to not handle index and hex well, so the default is pure strings #[derive(Serialize, Deserialize, Debug, Default, TomlExample)] -pub struct Theme { +pub struct ColorPalette { /// Default Background #[toml_example(default = "black")] pub background: Color, @@ -67,51 +65,3 @@ pub struct Theme { #[toml_example(default = "green")] pub user_online: Color, } - -impl Theme { - pub fn default_style(&self) -> Style { - Style::new().fg(self.foreground).bg(self.background) - } - pub fn default_highlight_style(&self) -> Style { - Style::new() - .fg(self.foreground_highlight) - .bg(self.background_highlight) - } - pub fn user_away_style(&self) -> Style { - Style::new().fg(self.user_away).bg(self.background) - } - pub fn user_dnd_style(&self) -> Style { - Style::new().fg(self.user_dnd).bg(self.background) - } - pub fn user_offline_style(&self) -> Style { - Style::new().fg(self.user_offline).bg(self.background) - } - pub fn user_online_style(&self) -> Style { - Style::new().fg(self.user_online).bg(self.background) - } - pub fn unread_message_style(&self) -> Style { - Style::new() - .fg(self.foreground_unread_message) - .bg(self.background_unread_message) - } - - pub fn table_header_style(&self) -> Style { - Style::new() - .bold() - .fg(self.table_header) - .bg(self.background) - } - - pub fn title_status_style(&self) -> Style { - Style::new() - .bg(self.background) - .fg(self.foreground_titlebar) - } - - pub fn title_important_style(&self) -> Style { - Style::new() - .bold() - .bg(self.background_important_titlebar) - .fg(self.foreground_important_titlebar) - } -} From 0192528bf629aef239d8b0ae64cae11a946a74c8 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz Date: Wed, 11 Dec 2024 16:53:59 +0100 Subject: [PATCH 88/89] add unit tests for theme --- src/config/mod.rs | 14 +++++++++ src/config/theme/mod.rs | 65 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/src/config/mod.rs b/src/config/mod.rs index c69b3cd..b3f1a0c 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -222,6 +222,9 @@ impl Config { #[cfg(test)] mod tests { + use ratatui::style::Color; + use ratatui::style::Style; + use super::*; #[test] @@ -252,6 +255,17 @@ mod tests { assert!(get().get_enable_mouse()); assert!(get().get_enable_paste()); } + + #[test] + fn default_theme() { + // since we cant control the order of the tests we cannot be sure that this returns suchess. + let _ = init("./test/"); + assert_eq!( + get_theme().default_style(), + Style::new().fg(Color::White).bg(Color::Black) + ); + } + #[test] fn init_logging() { let conf = Config::default(); diff --git a/src/config/theme/mod.rs b/src/config/theme/mod.rs index 236f101..cc8eaa8 100644 --- a/src/config/theme/mod.rs +++ b/src/config/theme/mod.rs @@ -66,3 +66,68 @@ impl Theme { .fg(self.data.foreground_important_titlebar) } } + +#[cfg(test)] +mod tests { + use ratatui::style::Color; + use ratatui::style::Style; + + use super::*; + + #[test] + fn default_values() { + let theme = Theme::default(); + assert_eq!( + theme.default_highlight_style(), + Style::new().fg(Color::default()).bg(Color::default()) + ); + assert_eq!( + theme.user_away_style(), + Style::new().fg(Color::default()).bg(Color::default()) + ); + assert_eq!( + theme.user_dnd_style(), + Style::new().fg(Color::default()).bg(Color::default()) + ); + assert_eq!( + theme.user_offline_style(), + Style::new().fg(Color::default()).bg(Color::default()) + ); + assert_eq!( + theme.user_online_style(), + Style::new().fg(Color::default()).bg(Color::default()) + ); + assert_eq!( + theme.unread_message_style(), + Style::new().fg(Color::default()).bg(Color::default()) + ); + assert_eq!( + theme.table_header_style(), + Style::new() + .fg(Color::default()) + .bg(Color::default()) + .bold() + ); + assert_eq!( + theme.title_status_style(), + Style::new().fg(Color::default()).bg(Color::default()) + ); + assert_eq!( + theme.title_important_style(), + Style::new() + .fg(Color::default()) + .bg(Color::default()) + .bold() + ); + } + + #[test] + fn set_data() { + let mut theme = Theme::default(); + theme.set_theme(options::ColorPalette::default()); + assert_eq!( + theme.unread_message_style(), + Style::new().fg(Color::default()).bg(Color::default()) + ); + } +} From 9ebb79beae7c52e762e36ad37d64d54802e1b2eb Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Thu, 12 Dec 2024 10:12:12 +0100 Subject: [PATCH 89/89] remove config test which was a race condition --- src/config/mod.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index b3f1a0c..f12e13b 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -227,11 +227,6 @@ mod tests { use super::*; - #[test] - #[should_panic(expected = "config not initialized")] - fn get_config_before_init() { - get(); - } #[test] #[should_panic( expected = "Could not Create Config dir: Os { code: 13, kind: PermissionDenied, message: \"Permission denied\" }"