typefaster-cli 0.1.3__tar.gz → 0.2.0__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.
Files changed (151) hide show
  1. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/.gitignore +1 -0
  2. typefaster_cli-0.2.0/PKG-INFO +145 -0
  3. typefaster_cli-0.2.0/README.md +113 -0
  4. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/__init__.py +1 -1
  5. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/net/token_store.py +1 -1
  6. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/services/daily_service.py +14 -3
  7. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/app.py +4 -0
  8. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/screens/_base.py +5 -1
  9. typefaster_cli-0.2.0/client/typefaster/ui/screens/account.py +193 -0
  10. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/screens/daily.py +8 -6
  11. typefaster_cli-0.2.0/client/typefaster/ui/screens/lobby_browser.py +150 -0
  12. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/screens/main_menu.py +18 -1
  13. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/screens/online_race.py +8 -4
  14. typefaster_cli-0.2.0/docs/DEEPDIVE.md +305 -0
  15. typefaster_cli-0.2.0/docs/INSTALL.md +64 -0
  16. typefaster_cli-0.2.0/docs/SECURITY-REVIEW.md +74 -0
  17. typefaster_cli-0.2.0/docs/deploy-oracle.md +89 -0
  18. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/pyproject.toml +1 -1
  19. typefaster_cli-0.2.0/scripts/deploy-oracle.sh +76 -0
  20. typefaster_cli-0.1.3/PKG-INFO +0 -156
  21. typefaster_cli-0.1.3/README.md +0 -124
  22. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/.dockerignore +0 -0
  23. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/.env.example +0 -0
  24. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  25. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  26. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  27. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  28. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/.github/dependabot.yml +0 -0
  29. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/.github/workflows/ci.yml +0 -0
  30. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/.github/workflows/codeql.yml +0 -0
  31. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/.github/workflows/release.yml +0 -0
  32. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/CONTRIBUTING.md +0 -0
  33. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/LICENSE +0 -0
  34. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/Makefile +0 -0
  35. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/SECURITY.md +0 -0
  36. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/Dockerfile +0 -0
  37. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/__main__.py +0 -0
  38. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/assets/__init__.py +0 -0
  39. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/assets/quotes.json +0 -0
  40. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/cli.py +0 -0
  41. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/domain/__init__.py +0 -0
  42. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/domain/anti_cheat.py +0 -0
  43. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/domain/calculators.py +0 -0
  44. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/domain/errors.py +0 -0
  45. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/domain/ghost.py +0 -0
  46. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/domain/models.py +0 -0
  47. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/domain/typing_engine.py +0 -0
  48. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/infra/__init__.py +0 -0
  49. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/infra/clock.py +0 -0
  50. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/infra/config.py +0 -0
  51. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/infra/db.py +0 -0
  52. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/infra/migrations.py +0 -0
  53. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/infra/paths.py +0 -0
  54. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/infra/quote_loader.py +0 -0
  55. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/infra/replay_store.py +0 -0
  56. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/infra/repository.py +0 -0
  57. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/infra/sqlite_repository.py +0 -0
  58. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/net/__init__.py +0 -0
  59. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/net/api.py +0 -0
  60. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/net/commands.py +0 -0
  61. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/services/__init__.py +0 -0
  62. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/services/container.py +0 -0
  63. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/services/ghost_service.py +0 -0
  64. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/services/profile_service.py +0 -0
  65. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/services/race_service.py +0 -0
  66. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/services/stats_service.py +0 -0
  67. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/__init__.py +0 -0
  68. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/online_app.py +0 -0
  69. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/screens/__init__.py +0 -0
  70. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/screens/help.py +0 -0
  71. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/screens/history.py +0 -0
  72. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/screens/leaderboard.py +0 -0
  73. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/screens/practice.py +0 -0
  74. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/screens/profile.py +0 -0
  75. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/screens/race.py +0 -0
  76. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/screens/results.py +0 -0
  77. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/screens/settings.py +0 -0
  78. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/screens/stats.py +0 -0
  79. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/theme.py +0 -0
  80. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/widgets/__init__.py +0 -0
  81. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/widgets/bigtext.py +0 -0
  82. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/widgets/live_stats.py +0 -0
  83. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/widgets/progress_bars.py +0 -0
  84. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/client/typefaster/ui/widgets/typing_field.py +0 -0
  85. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/docker-compose.yml +0 -0
  86. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/docs/RELEASING.md +0 -0
  87. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/docs/api-spec.md +0 -0
  88. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/docs/architecture.md +0 -0
  89. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/docs/deploy-fly.md +0 -0
  90. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/docs/deployment.md +0 -0
  91. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/docs/online-setup.md +0 -0
  92. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/docs/redis-schema.md +0 -0
  93. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/docs/roadmap.md +0 -0
  94. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/docs/sqlite-schema.md +0 -0
  95. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/docs/ui-design.md +0 -0
  96. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/docs/websocket-protocol.md +0 -0
  97. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/fly.toml +0 -0
  98. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/infra/README.md +0 -0
  99. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/infra/nginx/nginx.conf +0 -0
  100. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/infra/redis/redis.conf +0 -0
  101. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/packaging/homebrew/typefaster.rb +0 -0
  102. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/scripts/seed_quotes.py +0 -0
  103. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/Dockerfile +0 -0
  104. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/app/__init__.py +0 -0
  105. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/app/config.py +0 -0
  106. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/app/deps.py +0 -0
  107. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/app/logging_config.py +0 -0
  108. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/app/main.py +0 -0
  109. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/app/quotes.py +0 -0
  110. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/app/redis_keys.py +0 -0
  111. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/app/repositories.py +0 -0
  112. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/app/routers/__init__.py +0 -0
  113. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/app/routers/auth.py +0 -0
  114. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/app/routers/health.py +0 -0
  115. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/app/routers/leaderboards.py +0 -0
  116. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/app/routers/lobbies.py +0 -0
  117. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/app/routers/oauth.py +0 -0
  118. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/app/security.py +0 -0
  119. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/app/ws/__init__.py +0 -0
  120. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/app/ws/manager.py +0 -0
  121. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/pyproject.toml +0 -0
  122. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/tests/__init__.py +0 -0
  123. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/tests/conftest.py +0 -0
  124. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/tests/test_auth.py +0 -0
  125. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/tests/test_health_leaderboards.py +0 -0
  126. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/tests/test_lobbies.py +0 -0
  127. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/tests/test_oauth.py +0 -0
  128. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/tests/test_scoring_anticheat.py +0 -0
  129. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/server/tests/test_ws_race.py +0 -0
  130. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/shared/pyproject.toml +0 -0
  131. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/shared/typefaster_shared/__init__.py +0 -0
  132. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/shared/typefaster_shared/anti_cheat.py +0 -0
  133. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/shared/typefaster_shared/dto.py +0 -0
  134. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/shared/typefaster_shared/events.py +0 -0
  135. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/shared/typefaster_shared/scoring.py +0 -0
  136. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/tests/conftest.py +0 -0
  137. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/tests/fixtures/.gitkeep +0 -0
  138. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/tests/integration/test_container_and_cli.py +0 -0
  139. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/tests/integration/test_migrations.py +0 -0
  140. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/tests/integration/test_profile_stats.py +0 -0
  141. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/tests/integration/test_race_service.py +0 -0
  142. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/tests/integration/test_race_timing.py +0 -0
  143. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/tests/integration/test_sqlite_repository.py +0 -0
  144. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/tests/integration/test_ui_smoke.py +0 -0
  145. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/tests/unit/test_anti_cheat.py +0 -0
  146. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/tests/unit/test_calculators.py +0 -0
  147. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/tests/unit/test_config.py +0 -0
  148. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/tests/unit/test_ghost.py +0 -0
  149. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/tests/unit/test_net.py +0 -0
  150. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/tests/unit/test_quote_loader.py +0 -0
  151. {typefaster_cli-0.1.3 → typefaster_cli-0.2.0}/tests/unit/test_typing_engine.py +0 -0
@@ -30,3 +30,4 @@ typefaster.db*
30
30
  .vscode/
31
31
  .DS_Store
32
32
  .env
33
+ docs/MARKETING.md
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: typefaster-cli
3
+ Version: 0.2.0
4
+ Summary: A terminal-first typing game inspired by MonkeyType and TypeRacer.
5
+ Project-URL: Homepage, https://github.com/Anoshor/typefaster-cli
6
+ Project-URL: Repository, https://github.com/Anoshor/typefaster-cli
7
+ Author: Anoshor Paul
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: cli,game,monkeytype,terminal,tui,typeracer,typing,wpm
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Games/Entertainment
16
+ Requires-Python: >=3.11
17
+ Requires-Dist: httpx>=0.27
18
+ Requires-Dist: platformdirs>=4.2
19
+ Requires-Dist: rich>=13.7
20
+ Requires-Dist: textual>=0.60
21
+ Requires-Dist: typer>=0.12
22
+ Requires-Dist: websockets>=12.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: black>=24.0; extra == 'dev'
25
+ Requires-Dist: mypy>=1.10; extra == 'dev'
26
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
27
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.5; extra == 'dev'
30
+ Requires-Dist: textual-dev>=1.5; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # ⌨ TYPEFASTER
34
+
35
+ [![CI](https://github.com/Anoshor/typefaster-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/Anoshor/typefaster-cli/actions/workflows/ci.yml)
36
+ [![PyPI](https://img.shields.io/pypi/v/typefaster-cli)](https://pypi.org/project/typefaster-cli/)
37
+ [![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/downloads/)
38
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
39
+
40
+ A **terminal-first** typing game inspired by MonkeyType and TypeRacer — race
41
+ quotes, beat your ghost, climb leaderboards, and play **live multiplayer** with
42
+ friends. Pure TUI, no browser, no GUI.
43
+
44
+ ```bash
45
+ brew install Anoshor/typefaster/typefaster # or: pipx install typefaster-cli
46
+ typefaster
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Install
52
+
53
+ **Homebrew** (macOS / Linux)
54
+ ```bash
55
+ brew install Anoshor/typefaster/typefaster
56
+ ```
57
+
58
+ **pipx** (any OS with Python 3.11+) — fastest
59
+ ```bash
60
+ pipx install typefaster-cli
61
+ ```
62
+
63
+ Verify: `typefaster version`
64
+
65
+ ## Play (offline — no account, no internet)
66
+
67
+ Just run it:
68
+ ```bash
69
+ typefaster
70
+ ```
71
+ Keyboard-only menu:
72
+ - **Quick Race** — a fresh random quote each time; race your personal-best ghost.
73
+ - **Time Attack** — type for 30 / 60 / 120s (←/→ to change the duration inline).
74
+ - **Practice** — pick a mode/ghost.
75
+ - **Daily Challenge** — same quote for everyone each day, local leaderboard.
76
+ - **Stats / History / Profile / Leaderboard / Settings**.
77
+
78
+ Live WPM, accuracy, progress, and an animated ghost bar. Backspace corrects
79
+ mistakes (original errors still count, MonkeyType-style). All progress is saved
80
+ locally in SQLite.
81
+
82
+ Direct commands too:
83
+ ```bash
84
+ typefaster race # quote race
85
+ typefaster race --mode time --time 60
86
+ typefaster daily
87
+ typefaster stats | typefaster history
88
+ ```
89
+
90
+ ## Play online (multiplayer lobbies)
91
+
92
+ It works out of the box against the public server — **no setup**. From the main
93
+ menu pick **Account** to register/login, then **Play Online**:
94
+
95
+ ```text
96
+ Account → Register / Login (password, GitHub, or Google)
97
+ Play Online → ➕ Create a lobby → share the join code
98
+ → or type a friend's code + Enter to join
99
+ ```
100
+ In the waiting room press **R** to ready; the **server** runs the countdown,
101
+ sends everyone the same quote, shows live progress bars, and scores results
102
+ authoritatively (with anti-cheat). **Esc** leaves.
103
+
104
+ Prefer the CLI?
105
+ ```bash
106
+ typefaster register <name> # or: typefaster login --github / --google
107
+ typefaster lobby create --name "Friday" --time 60
108
+ typefaster lobby join ABC123
109
+ typefaster leaderboard global # global | daily | weekly
110
+ ```
111
+
112
+ ## Self-host the server (optional)
113
+
114
+ The game ships pointing at a public server, but you can run your own:
115
+ ```bash
116
+ git clone https://github.com/Anoshor/typefaster-cli && cd typefaster-cli
117
+ cp .env.example .env # set TYPEFASTER_JWT_SECRET
118
+ make up # Redis + FastAPI server on :8000 (Docker)
119
+ typefaster config set-server http://localhost:8000
120
+ ```
121
+ Deploy guides: [`docs/deploy-oracle.md`](docs/deploy-oracle.md) (free 24/7 VM) ·
122
+ [`docs/deploy-fly.md`](docs/deploy-fly.md) · TLS + hardening in
123
+ [`docs/SECURITY-REVIEW.md`](docs/SECURITY-REVIEW.md).
124
+
125
+ ## How it works
126
+
127
+ - **Client**: Python · Typer (CLI) · Textual + Rich (TUI) · SQLite (local
128
+ progress) · httpx + websockets (online).
129
+ - **Server**: FastAPI · WebSockets · Redis · Pydantic — **server-authoritative**
130
+ race timing and scoring.
131
+ - Deep dive: [`docs/DEEPDIVE.md`](docs/DEEPDIVE.md) · architecture:
132
+ [`docs/architecture.md`](docs/architecture.md).
133
+
134
+ ## Develop
135
+
136
+ ```bash
137
+ make install # editable install + dev deps
138
+ make play # run it
139
+ make check # ruff + mypy + pytest
140
+ ```
141
+ Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).
142
+
143
+ ## License
144
+
145
+ MIT — see [LICENSE](LICENSE). Crafted by **Anoshor Paul**.
@@ -0,0 +1,113 @@
1
+ # ⌨ TYPEFASTER
2
+
3
+ [![CI](https://github.com/Anoshor/typefaster-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/Anoshor/typefaster-cli/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/typefaster-cli)](https://pypi.org/project/typefaster-cli/)
5
+ [![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/downloads/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
7
+
8
+ A **terminal-first** typing game inspired by MonkeyType and TypeRacer — race
9
+ quotes, beat your ghost, climb leaderboards, and play **live multiplayer** with
10
+ friends. Pure TUI, no browser, no GUI.
11
+
12
+ ```bash
13
+ brew install Anoshor/typefaster/typefaster # or: pipx install typefaster-cli
14
+ typefaster
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Install
20
+
21
+ **Homebrew** (macOS / Linux)
22
+ ```bash
23
+ brew install Anoshor/typefaster/typefaster
24
+ ```
25
+
26
+ **pipx** (any OS with Python 3.11+) — fastest
27
+ ```bash
28
+ pipx install typefaster-cli
29
+ ```
30
+
31
+ Verify: `typefaster version`
32
+
33
+ ## Play (offline — no account, no internet)
34
+
35
+ Just run it:
36
+ ```bash
37
+ typefaster
38
+ ```
39
+ Keyboard-only menu:
40
+ - **Quick Race** — a fresh random quote each time; race your personal-best ghost.
41
+ - **Time Attack** — type for 30 / 60 / 120s (←/→ to change the duration inline).
42
+ - **Practice** — pick a mode/ghost.
43
+ - **Daily Challenge** — same quote for everyone each day, local leaderboard.
44
+ - **Stats / History / Profile / Leaderboard / Settings**.
45
+
46
+ Live WPM, accuracy, progress, and an animated ghost bar. Backspace corrects
47
+ mistakes (original errors still count, MonkeyType-style). All progress is saved
48
+ locally in SQLite.
49
+
50
+ Direct commands too:
51
+ ```bash
52
+ typefaster race # quote race
53
+ typefaster race --mode time --time 60
54
+ typefaster daily
55
+ typefaster stats | typefaster history
56
+ ```
57
+
58
+ ## Play online (multiplayer lobbies)
59
+
60
+ It works out of the box against the public server — **no setup**. From the main
61
+ menu pick **Account** to register/login, then **Play Online**:
62
+
63
+ ```text
64
+ Account → Register / Login (password, GitHub, or Google)
65
+ Play Online → ➕ Create a lobby → share the join code
66
+ → or type a friend's code + Enter to join
67
+ ```
68
+ In the waiting room press **R** to ready; the **server** runs the countdown,
69
+ sends everyone the same quote, shows live progress bars, and scores results
70
+ authoritatively (with anti-cheat). **Esc** leaves.
71
+
72
+ Prefer the CLI?
73
+ ```bash
74
+ typefaster register <name> # or: typefaster login --github / --google
75
+ typefaster lobby create --name "Friday" --time 60
76
+ typefaster lobby join ABC123
77
+ typefaster leaderboard global # global | daily | weekly
78
+ ```
79
+
80
+ ## Self-host the server (optional)
81
+
82
+ The game ships pointing at a public server, but you can run your own:
83
+ ```bash
84
+ git clone https://github.com/Anoshor/typefaster-cli && cd typefaster-cli
85
+ cp .env.example .env # set TYPEFASTER_JWT_SECRET
86
+ make up # Redis + FastAPI server on :8000 (Docker)
87
+ typefaster config set-server http://localhost:8000
88
+ ```
89
+ Deploy guides: [`docs/deploy-oracle.md`](docs/deploy-oracle.md) (free 24/7 VM) ·
90
+ [`docs/deploy-fly.md`](docs/deploy-fly.md) · TLS + hardening in
91
+ [`docs/SECURITY-REVIEW.md`](docs/SECURITY-REVIEW.md).
92
+
93
+ ## How it works
94
+
95
+ - **Client**: Python · Typer (CLI) · Textual + Rich (TUI) · SQLite (local
96
+ progress) · httpx + websockets (online).
97
+ - **Server**: FastAPI · WebSockets · Redis · Pydantic — **server-authoritative**
98
+ race timing and scoring.
99
+ - Deep dive: [`docs/DEEPDIVE.md`](docs/DEEPDIVE.md) · architecture:
100
+ [`docs/architecture.md`](docs/architecture.md).
101
+
102
+ ## Develop
103
+
104
+ ```bash
105
+ make install # editable install + dev deps
106
+ make play # run it
107
+ make check # ruff + mypy + pytest
108
+ ```
109
+ Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).
110
+
111
+ ## License
112
+
113
+ MIT — see [LICENSE](LICENSE). Crafted by **Anoshor Paul**.
@@ -1,3 +1,3 @@
1
1
  """TYPEFASTER-CLI — a terminal-first typing game."""
2
2
 
3
- __version__ = "0.1.3"
3
+ __version__ = "0.2.0"
@@ -18,7 +18,7 @@ class Session:
18
18
  # Default points at the public TYPEFASTER server so a fresh install can
19
19
  # register/play with zero config. Override with `typefaster config set-server`
20
20
  # (e.g. a local server or your own deployment).
21
- server_url: str = "https://typefaster-cli.fly.dev"
21
+ server_url: str = "https://140.245.248.113.sslip.io"
22
22
  token: str | None = None
23
23
  username: str | None = None
24
24
 
@@ -2,22 +2,33 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from datetime import date
5
+ from datetime import UTC, date, datetime
6
6
 
7
7
  from ..domain.models import DailyChallenge, RaceRecord
8
8
  from ..infra import quote_loader
9
9
  from ..infra.repository import Repository
10
10
 
11
11
 
12
+ def _utc_today() -> date:
13
+ """The daily challenge day in UTC.
14
+
15
+ Races are stored with a UTC ``started_at`` and the daily leaderboard filters
16
+ on that, so the day MUST be UTC too — otherwise (e.g. in IST) results land on
17
+ the wrong day and the challenge never updates. UTC also makes the daily
18
+ genuinely 'the same for everyone'.
19
+ """
20
+ return datetime.now(UTC).date()
21
+
22
+
12
23
  class DailyService:
13
24
  def __init__(self, repo: Repository) -> None:
14
25
  self._repo = repo
15
26
 
16
27
  def today(self, day: date | None = None) -> DailyChallenge:
17
- day = day or date.today()
28
+ day = day or _utc_today()
18
29
  quote = quote_loader.daily_quote(day)
19
30
  return self._repo.get_or_create_daily(day.isoformat(), quote)
20
31
 
21
32
  def leaderboard(self, day: date | None = None, limit: int = 20) -> list[RaceRecord]:
22
- day = day or date.today()
33
+ day = day or _utc_today()
23
34
  return self._repo.daily_leaderboard(day.isoformat(), limit=limit)
@@ -8,10 +8,12 @@ from ..domain.models import RaceResult
8
8
  from ..services.container import App as Services
9
9
  from ..services.container import build_app
10
10
  from ..services.race_service import RaceConfig, RaceSetup
11
+ from .screens.account import AccountScreen
11
12
  from .screens.daily import DailyScreen
12
13
  from .screens.help import HelpScreen
13
14
  from .screens.history import HistoryScreen
14
15
  from .screens.leaderboard import LeaderboardScreen
16
+ from .screens.lobby_browser import LobbyBrowserScreen
15
17
  from .screens.main_menu import MainMenu
16
18
  from .screens.practice import PracticeScreen
17
19
  from .screens.profile import ProfileScreen
@@ -30,6 +32,8 @@ _PANELS = {
30
32
  "leaderboard": LeaderboardScreen,
31
33
  "settings": SettingsScreen,
32
34
  "help": HelpScreen,
35
+ "account": AccountScreen,
36
+ "online": LobbyBrowserScreen,
33
37
  }
34
38
 
35
39
 
@@ -20,11 +20,15 @@ class PanelScreen(Screen[None]):
20
20
  with Vertical(id="panel-wrap"):
21
21
  yield Static(Text(self.title_text, justify="center"), id="title")
22
22
  with VerticalScroll():
23
- yield Static(self.body())
23
+ yield Static(self.body(), id="panel-body")
24
24
  yield Static("esc back", classes="dim")
25
25
 
26
26
  def body(self) -> RenderableType: # pragma: no cover - overridden
27
27
  return Text("")
28
28
 
29
+ def on_screen_resume(self) -> None:
30
+ # Re-query when we return here (e.g. after a race) so data is fresh.
31
+ self.query_one("#panel-body", Static).update(self.body())
32
+
29
33
  def action_back(self) -> None:
30
34
  self.app.pop_screen()
@@ -0,0 +1,193 @@
1
+ """In-TUI account screen: register, login (password / GitHub / Google), logout.
2
+
3
+ Actions are an OptionList (not single-letter keys, which would collide with the
4
+ username/password Inputs). Network calls (blocking httpx) run in worker threads
5
+ and post results back via ``call_from_thread``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import contextlib
11
+ import time
12
+ import webbrowser
13
+
14
+ from rich.text import Text
15
+ from textual.app import ComposeResult
16
+ from textual.containers import Vertical
17
+ from textual.screen import Screen
18
+ from textual.widgets import Input, OptionList, Static
19
+ from textual.widgets.option_list import Option
20
+
21
+ from ...net.api import ApiClient, ApiError
22
+ from ...net.token_store import Session
23
+
24
+ _ACTIONS = [
25
+ ("register", "Register (username + password)"),
26
+ ("login", "Login (username + password)"),
27
+ ("github", "Login with GitHub (browser)"),
28
+ ("google", "Login with Google (browser)"),
29
+ ("setserver", "Set server URL (from field above)"),
30
+ ("logout", "Logout"),
31
+ ]
32
+
33
+
34
+ class AccountScreen(Screen[None]):
35
+ BINDINGS = [("escape", "back", "Back")]
36
+
37
+ def compose(self) -> ComposeResult:
38
+ with Vertical(id="panel-wrap"):
39
+ yield Static(Text("ACCOUNT", justify="center"), id="title")
40
+ yield Static(self._status_text(), id="acct-status")
41
+ yield Input(placeholder="username", id="user")
42
+ yield Input(placeholder="password", password=True, id="pw")
43
+ yield Input(placeholder="server url", id="server", value=Session.load().server_url)
44
+ yield OptionList(*[Option(label, id=key) for key, label in _ACTIONS])
45
+ yield Static(
46
+ Text("Tab between fields · pick an action below · esc back", justify="center"),
47
+ classes="dim",
48
+ )
49
+ yield Static("", id="acct-msg")
50
+
51
+ def on_mount(self) -> None:
52
+ self.query_one("#user", Input).focus()
53
+
54
+ def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
55
+ match event.option.id:
56
+ case "register":
57
+ self._auth("register")
58
+ case "login":
59
+ self._auth("login")
60
+ case "github":
61
+ self._oauth("github")
62
+ case "google":
63
+ self._oauth("google")
64
+ case "setserver":
65
+ self._set_server()
66
+ case "logout":
67
+ self._logout()
68
+
69
+ # ── helpers ────────────────────────────────────────────────────────
70
+ def _status_text(self) -> Text:
71
+ s = Session.load()
72
+ who = s.username or "not logged in"
73
+ return Text.from_markup(f"server: [bold]{s.server_url}[/] · [bold cyan]{who}[/]")
74
+
75
+ def _msg(self, markup: str) -> None:
76
+ self.query_one("#acct-msg", Static).update(Text.from_markup(markup))
77
+
78
+ def _refresh_status(self) -> None:
79
+ self.query_one("#acct-status", Static).update(self._status_text())
80
+
81
+ def _field(self, wid: str) -> str:
82
+ return self.query_one(f"#{wid}", Input).value.strip()
83
+
84
+ # ── password auth ──────────────────────────────────────────────────
85
+ def _auth(self, kind: str) -> None:
86
+ user, pw = self._field("user"), self._field("pw")
87
+ if not user or not pw:
88
+ self._msg("[red]Enter a username and password first.[/]")
89
+ return
90
+ self._msg(f"[grey58]{kind.title()}ing…[/]")
91
+ self.run_worker(lambda: self._do_auth(kind, user, pw), thread=True, name="auth")
92
+
93
+ def _do_auth(self, kind: str, user: str, pw: str) -> None:
94
+ session = Session.load()
95
+ client = ApiClient(session)
96
+ try:
97
+ data = client.register(user, pw) if kind == "register" else client.login(user, pw)
98
+ session.token = data["access_token"]
99
+ session.username = data["username"]
100
+ session.save()
101
+ self.app.call_from_thread(self._on_auth_ok, session.username)
102
+ except ApiError as exc:
103
+ self.app.call_from_thread(self._msg, f"[red]{exc.detail}[/]")
104
+ finally:
105
+ client.close()
106
+
107
+ def _on_auth_ok(self, username: str) -> None:
108
+ self._refresh_status()
109
+ self._msg(f"[green]Logged in as[/] [bold]{username}[/]. Press esc, then 'Play Online'.")
110
+
111
+ # ── logout / server ────────────────────────────────────────────────
112
+ def _logout(self) -> None:
113
+ self.run_worker(self._do_logout, thread=True, name="logout")
114
+
115
+ def _do_logout(self) -> None:
116
+ session = Session.load()
117
+ client = ApiClient(session)
118
+ with contextlib.suppress(ApiError):
119
+ if session.logged_in:
120
+ client.logout()
121
+ client.close()
122
+ session.clear()
123
+ self.app.call_from_thread(self._after_logout)
124
+
125
+ def _after_logout(self) -> None:
126
+ self._refresh_status()
127
+ self._msg("[green]Logged out.[/]")
128
+
129
+ def _set_server(self) -> None:
130
+ url = self._field("server")
131
+ if not url:
132
+ return
133
+ session = Session.load()
134
+ if url.rstrip("/") != session.server_url:
135
+ session.token = None
136
+ session.username = None
137
+ session.server_url = url.rstrip("/")
138
+ session.save()
139
+ self._refresh_status()
140
+ self._msg(f"[green]Server set to[/] [bold]{session.server_url}[/]")
141
+
142
+ # ── OAuth device flow ──────────────────────────────────────────────
143
+ def _oauth(self, provider: str) -> None:
144
+ self._msg(f"[grey58]Starting {provider.title()} login…[/]")
145
+ self.run_worker(lambda: self._do_oauth(provider), thread=True, name="oauth")
146
+
147
+ def _do_oauth(self, provider: str) -> None:
148
+ session = Session.load()
149
+ client = ApiClient(session)
150
+ try:
151
+ start = client.oauth_start(provider)
152
+ except ApiError as exc:
153
+ self.app.call_from_thread(
154
+ self._msg, f"[red]{provider.title()} unavailable: {exc.detail}[/]"
155
+ )
156
+ client.close()
157
+ return
158
+
159
+ uri, code, device = start["verification_uri"], start["user_code"], start["device_code"]
160
+ interval = int(start.get("interval", 5))
161
+ deadline = time.time() + int(start.get("expires_in", 900))
162
+ self.app.call_from_thread(
163
+ self._msg,
164
+ f"Open [bold underline]{uri}[/] · enter code [bold cyan]{code}[/] · waiting…",
165
+ )
166
+ with contextlib.suppress(Exception):
167
+ webbrowser.open(uri)
168
+
169
+ try:
170
+ while time.time() < deadline:
171
+ time.sleep(interval)
172
+ try:
173
+ r = client.oauth_poll(provider, device)
174
+ except ApiError as exc:
175
+ self.app.call_from_thread(self._msg, f"[red]Login failed: {exc.detail}[/]")
176
+ return
177
+ st = r.get("status")
178
+ if st == "pending":
179
+ continue
180
+ if st == "slow_down":
181
+ interval += 5
182
+ continue
183
+ session.token = r["access_token"]
184
+ session.username = r["username"]
185
+ session.save()
186
+ self.app.call_from_thread(self._on_auth_ok, session.username)
187
+ return
188
+ self.app.call_from_thread(self._msg, "[red]Login timed out.[/]")
189
+ finally:
190
+ client.close()
191
+
192
+ def action_back(self) -> None:
193
+ self.app.pop_screen()
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from datetime import date
5
+ from datetime import UTC, datetime
6
6
 
7
7
  from rich.console import Group
8
8
  from rich.table import Table
@@ -23,15 +23,17 @@ class DailyScreen(Screen[None]):
23
23
  ]
24
24
 
25
25
  def compose(self) -> ComposeResult:
26
+ day = datetime.now(UTC).date().isoformat() # UTC — matches how results are filed
26
27
  with Vertical(id="panel-wrap"):
27
- yield Static(
28
- Text(f"DAILY CHALLENGE · {date.today().isoformat()}", justify="center"),
29
- id="title",
30
- )
28
+ yield Static(Text(f"DAILY CHALLENGE · {day} (UTC)", justify="center"), id="title")
31
29
  with VerticalScroll():
32
- yield Static(self._body())
30
+ yield Static(self._body(), id="panel-body")
33
31
  yield Static("⏎ play today's challenge esc back", classes="dim")
34
32
 
33
+ def on_screen_resume(self) -> None:
34
+ # Refresh best/attempts/leaderboard after returning from a race.
35
+ self.query_one("#panel-body", Static).update(self._body())
36
+
35
37
  def _body(self) -> Group:
36
38
  svc = self.app.services # type: ignore[attr-defined]
37
39
  challenge = svc.daily.today()