Skip to content

Commit ef07834

Browse files
roccolateclaude
andcommitted
release: bump to v0.9.3 — rendering optimization + version update
Add fast-path _bounds parameter to safe_addstr, skipping getmaxyx() and Mock-detection in the rendering hot path. All 7 calls in rendering.py (desktop, icons, taskbar, statusbar) now use pre-computed frame dimensions. Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent 65cba5a commit ef07834

12 files changed

Lines changed: 61 additions & 56 deletions

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "retrotui"
7-
version = "0.9.2"
7+
version = "0.9.3"
88
description = "RetroTUI desktop environment for terminal"
99
readme = "README.md"
1010
requires-python = ">=3.10"

retrotui/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
"""
22
RetroTUI Package
33
"""
4-
__version__ = '0.9.2'
4+
__version__ = '0.9.3'

retrotui/core/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109

110110
LOGGER = logging.getLogger(__name__)
111111

112-
APP_VERSION = '0.9.2'
112+
APP_VERSION = '0.9.3'
113113
_CONFIG_PERSIST_ERRORS = (OSError, UnicodeError, ValueError, TypeError)
114114
_RUNTIME_ISOLATION_ERRORS = (
115115
ArithmeticError,

retrotui/core/bios.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def run(self):
4949
if self._check_skip(): return False
5050

5151
y += 1
52-
safe_addstr(self.stdscr, y, x, f"RetroBIOS v0.9.2 (C) 2026 RetroTUI Corp.", attr)
52+
safe_addstr(self.stdscr, y, x, f"RetroBIOS v0.9.3 (C) 2026 RetroTUI Corp.", attr)
5353
y += 2
5454

5555
self.stdscr.refresh()

retrotui/core/rendering.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def _taskbar_buttons(app, width, stats=None):
9494
def draw_desktop(app, frame_size=None):
9595
"""Draw the desktop background pattern."""
9696
h, w = _resolve_frame_size(app, frame_size)
97+
bounds = (h, w)
9798
attr = theme_attr("desktop")
9899
pattern = getattr(getattr(app, "theme", None), "desktop_pattern", DESKTOP_PATTERN)
99100

@@ -105,12 +106,13 @@ def draw_desktop(app, frame_size=None):
105106
line = _desktop_line_cache['line']
106107

107108
for row in range(MENU_BAR_HEIGHT, h - BOTTOM_BARS_HEIGHT + 1):
108-
safe_addstr(app.stdscr, row, 0, line, attr)
109+
safe_addstr(app.stdscr, row, 0, line, attr, _bounds=bounds)
109110

110111

111112
def draw_icons(app, frame_size=None):
112113
"""Draw desktop icons (3x4 art + label)."""
113-
h, _ = _resolve_frame_size(app, frame_size)
114+
h, w = _resolve_frame_size(app, frame_size)
115+
bounds = (h, w)
114116
for idx, icon in enumerate(app.icons):
115117
# Use dynamic position helper
116118
x, y = app.get_icon_screen_pos(idx)
@@ -138,24 +140,25 @@ def draw_icons(app, frame_size=None):
138140
attr |= curses.A_BOLD
139141

140142
for row, line in enumerate(art_lines):
141-
safe_addstr(app.stdscr, y + row, x, line, attr)
143+
safe_addstr(app.stdscr, y + row, x, line, attr, _bounds=bounds)
142144
label = str(icon.get("label", "")).center(max(art_width, 2))
143-
safe_addstr(app.stdscr, y + render_height, x, label, attr)
145+
safe_addstr(app.stdscr, y + render_height, x, label, attr, _bounds=bounds)
144146

145147

146148
def draw_taskbar(app, frame_size=None):
147149
"""Draw taskbar row with minimized window buttons."""
148150
h, w = _resolve_frame_size(app, frame_size)
151+
bounds = (h, w)
149152
taskbar_y = h - BOTTOM_BARS_HEIGHT
150153
attr = theme_attr("taskbar")
151-
154+
152155
# Always clear the taskbar line
153-
safe_addstr(app.stdscr, taskbar_y, 0, ' ' * w, attr)
154-
156+
safe_addstr(app.stdscr, taskbar_y, 0, ' ' * w, attr, _bounds=bounds)
157+
155158
stats = _window_stats(app)
156159
buttons = _taskbar_buttons(app, w, stats=stats)
157160
for start_x, _end_x, label, _win in buttons:
158-
safe_addstr(app.stdscr, taskbar_y, start_x, f'[{label}]', attr | curses.A_BOLD)
161+
safe_addstr(app.stdscr, taskbar_y, start_x, f'[{label}]', attr | curses.A_BOLD, _bounds=bounds)
159162

160163

161164
def draw_statusbar(app, version, frame_size=None):
@@ -175,4 +178,4 @@ def draw_statusbar(app, version, frame_size=None):
175178
max_status_len = w - status_x
176179
if max_status_len <= 0:
177180
return
178-
safe_addstr(app.stdscr, statusbar_y, status_x, status_text[:max_status_len], attr)
181+
safe_addstr(app.stdscr, statusbar_y, status_x, status_text[:max_status_len], attr, _bounds=(h, w))

retrotui/utils.py

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -184,30 +184,34 @@ def theme_attr(role):
184184
_theme_attr_cache[role] = attr
185185
return attr
186186

187-
def safe_addstr(win, y, x, text, attr=0):
188-
"""Write string safely, clipping to window bounds."""
187+
def safe_addstr(win, y, x, text, attr=0, *, _bounds=None):
188+
"""Write string safely, clipping to window bounds.
189+
190+
When *_bounds* is a ``(h, w)`` tuple the expensive ``getmaxyx()`` call and
191+
Mock-detection logic are skipped entirely. The rendering hot-path should
192+
always supply pre-computed bounds.
193+
"""
189194
if x < 0 or y < 0:
190195
return
191-
# Be defensive: some tests provide a Mock for `win.getmaxyx()` which may
192-
# return a Mock object rather than a (h, w) tuple. Try to coerce into a
193-
# tuple when possible and bail out gracefully otherwise.
194-
res = win.getmaxyx()
195-
if not isinstance(res, (list, tuple)):
196-
try:
197-
# If the returned object is itself callable (a Mock), call it.
198-
if callable(res):
199-
res = res()
200-
except _SAFE_ADDSTR_PROBE_ERRORS:
201-
res = None
196+
if _bounds is not None:
197+
h, w = _bounds
198+
else:
199+
# Slow path: probe getmaxyx with Mock-tolerance for test environments.
200+
res = win.getmaxyx()
202201
if not isinstance(res, (list, tuple)):
203-
# Try to extract common attributes as a last resort
204202
try:
205-
h = int(getattr(res, 'h', None) or getattr(res, 'rows', None) or getattr(res, 'height', None))
206-
w = int(getattr(res, 'w', None) or getattr(res, 'cols', None) or getattr(res, 'width', None))
207-
res = (h, w)
203+
if callable(res):
204+
res = res()
208205
except _SAFE_ADDSTR_PROBE_ERRORS:
209-
return
210-
h, w = res
206+
res = None
207+
if not isinstance(res, (list, tuple)):
208+
try:
209+
h = int(getattr(res, 'h', None) or getattr(res, 'rows', None) or getattr(res, 'height', None))
210+
w = int(getattr(res, 'w', None) or getattr(res, 'cols', None) or getattr(res, 'width', None))
211+
res = (h, w)
212+
except _SAFE_ADDSTR_PROBE_ERRORS:
213+
return
214+
h, w = res
211215
if y >= h or x >= w:
212216
return
213217
max_len = w - x
@@ -216,8 +220,6 @@ def safe_addstr(win, y, x, text, attr=0):
216220
try:
217221
win.addnstr(y, x, text, max_len, attr)
218222
except curses.error:
219-
# Some curses backends still reject writes that touch lower-right cell.
220-
# Retry with one cell less only for this call.
221223
if max_len <= 1:
222224
return
223225
try:

tests/test_action_runner.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ def __init__(self, x, y, w, h, show_hidden_default=False):
418418
app,
419419
self.actions_mod.AppAction.FILE_MANAGER,
420420
logger,
421-
version="0.9.2",
421+
version="0.9.3",
422422
)
423423

424424
spawned = app._spawn_window.call_args.args[0]
@@ -438,7 +438,7 @@ def __init__(self, x, y, w, h, show_hidden_default=False):
438438
app,
439439
self.actions_mod.AppAction.FILE_MANAGER,
440440
logger,
441-
version="0.9.2",
441+
version="0.9.3",
442442
)
443443

444444
def test_execute_notepad_spawns_window_with_offset(self):
@@ -472,7 +472,7 @@ def __init__(self, x, y, w, h, wrap_default=False):
472472
app,
473473
self.actions_mod.AppAction.NOTEPAD,
474474
logger,
475-
version="0.9.2",
475+
version="0.9.3",
476476
)
477477

478478
spawned = app._spawn_window.call_args.args[0]
@@ -514,7 +514,7 @@ def test_execute_image_viewer_spawns_image_window(self):
514514
app,
515515
self.actions_mod.AppAction.IMAGE_VIEWER,
516516
logger,
517-
version="0.9.2",
517+
version="0.9.3",
518518
)
519519

520520
app._next_window_offset.assert_called_once_with(14, 3)
@@ -531,7 +531,7 @@ def test_execute_trash_spawns_trash_window(self):
531531
app,
532532
self.actions_mod.AppAction.TRASH_BIN,
533533
logger,
534-
version="0.9.2",
534+
version="0.9.3",
535535
)
536536

537537
app._next_window_offset.assert_called_once_with(15, 4)
@@ -570,7 +570,7 @@ def test_execute_desktop_icon_manager_spawns_editor_window(self):
570570
app,
571571
self.actions_mod.AppAction.DESKTOP_ICON_MANAGER,
572572
logger,
573-
version="0.9.2",
573+
version="0.9.3",
574574
)
575575

576576
app._next_window_offset.assert_called_once_with(22, 6)
@@ -593,7 +593,7 @@ def test_execute_icons_app_spawns_icons_window(self):
593593
app,
594594
self.actions_mod.AppAction.ICONS,
595595
logger,
596-
version="0.9.2",
596+
version="0.9.3",
597597
)
598598

599599
app._next_window_offset.assert_called_once_with(22, 6)
@@ -616,7 +616,7 @@ def test_execute_menu_editor_spawns_editor_window(self):
616616
app,
617617
self.actions_mod.AppAction.MENU_EDITOR,
618618
logger,
619-
version="0.9.2",
619+
version="0.9.3",
620620
)
621621

622622
app._next_window_offset.assert_called_once_with(20, 5)
@@ -651,7 +651,7 @@ def test_execute_log_viewer_spawns_log_window(self):
651651
app,
652652
self.actions_mod.AppAction.LOG_VIEWER,
653653
logger,
654-
version="0.9.2",
654+
version="0.9.3",
655655
)
656656

657657
app._next_window_offset.assert_called_once_with(16, 4)
@@ -670,7 +670,7 @@ def test_execute_process_manager_spawns_proc_window(self):
670670
app,
671671
self.actions_mod.AppAction.PROCESS_MANAGER,
672672
logger,
673-
version="0.9.2",
673+
version="0.9.3",
674674
)
675675

676676
app._next_window_offset.assert_called_once_with(14, 3)
@@ -689,7 +689,7 @@ def test_execute_clock_calendar_spawns_clock_window(self):
689689
app,
690690
self.actions_mod.AppAction.CLOCK_CALENDAR,
691691
logger,
692-
version="0.9.2",
692+
version="0.9.3",
693693
)
694694

695695
app._next_window_offset.assert_called_once_with(30, 6)

tests/test_constants_module.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def tearDownClass(cls):
2525
sys.modules.pop("curses", None)
2626

2727
def test_package_exposes_version(self):
28-
self.assertEqual(self.package.__version__, "0.9.2")
28+
self.assertEqual(self.package.__version__, "0.9.3")
2929

3030
def test_icons_and_ascii_icons_are_aligned(self):
3131
self.assertEqual(len(self.constants.ICONS), len(self.constants.ICONS_ASCII))
@@ -143,7 +143,7 @@ def test_package_init_source_sets_version(self):
143143

144144
exec(compile(source, str(source_path), "exec"), namespace)
145145

146-
self.assertEqual(namespace.get("__version__"), "0.9.2")
146+
self.assertEqual(namespace.get("__version__"), "0.9.3")
147147

148148

149149
if __name__ == "__main__":

tests/test_entry_and_content.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -216,24 +216,24 @@ def test_package_version_matches_runtime_constant(self):
216216
app_version = match.group(1)
217217

218218
self.assertEqual(package.__version__, app_version)
219-
self.assertEqual(package.__version__, "0.9.2")
219+
self.assertEqual(package.__version__, "0.9.3")
220220

221221
def test_content_builders_include_expected_sections(self):
222222
with mock.patch.dict(sys.modules, {"curses": _fake_curses_module()}):
223223
sys.modules.pop("retrotui.utils", None)
224224
sys.modules.pop("retrotui.core.content", None)
225225
content = importlib.import_module("retrotui.core.content")
226-
welcome = content.build_welcome_content("0.9.2")
226+
welcome = content.build_welcome_content("0.9.3")
227227
help_text = content.build_help_message()
228228
settings = content.build_settings_content()
229229

230230
with mock.patch.object(content, "get_system_info", return_value=["OS: test", "CPU: test"]):
231-
about = content.build_about_message("0.9.2")
231+
about = content.build_about_message("0.9.3")
232232

233-
self.assertTrue(any("v0.9.2" in line for line in welcome))
233+
self.assertTrue(any("v0.9.3" in line for line in welcome))
234234
self.assertIn("Ctrl+Q", help_text)
235235
self.assertTrue(any("Theme:" in line for line in settings))
236-
self.assertIn("RetroTUI v0.9.2", about)
236+
self.assertIn("RetroTUI v0.9.3", about)
237237
self.assertIn("OS: test", about)
238238

239239

tests/test_package_init_runtime.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ def test_runtime_import_exposes_version(self):
2222
_purge_retrotui_modules()
2323
project_root = _ensure_project_root_on_path()
2424
package = importlib.import_module("retrotui")
25-
self.assertEqual(package.__version__, "0.9.2")
25+
self.assertEqual(package.__version__, "0.9.3")
2626
self.assertEqual(Path(package.__file__).resolve(), project_root / "retrotui" / "__init__.py")
2727

2828
def test_direct_import_executes_package_init(self):
2929
_purge_retrotui_modules()
3030
project_root = _ensure_project_root_on_path()
3131
import retrotui # noqa: PLC0415
3232

33-
self.assertEqual(retrotui.__version__, "0.9.2")
33+
self.assertEqual(retrotui.__version__, "0.9.3")
3434
self.assertEqual(Path(retrotui.__file__).resolve(), project_root / "retrotui" / "__init__.py")
3535

3636

0 commit comments

Comments
 (0)