After commit c018f5fd29 Fenrir is being switched over to go. If you do not want to be part of the testing process, please switch to master.

This commit is contained in:
Storm Dragon committed 2026-10-27 20:30:55 -04:00
1 parent c018f5fd29
commit 0e50de4c76
1047 files changed
+58364 -63747

No files matched your search

+262
View File
@@ -0,0 +1,262 @@
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package reader
import (
"fmt"
"slices"
"strconv"
"strings"
"git.stormux.org/storm/fenrir/internal/command"
"git.stormux.org/storm/fenrir/internal/i18n"
"git.stormux.org/storm/fenrir/internal/presentation"
"git.stormux.org/storm/fenrir/internal/screen"
)
type terminalRegions struct {
window *screen.Region
live [10]*screen.Region
}
var readLiveRegions = [...]command.ID{
command.ReadLiveRegion1, command.ReadLiveRegion2, command.ReadLiveRegion3,
command.ReadLiveRegion4, command.ReadLiveRegion5, command.ReadLiveRegion6,
command.ReadLiveRegion7, command.ReadLiveRegion8, command.ReadLiveRegion9, command.ReadLiveRegion10,
}
var clearLiveRegions = [...]command.ID{
command.ClearLiveRegion1, command.ClearLiveRegion2, command.ClearLiveRegion3,
command.ClearLiveRegion4, command.ClearLiveRegion5, command.ClearLiveRegion6,
command.ClearLiveRegion7, command.ClearLiveRegion8, command.ClearLiveRegion9, command.ClearLiveRegion10,
}
func (r *Reader) region_state() *terminalRegions { return r.regions[r.screen.Terminal] }
func (r *Reader) ensure_regions() *terminalRegions {
if r.regions == nil {
r.regions = make(map[int]*terminalRegions)
}
if r.region_state() == nil {
r.regions[r.screen.Terminal] = &terminalRegions{}
}
return r.region_state()
}
func (s *terminalRegions) has_live() bool {
if s != nil {
for _, region := range s.live {
if region != nil {
return true
}
}
}
return false
}
func (s *terminalRegions) allows(x, y, cols int, window, live bool) bool {
if s == nil {
return true
}
if window && s.window != nil && !s.window.Contains(x, y, cols) {
return false
}
if live {
for _, region := range s.live {
if region != nil && region.Contains(x, y, cols) {
return false
}
}
}
return true
}
// filtered_snapshot never alters the source snapshot. Masking before diffing
// keeps excluded churn out of typing/scroll classification, rather than trying
// to remove strings from an already flattened presentation.
func (s *terminalRegions) filtered_snapshot(snapshot screen.Snapshot, window, live bool) screen.Snapshot {
if s == nil || (!window || s.window == nil) && (!live || !s.has_live()) {
return snapshot
}
snapshot.Cells = slices.Clone(snapshot.Cells)
snapshot.Attributes = slices.Clone(snapshot.Attributes)
for row := range snapshot.Rows {
for col := range snapshot.Cols {
if s.allows(col, row, snapshot.Cols, window, live) {
continue
}
index := row*snapshot.Cols + col
snapshot.Cells[index] = ' '
if len(snapshot.Attributes) == len(snapshot.Cells) {
snapshot.Attributes[index] = screen.Attribute{}
}
}
}
return snapshot
}
// Automatic_snapshot is also used by prompt restoration: an excluded update
// must not restore speech or bypass the automatic-reading filter.
func (r *Reader) Automatic_snapshot() screen.Snapshot {
return r.region_state().filtered_snapshot(r.screen, true, true)
}
func (r *Reader) Region_revision() uint64 { return r.regionRevision }
// live_snapshot retains only live-region cells for progress detection alongside
// a confirmed typing edit. Unlike automatic speech, progress may inspect them.
func (s *terminalRegions) live_snapshot(snapshot screen.Snapshot) screen.Snapshot {
snapshot.Cells = slices.Clone(snapshot.Cells)
for row := range snapshot.Rows {
for col := range snapshot.Cols {
if s.allows(col, row, snapshot.Cols, false, true) {
snapshot.Cells[row*snapshot.Cols+col] = ' '
}
}
}
return snapshot
}
// An explicit live region remains eligible for progress even when it lies
// outside a reading window. The window otherwise excludes outside progress.
func (s *terminalRegions) progress_snapshot(snapshot screen.Snapshot) screen.Snapshot {
if s == nil || s.window == nil {
return snapshot
}
snapshot.Cells = slices.Clone(snapshot.Cells)
for row := range snapshot.Rows {
for col := range snapshot.Cols {
if !s.window.Contains(col, row, snapshot.Cols) && s.allows(col, row, snapshot.Cols, false, true) {
snapshot.Cells[row*snapshot.Cols+col] = ' '
}
}
}
return snapshot
}
func (r *Reader) reset_region_feedback() {
r.regionRevision++
r.progress.Reset()
r.inputCount, r.inputBoundary = 0, false
r.lastCompletedWord = ""
r.attributeKnown, r.indentKnown = false, false
}
// Set_window is shared by marked keyboard commands and validated remote
// coordinates. Regions belong to the terminal, never a guessed process name.
func (r *Reader) Set_window(x1, y1, x2, y2 int) error {
if !r.screen.Active || min(x1, x2, y1, y2) < 0 || max(x1, x2) >= r.screen.Cols || max(y1, y2) >= r.screen.Rows {
return fmt.Errorf("Window coordinates must be inside the active screen")
}
r.ensure_regions().window = &screen.Region{Left: min(x1, x2), Top: min(y1, y2), Right: max(x1, x2), Bottom: max(y1, y2)}
r.reset_region_feedback()
return nil
}
func (r *Reader) Clear_window() {
if state := r.region_state(); state != nil {
state.window = nil
}
r.reset_region_feedback()
}
func (r *Reader) region_command(id command.ID) (presentation.CommandResult, bool) {
readSlot, clearSlot := slices.Index(readLiveRegions[:], id), slices.Index(clearLiveRegions[:], id)
known := readSlot >= 0 || clearSlot >= 0 || id == command.SetWindow || id == command.ClearWindow || id == command.SetLiveRegion ||
id == command.ListReadingRegions || id == command.ClearLiveRegions || id == command.ClearReadingRegions
if !known {
return presentation.CommandResult{}, false
}
result := presentation.CommandResult{}
if !r.screen.Active {
return presentation.CommandResult{Text: i18n.T("no text console"), Cue: presentation.Error}, true
}
state := r.region_state()
switch {
case readSlot >= 0 || clearSlot >= 0:
slot := max(readSlot, clearSlot)
number := strconv.Itoa(slot + 1)
if state == nil || state.live[slot] == nil {
result.Text = i18n.Format("Live region {0} not set", number)
} else if clearSlot >= 0 {
state.live[slot] = nil
r.reset_region_feedback()
result.Text = i18n.Format("Live region {0} cleared", number)
} else {
result.Text = state.live[slot].Text(r.screen)
if state.live[slot].Left >= r.screen.Cols || state.live[slot].Top >= r.screen.Rows {
result.Text = i18n.Format("Live region {0} outside screen", number)
} else if strings.TrimSpace(result.Text) == "" {
result.Text = i18n.T("blank")
}
}
case id == command.SetLiveRegion:
region, ok := r.review.Marked_region(true)
if !ok {
result.Text = i18n.T("One or two marks are needed")
break
}
state = r.ensure_regions()
slot := slices.Index(state.live[:], nil)
if slot < 0 {
result.Text = i18n.T("All 10 live regions are set")
break
}
state.live[slot] = &region
r.review.Clear_marks()
r.reset_region_feedback()
result.Text = i18n.Format("Live region {0} set", strconv.Itoa(slot+1))
case id == command.SetWindow:
region, ok := r.review.Marked_region(false)
if !ok {
result.Text = i18n.T("Set window begin and end marks")
break
}
r.ensure_regions().window = &region
r.review.Clear_marks()
r.reset_region_feedback()
result.Text = i18n.Format("Window Mode on for terminal {0}", strconv.Itoa(r.screen.Terminal))
case id == command.ClearWindow:
if state == nil || state.window == nil {
result.Text = i18n.T("Not in window Mode")
} else {
r.Clear_window()
result.Text = i18n.Format("Window Mode off for terminal {0}", strconv.Itoa(r.screen.Terminal))
}
case id == command.ClearLiveRegions:
if state != nil {
state.live = [10]*screen.Region{}
}
r.reset_region_feedback()
result.Text = i18n.T("Live regions cleared")
case id == command.ClearReadingRegions:
delete(r.regions, r.screen.Terminal)
r.reset_region_feedback()
result.Text = i18n.T("Reading regions cleared")
case id == command.ListReadingRegions:
var entries []string
if state != nil {
if state.window != nil {
entries = append(entries, i18n.Format("Reading window: {0}", region_description(*state.window)))
}
for slot, region := range state.live {
if region != nil {
entries = append(entries, i18n.Format("Live region {0}: {1}", strconv.Itoa(slot+1), region_description(*region)))
}
}
}
result.Text = strings.Join(entries, ". ")
if len(entries) == 0 {
result.Text = i18n.T("No reading regions set")
}
}
return result, true
}
func region_description(region screen.Region) string {
if region.ToLineEnd {
return i18n.Format("row {0}, column {1} to end of line", strconv.Itoa(region.Top+1), strconv.Itoa(region.Left+1))
}
return i18n.Format("row {0}, column {1} to row {2}, column {3}", strconv.Itoa(region.Top+1), strconv.Itoa(region.Left+1), strconv.Itoa(region.Bottom+1), strconv.Itoa(region.Right+1))
}