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

+92
View File
@@ -0,0 +1,92 @@
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
// Package prompt recognizes fresh prompts and work-completion lines.
package prompt
import (
"fmt"
"regexp"
"strings"
"git.stormux.org/storm/fenrir/internal/screen"
)
var defaultPatterns = []*regexp.Regexp{
regexp.MustCompile(`^(?:[$#>]|\[.*\]\s*[\\$#>]|[a-zA-Z0-9._-]+[\\$#>]|\S+@\S+.*[$#>])\s*$`),
regexp.MustCompile(`(?i)\?\s*(?:\[(?:y/n|yes/no)\]|\((?:y/n|yes/no)\))\s*:?\s*$`),
regexp.MustCompile(`(?i)^.*\bok\s*\[y/n\]\s*:?\s*$`),
regexp.MustCompile(`(?i)^(?:\[sudo\]\s*password\s+for\s+.+|.*\bpassword|(?:please\s+)?enter\s+passphrase.*)\s*:\s*$`),
regexp.MustCompile(`(?i)^(?:press\s+any\s+key\s+to\s+continue.*|are\s+you\s+sure\?.*|please\s+confirm.*)$`),
}
var completionPattern = regexp.MustCompile(`^Worked for (?:[0-9]+h(?: [0-9]+m)?(?: [0-9]+s)?|[0-9]+m(?: [0-9]+s)?|[0-9]+s)\s+•\s+(?:[1-9]|1[0-2]):[0-5][0-9]\s+(?:AM|PM)$`)
type Detector struct {
exactMatches []string
patterns []*regexp.Regexp
previous map[string]int
terminal int
generation uint64
rows, cols int
active bool
}
func New(exactMatches, customPatterns []string) (*Detector, error) {
detector := &Detector{exactMatches: append([]string(nil), exactMatches...)}
for _, pattern := range customPatterns {
compiled, err := regexp.Compile(pattern)
if err != nil {
return nil, fmt.Errorf("invalid prompt custom pattern %q: %w", pattern, err)
}
detector.patterns = append(detector.patterns, compiled)
}
return detector, nil
}
func (d *Detector) matches_prompt(text string) bool {
if text == "" {
return false
}
for _, exact := range d.exactMatches {
if text == strings.TrimSpace(exact) {
return true
}
}
for _, patterns := range [][]*regexp.Regexp{d.patterns, defaultPatterns} {
for _, pattern := range patterns {
if pattern.MatchString(text) {
return true
}
}
}
return false
}
// Observe runs even when speech is enabled, establishing a baseline before
// silence. Counts ignore row movement, so old prompts/completions scrolling
// through the screen cannot restore speech. Session/buffer/size changes only
// establish a new baseline; they never restore from another screen's history.
// Shell/interactive prompts must be on the cursor row. Completion lines can
// appear above an input area, as in a terminal coding assistant.
func (d *Detector) Observe(next screen.Snapshot) string {
counts := make(map[string]int)
for row := 0; row < next.Rows; row++ {
counts[strings.TrimSpace(next.Text(row))]++
}
stable := d.previous != nil && d.active && next.Active && d.terminal == next.Terminal &&
d.generation == next.Generation && d.rows == next.Rows && d.cols == next.Cols
var result string
if stable {
for row := 0; row < next.Rows; row++ {
text := strings.TrimSpace(next.Text(row))
if counts[text] > d.previous[text] && (completionPattern.MatchString(text) || row == next.Y && d.matches_prompt(text)) {
result = text
break
}
}
}
d.previous = counts
d.terminal, d.generation, d.rows, d.cols, d.active = next.Terminal, next.Generation, next.Rows, next.Cols, next.Active
return result
}