2023-10-06 22:35:27 -07:00
|
|
|
//! # Logic for loading and validating application configuration
|
2023-01-26 15:40:16 -08:00
|
|
|
use std::borrow::Cow;
|
2023-01-06 16:56:28 -08:00
|
|
|
use std::collections::hash_map::DefaultHasher;
|
2022-12-29 18:00:59 -08:00
|
|
|
use std::collections::HashMap;
|
2023-01-06 16:56:28 -08:00
|
|
|
use std::fmt;
|
2022-12-29 18:00:59 -08:00
|
|
|
use std::fs::File;
|
2023-01-06 16:56:28 -08:00
|
|
|
use std::hash::{Hash, Hasher};
|
2022-12-29 18:00:59 -08:00
|
|
|
use std::io::BufReader;
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use std::process;
|
|
|
|
|
|
|
|
use clap::Parser;
|
2023-06-28 23:42:31 -07:00
|
|
|
use matrix_sdk::ruma::{OwnedRoomAliasId, OwnedRoomId, OwnedUserId, UserId};
|
2024-02-27 21:21:05 -08:00
|
|
|
use ratatui::style::{Color, Modifier as StyleModifier, Style};
|
|
|
|
use ratatui::text::Span;
|
2023-11-16 08:36:22 -08:00
|
|
|
use ratatui_image::picker::ProtocolType;
|
2023-01-06 16:56:28 -08:00
|
|
|
use serde::{de::Error as SerdeError, de::Visitor, Deserialize, Deserializer};
|
2023-03-13 16:10:28 -07:00
|
|
|
use tracing::Level;
|
2022-12-29 18:00:59 -08:00
|
|
|
use url::Url;
|
|
|
|
|
2023-10-20 19:32:33 -07:00
|
|
|
use super::base::{IambId, RoomInfo, SortColumn, SortFieldRoom, SortFieldUser, SortOrder};
|
2023-06-28 23:42:31 -07:00
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
macro_rules! usage {
|
|
|
|
( $($args: tt)* ) => {
|
|
|
|
println!($($args)*);
|
|
|
|
process::exit(2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-20 19:32:33 -07:00
|
|
|
const DEFAULT_MEMBERS_SORT: [SortColumn<SortFieldUser>; 2] = [
|
|
|
|
SortColumn(SortFieldUser::PowerLevel, SortOrder::Ascending),
|
|
|
|
SortColumn(SortFieldUser::UserId, SortOrder::Ascending),
|
|
|
|
];
|
|
|
|
|
|
|
|
const DEFAULT_ROOM_SORT: [SortColumn<SortFieldRoom>; 3] = [
|
|
|
|
SortColumn(SortFieldRoom::Favorite, SortOrder::Ascending),
|
|
|
|
SortColumn(SortFieldRoom::LowPriority, SortOrder::Ascending),
|
|
|
|
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
|
|
|
|
];
|
|
|
|
|
2023-03-12 15:43:13 -07:00
|
|
|
const DEFAULT_REQ_TIMEOUT: u64 = 120;
|
|
|
|
|
2023-01-06 16:56:28 -08:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
fn is_profile_char(c: char) -> bool {
|
|
|
|
c.is_ascii_alphanumeric() || c == '.' || c == '-'
|
|
|
|
}
|
|
|
|
|
|
|
|
fn validate_profile_name(name: &str) -> bool {
|
|
|
|
if name.is_empty() {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut chars = name.chars();
|
|
|
|
|
|
|
|
if !chars.next().map_or(false, |c| c.is_ascii_alphanumeric()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
name.chars().all(is_profile_char)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn validate_profile_names(names: &HashMap<String, ProfileConfig>) {
|
|
|
|
for name in names.keys() {
|
|
|
|
if validate_profile_name(name.as_str()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
usage!(
|
|
|
|
"{:?} is not a valid profile name.\n\n\
|
|
|
|
Profile names can only contain the characters \
|
|
|
|
a-z, A-Z, and 0-9. Period (.) and hyphen (-) are allowed after the first character.",
|
|
|
|
name
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-14 20:28:01 -07:00
|
|
|
const VERSION: &str = match option_env!("VERGEN_GIT_SHA") {
|
|
|
|
None => env!("CARGO_PKG_VERSION"),
|
|
|
|
Some(_) => concat!(env!("CARGO_PKG_VERSION"), " (", env!("VERGEN_GIT_SHA"), ")"),
|
|
|
|
};
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
#[derive(Parser)]
|
2023-06-14 20:28:01 -07:00
|
|
|
#[clap(version = VERSION, about, long_about = None)]
|
2022-12-29 18:00:59 -08:00
|
|
|
#[clap(propagate_version = true)]
|
|
|
|
pub struct Iamb {
|
|
|
|
#[clap(short = 'P', long, value_parser)]
|
|
|
|
pub profile: Option<String>,
|
|
|
|
|
|
|
|
#[clap(short = 'C', long, value_parser)]
|
|
|
|
pub config_directory: Option<PathBuf>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
|
|
pub enum ConfigError {
|
|
|
|
#[error("Error reading configuration file: {0}")]
|
|
|
|
IO(#[from] std::io::Error),
|
|
|
|
|
|
|
|
#[error("Error loading configuration file: {0}")]
|
|
|
|
Invalid(#[from] serde_json::Error),
|
|
|
|
}
|
|
|
|
|
2023-03-13 16:10:28 -07:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub struct LogLevel(pub Level);
|
|
|
|
pub struct LogLevelVisitor;
|
|
|
|
|
|
|
|
impl From<LogLevel> for Level {
|
|
|
|
fn from(level: LogLevel) -> Level {
|
|
|
|
level.0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'de> Visitor<'de> for LogLevelVisitor {
|
|
|
|
type Value = LogLevel;
|
|
|
|
|
|
|
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
formatter.write_str("a valid log level (e.g. \"warn\" or \"debug\")")
|
|
|
|
}
|
|
|
|
|
|
|
|
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
|
|
|
where
|
|
|
|
E: SerdeError,
|
|
|
|
{
|
|
|
|
match value {
|
|
|
|
"info" => Ok(LogLevel(Level::INFO)),
|
|
|
|
"debug" => Ok(LogLevel(Level::DEBUG)),
|
|
|
|
"warn" => Ok(LogLevel(Level::WARN)),
|
|
|
|
"error" => Ok(LogLevel(Level::ERROR)),
|
|
|
|
"trace" => Ok(LogLevel(Level::TRACE)),
|
|
|
|
_ => Err(E::custom("Could not parse log level")),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'de> Deserialize<'de> for LogLevel {
|
|
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
|
|
where
|
|
|
|
D: Deserializer<'de>,
|
|
|
|
{
|
|
|
|
deserializer.deserialize_str(LogLevelVisitor)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-06 16:56:28 -08:00
|
|
|
#[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>;
|
|
|
|
|
2023-10-20 19:32:33 -07:00
|
|
|
fn merge_sorts(a: SortOverrides, b: SortOverrides) -> SortOverrides {
|
|
|
|
SortOverrides {
|
2024-02-27 18:36:09 -08:00
|
|
|
chats: b.chats.or(a.chats),
|
2023-10-20 19:32:33 -07:00
|
|
|
dms: b.dms.or(a.dms),
|
|
|
|
rooms: b.rooms.or(a.rooms),
|
|
|
|
spaces: b.spaces.or(a.spaces),
|
|
|
|
members: b.members.or(a.members),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-06 16:56:28 -08:00
|
|
|
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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-06 23:15:58 -07:00
|
|
|
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
pub enum UserDisplayStyle {
|
|
|
|
// The Matrix username for the sender (e.g., "@user:example.com").
|
|
|
|
#[default]
|
|
|
|
Username,
|
|
|
|
|
|
|
|
// The localpart of the Matrix username (e.g., "@user").
|
|
|
|
LocalPart,
|
|
|
|
|
|
|
|
// The display name for the Matrix user, calculated according to the rules from the spec.
|
|
|
|
//
|
|
|
|
// This is usually something like "Ada Lovelace" if the user has configured a display name, but
|
|
|
|
// it can wind up being the Matrix username if there are display name collisions in the room,
|
|
|
|
// in order to avoid any confusion.
|
|
|
|
DisplayName,
|
|
|
|
}
|
|
|
|
|
2023-11-16 08:36:22 -08:00
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct ImagePreviewValues {
|
|
|
|
pub size: ImagePreviewSize,
|
|
|
|
pub protocol: Option<ImagePreviewProtocolValues>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Default, Deserialize)]
|
|
|
|
pub struct ImagePreview {
|
|
|
|
pub size: Option<ImagePreviewSize>,
|
|
|
|
pub protocol: Option<ImagePreviewProtocolValues>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ImagePreview {
|
|
|
|
fn values(self) -> ImagePreviewValues {
|
|
|
|
ImagePreviewValues {
|
|
|
|
size: self.size.unwrap_or_default(),
|
|
|
|
protocol: self.protocol,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Deserialize)]
|
|
|
|
pub struct ImagePreviewSize {
|
|
|
|
pub width: usize,
|
|
|
|
pub height: usize,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for ImagePreviewSize {
|
|
|
|
fn default() -> Self {
|
|
|
|
ImagePreviewSize { width: 66, height: 10 }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Deserialize)]
|
|
|
|
pub struct ImagePreviewProtocolValues {
|
|
|
|
pub r#type: Option<ProtocolType>,
|
|
|
|
pub font_size: Option<(u16, u16)>,
|
|
|
|
}
|
|
|
|
|
2023-10-20 19:32:33 -07:00
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct SortValues {
|
2024-02-27 18:36:09 -08:00
|
|
|
pub chats: Vec<SortColumn<SortFieldRoom>>,
|
2023-10-20 19:32:33 -07:00
|
|
|
pub dms: Vec<SortColumn<SortFieldRoom>>,
|
|
|
|
pub rooms: Vec<SortColumn<SortFieldRoom>>,
|
|
|
|
pub spaces: Vec<SortColumn<SortFieldRoom>>,
|
|
|
|
pub members: Vec<SortColumn<SortFieldUser>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Default, Deserialize)]
|
|
|
|
pub struct SortOverrides {
|
2024-02-27 18:36:09 -08:00
|
|
|
pub chats: Option<Vec<SortColumn<SortFieldRoom>>>,
|
2023-10-20 19:32:33 -07:00
|
|
|
pub dms: Option<Vec<SortColumn<SortFieldRoom>>>,
|
|
|
|
pub rooms: Option<Vec<SortColumn<SortFieldRoom>>>,
|
|
|
|
pub spaces: Option<Vec<SortColumn<SortFieldRoom>>>,
|
|
|
|
pub members: Option<Vec<SortColumn<SortFieldUser>>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl SortOverrides {
|
|
|
|
pub fn values(self) -> SortValues {
|
|
|
|
let rooms = self.rooms.unwrap_or_else(|| Vec::from(DEFAULT_ROOM_SORT));
|
2024-02-27 18:36:09 -08:00
|
|
|
let chats = self.chats.unwrap_or_else(|| rooms.clone());
|
2023-10-20 19:32:33 -07:00
|
|
|
let dms = self.dms.unwrap_or_else(|| rooms.clone());
|
|
|
|
let spaces = self.spaces.unwrap_or_else(|| rooms.clone());
|
|
|
|
let members = self.members.unwrap_or_else(|| Vec::from(DEFAULT_MEMBERS_SORT));
|
|
|
|
|
2024-02-27 18:36:09 -08:00
|
|
|
SortValues { rooms, members, chats, dms, spaces }
|
2023-10-20 19:32:33 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-03 13:57:28 -08:00
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct TunableValues {
|
2023-03-13 16:10:28 -07:00
|
|
|
pub log_level: Level,
|
2023-02-09 17:53:33 -08:00
|
|
|
pub reaction_display: bool,
|
|
|
|
pub reaction_shortcode_display: bool,
|
2023-01-26 15:40:16 -08:00
|
|
|
pub read_receipt_send: bool,
|
|
|
|
pub read_receipt_display: bool,
|
2023-03-12 15:43:13 -07:00
|
|
|
pub request_timeout: u64,
|
2023-10-20 19:32:33 -07:00
|
|
|
pub sort: SortValues,
|
2023-01-26 15:40:16 -08:00
|
|
|
pub typing_notice_send: bool,
|
2023-01-03 13:57:28 -08:00
|
|
|
pub typing_notice_display: bool,
|
2023-01-06 16:56:28 -08:00
|
|
|
pub users: UserOverrides,
|
2023-07-06 23:15:58 -07:00
|
|
|
pub username_display: UserDisplayStyle,
|
2023-01-28 14:09:40 -08:00
|
|
|
pub default_room: Option<String>,
|
2023-05-01 12:07:54 +01:00
|
|
|
pub open_command: Option<Vec<String>>,
|
2023-11-16 08:36:22 -08:00
|
|
|
pub image_preview: Option<ImagePreviewValues>,
|
2023-01-03 13:57:28 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Default, Deserialize)]
|
|
|
|
pub struct Tunables {
|
2023-03-13 16:10:28 -07:00
|
|
|
pub log_level: Option<LogLevel>,
|
2023-02-09 17:53:33 -08:00
|
|
|
pub reaction_display: Option<bool>,
|
|
|
|
pub reaction_shortcode_display: Option<bool>,
|
2023-01-26 15:40:16 -08:00
|
|
|
pub read_receipt_send: Option<bool>,
|
|
|
|
pub read_receipt_display: Option<bool>,
|
2023-03-12 15:43:13 -07:00
|
|
|
pub request_timeout: Option<u64>,
|
2023-10-20 19:32:33 -07:00
|
|
|
#[serde(default)]
|
|
|
|
pub sort: SortOverrides,
|
2023-01-26 15:40:16 -08:00
|
|
|
pub typing_notice_send: Option<bool>,
|
2023-01-03 13:57:28 -08:00
|
|
|
pub typing_notice_display: Option<bool>,
|
2023-01-06 16:56:28 -08:00
|
|
|
pub users: Option<UserOverrides>,
|
2023-07-06 23:15:58 -07:00
|
|
|
pub username_display: Option<UserDisplayStyle>,
|
2023-01-28 14:09:40 -08:00
|
|
|
pub default_room: Option<String>,
|
2023-05-01 12:07:54 +01:00
|
|
|
pub open_command: Option<Vec<String>>,
|
2023-11-16 08:36:22 -08:00
|
|
|
pub image_preview: Option<ImagePreview>,
|
2023-01-03 13:57:28 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Tunables {
|
|
|
|
fn merge(self, other: Self) -> Self {
|
|
|
|
Tunables {
|
2023-03-13 16:10:28 -07:00
|
|
|
log_level: self.log_level.or(other.log_level),
|
2023-02-09 17:53:33 -08:00
|
|
|
reaction_display: self.reaction_display.or(other.reaction_display),
|
|
|
|
reaction_shortcode_display: self
|
|
|
|
.reaction_shortcode_display
|
|
|
|
.or(other.reaction_shortcode_display),
|
2023-01-26 15:40:16 -08:00
|
|
|
read_receipt_send: self.read_receipt_send.or(other.read_receipt_send),
|
|
|
|
read_receipt_display: self.read_receipt_display.or(other.read_receipt_display),
|
2023-03-12 15:43:13 -07:00
|
|
|
request_timeout: self.request_timeout.or(other.request_timeout),
|
2023-10-20 19:32:33 -07:00
|
|
|
sort: merge_sorts(self.sort, other.sort),
|
2023-01-26 15:40:16 -08:00
|
|
|
typing_notice_send: self.typing_notice_send.or(other.typing_notice_send),
|
2023-01-03 13:57:28 -08:00
|
|
|
typing_notice_display: self.typing_notice_display.or(other.typing_notice_display),
|
2023-01-06 16:56:28 -08:00
|
|
|
users: merge_users(self.users, other.users),
|
2023-07-06 23:15:58 -07:00
|
|
|
username_display: self.username_display.or(other.username_display),
|
2023-01-28 14:09:40 -08:00
|
|
|
default_room: self.default_room.or(other.default_room),
|
2023-05-01 12:07:54 +01:00
|
|
|
open_command: self.open_command.or(other.open_command),
|
2023-11-16 08:36:22 -08:00
|
|
|
image_preview: self.image_preview.or(other.image_preview),
|
2023-01-03 13:57:28 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn values(self) -> TunableValues {
|
|
|
|
TunableValues {
|
2023-03-13 16:10:28 -07:00
|
|
|
log_level: self.log_level.map(Level::from).unwrap_or(Level::INFO),
|
2023-02-09 17:53:33 -08:00
|
|
|
reaction_display: self.reaction_display.unwrap_or(true),
|
|
|
|
reaction_shortcode_display: self.reaction_shortcode_display.unwrap_or(false),
|
2023-01-26 15:40:16 -08:00
|
|
|
read_receipt_send: self.read_receipt_send.unwrap_or(true),
|
|
|
|
read_receipt_display: self.read_receipt_display.unwrap_or(true),
|
2023-03-12 15:43:13 -07:00
|
|
|
request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQ_TIMEOUT),
|
2023-10-20 19:32:33 -07:00
|
|
|
sort: self.sort.values(),
|
2023-01-26 15:40:16 -08:00
|
|
|
typing_notice_send: self.typing_notice_send.unwrap_or(true),
|
|
|
|
typing_notice_display: self.typing_notice_display.unwrap_or(true),
|
2023-01-06 16:56:28 -08:00
|
|
|
users: self.users.unwrap_or_default(),
|
2023-07-06 23:15:58 -07:00
|
|
|
username_display: self.username_display.unwrap_or_default(),
|
2023-01-28 14:09:40 -08:00
|
|
|
default_room: self.default_room,
|
2023-05-01 12:07:54 +01:00
|
|
|
open_command: self.open_command,
|
2023-11-16 08:36:22 -08:00
|
|
|
image_preview: self.image_preview.map(ImagePreview::values),
|
2023-01-03 13:57:28 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct DirectoryValues {
|
|
|
|
pub cache: PathBuf,
|
2023-01-05 18:12:25 -08:00
|
|
|
pub logs: PathBuf,
|
2023-07-07 20:35:01 -07:00
|
|
|
pub downloads: Option<PathBuf>,
|
2023-11-16 08:36:22 -08:00
|
|
|
pub image_previews: PathBuf,
|
2023-01-03 13:57:28 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Default, Deserialize)]
|
|
|
|
pub struct Directories {
|
|
|
|
pub cache: Option<PathBuf>,
|
2023-01-05 18:12:25 -08:00
|
|
|
pub logs: Option<PathBuf>,
|
|
|
|
pub downloads: Option<PathBuf>,
|
2023-11-16 08:36:22 -08:00
|
|
|
pub image_previews: Option<PathBuf>,
|
2023-01-03 13:57:28 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Directories {
|
|
|
|
fn merge(self, other: Self) -> Self {
|
2023-01-05 18:12:25 -08:00
|
|
|
Directories {
|
|
|
|
cache: self.cache.or(other.cache),
|
|
|
|
logs: self.logs.or(other.logs),
|
|
|
|
downloads: self.downloads.or(other.downloads),
|
2023-11-16 08:36:22 -08:00
|
|
|
image_previews: self.image_previews.or(other.image_previews),
|
2023-01-05 18:12:25 -08:00
|
|
|
}
|
2023-01-03 13:57:28 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
fn values(self) -> DirectoryValues {
|
2023-01-05 18:12:25 -08:00
|
|
|
let cache = self
|
|
|
|
.cache
|
2023-01-10 19:59:30 -08:00
|
|
|
.or_else(|| {
|
|
|
|
let mut dir = dirs::cache_dir()?;
|
|
|
|
dir.push("iamb");
|
|
|
|
dir.into()
|
|
|
|
})
|
2023-01-05 18:12:25 -08:00
|
|
|
.expect("no dirs.cache value configured!");
|
|
|
|
|
|
|
|
let logs = self.logs.unwrap_or_else(|| {
|
|
|
|
let mut dir = cache.clone();
|
|
|
|
dir.push("logs");
|
|
|
|
dir
|
|
|
|
});
|
|
|
|
|
2023-07-07 20:35:01 -07:00
|
|
|
let downloads = self.downloads.or_else(dirs::download_dir);
|
2023-01-05 18:12:25 -08:00
|
|
|
|
2023-11-16 08:36:22 -08:00
|
|
|
let image_previews = self.image_previews.unwrap_or_else(|| {
|
|
|
|
let mut dir = cache.clone();
|
|
|
|
dir.push("image_preview_downloads");
|
|
|
|
dir
|
|
|
|
});
|
|
|
|
|
|
|
|
DirectoryValues { cache, logs, downloads, image_previews }
|
2023-01-03 13:57:28 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-28 23:42:31 -07:00
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
|
|
|
#[serde(untagged)]
|
|
|
|
pub enum WindowPath {
|
|
|
|
AliasId(OwnedRoomAliasId),
|
|
|
|
RoomId(OwnedRoomId),
|
|
|
|
UserId(OwnedUserId),
|
|
|
|
Window(IambId),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
|
|
|
#[serde(untagged, deny_unknown_fields)]
|
|
|
|
pub enum WindowLayout {
|
|
|
|
Window { window: WindowPath },
|
|
|
|
Split { split: Vec<WindowLayout> },
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
|
|
|
|
#[serde(rename_all = "lowercase", tag = "style")]
|
|
|
|
pub enum Layout {
|
|
|
|
/// Restore the layout from the previous session.
|
|
|
|
#[default]
|
|
|
|
Restore,
|
|
|
|
|
|
|
|
/// Open a single window using the `default_room` value.
|
|
|
|
New,
|
|
|
|
|
|
|
|
/// Open the window layouts described under `tabs`.
|
|
|
|
Config { tabs: Vec<WindowLayout> },
|
|
|
|
}
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
#[derive(Clone, Deserialize)]
|
|
|
|
pub struct ProfileConfig {
|
|
|
|
pub user_id: OwnedUserId,
|
|
|
|
pub url: Url,
|
2023-01-03 13:57:28 -08:00
|
|
|
pub settings: Option<Tunables>,
|
|
|
|
pub dirs: Option<Directories>,
|
2023-06-28 23:42:31 -07:00
|
|
|
pub layout: Option<Layout>,
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Deserialize)]
|
|
|
|
pub struct IambConfig {
|
|
|
|
pub profiles: HashMap<String, ProfileConfig>,
|
|
|
|
pub default_profile: Option<String>,
|
2023-01-03 13:57:28 -08:00
|
|
|
pub settings: Option<Tunables>,
|
|
|
|
pub dirs: Option<Directories>,
|
2023-06-28 23:42:31 -07:00
|
|
|
pub layout: Option<Layout>,
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl IambConfig {
|
|
|
|
pub fn load(config_json: &Path) -> Result<Self, ConfigError> {
|
|
|
|
if !config_json.is_file() {
|
|
|
|
usage!(
|
|
|
|
"Please create a configuration file at {}\n\n\
|
|
|
|
For more information try '--help'",
|
|
|
|
config_json.display(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
let file = File::open(config_json)?;
|
|
|
|
let reader = BufReader::new(file);
|
|
|
|
let config = serde_json::from_reader(reader)?;
|
|
|
|
|
|
|
|
Ok(config)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct ApplicationSettings {
|
|
|
|
pub matrix_dir: PathBuf,
|
2023-06-28 23:42:31 -07:00
|
|
|
pub layout_json: PathBuf,
|
2022-12-29 18:00:59 -08:00
|
|
|
pub session_json: PathBuf,
|
|
|
|
pub profile_name: String,
|
|
|
|
pub profile: ProfileConfig,
|
2023-01-03 13:57:28 -08:00
|
|
|
pub tunables: TunableValues,
|
|
|
|
pub dirs: DirectoryValues,
|
2023-06-28 23:42:31 -07:00
|
|
|
pub layout: Layout,
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl ApplicationSettings {
|
|
|
|
pub fn load(cli: Iamb) -> Result<Self, Box<dyn std::error::Error>> {
|
|
|
|
let mut config_dir = cli.config_directory.or_else(dirs::config_dir).unwrap_or_else(|| {
|
|
|
|
usage!(
|
|
|
|
"No user configuration directory found;\
|
|
|
|
please specify one via -C.\n\n
|
|
|
|
For more information try '--help'"
|
|
|
|
);
|
|
|
|
});
|
|
|
|
config_dir.push("iamb");
|
|
|
|
let mut config_json = config_dir.clone();
|
|
|
|
config_json.push("config.json");
|
|
|
|
|
2023-01-03 13:57:28 -08:00
|
|
|
let IambConfig {
|
|
|
|
mut profiles,
|
|
|
|
default_profile,
|
|
|
|
dirs,
|
|
|
|
settings: global,
|
2023-06-28 23:42:31 -07:00
|
|
|
layout,
|
2023-01-03 13:57:28 -08:00
|
|
|
} = IambConfig::load(config_json.as_path())?;
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
validate_profile_names(&profiles);
|
|
|
|
|
2023-01-03 13:57:28 -08:00
|
|
|
let (profile_name, mut profile) = if let Some(profile) = cli.profile.or(default_profile) {
|
2022-12-29 18:00:59 -08:00
|
|
|
profiles.remove_entry(&profile).unwrap_or_else(|| {
|
|
|
|
usage!(
|
|
|
|
"No configured profile with the name {:?} in {}",
|
|
|
|
profile,
|
|
|
|
config_json.display()
|
|
|
|
);
|
|
|
|
})
|
|
|
|
} else if profiles.len() == 1 {
|
|
|
|
profiles.into_iter().next().unwrap()
|
|
|
|
} else {
|
|
|
|
usage!(
|
|
|
|
"No profile specified. \
|
|
|
|
Please use -P or add \"default_profile\" to {}.\n\n\
|
|
|
|
For more information try '--help'",
|
|
|
|
config_json.display()
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2023-06-28 23:42:31 -07:00
|
|
|
let layout = profile.layout.take().or(layout).unwrap_or_default();
|
|
|
|
|
2023-01-03 13:57:28 -08:00
|
|
|
let tunables = global.unwrap_or_default();
|
|
|
|
let tunables = profile.settings.take().unwrap_or_default().merge(tunables);
|
|
|
|
let tunables = tunables.values();
|
|
|
|
|
2023-06-28 23:42:31 -07:00
|
|
|
let dirs = dirs.unwrap_or_default();
|
|
|
|
let dirs = profile.dirs.take().unwrap_or_default().merge(dirs);
|
|
|
|
let dirs = dirs.values();
|
|
|
|
|
|
|
|
// Set up paths that live inside the profile's data directory.
|
2022-12-29 18:00:59 -08:00
|
|
|
let mut profile_dir = config_dir.clone();
|
|
|
|
profile_dir.push("profiles");
|
|
|
|
profile_dir.push(profile_name.as_str());
|
|
|
|
|
|
|
|
let mut matrix_dir = profile_dir.clone();
|
|
|
|
matrix_dir.push("matrix");
|
|
|
|
|
|
|
|
let mut session_json = profile_dir;
|
|
|
|
session_json.push("session.json");
|
|
|
|
|
2023-06-28 23:42:31 -07:00
|
|
|
// Set up paths that live inside the profile's cache directory.
|
|
|
|
let mut cache_dir = dirs.cache.clone();
|
|
|
|
cache_dir.push("profiles");
|
|
|
|
cache_dir.push(profile_name.as_str());
|
|
|
|
|
|
|
|
let mut layout_json = cache_dir.clone();
|
|
|
|
layout_json.push("layout.json");
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
let settings = ApplicationSettings {
|
|
|
|
matrix_dir,
|
2023-06-28 23:42:31 -07:00
|
|
|
layout_json,
|
2022-12-29 18:00:59 -08:00
|
|
|
session_json,
|
|
|
|
profile_name,
|
|
|
|
profile,
|
2023-01-03 13:57:28 -08:00
|
|
|
tunables,
|
|
|
|
dirs,
|
2023-06-28 23:42:31 -07:00
|
|
|
layout,
|
2022-12-29 18:00:59 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
Ok(settings)
|
|
|
|
}
|
2023-01-06 16:56:28 -08:00
|
|
|
|
2023-01-26 15:40:16 -08:00
|
|
|
pub fn get_user_char_span<'a>(&self, user_id: &'a UserId) -> Span<'a> {
|
|
|
|
let (color, c) = self
|
|
|
|
.tunables
|
|
|
|
.users
|
|
|
|
.get(user_id)
|
|
|
|
.map(|user| {
|
|
|
|
(
|
|
|
|
user.color.as_ref().map(|c| c.0),
|
|
|
|
user.name.as_ref().and_then(|s| s.chars().next()),
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
let color = color.unwrap_or_else(|| user_color(user_id.as_str()));
|
|
|
|
let style = user_style_from_color(color);
|
|
|
|
|
|
|
|
let c = c.unwrap_or_else(|| user_id.localpart().chars().next().unwrap_or(' '));
|
|
|
|
|
|
|
|
Span::styled(String::from(c), style)
|
|
|
|
}
|
|
|
|
|
2023-07-06 23:15:58 -07:00
|
|
|
pub fn get_user_overrides(
|
|
|
|
&self,
|
|
|
|
user_id: &UserId,
|
|
|
|
) -> (Option<Color>, Option<Cow<'static, str>>) {
|
|
|
|
self.tunables
|
2023-01-26 15:40:16 -08:00
|
|
|
.users
|
|
|
|
.get(user_id)
|
|
|
|
.map(|user| (user.color.as_ref().map(|c| c.0), user.name.clone().map(Cow::Owned)))
|
2023-07-06 23:15:58 -07:00
|
|
|
.unwrap_or_default()
|
|
|
|
}
|
2023-01-26 15:40:16 -08:00
|
|
|
|
2023-07-06 23:15:58 -07:00
|
|
|
pub fn get_user_style(&self, user_id: &UserId) -> Style {
|
|
|
|
let color = self
|
|
|
|
.tunables
|
|
|
|
.users
|
|
|
|
.get(user_id)
|
|
|
|
.and_then(|user| user.color.as_ref().map(|c| c.0))
|
|
|
|
.unwrap_or_else(|| user_color(user_id.as_str()));
|
|
|
|
|
|
|
|
user_style_from_color(color)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_user_span<'a>(&self, user_id: &'a UserId, info: &'a RoomInfo) -> Span<'a> {
|
|
|
|
let (color, name) = self.get_user_overrides(user_id);
|
|
|
|
|
|
|
|
let color = color.unwrap_or_else(|| user_color(user_id.as_str()));
|
2023-01-26 15:40:16 -08:00
|
|
|
let style = user_style_from_color(color);
|
2023-07-06 23:15:58 -07:00
|
|
|
let name = match (name, &self.tunables.username_display) {
|
|
|
|
(Some(name), _) => name,
|
|
|
|
(None, UserDisplayStyle::Username) => Cow::Borrowed(user_id.as_str()),
|
|
|
|
(None, UserDisplayStyle::LocalPart) => Cow::Borrowed(user_id.localpart()),
|
|
|
|
(None, UserDisplayStyle::DisplayName) => {
|
|
|
|
if let Some(display) = info.display_names.get(user_id) {
|
|
|
|
Cow::Borrowed(display.as_str())
|
|
|
|
} else {
|
|
|
|
Cow::Borrowed(user_id.as_str())
|
|
|
|
}
|
|
|
|
},
|
|
|
|
};
|
2023-01-26 15:40:16 -08:00
|
|
|
|
|
|
|
Span::styled(name, style)
|
2023-01-06 16:56:28 -08:00
|
|
|
}
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
2023-01-06 16:56:28 -08:00
|
|
|
use matrix_sdk::ruma::user_id;
|
2023-06-28 23:42:31 -07:00
|
|
|
use std::convert::TryFrom;
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_profile_name_invalid() {
|
|
|
|
assert_eq!(validate_profile_name(""), false);
|
|
|
|
assert_eq!(validate_profile_name(" "), false);
|
|
|
|
assert_eq!(validate_profile_name("a b"), false);
|
|
|
|
assert_eq!(validate_profile_name("foo^bar"), false);
|
|
|
|
assert_eq!(validate_profile_name("FOO/BAR"), false);
|
|
|
|
assert_eq!(validate_profile_name("-b-c"), false);
|
|
|
|
assert_eq!(validate_profile_name("-B-c"), false);
|
|
|
|
assert_eq!(validate_profile_name(".b-c"), false);
|
|
|
|
assert_eq!(validate_profile_name(".B-c"), false);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_profile_name_valid() {
|
|
|
|
assert_eq!(validate_profile_name("foo"), true);
|
|
|
|
assert_eq!(validate_profile_name("FOO"), 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);
|
|
|
|
assert_eq!(validate_profile_name("a.B-c"), true);
|
|
|
|
}
|
2023-01-06 16:56:28 -08:00
|
|
|
|
|
|
|
#[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();
|
2023-01-26 15:40:16 -08:00
|
|
|
assert_eq!(res.typing_notice_send, None);
|
2023-01-06 16:56:28 -08:00
|
|
|
assert_eq!(res.typing_notice_display, None);
|
|
|
|
assert_eq!(res.users, None);
|
|
|
|
|
2023-01-26 15:40:16 -08:00
|
|
|
let res: Tunables = serde_json::from_str("{\"typing_notice_send\": true}").unwrap();
|
|
|
|
assert_eq!(res.typing_notice_send, Some(true));
|
2023-01-06 16:56:28 -08:00
|
|
|
assert_eq!(res.typing_notice_display, None);
|
|
|
|
assert_eq!(res.users, None);
|
|
|
|
|
2023-01-26 15:40:16 -08:00
|
|
|
let res: Tunables = serde_json::from_str("{\"typing_notice_send\": false}").unwrap();
|
|
|
|
assert_eq!(res.typing_notice_send, Some(false));
|
2023-01-06 16:56:28 -08:00
|
|
|
assert_eq!(res.typing_notice_display, None);
|
|
|
|
assert_eq!(res.users, None);
|
|
|
|
|
|
|
|
let res: Tunables = serde_json::from_str("{\"users\": {}}").unwrap();
|
2023-01-26 15:40:16 -08:00
|
|
|
assert_eq!(res.typing_notice_send, None);
|
2023-01-06 16:56:28 -08:00
|
|
|
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();
|
2023-01-26 15:40:16 -08:00
|
|
|
assert_eq!(res.typing_notice_send, None);
|
2023-01-06 16:56:28 -08:00
|
|
|
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()));
|
|
|
|
}
|
2023-06-28 23:42:31 -07:00
|
|
|
|
2023-07-06 23:15:58 -07:00
|
|
|
#[test]
|
|
|
|
fn test_parse_tunables_username_display() {
|
|
|
|
let res: Tunables = serde_json::from_str("{\"username_display\": \"username\"}").unwrap();
|
|
|
|
assert_eq!(res.username_display, Some(UserDisplayStyle::Username));
|
|
|
|
|
|
|
|
let res: Tunables = serde_json::from_str("{\"username_display\": \"localpart\"}").unwrap();
|
|
|
|
assert_eq!(res.username_display, Some(UserDisplayStyle::LocalPart));
|
|
|
|
|
|
|
|
let res: Tunables =
|
|
|
|
serde_json::from_str("{\"username_display\": \"displayname\"}").unwrap();
|
|
|
|
assert_eq!(res.username_display, Some(UserDisplayStyle::DisplayName));
|
|
|
|
}
|
|
|
|
|
2023-10-20 19:32:33 -07:00
|
|
|
#[test]
|
|
|
|
fn test_parse_tunables_sort() {
|
|
|
|
let res: Tunables = serde_json::from_str(
|
|
|
|
r#"{"sort": {"members": ["server","~localpart"],"spaces":["~favorite", "alias"]}}"#,
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
res.sort.members,
|
|
|
|
Some(vec![
|
|
|
|
SortColumn(SortFieldUser::Server, SortOrder::Ascending),
|
|
|
|
SortColumn(SortFieldUser::LocalPart, SortOrder::Descending),
|
|
|
|
])
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
res.sort.spaces,
|
|
|
|
Some(vec![
|
|
|
|
SortColumn(SortFieldRoom::Favorite, SortOrder::Descending),
|
|
|
|
SortColumn(SortFieldRoom::Alias, SortOrder::Ascending),
|
|
|
|
])
|
|
|
|
);
|
|
|
|
assert_eq!(res.sort.rooms, None);
|
|
|
|
assert_eq!(res.sort.dms, None);
|
|
|
|
|
|
|
|
// Check that we get the right default "rooms" and "dms" values.
|
|
|
|
let res = res.values();
|
|
|
|
assert_eq!(res.sort.members, vec![
|
|
|
|
SortColumn(SortFieldUser::Server, SortOrder::Ascending),
|
|
|
|
SortColumn(SortFieldUser::LocalPart, SortOrder::Descending),
|
|
|
|
]);
|
|
|
|
assert_eq!(res.sort.spaces, vec![
|
|
|
|
SortColumn(SortFieldRoom::Favorite, SortOrder::Descending),
|
|
|
|
SortColumn(SortFieldRoom::Alias, SortOrder::Ascending),
|
|
|
|
]);
|
|
|
|
assert_eq!(res.sort.rooms, Vec::from(DEFAULT_ROOM_SORT));
|
|
|
|
assert_eq!(res.sort.dms, Vec::from(DEFAULT_ROOM_SORT));
|
|
|
|
}
|
|
|
|
|
2023-06-28 23:42:31 -07:00
|
|
|
#[test]
|
|
|
|
fn test_parse_layout() {
|
|
|
|
let user = WindowPath::UserId(user_id!("@user:example.com").to_owned());
|
|
|
|
let alias = WindowPath::AliasId(OwnedRoomAliasId::try_from("#room:example.com").unwrap());
|
|
|
|
let room = WindowPath::RoomId(OwnedRoomId::try_from("!room:example.com").unwrap());
|
|
|
|
let dms = WindowPath::Window(IambId::DirectList);
|
|
|
|
let welcome = WindowPath::Window(IambId::Welcome);
|
|
|
|
|
|
|
|
let res: Layout = serde_json::from_str("{\"style\": \"restore\"}").unwrap();
|
|
|
|
assert_eq!(res, Layout::Restore);
|
|
|
|
|
|
|
|
let res: Layout = serde_json::from_str("{\"style\": \"new\"}").unwrap();
|
|
|
|
assert_eq!(res, Layout::New);
|
|
|
|
|
|
|
|
let res: Layout = serde_json::from_str(
|
|
|
|
"{\"style\": \"config\", \"tabs\": [{\"window\":\"@user:example.com\"}]}",
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(res, Layout::Config {
|
|
|
|
tabs: vec![WindowLayout::Window { window: user.clone() }]
|
|
|
|
});
|
|
|
|
|
|
|
|
let res: Layout = serde_json::from_str(
|
|
|
|
"{\
|
|
|
|
\"style\": \"config\",\
|
|
|
|
\"tabs\": [\
|
|
|
|
{\"split\":[\
|
|
|
|
{\"window\":\"@user:example.com\"},\
|
|
|
|
{\"window\":\"#room:example.com\"}\
|
|
|
|
]},\
|
|
|
|
{\"split\":[\
|
|
|
|
{\"window\":\"!room:example.com\"},\
|
|
|
|
{\"split\":[\
|
|
|
|
{\"window\":\"iamb://dms\"},\
|
|
|
|
{\"window\":\"iamb://welcome\"}\
|
|
|
|
]}\
|
|
|
|
]}\
|
|
|
|
]}",
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
let split1 = WindowLayout::Split {
|
|
|
|
split: vec![
|
|
|
|
WindowLayout::Window { window: user.clone() },
|
|
|
|
WindowLayout::Window { window: alias },
|
|
|
|
],
|
|
|
|
};
|
|
|
|
let split2 = WindowLayout::Split {
|
|
|
|
split: vec![WindowLayout::Window { window: dms }, WindowLayout::Window {
|
|
|
|
window: welcome,
|
|
|
|
}],
|
|
|
|
};
|
|
|
|
let split3 = WindowLayout::Split {
|
|
|
|
split: vec![WindowLayout::Window { window: room }, split2],
|
|
|
|
};
|
|
|
|
let tabs = vec![split1, split3];
|
|
|
|
assert_eq!(res, Layout::Config { tabs });
|
|
|
|
}
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|