152 lines
3.5 KiB
Go
152 lines
3.5 KiB
Go
// 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))
|
|
}
|