keystrike 1.1.0__py3-none-any.whl
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.
- keystrike/__init__.py +1 -0
- keystrike/__main__.py +9 -0
- keystrike/app.py +111 -0
- keystrike/application/__init__.py +0 -0
- keystrike/application/build_lesson.py +202 -0
- keystrike/application/learn_budget_use_cases.py +33 -0
- keystrike/application/prepare_practice.py +53 -0
- keystrike/application/session_use_cases.py +372 -0
- keystrike/application/settings_use_cases.py +74 -0
- keystrike/application/stats_use_cases.py +107 -0
- keystrike/application/sync_use_cases.py +41 -0
- keystrike/cli.py +95 -0
- keystrike/domain/__init__.py +0 -0
- keystrike/domain/aggregate.py +179 -0
- keystrike/domain/confidence.py +221 -0
- keystrike/domain/daily_learn.py +79 -0
- keystrike/domain/enums.py +29 -0
- keystrike/domain/generator.py +120 -0
- keystrike/domain/learn_order.py +37 -0
- keystrike/domain/markov.py +84 -0
- keystrike/domain/models.py +119 -0
- keystrike/domain/null_adapters.py +55 -0
- keystrike/domain/protocols.py +83 -0
- keystrike/domain/regression.py +97 -0
- keystrike/domain/session.py +38 -0
- keystrike/domain/sync_merge.py +156 -0
- keystrike/infrastructure/__init__.py +0 -0
- keystrike/infrastructure/aggregates_cache.py +62 -0
- keystrike/infrastructure/atomic_write.py +13 -0
- keystrike/infrastructure/bundled_layouts/__init__.py +0 -0
- keystrike/infrastructure/bundled_layouts/_grid.py +61 -0
- keystrike/infrastructure/bundled_layouts/colemak.py +3 -0
- keystrike/infrastructure/bundled_layouts/colemak_dh.py +11 -0
- keystrike/infrastructure/bundled_layouts/dvorak.py +3 -0
- keystrike/infrastructure/bundled_layouts/qwerty.py +3 -0
- keystrike/infrastructure/clock.py +9 -0
- keystrike/infrastructure/code_generators/__init__.py +0 -0
- keystrike/infrastructure/id_gen.py +22 -0
- keystrike/infrastructure/languages/__init__.py +32 -0
- keystrike/infrastructure/languages/data/en_markov.json.gz +0 -0
- keystrike/infrastructure/layout_repo.py +38 -0
- keystrike/infrastructure/layout_toml.py +98 -0
- keystrike/infrastructure/paths.py +56 -0
- keystrike/infrastructure/session_repo_jsonl.py +161 -0
- keystrike/infrastructure/settings_repo_toml.py +73 -0
- keystrike/infrastructure/sync_git.py +197 -0
- keystrike/presentation/__init__.py +0 -0
- keystrike/presentation/bindings.py +10 -0
- keystrike/presentation/screens/__init__.py +0 -0
- keystrike/presentation/screens/home.py +102 -0
- keystrike/presentation/screens/practice.py +170 -0
- keystrike/presentation/screens/settings.py +145 -0
- keystrike/presentation/screens/stats.py +179 -0
- keystrike/presentation/textual_app.py +119 -0
- keystrike/presentation/theme.py +7 -0
- keystrike/presentation/widgets/__init__.py +0 -0
- keystrike/presentation/widgets/hud.py +92 -0
- keystrike/presentation/widgets/kb_heatmap.py +125 -0
- keystrike/presentation/widgets/typing_area.py +71 -0
- keystrike-1.1.0.dist-info/METADATA +175 -0
- keystrike-1.1.0.dist-info/RECORD +64 -0
- keystrike-1.1.0.dist-info/WHEEL +4 -0
- keystrike-1.1.0.dist-info/entry_points.txt +2 -0
- keystrike-1.1.0.dist-info/licenses/LICENSE +21 -0
keystrike/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.1.0"
|
keystrike/__main__.py
ADDED
keystrike/app.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Composition root: wire all dependencies and return a ready-to-run KeystrikeApp."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from random import Random
|
|
5
|
+
|
|
6
|
+
from keystrike.application.build_lesson import BuildLesson
|
|
7
|
+
from keystrike.application.learn_budget_use_cases import GetDailyLearnBudget
|
|
8
|
+
from keystrike.application.prepare_practice import PreparePracticeSession
|
|
9
|
+
from keystrike.application.session_use_cases import (
|
|
10
|
+
FinishSession,
|
|
11
|
+
RecordKeystroke,
|
|
12
|
+
StartSession,
|
|
13
|
+
)
|
|
14
|
+
from keystrike.application.settings_use_cases import CycleLayout, UpdateSettings
|
|
15
|
+
from keystrike.application.stats_use_cases import GetHeatmap, GetHistory, RebuildAggregates
|
|
16
|
+
from keystrike.application.sync_use_cases import GetSyncStatus, InitSync, PullSync, PushSync
|
|
17
|
+
from keystrike.infrastructure.aggregates_cache import FileAggregatesCache
|
|
18
|
+
from keystrike.infrastructure.clock import MonotonicClock
|
|
19
|
+
from keystrike.infrastructure.id_gen import UlidGenerator
|
|
20
|
+
from keystrike.infrastructure.languages import BundledLanguageProvider
|
|
21
|
+
from keystrike.infrastructure.layout_repo import CompositeLayoutRepository
|
|
22
|
+
from keystrike.infrastructure.paths import default_paths, ensure_dirs
|
|
23
|
+
from keystrike.infrastructure.session_repo_jsonl import JsonlSessionRepository
|
|
24
|
+
from keystrike.infrastructure.settings_repo_toml import TomlSettingsRepository
|
|
25
|
+
from keystrike.infrastructure.sync_git import GitSyncGateway
|
|
26
|
+
from keystrike.presentation.textual_app import KeystrikeApp
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class SyncServices:
|
|
31
|
+
init: InitSync
|
|
32
|
+
pull: PullSync
|
|
33
|
+
push: PushSync
|
|
34
|
+
status: GetSyncStatus
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def build_sync() -> SyncServices:
|
|
38
|
+
paths = default_paths()
|
|
39
|
+
ensure_dirs(paths)
|
|
40
|
+
session_repo = JsonlSessionRepository(paths)
|
|
41
|
+
aggregates_cache = FileAggregatesCache(paths)
|
|
42
|
+
store = GitSyncGateway(paths)
|
|
43
|
+
rebuild = RebuildAggregates(repo=session_repo, cache=aggregates_cache)
|
|
44
|
+
return SyncServices(
|
|
45
|
+
init=InitSync(gateway=store),
|
|
46
|
+
pull=PullSync(gateway=store, rebuild=rebuild),
|
|
47
|
+
push=PushSync(gateway=store),
|
|
48
|
+
status=GetSyncStatus(gateway=store),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build() -> KeystrikeApp:
|
|
53
|
+
paths = default_paths()
|
|
54
|
+
ensure_dirs(paths)
|
|
55
|
+
|
|
56
|
+
clock = MonotonicClock()
|
|
57
|
+
id_gen = UlidGenerator()
|
|
58
|
+
session_repo = JsonlSessionRepository(paths)
|
|
59
|
+
settings_repo = TomlSettingsRepository(paths)
|
|
60
|
+
layout_repo = CompositeLayoutRepository(paths)
|
|
61
|
+
aggregates_cache = FileAggregatesCache(paths)
|
|
62
|
+
language_provider = BundledLanguageProvider()
|
|
63
|
+
|
|
64
|
+
start = StartSession(clock=clock, id_gen=id_gen)
|
|
65
|
+
record = RecordKeystroke(clock=clock, repo=session_repo)
|
|
66
|
+
finish = FinishSession(
|
|
67
|
+
clock=clock,
|
|
68
|
+
repo=session_repo,
|
|
69
|
+
aggregates_cache=aggregates_cache,
|
|
70
|
+
settings_repo=settings_repo,
|
|
71
|
+
layout_repo=layout_repo,
|
|
72
|
+
)
|
|
73
|
+
rebuild_aggregates = RebuildAggregates(repo=session_repo, cache=aggregates_cache)
|
|
74
|
+
get_heatmap = GetHeatmap(cache=aggregates_cache, settings_repo=settings_repo)
|
|
75
|
+
get_history = GetHistory(repo=session_repo)
|
|
76
|
+
get_daily_learn_budget = GetDailyLearnBudget(
|
|
77
|
+
clock=clock,
|
|
78
|
+
repo=session_repo,
|
|
79
|
+
settings_repo=settings_repo,
|
|
80
|
+
)
|
|
81
|
+
cycle_layout = CycleLayout(settings_repo=settings_repo, layout_repo=layout_repo)
|
|
82
|
+
update_settings = UpdateSettings(repo=settings_repo)
|
|
83
|
+
build_lesson = BuildLesson(
|
|
84
|
+
layout_repo=layout_repo,
|
|
85
|
+
aggregates_cache=aggregates_cache,
|
|
86
|
+
settings_repo=settings_repo,
|
|
87
|
+
language_provider=language_provider,
|
|
88
|
+
rng=Random(),
|
|
89
|
+
)
|
|
90
|
+
prepare_practice = PreparePracticeSession(
|
|
91
|
+
settings_repo=settings_repo,
|
|
92
|
+
layout_repo=layout_repo,
|
|
93
|
+
build_lesson=build_lesson,
|
|
94
|
+
get_daily_learn_budget=get_daily_learn_budget,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return KeystrikeApp(
|
|
98
|
+
clock=clock,
|
|
99
|
+
start=start,
|
|
100
|
+
record=record,
|
|
101
|
+
finish=finish,
|
|
102
|
+
settings_repo=settings_repo,
|
|
103
|
+
layout_repo=layout_repo,
|
|
104
|
+
prepare_practice=prepare_practice,
|
|
105
|
+
rebuild_aggregates=rebuild_aggregates,
|
|
106
|
+
get_heatmap=get_heatmap,
|
|
107
|
+
get_history=get_history,
|
|
108
|
+
cycle_layout=cycle_layout,
|
|
109
|
+
update_settings=update_settings,
|
|
110
|
+
get_daily_learn_budget=get_daily_learn_budget,
|
|
111
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""BuildLesson: the adaptive engine — figure out which keys are unlocked,
|
|
2
|
+
pick a focus key, and generate practice text for them.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from random import Random
|
|
10
|
+
|
|
11
|
+
from keystrike.domain.aggregate import transition_key
|
|
12
|
+
from keystrike.domain.confidence import (
|
|
13
|
+
compute_unlocked,
|
|
14
|
+
confidence_of,
|
|
15
|
+
focus_key_from_transition,
|
|
16
|
+
practice_weight,
|
|
17
|
+
review_urgency,
|
|
18
|
+
select_focus,
|
|
19
|
+
select_focus_transition,
|
|
20
|
+
target_ms_per_char,
|
|
21
|
+
transition_confidence_of,
|
|
22
|
+
transition_practice_weight,
|
|
23
|
+
)
|
|
24
|
+
from keystrike.domain.generator import AdaptiveGenerator
|
|
25
|
+
from keystrike.domain.learn_order import keyboard_order
|
|
26
|
+
from keystrike.domain.models import (
|
|
27
|
+
KeyStats,
|
|
28
|
+
Layout,
|
|
29
|
+
LessonKey,
|
|
30
|
+
LessonState,
|
|
31
|
+
Settings,
|
|
32
|
+
TransitionStats,
|
|
33
|
+
)
|
|
34
|
+
from keystrike.domain.protocols import (
|
|
35
|
+
AggregatesCache,
|
|
36
|
+
LanguageProvider,
|
|
37
|
+
LayoutRepository,
|
|
38
|
+
SettingsRepository,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
WORD_COUNT = 12
|
|
42
|
+
_CONFIDENCE_GOOD = 1.0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _focus_reason(
|
|
46
|
+
focus: int,
|
|
47
|
+
stats: dict[int, KeyStats],
|
|
48
|
+
target: float,
|
|
49
|
+
now: float,
|
|
50
|
+
) -> str | None:
|
|
51
|
+
key_stats = stats.get(focus)
|
|
52
|
+
urgency = review_urgency(key_stats.last_seen if key_stats else 0.0, now)
|
|
53
|
+
confidence = confidence_of(focus, stats, target)
|
|
54
|
+
if urgency > 0 and confidence >= _CONFIDENCE_GOOD:
|
|
55
|
+
return "review"
|
|
56
|
+
if confidence < _CONFIDENCE_GOOD:
|
|
57
|
+
return "weak"
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _focus_reason_transition(
|
|
62
|
+
prev_cp: int,
|
|
63
|
+
next_cp: int,
|
|
64
|
+
transitions: dict[str, TransitionStats],
|
|
65
|
+
target: float,
|
|
66
|
+
now: float,
|
|
67
|
+
) -> str | None:
|
|
68
|
+
pair = chr(prev_cp) + chr(next_cp)
|
|
69
|
+
t_stats = transitions.get(transition_key(prev_cp, next_cp))
|
|
70
|
+
urgency = review_urgency(t_stats.last_seen if t_stats else 0.0, now)
|
|
71
|
+
confidence = transition_confidence_of(prev_cp, next_cp, transitions, target)
|
|
72
|
+
if urgency > 0 and confidence >= _CONFIDENCE_GOOD:
|
|
73
|
+
return f"{pair} review transition"
|
|
74
|
+
if confidence < _CONFIDENCE_GOOD:
|
|
75
|
+
return f"{pair} weak transition"
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(slots=True)
|
|
80
|
+
class Lesson:
|
|
81
|
+
text: str
|
|
82
|
+
state: LessonState
|
|
83
|
+
urgency: dict[int, float]
|
|
84
|
+
focus_reason: str | None
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def focus_key(self) -> int:
|
|
88
|
+
return next(k.codepoint for k in self.state.keys if k.is_focus)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def heatmap(self) -> dict[int, float]:
|
|
92
|
+
return {k.codepoint: k.confidence for k in self.state.keys}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _lesson_progress(
|
|
96
|
+
layout_name: str,
|
|
97
|
+
layout: Layout,
|
|
98
|
+
stats: dict[int, KeyStats],
|
|
99
|
+
settings: Settings,
|
|
100
|
+
now: float,
|
|
101
|
+
*,
|
|
102
|
+
transitions: dict[str, TransitionStats] | None = None,
|
|
103
|
+
) -> tuple[tuple[int, ...], int, LessonState, tuple[int, int] | None]:
|
|
104
|
+
target = target_ms_per_char(settings.target_speed_cpm)
|
|
105
|
+
order = keyboard_order(layout)
|
|
106
|
+
unlocked = compute_unlocked(order, settings.alphabet_size, stats, target)
|
|
107
|
+
focus_bigram = (
|
|
108
|
+
select_focus_transition(unlocked, transitions, target, now)
|
|
109
|
+
if transitions else None
|
|
110
|
+
)
|
|
111
|
+
if focus_bigram is not None:
|
|
112
|
+
focus = focus_key_from_transition(*focus_bigram)
|
|
113
|
+
else:
|
|
114
|
+
focus = select_focus(unlocked, stats, target, now)
|
|
115
|
+
|
|
116
|
+
keys = tuple(
|
|
117
|
+
LessonKey(
|
|
118
|
+
codepoint=cp,
|
|
119
|
+
unlocked=True,
|
|
120
|
+
confidence=confidence_of(cp, stats, target),
|
|
121
|
+
is_focus=(cp == focus),
|
|
122
|
+
)
|
|
123
|
+
for cp in unlocked
|
|
124
|
+
)
|
|
125
|
+
state = LessonState(
|
|
126
|
+
layout=layout_name,
|
|
127
|
+
keys=keys,
|
|
128
|
+
alphabet_size=settings.alphabet_size,
|
|
129
|
+
target_speed_cpm=settings.target_speed_cpm,
|
|
130
|
+
)
|
|
131
|
+
return unlocked, focus, state, focus_bigram
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(slots=True)
|
|
135
|
+
class BuildLesson:
|
|
136
|
+
layout_repo: LayoutRepository
|
|
137
|
+
aggregates_cache: AggregatesCache
|
|
138
|
+
settings_repo: SettingsRepository
|
|
139
|
+
language_provider: LanguageProvider
|
|
140
|
+
rng: Random
|
|
141
|
+
|
|
142
|
+
def __call__(self, layout_name: str) -> Lesson:
|
|
143
|
+
settings = self.settings_repo.load()
|
|
144
|
+
layout = self.layout_repo.get(layout_name)
|
|
145
|
+
aggregates = self.aggregates_cache.get(layout_name)
|
|
146
|
+
stats = aggregates.keys if aggregates else {}
|
|
147
|
+
transitions = aggregates.transitions if aggregates else {}
|
|
148
|
+
now = time.time()
|
|
149
|
+
unlocked, focus, state, focus_bigram = _lesson_progress(
|
|
150
|
+
layout_name, layout, stats, settings, now, transitions=transitions,
|
|
151
|
+
)
|
|
152
|
+
target = target_ms_per_char(settings.target_speed_cpm)
|
|
153
|
+
|
|
154
|
+
table = self.language_provider.transitions(settings.lang)
|
|
155
|
+
generator = AdaptiveGenerator(table=table, rng=self.rng)
|
|
156
|
+
alphabet_chars = frozenset(chr(cp) for cp in unlocked)
|
|
157
|
+
char_weights = {
|
|
158
|
+
chr(k.codepoint): practice_weight(
|
|
159
|
+
k.confidence,
|
|
160
|
+
urgency=review_urgency(
|
|
161
|
+
stats[k.codepoint].last_seen if k.codepoint in stats else 0.0, now,
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
for k in state.keys
|
|
165
|
+
}
|
|
166
|
+
transition_weights = {
|
|
167
|
+
transition_key(prev, nxt): transition_practice_weight(
|
|
168
|
+
transition_confidence_of(prev, nxt, transitions, target),
|
|
169
|
+
urgency=review_urgency(
|
|
170
|
+
transitions[transition_key(prev, nxt)].last_seen
|
|
171
|
+
if transition_key(prev, nxt) in transitions else 0.0,
|
|
172
|
+
now,
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
for prev in unlocked
|
|
176
|
+
for nxt in unlocked
|
|
177
|
+
}
|
|
178
|
+
text = generator.generate_lesson(
|
|
179
|
+
alphabet_chars,
|
|
180
|
+
chr(focus),
|
|
181
|
+
word_count=WORD_COUNT,
|
|
182
|
+
char_weights=char_weights,
|
|
183
|
+
layout=layout,
|
|
184
|
+
transition_weights=transition_weights,
|
|
185
|
+
focus_bigram=focus_bigram,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
urgency = {
|
|
189
|
+
cp: review_urgency(stats[cp].last_seen if cp in stats else 0.0, now)
|
|
190
|
+
for cp in unlocked
|
|
191
|
+
}
|
|
192
|
+
if focus_bigram is not None:
|
|
193
|
+
prev_cp, next_cp = focus_bigram
|
|
194
|
+
reason = _focus_reason_transition(prev_cp, next_cp, transitions, target, now)
|
|
195
|
+
else:
|
|
196
|
+
reason = _focus_reason(focus, stats, target, now)
|
|
197
|
+
return Lesson(
|
|
198
|
+
text=text,
|
|
199
|
+
state=state,
|
|
200
|
+
urgency=urgency,
|
|
201
|
+
focus_reason=reason,
|
|
202
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Use cases for the daily adaptive (learn) mode time budget."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime as dt
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from keystrike.domain.daily_learn import (
|
|
9
|
+
DailyLearnBudget,
|
|
10
|
+
compute_daily_learn_budget,
|
|
11
|
+
daily_learn_duration_ns,
|
|
12
|
+
session_local_date,
|
|
13
|
+
)
|
|
14
|
+
from keystrike.domain.protocols import Clock, SessionRepository, SettingsRepository
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(slots=True)
|
|
18
|
+
class GetDailyLearnBudget:
|
|
19
|
+
clock: Clock
|
|
20
|
+
repo: SessionRepository
|
|
21
|
+
settings_repo: SettingsRepository
|
|
22
|
+
tz: dt.tzinfo | None = None
|
|
23
|
+
|
|
24
|
+
def __call__(self, *, extra_ns: int = 0) -> DailyLearnBudget:
|
|
25
|
+
tz = self.tz or dt.datetime.now().astimezone().tzinfo or dt.UTC
|
|
26
|
+
today = session_local_date(self.clock.wall_epoch(), tz)
|
|
27
|
+
completed_ns = daily_learn_duration_ns(self.repo.iter_all_headers(), today, tz=tz)
|
|
28
|
+
limit_minutes = self.settings_repo.load().learn_daily_minutes
|
|
29
|
+
return compute_daily_learn_budget(
|
|
30
|
+
completed_ns=completed_ns,
|
|
31
|
+
limit_minutes=limit_minutes,
|
|
32
|
+
extra_ns=extra_ns,
|
|
33
|
+
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""PreparePracticeSession: build SessionPrep for adaptive practice."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from keystrike.application.build_lesson import BuildLesson
|
|
9
|
+
from keystrike.domain.enums import Mode
|
|
10
|
+
from keystrike.domain.models import Layout
|
|
11
|
+
from keystrike.domain.protocols import (
|
|
12
|
+
DailyLearnBudgetProvider,
|
|
13
|
+
LayoutRepository,
|
|
14
|
+
SettingsRepository,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
PrepareNextSession = Callable[[], "SessionPrep | None"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class SessionPrep:
|
|
22
|
+
target_text: str
|
|
23
|
+
layout: str
|
|
24
|
+
mode: Mode
|
|
25
|
+
focus_key: int | None
|
|
26
|
+
focus_reason: str | None
|
|
27
|
+
layout_obj: Layout | None
|
|
28
|
+
lesson_heatmap: dict[int, float] | None
|
|
29
|
+
lesson_urgency: dict[int, float] | None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True)
|
|
33
|
+
class PreparePracticeSession:
|
|
34
|
+
settings_repo: SettingsRepository
|
|
35
|
+
layout_repo: LayoutRepository
|
|
36
|
+
build_lesson: BuildLesson
|
|
37
|
+
get_daily_learn_budget: DailyLearnBudgetProvider
|
|
38
|
+
|
|
39
|
+
def __call__(self) -> SessionPrep | None:
|
|
40
|
+
if self.get_daily_learn_budget().limit_reached:
|
|
41
|
+
return None
|
|
42
|
+
settings = self.settings_repo.load()
|
|
43
|
+
lesson = self.build_lesson(settings.layout)
|
|
44
|
+
return SessionPrep(
|
|
45
|
+
target_text=lesson.text,
|
|
46
|
+
layout=settings.layout,
|
|
47
|
+
mode=Mode.ADAPTIVE,
|
|
48
|
+
focus_key=lesson.focus_key,
|
|
49
|
+
focus_reason=lesson.focus_reason,
|
|
50
|
+
layout_obj=self.layout_repo.get(settings.layout),
|
|
51
|
+
lesson_heatmap=lesson.heatmap,
|
|
52
|
+
lesson_urgency=lesson.urgency,
|
|
53
|
+
)
|