diff options
| author | Gabriel Handford | 2016-05-06 15:20:54 -0700 | 
|---|---|---|
| committer | Gabriel Handford | 2016-05-06 15:57:46 -0700 | 
| commit | 12878f6400420fa6c988187c8942799b73c31e6c (patch) | |
| tree | 4c134f3bbf0ea6ebd4f3b8cdad6bf27ec903da4d | |
| download | go-notifier-12878f6400420fa6c988187c8942799b73c31e6c.tar.bz2 | |
Importing
| -rw-r--r-- | .pre-commit-config.yaml | 11 | ||||
| -rw-r--r-- | LICENSE | 22 | ||||
| -rw-r--r-- | README.md | 28 | ||||
| -rw-r--r-- | corefoundation.go | 343 | ||||
| -rw-r--r-- | notifier.go | 19 | ||||
| -rw-r--r-- | notifier/notifier.go | 48 | ||||
| -rw-r--r-- | notifier_darwin.go | 57 | ||||
| -rw-r--r-- | notifier_darwin.m | 86 | ||||
| -rw-r--r-- | notifier_linux.go | 35 | ||||
| -rw-r--r-- | notifier_windows.go | 44 | ||||
| -rwxr-xr-x | toaster/Microsoft.WindowsAPICodePack.Shell.dll | bin | 0 -> 542208 bytes | |||
| -rwxr-xr-x | toaster/Microsoft.WindowsAPICodePack.dll | bin | 0 -> 104960 bytes | |||
| -rw-r--r-- | toaster/README.md | 34 | ||||
| -rwxr-xr-x | toaster/toast.exe | bin | 0 -> 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 @@ -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.dllBinary files differ new file mode 100755 index 0000000..82f3356 --- /dev/null +++ b/toaster/Microsoft.WindowsAPICodePack.Shell.dll diff --git a/toaster/Microsoft.WindowsAPICodePack.dll b/toaster/Microsoft.WindowsAPICodePack.dllBinary files differ new file mode 100755 index 0000000..5b1b620 --- /dev/null +++ b/toaster/Microsoft.WindowsAPICodePack.dll 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.exeBinary files differ new file mode 100755 index 0000000..eac669c --- /dev/null +++ b/toaster/toast.exe | 
