Initial support for application specific overrides.

This commit is contained in:
Storm Dragon committed 2026-10-28 03:31:33 -04:00
1 parent 901fd58ba8
commit b45f800489
37 files changed
+3909 -475

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package reader
import (
"git.stormux.org/storm/fenrir/internal/screen"
"slices"
)
// SessionState preserves shortcut-only state as well as configuration. Screen,
// input timestamps and detached review positions are deliberately not restored.
type SessionState struct {
automatic bool
mode, attributeConfigured, attributeMode, attributeResumeMode string
regions *terminalRegions
}
func clone_regions(source *terminalRegions) *terminalRegions {
if source == nil {
return nil
}
result := *source
clone := func(region *screen.Region) *screen.Region {
if region == nil {
return nil
}
value := *region
return &value
}
result.window = clone(source.window)
for index, region := range source.live {
result.live[index] = clone(region)
}
return &result
}
func (r *Reader) Capture_session() SessionState {
return r.Capture_terminal_session(r.screen.Terminal)
}
func (r *Reader) Capture_terminal_session(terminal int) SessionState {
return SessionState{r.automatic, r.trackingMode, r.attributeConfigured, r.attributeMode, r.attributeResumeMode, clone_regions(r.regions[terminal])}
}
func (r *Reader) Restore_session(state SessionState, terminal int) {
r.automatic = state.automatic
r.Configure_tracking(state.mode)
r.attributeConfigured, r.attributeMode, r.attributeResumeMode = state.attributeConfigured, state.attributeMode, state.attributeResumeMode
if r.regions == nil {
r.regions = make(map[int]*terminalRegions)
}
r.regions[terminal] = clone_regions(state.regions)
r.regionRevision++
r.attributeKnown, r.indentKnown = false, false
r.Reset_progress()
}
func (r *Reader) Clear_session_regions(terminal int) {
delete(r.regions, terminal)
r.regionRevision++
}
func input_snapshot(snapshot screen.Snapshot, region screen.Region) screen.Snapshot {
snapshot.Cells = slices.Clone(snapshot.Cells)
for row := range snapshot.Rows {
for col := range snapshot.Cols {
if !region.Contains(col, row, snapshot.Cols) {
snapshot.Cells[row*snapshot.Cols+col] = ' '
}
}
}
return snapshot
}
+44 -4
View File
@@ -22,6 +22,7 @@ import (
"strconv"
"time"
"git.stormux.org/storm/fenrir/internal/applicationoverrides"
"git.stormux.org/storm/fenrir/internal/command"
"git.stormux.org/storm/fenrir/internal/diagnostic"
"git.stormux.org/storm/fenrir/internal/i18n"
@@ -85,14 +86,18 @@ func (r *Reader) Set_mark() presentation.Message { return r.review.Set_
func (r *Reader) Clear_marks() { r.review.Clear_marks() }
func (r *Reader) Marked_text(copying bool) (string, bool) { return r.review.Marked_text(copying) }
func (r *Reader) Update(next screen.Snapshot) (output []presentation.Message) {
func (r *Reader) Update(next screen.Snapshot) []presentation.Message {
return r.Update_application(next, applicationoverrides.Frame{})
}
func (r *Reader) Update_application(next screen.Snapshot, frame applicationoverrides.Frame) (output []presentation.Message) {
r.completedWords = nil
previous := r.screen
// Python runs cursor-change commands only after recent direct application
// input (300 ms). Reader commands and passive output must preserve review.
if !r.keepReviewOnCursorChange && r.review.Active() && !r.applicationInputAt.IsZero() && time.Since(r.applicationInputAt) < 300*time.Millisecond &&
previous.Active && next.Active && previous.Terminal == next.Terminal && previous.Generation == next.Generation &&
previous.Rows == next.Rows && previous.Cols == next.Cols && (previous.X != next.X || previous.Y != next.Y) {
previous.Rows == next.Rows && previous.Cols == next.Cols && (previous.X != next.X || previous.Y != next.Y || frame.Recognized && frame.FocusChanged) {
r.review.Resume_tracking()
}
switched := r.review.Update(next)
@@ -102,7 +107,22 @@ func (r *Reader) Update(next screen.Snapshot) (output []presentation.Message) {
pendingInput := r.inputCount
defer func() { r.trace_update(previous, next, switched, pendingInput, path, output) }()
}
if frame.Recognized && frame.OwnsPresentation {
path = "application"
defer func() {
if !next.Active || !r.automatic {
return
}
if !r.review.Active() {
output = append(output, frame.Focus...)
}
output = append(output, frame.Updates...)
}()
}
defer func() {
if frame.OwnsPresentation {
return
}
if message := r.automatic_attributes(previous, next, switched); message != nil {
output = append([]presentation.Message{*message}, output...)
}
@@ -129,6 +149,9 @@ func (r *Reader) Update(next screen.Snapshot) (output []presentation.Message) {
path = "automatic-disabled"
return nil
}
if frame.OwnsPresentation {
return nil
}
output := []presentation.Message{{Text: i18n.Format("screen {0}", strconv.Itoa(next.Terminal)), Cue: presentation.TTYChanged}}
if r.region_state().allows(next.X, next.Y, next.Cols, true, true) {
output = append(output, presentation.Line_message(r.Automatic_snapshot(), next.Y))
@@ -138,9 +161,17 @@ func (r *Reader) Update(next screen.Snapshot) (output []presentation.Message) {
regions := r.region_state()
typingPrevious := regions.filtered_snapshot(previous, false, true)
typingNext := regions.filtered_snapshot(next, false, true)
if frame.OwnsPresentation {
if frame.Input == nil {
r.inputCount = 0
} else {
typingPrevious = input_snapshot(typingPrevious, *frame.Input)
typingNext = input_snapshot(typingNext, *frame.Input)
}
}
if echo, matched := r.typing_echo(typingPrevious, typingNext); matched {
r.progress.Reset()
if r.progressEnabled && regions.has_live() {
if !frame.OwnsPresentation && r.progressEnabled && regions.has_live() {
// Typing can update a ruler/progress region in the same snapshot.
// Inspect only live regions here, so typed percentages elsewhere
// cannot become progress cues, even on the same physical row.
@@ -150,7 +181,13 @@ func (r *Reader) Update(next screen.Snapshot) (output []presentation.Message) {
return echo
}
deleted := r.match_deletion(previous, next)
if r.progressEnabled {
if frame.OwnsPresentation && frame.Input == nil {
deleted = nil
}
if frame.OwnsPresentation {
r.progress.Reset()
}
if !frame.OwnsPresentation && r.progressEnabled {
deletionRow := -1
if len(deleted) > 0 {
deletionRow = next.Y
@@ -165,6 +202,9 @@ func (r *Reader) Update(next screen.Snapshot) (output []presentation.Message) {
}
return nil
}
if frame.OwnsPresentation {
return r.deletion_echo(deleted, next)
}
readingPrevious := regions.filtered_snapshot(previous, true, true)
readingNext := r.Automatic_snapshot()
if r.trackingMode == "highlight" && !r.review.Active() {