Files

93 lines
3.1 KiB
Go

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