From 77eee7bf6d25ac9323fccdf2c2c6e1dcb6acebe2 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Thu, 8 Aug 2024 09:48:48 -0700 Subject: [PATCH 1/5] Use/demo history from PR for https://github.com/golang/go/issues/68780 --- example/main.go | 4 ++- go.mod | 1 + go.sum | 4 +-- terminal.go | 72 ++++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 74 insertions(+), 7 deletions(-) diff --git a/example/main.go b/example/main.go index 827fb19..654713e 100644 --- a/example/main.go +++ b/example/main.go @@ -2,6 +2,7 @@ package main import ( "errors" + "flag" "fmt" "io" "os" @@ -56,7 +57,7 @@ func autoCompleteCallback(line string, pos int, key rune) (newLine string, newPo func Main() int { // Pending https://github.com/golang/go/issues/68780 - // flagHistory := flag.String("history", "", "History `file` to use") + flagHistory := flag.String("history", "/tmp/terminal_history", "History `file` to use") cli.Main() t, err := terminal.Open() if err != nil { @@ -65,6 +66,7 @@ func Main() int { defer t.Close() t.SetPrompt("Terminal demo> ") t.LoggerSetup() + t.SetHistoryFile(*flagHistory) fmt.Fprintf(t.Out, "Terminal is open\nis valid %t\nuse exit or ^D or ^C to exit\n", t.IsTerminal()) fmt.Fprintf(t.Out, "Use 'prompt ' to change the prompt\n") fmt.Fprintf(t.Out, "Try 'after duration text...' to see text showing in the middle of edits after said duration\n") diff --git a/go.mod b/go.mod index 49251d6..b6f19fd 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( ) // replace fortio.org/cli => ../cli +replace golang.org/x/term => github.com/ldemailly/term v0.0.0-20240808164309-1ca6c26d14bf require ( fortio.org/struct2env v0.4.1 // indirect diff --git a/go.sum b/go.sum index 057d009..664b212 100644 --- a/go.sum +++ b/go.sum @@ -8,9 +8,9 @@ fortio.org/version v1.0.4 h1:FWUMpJ+hVTNc4RhvvOJzb0xesrlRmG/a+D6bjbQ4+5U= fortio.org/version v1.0.4/go.mod h1:2JQp9Ax+tm6QKiGuzR5nJY63kFeANcgrZ0osoQFDVm0= github.com/kortschak/goroutine v1.1.2 h1:lhllcCuERxMIK5cYr8yohZZScL1na+JM5JYPRclWjck= github.com/kortschak/goroutine v1.1.2/go.mod h1:zKpXs1FWN/6mXasDQzfl7g0LrGFIOiA6cLs9eXKyaMY= +github.com/ldemailly/term v0.0.0-20240808164309-1ca6c26d14bf h1:tSC3QHTyGX/bno3fLgNBikbqJ5+/qBh8bFJVnCXsBrw= +github.com/ldemailly/term v0.0.0-20240808164309-1ca6c26d14bf/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/crypto/x509roots/fallback v0.0.0-20240626151235-a6a393ffd658 h1:i7K6wQLN/0oxF7FT3tKkfMCstxoT4VGG36YIB9ZKLzI= golang.org/x/crypto/x509roots/fallback v0.0.0-20240626151235-a6a393ffd658/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8= golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= diff --git a/terminal.go b/terminal.go index e9ed945..647f64d 100644 --- a/terminal.go +++ b/terminal.go @@ -2,6 +2,7 @@ package terminal // import "fortio.org/terminal" import ( + "bufio" "io" "os" @@ -10,10 +11,11 @@ import ( ) type Terminal struct { - fd int - oldState *term.State - term *term.Terminal - Out io.Writer + fd int + oldState *term.State + term *term.Terminal + Out io.Writer + historyFile string } // CRWriter is a writer that adds \r before each \n. @@ -100,6 +102,62 @@ func (t *Terminal) LoggerSetup() { log.SetColorMode() } +func (t *Terminal) SetHistoryFile(f string) { + t.historyFile = f + entries := readOrCreateHistory(f) + for _, e := range entries { + t.term.AddToHistory(e) + } + log.Infof("Loaded %d history entries from %s", len(entries), f) +} + +func readOrCreateHistory(f string) []string { + if f == "" { + log.Infof("No history file specified") + return nil + } + // open file or create it + h, err := os.OpenFile(f, os.O_RDWR|os.O_CREATE, 0o600) + if err != nil { + log.Errf("Error opening history file %s: %v", f, err) + return nil + } + defer h.Close() + // read lines separated by \n + var lines []string + scanner := bufio.NewScanner(h) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + if err := scanner.Err(); err != nil { + log.Errf("Error reading history file %s: %v", f, err) + return nil + } + return lines +} + +func saveHistory(f string, h []string) { + if f == "" { + log.Infof("No history file specified") + return + } + // open file or create it + hf, err := os.OpenFile(f, os.O_RDWR|os.O_CREATE, 0o600) + if err != nil { + log.Errf("Error opening history file %s: %v", f, err) + return + } + defer hf.Close() + // write lines separated by \n + for _, l := range h { + _, err := hf.WriteString(l + "\n") + if err != nil { + log.Errf("Error writing history file %s: %v", f, err) + return + } + } +} + func (t *Terminal) Close() error { if t.oldState == nil { return nil @@ -107,6 +165,12 @@ func (t *Terminal) Close() error { err := term.Restore(t.fd, t.oldState) t.oldState = nil t.Out = os.Stderr + // saving history if any + if t.historyFile != "" { + h := t.term.History() + log.Infof("Saving history (%d commands) to %s", len(h), t.historyFile) + saveHistory(t.historyFile, h) + } return err } From a8110b5f4594f5fa0b4998c30d7eaf575e5c6181 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Thu, 8 Aug 2024 11:00:47 -0700 Subject: [PATCH 2/5] further encapsulate x/term by not needing to check for term.ErrPasteIndicator, also add self to callback --- example/main.go | 7 ++----- go.mod | 2 +- terminal.go | 17 ++++++++++++++--- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/example/main.go b/example/main.go index 654713e..4754729 100644 --- a/example/main.go +++ b/example/main.go @@ -12,7 +12,6 @@ import ( "fortio.org/cli" "fortio.org/log" "fortio.org/terminal" - "golang.org/x/term" ) func main() { @@ -31,13 +30,13 @@ var commands = []string{promptCmd, afterCmd, exitCmd, helpCmd, testMLCmd} // func(line string, pos int, key rune) (newLine string, newPos int, ok bool) -func autoCompleteCallback(line string, pos int, key rune) (newLine string, newPos int, ok bool) { +func autoCompleteCallback(t *terminal.Terminal, line string, pos int, key rune) (newLine string, newPos int, ok bool) { log.LogVf("AutoCompleteCallback: %q %d %q", line, pos, key) if key != '\t' { return // only tab for now } if len(line) == 0 { - log.Infof("Available commands: %v", commands) + fmt.Fprintf(t.Out, "Available commands: %v\n", commands) return } if pos != len(line) { @@ -80,8 +79,6 @@ func Main() int { case errors.Is(err, io.EOF): log.Infof("EOF received, exiting.") return 0 - case errors.Is(err, term.ErrPasteIndicator): - log.Infof("Paste indicator received, which is fine.") default: return log.FErrf("Error reading line: %v", err) } diff --git a/go.mod b/go.mod index b6f19fd..518cb5b 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( golang.org/x/term v0.23.0 ) -// replace fortio.org/cli => ../cli +//nolint:gomoddirectives // pending https://github.com/golang/term/pull/15 replace golang.org/x/term => github.com/ldemailly/term v0.0.0-20240808164309-1ca6c26d14bf require ( diff --git a/terminal.go b/terminal.go index 647f64d..69c70b0 100644 --- a/terminal.go +++ b/terminal.go @@ -3,6 +3,7 @@ package terminal // import "fortio.org/terminal" import ( "bufio" + "errors" "io" "os" @@ -175,15 +176,25 @@ func (t *Terminal) Close() error { } func (t *Terminal) ReadLine() (string, error) { - return t.term.ReadLine() + c, err := t.term.ReadLine() + // That error isn't an error that needs to be propagated, + // it's just to allow copy/paste without autocomplete. + if err != nil && errors.Is(err, term.ErrPasteIndicator) { + return c, nil + } + return c, err } func (t *Terminal) SetPrompt(s string) { t.term.SetPrompt(s) } -type AutoCompleteCallback func(line string, pos int, key rune) (newLine string, newPos int, ok bool) +// Pass "this" back so AutoCompleteCallback can use t.Out etc. +// (compared to the original x/term callback). +type AutoCompleteCallback func(t *Terminal, line string, pos int, key rune) (newLine string, newPos int, ok bool) func (t *Terminal) SetAutoCompleteCallback(f AutoCompleteCallback) { - t.term.AutoCompleteCallback = f + t.term.AutoCompleteCallback = func(line string, pos int, key rune) (newLine string, newPos int, ok bool) { + return f(t, line, pos, key) + } } From 76a6ba8f92408edddfe8bc6e74a70c9953ab4d47 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Thu, 8 Aug 2024 13:11:52 -0700 Subject: [PATCH 3/5] switch to our fork so things can be go installed --- go.mod | 9 +++------ go.sum | 8 ++++++-- terminal.go | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 518cb5b..efcb4bf 100644 --- a/go.mod +++ b/go.mod @@ -5,16 +5,13 @@ go 1.22.6 require ( fortio.org/cli v1.8.0 fortio.org/log v1.16.0 - golang.org/x/term v0.23.0 + fortio.org/term v0.23.0-fortio-1 ) -//nolint:gomoddirectives // pending https://github.com/golang/term/pull/15 -replace golang.org/x/term => github.com/ldemailly/term v0.0.0-20240808164309-1ca6c26d14bf - require ( fortio.org/struct2env v0.4.1 // indirect fortio.org/version v1.0.4 // indirect github.com/kortschak/goroutine v1.1.2 // indirect - golang.org/x/crypto/x509roots/fallback v0.0.0-20240626151235-a6a393ffd658 // indirect - golang.org/x/sys v0.23.0 // indirect + golang.org/x/crypto/x509roots/fallback v0.0.0-20240806160748-b2d3a6a4b4d3 // indirect + golang.org/x/sys v0.24.0 // indirect ) diff --git a/go.sum b/go.sum index 664b212..d9044ab 100644 --- a/go.sum +++ b/go.sum @@ -4,13 +4,17 @@ fortio.org/log v1.16.0 h1:GhU8/9NkYZmEIzvTN/DTMedDAStLJraWUUVUA2EbNDc= fortio.org/log v1.16.0/go.mod h1:t58Spg9njjymvRioh5F6qKGSupEsnMjXLGWIS1i3khE= fortio.org/struct2env v0.4.1 h1:rJludAMO5eBvpWplWEQNqoVDFZr4RWMQX7RUapgZyc0= fortio.org/struct2env v0.4.1/go.mod h1:lENUe70UwA1zDUCX+8AsO663QCFqYaprk5lnPhjD410= +fortio.org/term v0.23.0-fortio-1 h1:bBfsi9yGE5aI5FDnBxluIc7+7Gc2vWdgdckJeDYRGsc= +fortio.org/term v0.23.0-fortio-1/go.mod h1:7buBfn81wEJUGWiVjFNiUE/vxWs5FdM9c7PyZpZRS30= fortio.org/version v1.0.4 h1:FWUMpJ+hVTNc4RhvvOJzb0xesrlRmG/a+D6bjbQ4+5U= fortio.org/version v1.0.4/go.mod h1:2JQp9Ax+tm6QKiGuzR5nJY63kFeANcgrZ0osoQFDVm0= github.com/kortschak/goroutine v1.1.2 h1:lhllcCuERxMIK5cYr8yohZZScL1na+JM5JYPRclWjck= github.com/kortschak/goroutine v1.1.2/go.mod h1:zKpXs1FWN/6mXasDQzfl7g0LrGFIOiA6cLs9eXKyaMY= -github.com/ldemailly/term v0.0.0-20240808164309-1ca6c26d14bf h1:tSC3QHTyGX/bno3fLgNBikbqJ5+/qBh8bFJVnCXsBrw= -github.com/ldemailly/term v0.0.0-20240808164309-1ca6c26d14bf/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/crypto/x509roots/fallback v0.0.0-20240626151235-a6a393ffd658 h1:i7K6wQLN/0oxF7FT3tKkfMCstxoT4VGG36YIB9ZKLzI= golang.org/x/crypto/x509roots/fallback v0.0.0-20240626151235-a6a393ffd658/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8= +golang.org/x/crypto/x509roots/fallback v0.0.0-20240806160748-b2d3a6a4b4d3 h1:oWb21rU9Q9XrRwXLB7jHc1rbp6EiiimZZv5MLxpu4T0= +golang.org/x/crypto/x509roots/fallback v0.0.0-20240806160748-b2d3a6a4b4d3/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8= golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/terminal.go b/terminal.go index 69c70b0..48b2b06 100644 --- a/terminal.go +++ b/terminal.go @@ -8,7 +8,7 @@ import ( "os" "fortio.org/log" - "golang.org/x/term" + "fortio.org/term" ) type Terminal struct { From 9bb4534f68848ebbb51fac4b517a5e6c1275b578 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Thu, 8 Aug 2024 13:18:40 -0700 Subject: [PATCH 4/5] review comment: errors.Is(nil,...) is safe and false Co-authored-by: ccoVeille <3875889+ccoVeille@users.noreply.github.com> --- terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal.go b/terminal.go index 48b2b06..2975ef9 100644 --- a/terminal.go +++ b/terminal.go @@ -179,7 +179,7 @@ func (t *Terminal) ReadLine() (string, error) { c, err := t.term.ReadLine() // That error isn't an error that needs to be propagated, // it's just to allow copy/paste without autocomplete. - if err != nil && errors.Is(err, term.ErrPasteIndicator) { + if errors.Is(err, term.ErrPasteIndicator) { return c, nil } return c, err From 1117422292d5d3e5d9d0e3eccf8dc457989766be Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Thu, 8 Aug 2024 13:24:57 -0700 Subject: [PATCH 5/5] don't save history if not interactive/terminal --- terminal.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/terminal.go b/terminal.go index 2975ef9..f31a5f3 100644 --- a/terminal.go +++ b/terminal.go @@ -104,6 +104,10 @@ func (t *Terminal) LoggerSetup() { } func (t *Terminal) SetHistoryFile(f string) { + if !t.IsTerminal() { + log.Infof("Not a terminal, not setting history file") + return + } t.historyFile = f entries := readOrCreateHistory(f) for _, e := range entries {