belote-cli 2.5.2__tar.gz → 2.5.5__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.
- {belote_cli-2.5.2 → belote_cli-2.5.5}/CHANGELOG.md +21 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/DEVELOPMENT.md +1 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/PKG-INFO +1 -1
- {belote_cli-2.5.2 → belote_cli-2.5.5}/pyproject.toml +1 -1
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/__init__.py +1 -1
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/core/run_state.py +2 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/engine/round_driver.py +16 -12
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/jokers/trick_timing.py +2 -6
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/main.py +1 -1
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/game.py +1 -3
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/input.py +32 -5
- {belote_cli-2.5.2 → belote_cli-2.5.5}/.claude/settings.local.json +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/.gitignore +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/GRIMAUD Standard Playing-Cards-1898.png +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/LICENSE +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/README.md +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/scripts/benchmark.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/__init__.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/ai.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/ansi.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/__init__.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/core/__init__.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/core/economy.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/core/scoring.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/engine/__init__.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/engine/event_bus.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/engine/modifier_patch.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/__init__.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/base.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/jokers/__init__.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/jokers/contract.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/jokers/corrupted.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/jokers/economy.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/jokers/hand_comp.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/partner_jokers/__init__.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/partner_jokers/passive.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/partner_jokers/risky.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/partner_jokers/shaper.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/planets.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/registry.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/tarots.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/vouchers.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/partner/__init__.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/partner/partner_state.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/partner/personality.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/partner/trust.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/progression/__init__.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/progression/save.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/progression/unlocks.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/run/__init__.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/run/ante.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/run/boss.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/run/decks.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/run/shop.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/ui/__init__.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/ui/announce.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/ui/collection.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/ui/hud.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/ui/menu.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/ui/rules.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/ui/shop.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/ui/trust_bar.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/config.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/context.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/deck.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/gameflow.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/main.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/rules.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/scoring.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/stats.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/themes.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/ui/__init__.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/ui/announce.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/ui/menu.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/ui/prompts.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/ui/render.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/__init__.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/belatro/__init__.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/belatro/test_belatro.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/belatro/test_boss_modifiers_integration.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/belatro/test_collection_logic.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/belatro/test_deck_variants.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/belatro/test_partner_trust.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/belatro/test_progression.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/belatro/test_round_driver.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/test_ai.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/test_belote.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/test_extended.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/test_game_logic.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/test_gameflow.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/test_new_coverage.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/test_official_rules.py +0 -0
- {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/test_properties.py +0 -0
|
@@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.5.5] - 2026-05-03
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **BelAtro Run Loop**: Fixed multiple critical crashes, including a `TypeError` in `drive_round` due to signature mismatch and an `IndexError` when tricks were accessed prematurely.
|
|
12
|
+
- **Event Bus Integrity**: Fixed `TrickWonEvent` instantiation error by providing missing `trick_number` and `trump` context.
|
|
13
|
+
- **Run State Consistency**: Added missing `consumable_slots` to `BelAtroRun` to prevent crashes when applying certain Vouchers (e.g., Le Couteau).
|
|
14
|
+
- **Cache Clear Bug**: Fixed an `AttributeError` when clearing the legal cards cache by ensuring the implementation function is properly decorated with `@lru_cache`.
|
|
15
|
+
- **Input & HUD**: Fixed the `[I]` key mapping for the score overlay HUD and synchronized the Windows `KeyReader` to support all game-specific keys.
|
|
16
|
+
|
|
17
|
+
## [2.5.4] - 2026-05-03
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- **Missing read_timeout**: Fixed an `AttributeError` in the main menu by implementing the `read_timeout` method in the `KeyReader` class.
|
|
21
|
+
- **IndentationError in trick_timing.py**: Fixed a critical syntax error in the BelAtro items module.
|
|
22
|
+
- **LePremierSang Logic**: Fixed `LePremierSang` joker to correctly apply +2 Mult (additive) and track its active state.
|
|
23
|
+
|
|
24
|
+
## [2.5.3] - 2026-05-03
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- **IndentationError in game.py**: Fixed a critical syntax error that prevented the game from starting.
|
|
28
|
+
|
|
8
29
|
## [2.5.2] - 2026-05-02
|
|
9
30
|
|
|
10
31
|
### Fixed
|
|
@@ -12,6 +12,7 @@ from ..partner.partner_state import PartnerState
|
|
|
12
12
|
from .economy import Economy
|
|
13
13
|
|
|
14
14
|
MAX_JOKER_SLOTS = 5
|
|
15
|
+
DEFAULT_CONSUMABLE_SLOTS = 2
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
@dataclass
|
|
@@ -29,6 +30,7 @@ class BelAtroRun:
|
|
|
29
30
|
jokers: list[Joker] = field(default_factory=list)
|
|
30
31
|
vouchers: list[Voucher] = field(default_factory=list)
|
|
31
32
|
joker_slots: int = MAX_JOKER_SLOTS
|
|
33
|
+
consumable_slots: int = DEFAULT_CONSUMABLE_SLOTS
|
|
32
34
|
|
|
33
35
|
# ── Economy ────────────────────────────────────────────
|
|
34
36
|
economy: Economy = field(default_factory=Economy)
|
|
@@ -40,9 +40,18 @@ if TYPE_CHECKING:
|
|
|
40
40
|
class RoundUICallbacks(ABC):
|
|
41
41
|
"""Interface for UI interaction during a round."""
|
|
42
42
|
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def prompt_bid(self, state: GameState) -> Suit | None: ...
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def prompt_card(self, state: GameState) -> tuple[Card, GameState]: ...
|
|
48
|
+
|
|
43
49
|
@abstractmethod
|
|
44
50
|
def on_card_played(self, state: GameState, seat: Seat, card: Card) -> None: ...
|
|
45
51
|
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def on_trick_end(self, state: GameState, winner: Seat, points: int) -> None: ...
|
|
54
|
+
|
|
46
55
|
@abstractmethod
|
|
47
56
|
def on_round_end(self, breakdown: object) -> None: ...
|
|
48
57
|
|
|
@@ -103,12 +112,7 @@ def drive_round(
|
|
|
103
112
|
bid: Suit | None = None
|
|
104
113
|
|
|
105
114
|
if bidder == Seat.SOUTH:
|
|
106
|
-
|
|
107
|
-
# (main.py) handled South's choice or we default to Pass
|
|
108
|
-
# Actually in drive_round we might need a callback for human input
|
|
109
|
-
# but BelAtro drive_round is usually automated for testing or
|
|
110
|
-
# the UI loop handles it. For now, we'll assume South passes if not handled.
|
|
111
|
-
pass
|
|
115
|
+
bid = ui_callbacks.prompt_bid(state)
|
|
112
116
|
elif (
|
|
113
117
|
bidder == Seat.NORTH
|
|
114
118
|
and partner is not None
|
|
@@ -159,9 +163,7 @@ def drive_round(
|
|
|
159
163
|
card: Card | None = None
|
|
160
164
|
|
|
161
165
|
if player == Seat.SOUTH:
|
|
162
|
-
|
|
163
|
-
# In a truly automated drive_round, we might need a callback.
|
|
164
|
-
break # Wait for human in main loop
|
|
166
|
+
card, state = ui_callbacks.prompt_card(state)
|
|
165
167
|
else:
|
|
166
168
|
# AI Play
|
|
167
169
|
card = ai_players[player].decide_card(state)
|
|
@@ -172,7 +174,7 @@ def drive_round(
|
|
|
172
174
|
state = play_card(state, card)
|
|
173
175
|
ui_callbacks.on_card_played(state, player, card)
|
|
174
176
|
|
|
175
|
-
if is_last_in_trick:
|
|
177
|
+
if is_last_in_trick(state):
|
|
176
178
|
last_trick = state.completed_tricks[-1]
|
|
177
179
|
|
|
178
180
|
winner = trick_winner_seat(
|
|
@@ -203,11 +205,14 @@ def drive_round(
|
|
|
203
205
|
TrickWonEvent(
|
|
204
206
|
winner=winner,
|
|
205
207
|
cards=cards,
|
|
206
|
-
|
|
208
|
+
trick_number=len(state.completed_tricks),
|
|
207
209
|
is_last=is_last,
|
|
210
|
+
card_points=points,
|
|
211
|
+
trump=state.trump,
|
|
208
212
|
),
|
|
209
213
|
state
|
|
210
214
|
)
|
|
215
|
+
ui_callbacks.on_trick_end(state, winner, points)
|
|
211
216
|
|
|
212
217
|
# Round End / Scoring
|
|
213
218
|
if state.phase == Phase.SCORING:
|
|
@@ -228,7 +233,6 @@ def drive_round(
|
|
|
228
233
|
return state
|
|
229
234
|
|
|
230
235
|
|
|
231
|
-
@property
|
|
232
236
|
def is_last_in_trick(state: GameState) -> bool:
|
|
233
237
|
"""Helper to check if a trick just ended."""
|
|
234
238
|
return len(state.current_trick) == 0 and len(state.completed_tricks) > 0
|
|
@@ -20,7 +20,8 @@ class LePremierSang(Joker):
|
|
|
20
20
|
|
|
21
21
|
def on_trick_won(self, event: TrickWonEvent, state: dict[str, Any]) -> JokerResult | None:
|
|
22
22
|
if event.trick_number == 1 and event.winner == Seat.SOUTH:
|
|
23
|
-
|
|
23
|
+
state[f"{self.id}_active"] = True
|
|
24
|
+
return JokerResult(add_mult=2.0)
|
|
24
25
|
return None
|
|
25
26
|
|
|
26
27
|
|
|
@@ -71,9 +72,4 @@ class LExecuteur(Joker):
|
|
|
71
72
|
if event.is_last and event.winner == Seat.SOUTH:
|
|
72
73
|
return JokerResult(add_chips=40, times_mult=1.5)
|
|
73
74
|
return None
|
|
74
|
-
s_unlockable = True
|
|
75
75
|
|
|
76
|
-
def on_trick_won(self, event: TrickWonEvent, state: dict[str, Any]) -> JokerResult | None:
|
|
77
|
-
if event.is_last and event.winner == Seat.SOUTH:
|
|
78
|
-
return JokerResult(add_chips=40, times_mult=1.5)
|
|
79
|
-
return None
|
|
@@ -449,6 +449,7 @@ def clear_legal_cards_cache() -> None:
|
|
|
449
449
|
_trick_winner_seat_impl.cache_clear()
|
|
450
450
|
|
|
451
451
|
|
|
452
|
+
@lru_cache(maxsize=2048)
|
|
452
453
|
def _calculate_legal_cards_impl(
|
|
453
454
|
hand_ids: tuple[int, ...],
|
|
454
455
|
trump: Suit | None,
|
|
@@ -873,7 +874,4 @@ def sort_south_hand(state: GameState) -> GameState:
|
|
|
873
874
|
)
|
|
874
875
|
|
|
875
876
|
return replace(state, hands=tuple(new_hands), initial_hands=tuple(new_initial))
|
|
876
|
-
[Seat.SOUTH.value], state.trump
|
|
877
|
-
)
|
|
878
877
|
|
|
879
|
-
return replace(state, hands=tuple(new_hands), initial_hands=tuple(new_initial))
|
|
@@ -76,7 +76,7 @@ class _UnixKeyReader:
|
|
|
76
76
|
r, _, _ = select.select([sys.stdin], [], [], 0.01)
|
|
77
77
|
if not r:
|
|
78
78
|
return KeyEvent(Key.ESC)
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
next_byte = os.read(self._stdin_fd, 1)
|
|
81
81
|
if next_byte == b"[":
|
|
82
82
|
# Likely an arrow key
|
|
@@ -107,7 +107,7 @@ class _UnixKeyReader:
|
|
|
107
107
|
elif (byte & 0xF0) == 0xE0: n = 2
|
|
108
108
|
elif (byte & 0xF8) == 0xF0: n = 3
|
|
109
109
|
else: return KeyEvent(Key.ESC)
|
|
110
|
-
|
|
110
|
+
|
|
111
111
|
full_buf = bytes([byte])
|
|
112
112
|
while len(full_buf) < n + 1:
|
|
113
113
|
chunk = os.read(self._stdin_fd, n + 1 - len(full_buf))
|
|
@@ -133,13 +133,19 @@ class _UnixKeyReader:
|
|
|
133
133
|
return KeyEvent(Key.MUTE)
|
|
134
134
|
if ch.lower() == "t":
|
|
135
135
|
return KeyEvent(Key.HIST)
|
|
136
|
-
if ch.lower() == "v":
|
|
136
|
+
if ch.lower() == "i" or ch.lower() == "v":
|
|
137
137
|
return KeyEvent(Key.OVERLAY)
|
|
138
138
|
|
|
139
139
|
return KeyEvent(Key.CHAR, ch)
|
|
140
140
|
except Exception:
|
|
141
141
|
return KeyEvent(Key.ESC)
|
|
142
142
|
|
|
143
|
+
def read_timeout(self, timeout: float) -> KeyEvent | None:
|
|
144
|
+
"""Read a key with a timeout. Returns None if no key is pressed."""
|
|
145
|
+
r, _, _ = select.select([sys.stdin], [], [], timeout)
|
|
146
|
+
if r:
|
|
147
|
+
return self.read()
|
|
148
|
+
return None
|
|
143
149
|
|
|
144
150
|
def interruptible_sleep(seconds: float, reader: KeyReader | None = None) -> KeyEvent | None:
|
|
145
151
|
"""Sleep for some time, but return immediately if a key is pressed."""
|
|
@@ -160,7 +166,8 @@ class KeyReader:
|
|
|
160
166
|
|
|
161
167
|
def __enter__(self) -> KeyReader: ...
|
|
162
168
|
def __exit__(self, *args: Any) -> None: ...
|
|
163
|
-
def
|
|
169
|
+
def read(self) -> KeyEvent: ...
|
|
170
|
+
def read_timeout(self, timeout: float) -> KeyEvent | None: ...
|
|
164
171
|
|
|
165
172
|
|
|
166
173
|
if os.name == "nt":
|
|
@@ -172,7 +179,7 @@ if os.name == "nt":
|
|
|
172
179
|
def __exit__(self, *args: Any) -> None:
|
|
173
180
|
pass
|
|
174
181
|
|
|
175
|
-
def
|
|
182
|
+
def read(self) -> KeyEvent:
|
|
176
183
|
import msvcrt # type: ignore[import-not-found]
|
|
177
184
|
|
|
178
185
|
ch = msvcrt.getch()
|
|
@@ -195,6 +202,16 @@ if os.name == "nt":
|
|
|
195
202
|
return KeyEvent(Key.SPACE)
|
|
196
203
|
if ch.lower() == b"q":
|
|
197
204
|
return KeyEvent(Key.QUIT)
|
|
205
|
+
if ch.lower() == b"h":
|
|
206
|
+
return KeyEvent(Key.HELP)
|
|
207
|
+
if ch.lower() == b"s":
|
|
208
|
+
return KeyEvent(Key.SORT)
|
|
209
|
+
if ch.lower() == b"m":
|
|
210
|
+
return KeyEvent(Key.MUTE)
|
|
211
|
+
if ch.lower() == b"t":
|
|
212
|
+
return KeyEvent(Key.HIST)
|
|
213
|
+
if ch.lower() in (b"i", b"v"):
|
|
214
|
+
return KeyEvent(Key.OVERLAY)
|
|
198
215
|
|
|
199
216
|
try:
|
|
200
217
|
char = ch.decode("utf-8")
|
|
@@ -202,6 +219,16 @@ if os.name == "nt":
|
|
|
202
219
|
except Exception:
|
|
203
220
|
return KeyEvent(Key.ESC)
|
|
204
221
|
|
|
222
|
+
def read_timeout(self, timeout: float) -> KeyEvent | None:
|
|
223
|
+
import msvcrt # type: ignore[import-not-found]
|
|
224
|
+
|
|
225
|
+
start = time.time()
|
|
226
|
+
while time.time() - start < timeout:
|
|
227
|
+
if msvcrt.kbhit():
|
|
228
|
+
return self.read()
|
|
229
|
+
time.sleep(0.01)
|
|
230
|
+
return None
|
|
231
|
+
|
|
205
232
|
KeyReader = _WindowsKeyReader # type: ignore[misc, assignment]
|
|
206
233
|
else:
|
|
207
234
|
KeyReader = _UnixKeyReader
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|