go-notify-darwin

Rich notifications on macOS for Go
Log | Files | Refs | README | LICENSE

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 }