334 lines · python
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-exception6##7##===----------------------------------------------------------------------===##8 9import curses10import curses.ascii11import threading12 13 14class CursesWin(object):15 def __init__(self, x, y, w, h):16 self.win = curses.newwin(h, w, y, x)17 self.focus = False18 19 def setFocus(self, focus):20 self.focus = focus21 22 def getFocus(self):23 return self.focus24 25 def canFocus(self):26 return True27 28 def handleEvent(self, event):29 return30 31 def draw(self):32 return33 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 = False41 42 def canFocus(self):43 return False44 45 def draw(self):46 w = self.win.getmaxyx()[1]47 text = self.text48 if len(text) > w:49 # trunc_length = len(text) - w50 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 = reverse59 60 def setText(self, text):61 self.text = text62 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 = title68 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 = 086 self.first_drawn = 087 self.win.leaveok(True)88 89 def draw(self):90 if len(self.items) == 0:91 self.win.erase()92 return93 94 h, w = self.win.getmaxyx()95 96 allLines = []97 firstSelected = -198 lastSelected = -199 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) - 1110 111 if firstSelected < self.first_drawn:112 self.first_drawn = firstSelected113 elif lastSelected >= self.first_drawn + h:114 self.first_drawn = lastSelected - h + 1115 116 self.win.erase()117 118 begin = self.first_drawn119 end = begin + h120 121 y = 0122 for i, line in list(enumerate(allLines))[begin:end]:123 attr = curses.A_NORMAL124 if i >= firstSelected and i <= lastSelected:125 attr = curses.A_REVERSE126 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 pass133 y += 1134 self.win.noutrefresh()135 136 def getSelected(self):137 if self.items:138 return self.selected139 return -1140 141 def setSelected(self, selected):142 self.selected = selected143 if self.selected < 0:144 self.selected = 0145 elif self.selected >= len(self.items):146 self.selected = len(self.items) - 1147 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 return166 167 168class InputHandler(threading.Thread):169 def __init__(self, screen, queue):170 super(InputHandler, self).__init__()171 self.screen = screen172 self.queue = queue173 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 = screen185 self.event_queue = event_queue186 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 = True196 197 self.focus = 0198 199 self.screen.refresh()200 201 def focusNext(self):202 self.wins[self.focus].setFocus(False)203 old = self.focus204 while True:205 self.focus += 1206 if self.focus >= len(self.wins):207 self.focus = 0208 if self.wins[self.focus].canFocus():209 break210 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 screen231 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 = win248 self.history = history249 self.enterCallback = enterCallback250 self.tabCompleteCallback = tabCompleteCallback251 252 self.prompt = ""253 self.content = ""254 self.index = 0255 self.startx = -1256 self.starty = -1257 258 def draw(self, prompt=None):259 if not prompt:260 prompt = self.prompt261 (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 -= 1265 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.content273 self.win.addstr(remain[: w - len(prompt)])274 remain = remain[w - len(prompt) :]275 while remain != "":276 y += 1277 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 = 0286 self.startx = x287 self.starty = y288 self.draw(prompt)289 290 def handleEvent(self, event):291 if not isinstance(event, int):292 return # not handled293 key = event294 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 += 1307 elif key == curses.KEY_BACKSPACE or key == curses.ascii.BS:308 if self.index > 0:309 self.index -= 1310 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-K316 self.content = self.content[: self.index]317 elif key == curses.KEY_LEFT or key == curses.ascii.STX: # left or CTRL-B318 if self.index > 0:319 self.index -= 1320 elif key == curses.KEY_RIGHT or key == curses.ascii.ACK: # right or CTRL-F321 if self.index < len(self.content):322 self.index += 1323 elif key == curses.ascii.SOH: # CTRL-A324 self.index = 0325 elif key == curses.ascii.ENQ: # CTRL-E326 self.index = len(self.content)327 elif key == curses.KEY_UP or key == curses.ascii.DLE: # up or CTRL-P328 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-N331 self.content = self.history.next()332 self.index = len(self.content)333 self.draw()334