├── .Rbuildignore ├── .gitignore ├── .travis.yml ├── DESCRIPTION ├── LICENSE ├── NAMESPACE ├── NEWS.md ├── R ├── install.R ├── linux.R ├── macos.R ├── notify.R ├── utils.R └── windows.R ├── README.md ├── appveyor.yml ├── inst ├── Info.plist ├── R.ico ├── R.png ├── linux.png ├── macos.png ├── oldwindows.png └── windows.png ├── man └── notify.Rd └── src ├── AppDelegate.h ├── AppDelegate.m ├── Makevars ├── dummy.c ├── install.libs.R └── main.m /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^Makefile$ 4 | ^README.Rmd$ 5 | ^.travis.yml$ 6 | ^appveyor.yml$ 7 | ^inst/tn$ 8 | ^src/macnotifier$ 9 | ^src/macnotifier\.dSYM$ 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | /inst/tn 5 | /src/*.o 6 | /src/macnotifier 7 | /src/notifier.so 8 | /src/macnotifier.dSYM 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: r 2 | sudo: false 3 | cache: packages 4 | 5 | r: 6 | - release 7 | - devel 8 | - oldrel 9 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: notifier 2 | Title: Cross Platform Desktop Notifications 3 | Version: 1.0.0 4 | Author: Gábor Csárdi 5 | Maintainer: Gábor Csárdi 6 | Description: Send desktop notifications from R, on macOS, Windows and Linux. 7 | License: MIT + file LICENSE 8 | LazyData: true 9 | URL: https://github.com/gaborcsardi/notifier 10 | BugReports: https://github.com/gaborcsardi/notifier/issues 11 | RoxygenNote: 6.0.1.9000 12 | Roxygen: list(markdown = TRUE) 13 | Imports: 14 | processx, 15 | utils 16 | Remotes: 17 | r-pkgs/processx 18 | Encoding: UTF-8 19 | SystemRequirements: notify-send on Unix-like systems, other than macOS. 20 | On DEB-based systems you probably need 'libnotify-bin', on RPM-based 21 | systems 'libnotify'. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2016 2 | COPYRIGHT HOLDER: Gábor Csárdi 3 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(notify) 4 | importFrom(processx,process) 5 | importFrom(processx,run) 6 | importFrom(utils,download.file) 7 | importFrom(utils,packageName) 8 | importFrom(utils,unzip) 9 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | 2 | # 1.0.0 3 | 4 | First public release. 5 | -------------------------------------------------------------------------------- /R/install.R: -------------------------------------------------------------------------------- 1 | 2 | detect_os <- function() { 3 | if (.Platform$OS.type == "windows") { 4 | "windows" 5 | } else if (Sys.info()["sysname"] == "Darwin") { 6 | "macos" 7 | } else if (Sys.info()["sysname"] == "Linux") { 8 | "linux" 9 | } else { 10 | "linux" # Try Linux, might work, anyway 11 | } 12 | } 13 | 14 | installer <- function() { 15 | 16 | ## Are we installing the package or loading in devtools? 17 | ## If the latter, do nothing... 18 | if (Sys.getenv("R_PACKAGE_DIR", "") == "") return() 19 | 20 | switch( 21 | detect_os(), 22 | windows = install_windows(), 23 | macos = install_macos(), 24 | linux = install_linux() 25 | ) 26 | } 27 | 28 | #' @importFrom utils download.file unzip 29 | 30 | install_windows <- function() { 31 | 32 | ## notifu 33 | 34 | if (!file.exists("inst/notifu")) { 35 | if(getRversion() < "3.3.0") do.call("setInternet2", list()) 36 | on.exit(unlink("notifu.zip"), add = TRUE) 37 | download.file( 38 | "https://github.com/rwinlib/notifu/releases/download/1.6.1/notifu-1.6.1.zip", 39 | "notifu.zip", 40 | quiet = TRUE 41 | ) 42 | notifu_dir <- file.path(Sys.getenv("R_PACKAGE_DIR"), "notifu") 43 | dir.create(notifu_dir, showWarnings = FALSE) 44 | unzip("notifu.zip", exdir = notifu_dir) 45 | } 46 | 47 | ## toaster 48 | 49 | if (!file.exists("inst/toaster")) { 50 | if(getRversion() < "3.3.0") do.call("setInternet2", list()) 51 | on.exit(unlink("toaster.zip"), add = TRUE) 52 | download.file( 53 | "https://github.com/rwinlib/toaster/releases/download/2016-12-04-2/toaster.zip", 54 | "toaster.zip", 55 | quiet = TRUE 56 | ) 57 | toaster_dir <- file.path(Sys.getenv("R_PACKAGE_DIR"), "toaster") 58 | dir.create(toaster_dir, showWarnings = FALSE) 59 | unzip("toaster.zip", exdir = toaster_dir) 60 | } 61 | } 62 | 63 | install_macos <- function() { 64 | ## Nothing extra to do on macos 65 | } 66 | 67 | install_linux <- function() { 68 | ## TODO: check if notify-send is available 69 | } 70 | 71 | installer() 72 | -------------------------------------------------------------------------------- /R/linux.R: -------------------------------------------------------------------------------- 1 | 2 | notify_linux <- function(msg, title, image) { 3 | 4 | ns <- Sys.which("notify-send") 5 | 6 | if (ns == "") { 7 | stop("Cannot find notify-send executable, you need to install it.\n", 8 | "You need the 'libnotify-bin' package on Debian/Ubuntu, or\n", 9 | "the 'libnotify' package on Fedora Linux.") 10 | } 11 | 12 | ## Otherwise error 13 | if (title == "") title <- " " 14 | 15 | if (is.null(image)) { 16 | image <- normalizePath(system.file(package = packageName(), "R.png")) 17 | } 18 | 19 | args <- c("-i", image, title, msg) 20 | run(ns, args) 21 | 22 | invisible() 23 | } 24 | -------------------------------------------------------------------------------- /R/macos.R: -------------------------------------------------------------------------------- 1 | 2 | #' @importFrom utils packageName 3 | #' @importFrom processx run 4 | 5 | notify_macos <- function(msg, title, image) { 6 | 7 | tn <- system.file(package = packageName(), "macos", "macnotifier") 8 | 9 | bundle_id <- if (Sys.getenv("RSTUDIO", "") == "1") { 10 | "org.rstudio.RStudio" 11 | } else { 12 | "org.r-project.R" 13 | } 14 | 15 | args <- c( 16 | "-message", msg, 17 | "-title", title, 18 | "-sender", bundle_id, 19 | if (! is.null(image)) c("-contentImage", normalizePath(image)) 20 | ) 21 | 22 | run(tn, args, timeout = 2) 23 | 24 | invisible() 25 | } 26 | -------------------------------------------------------------------------------- /R/notify.R: -------------------------------------------------------------------------------- 1 | 2 | #' Create a desktop notification 3 | #' 4 | #' How exactly the notification appears is platform dependent: 5 | #' * On macOS, we use the `terminal-notifier` tool, see 6 | #' https://github.com/julienXX/terminal-notifier 7 | #' * On Linux and *BSD systems, including Solaris the `notify-send` 8 | #' command line tool is used. This requires the `libnotify-bin` 9 | #' package on Ubuntu/Debian and similar systems, or the `libnotify` 10 | #' package on RedHat/CentOS/Fedora and similar systems. 11 | #' * On Windows 8 or newer Windows versions we use the `toaster` tool, 12 | #' see https://github.com/nels-o/toaster. 13 | #' * On older Windows versions we use the `notifu` program, see 14 | #' https://www.paralint.com/projects/notifu. 15 | #' 16 | #' All notification systems support showing a title and an image, 17 | #' as part of the notification. 18 | #' 19 | #' @param msg Message to show. It may contain newline characters. 20 | #' @param title Message title. Typically shown on the top, with 21 | #' larger font. 22 | #' @param image Image to show in the notification. By default the 23 | #' official R logo is shown on macOS, and an alternative logo is shown 24 | #' on other OSes, because of licensing reasons. You can specify a PNG 25 | #' file here, to show instead of the R logo. This currently does not 26 | #' work on older Windows versions (before Windows 8), which does not 27 | #' allow PNG files, only ICO icons. 28 | #' 29 | #' @export 30 | #' @examples 31 | #' \dontrun{ 32 | #' notify("Hello world!") 33 | #' notify("Hello world!", title = "Introduction") 34 | #' notify("Hello world!", title = "Introduction", image = "mylogo.png") 35 | #' } 36 | 37 | notify <- function(msg, title = "R notification", image = NULL) { 38 | 39 | stopifnot( 40 | is.character(msg), 41 | is.character(title), length(title) == 1, 42 | is.null(image) || (is.character(image) && length(image) == 1) 43 | ) 44 | 45 | msg <- paste(msg, collapse = " ") 46 | 47 | switch( 48 | detect_os(), 49 | windows = notify_windows(msg, title, image), 50 | macos = notify_macos(msg, title, image), 51 | linux = notify_linux(msg, title, image) 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | 2 | windows_version <- (function() { 3 | 4 | version <- NULL 5 | 6 | function() { 7 | if (!is.null(version)) return(version) 8 | 9 | ver <- tryCatch( 10 | system("cmd /c ver", intern = TRUE, ignore.stderr = TRUE), 11 | error = function(e) NA_character_ 12 | ) 13 | 14 | if (identical(ver, NA_character_)) { 15 | version <<- ver 16 | return(ver) 17 | } 18 | 19 | ver <- paste(ver, collapse = "") 20 | ver <- sub("^.*(Version|\\\u7248\\\u672c)[ ]+([0-9\\\\.]+).*$", "\\2", ver, 21 | perl = TRUE, ignore.case = TRUE) 22 | ver <- package_version(ver) 23 | 24 | version <<- ver 25 | version 26 | } 27 | })() 28 | -------------------------------------------------------------------------------- /R/windows.R: -------------------------------------------------------------------------------- 1 | 2 | notify_windows <- function(msg, title, image) { 3 | 4 | ## toaster is nicer, but only works from windows 8 5 | if (windows_version() < "6.2.9200") { 6 | notify_notifu(msg, title, image) 7 | 8 | } else { 9 | notify_toaster(msg, title, image) 10 | } 11 | } 12 | 13 | bg_procs <- new.env(parent = emptyenv()) 14 | 15 | #' @importFrom processx process 16 | 17 | notify_notifu <- function(msg, title, image) { 18 | 19 | notifu <- system.file(package = packageName(), "notifu", "notifu.exe") 20 | 21 | if (!file.exists(notifu)) { 22 | stop("Cannot find notifu.exe executable, ", sQuote("notifier"), 23 | " installation is broken", call. = FALSE) 24 | } 25 | 26 | if (is.null(image)) { 27 | image <- normalizePath(system.file(package = packageName(), "R.ico")) 28 | } 29 | 30 | ## Unfortunately notifu takes a while to finish, so it is a bit harder 31 | ## to clean it up, because we need to run it in the background. 32 | ## We store the process objects in a list, within an environment in the 33 | ## package. Every time a new process is added we also check for process 34 | ## objects that have been in the list for a while, and remove them. 35 | 36 | args <- c("/m", msg, "/p", title, "/i", image) 37 | p <- process$new(notifu, args) 38 | 39 | now <- Sys.time() 40 | bg_procs$p <- c(bg_procs$p, list(list(proc = p, ts = now))) 41 | 42 | timeout <- as.difftime(5, units = "secs") 43 | rm <- Filter( 44 | function(i) now - bg_procs$p[[i]]$ts > timeout, 45 | seq_along(bg_procs$p) 46 | ) 47 | if (length(rm)) bg_procs$p <- bg_procs$p[-rm] 48 | 49 | invisible() 50 | } 51 | 52 | #' @importFrom processx run 53 | 54 | notify_toaster <- function(msg, title, image) { 55 | 56 | toaster <- system.file(package = packageName(), "toaster", "toast.exe") 57 | 58 | if (!file.exists(toaster)) { 59 | stop("Cannot find toast.exe executable, ", sQuote("notifier"), 60 | " installation is broken", call. = FALSE) 61 | } 62 | 63 | if (is.null(image)) { 64 | image <- normalizePath(system.file(package = packageName(), "R.png")) 65 | } 66 | 67 | args <- c("-m", msg, "-t", title, "-p", image) 68 | run(toaster, args, timeout = 2) 69 | 70 | invisible() 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # notifier 3 | 4 | > Cross Platform Desktop Notifications 5 | 6 | [![Linux Build Status](https://travis-ci.org/gaborcsardi/notifier.svg?branch=master)](https://travis-ci.org/gaborcsardi/notifier) 7 | [![Windows Build status](https://ci.appveyor.com/api/projects/status/github/gaborcsardi/notifier?svg=true)](https://ci.appveyor.com/project/gaborcsardi/notifier) 8 | [![](https://www.r-pkg.org/badges/version/notifier)](https://www.r-pkg.org/pkg/notifier) 9 | [![CRAN RStudio mirror downloads](https://cranlogs.r-pkg.org/badges/notifier)](https://www.r-pkg.org/pkg/notifier) 10 | 11 | Send desktop notifications from R, on macOS, Windows and Linux. 12 | 13 | ## Installation 14 | 15 | `notifier` has been [removed from the CRAN repository by request of the maintainer](https://cran.r-project.org/web/packages/notifier/index.html), but version 1.0.0 can still be installed from the CRAN archives with 16 | 17 | ```r 18 | install.packages("https://cran.r-project.org/src/contrib/Archive/notifier/notifier_1.0.0.tar.gz") 19 | ``` 20 | 21 | To install the latest version from GitHub, however, you may issue the following instead: 22 | 23 | ```r 24 | remotes::install_github("gaborcsardi/notifier") 25 | ``` 26 | 27 | `notifier` has no R package dependencies, and no system requirements 28 | other than the `notify-send` command line tool on Linux and other 29 | Unix-like systems. `notify-send` is available in the default installation 30 | on most Desktop Linux installations. 31 | 32 | ## Usage 33 | 34 | ```r 35 | library(notifier) 36 | notify( 37 | title = "15 Packages out of date", 38 | msg = c("You can run update.packages() to update them.", 39 | "Outdated packages: Boom colorspace desc memuse networkD3", 40 | "pbapply revealjs rgl rmdformats timevis and 5 more") 41 | ) 42 | ``` 43 | 44 | Linux 45 | Windows 8 or newer 46 | 47 | macOS 48 | Old Windows 49 | 50 | ## Thanks 51 | 52 | `notifier` uses various tools on the different platform: 53 | 54 | * On macOS, it uses `terminal-notifier`: 55 | https://github.com/julienXX/terminal-notifier 56 | * On Linux and other Unix-like systems it uses `notify-send` from 57 | `libnotify`. 58 | * On recent Windows systems it uses `toaster`: 59 | https://github.com/nels-o/toaster 60 | * Older Windows versions it uses `notifu`: 61 | https://www.paralint.com/projects/notifu 62 | 63 | ## License 64 | 65 | MIT © Gábor Csárdi 66 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # DO NOT CHANGE the "init" and "install" sections below 2 | 3 | # Download script file from GitHub 4 | init: 5 | ps: | 6 | $ErrorActionPreference = "Stop" 7 | Invoke-WebRequest http://raw.github.com/krlmlr/r-appveyor/master/scripts/appveyor-tool.ps1 -OutFile "..\appveyor-tool.ps1" 8 | Import-Module '..\appveyor-tool.ps1' 9 | 10 | install: 11 | ps: Bootstrap 12 | 13 | # Adapt as necessary starting from here 14 | 15 | build_script: 16 | - travis-tool.sh install_deps 17 | 18 | test_script: 19 | - travis-tool.sh run_tests 20 | 21 | on_failure: 22 | - travis-tool.sh dump_logs 23 | 24 | artifacts: 25 | - path: '*.Rcheck\**\*.log' 26 | name: Logs 27 | 28 | - path: '*.Rcheck\**\*.out' 29 | name: Logs 30 | 31 | - path: '*.Rcheck\**\*.fail' 32 | name: Logs 33 | 34 | - path: '*.Rcheck\**\*.Rout' 35 | name: Logs 36 | 37 | - path: '\*_*.tar.gz' 38 | name: Bits 39 | 40 | - path: '\*_*.zip' 41 | name: Bits 42 | -------------------------------------------------------------------------------- /inst/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleExecutable 6 | notifier 7 | CFBundleIconFile 8 | Terminal 9 | CFBundleIdentifier 10 | org.gaborcsardi.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.8.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 17 23 | LSMinimumSystemVersion 24 | ${MACOSX_DEPLOYMENT_TARGET} 25 | LSUIElement 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | 32 | NSHumanReadableCopyright 33 | Copyright © 2012-2017 Eloy Durán, Julien Blanchard and Valere Jeantet. All rights reserved. R package by Gábor Csárdi 34 | NSUserNotificationAlertStyle 35 | alert 36 | 37 | 38 | -------------------------------------------------------------------------------- /inst/R.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaborcsardi/notifier/3a87a62fd41ca06ae056650fb3fdacb6856f6d15/inst/R.ico -------------------------------------------------------------------------------- /inst/R.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaborcsardi/notifier/3a87a62fd41ca06ae056650fb3fdacb6856f6d15/inst/R.png -------------------------------------------------------------------------------- /inst/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaborcsardi/notifier/3a87a62fd41ca06ae056650fb3fdacb6856f6d15/inst/linux.png -------------------------------------------------------------------------------- /inst/macos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaborcsardi/notifier/3a87a62fd41ca06ae056650fb3fdacb6856f6d15/inst/macos.png -------------------------------------------------------------------------------- /inst/oldwindows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaborcsardi/notifier/3a87a62fd41ca06ae056650fb3fdacb6856f6d15/inst/oldwindows.png -------------------------------------------------------------------------------- /inst/windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaborcsardi/notifier/3a87a62fd41ca06ae056650fb3fdacb6856f6d15/inst/windows.png -------------------------------------------------------------------------------- /man/notify.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/notify.R 3 | \name{notify} 4 | \alias{notify} 5 | \title{Create a desktop notification} 6 | \usage{ 7 | notify(msg, title = "R notification", image = NULL) 8 | } 9 | \arguments{ 10 | \item{msg}{Message to show. It may contain newline characters.} 11 | 12 | \item{title}{Message title. Typically shown on the top, with 13 | larger font.} 14 | 15 | \item{image}{Image to show in the notification. By default the 16 | official R logo is shown on macOS, and an alternative logo is shown 17 | on other OSes, because of licensing reasons. You can specify a PNG 18 | file here, to show instead of the R logo. This currently does not 19 | work on older Windows versions (before Windows 8), which does not 20 | allow PNG files, only ICO icons.} 21 | } 22 | \description{ 23 | How exactly the notification appears is platform dependent: 24 | \itemize{ 25 | \item On macOS, we use the \code{terminal-notifier} tool, see 26 | https://github.com/julienXX/terminal-notifier 27 | \item On Linux and *BSD systems, including Solaris the \code{notify-send} 28 | command line tool is used. This requires the \code{libnotify-bin} 29 | package on Ubuntu/Debian and similar systems, or the \code{libnotify} 30 | package on RedHat/CentOS/Fedora and similar systems. 31 | \item On Windows 8 or newer Windows versions we use the \code{toaster} tool, 32 | see https://github.com/nels-o/toaster. 33 | \item On older Windows versions we use the \code{notifu} program, see 34 | https://www.paralint.com/projects/notifu. 35 | } 36 | } 37 | \details{ 38 | All notification systems support showing a title and an image, 39 | as part of the notification. 40 | } 41 | \examples{ 42 | \dontrun{ 43 | notify("Hello world!") 44 | notify("Hello world!", title = "Introduction") 45 | notify("Hello world!", title = "Introduction", image = "mylogo.png") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface AppDelegate : NSObject 4 | -(void)bye; 5 | @end 6 | -------------------------------------------------------------------------------- /src/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | #import 3 | #import 4 | 5 | NSString * const TerminalNotifierBundleID = @"org.gaborcsardi.notifier"; 6 | NSString * const NotificationCenterUIBundleID = @"com.apple.notificationcenterui"; 7 | 8 | #define contains(str1, str2) ([str1 rangeOfString: str2 ].location != NSNotFound) 9 | 10 | NSString *_fakeBundleIdentifier = nil; 11 | NSUserNotification *currentNotification = nil; 12 | 13 | @implementation NSBundle (FakeBundleIdentifier) 14 | 15 | // Overriding bundleIdentifier works, but overriding NSUserNotificationAlertStyle does not work. 16 | 17 | - (NSString *)__bundleIdentifier; 18 | { 19 | if (self == [NSBundle mainBundle]) { 20 | return _fakeBundleIdentifier ? _fakeBundleIdentifier : TerminalNotifierBundleID; 21 | } else { 22 | return [self __bundleIdentifier]; 23 | } 24 | } 25 | 26 | @end 27 | 28 | static BOOL 29 | InstallFakeBundleIdentifierHook() 30 | { 31 | Class class = objc_getClass("NSBundle"); 32 | if (class) { 33 | method_exchangeImplementations(class_getInstanceMethod(class, @selector(bundleIdentifier)), 34 | class_getInstanceMethod(class, @selector(__bundleIdentifier))); 35 | return YES; 36 | } 37 | return NO; 38 | } 39 | 40 | @implementation NSUserDefaults (SubscriptAndUnescape) 41 | - (id)objectForKeyedSubscript:(id)key; 42 | { 43 | id obj = [self objectForKey:key]; 44 | if ([obj isKindOfClass:[NSString class]] && [(NSString *)obj hasPrefix:@"\\"]) { 45 | obj = [(NSString *)obj substringFromIndex:1]; 46 | } 47 | return obj; 48 | } 49 | @end 50 | 51 | 52 | @implementation AppDelegate 53 | 54 | +(void)initializeUserDefaults 55 | { 56 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 57 | NSDictionary *appDefaults; 58 | appDefaults = @{@"sender": @"com.apple.Terminal"}; 59 | [defaults registerDefaults:appDefaults]; 60 | } 61 | 62 | - (void)applicationDidFinishLaunching:(NSNotification *)notification; 63 | { 64 | NSUserNotification *userNotification = notification.userInfo[NSApplicationLaunchUserNotificationKey]; 65 | if (userNotification) { 66 | [self userActivatedNotification:userNotification]; 67 | 68 | } else { 69 | 70 | NSArray *runningProcesses = [[[NSWorkspace sharedWorkspace] runningApplications] valueForKey:@"bundleIdentifier"]; 71 | if ([runningProcesses indexOfObject:NotificationCenterUIBundleID] == NSNotFound) { 72 | NSLog(@"[!] Unable to post a notification for the current user (%@), as it has no running NotificationCenter instance.", NSUserName()); 73 | exit(1); 74 | } 75 | 76 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 77 | 78 | NSString *subtitle = defaults[@"subtitle"]; 79 | NSString *message = defaults[@"message"]; 80 | NSString *remove = defaults[@"remove"]; 81 | NSString *list = defaults[@"list"]; 82 | NSString *sound = defaults[@"sound"]; 83 | 84 | // If there is no message and data is piped to the application, use that 85 | // instead. 86 | if (message == nil && !isatty(STDIN_FILENO)) { 87 | NSData *inputData = [NSData dataWithData:[[NSFileHandle fileHandleWithStandardInput] readDataToEndOfFile]]; 88 | message = [[NSString alloc] initWithData:inputData encoding:NSUTF8StringEncoding]; 89 | } 90 | 91 | if (message == nil && remove == nil && list == nil) { 92 | exit(1); 93 | } 94 | 95 | if (list) { 96 | [self listNotificationWithGroupID:list]; 97 | exit(0); 98 | } 99 | 100 | // Install the fake bundle ID hook so we can fake the sender. This also 101 | // needs to be done to be able to remove a message. 102 | if (defaults[@"sender"]) { 103 | @autoreleasepool { 104 | if (InstallFakeBundleIdentifierHook()) { 105 | _fakeBundleIdentifier = defaults[@"sender"]; 106 | } 107 | } 108 | } 109 | 110 | if (remove) { 111 | [self removeNotificationWithGroupID:remove]; 112 | if (message == nil || ([message length] == 0)) { 113 | exit(0); 114 | } 115 | } 116 | 117 | if (message) { 118 | NSMutableDictionary *options = [NSMutableDictionary dictionary]; 119 | if (defaults[@"activate"]) options[@"bundleID"] = defaults[@"activate"]; 120 | if (defaults[@"group"]) options[@"groupID"] = defaults[@"group"]; 121 | if (defaults[@"execute"]) options[@"command"] = defaults[@"execute"]; 122 | if (defaults[@"appIcon"]) options[@"appIcon"] = defaults[@"appIcon"]; 123 | if (defaults[@"contentImage"]) options[@"contentImage"] = defaults[@"contentImage"]; 124 | if (defaults[@"closeLabel"]) options[@"closeLabel"] = defaults[@"closeLabel"]; 125 | if (defaults[@"dropdownLabel"]) options[@"dropdownLabel"] = defaults[@"dropdownLabel"]; 126 | if (defaults[@"actions"]) options[@"actions"] = defaults[@"actions"]; 127 | 128 | if([[[NSProcessInfo processInfo] arguments] containsObject:@"-reply"] == true) { 129 | options[@"reply"] = @"Reply"; 130 | if (defaults[@"reply"]) options[@"reply"] = defaults[@"reply"]; 131 | } 132 | 133 | options[@"output"] = @"outputEvent"; 134 | if([[[NSProcessInfo processInfo] arguments] containsObject:@"-json"] == true) { 135 | options[@"output"] = @"json"; 136 | } 137 | 138 | options[@"uuid"] = [NSString stringWithFormat:@"%ld", self.hash]; 139 | options[@"timeout"] = defaults[@"timeout"] ? defaults[@"timeout"] : @"0"; 140 | 141 | if (options[@"reply"] || defaults[@"timeout"] || defaults[@"actions"] || defaults[@"execute"] || defaults[@"open"] || options[@"bundleID"]) options[@"waitForResponse"] = @YES; 142 | 143 | if (defaults[@"open"]) { 144 | NSURL *url = [NSURL URLWithString:defaults[@"open"]]; 145 | if ((url && url.scheme && url.host) || [url isFileURL]) { 146 | options[@"open"] = defaults[@"open"]; 147 | }else{ 148 | NSLog(@"'%@' is not a valid URI.", defaults[@"open"]); 149 | exit(1); 150 | } 151 | } 152 | 153 | options[@"uuid"] = [NSString stringWithFormat:@"%ld", self.hash]; 154 | 155 | [self deliverNotificationWithTitle:defaults[@"title"] ?: @"Terminal" 156 | subtitle:subtitle 157 | message:message 158 | options:options 159 | sound:sound]; 160 | } 161 | } 162 | } 163 | 164 | - (NSImage*)getImageFromURL:(NSString *) url; 165 | { 166 | NSURL *imageURL = [NSURL URLWithString:url]; 167 | if([[imageURL scheme] length] == 0){ 168 | // Prefix 'file://' if no scheme 169 | imageURL = [NSURL fileURLWithPath:url]; 170 | } 171 | return [[NSImage alloc] initWithContentsOfURL:imageURL]; 172 | } 173 | 174 | - (void)deliverNotificationWithTitle:(NSString *)title 175 | subtitle:(NSString *)subtitle 176 | message:(NSString *)message 177 | options:(NSDictionary *)options 178 | sound:(NSString *)sound; 179 | { 180 | // First remove earlier notification with the same group ID. 181 | if (options[@"groupID"]) [self removeNotificationWithGroupID:options[@"groupID"]]; 182 | 183 | NSUserNotification *userNotification = [NSUserNotification new]; 184 | userNotification.title = title; 185 | userNotification.subtitle = subtitle; 186 | userNotification.informativeText = message; 187 | userNotification.userInfo = options; 188 | 189 | if(options[@"appIcon"]){ 190 | // replacement app icon 191 | [userNotification setValue:[self getImageFromURL:options[@"appIcon"]] forKey:@"_identityImage"]; 192 | [userNotification setValue:@(false) forKey:@"_identityImageHasBorder"]; 193 | } 194 | if(options[@"contentImage"]){ 195 | // content image 196 | userNotification.contentImage = [self getImageFromURL:options[@"contentImage"]]; 197 | } 198 | // Actions 199 | if (options[@"actions"]){ 200 | [userNotification setValue:@YES forKey:@"_showsButtons"]; 201 | NSArray *myActions = [options[@"actions"] componentsSeparatedByString:@","]; 202 | if (myActions.count > 1) { 203 | [userNotification setValue:@YES forKey:@"_alwaysShowAlternateActionMenu"]; 204 | [userNotification setValue:myActions forKey:@"_alternateActionButtonTitles"]; 205 | 206 | //Main Actions Title 207 | if(options[@"dropdownLabel"]){ 208 | userNotification.actionButtonTitle = options[@"dropdownLabel"]; 209 | userNotification.hasActionButton = true; 210 | } 211 | }else{ 212 | userNotification.actionButtonTitle = options[@"actions"]; 213 | } 214 | }else if (options[@"reply"]) { 215 | [userNotification setValue:@YES forKey:@"_showsButtons"]; 216 | userNotification.hasReplyButton = 1; 217 | userNotification.responsePlaceholder = options[@"reply"]; 218 | } 219 | 220 | // Close button 221 | if(options[@"closeLabel"]){ 222 | userNotification.otherButtonTitle = options[@"closeLabel"]; 223 | } 224 | 225 | if (sound != nil) { 226 | userNotification.soundName = [sound isEqualToString: @"default"] ? NSUserNotificationDefaultSoundName : sound; 227 | } 228 | 229 | NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter]; 230 | center.delegate = self; 231 | [center deliverNotification:userNotification]; 232 | } 233 | 234 | - (void)removeNotificationWithGroupID:(NSString *)groupID; 235 | { 236 | NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter]; 237 | for (NSUserNotification *userNotification in center.deliveredNotifications) { 238 | if ([@"ALL" isEqualToString:groupID] || [userNotification.userInfo[@"groupID"] isEqualToString:groupID]) { 239 | [center removeDeliveredNotification:userNotification]; 240 | } 241 | } 242 | } 243 | 244 | - (void)userActivatedNotification:(NSUserNotification *)userNotification; 245 | { 246 | [[NSUserNotificationCenter defaultUserNotificationCenter] removeDeliveredNotification:userNotification]; 247 | 248 | NSString *bundleID = userNotification.userInfo[@"bundleID"]; 249 | NSString *command = userNotification.userInfo[@"command"]; 250 | NSString *open = userNotification.userInfo[@"open"]; 251 | 252 | if (bundleID) [self activateAppWithBundleID:bundleID]; 253 | if (command) [self executeShellCommand:command]; 254 | if (open) [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:open]]; 255 | } 256 | 257 | - (BOOL)activateAppWithBundleID:(NSString *)bundleID; 258 | { 259 | id app = [SBApplication applicationWithBundleIdentifier:bundleID]; 260 | if (app) { 261 | [app activate]; 262 | return YES; 263 | 264 | } else { 265 | NSLog(@"Unable to find an application with the specified bundle indentifier."); 266 | return NO; 267 | } 268 | } 269 | 270 | - (BOOL)executeShellCommand:(NSString *)command; 271 | { 272 | NSPipe *pipe = [NSPipe pipe]; 273 | NSFileHandle *fileHandle = [pipe fileHandleForReading]; 274 | 275 | NSTask *task = [NSTask new]; 276 | task.launchPath = @"/bin/sh"; 277 | task.arguments = @[@"-c", command]; 278 | task.standardOutput = pipe; 279 | task.standardError = pipe; 280 | [task launch]; 281 | 282 | NSData *data = nil; 283 | NSMutableData *accumulatedData = [NSMutableData data]; 284 | while ((data = [fileHandle availableData]) && [data length]) { 285 | [accumulatedData appendData:data]; 286 | } 287 | 288 | [task waitUntilExit]; 289 | NSLog(@"command output:\n%@", [[NSString alloc] initWithData:accumulatedData encoding:NSUTF8StringEncoding]); 290 | return [task terminationStatus] == 0; 291 | } 292 | 293 | - (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center 294 | shouldPresentNotification:(NSUserNotification *)notification; 295 | { 296 | return YES; 297 | } 298 | 299 | // Once the notification is delivered we can exit. (Only if no actions or reply) 300 | - (void)userNotificationCenter:(NSUserNotificationCenter *)center 301 | didDeliverNotification:(NSUserNotification *)userNotification; 302 | { 303 | if (!userNotification.userInfo[@"waitForResponse"]) exit(0); 304 | 305 | currentNotification = userNotification; 306 | 307 | dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), 308 | ^{ 309 | __block BOOL notificationStillPresent; 310 | do { 311 | @autoreleasepool { 312 | notificationStillPresent = NO; 313 | for (NSUserNotification *nox in [[NSUserNotificationCenter defaultUserNotificationCenter] deliveredNotifications]) { 314 | if ([nox.userInfo[@"uuid"] isEqualToString:[NSString stringWithFormat:@"%ld", self.hash] ]) notificationStillPresent = YES; 315 | } 316 | if (notificationStillPresent) [NSThread sleepForTimeInterval:0.20f]; 317 | } 318 | } while (notificationStillPresent); 319 | 320 | dispatch_async(dispatch_get_main_queue(), ^{ 321 | NSDictionary *udict = @{@"activationType" : @"closed", @"activationValue" : userNotification.otherButtonTitle}; 322 | [self Quit:udict notification:userNotification]; 323 | exit(0); 324 | }); 325 | }); 326 | 327 | if ([userNotification.userInfo[@"timeout"] integerValue] > 0){ 328 | dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), 329 | ^{ 330 | [NSThread sleepForTimeInterval:[userNotification.userInfo[@"timeout"] integerValue]]; 331 | [center removeDeliveredNotification:currentNotification]; 332 | [center removeDeliveredNotification:userNotification]; 333 | NSDictionary *udict = @{@"activationType" : @"timeout"}; 334 | [self Quit:udict notification:userNotification]; 335 | exit(0); 336 | }); 337 | } 338 | } 339 | 340 | - (void)userNotificationCenter:(NSUserNotificationCenter *)center 341 | didActivateNotification:(NSUserNotification *)notification { 342 | 343 | if ([notification.userInfo[@"uuid"] isNotEqualTo:[NSString stringWithFormat:@"%ld", self.hash] ]) { 344 | return; 345 | }; 346 | 347 | unsigned long long additionalActionIndex = ULLONG_MAX; 348 | 349 | NSString *ActionsClicked = @""; 350 | switch (notification.activationType) { 351 | case NSUserNotificationActivationTypeAdditionalActionClicked: 352 | case NSUserNotificationActivationTypeActionButtonClicked: 353 | if ([[(NSObject*)notification valueForKey:@"_alternateActionButtonTitles"] count] > 1){ 354 | NSNumber *alternateActionIndex = [(NSObject*)notification valueForKey:@"_alternateActionIndex"]; 355 | additionalActionIndex = [alternateActionIndex unsignedLongLongValue]; 356 | ActionsClicked = [(NSObject*)notification valueForKey:@"_alternateActionButtonTitles"][additionalActionIndex]; 357 | 358 | NSDictionary *udict = @{@"activationType" : @"actionClicked", @"activationValue" : ActionsClicked, @"activationValueIndex" :[NSString stringWithFormat:@"%llu", additionalActionIndex]}; 359 | [self Quit:udict notification:notification]; 360 | }else{ 361 | NSDictionary *udict = @{@"activationType" : @"actionClicked", @"activationValue" : notification.actionButtonTitle}; 362 | [self Quit:udict notification:notification]; 363 | } 364 | break; 365 | 366 | case NSUserNotificationActivationTypeContentsClicked: 367 | [self userActivatedNotification:notification]; 368 | [self Quit:@{@"activationType" : @"contentsClicked"} notification:notification]; 369 | break; 370 | 371 | case NSUserNotificationActivationTypeReplied: 372 | [self Quit:@{@"activationType" : @"replied",@"activationValue":notification.response.string} notification:notification]; 373 | break; 374 | case NSUserNotificationActivationTypeNone: 375 | default: 376 | [self Quit:@{@"activationType" : @"none"} notification:notification]; 377 | break; 378 | } 379 | 380 | [center removeDeliveredNotification:notification]; 381 | [center removeDeliveredNotification:currentNotification]; 382 | exit(0); 383 | } 384 | 385 | - (BOOL)Quit:(NSDictionary *)udict notification:(NSUserNotification *)notification; 386 | { 387 | if ([notification.userInfo[@"output"] isEqualToString:@"outputEvent"]) { 388 | if ([udict[@"activationType"] isEqualToString:@"closed"]) { 389 | if ([udict[@"activationValue"] isEqualToString:@""]) { 390 | NSLog(@"@CLOSED"); 391 | }else{ 392 | NSLog(@"@%s", [udict[@"activationValue"] UTF8String]); 393 | } 394 | } else if ([udict[@"activationType"] isEqualToString:@"timeout"]) { 395 | NSLog(@"@TIMEOUT"); 396 | } else if ([udict[@"activationType"] isEqualToString:@"contentsClicked"]) { 397 | NSLog(@"@CONTENTCLICKED"); 398 | } else{ 399 | if ([udict[@"activationValue"] isEqualToString:@""]) { 400 | NSLog(@"@ACTIONCLICKED"); 401 | }else{ 402 | NSLog(@"@%s", [udict[@"activationValue"] UTF8String]); 403 | } 404 | } 405 | 406 | return 1; 407 | } 408 | 409 | NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; 410 | dateFormatter.dateFormat = @"yyyy-MM-dd HH:mm:ss Z"; 411 | 412 | // Dictionary with several key/value pairs and the above array of arrays 413 | NSMutableDictionary *dict = [NSMutableDictionary dictionary]; 414 | [dict addEntriesFromDictionary:udict]; 415 | [dict setValue:[dateFormatter stringFromDate:notification.actualDeliveryDate] forKey:@"deliveredAt"]; 416 | [dict setValue:[dateFormatter stringFromDate:[NSDate new]] forKey:@"activationAt"]; 417 | 418 | NSError *error = nil; 419 | NSData *json; 420 | 421 | // Dictionary convertable to JSON ? 422 | if ([NSJSONSerialization isValidJSONObject:dict]) 423 | { 424 | // Serialize the dictionary 425 | json = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:&error]; 426 | 427 | // If no errors, let's view the JSON 428 | if (json != nil && error == nil) 429 | { 430 | NSString *jsonString = [[NSString alloc] initWithData:json encoding:NSUTF8StringEncoding]; 431 | printf("%s", [jsonString cStringUsingEncoding:NSUTF8StringEncoding]); 432 | } 433 | } 434 | 435 | return 1; 436 | } 437 | 438 | - (void)listNotificationWithGroupID:(NSString *)listGroupID; 439 | { 440 | NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter]; 441 | 442 | NSMutableArray *lines = [NSMutableArray array]; 443 | for (NSUserNotification *userNotification in center.deliveredNotifications) { 444 | NSString *deliveredgroupID = userNotification.userInfo[@"groupID"]; 445 | NSString *title = userNotification.title; 446 | NSString *subtitle = userNotification.subtitle; 447 | NSString *message = userNotification.informativeText; 448 | NSString *deliveredAt = [userNotification.actualDeliveryDate description]; 449 | 450 | if ([@"ALL" isEqualToString:listGroupID] || [deliveredgroupID isEqualToString:listGroupID]) { 451 | NSMutableDictionary *dict = [NSMutableDictionary dictionary]; 452 | [dict setValue:deliveredgroupID forKey:@"GroupID"]; 453 | [dict setValue:title forKey:@"Title"]; 454 | [dict setValue:subtitle forKey:@"subtitle"]; 455 | [dict setValue:message forKey:@"message"]; 456 | [dict setValue:deliveredAt forKey:@"deliveredAt"]; 457 | [lines addObject:dict]; 458 | } 459 | } 460 | 461 | if (lines.count > 0) { 462 | NSData *json; 463 | NSError *error = nil; 464 | // Dictionary convertable to JSON ? 465 | if ([NSJSONSerialization isValidJSONObject:lines]) 466 | { 467 | // Serialize the dictionary 468 | json = [NSJSONSerialization dataWithJSONObject:lines options:NSJSONWritingPrettyPrinted error:&error]; 469 | 470 | // If no errors, let's view the JSON 471 | if (json != nil && error == nil) 472 | { 473 | NSString *jsonString = [[NSString alloc] initWithData:json encoding:NSUTF8StringEncoding]; 474 | printf("%s", [jsonString cStringUsingEncoding:NSUTF8StringEncoding]); 475 | } 476 | } 477 | 478 | } 479 | } 480 | 481 | - (void) bye; { 482 | //Look for the notification sent, remove it when found 483 | NSString *UUID = currentNotification.userInfo[@"uuid"] ; 484 | for (NSUserNotification *nox in [[NSUserNotificationCenter defaultUserNotificationCenter] deliveredNotifications]) { 485 | if ([nox.userInfo[@"uuid"] isEqualToString:UUID ]){ 486 | [[NSUserNotificationCenter defaultUserNotificationCenter] removeDeliveredNotification:nox] ; 487 | [[NSUserNotificationCenter defaultUserNotificationCenter] removeDeliveredNotification:nox] ; 488 | } 489 | } 490 | } 491 | 492 | @end 493 | -------------------------------------------------------------------------------- /src/Makevars: -------------------------------------------------------------------------------- 1 | 2 | OBJECTS = dummy.o 3 | 4 | .PHONY: all clean 5 | 6 | all: macnotifier $(SHLIB) 7 | 8 | macnotifier: main.m AppDelegate.m 9 | $(CC) $(CFLAGS) main.m AppDelegate.m -o $@ \ 10 | -framework AppKit -framework ScriptingBridge 11 | 12 | clean: 13 | rm -rf macnotifier $(SHLIB) $(OBJECTS) 14 | -------------------------------------------------------------------------------- /src/dummy.c: -------------------------------------------------------------------------------- 1 | 2 | void R_notifier_dummy() { } 3 | -------------------------------------------------------------------------------- /src/install.libs.R: -------------------------------------------------------------------------------- 1 | 2 | # Copy macOS binary to correct location 3 | execs <- c("macnotifier", "../inst/Info.plist") 4 | 5 | dest <- file.path(R_PACKAGE_DIR, paste0("macos", R_ARCH)) 6 | dir.create(dest, recursive = TRUE, showWarnings = FALSE) 7 | file.copy(execs, dest, overwrite = TRUE) 8 | -------------------------------------------------------------------------------- /src/main.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | AppDelegate *appDelegate ; 4 | 5 | void SIGTERM_handler(int signum) { 6 | [appDelegate bye]; 7 | exit(EXIT_FAILURE); 8 | } 9 | 10 | int main(int argc, char *argv[]) 11 | { 12 | signal(SIGTERM, SIGTERM_handler); 13 | signal(SIGINT, SIGTERM_handler); 14 | 15 | NSApplication *application = [NSApplication sharedApplication]; 16 | appDelegate = [AppDelegate new]; 17 | 18 | [application setDelegate:appDelegate]; 19 | [application run]; 20 | 21 | return EXIT_SUCCESS; 22 | } 23 | --------------------------------------------------------------------------------