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,154 @@
|
||||
// Package nvda2cthulhu accepts the local WebSocket protocol used by NVDA
|
||||
// compatibility bridges and routes it to Cthulhu's speech and braille queues.
|
||||
package nvda2cthulhu
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func (server *Server) deliver(request bridgeRequest) {
|
||||
switch request.kind {
|
||||
case "SpeakText":
|
||||
if server.handler.SpeakText(request.text, true) {
|
||||
server.logger.Debug("NVDA to Cthulhu speech accepted", "bytes", len(request.text))
|
||||
} else {
|
||||
server.logger.Warn("NVDA to Cthulhu speech rejected", "bytes", len(request.text))
|
||||
}
|
||||
case "BrailleText":
|
||||
if server.handler.BrailleMessage(request.text) {
|
||||
server.logger.Debug("NVDA to Cthulhu braille accepted", "bytes", len(request.text))
|
||||
} else {
|
||||
server.logger.Debug("NVDA to Cthulhu braille unavailable")
|
||||
}
|
||||
case "CancelSpeech":
|
||||
if server.handler.CancelSpeech() {
|
||||
server.logger.Debug("NVDA to Cthulhu cancellation accepted")
|
||||
} else {
|
||||
server.logger.Warn("NVDA to Cthulhu cancellation rejected")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type bridgeRequest struct {
|
||||
kind, text string
|
||||
}
|
||||
|
||||
func decodeRequest(messageType int, payload []byte) (bridgeRequest, error) {
|
||||
if len(payload) > maxPayload {
|
||||
return bridgeRequest{}, errors.New("message exceeds 64 KiB")
|
||||
}
|
||||
switch messageType {
|
||||
case websocket.TextMessage:
|
||||
return parseRequest(string(payload))
|
||||
case websocket.BinaryMessage:
|
||||
reader := bytes.NewReader(payload)
|
||||
value, err := decodeMessagePack(reader)
|
||||
if err != nil {
|
||||
return bridgeRequest{}, err
|
||||
}
|
||||
if reader.Len() != 0 {
|
||||
return bridgeRequest{}, errors.New("trailing MessagePack data")
|
||||
}
|
||||
return parseRequest(value)
|
||||
default:
|
||||
return bridgeRequest{}, errors.New("unsupported WebSocket message type")
|
||||
}
|
||||
}
|
||||
|
||||
func parseRequest(value any) (bridgeRequest, error) {
|
||||
switch payload := value.(type) {
|
||||
case string:
|
||||
if strings.TrimSpace(payload) == "" {
|
||||
return bridgeRequest{}, errors.New("blank text request")
|
||||
}
|
||||
if payload == "CancelSpeech" {
|
||||
return bridgeRequest{kind: "CancelSpeech"}, nil
|
||||
}
|
||||
return bridgeRequest{kind: "SpeakText", text: payload}, nil
|
||||
case map[any]any:
|
||||
if len(payload) != 1 {
|
||||
return bridgeRequest{}, errors.New("map request must contain exactly one entry")
|
||||
}
|
||||
for key, value := range payload {
|
||||
return keyedRequest(coerceText(key), value)
|
||||
}
|
||||
case map[string]any:
|
||||
if len(payload) != 1 {
|
||||
return bridgeRequest{}, errors.New("map request must contain exactly one entry")
|
||||
}
|
||||
for key, value := range payload {
|
||||
return keyedRequest(key, value)
|
||||
}
|
||||
case []any:
|
||||
if len(payload) == 0 {
|
||||
return bridgeRequest{}, errors.New("array request is empty")
|
||||
}
|
||||
kind := coerceText(payload[0])
|
||||
if kind == "CancelSpeech" {
|
||||
return bridgeRequest{kind: "CancelSpeech"}, nil
|
||||
}
|
||||
if len(payload) < 2 {
|
||||
return bridgeRequest{}, errors.New("array request has no value")
|
||||
}
|
||||
return keyedRequest(kind, payload[1])
|
||||
}
|
||||
return bridgeRequest{}, errors.New("unsupported MessagePack payload")
|
||||
}
|
||||
|
||||
func keyedRequest(kind string, value any) (bridgeRequest, error) {
|
||||
switch kind {
|
||||
case "CancelSpeech":
|
||||
return bridgeRequest{kind: "CancelSpeech"}, nil
|
||||
case "SpeakText", "BrailleText":
|
||||
if strings.TrimSpace(coerceText(value)) == "" {
|
||||
return bridgeRequest{}, errors.New("blank text request")
|
||||
}
|
||||
return bridgeRequest{kind: kind, text: coerceText(value)}, nil
|
||||
default:
|
||||
return bridgeRequest{}, errors.New("unsupported request kind")
|
||||
}
|
||||
}
|
||||
|
||||
func coerceText(value any) string {
|
||||
switch text := value.(type) {
|
||||
case string:
|
||||
return text
|
||||
case messagePackBinary:
|
||||
return string(text)
|
||||
case nil:
|
||||
return ""
|
||||
case bool:
|
||||
return strconv.FormatBool(text)
|
||||
case int:
|
||||
return strconv.Itoa(text)
|
||||
case int8:
|
||||
return strconv.FormatInt(int64(text), 10)
|
||||
case int16:
|
||||
return strconv.FormatInt(int64(text), 10)
|
||||
case int32:
|
||||
return strconv.FormatInt(int64(text), 10)
|
||||
case int64:
|
||||
return strconv.FormatInt(text, 10)
|
||||
case uint:
|
||||
return strconv.FormatUint(uint64(text), 10)
|
||||
case uint8:
|
||||
return strconv.FormatUint(uint64(text), 10)
|
||||
case uint16:
|
||||
return strconv.FormatUint(uint64(text), 10)
|
||||
case uint32:
|
||||
return strconv.FormatUint(uint64(text), 10)
|
||||
case uint64:
|
||||
return strconv.FormatUint(text, 10)
|
||||
case float32:
|
||||
return strconv.FormatFloat(float64(text), 'g', -1, 32)
|
||||
case float64:
|
||||
return strconv.FormatFloat(text, 'g', -1, 64)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user