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):
15 def __init__(self
, x
, y
, w
, h
):
16 self
.win
= curses
.newwin(h
, w
, y
, x
)
19 def setFocus(self
, focus
):
28 def handleEvent(self
, event
):
35 class 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))
46 w
= self
.win
.getmaxyx()[1]
49 # trunc_length = len(text) - w
52 self
.win
.addstr(0, 0, text
, curses
.A_REVERSE
)
54 self
.win
.addstr(0, 0, text
)
55 self
.win
.noutrefresh()
57 def setReverse(self
, reverse
):
58 self
.reverse
= reverse
60 def setText(self
, text
):
64 class TitledWin(CursesWin
):
65 def __init__(self
, x
, y
, w
, h
, title
):
66 super(TitledWin
, self
).__init
__(x
, y
+ 1, w
, h
- 1)
68 self
.title_win
= TextWin(x
, y
, w
)
69 self
.title_win
.setText(title
)
72 def setTitle(self
, title
):
73 self
.title_win
.setText(title
)
76 self
.title_win
.setReverse(self
.getFocus())
78 self
.win
.noutrefresh()
81 class ListWin(CursesWin
):
82 def __init__(self
, x
, y
, w
, h
):
83 super(ListWin
, self
).__init
__(x
, y
, w
, h
)
87 self
.win
.leaveok(True)
90 if len(self
.items
) == 0:
94 h
, w
= self
.win
.getmaxyx()
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]
105 if i
== self
.getSelected():
106 firstSelected
= len(allLines
)
107 allLines
.extend(lines
)
108 if i
== self
.selected
:
109 lastSelected
= len(allLines
) - 1
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
118 begin
= self
.first_drawn
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)
128 # Ignore the error we get from drawing over the bottom-right char.
130 self
.win
.addstr(y
, 0, line
[:w
], attr
)
134 self
.win
.noutrefresh()
136 def getSelected(self
):
141 def setSelected(self
, selected
):
142 self
.selected
= selected
143 if self
.selected
< 0:
145 elif self
.selected
>= len(self
.items
):
146 self
.selected
= len(self
.items
) - 1
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
)
158 def addItem(self
, item
):
159 self
.items
.append(item
)
161 def clearItems(self
):
164 def handleSelect(self
, index
):
168 class InputHandler(threading
.Thread
):
169 def __init__(self
, screen
, queue
):
170 super(InputHandler
, self
).__init
__()
176 c
= self
.screen
.getch()
180 class CursesUI(object):
181 """Responsible for updating the console UI with curses."""
183 def __init__(self
, screen
, event_queue
):
185 self
.event_queue
= event_queue
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))
194 self
.input_handler
= InputHandler(self
.screen
, self
.event_queue
)
195 self
.input_handler
.daemon
= True
199 self
.screen
.refresh()
202 self
.wins
[self
.focus
].setFocus(False)
206 if self
.focus
>= len(self
.wins
):
208 if self
.wins
[self
.focus
].canFocus():
210 self
.wins
[self
.focus
].setFocus(True)
212 def handleEvent(self
, event
):
213 if isinstance(event
, int):
214 if event
== curses
.KEY_F3
:
218 self
.input_handler
.start()
219 self
.wins
[self
.focus
].setFocus(True)
222 self
.screen
.noutrefresh()
224 for i
, win
in enumerate(self
.wins
):
227 # Draw the focused window last so that the cursor shows up.
229 self
.wins
[self
.focus
].draw()
230 curses
.doupdate() # redraw the physical screen
232 event
= self
.event_queue
.get()
234 for win
in self
.wins
:
235 if isinstance(event
, int):
236 if win
.getFocus() or not win
.canFocus():
237 win
.handleEvent(event
)
239 win
.handleEvent(event
)
240 self
.handleEvent(event
)
243 class CursesEditLine(object):
244 """Embed an 'editline'-compatible prompt inside a CursesWin."""
246 def __init__(self
, win
, history
, enterCallback
, tabCompleteCallback
):
248 self
.history
= history
249 self
.enterCallback
= enterCallback
250 self
.tabCompleteCallback
= tabCompleteCallback
258 def draw(self
, prompt
=None):
261 (h
, w
) = self
.win
.getmaxyx()
262 if (len(prompt
) + len(self
.content
)) / w
+ self
.starty
>= h
- 1:
266 raise RuntimeError("Input too long; aborting")
267 (y
, x
) = (self
.starty
, self
.startx
)
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
) :]
277 self
.win
.addstr(y
, 0, remain
[:w
])
280 length
= self
.index
+ len(prompt
)
281 self
.win
.move(self
.starty
+ length
/ w
, length
% w
)
283 def showPrompt(self
, y
, x
, prompt
=None):
290 def handleEvent(self
, event
):
291 if not isinstance(event
, int):
295 if self
.startx
== -1:
296 raise RuntimeError("Trying to handle input without prompt")
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
):
304 self
.content
[: self
.index
] + chr(key
) + self
.content
[self
.index
:]
307 elif key
== curses
.KEY_BACKSPACE
or key
== curses
.ascii
.BS
:
311 self
.content
[: self
.index
] + self
.content
[self
.index
+ 1 :]
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
320 elif key
== curses
.KEY_RIGHT
or key
== curses
.ascii
.ACK
: # right or CTRL-F
321 if self
.index
< len(self
.content
):
323 elif key
== curses
.ascii
.SOH
: # CTRL-A
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
)