@scemoon/cdh 1.0.0

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 (528) hide show
  1. package/ai-dlc-skill/SKILL.md +75 -0
  2. package/ai-dlc-skill/agents/deliver-agent.md +24 -0
  3. package/ai-dlc-skill/agents/master.md +44 -0
  4. package/ai-dlc-skill/agents/plan-agent.md +23 -0
  5. package/ai-dlc-skill/agents/understand-agent.md +23 -0
  6. package/ai-dlc-skill/agents/verify-agent.md +27 -0
  7. package/ai-dlc-skill/architecture/project-structure.md +78 -0
  8. package/ai-dlc-skill/brownfield/README.md +30 -0
  9. package/ai-dlc-skill/brownfield/generate-context.sh +64 -0
  10. package/ai-dlc-skill/brownfield/scripts/deps.sh +27 -0
  11. package/ai-dlc-skill/brownfield/scripts/discover.sh +34 -0
  12. package/ai-dlc-skill/brownfield/scripts/extract-api.sh +45 -0
  13. package/ai-dlc-skill/components/backend.md +34 -0
  14. package/ai-dlc-skill/components/desktop.md +30 -0
  15. package/ai-dlc-skill/components/mya.md +29 -0
  16. package/ai-dlc-skill/components/native.md +30 -0
  17. package/ai-dlc-skill/components/tta.md +29 -0
  18. package/ai-dlc-skill/components/web.md +31 -0
  19. package/ai-dlc-skill/components/wxa.md +29 -0
  20. package/ai-dlc-skill/contracts/README.md +25 -0
  21. package/ai-dlc-skill/core/adaptive-flow.md +36 -0
  22. package/ai-dlc-skill/core/security.md +29 -0
  23. package/ai-dlc-skill/cross-tool/cline/export.sh +31 -0
  24. package/ai-dlc-skill/cross-tool/copilot/export.sh +47 -0
  25. package/ai-dlc-skill/cross-tool/cursor/export.sh +32 -0
  26. package/ai-dlc-skill/cross-tool/export.sh +22 -0
  27. package/ai-dlc-skill/cross-tool/onecode/config.yaml.tpl +17 -0
  28. package/ai-dlc-skill/cross-tool/onecode/export-skills.sh +31 -0
  29. package/ai-dlc-skill/cross-tool/onecode/export.sh +48 -0
  30. package/ai-dlc-skill/phases/deliver/entry.md +26 -0
  31. package/ai-dlc-skill/phases/deliver/lifecycle.md +185 -0
  32. package/ai-dlc-skill/phases/deliver/prompt.md +40 -0
  33. package/ai-dlc-skill/phases/deliver/rules.md +61 -0
  34. package/ai-dlc-skill/phases/plan/entry.md +25 -0
  35. package/ai-dlc-skill/phases/plan/lifecycle.md +204 -0
  36. package/ai-dlc-skill/phases/plan/prompt.md +38 -0
  37. package/ai-dlc-skill/phases/plan/rules.md +25 -0
  38. package/ai-dlc-skill/phases/understand/entry.md +26 -0
  39. package/ai-dlc-skill/phases/understand/lifecycle.md +184 -0
  40. package/ai-dlc-skill/phases/understand/prompt.md +41 -0
  41. package/ai-dlc-skill/phases/understand/rules.md +37 -0
  42. package/ai-dlc-skill/phases/verify/entry.md +26 -0
  43. package/ai-dlc-skill/phases/verify/lifecycle.md +192 -0
  44. package/ai-dlc-skill/phases/verify/prompt.md +43 -0
  45. package/ai-dlc-skill/phases/verify/rules.md +73 -0
  46. package/ai-dlc-skill/practices/bdd.md +91 -0
  47. package/ai-dlc-skill/practices/sdd.md +84 -0
  48. package/ai-dlc-skill/practices/tdd.md +95 -0
  49. package/ai-dlc-skill/providers/README.md +114 -0
  50. package/ai-dlc-skill/providers/aliyun/deployment.yaml +66 -0
  51. package/ai-dlc-skill/providers/aliyun/preview.yaml +84 -0
  52. package/ai-dlc-skill/providers/aliyun/provider.yaml +160 -0
  53. package/ai-dlc-skill/providers/tcb/deployment.yaml +69 -0
  54. package/ai-dlc-skill/providers/tcb/preview.yaml +83 -0
  55. package/ai-dlc-skill/providers/tcb/provider.yaml +172 -0
  56. package/ai-dlc-skill/requirements.md +140 -0
  57. package/ai-dlc-skill/skill.yaml +147 -0
  58. package/ai-dlc-skill/templates/integration/CHANGELOG.md +41 -0
  59. package/ai-dlc-skill/templates/integration/asyncapi.yaml +43 -0
  60. package/ai-dlc-skill/templates/integration/contract-diff.md +63 -0
  61. package/ai-dlc-skill/templates/integration/contract-spec.md +49 -0
  62. package/ai-dlc-skill/templates/integration/openapi.yaml +18 -0
  63. package/ai-dlc-skill/templates/integration/runtime-contract.yaml +36 -0
  64. package/ai-dlc-skill/templates/plan/design.md +49 -0
  65. package/ai-dlc-skill/templates/plan/task-list.md +32 -0
  66. package/ai-dlc-skill/templates/project/README.md +127 -0
  67. package/ai-dlc-skill/templates/project/template.md +119 -0
  68. package/ai-dlc-skill/templates/understand/feature.feature +23 -0
  69. package/ai-dlc-skill/templates/understand/intent.md +29 -0
  70. package/ai-dlc-skill/templates/understand/spec-delta.md +47 -0
  71. package/ai-dlc-skill/templates/verify/test-unit.py +27 -0
  72. package/ai-dlc-skill/walkthrough/collect.sh +50 -0
  73. package/ai-dlc-skill/walkthrough/template.md +54 -0
  74. package/ai-dlc-skill/workflows/ai-dlc.yaml +110 -0
  75. package/ai-dlc-skill/workflows/deployment.yaml +199 -0
  76. package/cdh/__init__.py +0 -0
  77. package/cdh/__main__.py +4 -0
  78. package/cdh/__pycache__/__init__.cpython-313.pyc +0 -0
  79. package/cdh/__pycache__/__init__.cpython-314.pyc +0 -0
  80. package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
  81. package/cdh/__pycache__/scaffold.cpython-313.pyc +0 -0
  82. package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
  83. package/cdh/cli.py +361 -0
  84. package/cdh/scaffold.py +312 -0
  85. package/cli.js +2 -0
  86. package/onecode/__init__.py +1 -0
  87. package/onecode/__main__.py +4 -0
  88. package/onecode/__pycache__/__init__.cpython-314.pyc +0 -0
  89. package/onecode/__pycache__/cli.cpython-314.pyc +0 -0
  90. package/onecode/__pycache__/commands.cpython-314.pyc +0 -0
  91. package/onecode/__pycache__/config.cpython-314.pyc +0 -0
  92. package/onecode/__pycache__/config_screen.cpython-314.pyc +0 -0
  93. package/onecode/__pycache__/mock_data.cpython-314.pyc +0 -0
  94. package/onecode/agent/__init__.py +0 -0
  95. package/onecode/agent/__pycache__/__init__.cpython-314.pyc +0 -0
  96. package/onecode/agent/__pycache__/cdh_agent_acp.cpython-313.pyc +0 -0
  97. package/onecode/agent/__pycache__/cdh_agent_acp.cpython-314.pyc +0 -0
  98. package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
  99. package/onecode/agent/__pycache__/context.cpython-314.pyc +0 -0
  100. package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
  101. package/onecode/agent/__pycache__/harness_skill.cpython-314.pyc +0 -0
  102. package/onecode/agent/__pycache__/hooks.cpython-314.pyc +0 -0
  103. package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
  104. package/onecode/agent/__pycache__/permissions.cpython-314.pyc +0 -0
  105. package/onecode/agent/__pycache__/permissions_store.cpython-314.pyc +0 -0
  106. package/onecode/agent/__pycache__/session.cpython-314.pyc +0 -0
  107. package/onecode/agent/agents/__init__.py +29 -0
  108. package/onecode/agent/agents/__pycache__/__init__.cpython-314.pyc +0 -0
  109. package/onecode/agent/agents/__pycache__/types.cpython-314.pyc +0 -0
  110. package/onecode/agent/agents/types.py +662 -0
  111. package/onecode/agent/attachments.py +113 -0
  112. package/onecode/agent/cdh_loader.py +262 -0
  113. package/onecode/agent/context.py +338 -0
  114. package/onecode/agent/engine.py +1894 -0
  115. package/onecode/agent/hooks.py +86 -0
  116. package/onecode/agent/onecode_agent_acp.py +2203 -0
  117. package/onecode/agent/permissions.py +151 -0
  118. package/onecode/agent/permissions_store.py +62 -0
  119. package/onecode/agent/session.py +186 -0
  120. package/onecode/agent/snapshot.py +145 -0
  121. package/onecode/agent/tools/__init__.py +17 -0
  122. package/onecode/agent/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  123. package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
  124. package/onecode/agent/tools/__pycache__/apply_patch_tool.cpython-314.pyc +0 -0
  125. package/onecode/agent/tools/__pycache__/bash_tool.cpython-314.pyc +0 -0
  126. package/onecode/agent/tools/__pycache__/communication_tools.cpython-314.pyc +0 -0
  127. package/onecode/agent/tools/__pycache__/config_tool.cpython-314.pyc +0 -0
  128. package/onecode/agent/tools/__pycache__/cron_tools.cpython-314.pyc +0 -0
  129. package/onecode/agent/tools/__pycache__/errors.cpython-314.pyc +0 -0
  130. package/onecode/agent/tools/__pycache__/file_ops.cpython-314.pyc +0 -0
  131. package/onecode/agent/tools/__pycache__/file_tools.cpython-314.pyc +0 -0
  132. package/onecode/agent/tools/__pycache__/git_tools.cpython-314.pyc +0 -0
  133. package/onecode/agent/tools/__pycache__/lsp_tools.cpython-314.pyc +0 -0
  134. package/onecode/agent/tools/__pycache__/mcp_tools.cpython-314.pyc +0 -0
  135. package/onecode/agent/tools/__pycache__/permission_handler.cpython-314.pyc +0 -0
  136. package/onecode/agent/tools/__pycache__/permissions.cpython-314.pyc +0 -0
  137. package/onecode/agent/tools/__pycache__/protocol.cpython-314.pyc +0 -0
  138. package/onecode/agent/tools/__pycache__/registry.cpython-314.pyc +0 -0
  139. package/onecode/agent/tools/__pycache__/sandbox.cpython-314.pyc +0 -0
  140. package/onecode/agent/tools/__pycache__/schema_validation.cpython-314.pyc +0 -0
  141. package/onecode/agent/tools/__pycache__/skill_tools.cpython-314.pyc +0 -0
  142. package/onecode/agent/tools/__pycache__/task_tools.cpython-314.pyc +0 -0
  143. package/onecode/agent/tools/__pycache__/web_tools.cpython-314.pyc +0 -0
  144. package/onecode/agent/tools/agent_tools.py +134 -0
  145. package/onecode/agent/tools/apply_patch_tool.py +144 -0
  146. package/onecode/agent/tools/bash_tool.py +91 -0
  147. package/onecode/agent/tools/communication_tools.py +149 -0
  148. package/onecode/agent/tools/config_tool.py +130 -0
  149. package/onecode/agent/tools/cron_tools.py +179 -0
  150. package/onecode/agent/tools/errors.py +17 -0
  151. package/onecode/agent/tools/file_ops.py +187 -0
  152. package/onecode/agent/tools/file_tools.py +226 -0
  153. package/onecode/agent/tools/git_tools.py +88 -0
  154. package/onecode/agent/tools/lsp_tools.py +404 -0
  155. package/onecode/agent/tools/mcp_tools.py +129 -0
  156. package/onecode/agent/tools/permission_handler.py +46 -0
  157. package/onecode/agent/tools/permissions.py +77 -0
  158. package/onecode/agent/tools/protocol.py +20 -0
  159. package/onecode/agent/tools/registry.py +134 -0
  160. package/onecode/agent/tools/sandbox.py +264 -0
  161. package/onecode/agent/tools/schema_validation.py +100 -0
  162. package/onecode/agent/tools/skill_tools.py +52 -0
  163. package/onecode/agent/tools/task_tools.py +249 -0
  164. package/onecode/agent/tools/web_tools.py +170 -0
  165. package/onecode/builtin_skills/git/SKILL.md +64 -0
  166. package/onecode/builtin_skills/git/skill.yaml +7 -0
  167. package/onecode/builtin_skills/shell/SKILL.md +58 -0
  168. package/onecode/builtin_skills/shell/skill.yaml +8 -0
  169. package/onecode/cli.py +755 -0
  170. package/onecode/cloud/__init__.py +0 -0
  171. package/onecode/cloud/base.py +20 -0
  172. package/onecode/codebase/__init__.py +69 -0
  173. package/onecode/codebase/__pycache__/__init__.cpython-314.pyc +0 -0
  174. package/onecode/codebase/__pycache__/chunker.cpython-314.pyc +0 -0
  175. package/onecode/codebase/__pycache__/config.cpython-314.pyc +0 -0
  176. package/onecode/codebase/__pycache__/indexer.cpython-314.pyc +0 -0
  177. package/onecode/codebase/__pycache__/retriever.cpython-314.pyc +0 -0
  178. package/onecode/codebase/__pycache__/storage.cpython-314.pyc +0 -0
  179. package/onecode/codebase/__pycache__/tools.cpython-314.pyc +0 -0
  180. package/onecode/codebase/chunker.py +130 -0
  181. package/onecode/codebase/indexer.py +136 -0
  182. package/onecode/codebase/retriever.py +192 -0
  183. package/onecode/codebase/storage.py +130 -0
  184. package/onecode/codebase/tools.py +59 -0
  185. package/onecode/commands.py +276 -0
  186. package/onecode/config.py +285 -0
  187. package/onecode/config_screen.py +594 -0
  188. package/onecode/cron/__init__.py +3 -0
  189. package/onecode/lsp/__init__.py +3 -0
  190. package/onecode/mcp/__init__.py +4 -0
  191. package/onecode/mcp/__pycache__/__init__.cpython-314.pyc +0 -0
  192. package/onecode/mcp/__pycache__/client.cpython-314.pyc +0 -0
  193. package/onecode/mcp/__pycache__/manager.cpython-314.pyc +0 -0
  194. package/onecode/mcp/client.py +193 -0
  195. package/onecode/mcp/manager.py +191 -0
  196. package/onecode/memory/__init__.py +95 -0
  197. package/onecode/memory/__pycache__/__init__.cpython-314.pyc +0 -0
  198. package/onecode/memory/__pycache__/backend.cpython-314.pyc +0 -0
  199. package/onecode/memory/__pycache__/offload.cpython-314.pyc +0 -0
  200. package/onecode/memory/__pycache__/pyramid.cpython-314.pyc +0 -0
  201. package/onecode/memory/__pycache__/recall.cpython-314.pyc +0 -0
  202. package/onecode/memory/__pycache__/symbolic.cpython-314.pyc +0 -0
  203. package/onecode/memory/backend.py +185 -0
  204. package/onecode/memory/offload.py +114 -0
  205. package/onecode/memory/pyramid.py +202 -0
  206. package/onecode/memory/recall.py +168 -0
  207. package/onecode/memory/symbolic.py +164 -0
  208. package/onecode/models/__init__.py +4 -0
  209. package/onecode/models/__pycache__/__init__.cpython-314.pyc +0 -0
  210. package/onecode/models/__pycache__/errors.cpython-314.pyc +0 -0
  211. package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
  212. package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
  213. package/onecode/models/__pycache__/registry.cpython-314.pyc +0 -0
  214. package/onecode/models/errors.py +94 -0
  215. package/onecode/models/messages.py +662 -0
  216. package/onecode/models/provider.py +450 -0
  217. package/onecode/models/providers/__init__.py +17 -0
  218. package/onecode/models/providers/__pycache__/__init__.cpython-314.pyc +0 -0
  219. package/onecode/models/providers/__pycache__/anthropic_provider.cpython-314.pyc +0 -0
  220. package/onecode/models/providers/__pycache__/deepseek_provider.cpython-314.pyc +0 -0
  221. package/onecode/models/providers/__pycache__/glm_provider.cpython-314.pyc +0 -0
  222. package/onecode/models/providers/__pycache__/minimax_provider.cpython-314.pyc +0 -0
  223. package/onecode/models/providers/__pycache__/minimaxi_provider.cpython-314.pyc +0 -0
  224. package/onecode/models/providers/__pycache__/mock_provider.cpython-314.pyc +0 -0
  225. package/onecode/models/providers/__pycache__/ollama_provider.cpython-314.pyc +0 -0
  226. package/onecode/models/providers/__pycache__/openai_provider.cpython-314.pyc +0 -0
  227. package/onecode/models/providers/anthropic_provider.py +224 -0
  228. package/onecode/models/providers/deepseek_provider.py +210 -0
  229. package/onecode/models/providers/glm_provider.py +154 -0
  230. package/onecode/models/providers/minimax_provider.py +154 -0
  231. package/onecode/models/providers/minimaxi_provider.py +363 -0
  232. package/onecode/models/providers/ollama_provider.py +96 -0
  233. package/onecode/models/providers/openai_provider.py +233 -0
  234. package/onecode/models/registry.py +212 -0
  235. package/onecode/server/__init__.py +3 -0
  236. package/onecode/server/app.py +224 -0
  237. package/onecode/skills/__init__.py +11 -0
  238. package/onecode/skills/__pycache__/__init__.cpython-314.pyc +0 -0
  239. package/onecode/skills/__pycache__/argument_substitution.cpython-314.pyc +0 -0
  240. package/onecode/skills/__pycache__/create.cpython-314.pyc +0 -0
  241. package/onecode/skills/__pycache__/frontmatter.cpython-314.pyc +0 -0
  242. package/onecode/skills/__pycache__/loader.cpython-314.pyc +0 -0
  243. package/onecode/skills/__pycache__/manager.cpython-314.pyc +0 -0
  244. package/onecode/skills/__pycache__/model.cpython-314.pyc +0 -0
  245. package/onecode/skills/argument_substitution.py +39 -0
  246. package/onecode/skills/create.py +56 -0
  247. package/onecode/skills/frontmatter.py +33 -0
  248. package/onecode/skills/loader.py +157 -0
  249. package/onecode/skills/manager.py +75 -0
  250. package/onecode/skills/model.py +43 -0
  251. package/onecode/storage/__init__.py +0 -0
  252. package/onecode/storage/activity.py +64 -0
  253. package/onecode/storage/project.py +54 -0
  254. package/onecode/storage/session.py +113 -0
  255. package/onecode/tasks/__init__.py +26 -0
  256. package/onecode/tasks/manager.py +219 -0
  257. package/onecode/tasks/models.py +240 -0
  258. package/onecode/trace/__init__.py +0 -0
  259. package/onecode/trace/tracer.py +174 -0
  260. package/onecode/utils/__init__.py +0 -0
  261. package/onecode/utils/helpers.py +20 -0
  262. package/onecode-cli.js +2 -0
  263. package/package.json +32 -0
  264. package/pyproject.toml +60 -0
  265. package/run.js +86 -0
  266. package/tui/__init__.py +46 -0
  267. package/tui/__main__.py +4 -0
  268. package/tui/__pycache__/__init__.cpython-314.pyc +0 -0
  269. package/tui/__pycache__/_loop.cpython-314.pyc +0 -0
  270. package/tui/__pycache__/_path_fuzzy_search.cpython-314.pyc +0 -0
  271. package/tui/__pycache__/_path_match.cpython-314.pyc +0 -0
  272. package/tui/__pycache__/agent.cpython-314.pyc +0 -0
  273. package/tui/__pycache__/agent_schema.cpython-314.pyc +0 -0
  274. package/tui/__pycache__/agents.cpython-314.pyc +0 -0
  275. package/tui/__pycache__/answer.cpython-314.pyc +0 -0
  276. package/tui/__pycache__/app.cpython-314.pyc +0 -0
  277. package/tui/__pycache__/atomic.cpython-314.pyc +0 -0
  278. package/tui/__pycache__/complete.cpython-314.pyc +0 -0
  279. package/tui/__pycache__/constants.cpython-314.pyc +0 -0
  280. package/tui/__pycache__/conversation_markdown.cpython-314.pyc +0 -0
  281. package/tui/__pycache__/danger.cpython-314.pyc +0 -0
  282. package/tui/__pycache__/db.cpython-314.pyc +0 -0
  283. package/tui/__pycache__/dec.cpython-314.pyc +0 -0
  284. package/tui/__pycache__/directory.cpython-314.pyc +0 -0
  285. package/tui/__pycache__/directory_suggester.cpython-314.pyc +0 -0
  286. package/tui/__pycache__/directory_watcher.cpython-314.pyc +0 -0
  287. package/tui/__pycache__/format_path.cpython-314.pyc +0 -0
  288. package/tui/__pycache__/fuzzy.cpython-314.pyc +0 -0
  289. package/tui/__pycache__/fuzzy_index.cpython-314.pyc +0 -0
  290. package/tui/__pycache__/history.cpython-314.pyc +0 -0
  291. package/tui/__pycache__/jsonrpc.cpython-314.pyc +0 -0
  292. package/tui/__pycache__/menus.cpython-314.pyc +0 -0
  293. package/tui/__pycache__/message_log.cpython-314.pyc +0 -0
  294. package/tui/__pycache__/messages.cpython-314.pyc +0 -0
  295. package/tui/__pycache__/path_complete.cpython-314.pyc +0 -0
  296. package/tui/__pycache__/path_filter.cpython-314.pyc +0 -0
  297. package/tui/__pycache__/paths.cpython-314.pyc +0 -0
  298. package/tui/__pycache__/pill.cpython-314.pyc +0 -0
  299. package/tui/__pycache__/platform_commands.cpython-314.pyc +0 -0
  300. package/tui/__pycache__/protocol.cpython-314.pyc +0 -0
  301. package/tui/__pycache__/session_tracker.cpython-314.pyc +0 -0
  302. package/tui/__pycache__/settings.cpython-314.pyc +0 -0
  303. package/tui/__pycache__/settings_schema.cpython-314.pyc +0 -0
  304. package/tui/__pycache__/shell.cpython-314.pyc +0 -0
  305. package/tui/__pycache__/shell_read.cpython-314.pyc +0 -0
  306. package/tui/__pycache__/slash_command.cpython-314.pyc +0 -0
  307. package/tui/__pycache__/version.cpython-314.pyc +0 -0
  308. package/tui/_loop.py +86 -0
  309. package/tui/_path_fuzzy_search.py +48 -0
  310. package/tui/_path_match.py +141 -0
  311. package/tui/about.py +95 -0
  312. package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
  313. package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
  314. package/tui/acp/__pycache__/encode_tool_call_id.cpython-314.pyc +0 -0
  315. package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
  316. package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
  317. package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
  318. package/tui/acp/agent.py +886 -0
  319. package/tui/acp/api.py +55 -0
  320. package/tui/acp/encode_tool_call_id.py +12 -0
  321. package/tui/acp/messages.py +190 -0
  322. package/tui/acp/prompt.py +110 -0
  323. package/tui/acp/protocol.py +462 -0
  324. package/tui/agent.py +70 -0
  325. package/tui/agent_schema.py +76 -0
  326. package/tui/agents.py +45 -0
  327. package/tui/ansi/__init__.py +1 -0
  328. package/tui/ansi/__pycache__/__init__.cpython-314.pyc +0 -0
  329. package/tui/ansi/__pycache__/_ansi.cpython-314.pyc +0 -0
  330. package/tui/ansi/__pycache__/_ansi_colors.cpython-314.pyc +0 -0
  331. package/tui/ansi/__pycache__/_control_codes.cpython-314.pyc +0 -0
  332. package/tui/ansi/__pycache__/_keys.cpython-314.pyc +0 -0
  333. package/tui/ansi/__pycache__/_sgr_styles.cpython-314.pyc +0 -0
  334. package/tui/ansi/__pycache__/_stream_parser.cpython-314.pyc +0 -0
  335. package/tui/ansi/_ansi.py +1689 -0
  336. package/tui/ansi/_ansi_colors.py +264 -0
  337. package/tui/ansi/_control_codes.py +37 -0
  338. package/tui/ansi/_keys.py +251 -0
  339. package/tui/ansi/_sgr_styles.py +64 -0
  340. package/tui/ansi/_stream_parser.py +418 -0
  341. package/tui/answer.py +12 -0
  342. package/tui/app.py +1236 -0
  343. package/tui/atomic.py +37 -0
  344. package/tui/cli.py +313 -0
  345. package/tui/code_analyze.py +27 -0
  346. package/tui/complete.py +38 -0
  347. package/tui/constants.py +61 -0
  348. package/tui/conversation_markdown.py +30 -0
  349. package/tui/danger.py +372 -0
  350. package/tui/data/agents/ampcode.com.toml +51 -0
  351. package/tui/data/agents/augmentcode.com.toml +40 -0
  352. package/tui/data/agents/cdh.cloud-dev-harness.toml +22 -0
  353. package/tui/data/agents/claude.com.toml +41 -0
  354. package/tui/data/agents/copilot.github.com.toml +29 -0
  355. package/tui/data/agents/crowai.dev.toml +35 -0
  356. package/tui/data/agents/cursor.com.toml +38 -0
  357. package/tui/data/agents/deepagents.langchain.com +33 -0
  358. package/tui/data/agents/docker.com.toml +59 -0
  359. package/tui/data/agents/geminicli.com.toml +28 -0
  360. package/tui/data/agents/goose.ai.toml +51 -0
  361. package/tui/data/agents/inference.huggingface.co.toml +41 -0
  362. package/tui/data/agents/kimi.com.toml +35 -0
  363. package/tui/data/agents/kiro.dev.toml +42 -0
  364. package/tui/data/agents/openai.com.toml +53 -0
  365. package/tui/data/agents/openclaw.ai.toml +37 -0
  366. package/tui/data/agents/opencode.ai.toml +39 -0
  367. package/tui/data/agents/openhands.dev.toml +45 -0
  368. package/tui/data/agents/stakpak.dev.toml +61 -0
  369. package/tui/data/agents/vibe.mistral.ai.toml +30 -0
  370. package/tui/data/agents/vtcode.dev.toml +62 -0
  371. package/tui/data/images/frog.png +0 -0
  372. package/tui/data/sounds/question.wav +0 -0
  373. package/tui/data/sounds/turn-over.wav +0 -0
  374. package/tui/db.py +190 -0
  375. package/tui/dec.py +332 -0
  376. package/tui/directory.py +234 -0
  377. package/tui/directory_suggester.py +93 -0
  378. package/tui/directory_watcher.py +197 -0
  379. package/tui/format_path.py +25 -0
  380. package/tui/fuzzy.py +140 -0
  381. package/tui/fuzzy_index.py +275 -0
  382. package/tui/gist.py +33 -0
  383. package/tui/history.py +138 -0
  384. package/tui/jsonrpc.py +578 -0
  385. package/tui/menus.py +14 -0
  386. package/tui/message_log.py +69 -0
  387. package/tui/messages.py +139 -0
  388. package/tui/option_content.py +51 -0
  389. package/tui/os.py +0 -0
  390. package/tui/path_complete.py +217 -0
  391. package/tui/path_filter.py +124 -0
  392. package/tui/paths.py +71 -0
  393. package/tui/pill.py +23 -0
  394. package/tui/platform_commands.py +52 -0
  395. package/tui/prompt/__pycache__/extract.cpython-314.pyc +0 -0
  396. package/tui/prompt/__pycache__/resource.cpython-314.pyc +0 -0
  397. package/tui/prompt/extract.py +19 -0
  398. package/tui/prompt/resource.py +68 -0
  399. package/tui/protocol.py +28 -0
  400. package/tui/screens/__init__.py +0 -0
  401. package/tui/screens/__pycache__/__init__.cpython-314.pyc +0 -0
  402. package/tui/screens/__pycache__/log.cpython-314.pyc +0 -0
  403. package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
  404. package/tui/screens/__pycache__/projects_screen.cpython-314.pyc +0 -0
  405. package/tui/screens/__pycache__/sessions.cpython-314.pyc +0 -0
  406. package/tui/screens/__pycache__/settings.cpython-314.pyc +0 -0
  407. package/tui/screens/__pycache__/store.cpython-314.pyc +0 -0
  408. package/tui/screens/action_modal.py +94 -0
  409. package/tui/screens/agent_modal.py +175 -0
  410. package/tui/screens/command_edit_modal.py +74 -0
  411. package/tui/screens/log.py +553 -0
  412. package/tui/screens/main.py +411 -0
  413. package/tui/screens/main.tcss +655 -0
  414. package/tui/screens/permissions.py +404 -0
  415. package/tui/screens/permissions.tcss +72 -0
  416. package/tui/screens/projects.tcss +66 -0
  417. package/tui/screens/projects_app.py +319 -0
  418. package/tui/screens/projects_screen.py +142 -0
  419. package/tui/screens/session_resume_modal.py +172 -0
  420. package/tui/screens/session_resume_modal.tcss +35 -0
  421. package/tui/screens/sessions.py +166 -0
  422. package/tui/screens/sessions.tcss +119 -0
  423. package/tui/screens/settings.py +254 -0
  424. package/tui/screens/settings.tcss +101 -0
  425. package/tui/screens/store.py +500 -0
  426. package/tui/screens/store.tcss +274 -0
  427. package/tui/screens/tool_content_screen.py +59 -0
  428. package/tui/session_tracker.py +120 -0
  429. package/tui/settings.py +353 -0
  430. package/tui/settings_schema.py +431 -0
  431. package/tui/shell.py +304 -0
  432. package/tui/shell_read.py +42 -0
  433. package/tui/slash_command.py +34 -0
  434. package/tui/tui.tcss +246 -0
  435. package/tui/twitter.py +38 -0
  436. package/tui/version.py +80 -0
  437. package/tui/visuals/__init__.py +0 -0
  438. package/tui/visuals/__pycache__/__init__.cpython-314.pyc +0 -0
  439. package/tui/visuals/__pycache__/columns.cpython-314.pyc +0 -0
  440. package/tui/visuals/columns.py +273 -0
  441. package/tui/widgets/__init__.py +0 -0
  442. package/tui/widgets/__pycache__/__init__.cpython-314.pyc +0 -0
  443. package/tui/widgets/__pycache__/acp_content.cpython-314.pyc +0 -0
  444. package/tui/widgets/__pycache__/agent_response.cpython-314.pyc +0 -0
  445. package/tui/widgets/__pycache__/agent_thought.cpython-314.pyc +0 -0
  446. package/tui/widgets/__pycache__/ask_user.cpython-314.pyc +0 -0
  447. package/tui/widgets/__pycache__/condensed_path.cpython-314.pyc +0 -0
  448. package/tui/widgets/__pycache__/context_stats.cpython-314.pyc +0 -0
  449. package/tui/widgets/__pycache__/conversation.cpython-313.pyc +0 -0
  450. package/tui/widgets/__pycache__/conversation.cpython-314.pyc +0 -0
  451. package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
  452. package/tui/widgets/__pycache__/directory_input.cpython-314.pyc +0 -0
  453. package/tui/widgets/__pycache__/flash.cpython-314.pyc +0 -0
  454. package/tui/widgets/__pycache__/future_text.cpython-314.pyc +0 -0
  455. package/tui/widgets/__pycache__/grid_select.cpython-314.pyc +0 -0
  456. package/tui/widgets/__pycache__/highlighted_textarea.cpython-314.pyc +0 -0
  457. package/tui/widgets/__pycache__/mandelbrot.cpython-314.pyc +0 -0
  458. package/tui/widgets/__pycache__/markdown_note.cpython-314.pyc +0 -0
  459. package/tui/widgets/__pycache__/menu.cpython-314.pyc +0 -0
  460. package/tui/widgets/__pycache__/modified_files.cpython-314.pyc +0 -0
  461. package/tui/widgets/__pycache__/non_selectable_label.cpython-314.pyc +0 -0
  462. package/tui/widgets/__pycache__/note.cpython-314.pyc +0 -0
  463. package/tui/widgets/__pycache__/path_search.cpython-314.pyc +0 -0
  464. package/tui/widgets/__pycache__/plan.cpython-314.pyc +0 -0
  465. package/tui/widgets/__pycache__/project_directory_tree.cpython-314.pyc +0 -0
  466. package/tui/widgets/__pycache__/project_grid_select.cpython-314.pyc +0 -0
  467. package/tui/widgets/__pycache__/project_summary.cpython-314.pyc +0 -0
  468. package/tui/widgets/__pycache__/prompt.cpython-314.pyc +0 -0
  469. package/tui/widgets/__pycache__/question.cpython-314.pyc +0 -0
  470. package/tui/widgets/__pycache__/session_grid_select.cpython-314.pyc +0 -0
  471. package/tui/widgets/__pycache__/session_summary.cpython-314.pyc +0 -0
  472. package/tui/widgets/__pycache__/session_tabs.cpython-314.pyc +0 -0
  473. package/tui/widgets/__pycache__/shell_result.cpython-314.pyc +0 -0
  474. package/tui/widgets/__pycache__/shell_terminal.cpython-314.pyc +0 -0
  475. package/tui/widgets/__pycache__/side_bar.cpython-314.pyc +0 -0
  476. package/tui/widgets/__pycache__/slash_complete.cpython-314.pyc +0 -0
  477. package/tui/widgets/__pycache__/strike_text.cpython-314.pyc +0 -0
  478. package/tui/widgets/__pycache__/subagent.cpython-314.pyc +0 -0
  479. package/tui/widgets/__pycache__/terminal.cpython-313.pyc +0 -0
  480. package/tui/widgets/__pycache__/terminal.cpython-314.pyc +0 -0
  481. package/tui/widgets/__pycache__/throbber.cpython-314.pyc +0 -0
  482. package/tui/widgets/__pycache__/tool_call.cpython-313.pyc +0 -0
  483. package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
  484. package/tui/widgets/__pycache__/user_input.cpython-314.pyc +0 -0
  485. package/tui/widgets/acp_content.py +70 -0
  486. package/tui/widgets/agent_response.py +77 -0
  487. package/tui/widgets/agent_thought.py +168 -0
  488. package/tui/widgets/ask_user.py +264 -0
  489. package/tui/widgets/command_pane.py +223 -0
  490. package/tui/widgets/condensed_path.py +115 -0
  491. package/tui/widgets/context_stats.py +66 -0
  492. package/tui/widgets/conversation.py +2441 -0
  493. package/tui/widgets/danger_warning.py +65 -0
  494. package/tui/widgets/diff_view.py +59 -0
  495. package/tui/widgets/directory_input.py +8 -0
  496. package/tui/widgets/flash.py +81 -0
  497. package/tui/widgets/future_text.py +126 -0
  498. package/tui/widgets/grid_select.py +234 -0
  499. package/tui/widgets/highlighted_textarea.py +180 -0
  500. package/tui/widgets/mandelbrot.py +294 -0
  501. package/tui/widgets/markdown_note.py +13 -0
  502. package/tui/widgets/menu.py +147 -0
  503. package/tui/widgets/modified_files.py +106 -0
  504. package/tui/widgets/non_selectable_label.py +5 -0
  505. package/tui/widgets/note.py +18 -0
  506. package/tui/widgets/path_search.py +492 -0
  507. package/tui/widgets/plan.py +263 -0
  508. package/tui/widgets/project_directory_tree.py +90 -0
  509. package/tui/widgets/project_grid_select.py +35 -0
  510. package/tui/widgets/project_summary.py +33 -0
  511. package/tui/widgets/prompt.py +795 -0
  512. package/tui/widgets/question.py +360 -0
  513. package/tui/widgets/session_grid_select.py +224 -0
  514. package/tui/widgets/session_summary.py +115 -0
  515. package/tui/widgets/session_tabs.py +188 -0
  516. package/tui/widgets/shell_result.py +35 -0
  517. package/tui/widgets/shell_terminal.py +40 -0
  518. package/tui/widgets/side_bar.py +114 -0
  519. package/tui/widgets/slash_complete.py +235 -0
  520. package/tui/widgets/strike_text.py +70 -0
  521. package/tui/widgets/subagent.py +87 -0
  522. package/tui/widgets/terminal.py +577 -0
  523. package/tui/widgets/terminal_tool.py +351 -0
  524. package/tui/widgets/throbber.py +100 -0
  525. package/tui/widgets/tool_call.py +368 -0
  526. package/tui/widgets/user_input.py +23 -0
  527. package/tui/widgets/version.py +5 -0
  528. package/tui/widgets/welcome.py +31 -0
@@ -0,0 +1,1689 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ from itertools import accumulate
5
+ import re
6
+
7
+ from dataclasses import dataclass, field
8
+ from functools import lru_cache
9
+ from typing import Awaitable, Callable, Iterable, Literal, Mapping, NamedTuple
10
+
11
+ import rich.repr
12
+
13
+ from textual import events
14
+ from textual.color import Color
15
+ from textual.content import Content, EMPTY_CONTENT
16
+ from textual.geometry import clamp
17
+ from textual.style import Style, NULL_STYLE
18
+
19
+ from tui.ansi._ansi_colors import ANSI_COLORS
20
+ from tui.ansi._keys import TERMINAL_KEY_MAP, CURSOR_KEYS_APPLICATION
21
+ from tui.ansi._control_codes import CONTROL_CODES
22
+ from tui.ansi._sgr_styles import SGR_STYLES
23
+ from tui.ansi._stream_parser import (
24
+ StreamParser,
25
+ SeparatorToken,
26
+ PatternToken,
27
+ Pattern,
28
+ PatternCheck,
29
+ ParseResult,
30
+ Token,
31
+ )
32
+
33
+ from tui.dec import CHARSET_MAP
34
+
35
+
36
+ def character_range(start: int, end: int) -> frozenset:
37
+ """Build a set of characters between to code-points.
38
+
39
+ Args:
40
+ start: Start codepoint.
41
+ end: End codepoint (inclusive)
42
+
43
+ Returns:
44
+ A frozenset of the characters..
45
+ """
46
+ return frozenset(map(chr, range(start, end + 1)))
47
+
48
+
49
+ class ANSIToken:
50
+ pass
51
+
52
+
53
+ class DEC(NamedTuple):
54
+ slot: int
55
+ character_set: str
56
+
57
+
58
+ class DECInvoke(NamedTuple):
59
+ gl: int | None = None
60
+ gr: int | None = None
61
+ shift: int | None = None
62
+
63
+
64
+ DEC_SLOTS = {"(": 0, ")": 1, "*": 2, "+": 3, "-": 1, ".": 2, "//": 3}
65
+
66
+
67
+
68
+
69
+
70
+ class FEPattern(Pattern):
71
+ FINAL = character_range(0x30, 0x7E)
72
+ INTERMEDIATE = character_range(0x20, 0x2F)
73
+ CSI_TERMINATORS = character_range(0x40, 0x7E)
74
+ OSC_TERMINATORS = frozenset({"\x07", "\x9c"})
75
+ DSC_TERMINATORS = frozenset({"\x9c"})
76
+
77
+ def check(self) -> PatternCheck:
78
+ sequence = io.StringIO()
79
+ store = sequence.write
80
+ store(character := (yield))
81
+
82
+ match character:
83
+ # CSI
84
+ case "[":
85
+ CSI_TERMINATORS = self.CSI_TERMINATORS
86
+ while (character := (yield)) not in CSI_TERMINATORS:
87
+ store(character)
88
+ store(character)
89
+ return ("csi", sequence.getvalue())
90
+
91
+ # OSC
92
+ case "]":
93
+ last_character = ""
94
+ OSC_TERMINATORS = self.OSC_TERMINATORS
95
+ while (character := (yield)) not in OSC_TERMINATORS:
96
+ store(character)
97
+ if last_character == "\x1b" and character in {"\\", "\0x5c"}:
98
+ break
99
+ last_character = character
100
+ store(character)
101
+
102
+ return ("osc", sequence.getvalue())
103
+
104
+ # DCS
105
+ case "P":
106
+ last_character = ""
107
+ DSC_TERMINATORS = self.DSC_TERMINATORS
108
+ while (character := (yield)) not in DSC_TERMINATORS:
109
+ store(character)
110
+ if last_character == "\x1b" and character == "\\":
111
+ break
112
+ last_character = character
113
+ store(character)
114
+ return ("dcs", sequence.getvalue())
115
+
116
+ # Character set designation
117
+ case "(" | ")" | "*" | "+" | "-" | "." | "/":
118
+ if (character := (yield)) not in self.FINAL:
119
+ return False
120
+ store(character)
121
+ return ("dec", sequence.getvalue())
122
+
123
+ case "n" | "o" | "~" | "}" | "|" | "N" | "O":
124
+ return ("dec_invoke", sequence.getvalue())
125
+
126
+ # Line attribute
127
+ case "#":
128
+ store((yield))
129
+ return ("la", sequence.getvalue())
130
+ # ISO 2022: ESC SP
131
+ case " ":
132
+ store((yield))
133
+ return ("sp", sequence.getvalue())
134
+ case _:
135
+ return ("control", character)
136
+
137
+
138
+ class ANSIParser(StreamParser[tuple[str, str]]):
139
+ """Parse a stream of text containing escape sequences in to logical tokens."""
140
+
141
+ def parse(self) -> ParseResult[tuple[str, str]]:
142
+ NEW_LINE = "\n"
143
+ CARRIAGE_RETURN = "\r"
144
+ ESCAPE = "\x1b"
145
+ BACKSPACE = "\x08"
146
+
147
+ while True:
148
+ token = yield self.read_until(NEW_LINE, CARRIAGE_RETURN, ESCAPE, BACKSPACE)
149
+ if isinstance(token, SeparatorToken):
150
+ if token.text == ESCAPE:
151
+ token = yield self.read_patterns("\x1b", fe=FEPattern())
152
+ if isinstance(token, PatternToken):
153
+ yield token.value
154
+ else:
155
+ yield "separator", token.text
156
+ continue
157
+
158
+ yield "content", token.text
159
+
160
+
161
+ EMPTY_LINE = Content()
162
+
163
+
164
+ type ClearType = Literal["cursor_to_end", "cursor_to_beginning", "screen", "scrollback"]
165
+ ANSI_CLEAR: Mapping[int, ClearType] = {
166
+ 0: "cursor_to_end",
167
+ 1: "cursor_to_beginning",
168
+ 2: "screen",
169
+ 3: "scrollback",
170
+ }
171
+
172
+
173
+ @rich.repr.auto
174
+ class ANSIContent(NamedTuple):
175
+ """Content to be written to the terminal."""
176
+
177
+ text: str
178
+
179
+ def __rich_repr__(self) -> rich.repr.Result:
180
+ yield self.text
181
+
182
+
183
+ @rich.repr.auto
184
+ class ANSICursor(NamedTuple):
185
+ """Represents a single operation on the ANSI output.
186
+
187
+ All values may be `None` meaning "not set".
188
+ """
189
+
190
+ delta_x: int | None = None
191
+ """Relative x change."""
192
+ delta_y: int | None = None
193
+ """Relative y change."""
194
+ absolute_x: int | None = None
195
+ """Replace x."""
196
+ absolute_y: int | None = None
197
+ """Replace y."""
198
+ erase: bool = False
199
+ """Erase (replace with spaces)?"""
200
+ clear_range: tuple[int | None, int | None] | None = None
201
+ """Replace range (slice like)."""
202
+ relative: bool = False
203
+ """Should replace be relative (`False`) or absolute (`True`)"""
204
+ update_background: bool = False
205
+ """Optional style for remaining line."""
206
+ auto_scroll: bool = False
207
+ """Perform a scroll with the movement?"""
208
+ save: bool = False
209
+ """Save cursor position (DECSC)."""
210
+ restore: bool = False
211
+ """Restore cursor position (DECRC)."""
212
+ insert_lines: int = 0
213
+ """Insert blank lines at cursor (IL)."""
214
+ delete_lines: int = 0
215
+ """Delete lines at cursor (DL)."""
216
+
217
+ def __rich_repr__(self) -> rich.repr.Result:
218
+ yield "delta_x", self.delta_x, None
219
+ yield "delta_y", self.delta_y, None
220
+ yield "absolute_x", self.absolute_x, None
221
+ yield "absolute_y", self.absolute_y, None
222
+ yield "erase", self.erase, False
223
+ yield "clear_range", self.clear_range, None
224
+ yield "relative", self.relative, False
225
+ yield "update_background", self.update_background, False
226
+ yield "auto_scroll", self.auto_scroll, False
227
+
228
+ @lru_cache(maxsize=1024)
229
+ def get_clear_offsets(
230
+ self, cursor_offset: int, line_length: int
231
+ ) -> tuple[int, int]:
232
+ """Get replace offsets.
233
+
234
+ Args:
235
+ cursor_offset: Current cursor offset.
236
+ line_length: Length of line.
237
+
238
+ Returns:
239
+ A pair of offsets (inclusive).
240
+ """
241
+ assert (
242
+ self.clear_range is not None
243
+ ), "Only call this if the replace attribute has a value"
244
+ replace_start, replace_end = self.clear_range
245
+ if replace_start is None:
246
+ replace_start = cursor_offset
247
+ if replace_end is None:
248
+ replace_end = cursor_offset
249
+ if replace_start < 0:
250
+ replace_start = line_length + replace_start
251
+ if replace_end < 0:
252
+ replace_end = line_length + replace_end
253
+ if self.relative:
254
+ return (cursor_offset + replace_start, cursor_offset + replace_end)
255
+ else:
256
+ return (replace_start, replace_end)
257
+
258
+
259
+ @rich.repr.auto
260
+ class ANSINewLine:
261
+ """New line (diffrent in alternate buffer)"""
262
+
263
+
264
+ @rich.repr.auto
265
+ class ANSIStyle(NamedTuple):
266
+ """Update style."""
267
+
268
+ style: Style
269
+
270
+ def __rich_repr__(self) -> rich.repr.Result:
271
+ yield self.style
272
+
273
+
274
+ @rich.repr.auto
275
+ class ANSIClear(NamedTuple):
276
+ """Enumeration for clearing the 'screen'."""
277
+
278
+ clear: ClearType
279
+
280
+ def __rich_repr__(self) -> rich.repr.Result:
281
+ yield self.clear
282
+
283
+
284
+ @rich.repr.auto
285
+ class ANSIScrollMargin(NamedTuple):
286
+ """Set the scroll margin."""
287
+
288
+ top: int | None = None
289
+ bottom: int | None = None
290
+
291
+ def __rich_repr__(self) -> rich.repr.Result:
292
+ yield self.top
293
+ yield self.bottom
294
+
295
+
296
+ @rich.repr.auto
297
+ class ANSIScroll(NamedTuple):
298
+ """Scroll buffer."""
299
+
300
+ direction: Literal[+1, -1]
301
+ lines: int
302
+
303
+ def __rich_repr__(self) -> rich.repr.Result:
304
+ yield self.direction
305
+ yield self.lines
306
+
307
+
308
+ class ANSIFeatures(NamedTuple):
309
+ """Terminal feature flags."""
310
+
311
+ show_cursor: bool | None = None
312
+ alternate_screen: bool | None = None
313
+ bracketed_paste: bool | None = None
314
+ cursor_blink: bool | None = None
315
+ cursor_keys: bool | None = None
316
+ replace_mode: bool | None = None
317
+ auto_wrap: bool | None = None
318
+
319
+
320
+ MOUSE_TRACKING_MODES = Literal["button", "drag", "all"]
321
+ MOUSE_FORMAT = Literal["normal", "utf8", "sgr", "urxvt"]
322
+
323
+
324
+ class ANSIMouseTracking(NamedTuple):
325
+ """Set mouse tracking."""
326
+
327
+ mode: Literal["none"] | MOUSE_TRACKING_MODES | None = None
328
+ format: MOUSE_FORMAT | None = None
329
+ focus_events: bool | None = None
330
+ alternate_scroll: bool | None = None
331
+
332
+
333
+ # Not technically part of the terminal protocol
334
+ @rich.repr.auto
335
+ class ANSIWorkingDirectory(NamedTuple):
336
+ """Working directory changed"""
337
+
338
+ path: str
339
+
340
+ def __rich_repr__(self) -> rich.repr.Result:
341
+ yield self.path
342
+
343
+
344
+ @rich.repr.auto
345
+ class ANSICharacterSet(NamedTuple):
346
+ """Updated character set state."""
347
+
348
+ dec: DEC | None = None
349
+ dec_invoke: DECInvoke | None = None
350
+
351
+
352
+ @rich.repr.auto
353
+ class ANSICursorPositionRequest(NamedTuple):
354
+ pass
355
+
356
+
357
+ type ANSICommand = (
358
+ ANSIStyle
359
+ | ANSIContent
360
+ | ANSICursor
361
+ | ANSINewLine
362
+ | ANSIClear
363
+ | ANSIScrollMargin
364
+ | ANSIScroll
365
+ | ANSIWorkingDirectory
366
+ | ANSICharacterSet
367
+ | ANSIFeatures
368
+ | ANSIMouseTracking
369
+ | ANSICursorPositionRequest
370
+ )
371
+
372
+
373
+ class ANSIStream:
374
+ def __init__(self) -> None:
375
+ self.parser = ANSIParser()
376
+ self.style = NULL_STYLE
377
+ self.show_cursor = True
378
+
379
+ @classmethod
380
+ @lru_cache(maxsize=1024)
381
+ def _parse_sgr(cls, sgr: str) -> Style | None:
382
+ """Parse a SGR (Select Graphics Rendition) code in to a Style instance,
383
+ or `None` to indicate a reset.
384
+
385
+ Args:
386
+ sgr: SGR sequence.
387
+
388
+ Returns:
389
+ A Visual Style, or `None`.
390
+ """
391
+ codes = [
392
+ code if code < 255 else 255
393
+ for code in map(int, [sgr_code or "0" for sgr_code in sgr.split(";")])
394
+ ]
395
+ style = NULL_STYLE
396
+ while codes:
397
+ match codes:
398
+ case [38, 2, red, green, blue, *codes]:
399
+ # Foreground RGB
400
+ style += Style(foreground=Color(red, green, blue))
401
+ case [48, 2, red, green, blue, *codes]:
402
+ # Background RGB
403
+ style += Style(background=Color(red, green, blue))
404
+ case [38, 5, ansi_color, *codes]:
405
+ # Foreground ANSI
406
+ style += Style(foreground=ANSI_COLORS[ansi_color])
407
+ case [48, 5, ansi_color, *codes]:
408
+ # Background ANSI
409
+ style += Style(background=ANSI_COLORS[ansi_color])
410
+ case [0, *codes]:
411
+ # reset
412
+ return None
413
+ case [code, *codes]:
414
+ if sgr_style := SGR_STYLES.get(code):
415
+ style += sgr_style
416
+
417
+ return style
418
+
419
+ def feed(self, text: str) -> Iterable[ANSICommand]:
420
+ """Feed text potentially containing ANSI sequences, and parse in to
421
+ an iterable of ansi commands.
422
+
423
+ Args:
424
+ text: Text to feed.
425
+
426
+ Yields:
427
+ `ANSICommand` instances.
428
+ """
429
+
430
+ for token in self.parser.feed(text):
431
+ if not isinstance(token, Token):
432
+ yield from self.on_token(token)
433
+
434
+ ANSI_SEPARATORS = {
435
+ "\n": ANSICursor(delta_y=+1, absolute_x=0),
436
+ "\r": ANSICursor(absolute_x=0),
437
+ "\x08": ANSICursor(delta_x=-1),
438
+ }
439
+ CLEAR_LINE_CURSOR_TO_END = ANSICursor(
440
+ clear_range=(None, -1), erase=True, update_background=True
441
+ )
442
+ CLEAR_LINE_CURSOR_TO_BEGINNING = ANSICursor(
443
+ clear_range=(0, None), erase=True, update_background=True
444
+ )
445
+ CLEAR_LINE = ANSICursor(clear_range=(0, -1), erase=True, update_background=True)
446
+ CLEAR_SCREEN_CURSOR_TO_END = ANSIClear("cursor_to_end")
447
+ CLEAR_SCREEN_CURSOR_TO_BEGINNING = ANSIClear("cursor_to_beginning")
448
+ CLEAR_SCREEN = ANSIClear("screen")
449
+ CLEAR_SCREEN_SCROLLBACK = ANSIClear("scrollback")
450
+ SHOW_CURSOR = ANSIFeatures(show_cursor=True)
451
+ HIDE_CURSOR = ANSIFeatures(show_cursor=False)
452
+ ENABLE_ALTERNATE_SCREEN = ANSIFeatures(alternate_screen=True)
453
+ DISABLE_ALTERNATE_SCREEN = ANSIFeatures(alternate_screen=False)
454
+ ENABLE_BRACKETED_PASTE = ANSIFeatures(bracketed_paste=True)
455
+ DISABLE_BRACKETED_PASTE = ANSIFeatures(bracketed_paste=False)
456
+ ENABLE_CURSOR_BLINK = ANSIFeatures(cursor_blink=True)
457
+ DISABLE_CURSOR_BLINK = ANSIFeatures(cursor_blink=False)
458
+ ENABLE_CURSOR_KEYS_APPLICATION_MODE = ANSIFeatures(cursor_keys=True)
459
+ DISABLE_CURSOR_KEYS_APPLICATION_MODE = ANSIFeatures(cursor_keys=False)
460
+ ENABLE_REPLACE_MODE = ANSIFeatures(replace_mode=True)
461
+ DISABLE_REPLACE_MODE = ANSIFeatures(replace_mode=False)
462
+ ENABLE_AUTO_WRAP = ANSIFeatures(auto_wrap=True)
463
+ DISABLE_AUTO_WRAP = ANSIFeatures(auto_wrap=False)
464
+
465
+ INVOKE_G2_INTO_GL = DECInvoke(gl=2)
466
+ INVOKE_G3_INTO_GL = DECInvoke(gl=3)
467
+ INVOKE_G1_INTO_GR = DECInvoke(gr=1)
468
+ INVOKE_G2_INTO_GR = DECInvoke(gr=2)
469
+ INVOKE_G3_INTO_GR = DECInvoke(gr=3)
470
+ SHIFT_G2 = DECInvoke(shift=2)
471
+ SHIFT_G3 = DECInvoke(shift=3)
472
+
473
+ DEC_INVOKE_MAP = {
474
+ "n": INVOKE_G2_INTO_GL,
475
+ "o": INVOKE_G3_INTO_GL,
476
+ "~": INVOKE_G1_INTO_GR,
477
+ "}": INVOKE_G2_INTO_GR,
478
+ "|": INVOKE_G3_INTO_GR,
479
+ "N": SHIFT_G2,
480
+ "O": SHIFT_G3,
481
+ }
482
+
483
+ @classmethod
484
+ @lru_cache(maxsize=1024)
485
+ def _parse_csi(cls, csi: str) -> ANSICommand | None:
486
+ """Parse CSI sequence in to an ansi segment.
487
+
488
+ Args:
489
+ csi: CSI sequence.
490
+
491
+ Returns:
492
+ Ansi segment, or `None` if one couldn't be decoded.
493
+ """
494
+
495
+ if match := re.fullmatch(r"\[(\d+)?(?:;)?(\d*)?(\w)", csi):
496
+ match_groups = match.groups(default="")
497
+ match match_groups:
498
+ case [lines, _, "A"]:
499
+ # CUU - Cursor Up: ESC[nA
500
+ return ANSICursor(delta_y=-int(lines or 1))
501
+ case [lines, _, "B"]:
502
+ # CUD - Cursor Down: ESC[nB
503
+ return ANSICursor(delta_y=+int(lines or 1))
504
+ case [cells, _, "C"]:
505
+ # CUF - Cursor Forward: ESC[nC
506
+ return ANSICursor(delta_x=+int(cells or 1))
507
+ case [cells, _, "D"]:
508
+ # CUB - Cursor Back: ESC[nD
509
+ return ANSICursor(delta_x=-int(cells or 1))
510
+ case [lines, _, "E"]:
511
+ # CNL - Cursor Next Line: ESC[nE
512
+ return ANSICursor(absolute_x=0, delta_y=+int(lines or 1))
513
+ case [lines, _, "F"]:
514
+ # CPL - Cursor Previous Line: ESC[nF
515
+ return ANSICursor(absolute_x=0, delta_y=-int(lines or 1))
516
+ case [cells, _, "G"]:
517
+ # CHA - Cursor Horizontal Absolute: ESC[nG
518
+ return ANSICursor(absolute_x=+int(cells or 1) - 1)
519
+ case [row, column, "H" | "f"]:
520
+ # CUP - Cursor Position: ESC[n;mH
521
+ # HVP - Horizontal Vertical Position: ESC[n;mf
522
+ return ANSICursor(
523
+ absolute_x=int(column or 1) - 1,
524
+ absolute_y=int(row or 1) - 1,
525
+ )
526
+ case [characters, _, "P"]:
527
+ return ANSICursor(
528
+ clear_range=(0, int(characters or 1) - 1),
529
+ relative=True,
530
+ erase=True,
531
+ )
532
+ case [lines, _, "S"]:
533
+ return ANSIScroll(-1, int(lines))
534
+ case [lines, _, "T"]:
535
+ return ANSIScroll(+1, int(lines))
536
+ case [lines, _, "L"]:
537
+ return ANSICursor(insert_lines=int(lines or 1))
538
+ case [lines, _, "M"]:
539
+ return ANSICursor(delete_lines=int(lines or 1))
540
+ case [row, _, "d"]:
541
+ # VPA - Vertical Position Absolute: ESC[nd
542
+ return ANSICursor(absolute_y=int(row or 1) - 1)
543
+ case [characters, _, "X"]:
544
+ return ANSICursor(
545
+ clear_range=(0, int(characters or 1) - 1),
546
+ relative=True,
547
+ erase=False,
548
+ )
549
+ case ["0" | "", _, "J"]:
550
+ return cls.CLEAR_SCREEN_CURSOR_TO_END
551
+ case ["1", _, "J"]:
552
+ return cls.CLEAR_SCREEN_CURSOR_TO_BEGINNING
553
+ case ["2", _, "J"]:
554
+ return cls.CLEAR_SCREEN
555
+ case ["3", _, "J"]:
556
+ return cls.CLEAR_SCREEN_SCROLLBACK
557
+ case ["0" | "", _, "K"]:
558
+ return cls.CLEAR_LINE_CURSOR_TO_END
559
+ case ["1", _, "K"]:
560
+ return cls.CLEAR_LINE_CURSOR_TO_BEGINNING
561
+ case ["2", _, "K"]:
562
+ return cls.CLEAR_LINE
563
+ case [top, bottom, "r"]:
564
+ return ANSIScrollMargin(
565
+ int(top or "1") - 1 if top else None,
566
+ int(bottom or "1") - 1 if top else None,
567
+ )
568
+ case ["4", _, "h" | "l" as replace_mode]:
569
+ return (
570
+ cls.ENABLE_REPLACE_MODE
571
+ if replace_mode == "h"
572
+ else cls.DISABLE_REPLACE_MODE
573
+ )
574
+
575
+ case ["6", _, "n"]:
576
+ return ANSICursorPositionRequest()
577
+
578
+ case _:
579
+ return None
580
+
581
+ elif match := re.fullmatch(r"\[([0-9:;<=>?]*)([!-/]*)([@-~])", csi):
582
+ match match.groups(default=""):
583
+ case ["?25", "", "h"]:
584
+ return cls.SHOW_CURSOR
585
+ case ["?25", "", "l"]:
586
+ return cls.HIDE_CURSOR
587
+ case ["?1049", "", "h"]:
588
+ return cls.ENABLE_ALTERNATE_SCREEN
589
+ case ["?1049", "", "l"]:
590
+ return cls.DISABLE_ALTERNATE_SCREEN
591
+ case ["?2004", "", "h"]:
592
+ return cls.ENABLE_BRACKETED_PASTE
593
+ case ["?2004", "", "l"]:
594
+ return cls.DISABLE_BRACKETED_PASTE
595
+ case ["?12", "", "h"]:
596
+ return cls.ENABLE_CURSOR_BLINK
597
+ case ["?12", "", "l"]:
598
+ return cls.DISABLE_CURSOR_BLINK
599
+ case ["?1", "", "h"]:
600
+ return cls.ENABLE_CURSOR_KEYS_APPLICATION_MODE
601
+ case ["?1", "", "l"]:
602
+ return cls.DISABLE_CURSOR_KEYS_APPLICATION_MODE
603
+ case ["?7", "", "h"]:
604
+ return cls.ENABLE_AUTO_WRAP
605
+ case ["?7", "", "l"]:
606
+ return cls.DISABLE_AUTO_WRAP
607
+
608
+ # \x1b[22;0;0t
609
+ case [param1, param2, "t"]:
610
+ # 't' = XTWINOPS (Window manipulation)
611
+ return None
612
+ case _:
613
+ if match := re.fullmatch(r"\[\?([0-9;]+)([hl])", csi):
614
+ modes = [m for m in match.group(1).split(";")]
615
+ enable = match.group(2) == "h"
616
+ tracking: Literal["none"] | MOUSE_TRACKING_MODES | None = None
617
+ format: MOUSE_FORMAT | None = None
618
+ focus_events: bool | None = None
619
+ alternate_scroll: bool | None = None
620
+ for mode in modes:
621
+ if mode == "1000":
622
+ tracking = "button" if enable else "none"
623
+ elif mode == "1002":
624
+ tracking = "drag" if enable else "none"
625
+ elif mode == "1003":
626
+ tracking = "all" if enable else "none"
627
+ elif mode == "1006":
628
+ format = "sgr"
629
+ elif mode == "1015":
630
+ format = "urxvt"
631
+ elif mode == "1004":
632
+ focus_events = enable
633
+ elif mode == "1007":
634
+ alternate_scroll = enable
635
+ return ANSIMouseTracking(
636
+ mode=tracking,
637
+ format=format,
638
+ focus_events=focus_events,
639
+ alternate_scroll=alternate_scroll,
640
+ )
641
+ else:
642
+ return None
643
+
644
+ return None
645
+
646
+ def on_token(self, token: tuple[str, str]) -> Iterable[ANSICommand]:
647
+ match token:
648
+ case ["separator", separator]:
649
+ if separator == "\n":
650
+ yield ANSINewLine()
651
+ else:
652
+ yield self.ANSI_SEPARATORS[separator]
653
+
654
+ case ["osc", osc]:
655
+ match osc[1:].split(";"):
656
+ case ["8", *_, link]:
657
+ self.style += Style(link=link or None)
658
+ case ["2025", current_directory, *_]:
659
+ self.current_directory = current_directory
660
+ yield ANSIWorkingDirectory(current_directory)
661
+
662
+ case ["csi", csi]:
663
+ if csi.endswith("m"):
664
+ if (sgr_style := self._parse_sgr(csi[1:-1])) is None:
665
+ self.style = NULL_STYLE
666
+ else:
667
+ self.style += sgr_style
668
+ # Special case to use widget background rather
669
+ # than theme background
670
+ if (
671
+ sgr_style.background is not None
672
+ and sgr_style.background.ansi == -1
673
+ ):
674
+ self.style = (
675
+ Style(foreground=self.style.foreground)
676
+ + sgr_style.without_color
677
+ )
678
+ yield ANSIStyle(self.style)
679
+ else:
680
+ if (ansi_segment := self._parse_csi(csi)) is not None:
681
+ yield ansi_segment
682
+
683
+ case ["dec", dec]:
684
+ slot, character_set = list(dec)
685
+ yield ANSICharacterSet(DEC(DEC_SLOTS[slot], character_set))
686
+
687
+ case ["dec_invoke", dec_invoke]:
688
+ yield ANSICharacterSet(dec_invoke=self.DEC_INVOKE_MAP[dec_invoke[0]])
689
+
690
+ case ["control", code]:
691
+ if (control := CONTROL_CODES.get(code)) is not None:
692
+ if control == "ri": # control code
693
+ yield ANSICursor(delta_y=-1, auto_scroll=True)
694
+ elif control == "ind":
695
+ yield ANSICursor(delta_y=+1, auto_scroll=True)
696
+ elif control == "decsc":
697
+ yield ANSICursor(save=True)
698
+ elif control == "decrc":
699
+ yield ANSICursor(restore=True)
700
+ else:
701
+ pass
702
+ else:
703
+ pass
704
+
705
+ case ["content", text]:
706
+ yield ANSIContent(text)
707
+
708
+ case _:
709
+ pass
710
+
711
+
712
+ class LineFold(NamedTuple):
713
+ """A line from the terminal, folded for presentation."""
714
+
715
+ line_no: int
716
+ """The (unfolded) line number."""
717
+
718
+ line_offset: int
719
+ """The index of the folded line."""
720
+
721
+ offset: int
722
+ """The offset within the original line."""
723
+
724
+ content: Content
725
+ """The content."""
726
+
727
+ updates: int = 0
728
+ """Integer that increments on update."""
729
+
730
+
731
+ @dataclass
732
+ class LineRecord:
733
+ """A single line in the terminal."""
734
+
735
+ content: Content
736
+ """The content."""
737
+
738
+ style: Style = NULL_STYLE
739
+ """The style for the remaining line."""
740
+
741
+ folds: list[LineFold] = field(default_factory=list)
742
+ """Line "folds" for wrapped lines."""
743
+
744
+ updates: int = 0
745
+ """An integer used for caching."""
746
+
747
+
748
+ @rich.repr.auto
749
+ class ScrollMargin(NamedTuple):
750
+ """Margins at the top and bottom of a window that won't scroll."""
751
+
752
+ top: int | None = None
753
+ """Margin at the top (in lines), or `None` for no scroll margin set."""
754
+ bottom: int | None = None
755
+ """Margin at the bottom (in lines), or `None` for no scroll margin set."""
756
+
757
+ def __rich_repr__(self) -> rich.repr.Result:
758
+ yield self.top
759
+ yield self.bottom
760
+
761
+ def get_line_range(self, height: int) -> tuple[int, int]:
762
+ """Get the scrollable line range (inclusive).
763
+
764
+ Args:
765
+ height: terminal height.
766
+
767
+ Returns:
768
+ A tuple of the (exclusive) top and bottom line numbers that scroll.
769
+ """
770
+ return (
771
+ self.top or 0,
772
+ height - 1 if self.bottom is None else self.bottom,
773
+ )
774
+
775
+
776
+ @dataclass
777
+ class Buffer:
778
+ """A terminal buffer (scrollback or alternate)"""
779
+
780
+ name: str = "buffer"
781
+ """Name of the buffer (debugging aid)."""
782
+ lines: list[LineRecord] = field(default_factory=list)
783
+ """unfolded lines."""
784
+ line_to_fold: list[int] = field(default_factory=list)
785
+ """An index from folded lines on to unfolded lines."""
786
+ folded_lines: list[LineFold] = field(default_factory=list)
787
+ """Folded lines."""
788
+ scroll_margin: ScrollMargin = ScrollMargin(None, None)
789
+ """Scroll margins"""
790
+ cursor_line: int = 0
791
+ """Folded line index."""
792
+ cursor_offset: int = 0
793
+ """Folded line offset."""
794
+ max_line_width: int = 0
795
+ """The longest line in the buffer."""
796
+ updates: int = 0
797
+ """Updates count (used in caching)."""
798
+ _updated_lines: set[int] | None = None
799
+
800
+ @property
801
+ def line_count(self) -> int:
802
+ """Total number of lines."""
803
+ return len(self.lines)
804
+
805
+ @property
806
+ def height(self) -> int:
807
+ """Height of the buffer (number of folded lines)."""
808
+ height = len(self.folded_lines)
809
+ if (
810
+ height == 1
811
+ and not self.lines[-1].content.plain
812
+ and self.name == "scrollback"
813
+ ):
814
+ height -= 1
815
+ return height
816
+
817
+ @property
818
+ def last_line_no(self) -> int:
819
+ """Index of last lines."""
820
+ return len(self.lines) - 1
821
+
822
+ @property
823
+ def cursor(self) -> tuple[int, int]:
824
+ """The cursor offset within the un-folded lines."""
825
+
826
+ if self.cursor_line >= len(self.folded_lines):
827
+ return (len(self.folded_lines), 0)
828
+ cursor_folded_line = self.folded_lines[self.cursor_line]
829
+ cursor_line_offset = cursor_folded_line.line_offset
830
+ line_no = cursor_folded_line.line_no
831
+ line = self.lines[line_no]
832
+ position = 0
833
+ for folded_line_offset, folded_line in enumerate(line.folds):
834
+ if folded_line_offset == cursor_line_offset:
835
+ position += self.cursor_offset
836
+ break
837
+ position += len(folded_line.content)
838
+
839
+ return (line_no, position)
840
+
841
+ @property
842
+ def is_blank(self) -> bool:
843
+ """Is this buffer blank (spaces in all lines)?"""
844
+ return not any(
845
+ (line.content.plain.strip() or line.content.spans) for line in self.lines
846
+ )
847
+
848
+ def update_cursor(self, line_no: int, cursor_line_offset: int) -> None:
849
+ """Move the cursor to the given unfolded line and offset.
850
+
851
+ Sets `cursor_line` and `cursor_offset`.
852
+
853
+ Args:
854
+ line_no: Unfolded line number.
855
+ cursor_line_offset: Offset within the line.
856
+ """
857
+ line = self.lines[line_no]
858
+ fold_line_start = self.line_to_fold[line_no]
859
+ position = 0
860
+ fold_offset = 0
861
+ for fold_offset, fold in enumerate(line.folds):
862
+ line_length = len(fold.content)
863
+ if (
864
+ cursor_line_offset >= position
865
+ and cursor_line_offset < position + line_length
866
+ ):
867
+ self.cursor_line = fold_line_start + fold_offset
868
+ self.cursor_offset = cursor_line_offset - position
869
+ break
870
+ position += line_length
871
+ else:
872
+ self.cursor_line = fold_line_start + len(line.folds) - 1
873
+ self.cursor_offset = len(line.folds[-1].content)
874
+
875
+ def update_line(self, line_no: int) -> None:
876
+ """Record an updated line.
877
+
878
+ Args:
879
+ line_no: Line number to update.
880
+ """
881
+ if self._updated_lines is not None:
882
+ self._updated_lines.add(line_no)
883
+
884
+ def clear(self, updates: int) -> None:
885
+ """Clear the buffer to its initial state.
886
+
887
+ Args:
888
+ updates: the initial updates index.
889
+
890
+ """
891
+ del self.lines[:]
892
+ del self.line_to_fold[:]
893
+ del self.folded_lines[:]
894
+ self.cursor_line = 0
895
+ self.cursor_offset = 0
896
+ self.max_line_width = 0
897
+ self.updates = updates
898
+
899
+ def remove_last_line(self) -> None:
900
+ if not self.lines:
901
+ return
902
+ last_line_index = len(self.lines) - 1
903
+ del self.lines[-1]
904
+ del self.folded_lines[self.line_to_fold[last_line_index] :]
905
+ del self.line_to_fold[last_line_index]
906
+ self.updates += 1
907
+
908
+
909
+ @dataclass
910
+ class DECState:
911
+ """The (somewhat bonkers) mechanism for switching characters sets pre-unicode."""
912
+
913
+ slots: list[str] = field(default_factory=lambda: ["B", "B", "<", "0"])
914
+ gl_slot: int = 0
915
+ gr_slot: int = 2
916
+ shift: int | None = None
917
+
918
+ @property
919
+ def gl(self) -> str:
920
+ return self.slots[self.gl_slot]
921
+
922
+ @property
923
+ def gr(self) -> str:
924
+ return self.slots[self.gr_slot]
925
+
926
+ def update(self, dec: DEC | None, dec_invoke: DECInvoke | None) -> None:
927
+ if dec is not None:
928
+ self.slots[dec.slot] = dec.character_set
929
+ elif dec_invoke is not None:
930
+ if dec_invoke.shift:
931
+ self.shift = dec_invoke.shift
932
+ else:
933
+ if dec_invoke.gl is not None:
934
+ self.gl_slot = dec_invoke.gl
935
+ elif dec_invoke.gr is not None:
936
+ self.gr_slot = dec_invoke.gr
937
+
938
+ def translate(self, text: str) -> str:
939
+ translate_table: dict[int, str] | None
940
+ first_character: str | None = None
941
+ if self.shift is not None and (
942
+ translate_table := CHARSET_MAP.get(self.slots[self.shift], None)
943
+ ):
944
+ first_character = text[0].translate(translate_table)
945
+ self.shift = None
946
+
947
+ if translate_table := CHARSET_MAP.get(self.gl, None):
948
+ text = text.translate(translate_table)
949
+ if first_character is None:
950
+ return text
951
+ return f"{first_character}{text}"
952
+
953
+
954
+ @dataclass
955
+ class MouseTracking:
956
+ """The mouse tracking state."""
957
+
958
+ tracking: MOUSE_TRACKING_MODES = "all"
959
+ format: MOUSE_FORMAT = "normal"
960
+ focus_events: bool = False
961
+ alternate_scroll: bool = False
962
+
963
+
964
+ @rich.repr.auto
965
+ class TerminalState:
966
+ """Abstract terminal state."""
967
+
968
+ def __init__(
969
+ self,
970
+ write_stdin: Callable[[str], Awaitable],
971
+ *,
972
+ width: int = 80,
973
+ height: int = 24,
974
+ ) -> None:
975
+ """
976
+ Args:
977
+ width: Initial width.
978
+ height: Initial height.
979
+ """
980
+ self._write_stdin = write_stdin
981
+
982
+ self._ansi_stream = ANSIStream()
983
+ """ANSI stream processor."""
984
+
985
+ self.width = width
986
+ """Width of the terminal."""
987
+ self.height = height
988
+ """Height of the terminal."""
989
+ self.style = NULL_STYLE
990
+ """The current style."""
991
+ self.show_cursor = True
992
+ """Is the cursor visible?"""
993
+ self.alternate_screen = False
994
+ """Is the terminal in the alternate buffer state?"""
995
+ self.bracketed_paste = False
996
+ """Is bracketed pase enabled?"""
997
+ self.cursor_blink = False
998
+ """Should the cursor blink?"""
999
+ self.cursor_keys = False
1000
+ """Is cursor keys application mode enabled?"""
1001
+ self.replace_mode = True
1002
+ """Should content replaces characters (`True`) or insert (`False`)?"""
1003
+ self.auto_wrap = True
1004
+ """Should content wrap?"""
1005
+ self.current_directory: str = ""
1006
+ """Current working directory."""
1007
+ self.scrollback_buffer = Buffer("scrollback")
1008
+ """Scrollbar buffer lines."""
1009
+ self.alternate_buffer = Buffer("alternate")
1010
+ """Alternate buffer lines."""
1011
+ self.dec_state = DECState()
1012
+ """The DEC (character set) state."""
1013
+ self.mouse_tracking: MouseTracking | None = None
1014
+ """The mouse tracking state."""
1015
+
1016
+ self._saved_cursor: tuple[int, int, DECState] | None = None
1017
+ """Saved cursor position and DEC state (DECSC/DECRC)."""
1018
+
1019
+ self._updates: int = 0
1020
+ """Incrementing integer used in caching."""
1021
+
1022
+ def __rich_repr__(self) -> rich.repr.Result:
1023
+ yield "width", self.width
1024
+ yield "height", self.height
1025
+ yield "style", self.style, NULL_STYLE
1026
+ yield "show_cursor", self.show_cursor, True
1027
+ yield "alternate_screen", self.alternate_screen, False
1028
+ yield "bracketed_paste", self.bracketed_paste, False
1029
+ yield "cursor_blink", self.cursor_blink, False
1030
+ yield "replace_mode", self.replace_mode, True
1031
+ yield "auto_wrap", self.auto_wrap, True
1032
+ yield "dec_state", self.dec_state
1033
+ yield "mouse_tracking", self.mouse_tracking, None
1034
+
1035
+ async def write_stdin(self, text: str) -> bool:
1036
+ if self._write_stdin is not None:
1037
+ return await self._write_stdin(text)
1038
+ return False
1039
+ return True
1040
+
1041
+ @property
1042
+ def screen_start_line_no(self) -> int:
1043
+ return max(0, self.scrollback_buffer.height - self.height)
1044
+
1045
+ @property
1046
+ def screen_end_line_no(self) -> int:
1047
+ return self.buffer.height
1048
+
1049
+ @property
1050
+ def updates(self) -> int:
1051
+ """An integer that advanvces when the state is changed."""
1052
+ return self._updates
1053
+
1054
+ @property
1055
+ def buffer(self) -> Buffer:
1056
+ """The buffer (scrollack or alternate)"""
1057
+ if self.alternate_screen:
1058
+ return self.alternate_buffer
1059
+ return self.scrollback_buffer
1060
+
1061
+ @property
1062
+ def max_line_width(self) -> int | None:
1063
+ return self.scrollback_buffer.max_line_width
1064
+
1065
+ def advance_updates(self) -> int:
1066
+ """Advance the `updates` integer and return it.
1067
+
1068
+ Returns:
1069
+ int: Updates.
1070
+ """
1071
+ self._updates += 1
1072
+ return self._updates
1073
+
1074
+ def update_size(self, width: int | None = None, height: int | None = None) -> None:
1075
+ """Update the dimensions of the terminal.
1076
+
1077
+ Args:
1078
+ width: New width, or `None` for no change.
1079
+ height: New height, or `None` for no change.
1080
+ """
1081
+ previous_width = self.width
1082
+ if width is not None:
1083
+ self.width = width
1084
+ if height is not None:
1085
+ self.height = height
1086
+
1087
+ if previous_width != width:
1088
+ self._reflow()
1089
+
1090
+ def key_event_to_stdin(self, event: events.Key) -> str | None:
1091
+ """Get the stdin string for a key event.
1092
+
1093
+ This will depend on the terminal state.
1094
+
1095
+ Args:
1096
+ event: Key event.
1097
+
1098
+ Returns:
1099
+ A string to be sent to stdin, or `None` if no key was produced.
1100
+ """
1101
+ if (
1102
+ self.cursor_keys
1103
+ and (sequence := CURSOR_KEYS_APPLICATION.get(event.key)) is not None
1104
+ ):
1105
+ return sequence
1106
+
1107
+ if (mapped_key := TERMINAL_KEY_MAP.get(event.key)) is not None:
1108
+ return mapped_key
1109
+ if event.character:
1110
+ return event.character
1111
+ return None
1112
+
1113
+ def key_escape(self) -> str:
1114
+ """Generate the escape sequence for the escape key.
1115
+
1116
+ Returns:
1117
+ str: ANSI escape sequences.
1118
+ """
1119
+ return "\x1b"
1120
+
1121
+ def remove_trailing_blank_lines_from_scrollback(self) -> None:
1122
+ """Remove blank lines at the end of the scrollback buffer.
1123
+
1124
+ A line is considered blank if it is whitespace and has no color or style applied.
1125
+
1126
+ """
1127
+ buffer = self.scrollback_buffer
1128
+ while buffer.lines:
1129
+ last_line_content = buffer.lines[-1].content
1130
+ if last_line_content.spans or last_line_content.plain.rstrip():
1131
+ break
1132
+ buffer.remove_last_line()
1133
+
1134
+ def _reflow(self) -> None:
1135
+ buffer = self.buffer
1136
+ if not buffer.lines:
1137
+ return
1138
+
1139
+ buffer._updated_lines = None
1140
+ # Unfolded cursor position
1141
+ cursor_line, cursor_offset = buffer.cursor
1142
+
1143
+ buffer.folded_lines.clear()
1144
+ buffer.line_to_fold.clear()
1145
+ width = self.width
1146
+
1147
+ for line_no, line_record in enumerate(buffer.lines):
1148
+ line_expanded_tabs = line_record.content.expand_tabs(8)
1149
+ line_record.folds[:] = self._fold_line(line_no, line_expanded_tabs, width)
1150
+ line_record.updates = self.advance_updates()
1151
+ buffer.line_to_fold.append(len(buffer.folded_lines))
1152
+ buffer.folded_lines.extend(line_record.folds)
1153
+
1154
+ # After reflow, we need to work out where the cursor is within the folded lines
1155
+ # cursor_line = min(cursor_line, len(buffer.lines) - 1)
1156
+ if cursor_line >= len(buffer.lines):
1157
+ buffer.cursor_line = len(buffer.lines)
1158
+ buffer.cursor_offset = 0
1159
+ else:
1160
+ line = buffer.lines[cursor_line]
1161
+ fold_cursor_line = buffer.line_to_fold[cursor_line]
1162
+
1163
+ fold_cursor_offset = 0
1164
+ for fold in reversed(line.folds):
1165
+ if cursor_offset >= fold.offset:
1166
+ fold_cursor_line += fold.line_offset
1167
+ fold_cursor_offset = cursor_offset - fold.offset
1168
+ break
1169
+
1170
+ buffer.cursor_line = fold_cursor_line
1171
+ buffer.cursor_offset = fold_cursor_offset
1172
+
1173
+ async def write(
1174
+ self, text: str, *, hide_output: bool = False
1175
+ ) -> tuple[set[int] | None, set[int] | None]:
1176
+ """Write to the terminal.
1177
+
1178
+ Args:
1179
+ text: Text to write.
1180
+ hide_output: Hide visible output from buffers.
1181
+
1182
+ Returns:
1183
+ A pair of deltas or `None for full refresh, for scrollback and alternate screen.
1184
+ """
1185
+ alternate_buffer = self.alternate_buffer
1186
+ scrollback_buffer = self.scrollback_buffer
1187
+
1188
+ # Reset updated lines delta
1189
+ alternate_buffer._updated_lines = set()
1190
+ scrollback_buffer._updated_lines = set()
1191
+ # Write sequences and update
1192
+ if hide_output:
1193
+ for ansi_command in self._ansi_stream.feed(text):
1194
+ if not isinstance(ansi_command, (ANSIContent, ANSICursor)):
1195
+ await self._handle_ansi_command(ansi_command)
1196
+ else:
1197
+ for ansi_command in self._ansi_stream.feed(text):
1198
+ await self._handle_ansi_command(ansi_command)
1199
+
1200
+ # Get deltas
1201
+ scrollback_updates = (
1202
+ None
1203
+ if scrollback_buffer._updated_lines is None
1204
+ else scrollback_buffer._updated_lines.copy()
1205
+ )
1206
+ alternate_updates = (
1207
+ None
1208
+ if alternate_buffer._updated_lines is None
1209
+ else alternate_buffer._updated_lines.copy()
1210
+ )
1211
+ # Reset deltas
1212
+ self.alternate_buffer._updated_lines = set()
1213
+ self.scrollback_buffer._updated_lines = set()
1214
+ # Return deltas accumulated during write
1215
+ return (scrollback_updates, alternate_updates)
1216
+
1217
+ def get_cursor_line_offset(self, buffer: Buffer) -> int:
1218
+ """The cursor offset within the un-folded lines."""
1219
+ cursor_folded_line = buffer.folded_lines[buffer.cursor_line]
1220
+ cursor_line_offset = cursor_folded_line.line_offset
1221
+ line_no = cursor_folded_line.line_no
1222
+ line = buffer.lines[line_no]
1223
+ position = 0
1224
+ for folded_line_offset, folded_line in enumerate(line.folds):
1225
+ if folded_line_offset == cursor_line_offset:
1226
+ position += buffer.cursor_offset
1227
+ break
1228
+ position += len(folded_line.content)
1229
+ return position
1230
+
1231
+ def clear_buffer(self, clear: ClearType) -> None:
1232
+ buffer = self.buffer
1233
+ if clear == "screen":
1234
+ buffer.clear(self.advance_updates())
1235
+ buffer._updated_lines = None
1236
+ elif clear == "cursor_to_end":
1237
+ buffer._updated_lines = None
1238
+ folded_cursor_line = buffer.cursor_line
1239
+ cursor_line, cursor_line_offset = buffer.cursor
1240
+ while buffer.cursor_line >= len(buffer.folded_lines):
1241
+ self.add_line(buffer, EMPTY_LINE)
1242
+ line = buffer.lines[cursor_line]
1243
+ del buffer.lines[cursor_line + 1 :]
1244
+ del buffer.line_to_fold[cursor_line + 1 :]
1245
+ del buffer.folded_lines[folded_cursor_line + 1 :]
1246
+ self.update_line(buffer, cursor_line, line.content[:cursor_line_offset])
1247
+ else:
1248
+ # print(f"TODO: clear_buffer({clear!r})")
1249
+ buffer.clear(self.advance_updates())
1250
+
1251
+ def scroll_buffer(self, direction: int, lines: int) -> None:
1252
+ """Scroll the buffer.
1253
+
1254
+ Args:
1255
+ direction: +1 for down, -1 for up.
1256
+ lines: Number of lines.
1257
+ """
1258
+ buffer = self.buffer
1259
+ margin_top, margin_bottom = buffer.scroll_margin.get_line_range(self.height)
1260
+
1261
+ gutter_lines = max(0, buffer.height - self.height)
1262
+
1263
+ if direction == -1:
1264
+ # up (first in test)
1265
+ for line_no in range(margin_top, margin_bottom + 1):
1266
+ copy_line_no = line_no + lines
1267
+ copy_content = EMPTY_CONTENT
1268
+ copy_style = NULL_STYLE
1269
+ if copy_line_no <= margin_bottom:
1270
+ try:
1271
+ copy_line = buffer.lines[copy_line_no + gutter_lines]
1272
+ except IndexError:
1273
+ pass
1274
+ else:
1275
+ copy_content = copy_line.content
1276
+ copy_style = copy_line.style
1277
+
1278
+ self.update_line(
1279
+ buffer, line_no + gutter_lines, copy_content, copy_style
1280
+ )
1281
+ else:
1282
+ # down
1283
+ for line_no in reversed(range(margin_top, margin_bottom + 1)):
1284
+ copy_line_no = line_no - lines
1285
+ copy_content = EMPTY_CONTENT
1286
+ copy_style = NULL_STYLE
1287
+ if copy_line_no >= margin_top:
1288
+ try:
1289
+ copy_line = buffer.lines[copy_line_no + gutter_lines]
1290
+ except IndexError:
1291
+ pass
1292
+ else:
1293
+ copy_content = copy_line.content
1294
+ copy_style = copy_line.style
1295
+ self.update_line(
1296
+ buffer, line_no + gutter_lines, copy_content, copy_style
1297
+ )
1298
+
1299
+ @classmethod
1300
+ def _expand_content(cls, content: Content, offset: int, style: Style) -> Content:
1301
+ """Expand content to be at least as long as a given offset.
1302
+
1303
+ Args:
1304
+ content: Content to expand.
1305
+ offset: Offset within the content.
1306
+ style: Style of padding.
1307
+
1308
+ Returns:
1309
+ New Content.
1310
+ """
1311
+ if offset > len(content):
1312
+ content += Content.blank(offset - len(content), style)
1313
+ return content
1314
+
1315
+ async def _handle_ansi_command(self, ansi_command: ANSICommand) -> None:
1316
+ if isinstance(ansi_command, ANSINewLine):
1317
+ if self.alternate_screen:
1318
+ # New line behaves differently in alternate screen
1319
+ ansi_command = ANSICursor(delta_y=+1, auto_scroll=True)
1320
+ else:
1321
+ ansi_command = ANSICursor(delta_y=+1, absolute_x=0)
1322
+
1323
+ match ansi_command:
1324
+ case ANSIStyle(style):
1325
+ self.style = style
1326
+
1327
+ case ANSIContent(text):
1328
+ buffer = self.buffer
1329
+ folded_lines = buffer.folded_lines
1330
+ while buffer.cursor_line >= len(folded_lines):
1331
+ self.add_line(buffer, EMPTY_LINE)
1332
+ folded_line = folded_lines[buffer.cursor_line]
1333
+ previous_content = folded_line.content
1334
+ line_no = folded_line.line_no
1335
+ line = buffer.lines[line_no]
1336
+
1337
+ cursor_line_offset = self.get_cursor_line_offset(buffer)
1338
+ line_content = line.content
1339
+ if cursor_line_offset > len(line_content):
1340
+ line_content = self._expand_content(
1341
+ line_content, cursor_line_offset, line.style
1342
+ )
1343
+ content = Content.styled(
1344
+ self.dec_state.translate(text),
1345
+ self.style,
1346
+ strip_control_codes=False,
1347
+ )
1348
+ if self.replace_mode:
1349
+ updated_line = Content.assemble(
1350
+ line_content[:cursor_line_offset],
1351
+ content,
1352
+ line_content[cursor_line_offset + len(content) :],
1353
+ strip_control_codes=False,
1354
+ )
1355
+ else:
1356
+ updated_line = Content.assemble(
1357
+ line_content[:cursor_line_offset],
1358
+ content,
1359
+ line_content[cursor_line_offset:],
1360
+ strip_control_codes=False,
1361
+ )
1362
+ self.update_line(buffer, line_no, updated_line)
1363
+ buffer.update_cursor(line_no, cursor_line_offset + len(content))
1364
+ buffer.updates = self.advance_updates()
1365
+
1366
+ case ANSICursor(
1367
+ delta_x,
1368
+ delta_y,
1369
+ absolute_x,
1370
+ absolute_y,
1371
+ erase,
1372
+ clear_range,
1373
+ _relative,
1374
+ update_background,
1375
+ auto_scroll,
1376
+ save,
1377
+ restore,
1378
+ insert_lines,
1379
+ delete_lines,
1380
+ ):
1381
+ buffer = self.buffer
1382
+
1383
+ if save:
1384
+ self._saved_cursor = (
1385
+ buffer.cursor_offset,
1386
+ buffer.cursor_line,
1387
+ self.dec_state,
1388
+ )
1389
+ if restore:
1390
+ if self._saved_cursor is not None:
1391
+ buffer.cursor_offset, buffer.cursor_line, self.dec_state = (
1392
+ self._saved_cursor
1393
+ )
1394
+
1395
+ if insert_lines:
1396
+ count = insert_lines
1397
+ margin_top, margin_bottom = buffer.scroll_margin.get_line_range(self.height)
1398
+ gutter_lines = max(0, buffer.height - self.height)
1399
+ if margin_top <= buffer.cursor_line <= margin_bottom:
1400
+ region_top = buffer.cursor_line
1401
+ for _ in range(count):
1402
+ for line_no in range(margin_bottom, region_top, -1):
1403
+ copy_line_no = line_no - 1
1404
+ try:
1405
+ src = buffer.lines[copy_line_no + gutter_lines]
1406
+ except IndexError:
1407
+ self.update_line(buffer, line_no + gutter_lines, EMPTY_CONTENT, NULL_STYLE)
1408
+ else:
1409
+ self.update_line(buffer, line_no + gutter_lines, src.content, src.style)
1410
+ self.update_line(buffer, region_top + gutter_lines, EMPTY_CONTENT, NULL_STYLE)
1411
+
1412
+ if delete_lines:
1413
+ count = delete_lines
1414
+ margin_top, margin_bottom = buffer.scroll_margin.get_line_range(self.height)
1415
+ gutter_lines = max(0, buffer.height - self.height)
1416
+ if margin_top <= buffer.cursor_line <= margin_bottom:
1417
+ region_top = buffer.cursor_line
1418
+ for _ in range(count):
1419
+ for line_no in range(region_top, margin_bottom):
1420
+ copy_line_no = line_no + 1
1421
+ try:
1422
+ src = buffer.lines[copy_line_no + gutter_lines]
1423
+ except IndexError:
1424
+ self.update_line(buffer, line_no + gutter_lines, EMPTY_CONTENT, NULL_STYLE)
1425
+ else:
1426
+ self.update_line(buffer, line_no + gutter_lines, src.content, src.style)
1427
+ self.update_line(buffer, margin_bottom + gutter_lines, EMPTY_CONTENT, NULL_STYLE)
1428
+
1429
+ folded_lines = buffer.folded_lines
1430
+ while buffer.cursor_line >= len(folded_lines):
1431
+ self.add_line(buffer, EMPTY_LINE)
1432
+
1433
+ if auto_scroll and delta_y is not None:
1434
+ margins = buffer.scroll_margin.get_line_range(self.height)
1435
+ margin_top, margin_bottom = margins
1436
+
1437
+ screen_cursor_line = buffer.cursor_line - self.screen_start_line_no
1438
+
1439
+ if (
1440
+ screen_cursor_line >= margin_top
1441
+ and screen_cursor_line <= margin_bottom
1442
+ ):
1443
+ start_line_no = self.screen_start_line_no
1444
+
1445
+ scroll_cursor = screen_cursor_line + delta_y
1446
+ if scroll_cursor > (start_line_no + margin_bottom):
1447
+ self.scroll_buffer(-1, 1)
1448
+ return
1449
+ elif scroll_cursor < (start_line_no + margin_top):
1450
+ self.scroll_buffer(+1, 1)
1451
+ return
1452
+
1453
+ folded_line = folded_lines[buffer.cursor_line]
1454
+ previous_content = folded_line.content
1455
+ line = buffer.lines[folded_line.line_no]
1456
+ if update_background:
1457
+ line.style = self.style
1458
+
1459
+ if clear_range is not None:
1460
+ cursor_line_offset = self.get_cursor_line_offset(buffer)
1461
+
1462
+ line_content = line.content
1463
+ if cursor_line_offset > len(line.content):
1464
+ line_content = self._expand_content(
1465
+ line.content, cursor_line_offset, line.style
1466
+ )
1467
+
1468
+ # Start and end replace are *inclusive*
1469
+ clear_start, clear_end = ansi_command.get_clear_offsets(
1470
+ cursor_line_offset, len(line_content)
1471
+ )
1472
+
1473
+ before_clear = line_content[:clear_start]
1474
+ after_clear = line_content[clear_end + 1 :]
1475
+
1476
+ if erase:
1477
+ # Range is remove
1478
+ updated_line = Content.assemble(
1479
+ before_clear,
1480
+ after_clear,
1481
+ strip_control_codes=False,
1482
+ )
1483
+ self.update_line(buffer, folded_line.line_no, updated_line)
1484
+ else:
1485
+ # Range is replaced with spaces
1486
+ blank_width = clear_end - clear_start + 1
1487
+
1488
+ updated_line = Content.assemble(
1489
+ before_clear,
1490
+ Content.blank(blank_width, self.style),
1491
+ after_clear,
1492
+ strip_control_codes=False,
1493
+ )
1494
+ self.update_line(buffer, folded_line.line_no, updated_line)
1495
+
1496
+ if not previous_content.is_same(folded_line.content):
1497
+ buffer.updates = self.advance_updates()
1498
+
1499
+ if delta_x is not None:
1500
+ buffer.cursor_offset = clamp(
1501
+ buffer.cursor_offset + delta_x, 0, self.width - 1
1502
+ )
1503
+ buffer.update_line(buffer.cursor_line)
1504
+ if absolute_x is not None:
1505
+ buffer.cursor_offset = clamp(absolute_x, 0, self.width - 1)
1506
+ buffer.update_line(buffer.cursor_line)
1507
+
1508
+ current_cursor_line = buffer.cursor_line
1509
+ if delta_y is not None:
1510
+ buffer.update_line(buffer.cursor_line)
1511
+ buffer.cursor_line = max(
1512
+ self.screen_start_line_no, buffer.cursor_line + delta_y
1513
+ )
1514
+ buffer.update_line(buffer.cursor_line)
1515
+ if absolute_y is not None:
1516
+ buffer.update_line(buffer.cursor_line)
1517
+ if buffer.name == "scrollback":
1518
+ buffer.cursor_line = self.screen_start_line_no + max(
1519
+ 0, absolute_y
1520
+ )
1521
+ else:
1522
+ buffer.cursor_line = max(0, absolute_y)
1523
+ buffer.update_line(buffer.cursor_line)
1524
+
1525
+ if current_cursor_line != buffer.cursor_line:
1526
+ # Simplify when the cursor moves away from the current line
1527
+ line.content.simplify() # Reduce segments
1528
+ self._line_updated(buffer, current_cursor_line)
1529
+ self._line_updated(buffer, buffer.cursor_line)
1530
+
1531
+ case ANSIFeatures() as features:
1532
+ if features.show_cursor is not None:
1533
+ self.show_cursor = features.show_cursor
1534
+ if features.alternate_screen is not None:
1535
+ self.alternate_screen = features.alternate_screen
1536
+ if features.bracketed_paste is not None:
1537
+ self.bracketed_paste = features.bracketed_paste
1538
+ if features.cursor_blink is not None:
1539
+ self.cursor_blink = features.cursor_blink
1540
+ if features.cursor_keys is not None:
1541
+ self.cursor_keys = features.cursor_keys
1542
+ if features.replace_mode is not None:
1543
+ self.replace_mode = features.replace_mode
1544
+ if features.auto_wrap is not None:
1545
+ self.auto_wrap = features.auto_wrap
1546
+ self.advance_updates()
1547
+
1548
+ case ANSIClear(clear):
1549
+ self.clear_buffer(clear)
1550
+
1551
+ case ANSIScrollMargin(top, bottom):
1552
+ self.buffer.scroll_margin = ScrollMargin(top, bottom)
1553
+ # Setting the scroll margins moves the cursor to (1, 1)
1554
+ buffer = self.buffer
1555
+ self._line_updated(buffer, buffer.cursor_line)
1556
+ buffer.cursor_line = 0
1557
+ buffer.cursor_offset = 0
1558
+ self._line_updated(buffer, buffer.cursor_line)
1559
+
1560
+ case ANSIScroll(direction, lines):
1561
+ self.scroll_buffer(direction, lines)
1562
+
1563
+ case ANSICharacterSet(dec, dec_invoke):
1564
+ self.dec_state.update(dec, dec_invoke)
1565
+
1566
+ case ANSIWorkingDirectory(path):
1567
+ self.current_directory = path
1568
+
1569
+ case ANSIMouseTracking(tracking, format, focus_events, alternate_scroll):
1570
+ if tracking == "none":
1571
+ self.mouse_tracking = None
1572
+ return
1573
+ if (mouse_tracking := self.mouse_tracking) is None:
1574
+ mouse_tracking = self.mouse_tracking = MouseTracking()
1575
+ if tracking is not None:
1576
+ mouse_tracking.tracking = tracking
1577
+ if format is not None:
1578
+ mouse_tracking.format = format
1579
+ if focus_events is not None:
1580
+ mouse_tracking.focus_events = focus_events
1581
+ if alternate_scroll is not None:
1582
+ mouse_tracking.alternate_scroll = alternate_scroll
1583
+
1584
+ case ANSICursorPositionRequest():
1585
+ row = self.buffer.cursor_line + 1
1586
+ column = self.buffer.cursor_offset + 1
1587
+ await self.write_stdin(f"\x1b[{row};{column}R")
1588
+
1589
+ case _:
1590
+ pass
1591
+
1592
+ def _line_updated(self, buffer: Buffer, line_no: int) -> None:
1593
+ """Mark a line has having been udpated.
1594
+
1595
+ Args:
1596
+ buffer: Buffer to use.
1597
+ line_no: Line number to mark as updated.
1598
+ """
1599
+ try:
1600
+ buffer.lines[line_no].updates = self.advance_updates()
1601
+ if buffer._updated_lines is not None:
1602
+ buffer._updated_lines.add(line_no)
1603
+ except IndexError:
1604
+ pass
1605
+
1606
+ def _fold_line(self, line_no: int, line: Content, width: int) -> list[LineFold]:
1607
+ updates = self._updates
1608
+ if not self.auto_wrap:
1609
+ return [LineFold(line_no, 0, 0, line, updates)]
1610
+ if not width:
1611
+ return [LineFold(0, 0, 0, line, updates)]
1612
+ line_length = line.cell_length
1613
+ if line_length <= width:
1614
+ return [LineFold(line_no, 0, 0, line, updates)]
1615
+
1616
+ folded_lines = line.fold(width)
1617
+ offsets = [0, *accumulate(len(line) for line in folded_lines)][:-1]
1618
+ folds = [
1619
+ LineFold(line_no, line_offset, offset, folded_line, updates)
1620
+ for line_offset, (offset, folded_line) in enumerate(
1621
+ zip(offsets, folded_lines)
1622
+ )
1623
+ ]
1624
+ assert len(folds)
1625
+ return folds
1626
+
1627
+ def add_line(
1628
+ self, buffer: Buffer, content: Content, style: Style = NULL_STYLE
1629
+ ) -> None:
1630
+ updates = self.advance_updates()
1631
+ line_no = buffer.line_count
1632
+ width = self.width
1633
+ line_record = LineRecord(
1634
+ content,
1635
+ style,
1636
+ self._fold_line(line_no, content, width),
1637
+ updates,
1638
+ )
1639
+ buffer.lines.append(line_record)
1640
+ folds = line_record.folds
1641
+ buffer.line_to_fold.append(len(buffer.folded_lines))
1642
+ fold_count = len(buffer.folded_lines)
1643
+ if buffer._updated_lines is not None:
1644
+ buffer._updated_lines.update(range(fold_count, fold_count + len(folds)))
1645
+ buffer.folded_lines.extend(folds)
1646
+ buffer.updates = updates
1647
+
1648
+ def update_line(
1649
+ self, buffer: Buffer, line_index: int, line: Content, style: Style | None = None
1650
+ ) -> None:
1651
+ """Update a line (potentially refolding and moving subsequencte lines down).
1652
+
1653
+ Args:
1654
+ buffer: Buffer.
1655
+ line_index: Line index (unfolded).
1656
+ line: New line content.
1657
+ style: New background style, or `None` not to update.
1658
+ """
1659
+ while line_index >= len(buffer.lines):
1660
+ self.add_line(buffer, EMPTY_LINE)
1661
+
1662
+ line_expanded_tabs = line.expand_tabs(8)
1663
+ buffer.max_line_width = max(
1664
+ line_expanded_tabs.cell_length, buffer.max_line_width
1665
+ )
1666
+ line_record = buffer.lines[line_index]
1667
+ line_record.content = line
1668
+ if style is not None:
1669
+ line_record.style = style
1670
+ line_record.folds[:] = self._fold_line(
1671
+ line_index, line_expanded_tabs, self.width
1672
+ )
1673
+ line_record.updates = self.advance_updates()
1674
+
1675
+ if buffer._updated_lines is not None:
1676
+ fold_start = buffer.line_to_fold[line_index]
1677
+ buffer._updated_lines.update(
1678
+ range(fold_start, fold_start + len(line_record.folds))
1679
+ )
1680
+
1681
+ fold_line = buffer.line_to_fold[line_index]
1682
+ del buffer.line_to_fold[line_index:]
1683
+ del buffer.folded_lines[fold_line:]
1684
+
1685
+ for line_no in range(line_index, buffer.line_count):
1686
+ line_record = buffer.lines[line_no]
1687
+ buffer.line_to_fold.append(len(buffer.folded_lines))
1688
+ for fold in line_record.folds:
1689
+ buffer.folded_lines.append(fold)