diff --git a/Cargo.lock b/Cargo.lock index 0c85ac7..3fed98d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -579,6 +579,15 @@ version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bit_field" version = "0.10.2" @@ -674,6 +683,17 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "built" version = "0.7.7" @@ -1619,6 +1639,19 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "feruca" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06eccaab9dc53ad4bffb4ed748baf5c1f9475d5e9cac35e1b8eac69dac56899e" +dependencies = [ + "bincode", + "bstr", + "once_cell", + "rustc-hash", + "unicode-canonical-combining-class", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -2198,6 +2231,7 @@ dependencies = [ "dirs", "edit", "emojis", + "feruca", "futures", "gethostname", "html5ever", @@ -5713,6 +5747,12 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +[[package]] +name = "unicode-canonical-combining-class" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41c99d5174052d02ce765418e826597a1be18f32c114e35d9e22f92390239561" + [[package]] name = "unicode-ident" version = "1.0.18" diff --git a/Cargo.toml b/Cargo.toml index 24bbc6f..1ac3bf1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ clap = {version = "~4.3", features = ["derive"]} css-color-parser = "0.1.2" dirs = "4.0.0" emojis = "0.5" +feruca = "0.10.1" futures = "0.3" gethostname = "0.4.1" html5ever = "0.26.0" diff --git a/src/base.rs b/src/base.rs index 2660948..362a7c0 100644 --- a/src/base.rs +++ b/src/base.rs @@ -1486,6 +1486,9 @@ pub struct ChatStore { /// Whether the application is currently focused pub focused: bool, + + /// Collator for locale-aware text sorting. + pub collator: feruca::Collator, } impl ChatStore { @@ -1500,6 +1503,7 @@ impl ChatStore { cmds: crate::commands::setup_commands(), emojis: emoji_map(), + collator: Default::default(), names: Default::default(), rooms: Default::default(), presences: Default::default(), diff --git a/src/config.rs b/src/config.rs index fb3d022..b1352cb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ //! # Logic for loading and validating application configuration use std::borrow::Cow; use std::collections::hash_map::DefaultHasher; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::fmt; use std::fs::File; use std::hash::{Hash, Hasher}; @@ -105,7 +105,7 @@ fn validate_profile_name(name: &str) -> bool { name.chars().all(is_profile_char) } -fn validate_profile_names(names: &HashMap) { +fn validate_profile_names(names: &BTreeMap) { for name in names.keys() { if validate_profile_name(name.as_str()) { continue; @@ -787,7 +787,7 @@ pub struct ProfileConfig { #[derive(Clone, Deserialize)] pub struct IambConfig { - pub profiles: HashMap, + pub profiles: BTreeMap, pub default_profile: Option, pub settings: Option, pub dirs: Option, diff --git a/src/windows/mod.rs b/src/windows/mod.rs index 0513bc8..1228781 100644 --- a/src/windows/mod.rs +++ b/src/windows/mod.rs @@ -82,6 +82,7 @@ use crate::base::{ use self::{room::RoomState, welcome::WelcomeState}; use crate::message::MessageTimeStamp; +use feruca::Collator; pub mod room; pub mod welcome; @@ -170,7 +171,12 @@ fn user_cmp(a: &MemberItem, b: &MemberItem, field: &SortFieldUser) -> Ordering { } } -fn room_cmp(a: &T, b: &T, field: &SortFieldRoom) -> Ordering { +fn room_cmp( + a: &T, + b: &T, + field: &SortFieldRoom, + collator: &mut Collator, +) -> Ordering { match field { SortFieldRoom::Favorite => { let fava = a.has_tag(TagName::Favorite); @@ -186,7 +192,7 @@ fn room_cmp(a: &T, b: &T, field: &SortFieldRoom) -> Ordering { // If a has LowPriority and b doesn't, it should sort later in room list. lowa.cmp(&lowb) }, - SortFieldRoom::Name => a.name().cmp(b.name()), + SortFieldRoom::Name => collator.collate(a.name(), b.name()), SortFieldRoom::Alias => some_cmp(a.alias(), b.alias(), Ord::cmp), SortFieldRoom::RoomId => a.room_id().cmp(b.room_id()), SortFieldRoom::Unread => { @@ -209,9 +215,10 @@ fn room_fields_cmp( a: &T, b: &T, fields: &[SortColumn], + collator: &mut Collator, ) -> Ordering { for SortColumn(field, order) in fields { - match (room_cmp(a, b, field), order) { + match (room_cmp(a, b, field, collator), order) { (Ordering::Equal, _) => continue, (o, SortOrder::Ascending) => return o, (o, SortOrder::Descending) => return o.reverse(), @@ -219,7 +226,7 @@ fn room_fields_cmp( } // Break ties on ascending room id. - room_cmp(a, b, &SortFieldRoom::RoomId) + room_cmp(a, b, &SortFieldRoom::RoomId, collator) } fn user_fields_cmp( @@ -516,7 +523,8 @@ impl WindowOps for IambWindow { .map(|room_info| DirectItem::new(room_info, store)) .collect::>(); let fields = &store.application.settings.tunables.sort.dms; - items.sort_by(|a, b| room_fields_cmp(a, b, fields)); + let collator = &mut store.application.collator; + items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator)); state.set(items); @@ -561,7 +569,8 @@ impl WindowOps for IambWindow { .map(|room_info| RoomItem::new(room_info, store)) .collect::>(); let fields = &store.application.settings.tunables.sort.rooms; - items.sort_by(|a, b| room_fields_cmp(a, b, fields)); + let collator = &mut store.application.collator; + items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator)); state.set(items); @@ -592,7 +601,8 @@ impl WindowOps for IambWindow { items.extend(dms); let fields = &store.application.settings.tunables.sort.chats; - items.sort_by(|a, b| room_fields_cmp(a, b, fields)); + let collator = &mut store.application.collator; + items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator)); state.set(items); @@ -625,7 +635,8 @@ impl WindowOps for IambWindow { items.extend(dms); let fields = &store.application.settings.tunables.sort.chats; - items.sort_by(|a, b| room_fields_cmp(a, b, fields)); + let collator = &mut store.application.collator; + items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator)); state.set(items); @@ -645,7 +656,8 @@ impl WindowOps for IambWindow { .map(|room| SpaceItem::new(room, store)) .collect::>(); let fields = &store.application.settings.tunables.sort.spaces; - items.sort_by(|a, b| room_fields_cmp(a, b, fields)); + let collator = &mut store.application.collator; + items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator)); state.set(items); @@ -1627,6 +1639,8 @@ mod tests { #[test] fn test_sort_rooms() { + let mut collator = Collator::default(); + let collator = &mut collator; let server = server_name!("example.com"); let room1 = TestRoomItem { @@ -1659,13 +1673,13 @@ mod tests { // Sort by Name ascending. let mut rooms = vec![&room1, &room2, &room3]; let fields = &[SortColumn(SortFieldRoom::Name, SortOrder::Ascending)]; - rooms.sort_by(|a, b| room_fields_cmp(a, b, fields)); + rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator)); assert_eq!(rooms, vec![&room3, &room2, &room1]); // Sort by Name descending. let mut rooms = vec![&room1, &room2, &room3]; let fields = &[SortColumn(SortFieldRoom::Name, SortOrder::Descending)]; - rooms.sort_by(|a, b| room_fields_cmp(a, b, fields)); + rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator)); assert_eq!(rooms, vec![&room1, &room2, &room3]); // Sort by Favorite and Alias before Name to show order matters. @@ -1675,7 +1689,7 @@ mod tests { SortColumn(SortFieldRoom::Alias, SortOrder::Ascending), SortColumn(SortFieldRoom::Name, SortOrder::Ascending), ]; - rooms.sort_by(|a, b| room_fields_cmp(a, b, fields)); + rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator)); assert_eq!(rooms, vec![&room1, &room2, &room3]); // Now flip order of Favorite with Descending @@ -1685,12 +1699,14 @@ mod tests { SortColumn(SortFieldRoom::Alias, SortOrder::Ascending), SortColumn(SortFieldRoom::Name, SortOrder::Ascending), ]; - rooms.sort_by(|a, b| room_fields_cmp(a, b, fields)); + rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator)); assert_eq!(rooms, vec![&room2, &room3, &room1]); } #[test] fn test_sort_room_recents() { + let mut collator = Collator::default(); + let collator = &mut collator; let server = server_name!("example.com"); let room1 = TestRoomItem { @@ -1729,18 +1745,20 @@ mod tests { // Sort by Recent ascending. let mut rooms = vec![&room1, &room2, &room3]; let fields = &[SortColumn(SortFieldRoom::Recent, SortOrder::Ascending)]; - rooms.sort_by(|a, b| room_fields_cmp(a, b, fields)); + rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator)); assert_eq!(rooms, vec![&room2, &room3, &room1]); // Sort by Recent descending. let mut rooms = vec![&room1, &room2, &room3]; let fields = &[SortColumn(SortFieldRoom::Recent, SortOrder::Descending)]; - rooms.sort_by(|a, b| room_fields_cmp(a, b, fields)); + rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator)); assert_eq!(rooms, vec![&room1, &room3, &room2]); } #[test] fn test_sort_room_invites() { + let mut collator = Collator::default(); + let collator = &mut collator; let server = server_name!("example.com"); let room1 = TestRoomItem { @@ -1776,7 +1794,7 @@ mod tests { SortColumn(SortFieldRoom::Invite, SortOrder::Ascending), SortColumn(SortFieldRoom::Name, SortOrder::Ascending), ]; - rooms.sort_by(|a, b| room_fields_cmp(a, b, fields)); + rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator)); assert_eq!(rooms, vec![&room3, &room1, &room2]); // Sort invites after @@ -1785,7 +1803,7 @@ mod tests { SortColumn(SortFieldRoom::Invite, SortOrder::Descending), SortColumn(SortFieldRoom::Name, SortOrder::Ascending), ]; - rooms.sort_by(|a, b| room_fields_cmp(a, b, fields)); + rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator)); assert_eq!(rooms, vec![&room1, &room2, &room3]); } } diff --git a/src/windows/room/space.rs b/src/windows/room/space.rs index 5f59a13..912d313 100644 --- a/src/windows/room/space.rs +++ b/src/windows/room/space.rs @@ -214,7 +214,8 @@ impl StatefulWidget for Space<'_> { }) .collect::>(); let fields = &self.store.application.settings.tunables.sort.rooms; - items.sort_by(|a, b| room_fields_cmp(a, b, fields)); + let collator = &mut self.store.application.collator; + items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator)); state.list.set(items); state.last_fetch = Some(Instant::now());