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