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,151 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package speechhistoryui
|
||||
|
||||
/*
|
||||
#cgo pkg-config: gtk+-3.0
|
||||
#include <stdlib.h>
|
||||
#include "native.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"runtime"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Items []string `json:"items"`
|
||||
Selected int `json:"selected,omitempty"`
|
||||
Append string `json:"append,omitempty"`
|
||||
Present bool `json:"present,omitempty"`
|
||||
Close bool `json:"close,omitempty"`
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Action string `json:"action"`
|
||||
Selection string `json:"selection,omitempty"`
|
||||
Row int `json:"row,omitempty"`
|
||||
}
|
||||
|
||||
// Run keeps GTK on one OS thread and exchanges bounded messages with Cthulhu.
|
||||
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 Message, 16)
|
||||
readErrors := make(chan error, 1)
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(input)
|
||||
scanner.Buffer(make([]byte, 4096), 1024*1024)
|
||||
for scanner.Scan() {
|
||||
var update Message
|
||||
if err := json.Unmarshal(scanner.Bytes(), &update); err != nil {
|
||||
readErrors <- err
|
||||
return
|
||||
}
|
||||
select {
|
||||
case updates <- update:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
readErrors <- scanner.Err()
|
||||
}()
|
||||
var initial Message
|
||||
select {
|
||||
case initial = <-updates:
|
||||
case err := <-readErrors:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
monitor := initial.Mode == "monitor"
|
||||
mode := C.int(0)
|
||||
if monitor {
|
||||
mode = 1
|
||||
}
|
||||
if C.history_init(mode) == 0 {
|
||||
return errors.New("cannot open speech history on this display")
|
||||
}
|
||||
defer C.history_close()
|
||||
if !monitor {
|
||||
setItems(initial.Items, initial.Selected)
|
||||
}
|
||||
encoder := json.NewEncoder(output)
|
||||
// AT-SPI requests are served by GTK's main context. A longer polling
|
||||
// interval is paid once for every synchronous accessibility query, so a
|
||||
// single arrow key can accumulate hundreds of milliseconds of delay.
|
||||
ticker := time.NewTicker(time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case err := <-readErrors:
|
||||
return err
|
||||
case update := <-updates:
|
||||
if update.Close {
|
||||
return nil
|
||||
}
|
||||
if update.Present {
|
||||
C.history_present()
|
||||
}
|
||||
if update.Items != nil {
|
||||
setItems(update.Items, update.Selected)
|
||||
}
|
||||
if update.Append != "" {
|
||||
value := C.CString(update.Append)
|
||||
C.history_append(value)
|
||||
C.free(unsafe.Pointer(value))
|
||||
}
|
||||
case <-ticker.C:
|
||||
C.history_iterate()
|
||||
switch C.history_action() {
|
||||
case 1:
|
||||
if C.history_copy() != 0 {
|
||||
if err := encoder.Encode(Request{Action: "copy"}); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := encoder.Encode(Request{Action: "empty"}); err != nil {
|
||||
return err
|
||||
}
|
||||
case 2:
|
||||
if err := encoder.Encode(Request{Action: "remove", Selection: C.GoString(C.history_selected()), Row: int(C.history_selected_row())}); err != nil {
|
||||
return err
|
||||
}
|
||||
case 3:
|
||||
return nil
|
||||
case 4:
|
||||
C.history_clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setItems(items []string, selected int) {
|
||||
values := make([]*C.char, len(items))
|
||||
for i, item := range items {
|
||||
values[i] = C.CString(item)
|
||||
}
|
||||
defer func() {
|
||||
for _, value := range values {
|
||||
C.free(unsafe.Pointer(value))
|
||||
}
|
||||
}()
|
||||
if len(values) == 0 {
|
||||
C.history_set_items(nil, 0, 0)
|
||||
return
|
||||
}
|
||||
C.history_set_items((**C.char)(unsafe.Pointer(&values[0])), C.int(len(values)), C.int(selected))
|
||||
}
|
||||
Reference in new issue
Block a user