Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle limited capacity history and the order, truncate history file as needed #8

Merged
merged 6 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ all: lint test demo
GO_BUILD_TAGS:=no_net,no_json,no_pprof

demo:
go run -tags $(GO_BUILD_TAGS) ./example/ -loglevel debug
go run -tags $(GO_BUILD_TAGS) ./example/ -loglevel debug -only-valid
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
go run -tags $(GO_BUILD_TAGS) ./example/ -loglevel debug -only-valid
go run -tags $(GO_BUILD_TAGS) ./example/ -loglevel debug -only-valid

I'm a crazy person 🤪

BTW, do you know unmake?

Makefile linter
https://github.com/mcandre/unmake

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can tell :) fixing it, installed unmake but will ignore

warning: Makefile: MAKEFILE_PRECEDENCE: lowercase Makefile to makefile for launch speed
warning: Makefile:1: STRICT_POSIX: lead makefiles with the ".POSIX:" compliance marker, or else rename include files like *.include.mk
warning: Makefile:8: COMMAND_COMMENT: comment embedded inside commands will forward to the shell interpreter
warning: Makefile:14: COMMAND_COMMENT: comment embedded inside commands will forward to the shell interpreter

and

error: Makefile:3:15 found "=", expected: LF, comment, inline command, macro expansion, target, wait prerequisite marker

(it doesn't know about := ? uh?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dunno about unmake, but it helped me to spot some issues


tinygo-demo:
# No luck on mac https://github.com/tinygo-org/tinygo/issues/4395
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[![Go Reference](https://pkg.go.dev/badge/fortio.org/terminal.svg)](https://pkg.go.dev/fortio.org/terminal)
# terminal

Wrapper around [x/term](https://github.com/golang/term) for a "readline" like library for
Expand Down
55 changes: 45 additions & 10 deletions example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,39 @@
return
}

func AddOrReplaceHistory(t *terminal.Terminal, replace bool, l string) {
// if in default auto mode, we don't manage history
// we also don't add empty commands. (at start of program)
if t.AutoHistory() || l == "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also add something to do not log command starting with space, as it's a common pattern to ignore thing in history

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

never heard of leading space preventing history before:

$    echo "this had space"
$  history | tail
 568     echo "this had space"
 569  history | tail

also I do want spaces in history for grol indentation

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return
}
log.LogVf("Adding to history %q replace %t", l, replace)
if replace {
t.ReplaceLatest(l)
} else {
t.AddToHistory(l)
}

Check warning on line 68 in example/main.go

View check run for this annotation

Codecov / codecov/patch

example/main.go#L57-L68

Added lines #L57 - L68 were not covered by tests
}

func Main() int {
// Pending https://github.com/golang/go/issues/68780
flagHistory := flag.String("history", "/tmp/terminal_history", "History `file` to use")
flagMaxHistory := flag.Int("max-history", 10, "Max number of history lines to keep")
flagOnlyValid := flag.Bool("only-valid", false, "Demonstrates filtering of history, only adding valid commands to it")

Check warning on line 75 in example/main.go

View check run for this annotation

Codecov / codecov/patch

example/main.go#L74-L75

Added lines #L74 - L75 were not covered by tests
cli.Main()
t, err := terminal.Open()
if err != nil {
return log.FErrf("Error opening terminal: %v", err)
}
defer t.Close()
onlyValid := *flagOnlyValid
if onlyValid {
t.SetAutoHistory(false)
}

Check warning on line 85 in example/main.go

View check run for this annotation

Codecov / codecov/patch

example/main.go#L82-L85

Added lines #L82 - L85 were not covered by tests
t.SetPrompt("Terminal demo> ")
t.LoggerSetup()
if err := t.SetHistoryFile(*flagHistory); err != nil {
t.NewHistory(*flagMaxHistory)
if err = t.SetHistoryFile(*flagHistory); err != nil {

Check warning on line 89 in example/main.go

View check run for this annotation

Codecov / codecov/patch

example/main.go#L88-L89

Added lines #L88 - L89 were not covered by tests
// error already logged
return 1
}
Expand All @@ -74,8 +95,13 @@
fmt.Fprintf(t.Out, "Try 'after duration text...' to see text showing in the middle of edits after said duration\n")
fmt.Fprintf(t.Out, "Try <tab> for auto completion\n")
t.SetAutoCompleteCallback(autoCompleteCallback)
previousCommandWasValid := true // won't be used because `line` is empty at start
isValidCommand := true
var cmd string

Check warning on line 100 in example/main.go

View check run for this annotation

Codecov / codecov/patch

example/main.go#L98-L100

Added lines #L98 - L100 were not covered by tests
for {
l, err := t.ReadLine()
// Replace unless the previous command was valid.
AddOrReplaceHistory(t, !previousCommandWasValid, cmd)
cmd, err = t.ReadLine()

Check warning on line 104 in example/main.go

View check run for this annotation

Codecov / codecov/patch

example/main.go#L102-L104

Added lines #L102 - L104 were not covered by tests
switch {
case err == nil:
// no error is good, nothing in this switch.
Expand All @@ -85,14 +111,18 @@
default:
return log.FErrf("Error reading line: %v", err)
}
log.Infof("Read line got: %q", l)
log.Infof("Read line got: %q", cmd)
// Save previous command validity to know whether this one should replace it in history or not.
previousCommandWasValid = isValidCommand
isValidCommand = false // not valid unless proven otherwise (reaches the end validations etc)

Check warning on line 117 in example/main.go

View check run for this annotation

Codecov / codecov/patch

example/main.go#L114-L117

Added lines #L114 - L117 were not covered by tests
switch {
case l == exitCmd:
case cmd == exitCmd:

Check warning on line 119 in example/main.go

View check run for this annotation

Codecov / codecov/patch

example/main.go#L119

Added line #L119 was not covered by tests
return 0
case l == helpCmd:
case cmd == helpCmd:

Check warning on line 121 in example/main.go

View check run for this annotation

Codecov / codecov/patch

example/main.go#L121

Added line #L121 was not covered by tests
fmt.Fprintf(t.Out, "Available commands: %v\n", commands)
case strings.HasPrefix(l, afterCmd):
parts := strings.SplitN(l, " ", 3)
isValidCommand = true
case strings.HasPrefix(cmd, afterCmd):
parts := strings.SplitN(cmd, " ", 3)

Check warning on line 125 in example/main.go

View check run for this annotation

Codecov / codecov/patch

example/main.go#L123-L125

Added lines #L123 - L125 were not covered by tests
if len(parts) < 3 {
fmt.Fprintf(t.Out, "Usage: %s <duration> <text...>\n", afterCmd)
continue
Expand All @@ -107,10 +137,15 @@
time.Sleep(dur)
fmt.Fprintf(t.Out, "%s\n", parts[2])
}()
case strings.HasPrefix(l, promptCmd):
t.SetPrompt(l[len(promptCmd):])
isValidCommand = true
case strings.HasPrefix(cmd, promptCmd):
if onlyValid {
t.AddToHistory(cmd)
}
t.SetPrompt(cmd[len(promptCmd):])
isValidCommand = true

Check warning on line 146 in example/main.go

View check run for this annotation

Codecov / codecov/patch

example/main.go#L140-L146

Added lines #L140 - L146 were not covered by tests
default:
fmt.Fprintf(t.Out, "Unknown command %q\n", l)
fmt.Fprintf(t.Out, "Unknown command %q\n", cmd)

Check warning on line 148 in example/main.go

View check run for this annotation

Codecov / codecov/patch

example/main.go#L148

Added line #L148 was not covered by tests
}
}
}
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ go 1.22.6
require (
fortio.org/cli v1.8.0
fortio.org/log v1.16.0
fortio.org/term v0.23.0-fortio-1
fortio.org/term v0.23.0-fortio-4
)

// replace fortio.org/term => ../term

require (
fortio.org/struct2env v0.4.1 // indirect
fortio.org/version v1.0.4 // indirect
Expand Down
8 changes: 2 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@ 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/term v0.23.0-fortio-4 h1:vLK1/UYCLW1kznWcZb0Zcgmb6rC3Kw0mDhDCoI63boY=
fortio.org/term v0.23.0-fortio-4/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=
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=
77 changes: 64 additions & 13 deletions terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"errors"
"io"
"os"
"slices"
"strconv"

"fortio.org/log"
"fortio.org/term"
Expand All @@ -17,6 +19,8 @@
term *term.Terminal
Out io.Writer
historyFile string
capacity int
autoHistory bool
}

// Open opens stdin as a terminal, do `defer terminal.Close()`
Expand All @@ -41,6 +45,7 @@
return nil, err
}
t.term.SetBracketedPasteMode(true) // Seems useful to have it on by default.
t.capacity = term.DefaultHistoryEntries

Check warning on line 48 in terminal.go

View check run for this annotation

Codecov / codecov/patch

terminal.go#L48

Added line #L48 was not covered by tests
return t, nil
}

Expand All @@ -62,6 +67,10 @@
log.Infof("No history file specified")
return nil
}
if t.capacity <= 0 {
log.Infof("No history capacity set, ignoring history file %s", f)
return nil
}
if !t.IsTerminal() {
log.Infof("Not a terminal, not setting history file")
return nil
Expand All @@ -72,10 +81,16 @@
t.historyFile = "" // so we don't try to save during defer'ed close if we can't read
return err
}
for _, e := range entries {
start := 0
if len(entries) > t.capacity {
log.Infof("History file %s has more than %d entries, truncating.", f, t.capacity)
start = len(entries) - t.capacity
} else {
log.Infof("Loaded %d history entries from %s", len(entries), f)
}
for _, e := range entries[start:] {

Check warning on line 91 in terminal.go

View check run for this annotation

Codecov / codecov/patch

terminal.go#L84-L91

Added lines #L84 - L91 were not covered by tests
t.term.AddToHistory(e)
}
log.Infof("Loaded %d history entries from %s", len(entries), f)
return nil
}

Expand All @@ -92,10 +107,32 @@
}

// NewHistory creates/resets the history to a new one with the given capacity.
// need + 1 to fit "pending" command.
func (t *Terminal) NewHistory(capacity int) {
if capacity < 0 {
log.Errf("Invalid history capacity %d, ignoring", capacity)
return
}
t.capacity = capacity

Check warning on line 116 in terminal.go

View check run for this annotation

Codecov / codecov/patch

terminal.go#L112-L116

Added lines #L112 - L116 were not covered by tests
t.term.NewHistory(capacity)
}

// SetAutoHistory enables/disables auto history (default is enabled).
func (t *Terminal) SetAutoHistory(enabled bool) {
t.autoHistory = enabled
t.term.AutoHistory(enabled)

Check warning on line 123 in terminal.go

View check run for this annotation

Codecov / codecov/patch

terminal.go#L121-L123

Added lines #L121 - L123 were not covered by tests
}

// AutoHistory returns the current auto history setting.
func (t *Terminal) AutoHistory() bool {
return t.autoHistory

Check warning on line 128 in terminal.go

View check run for this annotation

Codecov / codecov/patch

terminal.go#L127-L128

Added lines #L127 - L128 were not covered by tests
Comment on lines +122 to +128
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you forked golang/term in fortio, but I find strange that you now have two flags about autoHistory, one in fortio/term and one in fortio/terminal

BTW, maybe renaming fortio/term to something like fortio/golang-term-fork would make it easier to understand why fortio has now two "term" packages

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as mentioned, this one (terminal) is standalone and fully encapsulate what happens to be an implementation detail

this being said, others that for instance don't want fortio cli/logger/etc... might use the lower level term directly if they also don't want to wait for PRs to merge upstream (which may or may not ever happen)

}

// ReplaceLatest replaces the current history with the given commands, returns the previous value.
func (t *Terminal) ReplaceLatest(command string) string {
return t.term.ReplaceLatest(command)

Check warning on line 133 in terminal.go

View check run for this annotation

Codecov / codecov/patch

terminal.go#L132-L133

Added lines #L132 - L133 were not covered by tests
}

func readOrCreateHistory(f string) ([]string, error) {
// open file or create it
h, err := os.OpenFile(f, os.O_RDWR|os.O_CREATE, 0o600)
Expand All @@ -108,7 +145,14 @@
var lines []string
scanner := bufio.NewScanner(h)
for scanner.Scan() {
lines = append(lines, scanner.Text())
// unquote to get the actual command
rl := scanner.Text()
l, err := strconv.Unquote(rl)
if err != nil {
log.Errf("Error unquoting history file %s for %q: %v", f, rl, err)
return nil, err
}
lines = append(lines, l)

Check warning on line 155 in terminal.go

View check run for this annotation

Codecov / codecov/patch

terminal.go#L148-L155

Added lines #L148 - L155 were not covered by tests
}
if err := scanner.Err(); err != nil {
log.Errf("Error reading history file %s: %v", f, err)
Expand All @@ -118,20 +162,16 @@
}

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)
hf, err := os.OpenFile(f, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o600)

Check warning on line 166 in terminal.go

View check run for this annotation

Codecov / codecov/patch

terminal.go#L166

Added line #L166 was not covered by tests
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")
_, err := hf.WriteString(strconv.Quote(l) + "\n")

Check warning on line 174 in terminal.go

View check run for this annotation

Codecov / codecov/patch

terminal.go#L174

Added line #L174 was not covered by tests
if err != nil {
log.Errf("Error writing history file %s: %v", f, err)
return
Expand All @@ -143,15 +183,26 @@
if t.oldState == nil {
return nil
}
// To avoid prompt being repeated on the last line (shouldn't be necessary but... is
// consider fixing in term instead)
t.term.SetPrompt("") // will still reprint the last command on ^C in middle of typing.

Check warning on line 188 in terminal.go

View check run for this annotation

Codecov / codecov/patch

terminal.go#L188

Added line #L188 was not covered by tests
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)
if t.historyFile == "" || t.capacity <= 0 {
log.Debugf("No history file %q or capacity %d, not saving history", t.historyFile, t.capacity)
return nil
}
h := t.term.History()
log.LogVf("got history %v", h)
slices.Reverse(h)
extra := len(h) - t.capacity
if extra > 0 {
h = h[extra:] // truncate to max capacity otherwise extra ones will get out of order

Check warning on line 202 in terminal.go

View check run for this annotation

Codecov / codecov/patch

terminal.go#L193-L202

Added lines #L193 - L202 were not covered by tests
}
log.Infof("Saving history (%d commands) to %s", len(h), t.historyFile)
saveHistory(t.historyFile, h)

Check warning on line 205 in terminal.go

View check run for this annotation

Codecov / codecov/patch

terminal.go#L204-L205

Added lines #L204 - L205 were not covered by tests
return err
}

Expand Down
Loading