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.
- {seedshield-0.2.3/SeedShield.egg-info → seedshield-0.2.4}/PKG-INFO +6 -6
- {seedshield-0.2.3 → seedshield-0.2.4/SeedShield.egg-info}/PKG-INFO +6 -6
- {seedshield-0.2.3 → seedshield-0.2.4}/SeedShield.egg-info/SOURCES.txt +2 -0
- {seedshield-0.2.3 → seedshield-0.2.4}/SeedShield.egg-info/requires.txt +1 -0
- {seedshield-0.2.3 → seedshield-0.2.4}/pyproject.toml +17 -13
- {seedshield-0.2.3 → seedshield-0.2.4}/seedshield/__init__.py +10 -10
- {seedshield-0.2.3 → seedshield-0.2.4}/seedshield/config.py +34 -24
- {seedshield-0.2.3 → seedshield-0.2.4}/seedshield/display_handler.py +82 -67
- {seedshield-0.2.3 → seedshield-0.2.4}/seedshield/input_handler.py +40 -23
- {seedshield-0.2.3 → seedshield-0.2.4}/seedshield/main.py +24 -20
- {seedshield-0.2.3 → seedshield-0.2.4}/seedshield/secure_memory.py +45 -18
- {seedshield-0.2.3 → seedshield-0.2.4}/seedshield/secure_word_interface.py +62 -102
- {seedshield-0.2.3 → seedshield-0.2.4}/seedshield/state_handler.py +18 -15
- {seedshield-0.2.3 → seedshield-0.2.4}/seedshield/ui_manager.py +5 -5
- {seedshield-0.2.3 → seedshield-0.2.4}/setup.py +1 -1
- {seedshield-0.2.3 → seedshield-0.2.4}/tests/__init__.py +1 -1
- {seedshield-0.2.3 → seedshield-0.2.4}/tests/conftest.py +40 -30
- seedshield-0.2.4/tests/test_config.py +54 -0
- {seedshield-0.2.3 → seedshield-0.2.4}/tests/test_display_handler.py +14 -15
- {seedshield-0.2.3 → seedshield-0.2.4}/tests/test_fixtures.py +18 -13
- {seedshield-0.2.3 → seedshield-0.2.4}/tests/test_input_handler.py +59 -58
- {seedshield-0.2.3 → seedshield-0.2.4}/tests/test_main.py +125 -108
- seedshield-0.2.4/tests/test_secure_memory.py +173 -0
- {seedshield-0.2.3 → seedshield-0.2.4}/tests/test_secure_word_interface.py +115 -76
- {seedshield-0.2.3 → seedshield-0.2.4}/tests/test_state_handler.py +8 -11
- {seedshield-0.2.3 → seedshield-0.2.4}/tests/test_ui_manager.py +89 -84
- {seedshield-0.2.3 → seedshield-0.2.4}/LICENSE +0 -0
- {seedshield-0.2.3 → seedshield-0.2.4}/MANIFEST.in +0 -0
- {seedshield-0.2.3 → seedshield-0.2.4}/README.md +0 -0
- {seedshield-0.2.3 → seedshield-0.2.4}/SeedShield.egg-info/dependency_links.txt +0 -0
- {seedshield-0.2.3 → seedshield-0.2.4}/SeedShield.egg-info/entry_points.txt +0 -0
- {seedshield-0.2.3 → seedshield-0.2.4}/SeedShield.egg-info/top_level.txt +0 -0
- {seedshield-0.2.3 → seedshield-0.2.4}/pre_build.py +0 -0
- {seedshield-0.2.3 → seedshield-0.2.4}/seedshield/data/english.txt +0 -0
- {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
|
+
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.
|
|
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
|
+
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.
|
|
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
|
|
@@ -4,13 +4,14 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "SeedShield"
|
|
7
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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.
|
|
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(
|
|
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:
|
|
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(
|
|
67
|
+
console_formatter = logging.Formatter("%(levelname)s: %(message)s")
|
|
56
68
|
console_handler.setFormatter(console_formatter)
|
|
57
69
|
|
|
58
|
-
# File handler with rotation
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
|
86
|
+
if viewport.start > 0:
|
|
64
87
|
try:
|
|
65
|
-
stdscr.addstr(
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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",
|
|
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,
|
|
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,
|
|
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,
|
|
241
|
+
self._add_menu(stdscr, height, state.reached_last)
|
|
218
242
|
|
|
219
243
|
# Return number of visible words
|
|
220
|
-
result: int =
|
|
244
|
+
result: int = viewport.end - viewport.start
|
|
221
245
|
return result
|
|
222
246
|
|
|
223
247
|
def _display_visible_words(
|
|
224
|
-
|
|
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
|
-
|
|
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[
|
|
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 -
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|