aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGabriel Handford2016-05-06 15:20:54 -0700
committerGabriel Handford2016-05-06 15:57:46 -0700
commit12878f6400420fa6c988187c8942799b73c31e6c (patch)
tree4c134f3bbf0ea6ebd4f3b8cdad6bf27ec903da4d
downloadgo-notifier-12878f6400420fa6c988187c8942799b73c31e6c.tar.bz2
Importing
-rw-r--r--.pre-commit-config.yaml11
-rw-r--r--LICENSE22
-rw-r--r--README.md28
-rw-r--r--corefoundation.go343
-rw-r--r--notifier.go19
-rw-r--r--notifier/notifier.go48
-rw-r--r--notifier_darwin.go57
-rw-r--r--notifier_darwin.m86
-rw-r--r--notifier_linux.go35
-rw-r--r--notifier_windows.go44
-rwxr-xr-xtoaster/Microsoft.WindowsAPICodePack.Shell.dllbin0 -> 542208 bytes
-rwxr-xr-xtoaster/Microsoft.WindowsAPICodePack.dllbin0 -> 104960 bytes
-rw-r--r--toaster/README.md34
-rwxr-xr-xtoaster/toast.exebin0 -> 14848 bytes
14 files changed, 727 insertions, 0 deletions
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..027e246
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,11 @@
+- repo: https://github.com/gabriel/pre-commit-golang
+ sha: c02a81d85a5295886022b8106c367518e6c3760e
+ hooks:
+ - id: go-fmt
+ - id: go-metalinter
+ args:
+ - --exclude=corefoundation.go
+ - --deadline=60s
+ - --vendor
+ - --cyclo-over=20
+ - --dupl-threshold=100
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..2d54c65
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Keybase
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a98417c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,28 @@
+## go-notifier
+
+Cross platform system notifications in go (golang).
+
+### Platforms
+
+For OS X, we use NSUserNotificationCenter APIs from cgo. This only supports OS X 10.9 and above.
+
+For Windows, we use [toaster](https://github.com/nels-o/toaster). This only supports Windows 8 and above.
+
+For Linux, we use [notify-send](http://man.cx/notify-send).
+
+### Install
+
+```sh
+go install github.com/keybase/go-notifier/notifier
+```
+
+### Resources
+
+Follows similar requirements of [node-notifier](https://github.com/mikaelbr/node-notifier),
+but only supports recent platform versions.
+
+Instead of [deckarep/gosx-notifier](https://github.com/deckarep/gosx-notifier), which uses an embedded version of [terminal-notifier](https://github.com/julienXX/terminal-notifier),
+this implementation uses cgo to talk directly to NSUserNotificationCenter APIs. It is also possible to use AppleScript APIs to generate notifications (see [this post](https://apple.stackexchange.com/questions/57412/how-can-i-trigger-a-notification-center-notification-from-an-applescript-or-shel/115373#115373)),
+but a cgo implementation was preferable.
+
+The [0xAX/notificator](https://github.com/0xAX/notificator) only supports growlnotify on Windows and OS X.
diff --git a/corefoundation.go b/corefoundation.go
new file mode 100644
index 0000000..e571ecb
--- /dev/null
+++ b/corefoundation.go
@@ -0,0 +1,343 @@
+// Copyright 2016 Keybase, Inc. All rights reserved. Use of
+// this source code is governed by the included BSD license.
+
+// +build darwin
+
+package notifier
+
+/*
+#cgo LDFLAGS: -framework CoreFoundation
+
+#include <CoreFoundation/CoreFoundation.h>
+*/
+import "C"
+import (
+ "errors"
+ "fmt"
+ "math"
+ "reflect"
+ "unicode/utf8"
+ "unsafe"
+)
+
+// Release releases a TypeRef
+func Release(ref C.CFTypeRef) {
+ if ref != nil {
+ C.CFRelease(ref)
+ }
+}
+
+// BytesToCFData will return a CFDataRef and if non-nil, must be released with
+// Release(ref).
+func BytesToCFData(b []byte) (C.CFDataRef, error) {
+ if uint64(len(b)) > math.MaxUint32 {
+ return nil, fmt.Errorf("Data is too large")
+ }
+ var p *C.UInt8
+ if len(b) > 0 {
+ p = (*C.UInt8)(&b[0])
+ }
+ cfData := C.CFDataCreate(nil, p, C.CFIndex(len(b)))
+ if cfData == nil {
+ return nil, fmt.Errorf("CFDataCreate failed")
+ }
+ return cfData, nil
+}
+
+// CFDataToBytes converts CFData to bytes.
+func CFDataToBytes(cfData C.CFDataRef) ([]byte, error) {
+ return C.GoBytes(unsafe.Pointer(C.CFDataGetBytePtr(cfData)), C.int(C.CFDataGetLength(cfData))), nil
+}
+
+// MapToCFDictionary will return a CFDictionaryRef and if non-nil, must be
+// released with Release(ref).
+func MapToCFDictionary(m map[C.CFTypeRef]C.CFTypeRef) (C.CFDictionaryRef, error) {
+ var keys, values []unsafe.Pointer
+ for key, value := range m {
+ keys = append(keys, unsafe.Pointer(key))
+ values = append(values, unsafe.Pointer(value))
+ }
+ numValues := len(values)
+ var keysPointer, valuesPointer *unsafe.Pointer
+ if numValues > 0 {
+ keysPointer = &keys[0]
+ valuesPointer = &values[0]
+ }
+ cfDict := C.CFDictionaryCreate(nil, keysPointer, valuesPointer, C.CFIndex(numValues), &C.kCFTypeDictionaryKeyCallBacks, &C.kCFTypeDictionaryValueCallBacks)
+ if cfDict == nil {
+ return nil, fmt.Errorf("CFDictionaryCreate failed")
+ }
+ return cfDict, nil
+}
+
+// CFDictionaryToMap converts CFDictionaryRef to a map.
+func CFDictionaryToMap(cfDict C.CFDictionaryRef) (m map[C.CFTypeRef]C.CFTypeRef) {
+ count := C.CFDictionaryGetCount(cfDict)
+ if count > 0 {
+ keys := make([]C.CFTypeRef, count)
+ values := make([]C.CFTypeRef, count)
+ C.CFDictionaryGetKeysAndValues(cfDict, (*unsafe.Pointer)(&keys[0]), (*unsafe.Pointer)(&values[0]))
+ m = make(map[C.CFTypeRef]C.CFTypeRef, count)
+ for i := C.CFIndex(0); i < count; i++ {
+ m[keys[i]] = values[i]
+ }
+ }
+ return
+}
+
+// StringToCFString will return a CFStringRef and if non-nil, must be released with
+// Release(ref).
+func StringToCFString(s string) (C.CFStringRef, error) {
+ if !utf8.ValidString(s) {
+ return nil, errors.New("Invalid UTF-8 string")
+ }
+ if uint64(len(s)) > math.MaxUint32 {
+ return nil, errors.New("String is too large")
+ }
+
+ bytes := []byte(s)
+ var p *C.UInt8
+ if len(bytes) > 0 {
+ p = (*C.UInt8)(&bytes[0])
+ }
+ return C.CFStringCreateWithBytes(nil, p, C.CFIndex(len(s)), C.kCFStringEncodingUTF8, C.false), nil
+}
+
+// CFStringToString converts a CFStringRef to a string.
+func CFStringToString(s C.CFStringRef) string {
+ p := C.CFStringGetCStringPtr(s, C.kCFStringEncodingUTF8)
+ if p != nil {
+ return C.GoString(p)
+ }
+ length := C.CFStringGetLength(s)
+ if length == 0 {
+ return ""
+ }
+ maxBufLen := C.CFStringGetMaximumSizeForEncoding(length, C.kCFStringEncodingUTF8)
+ if maxBufLen == 0 {
+ return ""
+ }
+ buf := make([]byte, maxBufLen)
+ var usedBufLen C.CFIndex
+ _ = C.CFStringGetBytes(s, C.CFRange{0, length}, C.kCFStringEncodingUTF8, C.UInt8(0), C.false, (*C.UInt8)(&buf[0]), maxBufLen, &usedBufLen)
+ return string(buf[:usedBufLen])
+}
+
+// ArrayToCFArray will return a CFArrayRef and if non-nil, must be released with
+// Release(ref).
+func ArrayToCFArray(a []C.CFTypeRef) C.CFArrayRef {
+ var values []unsafe.Pointer
+ for _, value := range a {
+ values = append(values, unsafe.Pointer(value))
+ }
+ numValues := len(values)
+ var valuesPointer *unsafe.Pointer
+ if numValues > 0 {
+ valuesPointer = &values[0]
+ }
+ return C.CFArrayCreate(nil, valuesPointer, C.CFIndex(numValues), &C.kCFTypeArrayCallBacks)
+}
+
+// CFArrayToArray converts a CFArrayRef to an array of CFTypes.
+func CFArrayToArray(cfArray C.CFArrayRef) (a []C.CFTypeRef) {
+ count := C.CFArrayGetCount(cfArray)
+ if count > 0 {
+ a = make([]C.CFTypeRef, count)
+ C.CFArrayGetValues(cfArray, C.CFRange{0, count}, (*unsafe.Pointer)(&a[0]))
+ }
+ return
+}
+
+// Convertable knows how to convert an instance to a CFTypeRef.
+type Convertable interface {
+ Convert() (C.CFTypeRef, error)
+}
+
+// ConvertMapToCFDictionary converts a map to a CFDictionary and if non-nil,
+// must be released with Release(ref).
+func ConvertMapToCFDictionary(attr map[string]interface{}) (C.CFDictionaryRef, error) {
+ m := make(map[C.CFTypeRef]C.CFTypeRef)
+ for key, i := range attr {
+ var valueRef C.CFTypeRef
+ switch i.(type) {
+ default:
+ return nil, fmt.Errorf("Unsupported value type: %v", reflect.TypeOf(i))
+ case C.CFTypeRef:
+ valueRef = i.(C.CFTypeRef)
+ case bool:
+ if i == true {
+ valueRef = C.CFTypeRef(C.kCFBooleanTrue)
+ } else {
+ valueRef = C.CFTypeRef(C.kCFBooleanFalse)
+ }
+ case []byte:
+ bytesRef, err := BytesToCFData(i.([]byte))
+ if err != nil {
+ return nil, err
+ }
+ valueRef = C.CFTypeRef(bytesRef)
+ defer Release(valueRef)
+ case string:
+ stringRef, err := StringToCFString(i.(string))
+ if err != nil {
+ return nil, err
+ }
+ valueRef = C.CFTypeRef(stringRef)
+ defer Release(valueRef)
+ case Convertable:
+ convertedRef, err := (i.(Convertable)).Convert()
+ if err != nil {
+ return nil, err
+ }
+ valueRef = C.CFTypeRef(convertedRef)
+ defer Release(valueRef)
+ }
+ keyRef, err := StringToCFString(key)
+ if err != nil {
+ return nil, err
+ }
+ m[C.CFTypeRef(keyRef)] = valueRef
+ }
+
+ cfDict, err := MapToCFDictionary(m)
+ if err != nil {
+ return nil, err
+ }
+ return cfDict, nil
+}
+
+// CFTypeDescription returns type string for CFTypeRef.
+func CFTypeDescription(ref C.CFTypeRef) string {
+ typeID := C.CFGetTypeID(ref)
+ typeDesc := C.CFCopyTypeIDDescription(typeID)
+ defer Release(C.CFTypeRef(typeDesc))
+ return CFStringToString(typeDesc)
+}
+
+// Convert converts a CFTypeRef to a go instance.
+func Convert(ref C.CFTypeRef) (interface{}, error) {
+ typeID := C.CFGetTypeID(ref)
+ if typeID == C.CFStringGetTypeID() {
+ return CFStringToString(C.CFStringRef(ref)), nil
+ } else if typeID == C.CFDictionaryGetTypeID() {
+ return ConvertCFDictionary(C.CFDictionaryRef(ref))
+ } else if typeID == C.CFArrayGetTypeID() {
+ arr := CFArrayToArray(C.CFArrayRef(ref))
+ results := make([]interface{}, 0, len(arr))
+ for _, ref := range arr {
+ v, err := Convert(ref)
+ if err != nil {
+ return nil, err
+ }
+ results = append(results, v)
+ return results, nil
+ }
+ } else if typeID == C.CFDataGetTypeID() {
+ b, err := CFDataToBytes(C.CFDataRef(ref))
+ if err != nil {
+ return nil, err
+ }
+ return b, nil
+ } else if typeID == C.CFNumberGetTypeID() {
+ return CFNumberToInterface(C.CFNumberRef(ref)), nil
+ } else if typeID == C.CFBooleanGetTypeID() {
+ if C.CFBooleanGetValue(C.CFBooleanRef(ref)) != 0 {
+ return true, nil
+ }
+ return false, nil
+ }
+
+ return nil, fmt.Errorf("Invalid type: %s", CFTypeDescription(ref))
+}
+
+// ConvertCFDictionary converts a CFDictionary to map (deep).
+func ConvertCFDictionary(d C.CFDictionaryRef) (map[interface{}]interface{}, error) {
+ m := CFDictionaryToMap(C.CFDictionaryRef(d))
+ result := make(map[interface{}]interface{})
+
+ for k, v := range m {
+ gk, err := Convert(k)
+ if err != nil {
+ return nil, err
+ }
+ gv, err := Convert(v)
+ if err != nil {
+ return nil, err
+ }
+ result[gk] = gv
+ }
+ return result, nil
+}
+
+// CFNumberToInterface converts the CFNumberRef to the most appropriate numeric
+// type.
+// This code is from github.com/kballard/go-osx-plist.
+func CFNumberToInterface(cfNumber C.CFNumberRef) interface{} {
+ typ := C.CFNumberGetType(cfNumber)
+ switch typ {
+ case C.kCFNumberSInt8Type:
+ var sint C.SInt8
+ C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint))
+ return int8(sint)
+ case C.kCFNumberSInt16Type:
+ var sint C.SInt16
+ C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint))
+ return int16(sint)
+ case C.kCFNumberSInt32Type:
+ var sint C.SInt32
+ C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint))
+ return int32(sint)
+ case C.kCFNumberSInt64Type:
+ var sint C.SInt64
+ C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint))
+ return int64(sint)
+ case C.kCFNumberFloat32Type:
+ var float C.Float32
+ C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float))
+ return float32(float)
+ case C.kCFNumberFloat64Type:
+ var float C.Float64
+ C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float))
+ return float64(float)
+ case C.kCFNumberCharType:
+ var char C.char
+ C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&char))
+ return byte(char)
+ case C.kCFNumberShortType:
+ var short C.short
+ C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&short))
+ return int16(short)
+ case C.kCFNumberIntType:
+ var i C.int
+ C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&i))
+ return int32(i)
+ case C.kCFNumberLongType:
+ var long C.long
+ C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&long))
+ return int(long)
+ case C.kCFNumberLongLongType:
+ // This is the only type that may actually overflow us
+ var longlong C.longlong
+ C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&longlong))
+ return int64(longlong)
+ case C.kCFNumberFloatType:
+ var float C.float
+ C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float))
+ return float32(float)
+ case C.kCFNumberDoubleType:
+ var double C.double
+ C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&double))
+ return float64(double)
+ case C.kCFNumberCFIndexType:
+ // CFIndex is a long
+ var index C.CFIndex
+ C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&index))
+ return int(index)
+ case C.kCFNumberNSIntegerType:
+ // We don't have a definition of NSInteger, but we know it's either an int or a long
+ var nsInt C.long
+ C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&nsInt))
+ return int(nsInt)
+ }
+ panic("Unknown CFNumber type")
+}
diff --git a/notifier.go b/notifier.go
new file mode 100644
index 0000000..cb342e5
--- /dev/null
+++ b/notifier.go
@@ -0,0 +1,19 @@
+// Copyright 2016 Keybase, Inc. All rights reserved. Use of
+// this source code is governed by the included BSD license.
+
+package notifier
+
+// Notification defines a notification
+type Notification struct {
+ Title string
+ Message string
+ ImagePath string
+ ImageURL string
+ BundleID string // For darwin
+ ToastPath string // For windows (Toaster)
+}
+
+// Notifier knows how to deliver a notification
+type Notifier interface {
+ DeliverNotification(notification Notification) error
+}
diff --git a/notifier/notifier.go b/notifier/notifier.go
new file mode 100644
index 0000000..6779892
--- /dev/null
+++ b/notifier/notifier.go
@@ -0,0 +1,48 @@
+// Copyright 2016 Keybase, Inc. All rights reserved. Use of
+// this source code is governed by the included BSD license.
+
+package main
+
+import (
+ "fmt"
+ "log"
+ "runtime"
+
+ "gopkg.in/alecthomas/kingpin.v2"
+
+ pkg "github.com/keybase/go-notifier"
+)
+
+var (
+ title = kingpin.Flag("title", "Title").String()
+ message = kingpin.Flag("message", "Message").String()
+ imagePath = kingpin.Flag("image-path", "Image path").String()
+ bundleID = kingpin.Flag("bundle-id", "Bundle identifier (for OS X)").String()
+ toastPath = kingpin.Flag("toast-path", "Path to toast.exe (for Windows)").String()
+)
+
+func main() {
+ kingpin.Version("0.1.1")
+ kingpin.Parse()
+
+ if runtime.GOOS == "windows" && *toastPath == "" {
+ log.Fatal(fmt.Errorf("Need to specify --toast-path for Windows"))
+ }
+
+ notifier, err := pkg.NewNotifier()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ notification := pkg.Notification{
+ Title: *title,
+ Message: *message,
+ ImagePath: *imagePath,
+ BundleID: *bundleID,
+ ToastPath: *toastPath,
+ }
+
+ if err := notifier.DeliverNotification(notification); err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/notifier_darwin.go b/notifier_darwin.go
new file mode 100644
index 0000000..b075f1c
--- /dev/null
+++ b/notifier_darwin.go
@@ -0,0 +1,57 @@
+// Copyright 2016 Keybase, Inc. All rights reserved. Use of
+// this source code is governed by the included BSD license.
+
+package notifier
+
+/*
+#cgo CFLAGS: -x objective-c
+#cgo LDFLAGS: -framework Cocoa
+#import <Cocoa/Cocoa.h>
+extern CFStringRef deliverNotification(CFStringRef title, CFStringRef subtitle, CFStringRef message, CFStringRef appIconURLString, CFStringRef groupID, CFStringRef bundleID, CFStringRef actionButtonTitle, CFStringRef otherButtonTitle);
+*/
+import "C"
+import "fmt"
+
+type darwinNotifier struct{}
+
+// NewNotifier constructs notifier for Windows
+func NewNotifier() (Notifier, error) {
+ return &darwinNotifier{}, nil
+}
+
+// DeliverNotification sends a notification
+func (n darwinNotifier) DeliverNotification(notification Notification) error {
+ titleRef, err := StringToCFString(notification.Title)
+ if err != nil {
+ return err
+ }
+ defer Release(C.CFTypeRef(titleRef))
+ messageRef, err := StringToCFString(notification.Message)
+ if err != nil {
+ return err
+ }
+ defer Release(C.CFTypeRef(messageRef))
+
+ var bundleIDRef C.CFStringRef
+ if notification.BundleID != "" {
+ bundleIDRef, err = StringToCFString(notification.BundleID)
+ if err != nil {
+ return err
+ }
+ defer Release(C.CFTypeRef(bundleIDRef))
+ }
+
+ var appIconURLStringRef C.CFStringRef
+ if notification.ImagePath != "" {
+ appIconURLString := fmt.Sprintf("file://%s", notification.ImagePath)
+ appIconURLStringRef, err = StringToCFString(appIconURLString)
+ if err != nil {
+ return err
+ }
+ defer Release(C.CFTypeRef(appIconURLStringRef))
+ }
+
+ C.deliverNotification(titleRef, nil, messageRef, appIconURLStringRef, bundleIDRef, bundleIDRef, nil, nil)
+
+ return nil
+}
diff --git a/notifier_darwin.m b/notifier_darwin.m
new file mode 100644
index 0000000..863f296
--- /dev/null
+++ b/notifier_darwin.m
@@ -0,0 +1,86 @@
+// Copyright 2016 Keybase, Inc. All rights reserved. Use of
+// this source code is governed by the included BSD license.
+
+// Modified from https://github.com/julienXX/terminal-notifier
+
+#import <Cocoa/Cocoa.h>
+#import <objc/runtime.h>
+
+NSString *_fakeBundleIdentifier = nil;
+@implementation NSBundle (FakeBundleIdentifier)
+- (NSString *)__bundleIdentifier {
+ if (self == [NSBundle mainBundle]) {
+ return _fakeBundleIdentifier ? _fakeBundleIdentifier : @"com.apple.Terminal";
+ } else {
+ return [self __bundleIdentifier];
+ }
+}
+@end
+
+static BOOL installFakeBundleIdentifierHook() {
+ Class class = objc_getClass("NSBundle");
+ if (class) {
+ method_exchangeImplementations(class_getInstanceMethod(class, @selector(bundleIdentifier)), class_getInstanceMethod(class, @selector(__bundleIdentifier)));
+ return YES;
+ }
+ return NO;
+}
+
+@interface NotificationDelegate : NSObject <NSUserNotificationCenterDelegate>
+@end
+
+CFStringRef deliverNotification(CFStringRef title, CFStringRef subtitle, CFStringRef message, CFStringRef appIconURLString,
+ CFStringRef bundleID, CFStringRef groupID,
+ CFStringRef actionButtonTitle, CFStringRef otherButtonTitle) {
+
+ if (bundleID) {
+ _fakeBundleIdentifier = (NSString *)bundleID;
+ }
+ installFakeBundleIdentifierHook();
+
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+ [defaults registerDefaults:@{@"sender": @"com.apple.Terminal"}];
+
+ NSUserNotification *userNotification = [[NSUserNotification alloc] init];
+ userNotification.title = (NSString *)title;
+ userNotification.subtitle = (NSString *)subtitle;
+ userNotification.informativeText = (NSString *)message;
+ NSMutableDictionary *options = [NSMutableDictionary dictionary];
+ if (groupID) {
+ options[@"groupID"] = (NSString *)groupID;
+ }
+ NSString *uuid = [[NSUUID UUID] UUIDString];
+ options[@"uuid"] = uuid;
+ userNotification.userInfo = options;
+ if (appIconURLString) {
+ NSURL *appIconURL = [NSURL URLWithString:(NSString *)appIconURLString];
+ NSImage *image = [[NSImage alloc] initWithContentsOfURL:appIconURL];
+ if (image) {
+ [userNotification setValue:image forKey:@"_identityImage"];
+ [userNotification setValue:@(false) forKey:@"_identityImageHasBorder"];
+ }
+ }
+
+ if (actionButtonTitle) {
+ userNotification.actionButtonTitle = (NSString *)actionButtonTitle;
+ }
+ if (otherButtonTitle) {
+ userNotification.otherButtonTitle = (NSString *)otherButtonTitle;
+ }
+
+ NSUserNotificationCenter *userNotificationCenter = [NSUserNotificationCenter defaultUserNotificationCenter];
+ //NSLog(@"Deliver: %@", userNotification);
+ userNotificationCenter.delegate = [[NotificationDelegate alloc] init];
+ [userNotificationCenter scheduleNotification:userNotification];
+ [[NSRunLoop mainRunLoop] run];
+ return nil;
+}
+
+@implementation NotificationDelegate
+- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)userNotification {
+ return YES;
+}
+- (void)userNotificationCenter:(NSUserNotificationCenter *)center didDeliverNotification:(NSUserNotification *)userNotification {
+ exit(0);
+}
+@end
diff --git a/notifier_linux.go b/notifier_linux.go
new file mode 100644
index 0000000..7996354
--- /dev/null
+++ b/notifier_linux.go
@@ -0,0 +1,35 @@
+// Copyright 2016 Keybase, Inc. All rights reserved. Use of
+// this source code is governed by the included BSD license.
+
+package notifier
+
+import (
+ "fmt"
+ "os/exec"
+)
+
+type linuxNotifier struct{}
+
+// NewNotifier constructs notifier for Windows
+func NewNotifier() (Notifier, error) {
+ return &linuxNotifier{}, nil
+}
+
+// DeliverNotification sends a notification
+func (n linuxNotifier) DeliverNotification(notification Notification) error {
+ args := []string{}
+ if notification.ImagePath != "" {
+ args = append(args, "-i", notification.ImagePath)
+ }
+ args = append(args, notification.Title)
+ args = append(args, notification.Message)
+ cmd := exec.Command("notify-send", args...)
+ if cmd == nil {
+ return fmt.Errorf("No command")
+ }
+ _, err := cmd.Output()
+ if err != nil {
+ return fmt.Errorf("Error running command: %s", err)
+ }
+ return nil
+}
diff --git a/notifier_windows.go b/notifier_windows.go
new file mode 100644
index 0000000..3f6ce09
--- /dev/null
+++ b/notifier_windows.go
@@ -0,0 +1,44 @@
+// Copyright 2016 Keybase, Inc. All rights reserved. Use of
+// this source code is governed by the included BSD license.
+
+package notifier
+
+import (
+ "fmt"
+ "os/exec"
+)
+
+type windowsNotifier struct{}
+
+// NewNotifier constructs notifier for Windows
+func NewNotifier() (Notifier, error) {
+ return &windowsNotifier{}, nil
+}
+
+// DeliverNotification sends a notification
+func (n windowsNotifier) DeliverNotification(notification Notification) error {
+ args := []string{}
+
+ if notification.Title != "" {
+ args = append(args, "-t", notification.Title)
+ }
+ if notification.Message != "" {
+ args = append(args, "-m", notification.Message)
+ }
+ if notification.ImagePath != "" {
+ args = append(args, "-p", notification.ImagePath)
+ }
+
+ // For testing
+ // toastPath := filepath.Join(os.Getenv("GOPATH"), "src/github.com/keybase/go-osnotify/toaster/toast.exe")
+
+ cmd := exec.Command(notification.toastPath, args...)
+ if cmd == nil {
+ return fmt.Errorf("No command")
+ }
+ _, err := cmd.Output()
+ if err != nil {
+ return fmt.Errorf("Error running command: %s", err)
+ }
+ return nil
+}
diff --git a/toaster/Microsoft.WindowsAPICodePack.Shell.dll b/toaster/Microsoft.WindowsAPICodePack.Shell.dll
new file mode 100755
index 0000000..82f3356
--- /dev/null
+++ b/toaster/Microsoft.WindowsAPICodePack.Shell.dll
Binary files differ
diff --git a/toaster/Microsoft.WindowsAPICodePack.dll b/toaster/Microsoft.WindowsAPICodePack.dll
new file mode 100755
index 0000000..5b1b620
--- /dev/null
+++ b/toaster/Microsoft.WindowsAPICodePack.dll
Binary files differ
diff --git a/toaster/README.md b/toaster/README.md
new file mode 100644
index 0000000..5ac985b
--- /dev/null
+++ b/toaster/README.md
@@ -0,0 +1,34 @@
+https://github.com/nels-o/toaster
+
+$ ./toast.exe
+No args provided.
+
+Welcome to toast.
+Provide toast with a message and display it-
+via the graphical notification system.
+-Nels
+
+---- Usage ----
+toast <string>|[-t <string>][-m <string>][-p <string>]
+
+---- Args ----
+<string> | Toast <string>, no add. args will be read.
+[-t] <title string> | Displayed on the first line of the toast.
+[-m] <message string> | Displayed on the remaining lines, wrapped.
+[-p] <image URI> | Display toast with an image
+[-q] | Deactivate sound (quiet).
+[-w] | Wait for toast to expire or activate.
+? | Print these intructions. Same as no args.
+Exit Status : Exit Code
+Failed : -1
+Success : 0
+Hidden : 1
+Dismissed : 2
+Timeout : 3
+
+---- Image Notes ----
+Images must be .png with:
+ maximum dimensions of 1024x1024
+ size <= 200kb
+These limitations are due to the Toast notification system.
+This should go without saying, but windows style paths are required.
diff --git a/toaster/toast.exe b/toaster/toast.exe
new file mode 100755
index 0000000..eac669c
--- /dev/null
+++ b/toaster/toast.exe
Binary files differ