forked from gopasspw/gopass
/
selection.go
138 lines (126 loc) · 2.86 KB
/
selection.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package termwiz
import (
"context"
"fmt"
"github.com/gopasspw/gopass/pkg/ctxutil"
"github.com/gdamore/tcell/termbox"
runewidth "github.com/mattn/go-runewidth"
)
func tbprint(x, y int, fg, bg termbox.Attribute, msg string) {
for _, c := range msg {
termbox.SetCell(x, y, c, fg, bg)
x += runewidth.RuneWidth(c)
}
}
// GetSelection show a navigateable multiple-choice list to the user
// and returns the selected entry along with the action
func GetSelection(ctx context.Context, prompt, usage string, choices []string) (string, int) {
if ctxutil.IsAlwaysYes(ctx) || !ctxutil.IsInteractive(ctx) {
return "impossible", 0
}
if prompt != "" {
prompt += " "
}
if err := termbox.Init(); err != nil {
panic(err)
}
defer termbox.Close()
const coldef = termbox.ColorDefault
termbox.Clear(coldef, coldef)
cur := 0
for {
// check for context cancelation
select {
case <-ctx.Done():
return "aborted", cur
default:
}
termbox.Clear(coldef, coldef)
tbprint(0, 0, coldef, coldef, prompt+"Please select:")
_, h := termbox.Size()
offset := 0
if len(choices)+2 > h && cur > h-3 {
offset = cur
}
for i := offset; i < len(choices) && i-offset < h; i++ {
c := choices[i]
mark := " "
if cur == i {
mark = ">"
}
tbprint(0, 1+i-offset, coldef, coldef, fmt.Sprintf("%s %s", mark, c))
}
tbprint(0, h-1, coldef, coldef, formatUsageLine(ctx, usage, offset, cur, choices))
_ = termbox.Flush()
var act string
if act, cur = tbpoll(cur, len(choices)); act != "" {
return act, cur
}
}
}
func formatUsageLine(ctx context.Context, usage string, offset, cur int, choices []string) string {
usageLine := usage
if usageLine == "" {
usageLine = "<↑/↓> to change the selection, <→> to show, <←> to copy, <s> to sync, <ESC> to quit"
}
if ctxutil.IsDebug(ctx) {
usageLine += " - DEBUG: " + fmt.Sprintf("Offset: %d - Cur: %d - Choices: %d", offset, cur, len(choices))
}
return usageLine
}
func tbpoll(cur, max int) (string, int) {
switch ev := termbox.PollEvent(); ev.Type {
case termbox.EventKey:
switch ev.Key {
case termbox.KeyEsc:
return "aborted", cur
case termbox.KeyArrowLeft:
return "copy", cur
case termbox.KeyArrowRight:
return "show", cur
case termbox.KeyEnter:
return "default", cur
case termbox.KeyArrowDown, termbox.KeyTab:
cur++
if cur >= max {
cur = 0
}
return "", cur
case termbox.KeyArrowUp:
cur--
if cur < 0 {
cur = max - 1
}
return "", cur
default:
if ev.Ch != 0 {
return tbchars(ev.Ch, cur, max)
}
}
}
return "", cur
}
func tbchars(ch rune, cur, max int) (string, int) {
switch ch {
case 'h':
return "copy", cur
case 'j':
cur++
if cur >= max {
cur = 0
}
return "", cur
case 'k':
cur--
if cur < 0 {
cur = max - 1
}
return "", cur
case 'l':
return "show", cur
case 's':
return "sync", cur
default:
return "", cur
}
}