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