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