diff options
| author | Gabriel Handford | 2016-05-11 20:01:34 -0700 |
|---|---|---|
| committer | Gabriel Handford | 2016-05-11 20:01:34 -0700 |
| commit | 1a97dd20fd0dd0bc898364570b516d729fbcd869 (patch) | |
| tree | 2bd7948cccbff60e6f84ac588aabad8ebab5d8c2 | |
| parent | bab68cd21095985c5d867e44f0844ab4be7c2a95 (diff) | |
| download | go-notifier-1a97dd20fd0dd0bc898364570b516d729fbcd869.tar.bz2 | |
Alert style notifications (for OS X)
- Embeds Info.plist with alert style default (in script)
- Includes timeout
- Outputs action
| -rw-r--r-- | Info.plist | 37 | ||||
| -rw-r--r-- | README.md | 7 | ||||
| -rwxr-xr-x | build_darwin.sh | 20 | ||||
| -rw-r--r-- | corefoundation.go | 14 | ||||
| -rw-r--r-- | notifier.go | 11 | ||||
| -rw-r--r-- | notifier/notifier.go | 6 | ||||
| -rw-r--r-- | notifier_darwin.go | 17 | ||||
| -rw-r--r-- | notifier_darwin.m | 88 |
8 files changed, 176 insertions, 24 deletions
diff --git a/Info.plist b/Info.plist new file mode 100644 index 0000000..653d4f2 --- /dev/null +++ b/Info.plist @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleExecutable</key> + <string>notifier</string> + <key>CFBundleIconFile</key> + <string>Terminal</string> + <key>CFBundleIdentifier</key> + <string>keybase.notifier</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>notifier</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>1.0.0</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>1</string> + <key>LSMinimumSystemVersion</key> + <string>10.9.0</string> + <key>LSUIElement</key> + <true/> + <key>NSAppTransportSecurity</key> + <dict> + <key>NSAllowsArbitraryLoads</key> + <true/> + </dict> + <key>NSHumanReadableCopyright</key> + <string>Copyright © 2016 Keybase</string> + <key>NSUserNotificationAlertStyle</key> + <string>alert</string> +</dict> +</plist> @@ -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 <Cocoa/Cocoa.h> -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 <Cocoa/Cocoa.h> #import <objc/runtime.h> @@ -27,10 +28,12 @@ static BOOL installFakeBundleIdentifierHook() { } @interface NotificationDelegate : NSObject <NSUserNotificationCenterDelegate> +@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 |
