wip
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
@@ -108,11 +109,15 @@ type Child struct {
|
||||
|
||||
// ringMu guards ring. The ring buffer carries the last `ringCap`
|
||||
// bytes the PTY produced, used by SPEC §7 get_process_output stream
|
||||
// mode and search_output scrollback.
|
||||
// mode and search_output scrollback. The ring is a fixed-size byte
|
||||
// array with a wrap-around write index — no per-chunk reslice or
|
||||
// reallocation. StreamRead serves contiguous slices by copying out
|
||||
// of the (possibly wrapped) ring into a fresh buffer.
|
||||
ringMu sync.Mutex
|
||||
ring []byte
|
||||
ringStart int64 // absolute offset of ring[0]
|
||||
ringWrites int64 // cumulative bytes written
|
||||
ring []byte // length == ringCap once allocated
|
||||
ringPos int // next byte to overwrite
|
||||
ringFull bool // true once ringWrites ≥ ringCap
|
||||
ringWrites int64 // cumulative bytes written
|
||||
|
||||
// portsMu guards ports. Best-effort port detection: regex on stream.
|
||||
portsMu sync.Mutex
|
||||
@@ -127,10 +132,36 @@ type Child struct {
|
||||
// exits and calls Start to bring the entry back up. Cleared when the
|
||||
// user explicitly kills the process from the palette.
|
||||
autoRestart atomic.Bool
|
||||
|
||||
// persistFn is set by Session after Spawn registers the entry. The
|
||||
// callback mirrors mutable bits (name, auto-restart) into the
|
||||
// persist store so a restarted patterm can rebuild this entry. Nil
|
||||
// when no persist store is attached (unit tests / non-command
|
||||
// entries).
|
||||
persistMu sync.Mutex
|
||||
persistFn func(*Child)
|
||||
}
|
||||
|
||||
func (c *Child) SetAutoRestart(v bool) { c.autoRestart.Store(v) }
|
||||
func (c *Child) AutoRestart() bool { return c.autoRestart.Load() }
|
||||
func (c *Child) SetAutoRestart(v bool) {
|
||||
c.autoRestart.Store(v)
|
||||
c.firePersist()
|
||||
}
|
||||
func (c *Child) AutoRestart() bool { return c.autoRestart.Load() }
|
||||
|
||||
func (c *Child) setPersistFn(fn func(*Child)) {
|
||||
c.persistMu.Lock()
|
||||
c.persistFn = fn
|
||||
c.persistMu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Child) firePersist() {
|
||||
c.persistMu.Lock()
|
||||
fn := c.persistFn
|
||||
c.persistMu.Unlock()
|
||||
if fn != nil {
|
||||
fn(c)
|
||||
}
|
||||
}
|
||||
|
||||
// PortSighting is one entry returned by get_process_ports.
|
||||
type PortSighting struct {
|
||||
@@ -152,7 +183,7 @@ func newChildEntry(id, name string, kind ChildKind, argv, env []string, parentID
|
||||
Kind: kind,
|
||||
ParentID: parentID,
|
||||
PresetRef: presetRef,
|
||||
ring: make([]byte, 0, ringCap),
|
||||
ring: make([]byte, ringCap),
|
||||
}
|
||||
st := StatusStopped
|
||||
c.status.Store(&st)
|
||||
@@ -254,6 +285,7 @@ func (c *Child) SetName(name string) {
|
||||
c.nameMu.Lock()
|
||||
c.Name = name
|
||||
c.nameMu.Unlock()
|
||||
c.firePersist()
|
||||
}
|
||||
|
||||
// ScreenVersion returns the current emulator snapshot version, bumped
|
||||
@@ -302,13 +334,22 @@ func (c *Child) recordWrite(chunk []byte) {
|
||||
c.lastWriteNS.Store(time.Now().UnixNano())
|
||||
c.screenVersion.Add(1)
|
||||
c.ringMu.Lock()
|
||||
c.ring = append(c.ring, chunk...)
|
||||
c.ringWrites += int64(len(chunk))
|
||||
if len(c.ring) > ringCap {
|
||||
drop := len(c.ring) - ringCap
|
||||
c.ring = c.ring[drop:]
|
||||
c.ringStart += int64(drop)
|
||||
// Chunks larger than ringCap are tail-truncated — only the last
|
||||
// ringCap bytes of the chunk can survive.
|
||||
src := chunk
|
||||
if len(src) > ringCap {
|
||||
src = src[len(src)-ringCap:]
|
||||
}
|
||||
for written := 0; written < len(src); {
|
||||
n := copy(c.ring[c.ringPos:], src[written:])
|
||||
c.ringPos += n
|
||||
if c.ringPos >= ringCap {
|
||||
c.ringPos = 0
|
||||
c.ringFull = true
|
||||
}
|
||||
written += n
|
||||
}
|
||||
c.ringWrites += int64(len(chunk))
|
||||
c.ringMu.Unlock()
|
||||
c.scanPortsFromChunk(chunk)
|
||||
}
|
||||
@@ -316,6 +357,11 @@ func (c *Child) recordWrite(chunk []byte) {
|
||||
// scanPortsFromChunk does best-effort port detection on a PTY chunk.
|
||||
// SPEC §7 get_process_ports — no probing, just stream scanning.
|
||||
func (c *Child) scanPortsFromChunk(chunk []byte) {
|
||||
// Cheap prefix check: most chunks don't contain a URL. Bail before
|
||||
// running the regex DFA over the whole chunk.
|
||||
if !bytes.Contains(chunk, []byte("http")) {
|
||||
return
|
||||
}
|
||||
matches := portRegex.FindAllSubmatch(chunk, -1)
|
||||
if len(matches) == 0 {
|
||||
return
|
||||
@@ -364,16 +410,38 @@ func (c *Child) Ports() []PortSighting {
|
||||
func (c *Child) StreamRead(since int64) ([]byte, int64) {
|
||||
c.ringMu.Lock()
|
||||
defer c.ringMu.Unlock()
|
||||
if since < c.ringStart {
|
||||
since = c.ringStart
|
||||
end := c.ringWrites
|
||||
var ringStart int64
|
||||
if c.ringFull {
|
||||
ringStart = end - int64(ringCap)
|
||||
}
|
||||
if since < ringStart {
|
||||
since = ringStart
|
||||
}
|
||||
end := c.ringStart + int64(len(c.ring))
|
||||
if since >= end {
|
||||
return nil, end
|
||||
}
|
||||
start := int(since - c.ringStart)
|
||||
out := make([]byte, end-since)
|
||||
copy(out, c.ring[start:])
|
||||
n := int(end - since)
|
||||
out := make([]byte, n)
|
||||
// Locate `since` in the ring. When the buffer hasn't wrapped yet,
|
||||
// bytes 0..ringPos hold writes 0..ringPos. After wrap, ringPos
|
||||
// points at the oldest byte, and the freshest byte is at
|
||||
// (ringPos - 1) mod ringCap.
|
||||
var pos int
|
||||
if c.ringFull {
|
||||
skip := int(since - ringStart) // bytes after the oldest
|
||||
pos = (c.ringPos + skip) % ringCap
|
||||
} else {
|
||||
pos = int(since)
|
||||
}
|
||||
first := ringCap - pos
|
||||
if first > n {
|
||||
first = n
|
||||
}
|
||||
copy(out, c.ring[pos:pos+first])
|
||||
if first < n {
|
||||
copy(out[first:], c.ring[:n-first])
|
||||
}
|
||||
return out, end
|
||||
}
|
||||
|
||||
@@ -395,19 +463,17 @@ func (c *Child) signal(sig syscall.Signal) error {
|
||||
// NudgeRedraw asks the child to throw away any diff-based render state
|
||||
// and emit a full frame on the next tick. Used after a focus switch so
|
||||
// ratatui/ink TUIs re-render coherently against the snapshot we just
|
||||
// replayed. We toggle the PTY size by one row so the kernel reliably
|
||||
// emits SIGWINCH (TIOCSWINSZ skips the signal if the size didn't
|
||||
// change), then send SIGWINCH explicitly for TUIs that miss or coalesce
|
||||
// the size-toggled signal. The emulator is left alone — it already
|
||||
// matches our intended size and the brief mismatch only affects what the
|
||||
// child writes during the second redraw.
|
||||
// replayed. Sends an explicit SIGWINCH; TIOCSWINSZ with the same size
|
||||
// is a no-op in the kernel, so an explicit signal is what most TUIs
|
||||
// actually act on anyway. Avoid resize-toggles here — under a drag-
|
||||
// resize the kernel still emits intermediate SIGWINCHes against the
|
||||
// host PTY and toggling our child's size on top produces inconsistent
|
||||
// grid state.
|
||||
func (c *Child) NudgeRedraw(cols, rows uint16) {
|
||||
pty := c.PTY()
|
||||
if pty == nil || rows < 2 {
|
||||
return
|
||||
}
|
||||
_ = pty.Resize(cols, rows-1)
|
||||
_ = pty.Resize(cols, rows)
|
||||
_ = c.signal(syscall.SIGWINCH)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user