adb-toolkit/frontend/src/components/views/ViewShell.tsx

150 lines
5.1 KiB
TypeScript

import { useState, useRef, useEffect } from 'react'
import { Terminal, Trash2, ChevronRight } from 'lucide-react'
import { RunShellCommand, RunAdbHostCommand } from '../../lib/wails'
interface HistoryEntry {
cmd: string
output: string
error?: boolean
mode: 'shell' | 'adb'
}
export default function ViewShell() {
const [history, setHistory] = useState<HistoryEntry[]>([
{ cmd: '', output: 'ADBKit Shell — commands run via adb shell (no pipes/redirects — args are split directly, no shell injection)\nSwitch to "adb" mode to run adb host commands (e.g. adb devices, adb logcat)', mode: 'shell' }
])
const [input, setInput] = useState('')
const [mode, setMode] = useState<'shell' | 'adb'>('shell')
const [loading, setLoading] = useState(false)
const [cmdHistory, setCmdHistory] = useState<string[]>([])
const [historyIdx, setHistoryIdx] = useState(-1)
const bottomRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [history])
const run = async () => {
const cmd = input.trim()
if (!cmd) return
setCmdHistory(prev => [cmd, ...prev.slice(0, 99)])
setHistoryIdx(-1)
setInput('')
setLoading(true)
try {
let output: string
if (mode === 'shell') {
output = await RunShellCommand(cmd)
} else {
output = await RunAdbHostCommand(cmd)
}
setHistory(prev => [...prev, { cmd, output: output || '(no output)', mode }])
} catch (e: any) {
setHistory(prev => [...prev, { cmd, output: String(e), error: true, mode }])
} finally {
setLoading(false)
}
}
const onKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
run()
return
}
if (e.key === 'ArrowUp') {
e.preventDefault()
const next = Math.min(historyIdx + 1, cmdHistory.length - 1)
setHistoryIdx(next)
setInput(cmdHistory[next] || '')
}
if (e.key === 'ArrowDown') {
e.preventDefault()
const next = Math.max(historyIdx - 1, -1)
setHistoryIdx(next)
setInput(next === -1 ? '' : cmdHistory[next])
}
}
return (
<div className="flex flex-col h-full">
{/* Toolbar */}
<div className="border-b border-bg-border px-4 py-2 flex items-center gap-3 shrink-0">
<Terminal size={14} className="text-accent-green" />
<span className="text-xs text-text-muted">Mode:</span>
<div className="flex gap-1 bg-bg-raised rounded p-0.5">
{(['shell', 'adb'] as const).map(m => (
<button
key={m}
onClick={() => setMode(m)}
className={`px-3 py-0.5 rounded text-xs font-medium transition-colors ${
mode === m ? 'bg-accent-green/20 text-accent-green' : 'text-text-muted hover:text-text-secondary'
}`}
>
{m === 'shell' ? 'adb shell' : 'adb host'}
</button>
))}
</div>
<div className="flex-1" />
<button
onClick={() => setHistory([{ cmd: '', output: 'Terminal cleared.', mode }])}
className="btn-ghost text-xs"
>
<Trash2 size={12} /> Clear
</button>
</div>
{/* Output */}
<div
className="flex-1 overflow-auto p-4 font-mono text-xs space-y-3 bg-bg-base cursor-text"
onClick={() => inputRef.current?.focus()}
>
{history.map((entry, i) => (
<div key={i}>
{entry.cmd && (
<div className="flex items-center gap-2 text-accent-green mb-1">
<span className="text-text-muted">[{entry.mode}]$</span>
<span>{entry.cmd}</span>
</div>
)}
<pre
className={`whitespace-pre-wrap break-words leading-relaxed ${
entry.error ? 'text-danger' : 'text-text-secondary'
}`}
>
{entry.output}
</pre>
</div>
))}
{loading && (
<div className="flex items-center gap-2 text-text-muted">
<span className="animate-pulse"></span>
<span>Running...</span>
</div>
)}
<div ref={bottomRef} />
</div>
{/* Input */}
<div className="border-t border-bg-border px-4 py-2 flex items-center gap-2 bg-bg-surface shrink-0">
<span className="text-accent-green font-mono text-xs shrink-0">[{mode}]$</span>
<ChevronRight size={12} className="text-text-muted shrink-0" />
<input
ref={inputRef}
autoFocus
className="flex-1 bg-transparent text-text-primary font-mono text-xs focus:outline-none placeholder:text-text-muted"
placeholder={mode === 'shell' ? 'ls /sdcard' : 'devices'}
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={onKeyDown}
disabled={loading}
/>
{loading && (
<div className="w-3 h-3 border border-accent-green border-t-transparent rounded-full animate-spin shrink-0" />
)}
</div>
</div>
)
}