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