Files

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))
}