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.
- codemaster_cli-2.2.0.dist-info/METADATA +645 -0
- codemaster_cli-2.2.0.dist-info/RECORD +170 -0
- codemaster_cli-2.2.0.dist-info/WHEEL +4 -0
- codemaster_cli-2.2.0.dist-info/entry_points.txt +3 -0
- vibe/__init__.py +6 -0
- vibe/acp/__init__.py +0 -0
- vibe/acp/acp_agent_loop.py +746 -0
- vibe/acp/entrypoint.py +81 -0
- vibe/acp/tools/__init__.py +0 -0
- vibe/acp/tools/base.py +100 -0
- vibe/acp/tools/builtins/bash.py +134 -0
- vibe/acp/tools/builtins/read_file.py +54 -0
- vibe/acp/tools/builtins/search_replace.py +129 -0
- vibe/acp/tools/builtins/todo.py +65 -0
- vibe/acp/tools/builtins/write_file.py +98 -0
- vibe/acp/tools/session_update.py +118 -0
- vibe/acp/utils.py +213 -0
- vibe/cli/__init__.py +0 -0
- vibe/cli/autocompletion/__init__.py +0 -0
- vibe/cli/autocompletion/base.py +22 -0
- vibe/cli/autocompletion/path_completion.py +177 -0
- vibe/cli/autocompletion/slash_command.py +99 -0
- vibe/cli/cli.py +188 -0
- vibe/cli/clipboard.py +69 -0
- vibe/cli/commands.py +116 -0
- vibe/cli/entrypoint.py +163 -0
- vibe/cli/history_manager.py +91 -0
- vibe/cli/plan_offer/adapters/http_whoami_gateway.py +67 -0
- vibe/cli/plan_offer/decide_plan_offer.py +87 -0
- vibe/cli/plan_offer/ports/whoami_gateway.py +23 -0
- vibe/cli/terminal_setup.py +323 -0
- vibe/cli/textual_ui/__init__.py +0 -0
- vibe/cli/textual_ui/ansi_markdown.py +58 -0
- vibe/cli/textual_ui/app.py +1546 -0
- vibe/cli/textual_ui/app.tcss +1020 -0
- vibe/cli/textual_ui/external_editor.py +32 -0
- vibe/cli/textual_ui/handlers/__init__.py +5 -0
- vibe/cli/textual_ui/handlers/event_handler.py +147 -0
- vibe/cli/textual_ui/widgets/__init__.py +0 -0
- vibe/cli/textual_ui/widgets/approval_app.py +192 -0
- vibe/cli/textual_ui/widgets/banner/banner.py +85 -0
- vibe/cli/textual_ui/widgets/banner/petit_chat.py +195 -0
- vibe/cli/textual_ui/widgets/braille_renderer.py +58 -0
- vibe/cli/textual_ui/widgets/chat_input/__init__.py +7 -0
- vibe/cli/textual_ui/widgets/chat_input/body.py +214 -0
- vibe/cli/textual_ui/widgets/chat_input/completion_manager.py +58 -0
- vibe/cli/textual_ui/widgets/chat_input/completion_popup.py +43 -0
- vibe/cli/textual_ui/widgets/chat_input/container.py +195 -0
- vibe/cli/textual_ui/widgets/chat_input/text_area.py +365 -0
- vibe/cli/textual_ui/widgets/compact.py +41 -0
- vibe/cli/textual_ui/widgets/config_app.py +171 -0
- vibe/cli/textual_ui/widgets/context_progress.py +30 -0
- vibe/cli/textual_ui/widgets/load_more.py +43 -0
- vibe/cli/textual_ui/widgets/loading.py +201 -0
- vibe/cli/textual_ui/widgets/messages.py +277 -0
- vibe/cli/textual_ui/widgets/no_markup_static.py +11 -0
- vibe/cli/textual_ui/widgets/path_display.py +28 -0
- vibe/cli/textual_ui/widgets/proxy_setup_app.py +127 -0
- vibe/cli/textual_ui/widgets/question_app.py +496 -0
- vibe/cli/textual_ui/widgets/spinner.py +194 -0
- vibe/cli/textual_ui/widgets/status_message.py +76 -0
- vibe/cli/textual_ui/widgets/teleport_message.py +31 -0
- vibe/cli/textual_ui/widgets/tool_widgets.py +371 -0
- vibe/cli/textual_ui/widgets/tools.py +201 -0
- vibe/cli/textual_ui/windowing/__init__.py +29 -0
- vibe/cli/textual_ui/windowing/history.py +105 -0
- vibe/cli/textual_ui/windowing/history_windowing.py +71 -0
- vibe/cli/textual_ui/windowing/state.py +105 -0
- vibe/cli/update_notifier/__init__.py +47 -0
- vibe/cli/update_notifier/adapters/filesystem_update_cache_repository.py +59 -0
- vibe/cli/update_notifier/adapters/github_update_gateway.py +101 -0
- vibe/cli/update_notifier/adapters/pypi_update_gateway.py +107 -0
- vibe/cli/update_notifier/ports/update_cache_repository.py +16 -0
- vibe/cli/update_notifier/ports/update_gateway.py +53 -0
- vibe/cli/update_notifier/update.py +139 -0
- vibe/cli/update_notifier/whats_new.py +49 -0
- vibe/core/__init__.py +5 -0
- vibe/core/agent_loop.py +1075 -0
- vibe/core/agents/__init__.py +31 -0
- vibe/core/agents/manager.py +165 -0
- vibe/core/agents/models.py +122 -0
- vibe/core/auth/__init__.py +6 -0
- vibe/core/auth/crypto.py +137 -0
- vibe/core/auth/github.py +178 -0
- vibe/core/autocompletion/__init__.py +0 -0
- vibe/core/autocompletion/completers.py +257 -0
- vibe/core/autocompletion/file_indexer/__init__.py +10 -0
- vibe/core/autocompletion/file_indexer/ignore_rules.py +156 -0
- vibe/core/autocompletion/file_indexer/indexer.py +179 -0
- vibe/core/autocompletion/file_indexer/store.py +169 -0
- vibe/core/autocompletion/file_indexer/watcher.py +71 -0
- vibe/core/autocompletion/fuzzy.py +189 -0
- vibe/core/autocompletion/path_prompt.py +108 -0
- vibe/core/autocompletion/path_prompt_adapter.py +149 -0
- vibe/core/config.py +673 -0
- vibe/core/config_PATCH_INSTRUCTIONS.md +77 -0
- vibe/core/llm/__init__.py +0 -0
- vibe/core/llm/backend/anthropic.py +630 -0
- vibe/core/llm/backend/base.py +38 -0
- vibe/core/llm/backend/factory.py +7 -0
- vibe/core/llm/backend/generic.py +425 -0
- vibe/core/llm/backend/mistral.py +381 -0
- vibe/core/llm/backend/vertex.py +115 -0
- vibe/core/llm/exceptions.py +195 -0
- vibe/core/llm/format.py +184 -0
- vibe/core/llm/message_utils.py +24 -0
- vibe/core/llm/types.py +120 -0
- vibe/core/middleware.py +209 -0
- vibe/core/output_formatters.py +85 -0
- vibe/core/paths/__init__.py +0 -0
- vibe/core/paths/config_paths.py +68 -0
- vibe/core/paths/global_paths.py +40 -0
- vibe/core/programmatic.py +56 -0
- vibe/core/prompts/__init__.py +32 -0
- vibe/core/prompts/cli.md +111 -0
- vibe/core/prompts/compact.md +48 -0
- vibe/core/prompts/dangerous_directory.md +5 -0
- vibe/core/prompts/explore.md +50 -0
- vibe/core/prompts/project_context.md +8 -0
- vibe/core/prompts/tests.md +1 -0
- vibe/core/proxy_setup.py +65 -0
- vibe/core/session/session_loader.py +222 -0
- vibe/core/session/session_logger.py +318 -0
- vibe/core/session/session_migration.py +41 -0
- vibe/core/skills/__init__.py +7 -0
- vibe/core/skills/manager.py +132 -0
- vibe/core/skills/models.py +92 -0
- vibe/core/skills/parser.py +39 -0
- vibe/core/system_prompt.py +466 -0
- vibe/core/telemetry/__init__.py +0 -0
- vibe/core/telemetry/send.py +185 -0
- vibe/core/teleport/errors.py +9 -0
- vibe/core/teleport/git.py +196 -0
- vibe/core/teleport/nuage.py +180 -0
- vibe/core/teleport/teleport.py +208 -0
- vibe/core/teleport/types.py +54 -0
- vibe/core/tools/base.py +336 -0
- vibe/core/tools/builtins/ask_user_question.py +134 -0
- vibe/core/tools/builtins/bash.py +357 -0
- vibe/core/tools/builtins/grep.py +310 -0
- vibe/core/tools/builtins/prompts/__init__.py +0 -0
- vibe/core/tools/builtins/prompts/ask_user_question.md +84 -0
- vibe/core/tools/builtins/prompts/bash.md +73 -0
- vibe/core/tools/builtins/prompts/grep.md +4 -0
- vibe/core/tools/builtins/prompts/read_file.md +13 -0
- vibe/core/tools/builtins/prompts/search_replace.md +43 -0
- vibe/core/tools/builtins/prompts/task.md +24 -0
- vibe/core/tools/builtins/prompts/todo.md +199 -0
- vibe/core/tools/builtins/prompts/write_file.md +42 -0
- vibe/core/tools/builtins/read_file.py +222 -0
- vibe/core/tools/builtins/search_replace.py +456 -0
- vibe/core/tools/builtins/task.py +154 -0
- vibe/core/tools/builtins/todo.py +134 -0
- vibe/core/tools/builtins/write_file.py +160 -0
- vibe/core/tools/manager.py +341 -0
- vibe/core/tools/mcp.py +397 -0
- vibe/core/tools/ui.py +68 -0
- vibe/core/trusted_folders.py +86 -0
- vibe/core/types.py +405 -0
- vibe/core/utils.py +396 -0
- vibe/setup/onboarding/__init__.py +39 -0
- vibe/setup/onboarding/base.py +14 -0
- vibe/setup/onboarding/onboarding.tcss +134 -0
- vibe/setup/onboarding/screens/__init__.py +5 -0
- vibe/setup/onboarding/screens/api_key.py +200 -0
- vibe/setup/onboarding/screens/provider_selection.py +87 -0
- vibe/setup/onboarding/screens/welcome.py +136 -0
- vibe/setup/trusted_folders/trust_folder_dialog.py +180 -0
- vibe/setup/trusted_folders/trust_folder_dialog.tcss +83 -0
- 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
|
+
}
|