Progressbar fix.

This commit is contained in:
Storm Dragon committed 2026-10-14 18:22:59 -04:00
1 parent 6754c8ef6c
commit 9205e7280a
10 files changed
+266 -3

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
# Progress reporting
Progress bars use the configured Off, Beep, Speak, or Both output mode and
percentage thresholds. Background reporting is optional. Updates do not take
focus or replace Braille for an unrelated object.
Native HTML `<progress>` and ARIA `progressbar` controls use their AT-SPI
progress role and value events. Steam's shader dialog instead exposes changing
static text such as `Processing Vulkan shaders (37%)`. The Steam webscript
recognizes that English status under the verified `steamwebhelper` process and
passes its percentage to the shared progress handler. Other text, containing
sections, and ordinary browser processes do not use this exception. Translated
Steam shader messages and other custom text-only progress widgets are not
covered by this recognition.
## Automated checks
```sh
GOCACHE=/tmp/cthulhu-go-cache go test ./internal/app ./internal/webscripts ./internal/presentation ./internal/sound -count=1
python3 tests/native_progress.py
CTHULHU_NATIVE_BROWSER_TESTS=progress python3 tests/native_browser.py
```
The isolated GTK test checks foreground, background, and hidden progress. The
isolated browser test drives real HTML and ARIA controls in a Chromium-family
browser and verifies the resulting tone requests. Unit tests cover Steam text
recognition, output modes, duplicate suppression, percentage thresholds,
security, stale events, background settings, and unrelated Braille ownership.
These checks do not establish audible output from the running reader.
## Live review before submission
Rebuild and restart the reader, then launch a Steam game that needs shader
processing. With Beep selected and Steam in the foreground, confirm that pitch
rises as the percentage increases without navigating onto the status text.
With Speak or Both selected, verify the configured speech threshold. With Off
selected, verify that automatic progress output stops.
Switch to another application during processing: reporting should follow the
background-progress setting without moving focus or replacing that application's
Braille. Check ordinary Steam navigation and notifications afterward. Open
`tests/browser-progress.html` in a browser and activate Update progress to check
native and ARIA bars. Physical keyboard, listening, and Braille checks remain
separate acceptance gates.
+1
View File
@@ -5,6 +5,7 @@ Cthulhu is a Linux screen reader written in Go.
- [Repository Guidelines](AGENTS.md): layout, commands, and working safeguards.
- [Contributing](CONTRIBUTING.md): development, accessibility verification, licensing, and the AI-assisted contribution policy.
- [LibreOffice support](LIBREOFFICE_SUPPORT.md): integration boundaries, initial Calc behavior, remaining work, and verification.
- [Progress reporting](PROGRESS_BARS.md): web progress bars, Steam shader processing, and verification.
AI-assisted contributions are welcome. Contributors remain responsible for human review and testing before submission; see the [full policy](CONTRIBUTING.md#ai-assisted-contributions).
+11 -3
View File
@@ -12,6 +12,7 @@ import (
"git.stormux.org/storm/cthulhu/internal/config"
"git.stormux.org/storm/cthulhu/internal/presentation"
"git.stormux.org/storm/cthulhu/internal/sound"
"git.stormux.org/storm/cthulhu/internal/webscripts"
)
type ProgressBarEntry struct {
@@ -64,7 +65,8 @@ func (history *ProgressBarHistory) report(id atspi.ObjectID, percentage float64,
// Handle progress before ordinary focused-object filtering. This path never
// changes window ownership, native focus, review or browser mode.
func (handler *accessibilityEventHandler) handle_progress_bar(event atspi.Event) bool {
if !isValueChangeEvent(event.Type) || event.Source.Role != "progress bar" {
percentage, shaderProgress := webscripts.ShaderProgress(event.Source.ApplicationProcess, event.Type, event.Source.Role, event.Source.Name)
if !shaderProgress && (!isValueChangeEvent(event.Type) || event.Source.Role != "progress bar") {
return false
}
settings := config.DefaultSettings()
@@ -75,10 +77,16 @@ func (handler *accessibilityEventHandler) handle_progress_bar(event atspi.Event)
// NVDA falls back to ordinary focused-control value reporting here.
return false
}
percentage, valid := presentation.ProgressPercentage(event.Source)
if !valid {
if shaderProgress && (event.Source.Defunct || event.Source.Indeterminate) {
return false
}
if !shaderProgress {
var valid bool
percentage, valid = presentation.ProgressPercentage(event.Source)
if !valid {
return false
}
}
state := handler.state
if handler.foreground != nil && event.Foreground.Epoch != state.foregroundState.Epoch {
return true
+68
View File
@@ -6,6 +6,7 @@ package app
import (
"context"
"fmt"
"log/slog"
"testing"
@@ -15,6 +16,73 @@ import (
"git.stormux.org/storm/cthulhu/internal/input"
)
func TestSteamShaderProgressUsesSharedOutputAndGates(t *testing.T) {
for _, step := range []struct {
name, mode string
background, reportBackground bool
hidden, defunct, locked, stale bool
wantHandled, wantBeep, wantSpeech bool
}{
{name: "beep", mode: "beep", wantHandled: true, wantBeep: true},
{name: "speak", mode: "speak", wantHandled: true, wantSpeech: true},
{name: "both", mode: "both", wantHandled: true, wantBeep: true, wantSpeech: true},
{name: "off", mode: "off"},
{name: "background suppressed", mode: "both", background: true, wantHandled: true},
{name: "background enabled", mode: "both", background: true, reportBackground: true, wantHandled: true, wantBeep: true, wantSpeech: true},
{name: "hidden", mode: "both", hidden: true},
{name: "defunct", mode: "both", defunct: true},
{name: "secure", mode: "both", locked: true, wantHandled: true},
{name: "stale", mode: "both", stale: true, wantHandled: true},
} {
t.Run(step.name, func(t *testing.T) {
settings := config.DefaultSettings()
settings.ProgressBar.OutputMode = step.mode
settings.ProgressBar.ReportBackground = step.reportBackground
live := &liveSettings{}
live.Store(settings)
speech := &fakeSpeechQueue{spoken: make(chan string, 8), requests: make(chan speechRequest, 8)}
frames := frameRecorder{frames: make(chan braille.Frame, 8)}
state := &eventLoopState{}
state.current = atspi.Snapshot{BusName: ":1.1", Path: "/button", Role: "button"}
before := state.current
gate := &focusGate{}
gate.secure.Store(step.locked)
handler := &accessibilityEventHandler{state: state, live: live, speech: speech, focusGate: gate, brailleOutput: frames}
event := atspi.NewEvent(atspi.AccessibleNameEvent, atspi.Snapshot{BusName: ":1.1", Path: "/shader-status", Role: "static",
ApplicationProcess: "steamwebhelper", StatesKnown: true, Visible: true, Showing: !step.hidden, Defunct: step.defunct})
event.ForegroundMismatch = step.background
if step.stale {
handler.foreground = &titleProbeSource{}
state.foregroundState.Epoch = 1
}
for _, percentage := range []int{18, 18, 19, 100} {
event.Source.Name = fmt.Sprintf("Processing Vulkan shaders (%d%%)", percentage)
if handled := handler.handle_progress_bar(event); handled != step.wantHandled {
t.Fatalf("handled=%t, want %t", handled, step.wantHandled)
}
}
wantCount := 0
if step.wantBeep {
wantCount = 3
} else if step.wantSpeech {
wantCount = 2
}
if len(speech.requests) != wantCount {
t.Fatalf("got %d requests, want %d", len(speech.requests), wantCount)
}
if wantCount > 0 {
request := <-speech.requests
if !request.Append || (len(request.Sound) == 1) != step.wantBeep || (request.Text == "18 percent") != step.wantSpeech {
t.Fatalf("shader output: %+v", request)
}
}
if state.current.ID() != before.ID() || state.focusedID.Valid() || len(frames.frames) != 0 {
t.Fatal("shader progress changed focus or unrelated braille")
}
})
}
}
func TestProgressSecureEpochAndBrailleGates(t *testing.T) {
settings := config.DefaultSettings()
settings.ProgressBar.ReportBackground = true
@@ -0,0 +1,79 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"os"
"os/exec"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/atspi"
)
// Standard web progress must reach the shared handler without Steam policy.
func TestNativeBrowserProgress(t *testing.T) {
if os.Getenv("CTHULHU_NATIVE_BROWSER") != "1" {
t.Skip("requires private browser session")
}
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
adapter := atspi.New()
if err := adapter.Start(ctx); err != nil {
t.Fatal(err)
}
defer adapter.Stop(context.Background())
if output, err := exec.CommandContext(ctx, "xdotool", "key", "--clearmodifiers", "Tab").CombinedOutput(); err != nil {
t.Fatalf("fixture focus: %s, %v", output, err)
}
for {
select {
case event := <-adapter.Events():
if event.Type == atspi.FocusedStateEvent && event.Detail1 == 1 && event.Source.Name == "Update progress" {
goto focused
}
case <-ctx.Done():
t.Fatal("update button focus missing")
}
}
focused:
speech := &fakeSpeechQueue{spoken: make(chan string, 8), requests: make(chan speechRequest, 8)}
handler := &accessibilityEventHandler{state: &eventLoopState{}, speech: speech}
if output, err := exec.CommandContext(ctx, "xdotool", "key", "--clearmodifiers", "space").CombinedOutput(); err != nil {
t.Fatalf("update progress: %s, %v", output, err)
}
seen := make(map[string]bool)
for len(seen) < 2 {
select {
case event := <-adapter.Events():
if !isValueChangeEvent(event.Type) || event.Source.Role != "progress bar" {
continue
}
wantTone := ""
switch event.Source.Name {
case "Native progress":
wantTone = "generated-progress-tone:20"
case "ARIA progress":
wantTone = "generated-progress-tone:30"
default:
continue
}
if seen[event.Source.Name] {
continue
}
if !handler.handle_progress_bar(event) || len(speech.requests) != 1 {
t.Fatalf("web progress was lost: %+v", event)
}
request := <-speech.requests
if len(request.Sound) != 1 || request.Sound[0].Name != wantTone || !request.Append || request.Text != "" {
t.Fatalf("web progress output: %+v", request)
}
seen[event.Source.Name] = true
case <-ctx.Done():
t.Fatalf("web progress events missing; received %v", seen)
}
}
}
+18
View File
@@ -7,6 +7,7 @@ package webscripts
import (
"regexp"
"sort"
"strconv"
"strings"
"time"
)
@@ -15,6 +16,23 @@ const steamNotificationDelay = 500 * time.Millisecond
var steamRelativeTime = regexp.MustCompile(`(?i)^(?:just now|now|\d+\s+(?:second|minute|hour|day|week|month|year)s?\s+ago)$`)
var steamShaderProgress = regexp.MustCompile(`^Processing Vulkan shaders \((\d{1,3})%\)$`)
// ShaderProgress recognizes Steam's English shader status, which Chromium
// exposes as static text rather than an AT-SPI progress bar. Match the leaf
// only: containing sections repeat this text alongside dialog controls.
func ShaderProgress(processName, eventType, role, name string) (float64, bool) {
if MatchProcess(processName) != Steam || eventType != "object:property-change:accessible-name" || role != "static" {
return 0, false
}
match := steamShaderProgress.FindStringSubmatch(strings.TrimSpace(name))
if match == nil {
return 0, false
}
percentage, err := strconv.Atoi(match[1])
return float64(percentage), err == nil && percentage <= 100
}
// NotificationRoot mirrors Python's Steam root selection: notification/alert
// ancestors take precedence over inherited live-region attributes on a leaf.
// Candidates are ordered from the event source towards its application root.
+24
View File
@@ -10,6 +10,30 @@ import (
"time"
)
func TestSteamShaderProgress(t *testing.T) {
for _, step := range []struct {
process, eventType, role, name string
percentage float64
valid bool
}{
{"steamwebhelper", "object:property-change:accessible-name", "static", "Processing Vulkan shaders (0%)", 0, true},
{"steamwebhelper", "object:property-change:accessible-name", "static", "Processing Vulkan shaders (37%)", 37, true},
{"steamwebhelper", "object:property-change:accessible-name", "static", "Processing Vulkan shaders (100%)", 100, true},
{"brave", "object:property-change:accessible-name", "static", "Processing Vulkan shaders (37%)", 0, false},
{"Chromium", "object:property-change:accessible-name", "static", "Processing Vulkan shaders (37%)", 0, false},
{"steamwebhelper", "object:property-change:accessible-name", "section", "Processing Vulkan shaders (37%) SKIP", 0, false},
{"steamwebhelper", "object:state-changed:focused", "static", "Processing Vulkan shaders (37%)", 0, false},
{"steamwebhelper", "object:property-change:accessible-name", "static", "Download (37%)", 0, false},
{"steamwebhelper", "object:property-change:accessible-name", "static", "Processing Vulkan shaders (101%)", 0, false},
{"steamwebhelper", "object:property-change:accessible-name", "static", "Processing Vulkan shaders (-1%)", 0, false},
} {
percentage, valid := ShaderProgress(step.process, step.eventType, step.role, step.name)
if valid != step.valid || valid && percentage != step.percentage {
t.Fatalf("%+v: percentage=%g valid=%t", step, percentage, valid)
}
}
}
func TestSteamNotificationMatchingAndRoot(t *testing.T) {
message := &Notification{BusName: ":1.42", RootPath: "/toast", Text: "Username is now online"}
for _, process := range []string{"Chromium", "chromium", "brave", "", "steamwebhelper.exe"} {
+1
View File
@@ -55,3 +55,4 @@ web-browse-braille-line,src/cthulhu/caret_navigation.py;src/cthulhu/scripts/web/
web-explicit-link-names,src/cthulhu/scripts/web/script_utilities.py:_treatObjectAsWhole,web,Read explicitly named links as whole objects without losing descendant badges or ordinary link navigation,Storm Dragon,internal/atspi internal/review internal/app,in-progress,"go test -race ./...; CTHULHU_NATIVE_BROWSER_TESTS=namedlinks,browser,live,keys python3 tests/native_browser.py",Private Brave/Xorg named-link and key checks passed,,Fixture speech verified; real Steam listening pending,Named-link browse braille uses the full accessible label; physical check pending,docs/testing/steam-browser.md; actual Steam explicit-name exposure and game-card acceptance remain live gates
steam-notifications,src/cthulhu/scripts/apps/steamwebhelper/script.py;tests/test_steam_notification_regressions.py,web,Coalesce verified Steam background toasts and present once through speech braille and notification history,Storm Dragon,internal/webscripts internal/atspi internal/app,in-progress,go test -race ./...; python3 tests/native_webscripts.py,Private GTK/AT-SPI process matching and alert-root capture passed,,Event-loop background delivery and Chromium isolation passed; real Steam toast pending,Event-loop flash verified; physical Steam toast display pending,docs/testing/steam-browser.md; independent root and bus queues deliberately prevent unrelated toast timestamps from merging
libreoffice-calc-basic,Orca src/orca/scripts/apps/soffice/script.py; Orca src/orca/ax_utilities_table.py; Orca src/orca/speech_generator.py,libreoffice,Present Calc cells and recover logical focus around editing and sheet changes,Storm Dragon,internal/app internal/atspi internal/presentation,in-progress,GOCACHE=/tmp/cthulhu-go-cache go test -race ./internal/atspi ./internal/app ./internal/presentation -count=1; /usr/bin/python3 tests/native_libreoffice.py,Private real LibreOffice Calc 26.2.6.3 on Xvfb and a private AT-SPI bus validates cell movement blanks F2 typing Escape Enter and sheet changes.,,Recorded speech assertions pass; audible and physical-input acceptance pending.,Recorded value and coordinate frames pass; physical Braille and editor routing acceptance pending.,Current Orca is the primary Office reference. Dedicated application policy follows verified process identity after foreground/security gates; native capture remains bounded. Formulas range announcements dynamic headers full-row reading and Writer workflows remain open. See LIBREOFFICE_SUPPORT.md.
steam-shader-progress,Steam shader dialog AT-SPI events,web,Report Steam Vulkan shader percentages through shared progress output,,internal/webscripts internal/app,in-progress,go test ./internal/app ./internal/webscripts ./internal/presentation ./internal/sound -count=1; python3 tests/native_progress.py; CTHULHU_NATIVE_BROWSER_TESTS=progress python3 tests/native_browser.py,Private GTK and Brave HTML/ARIA progress checks passed.,,Tone requests verified; live Steam listening pending.,Unrelated Braille ownership covered by unit tests; physical check pending.,English static shader-status name changes only; verified steamwebhelper identity; see PROGRESS_BARS.md.
1 id python_source subsystem behavior owner go_destination status automated_evidence x11_evidence wayland_evidence speech_evidence braille_evidence notes
55 web-explicit-link-names src/cthulhu/scripts/web/script_utilities.py:_treatObjectAsWhole web Read explicitly named links as whole objects without losing descendant badges or ordinary link navigation Storm Dragon internal/atspi internal/review internal/app in-progress go test -race ./...; CTHULHU_NATIVE_BROWSER_TESTS=namedlinks,browser,live,keys python3 tests/native_browser.py Private Brave/Xorg named-link and key checks passed Fixture speech verified; real Steam listening pending Named-link browse braille uses the full accessible label; physical check pending docs/testing/steam-browser.md; actual Steam explicit-name exposure and game-card acceptance remain live gates
56 steam-notifications src/cthulhu/scripts/apps/steamwebhelper/script.py;tests/test_steam_notification_regressions.py web Coalesce verified Steam background toasts and present once through speech braille and notification history Storm Dragon internal/webscripts internal/atspi internal/app in-progress go test -race ./...; python3 tests/native_webscripts.py Private GTK/AT-SPI process matching and alert-root capture passed Event-loop background delivery and Chromium isolation passed; real Steam toast pending Event-loop flash verified; physical Steam toast display pending docs/testing/steam-browser.md; independent root and bus queues deliberately prevent unrelated toast timestamps from merging
57 libreoffice-calc-basic Orca src/orca/scripts/apps/soffice/script.py; Orca src/orca/ax_utilities_table.py; Orca src/orca/speech_generator.py libreoffice Present Calc cells and recover logical focus around editing and sheet changes Storm Dragon internal/app internal/atspi internal/presentation in-progress GOCACHE=/tmp/cthulhu-go-cache go test -race ./internal/atspi ./internal/app ./internal/presentation -count=1; /usr/bin/python3 tests/native_libreoffice.py Private real LibreOffice Calc 26.2.6.3 on Xvfb and a private AT-SPI bus validates cell movement blanks F2 typing Escape Enter and sheet changes. Recorded speech assertions pass; audible and physical-input acceptance pending. Recorded value and coordinate frames pass; physical Braille and editor routing acceptance pending. Current Orca is the primary Office reference. Dedicated application policy follows verified process identity after foreground/security gates; native capture remains bounded. Formulas range announcements dynamic headers full-row reading and Writer workflows remain open. See LIBREOFFICE_SUPPORT.md.
58 steam-shader-progress Steam shader dialog AT-SPI events web Report Steam Vulkan shader percentages through shared progress output internal/webscripts internal/app in-progress go test ./internal/app ./internal/webscripts ./internal/presentation ./internal/sound -count=1; python3 tests/native_progress.py; CTHULHU_NATIVE_BROWSER_TESTS=progress python3 tests/native_browser.py Private GTK and Brave HTML/ARIA progress checks passed. Tone requests verified; live Steam listening pending. Unrelated Braille ownership covered by unit tests; physical check pending. English static shader-status name changes only; verified steamwebhelper identity; see PROGRESS_BARS.md.
+19
View File
@@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Cthulhu Browser Probe</title>
<label>Probe start <input autofocus></label>
<button id="update">Update progress</button>
<label for="native">Native progress</label>
<progress id="native" max="100" value="0"></progress>
<div id="aria" role="progressbar" aria-label="ARIA progress"
aria-valuemin="0" aria-valuemax="100" aria-valuenow="10"></div>
<span id="shader">Processing Vulkan shaders (0%)</span>
<script>
document.getElementById('update').addEventListener('click', () => {
document.getElementById('native').value = 20;
document.getElementById('aria').setAttribute('aria-valuenow', '30');
document.getElementById('shader').textContent = 'Processing Vulkan shaders (37%)';
});
</script>
</html>
+1
View File
@@ -10,6 +10,7 @@ import time
BROWSER_PROOFS = {
"progress": ("./internal/app", "^TestNativeBrowserProgress$", 30, 35, "browser-progress.html"),
"fragments": ("./internal/review", "^TestNativeBrowserFragmentLines$", 30, 35, "browser-fragments.html"),
"positions": ("./internal/atspi", "^TestNativeBrowserSetPositions$", 25, 30, "browser-positions.html"),
"tabs": ("./internal/app", "^TestNativeBrowserTabs$", 30, 35),