Files

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]
}