From 12878f6400420fa6c988187c8942799b73c31e6c Mon Sep 17 00:00:00 2001 From: Gabriel Handford Date: Fri, 6 May 2016 15:20:54 -0700 Subject: Importing --- .pre-commit-config.yaml | 11 + LICENSE | 22 ++ README.md | 28 ++ corefoundation.go | 343 +++++++++++++++++++++++++ notifier.go | 19 ++ notifier/notifier.go | 48 ++++ notifier_darwin.go | 57 ++++ notifier_darwin.m | 86 +++++++ notifier_linux.go | 35 +++ notifier_windows.go | 44 ++++ toaster/Microsoft.WindowsAPICodePack.Shell.dll | Bin 0 -> 542208 bytes toaster/Microsoft.WindowsAPICodePack.dll | Bin 0 -> 104960 bytes toaster/README.md | 34 +++ toaster/toast.exe | Bin 0 -> 14848 bytes 14 files changed, 727 insertions(+) create mode 100644 .pre-commit-config.yaml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 corefoundation.go create mode 100644 notifier.go create mode 100644 notifier/notifier.go create mode 100644 notifier_darwin.go create mode 100644 notifier_darwin.m create mode 100644 notifier_linux.go create mode 100644 notifier_windows.go create mode 100755 toaster/Microsoft.WindowsAPICodePack.Shell.dll create mode 100755 toaster/Microsoft.WindowsAPICodePack.dll create mode 100644 toaster/README.md create mode 100755 toaster/toast.exe 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 +*/ +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 +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 +#import + +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 +@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 Binary files /dev/null and b/toaster/Microsoft.WindowsAPICodePack.Shell.dll differ diff --git a/toaster/Microsoft.WindowsAPICodePack.dll b/toaster/Microsoft.WindowsAPICodePack.dll new file mode 100755 index 0000000..5b1b620 Binary files /dev/null and b/toaster/Microsoft.WindowsAPICodePack.dll 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 |[-t ][-m ][-p ] + +---- Args ---- + | Toast , no add. args will be read. +[-t] | 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 Binary files /dev/null and b/toaster/toast.exe differ -- cgit v1.2.3