Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,187 @@
|
||||
// Cthulhu screen reader.
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/braille"
|
||||
"git.stormux.org/storm/cthulhu/internal/presentation"
|
||||
"git.stormux.org/storm/cthulhu/internal/review"
|
||||
)
|
||||
|
||||
// eventLoopState is owned only by processEventsWithInput's goroutine. Embedded
|
||||
// groups keep each lifetime visible while retaining the existing individual
|
||||
// reset transitions. Workers observe state only through their existing APIs.
|
||||
type eventLoopState struct {
|
||||
eventForegroundState
|
||||
eventFocusState
|
||||
eventDocumentState
|
||||
eventInputState
|
||||
eventPresentationState
|
||||
}
|
||||
|
||||
type eventForegroundState struct {
|
||||
owner windowOwner
|
||||
foregroundState atspi.ForegroundState
|
||||
lastOverflow uint64
|
||||
}
|
||||
|
||||
type eventFocusState struct {
|
||||
current atspi.Snapshot
|
||||
brailleSnapshot atspi.Snapshot
|
||||
focusedID atspi.ObjectID
|
||||
managedContainerID atspi.ObjectID
|
||||
managedDescendantID atspi.ObjectID
|
||||
}
|
||||
|
||||
type eventDocumentState struct {
|
||||
selectedBrowserTab atspi.ObjectID
|
||||
tabFocusWindow atspi.ObjectID
|
||||
tabFocusSequence uint64
|
||||
documentLineFocus atspi.ObjectID
|
||||
documentLineRequest uint64
|
||||
documentLineEvent atspi.Event
|
||||
loadedDocument atspi.ObjectID
|
||||
loadingDocument atspi.ObjectID
|
||||
// A load-complete can verify chrome focus just before a busy:false event
|
||||
// temporarily loses its native focused state. Keep that document from
|
||||
// taking browse keys until focus actually enters it.
|
||||
chromeLoadedDocument atspi.ObjectID
|
||||
chromeLoadedWindow atspi.ObjectID
|
||||
}
|
||||
|
||||
type eventInputState struct {
|
||||
lastAutocompletePopupItem atspi.ObjectID
|
||||
focusCaretID atspi.ObjectID
|
||||
focusCaretSequence uint64
|
||||
suppressNextCaretPath string
|
||||
selectionCaretID atspi.ObjectID
|
||||
selectionCaretSequence uint64
|
||||
selectionCaretAt time.Time
|
||||
autocompleteTyping autocompleteEcho
|
||||
autocompleteAvailable autocompleteAvailability
|
||||
typingPath string
|
||||
typingText string
|
||||
selection textSelection
|
||||
documentSelection textSelection
|
||||
lastClipboardSequence uint64
|
||||
lastPasteSequence uint64
|
||||
lastPasteID atspi.ObjectID
|
||||
lastPasteAt time.Time
|
||||
}
|
||||
|
||||
type eventPresentationState struct {
|
||||
presentationOptions presentation.Options
|
||||
pending pendingPresentations
|
||||
lastSpokenPath string
|
||||
lastSpokenText string
|
||||
lastSound string
|
||||
lastLiveText string
|
||||
lastLiveAt time.Time
|
||||
comboHistory comboPresentation
|
||||
// Retain this identity after the history window closes so delayed GTK
|
||||
// events cannot enter speech history.
|
||||
historyWindow atspi.ObjectID
|
||||
lastFileManagerStatus string
|
||||
lastFileManagerStatusWindow atspi.ObjectID
|
||||
pendingPromptWindow atspi.ObjectID
|
||||
pendingPromptTextAnnounced bool
|
||||
tableDescriptionWindow atspi.ObjectID
|
||||
tableDescriptionText string
|
||||
tableDescriptionAt time.Time
|
||||
lastTable atspi.ObjectID
|
||||
lastTableCell atspi.ObjectID
|
||||
lastTableRow int
|
||||
lastTableColumn int
|
||||
lastTableSequence uint64
|
||||
lastTableAt time.Time
|
||||
lastTableFullRow bool
|
||||
lastTableText string
|
||||
progressBars ProgressBarHistory
|
||||
}
|
||||
|
||||
func (state *eventLoopState) ownedAutocompletePopup(event atspi.Event) bool {
|
||||
return event.Source.AutocompletePopup && event.PopupFor.Valid() &&
|
||||
state.current.Autocomplete && state.current.ID() == event.PopupFor &&
|
||||
state.owner.focus == event.PopupFor && state.owner.window.Valid() &&
|
||||
event.Window.Valid() && event.Window != state.owner.window &&
|
||||
event.Window.BusName == state.owner.applicationBus &&
|
||||
event.Source.BusName == state.owner.applicationBus
|
||||
}
|
||||
|
||||
// clearOwnedFocus retires only the state cleared by the former event-loop
|
||||
// closure. Session presentation history and chrome load verification survive.
|
||||
func (state *eventLoopState) clearOwnedFocus(brailleOutput brailleFrameEnqueuer, browser *browserModeController, objectReview *review.Worker, speech speechEnqueuer) {
|
||||
state.selection = textSelection{}
|
||||
state.documentSelection = textSelection{}
|
||||
state.lastClipboardSequence = 0
|
||||
state.lastPasteSequence = 0
|
||||
state.lastPasteID = atspi.ObjectID{}
|
||||
state.lastPasteAt = time.Time{}
|
||||
state.selectionCaretID = atspi.ObjectID{}
|
||||
state.brailleSnapshot = atspi.Snapshot{}
|
||||
if brailleOutput != nil {
|
||||
brailleOutput.EnqueueFrame(braille.Frame{Cursor: -1})
|
||||
}
|
||||
state.pending.clear()
|
||||
state.pendingPromptWindow = atspi.ObjectID{}
|
||||
state.pendingPromptTextAnnounced = false
|
||||
state.focusedID = atspi.ObjectID{}
|
||||
state.selectedBrowserTab = atspi.ObjectID{}
|
||||
state.tabFocusWindow, state.tabFocusSequence = atspi.ObjectID{}, 0
|
||||
state.managedContainerID = atspi.ObjectID{}
|
||||
state.managedDescendantID = atspi.ObjectID{}
|
||||
state.current = atspi.Snapshot{}
|
||||
state.focusCaretID, state.focusCaretSequence = atspi.ObjectID{}, 0
|
||||
state.lastSpokenPath, state.lastSpokenText, state.lastSound = "", "", ""
|
||||
state.lastLiveText, state.lastLiveAt = "", time.Time{}
|
||||
state.comboHistory = comboPresentation{}
|
||||
state.typingPath, state.typingText, state.suppressNextCaretPath = "", "", ""
|
||||
state.lastAutocompletePopupItem = atspi.ObjectID{}
|
||||
state.autocompleteTyping = autocompleteEcho{}
|
||||
state.autocompleteAvailable = autocompleteAvailability{}
|
||||
state.documentLineFocus = atspi.ObjectID{}
|
||||
state.documentLineRequest = 0
|
||||
state.documentLineEvent = atspi.Event{}
|
||||
state.loadedDocument = atspi.ObjectID{}
|
||||
state.loadingDocument = atspi.ObjectID{}
|
||||
if browser != nil {
|
||||
browser.clear()
|
||||
}
|
||||
if objectReview != nil {
|
||||
objectReview.ClearFocus()
|
||||
}
|
||||
speech.Interrupt()
|
||||
}
|
||||
|
||||
func (state *eventLoopState) syncForeground(foreground foregroundMonitor, logger *slog.Logger, clearOwnedFocus func()) {
|
||||
if foreground == nil {
|
||||
return
|
||||
}
|
||||
latest := foreground.Foreground()
|
||||
if latest.Epoch != state.foregroundState.Epoch {
|
||||
state.owner.clear()
|
||||
clearOwnedFocus()
|
||||
logger.Debug("X11 foreground ownership changed", "x_window", latest.Client.Window,
|
||||
"pid", latest.Client.PID, "known", latest.Client.Known, "epoch", latest.Epoch)
|
||||
}
|
||||
state.foregroundState = latest
|
||||
}
|
||||
Reference in new issue
Block a user