├── .gitignore ├── LICENSE ├── cli ├── .gitignore ├── build.sh ├── main.go └── vendor │ └── vendor.json ├── readme.md ├── screenshot-action-centre.png ├── screenshot-cli.png ├── screenshot-toast.png ├── toast.go └── vendor └── vendor.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/* 3 | !vendor/vendor.json 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Jacob Marshall 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | vendor/* 3 | !vendor/vendor.json 4 | -------------------------------------------------------------------------------- /cli/build.sh: -------------------------------------------------------------------------------- 1 | GOOS=windows GOARCH=amd64 go build -o ./bin/toast64.exe ./*.go 2 | GOOS=windows GOARCH=386 go build -o ./bin/toast32.exe ./*.go 3 | -------------------------------------------------------------------------------- /cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "time" 7 | 8 | "gopkg.in/toast.v1" 9 | "gopkg.in/urfave/cli.v1" 10 | ) 11 | 12 | func main() { 13 | app := cli.NewApp() 14 | 15 | app.Name = "toast" 16 | app.Usage = "Windows 10 toasts" 17 | app.Version = "v1" 18 | app.Compiled = time.Now() 19 | app.Authors = []cli.Author{ 20 | cli.Author{ 21 | Name: "Jacob Marshall", 22 | Email: "go-toast@jacobmarshall.co", 23 | }, 24 | } 25 | 26 | app.Flags = []cli.Flag{ 27 | cli.StringFlag{ 28 | Name: "app-id, id", 29 | Usage: "the app identifier (used for grouping multiple toasts)", 30 | }, 31 | cli.StringFlag{ 32 | Name: "title, t", 33 | Usage: "the main toast title/heading", 34 | }, 35 | cli.StringFlag{ 36 | Name: "message, m", 37 | Usage: "the toast's main message (new lines as separator)", 38 | }, 39 | cli.StringFlag{ 40 | Name: "icon, i", 41 | Usage: "the app icon path (displays to the left of the toast)", 42 | }, 43 | cli.StringFlag{ 44 | Name: "activation-type", 45 | Value: "protocol", 46 | Usage: "the type of action to invoke when the user clicks the toast", 47 | }, 48 | cli.StringFlag{ 49 | Name: "activation-arg", 50 | Usage: "the activation argument", 51 | }, 52 | cli.StringSliceFlag{ 53 | Name: "action", 54 | Usage: "optional action button", 55 | }, 56 | cli.StringSliceFlag{ 57 | Name: "action-type", 58 | Usage: "the type of action button", 59 | }, 60 | cli.StringSliceFlag{ 61 | Name: "action-arg", 62 | Usage: "the action button argument", 63 | }, 64 | cli.StringFlag{ 65 | Name: "audio", 66 | Value: "silent", 67 | Usage: "which kind of audio should be played", 68 | }, 69 | cli.BoolFlag{ 70 | Name: "loop", 71 | Usage: "whether to loop the audio", 72 | }, 73 | cli.StringFlag{ 74 | Name: "duration", 75 | Value: "short", 76 | Usage: "how long the toast should display for", 77 | }, 78 | } 79 | 80 | app.Action = func(c *cli.Context) error { 81 | appID := c.String("app-id") 82 | title := c.String("title") 83 | message := c.String("message") 84 | icon := c.String("icon") 85 | activationType := c.String("activation-type") 86 | activationArg := c.String("activation-arg") 87 | audio, _ := toast.Audio(c.String("audio")) 88 | duration, _ := toast.Duration(c.String("duration")) 89 | loop := c.Bool("loop") 90 | 91 | var actions []toast.Action 92 | actionTexts := c.StringSlice("action") 93 | actionTypes := c.StringSlice("action-type") 94 | actionArgs := c.StringSlice("action-arg") 95 | 96 | for index, actionLabel := range actionTexts { 97 | var actionType string = "protocol" 98 | var actionArg string 99 | if len(actionTypes) > index { 100 | actionType = actionTypes[index] 101 | } 102 | if len(actionArgs) > index { 103 | actionArg = actionArgs[index] 104 | } 105 | actions = append(actions, toast.Action{ 106 | Type: actionType, 107 | Label: actionLabel, 108 | Arguments: actionArg, 109 | }) 110 | } 111 | 112 | notification := &toast.Notification{ 113 | AppID: appID, 114 | Title: title, 115 | Message: message, 116 | Icon: icon, 117 | Actions: actions, 118 | ActivationType: activationType, 119 | ActivationArguments: activationArg, 120 | Audio: audio, 121 | Loop: loop, 122 | Duration: duration, 123 | } 124 | 125 | if err := notification.Push(); err != nil { 126 | log.Fatalln(err) 127 | } 128 | 129 | return nil 130 | } 131 | 132 | app.Run(os.Args) 133 | } 134 | -------------------------------------------------------------------------------- /cli/vendor/vendor.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "", 3 | "ignore": "test", 4 | "package": [ 5 | { 6 | "checksumSHA1": "gcLub3oB+u4QrOJZcYmk/y2AP4k=", 7 | "path": "github.com/nu7hatch/gouuid", 8 | "revision": "179d4d0c4d8d407a32af483c2354df1d2c91e6c3", 9 | "revisionTime": "2013-12-21T20:05:32Z" 10 | }, 11 | { 12 | "checksumSHA1": "9vSOc4Mdmlbtufhkno6UJ3SrDvk=", 13 | "path": "gopkg.in/toast.v1", 14 | "revision": "7275b4629f84b47238426495da88f43b4d1e31aa", 15 | "revisionTime": "2016-09-19T08:22:34Z" 16 | }, 17 | { 18 | "checksumSHA1": "/1TzqyoYSSdEkFrZG20XoNGwD8o=", 19 | "path": "gopkg.in/urfave/cli.v1", 20 | "revision": "a14d7d367bc02b1f57d88de97926727f2d936387", 21 | "revisionTime": "2016-08-29T00:43:50Z" 22 | } 23 | ], 24 | "rootPath": "github.com/go-toast/toast/cli" 25 | } 26 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Toast 2 | 3 | A go package for Windows 10 toast notifications. 4 | 5 | As seen in [jacobmarshall/pokevision-cli](https://github.com/jacobmarshall/pokevision-cli). 6 | 7 | ## CLI 8 | 9 | As well as using go-toast within your Go projects, you can also utilise the CLI - for any of your projects. 10 | 11 | Download [64bit](https://go-toast-downloads.s3.amazonaws.com/v1/toast64.exe) or [32bit](https://go-toast-downloads.s3.amazonaws.com/v1/toast32.exe) 12 | 13 | ```cmd 14 | C:\Users\Example\Downloads\toast64.exe \ 15 | --app-id "Example App" \ 16 | --title "Hello World" \ 17 | --message "Lorem ipsum dolor sit amet, consectetur adipiscing elit." \ 18 | --icon "C:\Users\Example\Pictures\icon.png" \ 19 | --audio "default" --loop \ 20 | --duration "long" \ 21 | --activation-arg "https://google.com" \ 22 | --action "Open maps" --action-arg "bingmaps:?q=sushi" \ 23 | --action "Open browser" --action-arg "http://..." 24 | ``` 25 | 26 | ![CLI](./screenshot-cli.png) 27 | 28 | ## Example 29 | 30 | ```go 31 | package main 32 | 33 | import ( 34 | "log" 35 | 36 | "gopkg.in/toast.v1" 37 | ) 38 | 39 | func main() { 40 | notification := toast.Notification{ 41 | AppID: "Example App", 42 | Title: "My notification", 43 | Message: "Some message about how important something is...", 44 | Icon: "go.png", // This file must exist (remove this line if it doesn't) 45 | Actions: []toast.Action{ 46 | {"protocol", "I'm a button", ""}, 47 | {"protocol", "Me too!", ""}, 48 | }, 49 | } 50 | err := notification.Push() 51 | if err != nil { 52 | log.Fatalln(err) 53 | } 54 | } 55 | ``` 56 | 57 | ## Screenshots 58 | 59 | ![Toast](./screenshot-toast.png) 60 | 61 | ![Action centre](./screenshot-action-centre.png) 62 | -------------------------------------------------------------------------------- /screenshot-action-centre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-toast/toast/01e6764cf0a44209189b7981cdc34284936f6891/screenshot-action-centre.png -------------------------------------------------------------------------------- /screenshot-cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-toast/toast/01e6764cf0a44209189b7981cdc34284936f6891/screenshot-cli.png -------------------------------------------------------------------------------- /screenshot-toast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-toast/toast/01e6764cf0a44209189b7981cdc34284936f6891/screenshot-toast.png -------------------------------------------------------------------------------- /toast.go: -------------------------------------------------------------------------------- 1 | package toast 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strings" 11 | "text/template" 12 | 13 | "github.com/nu7hatch/gouuid" 14 | "syscall" 15 | ) 16 | 17 | var toastTemplate *template.Template 18 | 19 | var ( 20 | ErrorInvalidAudio error = errors.New("toast: invalid audio") 21 | ErrorInvalidDuration = errors.New("toast: invalid duration") 22 | ) 23 | 24 | type toastAudio string 25 | 26 | const ( 27 | Default toastAudio = "ms-winsoundevent:Notification.Default" 28 | IM = "ms-winsoundevent:Notification.IM" 29 | Mail = "ms-winsoundevent:Notification.Mail" 30 | Reminder = "ms-winsoundevent:Notification.Reminder" 31 | SMS = "ms-winsoundevent:Notification.SMS" 32 | LoopingAlarm = "ms-winsoundevent:Notification.Looping.Alarm" 33 | LoopingAlarm2 = "ms-winsoundevent:Notification.Looping.Alarm2" 34 | LoopingAlarm3 = "ms-winsoundevent:Notification.Looping.Alarm3" 35 | LoopingAlarm4 = "ms-winsoundevent:Notification.Looping.Alarm4" 36 | LoopingAlarm5 = "ms-winsoundevent:Notification.Looping.Alarm5" 37 | LoopingAlarm6 = "ms-winsoundevent:Notification.Looping.Alarm6" 38 | LoopingAlarm7 = "ms-winsoundevent:Notification.Looping.Alarm7" 39 | LoopingAlarm8 = "ms-winsoundevent:Notification.Looping.Alarm8" 40 | LoopingAlarm9 = "ms-winsoundevent:Notification.Looping.Alarm9" 41 | LoopingAlarm10 = "ms-winsoundevent:Notification.Looping.Alarm10" 42 | LoopingCall = "ms-winsoundevent:Notification.Looping.Call" 43 | LoopingCall2 = "ms-winsoundevent:Notification.Looping.Call2" 44 | LoopingCall3 = "ms-winsoundevent:Notification.Looping.Call3" 45 | LoopingCall4 = "ms-winsoundevent:Notification.Looping.Call4" 46 | LoopingCall5 = "ms-winsoundevent:Notification.Looping.Call5" 47 | LoopingCall6 = "ms-winsoundevent:Notification.Looping.Call6" 48 | LoopingCall7 = "ms-winsoundevent:Notification.Looping.Call7" 49 | LoopingCall8 = "ms-winsoundevent:Notification.Looping.Call8" 50 | LoopingCall9 = "ms-winsoundevent:Notification.Looping.Call9" 51 | LoopingCall10 = "ms-winsoundevent:Notification.Looping.Call10" 52 | Silent = "silent" 53 | ) 54 | 55 | type toastDuration string 56 | 57 | const ( 58 | Short toastDuration = "short" 59 | Long = "long" 60 | ) 61 | 62 | func init() { 63 | toastTemplate = template.New("toast") 64 | toastTemplate.Parse(` 65 | [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null 66 | [Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null 67 | [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null 68 | 69 | $APP_ID = '{{if .AppID}}{{.AppID}}{{else}}Windows App{{end}}' 70 | 71 | $template = @" 72 | 73 | 74 | 75 | {{if .Icon}} 76 | 77 | {{end}} 78 | {{if .Title}} 79 | 80 | {{end}} 81 | {{if .Message}} 82 | 83 | {{end}} 84 | 85 | 86 | {{if ne .Audio "silent"}} 87 | 99 | "@ 100 | 101 | $xml = New-Object Windows.Data.Xml.Dom.XmlDocument 102 | $xml.LoadXml($template) 103 | $toast = New-Object Windows.UI.Notifications.ToastNotification $xml 104 | [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($APP_ID).Show($toast) 105 | `) 106 | } 107 | 108 | // Notification 109 | // 110 | // The toast notification data. The following fields are strongly recommended; 111 | // - AppID 112 | // - Title 113 | // 114 | // If no toastAudio is provided, then the toast notification will be silent. 115 | // You can set the toast to have a default audio by setting "Audio" to "toast.Default", or if your go app takes 116 | // user-provided input for audio, call the "toast.Audio(name)" func. 117 | // 118 | // The AppID is shown beneath the toast message (in certain cases), and above the notification within the Action 119 | // Center - and is used to group your notifications together. It is recommended that you provide a "pretty" 120 | // name for your app, and not something like "com.example.MyApp". 121 | // 122 | // If no Title is provided, but a Message is, the message will display as the toast notification's title - 123 | // which is a slightly different font style (heavier). 124 | // 125 | // The Icon should be an absolute path to the icon (as the toast is invoked from a temporary path on the user's 126 | // system, not the working directory). 127 | // 128 | // If you would like the toast to call an external process/open a webpage, then you can set ActivationArguments 129 | // to the uri you would like to trigger when the toast is clicked. For example: "https://google.com" would open 130 | // the Google homepage when the user clicks the toast notification. 131 | // By default, clicking the toast just hides/dismisses it. 132 | // 133 | // The following would show a notification to the user letting them know they received an email, and opens 134 | // gmail.com when they click the notification. It also makes the Windows 10 "mail" sound effect. 135 | // 136 | // toast := toast.Notification{ 137 | // AppID: "Google Mail", 138 | // Title: email.Subject, 139 | // Message: email.Preview, 140 | // Icon: "C:/Program Files/Google Mail/icons/logo.png", 141 | // ActivationArguments: "https://gmail.com", 142 | // Audio: toast.Mail, 143 | // } 144 | // 145 | // err := toast.Push() 146 | type Notification struct { 147 | // The name of your app. This value shows up in Windows 10's Action Centre, so make it 148 | // something readable for your users. It can contain spaces, however special characters 149 | // (eg. é) are not supported. 150 | AppID string 151 | 152 | // The main title/heading for the toast notification. 153 | Title string 154 | 155 | // The single/multi line message to display for the toast notification. 156 | Message string 157 | 158 | // An optional path to an image on the OS to display to the left of the title & message. 159 | Icon string 160 | 161 | // The type of notification level action (like toast.Action) 162 | ActivationType string 163 | 164 | // The activation/action arguments (invoked when the user clicks the notification) 165 | ActivationArguments string 166 | 167 | // Optional action buttons to display below the notification title & message. 168 | Actions []Action 169 | 170 | // The audio to play when displaying the toast 171 | Audio toastAudio 172 | 173 | // Whether to loop the audio (default false) 174 | Loop bool 175 | 176 | // How long the toast should show up for (short/long) 177 | Duration toastDuration 178 | } 179 | 180 | // Action 181 | // 182 | // Defines an actionable button. 183 | // See https://msdn.microsoft.com/en-us/windows/uwp/controls-and-patterns/tiles-and-notifications-adaptive-interactive-toasts for more info. 184 | // 185 | // Only protocol type action buttons are actually useful, as there's no way of receiving feedback from the 186 | // user's choice. Examples of protocol type action buttons include: "bingmaps:?q=sushi" to open up Windows 10's 187 | // maps app with a pre-populated search field set to "sushi". 188 | // 189 | // toast.Action{"protocol", "Open Maps", "bingmaps:?q=sushi"} 190 | type Action struct { 191 | Type string 192 | Label string 193 | Arguments string 194 | } 195 | 196 | func (n *Notification) applyDefaults() { 197 | if n.ActivationType == "" { 198 | n.ActivationType = "protocol" 199 | } 200 | if n.Duration == "" { 201 | n.Duration = Short 202 | } 203 | if n.Audio == "" { 204 | n.Audio = Default 205 | } 206 | } 207 | 208 | func (n *Notification) buildXML() (string, error) { 209 | var out bytes.Buffer 210 | err := toastTemplate.Execute(&out, n) 211 | if err != nil { 212 | return "", err 213 | } 214 | return out.String(), nil 215 | } 216 | 217 | // Builds the Windows PowerShell script & invokes it, causing the toast to display. 218 | // 219 | // Note: Running the PowerShell script is by far the slowest process here, and can take a few 220 | // seconds in some cases. 221 | // 222 | // notification := toast.Notification{ 223 | // AppID: "Example App", 224 | // Title: "My notification", 225 | // Message: "Some message about how important something is...", 226 | // Icon: "go.png", 227 | // Actions: []toast.Action{ 228 | // {"protocol", "I'm a button", ""}, 229 | // {"protocol", "Me too!", ""}, 230 | // }, 231 | // } 232 | // err := notification.Push() 233 | // if err != nil { 234 | // log.Fatalln(err) 235 | // } 236 | func (n *Notification) Push() error { 237 | n.applyDefaults() 238 | xml, err := n.buildXML() 239 | if err != nil { 240 | return err 241 | } 242 | return invokeTemporaryScript(xml) 243 | } 244 | 245 | // Returns a toastAudio given a user-provided input (useful for cli apps). 246 | // 247 | // If the "name" doesn't match, then the default toastAudio is returned, along with ErrorInvalidAudio. 248 | // 249 | // The following names are valid; 250 | // - default 251 | // - im 252 | // - mail 253 | // - reminder 254 | // - sms 255 | // - loopingalarm 256 | // - loopimgalarm[2-10] 257 | // - loopingcall 258 | // - loopingcall[2-10] 259 | // - silent 260 | // 261 | // Handle the error appropriately according to how your app should work. 262 | func Audio(name string) (toastAudio, error) { 263 | switch strings.ToLower(name) { 264 | case "default": 265 | return Default, nil 266 | case "im": 267 | return IM, nil 268 | case "mail": 269 | return Mail, nil 270 | case "reminder": 271 | return Reminder, nil 272 | case "sms": 273 | return SMS, nil 274 | case "loopingalarm": 275 | return LoopingAlarm, nil 276 | case "loopingalarm2": 277 | return LoopingAlarm2, nil 278 | case "loopingalarm3": 279 | return LoopingAlarm3, nil 280 | case "loopingalarm4": 281 | return LoopingAlarm4, nil 282 | case "loopingalarm5": 283 | return LoopingAlarm5, nil 284 | case "loopingalarm6": 285 | return LoopingAlarm6, nil 286 | case "loopingalarm7": 287 | return LoopingAlarm7, nil 288 | case "loopingalarm8": 289 | return LoopingAlarm8, nil 290 | case "loopingalarm9": 291 | return LoopingAlarm9, nil 292 | case "loopingalarm10": 293 | return LoopingAlarm10, nil 294 | case "loopingcall": 295 | return LoopingCall, nil 296 | case "loopingcall2": 297 | return LoopingCall2, nil 298 | case "loopingcall3": 299 | return LoopingCall3, nil 300 | case "loopingcall4": 301 | return LoopingCall4, nil 302 | case "loopingcall5": 303 | return LoopingCall5, nil 304 | case "loopingcall6": 305 | return LoopingCall6, nil 306 | case "loopingcall7": 307 | return LoopingCall7, nil 308 | case "loopingcall8": 309 | return LoopingCall8, nil 310 | case "loopingcall9": 311 | return LoopingCall9, nil 312 | case "loopingcall10": 313 | return LoopingCall10, nil 314 | case "silent": 315 | return Silent, nil 316 | default: 317 | return Default, ErrorInvalidAudio 318 | } 319 | } 320 | 321 | // Returns a toastDuration given a user-provided input (useful for cli apps). 322 | // 323 | // The default duration is short. If the "name" doesn't match, then the default toastDuration is returned, 324 | // along with ErrorInvalidDuration. Most of the time "short" is the most appropriate for a toast notification, 325 | // and Microsoft recommend not using "long", but it can be useful for important dialogs or looping sound toasts. 326 | // 327 | // The following names are valid; 328 | // - short 329 | // - long 330 | // 331 | // Handle the error appropriately according to how your app should work. 332 | func Duration(name string) (toastDuration, error) { 333 | switch strings.ToLower(name) { 334 | case "short": 335 | return Short, nil 336 | case "long": 337 | return Long, nil 338 | default: 339 | return Short, ErrorInvalidDuration 340 | } 341 | } 342 | 343 | func invokeTemporaryScript(content string) error { 344 | id, _ := uuid.NewV4() 345 | file := filepath.Join(os.TempDir(), id.String()+".ps1") 346 | defer os.Remove(file) 347 | bomUtf8 := []byte{0xEF, 0xBB, 0xBF} 348 | out := append(bomUtf8, []byte(content)...) 349 | err := ioutil.WriteFile(file, out, 0600) 350 | if err != nil { 351 | return err 352 | } 353 | cmd := exec.Command("PowerShell", "-ExecutionPolicy", "Bypass", "-File", file) 354 | cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} 355 | if err = cmd.Run(); err != nil { 356 | return err 357 | } 358 | return nil 359 | } 360 | -------------------------------------------------------------------------------- /vendor/vendor.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "", 3 | "ignore": "test", 4 | "package": [ 5 | { 6 | "checksumSHA1": "gcLub3oB+u4QrOJZcYmk/y2AP4k=", 7 | "path": "github.com/nu7hatch/gouuid", 8 | "revision": "179d4d0c4d8d407a32af483c2354df1d2c91e6c3", 9 | "revisionTime": "2013-12-21T20:05:32Z" 10 | } 11 | ], 12 | "rootPath": "github.com/go-toast/toast" 13 | } 14 | --------------------------------------------------------------------------------