Initial commit
This commit is contained in:
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Compiled binary (module name: bell-silencer)
|
||||||
|
bell-silencer
|
||||||
|
|
||||||
|
# Go build/test artifacts
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
13
go.mod
Normal file
13
go.mod
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
module github.com/bellpilot/bell-silencer
|
||||||
|
|
||||||
|
go 1.26.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/creack/pty v1.1.24 // indirect
|
||||||
|
github.com/ebitengine/oto/v3 v3.4.0 // indirect
|
||||||
|
github.com/ebitengine/purego v0.9.0 // indirect
|
||||||
|
github.com/gordonklaus/portaudio v0.0.0-20260203164431-765aa7dfa631 // indirect
|
||||||
|
github.com/hajimehoshi/oto/v2 v2.4.3 // indirect
|
||||||
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
|
golang.org/x/term v0.42.0 // indirect
|
||||||
|
)
|
||||||
20
go.sum
Normal file
20
go.sum
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
|
github.com/ebitengine/oto/v3 v3.4.0 h1:br0PgASsEWaoWn38b2Goe7m1GKFYfNgnsjSd5Gg+/bQ=
|
||||||
|
github.com/ebitengine/oto/v3 v3.4.0/go.mod h1:IOleLVD0m+CMak3mRVwsYY8vTctQgOM0iiL6S7Ar7eI=
|
||||||
|
github.com/ebitengine/purego v0.4.1 h1:atcZEBdukuoClmy7TI89amtqAsJUzDQyY/JU7HaK+io=
|
||||||
|
github.com/ebitengine/purego v0.4.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
||||||
|
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
|
||||||
|
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
|
github.com/gordonklaus/portaudio v0.0.0-20260203164431-765aa7dfa631 h1:8TBHztmhDfAAg34yddptshinXBtDQwgKGlMfdtSFETw=
|
||||||
|
github.com/gordonklaus/portaudio v0.0.0-20260203164431-765aa7dfa631/go.mod h1:esZFQEUwqC+l76f2R8bIWSwXMaPbp79PppwZ1eJhFco=
|
||||||
|
github.com/hajimehoshi/oto/v2 v2.4.3 h1:E+vVhzF2WHuw/UK+aLQh1Spqj+thgsAAg4rbSx+JySI=
|
||||||
|
github.com/hajimehoshi/oto/v2 v2.4.3/go.mod h1:Yx9MTrWMeSS6MqkjacVZAicmJ1bqA1SlgCQmk3ybx1E=
|
||||||
|
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||||
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||||
|
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||||
487
main.go
Normal file
487
main.go
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"embed"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/creack/pty"
|
||||||
|
"github.com/ebitengine/oto/v3"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed bells.wav
|
||||||
|
var bellFile embed.FS
|
||||||
|
|
||||||
|
// --- Audio engine ---
|
||||||
|
|
||||||
|
type audioEngine struct {
|
||||||
|
once sync.Once
|
||||||
|
ctx *oto.Context
|
||||||
|
pcm []byte
|
||||||
|
initErr error
|
||||||
|
bellCh chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var engine = &audioEngine{
|
||||||
|
bellCh: make(chan struct{}, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *audioEngine) init() {
|
||||||
|
e.once.Do(func() {
|
||||||
|
data, err := bellFile.ReadFile("bells.wav")
|
||||||
|
if err != nil {
|
||||||
|
e.initErr = fmt.Errorf("read bell file: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pcm, sampleRate, channels, format, err := parseWAV(data)
|
||||||
|
if err != nil {
|
||||||
|
e.initErr = fmt.Errorf("parse WAV: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// oto's FormatUnsignedInt8 path has an integer underflow bug: it computes
|
||||||
|
// float32(v8-(1<<7)) with uint8 arithmetic, wrapping values below 128 into
|
||||||
|
// large positive numbers. Convert to signed 16-bit to use the correct path.
|
||||||
|
if format == oto.FormatUnsignedInt8 {
|
||||||
|
pcm16 := make([]byte, len(pcm)*2)
|
||||||
|
for i, b := range pcm {
|
||||||
|
s := (int16(b) - 128) * 256
|
||||||
|
pcm16[2*i] = byte(s)
|
||||||
|
pcm16[2*i+1] = byte(s >> 8)
|
||||||
|
}
|
||||||
|
pcm = pcm16
|
||||||
|
format = oto.FormatSignedInt16LE
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, ready, err := oto.NewContext(&oto.NewContextOptions{
|
||||||
|
SampleRate: sampleRate,
|
||||||
|
ChannelCount: channels,
|
||||||
|
Format: format,
|
||||||
|
BufferSize: time.Millisecond * 20,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
e.initErr = fmt.Errorf("create audio context: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
<-ready
|
||||||
|
|
||||||
|
e.ctx = ctx
|
||||||
|
e.pcm = pcm
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ring queues at most one pending bell, dropping extras while one is already queued.
|
||||||
|
func (e *audioEngine) ring() {
|
||||||
|
select {
|
||||||
|
case e.bellCh <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// run is the single bell-playing goroutine. It must be started once before ring is called.
|
||||||
|
func (e *audioEngine) run() {
|
||||||
|
e.init()
|
||||||
|
if e.initErr != nil {
|
||||||
|
log.Printf("audio init failed (%v); falling back to terminal bell", e.initErr)
|
||||||
|
}
|
||||||
|
for range e.bellCh {
|
||||||
|
if e.initErr != nil {
|
||||||
|
os.Stdout.Write([]byte{'\x07'})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
player := e.ctx.NewPlayer(bytes.NewReader(e.pcm))
|
||||||
|
player.Play()
|
||||||
|
for player.IsPlaying() {
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- WAV parser ---
|
||||||
|
|
||||||
|
func parseWAV(data []byte) (pcm []byte, sampleRate, channels int, format oto.Format, err error) {
|
||||||
|
r := bytes.NewReader(data)
|
||||||
|
|
||||||
|
var id [4]byte
|
||||||
|
if err = binary.Read(r, binary.LittleEndian, &id); err != nil {
|
||||||
|
return nil, 0, 0, 0, fmt.Errorf("read RIFF id: %w", err)
|
||||||
|
}
|
||||||
|
if string(id[:]) != "RIFF" {
|
||||||
|
return nil, 0, 0, 0, fmt.Errorf("not a RIFF file")
|
||||||
|
}
|
||||||
|
var fileSize uint32
|
||||||
|
if err = binary.Read(r, binary.LittleEndian, &fileSize); err != nil {
|
||||||
|
return nil, 0, 0, 0, fmt.Errorf("read file size: %w", err)
|
||||||
|
}
|
||||||
|
if err = binary.Read(r, binary.LittleEndian, &id); err != nil {
|
||||||
|
return nil, 0, 0, 0, fmt.Errorf("read WAVE id: %w", err)
|
||||||
|
}
|
||||||
|
if string(id[:]) != "WAVE" {
|
||||||
|
return nil, 0, 0, 0, fmt.Errorf("not a WAVE file")
|
||||||
|
}
|
||||||
|
|
||||||
|
var hdr struct {
|
||||||
|
AudioFormat uint16
|
||||||
|
NumChannels uint16
|
||||||
|
SampleRate uint32
|
||||||
|
ByteRate uint32
|
||||||
|
BlockAlign uint16
|
||||||
|
BitsPerSample uint16
|
||||||
|
}
|
||||||
|
var fmtFound bool
|
||||||
|
|
||||||
|
for {
|
||||||
|
var chunkID [4]byte
|
||||||
|
if err = binary.Read(r, binary.LittleEndian, &chunkID); err != nil {
|
||||||
|
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return nil, 0, 0, 0, fmt.Errorf("read chunk id: %w", err)
|
||||||
|
}
|
||||||
|
var chunkSize uint32
|
||||||
|
if err = binary.Read(r, binary.LittleEndian, &chunkSize); err != nil {
|
||||||
|
return nil, 0, 0, 0, fmt.Errorf("read chunk size: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch string(chunkID[:]) {
|
||||||
|
case "fmt ":
|
||||||
|
if chunkSize < 16 {
|
||||||
|
return nil, 0, 0, 0, fmt.Errorf("fmt chunk too small (%d bytes)", chunkSize)
|
||||||
|
}
|
||||||
|
if err = binary.Read(r, binary.LittleEndian, &hdr); err != nil {
|
||||||
|
return nil, 0, 0, 0, fmt.Errorf("read fmt chunk: %w", err)
|
||||||
|
}
|
||||||
|
if chunkSize > 16 {
|
||||||
|
skip := int64(chunkSize - 16)
|
||||||
|
if chunkSize%2 != 0 {
|
||||||
|
skip++
|
||||||
|
}
|
||||||
|
if _, err = r.Seek(skip, io.SeekCurrent); err != nil {
|
||||||
|
return nil, 0, 0, 0, fmt.Errorf("skip fmt extra: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmtFound = true
|
||||||
|
case "data":
|
||||||
|
if !fmtFound {
|
||||||
|
return nil, 0, 0, 0, fmt.Errorf("data chunk before fmt chunk")
|
||||||
|
}
|
||||||
|
sz := chunkSize
|
||||||
|
if sz > uint32(r.Len()) {
|
||||||
|
sz = uint32(r.Len())
|
||||||
|
}
|
||||||
|
pcm = make([]byte, sz)
|
||||||
|
if _, err = io.ReadFull(r, pcm); err != nil {
|
||||||
|
return nil, 0, 0, 0, fmt.Errorf("read data chunk: %w", err)
|
||||||
|
}
|
||||||
|
if chunkSize%2 != 0 {
|
||||||
|
r.Seek(1, io.SeekCurrent)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
skip := int64(chunkSize)
|
||||||
|
if chunkSize%2 != 0 {
|
||||||
|
skip++
|
||||||
|
}
|
||||||
|
if _, err = r.Seek(skip, io.SeekCurrent); err != nil {
|
||||||
|
return nil, 0, 0, 0, fmt.Errorf("skip chunk: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fmtFound {
|
||||||
|
return nil, 0, 0, 0, fmt.Errorf("no fmt chunk in WAV file")
|
||||||
|
}
|
||||||
|
if pcm == nil {
|
||||||
|
return nil, 0, 0, 0, fmt.Errorf("no data chunk in WAV file")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case hdr.AudioFormat == 1 && hdr.BitsPerSample == 8:
|
||||||
|
format = oto.FormatUnsignedInt8
|
||||||
|
case hdr.AudioFormat == 1 && hdr.BitsPerSample == 16:
|
||||||
|
format = oto.FormatSignedInt16LE
|
||||||
|
case hdr.AudioFormat == 3 && hdr.BitsPerSample == 32:
|
||||||
|
format = oto.FormatFloat32LE
|
||||||
|
default:
|
||||||
|
return nil, 0, 0, 0, fmt.Errorf("unsupported WAV format: audio_format=%d, bits=%d", hdr.AudioFormat, hdr.BitsPerSample)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pcm, int(hdr.SampleRate), int(hdr.NumChannels), format, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- PTY filter loop ---
|
||||||
|
|
||||||
|
// filterLoop copies src to dst, stripping standalone BEL (0x07) bytes and calling bell()
|
||||||
|
// for each one. BEL bytes that appear as OSC string terminators are passed through unchanged
|
||||||
|
// so that the outer terminal does not get stuck mid-sequence.
|
||||||
|
//
|
||||||
|
// State machine covers the subset of ANSI/VT escape sequences that can contain 0x07:
|
||||||
|
//
|
||||||
|
// Normal → ESC → CSI (ESC [ ... final)
|
||||||
|
// ESC → OSC (ESC ] ... BEL or ST)
|
||||||
|
// ESC → Str (ESC P/X/^/_ ... ST) (DCS, SOS, PM, APC)
|
||||||
|
// ESC → <two-char>
|
||||||
|
//
|
||||||
|
// ST = String Terminator = ESC \
|
||||||
|
func filterLoop(dst io.Writer, src io.Reader, bell func()) error {
|
||||||
|
const (
|
||||||
|
stNormal = iota
|
||||||
|
stEsc // saw 0x1b
|
||||||
|
stCSI // saw 0x1b [ — terminated by 0x40–0x7E
|
||||||
|
stOSC // saw 0x1b ] — terminated by BEL or ST
|
||||||
|
stStr // saw 0x1b P/X/^/_ — terminated by ST only
|
||||||
|
stST // saw 0x1b inside stOSC or stStr, checking for '\' (ST)
|
||||||
|
)
|
||||||
|
|
||||||
|
buf := make([]byte, 32*1024)
|
||||||
|
state := stNormal
|
||||||
|
parent := stNormal // parent of stST (stOSC or stStr)
|
||||||
|
esc := make([]byte, 0, 256)
|
||||||
|
|
||||||
|
writeAll := func(p []byte) error {
|
||||||
|
_, err := dst.Write(p)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
flushEsc := func() error {
|
||||||
|
err := writeAll(esc)
|
||||||
|
esc = esc[:0]
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
n, readErr := src.Read(buf)
|
||||||
|
p := buf[:n]
|
||||||
|
i := 0
|
||||||
|
for i < len(p) {
|
||||||
|
b := p[i]
|
||||||
|
|
||||||
|
// Fast path: bulk-copy normal bytes until a special byte is hit.
|
||||||
|
if state == stNormal {
|
||||||
|
j := i
|
||||||
|
for i < len(p) && p[i] != 0x07 && p[i] != 0x1b {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if i > j {
|
||||||
|
if err := writeAll(p[j:i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i >= len(p) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
b = p[i]
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
|
||||||
|
switch state {
|
||||||
|
case stNormal:
|
||||||
|
if b == 0x07 {
|
||||||
|
bell()
|
||||||
|
} else { // 0x1b
|
||||||
|
esc = append(esc[:0], b)
|
||||||
|
state = stEsc
|
||||||
|
}
|
||||||
|
|
||||||
|
case stEsc:
|
||||||
|
switch b {
|
||||||
|
case '[':
|
||||||
|
esc = append(esc, b)
|
||||||
|
state = stCSI
|
||||||
|
case ']':
|
||||||
|
esc = append(esc, b)
|
||||||
|
state = stOSC
|
||||||
|
case 'P', 'X', '^', '_': // DCS, SOS, PM, APC
|
||||||
|
esc = append(esc, b)
|
||||||
|
state = stStr
|
||||||
|
case 0x1b:
|
||||||
|
// Another ESC: flush previous incomplete sequence, start fresh.
|
||||||
|
if err := writeAll(esc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
esc = esc[:0]
|
||||||
|
esc = append(esc, b)
|
||||||
|
// stay in stEsc
|
||||||
|
case 0x07:
|
||||||
|
// BEL right after bare ESC: flush ESC, ring bell.
|
||||||
|
if err := flushEsc(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
state = stNormal
|
||||||
|
bell()
|
||||||
|
default:
|
||||||
|
// Two-character escape sequence complete.
|
||||||
|
esc = append(esc, b)
|
||||||
|
if err := flushEsc(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
state = stNormal
|
||||||
|
}
|
||||||
|
|
||||||
|
case stCSI:
|
||||||
|
if b == 0x07 {
|
||||||
|
// Unexpected BEL mid-CSI: flush incomplete sequence, ring bell.
|
||||||
|
if err := flushEsc(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
state = stNormal
|
||||||
|
bell()
|
||||||
|
} else {
|
||||||
|
esc = append(esc, b)
|
||||||
|
if b >= 0x40 && b <= 0x7e { // final byte
|
||||||
|
if err := flushEsc(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
state = stNormal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case stOSC:
|
||||||
|
esc = append(esc, b)
|
||||||
|
if b == 0x07 {
|
||||||
|
// BEL terminates OSC — pass the whole sequence through unchanged.
|
||||||
|
if err := flushEsc(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
state = stNormal
|
||||||
|
} else if b == 0x1b {
|
||||||
|
parent = stOSC
|
||||||
|
state = stST
|
||||||
|
}
|
||||||
|
|
||||||
|
case stStr: // DCS/SOS/PM/APC — only ST terminates
|
||||||
|
esc = append(esc, b)
|
||||||
|
if b == 0x1b {
|
||||||
|
parent = stStr
|
||||||
|
state = stST
|
||||||
|
}
|
||||||
|
|
||||||
|
case stST:
|
||||||
|
esc = append(esc, b)
|
||||||
|
switch b {
|
||||||
|
case '\\': // String Terminator complete
|
||||||
|
if err := flushEsc(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
state = stNormal
|
||||||
|
case 0x07:
|
||||||
|
if parent == stOSC {
|
||||||
|
// BEL terminates OSC even after an intermediate ESC.
|
||||||
|
if err := flushEsc(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
state = stNormal
|
||||||
|
} else {
|
||||||
|
// Not a terminator for DCS/SOS/PM/APC; back to parent.
|
||||||
|
state = parent
|
||||||
|
}
|
||||||
|
case 0x1b:
|
||||||
|
// Another ESC inside string; stay in stST.
|
||||||
|
default:
|
||||||
|
// Not ST; back to parent string state.
|
||||||
|
state = parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if readErr != nil {
|
||||||
|
_ = flushEsc() // flush any buffered incomplete sequence
|
||||||
|
if isExpectedPTYErr(readErr) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return readErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isExpectedPTYErr returns true for errors that indicate normal PTY shutdown.
|
||||||
|
func isExpectedPTYErr(err error) bool {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var errno syscall.Errno
|
||||||
|
if errors.As(err, &errno) {
|
||||||
|
return errno == syscall.EIO
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Main ---
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: bellpilot <command> [args...]\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(run(os.Args[1], os.Args[2:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(name string, args []string) int {
|
||||||
|
go engine.run()
|
||||||
|
|
||||||
|
c := exec.Command(name, args...)
|
||||||
|
|
||||||
|
ptmx, err := pty.Start(c)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("start: %v", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
defer ptmx.Close()
|
||||||
|
|
||||||
|
// Propagate terminal resize to PTY.
|
||||||
|
winchCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(winchCh, syscall.SIGWINCH)
|
||||||
|
go func() {
|
||||||
|
for range winchCh {
|
||||||
|
if err := pty.InheritSize(os.Stdin, ptmx); err != nil {
|
||||||
|
log.Printf("resize: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
winchCh <- syscall.SIGWINCH
|
||||||
|
|
||||||
|
// Forward termination signals to the child.
|
||||||
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||||
|
go func() {
|
||||||
|
for sig := range sigCh {
|
||||||
|
c.Process.Signal(sig)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Put stdin in raw mode if it is a terminal.
|
||||||
|
if term.IsTerminal(int(os.Stdin.Fd())) {
|
||||||
|
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("make raw: %v", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
defer term.Restore(int(os.Stdin.Fd()), oldState)
|
||||||
|
}
|
||||||
|
|
||||||
|
go io.Copy(ptmx, os.Stdin)
|
||||||
|
|
||||||
|
if err := filterLoop(os.Stdout, ptmx, engine.ring); err != nil {
|
||||||
|
log.Printf("filter: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Wait(); err != nil {
|
||||||
|
var exitErr *exec.ExitError
|
||||||
|
if errors.As(err, &exitErr) {
|
||||||
|
return exitErr.ExitCode()
|
||||||
|
}
|
||||||
|
log.Printf("wait: %v", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user