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

Version 13.10.0 #517

Merged
merged 11 commits into from
Oct 31, 2024
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
## Changelog

### [13.10.0](https://kaos.sh/ek/13.10.0)

> [!IMPORTANT]
> This release contains breaking changes to the `input.Read`, `input.ReadPassword`, and `input.ReadPasswordSecure` methods. Prior to this release, all of these methods took a boolean argument to disallow empty input. Since we are adding input validators, you will need to use the `NotEmpty` validator for the same behaviour.

- `[fmtutil/table]` Added automatic breaks feature
- `[terminal/input]` Added input validation feature
- `[terminal/input]` Fixed bug with hiding the password when `HidePassword` is set to true and an empty input error is displayed
- `[terminal/input]` Fixed bug with printing new line after input field on error

### [13.9.2](https://kaos.sh/ek/13.9.2)

- `[knf]` Added helper `Q`
Expand Down
16 changes: 16 additions & 0 deletions fmtutil/table/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ type Table struct {
// Width is table maximum width
Width int

// Breaks is an interval for separators between given number of rows
Breaks int

// SeparatorSymbol is symbol used for borders rendering
BorderSymbol string

Expand Down Expand Up @@ -91,6 +94,9 @@ type Table struct {

// Slice with auto calculated sizes
columnSizes []int

// Cursor is number of the latest record
cursor int
}

// ////////////////////////////////////////////////////////////////////////////////// //
Expand All @@ -116,6 +122,9 @@ var SeparatorColorTag = "{s}"
// ColumnSeparatorSymbol is default column separator symbol
var ColumnSeparatorSymbol = "|"

// Breaks is an interval for separators between given number of rows
var Breaks = -1

// FullScreen is a flag for full-screen table by default
var FullScreen = true

Expand All @@ -126,6 +135,7 @@ func NewTable(headers ...string) *Table {
return &Table{
HeaderCapitalize: HeaderCapitalize,
FullScreen: FullScreen,
Breaks: Breaks,
Headers: headers,
Processor: convertSlice,
}
Expand Down Expand Up @@ -354,6 +364,10 @@ func renderData(t *Table) {

// renderRowData render data in row
func renderRowData(t *Table, data []string, totalColumns int) {
if t.Breaks > 0 && t.cursor > 0 && t.cursor%t.Breaks == 0 {
renderSeparator(t)
}

for columnIndex, columnData := range data {
if columnIndex == totalColumns {
break
Expand All @@ -377,6 +391,8 @@ func renderRowData(t *Table, data []string, totalColumns int) {
}
}

t.cursor++

fmtc.NewLine()
}

Expand Down
1 change: 1 addition & 0 deletions fmtutil/table/table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ func (s *TableSuite) TestRender(c *C) {
t.BorderColorTag = "{b}"
t.SeparatorColorTag = "{g}"
t.HeaderCapitalize = true
t.Breaks = 2

c.Assert(t.Render(), NotNil)

Expand Down
30 changes: 20 additions & 10 deletions terminal/input/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,35 @@ import (

func ExampleRead() {
// User must enter name
input, err := Read("Please enter user name", true)
userName, err := Read("Please enter user name", NotEmpty)

if err != nil {
fmt.Printf("Error: %v\n", err)
return
}

fmt.Printf("User name: %s\n", input)
fmt.Printf("User name: %s\n", userName)

// You can read user input without providing any title
fmt.Println("Please enter user name")
input, err = Read("", true)
userName, err = Read("")

if err != nil {
fmt.Printf("Error: %v\n", err)
return
}

fmt.Printf("User name: %s\n", input)
fmt.Printf("User name: %s\n", userName)

// You can define many validators at once
userEmail, err := Read("Please enter user email", NotEmpty, IsEmail)

if err != nil {
fmt.Printf("Error: %v\n", err)
return
}

fmt.Printf("User email: %s\n", userEmail)
}

func ExampleReadPassword() {
Expand All @@ -44,7 +54,7 @@ func ExampleReadPassword() {
NewLine = true

// User must enter the password
password, err := ReadPassword("Please enter password", true)
password, err := ReadPassword("Please enter password", NotEmpty)

if err != nil {
fmt.Printf("Error: %v\n", err)
Expand All @@ -60,7 +70,7 @@ func ExampleReadPasswordSecure() {
MaskSymbolColorTag = "{s}"

// User must enter the password
password, err := ReadPasswordSecure("Please enter password", true)
password, err := ReadPasswordSecure("Please enter password", NotEmpty)

if err != nil {
fmt.Printf("Error: %v\n", err)
Expand All @@ -87,7 +97,7 @@ func ExampleReadAnswer() {
}

func ExampleAddHistory() {
input, err := Read("Please enter user name", true)
input, err := Read("Please enter user name", NotEmpty)

if err != nil {
fmt.Printf("Error: %v\n", err)
Expand All @@ -101,7 +111,7 @@ func ExampleAddHistory() {
}

func ExampleSetHistoryCapacity() {
input, err := Read("Please enter user name", true)
input, err := Read("Please enter user name", NotEmpty)

if err != nil {
fmt.Printf("Error: %v\n", err)
Expand Down Expand Up @@ -142,7 +152,7 @@ func ExampleSetCompletionHandler() {
return ""
})

input, err := Read("Please enter command", true)
input, err := Read("Please enter command", NotEmpty)

if err != nil {
fmt.Printf("Error: %v\n", err)
Expand Down Expand Up @@ -177,7 +187,7 @@ func ExampleSetHintHandler() {
return ""
})

input, err := Read("Please enter command", true)
input, err := Read("Please enter command", NotEmpty)

if err != nil {
fmt.Printf("Error: %v\n", err)
Expand Down
57 changes: 34 additions & 23 deletions terminal/input/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,18 @@ var NewLine = false

// ////////////////////////////////////////////////////////////////////////////////// //

// ErrInvalidAnswer is error for wrong answer for Y/N question
var ErrInvalidAnswer = fmt.Errorf("Please enter Y or N")

// ////////////////////////////////////////////////////////////////////////////////// //

var oldTMUXFlag int8

// ////////////////////////////////////////////////////////////////////////////////// //

// Read reads user input
func Read(title string, nonEmpty bool) (string, error) {
return readUserInput(title, nonEmpty, false)
func Read(title string, validators ...Validator) (string, error) {
return readUserInput(title, false, validators)
}

// ReadAnswer reads user's answer to yes/no question
Expand All @@ -93,7 +98,7 @@ func ReadAnswer(title string, defaultAnswers ...string) (bool, error) {
fmtc.Println(TitleColorTag + getAnswerTitle(title, defaultAnswer) + "{!}")
}

fmtc.Println(Prompt + "y")
fmtc.Println(Prompt + "{s}Y{!}")

if NewLine {
fmtc.NewLine()
Expand All @@ -104,7 +109,7 @@ func ReadAnswer(title string, defaultAnswers ...string) (bool, error) {

for {
answer, err := readUserInput(
getAnswerTitle(title, defaultAnswer), false, false,
getAnswerTitle(title, defaultAnswer), false, nil,
)

if err != nil {
Expand All @@ -121,22 +126,22 @@ func ReadAnswer(title string, defaultAnswers ...string) (bool, error) {
case "N":
return false, nil
default:
terminal.Warn("Please enter Y or N")
terminal.Warn(ErrInvalidAnswer)
fmtc.NewLine()
}
}
}

// ReadPassword reads password or some private input that will be hidden
// after pressing Enter
func ReadPassword(title string, nonEmpty bool) (string, error) {
return readUserInput(title, nonEmpty, true)
func ReadPassword(title string, validators ...Validator) (string, error) {
return readUserInput(title, true, validators)
}

// ReadPasswordSecure reads password or some private input that will be hidden
// after pressing Enter
func ReadPasswordSecure(title string, nonEmpty bool) (*secstr.String, error) {
password, err := readUserInput(title, nonEmpty, true)
func ReadPasswordSecure(title string, validators ...Validator) (*secstr.String, error) {
password, err := readUserInput(title, true, validators)

if err != nil {
return nil, err
Expand Down Expand Up @@ -202,41 +207,51 @@ func getAnswerTitle(title, defaultAnswer string) string {

switch strings.ToUpper(defaultAnswer) {
case "Y":
return fmt.Sprintf(TitleColorTag+"%s ({*}Y{!*}/n){!}", title)
return fmt.Sprintf(TitleColorTag+"%s (Y/n){!}", title)
case "N":
return fmt.Sprintf(TitleColorTag+"%s (y/{*}N{!*}){!}", title)
return fmt.Sprintf(TitleColorTag+"%s (y/N){!}", title)
default:
return fmt.Sprintf(TitleColorTag+"%s (y/n){!}", title)
}
}

// readUserInput reads user input
func readUserInput(title string, nonEmpty, private bool) (string, error) {
func readUserInput(title string, private bool, validators []Validator) (string, error) {
if title != "" {
fmtc.Println(TitleColorTag + title + "{!}")
}

var input string
var err error

if NewLine {
defer fmtc.NewLine()
}

if private && HidePassword {
linenoise.SetMaskMode(true)
defer linenoise.SetMaskMode(false)
}

INPUT_LOOP:
for {
input, err = linenoise.Line(fmtc.Sprintf(Prompt))

if private && HidePassword {
linenoise.SetMaskMode(false)
}

if err != nil {
return "", err
}

if nonEmpty && strings.TrimSpace(input) == "" {
terminal.Warn("\nYou must enter non-empty value\n")
continue
if len(validators) != 0 {
for _, validator := range validators {
input, err = validator.Validate(input)

if err != nil {
fmtc.NewLine()
terminal.Warn(err.Error())
fmtc.NewLine()
continue INPUT_LOOP
}
}
}

if private && input != "" {
Expand All @@ -256,10 +271,6 @@ func readUserInput(title string, nonEmpty, private bool) (string, error) {
break
}

if NewLine {
fmtc.NewLine()
}

return input, err
}

Expand Down
21 changes: 18 additions & 3 deletions terminal/input/input_stubs.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,19 @@ import (

// ////////////////////////////////////////////////////////////////////////////////// //

// CompletionHandler is completion handler
type CompletionHandler = func(input string) []string

// HintHandler is hint handler
type HintHandler = func(input string) string

// ////////////////////////////////////////////////////////////////////////////////// //

// ❗ ErrKillSignal is error type when user cancel input
var ErrKillSignal = errors.New("")

// ////////////////////////////////////////////////////////////////////////////////// //

// ❗ Prompt is prompt string
var Prompt = "> "

Expand Down Expand Up @@ -51,8 +61,13 @@ var NewLine = false

// ////////////////////////////////////////////////////////////////////////////////// //

// ErrInvalidAnswer is error for wrong answer for Y/N question
var ErrInvalidAnswer = errors.New("")

// ////////////////////////////////////////////////////////////////////////////////// //

// ❗ Read reads user input
func Read(title string, nonEmpty bool) (string, error) {
func Read(title string, validators ...Validator) (string, error) {
panic("UNSUPPORTED")
}

Expand All @@ -63,13 +78,13 @@ func ReadAnswer(title string, defaultAnswers ...string) (bool, error) {

// ❗ ReadPassword reads password or some private input that will be hidden
// after pressing Enter
func ReadPassword(title string, nonEmpty bool) (string, error) {
func ReadPassword(title string, validators ...Validator) (string, error) {
panic("UNSUPPORTED")
}

// ❗ ReadPasswordSecure reads password or some private input that will be hidden
// after pressing Enter
func ReadPasswordSecure(title string, nonEmpty bool) (*secstr.String, error) {
func ReadPasswordSecure(title string, validators ...Validator) (*secstr.String, error) {
panic("UNSUPPORTED")
}

Expand Down
Loading
Loading