Avoid repeating Mumble server labels across columns

Collapse identical full-row labels only for Mumble while preserving mixed column data and repeated values from other providers. Add Qt native coverage and document live saved-server listening confirmation.
This commit is contained in:
Storm Dragon committed 2026-11-07 20:12:25 -05:00
1 parent 64440fbb78
commit 3dbd7f2554
8 files changed
+162 -1

No files matched your search

@@ -318,6 +318,7 @@ func (adapter *Adapter) enrichNativeWindowEvent(event *C.AtspiEvent, target *C.A
snapshot.TablePositionKnown = rowIndex >= 0 && columnIndex >= 0
snapshot.TableRowText = sanitizeSnapshotField(C.GoString(rowText))
snapshot.GUITable = guiTable != 0
snapshot.TableRowText = mumbleTableRowText(snapshot)
C.g_free(C.gpointer(tableName))
C.g_free(C.gpointer(tablePath))
C.g_free(C.gpointer(rowText))
+13
View File
@@ -11,6 +11,19 @@ import (
var mumbleTimestamp = regexp.MustCompile(`^\[\d{2}:\d{2}:\d{2}\]\s*`)
// Mumble supplies the same accessible label for every server-list column,
// including Ping and Users. Collapse only a whole row of identical labels;
// ordinary equal-valued columns and rows containing other data retain them.
func mumbleTableRowText(snapshot Snapshot) string {
if snapshot.ApplicationProcess == "mumble" && snapshot.GUITable &&
snapshot.Role == "table cell" && snapshot.Name != "" &&
snapshot.TableColumnCount > 1 && snapshot.TableColumnCount <= 32 &&
snapshot.TableRowText == snapshot.Name+strings.Repeat(", "+snapshot.Name, snapshot.TableColumnCount-1) {
return snapshot.Name
}
return snapshot.TableRowText
}
// Match legacy Mumble's inserted-message cleanup. Qt can insert the timestamp
// separately, and uses paragraph separators and noncharacters in its log.
func mumbleMessageText(text string) string {
+70
View File
@@ -86,3 +86,73 @@ func TestNativeMumbleTranscriptCapture(t *testing.T) {
})
}
}
func TestNativeMumbleTableRows(t *testing.T) {
if os.Getenv("CTHULHU_NATIVE_MUMBLE") != "1" {
t.Skip("requires private Qt/Xorg/AT-SPI session")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
adapter := New()
if err := adapter.Start(ctx); err != nil {
t.Fatal(err)
}
defer adapter.Stop(context.Background())
for _, process := range []string{"mumble", "table-test"} {
t.Run(process, func(t *testing.T) {
command := exec.CommandContext(ctx, "python3", "../../tests/qt_mumble_table.py", process)
output, err := command.StdoutPipe()
if err != nil {
t.Fatal(err)
}
command.Stderr = os.Stderr
if err := command.Start(); err != nil {
t.Fatal(err)
}
defer func() { command.Process.Kill(); command.Wait() }()
line, err := bufio.NewReader(output).ReadString('\n')
fields := strings.Fields(line)
if err != nil || len(fields) != 2 || fields[0] != "ready" {
t.Fatalf("fixture readiness: %q, %v", line, err)
}
if _, err := adapter.CaptureDesktop(ctx); err != nil {
t.Fatal(err)
}
if out, err := exec.CommandContext(ctx, "xdotool", "windowfocus", "--sync", fields[1]).CombinedOutput(); err != nil {
t.Fatalf("focus: %s, %v", out, err)
}
for rowIndex, label := range []string{"Server Alpha", "Server Beta", "Server Gamma, 界"} {
key := "Down"
if rowIndex == 0 {
key = "Home"
}
if out, err := exec.CommandContext(ctx, "xdotool", "key", key).CombinedOutput(); err != nil {
t.Fatalf("select row: %s, %v", out, err)
}
selected:
for {
select {
case <-ctx.Done():
t.Fatalf("row %d (%s) missing: %v", rowIndex, label, ctx.Err())
case event := <-adapter.Events():
cell := event.Source
if cell.ApplicationProcess != process || cell.Name != label || cell.TableColumnCount != 3 {
continue
}
want := strings.Join([]string{label, label, label}, ", ")
if process == "mumble" {
want = label
}
if rowIndex == 1 {
want = label + ", 12, 12"
}
if cell.TableRowText != want || !cell.GUITable || cell.TableRowIndex != rowIndex {
t.Fatalf("row %d context = %+v, want %q", rowIndex, cell, want)
}
break selected
}
}
}
})
}
}
+25
View File
@@ -6,6 +6,31 @@ package atspi
import "testing"
func TestMumbleTableRowText(t *testing.T) {
for _, test := range []struct {
name, process, label, row, want string
columns int
gui bool
}{
{"server label", "mumble", "Server Example", "Server Example, Server Example, Server Example", "Server Example", 3, true},
{"commas and Unicode", "mumble", "Server Example, 界", "Server Example, 界, Server Example, 界, Server Example, 界", "Server Example, 界", 3, true},
{"useful equal values", "mumble", "Server Example", "Server Example, 12, 12", "Server Example, 12, 12", 3, true},
{"mixed labels", "mumble", "Server Example", "Server Example, 12, Server Example", "Server Example, 12, Server Example", 3, true},
{"unrelated provider", "other", "Same", "Same, Same, Same", "Same, Same, Same", 3, true},
{"document table", "mumble", "Same", "Same, Same, Same", "Same, Same, Same", 3, false},
{"single column", "mumble", "Server Example", "Server Example", "Server Example", 1, true},
{"empty cells", "mumble", "", "", "", 3, true},
} {
t.Run(test.name, func(t *testing.T) {
snapshot := Snapshot{ApplicationProcess: test.process, Role: "table cell", Name: test.label,
TableRowText: test.row, TableColumnCount: test.columns, GUITable: test.gui}
if got := mumbleTableRowText(snapshot); got != test.want {
t.Fatalf("row = %q, want %q", got, test.want)
}
})
}
}
func TestMumbleMessageText(t *testing.T) {
for _, test := range []struct{ input, want string }{
{"[12:34:56]", ""},