/*
* meli
*
* Copyright 2017 - 2018 Manos Pitsidianakis
*
* This file is part of meli .
*
* meli is free software : you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* meli is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with meli . If not , see < http ://www.gnu.org/licenses/>.
* /
/*!
Notification handling components .
* /
use std ::process ::{ Command , Stdio } ;
use std ::sync ::{ Arc , Mutex } ;
use super ::* ;
#[ cfg(all(target_os = " linux " , feature = " dbus-notifications " )) ]
pub use dbus ::* ;
#[ cfg(all(target_os = " linux " , feature = " dbus-notifications " )) ]
mod dbus {
use super ::* ;
use crate ::types ::RateLimit ;
/// Passes notifications to the OS using Dbus
#[ derive(Debug) ]
pub struct DbusNotifications {
rate_limit : RateLimit ,
}
impl fmt ::Display for DbusNotifications {
fn fmt ( & self , f : & mut fmt ::Formatter ) -> fmt ::Result {
write! ( f , "" )
}
}
impl DbusNotifications {
pub fn new ( context : & Context ) -> Self {
DbusNotifications {
rate_limit : RateLimit ::new ( 1000 , 1000 , context . job_executor . clone ( ) ) ,
}
}
}
impl Component for DbusNotifications {
fn draw ( & mut self , _grid : & mut CellBuffer , _area : Area , _context : & mut Context ) { }
fn process_event ( & mut self , event : & mut UIEvent , context : & mut Context ) -> bool {
if ! context . settings . notifications . enable {
return false ;
}
if let UIEvent ::Notification ( ref title , ref body , ref kind ) = event {
if ! self . rate_limit . tick ( ) {
return false ;
}
let settings = & context . settings . notifications ;
let mut notification = notify_rust ::Notification ::new ( ) ;
notification
. appname ( "meli" )
. summary ( title . as_ref ( ) . map ( String ::as_str ) . unwrap_or ( "meli" ) )
. body ( & escape_str ( body ) ) ;
match * kind {
Some ( NotificationType ::NewMail ) = > {
notification . hint ( notify_rust ::Hint ::Category ( "email" . to_owned ( ) ) ) ;
notification . icon ( "mail-message-new" ) ;
notification . sound_name ( "message-new-email" ) ;
}
Some ( NotificationType ::SentMail ) = > {
notification . hint ( notify_rust ::Hint ::Category ( "email" . to_owned ( ) ) ) ;
notification . icon ( "mail-send" ) ;
notification . sound_name ( "message-sent-email" ) ;
}
Some ( NotificationType ::Saved ) = > {
notification . icon ( "document-save" ) ;
}
Some ( NotificationType ::Info ) = > {
notification . icon ( "dialog-information" ) ;
}
Some ( NotificationType ::Error ( melib ::ErrorKind ::Authentication ) ) = > {
notification . icon ( "dialog-password" ) ;
}
Some ( NotificationType ::Error ( melib ::ErrorKind ::Bug ) ) = > {
notification . icon ( "face-embarrassed" ) ;
}
Some ( NotificationType ::Error ( melib ::ErrorKind ::None ) )
| Some ( NotificationType ::Error ( melib ::ErrorKind ::External ) ) = > {
notification . icon ( "dialog-error" ) ;
}
Some ( NotificationType ::Error ( melib ::ErrorKind ::Network ( _ ) ) ) = > {
notification . icon ( "network-error" ) ;
}
Some ( NotificationType ::Error ( melib ::ErrorKind ::Timeout ) ) = > {
notification . icon ( "network-offline" ) ;
}
_ = > { }
}
if settings . play_sound . is_true ( ) {
if let Some ( ref sound_path ) = settings . sound_file {
notification . hint ( notify_rust ::Hint ::SoundFile ( sound_path . to_owned ( ) ) ) ;
}
} else {
notification . hint ( notify_rust ::Hint ::SuppressSound ( true ) ) ;
}
if let Err ( err ) = notification . show ( ) {
debug ! ( "Could not show dbus notification: {:?}" , & err ) ;
melib ::log (
format! ( "Could not show dbus notification: {}" , err ) ,
melib ::ERROR ,
) ;
}
}
false
}
fn set_dirty ( & mut self , _value : bool ) { }
fn is_dirty ( & self ) -> bool {
false
}
fn id ( & self ) -> ComponentId {
ComponentId ::nil ( )
}
fn set_id ( & mut self , _id : ComponentId ) { }
fn perform ( & mut self , _action : & str , _context : & mut Context ) -> Result < ( ) > {
Err ( "No actions available." . into ( ) )
}
}
fn escape_str ( s : & str ) -> String {
let mut ret : String = String ::with_capacity ( s . len ( ) ) ;
for c in s . chars ( ) {
match c {
'&' = > ret . push_str ( "&" ) ,
'<' = > ret . push_str ( "<" ) ,
'>' = > ret . push_str ( ">" ) ,
'\'' = > ret . push_str ( "'" ) ,
'"' = > ret . push_str ( """ ) ,
_ = > {
let i = c as u32 ;
if ( 0x1 ..= 0x8 ) . contains ( & i )
| | ( 0xb ..= 0xc ) . contains ( & i )
| | ( 0xe ..= 0x1f ) . contains ( & i )
| | ( 0x7f ..= 0x84 ) . contains ( & i )
| | ( 0x86 ..= 0x9f ) . contains ( & i )
{
use std ::fmt ::Write ;
let _ = write! ( ret , "&#{:x}%{:x};" , i , i ) ;
} else {
ret . push ( c ) ;
}
}
}
}
ret
}
}
/// Passes notifications to a user defined shell command
#[ derive(Default, Debug) ]
pub struct NotificationCommand { }
impl NotificationCommand {
pub fn new ( ) -> Self {
NotificationCommand { }
}
fn update_xbiff ( path : & str ) -> Result < ( ) > {
let mut file = std ::fs ::OpenOptions ::new ( )
. append ( true ) /* writes will append to a file instead of overwriting previous contents */
. create ( true ) /* a new file will be created if the file does not yet already exist. */
. open ( path ) ? ;
if file . metadata ( ) ? . len ( ) > 128 {
file . set_len ( 0 ) ? ;
} else {
std ::io ::Write ::write_all ( & mut file , b" z " ) ? ;
}
Ok ( ( ) )
}
}
impl fmt ::Display for NotificationCommand {
fn fmt ( & self , f : & mut fmt ::Formatter ) -> fmt ::Result {
write! ( f , "" )
}
}
impl Component for NotificationCommand {
fn draw ( & mut self , _grid : & mut CellBuffer , _area : Area , _context : & mut Context ) { }
fn process_event ( & mut self , event : & mut UIEvent , context : & mut Context ) -> bool {
if let UIEvent ::Notification ( ref title , ref body , ref kind ) = event {
if context . settings . notifications . enable {
if * kind = = Some ( NotificationType ::NewMail ) {
if let Some ( ref path ) = context . settings . notifications . xbiff_file_path {
if let Err ( err ) = Self ::update_xbiff ( path ) {
debug ! ( "Could not update xbiff file: {:?}" , & err ) ;
melib ::log ( format! ( "Could not update xbiff file: {}." , err ) , ERROR ) ;
}
}
}
let mut script = context . settings . notifications . script . as_ref ( ) ;
if * kind = = Some ( NotificationType ::NewMail )
& & context . settings . notifications . new_mail_script . is_some ( )
{
script = context . settings . notifications . new_mail_script . as_ref ( ) ;
}
if let Some ( ref bin ) = script {
match Command ::new ( bin )
. arg ( & kind . map ( | k | k . to_string ( ) ) . unwrap_or_default ( ) )
. arg ( title . as_ref ( ) . map ( String ::as_str ) . unwrap_or ( "meli" ) )
. arg ( body )
. stdin ( Stdio ::piped ( ) )
. stdout ( Stdio ::piped ( ) )
. spawn ( )
{
Ok ( child ) = > {
context . children . push ( child ) ;
}
Err ( err ) = > {
log (
format! ( "Could not run notification script: {}." , err ) ,
ERROR ,
) ;
debug ! ( "Could not run notification script: {:?}" , err ) ;
}
}
} else {
#[ cfg(target_os = " macos " ) ]
{
let applescript = format! ( "display notification \"{message}\" with title \"{title}\" subtitle \"{subtitle}\"" , message = body . replace ( '"' , "'" ) , title = title . as_ref ( ) . map ( String ::as_str ) . unwrap_or ( "meli" ) . replace ( '"' , "'" ) , subtitle = kind . map ( | k | k . to_string ( ) ) . unwrap_or_default ( ) . replace ( '"' , "'" ) ) ;
match Command ::new ( "osascript" )
. arg ( "-e" )
. arg ( applescript )
. stdin ( Stdio ::piped ( ) )
. stdout ( Stdio ::piped ( ) )
. spawn ( )
{
Ok ( child ) = > {
context . children . push ( child ) ;
return false ;
}
Err ( err ) = > {
log (
format! ( "Could not run notification script: {}." , err ) ,
ERROR ,
) ;
debug ! ( "Could not run notification script: {:?}" , err ) ;
}
}
}
context
. replies
. push_back ( UIEvent ::StatusEvent ( StatusEvent ::DisplayMessage ( format! (
"{title}{}{body}" ,
if title . is_some ( ) { " " } else { "" } ,
title = title . as_deref ( ) . unwrap_or_default ( ) ,
body = body ,
) ) ) ) ;
}
}
}
false
}
fn id ( & self ) -> ComponentId {
ComponentId ::nil ( )
}
fn is_dirty ( & self ) -> bool {
false
}
fn set_dirty ( & mut self , _value : bool ) { }
fn set_id ( & mut self , _id : ComponentId ) { }
fn perform ( & mut self , _action : & str , _context : & mut Context ) -> Result < ( ) > {
Err ( "No actions available." . into ( ) )
}
}
#[ derive(Debug) ]
struct NotificationLog {
title : Option < String > ,
body : String ,
kind : Option < NotificationType > ,
}
/// Notification history
#[ derive(Debug) ]
pub struct NotificationHistory {
history : Arc < Mutex < IndexMap < std ::time ::Instant , NotificationLog > > > ,
last_update : Arc < Mutex < std ::time ::Instant > > ,
id : ComponentId ,
}
/// Notification history view
#[ derive(Debug) ]
pub struct NotificationHistoryView {
theme_default : ThemeAttribute ,
history : Arc < Mutex < IndexMap < std ::time ::Instant , NotificationLog > > > ,
last_update : Arc < Mutex < std ::time ::Instant > > ,
my_last_update : std ::time ::Instant ,
cursor_pos : usize ,
dirty : bool ,
id : ComponentId ,
}
impl Default for NotificationHistory {
fn default ( ) -> Self {
Self ::new ( )
}
}
impl NotificationHistory {
pub fn new ( ) -> Self {
NotificationHistory {
history : Arc ::new ( Mutex ::new ( IndexMap ::default ( ) ) ) ,
last_update : Arc ::new ( Mutex ::new ( std ::time ::Instant ::now ( ) ) ) ,
id : ComponentId ::new_v4 ( ) ,
}
}
fn new_view ( & self , context : & Context ) -> NotificationHistoryView {
NotificationHistoryView {
theme_default : crate ::conf ::value ( context , "theme_default" ) ,
history : self . history . clone ( ) ,
last_update : self . last_update . clone ( ) ,
my_last_update : std ::time ::Instant ::now ( ) ,
cursor_pos : 0 ,
dirty : true ,
id : ComponentId ::new_v4 ( ) ,
}
}
}
impl fmt ::Display for NotificationHistory {
fn fmt ( & self , f : & mut fmt ::Formatter ) -> fmt ::Result {
write! ( f , "" )
}
}
impl fmt ::Display for NotificationHistoryView {
fn fmt ( & self , f : & mut fmt ::Formatter ) -> fmt ::Result {
write! ( f , "notifications" )
}
}
impl Component for NotificationHistory {
fn draw ( & mut self , _grid : & mut CellBuffer , _area : Area , _context : & mut Context ) { }
fn process_event ( & mut self , event : & mut UIEvent , _context : & mut Context ) -> bool {
if let UIEvent ::Notification ( ref title , ref body , ref kind ) = event {
self . history . lock ( ) . unwrap ( ) . insert (
std ::time ::Instant ::now ( ) ,
NotificationLog {
title : title . clone ( ) ,
body : body . to_string ( ) ,
kind : * kind ,
} ,
) ;
* self . last_update . lock ( ) . unwrap ( ) = std ::time ::Instant ::now ( ) ;
}
false
}
fn id ( & self ) -> ComponentId {
self . id
}
fn is_dirty ( & self ) -> bool {
false
}
fn set_dirty ( & mut self , _value : bool ) { }
fn set_id ( & mut self , id : ComponentId ) {
self . id = id ;
}
fn perform ( & mut self , action : & str , context : & mut Context ) -> Result < ( ) > {
match action {
"clear_history" = > {
self . history . lock ( ) . unwrap ( ) . clear ( ) ;
* self . last_update . lock ( ) . unwrap ( ) = std ::time ::Instant ::now ( ) ;
Ok ( ( ) )
}
"open_notification_log" = > {
context
. replies
. push_back ( UIEvent ::Action ( Tab ( New ( Some ( Box ::new (
self . new_view ( context ) ,
) ) ) ) ) ) ;
Ok ( ( ) )
}
_ = > Err ( "No actions available." . into ( ) ) ,
}
}
}
impl Component for NotificationHistoryView {
fn draw ( & mut self , grid : & mut CellBuffer , area : Area , context : & mut Context ) {
if ! self . is_dirty ( ) {
return ;
}
self . set_dirty ( false ) ;
self . my_last_update = std ::time ::Instant ::now ( ) ;
clear_area ( grid , area , self . theme_default ) ;
context . dirty_areas . push_back ( area ) ;
/* reserve top row for column headers */
let upper_left = pos_inc ( upper_left ! ( area ) , ( 0 , 1 ) ) ;
let bottom_right = bottom_right ! ( area ) ;
if get_y ( bottom_right ) < get_y ( upper_left ) {
return ;
}
let rows = get_y ( bottom_right ) - get_y ( upper_left ) + 1 ;
let page_no = ( self . cursor_pos ) . wrapping_div ( rows ) ;
let top_idx = page_no * rows ;
for ( i , ( instant , log ) ) in self
. history
. lock ( )
. unwrap ( )
. iter ( )
. rev ( )
. skip ( top_idx )
. enumerate ( )
{
let ( x , _ ) = write_string_to_grid (
& i . to_string ( ) ,
grid ,
self . theme_default . fg ,
self . theme_default . bg ,
self . theme_default . attrs ,
( pos_inc ( upper_left , ( 0 , i ) ) , bottom_right ) ,
None ,
) ;
let ( x , _ ) = write_string_to_grid (
& format! ( "{:#?}" , instant ) ,
grid ,
self . theme_default . fg ,
self . theme_default . bg ,
self . theme_default . attrs ,
( pos_inc ( upper_left , ( x + 2 , i ) ) , bottom_right ) ,
None ,
) ;
let ( x , _ ) = write_string_to_grid (
& format! ( "{:?}" , log . kind ) ,
grid ,
self . theme_default . fg ,
self . theme_default . bg ,
self . theme_default . attrs ,
( pos_inc ( upper_left , ( x + 2 , i ) ) , bottom_right ) ,
None ,
) ;
let ( x , _ ) = write_string_to_grid (
log . title . as_deref ( ) . unwrap_or_default ( ) ,
grid ,
self . theme_default . fg ,
self . theme_default . bg ,
self . theme_default . attrs ,
( pos_inc ( upper_left , ( x + 2 , i ) ) , bottom_right ) ,
None ,
) ;
write_string_to_grid (
& log . body ,
grid ,
self . theme_default . fg ,
self . theme_default . bg ,
self . theme_default . attrs ,
( pos_inc ( upper_left , ( x + 2 , i ) ) , bottom_right ) ,
None ,
) ;
}
}
fn process_event ( & mut self , event : & mut UIEvent , context : & mut Context ) -> bool {
let shortcuts = self . get_shortcuts ( context ) ;
match event {
UIEvent ::ConfigReload { old_settings : _ } = > {
self . theme_default = crate ::conf ::value ( context , "theme_default" ) ;
self . set_dirty ( true ) ;
}
UIEvent ::Input ( ref key ) if shortcut ! ( key = = shortcuts [ "general" ] [ "scroll_up" ] ) = > {
let _ret = self . perform ( "scroll_up" , context ) ;
debug_assert! ( _ret . is_ok ( ) ) ;
return true ;
}
UIEvent ::Input ( ref key ) if shortcut ! ( key = = shortcuts [ "general" ] [ "scroll_down" ] ) = > {
let _ret = self . perform ( "scroll_down" , context ) ;
debug_assert! ( _ret . is_ok ( ) ) ;
return true ;
}
UIEvent ::Input ( ref key ) if shortcut ! ( key = = shortcuts [ "general" ] [ "scroll_right" ] ) = > {
let _ret = self . perform ( "scroll_right" , context ) ;
debug_assert! ( _ret . is_ok ( ) ) ;
return true ;
}
UIEvent ::Input ( ref key ) if shortcut ! ( key = = shortcuts [ "general" ] [ "scroll_left" ] ) = > {
let _ret = self . perform ( "scroll_left" , context ) ;
debug_assert! ( _ret . is_ok ( ) ) ;
return true ;
}
_ = > { }
}
false
}
fn get_shortcuts ( & self , context : & Context ) -> ShortcutMaps {
let mut map : ShortcutMaps = Default ::default ( ) ;
let config_map = context . settings . shortcuts . general . key_values ( ) ;
map . insert ( "general" , config_map ) ;
map
}
fn id ( & self ) -> ComponentId {
self . id
}
fn is_dirty ( & self ) -> bool {
* self . last_update . lock ( ) . unwrap ( ) > self . my_last_update | | self . dirty
}
fn set_dirty ( & mut self , value : bool ) {
self . dirty = value ;
if value {
self . my_last_update = * self . last_update . lock ( ) . unwrap ( ) ;
}
}
fn set_id ( & mut self , id : ComponentId ) {
self . id = id ;
}
fn kill ( & mut self , uuid : Uuid , context : & mut Context ) {
debug_assert! ( uuid = = self . id ) ;
context . replies . push_back ( UIEvent ::Action ( Tab ( Kill ( uuid ) ) ) ) ;
}
fn perform ( & mut self , action : & str , _context : & mut Context ) -> Result < ( ) > {
match action {
"scroll_up" | "scroll_down" | "scroll_right" | "scroll_left" = > {
if action = = "scroll_up" {
self . cursor_pos = self . cursor_pos . saturating_sub ( 1 ) ;
} else if action = = "scroll_down" {
self . cursor_pos = std ::cmp ::min (
self . cursor_pos + 1 ,
self . history . lock ( ) . unwrap ( ) . len ( ) . saturating_sub ( 1 ) ,
) ;
}
self . set_dirty ( true ) ;
Ok ( ( ) )
}
_ = > Err ( "No actions available." . into ( ) ) ,
}
}
}