Files
cthulhu/internal/app/terminal_events.go

89 lines
3.8 KiB
Go

// Cthulhu screen reader.
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"strings"
"git.stormux.org/storm/cthulhu/internal/atspi"
"git.stormux.org/storm/cthulhu/internal/input"
)
func should_ignore_terminal_scrollback(event atspi.Event) bool {
// VTE can remove old lines at the start of its accessible text while
// appending output at a different offset. A deletion entirely before
// the caret line is not an edit to the prompt. VTE wrapped rows have
// no trailing newline, so do not require one. This event pattern
// occurs on both X11 and Wayland.
return event.Type == atspi.TextDeletedEvent &&
event.Source.Role == "terminal" && event.Detail1 == 0 &&
event.Source.TextCaretKnown && event.Source.BrailleLineStart > 0 &&
event.TextChangeLength > 0 &&
event.TextChangeLength <= event.Source.BrailleLineStart
}
// is_user_terminal_deletion identifies a deletion the user just caused at
// the caret on its own line: a Backspace or Delete edit. The deleted text
// is the user's confirmation that the edit took, so repaint coalescing
// must not swallow it. Program repaints start above the caret line, and
// deletions without a preceding delete key are program output churn.
func is_user_terminal_deletion(event atspi.Event, intent input.Intent) bool {
return intent == input.DeleteIntent &&
event.Type == atspi.TextDeletedEvent && event.Source.Role == "terminal" &&
event.Source.TextCaretKnown &&
event.Detail1 >= event.Source.BrailleLineStart &&
event.Detail1 <= event.Source.BrailleCaret
}
// is_latest_wins_terminal_repaint identifies an in-place rewrite: an
// insertion that replaced a just-deleted range. Status regions update
// this way several times a second and only the newest state is worth
// hearing. Editing feedback on the caret line after a typing or delete
// key stays immediate; appended output has no preceding deletion and
// never qualifies.
func is_latest_wins_terminal_repaint(event atspi.Event, intent input.Intent, replacedDeletion bool) bool {
if !replacedDeletion || event.Type != atspi.TextInsertedEvent || event.Source.Role != "terminal" {
return false
}
if (intent == input.TypingIntent || intent == input.DeleteIntent) &&
event.Source.TextCaretKnown && event.Detail1 >= event.Source.BrailleLineStart {
return false
}
return true
}
// Blank repaints carry no speech. Short nonblank updates can still be
// meaningful ("OK", a count, or one Unicode character) and must survive.
func is_trivial_terminal_repaint(event atspi.Event) bool {
return event.Type == atspi.TextInsertedEvent && event.Source.Role == "terminal" &&
strings.TrimSpace(event.Payload) == ""
}
// terminalReplacementOverlap reports whether an insertion rewrites any of
// the range a pending deletion removed. Offsets are not compared exactly:
// a scroll shift at the start of the buffer between the deletion and its
// replacement insertion moves the insertion's offset, and a repainted
// region is routinely rewritten at a neighboring offset.
func terminalReplacementOverlap(deletion, insertion atspi.Event) bool {
if deletion.Source.Role != "terminal" || insertion.Source.Role != "terminal" ||
deletion.Source.Path == "" || deletion.Source.ID() != insertion.Source.ID() {
return false
}
deletionEnd := deletion.Detail1 + max(deletion.TextChangeLength, 1)
insertionEnd := insertion.Detail1 + max(insertion.TextChangeLength, 1)
return insertion.Detail1 < deletionEnd && deletion.Detail1 < insertionEnd
}
// Preserve the first deadline so a continuously redrawn status still speaks
// its latest value. The event loop owns both the event and the timer.
func (pending *pendingPresentation) coalesce_terminal_repaint(event atspi.Event) {
if pending.event.Type == "" {
pending.schedule(event, input.NoIntent, terminalRepaintDebounce)
return
}
pending.event = event
}