Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support notifications via terminal bell #227

Merged
merged 3 commits into from
Mar 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/iamb.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,10 @@ overridden as described in *PROFILES*.
> Defines whether or not the message body is colored like the username.

**notifications** (type: notifications object)
> Configures push-notifications, which are delivered as desktop
> notifications if available.
> Configures push-notifications.
> *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
> 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
7 changes: 6 additions & 1 deletion src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use crate::{
ApplicationSettings,
DirectoryValues,
Notifications,
NotifyVia,
ProfileConfig,
SortOverrides,
TunableValues,
Expand Down Expand Up @@ -187,7 +188,11 @@ pub fn mock_tunables() -> TunableValues {
open_command: None,
username_display: UserDisplayStyle::Username,
message_user_color: false,
notifications: Notifications { enabled: false, show_message: None },
notifications: Notifications {
enabled: false,
via: NotifyVia::Desktop,
show_message: true,
},
image_preview: None,
user_gutter_width: 30,
}
Expand Down