201 lines
5.0 KiB
Go
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
|
|
}
|