├── .github └── workflows │ └── build.yml ├── LICENSE ├── README.md ├── alert_darwin.go ├── alert_js.go ├── alert_test.go ├── alert_unix.go ├── alert_unsupported.go ├── alert_windows.go ├── assets ├── information.png └── warning.png ├── beeep.go ├── beep_darwin.go ├── beep_js.go ├── beep_test.go ├── beep_unix.go ├── beep_unsupported.go ├── beep_windows.go ├── example_test.go ├── go.mod ├── go.sum ├── notify_darwin.go ├── notify_js.go ├── notify_test.go ├── notify_unix.go ├── notify_unix_nodbus.go ├── notify_unsupported.go └── notify_windows.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Build 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.20.x] 8 | os: [ubuntu-latest, macos-latest, windows-latest] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v3 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v3 17 | - name: Build 18 | run: go build 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Milan Nikolic 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## beeep 2 | [![Build Status](https://github.com/gen2brain/beeep/actions/workflows/build.yml/badge.svg)](https://github.com/gen2brain/beeep/actions) 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/gen2brain/beeep.svg)](https://pkg.go.dev/github.com/gen2brain/beeep) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/gen2brain/beeep?branch=master)](https://goreportcard.com/report/github.com/gen2brain/beeep) 5 | 6 | `beeep` provides a cross-platform library for sending desktop notifications, alerts and beeps. 7 | 8 | ### Installation 9 | 10 | go get -u github.com/gen2brain/beeep 11 | 12 | ### Build tags 13 | 14 | * `nodbus` - disable `godbus/dbus` and use only `notify-send` 15 | 16 | ### Examples 17 | 18 | ```go 19 | err := beeep.Beep(beeep.DefaultFreq, beeep.DefaultDuration) 20 | if err != nil { 21 | panic(err) 22 | } 23 | ``` 24 | 25 | ```go 26 | err := beeep.Notify("Title", "Message body", "assets/information.png") 27 | if err != nil { 28 | panic(err) 29 | } 30 | ``` 31 | 32 | ```go 33 | err := beeep.Alert("Title", "Message body", "assets/warning.png") 34 | if err != nil { 35 | panic(err) 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /alert_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin && !linux && !freebsd && !netbsd && !openbsd && !windows && !js 2 | // +build darwin,!linux,!freebsd,!netbsd,!openbsd,!windows,!js 3 | 4 | package beeep 5 | 6 | import ( 7 | "fmt" 8 | "os/exec" 9 | ) 10 | 11 | // Alert displays a desktop notification and plays a default system sound. 12 | func Alert(title, message, appIcon string) error { 13 | osa, err := exec.LookPath("osascript") 14 | if err != nil { 15 | return err 16 | } 17 | 18 | script := fmt.Sprintf("display notification %q with title %q sound name \"default\"", message, title) 19 | cmd := exec.Command(osa, "-e", script) 20 | return cmd.Run() 21 | } 22 | -------------------------------------------------------------------------------- /alert_js.go: -------------------------------------------------------------------------------- 1 | //go:build js 2 | // +build js 3 | 4 | package beeep 5 | 6 | // Alert displays a desktop notification and plays a beep. 7 | func Alert(title, message, appIcon string) error { 8 | if err := Notify(title, message, appIcon); err != nil { 9 | return err 10 | } 11 | return Beep(DefaultFreq, DefaultDuration) 12 | } 13 | -------------------------------------------------------------------------------- /alert_test.go: -------------------------------------------------------------------------------- 1 | package beeep 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAlert(t *testing.T) { 8 | err := Alert("Alert title", "Message body", "assets/warning.png") 9 | if err != nil { 10 | t.Error(err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /alert_unix.go: -------------------------------------------------------------------------------- 1 | //go:build linux || freebsd || netbsd || openbsd || illumos 2 | // +build linux freebsd netbsd openbsd illumos 3 | 4 | package beeep 5 | 6 | // Alert displays a desktop notification and plays a beep. 7 | func Alert(title, message, appIcon string) error { 8 | if err := Notify(title, message, appIcon); err != nil { 9 | return err 10 | } 11 | return Beep(DefaultFreq, DefaultDuration) 12 | } 13 | -------------------------------------------------------------------------------- /alert_unsupported.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !freebsd && !netbsd && !openbsd && !windows && !darwin && !illumos && !js 2 | // +build !linux,!freebsd,!netbsd,!openbsd,!windows,!darwin,!illumos,!js 3 | 4 | package beeep 5 | 6 | // Alert displays a desktop notification and plays a beep. 7 | func Alert(title, message, appIcon string) error { 8 | return ErrUnsupported 9 | } 10 | -------------------------------------------------------------------------------- /alert_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows && !linux && !freebsd && !netbsd && !openbsd && !darwin && !js 2 | // +build windows,!linux,!freebsd,!netbsd,!openbsd,!darwin,!js 3 | 4 | package beeep 5 | 6 | import ( 7 | toast "github.com/go-toast/toast" 8 | ) 9 | 10 | // Alert displays a desktop notification and plays a default system sound. 11 | func Alert(title, message, appIcon string) error { 12 | if isWindows10 { 13 | note := toastNotification(title, message, pathAbs(appIcon)) 14 | note.Audio = toast.Default 15 | return note.Push() 16 | } 17 | 18 | if err := Notify(title, message, appIcon); err != nil { 19 | return err 20 | } 21 | return Beep(DefaultFreq, DefaultDuration) 22 | } 23 | -------------------------------------------------------------------------------- /assets/information.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gen2brain/beeep/9c006672e7f4051844d5696b32db98b78abb7262/assets/information.png -------------------------------------------------------------------------------- /assets/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gen2brain/beeep/9c006672e7f4051844d5696b32db98b78abb7262/assets/warning.png -------------------------------------------------------------------------------- /beeep.go: -------------------------------------------------------------------------------- 1 | // Package beeep provides a cross-platform library for sending desktop notifications and beeps. 2 | package beeep 3 | 4 | import ( 5 | "errors" 6 | "path/filepath" 7 | "runtime" 8 | ) 9 | 10 | var ( 11 | // ErrUnsupported is returned when operating system is not supported. 12 | ErrUnsupported = errors.New("beeep: unsupported operating system: " + runtime.GOOS) 13 | ) 14 | 15 | func pathAbs(path string) string { 16 | var err error 17 | var abs string 18 | 19 | if path != "" { 20 | abs, err = filepath.Abs(path) 21 | if err != nil { 22 | abs = path 23 | } 24 | } 25 | 26 | return abs 27 | } 28 | -------------------------------------------------------------------------------- /beep_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin && !linux && !freebsd && !netbsd && !openbsd && !windows && !js 2 | // +build darwin,!linux,!freebsd,!netbsd,!openbsd,!windows,!js 3 | 4 | package beeep 5 | 6 | import ( 7 | "os" 8 | "os/exec" 9 | ) 10 | 11 | var ( 12 | // DefaultFreq - frequency, in Hz, middle A 13 | DefaultFreq = 0.0 14 | // DefaultDuration - duration in milliseconds 15 | DefaultDuration = 0 16 | ) 17 | 18 | // Beep beeps the PC speaker (https://en.wikipedia.org/wiki/PC_speaker). 19 | func Beep(freq float64, duration int) error { 20 | osa, err := exec.LookPath("osascript") 21 | if err != nil { 22 | // Output the only beep we can 23 | _, err = os.Stdout.Write([]byte{7}) 24 | return err 25 | } 26 | 27 | cmd := exec.Command(osa, "-e", `beep`) 28 | return cmd.Run() 29 | } 30 | -------------------------------------------------------------------------------- /beep_js.go: -------------------------------------------------------------------------------- 1 | //go:build js 2 | // +build js 3 | 4 | package beeep 5 | 6 | import ( 7 | "syscall/js" 8 | ) 9 | 10 | var ( 11 | // DefaultFreq - frequency, in Hz, middle A 12 | DefaultFreq = 0.0 13 | // DefaultDuration - duration in milliseconds 14 | DefaultDuration = 0 15 | ) 16 | 17 | // Beep beeps the PC speaker (https://en.wikipedia.org/wiki/PC_speaker). 18 | func Beep(freq float64, duration int) (err error) { 19 | defer func() { 20 | e := recover() 21 | 22 | if e == nil { 23 | return 24 | } 25 | 26 | if e, ok := e.(*js.Error); ok { 27 | err = e 28 | } else { 29 | panic(e) 30 | } 31 | }() 32 | 33 | a := js.Global().Get("document").Call("createElement", "audio") 34 | a.Set("src", `data:audio/wav;base64,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qJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU=`) 35 | a.Call("play") 36 | 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /beep_test.go: -------------------------------------------------------------------------------- 1 | package beeep 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestBeep(t *testing.T) { 8 | err := Beep(DefaultFreq, DefaultDuration) 9 | if err != nil { 10 | t.Error(err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /beep_unix.go: -------------------------------------------------------------------------------- 1 | //go:build linux || freebsd || netbsd || openbsd || illumos 2 | // +build linux freebsd netbsd openbsd illumos 3 | 4 | package beeep 5 | 6 | import ( 7 | "errors" 8 | "os" 9 | "syscall" 10 | "time" 11 | "unsafe" 12 | ) 13 | 14 | // Constants 15 | const ( 16 | // This number represents the fixed frequency of the original PC XT's timer chip, which is approximately 1.193 MHz. This number 17 | // is divided with the desired frequency to obtain a counter value, that is subsequently fed into the timer chip, tied to the PC speaker. 18 | clockTickRate = 1193180 19 | 20 | // linux/kd.h, start sound generation (0 for off) 21 | kiocsound = 0x4B2F 22 | 23 | // linux/input-event-codes.h 24 | evSnd = 0x12 // Event type 25 | sndTone = 0x02 // Sound 26 | ) 27 | 28 | var ( 29 | // DefaultFreq - frequency, in Hz, middle A 30 | DefaultFreq = 440.0 31 | // DefaultDuration - duration in milliseconds 32 | DefaultDuration = 200 33 | ) 34 | 35 | // inputEvent represents linux/input.h event structure. 36 | type inputEvent struct { 37 | Time syscall.Timeval // time in seconds since epoch at which event occurred 38 | Type uint16 // event type 39 | Code uint16 // event code related to the event type 40 | Value int32 // event value related to the event type 41 | } 42 | 43 | // ioctl system call manipulates the underlying device parameters of special files. 44 | func ioctl(fd, name, data uintptr) error { 45 | _, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, name, data) 46 | if e != 0 { 47 | return e 48 | } 49 | 50 | return nil 51 | } 52 | 53 | // Beep beeps the PC speaker (https://en.wikipedia.org/wiki/PC_speaker). 54 | // 55 | // On Linux it needs permission to access `/dev/tty0` or `/dev/input/by-path/platform-pcspkr-event-spkr` files for writing, 56 | // and `pcspkr` module must be loaded. User must be in correct groups, usually `input` and/or `tty`. 57 | // 58 | // If it can not open device files, it will fallback to sending Bell character (https://en.wikipedia.org/wiki/Bell_character). 59 | // For bell character in X11 terminals you can enable bell with `xset b on`. For console check `setterm` and `--blength` or `--bfreq` options. 60 | // 61 | // On macOS this just sends bell character. Enable `Audible bell` in Terminal --> Preferences --> Settings --> Advanced. 62 | // 63 | // On Windows it uses Beep function via syscall. 64 | // 65 | // On Web it plays hard coded beep sound. 66 | func Beep(freq float64, duration int) error { 67 | if freq == 0 { 68 | freq = DefaultFreq 69 | } else if freq > 20000 { 70 | freq = 20000 71 | } else if freq < 0 { 72 | freq = DefaultFreq 73 | } 74 | 75 | if duration == 0 { 76 | duration = DefaultDuration 77 | } 78 | 79 | period := int(float64(clockTickRate) / freq) 80 | 81 | var evdev bool 82 | 83 | f, err := os.OpenFile("/dev/tty0", os.O_WRONLY, 0644) 84 | if err != nil { 85 | e := err 86 | f, err = os.OpenFile("/dev/input/by-path/platform-pcspkr-event-spkr", os.O_WRONLY, 0644) 87 | if err != nil { 88 | e = errors.New("beeep: " + e.Error() + "; " + err.Error()) 89 | 90 | // Output the only beep we can 91 | _, err = os.Stdout.Write([]byte{7}) 92 | if err != nil { 93 | return errors.New(e.Error() + "; " + err.Error()) 94 | } 95 | 96 | return nil 97 | } 98 | 99 | evdev = true 100 | } 101 | 102 | defer f.Close() 103 | 104 | if evdev { // Use Linux evdev API 105 | ev := inputEvent{} 106 | ev.Type = evSnd 107 | ev.Code = sndTone 108 | ev.Value = int32(freq) 109 | 110 | d := *(*[unsafe.Sizeof(ev)]byte)(unsafe.Pointer(&ev)) 111 | 112 | // Start beep 113 | f.Write(d[:]) 114 | 115 | time.Sleep(time.Duration(duration) * time.Millisecond) 116 | 117 | ev.Value = 0 118 | d = *(*[unsafe.Sizeof(ev)]byte)(unsafe.Pointer(&ev)) 119 | 120 | // Stop beep 121 | f.Write(d[:]) 122 | } else { // Use ioctl 123 | // Start beep 124 | err = ioctl(f.Fd(), kiocsound, uintptr(period)) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | time.Sleep(time.Duration(duration) * time.Millisecond) 130 | 131 | // Stop beep 132 | err = ioctl(f.Fd(), kiocsound, uintptr(0)) 133 | if err != nil { 134 | return err 135 | } 136 | } 137 | 138 | return nil 139 | } 140 | -------------------------------------------------------------------------------- /beep_unsupported.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !freebsd && !netbsd && !openbsd && !windows && !darwin && !illumos && !js 2 | // +build !linux,!freebsd,!netbsd,!openbsd,!windows,!darwin,!illumos,!js 3 | 4 | package beeep 5 | 6 | var ( 7 | // DefaultFreq - frequency, in Hz, middle A 8 | DefaultFreq = 0.0 9 | // DefaultDuration - duration in milliseconds 10 | DefaultDuration = 0 11 | ) 12 | 13 | // Beep beeps the PC speaker (https://en.wikipedia.org/wiki/PC_speaker). 14 | func Beep(freq float64, duration int) error { 15 | return ErrUnsupported 16 | } 17 | -------------------------------------------------------------------------------- /beep_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows && !linux && !freebsd && !netbsd && !openbsd && !darwin && !js 2 | // +build windows,!linux,!freebsd,!netbsd,!openbsd,!darwin,!js 3 | 4 | package beeep 5 | 6 | import ( 7 | "syscall" 8 | ) 9 | 10 | var ( 11 | // DefaultFreq - frequency, in Hz, middle A 12 | DefaultFreq = 587.0 13 | // DefaultDuration - duration in milliseconds 14 | DefaultDuration = 500 15 | ) 16 | 17 | // Beep beeps the PC speaker (https://en.wikipedia.org/wiki/PC_speaker). 18 | func Beep(freq float64, duration int) error { 19 | if freq == 0 { 20 | freq = DefaultFreq 21 | } else if freq > 32767 { 22 | freq = 32767 23 | } else if freq < 37 { 24 | freq = DefaultFreq 25 | } 26 | 27 | if duration == 0 { 28 | duration = DefaultDuration 29 | } 30 | 31 | kernel32, _ := syscall.LoadLibrary("kernel32.dll") 32 | beep32, _ := syscall.GetProcAddress(kernel32, "Beep") 33 | 34 | defer syscall.FreeLibrary(kernel32) 35 | 36 | _, _, e := syscall.Syscall(uintptr(beep32), uintptr(2), uintptr(int(freq)), uintptr(duration), 0) 37 | if e != 0 { 38 | return e 39 | } 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package beeep 2 | 3 | func ExampleBeep() { 4 | Beep(DefaultFreq, DefaultDuration) 5 | } 6 | 7 | func ExampleNotify() { 8 | Notify("Title", "MessageBody", "assets/information.png") 9 | } 10 | 11 | func ExampleAlert() { 12 | Alert("Title", "MessageBody", "assets/warning.png") 13 | } 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gen2brain/beeep 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 7 | github.com/godbus/dbus/v5 v5.1.0 8 | github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af 9 | golang.org/x/sys v0.6.0 10 | ) 11 | 12 | require github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE= 2 | github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10= 3 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= 4 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 5 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= 6 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= 7 | github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk= 8 | github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o= 9 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 10 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 11 | -------------------------------------------------------------------------------- /notify_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin && !linux && !freebsd && !netbsd && !openbsd && !windows && !js 2 | // +build darwin,!linux,!freebsd,!netbsd,!openbsd,!windows,!js 3 | 4 | package beeep 5 | 6 | import ( 7 | "fmt" 8 | "os/exec" 9 | ) 10 | 11 | // Notify sends desktop notification. 12 | // 13 | // On macOS this executes AppleScript with `osascript` binary. 14 | func Notify(title, message, appIcon string) error { 15 | osa, err := exec.LookPath("osascript") 16 | if err != nil { 17 | return err 18 | } 19 | 20 | script := fmt.Sprintf("display notification %q with title %q", message, title) 21 | cmd := exec.Command(osa, "-e", script) 22 | return cmd.Run() 23 | } 24 | -------------------------------------------------------------------------------- /notify_js.go: -------------------------------------------------------------------------------- 1 | //go:build js 2 | // +build js 3 | 4 | package beeep 5 | 6 | import ( 7 | "syscall/js" 8 | ) 9 | 10 | // Notify sends desktop notification. 11 | // 12 | // On Web, in Firefox it just works, in Chrome you must call it from some "user gesture" like `onclick`, 13 | // and you must use TLS certificate, it doesn't work with plain http. 14 | func Notify(title, message, appIcon string) (err error) { 15 | defer func() { 16 | e := recover() 17 | 18 | if e == nil { 19 | return 20 | } 21 | 22 | if e, ok := e.(*js.Error); ok { 23 | err = e 24 | } else { 25 | panic(e) 26 | } 27 | }() 28 | 29 | n := js.Global().Get("Notification") 30 | 31 | opts := js.Global().Get("Object").Invoke() 32 | opts.Set("body", message) 33 | opts.Set("icon", pathAbs(appIcon)) 34 | 35 | if n.Get("permission").String() == "granted" { 36 | n.New(js.ValueOf(title), opts) 37 | } else { 38 | var f js.Func 39 | f = js.FuncOf(func(this js.Value, args []js.Value) interface{} { 40 | if args[0].String() == "granted" { 41 | n.New(js.ValueOf(title), opts) 42 | } 43 | f.Release() 44 | return nil 45 | }) 46 | 47 | n.Call("requestPermission", f) 48 | } 49 | 50 | return 51 | } 52 | -------------------------------------------------------------------------------- /notify_test.go: -------------------------------------------------------------------------------- 1 | package beeep 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNotify(t *testing.T) { 8 | err := Notify("Notify title", "Message body", "assets/information.png") 9 | if err != nil { 10 | t.Error(err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /notify_unix.go: -------------------------------------------------------------------------------- 1 | //go:build (linux && !nodbus) || (freebsd && !nodbus) || (netbsd && !nodbus) || (openbsd && !nodbus) 2 | // +build linux,!nodbus freebsd,!nodbus netbsd,!nodbus openbsd,!nodbus 3 | 4 | package beeep 5 | 6 | import ( 7 | "errors" 8 | "os/exec" 9 | 10 | "github.com/godbus/dbus/v5" 11 | ) 12 | 13 | // Notify sends desktop notification. 14 | // 15 | // On Linux it tries to send notification via D-Bus and it will fallback to `notify-send` binary. 16 | func Notify(title, message, appIcon string) error { 17 | appIcon = pathAbs(appIcon) 18 | 19 | cmd := func() error { 20 | send, err := exec.LookPath("sw-notify-send") 21 | if err != nil { 22 | send, err = exec.LookPath("notify-send") 23 | if err != nil { 24 | return err 25 | } 26 | } 27 | 28 | c := exec.Command(send, title, message, "-i", appIcon) 29 | return c.Run() 30 | } 31 | 32 | knotify := func() error { 33 | send, err := exec.LookPath("kdialog") 34 | if err != nil { 35 | return err 36 | } 37 | c := exec.Command(send, "--title", title, "--passivepopup", message, "10", "--icon", appIcon) 38 | return c.Run() 39 | } 40 | 41 | conn, err := dbus.SessionBus() 42 | if err != nil { 43 | return cmd() 44 | } 45 | obj := conn.Object("org.freedesktop.Notifications", dbus.ObjectPath("/org/freedesktop/Notifications")) 46 | 47 | call := obj.Call("org.freedesktop.Notifications.Notify", 0, "", uint32(0), appIcon, title, message, []string{}, map[string]dbus.Variant{}, int32(-1)) 48 | if call.Err != nil { 49 | e := cmd() 50 | if e != nil { 51 | e := knotify() 52 | if e != nil { 53 | return errors.New("beeep: " + call.Err.Error() + "; " + e.Error()) 54 | } 55 | } 56 | } 57 | 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /notify_unix_nodbus.go: -------------------------------------------------------------------------------- 1 | //go:build (linux && nodbus) || (freebsd && nodbus) || (netbsd && nodbus) || (openbsd && nodbus) || illumos 2 | // +build linux,nodbus freebsd,nodbus netbsd,nodbus openbsd,nodbus illumos 3 | 4 | package beeep 5 | 6 | import ( 7 | "errors" 8 | "os/exec" 9 | ) 10 | 11 | // Notify sends desktop notification. 12 | func Notify(title, message, appIcon string) error { 13 | appIcon = pathAbs(appIcon) 14 | 15 | cmd := func() error { 16 | send, err := exec.LookPath("sw-notify-send") 17 | if err != nil { 18 | send, err = exec.LookPath("notify-send") 19 | if err != nil { 20 | return err 21 | } 22 | } 23 | 24 | c := exec.Command(send, title, message, "-i", appIcon) 25 | return c.Run() 26 | } 27 | 28 | knotify := func() error { 29 | send, err := exec.LookPath("kdialog") 30 | if err != nil { 31 | return err 32 | } 33 | c := exec.Command(send, "--title", title, "--passivepopup", message, "10", "--icon", appIcon) 34 | return c.Run() 35 | } 36 | 37 | err := cmd() 38 | if err != nil { 39 | e := knotify() 40 | if e != nil { 41 | return errors.New("beeep: " + err.Error() + "; " + e.Error()) 42 | } 43 | } 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /notify_unsupported.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !freebsd && !netbsd && !openbsd && !windows && !darwin && !illumos && !js 2 | // +build !linux,!freebsd,!netbsd,!openbsd,!windows,!darwin,!illumos,!js 3 | 4 | package beeep 5 | 6 | // Notify sends desktop notification. 7 | func Notify(title, message, appIcon string) error { 8 | return ErrUnsupported 9 | } 10 | -------------------------------------------------------------------------------- /notify_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows && !linux && !freebsd && !netbsd && !openbsd && !darwin && !js 2 | // +build windows,!linux,!freebsd,!netbsd,!openbsd,!darwin,!js 3 | 4 | package beeep 5 | 6 | import ( 7 | "bufio" 8 | "bytes" 9 | "errors" 10 | "os/exec" 11 | "strings" 12 | "syscall" 13 | "time" 14 | 15 | toast "github.com/go-toast/toast" 16 | "github.com/tadvi/systray" 17 | "golang.org/x/sys/windows/registry" 18 | ) 19 | 20 | var isWindows10 bool 21 | var applicationID string 22 | 23 | func init() { 24 | k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) 25 | if err != nil { 26 | return 27 | } 28 | defer k.Close() 29 | 30 | maj, _, err := k.GetIntegerValue("CurrentMajorVersionNumber") 31 | if err != nil { 32 | return 33 | } 34 | 35 | isWindows10 = maj == 10 36 | 37 | if isWindows10 { 38 | applicationID = appID() 39 | } 40 | } 41 | 42 | // Notify sends desktop notification. 43 | func Notify(title, message, appIcon string) error { 44 | if isWindows10 { 45 | return toastNotify(title, message, appIcon) 46 | } 47 | 48 | err := baloonNotify(title, message, appIcon, false) 49 | if err != nil { 50 | e := msgNotify(title, message) 51 | if e != nil { 52 | return errors.New("beeep: " + err.Error() + "; " + e.Error()) 53 | } 54 | } 55 | 56 | return nil 57 | 58 | } 59 | 60 | func msgNotify(title, message string) error { 61 | msg, err := exec.LookPath("msg") 62 | if err != nil { 63 | return err 64 | } 65 | cmd := exec.Command(msg, "*", "/TIME:3", title+"\n\n"+message) 66 | cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} 67 | return cmd.Run() 68 | } 69 | 70 | func baloonNotify(title, message, appIcon string, bigIcon bool) error { 71 | tray, err := systray.New() 72 | if err != nil { 73 | return err 74 | } 75 | 76 | err = tray.ShowCustom(pathAbs(appIcon), title) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | go func() { 82 | go func() { 83 | _ = tray.Run() 84 | }() 85 | time.Sleep(3 * time.Second) 86 | _ = tray.Stop() 87 | }() 88 | 89 | return tray.ShowMessage(title, message, bigIcon) 90 | } 91 | 92 | func toastNotify(title, message, appIcon string) error { 93 | notification := toastNotification(title, message, pathAbs(appIcon)) 94 | return notification.Push() 95 | } 96 | 97 | func toastNotification(title, message, appIcon string) toast.Notification { 98 | return toast.Notification{ 99 | AppID: applicationID, 100 | Title: title, 101 | Message: message, 102 | Icon: appIcon, 103 | } 104 | } 105 | 106 | func appID() string { 107 | defID := "{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe" 108 | cmd := exec.Command("powershell", "-NoProfile", "Get-StartApps") 109 | cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} 110 | out, err := cmd.Output() 111 | if err != nil { 112 | return defID 113 | } 114 | 115 | scanner := bufio.NewScanner(bytes.NewReader(out)) 116 | for scanner.Scan() { 117 | line := strings.TrimSpace(scanner.Text()) 118 | if strings.Contains(line, "powershell.exe") { 119 | sp := strings.Split(line, " ") 120 | if len(sp) > 0 { 121 | return sp[len(sp)-1] 122 | } 123 | } 124 | } 125 | 126 | return defID 127 | } 128 | --------------------------------------------------------------------------------