Cthulhu is no longer a fork. Now it is a screen reader written in Golang.

This commit is contained in:
Storm Dragon committed 2026-10-14 00:16:34 -04:00
1 parent a998fe67de
commit 069fc674dc
708 files changed
+112482 -532

No files matched your search

+214
View File
@@ -0,0 +1,214 @@
# Copyright (c) 2026 Storm Dragon
# SPDX-License-Identifier: GPL-3.0-or-later
"""Experimental command/presentation SDK. stdout is reserved for the protocol."""
import json
import sys
import threading
from dataclasses import dataclass, field
MAX_FRAME = 64 * 1024
class ProtocolError(Exception):
pass
@dataclass
class Command:
id: str
title: str
description: str
handle: object
bindings: list = field(default_factory=list)
presentation: bool = False
def declaration(self):
declaration = {"id": self.id, "title": self.title, "description": self.description,
"bindings": self.bindings}
if self.presentation:
declaration["presentation"] = True
return declaration
def present(speech="", braille="", uninterruptible=False):
"""Return independent text messages from a presentation-enabled command."""
if type(uninterruptible) is not bool or (uninterruptible and not speech):
raise ValueError("Uninterruptible output requires speech and a boolean option")
result = {key: text for key, text in (("speech", speech), ("braille", braille)) if text}
if uninterruptible:
result["uninterruptible"] = True
return result
def _object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ProtocolError("Duplicate key")
result[key] = value
return result
def _depth(value, depth=0):
if depth > 32:
raise ProtocolError("JSON nesting limit")
if isinstance(value, (dict, list)):
for child in value.values() if isinstance(value, dict) else value:
_depth(child, depth + 1)
def _constant(value):
raise ProtocolError("Invalid JSON number")
def _encode(value):
_depth(value)
return json.dumps(value, ensure_ascii=False, allow_nan=False,
separators=(",", ":")).encode("utf-8")
def _fields(value, required, optional=()):
if (not isinstance(value, dict) or not set(required) <= value.keys()
or value.keys() - set(required) - set(optional)):
raise ProtocolError("Invalid fields")
def serve(commands, inputStream=None, outputStream=None):
"""Run in a dedicated child, then exit. Handlers receive (cancelled, arguments).
cancelled is a threading.Event shared by the session. Up to eight handlers
run concurrently. Shutdown/EOF sets it and joins handlers; the host terminates
children that ignore cancellation. Exceptions become generic RPC errors.
Streams are binary, owned by this call, and closed on return.
"""
inputStream = inputStream if inputStream is not None else sys.stdin.buffer
outputStream = outputStream if outputStream is not None else sys.stdout.buffer
cancelled = threading.Event()
writeLock = threading.Lock()
workers = []
slots = threading.BoundedSemaphore(8)
def write_reply(requestId, value=None, failed=False, release=None):
reply = {"jsonrpc": "2.0", "id": requestId}
if failed:
reply["error"] = {"code": -32603, "message": "Command failed"}
else:
reply["result"] = value
try:
frame = _encode(reply)
if len(frame) > MAX_FRAME:
raise ValueError()
except (ValueError, TypeError, UnicodeError, RecursionError, ProtocolError):
frame = _encode({"jsonrpc": "2.0", "id": requestId, "error": {
"code": -32603, "message": "Invalid or oversized result"}})
with writeLock:
# Bound eight handlers/waiting replies plus one serialized writer.
if release is not None:
release()
if not cancelled.is_set():
outputStream.write(frame + b"\n")
outputStream.flush()
def invoke(requestId, handler, arguments):
released = False
def release_slot():
nonlocal released
if not released:
slots.release()
released = True
try:
value = handler(cancelled, arguments)
except BaseException:
value, failed = None, True
else:
failed = False
try:
write_reply(requestId, value, failed, release_slot)
except (OSError, ValueError):
cancelled.set()
finally:
release_slot()
try:
commands = list(commands)
handlers = {command.id: command.handle for command in commands}
if (not 1 <= len(commands) <= 32 or len(handlers) != len(commands)
or any(not key or not callable(value) for key, value in handlers.items())):
raise ProtocolError("Invalid commands")
session, active, sequence = None, False, 0
while not cancelled.is_set():
frame = inputStream.readline(MAX_FRAME + 2)
if not frame:
return
if not frame.endswith(b"\n") or len(frame) > MAX_FRAME + 1:
raise ProtocolError("Invalid frame size or termination")
try:
message = json.loads(frame.decode("utf-8"), object_pairs_hook=_object,
parse_constant=_constant)
_depth(message)
except (ValueError, UnicodeError, RecursionError) as error:
raise ProtocolError("Invalid JSON") from error
_fields(message, ("jsonrpc", "method"), ("id", "params"))
if message["jsonrpc"] != "2.0":
raise ProtocolError("Invalid JSON-RPC version")
requestId = message.get("id", "")
if requestId:
if (not isinstance(requestId, str) or not requestId.isascii()
or not requestId.isdecimal() or len(requestId) > 20
or not sequence < int(requestId) < 2**64
or str(int(requestId)) != requestId):
raise ProtocolError("Invalid request sequence")
sequence = int(requestId)
elif "id" in message:
raise ProtocolError("Invalid request ID")
method, params = message["method"], message.get("params")
if method == "initialize":
_fields(params, ("protocol", "session", "apis", "capabilities"))
version = params["protocol"]
if (session is not None or not requestId or version != {"major": 1, "minor": 0}
or any(type(value) is not int for value in version.values())
or not isinstance(params["session"], str) or not params["session"]
or len(params["session"].encode("utf-8")) > 128
or params["apis"] not in (["commands"], ["commands", "presentation"])
or params["capabilities"] != params["apis"]
or (any(command.presentation for command in commands)
and params["apis"] != ["commands", "presentation"])):
raise ProtocolError("Invalid initialization")
session = params["session"]
write_reply(requestId, {"protocol": version, "session": session,
"commands": [command.declaration() for command in commands]})
elif method == "initialized":
_fields(params, ("session",))
if session is None or active or requestId or params["session"] != session:
raise ProtocolError("Invalid activation")
active = True
elif method == "command.invoke":
_fields(params, ("command", "arguments"))
if (not active or not requestId or not isinstance(params["command"], str)
or params["command"] not in handlers
or len(_encode(params["arguments"])) > 16 * 1024):
raise ProtocolError("Invalid invocation")
workers = [worker for worker in workers if worker.is_alive()]
if not slots.acquire(blocking=False):
raise ProtocolError("Command limit")
worker = threading.Thread(target=invoke, args=(requestId,
handlers[params["command"]], params["arguments"]))
workers.append(worker)
worker.start()
elif method == "shutdown":
if requestId or params is not None:
raise ProtocolError("Invalid shutdown")
return
else:
raise ProtocolError("Unknown method")
finally:
cancelled.set()
# Join before closing buffered stdout: a blocked writer must remain
# subject to the host deadline rather than race a Python buffer close.
for worker in workers:
worker.join()
inputStream.close()
outputStream.close()