84 lines
3.1 KiB
Go
84 lines
3.1 KiB
Go
// Fenrir console screen reader.
|
|
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
//
|
|
// 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 reader
|
|
|
|
import (
|
|
"slices"
|
|
"time"
|
|
"unicode"
|
|
|
|
"git.stormux.org/storm/fenrir/internal/screen"
|
|
)
|
|
|
|
// Progress is independent of automatic reading and speech mute.
|
|
func (r *Reader) Configure_progress(enabled bool) {
|
|
if r.progressEnabled != enabled {
|
|
r.progress.Reset()
|
|
}
|
|
r.progressEnabled = enabled
|
|
}
|
|
func (r *Reader) Next_progress(now time.Time) (float64, bool) {
|
|
if r.applicationActivity && r.progressEnabled {
|
|
// Use the existing spinner's fixed pitch, once every four seconds. This reports
|
|
// activity, not a guessed completion percentage, and needs no repaint.
|
|
if !r.applicationInputAt.IsZero() && now.Sub(r.applicationInputAt) < 300*time.Millisecond ||
|
|
!r.applicationActivityAt.IsZero() && now.Sub(r.applicationActivityAt) < 4*time.Second {
|
|
return 0, false
|
|
}
|
|
r.applicationActivityAt = now
|
|
return 50, true
|
|
}
|
|
return r.progress.Next(now)
|
|
}
|
|
func (r *Reader) Reset_progress() { r.progress.Reset(); r.applicationActivity = false }
|
|
|
|
func (r *Reader) Progress_active() bool {
|
|
return r.applicationActivity && r.progressEnabled || r.progress.Active()
|
|
}
|
|
|
|
func (r *Reader) Note_deletion() { r.progressDeletionAt = time.Now() }
|
|
|
|
// A recent deletion key alone is insufficient: match the actual cursor-row
|
|
// deletion so background progress, including another row in the same snapshot,
|
|
// remains available to the detector. The same evidence permits deletion echo
|
|
// while automatic reading is disabled, without speaking other screen changes.
|
|
func (r *Reader) match_deletion(previous, next screen.Snapshot) []rune {
|
|
if r.progressDeletionAt.IsZero() || time.Since(r.progressDeletionAt) > echoWindow || previous.Y != next.Y || next.X > previous.X {
|
|
return nil
|
|
}
|
|
before, after := previous.Line(next.Y), next.Line(next.Y)
|
|
count := previous.X - next.X
|
|
if count == 0 {
|
|
// Forward Delete leaves the cursor in place but shortens the line.
|
|
beforeEnd, afterEnd := len(before), len(after)
|
|
for beforeEnd > 0 && unicode.IsSpace(before[beforeEnd-1]) {
|
|
beforeEnd--
|
|
}
|
|
for afterEnd > 0 && unicode.IsSpace(after[afterEnd-1]) {
|
|
afterEnd--
|
|
}
|
|
count = beforeEnd - afterEnd
|
|
}
|
|
if count <= 0 || next.X+count > next.Cols || !slices.Equal(before[:next.X], after[:next.X]) ||
|
|
!slices.Equal(before[next.X+count:], after[next.X:next.Cols-count]) || !all_spaces(after[next.Cols-count:]) {
|
|
return nil
|
|
}
|
|
r.progressDeletionAt = time.Time{}
|
|
return before[next.X : next.X+count]
|
|
}
|