codemaster-cli 2.2.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 (170) hide show
  1. codemaster_cli-2.2.0.dist-info/METADATA +645 -0
  2. codemaster_cli-2.2.0.dist-info/RECORD +170 -0
  3. codemaster_cli-2.2.0.dist-info/WHEEL +4 -0
  4. codemaster_cli-2.2.0.dist-info/entry_points.txt +3 -0
  5. vibe/__init__.py +6 -0
  6. vibe/acp/__init__.py +0 -0
  7. vibe/acp/acp_agent_loop.py +746 -0
  8. vibe/acp/entrypoint.py +81 -0
  9. vibe/acp/tools/__init__.py +0 -0
  10. vibe/acp/tools/base.py +100 -0
  11. vibe/acp/tools/builtins/bash.py +134 -0
  12. vibe/acp/tools/builtins/read_file.py +54 -0
  13. vibe/acp/tools/builtins/search_replace.py +129 -0
  14. vibe/acp/tools/builtins/todo.py +65 -0
  15. vibe/acp/tools/builtins/write_file.py +98 -0
  16. vibe/acp/tools/session_update.py +118 -0
  17. vibe/acp/utils.py +213 -0
  18. vibe/cli/__init__.py +0 -0
  19. vibe/cli/autocompletion/__init__.py +0 -0
  20. vibe/cli/autocompletion/base.py +22 -0
  21. vibe/cli/autocompletion/path_completion.py +177 -0
  22. vibe/cli/autocompletion/slash_command.py +99 -0
  23. vibe/cli/cli.py +188 -0
  24. vibe/cli/clipboard.py +69 -0
  25. vibe/cli/commands.py +116 -0
  26. vibe/cli/entrypoint.py +163 -0
  27. vibe/cli/history_manager.py +91 -0
  28. vibe/cli/plan_offer/adapters/http_whoami_gateway.py +67 -0
  29. vibe/cli/plan_offer/decide_plan_offer.py +87 -0
  30. vibe/cli/plan_offer/ports/whoami_gateway.py +23 -0
  31. vibe/cli/terminal_setup.py +323 -0
  32. vibe/cli/textual_ui/__init__.py +0 -0
  33. vibe/cli/textual_ui/ansi_markdown.py +58 -0
  34. vibe/cli/textual_ui/app.py +1546 -0
  35. vibe/cli/textual_ui/app.tcss +1020 -0
  36. vibe/cli/textual_ui/external_editor.py +32 -0
  37. vibe/cli/textual_ui/handlers/__init__.py +5 -0
  38. vibe/cli/textual_ui/handlers/event_handler.py +147 -0
  39. vibe/cli/textual_ui/widgets/__init__.py +0 -0
  40. vibe/cli/textual_ui/widgets/approval_app.py +192 -0
  41. vibe/cli/textual_ui/widgets/banner/banner.py +85 -0
  42. vibe/cli/textual_ui/widgets/banner/petit_chat.py +195 -0
  43. vibe/cli/textual_ui/widgets/braille_renderer.py +58 -0
  44. vibe/cli/textual_ui/widgets/chat_input/__init__.py +7 -0
  45. vibe/cli/textual_ui/widgets/chat_input/body.py +214 -0
  46. vibe/cli/textual_ui/widgets/chat_input/completion_manager.py +58 -0
  47. vibe/cli/textual_ui/widgets/chat_input/completion_popup.py +43 -0
  48. vibe/cli/textual_ui/widgets/chat_input/container.py +195 -0
  49. vibe/cli/textual_ui/widgets/chat_input/text_area.py +365 -0
  50. vibe/cli/textual_ui/widgets/compact.py +41 -0
  51. vibe/cli/textual_ui/widgets/config_app.py +171 -0
  52. vibe/cli/textual_ui/widgets/context_progress.py +30 -0
  53. vibe/cli/textual_ui/widgets/load_more.py +43 -0
  54. vibe/cli/textual_ui/widgets/loading.py +201 -0
  55. vibe/cli/textual_ui/widgets/messages.py +277 -0
  56. vibe/cli/textual_ui/widgets/no_markup_static.py +11 -0
  57. vibe/cli/textual_ui/widgets/path_display.py +28 -0
  58. vibe/cli/textual_ui/widgets/proxy_setup_app.py +127 -0
  59. vibe/cli/textual_ui/widgets/question_app.py +496 -0
  60. vibe/cli/textual_ui/widgets/spinner.py +194 -0
  61. vibe/cli/textual_ui/widgets/status_message.py +76 -0
  62. vibe/cli/textual_ui/widgets/teleport_message.py +31 -0
  63. vibe/cli/textual_ui/widgets/tool_widgets.py +371 -0
  64. vibe/cli/textual_ui/widgets/tools.py +201 -0
  65. vibe/cli/textual_ui/windowing/__init__.py +29 -0
  66. vibe/cli/textual_ui/windowing/history.py +105 -0
  67. vibe/cli/textual_ui/windowing/history_windowing.py +71 -0
  68. vibe/cli/textual_ui/windowing/state.py +105 -0
  69. vibe/cli/update_notifier/__init__.py +47 -0
  70. vibe/cli/update_notifier/adapters/filesystem_update_cache_repository.py +59 -0
  71. vibe/cli/update_notifier/adapters/github_update_gateway.py +101 -0
  72. vibe/cli/update_notifier/adapters/pypi_update_gateway.py +107 -0
  73. vibe/cli/update_notifier/ports/update_cache_repository.py +16 -0
  74. vibe/cli/update_notifier/ports/update_gateway.py +53 -0
  75. vibe/cli/update_notifier/update.py +139 -0
  76. vibe/cli/update_notifier/whats_new.py +49 -0
  77. vibe/core/__init__.py +5 -0
  78. vibe/core/agent_loop.py +1075 -0
  79. vibe/core/agents/__init__.py +31 -0
  80. vibe/core/agents/manager.py +165 -0
  81. vibe/core/agents/models.py +122 -0
  82. vibe/core/auth/__init__.py +6 -0
  83. vibe/core/auth/crypto.py +137 -0
  84. vibe/core/auth/github.py +178 -0
  85. vibe/core/autocompletion/__init__.py +0 -0
  86. vibe/core/autocompletion/completers.py +257 -0
  87. vibe/core/autocompletion/file_indexer/__init__.py +10 -0
  88. vibe/core/autocompletion/file_indexer/ignore_rules.py +156 -0
  89. vibe/core/autocompletion/file_indexer/indexer.py +179 -0
  90. vibe/core/autocompletion/file_indexer/store.py +169 -0
  91. vibe/core/autocompletion/file_indexer/watcher.py +71 -0
  92. vibe/core/autocompletion/fuzzy.py +189 -0
  93. vibe/core/autocompletion/path_prompt.py +108 -0
  94. vibe/core/autocompletion/path_prompt_adapter.py +149 -0
  95. vibe/core/config.py +673 -0
  96. vibe/core/config_PATCH_INSTRUCTIONS.md +77 -0
  97. vibe/core/llm/__init__.py +0 -0
  98. vibe/core/llm/backend/anthropic.py +630 -0
  99. vibe/core/llm/backend/base.py +38 -0
  100. vibe/core/llm/backend/factory.py +7 -0
  101. vibe/core/llm/backend/generic.py +425 -0
  102. vibe/core/llm/backend/mistral.py +381 -0
  103. vibe/core/llm/backend/vertex.py +115 -0
  104. vibe/core/llm/exceptions.py +195 -0
  105. vibe/core/llm/format.py +184 -0
  106. vibe/core/llm/message_utils.py +24 -0
  107. vibe/core/llm/types.py +120 -0
  108. vibe/core/middleware.py +209 -0
  109. vibe/core/output_formatters.py +85 -0
  110. vibe/core/paths/__init__.py +0 -0
  111. vibe/core/paths/config_paths.py +68 -0
  112. vibe/core/paths/global_paths.py +40 -0
  113. vibe/core/programmatic.py +56 -0
  114. vibe/core/prompts/__init__.py +32 -0
  115. vibe/core/prompts/cli.md +111 -0
  116. vibe/core/prompts/compact.md +48 -0
  117. vibe/core/prompts/dangerous_directory.md +5 -0
  118. vibe/core/prompts/explore.md +50 -0
  119. vibe/core/prompts/project_context.md +8 -0
  120. vibe/core/prompts/tests.md +1 -0
  121. vibe/core/proxy_setup.py +65 -0
  122. vibe/core/session/session_loader.py +222 -0
  123. vibe/core/session/session_logger.py +318 -0
  124. vibe/core/session/session_migration.py +41 -0
  125. vibe/core/skills/__init__.py +7 -0
  126. vibe/core/skills/manager.py +132 -0
  127. vibe/core/skills/models.py +92 -0
  128. vibe/core/skills/parser.py +39 -0
  129. vibe/core/system_prompt.py +466 -0
  130. vibe/core/telemetry/__init__.py +0 -0
  131. vibe/core/telemetry/send.py +185 -0
  132. vibe/core/teleport/errors.py +9 -0
  133. vibe/core/teleport/git.py +196 -0
  134. vibe/core/teleport/nuage.py +180 -0
  135. vibe/core/teleport/teleport.py +208 -0
  136. vibe/core/teleport/types.py +54 -0
  137. vibe/core/tools/base.py +336 -0
  138. vibe/core/tools/builtins/ask_user_question.py +134 -0
  139. vibe/core/tools/builtins/bash.py +357 -0
  140. vibe/core/tools/builtins/grep.py +310 -0
  141. vibe/core/tools/builtins/prompts/__init__.py +0 -0
  142. vibe/core/tools/builtins/prompts/ask_user_question.md +84 -0
  143. vibe/core/tools/builtins/prompts/bash.md +73 -0
  144. vibe/core/tools/builtins/prompts/grep.md +4 -0
  145. vibe/core/tools/builtins/prompts/read_file.md +13 -0
  146. vibe/core/tools/builtins/prompts/search_replace.md +43 -0
  147. vibe/core/tools/builtins/prompts/task.md +24 -0
  148. vibe/core/tools/builtins/prompts/todo.md +199 -0
  149. vibe/core/tools/builtins/prompts/write_file.md +42 -0
  150. vibe/core/tools/builtins/read_file.py +222 -0
  151. vibe/core/tools/builtins/search_replace.py +456 -0
  152. vibe/core/tools/builtins/task.py +154 -0
  153. vibe/core/tools/builtins/todo.py +134 -0
  154. vibe/core/tools/builtins/write_file.py +160 -0
  155. vibe/core/tools/manager.py +341 -0
  156. vibe/core/tools/mcp.py +397 -0
  157. vibe/core/tools/ui.py +68 -0
  158. vibe/core/trusted_folders.py +86 -0
  159. vibe/core/types.py +405 -0
  160. vibe/core/utils.py +396 -0
  161. vibe/setup/onboarding/__init__.py +39 -0
  162. vibe/setup/onboarding/base.py +14 -0
  163. vibe/setup/onboarding/onboarding.tcss +134 -0
  164. vibe/setup/onboarding/screens/__init__.py +5 -0
  165. vibe/setup/onboarding/screens/api_key.py +200 -0
  166. vibe/setup/onboarding/screens/provider_selection.py +87 -0
  167. vibe/setup/onboarding/screens/welcome.py +136 -0
  168. vibe/setup/trusted_folders/trust_folder_dialog.py +180 -0
  169. vibe/setup/trusted_folders/trust_folder_dialog.tcss +83 -0
  170. vibe/whats_new.md +5 -0
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import StrEnum
4
+ import logging
5
+ from os import getenv
6
+
7
+ from vibe.cli.plan_offer.ports.whoami_gateway import (
8
+ WhoAmIGateway,
9
+ WhoAmIGatewayError,
10
+ WhoAmIGatewayUnauthorized,
11
+ WhoAmIResponse,
12
+ )
13
+ from vibe.core.config import DEFAULT_MISTRAL_API_ENV_KEY, Backend, ProviderConfig
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ CONSOLE_CLI_URL = "https://console.mistral.ai/codestral/cli"
18
+ UPGRADE_URL = CONSOLE_CLI_URL
19
+ SWITCH_TO_PRO_KEY_URL = CONSOLE_CLI_URL
20
+
21
+
22
+ class PlanOfferAction(StrEnum):
23
+ NONE = "none"
24
+ UPGRADE = "upgrade"
25
+ SWITCH_TO_PRO_KEY = "switch_to_pro_key"
26
+
27
+
28
+ ACTION_TO_URL: dict[PlanOfferAction, str] = {
29
+ PlanOfferAction.UPGRADE: UPGRADE_URL,
30
+ PlanOfferAction.SWITCH_TO_PRO_KEY: SWITCH_TO_PRO_KEY_URL,
31
+ }
32
+
33
+
34
+ class PlanType(StrEnum):
35
+ FREE = "free"
36
+ PRO = "pro"
37
+ UNKNOWN = "unknown"
38
+
39
+
40
+ async def decide_plan_offer(
41
+ api_key: str | None, gateway: WhoAmIGateway
42
+ ) -> tuple[PlanOfferAction, PlanType]:
43
+ if not api_key:
44
+ return PlanOfferAction.UPGRADE, PlanType.FREE
45
+ try:
46
+ response = await gateway.whoami(api_key)
47
+ except WhoAmIGatewayUnauthorized:
48
+ return PlanOfferAction.UPGRADE, PlanType.FREE
49
+ except WhoAmIGatewayError:
50
+ logger.warning("Failed to fetch plan status.", exc_info=True)
51
+ return PlanOfferAction.NONE, PlanType.UNKNOWN
52
+ return _action_and_plan_from_response(response)
53
+
54
+
55
+ def _action_and_plan_from_response(
56
+ response: WhoAmIResponse,
57
+ ) -> tuple[PlanOfferAction, PlanType]:
58
+ match response:
59
+ case WhoAmIResponse(is_pro_plan=True):
60
+ return PlanOfferAction.NONE, PlanType.PRO
61
+ case WhoAmIResponse(prompt_switching_to_pro_plan=True):
62
+ return PlanOfferAction.SWITCH_TO_PRO_KEY, PlanType.PRO
63
+ case WhoAmIResponse(advertise_pro_plan=True):
64
+ return PlanOfferAction.UPGRADE, PlanType.FREE
65
+ case _:
66
+ return PlanOfferAction.NONE, PlanType.UNKNOWN
67
+
68
+
69
+ def resolve_api_key_for_plan(provider: ProviderConfig) -> str | None:
70
+ api_env_key = DEFAULT_MISTRAL_API_ENV_KEY
71
+
72
+ if provider.backend == Backend.MISTRAL:
73
+ api_env_key = provider.api_key_env_var
74
+
75
+ return getenv(api_env_key)
76
+
77
+
78
+ def plan_offer_cta(action: PlanOfferAction) -> str | None:
79
+ if action is PlanOfferAction.NONE:
80
+ return
81
+ url = ACTION_TO_URL[action]
82
+ match action:
83
+ case PlanOfferAction.UPGRADE:
84
+ text = f"### Unlock more with Vibe - [Upgrade to Le Chat Pro]({url})"
85
+ case PlanOfferAction.SWITCH_TO_PRO_KEY:
86
+ text = f"### Switch to your [Le Chat Pro API key]({url})"
87
+ return text
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Protocol
5
+
6
+
7
+ @dataclass(frozen=True, slots=True)
8
+ class WhoAmIResponse:
9
+ is_pro_plan: bool
10
+ advertise_pro_plan: bool
11
+ prompt_switching_to_pro_plan: bool
12
+
13
+
14
+ class WhoAmIGatewayUnauthorized(Exception):
15
+ pass
16
+
17
+
18
+ class WhoAmIGatewayError(Exception):
19
+ pass
20
+
21
+
22
+ class WhoAmIGateway(Protocol):
23
+ async def whoami(self, api_key: str) -> WhoAmIResponse: ...
@@ -0,0 +1,323 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import platform
9
+ import subprocess
10
+ from typing import Any, Literal
11
+
12
+
13
+ class Terminal(Enum):
14
+ VSCODE = "vscode"
15
+ VSCODE_INSIDERS = "vscode_insiders"
16
+ CURSOR = "cursor"
17
+ ITERM2 = "iterm2"
18
+ WEZTERM = "wezterm"
19
+ GHOSTTY = "ghostty"
20
+ UNKNOWN = "unknown"
21
+
22
+
23
+ @dataclass
24
+ class SetupResult:
25
+ success: bool
26
+ terminal: Terminal
27
+ message: str
28
+ requires_restart: bool = False
29
+
30
+
31
+ def _is_cursor() -> bool:
32
+ path_indicators = [
33
+ "VSCODE_GIT_ASKPASS_NODE",
34
+ "VSCODE_GIT_ASKPASS_MAIN",
35
+ "VSCODE_IPC_HOOK_CLI",
36
+ "VSCODE_NLS_CONFIG",
37
+ ]
38
+ for var in path_indicators:
39
+ val = os.environ.get(var, "").lower()
40
+ if "cursor" in val:
41
+ return True
42
+ return False
43
+
44
+
45
+ def _detect_vscode_terminal() -> Literal[Terminal.VSCODE, Terminal.VSCODE_INSIDERS]:
46
+ term_version = os.environ.get("TERM_PROGRAM_VERSION", "").lower()
47
+ if term_version.endswith("-insider"):
48
+ return Terminal.VSCODE_INSIDERS
49
+
50
+ return Terminal.VSCODE
51
+
52
+
53
+ def detect_terminal() -> Terminal:
54
+ term_program = os.environ.get("TERM_PROGRAM", "").lower()
55
+
56
+ if term_program == "vscode":
57
+ if _is_cursor():
58
+ return Terminal.CURSOR
59
+ return _detect_vscode_terminal()
60
+
61
+ term_map = {
62
+ "iterm.app": Terminal.ITERM2,
63
+ "wezterm": Terminal.WEZTERM,
64
+ "ghostty": Terminal.GHOSTTY,
65
+ }
66
+ if term_program in term_map:
67
+ return term_map[term_program]
68
+
69
+ if os.environ.get("WEZTERM_PANE"):
70
+ return Terminal.WEZTERM
71
+ if os.environ.get("GHOSTTY_RESOURCES_DIR"):
72
+ return Terminal.GHOSTTY
73
+
74
+ return Terminal.UNKNOWN
75
+
76
+
77
+ def _get_vscode_keybindings_path(is_stable: bool) -> Path | None:
78
+ system = platform.system()
79
+
80
+ app_name = "Code" if is_stable else "Code - Insiders"
81
+
82
+ if system == "Darwin":
83
+ base = Path.home() / "Library" / "Application Support" / app_name / "User"
84
+ elif system == "Linux":
85
+ base = Path.home() / ".config" / app_name / "User"
86
+ elif system == "Windows":
87
+ appdata = os.environ.get("APPDATA", "")
88
+ if appdata:
89
+ base = Path(appdata) / app_name / "User"
90
+ else:
91
+ return None
92
+ else:
93
+ return None
94
+
95
+ return base / "keybindings.json"
96
+
97
+
98
+ def _get_cursor_keybindings_path() -> Path | None:
99
+ system = platform.system()
100
+
101
+ if system == "Darwin":
102
+ base = Path.home() / "Library" / "Application Support" / "Cursor" / "User"
103
+ elif system == "Linux":
104
+ base = Path.home() / ".config" / "Cursor" / "User"
105
+ elif system == "Windows":
106
+ appdata = os.environ.get("APPDATA", "")
107
+ if appdata:
108
+ base = Path(appdata) / "Cursor" / "User"
109
+ else:
110
+ return None
111
+ else:
112
+ return None
113
+
114
+ return base / "keybindings.json"
115
+
116
+
117
+ def _parse_keybindings(content: str) -> list[dict[str, Any]]:
118
+ content = content.strip()
119
+ if not content or content.startswith("//"):
120
+ return []
121
+
122
+ lines = [line for line in content.split("\n") if not line.strip().startswith("//")]
123
+ clean_content = "\n".join(lines)
124
+
125
+ try:
126
+ return json.loads(clean_content)
127
+ except json.JSONDecodeError:
128
+ return []
129
+
130
+
131
+ def _setup_vscode_like_terminal(terminal: Terminal) -> SetupResult:
132
+ """Setup keybindings for VS Code or Cursor."""
133
+ if terminal == Terminal.CURSOR:
134
+ keybindings_path = _get_cursor_keybindings_path()
135
+ editor_name = "Cursor"
136
+ else:
137
+ keybindings_path = _get_vscode_keybindings_path(terminal == Terminal.VSCODE)
138
+ editor_name = "VS Code" if terminal == Terminal.VSCODE else "VS Code Insiders"
139
+
140
+ if keybindings_path is None:
141
+ return SetupResult(
142
+ success=False,
143
+ terminal=terminal,
144
+ message=f"Could not determine keybindings path for {editor_name}",
145
+ )
146
+
147
+ new_binding = {
148
+ "key": "shift+enter",
149
+ "command": "workbench.action.terminal.sendSequence",
150
+ "args": {"text": "\u001b[13;2u"},
151
+ "when": "terminalFocus",
152
+ }
153
+
154
+ try:
155
+ keybindings = _read_existing_keybindings(keybindings_path)
156
+
157
+ if _has_shift_enter_binding(keybindings):
158
+ return SetupResult(
159
+ success=True,
160
+ terminal=terminal,
161
+ message=f"Shift+Enter already configured in {editor_name}",
162
+ )
163
+
164
+ keybindings.append(new_binding)
165
+ keybindings_path.write_text(
166
+ json.dumps(keybindings, indent=2, ensure_ascii=False) + "\n"
167
+ )
168
+
169
+ return SetupResult(
170
+ success=True,
171
+ terminal=terminal,
172
+ message=f"Added Shift+Enter binding to {keybindings_path}",
173
+ requires_restart=True,
174
+ )
175
+
176
+ except Exception as e:
177
+ return SetupResult(
178
+ success=False,
179
+ terminal=terminal,
180
+ message=f"Failed to configure {editor_name}: {e}",
181
+ )
182
+
183
+
184
+ def _read_existing_keybindings(keybindings_path: Path) -> list[dict[str, Any]]:
185
+ if keybindings_path.exists():
186
+ content = keybindings_path.read_text()
187
+ return _parse_keybindings(content)
188
+ keybindings_path.parent.mkdir(parents=True, exist_ok=True)
189
+ return []
190
+
191
+
192
+ def _has_shift_enter_binding(keybindings: list[dict[str, Any]]) -> bool:
193
+ for binding in keybindings:
194
+ if (
195
+ binding.get("key") == "shift+enter"
196
+ and binding.get("command") == "workbench.action.terminal.sendSequence"
197
+ and binding.get("when") == "terminalFocus"
198
+ ):
199
+ return True
200
+ return False
201
+
202
+
203
+ def _setup_iterm2() -> SetupResult:
204
+ if platform.system() != "Darwin":
205
+ return SetupResult(
206
+ success=False,
207
+ terminal=Terminal.ITERM2,
208
+ message="iTerm2 is only available on macOS",
209
+ )
210
+
211
+ plist_key = "0xd-0x20000-0x24"
212
+ plist_value = """<dict>
213
+ <key>Text</key>
214
+ <string>\\n</string>
215
+ <key>Action</key>
216
+ <integer>12</integer>
217
+ <key>Version</key>
218
+ <integer>1</integer>
219
+ <key>Keycode</key>
220
+ <integer>13</integer>
221
+ <key>Modifiers</key>
222
+ <integer>131072</integer>
223
+ </dict>"""
224
+
225
+ try:
226
+ result = subprocess.run(
227
+ ["defaults", "read", "com.googlecode.iterm2", "GlobalKeyMap"],
228
+ capture_output=True,
229
+ text=True,
230
+ )
231
+
232
+ if plist_key in result.stdout:
233
+ return SetupResult(
234
+ success=True,
235
+ terminal=Terminal.ITERM2,
236
+ message="Shift+Enter already configured in iTerm2",
237
+ )
238
+
239
+ subprocess.run(
240
+ [
241
+ "defaults",
242
+ "write",
243
+ "com.googlecode.iterm2",
244
+ "GlobalKeyMap",
245
+ "-dict-add",
246
+ plist_key,
247
+ plist_value,
248
+ ],
249
+ check=True,
250
+ capture_output=True,
251
+ )
252
+
253
+ return SetupResult(
254
+ success=True,
255
+ terminal=Terminal.ITERM2,
256
+ message="Added Shift+Enter binding to iTerm2 preferences",
257
+ requires_restart=True,
258
+ )
259
+
260
+ except subprocess.CalledProcessError as e:
261
+ return SetupResult(
262
+ success=False,
263
+ terminal=Terminal.ITERM2,
264
+ message=f"Failed to configure iTerm2: {e.stderr}",
265
+ )
266
+ except Exception as e:
267
+ return SetupResult(
268
+ success=False,
269
+ terminal=Terminal.ITERM2,
270
+ message=f"Failed to configure iTerm2: {e}",
271
+ )
272
+
273
+
274
+ def _setup_wezterm() -> SetupResult:
275
+ return SetupResult(
276
+ success=True,
277
+ terminal=Terminal.WEZTERM,
278
+ message="Please manually add the following to your .wezterm.lua:\n"
279
+ "local wezterm = require 'wezterm'\n"
280
+ "local config = wezterm.config_builder()\n\n"
281
+ "config.keys = {\n"
282
+ " {\n"
283
+ ' key = "Enter",\n'
284
+ ' mods = "SHIFT",\n'
285
+ ' action = wezterm.action.SendString("\\x1b[13;2u"),\n'
286
+ " }\n"
287
+ "}\n\n"
288
+ "return config",
289
+ )
290
+
291
+
292
+ def _setup_ghostty() -> SetupResult:
293
+ return SetupResult(
294
+ success=True,
295
+ terminal=Terminal.GHOSTTY,
296
+ message="Shift+Enter is already configured in Ghostty",
297
+ )
298
+
299
+
300
+ def setup_terminal() -> SetupResult:
301
+ terminal = detect_terminal()
302
+
303
+ match terminal:
304
+ case Terminal.VSCODE | Terminal.VSCODE_INSIDERS | Terminal.CURSOR:
305
+ return _setup_vscode_like_terminal(terminal)
306
+ case Terminal.ITERM2:
307
+ return _setup_iterm2()
308
+ case Terminal.WEZTERM:
309
+ return _setup_wezterm()
310
+ case Terminal.GHOSTTY:
311
+ return _setup_ghostty()
312
+ case Terminal.UNKNOWN:
313
+ return SetupResult(
314
+ success=False,
315
+ terminal=Terminal.UNKNOWN,
316
+ message="Could not detect terminal. Supported terminals:\n"
317
+ "- VS Code\n"
318
+ "- Cursor\n"
319
+ "- iTerm2\n"
320
+ "- WezTerm\n"
321
+ "- Ghostty\n\n"
322
+ "You can manually configure Shift+Enter to send: \\x1b[13;2u",
323
+ )
File without changes
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from pygments.token import Token
4
+ from textual.content import Content
5
+ from textual.highlight import HighlightTheme, highlight
6
+ from textual.widgets import Markdown
7
+ from textual.widgets._markdown import MarkdownFence
8
+
9
+
10
+ class AnsiHighlightTheme(HighlightTheme):
11
+ STYLES = {
12
+ Token.Comment: "ansi_bright_black italic",
13
+ Token.Error: "ansi_red",
14
+ Token.Generic.Strong: "bold",
15
+ Token.Generic.Emph: "italic",
16
+ Token.Generic.Error: "ansi_red",
17
+ Token.Generic.Heading: "ansi_blue underline",
18
+ Token.Generic.Subheading: "ansi_blue",
19
+ Token.Keyword: "ansi_magenta",
20
+ Token.Keyword.Constant: "ansi_cyan",
21
+ Token.Keyword.Namespace: "ansi_magenta",
22
+ Token.Keyword.Type: "ansi_cyan",
23
+ Token.Literal.Number: "ansi_yellow",
24
+ Token.Literal.String.Backtick: "ansi_bright_black",
25
+ Token.Literal.String: "ansi_green",
26
+ Token.Literal.String.Doc: "ansi_green italic",
27
+ Token.Literal.String.Double: "ansi_green",
28
+ Token.Name: "ansi_default",
29
+ Token.Name.Attribute: "ansi_yellow",
30
+ Token.Name.Builtin: "ansi_cyan",
31
+ Token.Name.Builtin.Pseudo: "italic",
32
+ Token.Name.Class: "ansi_yellow",
33
+ Token.Name.Constant: "ansi_red",
34
+ Token.Name.Decorator: "ansi_blue",
35
+ Token.Name.Function: "ansi_blue",
36
+ Token.Name.Function.Magic: "ansi_blue",
37
+ Token.Name.Tag: "ansi_blue",
38
+ Token.Name.Variable: "ansi_default",
39
+ Token.Number: "ansi_yellow",
40
+ Token.Operator: "ansi_default",
41
+ Token.Operator.Word: "ansi_magenta",
42
+ Token.String: "ansi_green",
43
+ Token.Whitespace: "",
44
+ }
45
+
46
+
47
+ class AnsiMarkdownFence(MarkdownFence):
48
+ @classmethod
49
+ def highlight(cls, code: str, language: str) -> Content:
50
+ return highlight(code, language=language or None, theme=AnsiHighlightTheme)
51
+
52
+
53
+ class AnsiMarkdown(Markdown):
54
+ BLOCKS = {
55
+ **Markdown.BLOCKS,
56
+ "fence": AnsiMarkdownFence,
57
+ "code_block": AnsiMarkdownFence,
58
+ }