Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,200 @@
|
||||
// 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
|
||||
}
|
||||
Reference in new issue
Block a user