cct-cli 0.7.9.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.
Files changed (325) hide show
  1. calc_terminal/__init__.py +14 -0
  2. calc_terminal/__main__.py +14 -0
  3. calc_terminal/activity.py +1334 -0
  4. calc_terminal/agent.py +3387 -0
  5. calc_terminal/agent_runtime.py +519 -0
  6. calc_terminal/ai_context.py +447 -0
  7. calc_terminal/ai_modes.py +752 -0
  8. calc_terminal/ai_personalization.py +286 -0
  9. calc_terminal/ai_preview_feedback.py +213 -0
  10. calc_terminal/aicore.py +2572 -0
  11. calc_terminal/anim.py +367 -0
  12. calc_terminal/app.py +3685 -0
  13. calc_terminal/art.py +639 -0
  14. calc_terminal/atomsim.py +368 -0
  15. calc_terminal/attachments.py +743 -0
  16. calc_terminal/benchmark_system.py +414 -0
  17. calc_terminal/browser/__init__.py +36 -0
  18. calc_terminal/browser/browser_state.py +346 -0
  19. calc_terminal/browser/devserver.py +176 -0
  20. calc_terminal/browser/engine.py +494 -0
  21. calc_terminal/browser/navigation.py +84 -0
  22. calc_terminal/browser/preview.py +429 -0
  23. calc_terminal/browser/preview_entry.py +95 -0
  24. calc_terminal/browser/project_detector.py +144 -0
  25. calc_terminal/browser/server.py +449 -0
  26. calc_terminal/browser/state.py +75 -0
  27. calc_terminal/browser/watcher.py +99 -0
  28. calc_terminal/browser_gui/__init__.py +1 -0
  29. calc_terminal/browser_gui/__main__.py +3 -0
  30. calc_terminal/browser_gui/launcher.py +173 -0
  31. calc_terminal/browser_gui/playwright_browser.py +117 -0
  32. calc_terminal/browser_gui/qt_browser.py +1501 -0
  33. calc_terminal/browser_gui/webview_browser.py +57 -0
  34. calc_terminal/capabilities/__init__.py +35 -0
  35. calc_terminal/capabilities/adapters/__init__.py +33 -0
  36. calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
  37. calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
  38. calc_terminal/capabilities/adapters/filesystem.py +206 -0
  39. calc_terminal/capabilities/adapters/git_adapter.py +202 -0
  40. calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
  41. calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
  42. calc_terminal/capabilities/adapters/platforms.py +200 -0
  43. calc_terminal/capabilities/adapters/python_exec.py +93 -0
  44. calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
  45. calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
  46. calc_terminal/capabilities/adapters/structural_bio.py +161 -0
  47. calc_terminal/capabilities/adapters/terminal.py +99 -0
  48. calc_terminal/capabilities/bus.py +178 -0
  49. calc_terminal/capabilities/discovery.py +207 -0
  50. calc_terminal/capabilities/schema.py +221 -0
  51. calc_terminal/cat.ico +0 -0
  52. calc_terminal/cat_browser.py +2018 -0
  53. calc_terminal/chat_store.py +703 -0
  54. calc_terminal/cli.py +1178 -0
  55. calc_terminal/code_editor.py +640 -0
  56. calc_terminal/collaboration.py +723 -0
  57. calc_terminal/commands_data.py +139 -0
  58. calc_terminal/compatibility_engine.py +352 -0
  59. calc_terminal/compute/__init__.py +31 -0
  60. calc_terminal/compute/fabric.py +350 -0
  61. calc_terminal/config.py +227 -0
  62. calc_terminal/core/__init__.py +41 -0
  63. calc_terminal/core/checkpoint.py +156 -0
  64. calc_terminal/core/input/__init__.py +45 -0
  65. calc_terminal/core/mode_registry.py +300 -0
  66. calc_terminal/core/project_graph.py +172 -0
  67. calc_terminal/core/recovery.py +129 -0
  68. calc_terminal/core/security_layer.py +112 -0
  69. calc_terminal/core/task_graph.py +202 -0
  70. calc_terminal/core/unified_runtime.py +184 -0
  71. calc_terminal/core/verification.py +257 -0
  72. calc_terminal/customization.py +1566 -0
  73. calc_terminal/derivations.py +153 -0
  74. calc_terminal/device_control.py +263 -0
  75. calc_terminal/diagnostics/__init__.py +27 -0
  76. calc_terminal/diagnostics/doctor_engine.py +382 -0
  77. calc_terminal/diagnostics/self_test.py +247 -0
  78. calc_terminal/doctor.py +519 -0
  79. calc_terminal/easter_eggs.py +274 -0
  80. calc_terminal/editor/__init__.py +1 -0
  81. calc_terminal/editor/actions.py +263 -0
  82. calc_terminal/editor/commands.py +160 -0
  83. calc_terminal/editor/shortcuts.py +226 -0
  84. calc_terminal/engine.py +259 -0
  85. calc_terminal/errors.py +120 -0
  86. calc_terminal/event_stream.py +146 -0
  87. calc_terminal/eventbus.py +133 -0
  88. calc_terminal/extensions.py +733 -0
  89. calc_terminal/fallback_cli.py +1321 -0
  90. calc_terminal/first_run.py +265 -0
  91. calc_terminal/fomoji_auth.py +1043 -0
  92. calc_terminal/formulas.py +82 -0
  93. calc_terminal/fs_cache.py +121 -0
  94. calc_terminal/fs_watcher.py +277 -0
  95. calc_terminal/game.py +193 -0
  96. calc_terminal/gen1.py +5 -0
  97. calc_terminal/generators.py +245 -0
  98. calc_terminal/gestures/__init__.py +42 -0
  99. calc_terminal/gestures/bindings.py +175 -0
  100. calc_terminal/gestures/manager.py +477 -0
  101. calc_terminal/goodbye.py +363 -0
  102. calc_terminal/gpu3d.py +290 -0
  103. calc_terminal/graphs.py +358 -0
  104. calc_terminal/hardware_analyzer.py +440 -0
  105. calc_terminal/host/__init__.py +30 -0
  106. calc_terminal/host/browser_manager.py +187 -0
  107. calc_terminal/host/desktop.py +1386 -0
  108. calc_terminal/host/launcher.py +395 -0
  109. calc_terminal/host/terminal.py +279 -0
  110. calc_terminal/identity.py +216 -0
  111. calc_terminal/input/__init__.py +54 -0
  112. calc_terminal/input/capabilities.py +258 -0
  113. calc_terminal/input/focus.py +87 -0
  114. calc_terminal/input/gestures.py +64 -0
  115. calc_terminal/input/pointer.py +114 -0
  116. calc_terminal/input/touch.py +345 -0
  117. calc_terminal/keys.py +84 -0
  118. calc_terminal/live_automation.py +165 -0
  119. calc_terminal/mathtext.py +433 -0
  120. calc_terminal/mcp.py +386 -0
  121. calc_terminal/memory.py +337 -0
  122. calc_terminal/memory_v2.py +479 -0
  123. calc_terminal/metrics.py +333 -0
  124. calc_terminal/mode_detection.py +146 -0
  125. calc_terminal/model.py +2431 -0
  126. calc_terminal/model_router.py +665 -0
  127. calc_terminal/models/__init__.py +0 -0
  128. calc_terminal/models/active_state.py +187 -0
  129. calc_terminal/models/dynamic_registry.py +584 -0
  130. calc_terminal/models/manager.py +781 -0
  131. calc_terminal/models/model_metadata.json +3526 -0
  132. calc_terminal/models/profiles.py +194 -0
  133. calc_terminal/models/registry.py +265 -0
  134. calc_terminal/models/schema.py +197 -0
  135. calc_terminal/models/validator.py +287 -0
  136. calc_terminal/models/verification_engine.py +368 -0
  137. calc_terminal/native_picker.py +215 -0
  138. calc_terminal/ollama_catalog.py +279 -0
  139. calc_terminal/ollama_download.py +233 -0
  140. calc_terminal/orchestrator.py +304 -0
  141. calc_terminal/package_research.py +322 -0
  142. calc_terminal/packages.py +1024 -0
  143. calc_terminal/pc_specs.py +116 -0
  144. calc_terminal/permissions.py +334 -0
  145. calc_terminal/pet.py +106 -0
  146. calc_terminal/pipeline.py +505 -0
  147. calc_terminal/platform/__init__.py +491 -0
  148. calc_terminal/platform/desktop.py +491 -0
  149. calc_terminal/platform/web.py +781 -0
  150. calc_terminal/preview/__init__.py +1 -0
  151. calc_terminal/preview/dev_server.py +303 -0
  152. calc_terminal/preview/diagnostics.py +131 -0
  153. calc_terminal/preview/live_reload.py +66 -0
  154. calc_terminal/preview/manager.py +129 -0
  155. calc_terminal/project_stats.py +209 -0
  156. calc_terminal/projects.py +328 -0
  157. calc_terminal/providers/__init__.py +0 -0
  158. calc_terminal/providers/adapters/__init__.py +80 -0
  159. calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
  160. calc_terminal/providers/adapters/base.py +106 -0
  161. calc_terminal/providers/adapters/chinese_adapters.py +420 -0
  162. calc_terminal/providers/adapters/gemini_adapter.py +101 -0
  163. calc_terminal/providers/adapters/ollama_adapter.py +83 -0
  164. calc_terminal/providers/adapters/openai_adapter.py +159 -0
  165. calc_terminal/providers/adapters/other_adapters.py +246 -0
  166. calc_terminal/providers/anthropic_provider.py +172 -0
  167. calc_terminal/providers/auto_update.py +416 -0
  168. calc_terminal/providers/base_provider.py +105 -0
  169. calc_terminal/providers/discovery_manager.py +207 -0
  170. calc_terminal/providers/gemini_provider.py +178 -0
  171. calc_terminal/providers/lifecycle.py +767 -0
  172. calc_terminal/providers/ollama_adapter.py +707 -0
  173. calc_terminal/providers/openai_provider.py +254 -0
  174. calc_terminal/providers/provider_manager.py +1827 -0
  175. calc_terminal/providers/providers.json +4075 -0
  176. calc_terminal/reactionsim.py +279 -0
  177. calc_terminal/registry.py +337 -0
  178. calc_terminal/report.py +162 -0
  179. calc_terminal/research/__init__.py +45 -0
  180. calc_terminal/research/artifact_intel.py +126 -0
  181. calc_terminal/research/data_lineage.py +123 -0
  182. calc_terminal/research/experiment_ledger.py +303 -0
  183. calc_terminal/research/reproducibility.py +131 -0
  184. calc_terminal/resilience/__init__.py +47 -0
  185. calc_terminal/resilience/agent_state.py +121 -0
  186. calc_terminal/resilience/capability_matcher.py +174 -0
  187. calc_terminal/resilience/circuit_breaker.py +158 -0
  188. calc_terminal/resilience/failover_engine.py +230 -0
  189. calc_terminal/resilience/health_monitor.py +192 -0
  190. calc_terminal/resilience/ollama_adapter.py +125 -0
  191. calc_terminal/resilience/orchestrator.py +312 -0
  192. calc_terminal/resilience/types.py +134 -0
  193. calc_terminal/sandbox.py +98 -0
  194. calc_terminal/scires.py +558 -0
  195. calc_terminal/security_scanner.py +126 -0
  196. calc_terminal/session.py +294 -0
  197. calc_terminal/sim3d.py +206 -0
  198. calc_terminal/solver.py +276 -0
  199. calc_terminal/sound.py +127 -0
  200. calc_terminal/task_reports.py +287 -0
  201. calc_terminal/terminal_host.py +201 -0
  202. calc_terminal/terminal_identity.py +411 -0
  203. calc_terminal/test_ai_mode_reliability.py +344 -0
  204. calc_terminal/test_browser.py +368 -0
  205. calc_terminal/test_code_editor_upgrade.py +485 -0
  206. calc_terminal/test_customization.py +1148 -0
  207. calc_terminal/test_customization_ui.py +612 -0
  208. calc_terminal/test_dynamic_registry.py +304 -0
  209. calc_terminal/test_extensions.py +436 -0
  210. calc_terminal/test_overhaul.py +557 -0
  211. calc_terminal/test_project_detect.py +255 -0
  212. calc_terminal/test_root_cause_fix.py +527 -0
  213. calc_terminal/test_stability.py +532 -0
  214. calc_terminal/test_terminal_identity.py +132 -0
  215. calc_terminal/test_v079_speed.py +460 -0
  216. calc_terminal/theme.py +1107 -0
  217. calc_terminal/timeline.py +139 -0
  218. calc_terminal/todos.py +246 -0
  219. calc_terminal/tool_call_normalizer.py +419 -0
  220. calc_terminal/tui.py +104 -0
  221. calc_terminal/ui/__init__.py +8 -0
  222. calc_terminal/ui/activity_panel.py +231 -0
  223. calc_terminal/ui/activity_stream_panel.py +238 -0
  224. calc_terminal/ui/animations.py +122 -0
  225. calc_terminal/ui/app.py +7271 -0
  226. calc_terminal/ui/attach_panel.py +597 -0
  227. calc_terminal/ui/attachments.py +424 -0
  228. calc_terminal/ui/backup_panel.py +810 -0
  229. calc_terminal/ui/browser_shell.py +887 -0
  230. calc_terminal/ui/cat_agent.py +357 -0
  231. calc_terminal/ui/chats_panel.py +899 -0
  232. calc_terminal/ui/command_palette.py +125 -0
  233. calc_terminal/ui/command_palette_modal.py +166 -0
  234. calc_terminal/ui/composer.py +1141 -0
  235. calc_terminal/ui/context_menu.py +197 -0
  236. calc_terminal/ui/conversation.py +1435 -0
  237. calc_terminal/ui/customization_panel.py +1229 -0
  238. calc_terminal/ui/dashboard.py +404 -0
  239. calc_terminal/ui/design_system.py +557 -0
  240. calc_terminal/ui/diff_panel.py +213 -0
  241. calc_terminal/ui/editor.py +2102 -0
  242. calc_terminal/ui/empty_state.py +302 -0
  243. calc_terminal/ui/events.py +487 -0
  244. calc_terminal/ui/extensions_panel.py +815 -0
  245. calc_terminal/ui/footer.py +166 -0
  246. calc_terminal/ui/gestures_panel.py +383 -0
  247. calc_terminal/ui/goodbye_screen.py +100 -0
  248. calc_terminal/ui/header.py +1034 -0
  249. calc_terminal/ui/help_panel.py +254 -0
  250. calc_terminal/ui/live_activities.py +914 -0
  251. calc_terminal/ui/mcp_panel.py +570 -0
  252. calc_terminal/ui/memory_center.py +524 -0
  253. calc_terminal/ui/mode_colors_panel.py +525 -0
  254. calc_terminal/ui/nav_screens.py +747 -0
  255. calc_terminal/ui/ollama_panel.py +536 -0
  256. calc_terminal/ui/palette.py +221 -0
  257. calc_terminal/ui/permission_panel.py +269 -0
  258. calc_terminal/ui/personalization_panel.py +517 -0
  259. calc_terminal/ui/personalize_center.py +1568 -0
  260. calc_terminal/ui/preview_panel.py +441 -0
  261. calc_terminal/ui/resizers.py +402 -0
  262. calc_terminal/ui/sidebar.py +1285 -0
  263. calc_terminal/ui/statusbar.py +168 -0
  264. calc_terminal/ui/theme_css.py +1396 -0
  265. calc_terminal/ui/thinking.py +226 -0
  266. calc_terminal/ui/timeline_panel.py +102 -0
  267. calc_terminal/ui/todo_panel.py +193 -0
  268. calc_terminal/ui/viewport.py +136 -0
  269. calc_terminal/ui/vision_panel.py +489 -0
  270. calc_terminal/ui/welcome_modal.py +343 -0
  271. calc_terminal/ui/widgets.py +160 -0
  272. calc_terminal/ui/workspace.py +831 -0
  273. calc_terminal/viewers/__init__.py +1 -0
  274. calc_terminal/viewers/document_viewer.py +252 -0
  275. calc_terminal/viewers/image_viewer.py +241 -0
  276. calc_terminal/viewers/pdf_viewer.py +203 -0
  277. calc_terminal/viewers/presentation_viewer.py +164 -0
  278. calc_terminal/viewers/registry.py +120 -0
  279. calc_terminal/viewers/spreadsheet_viewer.py +204 -0
  280. calc_terminal/vision/__init__.py +89 -0
  281. calc_terminal/vision/analysis.py +194 -0
  282. calc_terminal/vision/annotations.py +297 -0
  283. calc_terminal/vision/capture.py +171 -0
  284. calc_terminal/vision/context.py +231 -0
  285. calc_terminal/vision/correlation.py +169 -0
  286. calc_terminal/vision/cursor.py +258 -0
  287. calc_terminal/vision/events.py +66 -0
  288. calc_terminal/vision/frame_pipeline.py +259 -0
  289. calc_terminal/vision/priority.py +218 -0
  290. calc_terminal/vision/provider.py +180 -0
  291. calc_terminal/vision/safety.py +149 -0
  292. calc_terminal/vision/session.py +281 -0
  293. calc_terminal/vision/verify.py +162 -0
  294. calc_terminal/vision.py +514 -0
  295. calc_terminal/vscode_integration.py +113 -0
  296. calc_terminal/web/__init__.py +8 -0
  297. calc_terminal/web/cat_runtime.py +710 -0
  298. calc_terminal/web/server.py +2891 -0
  299. calc_terminal/web/static/css/app.css +3152 -0
  300. calc_terminal/web/static/icons/badge-72.png +0 -0
  301. calc_terminal/web/static/icons/cat.ico +0 -0
  302. calc_terminal/web/static/icons/icon-128.png +0 -0
  303. calc_terminal/web/static/icons/icon-144.png +0 -0
  304. calc_terminal/web/static/icons/icon-152.png +0 -0
  305. calc_terminal/web/static/icons/icon-192.png +0 -0
  306. calc_terminal/web/static/icons/icon-384.png +0 -0
  307. calc_terminal/web/static/icons/icon-512.png +0 -0
  308. calc_terminal/web/static/icons/icon-72.png +0 -0
  309. calc_terminal/web/static/icons/icon-96.png +0 -0
  310. calc_terminal/web/static/icons/icon.svg +34 -0
  311. calc_terminal/web/static/icons/new-project.png +0 -0
  312. calc_terminal/web/static/icons/open-project.png +0 -0
  313. calc_terminal/web/static/index.html +734 -0
  314. calc_terminal/web/static/js/app.js +2403 -0
  315. calc_terminal/web/static/manifest.json +88 -0
  316. calc_terminal/web/static/sw.js +230 -0
  317. calc_terminal/workflow_engine.py +769 -0
  318. calc_terminal/workspace.py +593 -0
  319. calc_terminal/workspace_index.py +385 -0
  320. cct_cli-0.7.9.0.dist-info/METADATA +210 -0
  321. cct_cli-0.7.9.0.dist-info/RECORD +325 -0
  322. cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
  323. cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
  324. cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
  325. cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,82 @@
1
+ from .mathtext import frac_part as F, sub, sup
2
+
3
+ FORMULAS = [
4
+ {
5
+ "name": "First Order Rate Constant",
6
+ "tag": "Kinetics",
7
+ "formula": ["k = ", F("2.303", "t"), f" log(R{sub(0)}/R)"],
8
+ "meaning": "Gives the rate constant of a first order reaction from concentrations at two times.",
9
+ "variables": "k = rate constant, t = time elapsed, R0 = initial concentration, R = concentration at time t",
10
+ "units": f"k -> time{sup(-1)} (e.g. s{sup(-1)}, min{sup(-1)})",
11
+ "conditions": "Valid only for reactions that are first order overall.",
12
+ "mistakes": "Forgetting the 2.303 factor when converting ln to log10; mixing up R0 and R inside the log.",
13
+ "trick": "log(R0/R) is always positive since R0 > R for a forward reaction — a negative result signals swapped values.",
14
+ "memory": "\"2.303 over t, log the ratio\" — say it as one phrase.",
15
+ "example": "R0 = 0.8 M, R = 0.4 M, t = 20 min -> k = (2.303/20) log(2) = 0.0347 min-1",
16
+ },
17
+ {
18
+ "name": "First Order Half-Life",
19
+ "tag": "Kinetics",
20
+ "formula": ["t\u00bd = ", F("0.693", "k")],
21
+ "meaning": "Time required for concentration to fall to half its initial value; constant for first order reactions.",
22
+ "variables": "t1/2 = half-life, k = rate constant",
23
+ "units": "t1/2 -> same time unit used for k-1",
24
+ "conditions": "Independent of initial concentration — a hallmark of first order kinetics.",
25
+ "mistakes": "Using this formula for zero or second order reactions, where half-life depends on concentration.",
26
+ "trick": "0.693 is just ln(2) — recall ln(2) \u2248 0.693 to rebuild the formula instantly.",
27
+ "memory": "\"Half-life never lies\" for first order — always the same value.",
28
+ "example": "k = 0.0347 min-1 -> t1/2 = 0.693/0.0347 \u2248 20 min",
29
+ },
30
+ {
31
+ "name": "Zero Order Rate Law",
32
+ "tag": "Kinetics",
33
+ "formula": ["k = ", F(f"R{sub(0)} \u2212 R", "t")],
34
+ "meaning": "Rate constant for a zero order reaction, where rate is independent of concentration.",
35
+ "variables": "R0 = initial concentration, R = concentration at time t, t = time",
36
+ "units": f"k -> mol L{sup(-1)} time{sup(-1)}",
37
+ "conditions": "Applies when reaction rate does not change with reactant concentration (e.g. surface-catalysed reactions).",
38
+ "mistakes": "Applying the first-order log formula by mistake; forgetting that k here carries concentration units.",
39
+ "trick": "Zero order graphs are a straight line of [R] vs t — spot it visually before calculating.",
40
+ "memory": "\"Zero order, straight decline.\"",
41
+ "example": "R0 = 1.0 M, R = 0.8 M, t = 20 min -> k = (1.0-0.8)/20 = 0.01 mol L-1 min-1",
42
+ },
43
+ {
44
+ "name": "Second Order Rate Law",
45
+ "tag": "Kinetics",
46
+ "formula": [F("1", "R"), " = ", F("1", f"R{sub(0)}"), " + kt"],
47
+ "meaning": "Relates concentration to time for a second order reaction.",
48
+ "variables": "R0 = initial concentration, R = concentration at time t, k = rate constant, t = time",
49
+ "units": f"k -> L mol{sup(-1)} time{sup(-1)}",
50
+ "conditions": "Applies when rate depends on the square of one reactant's concentration (or product of two reactants).",
51
+ "mistakes": "Forgetting to invert both concentrations before subtracting; unit mismatch with first-order k.",
52
+ "trick": "A straight line of 1/[R] vs t confirms second order behaviour.",
53
+ "memory": "\"Flip it, then add kt.\"",
54
+ "example": "R0 = 1 M, k = 0.02 L mol-1 min-1, t = 20 min -> 1/R = 1 + 0.4 = 1.4 -> R \u2248 0.714 M",
55
+ },
56
+ {
57
+ "name": "Arrhenius Equation",
58
+ "tag": "Kinetics",
59
+ "formula": ["k = A e", "^(\u2212Ea/RT)"],
60
+ "meaning": "Describes how rate constant depends on temperature and activation energy.",
61
+ "variables": "k = rate constant, A = pre-exponential factor, Ea = activation energy, R = gas constant, T = temperature (K)",
62
+ "units": "Ea -> J/mol (use R = 8.314 J/mol/K) or kJ/mol (use R = 0.008314 kJ/mol/K)",
63
+ "conditions": "Temperature must be in Kelvin; assumes Ea is constant over the range considered.",
64
+ "mistakes": "Mixing kJ and J units for Ea and R; using \u00b0C instead of Kelvin.",
65
+ "trick": f"For two temperatures, skip solving for A — use ln(k{sub(2)}/k{sub(1)}) = \u2212Ea/R (1/T{sub(2)} \u2212 1/T{sub(1)}).",
66
+ "memory": "\"Bigger T, bigger k — always increasing.\"",
67
+ "example": "Ea = 75 kJ/mol, k1 at 300 K, T2 = 310 K -> k2 > k1, computed via the two-point form.",
68
+ },
69
+ {
70
+ "name": "Mole-Mass Relation",
71
+ "tag": "Mole Concept",
72
+ "formula": ["n = ", F("m", "M")],
73
+ "meaning": "Converts a given mass of a substance into number of moles.",
74
+ "variables": "n = number of moles, m = given mass, M = molar mass",
75
+ "units": "n -> mol, m -> g, M -> g/mol",
76
+ "conditions": "M must correspond to the correct chemical formula (watch for hydrates, ions).",
77
+ "mistakes": "Using atomic mass instead of molar mass for compounds; forgetting water of crystallisation in hydrates.",
78
+ "trick": "Sanity check: multiply n x M and confirm it returns the original mass.",
79
+ "memory": "\"Moles = mass over molar mass — m over M.\"",
80
+ "example": "5 g of NaOH (M = 40 g/mol) -> n = 5/40 = 0.125 mol",
81
+ },
82
+ ]
@@ -0,0 +1,121 @@
1
+ """
2
+ CAT v0.7.9.0 — fs_cache.py: safe, self-invalidating filesystem metadata
3
+ cache (requirement #12 — SMART CACHING).
4
+
5
+ Caches the expensive-to-rebuild, cheap-to-key results CAT asks for over
6
+ and over inside one agent run:
7
+
8
+ * directory listings (keyed by path; invalidated by that directory's
9
+ mtime AND by explicit mutation notifications)
10
+ * workspace index (keyed by root + a cheap signature of the tree)
11
+
12
+ Correctness rules:
13
+ * NEVER return stale data after a known mutation: every write/rename/
14
+ delete tool call in agent.py notifies invalidate_path(), and each cache
15
+ hit re-checks the directory's own mtime first — an external change made
16
+ outside CAT is picked up automatically.
17
+ * TTL is short (2 s) on purpose: it collapses repeated scans WITHIN one
18
+ agent step burst without ever serving yesterday's view of the world.
19
+ """
20
+
21
+ if __name__ == "__main__":
22
+ print("This is a library file and is not meant to be run directly.")
23
+ import sys
24
+ sys.exit(1)
25
+
26
+ import os
27
+ import threading
28
+ import time
29
+ from typing import Any, Callable, Dict, Optional, Tuple
30
+
31
+ DEFAULT_TTL = 2.0 # seconds
32
+ MAX_ENTRIES = 256
33
+
34
+ _lock = threading.Lock()
35
+ _store: Dict[str, Tuple[float, Any]] = {} # key -> (expires_at, value)
36
+ _signatures: Dict[str, Tuple] = {} # key -> validity signature
37
+
38
+
39
+ def _dir_signature(path: str) -> Tuple:
40
+ """Cheap validity probe: directory mtime (+ its own existence). An
41
+ external add/remove/update inside the dir changes st_mtime, so the
42
+ cached listing goes stale immediately."""
43
+ try:
44
+ st = os.stat(path)
45
+ return (st.st_mtime_ns, st.st_size)
46
+ except OSError:
47
+ return ("missing",)
48
+
49
+
50
+ def get_cached(key: str):
51
+ with _lock:
52
+ entry = _store.get(key)
53
+ if entry is None:
54
+ return None
55
+ expires_at, value = entry
56
+ if expires_at < time.time():
57
+ _store.pop(key, None)
58
+ _signatures.pop(key, None)
59
+ return None
60
+ return value
61
+
62
+
63
+ def put_cached(key: str, value, ttl: float = DEFAULT_TTL,
64
+ signature: Optional[Tuple] = None) -> None:
65
+ with _lock:
66
+ if len(_store) >= MAX_ENTRIES:
67
+ # Drop the soonest-expiring entries first.
68
+ for k in sorted(_store, key=lambda k: _store[k][0])[:32]:
69
+ _store.pop(k, None)
70
+ _signatures.pop(k, None)
71
+ _store[key] = (time.time() + max(0.05, ttl), value)
72
+ if signature is not None:
73
+ _signatures[key] = signature
74
+
75
+
76
+ def valid_signature(key: str, signature: Tuple) -> bool:
77
+ with _lock:
78
+ return _signatures.get(key) == signature
79
+
80
+
81
+ def invalidate_path(path: str) -> None:
82
+ """Called by every mutating tool (write/create/delete/rename) so no
83
+ cached listing can ever outlive a known change. Invalidates the path
84
+ itself and all cached ancestors (a new file changes the parent's
85
+ listing too)."""
86
+ path = os.path.normpath(os.path.abspath(str(path)))
87
+ with _lock:
88
+ for key in [k for k in _store if k.startswith("dirls:")]:
89
+ try:
90
+ cached_path = key[len("dirls:"):]
91
+ norm = os.path.normpath(os.path.abspath(cached_path))
92
+ except Exception:
93
+ continue
94
+ if norm == path or path.startswith(norm + os.sep) \
95
+ or norm.startswith(path + os.sep):
96
+ _store.pop(key, None)
97
+ _signatures.pop(key, None)
98
+
99
+
100
+ def clear() -> None:
101
+ with _lock:
102
+ _store.clear()
103
+ _signatures.clear()
104
+
105
+
106
+ def cached_dir_listing(path: str, builder: Callable[[], Any],
107
+ ttl: float = DEFAULT_TTL):
108
+ """TTL+mtime-guarded directory listing. `builder` does the real scan;
109
+ it only runs when there is no fresh, still-valid cached copy."""
110
+ try:
111
+ real = os.path.abspath(path)
112
+ except Exception:
113
+ return builder()
114
+ key = "dirls:" + real
115
+ sig = _dir_signature(real)
116
+ cached = get_cached(key)
117
+ if cached is not None and valid_signature(key, sig):
118
+ return cached
119
+ value = builder()
120
+ put_cached(key, value, ttl=ttl, signature=sig)
121
+ return value
@@ -0,0 +1,277 @@
1
+ """
2
+ CAT v0.7.9.0 — fs_watcher.py: real-time filesystem watching for the
3
+ live File Explorer.
4
+
5
+ Architecture (the whole point of this module):
6
+
7
+ REAL FILESYSTEM
8
+ |
9
+ watcher (watchdog Observer, or a stdlib snapshot-poll fallback)
10
+ |
11
+ debounced filesystem events -> callback(set_of_changed_paths)
12
+ |
13
+ Workspace state manager (ui/app.py bridges to the UI thread)
14
+ |
15
+ Explorer tree refresh
16
+
17
+ The callback is ALWAYS invoked on a background thread — it must never
18
+ touch widgets. ui/app.py's `_on_fs_events` marshals the paths back to
19
+ the UI thread with call_from_thread and posts a WorkspaceFilesChanged
20
+ Message, exactly like every other worker-to-UI crossing in this app.
21
+
22
+ Design notes:
23
+
24
+ * watchdog's Observer is used when importable (real OS push events on
25
+ Windows — ReadDirectoryChangesW — plus inotify/FSEvents elsewhere).
26
+ When it isn't installed, a low-overhead stdlib poller takes over: it
27
+ walks only the active workspace root every POLL_INTERVAL seconds,
28
+ compares name/mtime snapshots, and reports only actual differences.
29
+ That fallback is throttled by construction (one walk per interval,
30
+ nothing between ticks) — never a busy loop.
31
+ * Events are debounced: bursts of related changes (a save writing a
32
+ temp file then renaming it over the target; `git checkout` touching
33
+ hundreds of files) coalesce into one callback ~DEBOUNCE seconds after
34
+ the last raw event, so the Explorer reloads once, not N times.
35
+ * One watcher instance per app. `start(root)` stops whatever root was
36
+ being watched before (new workspace => stop old watcher => start new
37
+ watcher), so the Explorer can never observe a stale folder.
38
+ """
39
+
40
+ if __name__ == "__main__":
41
+ print("This is a library file and is not meant to be run directly.")
42
+ import sys
43
+ sys.exit(1)
44
+
45
+ import os
46
+ import threading
47
+
48
+ try:
49
+ from watchdog.observers import Observer
50
+ from watchdog.events import FileSystemEventHandler as _WdHandler
51
+ WATCHDOG_AVAILABLE = True
52
+ except Exception:
53
+ WATCHDOG_AVAILABLE = False
54
+ Observer = None
55
+
56
+ class _WdHandler: # stand-in so the class body below still parses
57
+ pass
58
+
59
+
60
+ class WorkspaceWatcher:
61
+ """Watches one workspace root and reports debounced change sets.
62
+
63
+ Parameters
64
+ ----------
65
+ callback : callable(set[str])
66
+ Invoked with the set of changed absolute paths. Always called
67
+ from a background thread (the Observer's emitter thread or the
68
+ fallback poller thread). Exceptions raised inside it must be
69
+ its own responsibility.
70
+ """
71
+
72
+ DEBOUNCE = 0.35 # seconds of quiet before flushing a burst
73
+ POLL_INTERVAL = 2.0 # fallback poll cadence (no watchdog)
74
+
75
+ def __init__(self, callback):
76
+ self._callback = callback
77
+ self._root = None
78
+ self._lock = threading.Lock()
79
+ self._pending = set()
80
+ self._debounce_timer = None
81
+ self._observer = None
82
+ self._poll_thread = None
83
+ self._poll_stop = threading.Event()
84
+ self._snapshot = {}
85
+
86
+ # ------------------------------------------------------------ start --
87
+ def start(self, root):
88
+ """(Re)points the watcher at `root`. Stops the previous watch
89
+ first, so switching workspaces never leaves an old Observer (or
90
+ old poller) running against a folder that is no longer open."""
91
+ root = os.path.abspath(os.path.expanduser(root))
92
+ if not os.path.isdir(root):
93
+ return False
94
+ self.stop()
95
+ with self._lock:
96
+ self._root = root
97
+ self._pending.clear()
98
+ if WATCHDOG_AVAILABLE:
99
+ return self._start_observer(root)
100
+ return self._start_poller(root)
101
+
102
+ def _start_observer(self, root):
103
+ try:
104
+ observer = Observer(timeout=0.5)
105
+ observer.daemon = True
106
+ handler = _Handler(self)
107
+ # watch() on the root itself; recursive covers subfolders.
108
+ # Windows/ReadDirectoryChangesW delivers create/delete/
109
+ # rename/modify for everything below root.
110
+ observer.schedule(handler, root, recursive=True)
111
+ observer.start()
112
+ self._observer = observer
113
+ return True
114
+ except Exception:
115
+ # Watchdog exists but the platform refused the watch (rare
116
+ # — e.g. a drive gone mid-call): fall back to polling.
117
+ self._observer = None
118
+ return self._start_poller(root)
119
+
120
+ def _start_poller(self, root):
121
+ self._poll_stop.clear()
122
+ try:
123
+ self._snapshot = self._scan(root)
124
+ except Exception:
125
+ self._snapshot = {}
126
+ thread = threading.Thread(
127
+ target=self._poll_loop, args=(root,),
128
+ name="cat-fs-poll", daemon=True)
129
+ thread.start()
130
+ self._poll_thread = thread
131
+ return True
132
+
133
+ # ------------------------------------------------------------- stop --
134
+ def stop(self):
135
+ """Fully tears down whichever mechanism is running. Safe to call
136
+ twice / when never started."""
137
+ with self._lock:
138
+ self._root = None
139
+ self._cancel_debounce_locked()
140
+ observer = self._observer
141
+ self._observer = None
142
+ if observer is not None:
143
+ try:
144
+ observer.stop()
145
+ observer.join(timeout=2.0)
146
+ except Exception:
147
+ pass
148
+ self._poll_stop.set()
149
+ thread = self._poll_thread
150
+ self._poll_thread = None
151
+ if thread is not None and thread.is_alive():
152
+ thread.join(timeout=2.0)
153
+ with self._lock:
154
+ self._pending.clear()
155
+ self._snapshot = {}
156
+
157
+ @property
158
+ def root(self):
159
+ return self._root
160
+
161
+ @property
162
+ def active(self):
163
+ return self._observer is not None or self._poll_thread is not None
164
+
165
+ @property
166
+ def event_based(self):
167
+ """True when backed by real OS events (watchdog); False when the
168
+ throttled poll fallback is in charge."""
169
+ return self._observer is not None
170
+
171
+ # ------------------------------------------------- event collection --
172
+ def report(self, path):
173
+ """Thread-safe event intake. Called by the watchdog handler for
174
+ every raw filesystem event (and usable directly for explicit
175
+ internal TOOL_FILE_* notifications). Bursts are coalesced and
176
+ flushed once the stream goes quiet for DEBOUNCE seconds."""
177
+ if path:
178
+ with self._lock:
179
+ if self._root is None:
180
+ return
181
+ self._pending.add(os.path.abspath(path))
182
+ self._cancel_debounce_locked()
183
+ timer = threading.Timer(self.DEBOUNCE, self._flush)
184
+ timer.daemon = True
185
+ timer.start()
186
+ self._debounce_timer = timer
187
+
188
+ def _cancel_debounce_locked(self):
189
+ timer = self._debounce_timer
190
+ self._debounce_timer = None
191
+ if timer is not None:
192
+ timer.cancel()
193
+
194
+ def _flush(self):
195
+ with self._lock:
196
+ batch = self._pending
197
+ self._pending = set()
198
+ if batch and self._callback is not None:
199
+ try:
200
+ self._callback(batch)
201
+ except Exception:
202
+ pass
203
+
204
+ # ---------------------------------------------------- poll fallback --
205
+ def _scan(self, root):
206
+ """One shallow-everything snapshot: {path: mtime}. Directories
207
+ get st_mtime too (Windows keeps dir mtimes fresh on content
208
+ churn), so folder create/delete shows up as a diff without any
209
+ special handling. Symlinks are never followed."""
210
+ snap = {}
211
+ stack = [root]
212
+ while stack:
213
+ current = stack.pop()
214
+ try:
215
+ snap[current] = os.stat(current).st_mtime
216
+ with os.scandir(current) as entries:
217
+ for entry in entries:
218
+ try:
219
+ if entry.is_symlink():
220
+ continue
221
+ if entry.is_dir(follow_symlinks=False):
222
+ stack.append(entry.path)
223
+ else:
224
+ snap[entry.path] = entry.stat().st_mtime
225
+ except OSError:
226
+ continue
227
+ except (OSError, PermissionError):
228
+ continue
229
+ return snap
230
+
231
+ def _poll_loop(self, root):
232
+ while not self._poll_stop.wait(self.POLL_INTERVAL):
233
+ if self._root != root:
234
+ return
235
+ try:
236
+ fresh = self._scan(root)
237
+ except Exception:
238
+ continue
239
+ old = self._snapshot
240
+ changed = {p for p in fresh.keys() ^ old.keys()}
241
+ for p in fresh.keys() & old.keys():
242
+ if fresh[p] != old[p]:
243
+ changed.add(p)
244
+ if changed:
245
+ self._snapshot = fresh
246
+ for p in list(changed)[:64]: # cap the burst size
247
+ self.report(p)
248
+ else:
249
+ # Keep mtimes fresh even when nothing changed, so the
250
+ # next comparison stays cheap.
251
+ self._snapshot = fresh
252
+
253
+
254
+ class _Handler(_WdHandler):
255
+ """Watchdog adapter: forwards every interesting event kind into the
256
+ watcher's debounced intake."""
257
+
258
+ def __init__(self, watcher):
259
+ super().__init__()
260
+ self._watcher = watcher
261
+
262
+ def on_created(self, event):
263
+ self._emit(event.dest_path or event.src_path)
264
+
265
+ def on_deleted(self, event):
266
+ self._emit(event.src_path)
267
+
268
+ def on_modified(self, event):
269
+ self._emit(event.src_path)
270
+
271
+ def on_moved(self, event):
272
+ self._emit(event.src_path)
273
+ self._emit(event.dest_path)
274
+
275
+ def _emit(self, path):
276
+ if path:
277
+ self._watcher.report(path)
calc_terminal/game.py ADDED
@@ -0,0 +1,193 @@
1
+ """
2
+ Element Quiz — a small multiple-choice game bolted onto the same
3
+ premium panel/gradient rendering as the rest of Chemistry Calc
4
+ Terminal, so learning periodic-table facts feels like part of the
5
+ same app instead of a bolted-on mini-game.
6
+
7
+ Question types are generated from the same ELEMENTS table atomsim.py
8
+ already uses for the live Bohr simulation (Z, symbol, name, shells,
9
+ mass), so there's exactly one source of truth for element data.
10
+ """
11
+
12
+ import random
13
+ import time
14
+
15
+ from . import theme
16
+ from . import sound
17
+ from .atomsim import ELEMENTS
18
+ from .engine import WIDTH
19
+ LETTERS = ["A", "B", "C", "D"]
20
+ START_LIVES = 3
21
+
22
+
23
+ def _shells_str(shells):
24
+ return "-".join(str(s) for s in shells)
25
+
26
+
27
+ def _distractor_pool(exclude_z):
28
+ return [z for z in ELEMENTS if z != exclude_z]
29
+
30
+
31
+ def _make_options(correct, pool_values, n=4):
32
+ others = random.sample([v for v in pool_values if v != correct], min(n - 1, len(pool_values) - 1))
33
+ options = others + [correct]
34
+ random.shuffle(options)
35
+ return options
36
+
37
+
38
+ def make_question(difficulty=1):
39
+ """Build one multiple-choice question dict:
40
+ {prompt, options: [...], correct_index, explain}"""
41
+ z = random.choice(list(ELEMENTS.keys()))
42
+ sym, name, shells, mass = ELEMENTS[z]
43
+ qtype = random.choice([
44
+ "symbol_from_name", "name_from_symbol", "z_from_name",
45
+ "name_from_z", "shells_from_name", "mass_from_name",
46
+ ])
47
+
48
+ if qtype == "symbol_from_name":
49
+ prompt = f"What is the chemical symbol for {theme.text(name, bold=True)}?"
50
+ correct = sym
51
+ pool = [ELEMENTS[k][0] for k in ELEMENTS]
52
+ explain = f"{name} \u2192 {sym} (Z={z})"
53
+ elif qtype == "name_from_symbol":
54
+ prompt = f"Which element has the symbol {theme.cyan(sym, bold=True)}?"
55
+ correct = name
56
+ pool = [ELEMENTS[k][1] for k in ELEMENTS]
57
+ explain = f"{sym} \u2192 {name} (Z={z})"
58
+ elif qtype == "z_from_name":
59
+ prompt = f"What is the atomic number of {theme.text(name, bold=True)}?"
60
+ correct = str(z)
61
+ pool = [str(k) for k in ELEMENTS]
62
+ explain = f"{name} has {z} protons \u2192 Z = {z}"
63
+ elif qtype == "name_from_z":
64
+ prompt = f"Which element has atomic number {theme.orange(str(z), bold=True)}?"
65
+ correct = name
66
+ pool = [ELEMENTS[k][1] for k in ELEMENTS]
67
+ explain = f"Z={z} \u2192 {name} ({sym})"
68
+ elif qtype == "shells_from_name":
69
+ prompt = f"What is the electron shell structure (K-L-M...) of {theme.text(name, bold=True)}?"
70
+ correct = _shells_str(shells)
71
+ pool_z = random.sample(list(ELEMENTS.keys()), min(12, len(ELEMENTS)))
72
+ pool = [_shells_str(ELEMENTS[k][2]) for k in pool_z]
73
+ explain = f"{name} ({sym}, Z={z}) \u2192 {_shells_str(shells)}"
74
+ else: # mass_from_name
75
+ prompt = f"What is the approximate atomic mass of {theme.text(name, bold=True)}?"
76
+ correct = str(mass)
77
+ pool = [str(ELEMENTS[k][3]) for k in ELEMENTS]
78
+ explain = f"{name} ({sym}) \u2248 {mass} u"
79
+
80
+ options = _make_options(correct, pool)
81
+ if correct not in options:
82
+ options[-1] = correct
83
+ random.shuffle(options)
84
+ correct_index = options.index(correct)
85
+ return {"prompt": prompt, "options": options, "correct_index": correct_index, "explain": explain}
86
+
87
+
88
+ def _hearts(lives):
89
+ return theme.red("\u2665 " * lives, bold=True) + theme.faint("\u2661 " * (START_LIVES - lives))
90
+
91
+
92
+ def _streak_badge(streak):
93
+ if streak >= 8:
94
+ return theme.gradient(f"\U0001f525 x{streak} ON FIRE", theme.ORANGE, theme.RED, bold=True)
95
+ if streak >= 4:
96
+ return theme.orange(f"\U0001f525 x{streak} streak", bold=True)
97
+ if streak >= 1:
98
+ return theme.dim(f"streak x{streak}")
99
+ return theme.faint("streak x0")
100
+
101
+
102
+ def _grade(score):
103
+ if score >= 900:
104
+ return "S", theme.gradient("S \u2014 PERIODIC LEGEND", theme.CYAN, theme.PURPLE, bold=True)
105
+ if score >= 600:
106
+ return "A", theme.green("A \u2014 Sharp chemist", bold=True)
107
+ if score >= 350:
108
+ return "B", theme.cyan("B \u2014 Solid grasp", bold=True)
109
+ if score >= 150:
110
+ return "C", theme.orange("C \u2014 Getting there", bold=True)
111
+ return "D", theme.faint("D \u2014 Back to the formula library")
112
+
113
+
114
+ def run_game(app):
115
+ theme.clear_screen()
116
+ print()
117
+ print(theme.panel([
118
+ theme.gradient("\u2697 ELEMENT QUIZ \u2697", theme.CYAN, theme.PURPLE, bold=True),
119
+ "",
120
+ theme.dim("Multiple choice \u2014 symbols, names, atomic numbers, shells & mass."),
121
+ theme.dim(f"You have {START_LIVES} lives. Answer fast for a speed bonus."),
122
+ theme.dim("Type a letter (A-D) to answer, or /quit to stop anytime."),
123
+ ], title="game", color=theme.PURPLE, width=WIDTH, title_gradient=(theme.CYAN, theme.PURPLE)))
124
+
125
+ score = 0
126
+ streak = 0
127
+ best_streak_this_run = 0
128
+ lives = START_LIVES
129
+ qn = 0
130
+
131
+ while lives > 0:
132
+ qn += 1
133
+ q = make_question()
134
+ print()
135
+ header = (theme.faint(f"Q{qn}") + " " + _hearts(lives) + " " +
136
+ theme.dim(f"score {score}") + " " + _streak_badge(streak))
137
+ print(" " + header)
138
+ print(theme.panel(
139
+ [q["prompt"], ""] + [f" {theme.cyan(LETTERS[i])}. {opt}" for i, opt in enumerate(q["options"])],
140
+ title=f"question {qn}", color=theme.CYAN, width=WIDTH,
141
+ ))
142
+ start = time.time()
143
+ try:
144
+ raw = input(theme.dim(" answer \u25b8 ")).strip()
145
+ except (EOFError, KeyboardInterrupt):
146
+ print()
147
+ break
148
+ elapsed = time.time() - start
149
+ low = raw.strip().lower()
150
+ if low in ("/quit", "/back", "/exit", "quit"):
151
+ break
152
+
153
+ pick = None
154
+ if low and low[0].upper() in LETTERS:
155
+ pick = LETTERS.index(low[0].upper())
156
+
157
+ if pick == q["correct_index"]:
158
+ speed_bonus = 40 if elapsed < 3 else (20 if elapsed < 6 else 0)
159
+ base = 60
160
+ streak += 1
161
+ best_streak_this_run = max(best_streak_this_run, streak)
162
+ gained = base + speed_bonus + streak * 5
163
+ score += gained
164
+ sound.play("success")
165
+ tag = " \u26a1 speed bonus!" if speed_bonus == 40 else ""
166
+ print(" " + theme.green(f"\u2713 Correct! +{gained}{tag}", bold=True))
167
+ print(" " + theme.dim(q["explain"]))
168
+ else:
169
+ lives -= 1
170
+ streak = 0
171
+ sound.play("error")
172
+ correct_letter = LETTERS[q["correct_index"]]
173
+ print(" " + theme.red(f"\u2717 Not quite \u2014 correct answer was {correct_letter}.", bold=True))
174
+ print(" " + theme.dim(q["explain"]))
175
+ if lives == 0:
176
+ print(" " + theme.orange("Out of lives!", bold=True))
177
+
178
+ app.game_high_score = max(app.game_high_score, score)
179
+ app.game_best_streak = max(app.game_best_streak, best_streak_this_run)
180
+ _, grade_line = _grade(score)
181
+
182
+ print()
183
+ print(theme.panel([
184
+ theme.gradient("ROUND OVER", theme.PURPLE, theme.CYAN, bold=True),
185
+ "",
186
+ theme.text(f"Final score: {score}", bold=True),
187
+ theme.dim(f"Best streak this round: {best_streak_this_run}"),
188
+ theme.dim(f"Session high score: {app.game_high_score} \u00b7 Session best streak: {app.game_best_streak}"),
189
+ "",
190
+ grade_line,
191
+ ], title="results", color=theme.PURPLE, width=WIDTH, title_gradient=(theme.PURPLE, theme.CYAN)))
192
+ print()
193
+ input(theme.faint(" Press Enter to return home\u2026"))
calc_terminal/gen1.py ADDED
@@ -0,0 +1,5 @@
1
+ import os, sys
2
+ B=os.path.dirname(os.path.abspath(__file__))
3
+ def W(n,c):
4
+ with open(os.path.join(B,n),'w',encoding='utf-8') as f: f.write(c)
5
+ print(f"Wrote {n}")