efibootdude 0.5.2__tar.gz → 0.5.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of efibootdude might be problematic. Click here for more details.
- {efibootdude-0.5.2 → efibootdude-0.5.3}/PKG-INFO +2 -1
- {efibootdude-0.5.2 → efibootdude-0.5.3}/efibootdude/main.py +7 -7
- {efibootdude-0.5.2 → efibootdude-0.5.3}/pyproject.toml +2 -1
- efibootdude-0.5.2/deploy +0 -8
- efibootdude-0.5.2/efibootdude/PowerWindow.py +0 -740
- {efibootdude-0.5.2 → efibootdude-0.5.3}/.gitignore +0 -0
- {efibootdude-0.5.2 → efibootdude-0.5.3}/LICENSE +0 -0
- {efibootdude-0.5.2 → efibootdude-0.5.3}/README.md +0 -0
- {efibootdude-0.5.2 → efibootdude-0.5.3}/efibootdude/__init__.py +0 -0
- {efibootdude-0.5.2 → efibootdude-0.5.3}/images/efibootdude-screenshot.png +0 -0
- {efibootdude-0.5.2 → efibootdude-0.5.3}/tests/efibootmgr.peggy.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: efibootdude
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: A visual wrapper for efibootmgr
|
|
5
5
|
Keywords: app,installer,manager,appimages
|
|
6
6
|
Author-email: Joe Defen <joedef@duck.com>
|
|
@@ -11,6 +11,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
11
11
|
Classifier: Operating System :: POSIX :: Linux
|
|
12
12
|
License-File: LICENSE
|
|
13
13
|
Requires-Dist: importlib-metadata; python_version<"3.8"
|
|
14
|
+
Requires-Dist: console-window >= 1.0.0
|
|
14
15
|
Project-URL: Bug Tracker, https://github.com/joedefen/efibootdude/issues
|
|
15
16
|
Project-URL: Homepage, https://github.com/joedefen/efibootdude
|
|
16
17
|
|
|
@@ -20,7 +20,7 @@ import traceback
|
|
|
20
20
|
import curses as cs
|
|
21
21
|
import argparse
|
|
22
22
|
# import xml.etree.ElementTree as ET
|
|
23
|
-
from
|
|
23
|
+
from console_window import ConsoleWindow, OptionSpinner
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class EfiBootDude:
|
|
@@ -52,7 +52,7 @@ class EfiBootDude:
|
|
|
52
52
|
self.digests, self.width1, self.label_wid, self.boot_idx = [], 0, 0, 0
|
|
53
53
|
self.win = None
|
|
54
54
|
self.reinit()
|
|
55
|
-
self.win =
|
|
55
|
+
self.win = ConsoleWindow(head_line=True, body_rows=len(self.digests)+20, head_rows=10,
|
|
56
56
|
keys=spin.keys ^ other_keys, mod_pick=self.mod_pick)
|
|
57
57
|
self.win.pick_pos = self.boot_idx
|
|
58
58
|
|
|
@@ -224,13 +224,13 @@ class EfiBootDude:
|
|
|
224
224
|
|
|
225
225
|
def reboot(self):
|
|
226
226
|
""" Reboot the machine """
|
|
227
|
-
|
|
227
|
+
ConsoleWindow.stop_curses()
|
|
228
228
|
os.system('clear; stty sane; (set -x; sudo reboot now)')
|
|
229
229
|
|
|
230
230
|
# NOTE: probably will not get here...
|
|
231
231
|
os.system(r'/bin/echo -e "\n\n===== Press ENTER for menu ====> \c"; read FOO')
|
|
232
232
|
self.reinit()
|
|
233
|
-
|
|
233
|
+
ConsoleWindow._start_curses()
|
|
234
234
|
self.win.pick_pos = self.boot_idx
|
|
235
235
|
|
|
236
236
|
def write(self):
|
|
@@ -255,7 +255,7 @@ class EfiBootDude:
|
|
|
255
255
|
cmds.append(f'{prefix} --bootnext {self.mods.next}')
|
|
256
256
|
if self.mods.timeout:
|
|
257
257
|
cmds.append(f'{prefix} --timeout {self.mods.timeout}')
|
|
258
|
-
|
|
258
|
+
ConsoleWindow.stop_curses()
|
|
259
259
|
os.system('clear; stty sane')
|
|
260
260
|
print('Commands:')
|
|
261
261
|
for cmd in cmds:
|
|
@@ -271,7 +271,7 @@ class EfiBootDude:
|
|
|
271
271
|
os.system(r'/bin/echo -e "\n\n===== Press ENTER for menu ====> \c"; read FOO')
|
|
272
272
|
self.reinit()
|
|
273
273
|
|
|
274
|
-
|
|
274
|
+
ConsoleWindow._start_curses()
|
|
275
275
|
self.win.pick_pos = self.boot_idx
|
|
276
276
|
|
|
277
277
|
def main_loop(self):
|
|
@@ -530,7 +530,7 @@ if __name__ == '__main__':
|
|
|
530
530
|
except KeyboardInterrupt:
|
|
531
531
|
pass
|
|
532
532
|
except Exception as exce:
|
|
533
|
-
|
|
533
|
+
ConsoleWindow.stop_curses()
|
|
534
534
|
print("exception:", str(exce))
|
|
535
535
|
print(traceback.format_exc())
|
|
536
536
|
# if dump_str:
|
|
@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "efibootdude"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.3"
|
|
8
8
|
description = "A visual wrapper for efibootmgr"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Joe Defen", email = "joedef@duck.com" }
|
|
@@ -20,6 +20,7 @@ classifiers = [
|
|
|
20
20
|
]
|
|
21
21
|
dependencies = [
|
|
22
22
|
'importlib-metadata; python_version<"3.8"',
|
|
23
|
+
'console-window >= 1.0.0',
|
|
23
24
|
]
|
|
24
25
|
|
|
25
26
|
[project.urls]
|
efibootdude-0.5.2/deploy
DELETED
|
@@ -1,740 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""
|
|
4
|
-
Custom Wrapper for python curses.
|
|
5
|
-
"""
|
|
6
|
-
# pylint: disable=too-many-instance-attributes,too-many-arguments
|
|
7
|
-
# pylint: disable=invalid-name,broad-except,too-many-branches
|
|
8
|
-
|
|
9
|
-
# pylint: disable=wrong-import-position,disable=wrong-import-order
|
|
10
|
-
# import VirtEnv
|
|
11
|
-
# VirtEnv.ensure_venv(__name__)
|
|
12
|
-
|
|
13
|
-
import traceback
|
|
14
|
-
import atexit
|
|
15
|
-
import signal
|
|
16
|
-
import time
|
|
17
|
-
import curses
|
|
18
|
-
import textwrap
|
|
19
|
-
from types import SimpleNamespace
|
|
20
|
-
from curses.textpad import rectangle, Textbox
|
|
21
|
-
dump_str = None
|
|
22
|
-
|
|
23
|
-
def ignore_ctrl_c():
|
|
24
|
-
"""" Ignore SIGINT """
|
|
25
|
-
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
26
|
-
|
|
27
|
-
def restore_ctrl_c():
|
|
28
|
-
"""" Handle SIGINT """
|
|
29
|
-
signal.signal(signal.SIGINT, signal.default_int_handler)
|
|
30
|
-
|
|
31
|
-
class OptionSpinner:
|
|
32
|
-
"""Manage a bunch of options where the value is rotate thru
|
|
33
|
-
a fixed set of values pressing a key."""
|
|
34
|
-
def __init__(self):
|
|
35
|
-
"""Give the object with the attribute to change its
|
|
36
|
-
value (e.g., options from argparse or "self" from
|
|
37
|
-
the object managing the window).
|
|
38
|
-
|
|
39
|
-
And array of specs like:
|
|
40
|
-
['a - allow auto suggestions', 'allow_auto', True, False],
|
|
41
|
-
['/ - filter pattern', 'filter_str', self.filter_str],
|
|
42
|
-
A spec can have a trailing None + more comments shown after
|
|
43
|
-
the value.
|
|
44
|
-
"""
|
|
45
|
-
self.options, self.keys = [], []
|
|
46
|
-
self.margin = 4 # + actual width (1st column right pos)
|
|
47
|
-
self.align = self.margin # + actual width (1st column right pos)
|
|
48
|
-
self.default_obj = SimpleNamespace() # if not given one
|
|
49
|
-
self.attr_to_option = {} # given an attribute, find its option ns
|
|
50
|
-
self.key_to_option = {} # given key, options namespace
|
|
51
|
-
self.keys = set()
|
|
52
|
-
|
|
53
|
-
@staticmethod
|
|
54
|
-
def _make_option_ns():
|
|
55
|
-
return SimpleNamespace(
|
|
56
|
-
keys=[],
|
|
57
|
-
descr='',
|
|
58
|
-
obj=None,
|
|
59
|
-
attr='',
|
|
60
|
-
vals=None,
|
|
61
|
-
prompt=None,
|
|
62
|
-
comments=[],
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
def get_value(self, attr, coerce=False):
|
|
66
|
-
"""Get the value of the given attribute."""
|
|
67
|
-
ns = self.attr_to_option.get(attr, None)
|
|
68
|
-
obj = ns.obj if ns else None
|
|
69
|
-
value = getattr(obj, attr, None) if obj else None
|
|
70
|
-
if value is None and obj and coerce:
|
|
71
|
-
if ns.vals:
|
|
72
|
-
if value not in ns.vals:
|
|
73
|
-
value = ns.vals[0]
|
|
74
|
-
setattr(obj, attr, value)
|
|
75
|
-
else:
|
|
76
|
-
if value is None:
|
|
77
|
-
value = ''
|
|
78
|
-
setattr(ns.obj, ns.attr, '')
|
|
79
|
-
return value
|
|
80
|
-
|
|
81
|
-
def _register(self, ns):
|
|
82
|
-
""" Create the mappings needed"""
|
|
83
|
-
assert ns.attr not in self.attr_to_option
|
|
84
|
-
self.attr_to_option[ns.attr] = ns
|
|
85
|
-
for key in ns.keys:
|
|
86
|
-
assert key not in self.key_to_option, f'key ({chr(key)}, {key}) already used'
|
|
87
|
-
self.key_to_option[key] = ns
|
|
88
|
-
self.keys.add(key)
|
|
89
|
-
self.options.append(ns)
|
|
90
|
-
self.align = max(self.align, self.margin+len(ns.descr))
|
|
91
|
-
self.get_value(ns.attr, coerce=True)
|
|
92
|
-
|
|
93
|
-
def add(self, obj, specs):
|
|
94
|
-
""" Compatibility Method."""
|
|
95
|
-
for spec in specs:
|
|
96
|
-
ns = self._make_option_ns()
|
|
97
|
-
ns.descr = spec[0]
|
|
98
|
-
ns.obj = obj
|
|
99
|
-
ns.attr = spec[1]
|
|
100
|
-
ns.vals=spec[2:]
|
|
101
|
-
if None in ns.vals:
|
|
102
|
-
idx = ns.vals.index(None)
|
|
103
|
-
ns.vals = ns.vals[:idx]
|
|
104
|
-
ns.comments = ns.vals[idx+1:]
|
|
105
|
-
ns.keys = [ord(ns.descr[0])]
|
|
106
|
-
self._register(ns)
|
|
107
|
-
|
|
108
|
-
def add_key(self, attr, descr, obj=None, vals=None, prompt=None, keys=None, comments=None):
|
|
109
|
-
""" Standard method"""
|
|
110
|
-
ns = self._make_option_ns()
|
|
111
|
-
if keys:
|
|
112
|
-
ns.keys = list(keys) if isinstance(keys, (list, tuple, set)) else [keys]
|
|
113
|
-
else:
|
|
114
|
-
ns.keys = [ord(descr[0])]
|
|
115
|
-
if comments is None:
|
|
116
|
-
ns.comments = []
|
|
117
|
-
else:
|
|
118
|
-
ns.comments = list(comments) if isinstance(keys, (list, tuple)) else [comments]
|
|
119
|
-
ns.descr = descr
|
|
120
|
-
ns.attr = attr
|
|
121
|
-
ns.obj = obj if obj else self.default_obj
|
|
122
|
-
ns.vals, ns.prompt = vals, prompt
|
|
123
|
-
assert bool(ns.vals) ^ bool(ns.prompt)
|
|
124
|
-
self._register(ns)
|
|
125
|
-
|
|
126
|
-
@staticmethod
|
|
127
|
-
def show_help_nav_keys(win):
|
|
128
|
-
"""For help screens, show the navigation keys. """
|
|
129
|
-
for line in Window.get_nav_keys_blurb().splitlines():
|
|
130
|
-
if line:
|
|
131
|
-
win.add_header(line)
|
|
132
|
-
|
|
133
|
-
def show_help_body(self, win):
|
|
134
|
-
""" Write the help page section."""
|
|
135
|
-
win.add_body('Type keys to alter choice:', curses.A_UNDERLINE)
|
|
136
|
-
|
|
137
|
-
for ns in self.options:
|
|
138
|
-
# get / coerce the current value
|
|
139
|
-
value = self.get_value(ns.attr)
|
|
140
|
-
assert value is not None, f'cannot get value of {repr(ns.attr)}'
|
|
141
|
-
choices = ns.vals if ns.vals else [value]
|
|
142
|
-
|
|
143
|
-
win.add_body(f'{ns.descr:>{self.align}}: ')
|
|
144
|
-
|
|
145
|
-
for choice in choices:
|
|
146
|
-
shown = f'{choice}'
|
|
147
|
-
if isinstance(choice, bool):
|
|
148
|
-
shown = "ON" if choice else "off"
|
|
149
|
-
win.add_body(' ', resume=True)
|
|
150
|
-
win.add_body(shown, resume=True,
|
|
151
|
-
attr=curses.A_REVERSE if choice == value else None)
|
|
152
|
-
|
|
153
|
-
for comment in ns.comments:
|
|
154
|
-
win.add_body(f'{"":>{self.align}}: {comment}')
|
|
155
|
-
|
|
156
|
-
def do_key(self, key, win):
|
|
157
|
-
"""Do the automated processing of a key."""
|
|
158
|
-
ns = self.key_to_option.get(key, None)
|
|
159
|
-
if ns is None:
|
|
160
|
-
return None
|
|
161
|
-
value = self.get_value(ns.attr)
|
|
162
|
-
if ns.vals:
|
|
163
|
-
idx = ns.vals.index(value) if value in ns.vals else -1
|
|
164
|
-
value = ns.vals[(idx+1) % len(ns.vals)] # choose next
|
|
165
|
-
else:
|
|
166
|
-
value = win.answer(prompt=ns.prompt, seed=str(value))
|
|
167
|
-
setattr(ns.obj, ns.attr, value)
|
|
168
|
-
return value
|
|
169
|
-
|
|
170
|
-
class Window:
|
|
171
|
-
""" Layer above curses to encapsulate what we need """
|
|
172
|
-
timeout_ms = 200
|
|
173
|
-
static_scr = None
|
|
174
|
-
nav_keys = """
|
|
175
|
-
Navigation: H/M/L: top/middle/end-of-page
|
|
176
|
-
k, UP: up one row 0, HOME: first row
|
|
177
|
-
j, DOWN: down one row $, END: last row
|
|
178
|
-
Ctrl-u: half-page up Ctrl-b, PPAGE: page up
|
|
179
|
-
Ctrl-d: half-page down Ctrl-f, NPAGE: page down
|
|
180
|
-
"""
|
|
181
|
-
def __init__(self, head_line=True, head_rows=50, body_rows=200,
|
|
182
|
-
body_cols=200, keys=None, pick_mode=False, pick_size=1,
|
|
183
|
-
mod_pick=None):
|
|
184
|
-
self.scr = self._start_curses()
|
|
185
|
-
|
|
186
|
-
self.head = SimpleNamespace(
|
|
187
|
-
pad=curses.newpad(head_rows, body_cols),
|
|
188
|
-
rows=head_rows,
|
|
189
|
-
cols=body_cols,
|
|
190
|
-
row_cnt=0, # no. head rows added
|
|
191
|
-
texts = [],
|
|
192
|
-
view_cnt=0, # no. head rows viewable (NOT in body)
|
|
193
|
-
)
|
|
194
|
-
self.body = SimpleNamespace(
|
|
195
|
-
pad = curses.newpad(body_rows, body_cols),
|
|
196
|
-
rows= body_rows,
|
|
197
|
-
cols=body_cols,
|
|
198
|
-
row_cnt = 0,
|
|
199
|
-
texts = []
|
|
200
|
-
)
|
|
201
|
-
self.mod_pick = mod_pick # call back to modify highlighted row
|
|
202
|
-
self.hor_line_cnt = 1 if head_line else 0 # no. h-lines in header
|
|
203
|
-
self.scroll_pos = 0 # how far down into body are we?
|
|
204
|
-
self.max_scroll_pos = 0
|
|
205
|
-
self.pick_pos = 0 # in highlight mode, where are we?
|
|
206
|
-
self.last_pick_pos = -1 # last highlighted position
|
|
207
|
-
self.pick_mode = pick_mode # whether in highlight mode
|
|
208
|
-
self.pick_size = pick_size # whether in highlight mode
|
|
209
|
-
self.rows, self.cols = 0, 0
|
|
210
|
-
self.body_cols, self.body_rows = body_cols, body_rows
|
|
211
|
-
self.scroll_view_size = 0 # no. viewable lines of the body
|
|
212
|
-
self.handled_keys = set(keys) if isinstance(keys, (set, list)) else []
|
|
213
|
-
self._set_screen_dims()
|
|
214
|
-
self.calc()
|
|
215
|
-
|
|
216
|
-
def get_pad_width(self):
|
|
217
|
-
# how much space to actually draw chars?
|
|
218
|
-
return min(self.cols-1, self.body_cols)
|
|
219
|
-
|
|
220
|
-
@staticmethod
|
|
221
|
-
def get_nav_keys_blurb():
|
|
222
|
-
"""For a help screen, describe the nav keys"""
|
|
223
|
-
return textwrap.dedent(Window.nav_keys)
|
|
224
|
-
|
|
225
|
-
def _set_screen_dims(self):
|
|
226
|
-
"""Recalculate dimensions ... return True if geometry changed."""
|
|
227
|
-
rows, cols = self.scr.getmaxyx()
|
|
228
|
-
same = bool(rows == self.rows and cols == self.cols)
|
|
229
|
-
self.rows, self.cols = rows, cols
|
|
230
|
-
return same
|
|
231
|
-
|
|
232
|
-
@staticmethod
|
|
233
|
-
def _start_curses():
|
|
234
|
-
""" Curses initial setup. Note: not using curses.wrapper because we
|
|
235
|
-
don't wish to change the colors. """
|
|
236
|
-
atexit.register(Window.stop_curses)
|
|
237
|
-
ignore_ctrl_c()
|
|
238
|
-
Window.static_scr = scr = curses.initscr()
|
|
239
|
-
curses.noecho()
|
|
240
|
-
curses.cbreak()
|
|
241
|
-
curses.curs_set(0)
|
|
242
|
-
scr.keypad(1)
|
|
243
|
-
scr.timeout(Window.timeout_ms)
|
|
244
|
-
scr.clear()
|
|
245
|
-
return scr
|
|
246
|
-
|
|
247
|
-
def set_pick_mode(self, on=True, pick_size=1):
|
|
248
|
-
"""Set whether in highlight mode."""
|
|
249
|
-
was_on, was_size = self.pick_mode, self.pick_size
|
|
250
|
-
self.pick_mode = bool(on)
|
|
251
|
-
self.pick_size = max(pick_size, 1)
|
|
252
|
-
if self.pick_mode and (not was_on or was_size != self.pick_size):
|
|
253
|
-
self.last_pick_pos = -2 # indicates need to clear them all
|
|
254
|
-
|
|
255
|
-
@staticmethod
|
|
256
|
-
def stop_curses():
|
|
257
|
-
""" Curses shutdown (registered to be called on exit). """
|
|
258
|
-
if Window.static_scr:
|
|
259
|
-
curses.nocbreak()
|
|
260
|
-
curses.echo()
|
|
261
|
-
Window.static_scr.keypad(0)
|
|
262
|
-
curses.endwin()
|
|
263
|
-
Window.static_scr = None
|
|
264
|
-
restore_ctrl_c()
|
|
265
|
-
|
|
266
|
-
def calc(self):
|
|
267
|
-
"""Recalculate dimensions ... return True if geometry changed."""
|
|
268
|
-
same = self._set_screen_dims()
|
|
269
|
-
self.head.view_cnt = min(self.rows - self.hor_line_cnt, self.head.row_cnt)
|
|
270
|
-
self.scroll_view_size = self.rows - self.head.view_cnt - self.hor_line_cnt
|
|
271
|
-
self.max_scroll_pos = max(self.body.row_cnt - self.scroll_view_size, 0)
|
|
272
|
-
self.body_base = self.head.view_cnt + self.hor_line_cnt
|
|
273
|
-
return not same
|
|
274
|
-
|
|
275
|
-
def _put(self, ns, *args):
|
|
276
|
-
""" Add text to head/body pad using its namespace. args:
|
|
277
|
-
- bytes (converted to str/unicode)
|
|
278
|
-
- str (unicode)
|
|
279
|
-
- None (same as curses.A_NORMAL)
|
|
280
|
-
- int (curses attribute)
|
|
281
|
-
"""
|
|
282
|
-
def flush(attr=None):
|
|
283
|
-
nonlocal self, is_body, row, text, seg, first
|
|
284
|
-
if (is_body and self.pick_mode) or attr is None:
|
|
285
|
-
attr = curses.A_NORMAL
|
|
286
|
-
if seg and first:
|
|
287
|
-
ns.pad.addstr(row, 0, seg[0:self.get_pad_width()], attr)
|
|
288
|
-
elif seg:
|
|
289
|
-
_, x = ns.pad.getyx()
|
|
290
|
-
cols = self.get_pad_width() - x
|
|
291
|
-
if cols > 0:
|
|
292
|
-
ns.pad.addstr(seg[0:cols], attr)
|
|
293
|
-
text += seg
|
|
294
|
-
seg, first, attr = '', False, None
|
|
295
|
-
|
|
296
|
-
is_body = bool(id(ns) == id(self.body))
|
|
297
|
-
if ns.row_cnt < ns.rows:
|
|
298
|
-
row = max(ns.row_cnt, 0)
|
|
299
|
-
text, seg, first = '', '', True
|
|
300
|
-
for arg in args:
|
|
301
|
-
if isinstance(arg, bytes):
|
|
302
|
-
arg = arg.decode('utf-8')
|
|
303
|
-
if isinstance(arg, str):
|
|
304
|
-
seg += arg # note: add w/o spacing
|
|
305
|
-
elif arg is None or isinstance(arg, (int)):
|
|
306
|
-
# assume arg is attribute ... flushes text
|
|
307
|
-
flush(attr=arg)
|
|
308
|
-
flush()
|
|
309
|
-
ns.texts.append(text) # text only history
|
|
310
|
-
ns.row_cnt += 1
|
|
311
|
-
|
|
312
|
-
def put_head(self, *args):
|
|
313
|
-
""" Put a line above the line."""
|
|
314
|
-
self._put(self.head, *args)
|
|
315
|
-
|
|
316
|
-
def put_body(self, *args):
|
|
317
|
-
""" Put a line below the line."""
|
|
318
|
-
self._put(self.body, *args)
|
|
319
|
-
|
|
320
|
-
def _add(self, ns, text, attr=None, resume=False):
|
|
321
|
-
""" Add text to head/body pad using its namespace"""
|
|
322
|
-
is_body = bool(id(ns) == id(self.body))
|
|
323
|
-
if ns.row_cnt < ns.rows:
|
|
324
|
-
row = max(ns.row_cnt - (1 if resume else 0), 0)
|
|
325
|
-
if (is_body and self.pick_mode) or attr is None:
|
|
326
|
-
attr = curses.A_NORMAL
|
|
327
|
-
if resume:
|
|
328
|
-
_, x = ns.pad.getyx()
|
|
329
|
-
cols = self.get_pad_width() - x
|
|
330
|
-
if cols > 0:
|
|
331
|
-
ns.pad.addstr(text[0:cols], attr)
|
|
332
|
-
ns.texts[row] += text
|
|
333
|
-
else:
|
|
334
|
-
ns.pad.addstr(row, 0, text[0:self.cols], attr)
|
|
335
|
-
ns.texts.append(text) # text only history
|
|
336
|
-
ns.row_cnt += 1
|
|
337
|
-
|
|
338
|
-
def add_header(self, text, attr=None, resume=False):
|
|
339
|
-
"""Add text to header"""
|
|
340
|
-
self._add(self.head, text, attr, resume)
|
|
341
|
-
|
|
342
|
-
def add_body(self, text, attr=None, resume=False):
|
|
343
|
-
""" Add text to body (below header and header line)"""
|
|
344
|
-
self._add(self.body, text, attr, resume)
|
|
345
|
-
|
|
346
|
-
def draw(self, y, x, text, text_attr=None, width=None, leftpad=False, header=False):
|
|
347
|
-
"""Draws the given text (as utf-8 or unicode) at position (row=y,col=x)
|
|
348
|
-
with optional text attributes and width.
|
|
349
|
-
This is more compatible with my older, simpler Window class.
|
|
350
|
-
"""
|
|
351
|
-
ns = self.head if header else self.body
|
|
352
|
-
text_attr = text_attr if text_attr else curses.A_NORMAL
|
|
353
|
-
if y < 0 or y >= ns.rows or x < 0 or x >= ns.cols:
|
|
354
|
-
return # nada if out of bounds
|
|
355
|
-
ns.row_cnt = max(ns.row_cnt, y+1)
|
|
356
|
-
|
|
357
|
-
uni = text if isinstance(text, str) else text.decode('utf-8')
|
|
358
|
-
|
|
359
|
-
if width is not None:
|
|
360
|
-
width = min(width, self.get_pad_width() - x)
|
|
361
|
-
if width <= 0:
|
|
362
|
-
return
|
|
363
|
-
padlen = width - len(uni)
|
|
364
|
-
if padlen > 0:
|
|
365
|
-
if leftpad:
|
|
366
|
-
uni = padlen * ' ' + uni
|
|
367
|
-
else: # rightpad
|
|
368
|
-
uni += padlen * ' '
|
|
369
|
-
text = uni[:width].encode('utf-8')
|
|
370
|
-
else:
|
|
371
|
-
text = uni.encode('utf-8')
|
|
372
|
-
|
|
373
|
-
try:
|
|
374
|
-
while y >= len(ns.texts):
|
|
375
|
-
ns.texts.append('')
|
|
376
|
-
ns.texts[y] = ns.texts[y][:x].ljust(x) + uni + ns.texts[y][x+len(uni):]
|
|
377
|
-
ns.pad.addstr(y, x, text, text_attr)
|
|
378
|
-
except curses.error:
|
|
379
|
-
# this sucks, but curses returns an error if drawing the last character
|
|
380
|
-
# on the screen always. this can happen if resizing screen even if
|
|
381
|
-
# special care is taken. So, we just ignore errors. Anyhow, you cannot
|
|
382
|
-
# get decent error handling.
|
|
383
|
-
pass
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
def highlight_picked(self):
|
|
387
|
-
"""Highlight the current pick and un-highlight the previous pick."""
|
|
388
|
-
def get_text(pos):
|
|
389
|
-
nonlocal self
|
|
390
|
-
return self.body.texts[pos][0:self.cols] if pos < len(self.body.texts) else ''
|
|
391
|
-
|
|
392
|
-
if not self.pick_mode:
|
|
393
|
-
return
|
|
394
|
-
pos0, pos1 = self.last_pick_pos, self.pick_pos
|
|
395
|
-
if pos0 == -2: # special flag to clear all formatting
|
|
396
|
-
for row in range(self.body.row_cnt):
|
|
397
|
-
line = get_text(row).ljust(self.get_pad_width())
|
|
398
|
-
self.body.pad.addstr(row, 0, get_text(row), curses.A_NORMAL)
|
|
399
|
-
if pos0 != pos1:
|
|
400
|
-
if 0 <= pos0 < self.body.row_cnt:
|
|
401
|
-
for i in range(self.pick_size):
|
|
402
|
-
line = get_text(pos0+i).ljust(self.get_pad_width())
|
|
403
|
-
self.body.pad.addstr(pos0+i, 0, line, curses.A_NORMAL)
|
|
404
|
-
if 0 <= pos1 < self.body.row_cnt:
|
|
405
|
-
for i in range(self.pick_size):
|
|
406
|
-
line = get_text(pos1+i)
|
|
407
|
-
if self.mod_pick:
|
|
408
|
-
line = self.mod_pick(line)
|
|
409
|
-
line = line.ljust(self.get_pad_width())
|
|
410
|
-
self.body.pad.addstr(pos1+i, 0, line, curses.A_REVERSE)
|
|
411
|
-
self.last_pick_pos = pos1
|
|
412
|
-
|
|
413
|
-
def _scroll_indicator_row(self):
|
|
414
|
-
""" Compute the absolute scroll indicator row:
|
|
415
|
-
- We want the top to be only when scroll_pos==0
|
|
416
|
-
- We want the bottom to be only when scroll_pos=max_scroll_pos-1
|
|
417
|
-
"""
|
|
418
|
-
if self.max_scroll_pos <= 1:
|
|
419
|
-
return self.body_base
|
|
420
|
-
y2, y1 = self.scroll_view_size-1, 1
|
|
421
|
-
x2, x1 = self.max_scroll_pos, 1
|
|
422
|
-
x = self.scroll_pos
|
|
423
|
-
pos = y1 + (y2-y1)*(x-x1)/(x2-x1)
|
|
424
|
-
return min(self.body_base + int(max(pos, 0)), self.rows-1)
|
|
425
|
-
|
|
426
|
-
def _scroll_indicator_col(self):
|
|
427
|
-
""" Compute the absolute scroll indicator col:
|
|
428
|
-
- We want the left to be only when scroll_pos==0
|
|
429
|
-
- We want the right to be only when scroll_pos=max_scroll_pos-1
|
|
430
|
-
"""
|
|
431
|
-
if self.pick_mode:
|
|
432
|
-
return self._calc_indicator(
|
|
433
|
-
self.pick_pos, 0, self.body.row_cnt-1, 0, self.cols-1)
|
|
434
|
-
return self._calc_indicator(
|
|
435
|
-
self.scroll_pos, 0, self.max_scroll_pos, 0, self.cols-1)
|
|
436
|
-
|
|
437
|
-
def _calc_indicator(self, pos, pos0, pos9, ind0, ind9):
|
|
438
|
-
if self.max_scroll_pos <= 0:
|
|
439
|
-
return -1 # not scrollable
|
|
440
|
-
if pos9 - pos0 <= 0:
|
|
441
|
-
return -1 # not scrollable
|
|
442
|
-
if pos <= pos0:
|
|
443
|
-
return ind0
|
|
444
|
-
if pos >= pos9:
|
|
445
|
-
return ind9
|
|
446
|
-
ind = int(round(ind0 + (ind9-ind0+1)*(pos-pos0)/(pos9-pos0+1)))
|
|
447
|
-
return min(max(ind, ind0+1), ind9-1)
|
|
448
|
-
|
|
449
|
-
def render(self):
|
|
450
|
-
"""Draw everything added. In a loop cuz curses is a
|
|
451
|
-
piece of shit."""
|
|
452
|
-
for _ in range(128):
|
|
453
|
-
try:
|
|
454
|
-
self.render_once()
|
|
455
|
-
return
|
|
456
|
-
except curses.error:
|
|
457
|
-
time.sleep(0.16)
|
|
458
|
-
self._set_screen_dims()
|
|
459
|
-
continue
|
|
460
|
-
try:
|
|
461
|
-
self.render_once()
|
|
462
|
-
except Exception:
|
|
463
|
-
Window.stop_curses()
|
|
464
|
-
print(f"""curses err:
|
|
465
|
-
head.row_cnt={self.head.row_cnt}
|
|
466
|
-
head.view_cnt={self.head.view_cnt}
|
|
467
|
-
hor_line_cnt={self.hor_line_cnt}
|
|
468
|
-
body.row_cnt={self.body.row_cnt}
|
|
469
|
-
scroll_pos={self.scroll_pos}
|
|
470
|
-
max_scroll_pos={self.max_scroll_pos}
|
|
471
|
-
pick_pos={self.pick_pos}
|
|
472
|
-
last_pick_pos={self.last_pick_pos}
|
|
473
|
-
pick_mode={self.pick_mode}
|
|
474
|
-
pick_size={self.pick_size}
|
|
475
|
-
rows={self.rows}
|
|
476
|
-
cols={self.cols}
|
|
477
|
-
""")
|
|
478
|
-
raise
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
def fix_positions(self, delta=0):
|
|
482
|
-
""" Ensure the vertical positions are on the playing field """
|
|
483
|
-
self.calc()
|
|
484
|
-
# if self.scroll_view_size <= 0:
|
|
485
|
-
# self.scr.refresh()
|
|
486
|
-
if self.pick_mode:
|
|
487
|
-
self.pick_pos += delta
|
|
488
|
-
else:
|
|
489
|
-
self.scroll_pos += delta
|
|
490
|
-
self.pick_pos += delta
|
|
491
|
-
|
|
492
|
-
indent = 0
|
|
493
|
-
if self.body_base < self.rows:
|
|
494
|
-
ind_pos = 0 if self.pick_mode else self._scroll_indicator_row()
|
|
495
|
-
if self.pick_mode:
|
|
496
|
-
self.pick_pos = max(self.pick_pos, 0)
|
|
497
|
-
self.pick_pos = min(self.pick_pos, self.body.row_cnt-1)
|
|
498
|
-
if self.pick_pos >= 0:
|
|
499
|
-
self.pick_pos -= (self.pick_pos % self.pick_size)
|
|
500
|
-
if self.pick_pos < 0:
|
|
501
|
-
self.scroll_pos = 0
|
|
502
|
-
elif self.scroll_pos > self.pick_pos:
|
|
503
|
-
# light position is below body bottom
|
|
504
|
-
self.scroll_pos = self.pick_pos
|
|
505
|
-
elif self.scroll_pos < self.pick_pos - (self.scroll_view_size - self.pick_size):
|
|
506
|
-
# light position is above body top
|
|
507
|
-
self.scroll_pos = self.pick_pos - (self.scroll_view_size - self.pick_size)
|
|
508
|
-
self.scroll_pos = max(self.scroll_pos, 0)
|
|
509
|
-
self.scroll_pos = min(self.scroll_pos, self.max_scroll_pos)
|
|
510
|
-
indent = 1
|
|
511
|
-
else:
|
|
512
|
-
self.scroll_pos = max(self.scroll_pos, 0)
|
|
513
|
-
self.scroll_pos = min(self.scroll_pos, self.max_scroll_pos)
|
|
514
|
-
self.pick_pos = self.scroll_pos + ind_pos - self.body_base
|
|
515
|
-
# indent = 1 if self.body.row_cnt > self.scroll_view_size else 0
|
|
516
|
-
return indent
|
|
517
|
-
|
|
518
|
-
def render_once(self):
|
|
519
|
-
"""Draw everything added."""
|
|
520
|
-
|
|
521
|
-
indent = self.fix_positions()
|
|
522
|
-
|
|
523
|
-
if indent > 0 and self.pick_mode:
|
|
524
|
-
self.scr.vline(self.body_base, 0, ' ', self.scroll_view_size)
|
|
525
|
-
if self.pick_pos >= 0:
|
|
526
|
-
pos = self.pick_pos - self.scroll_pos + self.body_base
|
|
527
|
-
self.scr.addstr(pos, 0, '>', curses.A_REVERSE)
|
|
528
|
-
|
|
529
|
-
if self.head.view_cnt < self.rows:
|
|
530
|
-
self.scr.hline(self.head.view_cnt, 0, curses.ACS_HLINE, self.cols)
|
|
531
|
-
ind_pos = self._scroll_indicator_col()
|
|
532
|
-
if ind_pos >= 0:
|
|
533
|
-
bot, cnt = ind_pos, 1
|
|
534
|
-
if 0 < ind_pos < self.cols-1:
|
|
535
|
-
width = self.scroll_view_size/self.body.row_cnt*self.cols
|
|
536
|
-
bot = max(int(round(ind_pos-width/2)), 1)
|
|
537
|
-
top = min(int(round(ind_pos+width/2)), self.cols-1)
|
|
538
|
-
cnt = top - bot
|
|
539
|
-
# self.scr.addstr(self.head.view_cnt, bot, '-'*cnt, curses.A_REVERSE)
|
|
540
|
-
# self.scr.hline(self.head.view_cnt, bot, curses.ACS_HLINE, curses.A_REVERSE, cnt)
|
|
541
|
-
for idx in range(bot, bot+cnt):
|
|
542
|
-
self.scr.addch(self.head.view_cnt, idx, curses.ACS_HLINE, curses.A_REVERSE)
|
|
543
|
-
|
|
544
|
-
self.scr.refresh()
|
|
545
|
-
|
|
546
|
-
if self.body_base < self.rows:
|
|
547
|
-
if self.pick_mode:
|
|
548
|
-
self.highlight_picked()
|
|
549
|
-
self.body.pad.refresh(self.scroll_pos, 0,
|
|
550
|
-
self.body_base, indent, self.rows-1, self.cols-1)
|
|
551
|
-
|
|
552
|
-
if self.rows > 0:
|
|
553
|
-
last_row = min(self.head.view_cnt, self.rows)-1
|
|
554
|
-
if last_row >= 0:
|
|
555
|
-
self.head.pad.refresh(0, 0, 0, indent, last_row, self.cols-1)
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
def answer(self, prompt='Type string [then Enter]', seed='', width=80):
|
|
559
|
-
"""Popup"""
|
|
560
|
-
def mod_key(key):
|
|
561
|
-
return 7 if key == 10 else key
|
|
562
|
-
|
|
563
|
-
# need 3 extra cols for rectangle (so we don't draw in southeast corner)
|
|
564
|
-
# and 3 rows (top/prompt/bottom)
|
|
565
|
-
# +Prompt--- -----------------+
|
|
566
|
-
# | Seed-for-answer |
|
|
567
|
-
# +---------Press ENTER to submit+
|
|
568
|
-
if self.rows < 3 or self.cols < 30:
|
|
569
|
-
return seed
|
|
570
|
-
width = min(width, self.cols-3) # max text width
|
|
571
|
-
row0, row9 = self.rows//2 - 1, self.rows//2 + 1
|
|
572
|
-
col0 = (self.cols - (width+2)) // 2
|
|
573
|
-
col9 = col0 + width + 2 - 1
|
|
574
|
-
|
|
575
|
-
self.scr.clear()
|
|
576
|
-
win = curses.newwin(1, width, row0+1, col0+1) # input window
|
|
577
|
-
rectangle(self.scr, row0, col0, row9, col9)
|
|
578
|
-
self.scr.addstr(row0, col0+1, prompt[0:width])
|
|
579
|
-
win.addstr(seed[0:width-1])
|
|
580
|
-
ending = 'Press ENTER to submit'[:width]
|
|
581
|
-
self.scr.addstr(row9, col0+1+width-len(ending), ending)
|
|
582
|
-
self.scr.refresh()
|
|
583
|
-
curses.curs_set(2)
|
|
584
|
-
answer = Textbox(win).edit(mod_key).strip()
|
|
585
|
-
curses.curs_set(0)
|
|
586
|
-
return answer
|
|
587
|
-
|
|
588
|
-
def alert(self, title='ALERT', message='', height=1, width=80):
|
|
589
|
-
"""Alert box"""
|
|
590
|
-
def mod_key(key):
|
|
591
|
-
return 7 if key in (10, curses.KEY_ENTER) else key
|
|
592
|
-
|
|
593
|
-
# need 3 extra cols for rectangle (so we don't draw in southeast corner)
|
|
594
|
-
# and 3 rows (top/prompt/bottom)
|
|
595
|
-
# +Prompt--- -----------------+
|
|
596
|
-
# | First line for message... |
|
|
597
|
-
# | Last line for message. |
|
|
598
|
-
# +-----------Press ENTER to ack+
|
|
599
|
-
if self.rows < 2+height or self.cols < 30:
|
|
600
|
-
return
|
|
601
|
-
width = min(width, self.cols-3) # max text width
|
|
602
|
-
row0 = (self.rows+height-1)//2 - 1
|
|
603
|
-
row9 = row0 + height + 1
|
|
604
|
-
col0 = (self.cols - (width+2)) // 2
|
|
605
|
-
col9 = col0 + width + 2 - 1
|
|
606
|
-
|
|
607
|
-
self.scr.clear()
|
|
608
|
-
for row in range(self.rows):
|
|
609
|
-
self.scr.insstr(row, 0, ' '*self.cols, curses.A_REVERSE)
|
|
610
|
-
pad = curses.newpad(20, 200)
|
|
611
|
-
win = curses.newwin(1, 1, row9-1, col9-2) # input window
|
|
612
|
-
rectangle(self.scr, row0, col0, row9, col9)
|
|
613
|
-
self.scr.addstr(row0, col0+1, title[0:width], curses.A_REVERSE)
|
|
614
|
-
pad.addstr(message)
|
|
615
|
-
ending = 'Press ENTER to ack'[:width]
|
|
616
|
-
self.scr.addstr(row9, col0+1+width-len(ending), ending)
|
|
617
|
-
self.scr.refresh()
|
|
618
|
-
pad.refresh(0, 0, row0+1, col0+1, row9-1, col9-1)
|
|
619
|
-
Textbox(win).edit(mod_key).strip()
|
|
620
|
-
return
|
|
621
|
-
|
|
622
|
-
def clear(self):
|
|
623
|
-
"""Clear in prep for new screen"""
|
|
624
|
-
self.scr.clear()
|
|
625
|
-
self.head.pad.clear()
|
|
626
|
-
self.body.pad.clear()
|
|
627
|
-
self.head.texts, self.body.texts, self.last_pick_pos = [], [], -1
|
|
628
|
-
self.head.row_cnt = self.body.row_cnt = 0
|
|
629
|
-
|
|
630
|
-
def prompt(self, seconds=1.0):
|
|
631
|
-
"""Here is where we sleep waiting for commands or timeout"""
|
|
632
|
-
ctl_b, ctl_d, ctl_f, ctl_u = 2, 4, 6, 21
|
|
633
|
-
elapsed = 0.0
|
|
634
|
-
while elapsed < seconds:
|
|
635
|
-
key = self.scr.getch()
|
|
636
|
-
if key == curses.ERR:
|
|
637
|
-
elapsed += self.timeout_ms / 1000
|
|
638
|
-
continue
|
|
639
|
-
if key in (curses.KEY_RESIZE, ) or curses.is_term_resized(self.rows, self.cols):
|
|
640
|
-
# self.scr.erase()
|
|
641
|
-
self._set_screen_dims()
|
|
642
|
-
# self.render()
|
|
643
|
-
break
|
|
644
|
-
|
|
645
|
-
# App keys...
|
|
646
|
-
if key in self.handled_keys:
|
|
647
|
-
return key # return for handling
|
|
648
|
-
|
|
649
|
-
# Navigation Keys...
|
|
650
|
-
pos = self.pick_pos if self.pick_mode else self.scroll_pos
|
|
651
|
-
delta = self.pick_size if self.pick_mode else 1
|
|
652
|
-
was_pos = pos
|
|
653
|
-
if key in (ord('k'), curses.KEY_UP):
|
|
654
|
-
pos -= delta
|
|
655
|
-
elif key in (ord('j'), curses.KEY_DOWN):
|
|
656
|
-
pos += delta
|
|
657
|
-
elif key in (ctl_b, curses.KEY_PPAGE):
|
|
658
|
-
pos -= self.scroll_view_size
|
|
659
|
-
elif key in (ctl_u, ):
|
|
660
|
-
pos -= self.scroll_view_size//2
|
|
661
|
-
elif key in (ctl_f, curses.KEY_NPAGE):
|
|
662
|
-
pos += self.scroll_view_size
|
|
663
|
-
elif key in (ctl_d, ):
|
|
664
|
-
pos += self.scroll_view_size//2
|
|
665
|
-
elif key in (ord('0'), curses.KEY_HOME):
|
|
666
|
-
pos = 0
|
|
667
|
-
elif key in (ord('$'), curses.KEY_END):
|
|
668
|
-
pos = self.body.row_cnt - 1
|
|
669
|
-
elif key in (ord('H'), ):
|
|
670
|
-
pos = self.scroll_pos
|
|
671
|
-
elif key in (ord('M'), ):
|
|
672
|
-
pos = self.scroll_pos + self.scroll_view_size//2
|
|
673
|
-
elif key in (ord('L'), ):
|
|
674
|
-
pos = self.scroll_pos + self.scroll_view_size-1
|
|
675
|
-
|
|
676
|
-
if self.pick_mode:
|
|
677
|
-
self.pick_pos = pos
|
|
678
|
-
else:
|
|
679
|
-
self.scroll_pos = pos
|
|
680
|
-
self.pick_pos = pos
|
|
681
|
-
|
|
682
|
-
self.fix_positions()
|
|
683
|
-
|
|
684
|
-
if pos != was_pos:
|
|
685
|
-
self.render()
|
|
686
|
-
# ignore unhandled keys
|
|
687
|
-
return None
|
|
688
|
-
|
|
689
|
-
def no_runner():
|
|
690
|
-
"""Appease sbrun"""
|
|
691
|
-
|
|
692
|
-
if __name__ == '__main__':
|
|
693
|
-
def main():
|
|
694
|
-
"""Test program"""
|
|
695
|
-
def do_key(key):
|
|
696
|
-
nonlocal spin, win, opts
|
|
697
|
-
value = spin.do_key(key, win)
|
|
698
|
-
if key in (ord('p'), ord('s')):
|
|
699
|
-
win.set_pick_mode(on=opts.pick_mode, pick_size=opts.pick_size)
|
|
700
|
-
elif key == ord('n'):
|
|
701
|
-
win.alert(title='Info', message=f'got: {value}')
|
|
702
|
-
return value
|
|
703
|
-
|
|
704
|
-
spin = OptionSpinner()
|
|
705
|
-
spin.add_key('help_mode', '? - toggle help screen', vals=[False, True])
|
|
706
|
-
spin.add_key('pick_mode', 'p - toggle pick mode', vals=[False, True])
|
|
707
|
-
spin.add_key('pick_size', 's - #rows in pick', vals=[1, 2, 3])
|
|
708
|
-
spin.add_key('name', 'n - select name', prompt='Provide Your Name:')
|
|
709
|
-
spin.add_key('mult', 'm - row multiplier', vals=[0.5, 0.9, 1.0, 1.1, 2, 4, 16])
|
|
710
|
-
opts = spin.default_obj
|
|
711
|
-
|
|
712
|
-
win = Window(head_line=True, keys=spin.keys)
|
|
713
|
-
opts.name = "[hit 'n' to enter name]"
|
|
714
|
-
for loop in range(100000000000):
|
|
715
|
-
body_size = int(round(win.scroll_view_size*opts.mult))
|
|
716
|
-
if opts.help_mode:
|
|
717
|
-
win.set_pick_mode(False)
|
|
718
|
-
spin.show_help_nav_keys(win)
|
|
719
|
-
spin.show_help_body(win)
|
|
720
|
-
else:
|
|
721
|
-
win.set_pick_mode(opts.pick_mode, opts.pick_size)
|
|
722
|
-
win.add_header(f'Header: {loop} "{opts.name}"')
|
|
723
|
-
for idx, line in enumerate(range(body_size//opts.pick_size)):
|
|
724
|
-
win.add_body(f'Main pick: {loop}.{line}')
|
|
725
|
-
for num in range(1, opts.pick_size):
|
|
726
|
-
win.draw(num+idx*opts.pick_size, 0, f' addon: {loop}.{line}')
|
|
727
|
-
win.render()
|
|
728
|
-
_ = do_key(win.prompt(seconds=5))
|
|
729
|
-
win.clear()
|
|
730
|
-
|
|
731
|
-
try:
|
|
732
|
-
main()
|
|
733
|
-
except KeyboardInterrupt:
|
|
734
|
-
pass
|
|
735
|
-
except Exception as exce:
|
|
736
|
-
Window.stop_curses()
|
|
737
|
-
print("exception:", str(exce))
|
|
738
|
-
print(traceback.format_exc())
|
|
739
|
-
if dump_str:
|
|
740
|
-
print(dump_str)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|