From 1a97dd20fd0dd0bc898364570b516d729fbcd869 Mon Sep 17 00:00:00 2001
From: Gabriel Handford
Date: Wed, 11 May 2016 20:01:34 -0700
Subject: Alert style notifications (for OS X)
- Embeds Info.plist with alert style default (in script)
- Includes timeout
- Outputs action
---
Info.plist | 37 ++++++++++++++++++++++
README.md | 7 +++++
build_darwin.sh | 20 ++++++++++++
corefoundation.go | 14 +++++++++
notifier.go | 11 +++++--
notifier/notifier.go | 6 ++++
notifier_darwin.go | 17 +++-------
notifier_darwin.m | 88 +++++++++++++++++++++++++++++++++++++++++++++++-----
8 files changed, 176 insertions(+), 24 deletions(-)
create mode 100644 Info.plist
create mode 100755 build_darwin.sh
diff --git a/Info.plist b/Info.plist
new file mode 100644
index 0000000..653d4f2
--- /dev/null
+++ b/Info.plist
@@ -0,0 +1,37 @@
+
+
+
+
+ CFBundleExecutable
+ notifier
+ CFBundleIconFile
+ Terminal
+ CFBundleIdentifier
+ keybase.notifier
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ notifier
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1
+ LSMinimumSystemVersion
+ 10.9.0
+ LSUIElement
+
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
+ NSHumanReadableCopyright
+ Copyright © 2016 Keybase
+ NSUserNotificationAlertStyle
+ alert
+
+
diff --git a/README.md b/README.md
index a98417c..a6b3eb3 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,11 @@ For Linux, we use [notify-send](http://man.cx/notify-send).
go install github.com/keybase/go-notifier/notifier
```
+### Alerts
+
+If you need alert style (actionable) notifications (on OS X), you need to include an Info.plist
+in the binary and sign it. You can look at `build_darwin.sh` on how to do this.
+
### Resources
Follows similar requirements of [node-notifier](https://github.com/mikaelbr/node-notifier),
@@ -26,3 +31,5 @@ this implementation uses cgo to talk directly to NSUserNotificationCenter APIs.
but a cgo implementation was preferable.
The [0xAX/notificator](https://github.com/0xAX/notificator) only supports growlnotify on Windows and OS X.
+
+The [vjeantet/alerter](https://github.com/vjeantet/alerter) app allows you to use alert style notifications on OS X.
diff --git a/build_darwin.sh b/build_darwin.sh
new file mode 100755
index 0000000..0b3d662
--- /dev/null
+++ b/build_darwin.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+
+set -e -u -o pipefail # Fail on error
+
+dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+cd "$dir"
+
+output="$GOPATH/bin/notifier"
+codesignid=${CODESIGNID:-"Developer ID Application: Keybase, Inc. (99229SGT5K)"}
+
+echo "Building"
+go build -ldflags "-s" -o "$output" ./notifier
+echo "Code signing"
+codesign --verbose --force --sign "$codesignid" "$output"
+
+#echo "Checking plist"
+#otool -X -s __TEXT __info_plist "$output" | xxd -r
+
+echo "Checking codesign"
+codesign --verify --verbose=4 "$output"
diff --git a/corefoundation.go b/corefoundation.go
index e571ecb..ebec7a4 100644
--- a/corefoundation.go
+++ b/corefoundation.go
@@ -148,6 +148,20 @@ func CFArrayToArray(cfArray C.CFArrayRef) (a []C.CFTypeRef) {
return
}
+// StringsToCFArray converts strings to CFArrayRef
+func StringsToCFArray(strs []string) C.CFArrayRef {
+ strRefs := []C.CFTypeRef{}
+ for _, s := range strs {
+ strRef, err := StringToCFString(s)
+ if err != nil {
+ return nil
+ }
+ defer Release(C.CFTypeRef(strRef))
+ strRefs = append(strRefs, C.CFTypeRef(strRef))
+ }
+ return ArrayToCFArray(strRefs)
+}
+
// Convertable knows how to convert an instance to a CFTypeRef.
type Convertable interface {
Convert() (C.CFTypeRef, error)
diff --git a/notifier.go b/notifier.go
index 35f3e0e..daf077d 100644
--- a/notifier.go
+++ b/notifier.go
@@ -8,9 +8,14 @@ type Notification struct {
Title string
Message string
ImagePath string
- BundleID string // For darwin
- Actions []string // For darwin
- ToastPath string // For windows (Toaster)
+
+ // For darwin
+ Actions []string
+ Timeout float64
+ BundleID string
+
+ // For windows
+ ToastPath string // Path to toast.exe
}
// Notifier knows how to deliver a notification
diff --git a/notifier/notifier.go b/notifier/notifier.go
index bc8f517..169a987 100644
--- a/notifier/notifier.go
+++ b/notifier/notifier.go
@@ -18,9 +18,15 @@ func main() {
kingpin.Flag("title", "Title").StringVar(¬ification.Title)
kingpin.Flag("message", "Message").StringVar(¬ification.Message)
kingpin.Flag("image-path", "Image path").StringVar(¬ification.ImagePath)
+
+ // OS X
kingpin.Flag("action", "Actions (for OS X)").StringsVar(¬ification.Actions)
+ kingpin.Flag("timeout", "Timeout in seconds (for OS X)").Float64Var(¬ification.Timeout)
kingpin.Flag("bundle-id", "Bundle identifier (for OS X)").StringVar(¬ification.BundleID)
+
+ // Windows
kingpin.Flag("toast-path", "Path to toast.exe (for Windows)").StringVar(¬ification.ToastPath)
+
kingpin.Version("0.1.2")
kingpin.Parse()
diff --git a/notifier_darwin.go b/notifier_darwin.go
index 283c329..4287bd5 100644
--- a/notifier_darwin.go
+++ b/notifier_darwin.go
@@ -5,9 +5,9 @@ package notifier
/*
#cgo CFLAGS: -x objective-c
-#cgo LDFLAGS: -framework Cocoa
+#cgo LDFLAGS: -framework Cocoa -sectcreate __TEXT __info_plist Info.plist
#import
-extern CFStringRef deliverNotification(CFStringRef title, CFStringRef subtitle, CFStringRef message, CFStringRef appIconURLString, CFArrayRef actions, CFStringRef groupID, CFStringRef bundleID);
+extern CFStringRef deliverNotification(CFStringRef title, CFStringRef subtitle, CFStringRef message, CFStringRef appIconURLString, CFArrayRef actions, CFStringRef groupID, CFStringRef bundleID, NSTimeInterval timeout);
*/
import "C"
import "fmt"
@@ -51,19 +51,10 @@ func (n darwinNotifier) DeliverNotification(notification Notification) error {
defer Release(C.CFTypeRef(appIconURLStringRef))
}
- actions := []C.CFTypeRef{}
- for _, action := range notification.Actions {
- actionRef, err := StringToCFString(action)
- if err != nil {
- return err
- }
- defer Release(C.CFTypeRef(actionRef))
- actions = append(actions, C.CFTypeRef(actionRef))
- }
- actionsRef := ArrayToCFArray(actions)
+ actionsRef := StringsToCFArray(notification.Actions)
defer Release(C.CFTypeRef(actionsRef))
- C.deliverNotification(titleRef, nil, messageRef, appIconURLStringRef, actionsRef, bundleIDRef, bundleIDRef)
+ C.deliverNotification(titleRef, nil, messageRef, appIconURLStringRef, actionsRef, bundleIDRef, nil, C.NSTimeInterval(notification.Timeout))
return nil
}
diff --git a/notifier_darwin.m b/notifier_darwin.m
index 6e46b15..6e4c5ac 100644
--- a/notifier_darwin.m
+++ b/notifier_darwin.m
@@ -2,6 +2,7 @@
// this source code is governed by the included BSD license.
// Modified from https://github.com/julienXX/terminal-notifier
+// Modified from https://github.com/vjeantet/alerter
#import
#import
@@ -27,10 +28,12 @@ static BOOL installFakeBundleIdentifierHook() {
}
@interface NotificationDelegate : NSObject
+@property NSTimeInterval timeout;
+@property (retain) NSString *uuid;
@end
CFStringRef deliverNotification(CFStringRef titleRef, CFStringRef subtitleRef, CFStringRef messageRef, CFStringRef appIconURLStringRef,
- CFArrayRef actionsRef, CFStringRef bundleIDRef, CFStringRef groupIDRef) {
+ CFArrayRef actionsRef, CFStringRef bundleIDRef, CFStringRef groupIDRef, NSTimeInterval timeout) {
if (bundleIDRef) {
_fakeBundleIdentifier = (NSString *)bundleIDRef;
@@ -58,26 +61,95 @@ CFStringRef deliverNotification(CFStringRef titleRef, CFStringRef subtitleRef, C
}
NSArray *actions = (NSArray *)actionsRef;
if ([actions count] >= 1) {
- userNotification.actionButtonTitle = [actions objectAtIndex:0];
[userNotification setValue:@YES forKey:@"_showsButtons"];
- }
- if ([actions count] >= 2) {
- userNotification.otherButtonTitle = [actions objectAtIndex:1];
+ if ([actions count] >= 2) {
+ [userNotification setValue:@YES forKey:@"_alwaysShowAlternateActionMenu"];
+ [userNotification setValue:actions forKey:@"_alternateActionButtonTitles"];
+ } else {
+ userNotification.actionButtonTitle = [actions objectAtIndex:0];
+ }
}
NSUserNotificationCenter *userNotificationCenter = [NSUserNotificationCenter defaultUserNotificationCenter];
//NSLog(@"Deliver: %@", userNotification);
- userNotificationCenter.delegate = [[NotificationDelegate alloc] init];
- [userNotificationCenter scheduleNotification:userNotification];
+ NotificationDelegate *delegate = [[NotificationDelegate alloc] init];
+ delegate.timeout = timeout;
+ delegate.uuid = uuid;
+ userNotificationCenter.delegate = delegate;
+ [userNotificationCenter deliverNotification:userNotification];
+
[[NSRunLoop mainRunLoop] run];
+
return nil;
}
@implementation NotificationDelegate
+
- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)userNotification {
return YES;
}
+
+- (void)remove:(NSUserNotification *)userNotification center:(NSUserNotificationCenter *)center {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [center removeDeliveredNotification:userNotification];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ fflush(stdout);
+ fflush(stderr);
+ exit(0);
+ });
+ });
+}
+
+- (NSString *)JSON:(NSDictionary *)dict {
+ NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil];
+ if (!jsonData) return @"";
+ return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
+}
+
- (void)userNotificationCenter:(NSUserNotificationCenter *)center didDeliverNotification:(NSUserNotification *)userNotification {
- exit(0);
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ NSDate *start = [NSDate date];
+ while (-[start timeIntervalSinceNow] < self.timeout) {
+ bool found = NO;
+ for (NSUserNotification *deliveredNotification in [[NSUserNotificationCenter defaultUserNotificationCenter] deliveredNotifications]) {
+ if ([deliveredNotification.userInfo[@"uuid"] isEqual:self.uuid]) {
+ [NSThread sleepForTimeInterval:0.5];
+ found = YES;
+ break;
+ }
+ }
+ if (!found) break;
+ }
+ [self remove:userNotification center:center];
+ });
+}
+
+- (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)userNotification {
+ switch (userNotification.activationType) {
+ case NSUserNotificationActivationTypeAdditionalActionClicked:
+ case NSUserNotificationActivationTypeActionButtonClicked: {
+ NSString *action = nil;
+ if ([[(NSObject*)userNotification valueForKey:@"_alternateActionButtonTitles"] count] > 1) {
+ NSNumber *alternateActionIndex = [(NSObject*)userNotification valueForKey:@"_alternateActionIndex"];
+ int actionIndex = [alternateActionIndex intValue];
+ action = [(NSObject*)userNotification valueForKey:@"_alternateActionButtonTitles"][actionIndex];
+ } else {
+ action = userNotification.actionButtonTitle;
+ }
+ NSLog(@"%@", [self JSON:@{@"action": action}]);
+ break;
+ }
+ case NSUserNotificationActivationTypeContentsClicked:
+ //NSLog(@"contents");
+ break;
+ case NSUserNotificationActivationTypeReplied:
+ //NSLog(@"replied");
+ break;
+ case NSUserNotificationActivationTypeNone:
+ //NSLog(@"none");
+ break;
+ }
+ [self remove:userNotification center:center];
}
+
@end
--
cgit v1.2.3