From be556291c6c6263a469069231cb2e9b408a8453c Mon Sep 17 00:00:00 2001 From: Jochen Date: Mon, 25 Jul 2022 16:13:57 +0200 Subject: [PATCH] fix: make sure the interactive printers can cleanup after Ctrl+C After canceling the interactive printers with Ctrl+C, `os.Exit` was directly called in the keyboard listener. Because of this the `defer` statements of the surrounding function were not called (for example `cursor.Show`) and the keyboard listener was still running. This resulted in a broken user "terminal" (no Cursor, no Ctrl+C) --- interactive_multiselect_printer.go | 10 ++++++++-- interactive_select_printer.go | 11 +++++++++-- interactive_textinput_printer.go | 9 +++++++-- internal/cancelation_signal.go | 21 +++++++++++++++++++++ 4 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 internal/cancelation_signal.go diff --git a/interactive_multiselect_printer.go b/interactive_multiselect_printer.go index a9c1a192b..6ce6dbb56 100644 --- a/interactive_multiselect_printer.go +++ b/interactive_multiselect_printer.go @@ -2,13 +2,13 @@ package pterm import ( "fmt" - "os" "sort" "atomicgo.dev/cursor" "atomicgo.dev/keyboard" "atomicgo.dev/keyboard/keys" "github.com/lithammer/fuzzysearch/fuzzy" + "github.com/pterm/pterm/internal" ) var ( @@ -72,6 +72,11 @@ func (p InteractiveMultiselectPrinter) WithMaxHeight(maxHeight int) *Interactive // Show shows the interactive multiselect menu and returns the selected entry. func (p *InteractiveMultiselectPrinter) Show(text ...string) ([]string, error) { + // should be the first defer statement to make sure it is executed last + // and all the needed cleanup can be done before + cancel, exit := internal.NewCancelationSignal() + defer exit() + if len(text) == 0 || Sprint(text[0]) == "" { text = []string{p.DefaultText} } @@ -219,7 +224,8 @@ func (p *InteractiveMultiselectPrinter) Show(text ...string) ([]string, error) { area.Update(p.renderSelectMenu()) case keys.CtrlC: - os.Exit(1) + cancel() + return true, nil case keys.Enter: // Select option if not already selected p.selectOption(p.fuzzySearchMatches[p.selectedOption]) diff --git a/interactive_select_printer.go b/interactive_select_printer.go index c9fe5ded6..13e25f7b2 100644 --- a/interactive_select_printer.go +++ b/interactive_select_printer.go @@ -2,13 +2,13 @@ package pterm import ( "fmt" - "os" "sort" "atomicgo.dev/cursor" "atomicgo.dev/keyboard" "atomicgo.dev/keyboard/keys" "github.com/lithammer/fuzzysearch/fuzzy" + "github.com/pterm/pterm/internal" ) var ( @@ -72,6 +72,11 @@ func (p InteractiveSelectPrinter) WithMaxHeight(maxHeight int) *InteractiveSelec // Show shows the interactive select menu and returns the selected entry. func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { + // should be the first defer statement to make sure it is executed last + // and all the needed cleanup can be done before + cancel, exit := internal.NewCancelationSignal() + defer exit() + if len(text) == 0 || Sprint(text[0]) == "" { text = []string{p.DefaultText} } @@ -124,6 +129,7 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { cursor.Hide() defer cursor.Show() + err = keyboard.Listen(func(keyInfo keys.Key) (stop bool, err error) { key := keyInfo.Code @@ -216,7 +222,8 @@ func (p *InteractiveSelectPrinter) Show(text ...string) (string, error) { area.Update(p.renderSelectMenu()) case keys.CtrlC: - os.Exit(1) + cancel() + return true, nil case keys.Enter: if len(p.fuzzySearchMatches) == 0 { return false, nil diff --git a/interactive_textinput_printer.go b/interactive_textinput_printer.go index f6c682405..5d5ddd4b2 100644 --- a/interactive_textinput_printer.go +++ b/interactive_textinput_printer.go @@ -1,7 +1,6 @@ package pterm import ( - "os" "strings" "atomicgo.dev/cursor" @@ -51,6 +50,11 @@ func (p *InteractiveTextInputPrinter) WithMultiLine(multiLine ...bool) *Interact // Show shows the interactive select menu and returns the selected entry. func (p *InteractiveTextInputPrinter) Show(text ...string) (string, error) { + // should be the first defer statement to make sure it is executed last + // and all the needed cleanup can be done before + cancel, exit := internal.NewCancelationSignal() + defer exit() + var areaText string if len(text) == 0 || Sprint(text[0]) == "" { @@ -130,7 +134,8 @@ func (p *InteractiveTextInputPrinter) Show(text ...string) (string, error) { p.cursorXPos = 0 } case keys.CtrlC: - os.Exit(0) + cancel() + return true, nil case keys.Down: if p.cursorYPos+1 < len(p.input) { p.cursorXPos = (internal.GetStringMaxWidth(p.input[p.cursorYPos]) + p.cursorXPos) - internal.GetStringMaxWidth(p.input[p.cursorYPos+1]) diff --git a/internal/cancelation_signal.go b/internal/cancelation_signal.go new file mode 100644 index 000000000..d6f79b0a1 --- /dev/null +++ b/internal/cancelation_signal.go @@ -0,0 +1,21 @@ +package internal + +import ( + "os" +) + +// NewCancelationSignal for keeping track of a cancelation +func NewCancelationSignal() (func(), func()) { + canceled := false + + cancel := func() { + canceled = true + } + exit := func() { + if canceled { + os.Exit(1) + } + } + + return cancel, exit +}