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