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