... |
... |
@@ -16,8 +16,10 @@ |
16
|
16
|
#
|
17
|
17
|
# Authors:
|
18
|
18
|
# Tristan Van Berkom <tristan vanberkom codethink co uk>
|
|
19
|
+import os
|
|
20
|
+import sys
|
19
|
21
|
import click
|
20
|
|
-from blessings import Terminal
|
|
22
|
+import curses
|
21
|
23
|
|
22
|
24
|
# Import a widget internal for formatting time codes
|
23
|
25
|
from .widget import TimeCode
|
... |
... |
@@ -43,6 +45,13 @@ from .._scheduler import ElementJob |
43
|
45
|
#
|
44
|
46
|
class Status():
|
45
|
47
|
|
|
48
|
+ # Table of the terminal capabilities we require and use
|
|
49
|
+ _TERM_CAPABILITIES = {
|
|
50
|
+ 'move_up': 'cuu1',
|
|
51
|
+ 'move_x': 'hpa',
|
|
52
|
+ 'clear_eol': 'el'
|
|
53
|
+ }
|
|
54
|
+
|
46
|
55
|
def __init__(self, context,
|
47
|
56
|
content_profile, format_profile,
|
48
|
57
|
success_profile, error_profile,
|
... |
... |
@@ -56,7 +65,6 @@ class Status(): |
56
|
65
|
self._stream = stream
|
57
|
66
|
self._jobs = []
|
58
|
67
|
self._last_lines = 0 # Number of status lines we last printed to console
|
59
|
|
- self._term = Terminal()
|
60
|
68
|
self._spacing = 1
|
61
|
69
|
self._colors = colors
|
62
|
70
|
self._header = _StatusHeader(context,
|
... |
... |
@@ -69,6 +77,7 @@ class Status(): |
69
|
77
|
self._alloc_columns = None
|
70
|
78
|
self._line_length = 0
|
71
|
79
|
self._need_alloc = True
|
|
80
|
+ self._term_caps = self._init_terminal()
|
72
|
81
|
|
73
|
82
|
# add_job()
|
74
|
83
|
#
|
... |
... |
@@ -121,7 +130,7 @@ class Status(): |
121
|
130
|
#
|
122
|
131
|
def clear(self):
|
123
|
132
|
|
124
|
|
- if not self._term.does_styling:
|
|
133
|
+ if not self._term_caps:
|
125
|
134
|
return
|
126
|
135
|
|
127
|
136
|
for _ in range(self._last_lines):
|
... |
... |
@@ -138,7 +147,7 @@ class Status(): |
138
|
147
|
# not necessary to call clear().
|
139
|
148
|
def render(self):
|
140
|
149
|
|
141
|
|
- if not self._term.does_styling:
|
|
150
|
+ if not self._term_caps:
|
142
|
151
|
return
|
143
|
152
|
|
144
|
153
|
elapsed = self._stream.elapsed_time
|
... |
... |
@@ -185,6 +194,55 @@ class Status(): |
185
|
194
|
###################################################
|
186
|
195
|
# Private Methods #
|
187
|
196
|
###################################################
|
|
197
|
+
|
|
198
|
+ # _init_terminal()
|
|
199
|
+ #
|
|
200
|
+ # Initialize the terminal and return the resolved terminal
|
|
201
|
+ # capabilities dictionary.
|
|
202
|
+ #
|
|
203
|
+ # Returns:
|
|
204
|
+ # (dict|None): The resolved terminal capabilities dictionary,
|
|
205
|
+ # or None if the terminal does not support all
|
|
206
|
+ # of the required capabilities.
|
|
207
|
+ #
|
|
208
|
+ def _init_terminal(self):
|
|
209
|
+
|
|
210
|
+ # We need both output streams to be connected to a terminal
|
|
211
|
+ if not (sys.stdout.isatty() and sys.stderr.isatty()):
|
|
212
|
+ return None
|
|
213
|
+
|
|
214
|
+ # Initialized terminal, curses might decide it doesnt
|
|
215
|
+ # support this terminal
|
|
216
|
+ try:
|
|
217
|
+ curses.setupterm(os.environ.get('TERM', 'dumb'))
|
|
218
|
+ except curses.error:
|
|
219
|
+ return None
|
|
220
|
+
|
|
221
|
+ term_caps = {}
|
|
222
|
+
|
|
223
|
+ # Resolve the string capabilities we need for the capability
|
|
224
|
+ # names we need.
|
|
225
|
+ #
|
|
226
|
+ for capname, capval in self._TERM_CAPABILITIES.items():
|
|
227
|
+ code = curses.tigetstr(capval)
|
|
228
|
+
|
|
229
|
+ # If any of the required capabilities resolve empty strings or None,
|
|
230
|
+ # then we don't have the capabilities we need for a status bar on
|
|
231
|
+ # this terminal.
|
|
232
|
+ if not code:
|
|
233
|
+ return None
|
|
234
|
+
|
|
235
|
+ # Decode sequences as latin1, as they are always 8-bit bytes,
|
|
236
|
+ # so when b'\xff' is returned, this must be decoded to u'\xff'.
|
|
237
|
+ #
|
|
238
|
+ # This technique is employed by the python blessings library
|
|
239
|
+ # as well, and should provide better compatibility with most
|
|
240
|
+ # terminals.
|
|
241
|
+ #
|
|
242
|
+ term_caps[capname] = code.decode('latin1')
|
|
243
|
+
|
|
244
|
+ return term_caps
|
|
245
|
+
|
188
|
246
|
def _check_term_width(self):
|
189
|
247
|
term_width, _ = click.get_terminal_size()
|
190
|
248
|
if self._term_width != term_width:
|
... |
... |
@@ -192,12 +250,24 @@ class Status(): |
192
|
250
|
self._need_alloc = True
|
193
|
251
|
|
194
|
252
|
def _move_up(self):
|
|
253
|
+ assert self._term_caps is not None
|
|
254
|
+
|
195
|
255
|
# Explicitly move to beginning of line, fixes things up
|
196
|
256
|
# when there was a ^C or ^Z printed to the terminal.
|
197
|
|
- click.echo(self._term.move_x(0) + self._term.move_up, nl=False, err=True)
|
|
257
|
+ move_x = curses.tparm(self._term_caps['move_x'].encode('latin1'), 0)
|
|
258
|
+ move_x = move_x.decode('latin1')
|
|
259
|
+
|
|
260
|
+ move_up = curses.tparm(self._term_caps['move_up'].encode('latin1'))
|
|
261
|
+ move_up = move_up.decode('latin1')
|
|
262
|
+
|
|
263
|
+ click.echo(move_x + move_up, nl=False, err=True)
|
198
|
264
|
|
199
|
265
|
def _clear_line(self):
|
200
|
|
- click.echo(self._term.clear_eol, nl=False, err=True)
|
|
266
|
+ assert self._term_caps is not None
|
|
267
|
+
|
|
268
|
+ clear_eol = curses.tparm(self._term_caps['clear_eol'].encode('latin1'))
|
|
269
|
+ clear_eol = clear_eol.decode('latin1')
|
|
270
|
+ click.echo(clear_eol, nl=False, err=True)
|
201
|
271
|
|
202
|
272
|
def _allocate(self):
|
203
|
273
|
if not self._need_alloc:
|