xref: /openbsd-src/gnu/llvm/lldb/utils/lui/cui.py (revision 061da546b983eb767bad15e67af1174fb0bcf31c)
1*061da546Spatrick##===-- cui.py -----------------------------------------------*- Python -*-===##
2*061da546Spatrick##
3*061da546Spatrick# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4*061da546Spatrick# See https://llvm.org/LICENSE.txt for license information.
5*061da546Spatrick# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6*061da546Spatrick##
7*061da546Spatrick##===----------------------------------------------------------------------===##
8*061da546Spatrick
9*061da546Spatrickimport curses
10*061da546Spatrickimport curses.ascii
11*061da546Spatrickimport threading
12*061da546Spatrick
13*061da546Spatrick
14*061da546Spatrickclass CursesWin(object):
15*061da546Spatrick
16*061da546Spatrick    def __init__(self, x, y, w, h):
17*061da546Spatrick        self.win = curses.newwin(h, w, y, x)
18*061da546Spatrick        self.focus = False
19*061da546Spatrick
20*061da546Spatrick    def setFocus(self, focus):
21*061da546Spatrick        self.focus = focus
22*061da546Spatrick
23*061da546Spatrick    def getFocus(self):
24*061da546Spatrick        return self.focus
25*061da546Spatrick
26*061da546Spatrick    def canFocus(self):
27*061da546Spatrick        return True
28*061da546Spatrick
29*061da546Spatrick    def handleEvent(self, event):
30*061da546Spatrick        return
31*061da546Spatrick
32*061da546Spatrick    def draw(self):
33*061da546Spatrick        return
34*061da546Spatrick
35*061da546Spatrick
36*061da546Spatrickclass TextWin(CursesWin):
37*061da546Spatrick
38*061da546Spatrick    def __init__(self, x, y, w):
39*061da546Spatrick        super(TextWin, self).__init__(x, y, w, 1)
40*061da546Spatrick        self.win.bkgd(curses.color_pair(1))
41*061da546Spatrick        self.text = ''
42*061da546Spatrick        self.reverse = False
43*061da546Spatrick
44*061da546Spatrick    def canFocus(self):
45*061da546Spatrick        return False
46*061da546Spatrick
47*061da546Spatrick    def draw(self):
48*061da546Spatrick        w = self.win.getmaxyx()[1]
49*061da546Spatrick        text = self.text
50*061da546Spatrick        if len(text) > w:
51*061da546Spatrick            #trunc_length = len(text) - w
52*061da546Spatrick            text = text[-w + 1:]
53*061da546Spatrick        if self.reverse:
54*061da546Spatrick            self.win.addstr(0, 0, text, curses.A_REVERSE)
55*061da546Spatrick        else:
56*061da546Spatrick            self.win.addstr(0, 0, text)
57*061da546Spatrick        self.win.noutrefresh()
58*061da546Spatrick
59*061da546Spatrick    def setReverse(self, reverse):
60*061da546Spatrick        self.reverse = reverse
61*061da546Spatrick
62*061da546Spatrick    def setText(self, text):
63*061da546Spatrick        self.text = text
64*061da546Spatrick
65*061da546Spatrick
66*061da546Spatrickclass TitledWin(CursesWin):
67*061da546Spatrick
68*061da546Spatrick    def __init__(self, x, y, w, h, title):
69*061da546Spatrick        super(TitledWin, self).__init__(x, y + 1, w, h - 1)
70*061da546Spatrick        self.title = title
71*061da546Spatrick        self.title_win = TextWin(x, y, w)
72*061da546Spatrick        self.title_win.setText(title)
73*061da546Spatrick        self.draw()
74*061da546Spatrick
75*061da546Spatrick    def setTitle(self, title):
76*061da546Spatrick        self.title_win.setText(title)
77*061da546Spatrick
78*061da546Spatrick    def draw(self):
79*061da546Spatrick        self.title_win.setReverse(self.getFocus())
80*061da546Spatrick        self.title_win.draw()
81*061da546Spatrick        self.win.noutrefresh()
82*061da546Spatrick
83*061da546Spatrick
84*061da546Spatrickclass ListWin(CursesWin):
85*061da546Spatrick
86*061da546Spatrick    def __init__(self, x, y, w, h):
87*061da546Spatrick        super(ListWin, self).__init__(x, y, w, h)
88*061da546Spatrick        self.items = []
89*061da546Spatrick        self.selected = 0
90*061da546Spatrick        self.first_drawn = 0
91*061da546Spatrick        self.win.leaveok(True)
92*061da546Spatrick
93*061da546Spatrick    def draw(self):
94*061da546Spatrick        if len(self.items) == 0:
95*061da546Spatrick            self.win.erase()
96*061da546Spatrick            return
97*061da546Spatrick
98*061da546Spatrick        h, w = self.win.getmaxyx()
99*061da546Spatrick
100*061da546Spatrick        allLines = []
101*061da546Spatrick        firstSelected = -1
102*061da546Spatrick        lastSelected = -1
103*061da546Spatrick        for i, item in enumerate(self.items):
104*061da546Spatrick            lines = self.items[i].split('\n')
105*061da546Spatrick            lines = lines if lines[len(lines) - 1] != '' else lines[:-1]
106*061da546Spatrick            if len(lines) == 0:
107*061da546Spatrick                lines = ['']
108*061da546Spatrick
109*061da546Spatrick            if i == self.getSelected():
110*061da546Spatrick                firstSelected = len(allLines)
111*061da546Spatrick            allLines.extend(lines)
112*061da546Spatrick            if i == self.selected:
113*061da546Spatrick                lastSelected = len(allLines) - 1
114*061da546Spatrick
115*061da546Spatrick        if firstSelected < self.first_drawn:
116*061da546Spatrick            self.first_drawn = firstSelected
117*061da546Spatrick        elif lastSelected >= self.first_drawn + h:
118*061da546Spatrick            self.first_drawn = lastSelected - h + 1
119*061da546Spatrick
120*061da546Spatrick        self.win.erase()
121*061da546Spatrick
122*061da546Spatrick        begin = self.first_drawn
123*061da546Spatrick        end = begin + h
124*061da546Spatrick
125*061da546Spatrick        y = 0
126*061da546Spatrick        for i, line in list(enumerate(allLines))[begin:end]:
127*061da546Spatrick            attr = curses.A_NORMAL
128*061da546Spatrick            if i >= firstSelected and i <= lastSelected:
129*061da546Spatrick                attr = curses.A_REVERSE
130*061da546Spatrick                line = '{0:{width}}'.format(line, width=w - 1)
131*061da546Spatrick
132*061da546Spatrick            # Ignore the error we get from drawing over the bottom-right char.
133*061da546Spatrick            try:
134*061da546Spatrick                self.win.addstr(y, 0, line[:w], attr)
135*061da546Spatrick            except curses.error:
136*061da546Spatrick                pass
137*061da546Spatrick            y += 1
138*061da546Spatrick        self.win.noutrefresh()
139*061da546Spatrick
140*061da546Spatrick    def getSelected(self):
141*061da546Spatrick        if self.items:
142*061da546Spatrick            return self.selected
143*061da546Spatrick        return -1
144*061da546Spatrick
145*061da546Spatrick    def setSelected(self, selected):
146*061da546Spatrick        self.selected = selected
147*061da546Spatrick        if self.selected < 0:
148*061da546Spatrick            self.selected = 0
149*061da546Spatrick        elif self.selected >= len(self.items):
150*061da546Spatrick            self.selected = len(self.items) - 1
151*061da546Spatrick
152*061da546Spatrick    def handleEvent(self, event):
153*061da546Spatrick        if isinstance(event, int):
154*061da546Spatrick            if len(self.items) > 0:
155*061da546Spatrick                if event == curses.KEY_UP:
156*061da546Spatrick                    self.setSelected(self.selected - 1)
157*061da546Spatrick                if event == curses.KEY_DOWN:
158*061da546Spatrick                    self.setSelected(self.selected + 1)
159*061da546Spatrick                if event == curses.ascii.NL:
160*061da546Spatrick                    self.handleSelect(self.selected)
161*061da546Spatrick
162*061da546Spatrick    def addItem(self, item):
163*061da546Spatrick        self.items.append(item)
164*061da546Spatrick
165*061da546Spatrick    def clearItems(self):
166*061da546Spatrick        self.items = []
167*061da546Spatrick
168*061da546Spatrick    def handleSelect(self, index):
169*061da546Spatrick        return
170*061da546Spatrick
171*061da546Spatrick
172*061da546Spatrickclass InputHandler(threading.Thread):
173*061da546Spatrick
174*061da546Spatrick    def __init__(self, screen, queue):
175*061da546Spatrick        super(InputHandler, self).__init__()
176*061da546Spatrick        self.screen = screen
177*061da546Spatrick        self.queue = queue
178*061da546Spatrick
179*061da546Spatrick    def run(self):
180*061da546Spatrick        while True:
181*061da546Spatrick            c = self.screen.getch()
182*061da546Spatrick            self.queue.put(c)
183*061da546Spatrick
184*061da546Spatrick
185*061da546Spatrickclass CursesUI(object):
186*061da546Spatrick    """ Responsible for updating the console UI with curses. """
187*061da546Spatrick
188*061da546Spatrick    def __init__(self, screen, event_queue):
189*061da546Spatrick        self.screen = screen
190*061da546Spatrick        self.event_queue = event_queue
191*061da546Spatrick
192*061da546Spatrick        curses.start_color()
193*061da546Spatrick        curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
194*061da546Spatrick        curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_BLACK)
195*061da546Spatrick        curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
196*061da546Spatrick        self.screen.bkgd(curses.color_pair(1))
197*061da546Spatrick        self.screen.clear()
198*061da546Spatrick
199*061da546Spatrick        self.input_handler = InputHandler(self.screen, self.event_queue)
200*061da546Spatrick        self.input_handler.daemon = True
201*061da546Spatrick
202*061da546Spatrick        self.focus = 0
203*061da546Spatrick
204*061da546Spatrick        self.screen.refresh()
205*061da546Spatrick
206*061da546Spatrick    def focusNext(self):
207*061da546Spatrick        self.wins[self.focus].setFocus(False)
208*061da546Spatrick        old = self.focus
209*061da546Spatrick        while True:
210*061da546Spatrick            self.focus += 1
211*061da546Spatrick            if self.focus >= len(self.wins):
212*061da546Spatrick                self.focus = 0
213*061da546Spatrick            if self.wins[self.focus].canFocus():
214*061da546Spatrick                break
215*061da546Spatrick        self.wins[self.focus].setFocus(True)
216*061da546Spatrick
217*061da546Spatrick    def handleEvent(self, event):
218*061da546Spatrick        if isinstance(event, int):
219*061da546Spatrick            if event == curses.KEY_F3:
220*061da546Spatrick                self.focusNext()
221*061da546Spatrick
222*061da546Spatrick    def eventLoop(self):
223*061da546Spatrick
224*061da546Spatrick        self.input_handler.start()
225*061da546Spatrick        self.wins[self.focus].setFocus(True)
226*061da546Spatrick
227*061da546Spatrick        while True:
228*061da546Spatrick            self.screen.noutrefresh()
229*061da546Spatrick
230*061da546Spatrick            for i, win in enumerate(self.wins):
231*061da546Spatrick                if i != self.focus:
232*061da546Spatrick                    win.draw()
233*061da546Spatrick            # Draw the focused window last so that the cursor shows up.
234*061da546Spatrick            if self.wins:
235*061da546Spatrick                self.wins[self.focus].draw()
236*061da546Spatrick            curses.doupdate()  # redraw the physical screen
237*061da546Spatrick
238*061da546Spatrick            event = self.event_queue.get()
239*061da546Spatrick
240*061da546Spatrick            for win in self.wins:
241*061da546Spatrick                if isinstance(event, int):
242*061da546Spatrick                    if win.getFocus() or not win.canFocus():
243*061da546Spatrick                        win.handleEvent(event)
244*061da546Spatrick                else:
245*061da546Spatrick                    win.handleEvent(event)
246*061da546Spatrick            self.handleEvent(event)
247*061da546Spatrick
248*061da546Spatrick
249*061da546Spatrickclass CursesEditLine(object):
250*061da546Spatrick    """ Embed an 'editline'-compatible prompt inside a CursesWin. """
251*061da546Spatrick
252*061da546Spatrick    def __init__(self, win, history, enterCallback, tabCompleteCallback):
253*061da546Spatrick        self.win = win
254*061da546Spatrick        self.history = history
255*061da546Spatrick        self.enterCallback = enterCallback
256*061da546Spatrick        self.tabCompleteCallback = tabCompleteCallback
257*061da546Spatrick
258*061da546Spatrick        self.prompt = ''
259*061da546Spatrick        self.content = ''
260*061da546Spatrick        self.index = 0
261*061da546Spatrick        self.startx = -1
262*061da546Spatrick        self.starty = -1
263*061da546Spatrick
264*061da546Spatrick    def draw(self, prompt=None):
265*061da546Spatrick        if not prompt:
266*061da546Spatrick            prompt = self.prompt
267*061da546Spatrick        (h, w) = self.win.getmaxyx()
268*061da546Spatrick        if (len(prompt) + len(self.content)) / w + self.starty >= h - 1:
269*061da546Spatrick            self.win.scroll(1)
270*061da546Spatrick            self.starty -= 1
271*061da546Spatrick            if self.starty < 0:
272*061da546Spatrick                raise RuntimeError('Input too long; aborting')
273*061da546Spatrick        (y, x) = (self.starty, self.startx)
274*061da546Spatrick
275*061da546Spatrick        self.win.move(y, x)
276*061da546Spatrick        self.win.clrtobot()
277*061da546Spatrick        self.win.addstr(y, x, prompt)
278*061da546Spatrick        remain = self.content
279*061da546Spatrick        self.win.addstr(remain[:w - len(prompt)])
280*061da546Spatrick        remain = remain[w - len(prompt):]
281*061da546Spatrick        while remain != '':
282*061da546Spatrick            y += 1
283*061da546Spatrick            self.win.addstr(y, 0, remain[:w])
284*061da546Spatrick            remain = remain[w:]
285*061da546Spatrick
286*061da546Spatrick        length = self.index + len(prompt)
287*061da546Spatrick        self.win.move(self.starty + length / w, length % w)
288*061da546Spatrick
289*061da546Spatrick    def showPrompt(self, y, x, prompt=None):
290*061da546Spatrick        self.content = ''
291*061da546Spatrick        self.index = 0
292*061da546Spatrick        self.startx = x
293*061da546Spatrick        self.starty = y
294*061da546Spatrick        self.draw(prompt)
295*061da546Spatrick
296*061da546Spatrick    def handleEvent(self, event):
297*061da546Spatrick        if not isinstance(event, int):
298*061da546Spatrick            return  # not handled
299*061da546Spatrick        key = event
300*061da546Spatrick
301*061da546Spatrick        if self.startx == -1:
302*061da546Spatrick            raise RuntimeError('Trying to handle input without prompt')
303*061da546Spatrick
304*061da546Spatrick        if key == curses.ascii.NL:
305*061da546Spatrick            self.enterCallback(self.content)
306*061da546Spatrick        elif key == curses.ascii.TAB:
307*061da546Spatrick            self.tabCompleteCallback(self.content)
308*061da546Spatrick        elif curses.ascii.isprint(key):
309*061da546Spatrick            self.content = self.content[:self.index] + \
310*061da546Spatrick                chr(key) + self.content[self.index:]
311*061da546Spatrick            self.index += 1
312*061da546Spatrick        elif key == curses.KEY_BACKSPACE or key == curses.ascii.BS:
313*061da546Spatrick            if self.index > 0:
314*061da546Spatrick                self.index -= 1
315*061da546Spatrick                self.content = self.content[
316*061da546Spatrick                    :self.index] + self.content[self.index + 1:]
317*061da546Spatrick        elif key == curses.KEY_DC or key == curses.ascii.DEL or key == curses.ascii.EOT:
318*061da546Spatrick            self.content = self.content[
319*061da546Spatrick                :self.index] + self.content[self.index + 1:]
320*061da546Spatrick        elif key == curses.ascii.VT:  # CTRL-K
321*061da546Spatrick            self.content = self.content[:self.index]
322*061da546Spatrick        elif key == curses.KEY_LEFT or key == curses.ascii.STX:  # left or CTRL-B
323*061da546Spatrick            if self.index > 0:
324*061da546Spatrick                self.index -= 1
325*061da546Spatrick        elif key == curses.KEY_RIGHT or key == curses.ascii.ACK:  # right or CTRL-F
326*061da546Spatrick            if self.index < len(self.content):
327*061da546Spatrick                self.index += 1
328*061da546Spatrick        elif key == curses.ascii.SOH:  # CTRL-A
329*061da546Spatrick            self.index = 0
330*061da546Spatrick        elif key == curses.ascii.ENQ:  # CTRL-E
331*061da546Spatrick            self.index = len(self.content)
332*061da546Spatrick        elif key == curses.KEY_UP or key == curses.ascii.DLE:  # up or CTRL-P
333*061da546Spatrick            self.content = self.history.previous(self.content)
334*061da546Spatrick            self.index = len(self.content)
335*061da546Spatrick        elif key == curses.KEY_DOWN or key == curses.ascii.SO:  # down or CTRL-N
336*061da546Spatrick            self.content = self.history.next()
337*061da546Spatrick            self.index = len(self.content)
338*061da546Spatrick        self.draw()
339