mirror of
https://github.com/mistralai/mistral-vibe
synced 2026-04-26 01:24:55 +02:00
Co-authored-by: Bastien <bastien.baret@gmail.com> Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai> Co-authored-by: Julien Legrand <72564015+JulienLGRD@users.noreply.github.com> Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Quentin <quentin.torroba@mistral.ai> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
339 lines
9.5 KiB
Python
339 lines
9.5 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import platform
|
|
import subprocess
|
|
from typing import Any, Literal
|
|
|
|
from vibe.core.utils.io import read_safe
|
|
|
|
|
|
class Terminal(Enum):
|
|
VSCODE = "vscode"
|
|
VSCODE_INSIDERS = "vscode_insiders"
|
|
CURSOR = "cursor"
|
|
JETBRAINS = "jetbrains"
|
|
ITERM2 = "iterm2"
|
|
WEZTERM = "wezterm"
|
|
GHOSTTY = "ghostty"
|
|
UNKNOWN = "unknown"
|
|
|
|
|
|
@dataclass
|
|
class SetupResult:
|
|
success: bool
|
|
terminal: Terminal
|
|
message: str
|
|
requires_restart: bool = False
|
|
|
|
|
|
def _is_cursor() -> bool:
|
|
path_indicators = [
|
|
"VSCODE_GIT_ASKPASS_NODE",
|
|
"VSCODE_GIT_ASKPASS_MAIN",
|
|
"VSCODE_IPC_HOOK_CLI",
|
|
"VSCODE_NLS_CONFIG",
|
|
]
|
|
for var in path_indicators:
|
|
val = os.environ.get(var, "").lower()
|
|
if "cursor" in val:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _detect_vscode_terminal() -> Literal[Terminal.VSCODE, Terminal.VSCODE_INSIDERS]:
|
|
term_version = os.environ.get("TERM_PROGRAM_VERSION", "").lower()
|
|
if term_version.endswith("-insider"):
|
|
return Terminal.VSCODE_INSIDERS
|
|
|
|
return Terminal.VSCODE
|
|
|
|
|
|
def _detect_terminal_from_env() -> Terminal | None:
|
|
if os.environ.get("WEZTERM_PANE"):
|
|
return Terminal.WEZTERM
|
|
if os.environ.get("GHOSTTY_RESOURCES_DIR"):
|
|
return Terminal.GHOSTTY
|
|
if "jetbrains" in os.environ.get("TERMINAL_EMULATOR", "").lower():
|
|
return Terminal.JETBRAINS
|
|
return None
|
|
|
|
|
|
def detect_terminal() -> Terminal:
|
|
term_program = os.environ.get("TERM_PROGRAM", "").lower()
|
|
|
|
if term_program == "vscode":
|
|
if _is_cursor():
|
|
return Terminal.CURSOR
|
|
return _detect_vscode_terminal()
|
|
|
|
term_map = {
|
|
"iterm.app": Terminal.ITERM2,
|
|
"wezterm": Terminal.WEZTERM,
|
|
"ghostty": Terminal.GHOSTTY,
|
|
}
|
|
if term_program in term_map:
|
|
return term_map[term_program]
|
|
|
|
return _detect_terminal_from_env() or Terminal.UNKNOWN
|
|
|
|
|
|
def _get_vscode_keybindings_path(is_stable: bool) -> Path | None:
|
|
system = platform.system()
|
|
|
|
app_name = "Code" if is_stable else "Code - Insiders"
|
|
|
|
if system == "Darwin":
|
|
base = Path.home() / "Library" / "Application Support" / app_name / "User"
|
|
elif system == "Linux":
|
|
base = Path.home() / ".config" / app_name / "User"
|
|
elif system == "Windows":
|
|
appdata = os.environ.get("APPDATA", "")
|
|
if appdata:
|
|
base = Path(appdata) / app_name / "User"
|
|
else:
|
|
return None
|
|
else:
|
|
return None
|
|
|
|
return base / "keybindings.json"
|
|
|
|
|
|
def _get_cursor_keybindings_path() -> Path | None:
|
|
system = platform.system()
|
|
|
|
if system == "Darwin":
|
|
base = Path.home() / "Library" / "Application Support" / "Cursor" / "User"
|
|
elif system == "Linux":
|
|
base = Path.home() / ".config" / "Cursor" / "User"
|
|
elif system == "Windows":
|
|
appdata = os.environ.get("APPDATA", "")
|
|
if appdata:
|
|
base = Path(appdata) / "Cursor" / "User"
|
|
else:
|
|
return None
|
|
else:
|
|
return None
|
|
|
|
return base / "keybindings.json"
|
|
|
|
|
|
def _parse_keybindings(content: str) -> list[dict[str, Any]]:
|
|
content = content.strip()
|
|
if not content or content.startswith("//"):
|
|
return []
|
|
|
|
lines = [line for line in content.split("\n") if not line.strip().startswith("//")]
|
|
clean_content = "\n".join(lines)
|
|
|
|
try:
|
|
return json.loads(clean_content)
|
|
except json.JSONDecodeError:
|
|
return []
|
|
|
|
|
|
def _setup_vscode_like_terminal(terminal: Terminal) -> SetupResult:
|
|
"""Setup keybindings for VS Code or Cursor."""
|
|
if terminal == Terminal.CURSOR:
|
|
keybindings_path = _get_cursor_keybindings_path()
|
|
editor_name = "Cursor"
|
|
else:
|
|
keybindings_path = _get_vscode_keybindings_path(terminal == Terminal.VSCODE)
|
|
editor_name = "VS Code" if terminal == Terminal.VSCODE else "VS Code Insiders"
|
|
|
|
if keybindings_path is None:
|
|
return SetupResult(
|
|
success=False,
|
|
terminal=terminal,
|
|
message=f"Could not determine keybindings path for {editor_name}",
|
|
)
|
|
|
|
new_binding = {
|
|
"key": "shift+enter",
|
|
"command": "workbench.action.terminal.sendSequence",
|
|
"args": {"text": "\u001b[13;2u"},
|
|
"when": "terminalFocus",
|
|
}
|
|
|
|
try:
|
|
keybindings = _read_existing_keybindings(keybindings_path)
|
|
|
|
if _has_shift_enter_binding(keybindings):
|
|
return SetupResult(
|
|
success=True,
|
|
terminal=terminal,
|
|
message=f"Shift+Enter already configured in {editor_name}",
|
|
)
|
|
|
|
keybindings.append(new_binding)
|
|
keybindings_path.write_text(
|
|
json.dumps(keybindings, indent=2, ensure_ascii=False) + "\n"
|
|
)
|
|
|
|
return SetupResult(
|
|
success=True,
|
|
terminal=terminal,
|
|
message=f"Added Shift+Enter binding to {keybindings_path}",
|
|
requires_restart=True,
|
|
)
|
|
|
|
except Exception as e:
|
|
return SetupResult(
|
|
success=False,
|
|
terminal=terminal,
|
|
message=f"Failed to configure {editor_name}: {e}",
|
|
)
|
|
|
|
|
|
def _read_existing_keybindings(keybindings_path: Path) -> list[dict[str, Any]]:
|
|
if keybindings_path.exists():
|
|
content = read_safe(keybindings_path).text
|
|
return _parse_keybindings(content)
|
|
keybindings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
return []
|
|
|
|
|
|
def _has_shift_enter_binding(keybindings: list[dict[str, Any]]) -> bool:
|
|
for binding in keybindings:
|
|
if (
|
|
binding.get("key") == "shift+enter"
|
|
and binding.get("command") == "workbench.action.terminal.sendSequence"
|
|
and binding.get("when") == "terminalFocus"
|
|
):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _setup_iterm2() -> SetupResult:
|
|
if platform.system() != "Darwin":
|
|
return SetupResult(
|
|
success=False,
|
|
terminal=Terminal.ITERM2,
|
|
message="iTerm2 is only available on macOS",
|
|
)
|
|
|
|
plist_key = "0xd-0x20000-0x24"
|
|
plist_value = """<dict>
|
|
<key>Text</key>
|
|
<string>\\n</string>
|
|
<key>Action</key>
|
|
<integer>12</integer>
|
|
<key>Version</key>
|
|
<integer>1</integer>
|
|
<key>Keycode</key>
|
|
<integer>13</integer>
|
|
<key>Modifiers</key>
|
|
<integer>131072</integer>
|
|
</dict>"""
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
["defaults", "read", "com.googlecode.iterm2", "GlobalKeyMap"],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
|
|
if plist_key in result.stdout:
|
|
return SetupResult(
|
|
success=True,
|
|
terminal=Terminal.ITERM2,
|
|
message="Shift+Enter already configured in iTerm2",
|
|
)
|
|
|
|
subprocess.run(
|
|
[
|
|
"defaults",
|
|
"write",
|
|
"com.googlecode.iterm2",
|
|
"GlobalKeyMap",
|
|
"-dict-add",
|
|
plist_key,
|
|
plist_value,
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
|
|
return SetupResult(
|
|
success=True,
|
|
terminal=Terminal.ITERM2,
|
|
message="Added Shift+Enter binding to iTerm2 preferences",
|
|
requires_restart=True,
|
|
)
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
return SetupResult(
|
|
success=False,
|
|
terminal=Terminal.ITERM2,
|
|
message=f"Failed to configure iTerm2: {e.stderr}",
|
|
)
|
|
except Exception as e:
|
|
return SetupResult(
|
|
success=False,
|
|
terminal=Terminal.ITERM2,
|
|
message=f"Failed to configure iTerm2: {e}",
|
|
)
|
|
|
|
|
|
def _setup_wezterm() -> SetupResult:
|
|
return SetupResult(
|
|
success=True,
|
|
terminal=Terminal.WEZTERM,
|
|
message="Please manually add the following to your .wezterm.lua:\n"
|
|
"local wezterm = require 'wezterm'\n"
|
|
"local config = wezterm.config_builder()\n\n"
|
|
"config.keys = {\n"
|
|
" {\n"
|
|
' key = "Enter",\n'
|
|
' mods = "SHIFT",\n'
|
|
' action = wezterm.action.SendString("\\x1b[13;2u"),\n'
|
|
" }\n"
|
|
"}\n\n"
|
|
"return config",
|
|
)
|
|
|
|
|
|
def _setup_ghostty() -> SetupResult:
|
|
return SetupResult(
|
|
success=True,
|
|
terminal=Terminal.GHOSTTY,
|
|
message="Shift+Enter is already configured in Ghostty",
|
|
)
|
|
|
|
|
|
def setup_terminal() -> SetupResult:
|
|
terminal = detect_terminal()
|
|
|
|
match terminal:
|
|
case Terminal.VSCODE | Terminal.VSCODE_INSIDERS | Terminal.CURSOR:
|
|
return _setup_vscode_like_terminal(terminal)
|
|
case Terminal.JETBRAINS:
|
|
return SetupResult(
|
|
success=False,
|
|
terminal=Terminal.JETBRAINS,
|
|
message="Jetbrains terminal is not supported.\n"
|
|
"You can manually configure Shift+Enter to send: \\x1b[13;2u",
|
|
)
|
|
case Terminal.ITERM2:
|
|
return _setup_iterm2()
|
|
case Terminal.WEZTERM:
|
|
return _setup_wezterm()
|
|
case Terminal.GHOSTTY:
|
|
return _setup_ghostty()
|
|
case Terminal.UNKNOWN:
|
|
return SetupResult(
|
|
success=False,
|
|
terminal=Terminal.UNKNOWN,
|
|
message="Could not detect terminal. Supported terminals:\n"
|
|
"- VS Code\n"
|
|
"- Cursor\n"
|
|
"- iTerm2\n"
|
|
"- WezTerm\n"
|
|
"- Ghostty\n\n"
|
|
"You can manually configure Shift+Enter to send: \\x1b[13;2u",
|
|
)
|