##===-- cui.py -----------------------------------------------*- Python -*-===##
##
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
##
##===----------------------------------------------------------------------===##
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()