Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
d7b0757
progressbar
sunshineplan Sep 25, 2025
3f9deed
choice
sunshineplan Sep 25, 2025
e5cce6e
clock
sunshineplan Sep 25, 2025
ab95d24
clock comment
sunshineplan Sep 25, 2025
378ab70
confirm
sunshineplan Sep 26, 2025
e482c4b
Update confirm.go
sunshineplan Sep 26, 2025
4a10d0e
Update choice
sunshineplan Sep 26, 2025
8ef2433
ring and loadbalance
sunshineplan Sep 28, 2025
17fad27
Update ring
sunshineplan Sep 29, 2025
36c495a
ring comment
sunshineplan Sep 29, 2025
2a15a5e
loadbalance
sunshineplan Sep 29, 2025
d4438aa
loadbalance comment
sunshineplan Sep 29, 2025
4dad95c
list
sunshineplan Sep 29, 2025
237a6db
Update list.go
sunshineplan Sep 30, 2025
abf2100
map
sunshineplan Oct 9, 2025
0ef2013
value
sunshineplan Oct 9, 2025
e5090c7
counter
sunshineplan Oct 9, 2025
6a512c2
Update counter
sunshineplan Oct 9, 2025
df624c8
Update counter
sunshineplan Oct 10, 2025
7463394
Update counter
sunshineplan Oct 10, 2025
6140a0c
pool
sunshineplan Oct 10, 2025
0be3286
pop3
sunshineplan Oct 11, 2025
a779268
Update pop3.go
sunshineplan Oct 11, 2025
a5d6932
retry
sunshineplan Oct 11, 2025
92b27ae
csv
sunshineplan Oct 13, 2025
7110ae0
flags
sunshineplan Oct 14, 2025
e64cbab
html
sunshineplan Oct 14, 2025
b450898
html comment
sunshineplan Oct 15, 2025
1f4d98b
httpsvr
sunshineplan Oct 15, 2025
64628d6
log
sunshineplan Oct 17, 2025
35ef404
container and log
sunshineplan Oct 18, 2025
b5f09e8
log
sunshineplan Oct 20, 2025
cf19079
log comment
sunshineplan Oct 20, 2025
d8be22f
mail
sunshineplan Oct 21, 2025
11f7b66
ocr
sunshineplan Oct 21, 2025
a6a5ce9
slice
sunshineplan Oct 21, 2025
38e4e9c
unit
sunshineplan Oct 21, 2025
dcf9cba
processing/text
sunshineplan Oct 22, 2025
180f9c4
smtp
sunshineplan Oct 22, 2025
a7b42b3
txt
sunshineplan Oct 22, 2025
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
61 changes: 31 additions & 30 deletions choice/choice.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"bytes"
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
Expand All @@ -29,17 +30,13 @@ func Menu[E any](choices []E, showQuit bool) string {
if len(choices) == 0 {
return ""
}
var digit int
for n := len(choices); n != 0; digit++ {
n /= 10
}
option := fmt.Sprintf("%%%dd", digit)
digit := len(strconv.Itoa(len(choices)))
var b strings.Builder
for i, choice := range choices {
fmt.Fprintf(&b, "%s. %s\n", fmt.Sprintf(option, i+1), choiceStr(choice))
fmt.Fprintf(&b, "%*d. %s\n", digit, i+1, choiceStr(choice))
}
if showQuit {
fmt.Fprintf(&b, "%s. Quit\n", fmt.Sprintf(fmt.Sprintf("%%%ds", digit), "q"))
fmt.Fprintf(&b, "%*d. Quit\n", digit, 0)
}
return b.String()
}
Expand All @@ -51,13 +48,8 @@ var _ error = choiceError("")

type choiceError string

func (err choiceError) Error() string {
return "bad choice: " + string(err)
}

func (choiceError) Unwrap() error {
return ErrBadChoice
}
func (err choiceError) Error() string { return "bad choice: " + string(err) }
func (choiceError) Unwrap() error { return ErrBadChoice }

func choose[E any](choice string, choices []E) (res E, err error) {
n, err := strconv.Atoi(choice)
Expand All @@ -79,6 +71,10 @@ func Choose[E any](choices []E) (choice bool, res E, err error) {

// ChooseWithDefault function allows the user to make a choice from the given options with an optional default value.
func ChooseWithDefault[E any](choices []E, def int) (choice bool, res E, err error) {
return chooseWithDefault(os.Stdin, choices, def)
}

func chooseWithDefault[E any](r io.Reader, choices []E, def int) (choice bool, res E, err error) {
if n := len(choices); n == 0 {
err = errors.New("no choices")
return
Expand All @@ -92,26 +88,31 @@ func ChooseWithDefault[E any](choices []E, def int) (choice bool, res E, err err
} else {
prompt = "Please choose: "
}
scanner := bufio.NewScanner(os.Stdin)
var b []byte
if def <= 0 {
for len(b) == 0 {
fmt.Print(prompt)
scanner.Scan()
b = bytes.TrimSpace(scanner.Bytes())
}
} else {
fmt.Print(prompt)
scanner.Scan()
b = bytes.TrimSpace(scanner.Bytes())
if len(b) == 0 {
return true, choices[def-1], nil
}
b, err := readLine(bufio.NewScanner(r), prompt, def <= 0)
if err != nil {
return
}
if len(b) == 0 && def > 0 {
return true, choices[def-1], nil
}
if bytes.EqualFold(b, []byte("q")) {
if bytes.EqualFold(b, []byte("0")) || bytes.EqualFold(b, []byte("q")) {
return
}
choice = true
res, err = choose(string(b), choices)
return
}

func readLine(scanner *bufio.Scanner, prompt string, required bool) ([]byte, error) {
for {
fmt.Print(prompt)
if !scanner.Scan() {
return nil, scanner.Err()
}
b := bytes.TrimSpace(scanner.Bytes())
if required && len(b) == 0 {
continue
}
return b, nil
}
}
103 changes: 102 additions & 1 deletion choice/choice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestMenu(t *testing.T) {
8. hh
9. ii
10. jj
q. Quit
0. Quit
`
if s := Menu(choices, true); s != expect {
t.Errorf("expected %q; got %q", expect, s)
Expand All @@ -54,3 +54,104 @@ func TestError(t *testing.T) {
t.Error("expected err is ErrBadChoice; got not")
}
}

func TestChooseWithDefault(t *testing.T) {
tests := []struct {
name string
input string
choices []string
def int
wantChoice bool
wantRes string
wantErr bool
}{
{
name: "valid choice",
input: "2\n",
choices: []string{"a", "b", "c"},
def: 0,
wantChoice: true,
wantRes: "b",
wantErr: false,
},
{
name: "default used when empty input",
input: "\n",
choices: []string{"a", "b", "c"},
def: 2,
wantChoice: true,
wantRes: "b",
wantErr: false,
},
{
name: "quit with 0",
input: "0\n",
choices: []string{"a", "b"},
def: 0,
wantChoice: false,
wantRes: "",
wantErr: false,
},
{
name: "quit with q",
input: "q\n",
choices: []string{"a", "b"},
def: 0,
wantChoice: false,
wantRes: "",
wantErr: false,
},
{
name: "invalid input",
input: "x\n",
choices: []string{"a", "b"},
def: 0,
wantChoice: true,
wantRes: "",
wantErr: true,
},
{
name: "out of range",
input: "5\n",
choices: []string{"a", "b"},
def: 0,
wantChoice: true,
wantRes: "",
wantErr: true,
},
{
name: "no choices",
input: "1\n",
choices: []string{},
def: 0,
wantChoice: false,
wantRes: "",
wantErr: true,
},
{
name: "invalid default",
input: "1\n",
choices: []string{"a"},
def: 5,
wantChoice: false,
wantRes: "",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := strings.NewReader(tt.input)
choice, res, err := chooseWithDefault(r, tt.choices, tt.def)
if choice != tt.wantChoice {
t.Errorf("got choice=%v, want %v", choice, tt.wantChoice)
}
if res != tt.wantRes {
t.Errorf("got res=%q, want %q", res, tt.wantRes)
}
if (err != nil) != tt.wantErr {
t.Errorf("got err=%v, wantErr=%v", err, tt.wantErr)
}
})
}
}
Loading
Loading