feat: release OpsXShell 2.1.0 native Tauri edition
This commit is contained in:
1 parent
e9a0d21758
commit
a3dba0c6ed
311 files changed
+31748
-14897
No files matched your search
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Local Shell picker — choose Zsh / Bash (default marked)
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState, useCallback } from 'react'
|
||||
import { Laptop, ChevronDown } from 'lucide-react'
|
||||
import { useTabStore } from '../../../stores/tabStore'
|
||||
import { shellDisplayFromPath } from '../../../utils/shellName'
|
||||
|
||||
export interface ShellOption {
|
||||
path: string
|
||||
name: string
|
||||
kind: string
|
||||
isDefault: boolean
|
||||
}
|
||||
|
||||
interface LocalShellMenuProps {
|
||||
/** Compact icon-only trigger (sidebar) */
|
||||
compact?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function LocalShellMenu({ compact = false, className = '' }: LocalShellMenuProps) {
|
||||
const { addTab } = useTabStore()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [shells, setShells] = useState<ShellOption[]>([])
|
||||
const rootRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const fallback = () => setShells([
|
||||
{ path: '/bin/zsh', name: 'Zsh', kind: 'zsh', isDefault: true },
|
||||
{ path: '/bin/bash', name: 'Bash', kind: 'bash', isDefault: false },
|
||||
])
|
||||
|
||||
void window.opsxApi?.localListShells?.().then((res) => {
|
||||
if (res?.success && res.shells?.length) {
|
||||
setShells(res.shells)
|
||||
} else {
|
||||
fallback()
|
||||
}
|
||||
}).catch(fallback)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const onDoc = (e: MouseEvent) => {
|
||||
if (rootRef.current && !rootRef.current.contains(e.target as Node)) {
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', onDoc)
|
||||
return () => document.removeEventListener('mousedown', onDoc)
|
||||
}, [open])
|
||||
|
||||
const openShell = useCallback(
|
||||
(opt?: ShellOption) => {
|
||||
const chosen =
|
||||
opt ||
|
||||
shells.find((s) => s.isDefault) ||
|
||||
shells[0] || {
|
||||
path: '',
|
||||
name: 'Local',
|
||||
kind: 'unknown',
|
||||
isDefault: true,
|
||||
}
|
||||
const display = shellDisplayFromPath(chosen.path || chosen.name)
|
||||
addTab({
|
||||
title: display.title,
|
||||
type: 'terminal',
|
||||
isLocal: true,
|
||||
localShellPath: chosen.path || undefined,
|
||||
localShellKind: display.kind,
|
||||
})
|
||||
setOpen(false)
|
||||
},
|
||||
[addTab, shells]
|
||||
)
|
||||
|
||||
const badgeClass = (kind: string) => {
|
||||
if (kind === 'zsh') return 'bg-teal-700 text-teal-100'
|
||||
if (kind === 'bash') return 'bg-dark-600 text-emerald-300'
|
||||
if (kind === 'fish') return 'bg-violet-700 text-violet-100'
|
||||
return 'bg-dark-600 text-content-secondary'
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={rootRef} className={`relative ${className}`}>
|
||||
{compact ? (
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openShell()}
|
||||
title="打开默认本地 Shell"
|
||||
className="flex items-center justify-center px-2.5 py-2 bg-dark-700 hover:bg-dark-600 border border-dark-600/60 rounded-l-lg text-sm font-medium transition-colors text-content-primary border-r-0"
|
||||
>
|
||||
<Laptop className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
title="选择 Shell"
|
||||
className="flex items-center justify-center px-1.5 py-2 bg-dark-700 hover:bg-dark-600 border border-dark-600/60 rounded-r-lg text-content-secondary"
|
||||
>
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="w-full flex items-center gap-3 p-4 rounded-2xl border border-dark-700/50 bg-dark-800/70 hover:bg-dark-800 hover:border-accent-blue/40 transition-all text-left"
|
||||
>
|
||||
<div className="w-11 h-11 rounded-xl bg-accent-blue/15 border border-accent-blue/20 flex items-center justify-center">
|
||||
<Laptop className="w-5 h-5 text-accent-blue" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-content-primary">本地 Shell</div>
|
||||
<div className="text-xs text-txt-muted mt-0.5">选择 Zsh / Bash 打开本机终端</div>
|
||||
</div>
|
||||
<ChevronDown className={`w-4 h-4 text-txt-faint transition-transform ${open ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{open && (
|
||||
<div
|
||||
className={`
|
||||
absolute z-50 mt-1 py-1.5 rounded-xl border border-dark-600/80 bg-dark-800 shadow-xl
|
||||
min-w-[200px]
|
||||
${compact ? 'right-0' : 'left-0 right-0'}
|
||||
`}
|
||||
>
|
||||
<div className="px-3 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-txt-faint">
|
||||
本地 Shell
|
||||
</div>
|
||||
{shells.map((s) => (
|
||||
<button
|
||||
key={s.path}
|
||||
type="button"
|
||||
onClick={() => openShell(s)}
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2 hover:bg-dark-700/70 transition-colors text-left"
|
||||
>
|
||||
<span
|
||||
className={`w-7 h-7 rounded-lg flex items-center justify-center text-[10px] font-bold flex-shrink-0 ${badgeClass(s.kind)}`}
|
||||
>
|
||||
{s.kind === 'bash' ? '$' : s.kind === 'zsh' ? 'zsh' : s.kind.slice(0, 2)}
|
||||
</span>
|
||||
<span className="text-sm text-content-primary flex-1">{s.name}</span>
|
||||
{s.isDefault && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-md bg-dark-700 text-txt-muted border border-dark-600">
|
||||
默认
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Programmatic open helper for menu / palette */
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function openLocalShellTab(shell?: { path?: string; kind?: string; name?: string }) {
|
||||
const { addTab } = useTabStore.getState()
|
||||
const display = shellDisplayFromPath(shell?.path || shell?.name)
|
||||
addTab({
|
||||
title: display.title,
|
||||
type: 'terminal',
|
||||
isLocal: true,
|
||||
localShellPath: shell?.path,
|
||||
localShellKind: shell?.kind || display.kind,
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user