1package main
2
3import (
4	"errors"
5	"github.com/xyproto/vt100"
6	"os"
7	"os/signal"
8	"sync"
9	"syscall"
10	"time"
11	"unicode"
12)
13
14// Returns the Nth letter in a given string, as lowercase. Ignores numbers, special characters, whitespace etc.
15func getLetter(s string, pos int) (rune, error) {
16	counter := 0
17	for _, letter := range s {
18		if unicode.IsLetter(letter) {
19			if counter == pos {
20				return unicode.ToLower(letter), nil
21			}
22			counter++
23		}
24	}
25	return rune(0), errors.New("no letter")
26}
27
28// Menu starts a loop where keypresses are handled. When a choice is made, a number is returned.
29// -1 is "no choice", 0 and up is which choice were selected.
30func Menu(title, titleColor string, choices []string, selectionDelay time.Duration, fg, hi, active, arrowColor string) int {
31	c := vt100.NewCanvas()
32	tty, err := vt100.NewTTY()
33	if err != nil {
34		panic(err)
35	}
36
37	// Mutex used when the terminal is resized
38	resizeMut := &sync.RWMutex{}
39
40	var (
41		menu    = NewMenuWidget(title, titleColor, choices, fg, hi, active, arrowColor, c.W(), c.H())
42		sigChan = make(chan os.Signal, 1)
43	)
44
45	signal.Notify(sigChan, syscall.SIGWINCH)
46	go func() {
47		for range sigChan {
48			resizeMut.Lock()
49			// Create a new canvas, with the new size
50			nc := c.Resized()
51			if nc != nil {
52				vt100.Clear()
53				c = nc
54				c.Redraw()
55			}
56
57			// Inform all elements that the terminal was resized
58			menu.Resize()
59			resizeMut.Unlock()
60		}
61	}()
62
63	vt100.Init()
64	defer vt100.Close()
65
66	// The loop time that is aimed for
67	loopDuration := time.Millisecond * 20
68	start := time.Now()
69
70	running := true
71
72	vt100.Clear()
73	c.Redraw()
74
75	for running {
76
77		// Draw elements in their new positions
78		//vt100.Clear()
79
80		resizeMut.RLock()
81		menu.Draw(c)
82		resizeMut.RUnlock()
83
84		// Update the canvas
85		c.Draw()
86
87		// Don't output keypress terminal codes on the screen
88		tty.NoBlock()
89
90		// Wait a bit
91		end := time.Now()
92		passed := end.Sub(start)
93		if passed < loopDuration {
94			remaining := loopDuration - passed
95			time.Sleep(remaining)
96		}
97		start = time.Now()
98
99		// Handle events
100		key := tty.Key()
101		switch key {
102		case 253, 252, 107, 16: // Up, left, k or ctrl-p
103			resizeMut.Lock()
104			menu.Up(c)
105			resizeMut.Unlock()
106		case 255, 254, 106, 14: // Down, right, j or ctrl-n
107			resizeMut.Lock()
108			menu.Down(c)
109			resizeMut.Unlock()
110		case 1: // Top, ctrl-a
111			resizeMut.Lock()
112			menu.SelectFirst()
113			resizeMut.Unlock()
114		case 5: // Bottom, ctrl-e
115			resizeMut.Lock()
116			menu.SelectLast()
117			resizeMut.Unlock()
118		case 27, 113: // ESC or q
119			running = false
120		case 32, 13: // Space or Return
121			resizeMut.Lock()
122			menu.Select()
123			resizeMut.Unlock()
124			running = false
125		case 48, 49, 50, 51, 52, 53, 54, 55, 56, 57: // 0 .. 9
126			number := uint(key - 48)
127			resizeMut.Lock()
128			menu.SelectIndex(number)
129			resizeMut.Unlock()
130		default:
131			letterNumber := 0
132			// Check if the key matches the first letter (a-z,A-Z) in the choices
133			if 65 <= key && key <= 90 {
134				letterNumber = key - 65
135			} else if 97 <= key && key <= 122 {
136				letterNumber = key - 97
137			} else {
138				break
139			}
140			var r rune = rune(letterNumber + 97)
141
142			// TODO: Find the next item starting with this letter, with wraparound
143			// Select the item that starts with this letter, if possible. Try the first, then the second, etc, up to 5
144			keymap := make(map[rune]int)
145			for index, choice := range choices {
146				for pos := 0; pos < 5; pos++ {
147					letter, err := getLetter(choice, pos)
148					if err == nil {
149						_, exists := keymap[letter]
150						// If the letter is not already stored in the keymap, and it's not q, j or k
151						if !exists && (letter != 113) && (letter != 106) && (letter != 107) {
152							keymap[letter] = index
153							// Found a letter for this choice, move on
154							break
155						}
156					}
157				}
158				// Did not find a letter for this choice, move on
159			}
160
161			// Choose the index for the letter that was pressed and found in the keymap, if found
162			for letter, index := range keymap {
163				if letter == r {
164					resizeMut.Lock()
165					menu.SelectIndex(uint(index))
166					resizeMut.Unlock()
167				}
168			}
169		}
170
171		// If a key was pressed, draw the canvas
172		if key != 0 {
173			c.Draw()
174		}
175
176	}
177
178	if menu.Selected() >= 0 {
179		// Draw the selected item in a different color for a short while
180		resizeMut.Lock()
181		menu.SelectDraw(c)
182		resizeMut.Unlock()
183		c.Draw()
184		time.Sleep(selectionDelay)
185	}
186
187	tty.Close()
188
189	return menu.Selected()
190}
191