noti.go (5621B)
1 //go:build darwin && cgo 2 3 package notify 4 5 /* 6 #cgo LDFLAGS: -framework Foundation -framework UserNotifications 7 #cgo CFLAGS: -x objective-c -fobjc-arc -fmodules -O3 -Wall -Wextra 8 #pragma clang diagnostic ignored "-Wformat-security" 9 #pragma clang diagnostic ignored "-Wunused-variable" 10 #include <stdlib.h> 11 #include <stdint.h> 12 #include "noti.h" 13 */ 14 import "C" 15 16 import ( 17 "bytes" 18 "context" 19 "encoding/json" 20 "log/slog" 21 "runtime" 22 "sync/atomic" 23 "unsafe" 24 ) 25 26 func init() { 27 runtime.LockOSThread() 28 C.noti_setup() 29 } 30 31 var ( 32 // logger is the package logger to use. 33 logger atomic.Pointer[slog.Logger] 34 35 // callback is the pure Go hook that the application registers in order to receive user 36 // interactions from notifications. 37 callback atomic.Pointer[CallbackFunc] 38 ) 39 40 // SetCallback sets the application callback to be invoked upon user interaction 41 // with a notification. 42 func SetCallback(cb CallbackFunc) { 43 callback.Store(&cb) 44 } 45 46 // SetLogger sets the package logger to use. 47 func SetLogger(l *slog.Logger) { 48 logger.Swap(l) 49 } 50 51 // Init declares the set of notification categories for the application. 52 func Init(categories ...Category) { 53 for _, category := range categories { 54 C.noti_category_register(category.toCType()) 55 } 56 } 57 58 // Notify pushes the notification to the system. 59 // Set a logger to view errors. 60 func Notify(n Notification) { 61 C.noti_notify(n.toCType()) 62 } 63 64 // Cancel notifications for the package. 65 func Cancel() { 66 C.noti_cancel() 67 } 68 69 /* 70 To keep things simple (dealing with nested objects), we tie the C memory 71 to the lifetime of the various Go objects by doing the frees in a runtime 72 finalizer. 73 */ 74 75 func (cc *Category) toCType() (c C.Category) { 76 var p runtime.Pinner 77 78 c.id = C.CString(cc.ID) 79 c.summary = C.CString(cc.Summary) 80 c.preview = C.CString(cc.Preview) 81 82 cActions := make([]C.Action, len(cc.Actions)) 83 cTextInputActions := make([]C.TextInputAction, len(cc.TextInputActions)) 84 85 for ii, action := range cc.Actions { 86 cActions[ii] = action.toCType() 87 } 88 89 for ii, action := range cc.TextInputActions { 90 cTextInputActions[ii] = action.toCType() 91 } 92 93 c.actions = unsafe.SliceData(cActions) 94 c.actions_len = C.uintptr_t(len(cActions)) 95 c.text_input_actions = unsafe.SliceData(cTextInputActions) 96 c.text_input_actions_len = C.uintptr_t(len(cTextInputActions)) 97 98 runtime.SetFinalizer(cc, func(_ *Category) { 99 C.free(unsafe.Pointer(c.id)) 100 C.free(unsafe.Pointer(c.summary)) 101 C.free(unsafe.Pointer(c.preview)) 102 p.Unpin() 103 }) 104 105 p.Pin(unsafe.SliceData(cActions)) 106 p.Pin(unsafe.SliceData(cTextInputActions)) 107 108 return c 109 } 110 111 func (n *Notification) toCType() (c C.Notification) { 112 var p runtime.Pinner 113 114 c.title = C.CString(n.Title) 115 c.sub_title = C.CString(n.Subtitle) 116 c.body = C.CString(n.Body) 117 c.category_id = C.CString(n.Category) 118 119 userDataBytes, err := json.Marshal(n.UserData) 120 if len(userDataBytes) == 0 || bytes.Equal(userDataBytes, []byte("null")) || err != nil { 121 c.user_data = C.CString(`"{}"`) 122 } else { 123 c.user_data = C.CString(string(userDataBytes)) 124 } 125 126 cAttachments := make([]*C.char, len(n.Attachments)) 127 cActions := make([]C.Action, len(n.Actions)) 128 cTextInputActions := make([]C.TextInputAction, len(n.TextInputActions)) 129 130 for ii, a := range n.Attachments { 131 cAttachments[ii] = C.CString(a) 132 } 133 134 for ii, action := range n.Actions { 135 cActions[ii] = action.toCType() 136 } 137 138 for ii, action := range n.TextInputActions { 139 cTextInputActions[ii] = action.toCType() 140 } 141 142 c.attachments = unsafe.SliceData(cAttachments) 143 c.attachments_len = C.uintptr_t(len(cAttachments)) 144 c.actions = unsafe.SliceData(cActions) 145 c.actions_len = C.uintptr_t(len(cActions)) 146 c.text_input_actions = unsafe.SliceData(cTextInputActions) 147 c.text_input_actions_len = C.uintptr_t(len(cTextInputActions)) 148 149 runtime.SetFinalizer(n, func(_ *Notification) { 150 C.free(unsafe.Pointer(c.title)) 151 C.free(unsafe.Pointer(c.sub_title)) 152 C.free(unsafe.Pointer(c.body)) 153 C.free(unsafe.Pointer(c.category_id)) 154 C.free(unsafe.Pointer(c.user_data)) 155 for ii := range cAttachments { 156 C.free(unsafe.Pointer(cAttachments[ii])) 157 } 158 p.Unpin() 159 }) 160 161 p.Pin(unsafe.SliceData(userDataBytes)) 162 p.Pin(unsafe.SliceData(cAttachments)) 163 p.Pin(unsafe.SliceData(cActions)) 164 p.Pin(unsafe.SliceData(cTextInputActions)) 165 166 return c 167 } 168 169 func (a *Action) toCType() (c C.Action) { 170 c.id = C.CString(a.ID) 171 c.icon = C.CString(a.Icon) 172 c.title = C.CString(a.Title) 173 174 runtime.SetFinalizer(a, func(_ *Action) { 175 C.free(unsafe.Pointer(c.id)) 176 C.free(unsafe.Pointer(c.title)) 177 C.free(unsafe.Pointer(c.icon)) 178 }) 179 180 return c 181 } 182 183 func (a *TextInputAction) toCType() (c C.TextInputAction) { 184 c.id = C.CString(a.ID) 185 c.icon = C.CString(a.Icon) 186 c.title = C.CString(a.Title) 187 c.button_title = C.CString(a.ButtonTitle) 188 c.placeholder = C.CString(a.Placeholder) 189 190 runtime.SetFinalizer(a, func(_ *TextInputAction) { 191 C.free(unsafe.Pointer(c.id)) 192 C.free(unsafe.Pointer(c.title)) 193 C.free(unsafe.Pointer(c.icon)) 194 C.free(unsafe.Pointer(c.button_title)) 195 C.free(unsafe.Pointer(c.placeholder)) 196 }) 197 198 return c 199 } 200 201 //export gocallback 202 func gocallback( 203 categoryID *C.cchar_t, 204 actionID *C.cchar_t, 205 userText *C.cchar_t, 206 userData *C.cchar_t, 207 ) { 208 cb := callback.Load() 209 210 if cb == nil { 211 return 212 } 213 214 category := C.GoString(categoryID) 215 action := C.GoString(actionID) 216 text := C.GoString(userText) 217 data := C.GoString(userData) 218 219 var v map[string]string 220 err := json.Unmarshal([]byte(data), &v) 221 222 (*cb)(CallbackArgs{ 223 Category: CategoryID(category), 224 Action: ActionID(action), 225 UserText: text, 226 UserData: v, 227 Err: err, 228 }) 229 } 230 231 //export golog 232 func golog(level C.int, msg *C.cchar_t) { 233 slog.Log(context.Background(), slog.Level(level), C.GoString(msg)) 234 }