mirror of
https://github.com/rr-/pq-cli.git
synced 2026-11-12 22:40:36 -07:00
Curses screens, windows and pads are typed as curses.window, and the widgets that may get no parent window take curses.window | None. - EventHandler is generic over a ParamSpec, so each handler declares the arguments it passes, and MenuView is generic over the item its on_confirm hands out. Choice callbacks take no arguments. - Scrollable is generic over its items, which DataTable and ListBox fix to tuple[str, str] and str. - random.choice() and choice_low() are generic over the sequence, so the casts at their call sites are gone. BasicUserInterface.menu() and util.first() are generic too. - Signal callbacks and their payloads are Callable[..., object] and object, since emit() only forwards them. - JSON values in game_data are object and narrowed by isinstance, and data files are Traversable. - Signal handlers take an int and a FrameType | None, and list_players() takes a TextIO. - The focus_standout() context managers return Generator[None, None, None]. Any remains in three places: Player.__setstate__, whose pickled __dict__ holds values of every type; Focusable.__init__, a mixin that passes *args and **kwargs through to the next class; and the values load_game_data() collects, whose element type depends on the field each one fills.
71 lines
2.2 KiB
Python
71 lines
2.2 KiB
Python
import curses
|
|
import datetime
|
|
|
|
from pqcli.lingo import format_timespan
|
|
from pqcli.ui.curses.colors import COLOR_PROGRESSBAR, has_colors
|
|
from pqcli.ui.curses.widgets.base import WindowWrapper
|
|
|
|
|
|
class ProgressBar(WindowWrapper):
|
|
def __init__(
|
|
self,
|
|
parent: curses.window | None,
|
|
h: int,
|
|
w: int,
|
|
y: int,
|
|
x: int,
|
|
show_time: bool,
|
|
) -> None:
|
|
super().__init__(parent, h, w, y, x)
|
|
|
|
self._cur_pos = 0.0
|
|
self._max_pos = 1.0
|
|
|
|
self._show_time = show_time
|
|
self._last_tick: tuple[datetime.datetime, float] | None = None
|
|
|
|
@property
|
|
def time_left(self) -> datetime.timedelta | None:
|
|
if self._last_tick is None:
|
|
return None
|
|
time_then, pos_then = self._last_tick
|
|
time_now, pos_now = datetime.datetime.now(), self._cur_pos
|
|
div = (time_now - time_then).total_seconds()
|
|
if div == 0:
|
|
return None
|
|
speed = (pos_now - pos_then) / div
|
|
if not speed:
|
|
return None
|
|
return datetime.timedelta(seconds=(self._max_pos - pos_now) / speed)
|
|
|
|
def set_position(self, cur_pos: float, max_pos: float) -> None:
|
|
if not self._win:
|
|
return
|
|
self._win.erase()
|
|
|
|
if self._last_tick is None or cur_pos == 0 or max_pos != self._max_pos:
|
|
self._last_tick = (datetime.datetime.now(), cur_pos)
|
|
self._cur_pos = cur_pos
|
|
self._max_pos = max_pos
|
|
|
|
text = f"{cur_pos / max_pos:.02%}"
|
|
if self.time_left and self._show_time:
|
|
text += f" ({format_timespan(self.time_left)})"
|
|
|
|
x = max(0, (self.getmaxyx()[1] - len(text)) // 2)
|
|
self._win.addnstr(0, x, text, min(len(text), self.getmaxyx()[1] - 1))
|
|
x = int(cur_pos * self.getmaxyx()[1] // max_pos)
|
|
if x > 0:
|
|
self._win.chgat(
|
|
0,
|
|
0,
|
|
(
|
|
curses.color_pair(COLOR_PROGRESSBAR)
|
|
if has_colors()
|
|
else curses.A_REVERSE
|
|
),
|
|
)
|
|
if x < self.getmaxyx()[1]:
|
|
self._win.chgat(0, x, curses.A_NORMAL)
|
|
self._win.noutrefresh()
|