Skip to content

Commit

Permalink
echo stdin to stdout by default (configurable)
Browse files Browse the repository at this point in the history
  • Loading branch information
mitranim committed Aug 18, 2022
1 parent 9c7b319 commit 11705eb
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 142 deletions.
7 changes: 3 additions & 4 deletions gow_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,13 @@ import (
)

type Cmd struct {
Mained
sync.Mutex
Buf [1]byte
Cmd *exec.Cmd
Stdin io.WriteCloser
}

func (self *Cmd) Init() {}

func (self *Cmd) Deinit() {
defer gg.Lock(self).Unlock()
self.DeinitUnsync()
Expand All @@ -32,11 +31,11 @@ func (self *Cmd) DeinitUnsync() {
self.Stdin = nil
}

func (self *Cmd) Restart(main *Main) {
func (self *Cmd) Restart() {
defer gg.Lock(self).Unlock()

self.DeinitUnsync()

main := self.Main()
cmd := main.Opt.MakeCmd()
stdin, err := cmd.StdinPipe()
if err != nil {
Expand Down
123 changes: 10 additions & 113 deletions gow_main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"os/exec"
"os/signal"
"syscall"
"time"

"github.com/mitranim/gg"
)
Expand All @@ -32,10 +31,9 @@ func main() {
type Main struct {
Opt Opt
Cmd Cmd
TermState TermState
Stdio Stdio
Watcher Watcher
LastChar byte
LastInst time.Time
TermState TermState
ChanSignals gg.Chan[os.Signal]
ChanRestart gg.Chan[struct{}]
ChanKill gg.Chan[syscall.Signal]
Expand All @@ -47,11 +45,11 @@ func (self *Main) Init() {
self.ChanRestart.Init()
self.ChanKill.Init()

self.Cmd.Init()
self.StdinInit()
self.Cmd.Init(self)
self.SigInit()
self.WatchInit()
self.TermInit()
self.TermState.Init(self)
self.Stdio.Init(self)
}

/**
Expand All @@ -69,121 +67,20 @@ We MUST call this manually before using `syscall.Kill` or `syscall.Exit` on the
current process. Syscalls terminate the process bypassing Go `defer`.
*/
func (self *Main) Deinit() {
self.TermDeinit()
self.Stdio.Deinit()
self.TermState.Deinit()
self.WatchDeinit()
self.SigDeinit()
self.Cmd.Deinit()
}

func (self *Main) Run() {
go self.StdinRun()
go self.Stdio.Run()
go self.SigRun()
go self.WatchRun()
self.CmdRun()
}

func (self *Main) TermInit() {
if self.Opt.Raw {
self.TermState.Init()
}
}

func (self *Main) TermDeinit() { self.TermState.Deinit() }

func (self *Main) StdinInit() { self.AfterByte(0) }

/**
See `Main.InitTerm`. "Raw mode" allows us to support our own control codes,
but we're also responsible for interpreting common ASCII codes into OS signals.
*/
func (self *Main) StdinRun() {
buf := make([]byte, 1, 1)

for {
size, err := os.Stdin.Read(buf)
if err != nil || size == 0 {
return
}
self.OnByte(buf[0])
}
}

/**
Interpret known ASCII codes as OS signals.
Otherwise forward the input to the subprocess.
*/
func (self *Main) OnByte(val byte) {
defer recLog()
defer self.AfterByte(val)

switch val {
case CODE_INTERRUPT:
self.OnCodeInterrupt()

case CODE_QUIT:
self.OnCodeQuit()

case CODE_PRINT_COMMAND:
self.OnCodePrintCommand()

case CODE_RESTART:
self.OnCodeRestart()

case CODE_STOP:
self.OnCodeStop()

default:
self.OnByteAny(val)
}
}

func (self *Main) AfterByte(val byte) {
self.LastChar = val
self.LastInst = time.Now()
}

func (self *Main) OnCodeInterrupt() {
self.OnCodeSig(CODE_INTERRUPT, syscall.SIGINT, `^C`)
}

func (self *Main) OnCodeQuit() {
self.OnCodeSig(CODE_QUIT, syscall.SIGQUIT, `^\`)
}

func (self *Main) OnCodePrintCommand() {
log.Printf(`current command: %q`, os.Args)
}

func (self *Main) OnCodeRestart() {
if self.Opt.Verb {
log.Println(`received ^R, restarting`)
}
self.Restart()
}

func (self *Main) OnCodeStop() {
self.OnCodeSig(CODE_STOP, syscall.SIGTERM, `^T`)
}

func (self *Main) OnByteAny(char byte) { self.Cmd.WriteChar(char) }

func (self *Main) OnCodeSig(code byte, sig syscall.Signal, desc string) {
if self.IsCodeRepeated(code) {
log.Printf(`received %[1]v%[1]v, shutting down`, desc)
self.Kill(sig)
return
}

if self.Opt.Verb {
log.Printf(`received %[1]v, stopping subprocess`, desc)
}
self.Cmd.Broadcast(sig)
}

func (self *Main) IsCodeRepeated(val byte) bool {
return self.LastChar == val && time.Now().Sub(self.LastInst) < time.Second
}

/**
We override Go's default signal handling to ensure cleanup before exit.
Cleanup is necessary to restore the previous terminal state and kill any
Expand Down Expand Up @@ -234,13 +131,13 @@ func (self *Main) WatchDeinit() {

func (self *Main) WatchRun() {
if self.Watcher != nil {
self.Watcher.Run(self)
self.Watcher.Run()
}
}

func (self *Main) CmdRun() {
for {
self.Cmd.Restart(self)
self.Cmd.Restart()

select {
case <-self.ChanRestart:
Expand Down
17 changes: 8 additions & 9 deletions gow_misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,6 @@ var (
`\r`, gg.Newline,
`\n`, gg.Newline,
).Replace

REP_MULTI_SINGLE = strings.NewReplacer(
"\r\n", `\n`,
"\r", `\n`,
"\n", `\n`,
).Replace
)

/**
Expand All @@ -54,7 +48,7 @@ type FsEvent interface{ Path() string }
type Watcher interface {
Init(*Main)
Deinit()
Run(*Main)
Run()
}

func commaSplit(val string) []string {
Expand All @@ -64,8 +58,6 @@ func commaSplit(val string) []string {
return strings.Split(val, `,`)
}

func commaJoin(val []string) string { return strings.Join(val, `,`) }

func cleanExtension(val string) string {
ext := filepath.Ext(val)
if len(ext) > 0 && ext[0] == '.' {
Expand Down Expand Up @@ -111,3 +103,10 @@ func withNewline[A ~string](val A) A {
}
return val + A(gg.Newline)
}

// The field is private to avoid accidental cyclic walking by pretty-printing
// tools, not for pointless "encapsulation".
type Mained struct{ main *Main }

func (self *Mained) Init(val *Main) { self.main = val }
func (self *Mained) Main() *Main { return self.main }
23 changes: 12 additions & 11 deletions gow_opt.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,18 @@ func OptDefault() Opt { return gg.FlagParseTo[Opt](nil) }

type Opt struct {
Args []string `flag:""`
Help bool `flag:"-h" desc:"Print help and exit."`
Cmd string `flag:"-g" init:"go" desc:"Go tool to use."`
Verb bool `flag:"-v" desc:"Verbose logging."`
ClearHard bool `flag:"-c" desc:"Clear terminal on restart."`
ClearSoft bool `flag:"-s" desc:"Soft-clear terminal, keeping scrollback."`
Raw bool `flag:"-r" init:"true" desc:"Enable terminal raw mode and hotkeys."`
Sep FlagStrMultiline `flag:"-S" desc:"Separator printed after each run; multi; supports \\n."`
Trace bool `flag:"-t" desc:"Print error trace on exit. Useful for debugging gow."`
Extensions FlagExtensions `flag:"-e" init:"go,mod" desc:"Extensions to watch; multi."`
Watch FlagWatch `flag:"-w" init:"." desc:"Paths to watch, relative to CWD; multi."`
IgnoredPaths FlagIgnoredPaths `flag:"-i" desc:"Ignored paths, relative to CWD; multi."`
Help bool `flag:"-h" desc:"Print help and exit."`
Cmd string `flag:"-g" init:"go" desc:"Go tool to use."`
Verb bool `flag:"-v" desc:"Verbose logging."`
ClearHard bool `flag:"-c" desc:"Clear terminal on restart."`
ClearSoft bool `flag:"-s" desc:"Soft-clear terminal, keeping scrollback."`
Raw bool `flag:"-r" init:"true" desc:"Enable terminal raw mode and hotkeys."`
Sep FlagStrMultiline `flag:"-S" desc:"Separator printed after each run; multi; supports \\n."`
Trace bool `flag:"-t" desc:"Print error trace on exit. Useful for debugging gow."`
RawEcho bool `flag:"-re" init:"true" desc:"In raw mode, echo stdin to stdout like most apps."`
Extensions FlagExtensions `flag:"-e" init:"go,mod" desc:"Extensions to watch; multi."`
Watch FlagWatch `flag:"-w" init:"." desc:"Paths to watch, relative to CWD; multi."`
IgnoredPaths FlagIgnoredPaths `flag:"-i" desc:"Ignored paths, relative to CWD; multi."`
}

func (self *Opt) Init(src []string) {
Expand Down
Loading

0 comments on commit 11705eb

Please sign in to comment.