1##===-- cui.py -----------------------------------------------*- Python -*-===## 2## 3# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 4# See https://llvm.org/LICENSE.txt for license information. 5# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 6## 7##===----------------------------------------------------------------------===## 8 9import curses 10import curses.ascii 11import threading 12 13 14class CursesWin(object): 15 16 def __init__(self, x, y, w, h): 17 self.win = curses.newwin(h, w, y, x) 18 self.focus = False 19 20 def setFocus(self, focus): 21 self.focus = focus 22 23 def getFocus(self): 24 return self.focus 25 26 def canFocus(self): 27 return True 28 29 def handleEvent(self, event): 30 return 31 32 def draw(self): 33 return 34 35 36class TextWin(CursesWin): 37 38 def __init__(self, x, y, w): 39 super(TextWin, self).__init__(x, y, w, 1) 40 self.win.bkgd(curses.color_pair(1)) 41 self.text = '' 42 self.reverse = False 43 44 def canFocus(self): 45 return False 46 47 def draw(self): 48 w = self.win.getmaxyx()[1] 49 text = self.text 50 if len(text) > w: 51 #trunc_length = len(text) - w 52 text = text[-w + 1:] 53 if self.reverse: 54 self.win.addstr(0, 0, text, curses.A_REVERSE) 55 else: 56 self.win.addstr(0, 0, text) 57 self.win.noutrefresh() 58 59 def setReverse(self, reverse): 60 self.reverse = reverse 61 62 def setText(self, text): 63 self.text = text 64 65 66class TitledWin(CursesWin): 67 68 def __init__(self, x, y, w, h, title): 69 super(TitledWin, self).__init__(x, y + 1, w, h - 1) 70 self.title = title 71 self.title_win = TextWin(x, y, w) 72 self.title_win.setText(title) 73 self.draw() 74 75 def setTitle(self, title): 76 self.title_win.setText(title) 77 78 def draw(self): 79 self.title_win.setReverse(self.getFocus()) 80 self.title_win.draw() 81 self.win.noutrefresh() 82 83 84class ListWin(CursesWin): 85 86 def __init__(self, x, y, w, h): 87 super(ListWin, self).__init__(x, y, w, h) 88 self.items = [] 89 self.selected = 0 90 self.first_drawn = 0 91 self.win.leaveok(True) 92 93 def draw(self): 94 if len(self.items) == 0: 95 self.win.erase() 96 return 97 98 h, w = self.win.getmaxyx() 99 100 allLines = [] 101 firstSelected = -1 102 lastSelected = -1 103 for i, item in enumerate(self.items): 104 lines = self.items[i].split('\n') 105 lines = lines if lines[len(lines) - 1] != '' else lines[:-1] 106 if len(lines) == 0: 107 lines = [''] 108 109 if i == self.getSelected(): 110 firstSelected = len(allLines) 111 allLines.extend(lines) 112 if i == self.selected: 113 lastSelected = len(allLines) - 1 114 115 if firstSelected < self.first_drawn: 116 self.first_drawn = firstSelected 117 elif lastSelected >= self.first_drawn + h: 118 self.first_drawn = lastSelected - h + 1 119 120 self.win.erase() 121 122 begin = self.first_drawn 123 end = begin + h 124 125 y = 0 126 for i, line in list(enumerate(allLines))[begin:end]: 127 attr = curses.A_NORMAL 128 if i >= firstSelected and i <= lastSelected: 129 attr = curses.A_REVERSE 130 line = '{0:{width}}'.format(line, width=w - 1) 131 132 # Ignore the error we get from drawing over the bottom-right char. 133 try: 134 self.win.addstr(y, 0, line[:w], attr) 135 except curses.error: 136 pass 137 y += 1 138 self.win.noutrefresh() 139 140 def getSelected(self): 141 if self.items: 142 return self.selected 143 return -1 144 145 def setSelected(self, selected): 146 self.selected = selected 147 if self.selected < 0: 148 self.selected = 0 149 elif self.selected >= len(self.items): 150 self.selected = len(self.items) - 1 151 152 def handleEvent(self, event): 153 if isinstance(event, int): 154 if len(self.items) > 0: 155 if event == curses.KEY_UP: 156 self.setSelected(self.selected - 1) 157 if event == curses.KEY_DOWN: 158 self.setSelected(self.selected + 1) 159 if event == curses.ascii.NL: 160 self.handleSelect(self.selected) 161 162 def addItem(self, item): 163 self.items.append(item) 164 165 def clearItems(self): 166 self.items = [] 167 168 def handleSelect(self, index): 169 return 170 171 172class InputHandler(threading.Thread): 173 174 def __init__(self, screen, queue): 175 super(InputHandler, self).__init__() 176 self.screen = screen 177 self.queue = queue 178 179 def run(self): 180 while True: 181 c = self.screen.getch() 182 self.queue.put(c) 183 184 185class CursesUI(object): 186 """ Responsible for updating the console UI with curses. """ 187 188 def __init__(self, screen, event_queue): 189 self.screen = screen 190 self.event_queue = event_queue 191 192 curses.start_color() 193 curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) 194 curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_BLACK) 195 curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK) 196 self.screen.bkgd(curses.color_pair(1)) 197 self.screen.clear() 198 199 self.input_handler = InputHandler(self.screen, self.event_queue) 200 self.input_handler.daemon = True 201 202 self.focus = 0 203 204 self.screen.refresh() 205 206 def focusNext(self): 207 self.wins[self.focus].setFocus(False) 208 old = self.focus 209 while True: 210 self.focus += 1 211 if self.focus >= len(self.wins): 212 self.focus = 0 213 if self.wins[self.focus].canFocus(): 214 break 215 self.wins[self.focus].setFocus(True) 216 217 def handleEvent(self, event): 218 if isinstance(event, int): 219 if event == curses.KEY_F3: 220 self.focusNext() 221 222 def eventLoop(self): 223 224 self.input_handler.start() 225 self.wins[self.focus].setFocus(True) 226 227 while True: 228 self.screen.noutrefresh() 229 230 for i, win in enumerate(self.wins): 231 if i != self.focus: 232 win.draw() 233 # Draw the focused window last so that the cursor shows up. 234 if self.wins: 235 self.wins[self.focus].draw() 236 curses.doupdate() # redraw the physical screen 237 238 event = self.event_queue.get() 239 240 for win in self.wins: 241 if isinstance(event, int): 242 if win.getFocus() or not win.canFocus(): 243 win.handleEvent(event) 244 else: 245 win.handleEvent(event) 246 self.handleEvent(event) 247 248 249class CursesEditLine(object): 250 """ Embed an 'editline'-compatible prompt inside a CursesWin. """ 251 252 def __init__(self, win, history, enterCallback, tabCompleteCallback): 253 self.win = win 254 self.history = history 255 self.enterCallback = enterCallback 256 self.tabCompleteCallback = tabCompleteCallback 257 258 self.prompt = '' 259 self.content = '' 260 self.index = 0 261 self.startx = -1 262 self.starty = -1 263 264 def draw(self, prompt=None): 265 if not prompt: 266 prompt = self.prompt 267 (h, w) = self.win.getmaxyx() 268 if (len(prompt) + len(self.content)) / w + self.starty >= h - 1: 269 self.win.scroll(1) 270 self.starty -= 1 271 if self.starty < 0: 272 raise RuntimeError('Input too long; aborting') 273 (y, x) = (self.starty, self.startx) 274 275 self.win.move(y, x) 276 self.win.clrtobot() 277 self.win.addstr(y, x, prompt) 278 remain = self.content 279 self.win.addstr(remain[:w - len(prompt)]) 280 remain = remain[w - len(prompt):] 281 while remain != '': 282 y += 1 283 self.win.addstr(y, 0, remain[:w]) 284 remain = remain[w:] 285 286 length = self.index + len(prompt) 287 self.win.move(self.starty + length / w, length % w) 288 289 def showPrompt(self, y, x, prompt=None): 290 self.content = '' 291 self.index = 0 292 self.startx = x 293 self.starty = y 294 self.draw(prompt) 295 296 def handleEvent(self, event): 297 if not isinstance(event, int): 298 return # not handled 299 key = event 300 301 if self.startx == -1: 302 raise RuntimeError('Trying to handle input without prompt') 303 304 if key == curses.ascii.NL: 305 self.enterCallback(self.content) 306 elif key == curses.ascii.TAB: 307 self.tabCompleteCallback(self.content) 308 elif curses.ascii.isprint(key): 309 self.content = self.content[:self.index] + \ 310 chr(key) + self.content[self.index:] 311 self.index += 1 312 elif key == curses.KEY_BACKSPACE or key == curses.ascii.BS: 313 if self.index > 0: 314 self.index -= 1 315 self.content = self.content[ 316 :self.index] + self.content[self.index + 1:] 317 elif key == curses.KEY_DC or key == curses.ascii.DEL or key == curses.ascii.EOT: 318 self.content = self.content[ 319 :self.index] + self.content[self.index + 1:] 320 elif key == curses.ascii.VT: # CTRL-K 321 self.content = self.content[:self.index] 322 elif key == curses.KEY_LEFT or key == curses.ascii.STX: # left or CTRL-B 323 if self.index > 0: 324 self.index -= 1 325 elif key == curses.KEY_RIGHT or key == curses.ascii.ACK: # right or CTRL-F 326 if self.index < len(self.content): 327 self.index += 1 328 elif key == curses.ascii.SOH: # CTRL-A 329 self.index = 0 330 elif key == curses.ascii.ENQ: # CTRL-E 331 self.index = len(self.content) 332 elif key == curses.KEY_UP or key == curses.ascii.DLE: # up or CTRL-P 333 self.content = self.history.previous(self.content) 334 self.index = len(self.content) 335 elif key == curses.KEY_DOWN or key == curses.ascii.SO: # down or CTRL-N 336 self.content = self.history.next() 337 self.index = len(self.content) 338 self.draw() 339