├── LICENSE-MIT ├── README.md ├── cmd └── notify │ ├── .gitignore │ └── main.go ├── example.png ├── gopher.png ├── gosx-notifier.go ├── gosx-notifier_test.go ├── osx └── terminal-notifier.app │ └── Contents │ ├── Info.plist │ ├── MacOS │ └── terminal-notifier │ ├── PkgInfo │ └── Resources │ ├── Terminal.icns │ └── en.lproj │ ├── Credits.rtf │ ├── InfoPlist.strings │ └── MainMenu.nib ├── terminal-app-binary.go └── terminal-app-zip.go /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Ralph Caraveo 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gosx-notifier 2 | =========================== 3 | A [Go](http://golang.org) lib for sending desktop notifications to OSX Mountain Lion's (10.8 or higher REQUIRED) 4 | [Notification Center](http://www.macworld.com/article/1165411/mountain_lion_hands_on_with_notification_center.html). 5 | 6 | [![GoDoc](http://godoc.org/github.com/deckarep/gosx-notifier?status.png)](http://godoc.org/github.com/deckarep/gosx-notifier) 7 | 8 | Update 4/3/2014 9 | ------ 10 | On OSX 10.9 and above gosx-notifier now supports images and icons. 11 | ![Now with custom icon support](../master/example.png?raw=true) 12 | 13 | Synopsis 14 | -------- 15 | OSX Mountain Lion comes packaged with a built-in notification center. For whatever reason, [Apple sandboxed the 16 | notification center API](http://forums.macrumors.com/showthread.php?t=1403807) to apps hosted in its App Store. The end 17 | result? A potentially useful API shackled to Apple's ecosystem. 18 | 19 | Thankfully, [Eloy Durán](https://github.com/alloy) put together [an osx app](https://github.com/alloy/terminal-notifier) that allows terminal access to the sandboxed API. **gosx-notifier** embeds this app with a simple interface to the closed API. 20 | 21 | It's not perfect, and the implementor will quickly notice its limitations. However, it's a start and any pull requests are accepted and encouraged! 22 | 23 | Dependencies: 24 | ------------- 25 | There are none! If you utilize this package and create a binary executable it will auto-magically install the terminal-notifier component into a temp directory of the server. This is possible because in this latest version the terminal-notifier binary is now statically embedded into the Go source files. 26 | 27 | 28 | Installation and Requirements 29 | ----------------------------- 30 | The following command will install the notification api for Go along with the binaries. Also, utilizing this lib requires OSX 10.8 or higher. It will simply not work on lower versions of OSX. 31 | 32 | ```sh 33 | go get github.com/deckarep/gosx-notifier 34 | ``` 35 | 36 | Using the Command Line 37 | ------------- 38 | ```Go 39 | notify "Wow! A notification!!!" 40 | ``` 41 | 42 | useful for knowing when long running commands finish 43 | 44 | ```Go 45 | longRunningCommand && notify done! 46 | ``` 47 | 48 | Using the Code 49 | ------------------ 50 | It's a pretty straightforward API: 51 | 52 | ```Go 53 | package main 54 | 55 | import ( 56 | "github.com/deckarep/gosx-notifier" 57 | "log" 58 | ) 59 | 60 | func main() { 61 | //At a minimum specifiy a message to display to end-user. 62 | note := gosxnotifier.NewNotification("Check your Apple Stock!") 63 | 64 | //Optionally, set a title 65 | note.Title = "It's money making time 💰" 66 | 67 | //Optionally, set a subtitle 68 | note.Subtitle = "My subtitle" 69 | 70 | //Optionally, set a sound from a predefined set. 71 | note.Sound = gosxnotifier.Basso 72 | 73 | //Optionally, set a group which ensures only one notification is ever shown replacing previous notification of same group id. 74 | note.Group = "com.unique.yourapp.identifier" 75 | 76 | //Optionally, set a sender (Notification will now use the Safari icon) 77 | note.Sender = "com.apple.Safari" 78 | 79 | //Optionally, specifiy a url or bundleid to open should the notification be 80 | //clicked. 81 | note.Link = "http://www.yahoo.com" //or BundleID like: com.apple.Terminal 82 | 83 | //Optionally, an app icon (10.9+ ONLY) 84 | note.AppIcon = "gopher.png" 85 | 86 | //Optionally, a content image (10.9+ ONLY) 87 | note.ContentImage = "gopher.png" 88 | 89 | //Then, push the notification 90 | err := note.Push() 91 | 92 | //If necessary, check error 93 | if err != nil { 94 | log.Println("Uh oh!") 95 | } 96 | } 97 | ``` 98 | 99 | Sample App: Desktop Pinger Notification - monitors your websites and will notifiy you when a website is down. 100 | ```Go 101 | package main 102 | 103 | import ( 104 | "github.com/deckarep/gosx-notifier" 105 | "net/http" 106 | "strings" 107 | "time" 108 | ) 109 | 110 | //a slice of string sites that you are interested in watching 111 | var sites []string = []string{ 112 | "http://www.yahoo.com", 113 | "http://www.google.com", 114 | "http://www.bing.com"} 115 | 116 | func main() { 117 | ch := make(chan string) 118 | 119 | for _, s := range sites { 120 | go pinger(ch, s) 121 | } 122 | 123 | for { 124 | select { 125 | case result := <-ch: 126 | if strings.HasPrefix(result, "-") { 127 | s := strings.Trim(result, "-") 128 | showNotification("Urgent, can't ping website: " + s) 129 | } 130 | } 131 | } 132 | } 133 | 134 | func showNotification(message string) { 135 | 136 | note := gosxnotifier.NewNotification(message) 137 | note.Title = "Site Down" 138 | note.Sound = gosxnotifier.Default 139 | 140 | note.Push() 141 | } 142 | 143 | //Prefixing a site with a + means it's up, while - means it's down 144 | func pinger(ch chan string, site string) { 145 | for { 146 | res, err := http.Get(site) 147 | 148 | if err != nil { 149 | ch <- "-" + site 150 | } else { 151 | if res.StatusCode != 200 { 152 | ch <- "-" + site 153 | } else { 154 | ch <- "+" + site 155 | } 156 | res.Body.Close() 157 | } 158 | time.Sleep(30 * time.Second) 159 | } 160 | } 161 | ``` 162 | 163 | Usage Ideas 164 | ----------- 165 | * Monitor your awesome server cluster and push notifications when something goes haywire (we've all been there) 166 | * Scrape Hacker News looking for articles of certain keywords and push a notification 167 | * Monitor your stock performance, push a notification, before you lose all your money 168 | * Hook it up to ifttt.com and push a notification when your motion-sensor at home goes off 169 | 170 | Coming Soon 171 | ----------- 172 | * Remove ID 173 | 174 | Licence 175 | ------- 176 | This project is dual licensed under [any licensing defined by the underlying apps](https://github.com/alloy/terminal-notifier) and MIT licensed for this version written in Go. 177 | 178 | 179 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/deckarep/gosx-notifier/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 180 | -------------------------------------------------------------------------------- /cmd/notify/.gitignore: -------------------------------------------------------------------------------- 1 | notify 2 | -------------------------------------------------------------------------------- /cmd/notify/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strings" 7 | 8 | "github.com/deckarep/gosx-notifier" 9 | ) 10 | 11 | func main() { 12 | notification := strings.Join(os.Args[1:], " ") 13 | 14 | note := gosxnotifier.NewNotification(notification) 15 | 16 | note.Title = "Notify" 17 | 18 | err := note.Push() 19 | 20 | //If necessary, check error 21 | if err != nil { 22 | log.Println("Uh oh! Error with Notify") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deckarep/gosx-notifier/e127226297fb751aa3b582db5e92361fcbfc5a6c/example.png -------------------------------------------------------------------------------- /gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deckarep/gosx-notifier/e127226297fb751aa3b582db5e92361fcbfc5a6c/gopher.png -------------------------------------------------------------------------------- /gosx-notifier.go: -------------------------------------------------------------------------------- 1 | package gosxnotifier 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | type Sound string 12 | 13 | const ( 14 | Default Sound = "'default'" 15 | Basso Sound = "Basso" 16 | Blow Sound = "Blow" 17 | Bottle Sound = "Bottle" 18 | Frog Sound = "Frog" 19 | Funk Sound = "Funk" 20 | Glass Sound = "Glass" 21 | Hero Sound = "Hero" 22 | Morse Sound = "Morse" 23 | Ping Sound = "Ping" 24 | Pop Sound = "Pop" 25 | Purr Sound = "Purr" 26 | Sosumi Sound = "Sosumi" 27 | Submarine Sound = "Submarine" 28 | Tink Sound = "Tink" 29 | ) 30 | 31 | type Notification struct { 32 | Message string //required 33 | Title string //optional 34 | Subtitle string //optional 35 | Sound Sound //optional 36 | Link string //optional 37 | Sender string //optional 38 | Group string //optional 39 | AppIcon string //optional 40 | ContentImage string //optional 41 | } 42 | 43 | func NewNotification(message string) *Notification { 44 | n := &Notification{Message: message} 45 | return n 46 | } 47 | 48 | func (n *Notification) Push() error { 49 | if supportedOS() { 50 | commandTuples := make([]string, 0) 51 | 52 | //check required commands 53 | if n.Message == "" { 54 | return errors.New("Please specifiy a proper message argument.") 55 | } else { 56 | commandTuples = append(commandTuples, []string{"-message", n.Message}...) 57 | } 58 | 59 | //add title if found 60 | if n.Title != "" { 61 | commandTuples = append(commandTuples, []string{"-title", n.Title}...) 62 | } 63 | 64 | //add subtitle if found 65 | if n.Subtitle != "" { 66 | commandTuples = append(commandTuples, []string{"-subtitle", n.Subtitle}...) 67 | } 68 | 69 | //add sound if specified 70 | if n.Sound != "" { 71 | commandTuples = append(commandTuples, []string{"-sound", string(n.Sound)}...) 72 | } 73 | 74 | //add group if specified 75 | if n.Group != "" { 76 | commandTuples = append(commandTuples, []string{"-group", n.Group}...) 77 | } 78 | 79 | //add appIcon if specified 80 | if n.AppIcon != "" { 81 | img, err := normalizeImagePath(n.AppIcon) 82 | 83 | if err != nil { 84 | return err 85 | } 86 | 87 | commandTuples = append(commandTuples, []string{"-appIcon", img}...) 88 | } 89 | 90 | //add contentImage if specified 91 | if n.ContentImage != "" { 92 | img, err := normalizeImagePath(n.ContentImage) 93 | 94 | if err != nil { 95 | return err 96 | } 97 | commandTuples = append(commandTuples, []string{"-contentImage", img}...) 98 | } 99 | 100 | //add url if specified 101 | url, err := url.Parse(n.Link) 102 | if err != nil { 103 | n.Link = "" 104 | } 105 | if url != nil && n.Link != "" { 106 | commandTuples = append(commandTuples, []string{"-open", n.Link}...) 107 | } 108 | 109 | //add bundle id if specified 110 | if strings.HasPrefix(strings.ToLower(n.Link), "com.") { 111 | commandTuples = append(commandTuples, []string{"-activate", n.Link}...) 112 | } 113 | 114 | //add sender if specified 115 | if strings.HasPrefix(strings.ToLower(n.Sender), "com.") { 116 | commandTuples = append(commandTuples, []string{"-sender", n.Sender}...) 117 | } 118 | 119 | if len(commandTuples) == 0 { 120 | return errors.New("Please provide a Message and Type at a minimum.") 121 | } 122 | 123 | _, err = exec.Command(FinalPath, commandTuples...).Output() 124 | if err != nil { 125 | return err 126 | } 127 | } 128 | return nil 129 | } 130 | 131 | func normalizeImagePath(image string) (string, error) { 132 | if imagePath, err := filepath.Abs(image); err != nil { 133 | return "", errors.New("Could not resolve image path of image: " + image) 134 | } else { 135 | return imagePath, nil 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /gosx-notifier_test.go: -------------------------------------------------------------------------------- 1 | package gosxnotifier 2 | 3 | import ( 4 | "log" 5 | "path/filepath" 6 | "testing" 7 | ) 8 | 9 | func Test_Install(t *testing.T) { 10 | //assert file exists 11 | 12 | if !exists(FinalPath) { 13 | t.Error("Test_Install failed to install the terminal-notifier.app bundle") 14 | } else { 15 | log.Println("terminal-notifier.app bundle installed successfully at: ", FinalPath) 16 | } 17 | } 18 | 19 | func Test_NewNotifier(t *testing.T) { 20 | n := NewNotification("Hello") 21 | 22 | //assert defaults 23 | if n.Message != "Hello" { 24 | t.Error("NewNotification doesn't have a Message specified") 25 | } 26 | } 27 | 28 | func Test_Push(t *testing.T) { 29 | n := NewNotification("Testing Push") 30 | err := n.Push() 31 | 32 | if err != nil { 33 | t.Error("Test_Push failed with error: ", err) 34 | } 35 | } 36 | 37 | func Test_Title(t *testing.T) { 38 | n := NewNotification("Testing Title") 39 | n.Title = "gosx-notifier is amazing!" 40 | err := n.Push() 41 | 42 | if err != nil { 43 | t.Error("Test_Title failed with error: ", err) 44 | } 45 | } 46 | 47 | func Test_Subtitle(t *testing.T) { 48 | n := NewNotification("Testing Subtitle") 49 | n.Subtitle = "gosx-notifier rocks!" 50 | 51 | err := n.Push() 52 | 53 | if err != nil { 54 | t.Error("Test_Subtitle failed with error: ", err) 55 | } 56 | } 57 | 58 | func Test_Sender(t *testing.T) { 59 | 60 | for _, s := range []string{"com.apple.Safari", "com.apple.iTunes"} { 61 | 62 | n := NewNotification("Testing Icon") 63 | n.Title = s 64 | n.Sender = s 65 | 66 | err := n.Push() 67 | 68 | if err != nil { 69 | t.Error("Test_Sender failed with error: ", err) 70 | } 71 | } 72 | } 73 | 74 | func Test_Group(t *testing.T) { 75 | const app_id string = "github.com/deckarep/gosx-notifier" 76 | 77 | for i := 0; i < 3; i++ { 78 | n := NewNotification("Testing Group Functionality...") 79 | n.Group = app_id 80 | 81 | err := n.Push() 82 | 83 | if err != nil { 84 | t.Error("Test_Group failed with error: ", err) 85 | } 86 | 87 | } 88 | } 89 | 90 | func Test_AppIcon(t *testing.T) { 91 | const appIcon string = "gopher.png" 92 | 93 | n := NewNotification("Testing App Icon") 94 | 95 | if icon, err := filepath.Abs(appIcon); err != nil { 96 | t.Error("Test_AppIcon could not get the absolute file of: ", appIcon) 97 | } else { 98 | n.AppIcon = icon 99 | } 100 | 101 | err := n.Push() 102 | 103 | if err != nil { 104 | t.Error("Test_AppIcon failed with error: ", err) 105 | } 106 | } 107 | 108 | func Test_ContentImage(t *testing.T) { 109 | const contentImage string = "gopher.png" 110 | 111 | n := NewNotification("Testing Content Image") 112 | 113 | if img, err := filepath.Abs(contentImage); err != nil { 114 | t.Error("Test_AppIcon could not get the absolute file of: ", contentImage) 115 | } else { 116 | n.ContentImage = img 117 | } 118 | 119 | err := n.Push() 120 | 121 | if err != nil { 122 | t.Error("Test_ContentImage failed with error: ", err) 123 | } 124 | } 125 | 126 | func Test_ContentImageAndIcon(t *testing.T) { 127 | const image string = "gopher.png" 128 | 129 | n := NewNotification("Testing Content Image and Icon") 130 | n.Title = "Hey Gopher!" 131 | n.Subtitle = "I eat Goroutines for breakfast!" 132 | 133 | if img, err := filepath.Abs(image); err != nil { 134 | t.Error("Test_AppIcon could not get the absolute file of: ", image) 135 | } else { 136 | n.ContentImage = img 137 | n.AppIcon = img 138 | } 139 | 140 | err := n.Push() 141 | 142 | if err != nil { 143 | t.Error("Test_ContentImageAndIcon failed with error: ", err) 144 | } 145 | } 146 | 147 | /* 148 | Not an easy way to verify the tests below actually work as designed, but here for completion. 149 | */ 150 | 151 | func Test_Sound(t *testing.T) { 152 | n := NewNotification("Testing Sound") 153 | n.Sound = Default 154 | err := n.Push() 155 | 156 | if err != nil { 157 | t.Error("Test_Sound failed with error: ", err) 158 | } 159 | } 160 | 161 | func Test_Link_Url(t *testing.T) { 162 | n := NewNotification("Testing Link Url") 163 | n.Link = "http://www.yahoo.com" 164 | err := n.Push() 165 | 166 | if err != nil { 167 | t.Error("Test_Link failed with error: ", err) 168 | } 169 | } 170 | 171 | func Test_Link_App_Bundle(t *testing.T) { 172 | n := NewNotification("Testing Link Terminal") 173 | n.Link = "com.apple.Safari" 174 | err := n.Push() 175 | 176 | if err != nil { 177 | t.Error("Test_Link failed with error: ", err) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /osx/terminal-notifier.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 12E55 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | terminal-notifier 11 | CFBundleIconFile 12 | Terminal 13 | CFBundleIdentifier 14 | nl.superalloy.oss.terminal-notifier 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | terminal-notifier 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 1.5.0 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | 8 27 | DTCompiler 28 | com.apple.compilers.llvm.clang.1_0 29 | DTPlatformBuild 30 | 5A11365x 31 | DTPlatformVersion 32 | GM 33 | DTSDKBuild 34 | 13A538c 35 | DTSDKName 36 | macosx10.9 37 | DTXcode 38 | 0500 39 | DTXcodeBuild 40 | 5A11365x 41 | LSMinimumSystemVersion 42 | 10.8 43 | LSUIElement 44 | 45 | NSHumanReadableCopyright 46 | Copyright © 2012 Eloy Durán. All rights reserved. 47 | NSMainNibFile 48 | MainMenu 49 | NSPrincipalClass 50 | NSApplication 51 | 52 | 53 | -------------------------------------------------------------------------------- /osx/terminal-notifier.app/Contents/MacOS/terminal-notifier: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deckarep/gosx-notifier/e127226297fb751aa3b582db5e92361fcbfc5a6c/osx/terminal-notifier.app/Contents/MacOS/terminal-notifier -------------------------------------------------------------------------------- /osx/terminal-notifier.app/Contents/PkgInfo: -------------------------------------------------------------------------------- 1 | APPL???? -------------------------------------------------------------------------------- /osx/terminal-notifier.app/Contents/Resources/Terminal.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deckarep/gosx-notifier/e127226297fb751aa3b582db5e92361fcbfc5a6c/osx/terminal-notifier.app/Contents/Resources/Terminal.icns -------------------------------------------------------------------------------- /osx/terminal-notifier.app/Contents/Resources/en.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;} 2 | {\colortbl;\red255\green255\blue255;} 3 | \paperw9840\paperh8400 4 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural 5 | 6 | \f0\b\fs24 \cf0 Engineering: 7 | \b0 \ 8 | Some people\ 9 | \ 10 | 11 | \b Human Interface Design: 12 | \b0 \ 13 | Some other people\ 14 | \ 15 | 16 | \b Testing: 17 | \b0 \ 18 | Hopefully not nobody\ 19 | \ 20 | 21 | \b Documentation: 22 | \b0 \ 23 | Whoever\ 24 | \ 25 | 26 | \b With special thanks to: 27 | \b0 \ 28 | Mom\ 29 | } 30 | -------------------------------------------------------------------------------- /osx/terminal-notifier.app/Contents/Resources/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deckarep/gosx-notifier/e127226297fb751aa3b582db5e92361fcbfc5a6c/osx/terminal-notifier.app/Contents/Resources/en.lproj/InfoPlist.strings -------------------------------------------------------------------------------- /osx/terminal-notifier.app/Contents/Resources/en.lproj/MainMenu.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deckarep/gosx-notifier/e127226297fb751aa3b582db5e92361fcbfc5a6c/osx/terminal-notifier.app/Contents/Resources/en.lproj/MainMenu.nib -------------------------------------------------------------------------------- /terminal-app-zip.go: -------------------------------------------------------------------------------- 1 | package gosxnotifier 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | "runtime" 12 | ) 13 | 14 | const ( 15 | zipPath = "terminal-notifier.temp.zip" 16 | executablePath = "terminal-notifier.app/Contents/MacOS/terminal-notifier" 17 | tempDirSuffix = "gosxnotifier" 18 | ) 19 | 20 | var ( 21 | rootPath string 22 | FinalPath string 23 | ) 24 | 25 | func supportedOS() bool { 26 | if runtime.GOOS == "darwin" { 27 | return true 28 | } else { 29 | log.Print("OS does not support terminal-notifier") 30 | return false 31 | } 32 | } 33 | 34 | func init() { 35 | if supportedOS() { 36 | err := installTerminalNotifier() 37 | if err != nil { 38 | log.Fatalf("Could not install Terminal Notifier to a temp directory: %s", err) 39 | } else { 40 | FinalPath = filepath.Join(rootPath, executablePath) 41 | } 42 | } 43 | } 44 | 45 | func exists(file string) bool { 46 | if _, err := os.Stat(file); os.IsNotExist(err) { 47 | return false 48 | } 49 | return true 50 | } 51 | 52 | func installTerminalNotifier() error { 53 | rootPath = filepath.Join(os.TempDir(), tempDirSuffix) 54 | 55 | //if terminal-notifier.app already installed no-need to re-install 56 | if exists(filepath.Join(rootPath, executablePath)) { 57 | return nil 58 | } 59 | buf := bytes.NewReader(terminalnotifier()) 60 | reader, err := zip.NewReader(buf, int64(buf.Len())) 61 | if err != nil { 62 | return err 63 | } 64 | err = unpackZip(reader, rootPath) 65 | if err != nil { 66 | return fmt.Errorf("could not unpack zip terminal-notifier file: %s", err) 67 | } 68 | 69 | err = os.Chmod(filepath.Join(rootPath, executablePath), 0755) 70 | if err != nil { 71 | return fmt.Errorf("could not make terminal-notifier executable: %s", err) 72 | } 73 | 74 | return nil 75 | } 76 | 77 | func unpackZip(reader *zip.Reader, tempPath string) error { 78 | for _, zipFile := range reader.File { 79 | name := zipFile.Name 80 | mode := zipFile.Mode() 81 | if mode.IsDir() { 82 | if err := os.MkdirAll(filepath.Join(tempPath, name), 0755); err != nil { 83 | return err 84 | } 85 | } else { 86 | if err := unpackZippedFile(name, tempPath, zipFile); err != nil { 87 | return err 88 | } 89 | } 90 | } 91 | 92 | return nil 93 | } 94 | 95 | func unpackZippedFile(filename, tempPath string, zipFile *zip.File) error { 96 | writer, err := os.Create(filepath.Join(tempPath, filename)) 97 | 98 | if err != nil { 99 | return err 100 | } 101 | 102 | defer writer.Close() 103 | 104 | reader, err := zipFile.Open() 105 | if err != nil { 106 | return err 107 | } 108 | 109 | defer reader.Close() 110 | 111 | if _, err = io.Copy(writer, reader); err != nil { 112 | return err 113 | } 114 | 115 | return nil 116 | } 117 | --------------------------------------------------------------------------------