* feat(terminal): 支持为自定义本地 Shell 配置启动参数 (#1221) 自定义本地 Shell 此前只能填可执行文件路径,无法指定启动参数, 导致接入 msys2 bash 时缺少 `--login -i`,shell 不经 profile 初始化、 环境变量缺失而无法使用。 - TerminalSettings 新增 localShellArgs(string[],默认 []),随设置自动迁移 - 新增 domain/shellArgs:引号感知的命令行分词/回显格式化 - resolveShellSetting 透传自定义参数;参数为空时回退到 bridge 默认参数, 命中已发现 shell(WSL/Git Bash)时忽略自定义参数,避免串味 - 自定义 Shell 弹窗新增「启动参数」输入,设置行展示完整命令(en/zh-CN/ru) 参数复用已有管线 session.localShellArgs → startLocalSession → pty.spawn, 不涉及云同步(与机器相关的 localShell 一致,均不同步)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(terminal): 修复自定义 Shell 参数的两处 review 问题 - 切换到默认/已发现 shell 时清空 localShellArgs:自定义参数仅对自定义 路径有意义,清空可避免在发现列表尚未加载(启动竞态)或所选 shell ID 在本机不可用时,旧参数被当作该 shell 的启动参数而泄漏导致启动失败 - formatShellArgs 对含双引号的参数改用单引号包裹,修复 `-c "..."` 这类参数重新打开弹窗保存时被破坏的往返问题 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(terminal): shellArgs 量化处理含两种引号的参数往返 采用单引号优先的引号策略:单引号内全字面(Windows 路径、双引号原样保留, 无需转义);仅当 token 自身含单引号时改用双引号包裹并转义 \ 与 "。 解析端仅在双引号区内将 \" / \\ 视为转义,其余反斜杠保持字面, 确保未加引号的 Windows 路径不被破坏。修复 `echo "it's ok"` 这类同时含 单双引号的参数在弹窗重新保存时被破坏的问题。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(terminal): 自定义 Shell 已选中后可重新打开编辑参数 自定义 shell 选中时下拉框值已是 __custom__,再次点选「自定义…」不会触发 onValueChange,导致无法重新打开弹窗修改参数/路径。改为把自定义 shell 概要 做成可点击的编辑入口(铅笔图标),点击即重新填充草稿并打开弹窗; 打开逻辑抽到 openCustomShellModal 复用。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(terminal): shellArgs 改用 POSIX 单引号方案,修复尾部反斜杠与空参数 - 解析:两种引号区内均全字面(双引号不再把 \" 当转义),保留 Windows 路径尾部反斜杠等手输入;引号外仅 \' 转义为字面单引号(支撑 '\'' 习语), 其余反斜杠保持字面,未加引号的 Windows 路径不受影响 - 格式化:统一单引号包裹(内容全字面),内嵌单引号用 POSIX '\'' 习语; 显式空参数输出为 '' 以免重新保存时被丢弃 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(terminal): customArgs take precedence over discovered shell defaults when user explicitly sets them --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
88 lines
3.2 KiB
TypeScript
88 lines
3.2 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { netcattyBridge } from "../infrastructure/services/netcattyBridge";
|
|
|
|
let shellCache: DiscoveredShell[] | null = null;
|
|
let shellPromise: Promise<DiscoveredShell[]> | null = null;
|
|
|
|
export function useDiscoveredShells(): DiscoveredShell[] {
|
|
const [shells, setShells] = useState<DiscoveredShell[]>(shellCache ?? []);
|
|
|
|
useEffect(() => {
|
|
if (shellCache) {
|
|
setShells(shellCache);
|
|
return;
|
|
}
|
|
|
|
const bridge = netcattyBridge.get();
|
|
if (!bridge?.discoverShells) return;
|
|
|
|
if (!shellPromise) {
|
|
shellPromise = bridge.discoverShells();
|
|
}
|
|
|
|
shellPromise.then((result) => {
|
|
shellCache = result;
|
|
setShells(result);
|
|
}).catch((err) => {
|
|
console.warn("Failed to discover shells:", err);
|
|
// Clear the failed promise so the next mount can retry
|
|
shellPromise = null;
|
|
});
|
|
}, []);
|
|
|
|
return shells;
|
|
}
|
|
|
|
/**
|
|
* Resolve a localShell setting value to shell command and args.
|
|
* The value can be a discovered shell id (e.g., "wsl-ubuntu", "pwsh")
|
|
* or a custom path/command (e.g., "/usr/local/bin/fish" or "fish").
|
|
* `customArgs` are the user-configured launch args (e.g. ["--login", "-i"] for
|
|
* msys2 bash). When present, they take precedence over discovered shell defaults
|
|
* so custom commands like "bash" or "fish" can collide with discovered IDs
|
|
* without losing the user's explicit args. Returns { command, args } or null
|
|
* when discovery hasn't loaded yet and the value might be a shell ID that can't
|
|
* be resolved yet.
|
|
*/
|
|
export function resolveShellSetting(
|
|
localShell: string,
|
|
discoveredShells: DiscoveredShell[],
|
|
customArgs?: string[]
|
|
): { command: string; args?: string[] } | null {
|
|
if (!localShell) return null;
|
|
|
|
// Try to match as a discovered shell id. Discovered shells provide their own
|
|
// args (e.g. WSL "-d Ubuntu"), unless the user explicitly configured custom
|
|
// args for a command/path that happens to share the same value as an ID.
|
|
const shell = discoveredShells.find(s => s.id === localShell);
|
|
if (shell) {
|
|
return { command: shell.command, args: customArgs?.length ? customArgs : shell.args };
|
|
}
|
|
|
|
// No ID match — treat as a custom shell path/command and pass through.
|
|
// This handles both custom executables (e.g., "/usr/local/bin/fish", "pwsh-preview")
|
|
// and stale/synced IDs that no longer exist on this machine (graceful fallback
|
|
// to whatever the OS resolves the name to, or a spawn error the user can see).
|
|
// Omit args when none are configured so the bridge's getLocalShellArgs fallback
|
|
// (login flags, PowerShell -NoLogo) still applies — only override it when the
|
|
// user has explicitly set launch args (#1221).
|
|
return { command: localShell, args: customArgs?.length ? customArgs : undefined };
|
|
}
|
|
|
|
const DISTRO_ICONS = new Set([
|
|
"ubuntu", "debian", "kali", "alpine", "opensuse",
|
|
"fedora", "arch", "oracle", "linux",
|
|
]);
|
|
|
|
export function getShellIconPath(iconId: string): string {
|
|
if (DISTRO_ICONS.has(iconId)) {
|
|
return `/distro/${iconId}.svg`;
|
|
}
|
|
return `/shells/${iconId}.svg`;
|
|
}
|
|
|
|
/** Distro icons are monochrome black and need `dark:invert` in dark mode */
|
|
export function isMonochromeShellIcon(iconId: string): boolean {
|
|
return DISTRO_ICONS.has(iconId);
|
|
}
|