Support notifications via terminal bell (#227)

Co-authored-by: Benjamin Grosse <ste3ls@gmail.com>
This commit is contained in:
Ulyssa 2024-03-24 10:19:34 -07:00 committed by GitHub
parent db9cb92737
commit 99996e275b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 78 additions and 27 deletions

View file

@ -90,9 +90,10 @@ overridden as described in *PROFILES*.
> Defines whether or not the message body is colored like the username. > Defines whether or not the message body is colored like the username.
**notifications** (type: notifications object) **notifications** (type: notifications object)
> Configures push-notifications, which are delivered as desktop > Configures push-notifications.
> notifications if available.
> *enabled* `true` to enable the feature, defaults to `false`. > *enabled* `true` to enable the feature, defaults to `false`.
> *via* `"desktop"` to use desktop mechanism (default), or `"bell"` to use
> terminal bell.
> *show_message* to show the message in the desktop notification. Defaults > *show_message* to show the message in the desktop notification. Defaults
> to `true`. Messages are truncated beyond a small length. > to `true`. Messages are truncated beyond a small length.
> The notification _rules_ are stored server side, loaded once at startup, > The notification _rules_ are stored server side, loaded once at startup,

View file

@ -1273,6 +1273,9 @@ pub struct ChatStore {
/// Last draw time, used to match with RoomInfo's draw_last. /// Last draw time, used to match with RoomInfo's draw_last.
pub draw_curr: Option<Instant>, pub draw_curr: Option<Instant>,
/// Whether to ring the terminal bell on the next redraw.
pub ring_bell: bool,
} }
impl ChatStore { impl ChatStore {
@ -1294,6 +1297,7 @@ impl ChatStore {
need_load: Default::default(), need_load: Default::default(),
sync_info: Default::default(), sync_info: Default::default(),
draw_curr: None, draw_curr: None,
ring_bell: false,
} }
} }

View file

@ -86,6 +86,10 @@ fn is_profile_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '.' || c == '-' c.is_ascii_alphanumeric() || c == '.' || c == '-'
} }
fn default_true() -> bool {
true
}
fn validate_profile_name(name: &str) -> bool { fn validate_profile_name(name: &str) -> bool {
if name.is_empty() { if name.is_empty() {
return false; return false;
@ -391,10 +395,24 @@ pub enum UserDisplayStyle {
DisplayName, DisplayName,
} }
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum NotifyVia {
/// Deliver notifications via terminal bell.
Bell,
/// Deliver notifications via desktop mechanism.
#[default]
Desktop,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct Notifications { pub struct Notifications {
#[serde(default)]
pub enabled: bool, pub enabled: bool,
pub show_message: Option<bool>, #[serde(default)]
pub via: NotifyVia,
#[serde(default = "default_true")]
pub show_message: bool,
} }
#[derive(Clone)] #[derive(Clone)]

View file

@ -19,7 +19,7 @@ use std::collections::VecDeque;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::fmt::Display; use std::fmt::Display;
use std::fs::{create_dir_all, File}; use std::fs::{create_dir_all, File};
use std::io::{stdout, BufWriter, Stdout}; use std::io::{stdout, BufWriter, Stdout, Write};
use std::ops::DerefMut; use std::ops::DerefMut;
use std::process; use std::process;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
@ -293,6 +293,10 @@ impl Application {
let sstate = &mut self.screen; let sstate = &mut self.screen;
let term = &mut self.terminal; let term = &mut self.terminal;
if store.application.ring_bell {
store.application.ring_bell = term.backend_mut().write_all(&[7]).is_err();
}
if full { if full {
term.clear()?; term.clear()?;
} }

View file

@ -15,7 +15,7 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::{ use crate::{
base::{AsyncProgramStore, IambError, IambResult}, base::{AsyncProgramStore, IambError, IambResult},
config::ApplicationSettings, config::{ApplicationSettings, NotifyVia},
}; };
pub async fn register_notifications( pub async fn register_notifications(
@ -26,6 +26,7 @@ pub async fn register_notifications(
if !settings.tunables.notifications.enabled { if !settings.tunables.notifications.enabled {
return; return;
} }
let notify_via = settings.tunables.notifications.via;
let show_message = settings.tunables.notifications.show_message; let show_message = settings.tunables.notifications.show_message;
let server_settings = client.notification_settings().await; let server_settings = client.notification_settings().await;
let Some(startup_ts) = MilliSecondsSinceUnixEpoch::from_system_time(SystemTime::now()) else { let Some(startup_ts) = MilliSecondsSinceUnixEpoch::from_system_time(SystemTime::now()) else {
@ -47,29 +48,19 @@ pub async fn register_notifications(
return; return;
} }
match parse_notification(notification, room).await { match parse_notification(notification, room, show_message).await {
Ok((summary, body, server_ts)) => { Ok((summary, body, server_ts)) => {
if server_ts < startup_ts { if server_ts < startup_ts {
return; return;
} }
let mut desktop_notification = notify_rust::Notification::new();
desktop_notification
.summary(&summary)
.appname("iamb")
.timeout(notify_rust::Timeout::Milliseconds(3000))
.action("default", "default");
if is_missing_mention(&body, mode, &client) { if is_missing_mention(&body, mode, &client) {
return; return;
} }
if show_message != Some(false) {
if let Some(body) = body { match notify_via {
desktop_notification.body(&body); NotifyVia::Desktop => send_notification_desktop(summary, body),
} NotifyVia::Bell => send_notification_bell(&store).await,
}
if let Err(err) = desktop_notification.show() {
tracing::error!("Failed to send notification: {err}")
} }
}, },
Err(err) => { Err(err) => {
@ -81,6 +72,28 @@ pub async fn register_notifications(
.await; .await;
} }
async fn send_notification_bell(store: &AsyncProgramStore) {
let mut locked = store.lock().await;
locked.application.ring_bell = true;
}
fn send_notification_desktop(summary: String, body: Option<String>) {
let mut desktop_notification = notify_rust::Notification::new();
desktop_notification
.summary(&summary)
.appname("iamb")
.timeout(notify_rust::Timeout::Milliseconds(3000))
.action("default", "default");
if let Some(body) = body {
desktop_notification.body(&body);
}
if let Err(err) = desktop_notification.show() {
tracing::error!("Failed to send notification: {err}")
}
}
async fn global_or_room_mode( async fn global_or_room_mode(
settings: &NotificationSettings, settings: &NotificationSettings,
room: &MatrixRoom, room: &MatrixRoom,
@ -129,6 +142,7 @@ async fn is_open(store: &AsyncProgramStore, room_id: &RoomId) -> bool {
pub async fn parse_notification( pub async fn parse_notification(
notification: Notification, notification: Notification,
room: MatrixRoom, room: MatrixRoom,
show_body: bool,
) -> IambResult<(String, Option<String>, MilliSecondsSinceUnixEpoch)> { ) -> IambResult<(String, Option<String>, MilliSecondsSinceUnixEpoch)> {
let event = notification.event.deserialize().map_err(IambError::from)?; let event = notification.event.deserialize().map_err(IambError::from)?;
@ -142,12 +156,17 @@ pub async fn parse_notification(
.and_then(|m| m.display_name()) .and_then(|m| m.display_name())
.unwrap_or_else(|| sender_id.localpart()); .unwrap_or_else(|| sender_id.localpart());
let body = event_notification_body( let body = if show_body {
event_notification_body(
&event, &event,
sender_name, sender_name,
room.is_direct().await.map_err(IambError::from)?, room.is_direct().await.map_err(IambError::from)?,
) )
.map(truncate); .map(truncate)
} else {
None
};
return Ok((sender_name.to_string(), body, server_ts)); return Ok((sender_name.to_string(), body, server_ts));
} }

View file

@ -28,6 +28,7 @@ use crate::{
ApplicationSettings, ApplicationSettings,
DirectoryValues, DirectoryValues,
Notifications, Notifications,
NotifyVia,
ProfileConfig, ProfileConfig,
SortOverrides, SortOverrides,
TunableValues, TunableValues,
@ -187,7 +188,11 @@ pub fn mock_tunables() -> TunableValues {
open_command: None, open_command: None,
username_display: UserDisplayStyle::Username, username_display: UserDisplayStyle::Username,
message_user_color: false, message_user_color: false,
notifications: Notifications { enabled: false, show_message: None }, notifications: Notifications {
enabled: false,
via: NotifyVia::Desktop,
show_message: true,
},
image_preview: None, image_preview: None,
user_gutter_width: 30, user_gutter_width: 30,
} }