Bug fixes and package updates.

This commit is contained in:
ak47 committed 2026-11-07 13:14:22 -05:00
1 parent 19b789ef5c
commit d0c977281d
11 files changed
+176 -27

No files matched your search

+1
View File
@@ -4,6 +4,7 @@ dist/
coverage.out
/cthulhu
/cthulhu-braille
/cthulhu-input-helper
/parityledger
__pycache__/
*.py[cod]
+1
View File
@@ -22,6 +22,7 @@ Use Go 1.24+ with CGO and development libraries listed by the Arch `PKGBUILD`.
- `go build -o build/cthulhu ./cmd/cthulhu`: build the reader.
- `go build -tags brlapi -o build/cthulhu-braille ./cmd/cthulhu-braille`: build the Braille helper.
- `go build -o build/cthulhu-input-helper ./cmd/cthulhu-input-helper`: build the remote input helper; install it beside the reader for incoming keyboard control.
- `go test ./... -count=1`: run default tests; native cases may skip.
- `go test -tags brlapi ./... -count=1`: run packaging checks.
- `go vet ./...`: check common Go errors.
+3 -2
View File
@@ -105,12 +105,13 @@ Tentacles already use an external process protocol: preserve existing host safeg
### Local installation and reload
After building, install both Go binaries beside one another so the reader can
find its Braille helper:
After building, install all three Go binaries beside one another so the reader
can find its Braille and remote input helpers:
```sh
install -Dm755 build/cthulhu "$HOME/.local/bin/cthulhu"
install -Dm755 build/cthulhu-braille "$HOME/.local/bin/cthulhu-braille"
install -Dm755 build/cthulhu-input-helper "$HOME/.local/bin/cthulhu-input-helper"
"$HOME/.local/bin/cthulhu" --check-config
```
+12 -4
View File
@@ -23,9 +23,10 @@ AI-assisted contributions are welcome. Contributors remain responsible for human
## Building
Run `./build-cthulhu.sh` to build `cthulhu` and `cthulhu-braille` in the
checkout root. The script works from any directory. It uses Go's automatic
toolchain selection with Go 1.21 or newer. If Go is missing or older, it
Run `./build-cthulhu.sh` to build `cthulhu`, `cthulhu-braille`, and
`cthulhu-input-helper` in the checkout root. The script works from any
directory. It uses Go's automatic toolchain selection with Go 1.21 or newer.
If Go is missing or older, it
downloads the version required by `go.mod`, verifies the archive's SHA-256,
and caches it under `${XDG_CACHE_HOME:-$HOME/.cache}/cthulhu/toolchains`.
It does not replace system Go or require root.
@@ -38,7 +39,7 @@ Bootstrapping Go additionally needs `curl`, `tar`, and `sha256sum`.
See [Contributing](CONTRIBUTING.md#development-and-architecture) and the
Arch package recipes under `distro-packages/Arch-Linux/` for dependencies.
For packaging, put both binaries in a separate directory and pass build flags
For packaging, put all three binaries in a separate directory and pass build flags
after `--`:
```sh
@@ -58,6 +59,7 @@ that must use an installed toolchain:
GOTOOLCHAIN=local GOPROXY=off ./build-cthulhu.sh --output-dir build -- -mod=readonly
install -Dm755 build/cthulhu "$pkgdir/usr/bin/cthulhu"
install -Dm755 build/cthulhu-braille "$pkgdir/usr/bin/cthulhu-braille"
install -Dm755 build/cthulhu-input-helper "$pkgdir/usr/bin/cthulhu-input-helper"
```
For offline builds, provision a suitable Go version and module cache in
@@ -65,6 +67,12 @@ advance. Build inside the target distribution to link against its native
libraries; downloading Go does not supply those libraries. `--help` lists
the script's options without checking dependencies or accessing the network.
Keep all three executables together when installing or using a custom launch
script. `cthulhu-input-helper` receives remote keyboard input on Xorg. Without
it, Cthulhu can control another computer, but cannot accept its keyboard input.
The October 2026 release predates this helper and incoming Remote keyboard
support; packaging that release cannot enable the newer feature.
## Start with the laptop keyboard layout
Run `cthulhu -l` to use the laptop layout with Caps Lock as the Cthulhu
+3 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Build both Cthulhu binaries without replacing the system Go installation.
# Build Cthulhu and its two helpers without replacing the system Go installation.
set -euo pipefail
projectDir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
@@ -15,11 +15,11 @@ Usage: build-cthulhu.sh [-o DIR | --output-dir DIR] [-- GO_BUILD_FLAGS...]
Build cthulhu, cthulhu-braille and cthulhu-input-helper, obtaining a suitable Go toolchain if needed.
-o, --output-dir DIR Write both binaries to DIR (default: checkout root).
-o, --output-dir DIR Write all three binaries to DIR (default: checkout root).
Relative paths start at the caller's directory.
Missing directories are created.
-h, --help Show help without checking build dependencies.
-- Pass remaining flags to both go build commands.
-- Pass remaining flags to all three go build commands.
GOTOOLCHAIN defaults to auto. Set GOTOOLCHAIN=local to require installed Go
and prevent toolchain downloads. Other Go settings, compiler/linker flags,
@@ -5,7 +5,7 @@ pkgbase=cthulhu-git
pkgname=cthulhu-git
_pkgname=cthulhu
pkgver=2026.10.31.r588.g9680b58
pkgrel=1
pkgrel=2
pkgdesc="Desktop-agnostic screen reader with speech, Braille, and Tentacle support"
url="https://git.stormux.org/storm/cthulhu"
arch=(x86_64 aarch64 armv7h)
@@ -84,7 +84,10 @@ build() {
go build -o build/cthulhu ./cmd/cthulhu
go build -tags brlapi -o build/cthulhu-braille ./cmd/cthulhu-braille
go build -o build/cthulhu-input-helper ./cmd/cthulhu-input-helper
# Include the helper once incoming Remote keyboard support reaches master.
if [[ -d cmd/cthulhu-input-helper ]]; then
go build -o build/cthulhu-input-helper ./cmd/cthulhu-input-helper
fi
}
check() {
@@ -99,7 +102,9 @@ package() {
cd "${_pkgname}"
install -Dm755 build/cthulhu "${pkgdir}/usr/bin/cthulhu"
install -Dm755 build/cthulhu-braille "${pkgdir}/usr/bin/cthulhu-braille"
install -Dm755 build/cthulhu-input-helper "${pkgdir}/usr/bin/cthulhu-input-helper"
if [[ -d cmd/cthulhu-input-helper ]]; then
install -Dm755 build/cthulhu-input-helper "${pkgdir}/usr/bin/cthulhu-input-helper"
fi
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
install -Dm644 AUTHORS "${pkgdir}/usr/share/doc/${pkgname}/AUTHORS"
}
+1 -1
View File
@@ -1,7 +1,7 @@
pkgbase = cthulhu
pkgdesc = Desktop-agnostic screen reader with speech, Braille, and Tentacle support
pkgver = 2026.10.31
pkgrel = 3
pkgrel = 4
url = https://git.stormux.org/storm/cthulhu
arch = x86_64
arch = aarch64
+8 -3
View File
@@ -3,7 +3,7 @@
pkgname=cthulhu
pkgver=2026.10.31
pkgrel=3
pkgrel=4
pkgdesc="Desktop-agnostic screen reader with speech, Braille, and Tentacle support"
url="https://git.stormux.org/storm/cthulhu"
arch=(x86_64 aarch64 armv7h)
@@ -80,7 +80,10 @@ build() {
go build -o build/cthulhu ./cmd/cthulhu
go build -tags brlapi -o build/cthulhu-braille ./cmd/cthulhu-braille
go build -o build/cthulhu-input-helper ./cmd/cthulhu-input-helper
# Older release tags predate incoming Remote keyboard support.
if [[ -d cmd/cthulhu-input-helper ]]; then
go build -o build/cthulhu-input-helper ./cmd/cthulhu-input-helper
fi
}
check() {
@@ -95,7 +98,9 @@ package() {
cd "${pkgname}"
install -Dm755 build/cthulhu "${pkgdir}/usr/bin/cthulhu"
install -Dm755 build/cthulhu-braille "${pkgdir}/usr/bin/cthulhu-braille"
install -Dm755 build/cthulhu-input-helper "${pkgdir}/usr/bin/cthulhu-input-helper"
if [[ -d cmd/cthulhu-input-helper ]]; then
install -Dm755 build/cthulhu-input-helper "${pkgdir}/usr/bin/cthulhu-input-helper"
fi
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
install -Dm644 AUTHORS "${pkgdir}/usr/share/doc/${pkgname}/AUTHORS"
}
@@ -34,13 +34,18 @@ import (
// This uses the source-built external executable and real host/SDK IPC. Only
// OpenLocal submissions are synthetic; it cannot prove physical-source input.
func remote_command_fixture(t *testing.T, setup ...func(*tentacleRuntime)) (*tentacleRuntime, *consentTestForms, input.Command) {
t.Helper()
return remote_command_fixture_grants(t, []string{"output-observe", "remote-present"}, setup...)
}
func remote_command_fixture_grants(t *testing.T, grants []string, setup ...func(*tentacleRuntime)) (*tentacleRuntime, *consentTestForms, input.Command) {
t.Helper()
program := os.Getenv("CTHULHU_REMOTE_BINARY")
if program == "" {
t.Skip("requires the separately built Remote executable")
}
directory := t.TempDir()
spec := tentacle.Spec{ID: "remote", Program: []string{program, "--data-dir", filepath.Join(directory, "state")}, Presentation: true, Interaction: true, SessionEvents: true, RemoteGrants: []string{"output-observe", "remote-present"}}
spec := tentacle.Spec{ID: "remote", Program: []string{program, "--data-dir", filepath.Join(directory, "state")}, Presentation: true, Interaction: true, SessionEvents: true, RemoteGrants: grants}
data, _ := json.Marshal([]tentacle.Spec{spec})
launchPath := filepath.Join(directory, "launch.json")
if err := os.WriteFile(launchPath, data, 0600); err != nil {
@@ -130,7 +135,7 @@ func select_remote_relay(t *testing.T, forms *consentTestForms, address, role st
return options
}
func approve_remote_form(t *testing.T, forms *consentTestForms, consent consentFormRequest, role string) consentFormRequest {
func approve_remote_form(t *testing.T, forms *consentTestForms, consent consentFormRequest, role string, extra ...string) consentFormRequest {
t.Helper()
if !strings.Contains(consent.form.Title, "Remote access for remote") {
t.Fatal("missing reader approval")
@@ -144,7 +149,11 @@ func approve_remote_form(t *testing.T, forms *consentTestForms, consent consentF
if role == "controlled" {
grant = "output-observe"
}
consent.reply <- internalUIResult{Activated: "approve", Values: map[string]interaction.Value{grant: {Checked: true}}}
values := map[string]interaction.Value{grant: {Checked: true}}
for _, name := range extra {
values[name] = interaction.Value{Checked: true}
}
consent.reply <- internalUIResult{Activated: "approve", Values: values}
progress := next_remote_form(t, forms)
if progress.form.Title != "Connecting Remote" {
t.Fatal("approval did not start connection", progress.form.Title)
@@ -289,8 +298,10 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
{"controller", "disconnect", true}, {"controller", "lock", true},
{"controller", "handoff", true}, {"controller", "disable", true},
{"controller", "backend-failure", true},
{"controlled", "missing-helper", false},
} {
role, ending := test.role, test.ending
keyboard := role == "controlled" && (ending == "disconnect" || ending == "missing-helper")
name := role + "/" + ending
if test.playback {
name += "/production-speech"
@@ -366,6 +377,18 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
}
io.WriteString(connection, `{"type":"channel_joined","channel":"synthetic-fixture","clients":[{"id":7,"connection_type":"`+peerRole+`"}]}`+"\n")
close(joined)
if keyboard {
select {
case <-speechReady:
case <-t.Context().Done():
return
}
// NVDA and Cthulhu controllers use these same key frames.
for _, pressed := range []bool{true, false} {
json.NewEncoder(connection).Encode(map[string]any{"type": "key", "origin": 7,
"vk_code": 65, "extended": false, "pressed": pressed})
}
}
if role == "controller" {
select {
case <-speechReady:
@@ -409,7 +432,18 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
})
})
}
runtime, forms, shortcut := remote_command_fixture(t, configure...)
grants := []string{"output-observe", "remote-present"}
var keyLog string
if keyboard {
grants = append(grants, "keyboard-inject")
helper, log := keyboard_owner_helper(t)
keyLog = log
if ending == "missing-helper" {
helper = filepath.Join(t.TempDir(), "absent")
}
configure = append(configure, func(runtime *tentacleRuntime) { runtime.remoteInputHelper = helper })
}
runtime, forms, shortcut := remote_command_fixture_grants(t, grants, configure...)
if role == "controlled" {
worker := runtime.output.speech
worker.speaker = &observationSpeaker{fakeSpeaker{spoken: make(chan string, 64)}}
@@ -459,7 +493,11 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
credentials.reply <- internalUIResult{Activated: "connect", Values: map[string]interaction.Value{"channel-key": {Text: "synthetic-fixture"}}}
next_remote_form(t, forms) // Verified certificate progress.
consent := next_remote_form(t, forms)
status := approve_remote_form(t, forms, consent, role)
var extra []string
if keyboard {
extra = append(extra, "keyboard-inject")
}
status := approve_remote_form(t, forms, consent, role, extra...)
select {
case <-joined:
case <-time.After(5 * time.Second):
@@ -507,6 +545,20 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
}
}
close(speechReady)
if ending == "missing-helper" {
remote_stream_wait(t, closed)
if _, err := os.Stat(keyLog); !os.IsNotExist(err) {
t.Fatal("missing helper unexpectedly delivered input", err)
}
return
}
if keyboard {
fixture := &keyboardOwnerFixture{log: keyLog}
fixture.wait(t, func() bool {
effects := fixture.effects()
return strings.Contains(effects, " a true\n") && strings.Contains(effects, " a false\n")
})
}
if ending == "backend-failure" {
remote_stream_wait(t, closed)
// Backend failure retires the lease, while the process and
+8 -6
View File
@@ -42,7 +42,7 @@ elif tool in ("go", "bootstrap-go"):
version = "1.24.0" if tool == "bootstrap-go" else os.environ["INSTALLED_GO"]
print("go version go" + version + " linux/amd64")
elif args[0] == "build":
if args[-1].endswith("cthulhu-braille") and os.environ.get("FAIL_BRAILLE"):
if args[-1] == os.environ.get("FAIL_BUILD_TARGET"):
sys.exit(2)
Path(args[args.index("-o") + 1]).touch()
'''
@@ -162,11 +162,13 @@ class BuildScriptTests(unittest.TestCase):
self.assertNotEqual(result.returncode, 0)
self.assertFalse(self.calls("curl"))
def test_braille_failure_is_not_reported_as_success(self):
self.env["FAIL_BRAILLE"] = "1"
result = self.run_build()
self.assertNotEqual(result.returncode, 0)
self.assertNotIn("Cthulhu has been built", result.stdout)
def test_helper_failure_is_not_reported_as_success(self):
for helper in ("cthulhu-braille", "cthulhu-input-helper"):
with self.subTest(helper=helper):
self.env["FAIL_BUILD_TARGET"] = f"./cmd/{helper}"
result = self.run_build()
self.assertNotEqual(result.returncode, 0)
self.assertNotIn("Cthulhu has been built", result.stdout)
def test_disabled_cgo_and_output_conflicts_fail_before_building(self):
self.env["CGO_ENABLED"] = "0"
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Check package contents for sources before and after Remote input support."""
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
MOCK_GO = '''#!/usr/bin/env python3
import os
from pathlib import Path
import sys
args = sys.argv[1:]
assert args[0] == "build"
assert Path(args[-1]).is_dir(), "build target is absent from this source version"
if args[-1] == os.environ.get("FAIL_BUILD_TARGET"):
sys.exit(2)
target = Path(args[args.index("-o") + 1])
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text("#!/bin/sh\\nexit 0\\n")
target.chmod(0o755)
'''
class PackageBinaryTests(unittest.TestCase):
def test_build_and_install_source_supported_helpers(self):
root = Path(__file__).resolve().parents[1]
recipes = sorted((root / "distro-packages").rglob("PKGBUILD"))
self.assertTrue(recipes)
for recipe in recipes:
for has_input, fail_input in ((False, False), (True, False), (True, True)):
with self.subTest(recipe=recipe.parent.name, has_input=has_input,
fail_input=fail_input), tempfile.TemporaryDirectory() as tmp:
work = Path(tmp)
source = work / "cthulhu"
binaries = ["cthulhu", "cthulhu-braille"]
if has_input:
binaries.append("cthulhu-input-helper")
for binary in binaries:
(source / "cmd" / binary).mkdir(parents=True)
for name in ("LICENSE", "AUTHORS"):
(source / name).write_text("fixture\n")
# A stale artifact must not enter an older source's package.
(source / "build").mkdir()
(source / "build" / "cthulhu-input-helper").write_text("stale")
tools = work / "tools"
tools.mkdir()
go = tools / "go"
go.write_text(MOCK_GO)
go.chmod(0o755)
stage = work / "package"
environment = dict(os.environ, PATH=f"{tools}:{os.environ['PATH']}",
srcdir=str(work), pkgdir=str(stage))
if fail_input:
environment["FAIL_BUILD_TARGET"] = "./cmd/cthulhu-input-helper"
result = subprocess.run(
["bash", "-ec", 'source "$1"\nbuild\ncd "$srcdir"\npackage',
"package-test", str(recipe)], cwd=work, env=environment,
capture_output=True, text=True, check=False)
if fail_input:
self.assertNotEqual(result.returncode, 0)
self.assertFalse(stage.exists())
continue
self.assertEqual(result.returncode, 0, result.stderr)
installed = stage / "usr/bin"
self.assertEqual(sorted(p.name for p in installed.iterdir()), sorted(binaries))
for binary in binaries:
self.assertTrue(os.access(installed / binary, os.X_OK))
if __name__ == "__main__":
unittest.main()