// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "fmt" "strings" "git.stormux.org/storm/cthulhu/internal/atspi" "git.stormux.org/storm/cthulhu/internal/sound" ) type speechLine struct { Object atspi.ObjectID Raw string } type indentationData struct { signature string columns int } type indentationHistory map[atspi.ObjectID]indentationData func indentation_data(line string) indentationData { var data indentationData for _, value := range line { switch value { case ' ', '\u00a0': data.columns++ value = ' ' case '\t': data.columns += 4 - data.columns%4 default: return data } data.signature += string(value) } return data } func (worker *speechWorker) line_text(text string, line *speechLine) string { if line == nil || worker.reading == nil { return text } data := indentation_data(line.Raw) previous, known := worker.indentation[line.Object] if worker.indentation == nil || len(worker.indentation) >= 128 && !known { worker.indentation = make(indentationHistory) } worker.indentation[line.Object] = data settings := worker.reading() if !settings.EnableIndentation || settings.OnlySpeakDisplayedText || known && previous.signature == data.signature { return text } mode := settings.IndentationPresentationMode if (mode == "beeps" || mode == "both") && worker.indentationCue != nil { frequency := min(2700, 200+data.columns*30) if data.columns > previous.columns { frequency += 50 } else if data.columns < previous.columns { frequency = max(100, frequency-50) } worker.indentationCue(sound.Cue{Name: sound.IndentationCueName(frequency)}) } if mode == "beeps" { return text } word := "columns" if data.columns == 1 { word = "column" } return strings.TrimSuffix(fmt.Sprintf("indentation %d %s, %s", data.columns, word, text), ", ") } func document_speech_line(line atspi.DocumentLine) *speechLine { if len(line.Contents) == 0 { return nil } content := line.Contents[0] if content.Snapshot.Role == "password text" { return nil } raw := content.RawText if raw == "" { raw = content.Text } return &speechLine{Object: content.Snapshot.ID(), Raw: raw} }