2013-10-12 01:13:07 +08:00
|
|
|
##===-- cui.py -----------------------------------------------*- Python -*-===##
|
|
|
|
##
|
|
|
|
## The LLVM Compiler Infrastructure
|
|
|
|
##
|
|
|
|
## This file is distributed under the University of Illinois Open Source
|
|
|
|
## License. See LICENSE.TXT for details.
|
|
|
|
##
|
|
|
|
##===----------------------------------------------------------------------===##
|
|
|
|
|
2013-10-10 06:11:30 +08:00
|
|
|
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()
|
|
|
|
|
2013-10-16 10:00:21 +08:00
|
|
|
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
|
2013-10-16 10:01:41 +08:00
|
|
|
self.win.leaveok(True)
|
2013-10-16 10:00:21 +08:00
|
|
|
|
|
|
|
def draw(self):
|
2013-10-16 10:01:41 +08:00
|
|
|
if len(self.items) == 0:
|
|
|
|
self.win.erase()
|
|
|
|
return
|
|
|
|
|
2013-10-16 10:00:21 +08:00
|
|
|
h, w = self.win.getmaxyx()
|
2013-10-16 10:01:41 +08:00
|
|
|
|
|
|
|
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
|
2013-10-16 10:00:21 +08:00
|
|
|
|
2013-10-16 11:01:04 +08:00
|
|
|
self.win.erase()
|
2013-10-16 10:00:21 +08:00
|
|
|
|
|
|
|
begin = self.first_drawn
|
|
|
|
end = begin + h
|
2013-10-16 10:01:41 +08:00
|
|
|
|
|
|
|
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
|
2013-10-16 10:00:21 +08:00
|
|
|
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
|
|
|
|
|
2013-10-10 06:11:30 +08:00
|
|
|
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()
|
2013-10-16 11:01:04 +08:00
|
|
|
curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
|
2013-10-10 06:11:30 +08:00
|
|
|
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()
|