Add global command bypass with Cthulhu+Z
This commit is contained in:
1 parent
d01965c00d
commit
4c95c9f278
26 files changed
+657
-18
No files matched your search
@@ -183,6 +183,22 @@ application choices override the global value. Sticky browse mode still keeps
|
||||
browse mode, and sticky focus mode still keeps focus mode. Use Cthulhu+A to
|
||||
switch back to browse mode when finished interacting.
|
||||
|
||||
## Pass command keys to an application
|
||||
|
||||
On Xorg, **Cthulhu+Z** toggles command bypass in either keyboard layout.
|
||||
Cthulhu announces and displays **Cthulhu command keys off.** While bypass is
|
||||
active, application shortcuts receive keys normally used for Cthulhu commands,
|
||||
including the configured Cthulhu modifier keys. Automatic speech, key echo,
|
||||
and Braille continue under their existing settings. Cthulhu+Z remains available
|
||||
to restore commands and announces **Cthulhu command keys on.**
|
||||
|
||||
Switching applications or locking the screen also restores commands. Moving
|
||||
between windows in the same application keeps bypass active. Bypass is temporary
|
||||
and does not change your saved preferences or browse/focus mode. Existing XTerm
|
||||
handoff still takes precedence. Wayland command bypass is not yet supported.
|
||||
|
||||
See [command bypass verification](docs/testing/command-bypass.md).
|
||||
|
||||
## Emoji menu
|
||||
|
||||
On Xorg, press **Cthulhu+Control+E** in either keyboard layout to open the
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# Command bypass
|
||||
|
||||
Cthulhu+Z temporarily passes reader command keys to the current application on
|
||||
Xorg. It retains the bypass shortcut and keeps speech and Braille running.
|
||||
Switching applications, leaving the foreground client, or locking restores
|
||||
commands. Same-process window changes and browser content changes preserve
|
||||
bypass. Accessible application identity is the fallback when native process
|
||||
identity is unavailable. Unknown foreground samples do not restore commands.
|
||||
|
||||
The shared focus gate owns bypass state; command dispatch changes it and the
|
||||
accessibility event loop observes foreground/application departure. The X11
|
||||
command owner applies requested routing and grabs after held keys release.
|
||||
Unbound modifier chords use existing replay, with Caps Lock's native action
|
||||
included while bypass is active. OCR review ends on entry. Secure input and
|
||||
XTerm handoff retain their existing precedence. Reader shutdown discards state.
|
||||
Wayland reports bypass unavailable; its current backend has no replay path.
|
||||
|
||||
## Automated checks
|
||||
|
||||
Run affected Go packages and build the native listener test executable:
|
||||
|
||||
```sh
|
||||
go test ./internal/input ./internal/app ./internal/platform/xorg -count=1
|
||||
go test -c -o /tmp/cthulhu-bypass-xorg.test ./internal/platform/xorg
|
||||
CTHULHU_XRECORD_TEST=/tmp/cthulhu-bypass-xorg.test CTHULHU_NATIVE_LISTENER_ONLY=1 CTHULHU_NATIVE_X11_TESTS='^TestNativeCommandBypass$' python3 tests/native_x11.py
|
||||
```
|
||||
|
||||
The native harness owns a private Xvfb display and D-Bus session. It checks both
|
||||
layouts with Insert, KP_Insert, and Caps Lock; complete press/release delivery
|
||||
to an xev client; browse, arrow and keypad pass-through; balanced toggle releases;
|
||||
Caps Lock's actual lock state; and restoration of browse and global commands.
|
||||
Go tests check stale-command suppression, continued speech/Braille output,
|
||||
application departure without accessibility events, unknown foreground samples,
|
||||
same-application windows, and recovery when bindings are changed during bypass.
|
||||
|
||||
These checks do not establish installed-binary behavior, audible speech,
|
||||
physical keyboard input, or physical Braille output.
|
||||
|
||||
## Live acceptance
|
||||
|
||||
1. Quit the running reader with Cthulhu+Q. Press Ctrl+Escape to enter i3 personal
|
||||
mode, then press Shift+G to rebuild and start the reader. Ctrl+Escape and
|
||||
Shift+G are separate key presses.
|
||||
2. In a graphical terminal other than XTerm, press Cthulhu+Z. Expect
|
||||
"Cthulhu command keys off." in speech and a Braille status flash. Type and
|
||||
use arrows: application input and configured echo should continue. Press
|
||||
Cthulhu+T: it should reach the terminal and must not read the time.
|
||||
3. Press Cthulhu+Z again. Expect "Cthulhu command keys on." Press Cthulhu+T:
|
||||
expect the time, with focus remaining in the terminal.
|
||||
4. In a browser in browse mode, enable bypass. Use a page with a known letter
|
||||
shortcut, then press that key. Expect the page's action rather than structural
|
||||
navigation. Arrows should follow the browser's native behavior. Tab should
|
||||
still move focus with ordinary speech and Braille presentation.
|
||||
5. Disable bypass and press H. Expect the next heading. Enable bypass again,
|
||||
switch to a different application, then press Cthulhu+T. Expect the time
|
||||
without first toggling bypass off. Return to the browser and verify that H
|
||||
again navigates by heading when browse mode is active.
|
||||
6. With bypass enabled, switch between windows of the same application. Expect
|
||||
bypass to remain active. If Caps Lock is a configured modifier, press and
|
||||
release it alone during bypass: expect normal lock behavior. Turn it off
|
||||
again before restoring reader commands.
|
||||
|
||||
Report the exact speech, Braille text, focused control, and application key
|
||||
behavior for any mismatch. Inspect ~/devel/cthulhu-go.log after an unexpected
|
||||
result, preserving the fresh log before another restart.
|
||||
@@ -0,0 +1,88 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/platform/xorg"
|
||||
)
|
||||
|
||||
// commandBypass owns the temporary command policy shared by command dispatch
|
||||
// and focus observation. It lasts until another toggle, application departure,
|
||||
// screen lock, or reader shutdown. Output policy is independent of bypass.
|
||||
type commandBypass struct {
|
||||
mutex sync.Mutex
|
||||
keys interface{ SetCommandBypass(bool) }
|
||||
active bool
|
||||
pid int
|
||||
bus string
|
||||
nativePID bool
|
||||
}
|
||||
|
||||
func (bypass *commandBypass) enabled() bool {
|
||||
if bypass == nil {
|
||||
return false
|
||||
}
|
||||
bypass.mutex.Lock()
|
||||
defer bypass.mutex.Unlock()
|
||||
return bypass.active
|
||||
}
|
||||
|
||||
func (bypass *commandBypass) toggle() string {
|
||||
if bypass == nil {
|
||||
return "Command bypass is unavailable on this display backend."
|
||||
}
|
||||
bypass.mutex.Lock()
|
||||
defer bypass.mutex.Unlock()
|
||||
bypass.active = !bypass.active
|
||||
bypass.keys.SetCommandBypass(bypass.active)
|
||||
if bypass.active {
|
||||
return "Cthulhu command keys off."
|
||||
}
|
||||
return "Cthulhu command keys on."
|
||||
}
|
||||
|
||||
func (bypass *commandBypass) restore() {
|
||||
if bypass == nil {
|
||||
return
|
||||
}
|
||||
bypass.mutex.Lock()
|
||||
defer bypass.mutex.Unlock()
|
||||
bypass.restoreLocked()
|
||||
}
|
||||
|
||||
func (bypass *commandBypass) restoreLocked() {
|
||||
if bypass.active {
|
||||
bypass.active = false
|
||||
bypass.keys.SetCommandBypass(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (bypass *commandBypass) observeForeground(client xorg.Foreground) {
|
||||
if bypass == nil || !client.Known {
|
||||
return
|
||||
}
|
||||
bypass.mutex.Lock()
|
||||
defer bypass.mutex.Unlock()
|
||||
if client.Window == 0 || bypass.pid > 0 && client.PID > 0 && bypass.pid != client.PID {
|
||||
bypass.restoreLocked()
|
||||
bypass.bus = ""
|
||||
}
|
||||
bypass.nativePID = client.Window != 0 && client.PID > 0
|
||||
if client.Window == 0 || client.PID > 0 {
|
||||
bypass.pid = client.PID
|
||||
}
|
||||
}
|
||||
|
||||
func (bypass *commandBypass) observeApplication(bus string) {
|
||||
if bypass == nil || bus == "" {
|
||||
return
|
||||
}
|
||||
bypass.mutex.Lock()
|
||||
defer bypass.mutex.Unlock()
|
||||
// Native process identity also covers applications without AT-SPI and
|
||||
// keeps browser content processes from looking like application switches.
|
||||
if !bypass.nativePID && bypass.bus != "" && bypass.bus != bus {
|
||||
bypass.restoreLocked()
|
||||
}
|
||||
bypass.bus = bus
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/config"
|
||||
"git.stormux.org/storm/cthulhu/internal/input"
|
||||
"git.stormux.org/storm/cthulhu/internal/platform/xorg"
|
||||
)
|
||||
|
||||
type bypassTestCommands struct {
|
||||
fakeCommands
|
||||
bypassed atomic.Bool
|
||||
changes chan bool
|
||||
}
|
||||
|
||||
func (keys *bypassTestCommands) SetCommandBypass(active bool) {
|
||||
keys.bypassed.Store(active)
|
||||
if keys.changes != nil {
|
||||
keys.changes <- active
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandBypassLifetime(t *testing.T) {
|
||||
keys := &bypassTestCommands{}
|
||||
bypass := newFocusGate(keys).bypass
|
||||
bypass.observeForeground(xorg.Foreground{Known: true, Window: 1, PID: 100})
|
||||
bypass.observeApplication(":1.10")
|
||||
if bypass.toggle() != "Cthulhu command keys off." || !keys.bypassed.Load() {
|
||||
t.Fatal("bypass did not enable")
|
||||
}
|
||||
bypass.observeForeground(xorg.Foreground{})
|
||||
bypass.observeForeground(xorg.Foreground{Known: true, Window: 2, PID: 100})
|
||||
bypass.observeApplication(":1.11")
|
||||
if !bypass.enabled() {
|
||||
t.Fatal("same application window or browser process ended bypass")
|
||||
}
|
||||
bypass.observeForeground(xorg.Foreground{Known: true, Window: 2})
|
||||
bypass.observeForeground(xorg.Foreground{Known: true, Window: 3, PID: 200})
|
||||
if bypass.enabled() || keys.bypassed.Load() {
|
||||
t.Fatal("application switch did not restore commands")
|
||||
}
|
||||
bypass.toggle()
|
||||
bypass.observeForeground(xorg.Foreground{Known: true})
|
||||
if bypass.enabled() {
|
||||
t.Fatal("leaving foreground retained bypass")
|
||||
}
|
||||
bypass.observeApplication(":1.20")
|
||||
bypass.toggle()
|
||||
bypass.observeApplication(":1.21")
|
||||
if bypass.enabled() {
|
||||
t.Fatal("accessible application switch retained bypass without PID")
|
||||
}
|
||||
bypass.toggle()
|
||||
if bypass.toggle() != "Cthulhu command keys on." || keys.bypassed.Load() {
|
||||
t.Fatal("second toggle did not restore commands")
|
||||
}
|
||||
bypass.toggle()
|
||||
bypass.restore()
|
||||
if bypass.enabled() || keys.bypassed.Load() {
|
||||
t.Fatal("lifecycle reset retained bypass")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandBypassRestoresForInaccessibleApplication(t *testing.T) {
|
||||
adapter := &foregroundAdapter{fakeAdapter: fakeAdapter{events: make(chan atspi.Event, 4)}, changes: make(chan struct{}, 1)}
|
||||
state := atspi.ForegroundState{Client: xorg.Foreground{Known: true, Window: 10, PID: 100}, Epoch: 1}
|
||||
adapter.state.Store(&state)
|
||||
keys := &bypassTestCommands{changes: make(chan bool, 4)}
|
||||
gate := newFocusGate(keys)
|
||||
speech := &fakeSpeechQueue{spoken: make(chan string, 4)}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
processEventsWithInput(ctx, slog.New(slog.DiscardHandler), speech, adapter, nil, nil, 0, nil, nil, gate, nil)
|
||||
}()
|
||||
defer func() { cancel(); <-done }()
|
||||
focus := ownerEvent(atspi.WindowActivatedEvent, ":1.10", "/window", "/button")
|
||||
focus.Source.Name, focus.Source.Role, focus.Foreground = "Current", "push button", state
|
||||
adapter.events <- focus
|
||||
expectActivationSpeech(t, speech.spoken, "Current, push button")
|
||||
gate.bypass.toggle()
|
||||
<-keys.changes
|
||||
next := atspi.ForegroundState{Client: xorg.Foreground{Known: true, Window: 20, PID: 200}, Epoch: 2}
|
||||
adapter.state.Store(&next)
|
||||
adapter.changes <- struct{}{}
|
||||
select {
|
||||
case active := <-keys.changes:
|
||||
if active {
|
||||
t.Fatal("new application retained bypass")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("foreground switch without accessibility did not restore commands")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandBypassDispatchKeepsSpeechAndBraille(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
keys := &bypassTestCommands{fakeCommands: fakeCommands{commands: make(chan input.CommandEvent, 8)}}
|
||||
gate := newFocusGate(keys)
|
||||
spoken := &modeTestSpeaker{spoken: make(chan string, 8)}
|
||||
worker := newSpeechWorker(slog.New(slog.DiscardHandler), spoken)
|
||||
if err := worker.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer worker.Stop(context.Background())
|
||||
display := &modeTestBraille{messages: make(chan string, 8)}
|
||||
live := &liveSettings{}
|
||||
live.Store(config.DefaultSettings())
|
||||
output := outputQueue{speech: worker, braille: display, live: live}
|
||||
requests := make(chan input.Command, 8)
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- (commandRuntime{logger: slog.New(slog.DiscardHandler), commands: keys, focusGate: gate,
|
||||
speechOutput: output, live: live, reviewRequests: requests}).run(ctx)
|
||||
}()
|
||||
defer func() {
|
||||
cancel()
|
||||
if err := <-done; err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
checkOutput := func(want string) {
|
||||
t.Helper()
|
||||
for _, messages := range []chan string{spoken.spoken, display.messages} {
|
||||
select {
|
||||
case got := <-messages:
|
||||
if got != want {
|
||||
t.Fatalf("output = %q, want %q", got, want)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("missing speech or braille")
|
||||
}
|
||||
}
|
||||
}
|
||||
keys.commands <- input.CommandEvent{Command: input.ToggleCommandBypass}
|
||||
checkOutput("Cthulhu command keys off.")
|
||||
keys.commands <- input.CommandEvent{Command: input.PresentCurrentLine}
|
||||
keys.commands <- input.CommandEvent{Command: input.Quit}
|
||||
gatedSpeech{speech: output, gate: gate}.Enqueue(speechRequest{Text: "Focused control", Role: "system", Cause: "focus"})
|
||||
checkOutput("Focused control")
|
||||
keys.commands <- input.CommandEvent{Command: input.ToggleCommandBypass}
|
||||
checkOutput("Cthulhu command keys on.")
|
||||
select {
|
||||
case command := <-requests:
|
||||
t.Fatalf("bypassed command dispatched: %s", command)
|
||||
default:
|
||||
}
|
||||
keys.commands <- input.CommandEvent{Command: input.PresentCurrentLine}
|
||||
select {
|
||||
case <-requests:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("restored command did not dispatch")
|
||||
}
|
||||
}
|
||||
@@ -171,6 +171,9 @@ func (handler *accessibilityEventHandler) handle(event atspi.Event) {
|
||||
return
|
||||
}
|
||||
consumed := handler.acceptFocusOwnership(event, notification)
|
||||
if focusGate != nil && isFocusGainEvent(event) && !notification && state.owner.applicationBus == event.Source.BusName {
|
||||
focusGate.bypass.observeApplication(event.Source.BusName)
|
||||
}
|
||||
if isFocusGainEvent(event) && !notification && live != nil && live.onApplication != nil &&
|
||||
state.owner.applicationBus == event.Source.BusName && state.owner.window == eventWindow(event) {
|
||||
application := handler.titles.applicationForEvent(event)
|
||||
|
||||
@@ -155,6 +155,9 @@ func processEventsWithAttributes(
|
||||
if foreground != nil {
|
||||
foregroundChanges = foreground.ForegroundChanges()
|
||||
state.foregroundState = foreground.Foreground()
|
||||
if focusGate != nil {
|
||||
focusGate.bypass.observeForeground(state.foregroundState.Client)
|
||||
}
|
||||
}
|
||||
titles := &windowTitleController{state: &state, live: live, gate: focusGate, speech: speech, logger: logger, foreground: foreground, inputSource: inputSource}
|
||||
titles.source, _ = adapter.(windowTitleSource)
|
||||
@@ -213,6 +216,9 @@ func processEventsWithAttributes(
|
||||
}
|
||||
syncForeground := func() {
|
||||
state.syncForeground(foreground, logger, clearOwnedFocus)
|
||||
if focusGate != nil && foreground != nil {
|
||||
focusGate.bypass.observeForeground(state.foregroundState.Client)
|
||||
}
|
||||
if wineReview != nil {
|
||||
wineReview.foregroundChanged()
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ type commandGrabController interface {
|
||||
}
|
||||
|
||||
type focusGate struct {
|
||||
bypass *commandBypass
|
||||
tentacleOutput *tentacleOutput
|
||||
remotePolicy *tentacle.RemotePolicy
|
||||
|
||||
@@ -46,6 +47,9 @@ type focusGate struct {
|
||||
|
||||
func newFocusGate(commands commandListener) *focusGate {
|
||||
gate := &focusGate{lockChanges: make(chan struct{}, 1)}
|
||||
if keys, ok := commands.(interface{ SetCommandBypass(bool) }); ok {
|
||||
gate.bypass = &commandBypass{keys: keys}
|
||||
}
|
||||
if controller, ok := commands.(commandGrabController); ok {
|
||||
gate.commands = controller
|
||||
}
|
||||
|
||||
@@ -158,6 +158,8 @@ func learnCommandDescription(command input.Command) string {
|
||||
return "Show the actions available for the current object"
|
||||
case input.ToggleLearnMode:
|
||||
return "Enter or exit learn mode."
|
||||
case input.ToggleCommandBypass:
|
||||
return "Pass command keys to the application until pressed again or you switch applications."
|
||||
case input.DisableTentacles:
|
||||
return "Disable all tentacles until Cthulhu restarts."
|
||||
case input.Quit:
|
||||
|
||||
@@ -133,11 +133,36 @@ func (worker commandRuntime) run(ctx context.Context) error {
|
||||
continue
|
||||
}
|
||||
command := event.Command
|
||||
var bypass *commandBypass
|
||||
if focusGate != nil {
|
||||
bypass = focusGate.bypass
|
||||
}
|
||||
if command != input.ToggleCommandBypass && command != input.OCRDismiss && bypass.enabled() {
|
||||
continue
|
||||
}
|
||||
if speechMessages != nil {
|
||||
speechMessages.ProtectCommand(event.Timestamp)
|
||||
}
|
||||
focusGate.dismiss_confirmation()
|
||||
logger.Debug("keyboard command received", "command", command, "input_timestamp", event.Timestamp)
|
||||
if command == input.ToggleCommandBypass {
|
||||
if focusGate != nil {
|
||||
focusGate.mutex.Lock()
|
||||
if focusGate.Locked() || focusGate.Suspended() {
|
||||
focusGate.mutex.Unlock()
|
||||
continue
|
||||
}
|
||||
}
|
||||
message := bypass.toggle()
|
||||
if focusGate != nil {
|
||||
focusGate.mutex.Unlock()
|
||||
}
|
||||
if bypass.enabled() {
|
||||
worker.ocr.Retire()
|
||||
}
|
||||
speechOutput.Enqueue(speechRequest{Text: message, Role: "system", Cause: "command-bypass"})
|
||||
continue
|
||||
}
|
||||
if worker.internalUI.Handle(event) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -139,6 +139,9 @@ func (gate *focusGate) SetLocked(active bool, adapter Adapter, listener InputLis
|
||||
if !gate.secure.CompareAndSwap(!active, active) {
|
||||
return
|
||||
}
|
||||
if active {
|
||||
gate.bypass.restore()
|
||||
}
|
||||
gate.retireTentacleOutput()
|
||||
gate.sync_remote_policy()
|
||||
for _, target := range []any{adapter, listener, commands} {
|
||||
|
||||
@@ -63,6 +63,8 @@ func CommandLabel(command Command) string {
|
||||
return "List " + strings.ToLower(binding.Title)
|
||||
}
|
||||
switch command {
|
||||
case ToggleCommandBypass:
|
||||
return "Toggle passing command keys to the application"
|
||||
case PresentCurrentLine:
|
||||
return "Read the current line"
|
||||
case PresentWindowTitle:
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package input
|
||||
|
||||
// BypassCommandBindings leaves only the command which restores reader keys.
|
||||
// Keep a recovery shortcut if preferences remove it during an active bypass.
|
||||
func BypassCommandBindings(bindings CommandBindings) CommandBindings {
|
||||
result := make(CommandBindings)
|
||||
for chord, command := range bindings {
|
||||
if command == ToggleCommandBypass {
|
||||
result[chord] = command
|
||||
}
|
||||
}
|
||||
if len(result) == 0 {
|
||||
result[CommandChord{Symbol: 'z', CthulhuModifier: true}] = ToggleCommandBypass
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package input
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBypassRetainsOnlyRecoveryCommand(t *testing.T) {
|
||||
for _, layout := range []KeyboardLayout{DesktopKeyboardLayout, LaptopKeyboardLayout} {
|
||||
bindings := BrowseCommandBindings(layout)
|
||||
chord := CommandChord{Symbol: 'z', CthulhuModifier: true}
|
||||
if bindings[chord] != ToggleCommandBypass {
|
||||
t.Fatal("missing default bypass shortcut")
|
||||
}
|
||||
filtered := BypassCommandBindings(bindings)
|
||||
if len(filtered) != 1 || filtered[chord] != ToggleCommandBypass {
|
||||
t.Fatalf("bypass bindings = %v", filtered)
|
||||
}
|
||||
delete(bindings, chord)
|
||||
custom := CommandChord{Symbol: 0xffc6}
|
||||
bindings[custom] = ToggleCommandBypass
|
||||
filtered = BypassCommandBindings(bindings)
|
||||
if len(filtered) != 1 || filtered[custom] != ToggleCommandBypass {
|
||||
t.Fatalf("custom bypass bindings = %v", filtered)
|
||||
}
|
||||
delete(bindings, custom)
|
||||
if BypassCommandBindings(bindings)[chord] != ToggleCommandBypass {
|
||||
t.Fatal("missing recovery after shortcut removal")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ const (
|
||||
CycleProgressBarOutput Command = "cycle-progress-bar-output"
|
||||
PresentVersion Command = "present-version"
|
||||
ToggleLearnMode Command = "toggle-learn-mode"
|
||||
ToggleCommandBypass Command = "toggle-command-bypass"
|
||||
EmojiMenu Command = "emoji-menu"
|
||||
ShowActions Command = "show-actions"
|
||||
InternalUIKey Command = "internal-ui-key"
|
||||
@@ -220,6 +221,7 @@ func DefaultCommandBindings(layout KeyboardLayout) CommandBindings {
|
||||
{Symbol: 'v', Modifiers: 1, CthulhuModifier: true}: PresentVersion,
|
||||
{Symbol: 'a', CthulhuModifier: true}: ToggleDocumentMode,
|
||||
{Symbol: 'h', CthulhuModifier: true}: ToggleLearnMode,
|
||||
{Symbol: 'z', CthulhuModifier: true}: ToggleCommandBypass,
|
||||
{Symbol: 'h', Modifiers: 1, CthulhuModifier: true}: DocumentPreviousHeading,
|
||||
{Symbol: 'k', CthulhuModifier: true, Modifiers: controlModifier}: DocumentNextLink,
|
||||
{Symbol: 'k', Modifiers: controlModifier | 1, CthulhuModifier: true}: DocumentPreviousLink,
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
package xorg
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/input"
|
||||
)
|
||||
|
||||
func TestNativeCommandBypass(t *testing.T) {
|
||||
if os.Getenv("CTHULHU_NATIVE_X11") != "1" {
|
||||
t.Skip("requires isolated X11 display")
|
||||
}
|
||||
for _, layout := range []input.KeyboardLayout{input.DesktopKeyboardLayout, input.LaptopKeyboardLayout} {
|
||||
for _, modifier := range []string{"Insert", "KP_Insert", "Caps_Lock"} {
|
||||
t.Run(string(rune('0'+layout))+"/"+modifier, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
previousFocus, err := exec.CommandContext(ctx, "xdotool", "getwindowfocus").Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer exec.Command("xdotool", "windowfocus", strings.TrimSpace(string(previousFocus))).Run()
|
||||
probe := exec.CommandContext(ctx, "stdbuf", "-oL", "xev", "-name", "Cthulhu Bypass Probe", "-event", "keyboard")
|
||||
out, err := probe.StdoutPipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := probe.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { probe.Process.Kill(); probe.Wait() }()
|
||||
seen := make(chan string, 512)
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(out)
|
||||
for scanner.Scan() {
|
||||
if line := scanner.Text(); strings.Contains(line, "keysym") {
|
||||
select {
|
||||
case seen <- line:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
key := func(args ...string) {
|
||||
t.Helper()
|
||||
if out, err := exec.CommandContext(ctx, "xdotool", args...).CombinedOutput(); err != nil {
|
||||
t.Fatalf("xdotool %v: %v %s", args, err, out)
|
||||
}
|
||||
}
|
||||
window := ""
|
||||
for window == "" {
|
||||
out, _ := exec.CommandContext(ctx, "xdotool", "search", "--onlyvisible", "--name", "^Cthulhu Bypass Probe$").Output()
|
||||
window = strings.TrimSpace(string(out))
|
||||
if ctx.Err() != nil {
|
||||
t.Fatal(ctx.Err())
|
||||
}
|
||||
}
|
||||
key("windowfocus", window)
|
||||
listener := NewCommands([]string{modifier}, layout)
|
||||
if err := listener.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer listener.Stop(context.Background())
|
||||
wait := func(check func() bool) {
|
||||
t.Helper()
|
||||
for !check() {
|
||||
select {
|
||||
case err := <-listener.Failures():
|
||||
t.Fatal(err)
|
||||
case <-ctx.Done():
|
||||
t.Fatal(ctx.Err())
|
||||
case <-time.After(time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
next := func(want input.Command) {
|
||||
t.Helper()
|
||||
select {
|
||||
case event := <-listener.Commands():
|
||||
if event.Command != want {
|
||||
t.Fatalf("command = %s, want %s", event.Command, want)
|
||||
}
|
||||
case err := <-listener.Failures():
|
||||
t.Fatal(err)
|
||||
case <-ctx.Done():
|
||||
t.Fatal(ctx.Err())
|
||||
}
|
||||
}
|
||||
listener.SetBrowseNavigation(true)
|
||||
wait(listener.BrowseNavigationEnabled)
|
||||
key("keydown", modifier, "keydown", "z")
|
||||
next(input.ToggleCommandBypass)
|
||||
listener.SetCommandBypass(true)
|
||||
// A binding update acknowledges only after the complete chord.
|
||||
applied := make(chan error, 1)
|
||||
go func() { applied <- listener.SetKeyBindings(ctx, nil) }()
|
||||
select {
|
||||
case err := <-applied:
|
||||
t.Fatalf("update applied with chord held: %v", err)
|
||||
case <-time.After(25 * time.Millisecond):
|
||||
}
|
||||
key("keyup", modifier, "keyup", "z")
|
||||
if err := <-applied; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wait(listener.CommandBypassEnabled)
|
||||
if listener.BrowseNavigationEnabled() {
|
||||
t.Fatal("browse navigation survived bypass")
|
||||
}
|
||||
key("key", "h", "Down", "KP_Begin")
|
||||
key("keydown", modifier, "key", "s", "keyup", modifier)
|
||||
key("key", "F12")
|
||||
counts := map[string]int{}
|
||||
for counts["F12"] < 2 {
|
||||
select {
|
||||
case line := <-seen:
|
||||
for _, name := range []string{"h", "Down", "KP_Begin", modifier, "s", "F12", "z"} {
|
||||
if strings.Contains(strings.ToLower(line), strings.ToLower(", "+name+")")) {
|
||||
counts[name]++
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("application events = %v: %v", counts, ctx.Err())
|
||||
}
|
||||
}
|
||||
for _, name := range []string{"h", "Down", "KP_Begin", modifier, "s"} {
|
||||
if counts[name] != 2 {
|
||||
t.Fatalf("application events = %v; want one press/release for %s", counts, name)
|
||||
}
|
||||
}
|
||||
if counts["z"] != 0 {
|
||||
t.Fatal("bypass toggle leaked to application")
|
||||
}
|
||||
select {
|
||||
case event := <-listener.Commands():
|
||||
t.Fatalf("command escaped bypass: %s", event.Command)
|
||||
default:
|
||||
}
|
||||
// Caps Lock's native action is restored by the replayed chord.
|
||||
if modifier == "Caps_Lock" {
|
||||
select {
|
||||
case state := <-listener.LockStates():
|
||||
if state.Name != "Caps Lock" || !state.Locked {
|
||||
t.Fatalf("lock = %+v", state)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Caps Lock did not toggle during bypass")
|
||||
}
|
||||
key("key", "Caps_Lock")
|
||||
select {
|
||||
case state := <-listener.LockStates():
|
||||
if state.Locked {
|
||||
t.Fatal("Caps Lock did not unlock during bypass")
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatal("missing Caps Lock unlock")
|
||||
}
|
||||
}
|
||||
key("keydown", modifier, "key", "z", "keyup", modifier)
|
||||
next(input.ToggleCommandBypass)
|
||||
listener.SetCommandBypass(false)
|
||||
wait(func() bool { return !listener.CommandBypassEnabled() && listener.BrowseNavigationEnabled() })
|
||||
key("key", "h")
|
||||
next(input.DocumentNextHeading)
|
||||
listener.SetBrowseNavigation(false)
|
||||
wait(func() bool { return !listener.browseApplied.Load() })
|
||||
key("keydown", modifier, "key", "s", "keyup", modifier)
|
||||
next(input.CycleSpeechMode)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ type CommandListener struct {
|
||||
ocrPending atomic.Bool
|
||||
browse atomic.Bool
|
||||
browseApplied atomic.Bool
|
||||
bypass atomic.Bool
|
||||
bypassApplied atomic.Bool
|
||||
nativeButtonFocus atomic.Bool
|
||||
nativeButtonApplied atomic.Bool
|
||||
grabActive atomic.Bool
|
||||
@@ -82,6 +84,11 @@ func NewCommands(modifiers []string, layout input.KeyboardLayout) *CommandListen
|
||||
// complete chords, preserving every key release during a mode transition.
|
||||
func (listener *CommandListener) SetBrowseNavigation(enabled bool) { listener.browse.Store(enabled) }
|
||||
|
||||
// SetCommandBypass retains only the bypass shortcut after held keys release.
|
||||
func (listener *CommandListener) SetCommandBypass(enabled bool) { listener.bypass.Store(enabled) }
|
||||
|
||||
func (listener *CommandListener) CommandBypassEnabled() bool { return listener.bypassApplied.Load() }
|
||||
|
||||
// SetNativeButtonFocus leaves Return to a standard button with real DOM focus.
|
||||
// Virtual browse targets elsewhere still use Cthulhu's document activation.
|
||||
func (listener *CommandListener) SetNativeButtonFocus(focused bool) {
|
||||
@@ -94,7 +101,7 @@ func (listener *CommandListener) SetCommandGrabs(enabled bool) {
|
||||
|
||||
// BrowseNavigationEnabled reports whether requested browse grabs have reached X11.
|
||||
func (listener *CommandListener) BrowseNavigationEnabled() bool {
|
||||
return !listener.ocrApplied.Load() && !listener.secure.Load() && listener.browse.Load() && listener.browseApplied.Load() && listener.commandsApplied.Load()
|
||||
return !listener.bypass.Load() && !listener.ocrApplied.Load() && !listener.secure.Load() && listener.browse.Load() && listener.browseApplied.Load() && listener.commandsApplied.Load()
|
||||
}
|
||||
|
||||
func (listener *CommandListener) CommandGrabsEnabled() bool {
|
||||
|
||||
@@ -159,7 +159,10 @@ func configure_native_bindings(device *C.CommandDevice, layout input.KeyboardLay
|
||||
}
|
||||
global := resolvedBindings(layout, false, overrides)
|
||||
browse := resolvedBindings(layout, true, overrides)
|
||||
if ocrReview {
|
||||
if device.bypass != 0 {
|
||||
global = input.BypassCommandBindings(global)
|
||||
browse = global
|
||||
} else if ocrReview {
|
||||
browse = input.OCRCommandBindings(global)
|
||||
if len(pending) > 0 && pending[0] {
|
||||
browse = input.OCRPendingBindings(global)
|
||||
@@ -202,7 +205,7 @@ func configure_native_bindings(device *C.CommandDevice, layout input.KeyboardLay
|
||||
// Own Control/Shift from their press in temporary review. Otherwise the
|
||||
// application can see a modifier press whose release the arrow grab owns.
|
||||
// Unbound chords are replayed whole after all their physical keys release.
|
||||
if ocrReview && !(len(pending) > 0 && pending[0]) {
|
||||
if device.bypass == 0 && ocrReview && !(len(pending) > 0 && pending[0]) {
|
||||
for _, symbol := range []uint32{0xffe1, 0xffe2, 0xffe3, 0xffe4} {
|
||||
for _, mask := range []uint16{0, 1, 4, 5} {
|
||||
native = append(native, C.NativeBinding{symbol: C.ulong(symbol), modifiers: C.uint(mask), browseOnly: 1})
|
||||
|
||||
@@ -147,7 +147,7 @@ func (owner *commandOwner) handleRoutedKey(key C.CommandKey) bool {
|
||||
}
|
||||
|
||||
func (owner *commandOwner) finishChord() bool {
|
||||
if !owner.bound && owner.device.capsCode != 0 && is_caps_lock_toggle(owner.buffered) {
|
||||
if owner.device.bypass == 0 && !owner.bound && owner.device.capsCode != 0 && is_caps_lock_toggle(owner.buffered) {
|
||||
C.toggle_command_caps_lock(owner.device)
|
||||
if result := C.replay_command_keys(owner.device, &owner.buffered[0], C.int(len(owner.buffered))); result != 0 {
|
||||
owner.listener.failures <- fmt.Errorf("release Caps Lock toggle chord: native error %d", int(result))
|
||||
|
||||
@@ -674,7 +674,7 @@ int replay_command_keys(CommandDevice *device, CommandKey *keys, int count) {
|
||||
for (int i=0;i<count;i++) {
|
||||
// Replaying an unbound chord must not restore Caps Lock's lock action
|
||||
// while that key is configured as the reader modifier.
|
||||
if (device->capsCode && keys[i].code==device->capsCode) continue;
|
||||
if (!device->bypass && device->capsCode && keys[i].code==device->capsCode) continue;
|
||||
if (!XTestFakeKeyEvent(device->display,keys[i].code,keys[i].pressed,CurrentTime)) injected=0;
|
||||
}
|
||||
if (device->commands) grab_command_keys(device);
|
||||
@@ -683,6 +683,11 @@ int replay_command_keys(CommandDevice *device, CommandKey *keys, int count) {
|
||||
int error=grab_error;
|
||||
error_display=NULL;
|
||||
pthread_mutex_unlock(&error_mutex);
|
||||
if (device->bypass && device->capsCode) {
|
||||
XkbStateRec state;
|
||||
if (XkbGetState(device->display,XkbUseCoreKbd,&state)!=Success) return -7;
|
||||
device->capsLocked=(state.locked_mods & LockMask)!=0;
|
||||
}
|
||||
if (!injected) return -1;
|
||||
return error;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ typedef struct {
|
||||
struct xkb_compose_state *composeState;
|
||||
unsigned int lockIndicators[3];
|
||||
int commands;
|
||||
int bypass;
|
||||
int active;
|
||||
int internalUI;
|
||||
int localKeyboard, xiOpcode;
|
||||
|
||||
@@ -67,10 +67,13 @@ func (listener *CommandListener) set_ocr_context(ctx context.Context, enabled, p
|
||||
}
|
||||
|
||||
func (listener *CommandListener) OCRReviewEnabled() bool {
|
||||
return !listener.ocrPending.Load() && !listener.secure.Load() && listener.commandsEnabled.Load() && listener.commandsApplied.Load() && listener.ocrApplied.Load()
|
||||
return !listener.bypass.Load() && !listener.ocrPending.Load() && !listener.secure.Load() && listener.commandsEnabled.Load() && listener.commandsApplied.Load() && listener.ocrApplied.Load()
|
||||
}
|
||||
|
||||
func (owner *commandOwner) context_bindings() input.CommandBindings {
|
||||
if owner.device.bypass != 0 {
|
||||
return input.BypassCommandBindings(owner.globalBindings)
|
||||
}
|
||||
if owner.ocrReview {
|
||||
if owner.ocrPending {
|
||||
return input.OCRPendingBindings(owner.globalBindings)
|
||||
@@ -85,7 +88,7 @@ func (owner *commandOwner) context_bindings() input.CommandBindings {
|
||||
func configure_context_bindings(device *C.CommandDevice, layout input.KeyboardLayout, modifiers []string, overrides []input.BindingOverride, browse, ocrReview bool, pending ...bool) error {
|
||||
previous, enabled := device.browse, device.commands
|
||||
device.browse = 0
|
||||
if browse || ocrReview {
|
||||
if device.bypass == 0 && (browse || ocrReview) {
|
||||
device.browse = 1
|
||||
}
|
||||
err := configure_native_bindings(device, layout, modifiers, overrides, ocrReview, pending...)
|
||||
|
||||
@@ -67,6 +67,7 @@ func (listener *CommandListener) Start(ctx context.Context) error {
|
||||
defer listener.browseApplied.Store(false)
|
||||
defer listener.ocrApplied.Store(false)
|
||||
defer listener.commandsApplied.Store(false)
|
||||
defer listener.bypassApplied.Store(false)
|
||||
listener.commandsApplied.Store(true)
|
||||
layout := listener.layout
|
||||
modifiers := append([]string(nil), listener.modifiers...)
|
||||
|
||||
@@ -164,7 +164,7 @@ func (owner *commandOwner) applyBindingUpdate() bool {
|
||||
nextOverrides, nextOCR = owner.overrides, *update.ocrReview
|
||||
}
|
||||
err := update.ctx.Err()
|
||||
if err == nil && nextOCR && (!owner.commandGrabsAllowed() || owner.learnMode) {
|
||||
if err == nil && nextOCR && (!owner.commandGrabsAllowed() || owner.learnMode || owner.listener.bypass.Load()) {
|
||||
err = errors.New("OCR review input unavailable")
|
||||
}
|
||||
if err == nil && nextOCR {
|
||||
@@ -226,6 +226,26 @@ func (owner *commandOwner) resetForSecureInput() {
|
||||
}
|
||||
|
||||
func (owner *commandOwner) syncGrabs() bool {
|
||||
if len(owner.held) == 0 && (owner.device.bypass != 0) != owner.listener.bypass.Load() {
|
||||
if owner.ocrReview {
|
||||
if !owner.leave_ocr_review() || !owner.listener.enqueueSecureCommand(input.CommandEvent{Command: input.OCRDismiss}) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
previous := owner.device.bypass
|
||||
owner.device.bypass = 0
|
||||
if owner.listener.bypass.Load() {
|
||||
owner.device.bypass = 1
|
||||
}
|
||||
if err := configure_context_bindings(owner.device, owner.layout, owner.modifiers, owner.overrides, owner.listener.browse.Load(), false); err != nil {
|
||||
owner.device.bypass = previous
|
||||
owner.listener.failures <- err
|
||||
return false
|
||||
}
|
||||
owner.router.ReplaceBindings(owner.context_bindings())
|
||||
owner.listener.bypassApplied.Store(owner.device.bypass != 0)
|
||||
owner.listener.browseApplied.Store(owner.device.browse != 0)
|
||||
}
|
||||
if owner.ocrReview && len(owner.held) == 0 {
|
||||
foreground := C.read_foreground(owner.device.display)
|
||||
if foreground.known == 0 || foreground.window == 0 || foreground.window != owner.device.browseWindow {
|
||||
@@ -265,8 +285,8 @@ func (owner *commandOwner) syncGrabs() bool {
|
||||
owner.listener.commandsApplied.Store(enabled)
|
||||
}
|
||||
if !owner.ocrReview && len(owner.held) == 0 && (owner.device.commands != 0) &&
|
||||
(owner.device.browse != 0) != owner.listener.browse.Load() {
|
||||
enabled := owner.listener.browse.Load()
|
||||
(owner.device.browse != 0) != (owner.listener.browse.Load() && owner.device.bypass == 0) {
|
||||
enabled := owner.listener.browse.Load() && owner.device.bypass == 0
|
||||
value := C.int(0)
|
||||
if enabled {
|
||||
value = 1
|
||||
@@ -275,11 +295,7 @@ func (owner *commandOwner) syncGrabs() bool {
|
||||
owner.listener.failures <- fmt.Errorf("change X11 browse grabs: native error %d", int(err))
|
||||
return false
|
||||
}
|
||||
bindings := resolvedBindings(owner.layout, false, owner.overrides)
|
||||
if enabled {
|
||||
bindings = resolvedBindings(owner.layout, true, owner.overrides)
|
||||
}
|
||||
owner.router.ReplaceBindings(bindings)
|
||||
owner.router.ReplaceBindings(owner.context_bindings())
|
||||
owner.listener.browseApplied.Store(enabled)
|
||||
}
|
||||
owner.device.nativeButtonFocus = 0
|
||||
|
||||
@@ -1100,7 +1100,7 @@ func TestNativeModifierConfigurationUpdate(t *testing.T) {
|
||||
if after := lockState(); after != before {
|
||||
t.Fatalf("configured Caps Lock alone toggled capitalization: %s -> %s", before, after)
|
||||
}
|
||||
key("keydown", "Caps_Lock", "key", "z", "keyup", "Caps_Lock")
|
||||
key("keydown", "Caps_Lock", "key", "y", "keyup", "Caps_Lock")
|
||||
if after := lockState(); after != before {
|
||||
t.Fatalf("unbound Caps Lock chord toggled capitalization: %s -> %s", before, after)
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -77,3 +77,4 @@ web-message-article-focus,src/cthulhu/scripts/web/speech_generator.py;src/cthulh
|
||||
web-disclosure-refresh,src/cthulhu/action_presenter.py; src/cthulhu/scripts/web/script_utilities.py,web,Discover and keep disclosure options open during Enter or Actions-list activation and caret navigation,,internal/atspi internal/app,implemented,"CTHULHU_BROWSER=/usr/bin/chromium CTHULHU_NATIVE_BROWSER_TESTS=actions,disclosure,reveal,positions,ticking,document python3 tests/native_browser.py",Private Chromium/Xorg fixture covers both activation paths; character and line reading; blur and document-scroll dismissal; offscreen text and images. Original public page also checked in a private Chromium profile.,,Original-page audible speech acceptance pending.,Physical Braille acceptance pending.,Preserve target generations and strict ownership; refresh action snapshots and ancestor order. Text caret placement reveals its range without an extra whole-object scroll that dismisses popups; non-text scrolling retained. User installed Brave acceptance pending. Link-sounds native Tab pan baseline fails before and after this fix. See docs/testing/browser-clickables.md.
|
||||
mumble-message-history,src/cthulhu/scripts/apps/Mumble/chat.py;src/cthulhu/chat.py,messaging,Review or copy twelve recent Mumble Activity log messages with Cthulhu+F1 through F12,,internal/atspi internal/messaging internal/app internal/input,in-progress,Message cleanup and twelve-slot ordering tests; focused messaging race checks; python3 -B tests/native_messaging.py.,Private Qt transcript and unrelated-provider checks; synthetic X11 review and copy chords through F12; clipboard checks.,,Recorded presentation tested; actual Mumble listening pending.,Single flash and review text tested; physical display pending.,Legacy combined history defaults. Strip timestamps and Qt noncharacters; ignore timestamp-only insertions; preserve repeated messages and keyboard focus. Pidgin keeps its nine-message review range. History is session-only and clears on lock or transcript retirement. See docs/testing/messaging.md for live acceptance.
|
||||
web-inline-visual-lines,Orca src/orca/scripts/web/script_utilities.py;src/orca/ax_utilities.py,web,Group same-row ordinary links and list items during browse line navigation,,internal/review internal/presentation internal/app internal/atspi,in-progress,Review AT-SPI presentation and app package tests; private browser fragments lists namedlinks linkedheadings document selection; Firefox baseline and tabs,Private Brave and Firefox Xvfb sessions,,Speech requests verified; live listening pending,Native fragment routing checked; physical display acceptance pending,See docs/testing/browser-visual-lines.md. Preserve block boundaries and grid items; atomic named controls remain independent. Human review and original-page live acceptance pending.
|
||||
command-bypass,Orca src/orca/bypass_mode_manager.py;src/orca/bypass_mode_manager_command_definitions.py,input,Pass reader command keys to the current application with Cthulhu+Z while retaining automatic output,,internal/input internal/app internal/platform/xorg,in-progress,go test ./internal/input ./internal/app ./internal/platform/xorg -count=1; focused race tests; see docs/testing/command-bypass.md,Private Xvfb and D-Bus listener checks pass for both layouts and all three modifier keys; verifies key delivery and Caps Lock state plus restoration and related grab regressions.,Not implemented; reports bypass unavailable.,Queued speech and continued automatic output tested; audible acceptance pending.,Braille flash delivery tested; physical display acceptance pending.,Restores on second toggle or application departure or screen lock. Same-application windows preserve bypass. Existing XTerm handoff retained. Human review and live acceptance pending.
|
||||
|
+3
-2
@@ -175,9 +175,10 @@ def run_inside(directory):
|
||||
subprocess.run(['xdotool', 'windowfocus', window], check=True)
|
||||
nativeTest = os.environ.get('CTHULHU_XRECORD_TEST')
|
||||
if nativeTest:
|
||||
subprocess.run([nativeTest, '-test.run', '^TestNative(ModifierPressRelease|CommandGrabSuspensionAndRestoration|BrowseKeyTransitions|DispatchBeforeRelease|CustomBindingsUpdateAndRelease|TentacleBindingsUpdateAndRelease|ModifierConfigurationUpdate|LockStatesAndShiftCapsLock|XTermHandoffKeepsKeypadReleased|SuspendedQueuedKey|LearnModeCapturesKeysAndRestoresCommands|SecureInputDropsKeysAndRestoresGrabs)$', '-test.timeout=40s'], env=dict(os.environ, CTHULHU_NATIVE_X11='1', CTHULHU_NATIVE_BROWSER='1'), check=True, timeout=45, cwd=Path(__file__).resolve().parents[1] / 'internal/platform/xorg')
|
||||
native_tests = os.environ.get('CTHULHU_NATIVE_X11_TESTS', '^TestNative(ModifierPressRelease|CommandBypass|CommandGrabSuspensionAndRestoration|BrowseKeyTransitions|DispatchBeforeRelease|CustomBindingsUpdateAndRelease|TentacleBindingsUpdateAndRelease|ModifierConfigurationUpdate|LockStatesAndShiftCapsLock|XTermHandoffKeepsKeypadReleased|SuspendedQueuedKey|LearnModeCapturesKeysAndRestoresCommands|SecureInputDropsKeysAndRestoresGrabs)$')
|
||||
subprocess.run([nativeTest, '-test.run', native_tests, '-test.timeout=40s'], env=dict(os.environ, CTHULHU_NATIVE_X11='1', CTHULHU_NATIVE_BROWSER='1'), check=True, timeout=45, cwd=Path(__file__).resolve().parents[1] / 'internal/platform/xorg')
|
||||
if nativeTest and os.environ.get('CTHULHU_NATIVE_LISTENER_ONLY') == '1':
|
||||
print('PASS: native command bindings, press/release ordering, browse transitions, suspension, custom updates, and grab rollback')
|
||||
print('PASS: selected native X11 command listener tests')
|
||||
return
|
||||
app = subprocess.Popen([str(binary), '--config', str(config), '--debug-file', str(log)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
processes.append(app)
|
||||
|
||||
Reference in new issue
Block a user