dwipe 3.2.0__tar.gz → 3.2.2__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.
Files changed (27) hide show
  1. {dwipe-3.2.0 → dwipe-3.2.2}/PKG-INFO +9 -3
  2. {dwipe-3.2.0 → dwipe-3.2.2}/README.md +5 -1
  3. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/DeviceInfo.py +12 -10
  4. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/DiskWipe.py +130 -11
  5. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/README-DEV.md +30 -15
  6. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/Utils.py +8 -1
  7. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/__init__.py +1 -1
  8. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/main.py +11 -0
  9. {dwipe-3.2.0 → dwipe-3.2.2}/pyproject.toml +6 -2
  10. {dwipe-3.2.0 → dwipe-3.2.2}/LICENSE +0 -0
  11. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/DeviceChangeMonitor.py +0 -0
  12. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/DeviceWorker.py +0 -0
  13. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/DrivePreChecker.py +0 -0
  14. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/FirmwareWipeTask.py +0 -0
  15. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/NvmeTool.py +0 -0
  16. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/PersistentState.py +0 -0
  17. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/Prereqs.py +0 -0
  18. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/SataTool.py +0 -0
  19. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/StructuredLogger.py +0 -0
  20. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/Tunables.py +0 -0
  21. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/VerifyTask.py +0 -0
  22. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/WipeJob.py +0 -0
  23. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/WipeTask.py +0 -0
  24. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/WriteTask.py +0 -0
  25. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/device_io.py +0 -0
  26. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/markers.py +0 -0
  27. {dwipe-3.2.0 → dwipe-3.2.2}/dwipe/verification.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dwipe
3
- Version: 3.2.0
3
+ Version: 3.2.2
4
4
  Summary: A tool to wipe disks and partitions for Linux
5
5
  Keywords: disk,partition,wipe,clean,scrub
6
6
  Author-email: Joe Defen <joedef@google.com>
@@ -15,15 +15,17 @@ Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Operating System :: POSIX :: Linux
17
17
  License-File: LICENSE
18
- Requires-Dist: console-window == 2.1.3
18
+ Requires-Dist: console-window == 2.2.2
19
19
  Requires-Dist: pytest >= 7.0 ; extra == "dev"
20
20
  Requires-Dist: pytest-cov >= 4.0 ; extra == "dev"
21
21
  Requires-Dist: ruff >= 0.9 ; extra == "dev"
22
22
  Requires-Dist: build >= 1.2 ; extra == "dev"
23
23
  Requires-Dist: twine >= 7 ; extra == "dev"
24
24
  Requires-Dist: packaging >= 26 ; extra == "dev"
25
+ Requires-Dist: tomli >= 2.0 ; extra == "dev" and ( python_version < "3.11")
25
26
  Requires-Dist: pytest >= 7.0 ; extra == "test"
26
27
  Requires-Dist: pytest-cov >= 4.0 ; extra == "test"
28
+ Requires-Dist: tomli >= 2.0 ; extra == "test" and ( python_version < "3.11")
27
29
  Project-URL: Bug Tracker, https://github.com/joedefen/dwipe/issues
28
30
  Project-URL: Homepage, https://github.com/joedefen/dwipe
29
31
  Provides-Extra: dev
@@ -33,7 +35,7 @@ Import-Name: dwipe
33
35
  # dwipe
34
36
  `dwipe` is a tool to wipe disks and partitions for Linux to help secure your data. `dwipe` aims to reduce mistakes by providing ample information about your devices during selection.
35
37
 
36
- ![Demo of dwipe](https://raw.githubusercontent.com/joedefen/dwipe/master/images/dwipe-2026-02-05.07-58.gif)
38
+ ![Demo of dwipe](https://raw.githubusercontent.com/joedefen/dwipe/main/images/dwipe-2026-02-05.07-58.gif)
37
39
  ### Quick Comparison
38
40
 
39
41
  | Feature | dwipe | nwipe | shred | dd |
@@ -137,6 +139,8 @@ Simply run `dwipe` from the command line without arguments: `dwipe`
137
139
  - Percentage of disk to verify after wipe (1% = quick spot-check, 100% = full verify)
138
140
 
139
141
  #### Display and Performance
142
+ - `--theme {default,dark-mono,light-mono,solarized-dark,solarized-light,gruvbox,nord}` - Color palette
143
+ - `--no-color` - Draw semantic roles with attributes (bold/reverse) instead of color; also honored when the `NO_COLOR` environment variable is set
140
144
  - `--dense {True,False}` - Compact view mode (default: your last preference)
141
145
  - `True` - Compact spacing (fewer blank lines between disks)
142
146
  - `False` - Spaced view (blank lines between disks for readability)
@@ -190,11 +194,13 @@ Simply run `dwipe` from the command line without arguments: `dwipe`
190
194
  - `solarized-light` - Solarized Light palette (for light terminal backgrounds)
191
195
  - `gruvbox` - Gruvbox Dark palette
192
196
  - `nord` - Nord palette
197
+ - `mono` - Draw without color at all, using bold/reverse attributes instead (also available as `--no-color` and through the `NO_COLOR` environment variable)
193
198
 
194
199
  **Changing themes:**
195
200
  - Press **t** from the main screen to open the theme preview screen
196
201
  - The theme screen shows color examples for each color purpose (DANGER, SUCCESS, WARNING, etc.)
197
202
  - Press **t** while on the theme screen to cycle through available themes and preview them live
203
+ - The list ends with **mono**, which turns color off and leaves the selected palette underneath
198
204
  - Press **ESC** or **ENTER** to return to the main screen
199
205
  - Selected theme is saved and persists across sessions
200
206
 
@@ -1,7 +1,7 @@
1
1
  # dwipe
2
2
  `dwipe` is a tool to wipe disks and partitions for Linux to help secure your data. `dwipe` aims to reduce mistakes by providing ample information about your devices during selection.
3
3
 
4
- ![Demo of dwipe](https://raw.githubusercontent.com/joedefen/dwipe/master/images/dwipe-2026-02-05.07-58.gif)
4
+ ![Demo of dwipe](https://raw.githubusercontent.com/joedefen/dwipe/main/images/dwipe-2026-02-05.07-58.gif)
5
5
  ### Quick Comparison
6
6
 
7
7
  | Feature | dwipe | nwipe | shred | dd |
@@ -105,6 +105,8 @@ Simply run `dwipe` from the command line without arguments: `dwipe`
105
105
  - Percentage of disk to verify after wipe (1% = quick spot-check, 100% = full verify)
106
106
 
107
107
  #### Display and Performance
108
+ - `--theme {default,dark-mono,light-mono,solarized-dark,solarized-light,gruvbox,nord}` - Color palette
109
+ - `--no-color` - Draw semantic roles with attributes (bold/reverse) instead of color; also honored when the `NO_COLOR` environment variable is set
108
110
  - `--dense {True,False}` - Compact view mode (default: your last preference)
109
111
  - `True` - Compact spacing (fewer blank lines between disks)
110
112
  - `False` - Spaced view (blank lines between disks for readability)
@@ -158,11 +160,13 @@ Simply run `dwipe` from the command line without arguments: `dwipe`
158
160
  - `solarized-light` - Solarized Light palette (for light terminal backgrounds)
159
161
  - `gruvbox` - Gruvbox Dark palette
160
162
  - `nord` - Nord palette
163
+ - `mono` - Draw without color at all, using bold/reverse attributes instead (also available as `--no-color` and through the `NO_COLOR` environment variable)
161
164
 
162
165
  **Changing themes:**
163
166
  - Press **t** from the main screen to open the theme preview screen
164
167
  - The theme screen shows color examples for each color purpose (DANGER, SUCCESS, WARNING, etc.)
165
168
  - Press **t** while on the theme screen to cycle through available themes and preview them live
169
+ - The list ends with **mono**, which turns color off and leaves the selected palette underneath
166
170
  - Press **ESC** or **ENTER** to return to the main screen
167
171
  - Selected theme is saved and persists across sessions
168
172
 
@@ -13,7 +13,7 @@ import traceback
13
13
  from fnmatch import fnmatch
14
14
  from types import SimpleNamespace
15
15
 
16
- from console_window import Theme
16
+ from console_window import Theme, appearance
17
17
 
18
18
  from .DeviceWorker import DeviceWorkerManager, ProbeState
19
19
  from .DrivePreChecker import DrivePreChecker
@@ -1045,38 +1045,40 @@ class DeviceInfo:
1045
1045
  else:
1046
1046
  emit += f'{sep}{ns.marker}'
1047
1047
 
1048
- # Determine color attribute based on state
1048
+ # Determine color attribute based on state. Semantic attributes, not
1049
+ # pairs: the same states must degrade to the monochrome hierarchy when
1050
+ # color is off (--no-color or NO_COLOR).
1049
1051
  attr = None
1050
1052
  # Check for newly inserted flag first (hot-swapped devices should always show orange)
1051
1053
  if getattr(ns, 'newly_inserted', False):
1052
1054
  # Newly inserted device - orange/bright
1053
1055
  if ns.state in ('Mnt', 'iMnt', 'Blk', 'iBlk'):
1054
1056
  # Dim the orange for mounted/blocked devices
1055
- attr = curses.color_pair(Theme.HOTSWAP) | curses.A_DIM
1057
+ attr = appearance.semantic_attr(Theme.HOTSWAP) | curses.A_DIM
1056
1058
  else:
1057
- attr = curses.color_pair(Theme.HOTSWAP) | curses.A_BOLD
1059
+ attr = appearance.semantic_attr(Theme.HOTSWAP) | curses.A_BOLD
1058
1060
  elif ns.state == 's':
1059
1061
  # Yellow/warning color for stopped/partial wipes (with bold for visibility)
1060
- attr = curses.color_pair(Theme.WARNING) | curses.A_BOLD
1062
+ attr = appearance.semantic_attr(Theme.WARNING) | curses.A_BOLD
1061
1063
  elif ns.state == 'W' and getattr(ns, 'wiped_this_session', False):
1062
1064
  # Green/success color for completed wipes (done in THIS session only) - bold and bright
1063
- attr = curses.color_pair(Theme.SUCCESS) | curses.A_BOLD
1065
+ attr = appearance.semantic_attr(Theme.SUCCESS) | curses.A_BOLD
1064
1066
  elif ns.state == 'W':
1065
1067
  # Green/success color for completed wipes before this session
1066
- attr = curses.color_pair(Theme.OLD_SUCCESS) | curses.A_BOLD
1068
+ attr = appearance.semantic_attr(Theme.OLD_SUCCESS) | curses.A_BOLD
1067
1069
  elif ns.state.endswith('%') and ns.state not in ('100%',):
1068
1070
  # Active wipe in progress - bright cyan/blue with bold
1069
- attr = curses.color_pair(Theme.INFO) | curses.A_BOLD
1071
+ attr = appearance.semantic_attr(Theme.INFO) | curses.A_BOLD
1070
1072
  elif ns.state == '^':
1071
1073
  # Newly inserted device (hot-swapped) - orange/bright
1072
- attr = curses.color_pair(Theme.HOTSWAP) | curses.A_BOLD
1074
+ attr = appearance.semantic_attr(Theme.HOTSWAP) | curses.A_BOLD
1073
1075
  elif ns.state in ('Mnt', 'iMnt', 'Blk', 'iBlk'):
1074
1076
  # Dim mounted or blocked devices
1075
1077
  attr = curses.A_DIM
1076
1078
 
1077
1079
  # Override with red/danger color if verify failed
1078
1080
  if hasattr(ns, 'verify_failed_msg') and ns.verify_failed_msg:
1079
- attr = curses.color_pair(Theme.DANGER) | curses.A_BOLD
1081
+ attr = appearance.semantic_attr(Theme.DANGER) | curses.A_BOLD
1080
1082
 
1081
1083
  # Make disk lines bold
1082
1084
  if ns.parent is None:
@@ -29,6 +29,7 @@ from console_window import (
29
29
  Screen,
30
30
  ScreenStack,
31
31
  Theme,
32
+ appearance,
32
33
  )
33
34
 
34
35
  from .DeviceChangeMonitor import DeviceChangeMonitor
@@ -61,6 +62,52 @@ LOG_ST = 2
61
62
  THEME_ST = 3
62
63
  SCREEN_NAMES = ('MAIN', 'HELP', 'HISTORY', 'THEMES')
63
64
 
65
+ #: dwipe's own pseudo-theme. ``mono`` is not a palette: choosing it turns
66
+ #: semantic color off for the whole process, exactly like ``--no-color``. The
67
+ #: library leaves pseudo-themes to the application (``console_window.appearance``
68
+ #: names ``mono`` as its standing example), so dwipe owns the name and the
69
+ #: switch behind it.
70
+ MONO_THEME = 'mono'
71
+ #: The entries the theme picker cycles through: every library palette, then the
72
+ #: mono pseudo-theme, so ``t`` wraps from the last palette into mono.
73
+ THEME_CHOICES = (*Theme.list_all(), MONO_THEME)
74
+
75
+
76
+ def resolve_theme_selection(cli_theme=None, no_color=False,
77
+ saved_theme='', environ=None):
78
+ """Resolve startup appearance into ``(selected, theme, colors)``.
79
+
80
+ ``selected`` is the value the theme option should hold: a palette name or
81
+ :data:`MONO_THEME`. ``theme`` is the palette to install (``''`` keeps the
82
+ library default) and ``colors`` is the process-wide switch.
83
+
84
+ Precedence is the library's: ``--no-color``, an explicit ``--theme``, the
85
+ ``NO_COLOR`` convention, then the remembered choice. A remembered ``mono``
86
+ is normalized to "no palette plus color off" before resolving.
87
+
88
+ A per-run ``--no-color``/``NO_COLOR`` keeps the remembered palette in
89
+ ``selected`` rather than replacing it with ``mono``. The switch is not a
90
+ saved preference, so the next ordinary start has to come back to the same
91
+ palette; only choosing ``mono`` in the picker saves it.
92
+ """
93
+ saved_theme = saved_theme or ''
94
+ saved_colors = saved_theme != MONO_THEME
95
+ saved_palette = '' if saved_theme == MONO_THEME else saved_theme
96
+ theme, colors = appearance.resolve_appearance(
97
+ cli_theme=cli_theme,
98
+ no_color=no_color,
99
+ saved_theme=saved_palette,
100
+ saved_colors=saved_colors,
101
+ environ=environ,
102
+ )
103
+ if cli_theme:
104
+ selected = Theme.resolve(cli_theme)
105
+ elif not colors and saved_theme == MONO_THEME:
106
+ selected = MONO_THEME
107
+ else:
108
+ selected = Theme.resolve(theme or Theme.get_current())
109
+ return selected, theme, colors
110
+
64
111
 
65
112
  class DiskWipe:
66
113
  """Main application controller and UI manager"""
@@ -118,6 +165,47 @@ class DiskWipe:
118
165
  on_cancel=self._on_filter_cancel
119
166
  )
120
167
 
168
+ def _settle_appearance(self, environ=None):
169
+ """Apply the startup color decision to the process and the theme option.
170
+
171
+ The remembered theme, ``--theme``, ``--no-color`` and the ``NO_COLOR``
172
+ convention all meet here; see :func:`resolve_theme_selection` for the
173
+ precedence and for why a per-run ``--no-color`` does not overwrite the
174
+ remembered palette.
175
+ """
176
+ saved_theme = self.persistent_state.state.get('theme', '') or ''
177
+ selected, theme, colors = resolve_theme_selection(
178
+ cli_theme=getattr(self.opts, 'theme', None),
179
+ no_color=bool(getattr(self.opts, 'no_color', False)),
180
+ saved_theme=saved_theme,
181
+ environ=environ,
182
+ )
183
+ appearance.set_colors_enabled(colors)
184
+ if theme:
185
+ Theme.set(theme)
186
+ self.opts.theme = selected
187
+ return selected, colors
188
+
189
+ def apply_theme_choice(self, choice):
190
+ """Make a theme-picker *choice* live and remember it.
191
+
192
+ ``mono`` is the pseudo-theme: a color mode rather than a palette, so it
193
+ flips the process-wide switch and leaves the installed palette where it
194
+ was (the same way ``--no-color`` keeps ``--theme``). Choosing a real
195
+ palette is a request for color, so it lifts the switch again.
196
+ """
197
+ self.opts.theme = choice
198
+ if choice == MONO_THEME:
199
+ appearance.set_colors_enabled(False)
200
+ else:
201
+ appearance.set_colors_enabled(True)
202
+ self.win.set_theme(choice)
203
+ # A picker choice is a preference, so write it now: quitting from the
204
+ # same frame must not lose it.
205
+ self.persistent_state.state['theme'] = choice
206
+ self.persistent_state.dirty = True
207
+ self.persistent_state.save()
208
+
121
209
  def _start_wipe(self):
122
210
  """Start the wipe job after confirmation"""
123
211
  if self.confirmation.identity and self.confirmation.identity in self.partitions:
@@ -513,8 +601,8 @@ class DiskWipe:
513
601
  def main_loop(self):
514
602
  """Main event loop"""
515
603
 
516
- # Create screen instances
517
- ThemeScreen = Theme.create_picker_screen(DiskWipeScreen)
604
+ # Create screen instances. ThemeScreen is defined below with the other
605
+ # screens; it adds dwipe's ``mono`` entry to the library's palette list.
518
606
  self.screens = {
519
607
  MAIN_ST: MainScreen(self),
520
608
  HELP_ST: HelpScreen(self),
@@ -553,6 +641,10 @@ class DiskWipe:
553
641
  exit(1)
554
642
 
555
643
  self.win = ConsoleWindow(opts=win_opts)
644
+ # Color is one decision with two parts: which palette and whether to use
645
+ # it. Settle both before the first draw so NO_COLOR works as well as
646
+ # the switches.
647
+ self._settle_appearance()
556
648
  # Initialize screen stack
557
649
  self.stack = ScreenStack(self.win, None, SCREEN_NAMES, self.screens)
558
650
 
@@ -585,9 +677,8 @@ class DiskWipe:
585
677
  spin.add_key('expand', 'e - expand history entry', genre='action', scope=LOG_ST)
586
678
  spin.add_key('copy', 'c - copy entry to clipboard', genre='action', scope=LOG_ST)
587
679
  spin.add_key('show_keys', 'K - show keys (demo mode)', genre='action')
588
- # Load theme from persistent state
589
- self.opts.theme = self.persistent_state.state.get('theme', '')
590
- Theme.set(self.opts.theme)
680
+ # The appearance (and therefore opts.theme) was settled when the window
681
+ # was created, so the picker and the saved preference already agree.
591
682
  self.win.set_handled_keys(self.spin.keys)
592
683
 
593
684
  # Background device change monitor started above
@@ -687,6 +778,28 @@ class DiskWipeScreen(Screen):
687
778
  """ Show last key for demo"""
688
779
  self.app.win.set_demo_mode(enabled=None) # toggle it
689
780
 
781
+
782
+ class ThemeScreen(Theme.create_picker_screen(DiskWipeScreen, cycle_key='t')):
783
+ """The library's theme picker plus dwipe's ``mono`` pseudo-theme.
784
+
785
+ ``mono`` is a color mode, not a palette, so the picker's swatches follow the
786
+ shared switch and show the monochrome role hierarchy while it is selected.
787
+ Overriding ``theme_label`` names that choice in the header instead of naming
788
+ the palette left installed underneath it.
789
+ """
790
+
791
+ def theme_label(self, theme_name):
792
+ if not appearance.colors_enabled():
793
+ return MONO_THEME
794
+ return super().theme_label(theme_name)
795
+
796
+ def spin_theme_ACTION(self):
797
+ app = self.app
798
+ current = getattr(app.opts, 'theme', None)
799
+ index = THEME_CHOICES.index(current) if current in THEME_CHOICES else -1
800
+ app.apply_theme_choice(THEME_CHOICES[(index + 1) % len(THEME_CHOICES)])
801
+
802
+
690
803
  class MainScreen(DiskWipeScreen):
691
804
  """Main device list screen"""
692
805
 
@@ -1064,14 +1177,15 @@ class MainScreen(DiskWipeScreen):
1064
1177
  msg = ' ' * 5 + msg
1065
1178
 
1066
1179
  # Add confirmation message base as DECOR (non-pickable)
1067
- app.win.add_body(msg, attr=cs.color_pair(Theme.DANGER) | cs.A_BOLD,
1180
+ app.win.add_body(msg,
1181
+ attr=appearance.semantic_attr(Theme.DANGER) | cs.A_BOLD,
1068
1182
  context=Context(genre='DECOR'))
1069
1183
 
1070
1184
  # Add input or hint on same line
1071
1185
  if app.confirmation.input_buffer:
1072
1186
  # Show current input with cursor
1073
1187
  app.win.add_body(app.confirmation.input_buffer + '_',
1074
- attr=cs.color_pair(Theme.DANGER) | cs.A_BOLD,
1188
+ attr=appearance.semantic_attr(Theme.DANGER) | cs.A_BOLD,
1075
1189
  resume=True)
1076
1190
  else:
1077
1191
  # Show hint in dimmed italic
@@ -1402,9 +1516,13 @@ class HelpScreen(DiskWipeScreen):
1402
1516
  f'--slowdown-stop: {opts.slowdown_stop:<{wid}} Stop if disk slows',
1403
1517
  f'--stall-timeout: {opts.stall_timeout:<{wid}} Stall timeout in sec',
1404
1518
  f'--auto-block: . {"on" if getattr(opts, "auto_block_containers", True) else "off":<{wid}} Auto-block containers',
1519
+ f'--theme: . . . . {(getattr(opts, "theme", None) or "-"):<{wid}} Color palette (or mono)',
1520
+ f'--no-color: . . . {"on" if getattr(opts, "no_color", False) else "off":<{wid}} Draw without color',
1405
1521
  ]
1406
1522
  for opt in cli_options:
1407
1523
  app.win.add_body(opt, attr=cs.A_DIM)
1524
+ app.win.add_body('Theme "mono" (last in the picker) turns color off; '
1525
+ 'NO_COLOR does the same at startup.', attr=cs.A_DIM)
1408
1526
 
1409
1527
 
1410
1528
  class HistoryScreen(DiskWipeScreen):
@@ -1547,13 +1665,14 @@ class HistoryScreen(DiskWipeScreen):
1547
1665
  # Add deep match indicator if this entry matched only in JSON
1548
1666
  deep_indicator = " *" if timestamp in self.search_matches else ""
1549
1667
 
1550
- # Choose color based on log level
1668
+ # Choose color based on log level. Semantic attributes, not pairs:
1669
+ # the same rows must degrade to bold/reverse when color is off.
1551
1670
  if level == 'ERR':
1552
- level_attr = cs.color_pair(Theme.ERROR) | cs.A_BOLD
1671
+ level_attr = appearance.semantic_attr(Theme.ERROR) | cs.A_BOLD
1553
1672
  elif level in ('WIPE_STOPPED', 'VERIFY_STOPPED'):
1554
- level_attr = cs.color_pair(Theme.WARNING) | cs.A_BOLD
1673
+ level_attr = appearance.semantic_attr(Theme.WARNING) | cs.A_BOLD
1555
1674
  elif level in ('WIPE_COMPLETE', 'VERIFY_COMPLETE'):
1556
- level_attr = cs.color_pair(Theme.SUCCESS) | cs.A_BOLD
1675
+ level_attr = appearance.semantic_attr(Theme.SUCCESS) | cs.A_BOLD
1557
1676
  else:
1558
1677
  level_attr = cs.A_BOLD
1559
1678
 
@@ -14,7 +14,8 @@ to the modules it describes; every path below is therefore relative to
14
14
  | --- | --- |
15
15
  | [`dwipe/__init__.py`](../dwipe/__init__.py) | `__version__`, and nothing else. The build backend reads it, `dwipe --version` prints it, `bless` reads the line without importing the package. |
16
16
  | [`pyproject.toml`](../pyproject.toml) | Name, description, keywords, `requires-python`, runtime dependencies, the SPDX `license` and `license-files`, the `test`/`dev` extras, and the pytest, coverage and ruff configuration. |
17
- | [`bless`](../bless) | The release gate. Copied verbatim from `pwr-tray`/`my-snaps`; it holds no copy of this project's identity. |
17
+ | [`bless`](../bless) | The release gate. Emitted from `~/Projects/blessd/bless` by that repository's `build.py` and never edited here; it holds no copy of this project's identity. |
18
+ | `~/Projects/blessd/docs/release-strategy.md` | The shared rationale behind the gate: why the upload is local and irreversible, and how the branch model follows from that. |
18
19
  | [`tests/inspect_dist.py`](../tests/inspect_dist.py) | The distribution inspector. `bless` refuses to upload a wheel and sdist it has not inspected with this file. |
19
20
  | [`runner`](../runner) | Runs the tree from source (`python -m dwipe.main`) for debugging. |
20
21
  | [`linter`](../linter) | The advisory pylint run for one file, with the project root on `sys.path`. |
@@ -82,7 +83,7 @@ again. Enter, `quit` or Ctrl-C stops with nothing tagged, uploaded or pushed.
82
83
  **Stages, in order**
83
84
 
84
85
  1. **preflight** -- clean tree, the `vX.Y.Z` tag absent, `twine>=7` and
85
- `packaging>=26` present, and `main`/`dev` both local and on `origin`.
86
+ `packaging>=26` present, and `python -m build` answering.
86
87
  2. **index check** -- refuses if `__version__` is already on PyPI, printing
87
88
  every published version, before anything is built or uploaded.
88
89
  3. **gates** -- the three commands above.
@@ -170,17 +171,31 @@ the tagged commit. Everything else about the release is one of the stages above.
170
171
 
171
172
  ## Notes on this adoption
172
173
 
173
- * [`bless`](../bless) was copied byte-for-byte from `../pwr-tray/bless` (SHA-256
174
- `4f756337840b0619fbf6f077b00d4bb3fb9aee95ff18bbeb6a0521d39d52131e`) and is
175
- not edited here: reuse is "copy verbatim", and any later upstream change is
176
- adopted as a new copy.
177
- * `dwipe` needed four things `pwr-tray` already had: one version location
178
- (`dynamic = ["version"]`), an inspector, the `dev` extra with `build`,
179
- `twine>=7` and `packaging>=26`, and a console script that answers
180
- `--version`. The metadata was moved to the PEP 639 `license` expression and
181
- `license-files` at the same time, which is why the build backend floor is
182
- `flit_core>=3.12`.
174
+ * [`bless`](../bless) is emitted from `~/Projects/blessd/bless` by that
175
+ repository's `build.py`; it is never edited here. The policy the gate
176
+ enforces -- the branch plan, the prompt, the index check, the build, the
177
+ emitter -- and the tests of that policy live in `~/Projects/blessd`, once.
178
+ This tree carries the build with SHA-256
179
+ `3ef60e9e806ca58a3d900dd6622bb8be0052210da1ef26f9918bf0a1deb1e3b2`
180
+ (55222 bytes), which
181
+
182
+ python3 ~/Projects/blessd/build.py --into . --force
183
+
184
+ reproduces byte for byte, so `cmp ~/Projects/blessd/bless bless` is silent.
185
+ A later gate is adopted the same way, as a new emission rather than an edit.
186
+ * `dwipe` had to provide what the gate reads instead of being told it: one
187
+ version location (`dynamic = ["version"]`, the line in
188
+ [`dwipe/__init__.py`](../dwipe/__init__.py)), an inspector
189
+ ([`tests/inspect_dist.py`](../tests/inspect_dist.py)), a console script that
190
+ answers `--version` (`dwipe`), the `dev` extra with `build`, `twine>=7` and
191
+ `packaging>=26`, and the `main`/`dev` branch pair -- the gate's default
192
+ model, so no `[tool.bless]` table is needed. The metadata was moved to the
193
+ PEP 639 `license` expression and `license-files` at the same time, which is
194
+ why the build backend floor is `flit_core>=3.12`.
183
195
  * The ruff rule set was added with the gate, so the whole tree was brought to a
184
- clean `ruff check .` once. [`tests/test_bless.py`](../tests/test_bless.py) and
185
- [`tests/test_repo_hygiene.py`](../tests/test_repo_hygiene.py) keep the gate,
186
- the inspector and the single version location from drifting.
196
+ clean `ruff check .` once. [`tests/test_bless.py`](../tests/test_bless.py) now
197
+ covers only this tree's identity -- the name, module, console script and
198
+ branch model `bless` derives from `pyproject.toml`, the version reported by
199
+ `dwipe --version` -- and the inspector; the gate's policy tests are part of
200
+ the shared repository. [`tests/test_repo_hygiene.py`](../tests/test_repo_hygiene.py)
201
+ keeps the gate, the dev extra and the single version location from drifting.
@@ -71,7 +71,14 @@ class Utils:
71
71
  """Rerun using the module name"""
72
72
  if os.geteuid() != 0: # Re-run the script with sudo
73
73
  os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
74
- vp = ['sudo', sys.executable, '-m', module_name] + sys.argv[1:]
74
+ args = sys.argv[1:]
75
+ # sudo resets the environment by default, so an exported NO_COLOR
76
+ # would not reach the process that actually draws the UI. Carry it
77
+ # across as the equivalent switch; --no-color already wins over
78
+ # --theme, matching the no-color.org convention.
79
+ if os.environ.get('NO_COLOR') and '--no-color' not in args:
80
+ args = ['--no-color', *args]
81
+ vp = ['sudo', sys.executable, '-m', module_name] + args
75
82
  os.execvp('sudo', vp)
76
83
 
77
84
  @staticmethod
@@ -7,4 +7,4 @@ upload, ``dwipe --version`` prints it, and ``tests/inspect_dist.py`` checks the
7
7
  built artifacts against it.
8
8
  """
9
9
 
10
- __version__ = "3.2.0"
10
+ __version__ = "3.2.2"
@@ -12,6 +12,8 @@ import os
12
12
  import sys
13
13
  import traceback
14
14
 
15
+ from console_window import cli
16
+
15
17
  from . import __version__
16
18
  from .DiskWipe import DiskWipe
17
19
  from .PersistentState import PersistentState
@@ -55,6 +57,11 @@ def main():
55
57
  parser.add_argument('--auto-block-containers', choices=['on', 'off'],
56
58
  default='on' if persist.state['auto_block_containers'] else 'off',
57
59
  help='auto-block LUKS/LVM/RAID/swap containers on first sight (on/off)')
60
+ # --theme / --no-color come from the library, so the appearance switch and
61
+ # palette live in its one process-wide implementation rather than a copy
62
+ # here. --no-color is a per-run switch; the remembered choice is settled
63
+ # later by DiskWipe, alongside NO_COLOR.
64
+ cli.add_display_arguments(parser, prog='dwipe')
58
65
  opts = parser.parse_args()
59
66
 
60
67
  # Convert string booleans to actual booleans
@@ -67,6 +74,10 @@ def main():
67
74
  # Re-run the script with sudo needed and opted
68
75
  Utils.rerun_module_as_root('dwipe.main')
69
76
 
77
+ # Apply --ascii/--glyphs/--theme/--no-color once, in the process that
78
+ # will draw the UI: a sudo re-run starts over from the same argv.
79
+ cli.apply_display_arguments(opts, prog='dwipe')
80
+
70
81
  prereqs = Prereqs(verbose=True)
71
82
  # blkid for filesystem detection; hdparm and nvme for firmware wipes
72
83
  prereqs.check_all(['blkid', 'hdparm', 'nvme'])
@@ -79,15 +79,18 @@ classifiers = [
79
79
  "Operating System :: POSIX :: Linux"
80
80
  ]
81
81
  dependencies = [
82
- 'console-window == 2.1.3',
82
+ 'console-window == 2.2.2',
83
83
  ]
84
84
 
85
85
  [project.optional-dependencies]
86
86
  # The minimum a contributor needs to run the suite (what CI installs on every
87
- # supported Python).
87
+ # supported Python). `tomli` only appears below 3.11, where the stdlib has no
88
+ # `tomllib`, yet tests and `bless` read pyproject.toml on every interpreter
89
+ # `requires-python` allows.
88
90
  test = [
89
91
  'pytest >= 7.0',
90
92
  'pytest-cov >= 4.0',
93
+ 'tomli >= 2.0; python_version < "3.11"',
91
94
  ]
92
95
  # The release toolchain. `./bless` refuses to start unless `twine` and
93
96
  # `packaging` meet its own floors (`twine>=7`, `packaging>=26`), so declaring
@@ -100,6 +103,7 @@ dev = [
100
103
  'build >= 1.2',
101
104
  'twine >= 7',
102
105
  'packaging >= 26',
106
+ 'tomli >= 2.0; python_version < "3.11"',
103
107
  ]
104
108
 
105
109
  [project.urls]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes