##===-- 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()