mirror of
https://github.com/youwen5/iamb.git
synced 2025-06-19 21:29:52 -07:00
Support configuring a user's color and name (#19)
This commit is contained in:
parent
739aab1534
commit
504b520fe1
8 changed files with 343 additions and 98 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1128,7 +1128,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iamb"
|
name = "iamb"
|
||||||
version = "0.0.1"
|
version = "0.0.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
# iamb
|
# iamb
|
||||||
|
|
||||||
|
[](https://github.com/ulyssa/iamb/actions?query=workflow%3ACI+)
|
||||||
|
[](https://crates.io/crates/iamb)
|
||||||
|
[](https://crates.io/crates/iamb)
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
`iamb` is a Matrix client for the terminal that uses Vim keybindings.
|
`iamb` is a Matrix client for the terminal that uses Vim keybindings.
|
||||||
|
|
42
src/base.rs
42
src/base.rs
|
@ -41,7 +41,7 @@ use modalkit::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
message::{user_style, Message, Messages},
|
message::{Message, Messages},
|
||||||
worker::Requester,
|
worker::Requester,
|
||||||
ApplicationSettings,
|
ApplicationSettings,
|
||||||
};
|
};
|
||||||
|
@ -222,24 +222,20 @@ impl RoomInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_typing_spans(&self) -> Spans {
|
fn get_typing_spans(&self, settings: &ApplicationSettings) -> Spans {
|
||||||
let typers = self.get_typers();
|
let typers = self.get_typers();
|
||||||
let n = typers.len();
|
let n = typers.len();
|
||||||
|
|
||||||
match n {
|
match n {
|
||||||
0 => Spans(vec![]),
|
0 => Spans(vec![]),
|
||||||
1 => {
|
1 => {
|
||||||
let user = typers[0].as_str();
|
let user = settings.get_user_span(typers[0].as_ref());
|
||||||
let user = Span::styled(user, user_style(user));
|
|
||||||
|
|
||||||
Spans(vec![user, Span::from(" is typing...")])
|
Spans(vec![user, Span::from(" is typing...")])
|
||||||
},
|
},
|
||||||
2 => {
|
2 => {
|
||||||
let user1 = typers[0].as_str();
|
let user1 = settings.get_user_span(typers[0].as_ref());
|
||||||
let user1 = Span::styled(user1, user_style(user1));
|
let user2 = settings.get_user_span(typers[1].as_ref());
|
||||||
|
|
||||||
let user2 = typers[1].as_str();
|
|
||||||
let user2 = Span::styled(user2, user_style(user2));
|
|
||||||
|
|
||||||
Spans(vec![
|
Spans(vec![
|
||||||
user1,
|
user1,
|
||||||
|
@ -274,7 +270,7 @@ impl RoomInfo {
|
||||||
let top = Rect::new(area.x, area.y, area.width, area.height - 1);
|
let top = Rect::new(area.x, area.y, area.width, area.height - 1);
|
||||||
let bar = Rect::new(area.x, area.y + top.height, area.width, 1);
|
let bar = Rect::new(area.x, area.y + top.height, area.width, 1);
|
||||||
|
|
||||||
Paragraph::new(self.get_typing_spans())
|
Paragraph::new(self.get_typing_spans(settings))
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center)
|
||||||
.render(bar, buf);
|
.render(bar, buf);
|
||||||
|
|
||||||
|
@ -448,11 +444,14 @@ impl ApplicationInfo for IambInfo {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::config::{user_style, user_style_from_color};
|
||||||
use crate::tests::*;
|
use crate::tests::*;
|
||||||
|
use modalkit::tui::style::Color;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_typing_spans() {
|
fn test_typing_spans() {
|
||||||
let mut info = RoomInfo::default();
|
let mut info = RoomInfo::default();
|
||||||
|
let settings = mock_settings();
|
||||||
|
|
||||||
let users0 = vec![];
|
let users0 = vec![];
|
||||||
let users1 = vec![TEST_USER1.clone()];
|
let users1 = vec![TEST_USER1.clone()];
|
||||||
|
@ -473,18 +472,18 @@ pub mod tests {
|
||||||
|
|
||||||
// Nothing set.
|
// Nothing set.
|
||||||
assert_eq!(info.users_typing, None);
|
assert_eq!(info.users_typing, None);
|
||||||
assert_eq!(info.get_typing_spans(), Spans(vec![]));
|
assert_eq!(info.get_typing_spans(&settings), Spans(vec![]));
|
||||||
|
|
||||||
// Empty typing list.
|
// Empty typing list.
|
||||||
info.set_typing(users0);
|
info.set_typing(users0);
|
||||||
assert!(info.users_typing.is_some());
|
assert!(info.users_typing.is_some());
|
||||||
assert_eq!(info.get_typing_spans(), Spans(vec![]));
|
assert_eq!(info.get_typing_spans(&settings), Spans(vec![]));
|
||||||
|
|
||||||
// Single user typing.
|
// Single user typing.
|
||||||
info.set_typing(users1);
|
info.set_typing(users1);
|
||||||
assert!(info.users_typing.is_some());
|
assert!(info.users_typing.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
info.get_typing_spans(),
|
info.get_typing_spans(&settings),
|
||||||
Spans(vec![
|
Spans(vec![
|
||||||
Span::styled("@user1:example.com", user_style("@user1:example.com")),
|
Span::styled("@user1:example.com", user_style("@user1:example.com")),
|
||||||
Span::from(" is typing...")
|
Span::from(" is typing...")
|
||||||
|
@ -495,7 +494,7 @@ pub mod tests {
|
||||||
info.set_typing(users2);
|
info.set_typing(users2);
|
||||||
assert!(info.users_typing.is_some());
|
assert!(info.users_typing.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
info.get_typing_spans(),
|
info.get_typing_spans(&settings),
|
||||||
Spans(vec![
|
Spans(vec![
|
||||||
Span::styled("@user1:example.com", user_style("@user1:example.com")),
|
Span::styled("@user1:example.com", user_style("@user1:example.com")),
|
||||||
Span::raw(" and "),
|
Span::raw(" and "),
|
||||||
|
@ -507,11 +506,22 @@ pub mod tests {
|
||||||
// Four users typing.
|
// Four users typing.
|
||||||
info.set_typing(users4);
|
info.set_typing(users4);
|
||||||
assert!(info.users_typing.is_some());
|
assert!(info.users_typing.is_some());
|
||||||
assert_eq!(info.get_typing_spans(), Spans::from("Several people are typing..."));
|
assert_eq!(info.get_typing_spans(&settings), Spans::from("Several people are typing..."));
|
||||||
|
|
||||||
// Five users typing.
|
// Five users typing.
|
||||||
info.set_typing(users5);
|
info.set_typing(users5);
|
||||||
assert!(info.users_typing.is_some());
|
assert!(info.users_typing.is_some());
|
||||||
assert_eq!(info.get_typing_spans(), Spans::from("Many people are typing..."));
|
assert_eq!(info.get_typing_spans(&settings), Spans::from("Many people are typing..."));
|
||||||
|
|
||||||
|
// Test that USER5 gets rendered using the configured color and name.
|
||||||
|
info.set_typing(vec![TEST_USER5.clone()]);
|
||||||
|
assert!(info.users_typing.is_some());
|
||||||
|
assert_eq!(
|
||||||
|
info.get_typing_spans(&settings),
|
||||||
|
Spans(vec![
|
||||||
|
Span::styled("USER 5", user_style_from_color(Color::Black)),
|
||||||
|
Span::from(" is typing...")
|
||||||
|
])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
209
src/config.rs
209
src/config.rs
|
@ -1,14 +1,22 @@
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process;
|
use std::process;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use matrix_sdk::ruma::OwnedUserId;
|
use matrix_sdk::ruma::{OwnedUserId, UserId};
|
||||||
use serde::Deserialize;
|
use serde::{de::Error as SerdeError, de::Visitor, Deserialize, Deserializer};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
use modalkit::tui::{
|
||||||
|
style::{Color, Modifier as StyleModifier, Style},
|
||||||
|
text::Span,
|
||||||
|
};
|
||||||
|
|
||||||
macro_rules! usage {
|
macro_rules! usage {
|
||||||
( $($args: tt)* ) => {
|
( $($args: tt)* ) => {
|
||||||
println!($($args)*);
|
println!($($args)*);
|
||||||
|
@ -16,6 +24,38 @@ macro_rules! usage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const COLORS: [Color; 13] = [
|
||||||
|
Color::Blue,
|
||||||
|
Color::Cyan,
|
||||||
|
Color::Green,
|
||||||
|
Color::LightBlue,
|
||||||
|
Color::LightGreen,
|
||||||
|
Color::LightCyan,
|
||||||
|
Color::LightMagenta,
|
||||||
|
Color::LightRed,
|
||||||
|
Color::LightYellow,
|
||||||
|
Color::Magenta,
|
||||||
|
Color::Red,
|
||||||
|
Color::Reset,
|
||||||
|
Color::Yellow,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn user_color(user: &str) -> Color {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
user.hash(&mut hasher);
|
||||||
|
let color = hasher.finish() as usize % COLORS.len();
|
||||||
|
|
||||||
|
COLORS[color]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn user_style_from_color(color: Color) -> Style {
|
||||||
|
Style::default().fg(color).add_modifier(StyleModifier::BOLD)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn user_style(user: &str) -> Style {
|
||||||
|
user_style_from_color(user_color(user))
|
||||||
|
}
|
||||||
|
|
||||||
fn is_profile_char(c: char) -> bool {
|
fn is_profile_char(c: char) -> bool {
|
||||||
c.is_ascii_alphanumeric() || c == '.' || c == '-'
|
c.is_ascii_alphanumeric() || c == '.' || c == '-'
|
||||||
}
|
}
|
||||||
|
@ -69,16 +109,88 @@ pub enum ConfigError {
|
||||||
Invalid(#[from] serde_json::Error),
|
Invalid(#[from] serde_json::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct UserColor(pub Color);
|
||||||
|
pub struct UserColorVisitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for UserColorVisitor {
|
||||||
|
type Value = UserColor;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("a valid color")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: SerdeError,
|
||||||
|
{
|
||||||
|
match value {
|
||||||
|
"none" => Ok(UserColor(Color::Reset)),
|
||||||
|
"red" => Ok(UserColor(Color::Red)),
|
||||||
|
"black" => Ok(UserColor(Color::Black)),
|
||||||
|
"green" => Ok(UserColor(Color::Green)),
|
||||||
|
"yellow" => Ok(UserColor(Color::Yellow)),
|
||||||
|
"blue" => Ok(UserColor(Color::Blue)),
|
||||||
|
"magenta" => Ok(UserColor(Color::Magenta)),
|
||||||
|
"cyan" => Ok(UserColor(Color::Cyan)),
|
||||||
|
"gray" => Ok(UserColor(Color::Gray)),
|
||||||
|
"dark-gray" => Ok(UserColor(Color::DarkGray)),
|
||||||
|
"light-red" => Ok(UserColor(Color::LightRed)),
|
||||||
|
"light-green" => Ok(UserColor(Color::LightGreen)),
|
||||||
|
"light-yellow" => Ok(UserColor(Color::LightYellow)),
|
||||||
|
"light-blue" => Ok(UserColor(Color::LightBlue)),
|
||||||
|
"light-magenta" => Ok(UserColor(Color::LightMagenta)),
|
||||||
|
"light-cyan" => Ok(UserColor(Color::LightCyan)),
|
||||||
|
"white" => Ok(UserColor(Color::White)),
|
||||||
|
_ => Err(E::custom("Could not parse color")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for UserColor {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
deserializer.deserialize_str(UserColorVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||||
|
pub struct UserDisplayTunables {
|
||||||
|
pub color: Option<UserColor>,
|
||||||
|
pub name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type UserOverrides = HashMap<OwnedUserId, UserDisplayTunables>;
|
||||||
|
|
||||||
|
fn merge_users(a: Option<UserOverrides>, b: Option<UserOverrides>) -> Option<UserOverrides> {
|
||||||
|
match (a, b) {
|
||||||
|
(Some(a), None) => Some(a),
|
||||||
|
(None, Some(b)) => Some(b),
|
||||||
|
(Some(mut a), Some(b)) => {
|
||||||
|
for (k, v) in b {
|
||||||
|
a.insert(k, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(a)
|
||||||
|
},
|
||||||
|
(None, None) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TunableValues {
|
pub struct TunableValues {
|
||||||
pub typing_notice: bool,
|
pub typing_notice: bool,
|
||||||
pub typing_notice_display: bool,
|
pub typing_notice_display: bool,
|
||||||
|
pub users: UserOverrides,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Deserialize)]
|
#[derive(Clone, Default, Deserialize)]
|
||||||
pub struct Tunables {
|
pub struct Tunables {
|
||||||
pub typing_notice: Option<bool>,
|
pub typing_notice: Option<bool>,
|
||||||
pub typing_notice_display: Option<bool>,
|
pub typing_notice_display: Option<bool>,
|
||||||
|
pub users: Option<UserOverrides>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tunables {
|
impl Tunables {
|
||||||
|
@ -86,6 +198,7 @@ impl Tunables {
|
||||||
Tunables {
|
Tunables {
|
||||||
typing_notice: self.typing_notice.or(other.typing_notice),
|
typing_notice: self.typing_notice.or(other.typing_notice),
|
||||||
typing_notice_display: self.typing_notice_display.or(other.typing_notice_display),
|
typing_notice_display: self.typing_notice_display.or(other.typing_notice_display),
|
||||||
|
users: merge_users(self.users, other.users),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,6 +206,7 @@ impl Tunables {
|
||||||
TunableValues {
|
TunableValues {
|
||||||
typing_notice: self.typing_notice.unwrap_or(true),
|
typing_notice: self.typing_notice.unwrap_or(true),
|
||||||
typing_notice_display: self.typing_notice.unwrap_or(true),
|
typing_notice_display: self.typing_notice.unwrap_or(true),
|
||||||
|
users: self.users.unwrap_or_default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -255,11 +369,32 @@ impl ApplicationSettings {
|
||||||
|
|
||||||
Ok(settings)
|
Ok(settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_user_span<'a>(&self, user_id: &'a UserId) -> Span<'a> {
|
||||||
|
if let Some(user) = self.tunables.users.get(user_id) {
|
||||||
|
let color = if let Some(UserColor(c)) = user.color {
|
||||||
|
c
|
||||||
|
} else {
|
||||||
|
user_color(user_id.as_str())
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = user_style_from_color(color);
|
||||||
|
|
||||||
|
if let Some(name) = &user.name {
|
||||||
|
Span::styled(name.clone(), style)
|
||||||
|
} else {
|
||||||
|
Span::styled(user_id.as_str(), style)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Span::styled(user_id.as_str(), user_style(user_id.as_str()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use matrix_sdk::ruma::user_id;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_profile_name_invalid() {
|
fn test_profile_name_invalid() {
|
||||||
|
@ -283,4 +418,74 @@ mod tests {
|
||||||
assert_eq!(validate_profile_name("a.b-c"), true);
|
assert_eq!(validate_profile_name("a.b-c"), true);
|
||||||
assert_eq!(validate_profile_name("a.B-c"), true);
|
assert_eq!(validate_profile_name("a.B-c"), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merge_users() {
|
||||||
|
let a = None;
|
||||||
|
let b = vec![(user_id!("@a:b.c").to_owned(), UserDisplayTunables {
|
||||||
|
color: Some(UserColor(Color::Red)),
|
||||||
|
name: Some("Hello".into()),
|
||||||
|
})]
|
||||||
|
.into_iter()
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
let c = vec![(user_id!("@a:b.c").to_owned(), UserDisplayTunables {
|
||||||
|
color: Some(UserColor(Color::Green)),
|
||||||
|
name: Some("World".into()),
|
||||||
|
})]
|
||||||
|
.into_iter()
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
|
let res = merge_users(a.clone(), a.clone());
|
||||||
|
assert_eq!(res, None);
|
||||||
|
|
||||||
|
let res = merge_users(a.clone(), Some(b.clone()));
|
||||||
|
assert_eq!(res, Some(b.clone()));
|
||||||
|
|
||||||
|
let res = merge_users(Some(b.clone()), a.clone());
|
||||||
|
assert_eq!(res, Some(b.clone()));
|
||||||
|
|
||||||
|
let res = merge_users(Some(b.clone()), Some(b.clone()));
|
||||||
|
assert_eq!(res, Some(b.clone()));
|
||||||
|
|
||||||
|
let res = merge_users(Some(b.clone()), Some(c.clone()));
|
||||||
|
assert_eq!(res, Some(c.clone()));
|
||||||
|
|
||||||
|
let res = merge_users(Some(c.clone()), Some(b.clone()));
|
||||||
|
assert_eq!(res, Some(b.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_tunables() {
|
||||||
|
let res: Tunables = serde_json::from_str("{}").unwrap();
|
||||||
|
assert_eq!(res.typing_notice, None);
|
||||||
|
assert_eq!(res.typing_notice_display, None);
|
||||||
|
assert_eq!(res.users, None);
|
||||||
|
|
||||||
|
let res: Tunables = serde_json::from_str("{\"typing_notice\": true}").unwrap();
|
||||||
|
assert_eq!(res.typing_notice, Some(true));
|
||||||
|
assert_eq!(res.typing_notice_display, None);
|
||||||
|
assert_eq!(res.users, None);
|
||||||
|
|
||||||
|
let res: Tunables = serde_json::from_str("{\"typing_notice\": false}").unwrap();
|
||||||
|
assert_eq!(res.typing_notice, Some(false));
|
||||||
|
assert_eq!(res.typing_notice_display, None);
|
||||||
|
assert_eq!(res.users, None);
|
||||||
|
|
||||||
|
let res: Tunables = serde_json::from_str("{\"users\": {}}").unwrap();
|
||||||
|
assert_eq!(res.typing_notice, None);
|
||||||
|
assert_eq!(res.typing_notice_display, None);
|
||||||
|
assert_eq!(res.users, Some(HashMap::new()));
|
||||||
|
|
||||||
|
let res: Tunables = serde_json::from_str(
|
||||||
|
"{\"users\": {\"@a:b.c\": {\"color\": \"black\", \"name\": \"Tim\"}}}",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(res.typing_notice, None);
|
||||||
|
assert_eq!(res.typing_notice_display, None);
|
||||||
|
let users = vec![(user_id!("@a:b.c").to_owned(), UserDisplayTunables {
|
||||||
|
color: Some(UserColor(Color::Black)),
|
||||||
|
name: Some("Tim".into()),
|
||||||
|
})];
|
||||||
|
assert_eq!(res.users, Some(users.into_iter().collect()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,35 +22,22 @@ use matrix_sdk::ruma::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use modalkit::tui::{
|
use modalkit::tui::{
|
||||||
style::{Color, Modifier as StyleModifier, Style},
|
style::{Modifier as StyleModifier, Style},
|
||||||
text::{Span, Spans, Text},
|
text::{Span, Spans, Text},
|
||||||
};
|
};
|
||||||
|
|
||||||
use modalkit::editing::{base::ViewportContext, cursor::Cursor};
|
use modalkit::editing::{base::ViewportContext, cursor::Cursor};
|
||||||
|
|
||||||
use crate::base::{IambResult, RoomInfo};
|
use crate::{
|
||||||
|
base::{IambResult, RoomInfo},
|
||||||
|
config::ApplicationSettings,
|
||||||
|
};
|
||||||
|
|
||||||
pub type MessageEvent = MessageLikeEvent<RoomMessageEventContent>;
|
pub type MessageEvent = MessageLikeEvent<RoomMessageEventContent>;
|
||||||
pub type MessageFetchResult = IambResult<(Option<String>, Vec<MessageEvent>)>;
|
pub type MessageFetchResult = IambResult<(Option<String>, Vec<MessageEvent>)>;
|
||||||
pub type MessageKey = (MessageTimeStamp, OwnedEventId);
|
pub type MessageKey = (MessageTimeStamp, OwnedEventId);
|
||||||
pub type Messages = BTreeMap<MessageKey, Message>;
|
pub type Messages = BTreeMap<MessageKey, Message>;
|
||||||
|
|
||||||
const COLORS: [Color; 13] = [
|
|
||||||
Color::Blue,
|
|
||||||
Color::Cyan,
|
|
||||||
Color::Green,
|
|
||||||
Color::LightBlue,
|
|
||||||
Color::LightGreen,
|
|
||||||
Color::LightCyan,
|
|
||||||
Color::LightMagenta,
|
|
||||||
Color::LightRed,
|
|
||||||
Color::LightYellow,
|
|
||||||
Color::Magenta,
|
|
||||||
Color::Red,
|
|
||||||
Color::Reset,
|
|
||||||
Color::Yellow,
|
|
||||||
];
|
|
||||||
|
|
||||||
const USER_GUTTER: usize = 30;
|
const USER_GUTTER: usize = 30;
|
||||||
const TIME_GUTTER: usize = 12;
|
const TIME_GUTTER: usize = 12;
|
||||||
const MIN_MSG_LEN: usize = 30;
|
const MIN_MSG_LEN: usize = 30;
|
||||||
|
@ -66,18 +53,6 @@ const USER_GUTTER_EMPTY_SPAN: Span<'static> = Span {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) fn user_color(user: &str) -> Color {
|
|
||||||
let mut hasher = DefaultHasher::new();
|
|
||||||
user.hash(&mut hasher);
|
|
||||||
let color = hasher.finish() as usize % COLORS.len();
|
|
||||||
|
|
||||||
COLORS[color]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn user_style(user: &str) -> Style {
|
|
||||||
Style::default().fg(user_color(user)).add_modifier(StyleModifier::BOLD)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WrappedLinesIterator<'a> {
|
struct WrappedLinesIterator<'a> {
|
||||||
iter: Lines<'a>,
|
iter: Lines<'a>,
|
||||||
curr: Option<&'a str>,
|
curr: Option<&'a str>,
|
||||||
|
@ -390,7 +365,13 @@ impl Message {
|
||||||
Message { content, sender, timestamp }
|
Message { content, sender, timestamp }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show(&self, selected: bool, vwctx: &ViewportContext<MessageCursor>) -> Text {
|
pub fn show(
|
||||||
|
&self,
|
||||||
|
prev: Option<&Message>,
|
||||||
|
selected: bool,
|
||||||
|
vwctx: &ViewportContext<MessageCursor>,
|
||||||
|
settings: &ApplicationSettings,
|
||||||
|
) -> Text {
|
||||||
let width = vwctx.get_width();
|
let width = vwctx.get_width();
|
||||||
let msg = self.as_ref();
|
let msg = self.as_ref();
|
||||||
|
|
||||||
|
@ -414,7 +395,7 @@ impl Message {
|
||||||
let trailing = Span::styled(space(lw.saturating_sub(w)), style);
|
let trailing = Span::styled(space(lw.saturating_sub(w)), style);
|
||||||
|
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
let user = self.show_sender(true);
|
let user = self.show_sender(prev, true, settings);
|
||||||
|
|
||||||
if let Some(time) = self.timestamp.show() {
|
if let Some(time) = self.timestamp.show() {
|
||||||
lines.push(Spans(vec![user, line, trailing, time]))
|
lines.push(Spans(vec![user, line, trailing, time]))
|
||||||
|
@ -435,7 +416,7 @@ impl Message {
|
||||||
let trailing = Span::styled(space(lw.saturating_sub(w)), style);
|
let trailing = Span::styled(space(lw.saturating_sub(w)), style);
|
||||||
|
|
||||||
let prefix = if i == 0 {
|
let prefix = if i == 0 {
|
||||||
self.show_sender(true)
|
self.show_sender(prev, true, settings)
|
||||||
} else {
|
} else {
|
||||||
USER_GUTTER_EMPTY_SPAN
|
USER_GUTTER_EMPTY_SPAN
|
||||||
};
|
};
|
||||||
|
@ -443,7 +424,7 @@ impl Message {
|
||||||
lines.push(Spans(vec![prefix, line, trailing]))
|
lines.push(Spans(vec![prefix, line, trailing]))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
lines.push(Spans::from(self.show_sender(false)));
|
lines.push(Spans::from(self.show_sender(prev, false, settings)));
|
||||||
|
|
||||||
for (line, _) in wrap(msg, width.saturating_sub(2)) {
|
for (line, _) in wrap(msg, width.saturating_sub(2)) {
|
||||||
let line = format!(" {}", line);
|
let line = format!(" {}", line);
|
||||||
|
@ -456,14 +437,26 @@ impl Message {
|
||||||
return Text { lines };
|
return Text { lines };
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_sender(&self, align_right: bool) -> Span {
|
fn show_sender(
|
||||||
let sender = self.sender.to_string();
|
&self,
|
||||||
let style = user_style(sender.as_str());
|
prev: Option<&Message>,
|
||||||
|
align_right: bool,
|
||||||
|
settings: &ApplicationSettings,
|
||||||
|
) -> Span {
|
||||||
|
let user = if matches!(prev, Some(prev) if self.sender == prev.sender) {
|
||||||
|
USER_GUTTER_EMPTY_SPAN
|
||||||
|
} else {
|
||||||
|
settings.get_user_span(self.sender.as_ref())
|
||||||
|
};
|
||||||
|
|
||||||
|
let Span { content, style } = user;
|
||||||
|
let stop = content.len().min(28);
|
||||||
|
let s = &content[..stop];
|
||||||
|
|
||||||
let sender = if align_right {
|
let sender = if align_right {
|
||||||
format!("{: >width$} ", sender, width = 28)
|
format!("{: >width$} ", s, width = 28)
|
||||||
} else {
|
} else {
|
||||||
format!("{: <width$} ", sender, width = 28)
|
format!("{: <width$} ", s, width = 28)
|
||||||
};
|
};
|
||||||
|
|
||||||
Span::styled(sender, style)
|
Span::styled(sender, style)
|
||||||
|
|
40
src/tests.rs
40
src/tests.rs
|
@ -1,4 +1,6 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::mpsc::sync_channel;
|
||||||
|
|
||||||
use matrix_sdk::ruma::{
|
use matrix_sdk::ruma::{
|
||||||
event_id,
|
event_id,
|
||||||
|
@ -12,15 +14,20 @@ use matrix_sdk::ruma::{
|
||||||
UInt,
|
UInt,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::mpsc::sync_channel;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
use modalkit::tui::style::Color;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
base::{ChatStore, ProgramStore, RoomFetchStatus, RoomInfo},
|
base::{ChatStore, ProgramStore, RoomFetchStatus, RoomInfo},
|
||||||
config::{ApplicationSettings, DirectoryValues, ProfileConfig, TunableValues},
|
config::{
|
||||||
|
ApplicationSettings,
|
||||||
|
DirectoryValues,
|
||||||
|
ProfileConfig,
|
||||||
|
TunableValues,
|
||||||
|
UserColor,
|
||||||
|
UserDisplayTunables,
|
||||||
|
},
|
||||||
message::{
|
message::{
|
||||||
Message,
|
Message,
|
||||||
MessageContent,
|
MessageContent,
|
||||||
|
@ -35,9 +42,9 @@ lazy_static! {
|
||||||
pub static ref TEST_ROOM1_ID: OwnedRoomId = RoomId::new(server_name!("example.com")).to_owned();
|
pub static ref TEST_ROOM1_ID: OwnedRoomId = RoomId::new(server_name!("example.com")).to_owned();
|
||||||
pub static ref TEST_USER1: OwnedUserId = user_id!("@user1:example.com").to_owned();
|
pub static ref TEST_USER1: OwnedUserId = user_id!("@user1:example.com").to_owned();
|
||||||
pub static ref TEST_USER2: OwnedUserId = user_id!("@user2:example.com").to_owned();
|
pub static ref TEST_USER2: OwnedUserId = user_id!("@user2:example.com").to_owned();
|
||||||
pub static ref TEST_USER3: OwnedUserId = user_id!("@user2:example.com").to_owned();
|
pub static ref TEST_USER3: OwnedUserId = user_id!("@user3:example.com").to_owned();
|
||||||
pub static ref TEST_USER4: OwnedUserId = user_id!("@user2:example.com").to_owned();
|
pub static ref TEST_USER4: OwnedUserId = user_id!("@user4:example.com").to_owned();
|
||||||
pub static ref TEST_USER5: OwnedUserId = user_id!("@user2:example.com").to_owned();
|
pub static ref TEST_USER5: OwnedUserId = user_id!("@user5:example.com").to_owned();
|
||||||
pub static ref MSG1_KEY: MessageKey = (LocalEcho, EventId::new(server_name!("example.com")));
|
pub static ref MSG1_KEY: MessageKey = (LocalEcho, EventId::new(server_name!("example.com")));
|
||||||
pub static ref MSG2_KEY: MessageKey =
|
pub static ref MSG2_KEY: MessageKey =
|
||||||
(OriginServer(UInt::new(1).unwrap()), EventId::new(server_name!("example.com")));
|
(OriginServer(UInt::new(1).unwrap()), EventId::new(server_name!("example.com")));
|
||||||
|
@ -118,6 +125,19 @@ pub fn mock_dirs() -> DirectoryValues {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mock_tunables() -> TunableValues {
|
||||||
|
TunableValues {
|
||||||
|
typing_notice: true,
|
||||||
|
typing_notice_display: true,
|
||||||
|
users: vec![(TEST_USER5.clone(), UserDisplayTunables {
|
||||||
|
color: Some(UserColor(Color::Black)),
|
||||||
|
name: Some("USER 5".into()),
|
||||||
|
})]
|
||||||
|
.into_iter()
|
||||||
|
.collect::<HashMap<_, _>>(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn mock_settings() -> ApplicationSettings {
|
pub fn mock_settings() -> ApplicationSettings {
|
||||||
ApplicationSettings {
|
ApplicationSettings {
|
||||||
matrix_dir: PathBuf::new(),
|
matrix_dir: PathBuf::new(),
|
||||||
|
@ -129,7 +149,7 @@ pub fn mock_settings() -> ApplicationSettings {
|
||||||
settings: None,
|
settings: None,
|
||||||
dirs: None,
|
dirs: None,
|
||||||
},
|
},
|
||||||
tunables: TunableValues { typing_notice: true, typing_notice_display: true },
|
tunables: mock_tunables(),
|
||||||
dirs: mock_dirs(),
|
dirs: mock_dirs(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,8 +51,7 @@ use modalkit::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::base::{
|
||||||
base::{
|
|
||||||
ChatStore,
|
ChatStore,
|
||||||
IambBufferId,
|
IambBufferId,
|
||||||
IambId,
|
IambId,
|
||||||
|
@ -62,8 +61,6 @@ use crate::{
|
||||||
ProgramContext,
|
ProgramContext,
|
||||||
ProgramStore,
|
ProgramStore,
|
||||||
RoomAction,
|
RoomAction,
|
||||||
},
|
|
||||||
message::user_style,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::{room::RoomState, welcome::WelcomeState};
|
use self::{room::RoomState, welcome::WelcomeState};
|
||||||
|
@ -845,15 +842,18 @@ impl ToString for MemberItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ListItem<IambInfo> for MemberItem {
|
impl ListItem<IambInfo> for MemberItem {
|
||||||
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text {
|
fn show(
|
||||||
let mut style = user_style(self.member.user_id().as_str());
|
&self,
|
||||||
|
selected: bool,
|
||||||
|
_: &ViewportContext<ListCursor>,
|
||||||
|
store: &mut ProgramStore,
|
||||||
|
) -> Text {
|
||||||
|
let mut user = store.application.settings.get_user_span(self.member.user_id());
|
||||||
|
|
||||||
if selected {
|
if selected {
|
||||||
style = style.add_modifier(StyleModifier::REVERSED);
|
user.style = user.style.add_modifier(StyleModifier::REVERSED);
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = Span::styled(self.to_string(), style);
|
|
||||||
|
|
||||||
let state = match self.member.membership() {
|
let state = match self.member.membership() {
|
||||||
MembershipState::Ban => Span::raw(" (banned)").into(),
|
MembershipState::Ban => Span::raw(" (banned)").into(),
|
||||||
MembershipState::Invite => Span::raw(" (invited)").into(),
|
MembershipState::Invite => Span::raw(" (invited)").into(),
|
||||||
|
|
|
@ -61,6 +61,7 @@ use modalkit::editing::{
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
base::{IambBufferId, IambInfo, ProgramContext, ProgramStore, RoomFocus, RoomInfo},
|
base::{IambBufferId, IambInfo, ProgramContext, ProgramStore, RoomFocus, RoomInfo},
|
||||||
|
config::ApplicationSettings,
|
||||||
message::{Message, MessageCursor, MessageKey},
|
message::{Message, MessageCursor, MessageKey},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -148,7 +149,13 @@ impl ScrollbackState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scrollview(&mut self, idx: MessageKey, pos: MovePosition, info: &RoomInfo) {
|
fn scrollview(
|
||||||
|
&mut self,
|
||||||
|
idx: MessageKey,
|
||||||
|
pos: MovePosition,
|
||||||
|
info: &RoomInfo,
|
||||||
|
settings: &ApplicationSettings,
|
||||||
|
) {
|
||||||
let selidx = if let Some(key) = self.cursor.to_key(info) {
|
let selidx = if let Some(key) = self.cursor.to_key(info) {
|
||||||
key
|
key
|
||||||
} else {
|
} else {
|
||||||
|
@ -165,7 +172,7 @@ impl ScrollbackState {
|
||||||
|
|
||||||
for (key, item) in info.messages.range(..=&idx).rev() {
|
for (key, item) in info.messages.range(..=&idx).rev() {
|
||||||
let sel = selidx == key;
|
let sel = selidx == key;
|
||||||
let len = item.show(sel, &self.viewctx).lines.len();
|
let len = item.show(None, sel, &self.viewctx, settings).lines.len();
|
||||||
|
|
||||||
if key == &idx {
|
if key == &idx {
|
||||||
lines += len / 2;
|
lines += len / 2;
|
||||||
|
@ -187,7 +194,7 @@ impl ScrollbackState {
|
||||||
|
|
||||||
for (key, item) in info.messages.range(..=&idx).rev() {
|
for (key, item) in info.messages.range(..=&idx).rev() {
|
||||||
let sel = key == selidx;
|
let sel = key == selidx;
|
||||||
let len = item.show(sel, &self.viewctx).lines.len();
|
let len = item.show(None, sel, &self.viewctx, settings).lines.len();
|
||||||
|
|
||||||
lines += len;
|
lines += len;
|
||||||
|
|
||||||
|
@ -202,7 +209,7 @@ impl ScrollbackState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shift_cursor(&mut self, info: &RoomInfo) {
|
fn shift_cursor(&mut self, info: &RoomInfo, settings: &ApplicationSettings) {
|
||||||
let last_key = if let Some(k) = info.messages.last_key_value() {
|
let last_key = if let Some(k) = info.messages.last_key_value() {
|
||||||
k.0
|
k.0
|
||||||
} else {
|
} else {
|
||||||
|
@ -227,7 +234,7 @@ impl ScrollbackState {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
lines += item.show(false, &self.viewctx).height().max(1);
|
lines += item.show(None, false, &self.viewctx, settings).height().max(1);
|
||||||
|
|
||||||
if lines >= self.viewctx.get_height() {
|
if lines >= self.viewctx.get_height() {
|
||||||
// We've reached the end of the viewport; move cursor into it.
|
// We've reached the end of the viewport; move cursor into it.
|
||||||
|
@ -930,7 +937,8 @@ impl ScrollActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
|
||||||
ctx: &ProgramContext,
|
ctx: &ProgramContext,
|
||||||
store: &mut ProgramStore,
|
store: &mut ProgramStore,
|
||||||
) -> EditResult<EditInfo, IambInfo> {
|
) -> EditResult<EditInfo, IambInfo> {
|
||||||
let info = store.application.get_room_info(self.room_id.clone());
|
let info = store.application.rooms.entry(self.room_id.clone()).or_default();
|
||||||
|
let settings = &store.application.settings;
|
||||||
let mut corner = self.viewctx.corner.clone();
|
let mut corner = self.viewctx.corner.clone();
|
||||||
|
|
||||||
let last_key = if let Some(k) = info.messages.last_key_value() {
|
let last_key = if let Some(k) = info.messages.last_key_value() {
|
||||||
|
@ -956,7 +964,7 @@ impl ScrollActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
|
||||||
|
|
||||||
for (key, item) in info.messages.range(..=&corner_key).rev() {
|
for (key, item) in info.messages.range(..=&corner_key).rev() {
|
||||||
let sel = key == cursor_key;
|
let sel = key == cursor_key;
|
||||||
let txt = item.show(sel, &self.viewctx);
|
let txt = item.show(None, sel, &self.viewctx, settings);
|
||||||
let len = txt.height().max(1);
|
let len = txt.height().max(1);
|
||||||
let max = len.saturating_sub(1);
|
let max = len.saturating_sub(1);
|
||||||
|
|
||||||
|
@ -982,7 +990,7 @@ impl ScrollActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
|
||||||
MoveDir2D::Down => {
|
MoveDir2D::Down => {
|
||||||
for (key, item) in info.messages.range(&corner_key..) {
|
for (key, item) in info.messages.range(&corner_key..) {
|
||||||
let sel = key == cursor_key;
|
let sel = key == cursor_key;
|
||||||
let txt = item.show(sel, &self.viewctx);
|
let txt = item.show(None, sel, &self.viewctx, settings);
|
||||||
let len = txt.height().max(1);
|
let len = txt.height().max(1);
|
||||||
let max = len.saturating_sub(1);
|
let max = len.saturating_sub(1);
|
||||||
|
|
||||||
|
@ -1018,7 +1026,7 @@ impl ScrollActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.viewctx.corner = corner;
|
self.viewctx.corner = corner;
|
||||||
self.shift_cursor(info);
|
self.shift_cursor(info, settings);
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
@ -1038,10 +1046,11 @@ impl ScrollActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
|
||||||
Err(err)
|
Err(err)
|
||||||
},
|
},
|
||||||
Axis::Vertical => {
|
Axis::Vertical => {
|
||||||
let info = store.application.get_room_info(self.room_id.clone());
|
let info = store.application.rooms.entry(self.room_id.clone()).or_default();
|
||||||
|
let settings = &store.application.settings;
|
||||||
|
|
||||||
if let Some(key) = self.cursor.to_key(info).cloned() {
|
if let Some(key) = self.cursor.to_key(info).cloned() {
|
||||||
self.scrollview(key, pos, info);
|
self.scrollview(key, pos, info, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
@ -1126,6 +1135,7 @@ impl<'a> StatefulWidget for Scrollback<'a> {
|
||||||
|
|
||||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||||
let info = self.store.application.rooms.entry(state.room_id.clone()).or_default();
|
let info = self.store.application.rooms.entry(state.room_id.clone()).or_default();
|
||||||
|
let settings = &self.store.application.settings;
|
||||||
let area = info.render_typing(area, buf, &self.store.application.settings);
|
let area = info.render_typing(area, buf, &self.store.application.settings);
|
||||||
|
|
||||||
state.set_term_info(area);
|
state.set_term_info(area);
|
||||||
|
@ -1157,10 +1167,13 @@ impl<'a> StatefulWidget for Scrollback<'a> {
|
||||||
|
|
||||||
let mut lines = vec![];
|
let mut lines = vec![];
|
||||||
let mut sawit = false;
|
let mut sawit = false;
|
||||||
|
let mut prev = None;
|
||||||
|
|
||||||
for (key, item) in info.messages.range(&corner_key..) {
|
for (key, item) in info.messages.range(&corner_key..) {
|
||||||
let sel = key == cursor_key;
|
let sel = key == cursor_key;
|
||||||
let txt = item.show(self.focused && sel, &state.viewctx);
|
let txt = item.show(prev, self.focused && sel, &state.viewctx, settings);
|
||||||
|
|
||||||
|
prev = Some(item);
|
||||||
|
|
||||||
for (row, line) in txt.lines.into_iter().enumerate() {
|
for (row, line) in txt.lines.into_iter().enumerate() {
|
||||||
if sawit && lines.len() >= height {
|
if sawit && lines.len() >= height {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue