SeedShield 0.2.3__tar.gz → 0.2.4__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 (35) hide show
  1. {seedshield-0.2.3/SeedShield.egg-info → seedshield-0.2.4}/PKG-INFO +6 -6
  2. {seedshield-0.2.3 → seedshield-0.2.4/SeedShield.egg-info}/PKG-INFO +6 -6
  3. {seedshield-0.2.3 → seedshield-0.2.4}/SeedShield.egg-info/SOURCES.txt +2 -0
  4. {seedshield-0.2.3 → seedshield-0.2.4}/SeedShield.egg-info/requires.txt +1 -0
  5. {seedshield-0.2.3 → seedshield-0.2.4}/pyproject.toml +17 -13
  6. {seedshield-0.2.3 → seedshield-0.2.4}/seedshield/__init__.py +10 -10
  7. {seedshield-0.2.3 → seedshield-0.2.4}/seedshield/config.py +34 -24
  8. {seedshield-0.2.3 → seedshield-0.2.4}/seedshield/display_handler.py +82 -67
  9. {seedshield-0.2.3 → seedshield-0.2.4}/seedshield/input_handler.py +40 -23
  10. {seedshield-0.2.3 → seedshield-0.2.4}/seedshield/main.py +24 -20
  11. {seedshield-0.2.3 → seedshield-0.2.4}/seedshield/secure_memory.py +45 -18
  12. {seedshield-0.2.3 → seedshield-0.2.4}/seedshield/secure_word_interface.py +62 -102
  13. {seedshield-0.2.3 → seedshield-0.2.4}/seedshield/state_handler.py +18 -15
  14. {seedshield-0.2.3 → seedshield-0.2.4}/seedshield/ui_manager.py +5 -5
  15. {seedshield-0.2.3 → seedshield-0.2.4}/setup.py +1 -1
  16. {seedshield-0.2.3 → seedshield-0.2.4}/tests/__init__.py +1 -1
  17. {seedshield-0.2.3 → seedshield-0.2.4}/tests/conftest.py +40 -30
  18. seedshield-0.2.4/tests/test_config.py +54 -0
  19. {seedshield-0.2.3 → seedshield-0.2.4}/tests/test_display_handler.py +14 -15
  20. {seedshield-0.2.3 → seedshield-0.2.4}/tests/test_fixtures.py +18 -13
  21. {seedshield-0.2.3 → seedshield-0.2.4}/tests/test_input_handler.py +59 -58
  22. {seedshield-0.2.3 → seedshield-0.2.4}/tests/test_main.py +125 -108
  23. seedshield-0.2.4/tests/test_secure_memory.py +173 -0
  24. {seedshield-0.2.3 → seedshield-0.2.4}/tests/test_secure_word_interface.py +115 -76
  25. {seedshield-0.2.3 → seedshield-0.2.4}/tests/test_state_handler.py +8 -11
  26. {seedshield-0.2.3 → seedshield-0.2.4}/tests/test_ui_manager.py +89 -84
  27. {seedshield-0.2.3 → seedshield-0.2.4}/LICENSE +0 -0
  28. {seedshield-0.2.3 → seedshield-0.2.4}/MANIFEST.in +0 -0
  29. {seedshield-0.2.3 → seedshield-0.2.4}/README.md +0 -0
  30. {seedshield-0.2.3 → seedshield-0.2.4}/SeedShield.egg-info/dependency_links.txt +0 -0
  31. {seedshield-0.2.3 → seedshield-0.2.4}/SeedShield.egg-info/entry_points.txt +0 -0
  32. {seedshield-0.2.3 → seedshield-0.2.4}/SeedShield.egg-info/top_level.txt +0 -0
  33. {seedshield-0.2.3 → seedshield-0.2.4}/pre_build.py +0 -0
  34. {seedshield-0.2.3 → seedshield-0.2.4}/seedshield/data/english.txt +0 -0
  35. {seedshield-0.2.3 → seedshield-0.2.4}/setup.cfg +0 -0
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SeedShield
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Secure BIP39 word viewer with masking and reveal functionality
5
5
  Author-email: Barlog951 <barlog951@gmail.com>
6
+ License: MIT
6
7
  Project-URL: Documentation, https://github.com/Barlog951/SeedShield/blob/main/README.md
7
8
  Project-URL: Source Code, https://github.com/Barlog951/SeedShield
8
9
  Project-URL: Bug Tracker, https://github.com/Barlog951/SeedShield/issues
@@ -12,15 +13,13 @@ Classifier: Intended Audience :: End Users/Desktop
12
13
  Classifier: License :: OSI Approved :: MIT License
13
14
  Classifier: Operating System :: OS Independent
14
15
  Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.6
16
- Classifier: Programming Language :: Python :: 3.7
17
- Classifier: Programming Language :: Python :: 3.8
18
- Classifier: Programming Language :: Python :: 3.9
19
16
  Classifier: Programming Language :: Python :: 3.10
20
17
  Classifier: Programming Language :: Python :: 3.11
21
18
  Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
22
21
  Classifier: Topic :: Security
23
- Requires-Python: >=3.6
22
+ Requires-Python: >=3.10
24
23
  Description-Content-Type: text/markdown
25
24
  License-File: LICENSE
26
25
  Requires-Dist: pyperclip>=1.8.0
@@ -36,6 +35,7 @@ Requires-Dist: windows-curses; extra == "windows"
36
35
  Provides-Extra: dev
37
36
  Requires-Dist: build; extra == "dev"
38
37
  Requires-Dist: twine; extra == "dev"
38
+ Requires-Dist: black; extra == "dev"
39
39
  Dynamic: license-file
40
40
 
41
41
  # SeedShield
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SeedShield
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Secure BIP39 word viewer with masking and reveal functionality
5
5
  Author-email: Barlog951 <barlog951@gmail.com>
6
+ License: MIT
6
7
  Project-URL: Documentation, https://github.com/Barlog951/SeedShield/blob/main/README.md
7
8
  Project-URL: Source Code, https://github.com/Barlog951/SeedShield
8
9
  Project-URL: Bug Tracker, https://github.com/Barlog951/SeedShield/issues
@@ -12,15 +13,13 @@ Classifier: Intended Audience :: End Users/Desktop
12
13
  Classifier: License :: OSI Approved :: MIT License
13
14
  Classifier: Operating System :: OS Independent
14
15
  Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.6
16
- Classifier: Programming Language :: Python :: 3.7
17
- Classifier: Programming Language :: Python :: 3.8
18
- Classifier: Programming Language :: Python :: 3.9
19
16
  Classifier: Programming Language :: Python :: 3.10
20
17
  Classifier: Programming Language :: Python :: 3.11
21
18
  Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
22
21
  Classifier: Topic :: Security
23
- Requires-Python: >=3.6
22
+ Requires-Python: >=3.10
24
23
  Description-Content-Type: text/markdown
25
24
  License-File: LICENSE
26
25
  Requires-Dist: pyperclip>=1.8.0
@@ -36,6 +35,7 @@ Requires-Dist: windows-curses; extra == "windows"
36
35
  Provides-Extra: dev
37
36
  Requires-Dist: build; extra == "dev"
38
37
  Requires-Dist: twine; extra == "dev"
38
+ Requires-Dist: black; extra == "dev"
39
39
  Dynamic: license-file
40
40
 
41
41
  # SeedShield
@@ -22,10 +22,12 @@ seedshield/ui_manager.py
22
22
  seedshield/data/english.txt
23
23
  tests/__init__.py
24
24
  tests/conftest.py
25
+ tests/test_config.py
25
26
  tests/test_display_handler.py
26
27
  tests/test_fixtures.py
27
28
  tests/test_input_handler.py
28
29
  tests/test_main.py
30
+ tests/test_secure_memory.py
29
31
  tests/test_secure_word_interface.py
30
32
  tests/test_state_handler.py
31
33
  tests/test_ui_manager.py
@@ -3,6 +3,7 @@ pyperclip>=1.8.0
3
3
  [dev]
4
4
  build
5
5
  twine
6
+ black
6
7
 
7
8
  [test]
8
9
  pytest>=6.0
@@ -4,13 +4,14 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "SeedShield"
7
- version = "0.2.3"
7
+ dynamic = ["version"]
8
8
  readme = { file = "README.md", content-type = "text/markdown" }
9
9
  description = "Secure BIP39 word viewer with masking and reveal functionality"
10
+ license = {text = "MIT"}
10
11
  authors = [
11
12
  {name = "Barlog951", email = "barlog951@gmail.com"}
12
13
  ]
13
- requires-python = ">=3.6"
14
+ requires-python = ">=3.10"
14
15
  dependencies = [
15
16
  "pyperclip>=1.8.0",
16
17
  ]
@@ -21,13 +22,11 @@ classifiers = [
21
22
  "License :: OSI Approved :: MIT License",
22
23
  "Operating System :: OS Independent",
23
24
  "Programming Language :: Python :: 3",
24
- "Programming Language :: Python :: 3.6",
25
- "Programming Language :: Python :: 3.7",
26
- "Programming Language :: Python :: 3.8",
27
- "Programming Language :: Python :: 3.9",
28
25
  "Programming Language :: Python :: 3.10",
29
26
  "Programming Language :: Python :: 3.11",
30
27
  "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Programming Language :: Python :: 3.14",
31
30
  "Topic :: Security",
32
31
  ]
33
32
 
@@ -48,15 +47,23 @@ test = [
48
47
  windows = ["windows-curses"]
49
48
  dev = [
50
49
  "build",
51
- "twine"
50
+ "twine",
51
+ "black"
52
52
  ]
53
53
 
54
54
  [project.scripts]
55
55
  seedshield = "seedshield.main:main"
56
56
 
57
+ [tool.setuptools.dynamic]
58
+ # Single source of truth for the version; semantic-release updates config.py
59
+ version = {attr = "seedshield.config.VERSION"}
60
+
57
61
  [tool.pytest.ini_options]
58
62
  testpaths = ["tests"]
63
+ pythonpath = ["."]
59
64
  python_files = ["test_*.py"]
65
+ python_classes = ["Test*"]
66
+ python_functions = ["test_*"]
60
67
  markers = [
61
68
  "timeout: mark test with timeout value in seconds"
62
69
  ]
@@ -67,12 +74,9 @@ disable = [
67
74
  "C0103", # invalid-name
68
75
  ]
69
76
 
70
- [tool.flake8]
71
- max-line-length = 100
72
- exclude = [".git", "__pycache__", "build", "dist"]
73
-
74
77
  [tool.mypy]
75
- python_version = "3.8"
78
+ # Type-check against the supported floor
79
+ python_version = "3.10"
76
80
  warn_return_any = true
77
81
  warn_unused_configs = true
78
- disallow_untyped_defs = true
82
+ disallow_untyped_defs = true
@@ -19,14 +19,14 @@ __version__ = VERSION
19
19
 
20
20
  # Define available components
21
21
  __all__ = [
22
- 'SecureWordInterface',
23
- 'InputHandler',
24
- 'DisplayHandler',
25
- 'StateHandler',
26
- 'UIManager',
27
- 'secure_clear_string',
28
- 'secure_clear_list',
29
- 'secure_clipboard_clear',
30
- 'setup_logging',
31
- 'logger'
22
+ "SecureWordInterface",
23
+ "InputHandler",
24
+ "DisplayHandler",
25
+ "StateHandler",
26
+ "UIManager",
27
+ "secure_clear_string",
28
+ "secure_clear_list",
29
+ "secure_clipboard_clear",
30
+ "setup_logging",
31
+ "logger",
32
32
  ]
@@ -9,10 +9,11 @@ import os
9
9
  import logging
10
10
  import logging.handlers
11
11
  import sys
12
+ from typing import Optional
12
13
 
13
14
  # Application constants
14
15
  APP_NAME = "SeedShield"
15
- VERSION = "0.2.3"
16
+ VERSION = "0.2.4"
16
17
 
17
18
  # Security settings
18
19
  REVEAL_TIMEOUT = 3 # Seconds before auto-hiding revealed words
@@ -32,16 +33,22 @@ SCROLL_INDICATOR_DOWN = "↓ More ↓"
32
33
  MENU_TEXT = {
33
34
  "standard": "'n' - new input, 's' - show one by one, 'q' - quit, ↑↓ - scroll",
34
35
  "with_reset": "'n' - input, 's' - show one by one, 'r' - reset, 'q' - quit, ↑↓ - scroll",
35
- "mouse_help": "Mouse over to reveal word"
36
+ "mouse_help": "Mouse over to reveal word",
36
37
  }
37
38
 
38
39
 
39
- def setup_logging(log_level: int = logging.INFO) -> logging.Logger:
40
+ def setup_logging(
41
+ log_level: int = logging.WARNING, log_file: Optional[str] = None
42
+ ) -> logging.Logger:
40
43
  """
41
44
  Set up application logging with proper security measures.
42
45
 
46
+ File logging is opt-in: no usage trail is written to disk unless a log
47
+ file is explicitly requested (e.g. via the --verbose flag).
48
+
43
49
  Args:
44
- log_level: Desired logging level (default: INFO)
50
+ log_level: Desired logging level (default: WARNING)
51
+ log_file: Optional path for file logging; None disables it
45
52
 
46
53
  Returns:
47
54
  logging.Logger: Configured logger instance
@@ -49,35 +56,38 @@ def setup_logging(log_level: int = logging.INFO) -> logging.Logger:
49
56
  log = logging.getLogger(APP_NAME)
50
57
  log.setLevel(log_level)
51
58
 
59
+ # Reconfiguring must not stack duplicate handlers
60
+ for handler in log.handlers[:]:
61
+ log.removeHandler(handler)
62
+ handler.close()
63
+
52
64
  # Console handler for error messages only
53
65
  console_handler = logging.StreamHandler(sys.stderr)
54
66
  console_handler.setLevel(logging.ERROR)
55
- console_formatter = logging.Formatter('%(levelname)s: %(message)s')
67
+ console_formatter = logging.Formatter("%(levelname)s: %(message)s")
56
68
  console_handler.setFormatter(console_formatter)
57
69
 
58
- # File handler with rotation to prevent excessive log growth
59
- try:
60
- file_handler = logging.handlers.RotatingFileHandler(
61
- DEFAULT_LOG_PATH,
62
- maxBytes=LOG_MAX_SIZE,
63
- backupCount=LOG_BACKUP_COUNT
64
- )
65
- file_handler.setLevel(log_level)
66
- file_formatter = logging.Formatter(
67
- '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
68
- datefmt='%Y-%m-%d %H:%M:%S'
69
- )
70
- file_handler.setFormatter(file_formatter)
71
-
72
- log.addHandler(file_handler)
73
- except (PermissionError, IOError, OSError):
74
- # Fall back to console-only logging if file logging fails
75
- pass
70
+ # File handler with rotation, only when explicitly requested
71
+ if log_file is not None:
72
+ try:
73
+ file_handler = logging.handlers.RotatingFileHandler(
74
+ log_file, maxBytes=LOG_MAX_SIZE, backupCount=LOG_BACKUP_COUNT
75
+ )
76
+ file_handler.setLevel(log_level)
77
+ file_formatter = logging.Formatter(
78
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
79
+ )
80
+ file_handler.setFormatter(file_formatter)
81
+
82
+ log.addHandler(file_handler)
83
+ except (PermissionError, IOError, OSError):
84
+ # Fall back to console-only logging if file logging fails
85
+ pass
76
86
 
77
87
  log.addHandler(console_handler)
78
88
 
79
89
  return log
80
90
 
81
91
 
82
- # Create the application logger
92
+ # Create the application logger (console-only; file logging is opt-in)
83
93
  logger = setup_logging()
@@ -6,6 +6,7 @@ ensuring proper masking and interaction.
6
6
  """
7
7
 
8
8
  import curses
9
+ from dataclasses import dataclass
9
10
  from typing import List, Optional, Any
10
11
 
11
12
  from .config import logger, MASK_CHARACTER, MASK_LENGTH
@@ -13,6 +14,35 @@ from .config import SCROLL_INDICATOR_UP, SCROLL_INDICATOR_DOWN, MENU_TEXT
13
14
  from .state_handler import StateHandler
14
15
 
15
16
 
17
+ @dataclass(frozen=True)
18
+ class Viewport:
19
+ """Terminal viewport for one render pass; start doubles as scroll position."""
20
+
21
+ height: int
22
+ width: int
23
+ start: int
24
+ end: int
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class WordRow:
29
+ """A single word row ready for rendering."""
30
+
31
+ word: str
32
+ display_num: int
33
+ y_pos: int
34
+ revealed: bool
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class DisplayState:
39
+ """View state for one display pass."""
40
+
41
+ scroll: int
42
+ cursor: Optional[int]
43
+ reached_last: bool
44
+
45
+
16
46
  class DisplayHandler:
17
47
  """
18
48
  Handles display and UI rendering for the secure word interface.
@@ -35,7 +65,6 @@ class DisplayHandler:
35
65
  self.words = words
36
66
  self.ui_manager = ui_manager
37
67
  self.mask = MASK_CHARACTER * MASK_LENGTH
38
- self.legacy_mask = self.mask # For backward compatibility with tests
39
68
  self.state_handler: Optional[StateHandler] = None # Will be set later
40
69
 
41
70
  # Store the last state to optimize rendering
@@ -44,40 +73,41 @@ class DisplayHandler:
44
73
 
45
74
  logger.debug("DisplayHandler initialized with %s words", len(words))
46
75
 
47
- def _add_scroll_indicators(
48
- self, stdscr: Any, visible_start: int, visible_end: int,
49
- positions: List[int], height: int, width: int
50
- ) -> None:
76
+ def _add_scroll_indicators(self, stdscr: Any, viewport: Viewport, total: int) -> None:
51
77
  """
52
78
  Add scroll indicators to the display if needed.
53
79
 
54
80
  Args:
55
81
  stdscr: Curses window objec
56
- visible_start: Index of first visible word
57
- visible_end: Index of last visible word
58
- positions: List of word positions
59
- height: Terminal heigh
60
- width: Terminal width
82
+ viewport: Current terminal viewport
83
+ total: Total number of positions in the list
61
84
  """
62
85
  # Show up indicator if there are hidden items above
63
- if visible_start > 0:
86
+ if viewport.start > 0:
64
87
  try:
65
- stdscr.addstr(0, max(0, width - len(SCROLL_INDICATOR_UP) - 1), SCROLL_INDICATOR_UP)
66
- logger.debug("Added up scroll indicator at position 0,%s",
67
- width - len(SCROLL_INDICATOR_UP) - 1)
88
+ stdscr.addstr(
89
+ 0, max(0, viewport.width - len(SCROLL_INDICATOR_UP) - 1), SCROLL_INDICATOR_UP
90
+ )
91
+ logger.debug(
92
+ "Added up scroll indicator at position 0,%s",
93
+ viewport.width - len(SCROLL_INDICATOR_UP) - 1,
94
+ )
68
95
  except curses.error:
69
96
  pass
70
97
 
71
98
  # Show down indicator if there are hidden items below
72
- if visible_end < len(positions):
99
+ if viewport.end < total:
73
100
  try:
74
101
  stdscr.addstr(
75
- height - 7,
76
- max(0, width - len(SCROLL_INDICATOR_DOWN) - 1),
77
- SCROLL_INDICATOR_DOWN
102
+ viewport.height - 7,
103
+ max(0, viewport.width - len(SCROLL_INDICATOR_DOWN) - 1),
104
+ SCROLL_INDICATOR_DOWN,
105
+ )
106
+ logger.debug(
107
+ "Added down scroll indicator at position %s,%s",
108
+ viewport.height - 7,
109
+ viewport.width - len(SCROLL_INDICATOR_DOWN) - 1,
78
110
  )
79
- logger.debug("Added down scroll indicator at position %s,%s",
80
- height - 7, width - len(SCROLL_INDICATOR_DOWN) - 1)
81
111
  except curses.error:
82
112
  pass
83
113
 
@@ -132,30 +162,26 @@ class DisplayHandler:
132
162
  except curses.error:
133
163
  pass
134
164
 
135
- def _render_word(self, stdscr: Any, word: str, display_num: int,
136
- y_pos: int, is_revealed: bool, width: int) -> None:
165
+ def _render_word(self, stdscr: Any, row: WordRow, width: int) -> None:
137
166
  """
138
167
  Render a single word with proper masking and formatting.
139
168
 
140
169
  Args:
141
170
  stdscr: Curses window objec
142
- word: The word to display
143
- display_num: The display number
144
- y_pos: The y position to render a
145
- is_revealed: Whether the word should be revealed
171
+ row: The word row to render
146
172
  width: Terminal width
147
173
  """
148
- display_text = f"{display_num:2d}. {word if is_revealed else self.mask}"
174
+ display_text = f"{row.display_num:2d}. {row.word if row.revealed else self.mask}"
149
175
 
150
176
  # Ensure text fits within terminal width
151
177
  if len(display_text) >= width:
152
- display_text = display_text[:width - 1]
178
+ display_text = display_text[: width - 1]
153
179
 
154
180
  # Highlight revealed word
155
- if is_revealed:
156
- stdscr.addstr(y_pos, 0, display_text, curses.A_BOLD)
181
+ if row.revealed:
182
+ stdscr.addstr(row.y_pos, 0, display_text, curses.A_BOLD)
157
183
  else:
158
- stdscr.addstr(y_pos, 0, display_text)
184
+ stdscr.addstr(row.y_pos, 0, display_text)
159
185
 
160
186
  def _get_word_for_position(self, pos: int) -> str:
161
187
  """
@@ -170,22 +196,18 @@ class DisplayHandler:
170
196
  if 0 <= pos - 1 < len(self.words):
171
197
  return self.words[pos - 1]
172
198
 
173
- logger.warning("Invalid word position: %s", pos)
199
+ # Never log the position value: positions encode the seed
200
+ logger.warning("Invalid word position requested")
174
201
  return f"INVALID({pos})"
175
202
 
176
- def display_words(
177
- self, stdscr: Any, positions: List[int], scroll_position: int,
178
- cursor_pos: Optional[int], is_last_reached: bool
179
- ) -> int:
203
+ def display_words(self, stdscr: Any, positions: List[int], state: DisplayState) -> int:
180
204
  """
181
205
  Display words with masking in the terminal interface.
182
206
 
183
207
  Args:
184
208
  stdscr: Curses window objec
185
209
  positions: List of word positions to display
186
- scroll_position: Current scroll position
187
- cursor_pos: Position of cursor (revealed word)
188
- is_last_reached: Whether last word has been reached
210
+ state: Current view state (scroll, cursor, reached_last)
189
211
 
190
212
  Returns:
191
213
  int: Number of visible words that could fit in the current view
@@ -197,34 +219,33 @@ class DisplayHandler:
197
219
 
198
220
  # Calculate display metrics
199
221
  max_display_lines = max(1, height - 7) # Reserve space for menu and indicators
200
- visible_start = scroll_position
201
- visible_end = min(len(positions), scroll_position + max_display_lines // 2)
222
+ viewport = Viewport(
223
+ height=height,
224
+ width=width,
225
+ start=state.scroll,
226
+ end=min(len(positions), state.scroll + max_display_lines // 2),
227
+ )
202
228
 
203
- logger.debug("Displaying words %s-%s of %s", visible_start, visible_end, len(positions))
229
+ logger.debug("Displaying words %s-%s of %s", viewport.start, viewport.end, len(positions))
204
230
 
205
231
  # Clear screen for fresh rendering
206
232
  stdscr.clear()
207
233
 
208
234
  # Display words with proper masking
209
- self._display_visible_words(stdscr, positions, visible_start, visible_end,
210
- scroll_position, cursor_pos, height, width)
235
+ self._display_visible_words(stdscr, positions, viewport, state.cursor)
211
236
 
212
237
  # Add scroll indicators if needed
213
- self._add_scroll_indicators(stdscr, visible_start, visible_end, positions,
214
- height, width)
238
+ self._add_scroll_indicators(stdscr, viewport, len(positions))
215
239
 
216
240
  # Add command menu
217
- self._add_menu(stdscr, height, is_last_reached)
241
+ self._add_menu(stdscr, height, state.reached_last)
218
242
 
219
243
  # Return number of visible words
220
- result: int = visible_end - visible_start
244
+ result: int = viewport.end - viewport.start
221
245
  return result
222
246
 
223
247
  def _display_visible_words(
224
- self, stdscr: Any, positions: List[int],
225
- visible_start: int, visible_end: int,
226
- scroll_position: int, cursor_pos: Optional[int],
227
- height: int, width: int
248
+ self, stdscr: Any, positions: List[int], viewport: Viewport, cursor_pos: Optional[int]
228
249
  ) -> None:
229
250
  """
230
251
  Display the visible words on the screen.
@@ -232,31 +253,28 @@ class DisplayHandler:
232
253
  Args:
233
254
  stdscr: Curses window objec
234
255
  positions: List of word positions
235
- visible_start: Index of first visible word
236
- visible_end: Index of last visible word
237
- scroll_position: Current scroll position
256
+ viewport: Current terminal viewport
238
257
  cursor_pos: Position of cursor (revealed word)
239
- height: Terminal heigh
240
- width: Terminal width
241
258
  """
242
- for i, pos in enumerate(positions[visible_start:visible_end], visible_start):
259
+ for i, pos in enumerate(positions[viewport.start : viewport.end], viewport.start):
243
260
  try:
244
261
  # Get word for the position
245
262
  word = self._get_word_for_position(pos)
246
263
 
247
264
  # Calculate position and check if it's visible
248
- y_pos = i * 2 - scroll_position * 2
265
+ y_pos = i * 2 - viewport.start * 2
249
266
 
250
267
  # Only render if within displayable area
251
- if 0 <= y_pos < height - 6:
252
- display_num = i + 1
253
- is_revealed = cursor_pos == i
254
- self._render_word(stdscr, word, display_num, y_pos, is_revealed, width)
268
+ if 0 <= y_pos < viewport.height - 6:
269
+ row = WordRow(
270
+ word=word, display_num=i + 1, y_pos=y_pos, revealed=cursor_pos == i
271
+ )
272
+ self._render_word(stdscr, row, viewport.width)
255
273
 
256
274
  except curses.error:
257
275
  # Handle rendering errors
258
276
  logger.debug("Error displaying word at position %s", i)
259
- except Exception as e:
277
+ except Exception as e: # pylint: disable=broad-exception-caught
260
278
  # Handle unexpected errors
261
279
  logger.error("Unexpected error displaying word at position %s: %s", i, str(e))
262
280
 
@@ -306,6 +324,3 @@ class DisplayHandler:
306
324
 
307
325
  # Cursor is already visible
308
326
  return scroll_pos
309
-
310
- # For backward compatibility with tests
311
- handle_scroll = handle_autoscroll