diff --git a/src/backend/nc_message.rs b/src/backend/nc_message.rs index b7ef6a5..a4c2225 100644 --- a/src/backend/nc_message.rs +++ b/src/backend/nc_message.rs @@ -1,85 +1,88 @@ use crate::backend::nc_request::NCReqDataMessage; use chrono::prelude::*; +/// `NextCloud` message interface #[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 { + /// return message time stamp as string pub fn get_time_str(&self) -> String { - let time: DateTime = - DateTime::from(DateTime::::from_timestamp(self.data.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() } - pub fn get_name(&self) -> String { - self.data.actorDisplayName.clone() + /// return opponent display name + pub fn get_name(&self) -> &str { + &self.0.actorDisplayName } - pub fn get_message(&self) -> String { - self.message.clone() + /// 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 { - let mut reactions = String::new(); - for (icon, number) in &self.data.reactions { - reactions = reactions + "('" + icon + "' times " + &number.to_string() + "), "; - } - reactions + self.0 + .reactions + .iter() + .map(|(icon, number)| format!("('{icon}' times {}), ", &number.to_string())) + .collect::>() + .join(", ") } + /// get message identifier pub fn get_id(&self) -> i32 { - self.data.id + self.0.id } - pub fn to_data(&self) -> NCReqDataMessage { - self.data.clone() + /// return inner data message + pub fn data(&self) -> &NCReqDataMessage { + &self.0 } + /// return `true` if message is a comment pub fn is_comment(&self) -> bool { - self.data.messageType == "comment" + self.0.messageType == "comment" } + /// return `true` if message is a deleted comment pub fn is_comment_deleted(&self) -> bool { - self.data.messageType == "comment_deleted" + self.0.messageType == "comment_deleted" } + /// return `true` if message is a system message pub fn is_system(&self) -> bool { - self.data.messageType == "system" + self.0.messageType == "system" } + /// return `true` if message is an edited message pub fn is_edit_note(&self) -> bool { - if self.is_system() { - self.data.systemMessage == "message_edited" - } else { - false - } + self.is_system() && self.0.systemMessage == "message_edited" } + /// return `true` if message is a reaction pub fn is_reaction(&self) -> bool { - if self.is_system() { - self.data.systemMessage == "reaction" - } else { - false - } + self.is_system() && self.0.systemMessage == "reaction" } + /// return `true` if message is a command pub fn is_command(&self) -> bool { - self.data.messageType == "command" + self.0.messageType == "command" } + /// return `true` if message has any reactions pub fn has_reactions(&self) -> bool { - !self.data.reactions.is_empty() + !self.0.reactions.is_empty() } } diff --git a/src/backend/nc_notify.rs b/src/backend/nc_notify.rs index e8c878f..8e4396c 100644 --- a/src/backend/nc_notify.rs +++ b/src/backend/nc_notify.rs @@ -1,21 +1,24 @@ 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, - persistent: bool, + timeout: Timeout, silent: bool, } 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: if data.notifications.persistent { + Timeout::Never + } else { + Timeout::Milliseconds(data.notifications.timeout_ms) + }, + silent: data.notifications.silent, } } @@ -25,19 +28,19 @@ 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 { + 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()?; @@ -46,27 +49,22 @@ 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 - .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(()) } -} -impl Default for NCNotify { - fn default() -> Self { - Self::new() + /// return `true` if notification is persistent (has infinite display timeout) + pub fn is_persistent(&self) -> bool { + self.timeout == Timeout::Never } } diff --git a/src/backend/nc_request.rs b/src/backend/nc_request.rs index 73a2861..9769b0a 100644 --- a/src/backend/nc_request.rs +++ b/src/backend/nc_request.rs @@ -2,22 +2,22 @@ #![allow(unused_variables)] #![allow(dead_code)] +use crate::config; 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}; - -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)] @@ -30,7 +30,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, } @@ -245,29 +245,32 @@ 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 mut headers = header::HeaderMap::new(); - headers.insert("OCS-APIRequest", header::HeaderValue::from_static("true")); - headers.insert( - "Accept", - header::HeaderValue::from_static("application/json"), - ); + use std::io::Write; + + 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 = 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(); { 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 = - 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() @@ -288,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?; @@ -313,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?; @@ -349,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?; @@ -391,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() { @@ -463,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".into()), + ("lastKnownMessageId", lastId.to_string()), + ("timeout", "0".into()), + ("includeLastKnown", "0".into()), + ]) } 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() { @@ -538,10 +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 mut name = path.clone(); - name.push(url.replace('/', "_")); - let mut file = File::create(name)?; + 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())?; } diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index a781e7b..a54bd04 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -6,15 +6,8 @@ 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)] +#[derive(Debug, FromPrimitive, PartialEq)] pub enum NCRoomTypes { OneToOne = 1, Group, @@ -30,20 +23,18 @@ 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, } 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?; + ) -> Result<(), Box> { + let response = requester.fetch_chat_initial(token, 200).await?; for message in response { messages.push(message.into()); } @@ -54,7 +45,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,28 +55,25 @@ 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(), ) { - 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", 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(); } - let type_num = room_data.roomtype; let participants = requester .fetch_participants(&room_data.token) .await @@ -94,15 +82,15 @@ 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, }) } - 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 @@ -117,14 +105,14 @@ 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(); } 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(), ) @@ -163,7 +151,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,17 +181,19 @@ impl NCRoom { pub async fn update_if_id_is_newer( &mut self, - messageid: i32, + 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 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?; } @@ -212,7 +202,7 @@ impl NCRoom { "Message Id was older than message stored '{}'! Stored {} Upstream {}", self.to_string(), last_internal_id, - messageid + message_id ); } Ordering::Equal => (), @@ -230,13 +220,12 @@ impl NCRoom { } pub fn write_to_log(&mut self) -> Result<(), std::io::Error> { - let mut data = Vec::::new(); + use std::io::Write; + + let data: Vec<_> = self.messages.iter().map(NCMessage::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) { + let mut file = match std::fs::File::create(path) { Err(why) => { log::warn!( "Couldn't create log file {} for {}: {}", @@ -271,13 +260,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 299cd99..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 { @@ -35,15 +28,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 { @@ -85,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 { @@ -178,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"); @@ -187,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 {}: {}", @@ -251,20 +245,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)> { 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 45250c1..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 @@ -46,14 +46,13 @@ 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() .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(), @@ -126,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/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), 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; 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> {