Skip to content

Commit

Permalink
Support notifications via terminal bell
Browse files Browse the repository at this point in the history
  • Loading branch information
ulyssa committed Mar 24, 2024
1 parent db9cb92 commit 9b5c0ed
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 24 deletions.
1 change: 1 addition & 0 deletions docs/iamb.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ overridden as described in *PROFILES*.
> Configures push-notifications, which are delivered as desktop
> notifications if available.
> *enabled* `true` to enable the feature, defaults to `false`.
> *via* `"desktop"` to use desktop mechanism, or `"bell"` to use terminal bell.
> *show_message* to show the message in the desktop notification. Defaults
> to `true`. Messages are truncated beyond a small length.
> The notification _rules_ are stored server side, loaded once at startup,
Expand Down
4 changes: 4 additions & 0 deletions src/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,9 @@ pub struct ChatStore {

/// Last draw time, used to match with RoomInfo's draw_last.
pub draw_curr: Option<Instant>,

/// Whether to ring the terminal bell on the next redraw.
pub ring_bell: bool,
}

impl ChatStore {
Expand All @@ -1294,6 +1297,7 @@ impl ChatStore {
need_load: Default::default(),
sync_info: Default::default(),
draw_curr: None,
ring_bell: false,
}
}

Expand Down
20 changes: 19 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ fn is_profile_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '.' || c == '-'
}

fn default_true() -> bool {
true
}

fn validate_profile_name(name: &str) -> bool {
if name.is_empty() {
return false;
Expand Down Expand Up @@ -391,10 +395,24 @@ pub enum UserDisplayStyle {
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)]
pub struct Notifications {
#[serde(default)]
pub enabled: bool,
pub show_message: Option<bool>,
#[serde(default)]
pub via: NotifyVia,
#[serde(default = "default_true")]
pub show_message: bool,
}

#[derive(Clone)]
Expand Down
6 changes: 5 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use std::collections::VecDeque;
use std::convert::TryFrom;
use std::fmt::Display;
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::process;
use std::sync::atomic::{AtomicUsize, Ordering};
Expand Down Expand Up @@ -293,6 +293,10 @@ impl Application {
let sstate = &mut self.screen;
let term = &mut self.terminal;

if store.application.ring_bell {
store.application.ring_bell = term.backend_mut().write_all(&[7]).is_err();
}

if full {
term.clear()?;
}
Expand Down
63 changes: 41 additions & 22 deletions src/notifications.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use unicode_segmentation::UnicodeSegmentation;

use crate::{
base::{AsyncProgramStore, IambError, IambResult},
config::ApplicationSettings,
config::{ApplicationSettings, NotifyVia},
};

pub async fn register_notifications(
Expand All @@ -26,6 +26,7 @@ pub async fn register_notifications(
if !settings.tunables.notifications.enabled {
return;
}
let notify_via = settings.tunables.notifications.via;
let show_message = settings.tunables.notifications.show_message;
let server_settings = client.notification_settings().await;
let Some(startup_ts) = MilliSecondsSinceUnixEpoch::from_system_time(SystemTime::now()) else {
Expand All @@ -47,29 +48,19 @@ pub async fn register_notifications(
return;
}

match parse_notification(notification, room).await {
match parse_notification(notification, room, show_message).await {
Ok((summary, body, server_ts)) => {
if server_ts < startup_ts {
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) {
return;
}
if show_message != Some(false) {
if let Some(body) = body {
desktop_notification.body(&body);
}
}
if let Err(err) = desktop_notification.show() {
tracing::error!("Failed to send notification: {err}")

match notify_via {
NotifyVia::Desktop => send_notification_desktop(summary, body),
NotifyVia::Bell => send_notification_bell(&store).await,
}
},
Err(err) => {
Expand All @@ -81,6 +72,28 @@ pub async fn register_notifications(
.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(
settings: &NotificationSettings,
room: &MatrixRoom,
Expand Down Expand Up @@ -129,6 +142,7 @@ async fn is_open(store: &AsyncProgramStore, room_id: &RoomId) -> bool {
pub async fn parse_notification(
notification: Notification,
room: MatrixRoom,
show_body: bool,
) -> IambResult<(String, Option<String>, MilliSecondsSinceUnixEpoch)> {
let event = notification.event.deserialize().map_err(IambError::from)?;

Expand All @@ -142,12 +156,17 @@ pub async fn parse_notification(
.and_then(|m| m.display_name())
.unwrap_or_else(|| sender_id.localpart());

let body = event_notification_body(
&event,
sender_name,
room.is_direct().await.map_err(IambError::from)?,
)
.map(truncate);
let body = if show_body {
event_notification_body(
&event,
sender_name,
room.is_direct().await.map_err(IambError::from)?,
)
.map(truncate)
} else {
None
};

return Ok((sender_name.to_string(), body, server_ts));
}

Expand Down

0 comments on commit 9b5c0ed

Please sign in to comment.