// 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" "unicode/utf8" "git.stormux.org/storm/fenrir/internal/diagnostic" "git.stormux.org/storm/fenrir/internal/presentation" "git.stormux.org/storm/fenrir/internal/screen" ) // AttributeChange contains only geometry and appearance, never cell text. // End is exclusive. Keep details bounded even during a whole-screen repaint. type AttributeChange struct { Row, Start, End int Before, After screen.Attribute } func (r *Reader) trace_update(previous, next screen.Snapshot, switched bool, pendingInput int, path string, output []presentation.Message) { comparable := previous.Rows == next.Rows && previous.Cols == next.Cols && next.Rows > 0 && next.Cols > 0 && len(previous.Cells) == next.Rows*next.Cols && len(next.Cells) == len(previous.Cells) previousAttributes := len(previous.Attributes) > 0 && len(previous.Attributes) == len(previous.Cells) nextAttributes := len(next.Attributes) > 0 && len(next.Attributes) == len(next.Cells) textRows, attributeRows := []int{}, []int{} attributeChanges := []AttributeChange{} truncated := false changedCells, changedAttributes := 0, 0 if comparable { for row := range next.Rows { if !slices.Equal(previous.Line(row), next.Line(row)) { textRows = append(textRows, row) } rowChanged := false for col := range next.Cols { index := row*next.Cols + col if previous.Cells[index] != next.Cells[index] { changedCells++ } if !previousAttributes || !nextAttributes || previous.Attributes[index] == next.Attributes[index] { continue } rowChanged = true changedAttributes++ before, after := previous.Attributes[index], next.Attributes[index] last := len(attributeChanges) - 1 if last >= 0 && attributeChanges[last].Row == row && attributeChanges[last].End == col && attributeChanges[last].Before == before && attributeChanges[last].After == after { attributeChanges[last].End++ } else if len(attributeChanges) < 32 { attributeChanges = append(attributeChanges, AttributeChange{Row: row, Start: col, End: col + 1, Before: before, After: after}) } else { truncated = true } } if rowChanged { attributeRows = append(attributeRows, row) } } } outputLengths := make([]int, len(output)) for index, message := range output { outputLengths[index] = utf8.RuneCountInString(message.Text) } diagnostic.Record("reader update", "terminal", next.Terminal, "previous_terminal", previous.Terminal, "generation", next.Generation, "previous_generation", previous.Generation, "active", next.Active, "switched", switched, "size", [2]int{next.Rows, next.Cols}, "previous_size", [2]int{previous.Rows, previous.Cols}, "cursor", [2]int{next.X, next.Y}, "previous_cursor", [2]int{previous.X, previous.Y}, "mode", r.trackingMode, "automatic", r.automatic, "cursor_echo", r.cursorEcho, "review", r.review.Active(), "pending_typing", pendingInput, "path", path, "comparable", comparable, "text_rows", textRows, "text_cells", changedCells, "previous_attributes", previousAttributes, "attributes", nextAttributes, "attribute_rows", attributeRows, "attribute_cells", changedAttributes, "attribute_changes", attributeChanges, "attribute_changes_truncated", truncated, "messages", len(output), "message_lengths", outputLengths) }