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,282 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package preferences
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
#include "native.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/tentacle"
|
||||
)
|
||||
|
||||
type tentacleRow struct {
|
||||
key string
|
||||
status tentacle.EntryStatus
|
||||
installed bool
|
||||
}
|
||||
|
||||
type tentaclePage struct {
|
||||
snapshot *tentacle.Snapshot
|
||||
rows []tentacleRow
|
||||
origins []string
|
||||
selectedKey string
|
||||
}
|
||||
|
||||
func tentacle_rows(snapshot *tentacle.Snapshot, origin string) []tentacleRow {
|
||||
if snapshot == nil {
|
||||
return nil
|
||||
}
|
||||
var rows []tentacleRow
|
||||
seen := make(map[string]bool)
|
||||
repositoryIDs := make(map[string]string)
|
||||
for _, repository := range snapshot.Repositories {
|
||||
repositoryIDs[repository.URL] = repository.ID
|
||||
}
|
||||
for _, status := range snapshot.Entries {
|
||||
entry := status.Entry
|
||||
localID := entry.LocalID
|
||||
if localID == "" {
|
||||
localID = entry.ID
|
||||
}
|
||||
key := entry.Repository + "\x00" + entry.RepositoryID + "\x00" + localID
|
||||
seen[key] = true
|
||||
if origin == "" || entry.Repository == origin && entry.RepositoryID == repositoryIDs[origin] {
|
||||
rows = append(rows, tentacleRow{key: key, status: status, installed: true})
|
||||
}
|
||||
}
|
||||
for _, repository := range snapshot.Repositories {
|
||||
if origin != "" && repository.URL != origin {
|
||||
continue
|
||||
}
|
||||
for _, manifest := range repository.Tentacles {
|
||||
key := repository.URL + "\x00" + repository.ID + "\x00" + manifest.ID
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
status := tentacle.EntryStatus{
|
||||
Entry: tentacle.Entry{ID: manifest.ID, LocalID: manifest.ID, Name: manifest.Name, Version: manifest.Version, Repository: repository.URL, RepositoryID: repository.ID},
|
||||
State: "Available",
|
||||
Message: manifest.Description,
|
||||
}
|
||||
rows = append(rows, tentacleRow{key: key, status: status})
|
||||
}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func (page *tentaclePage) selected() *tentacleRow {
|
||||
row := int(C.prefs_tentacle_selected())
|
||||
if row < 0 || row >= len(page.rows) {
|
||||
return nil
|
||||
}
|
||||
return &page.rows[row]
|
||||
}
|
||||
|
||||
func (page *tentaclePage) origin() string {
|
||||
index := int(C.prefs_tentacle_repository_selected()) - 1
|
||||
if index < 0 || index >= len(page.origins) {
|
||||
return ""
|
||||
}
|
||||
return page.origins[index]
|
||||
}
|
||||
|
||||
func (page *tentaclePage) controls() {
|
||||
selected := page.selected()
|
||||
installed, enabled := false, false
|
||||
if selected != nil {
|
||||
installed, enabled = selected.installed, selected.status.Entry.Enabled
|
||||
}
|
||||
available := page.snapshot != nil && !page.snapshot.Suppressed && page.snapshot.Message == ""
|
||||
C.prefs_tentacle_controls(boolean(available), boolean(installed), boolean(enabled), boolean(page.origin() != ""))
|
||||
}
|
||||
|
||||
func (page *tentaclePage) render() {
|
||||
page.rows = tentacle_rows(page.snapshot, page.origin())
|
||||
C.prefs_tentacles_clear()
|
||||
selectedRow := -1
|
||||
for index, row := range page.rows {
|
||||
entry := row.status.Entry
|
||||
name := entry.Name
|
||||
if name == "" {
|
||||
name = entry.ID
|
||||
}
|
||||
repository := tentacle.RepositoryLabel(entry.Repository)
|
||||
if entry.Repository == "" {
|
||||
repository = "Local development"
|
||||
}
|
||||
texts := []*C.char{C.CString(name), C.CString(row.status.State), C.CString(entry.Version), C.CString(repository), C.CString(row.status.Message), C.CString(strings.Join(row.status.Shortcuts, "; "))}
|
||||
C.prefs_tentacle_append(boolean(entry.Enabled), texts[0], texts[1], texts[2], texts[3], texts[4], texts[5])
|
||||
for _, text := range texts {
|
||||
C.free(unsafe.Pointer(text))
|
||||
}
|
||||
if row.key == page.selectedKey {
|
||||
selectedRow = index
|
||||
}
|
||||
}
|
||||
C.prefs_tentacle_select(C.int(selectedRow))
|
||||
page.controls()
|
||||
}
|
||||
|
||||
func (page *tentaclePage) populate(snapshot *tentacle.Snapshot) {
|
||||
if selected := page.selected(); selected != nil {
|
||||
page.selectedKey = selected.key
|
||||
}
|
||||
origin := page.origin()
|
||||
var origins []string
|
||||
if snapshot != nil {
|
||||
for _, repository := range snapshot.Repositories {
|
||||
origins = append(origins, repository.URL)
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(page.origins, origins) {
|
||||
C.prefs_tentacle_repositories_clear()
|
||||
selectedPage := 0
|
||||
labels := make(map[string]int)
|
||||
for _, value := range origins {
|
||||
labels[tentacle.RepositoryLabel(value)]++
|
||||
}
|
||||
for index, value := range origins {
|
||||
label := tentacle.RepositoryLabel(value)
|
||||
if labels[label] > 1 {
|
||||
label = value
|
||||
}
|
||||
text := C.CString(label)
|
||||
C.prefs_tentacle_repository_append(text)
|
||||
C.free(unsafe.Pointer(text))
|
||||
if value == origin {
|
||||
selectedPage = index + 1
|
||||
}
|
||||
}
|
||||
page.origins = origins
|
||||
C.prefs_tentacle_repository_select(C.int(selectedPage))
|
||||
}
|
||||
page.snapshot = snapshot
|
||||
page.render()
|
||||
}
|
||||
|
||||
func (page *tentaclePage) action(action int) (*tentacle.ManagementRequest, error) {
|
||||
if action == 16 {
|
||||
if selected := page.selected(); selected != nil {
|
||||
page.selectedKey = selected.key
|
||||
}
|
||||
page.controls()
|
||||
return nil, nil
|
||||
}
|
||||
if action == 17 {
|
||||
page.render()
|
||||
return nil, nil
|
||||
}
|
||||
if action == 15 {
|
||||
return &tentacle.ManagementRequest{Action: "list"}, nil
|
||||
}
|
||||
if page.snapshot == nil || page.snapshot.Suppressed || page.snapshot.Message != "" {
|
||||
message := "Tentacle management is unavailable for this run."
|
||||
if page.snapshot != nil && page.snapshot.Message != "" {
|
||||
message = page.snapshot.Message
|
||||
}
|
||||
return nil, errors.New(message)
|
||||
}
|
||||
if action == 9 {
|
||||
id := C.CString("tentacleRepositoryURL")
|
||||
origin, err := tentacle.RepositoryURL(C.GoString(C.prefs_entry_get(id)))
|
||||
C.free(unsafe.Pointer(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tentacle.ManagementRequest{Action: "repository-add", URL: origin}, nil
|
||||
}
|
||||
if action == 12 || action == 18 {
|
||||
origin := page.origin()
|
||||
if origin == "" {
|
||||
return nil, errors.New("Select a repository tab first.")
|
||||
}
|
||||
operation := "repository-remove"
|
||||
if action == 18 {
|
||||
operation = "repository-refresh"
|
||||
}
|
||||
return &tentacle.ManagementRequest{Action: operation, URL: origin}, nil
|
||||
}
|
||||
selected := page.selected()
|
||||
if selected == nil {
|
||||
return nil, errors.New("Select a tentacle first.")
|
||||
}
|
||||
page.selectedKey = selected.key
|
||||
if !selected.installed {
|
||||
if action != 11 && action != 19 {
|
||||
return nil, errors.New("Install this tentacle first.")
|
||||
}
|
||||
entry := selected.status.Entry
|
||||
var manifest *tentacle.Manifest
|
||||
for _, repository := range page.snapshot.Repositories {
|
||||
if repository.URL == entry.Repository && repository.ID == entry.RepositoryID {
|
||||
for index := range repository.Tentacles {
|
||||
if repository.Tentacles[index].ID == entry.LocalID {
|
||||
manifest = &repository.Tentacles[index]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if manifest == nil || len(manifest.Releases) == 0 {
|
||||
return nil, errors.New("No package has been published for this tentacle. Refresh the repository after a release is published.")
|
||||
}
|
||||
permissions := "This tentacle can register reader commands."
|
||||
if slices.Contains(manifest.Capabilities, "presentation") {
|
||||
permissions = "This tentacle can register reader commands and send speech and braille messages."
|
||||
}
|
||||
if slices.Contains(manifest.Capabilities, "interaction") {
|
||||
permissions = "This tentacle can register reader commands, send speech and braille messages, open internal forms, read the focused item and clipboard when invoked, and write clipboard text."
|
||||
}
|
||||
operation, label := "install", "Install"
|
||||
if action == 11 {
|
||||
operation, label = "install-enable", "Install and enable"
|
||||
}
|
||||
message := fmt.Sprintf("%s %s version %s from %s?\n\n%s\n\nOnly install packages from publishers you trust. Tentacles run with your account's access; resource limits do not protect your files or network.\n\nTentacle management changes take effect immediately.", label, entry.Name, entry.Version, entry.Repository, permissions)
|
||||
text, button := C.CString(message), C.CString(label)
|
||||
accepted := C.prefs_tentacle_confirm(text, button)
|
||||
C.free(unsafe.Pointer(text))
|
||||
C.free(unsafe.Pointer(button))
|
||||
if accepted == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return &tentacle.ManagementRequest{Action: operation, ID: entry.LocalID, URL: entry.Repository}, nil
|
||||
}
|
||||
operation := ""
|
||||
switch action {
|
||||
case 11:
|
||||
operation = "enable"
|
||||
if selected.status.Entry.Enabled {
|
||||
operation = "disable"
|
||||
}
|
||||
case 13:
|
||||
operation = "retry"
|
||||
case 14:
|
||||
operation = "remove"
|
||||
case 20:
|
||||
operation = "settings"
|
||||
default:
|
||||
return nil, errors.New("Unknown tentacle action.")
|
||||
}
|
||||
return &tentacle.ManagementRequest{Action: operation, ID: selected.status.Entry.ID}, nil
|
||||
}
|
||||
|
||||
func (page *tentaclePage) accepted(request *tentacle.ManagementRequest) {
|
||||
if request.Action == "repository-add" {
|
||||
id, empty := C.CString("tentacleRepositoryURL"), C.CString("")
|
||||
C.prefs_entry_set(id, empty)
|
||||
C.free(unsafe.Pointer(id))
|
||||
C.free(unsafe.Pointer(empty))
|
||||
}
|
||||
page.controls()
|
||||
}
|
||||
Reference in new issue
Block a user