Share document Where Am I reports with Remote

This commit is contained in:
ak47 committed 2026-11-05 17:11:08 -05:00
1 parent 02fb70e81a
commit 103fbfd81a
13 files changed
+249 -19

No files matched your search

+26 -1
View File
@@ -121,7 +121,18 @@ without changing its local text. Layout text and privacy context remain from the
same capture across tree replacement. Boundaries and errors remain private. same capture across tree replacement. Boundaries and errors remain private.
Existing role chunks, pronunciation, spelling, phonetic expansion and character Existing role chunks, pronunciation, spelling, phonetic expansion and character
delivery retain their normal ordering and original lease/generation admission. delivery retain their normal ordering and original lease/generation admission.
Where Am I and other unreviewed producers remain private. Where Am I at a document position now exports only when that position retains
complete captured source ancestry. The event loop stores classification with
the current snapshot, document focus and epoch; it stores no lease authority.
Every snapshot replacement clears classification, including same-object native
updates and Say All position updates without ancestry. Focus/epoch replacement,
missing or mismatched ancestry, password/unknown/invalid roles and defunct
sources keep the report local. Foreground synchronization runs before this
check. Lock and terminal handoff suppress the command. Missing-location status
stays private. Native focus reports and other unreviewed producers remain private.
Existing text, role chunks, formula reporting, pronunciation, focus, caret and
Braille behavior are preserved; the speech worker captures the current lease
and generation when the explicit command enters its queue.
Browse-mode current-line reporting, spelling and phonetic spelling now export Browse-mode current-line reporting, spelling and phonetic spelling now export
only with the command's captured source ancestry. The review worker retains only with the command's captured source ancestry. The review worker retains
@@ -478,6 +489,20 @@ exclusion and separate role utterance boundaries. An overlay restoring the
previous unclassified producers reproduces missing ordinary export. No full previous unclassified producers reproduces missing ordinary export. No full
reader suite, native desktop, hardware or audible checks were performed. reader suite, native desktop, hardware or audible checks were performed.
The document Where Am I increment passed focused application reading/focus/
speech/Remote race regressions, complete input/review/AT-SPI and external Remote
race suites, ten repetitions of the new source/privacy cases, the speech
regression harness, application vet, a separate reader build and Tentacles
`make check`. The ordinary-source regression failed before implementation.
Owned-tree cases check private/missing/stale ancestry, same-object native updates,
Say All position replacement, focus/epoch retirement and lock/handoff, including
handoff discovered by foreground synchronization. The source-command/private-relay
fixture verifies ordinary exports, private exclusion and separate role boundaries.
The full application race suite timed out after 180 seconds in package-upgrade
tests; it is not a pass. The source-command race run needed approval for local
fixture sockets; its approved rerun passed. No native desktop, hardware or audible
checks were performed. See the [live Where Am I steps](../../cthulhu-tentacles/remote/command.md#physical-keyboard-acceptance-still-pending).
Next: verify real Speech Dispatcher scheduling and audible local/remote Next: verify real Speech Dispatcher scheduling and audible local/remote
arbitration. Physical input capture/release and ordinary reading while connected arbitration. Physical input capture/release and ordinary reading while connected
still need acceptance. still need acceptance.
+1 -1
View File
@@ -36,7 +36,7 @@ func (handler *accessibilityEventHandler) prepareApplicationEvent(event atspi.Ev
} }
state.pending.caret.clear() state.pending.caret.clear()
state.focusedID = result.event.Source.ID() state.focusedID = result.event.Source.ID()
state.current = result.event.Source state.setCurrent(result.event.Source)
state.managedContainerID, state.managedDescendantID = atspi.ObjectID{}, atspi.ObjectID{} state.managedContainerID, state.managedDescendantID = atspi.ObjectID{}, atspi.ObjectID{}
if result.event.Source.Role != "table" && result.event.Source.Role != "table cell" { if result.event.Source.Role != "table" && result.event.Source.Role != "table cell" {
// A late-created editor has no ordinary focus event to reset // A late-created editor has no ordinary focus event to reset
+45
View File
@@ -0,0 +1,45 @@
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"git.stormux.org/storm/cthulhu/internal/atspi"
"git.stormux.org/storm/cthulhu/internal/review"
)
// The event loop retains classification, not authority, alongside its current
// snapshot. Every replacement clears it, including a native update of the same
// object. Only complete owned document ancestry can opt Where Am I into export.
type currentSpeechSource struct {
origin speechOrigin
focus atspi.ObjectID
epoch uint64
}
func (state *eventLoopState) setCurrent(snapshot atspi.Snapshot) {
state.current = snapshot
state.currentSpeech = currentSpeechSource{}
}
func (state *eventLoopState) setCurrentDocument(result review.Result) {
state.setCurrent(result.DocumentPosition.Snapshot)
sources := result.DocumentCaretContext
if result.Err != nil || !state.current.ID().Valid() || len(sources) == 0 || sources[0].ID() != state.current.ID() {
return
}
origin := review_speech_origin(sources)
if source := accessibility_speech_origin(state.current); source != speechOriginLocal {
origin = source
}
state.currentSpeech = currentSpeechSource{origin: origin, focus: result.DocumentFocus, epoch: result.DocumentEpoch}
}
func (handler *eventReviewHandler) currentSpeechOrigin() speechOrigin {
source := handler.state.currentSpeech
if handler.objectReview == nil || !source.focus.Valid() || source.focus != handler.objectReview.Focus() ||
!handler.objectReview.CurrentDocumentResult(review.Result{DocumentEpoch: source.epoch}) {
return speechOriginUnknown
}
return source.origin
}
+3 -3
View File
@@ -35,7 +35,7 @@ func (handler *accessibilityEventHandler) handleCaret(event atspi.Event, activit
if !state.focusedID.Valid() { if !state.focusedID.Valid() {
state.owner.follow(event) state.owner.follow(event)
state.focusedID = event.Source.ID() state.focusedID = event.Source.ID()
state.current = event.Source state.setCurrent(event.Source)
} }
} }
if event.Source.ReadOnlyCombo.Valid() && event.Source.ReadOnlyCombo == state.focusedID && if event.Source.ReadOnlyCombo.Valid() && event.Source.ReadOnlyCombo == state.focusedID &&
@@ -64,7 +64,7 @@ func (handler *accessibilityEventHandler) handleCaret(event atspi.Event, activit
return true return true
} }
if browser != nil && browser.ready && !event.Source.Editable { if browser != nil && browser.ready && !event.Source.Editable {
state.current = event.Source state.setCurrent(event.Source)
if objectReview != nil { if objectReview != nil {
objectReview.SyncFocus(event.Source.ID()) objectReview.SyncFocus(event.Source.ID())
if err := objectReview.SyncDocumentCaret(event.Source.ID(), event.Detail1); err != nil { if err := objectReview.SyncDocumentCaret(event.Source.ID(), event.Detail1); err != nil {
@@ -98,7 +98,7 @@ func (handler *accessibilityEventHandler) handleCaret(event atspi.Event, activit
// document; focus mode still assembles visual lines across objects. // document; focus mode still assembles visual lines across objects.
if browser != nil && browser.ready && browser.mode == review.FocusMode && if browser != nil && browser.ready && browser.mode == review.FocusMode &&
intent == input.LineIntent && !event.Source.Editable && objectReview != nil { intent == input.LineIntent && !event.Source.Editable && objectReview != nil {
state.current = event.Source state.setCurrent(event.Source)
objectReview.SyncFocus(event.Source.ID()) objectReview.SyncFocus(event.Source.ID())
if request, err := objectReview.ReadDocumentLine(event.Source.ID(), event.Detail1); err == nil { if request, err := objectReview.ReadDocumentLine(event.Source.ID(), event.Detail1); err == nil {
state.documentLineFocus = event.Source.ID() state.documentLineFocus = event.Source.ID()
+3 -3
View File
@@ -117,13 +117,13 @@ func (handler *accessibilityEventHandler) acceptFocusedEvent(event atspi.Event,
// this relationship; no extra mutable ownership state is retained. // this relationship; no extra mutable ownership state is retained.
if belongsToFocusedEditor(event.Source, state.focusedID) && if belongsToFocusedEditor(event.Source, state.focusedID) &&
(isTextChangeEvent(event.Type) || event.Type == atspi.CaretEvent || event.Type == atspi.TextSelectionChangedEvent) { (isTextChangeEvent(event.Type) || event.Type == atspi.CaretEvent || event.Type == atspi.TextSelectionChangedEvent) {
state.current = event.Source state.setCurrent(event.Source)
} }
if isFocusEvent(event.Type) || isManagedDescendantEvent(event.Type) || if isFocusEvent(event.Type) || isManagedDescendantEvent(event.Type) ||
event.Type == atspi.AccessibleNameEvent && presentsReusedRow(event.Source) && event.Type == atspi.AccessibleNameEvent && presentsReusedRow(event.Source) &&
event.Source.ID() == state.managedDescendantID || event.Source.ID() == state.managedDescendantID ||
(event.Source.Path != "" && event.Source.ID() == state.focusedID) { (event.Source.Path != "" && event.Source.ID() == state.focusedID) {
state.current = event.Source state.setCurrent(event.Source)
} }
if isTextChangeEvent(event.Type) { if isTextChangeEvent(event.Type) {
if !speaksTextChanges(event.Source) && !event.Source.LiveRegion { if !speaksTextChanges(event.Source) && !event.Source.LiveRegion {
@@ -135,7 +135,7 @@ func (handler *accessibilityEventHandler) acceptFocusedEvent(event atspi.Event,
} }
} }
if event.Type == atspi.CaretEvent && browser != nil && browser.ready && !event.Source.Editable { if event.Type == atspi.CaretEvent && browser != nil && browser.ready && !event.Source.Editable {
state.current = event.Source state.setCurrent(event.Source)
} }
if brailleOutput != nil && (isFocusGainEvent(event) || if brailleOutput != nil && (isFocusGainEvent(event) ||
isManagedDescendantEvent(event.Type) || event.Type == atspi.CaretEvent || isManagedDescendantEvent(event.Type) || event.Type == atspi.CaretEvent ||
+1 -1
View File
@@ -72,7 +72,7 @@ func (handler *accessibilityEventHandler) acceptFocusOwnership(event atspi.Event
state.pending.clear() state.pending.clear()
focusGate.SetSuspended(true) focusGate.SetSuspended(true)
state.focusedID = event.Source.ID() state.focusedID = event.Source.ID()
state.current = event.Source state.setCurrent(event.Source)
if objectReview != nil { if objectReview != nil {
objectReview.SyncFocus(event.Source.ID()) objectReview.SyncFocus(event.Source.ID())
} }
+2 -2
View File
@@ -85,7 +85,7 @@ func (handler *accessibilityEventHandler) handleLifecycle(event atspi.Event) boo
state.lastSpokenPath, state.lastSpokenText, state.lastSound = "", "", "" state.lastSpokenPath, state.lastSpokenText, state.lastSound = "", "", ""
} }
state.owner.follow(event) state.owner.follow(event)
state.current = event.Source state.setCurrent(event.Source)
state.focusedID = event.Source.ID() state.focusedID = event.Source.ID()
if event.Type == atspi.BusyStateEvent && state.loadedDocument != event.Source.ID() { if event.Type == atspi.BusyStateEvent && state.loadedDocument != event.Source.ID() {
speech.Enqueue(speechRequest{Text: finishedLoadingText(event.Source.Name), Role: "system", Cause: "document-loaded", Append: true}) speech.Enqueue(speechRequest{Text: finishedLoadingText(event.Source.Name), Role: "system", Cause: "document-loaded", Append: true})
@@ -126,7 +126,7 @@ func (handler *accessibilityEventHandler) handleLifecycle(event atspi.Event) boo
state.chromeLoadedDocument = atspi.ObjectID{} state.chromeLoadedDocument = atspi.ObjectID{}
state.chromeLoadedWindow = atspi.ObjectID{} state.chromeLoadedWindow = atspi.ObjectID{}
state.owner.follow(event) state.owner.follow(event)
state.current = event.Source state.setCurrent(event.Source)
state.focusedID = event.Source.ID() state.focusedID = event.Source.ID()
browser.loaded(event.Source.ID()) browser.loaded(event.Source.ID())
if objectReview != nil { if objectReview != nil {
+9 -4
View File
@@ -33,10 +33,13 @@ type eventReviewHandler struct {
} }
func (handler *eventReviewHandler) whereAmI() { func (handler *eventReviewHandler) whereAmI() {
if handler.focusGate.Locked() { if handler.focusGate.Locked() || handler.focusGate.Suspended() {
return return
} }
handler.syncForeground() handler.syncForeground()
if handler.focusGate.Locked() || handler.focusGate.Suspended() {
return
}
handler.refreshPresentation() handler.refreshPresentation()
state := handler.state state := handler.state
state.pending.terminalFocus.clear() state.pending.terminalFocus.clear()
@@ -52,11 +55,13 @@ func (handler *eventReviewHandler) whereAmI() {
} }
text := spoken.Text text := spoken.Text
role := state.current.Role role := state.current.Role
origin := handler.currentSpeechOrigin()
if text == "" { if text == "" {
text = "Could not find current location." text = "Could not find current location."
role = "system" role = "system"
origin = speechOriginStatus
} }
handler.speech.Enqueue(speechRequest{Text: text, Chunks: spoken.WithText(text).Chunks, Role: role, Cause: "where-am-i"}) handler.speech.Enqueue(speechRequest{origin: origin, Text: text, Chunks: spoken.WithText(text).Chunks, Role: role, Cause: "where-am-i"})
handler.logger.Debug("queued where am I presentation", "role", role) handler.logger.Debug("queued where am I presentation", "role", role)
} }
@@ -90,7 +95,7 @@ func (handler *eventReviewHandler) handle(result review.Result) {
} }
if result.ReadAllPosition { if result.ReadAllPosition {
if objectReview.CurrentDocumentResult(result) && result.DocumentFocus == objectReview.Focus() { if objectReview.CurrentDocumentResult(result) && result.DocumentFocus == objectReview.Focus() {
state.current = result.DocumentPosition.Snapshot state.setCurrentDocument(result)
state.focusedID = state.current.ID() state.focusedID = state.current.ID()
} }
return return
@@ -173,7 +178,7 @@ func (handler *eventReviewHandler) handle(result review.Result) {
if result.DocumentCaretCommand != review.CaretReadLine || result.DocumentBrailleRoute { if result.DocumentCaretCommand != review.CaretReadLine || result.DocumentBrailleRoute {
state.documentSelection = textSelection{} state.documentSelection = textSelection{}
} }
state.current = result.DocumentPosition.Snapshot state.setCurrentDocument(result)
if brailleOutput != nil { if brailleOutput != nil {
state.brailleSnapshot = state.current state.brailleSnapshot = state.current
brailleOptions := config.DefaultSettings().Braille brailleOptions := config.DefaultSettings().Braille
+2 -1
View File
@@ -47,6 +47,7 @@ type eventForegroundState struct {
type eventFocusState struct { type eventFocusState struct {
libreOfficeSelection libreOfficeSelectionState libreOfficeSelection libreOfficeSelectionState
current atspi.Snapshot current atspi.Snapshot
currentSpeech currentSpeechSource
brailleSnapshot atspi.Snapshot brailleSnapshot atspi.Snapshot
focusedID atspi.ObjectID focusedID atspi.ObjectID
managedContainerID atspi.ObjectID managedContainerID atspi.ObjectID
@@ -165,7 +166,7 @@ func (state *eventLoopState) clearObjectFocus(brailleOutput brailleFrameEnqueuer
state.tabFocusWindow, state.tabFocusSequence = atspi.ObjectID{}, 0 state.tabFocusWindow, state.tabFocusSequence = atspi.ObjectID{}, 0
state.managedContainerID = atspi.ObjectID{} state.managedContainerID = atspi.ObjectID{}
state.managedDescendantID = atspi.ObjectID{} state.managedDescendantID = atspi.ObjectID{}
state.current = atspi.Snapshot{} state.setCurrent(atspi.Snapshot{})
state.focusCaretID, state.focusCaretSequence = atspi.ObjectID{}, 0 state.focusCaretID, state.focusCaretSequence = atspi.ObjectID{}, 0
state.lastSpokenPath, state.lastSpokenText, state.lastSound = "", "", "" state.lastSpokenPath, state.lastSpokenText, state.lastSound = "", "", ""
state.lastLiveText, state.lastLiveAt = "", time.Time{} state.lastLiveText, state.lastLiveAt = "", time.Time{}
+1 -1
View File
@@ -96,7 +96,7 @@ func (handler *accessibilityEventHandler) handle_progress_bar(event atspi.Event)
} }
// Like NVDA, refresh braille only if this is already the displayed object. // Like NVDA, refresh braille only if this is already the displayed object.
if event.Source.ID() == state.current.ID() { if event.Source.ID() == state.current.ID() {
state.current = event.Source state.setCurrent(event.Source)
state.brailleSnapshot = event.Source state.brailleSnapshot = event.Source
if handler.brailleOutput != nil { if handler.brailleOutput != nil {
handler.brailleOutput.EnqueueFrame(brailleFocusFrameWithOptions(event.Source, settings.Braille, settings.TextAttributes)) handler.brailleOutput.EnqueueFrame(brailleFocusFrameWithOptions(event.Source, settings.Braille, settings.TextAttributes))
@@ -0,0 +1,149 @@
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"log/slog"
"strings"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/atspi"
"git.stormux.org/storm/cthulhu/internal/input"
"git.stormux.org/storm/cthulhu/internal/presentation"
"git.stormux.org/storm/cthulhu/internal/review"
)
func where_am_i_observation_handler(t *testing.T, privacy string) (*eventReviewHandler, *fakeSpeechQueue) {
t.Helper()
// Use owned-tree ancestry, as document movement does, rather than treating
// the presentation's filtered SpeechContext as a complete source capture.
position := review_observation_result(t, "object", privacy).Position
focus := atspi.NewObjectID(":1.42", "/document")
worker, err := review.NewWorker(&fakeAdapter{}, time.Second)
if err != nil {
t.Fatal(err)
}
worker.SyncFocus(focus)
queue := &fakeSpeechQueue{requests: make(chan speechRequest, 4), spoken: make(chan string, 4)}
handler := &eventReviewHandler{state: &eventLoopState{}, objectReview: worker, speech: queue,
logger: slog.New(slog.DiscardHandler), focusGate: &focusGate{}, syncForeground: func() {}}
handler.state.current = atspi.Snapshot{BusName: focus.BusName, Path: focus.Path, Role: "document web"}
handler.state.presentationOptions = presentation.DefaultOptions()
handler.handle(review.Result{Moved: true, DocumentCaretCommand: review.CaretNextLine, DocumentFocus: focus,
DocumentPosition: review.DocumentPosition{Snapshot: position.Current}, DocumentCaretContext: position.SourceContext,
DocumentCaretText: position.Current.Name})
<-queue.requests
<-queue.spoken
return handler, queue
}
func TestRemoteWhereAmIInvalidatesSource(t *testing.T) {
for _, change := range []string{"same-object-update", "say-all-position", "focus", "epoch", "foreground", "missing-owner", "no-location"} {
t.Run(change, func(t *testing.T) {
handler, queue := where_am_i_observation_handler(t, "ordinary")
switch change {
case "same-object-update":
// Even identical native text cannot reuse older capture ancestry.
events := accessibilityEventHandler{state: handler.state, objectReview: handler.objectReview, logger: handler.logger}
events.acceptFocusedEvent(atspi.NewEvent(atspi.FocusEvent, handler.state.current), input.Activity{})
case "say-all-position":
handler.handle(review.Result{ReadAllPosition: true, DocumentFocus: handler.objectReview.Focus(),
DocumentPosition: review.DocumentPosition{Snapshot: handler.state.current}})
case "focus":
handler.objectReview.SyncFocus(atspi.NewObjectID(":1.42", "/other"))
case "epoch":
handler.objectReview.ResetDocument(handler.objectReview.Focus())
case "foreground":
handler.syncForeground = func() { handler.state.setCurrent(handler.state.current) }
case "missing-owner":
handler.objectReview = nil
case "no-location":
handler.state.current.Name, handler.state.current.Role = "", ""
}
handler.whereAmI()
request := <-queue.requests
if exportable_speech(request) {
t.Fatal("Where Am I reused retired source classification")
}
if change != "no-location" && !strings.Contains(request.Text, "Review colour.") {
t.Fatal("source invalidation changed local reading", request.Text)
}
})
}
}
func TestRemoteWhereAmIRejectsMismatchedSource(t *testing.T) {
handler, queue := where_am_i_observation_handler(t, "ordinary")
current := handler.state.current
handler.state.setCurrentDocument(review.Result{DocumentFocus: handler.objectReview.Focus(),
DocumentPosition: review.DocumentPosition{Snapshot: current},
DocumentCaretContext: []atspi.Snapshot{{BusName: current.BusName, Path: "/other", Role: "static"}}})
handler.whereAmI()
if exportable_speech(<-queue.requests) {
t.Fatal("unrelated ancestry authorized Where Am I export")
}
}
func TestRemoteWhereAmISecureFocus(t *testing.T) {
for _, mode := range []string{"lock", "handoff", "foreground-handoff"} {
handler, queue := where_am_i_observation_handler(t, "ordinary")
if mode == "lock" {
handler.focusGate.SetLocked(true, nil, nil, nil, nil, &brailleWorker{})
} else if mode == "foreground-handoff" {
handler.syncForeground = func() { handler.focusGate.SetSuspended(true) }
} else {
handler.focusGate.SetSuspended(true)
}
handler.whereAmI()
if len(queue.requests) != 0 {
t.Fatal("Where Am I spoke during lock or handoff")
}
}
}
func TestRemoteWhereAmIResolvedOutput(t *testing.T) {
worker, session, _, _ := observation_fixture(t)
worker.textTransform = func(text string) string { return strings.ReplaceAll(text, "colour", "color") }
for _, privacy := range []string{"ordinary", "snapshot-ancestor", "missing", "stale"} {
request := where_am_i_observation_request(t, privacy)
if err := worker.EnqueueAndWait(t.Context(), request); err != nil {
t.Fatal(err)
}
for len(worker.speaker.(*observationSpeaker).spoken) > 0 {
want := <-worker.speaker.(*observationSpeaker).spoken
if privacy == "ordinary" {
value := observed_speech(t, session)
if value.Kind != "speak" || len(value.Parts) != 2 || value.Parts[0].Text != want || !value.Parts[1].End {
t.Fatal("Where Am I export differed from resolved local chunks", value, want)
}
}
}
worker.InterruptForUser()
if value := observed_speech(t, session); value.Kind != "cancel" {
t.Fatal("private Where Am I reached the child", value)
}
}
}
func where_am_i_observation_request(t *testing.T, privacy string) speechRequest {
t.Helper()
handler, queue := where_am_i_observation_handler(t, privacy)
handler.whereAmI()
return <-queue.requests
}
func TestRemoteWhereAmISourcePrivacy(t *testing.T) {
for _, privacy := range []string{"ordinary", "password", "ancestor", "snapshot-ancestor", "unknown", "unknown-role", "invalid", "defunct", "unknown-ancestor", "unknown-snapshot-ancestor", "missing", "stale"} {
t.Run(privacy, func(t *testing.T) {
request := where_am_i_observation_request(t, privacy)
if exportable_speech(request) != (privacy == "ordinary") {
t.Fatalf("privacy %s produced exportable=%v", privacy, exportable_speech(request))
}
if privacy != "password" && !strings.Contains(request.Text, "Review colour.") {
t.Fatal("source privacy changed local Where Am I", request.Text)
}
})
}
}
@@ -580,6 +580,11 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
} }
} }
} }
for _, privacy := range []string{"ordinary", "snapshot-ancestor", "missing", "stale"} {
if err := worker.EnqueueAndWait(t.Context(), where_am_i_observation_request(t, privacy)); err != nil {
t.Fatal(err)
}
}
worker.InterruptForUser() worker.InterruptForUser()
var received strings.Builder var received strings.Builder
ends, frames := 0, 0 ends, frames := 0, 0
@@ -615,7 +620,7 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
t.Fatal("reader output did not reach the relay in order") t.Fatal("reader output did not reach the relay in order")
} }
} }
if received.String() != "color "+remoteText+"First color.Second color.Current color.Review color.staticFlat color. Review color." || frames < 8 || ends != 7 { if received.String() != "color "+remoteText+"First color.Second color.Current color.Review color.staticFlat color. Review color.Review color.static" || frames < 10 || ends != 9 {
t.Fatalf("observation leaked private text or lost resolved output/boundaries: frames=%d ends=%d suffix=%q", frames, ends, received.String()[max(0, received.Len()-160):]) t.Fatalf("observation leaked private text or lost resolved output/boundaries: frames=%d ends=%d suffix=%q", frames, ends, received.String()[max(0, received.Len()-160):])
} }
} }
+1 -1
View File
@@ -91,7 +91,7 @@ func (c *wineReviewFocusController) changed() {
c.worker.SyncFocus(selected.ID()) c.worker.SyncFocus(selected.ID())
} }
c.active, c.epoch = selected, fg.Epoch c.active, c.epoch = selected, fg.Epoch
c.state.current = atspi.Snapshot{BusName: selected.Sender, Path: selected.ID().Path, Role: "text"} c.state.setCurrent(atspi.Snapshot{BusName: selected.Sender, Path: selected.ID().Path, Role: "text"})
if selected.Protected { if selected.Protected {
c.state.current.Role = "password text" c.state.current.Role = "password text"
} }