diff --git a/CHANGELOG.md b/CHANGELOG.md index 433634a..f75a3d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Procedure when bumping the version number: - Unicode-based grapheme width estimation method - `width_estimation_method` config option - `--width-estimation-method` option +- Room links are now included in the `I` message links list ### Changed diff --git a/cove/src/ui/euph/links.rs b/cove/src/ui/euph/links.rs index a452669..20e2219 100644 --- a/cove/src/ui/euph/links.rs +++ b/cove/src/ui/euph/links.rs @@ -7,16 +7,25 @@ use toss::{ widgets::{Join2, Text}, }; -use crate::ui::{ - UiError, key_bindings, util, - widgets::{ListBuilder, ListState, Popup}, +use crate::{ + euph::{self, SpanType}, + ui::{ + UiError, key_bindings, util, + widgets::{ListBuilder, ListState, Popup}, + }, }; use super::popup::PopupResult; +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] +enum Link { + Url(String), + Room(String), +} + pub struct LinksState { config: &'static Config, - links: Vec, + links: Vec, list: ListState, } @@ -24,12 +33,34 @@ const NUMBER_KEYS: [char; 10] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0 impl LinksState { pub fn new(config: &'static Config, content: &str) -> Self { - let links = LinkFinder::new() + let mut links = vec![]; + + // Collect URL-like links + for link in LinkFinder::new() .url_must_have_scheme(false) .kinds(&[LinkKind::Url]) .links(content) - .map(|l| l.as_str().to_string()) - .collect(); + { + links.push(( + link.start(), + link.end(), + Link::Url(link.as_str().to_string()), + )); + } + + // Collect room links + for (span, range) in euph::find_spans(content) { + if span == SpanType::Room { + let name = &content[range.start + 1..range.end]; + links.push((range.start, range.end, Link::Room(name.to_string()))); + } + } + + links.sort(); + let links = links + .into_iter() + .map(|(_, _, link)| link) + .collect::>(); Self { config, @@ -49,29 +80,29 @@ impl LinksState { for (id, link) in self.links.iter().enumerate() { let link = link.clone(); - if let Some(&number_key) = NUMBER_KEYS.get(id) { - list_builder.add_sel(id, move |selected| { - let text = if selected { - Styled::new(format!("[{number_key}]"), style_selected.bold()) - .then(" ", style_selected) - .then(link, style_selected) - } else { - Styled::new(format!("[{number_key}]"), Style::new().dark_grey().bold()) - .then_plain(" ") - .then_plain(link) - }; - Text::new(text) - }); - } else { - list_builder.add_sel(id, move |selected| { - let text = if selected { - Styled::new(format!(" {link}"), style_selected) - } else { - Styled::new_plain(format!(" {link}")) - }; - Text::new(text) - }); - } + list_builder.add_sel(id, move |selected| { + let mut text = Styled::default(); + + // Number key indicator + text = match NUMBER_KEYS.get(id) { + None if selected => text.then(" ", style_selected), + None => text.then_plain(" "), + Some(key) if selected => text.then(format!("[{key}] "), style_selected.bold()), + Some(key) => text.then(format!("[{key}] "), Style::new().dark_grey().bold()), + }; + + // The link itself + text = match link { + Link::Url(url) if selected => text.then(url, style_selected), + Link::Url(url) => text.then_plain(url), + Link::Room(name) if selected => { + text.then(format!("&{name}"), style_selected.bold()) + } + Link::Room(name) => text.then(format!("&{name}"), Style::new().blue().bold()), + }; + + Text::new(text) + }); } let hint_style = Style::new().grey().italic(); @@ -95,18 +126,24 @@ impl LinksState { } fn open_link_by_id(&self, id: usize) -> PopupResult { - if let Some(link) = self.links.get(id) { - // The `http://` or `https://` schema is necessary for open::that to - // successfully open the link in the browser. - let link = if link.starts_with("http://") || link.starts_with("https://") { - link.clone() - } else { - format!("https://{link}") - }; + match self.links.get(id) { + Some(Link::Url(url)) => { + // The `http://` or `https://` schema is necessary for + // open::that to successfully open the link in the browser. + let link = if url.starts_with("http://") || url.starts_with("https://") { + url.clone() + } else { + format!("https://{url}") + }; - if let Err(error) = open::that(&link) { - return PopupResult::ErrorOpeningLink { link, error }; + if let Err(error) = open::that(&link) { + return PopupResult::ErrorOpeningLink { link, error }; + } } + + Some(Link::Room(name)) => return PopupResult::SwitchToRoom { name: name.clone() }, + + _ => {} } PopupResult::Handled } diff --git a/cove/src/ui/euph/popup.rs b/cove/src/ui/euph/popup.rs index 3f8caaa..c434fb6 100644 --- a/cove/src/ui/euph/popup.rs +++ b/cove/src/ui/euph/popup.rs @@ -35,5 +35,6 @@ pub enum PopupResult { NotHandled, Handled, Close, + SwitchToRoom { name: String }, ErrorOpeningLink { link: String, error: io::Error }, } diff --git a/cove/src/ui/euph/room.rs b/cove/src/ui/euph/room.rs index 7d4b49c..83d7e96 100644 --- a/cove/src/ui/euph/room.rs +++ b/cove/src/ui/euph/room.rs @@ -27,7 +27,7 @@ use crate::{ util, widgets::ListState, }, - vault::EuphRoomVault, + vault::{EuphRoomVault, RoomIdentifier}, }; use super::{ @@ -500,18 +500,22 @@ impl EuphRoom { false } - pub async fn handle_input_event(&mut self, event: &mut InputEvent<'_>, keys: &Keys) -> bool { + pub async fn handle_input_event( + &mut self, + event: &mut InputEvent<'_>, + keys: &Keys, + ) -> RoomResult { if !self.popups.is_empty() { if event.matches(&keys.general.abort) { self.popups.pop_back(); - return true; + return RoomResult::Handled; } // Prevent event from reaching anything below the popup - return false; + return RoomResult::NotHandled; } let result = match &mut self.state { - State::Normal => return self.handle_normal_input_event(event, keys).await, + State::Normal => return self.handle_normal_input_event(event, keys).await.into(), State::Auth(editor) => auth::handle_input_event(event, keys, &self.room, editor), State::Nick(editor) => nick::handle_input_event(event, keys, &self.room, editor), State::Account(account) => account.handle_input_event(event, keys, &self.room), @@ -522,18 +526,24 @@ impl EuphRoom { }; match result { - PopupResult::NotHandled => false, - PopupResult::Handled => true, + PopupResult::NotHandled => RoomResult::NotHandled, + PopupResult::Handled => RoomResult::Handled, PopupResult::Close => { self.state = State::Normal; - true + RoomResult::Handled } + PopupResult::SwitchToRoom { name } => RoomResult::SwitchToRoom { + room: RoomIdentifier { + domain: self.vault().room().domain.clone(), + name, + }, + }, PopupResult::ErrorOpeningLink { link, error } => { self.popups.push_front(RoomPopup::Error { description: format!("Failed to open link: {link}"), reason: format!("{error}"), }); - true + RoomResult::Handled } } } @@ -638,3 +648,18 @@ impl EuphRoom { true } } + +pub enum RoomResult { + NotHandled, + Handled, + SwitchToRoom { room: RoomIdentifier }, +} + +impl From for RoomResult { + fn from(value: bool) -> Self { + match value { + true => Self::Handled, + false => Self::NotHandled, + } + } +} diff --git a/cove/src/ui/rooms.rs b/cove/src/ui/rooms.rs index 04bce02..80089da 100644 --- a/cove/src/ui/rooms.rs +++ b/cove/src/ui/rooms.rs @@ -29,7 +29,7 @@ use crate::{ use super::{ UiError, UiEvent, - euph::room::EuphRoom, + euph::room::{EuphRoom, RoomResult}, key_bindings, util, widgets::{ListBuilder, ListState}, }; @@ -574,8 +574,15 @@ impl Rooms { } State::ShowRoom(name) => { if let Some(room) = self.euph_rooms.get_mut(name) { - if room.handle_input_event(event, keys).await { - return true; + match room.handle_input_event(event, keys).await { + RoomResult::NotHandled => {} + RoomResult::Handled => return true, + RoomResult::SwitchToRoom { room } => { + self.list.move_cursor_to_id(&room); + self.connect_to_room(room.clone()).await; + self.state = State::ShowRoom(room); + return true; + } } if event.matches(&keys.general.abort) { self.state = State::ShowList;