cui.py 10 KB
##===-- 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()