1 # -*- coding: utf-8 -*-
5 from PyQt4
.QtCore
import QRect
, Qt
, pyqtSignal
6 from PyQt4
.QtGui
import (
7 QApplication
, QClipboard
, QWidget
, QPainter
, QFont
, QBrush
, QColor
,
8 QPen
, QPixmap
, QImage
, QContextMenuEvent
)
10 from .backend
import Session
16 class TerminalWidget(QWidget
):
18 foreground_color_map
= {
31 12: "#00f", # concelaed
33 14: "#000", # negative
36 background_color_map
= {
47 15: "#fff", # negative
50 Qt
.Key_Backspace
: chr(127),
51 Qt
.Key_Escape
: chr(27),
52 Qt
.Key_AsciiTilde
: "~~",
58 Qt
.Key_PageDown
: "~2",
77 session_closed
= pyqtSignal()
79 def __init__(self
, parent
=None, command
="/bin/bash",
80 font_name
="Monospace", font_size
=18):
81 super(TerminalWidget
, self
).__init
__(parent
)
82 self
.parent().setTabOrder(self
, self
)
83 self
.setFocusPolicy(Qt
.WheelFocus
)
84 self
.setAutoFillBackground(False)
85 self
.setAttribute(Qt
.WA_OpaquePaintEvent
, True)
86 self
.setCursor(Qt
.IBeamCursor
)
87 font
= QFont(font_name
)
88 font
.setPixelSize(font_size
)
91 self
._last
_update
= None
94 self
._cursor
_rect
= None
99 self
._press
_pos
= None
100 self
._selection
= None
101 self
._clipboard
= QApplication
.clipboard()
102 QApplication
.instance().lastWindowClosed
.connect(Session
.close_all
)
106 def execute(self
, command
="/bin/bash"):
107 self
._session
= Session()
108 self
._session
.start(command
)
109 self
._timer
_id
= None
110 # start timer either with high or low priority
112 self
.focusInEvent(None)
114 self
.focusOutEvent(None)
117 self
._session
.write(s
)
123 return self
._session
.pid()
125 def setFont(self
, font
):
126 super(TerminalWidget
, self
).setFont(font
)
127 self
._update
_metrics
()
129 def focusNextPrevChild(self
, next
):
130 if not self
._session
.is_alive():
134 def focusInEvent(self
, event
):
135 if not self
._session
.is_alive():
137 if self
._timer
_id
is not None:
138 self
.killTimer(self
._timer
_id
)
139 self
._timer
_id
= self
.startTimer(250)
142 def focusOutEvent(self
, event
):
143 if not self
._session
.is_alive():
145 # reduced update interval
146 # -> slower screen updates
147 # -> but less load on main app which results in better responsiveness
148 if self
._timer
_id
is not None:
149 self
.killTimer(self
._timer
_id
)
150 self
._timer
_id
= self
.startTimer(750)
152 def resizeEvent(self
, event
):
153 if not self
._session
.is_alive():
155 self
._columns
, self
._rows
= self
._pixel
2pos
(
156 self
.width(), self
.height())
157 self
._session
.resize(self
._columns
, self
._rows
)
159 def closeEvent(self
, event
):
160 if not self
._session
.is_alive():
162 self
._session
.close()
164 def timerEvent(self
, event
):
165 if not self
._session
.is_alive():
166 if self
._timer
_id
is not None:
167 self
.killTimer(self
._timer
_id
)
168 self
._timer
_id
= None
170 print "Session closed"
171 self
.session_closed
.emit()
173 last_change
= self
._session
.last_change()
176 if not self
._last
_update
or last_change
> self
._last
_update
:
177 self
._last
_update
= last_change
178 old_screen
= self
._screen
179 (self
._cursor
_col
, self
._cursor
_row
), self
._screen
= self
._session
.dump()
180 self
._update
_cursor
_rect
()
181 if old_screen
!= self
._screen
:
184 self
._blink
= not self
._blink
187 def _update_metrics(self
):
188 fm
= self
.fontMetrics()
189 self
._char
_height
= fm
.height()
190 self
._char
_width
= fm
.width("W")
192 def _update_cursor_rect(self
):
193 cx
, cy
= self
._pos
2pixel
(self
._cursor
_col
, self
._cursor
_row
)
194 self
._cursor
_rect
= QRect(cx
, cy
, self
._char
_width
, self
._char
_height
)
197 self
._update
_metrics
()
198 self
._update
_cursor
_rect
()
199 self
.resizeEvent(None)
202 def update_screen(self
):
206 def paintEvent(self
, event
):
207 painter
= QPainter(self
)
210 self
._paint
_screen
(painter
)
212 if self
._cursor
_rect
and not self
._selection
:
213 self
._paint
_cursor
(painter
)
215 self
._paint
_selection
(painter
)
218 def _pixel2pos(self
, x
, y
):
219 col
= int(round(x
/ self
._char
_width
))
220 row
= int(round(y
/ self
._char
_height
))
223 def _pos2pixel(self
, col
, row
):
224 x
= col
* self
._char
_width
225 y
= row
* self
._char
_height
228 def _paint_cursor(self
, painter
):
233 painter
.setPen(QPen(QColor(color
)))
234 painter
.drawRect(self
._cursor
_rect
)
236 def _paint_screen(self
, painter
):
237 # Speed hacks: local name lookups are faster
238 vars().update(QColor
=QColor
, QBrush
=QBrush
, QPen
=QPen
, QRect
=QRect
)
239 background_color_map
= self
.background_color_map
240 foreground_color_map
= self
.foreground_color_map
241 char_width
= self
._char
_width
242 char_height
= self
._char
_height
243 painter_drawText
= painter
.drawText
244 painter_fillRect
= painter
.fillRect
245 painter_setPen
= painter
.setPen
246 align
= Qt
.AlignTop | Qt
.AlignLeft
248 background_color
= background_color_map
[14]
249 foreground_color
= foreground_color_map
[15]
250 brush
= QBrush(QColor(background_color
))
251 painter_fillRect(self
.rect(), brush
)
252 pen
= QPen(QColor(foreground_color
))
256 text_append
= text
.append
257 for row
, line
in enumerate(self
._screen
):
261 if isinstance(item
, basestring
):
265 x
, y
, x
+ char_width
* length
, y
+ char_height
)
266 painter_fillRect(rect
, brush
)
267 painter_drawText(rect
, align
, item
)
271 foreground_color_idx
, background_color_idx
, underline_flag
= item
272 foreground_color
= foreground_color_map
[
273 foreground_color_idx
]
274 background_color
= background_color_map
[
275 background_color_idx
]
276 pen
= QPen(QColor(foreground_color
))
277 brush
= QBrush(QColor(background_color
))
279 # painter.setBrush(brush)
281 text_append(text_line
)
284 def _paint_selection(self
, painter
):
285 pcol
= QColor(200, 200, 200, 50)
287 bcol
= QColor(230, 230, 230, 50)
290 painter
.setBrush(brush
)
291 for (start_col
, start_row
, end_col
, end_row
) in self
._selection
:
292 x
, y
= self
._pos
2pixel
(start_col
, start_row
)
293 width
, height
= self
._pos
2pixel
(
294 end_col
- start_col
, end_row
- start_row
)
295 rect
= QRect(x
, y
, width
, height
)
296 # painter.drawRect(rect)
297 painter
.fillRect(rect
, brush
)
301 font
.setPixelSize(font
.pixelSize() + 2)
307 font
.setPixelSize(font
.pixelSize() - 2)
311 return_pressed
= pyqtSignal()
313 def keyPressEvent(self
, event
):
314 text
= unicode(event
.text())
316 modifiers
= event
.modifiers()
317 ctrl
= modifiers
== Qt
.ControlModifier
318 if ctrl
and key
== Qt
.Key_Plus
:
320 elif ctrl
and key
== Qt
.Key_Minus
:
323 if text
and key
!= Qt
.Key_Backspace
:
324 self
.send(text
.encode("utf-8"))
326 s
= self
.keymap
.get(key
)
328 self
.send(s
.encode("utf-8"))
330 print "Unkonwn key combination"
331 print "Modifiers:", modifiers
334 if not name
.startswith("Key_"):
336 value
= getattr(Qt
, name
)
338 print "Symbol: Qt.%s" % name
339 print "Text: %r" % text
341 if key
in (Qt
.Key_Enter
, Qt
.Key_Return
):
342 self
.return_pressed
.emit()
344 def mousePressEvent(self
, event
):
345 button
= event
.button()
346 if button
== Qt
.RightButton
:
347 ctx_event
= QContextMenuEvent(QContextMenuEvent
.Mouse
, event
.pos())
348 self
.contextMenuEvent(ctx_event
)
349 self
._press
_pos
= None
350 elif button
== Qt
.LeftButton
:
351 self
._press
_pos
= event
.pos()
352 self
._selection
= None
354 elif button
== Qt
.MiddleButton
:
355 self
._press
_pos
= None
356 self
._selection
= None
357 text
= unicode(self
._clipboard
.text(QClipboard
.Selection
))
358 self
.send(text
.encode("utf-8"))
359 # self.update_screen()
361 def mouseReleaseEvent(self
, QMouseEvent
):
362 pass # self.update_screen()
364 def _selection_rects(self
, start_pos
, end_pos
):
365 sx
, sy
= start_pos
.x(), start_pos
.y()
366 start_col
, start_row
= self
._pixel
2pos
(sx
, sy
)
367 ex
, ey
= end_pos
.x(), end_pos
.y()
368 end_col
, end_row
= self
._pixel
2pos
(ex
, ey
)
369 if start_row
== end_row
:
370 if ey
> sy
or end_row
== 0:
374 if start_col
== end_col
:
375 if ex
> sx
or end_col
== 0:
379 if start_row
> end_row
:
380 start_row
, end_row
= end_row
, start_row
381 if start_col
> end_col
:
382 start_col
, end_col
= end_col
, start_col
383 if end_row
- start_row
== 1:
384 return [(start_col
, start_row
, end_col
, end_row
)]
387 (start_col
, start_row
, self
._columns
, start_row
+ 1),
388 (0, start_row
+ 1, self
._columns
, end_row
- 1),
389 (0, end_row
- 1, end_col
, end_row
)
392 def text(self
, rect
=None):
394 return "\n".join(self
._text
)
397 (start_col
, start_row
, end_col
, end_row
) = rect
398 for row
in range(start_row
, end_row
):
399 text
.append(self
._text
[row
][start_col
:end_col
])
402 def text_selection(self
):
404 for (start_col
, start_row
, end_col
, end_row
) in self
._selection
:
405 for row
in range(start_row
, end_row
):
406 text
.append(self
._text
[row
][start_col
:end_col
])
407 return "\n".join(text
)
409 def column_count(self
):
415 def mouseMoveEvent(self
, event
):
417 move_pos
= event
.pos()
418 self
._selection
= self
._selection
_rects
(self
._press
_pos
, move_pos
)
420 sel
= self
.text_selection()
422 print "%r copied to xselection" % sel
423 self
._clipboard
.setText(sel
, QClipboard
.Selection
)
427 def mouseDoubleClickEvent(self
, event
):
428 self
._press
_pos
= None
429 # double clicks create a selection for the word under the cursor
431 x
, y
= pos
.x(), pos
.y()
432 col
, row
= self
._pixel
2pos
(x
, y
)
433 line
= self
._text
[row
]
438 char
= line
[start_col
]
439 if not char
.isalnum() and char
not in ("_",):
446 while end_col
< self
._columns
:
448 if not char
.isalnum() and char
not in ("_",):
453 (start_col
+ found_left
, row
, end_col
- found_right
+ 1, row
+ 1)]
455 sel
= self
.text_selection()
457 print "%r copied to xselection" % sel
458 self
._clipboard
.setText(sel
, QClipboard
.Selection
)
463 return (self
._session
and self
._session
.is_alive()) or False