// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package notificationui /* #cgo pkg-config: gtk+-3.0 #include #include "native.h" */ import "C" import ( "bufio" "context" "encoding/json" "errors" "io" "runtime" "time" "unsafe" ) // Run owns all GTK calls on one OS thread. The parent supplies a private pipe; // no widget invokes speech directly. func Run(ctx context.Context, input io.Reader, output io.Writer) error { runtime.LockOSThread() defer runtime.UnlockOSThread() ctx, cancel := context.WithCancel(ctx) defer cancel() updates := make(chan Response, 1) errorsIn := make(chan error, 1) go func() { scanner := bufio.NewScanner(input) scanner.Buffer(make([]byte, 4096), 1024*1024) for scanner.Scan() { var response Response if err := json.Unmarshal(scanner.Bytes(), &response); err != nil { errorsIn <- err return } select { case updates <- response: case <-ctx.Done(): return } } errorsIn <- scanner.Err() }() var initial Response select { case initial = <-updates: case err := <-errorsIn: if errors.Is(err, io.EOF) { return nil } return err case <-ctx.Done(): return ctx.Err() } title := C.CString(initial.Title) defer C.free(unsafe.Pointer(title)) messageHeader := C.CString("Notifications") defer C.free(unsafe.Pointer(messageHeader)) receivedHeader := C.CString("Received") defer C.free(unsafe.Pointer(receivedHeader)) if C.notification_list_init(title, messageHeader, receivedHeader) == 0 { return errors.New("cannot open the notification list on this display") } defer C.notification_list_close() apply(initial) C.notification_list_show() encoder := json.NewEncoder(output) ticker := time.NewTicker(10 * time.Millisecond) defer ticker.Stop() for { select { case <-ctx.Done(): return ctx.Err() case err := <-errorsIn: if errors.Is(err, io.EOF) { return nil } return err case response := <-updates: if response.Close { return nil } if response.Present { C.notification_list_present() continue } apply(response) case <-ticker.C: C.notification_list_iterate() switch C.notification_list_action() { case 1: if C.notification_list_copy() != 0 { if err := encoder.Encode(Request{Copy: true}); err != nil { return err } } case 2: selection := C.GoString(C.notification_list_selected_id()) row := int(C.notification_list_selected_row()) if err := encoder.Encode(Request{Dismiss: true, Selection: selection, Row: &row}); err != nil { return err } case 3: if err := encoder.Encode(Request{Clear: true}); err != nil { return err } case 4: return nil case 5: selection := C.GoString(C.notification_list_selected_id()) row := int(C.notification_list_selected_row()) if err := encoder.Encode(Request{Selection: selection, Row: &row}); err != nil { return err } case 6: action := C.GoString(C.notification_list_selected_action()) selection := C.GoString(C.notification_list_selected_id()) row := int(C.notification_list_selected_row()) if err := encoder.Encode(Request{Action: action, Selection: selection, Row: &row}); err != nil { return err } } } } } func apply(response Response) { messages := make([]*C.char, len(response.Entries)) received := make([]*C.char, len(response.Entries)) identifiers := make([]*C.char, len(response.Entries)) for index, entry := range response.Entries { identifiers[index] = C.CString(entry.ID) messages[index] = C.CString(entry.Message) received[index] = C.CString(entry.Received) } defer func() { for _, value := range messages { C.free(unsafe.Pointer(value)) } for _, value := range received { C.free(unsafe.Pointer(value)) } for _, value := range identifiers { C.free(unsafe.Pointer(value)) } }() selectedID := C.CString(response.Selected) defer C.free(unsafe.Pointer(selectedID)) var selectedEntry Entry for _, entry := range response.Entries { if entry.ID == response.Selected { selectedEntry = entry break } } actionKeys := make([]*C.char, len(selectedEntry.Actions)) actionLabels := make([]*C.char, len(selectedEntry.Actions)) for index, action := range selectedEntry.Actions { actionKeys[index] = C.CString(action.Key) actionLabels[index] = C.CString(action.Label) } defer func() { for _, value := range actionKeys { C.free(unsafe.Pointer(value)) } for _, value := range actionLabels { C.free(unsafe.Pointer(value)) } }() C.notification_list_set_entries( cStringArray(messages), cStringArray(received), C.int(len(response.Entries)), cStringArray(identifiers), selectedID, cBoolean(selectedEntry.CanControl), cStringArray(actionKeys), cStringArray(actionLabels), C.int(len(selectedEntry.Actions)), ) } func cStringArray(values []*C.char) **C.char { if len(values) == 0 { return nil } return (**C.char)(unsafe.Pointer(&values[0])) } func cBoolean(value bool) C.int { if value { return 1 } return 0 }