efibootdude 0.0__py3-none-any.whl

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.

@@ -0,0 +1,740 @@
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
efibootdude/main.py ADDED
@@ -0,0 +1,498 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Interactive, visual thin layer atop efibootmgr
5
+ """
6
+ # pylint: disable=broad-exception-caught,consider-using-with
7
+ # pylint: disable=too-many-instance-attributes,too-many-branches
8
+ # pylint: disable=too-many-return-statements,too-many-statements
9
+ # pylint: disable=consider-using-in,too-many-nested-blocks
10
+ # pylint: disable=wrong-import-position,disable=wrong-import-order
11
+ # pylint: disable=too-many-locals
12
+
13
+ import os
14
+ import sys
15
+ import re
16
+ import shutil
17
+ from types import SimpleNamespace
18
+ import subprocess
19
+ import traceback
20
+ import curses as cs
21
+ import xml.etree.ElementTree as ET
22
+ from efibootdude.PowerWindow import Window, OptionSpinner
23
+
24
+
25
+ class EfiBootDude:
26
+ """ Main class for curses atop efibootmgr"""
27
+ singleton = None
28
+
29
+ def __init__(self):
30
+ # self.cmd_loop = CmdLoop(db=False) # just running as command
31
+ assert not EfiBootDude.singleton
32
+ EfiBootDude.singleton = self
33
+
34
+ spin = self.spin = OptionSpinner()
35
+ spin.add_key('help_mode', '? - toggle help screen', vals=[False, True])
36
+ spin.add_key('verbose', 'v - toggle verbose', vals=[False, True])
37
+
38
+ # FIXME: keys
39
+ other = 'tudrnmwf*zqx'
40
+ other_keys = set(ord(x) for x in other)
41
+ other_keys.add(cs.KEY_ENTER)
42
+ # other_keys.add(27) # ESCAPE
43
+ other_keys.add(10) # another form of ENTER
44
+ self.opts = spin.default_obj
45
+
46
+ self.actions = {} # currently available actions
47
+ self.check_preqreqs()
48
+ self.mounts, self.uuids = {}, {}
49
+ self.mods = SimpleNamespace()
50
+ self.digests, self.width1, self.label_wid, self.boot_idx = [], 0, 0, 0
51
+ self.win = None
52
+ self.reinit()
53
+ self.win = Window(head_line=True, body_rows=len(self.digests)+20, head_rows=10,
54
+ keys=spin.keys ^ other_keys, mod_pick=self.mod_pick)
55
+ self.win.pick_pos = self.boot_idx
56
+
57
+ def reinit(self):
58
+ """ RESET EVERYTHING"""
59
+ self.mounts = self.get_mounts()
60
+ self.uuids = self.get_part_uuids() # uuid in lower case
61
+ self.mods = SimpleNamespace(
62
+ dirty=False, # if anything changed
63
+ order=False,
64
+ timeout=None,
65
+ removes=set(),
66
+ tags={},
67
+ # adds=set(),
68
+ next=None,
69
+ actives=set(),
70
+ inactives=set(),
71
+ )
72
+ self.digests, self.width1, self.label_wid, self.boot_idx = [], 0, 0, 0
73
+ self.digest_boots()
74
+ if self.win:
75
+ self.win.pick_pos = self.boot_idx
76
+
77
+ @staticmethod
78
+ def get_mounts():
79
+ """ Get a dictionary of device-to-mount-point """
80
+ mounts = {}
81
+ with open('/proc/mounts', 'r', encoding='utf-8') as mounts_file:
82
+ for line in mounts_file:
83
+ parts = line.split()
84
+ dev = parts[0]
85
+ mount_point = parts[1]
86
+ mounts[dev] = mount_point
87
+ return mounts
88
+
89
+ def get_part_uuids(self):
90
+ """ Get all the Partition UUIDS"""
91
+ uuids = {}
92
+ with open('/run/blkid/blkid.tab', encoding='utf8') as fh:
93
+ # sample: <device ... TYPE="vfat"
94
+ # PARTUUID="25d2dea1-9f68-1644-91dd-4836c0b3a30a">/dev/nvme0n1p1</device>
95
+ for xml_line in fh:
96
+ element = ET.fromstring(xml_line)
97
+ if 'PARTUUID' in element.attrib:
98
+ device=element.text.strip()
99
+ name = self.mounts.get(device, device)
100
+ uuids[element.attrib['PARTUUID'].lower()] = name
101
+ return uuids
102
+
103
+ @staticmethod
104
+ def extract_uuid(line):
105
+ """ Find uuid string in a line """
106
+ # Define the regex pattern for UUID (e.g., 25d2dea1-9f68-1644-91dd-4836c0b3a30a)
107
+ pattern = r'\b[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\b'
108
+ # Search for the pattern in the line
109
+ match = re.search(pattern, line, re.IGNORECASE)
110
+ return match.group(0).lower() if match else None
111
+
112
+
113
+ def digest_boots(self):
114
+ """ Digest the output of 'efibootmgr'."""
115
+ # Define the command to run
116
+ command = 'efibootmgr'.split()
117
+ # Run the command and capture the output
118
+ result = subprocess.run(command, stdout=subprocess.PIPE, text=True, check=True)
119
+ lines = result.stdout.splitlines()
120
+ rv = []
121
+ width1 = 0 # width of info1
122
+ label_wid = 0
123
+ boots = {}
124
+ for line in ['BootNext: ---'] + lines:
125
+ parts = line.split(maxsplit=1)
126
+ if len(parts) < 2:
127
+ continue
128
+ key, info = parts[0], parts[1]
129
+
130
+ if key == 'BootOrder:':
131
+ boot_order = info
132
+ continue
133
+
134
+ ns = SimpleNamespace(
135
+ ident=None,
136
+ is_boot=False,
137
+ active='',
138
+ label='',
139
+ info1='',
140
+ info2='',
141
+ )
142
+
143
+ mat = re.match(r'\bBoot([0-9a-f]+)\b(\*?)' # Boot0024*
144
+ + r'\s+(\w.*\w)\s+' # Linux Boot Manager
145
+ + r'\b(\w+\(.*)$', # HD(4,GPT,cd15e3b1-...
146
+ line, re.IGNORECASE)
147
+ if not mat:
148
+ ns.ident = key
149
+ ns.label = info
150
+ if key == 'BootNext:' and len(rv) > 0:
151
+ rv[0] = ns
152
+ else:
153
+ rv.append(ns)
154
+ continue
155
+
156
+ ns.ident = mat.group(1)
157
+ ns.is_boot = True
158
+ ns.active = mat.group(2)
159
+ ns.label = mat.group(3)
160
+ label_wid = max(label_wid, len(ns.label))
161
+ other = mat.group(4)
162
+
163
+ mat = re.search(r'/?File\(([^)]*)\)', other, re.IGNORECASE)
164
+ device, subpath = '', '' # e.g., /boot/efi, \EFI\UBUNTU\SHIMX64.EFI
165
+ if mat:
166
+ subpath = mat.group(1) + ' '
167
+ start, end = mat.span()
168
+ other = other[:start] + other[end:]
169
+
170
+ uuid = self.extract_uuid(other)
171
+ if uuid and uuid in self.uuids:
172
+ device = self.uuids[uuid]
173
+
174
+ if device:
175
+ ns.info1 = device
176
+ ns.info2 = subpath if subpath else other
177
+ width1 = max(width1, len(ns.info1))
178
+ elif subpath:
179
+ ns.info1 = subpath
180
+ ns.info2 = other
181
+ else:
182
+ ns.info1 = other
183
+ boots[ns.ident] = ns
184
+
185
+ self.boot_idx = len(rv)
186
+ self.width1 = width1
187
+ self.label_wid = label_wid
188
+
189
+ for ident in boot_order.split(','):
190
+ if ident in boots:
191
+ rv.append(boots.pop(ident))
192
+ rv += list(boots.values())
193
+
194
+ self.digests = rv
195
+ return rv
196
+
197
+ @staticmethod
198
+ def check_preqreqs():
199
+ """ Check that needed programs are installed. """
200
+ ok = True
201
+ for prog in 'efibootmgr'.split():
202
+ if shutil.which(prog) is None:
203
+ ok = False
204
+ print(f'ERROR: cannot find {prog!r} on $PATH')
205
+ if not ok:
206
+ sys.exit(1)
207
+
208
+ @staticmethod
209
+ def get_word0(line):
210
+ """ Get words[1] from a string. """
211
+ words = line.split(maxsplit=1)
212
+ return words[0]
213
+
214
+ def write(self):
215
+ """ Commit the changes. """
216
+ if not self.mods.dirty:
217
+ return
218
+ cmds = []
219
+ prefix = 'sudo efibootmgr --quiet'
220
+ for ident in self.mods.removes:
221
+ cmds.append(f'{prefix} --delete-bootnum --bootnum {ident}')
222
+ for ident in self.mods.actives:
223
+ cmds.append(f'{prefix} --active --bootnum {ident}')
224
+ for ident in self.mods.inactives:
225
+ cmds.append(f'{prefix} --inactive --bootnum {ident}')
226
+ for ident, tag in self.mods.tags.items():
227
+ cmds.append(f'{prefix} --bootnum {ident} --label "{tag}"')
228
+ if self.mods.order:
229
+ orders = [ns.ident for ns in self.digests if ns.is_boot]
230
+ cmds.append(f'{prefix} --bootorder {','.join(orders)}')
231
+ if self.mods.next:
232
+ cmds.append(f'{prefix} --bootnext {self.mods.next}')
233
+ if self.mods.timeout:
234
+ cmds.append(f'{prefix} --timeout {self.mods.timeout}')
235
+ Window.stop_curses()
236
+ os.system('clear; stty sane')
237
+ print('Commands:')
238
+ for cmd in cmds:
239
+ print(f' + {cmd}')
240
+ yes = input("Run the above commands? (yes/No) ")
241
+
242
+ if yes.lower().startswith('y'):
243
+ os.system('/bin/echo; /bin/echo')
244
+
245
+ for cmd in cmds:
246
+ os.system(f'set -x; {cmd}; /bin/echo " <<<ExitCode=$?>>>"')
247
+
248
+ os.system(r'/bin/echo -e "\n\n===== Press ENTER for menu ====> \c"; read FOO')
249
+ self.reinit()
250
+
251
+ Window._start_curses()
252
+ self.win.pick_pos = self.boot_idx
253
+
254
+
255
+
256
+ def main_loop(self):
257
+ """ TBD """
258
+
259
+ self.opts.name = "[hit 'n' to enter name]"
260
+ while True:
261
+ if self.opts.help_mode:
262
+ self.win.set_pick_mode(False)
263
+ self.spin.show_help_nav_keys(self.win)
264
+ self.spin.show_help_body(self.win)
265
+ # FIXME: keys
266
+ lines = [
267
+ ' q or x - quit program (CTL-C disabled)',
268
+ ' u - up - move boot entry up',
269
+ ' d - down - move boot entry down',
270
+ # ' c - copy - copy boot entry',
271
+ ' r - remove - remove boot',
272
+ # ' a - add - add boot entry',
273
+ ' n - next - set next boot default',
274
+ ' t - tag - set a new label for the boot entry',
275
+ ' * - toggle whether entry is active'
276
+ ' m - modify - modify the value'
277
+ ' w - write - write the changes',
278
+ ' f - freshen - clear changes and re-read boot state',
279
+ ]
280
+ for line in lines:
281
+ self.win.put_body(line)
282
+ else:
283
+ # self.win.set_pick_mode(self.opts.pick_mode, self.opts.pick_size)
284
+ self.win.set_pick_mode(True)
285
+ self.win.add_header(self.get_keys_line(), attr=cs.A_BOLD)
286
+ for ns in self.digests:
287
+ info1 = ns.info1
288
+ if not self.opts.verbose:
289
+ mat = re.search(r'/?VenHw\(.*$', info1, re.IGNORECASE)
290
+ if mat:
291
+ start, end = mat.span()
292
+ info1 = info1[:start] + info1[end:]
293
+
294
+ line = f'{ns.active:>1} {ns.ident:>4} {ns.label:<{self.label_wid}}'
295
+ line += f' {info1:<{self.width1}} {ns.info2}'
296
+ self.win.add_body(line)
297
+ self.win.render()
298
+
299
+ _ = self.do_key(self.win.prompt(seconds=300))
300
+ self.win.clear()
301
+
302
+ def get_keys_line(self):
303
+ """ TBD """
304
+ # EXPAND
305
+ line = ''
306
+ for key, verb in self.actions.items():
307
+ if key[0] == verb[0]:
308
+ line += f' {verb}'
309
+ else:
310
+ line += f' {key}:{verb}'
311
+ # or EXPAND
312
+ line += ' ?:help quit'
313
+ # for action in self.actions:
314
+ # line += f' {action[0]}:{action}'
315
+ return line[1:]
316
+
317
+ def get_actions(self):
318
+ """ Determine the type of the current line and available commands."""
319
+ # FIXME: keys
320
+ actions = {}
321
+ digests = self.digests
322
+ if 0 <= self.win.pick_pos < len(digests):
323
+ ns = digests[self.win.pick_pos]
324
+ if ns.is_boot:
325
+ if self.win.pick_pos > self.boot_idx:
326
+ actions['u'] = 'up'
327
+ if self.win.pick_pos < len(self.digests)-1:
328
+ actions['d'] = 'down'
329
+ # actions['c'] = 'copy'
330
+ actions['r'] = 'rmv'
331
+ # actions['a'] = 'add'
332
+ actions['n'] = 'next'
333
+ actions['t'] = 'tag'
334
+ actions['*'] = 'inact' if ns.active else 'act'
335
+ elif ns.ident in ('Timeout:', ):
336
+ actions['m'] = 'modify'
337
+ if self.mods.dirty:
338
+ actions['w'] = 'write'
339
+ actions['f'] = 'fresh'
340
+
341
+ return actions
342
+
343
+ @staticmethod
344
+ def mod_pick(line):
345
+ """ Callback to modify the "pick line" being highlighted;
346
+ We use it to alter the state
347
+ """
348
+ this = EfiBootDude.singleton
349
+ this.actions = this.get_actions()
350
+ header = this.get_keys_line()
351
+ wds = header.split()
352
+ this.win.head.pad.move(0, 0)
353
+ for wd in wds:
354
+ if wd:
355
+ this.win.add_header(wd[0], attr=cs.A_BOLD|cs.A_UNDERLINE, resume=True)
356
+ if wd[1:]:
357
+ this.win.add_header(wd[1:] + ' ', resume=True)
358
+
359
+ _, col = this.win.head.pad.getyx()
360
+ pad = ' ' * (this.win.get_pad_width()-col)
361
+ this.win.add_header(pad, resume=True)
362
+ return line
363
+
364
+ def do_key(self, key):
365
+ """ TBD """
366
+ if not key:
367
+ return True
368
+ if key == cs.KEY_ENTER or key == 10: # Handle ENTER
369
+ if self.opts.help_mode:
370
+ self.opts.help_mode = False
371
+ return True
372
+ return None
373
+
374
+ if key in self.spin.keys:
375
+ value = self.spin.do_key(key, self.win)
376
+ return value
377
+
378
+ if key in (ord('q'), ord('x')):
379
+
380
+ answer = 'y'
381
+ if self.mods.dirty:
382
+ answer = self.win.answer(
383
+ prompt='Enter "y" to abandon edits and exit [then Enter]')
384
+ if answer.strip().lower().startswith('y'):
385
+ self.win.stop_curses()
386
+ os.system('clear; stty sane')
387
+ sys.exit(0)
388
+ return None
389
+
390
+ ns = self.digests[self.win.pick_pos]
391
+
392
+ if key == ord('m'):
393
+ if ns.ident == 'Timeout:':
394
+ seed = ns.label.split()[0]
395
+ while True:
396
+ answer = self.win.answer(
397
+ prompt='Enter timeout seconds or clear to abort [then Enter]',
398
+ seed=seed, width=80)
399
+ seed = answer = answer.strip()
400
+ if not answer:
401
+ break
402
+ if re.match(r'\d+$', answer):
403
+ ns.label = f'{answer} seconds'
404
+ self.mods.timeout = answer
405
+ self.mods.dirty = True
406
+ break
407
+ return None
408
+
409
+ if key == ord('u') and ns.is_boot:
410
+ digests, pos = self.digests, self.win.pick_pos
411
+ if pos > self.boot_idx:
412
+ digests[pos-1], digests[pos] = digests[pos], digests[pos-1]
413
+ self.win.pick_pos -= 1
414
+ self.mods.order = True
415
+ self.mods.dirty = True
416
+ return None
417
+ if key == ord('d') and ns.is_boot:
418
+ digests, pos = self.digests, self.win.pick_pos
419
+ if pos < len(self.digests)-1:
420
+ digests[pos+1], digests[pos] = digests[pos], digests[pos+1]
421
+ self.win.pick_pos += 1
422
+ self.mods.order = True
423
+ self.mods.dirty = True
424
+ return None
425
+ if key == ord('r') and ns.is_boot:
426
+ ident = self.digests[self.win.pick_pos].ident
427
+ del self.digests[self.win.pick_pos]
428
+ self.mods.removes.add(ident)
429
+ self.mods.actives.discard(ident)
430
+ self.mods.inactives.discard(ident)
431
+ self.mods.dirty = True
432
+ return None
433
+ if key == ord('n') and ns.is_boot:
434
+ ident = ns.ident
435
+ self.digests[0].label = ident
436
+ self.mods.next = ident
437
+ self.mods.dirty = True
438
+ return None
439
+
440
+ if key == ord('*') and ns.is_boot:
441
+ ident = ns.ident
442
+ if ns.active:
443
+ ns.active = ''
444
+ self.mods.actives.discard(ident)
445
+ self.mods.inactives.add(ident)
446
+ else:
447
+ ns.active = '*'
448
+ self.mods.actives.add(ident)
449
+ self.mods.inactives.discard(ident)
450
+ self.mods.dirty = True
451
+
452
+ if key == ord('t') and ns.is_boot:
453
+ seed = ns.label
454
+ while True:
455
+ answer = self.win.answer(prompt='Enter new label or clear to abort [then Enter], ',
456
+ seed=seed, width=80)
457
+ seed = answer = answer.strip()
458
+ if not answer:
459
+ break
460
+ if re.match(r'([\w\s])+$', answer):
461
+ ns.label = f'{answer}'
462
+ self.mods.tags[ns.ident] = answer
463
+ self.mods.dirty = True
464
+ break
465
+
466
+ if key == ord('f') and self.mods.dirty:
467
+ answer = self.win.answer(
468
+ prompt='Enter "y" to clear edits and refresh [then Enter]')
469
+ if answer.strip().lower().startswith('y'):
470
+ self.reinit()
471
+ return None
472
+
473
+ if key == ord('w') and self.mods.dirty:
474
+ self.write()
475
+ return None
476
+
477
+ # FIXME: handle more keys
478
+ return None
479
+
480
+
481
+ def main():
482
+ """ The program """
483
+
484
+ dude = EfiBootDude()
485
+ dude.main_loop()
486
+
487
+ if __name__ == '__main__':
488
+
489
+ try:
490
+ main()
491
+ except KeyboardInterrupt:
492
+ pass
493
+ except Exception as exce:
494
+ Window.stop_curses()
495
+ print("exception:", str(exce))
496
+ print(traceback.format_exc())
497
+ # if dump_str:
498
+ # print(dump_str)
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Joe D
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.1
2
+ Name: efibootdude
3
+ Version: 0.0
4
+ Summary: A visual wrapper for efibootmgr
5
+ Author-email: Joe Defen <joedef@google.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/joedefen/efibootdude
8
+ Project-URL: Bug Tracker, https://github.com/joedefen/efibootdude/issues
9
+ Keywords: app,installer,manager,appimages
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: psutil >=5.9
17
+ Requires-Dist: importlib-metadata ; python_version < "3.8"
18
+
19
+ # EfiBootDude
20
+ `efibootdude` presents a visual (curses) interface to `efibootmgr` which allows editing the bios
21
+ boot menu and parameters while running Linux.
22
+
23
+ * Install `efibootdude` using `pipx install efibootdude`, or however you do so.
24
+ * Prerequisites: install [rhboot/efibootmgr](https://github.com/rhboot/efibootmgr)
25
+ * For example, on a Debian derived distro, use `sudo apt install efibootmgr`.
26
+
27
+
28
+ `efibootdude` covers only the most commonly used capabilities of `efibootmgr` including:
29
+ * reordering boot entries,
30
+ * removing boot entries,
31
+ * setting the boot entry for the next boot only,
32
+ * setting boot entries active or inactive, and
33
+ * setting the boot menu timeout value (until it boots the default entry).
34
+
35
+ To be sure, there are many other esoteric uses of `efibootmanager`.
36
+
37
+ ## Usage
38
+ After running `efibootdude`, you'll see a screen like this:
39
+ ![efibootdude-screenshot](https://github.com/joedefen/efibootdude/blob/main/images/efibootdude-screenshot.png?raw=true).
40
+ At this point
41
+ * The current line starts with `>` and is highlighted.
42
+ * The top line shows actions for the current line.
43
+ * Enter `?` for a more complete explanation of the keys, navigation keys, etc.
44
+ * With this current line, we can:
45
+ * Use `u` or `d` to move it up or down in the boot order.
46
+ * Use `t` to relabel the boot entry.
47
+ * Use `r` to remove it.
48
+ * And so forth.
49
+ * When ready to write the changes to the BIOS, enter 'w'.
50
+ * When writing the changes, `efibootdude` drops out of menu mode so you can
51
+ verify the underlying commands, error codes, and error messages.
52
+
53
+ ## Caveats
54
+ * Some operations may not work permanently even though there is no indication from `efibootmgr`
55
+ (e.g., on my desktop, I cannot re-label boot entries).
56
+ * Some operations may only work (again) after re-booting (e.g., you might find activating
57
+ an entry does not work, but it does so after a reboot).
58
+
59
+ ## About this Project
60
+ This project was inspired by [Elinvention/efiboots](https://github.com/Elinvention/efiboots). Relative to that project, the aims of `efibootdude` are:
61
+ * to be easier to install especially when not in your distro's repos.
62
+ * to clearly present the partition of the boot entries (as a mount point if mounted and, otherwise, the device pathname).
63
+ * to show the underlying commands being run for education, for verification, and for help on investigating issues.
@@ -0,0 +1,9 @@
1
+ efibootdude/PowerWindow.py,sha256=pQGXsAMeuiHn-vtEiVG0rgW1eCslh3ukC-VrPhH_j3k,28587
2
+ efibootdude/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ efibootdude/main.py,sha256=xl6_bKWJVxzbN31SUWUAWWI5DH5F_iX5HpR-i23JjJ0,17609
4
+ efibootdude-0.0.dist-info/LICENSE,sha256=qB9OdnyyF6WYHiEIXVm0rOSdcf8e2ctorrtWs6CC5lU,1062
5
+ efibootdude-0.0.dist-info/METADATA,sha256=hNAKcQp31odNUlDLHVZ41ngqLq19Y0WuttYgXw08ZtY,2995
6
+ efibootdude-0.0.dist-info/WHEEL,sha256=mguMlWGMX-VHnMpKOjjQidIo1ssRlCFu4a4mBpz1s2M,91
7
+ efibootdude-0.0.dist-info/entry_points.txt,sha256=3KZ_86ZSy4f-weuSruTlMGaQ96jZUm7mjyITVnjE184,54
8
+ efibootdude-0.0.dist-info/top_level.txt,sha256=BrMsK3JmOrVJUNkHX1dicqWrdjbm4itRtmXJMxte-VU,12
9
+ efibootdude-0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (70.1.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ efibootdude = efibootdude.main:main
@@ -0,0 +1 @@
1
+ efibootdude