From 11705ebc30f58ef0ec126d9bb7589d2711028d1f Mon Sep 17 00:00:00 2001 From: Nelo Mitranim Date: Thu, 18 Aug 2022 12:42:01 +0200 Subject: [PATCH] echo stdin to stdout by default (configurable) --- gow_cmd.go | 7 +-- gow_main.go | 123 ++++------------------------------------ gow_misc.go | 17 +++--- gow_opt.go | 23 ++++---- gow_stdio.go | 134 ++++++++++++++++++++++++++++++++++++++++++++ gow_term.go | 22 ++++++-- gow_watch_notify.go | 6 +- 7 files changed, 190 insertions(+), 142 deletions(-) create mode 100644 gow_stdio.go diff --git a/gow_cmd.go b/gow_cmd.go index 9766668..d31a4a4 100644 --- a/gow_cmd.go +++ b/gow_cmd.go @@ -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() @@ -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 { diff --git a/gow_main.go b/gow_main.go index 030a0f7..1a3cab6 100644 --- a/gow_main.go +++ b/gow_main.go @@ -11,7 +11,6 @@ import ( "os/exec" "os/signal" "syscall" - "time" "github.com/mitranim/gg" ) @@ -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] @@ -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) } /** @@ -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 @@ -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: diff --git a/gow_misc.go b/gow_misc.go index e1f2758..602ff5c 100644 --- a/gow_misc.go +++ b/gow_misc.go @@ -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 ) /** @@ -54,7 +48,7 @@ type FsEvent interface{ Path() string } type Watcher interface { Init(*Main) Deinit() - Run(*Main) + Run() } func commaSplit(val string) []string { @@ -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] == '.' { @@ -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 } diff --git a/gow_opt.go b/gow_opt.go index 17abd7d..48f96a4 100644 --- a/gow_opt.go +++ b/gow_opt.go @@ -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) { diff --git a/gow_stdio.go b/gow_stdio.go new file mode 100644 index 0000000..eb096d9 --- /dev/null +++ b/gow_stdio.go @@ -0,0 +1,134 @@ +package main + +import ( + "os" + "syscall" + "time" + + "github.com/mitranim/gg" +) + +type Stdio struct { + Mained + Buf []byte + LastChar byte + LastInst time.Time +} + +func (self *Stdio) Init(main *Main) { + self.Mained.Init(main) + self.Buf = make([]byte, 1) +} + +/** +Doesn't require special cleanup before stopping `gow`. We run only one stdio +loop, without ever replacing it. +*/ +func (*Stdio) Deinit() {} + +/** +See `(*TermState).Init`. Terminal raw mode allows us to support our own control +codes, but we're also responsible for interpreting common ASCII codes into OS +signals and for echoing other characters to stdout. +*/ +func (self *Stdio) Run() { + if !self.Main().Opt.Raw { + return + } + + self.LastInst = time.Now() + + for { + size, err := os.Stdin.Read(self.Buf) + if err != nil || size == 0 { + return + } + self.OnByte(self.Buf[0]) + } +} + +/** +Interpret known ASCII codes as OS signals. +Otherwise forward the input to the subprocess. +*/ +func (self *Stdio) OnByte(char byte) { + defer recLog() + defer self.AfterByte(char) + + switch char { + 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(char) + } +} + +func (self *Stdio) AfterByte(char byte) { + self.LastChar = char + self.LastInst = time.Now() +} + +func (self *Stdio) OnCodeInterrupt() { + self.OnCodeSig(CODE_INTERRUPT, syscall.SIGINT, `^C`) +} + +func (self *Stdio) OnCodeQuit() { + self.OnCodeSig(CODE_QUIT, syscall.SIGQUIT, `^\`) +} + +func (self *Stdio) OnCodePrintCommand() { + log.Printf(`current command: %q`, os.Args) +} + +func (self *Stdio) OnCodeRestart() { + main := self.Main() + if main.Opt.Verb { + log.Println(`received ^R, restarting`) + } + main.Restart() +} + +func (self *Stdio) OnCodeStop() { + self.OnCodeSig(CODE_STOP, syscall.SIGTERM, `^T`) +} + +func (self *Stdio) OnByteAny(char byte) { + main := self.Main() + main.Cmd.WriteChar(char) + + if main.Opt.RawEcho { + gg.Nop2(os.Stdout.Write(self.Buf)) + } +} + +func (self *Stdio) OnCodeSig(code byte, sig syscall.Signal, desc string) { + main := self.Main() + + if self.IsCodeRepeated(code) { + log.Printf(`received %[1]v%[1]v, shutting down`, desc) + main.Kill(sig) + return + } + + if main.Opt.Verb { + log.Printf(`received %[1]v, stopping subprocess`, desc) + } + main.Cmd.Broadcast(sig) +} + +func (self *Stdio) IsCodeRepeated(char byte) bool { + return self.LastChar == char && time.Since(self.LastInst) < time.Second +} diff --git a/gow_term.go b/gow_term.go index 6ab19fb..2d934d2 100644 --- a/gow_term.go +++ b/gow_term.go @@ -25,9 +25,13 @@ References: */ type TermState struct{ gg.Opt[unix.Termios] } -func (self *TermState) Init() { +func (self *TermState) Init(main *Main) { self.Deinit() + if !main.Opt.Raw { + return + } + state, err := unix.IoctlGetTermios(FD_TERM, ioctlReadTermios) if err != nil { log.Println(`unable to read terminal state:`, err) @@ -35,12 +39,22 @@ func (self *TermState) Init() { } prev := *state + /** + Don't echo stdin to stdout. Most terminals, in addition to echoing non-special + characters, also have special support for various ASCII control codes. Codes + that send signals are cosmetically printed as hotkeys such as `^C`, `^R`, + and so on. The delete code (127) should cause the terminal to delete one + character before the caret, moving the caret. At the time of writing, the + built-in MacOS terminal doesn't properly echo characters when operating in + raw mode. For example, the delete code is printed back as `^?`, which is + rather jarring. As a workaround, we suppress default echoing in raw mode, + and do it ourselves in the `Stdio` type. + */ + state.Lflag &^= unix.ECHO + // Don't buffer lines. state.Lflag &^= unix.ICANON - // Don't echo characters or special codes. - state.Lflag &^= unix.ECHO - // No signals. state.Lflag &^= unix.ISIG diff --git a/gow_watch_notify.go b/gow_watch_notify.go index 638263e..34afff1 100644 --- a/gow_watch_notify.go +++ b/gow_watch_notify.go @@ -9,11 +9,13 @@ import ( // Implementation of `Watcher` that uses "github.com/rjeczalik/notify". type WatchNotify struct { + Mained Done gg.Chan[struct{}] Events gg.Chan[notify.EventInfo] } func (self *WatchNotify) Init(main *Main) { + self.Mained.Init(main) self.Done.Init() self.Events.InitCap(1) @@ -36,7 +38,9 @@ func (self *WatchNotify) Deinit() { } } -func (self WatchNotify) Run(main *Main) { +func (self WatchNotify) Run() { + main := self.Main() + for { select { case <-self.Done: