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