ui/curses: wait for keys instead of polling

The main loop slept 100 ms whenever no key was pending, so keys took
up to a tick to register and every screen woke ten times a second.
Each view now declares how often it needs idle(): the game view keeps
its 100 ms tick, while menus wait for a key. getch() blocks for that
long, so keys register at once, and over 15 s in the roster menu the
process goes from about 300 wake-ups to 6.
This commit is contained in:
Dash committed 2026-10-17 19:57:28 +02:00
1 parent c9bce751ac
commit 9ceda9b873
3 files changed
+14 -4

No files matched your search

+9 -4
View File
@@ -2,7 +2,6 @@ import argparse
import curses import curses
import os import os
import signal import signal
import time
from pathlib import Path from pathlib import Path
from types import FrameType from types import FrameType
@@ -54,7 +53,6 @@ class CursesUserInterface(BaseUserInterface):
super().__init__(roster, player, args, data) super().__init__(roster, player, args, data)
os.environ.setdefault("ESCDELAY", "25") os.environ.setdefault("ESCDELAY", "25")
self._screen = curses.initscr() self._screen = curses.initscr()
self._screen.nodelay(True)
self._screen.keypad(True) self._screen.keypad(True)
curses.noecho() curses.noecho()
curses.curs_set(0) curses.curs_set(0)
@@ -87,6 +85,7 @@ class CursesUserInterface(BaseUserInterface):
self._roster_view.on_quit += self._quit self._roster_view.on_quit += self._quit
self._view: BaseView = self._roster_view self._view: BaseView = self._roster_view
self._view.start() self._view.start()
self._wait_for_keys()
def run(self) -> None: def run(self) -> None:
try: try:
@@ -98,8 +97,7 @@ class CursesUserInterface(BaseUserInterface):
while True: while True:
key = self._screen.getch() key = self._screen.getch()
if key == curses.ERR: if key == curses.ERR: # the view's idle interval passed
time.sleep(0.1)
self._view.idle() self._view.idle()
continue continue
@@ -250,6 +248,13 @@ class CursesUserInterface(BaseUserInterface):
self._view.stop() self._view.stop()
self._view = view self._view = view
self._view.start() self._view.start()
self._wait_for_keys()
def _wait_for_keys(self) -> None:
# getch() blocks until a key comes or the view's idle interval
# passes, so menus sit still and keys never wait for a tick.
interval = self._view.idle_interval
self._screen.timeout(-1 if interval is None else interval)
def _create_character( def _create_character(
self, self,
+3
View File
@@ -2,6 +2,9 @@ import curses
class BaseView: class BaseView:
# How often idle() runs, in milliseconds; None waits for a key.
idle_interval: int | None = None
def __init__(self, screen: curses.window) -> None: def __init__(self, screen: curses.window) -> None:
self.screen = screen self.screen = screen
@@ -42,6 +42,8 @@ class PageScrollable(Protocol):
class GameView(BaseView): class GameView(BaseView):
idle_interval = 100
def __init__( def __init__(
self, self,
screen: curses.window, screen: curses.window,