|  | ##===-- cui.py -----------------------------------------------*- Python -*-===## | 
|  | ## | 
|  | # The LLVM Compiler Infrastructure | 
|  | ## | 
|  | # This file is distributed under the University of Illinois Open Source | 
|  | # License. See LICENSE.TXT for details. | 
|  | ## | 
|  | ##===----------------------------------------------------------------------===## | 
|  |  | 
|  | import curses | 
|  | import curses.ascii | 
|  | import threading | 
|  |  | 
|  |  | 
|  | class CursesWin(object): | 
|  |  | 
|  | def __init__(self, x, y, w, h): | 
|  | self.win = curses.newwin(h, w, y, x) | 
|  | self.focus = False | 
|  |  | 
|  | def setFocus(self, focus): | 
|  | self.focus = focus | 
|  |  | 
|  | def getFocus(self): | 
|  | return self.focus | 
|  |  | 
|  | def canFocus(self): | 
|  | return True | 
|  |  | 
|  | def handleEvent(self, event): | 
|  | return | 
|  |  | 
|  | def draw(self): | 
|  | return | 
|  |  | 
|  |  | 
|  | class TextWin(CursesWin): | 
|  |  | 
|  | def __init__(self, x, y, w): | 
|  | super(TextWin, self).__init__(x, y, w, 1) | 
|  | self.win.bkgd(curses.color_pair(1)) | 
|  | self.text = '' | 
|  | self.reverse = False | 
|  |  | 
|  | def canFocus(self): | 
|  | return False | 
|  |  | 
|  | def draw(self): | 
|  | w = self.win.getmaxyx()[1] | 
|  | text = self.text | 
|  | if len(text) > w: | 
|  | #trunc_length = len(text) - w | 
|  | text = text[-w + 1:] | 
|  | if self.reverse: | 
|  | self.win.addstr(0, 0, text, curses.A_REVERSE) | 
|  | else: | 
|  | self.win.addstr(0, 0, text) | 
|  | self.win.noutrefresh() | 
|  |  | 
|  | def setReverse(self, reverse): | 
|  | self.reverse = reverse | 
|  |  | 
|  | def setText(self, text): | 
|  | self.text = text | 
|  |  | 
|  |  | 
|  | class TitledWin(CursesWin): | 
|  |  | 
|  | def __init__(self, x, y, w, h, title): | 
|  | super(TitledWin, self).__init__(x, y + 1, w, h - 1) | 
|  | self.title = title | 
|  | self.title_win = TextWin(x, y, w) | 
|  | self.title_win.setText(title) | 
|  | self.draw() | 
|  |  | 
|  | def setTitle(self, title): | 
|  | self.title_win.setText(title) | 
|  |  | 
|  | def draw(self): | 
|  | self.title_win.setReverse(self.getFocus()) | 
|  | self.title_win.draw() | 
|  | self.win.noutrefresh() | 
|  |  | 
|  |  | 
|  | class ListWin(CursesWin): | 
|  |  | 
|  | def __init__(self, x, y, w, h): | 
|  | super(ListWin, self).__init__(x, y, w, h) | 
|  | self.items = [] | 
|  | self.selected = 0 | 
|  | self.first_drawn = 0 | 
|  | self.win.leaveok(True) | 
|  |  | 
|  | def draw(self): | 
|  | if len(self.items) == 0: | 
|  | self.win.erase() | 
|  | return | 
|  |  | 
|  | h, w = self.win.getmaxyx() | 
|  |  | 
|  | allLines = [] | 
|  | firstSelected = -1 | 
|  | lastSelected = -1 | 
|  | for i, item in enumerate(self.items): | 
|  | lines = self.items[i].split('\n') | 
|  | lines = lines if lines[len(lines) - 1] != '' else lines[:-1] | 
|  | if len(lines) == 0: | 
|  | lines = [''] | 
|  |  | 
|  | if i == self.getSelected(): | 
|  | firstSelected = len(allLines) | 
|  | allLines.extend(lines) | 
|  | if i == self.selected: | 
|  | lastSelected = len(allLines) - 1 | 
|  |  | 
|  | if firstSelected < self.first_drawn: | 
|  | self.first_drawn = firstSelected | 
|  | elif lastSelected >= self.first_drawn + h: | 
|  | self.first_drawn = lastSelected - h + 1 | 
|  |  | 
|  | self.win.erase() | 
|  |  | 
|  | begin = self.first_drawn | 
|  | end = begin + h | 
|  |  | 
|  | y = 0 | 
|  | for i, line in list(enumerate(allLines))[begin:end]: | 
|  | attr = curses.A_NORMAL | 
|  | if i >= firstSelected and i <= lastSelected: | 
|  | attr = curses.A_REVERSE | 
|  | line = '{0:{width}}'.format(line, width=w - 1) | 
|  |  | 
|  | # Ignore the error we get from drawing over the bottom-right char. | 
|  | try: | 
|  | self.win.addstr(y, 0, line[:w], attr) | 
|  | except curses.error: | 
|  | pass | 
|  | y += 1 | 
|  | self.win.noutrefresh() | 
|  |  | 
|  | def getSelected(self): | 
|  | if self.items: | 
|  | return self.selected | 
|  | return -1 | 
|  |  | 
|  | def setSelected(self, selected): | 
|  | self.selected = selected | 
|  | if self.selected < 0: | 
|  | self.selected = 0 | 
|  | elif self.selected >= len(self.items): | 
|  | self.selected = len(self.items) - 1 | 
|  |  | 
|  | def handleEvent(self, event): | 
|  | if isinstance(event, int): | 
|  | if len(self.items) > 0: | 
|  | if event == curses.KEY_UP: | 
|  | self.setSelected(self.selected - 1) | 
|  | if event == curses.KEY_DOWN: | 
|  | self.setSelected(self.selected + 1) | 
|  | if event == curses.ascii.NL: | 
|  | self.handleSelect(self.selected) | 
|  |  | 
|  | def addItem(self, item): | 
|  | self.items.append(item) | 
|  |  | 
|  | def clearItems(self): | 
|  | self.items = [] | 
|  |  | 
|  | def handleSelect(self, index): | 
|  | return | 
|  |  | 
|  |  | 
|  | class InputHandler(threading.Thread): | 
|  |  | 
|  | def __init__(self, screen, queue): | 
|  | super(InputHandler, self).__init__() | 
|  | self.screen = screen | 
|  | self.queue = queue | 
|  |  | 
|  | def run(self): | 
|  | while True: | 
|  | c = self.screen.getch() | 
|  | self.queue.put(c) | 
|  |  | 
|  |  | 
|  | class CursesUI(object): | 
|  | """ Responsible for updating the console UI with curses. """ | 
|  |  | 
|  | def __init__(self, screen, event_queue): | 
|  | self.screen = screen | 
|  | self.event_queue = event_queue | 
|  |  | 
|  | curses.start_color() | 
|  | curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) | 
|  | curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_BLACK) | 
|  | curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK) | 
|  | self.screen.bkgd(curses.color_pair(1)) | 
|  | self.screen.clear() | 
|  |  | 
|  | self.input_handler = InputHandler(self.screen, self.event_queue) | 
|  | self.input_handler.daemon = True | 
|  |  | 
|  | self.focus = 0 | 
|  |  | 
|  | self.screen.refresh() | 
|  |  | 
|  | def focusNext(self): | 
|  | self.wins[self.focus].setFocus(False) | 
|  | old = self.focus | 
|  | while True: | 
|  | self.focus += 1 | 
|  | if self.focus >= len(self.wins): | 
|  | self.focus = 0 | 
|  | if self.wins[self.focus].canFocus(): | 
|  | break | 
|  | self.wins[self.focus].setFocus(True) | 
|  |  | 
|  | def handleEvent(self, event): | 
|  | if isinstance(event, int): | 
|  | if event == curses.KEY_F3: | 
|  | self.focusNext() | 
|  |  | 
|  | def eventLoop(self): | 
|  |  | 
|  | self.input_handler.start() | 
|  | self.wins[self.focus].setFocus(True) | 
|  |  | 
|  | while True: | 
|  | self.screen.noutrefresh() | 
|  |  | 
|  | for i, win in enumerate(self.wins): | 
|  | if i != self.focus: | 
|  | win.draw() | 
|  | # Draw the focused window last so that the cursor shows up. | 
|  | if self.wins: | 
|  | self.wins[self.focus].draw() | 
|  | curses.doupdate()  # redraw the physical screen | 
|  |  | 
|  | event = self.event_queue.get() | 
|  |  | 
|  | for win in self.wins: | 
|  | if isinstance(event, int): | 
|  | if win.getFocus() or not win.canFocus(): | 
|  | win.handleEvent(event) | 
|  | else: | 
|  | win.handleEvent(event) | 
|  | self.handleEvent(event) | 
|  |  | 
|  |  | 
|  | class CursesEditLine(object): | 
|  | """ Embed an 'editline'-compatible prompt inside a CursesWin. """ | 
|  |  | 
|  | def __init__(self, win, history, enterCallback, tabCompleteCallback): | 
|  | self.win = win | 
|  | self.history = history | 
|  | self.enterCallback = enterCallback | 
|  | self.tabCompleteCallback = tabCompleteCallback | 
|  |  | 
|  | self.prompt = '' | 
|  | self.content = '' | 
|  | self.index = 0 | 
|  | self.startx = -1 | 
|  | self.starty = -1 | 
|  |  | 
|  | def draw(self, prompt=None): | 
|  | if not prompt: | 
|  | prompt = self.prompt | 
|  | (h, w) = self.win.getmaxyx() | 
|  | if (len(prompt) + len(self.content)) / w + self.starty >= h - 1: | 
|  | self.win.scroll(1) | 
|  | self.starty -= 1 | 
|  | if self.starty < 0: | 
|  | raise RuntimeError('Input too long; aborting') | 
|  | (y, x) = (self.starty, self.startx) | 
|  |  | 
|  | self.win.move(y, x) | 
|  | self.win.clrtobot() | 
|  | self.win.addstr(y, x, prompt) | 
|  | remain = self.content | 
|  | self.win.addstr(remain[:w - len(prompt)]) | 
|  | remain = remain[w - len(prompt):] | 
|  | while remain != '': | 
|  | y += 1 | 
|  | self.win.addstr(y, 0, remain[:w]) | 
|  | remain = remain[w:] | 
|  |  | 
|  | length = self.index + len(prompt) | 
|  | self.win.move(self.starty + length / w, length % w) | 
|  |  | 
|  | def showPrompt(self, y, x, prompt=None): | 
|  | self.content = '' | 
|  | self.index = 0 | 
|  | self.startx = x | 
|  | self.starty = y | 
|  | self.draw(prompt) | 
|  |  | 
|  | def handleEvent(self, event): | 
|  | if not isinstance(event, int): | 
|  | return  # not handled | 
|  | key = event | 
|  |  | 
|  | if self.startx == -1: | 
|  | raise RuntimeError('Trying to handle input without prompt') | 
|  |  | 
|  | if key == curses.ascii.NL: | 
|  | self.enterCallback(self.content) | 
|  | elif key == curses.ascii.TAB: | 
|  | self.tabCompleteCallback(self.content) | 
|  | elif curses.ascii.isprint(key): | 
|  | self.content = self.content[:self.index] + \ | 
|  | chr(key) + self.content[self.index:] | 
|  | self.index += 1 | 
|  | elif key == curses.KEY_BACKSPACE or key == curses.ascii.BS: | 
|  | if self.index > 0: | 
|  | self.index -= 1 | 
|  | self.content = self.content[ | 
|  | :self.index] + self.content[self.index + 1:] | 
|  | elif key == curses.KEY_DC or key == curses.ascii.DEL or key == curses.ascii.EOT: | 
|  | self.content = self.content[ | 
|  | :self.index] + self.content[self.index + 1:] | 
|  | elif key == curses.ascii.VT:  # CTRL-K | 
|  | self.content = self.content[:self.index] | 
|  | elif key == curses.KEY_LEFT or key == curses.ascii.STX:  # left or CTRL-B | 
|  | if self.index > 0: | 
|  | self.index -= 1 | 
|  | elif key == curses.KEY_RIGHT or key == curses.ascii.ACK:  # right or CTRL-F | 
|  | if self.index < len(self.content): | 
|  | self.index += 1 | 
|  | elif key == curses.ascii.SOH:  # CTRL-A | 
|  | self.index = 0 | 
|  | elif key == curses.ascii.ENQ:  # CTRL-E | 
|  | self.index = len(self.content) | 
|  | elif key == curses.KEY_UP or key == curses.ascii.DLE:  # up or CTRL-P | 
|  | self.content = self.history.previous(self.content) | 
|  | self.index = len(self.content) | 
|  | elif key == curses.KEY_DOWN or key == curses.ascii.SO:  # down or CTRL-N | 
|  | self.content = self.history.next() | 
|  | self.index = len(self.content) | 
|  | self.draw() |