1 ##===-- cui.py -----------------------------------------------*- Python -*-===##
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
7 ##===----------------------------------------------------------------------===##
14 class CursesWin(object):
16 def __init__(self
, x
, y
, w
, h
):
17 self
.win
= curses
.newwin(h
, w
, y
, x
)
20 def setFocus(self
, focus
):
29 def handleEvent(self
, event
):
36 class TextWin(CursesWin
):
38 def __init__(self
, x
, y
, w
):
39 super(TextWin
, self
).__init
__(x
, y
, w
, 1)
40 self
.win
.bkgd(curses
.color_pair(1))
48 w
= self
.win
.getmaxyx()[1]
51 #trunc_length = len(text) - w
54 self
.win
.addstr(0, 0, text
, curses
.A_REVERSE
)
56 self
.win
.addstr(0, 0, text
)
57 self
.win
.noutrefresh()
59 def setReverse(self
, reverse
):
60 self
.reverse
= reverse
62 def setText(self
, text
):
66 class TitledWin(CursesWin
):
68 def __init__(self
, x
, y
, w
, h
, title
):
69 super(TitledWin
, self
).__init
__(x
, y
+ 1, w
, h
- 1)
71 self
.title_win
= TextWin(x
, y
, w
)
72 self
.title_win
.setText(title
)
75 def setTitle(self
, title
):
76 self
.title_win
.setText(title
)
79 self
.title_win
.setReverse(self
.getFocus())
81 self
.win
.noutrefresh()
84 class ListWin(CursesWin
):
86 def __init__(self
, x
, y
, w
, h
):
87 super(ListWin
, self
).__init
__(x
, y
, w
, h
)
91 self
.win
.leaveok(True)
94 if len(self
.items
) == 0:
98 h
, w
= self
.win
.getmaxyx()
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]
109 if i
== self
.getSelected():
110 firstSelected
= len(allLines
)
111 allLines
.extend(lines
)
112 if i
== self
.selected
:
113 lastSelected
= len(allLines
) - 1
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
122 begin
= self
.first_drawn
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)
132 # Ignore the error we get from drawing over the bottom-right char.
134 self
.win
.addstr(y
, 0, line
[:w
], attr
)
138 self
.win
.noutrefresh()
140 def getSelected(self
):
145 def setSelected(self
, selected
):
146 self
.selected
= selected
147 if self
.selected
< 0:
149 elif self
.selected
>= len(self
.items
):
150 self
.selected
= len(self
.items
) - 1
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
)
162 def addItem(self
, item
):
163 self
.items
.append(item
)
165 def clearItems(self
):
168 def handleSelect(self
, index
):
172 class InputHandler(threading
.Thread
):
174 def __init__(self
, screen
, queue
):
175 super(InputHandler
, self
).__init
__()
181 c
= self
.screen
.getch()
185 class CursesUI(object):
186 """ Responsible for updating the console UI with curses. """
188 def __init__(self
, screen
, event_queue
):
190 self
.event_queue
= event_queue
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))
199 self
.input_handler
= InputHandler(self
.screen
, self
.event_queue
)
200 self
.input_handler
.daemon
= True
204 self
.screen
.refresh()
207 self
.wins
[self
.focus
].setFocus(False)
211 if self
.focus
>= len(self
.wins
):
213 if self
.wins
[self
.focus
].canFocus():
215 self
.wins
[self
.focus
].setFocus(True)
217 def handleEvent(self
, event
):
218 if isinstance(event
, int):
219 if event
== curses
.KEY_F3
:
224 self
.input_handler
.start()
225 self
.wins
[self
.focus
].setFocus(True)
228 self
.screen
.noutrefresh()
230 for i
, win
in enumerate(self
.wins
):
233 # Draw the focused window last so that the cursor shows up.
235 self
.wins
[self
.focus
].draw()
236 curses
.doupdate() # redraw the physical screen
238 event
= self
.event_queue
.get()
240 for win
in self
.wins
:
241 if isinstance(event
, int):
242 if win
.getFocus() or not win
.canFocus():
243 win
.handleEvent(event
)
245 win
.handleEvent(event
)
246 self
.handleEvent(event
)
249 class CursesEditLine(object):
250 """ Embed an 'editline'-compatible prompt inside a CursesWin. """
252 def __init__(self
, win
, history
, enterCallback
, tabCompleteCallback
):
254 self
.history
= history
255 self
.enterCallback
= enterCallback
256 self
.tabCompleteCallback
= tabCompleteCallback
264 def draw(self
, prompt
=None):
267 (h
, w
) = self
.win
.getmaxyx()
268 if (len(prompt
) + len(self
.content
)) / w
+ self
.starty
>= h
- 1:
272 raise RuntimeError('Input too long; aborting')
273 (y
, x
) = (self
.starty
, self
.startx
)
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
):]
283 self
.win
.addstr(y
, 0, remain
[:w
])
286 length
= self
.index
+ len(prompt
)
287 self
.win
.move(self
.starty
+ length
/ w
, length
% w
)
289 def showPrompt(self
, y
, x
, prompt
=None):
296 def handleEvent(self
, event
):
297 if not isinstance(event
, int):
301 if self
.startx
== -1:
302 raise RuntimeError('Trying to handle input without prompt')
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
:]
312 elif key
== curses
.KEY_BACKSPACE
or key
== curses
.ascii
.BS
:
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
325 elif key
== curses
.KEY_RIGHT
or key
== curses
.ascii
.ACK
: # right or CTRL-F
326 if self
.index
< len(self
.content
):
328 elif key
== curses
.ascii
.SOH
: # CTRL-A
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
)