Files

201 lines
5.0 KiB
Go

// 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 <stdlib.h>
#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
}