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