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