225 lines
8.4 KiB
TypeScript
225 lines
8.4 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import { AndroidVM } from '../types';
|
|
import {
|
|
Terminal,
|
|
Send,
|
|
Copy,
|
|
Check,
|
|
Trash2,
|
|
Play,
|
|
ShieldAlert,
|
|
Cpu,
|
|
Layers,
|
|
Search,
|
|
} from 'lucide-react';
|
|
|
|
interface AdbConsoleProps {
|
|
vm: AndroidVM;
|
|
vms: AndroidVM[];
|
|
onSelectVm: (vmId: string) => void;
|
|
onExecuteAdb: (command: string) => Promise<string>;
|
|
}
|
|
|
|
export const AdbConsole: React.FC<AdbConsoleProps> = ({
|
|
vm,
|
|
vms,
|
|
onSelectVm,
|
|
onExecuteAdb,
|
|
}) => {
|
|
const [commandInput, setCommandInput] = useState('');
|
|
const [terminalHistory, setTerminalHistory] = useState<
|
|
{ command: string; output: string; timestamp: string }[]
|
|
>([
|
|
{
|
|
command: 'adb connect 192.168.122.105:5555',
|
|
output: `* daemon not running; starting now at tcp:5037\n* daemon started successfully\nconnected to ${vm.ipAddress}:${vm.adbPort}`,
|
|
timestamp: new Date().toLocaleTimeString(),
|
|
},
|
|
]);
|
|
const [isExecuting, setIsExecuting] = useState(false);
|
|
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
|
|
const terminalEndRef = useRef<HTMLDivElement>(null);
|
|
|
|
const scrollToBottom = () => {
|
|
terminalEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
};
|
|
|
|
useEffect(() => {
|
|
scrollToBottom();
|
|
}, [terminalHistory]);
|
|
|
|
const handleRunCommand = async (cmdToRun?: string) => {
|
|
const cmd = (cmdToRun || commandInput).trim();
|
|
if (!cmd) return;
|
|
|
|
setIsExecuting(true);
|
|
const result = await onExecuteAdb(cmd);
|
|
|
|
setTerminalHistory((prev) => [
|
|
...prev,
|
|
{
|
|
command: cmd,
|
|
output: result,
|
|
timestamp: new Date().toLocaleTimeString(),
|
|
},
|
|
]);
|
|
|
|
setCommandInput('');
|
|
setIsExecuting(false);
|
|
};
|
|
|
|
const presetCommands = [
|
|
{ label: 'System Build Props', cmd: 'getprop' },
|
|
{ label: 'List Packages', cmd: 'pm list packages' },
|
|
{ label: 'Check Root Privilege', cmd: 'su' },
|
|
{ label: 'CPU Info', cmd: 'cat /proc/cpuinfo' },
|
|
{ label: 'Battery Dumpsys', cmd: 'dumpsys battery' },
|
|
{ label: 'Live Logcat', cmd: 'logcat -d' },
|
|
];
|
|
|
|
const handleCopy = (text: string, index: number) => {
|
|
navigator.clipboard.writeText(text);
|
|
setCopiedIndex(index);
|
|
setTimeout(() => setCopiedIndex(null), 2000);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Target VM Selector & Network Status Bar */}
|
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 bg-slate-900/80 p-4 rounded-2xl border border-slate-800">
|
|
<div className="flex items-center space-x-3 w-full sm:w-auto">
|
|
<Terminal className="w-5 h-5 text-emerald-400" />
|
|
<div className="flex items-center space-x-2">
|
|
<span className="text-xs font-bold text-slate-300">Target Instance:</span>
|
|
<select
|
|
value={vm.id}
|
|
onChange={(e) => onSelectVm(e.target.value)}
|
|
className="bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-emerald-400 font-bold focus:outline-none focus:border-emerald-500"
|
|
>
|
|
{vms.map((v) => (
|
|
<option key={v.id} value={v.id}>
|
|
{v.name} ({v.ipAddress}:{v.adbPort})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-3 text-xs font-mono">
|
|
<span className="px-2.5 py-1 bg-emerald-950/60 border border-emerald-800 text-emerald-300 rounded-lg">
|
|
ADB TCP: {vm.ipAddress}:{vm.adbPort}
|
|
</span>
|
|
<span className="px-2.5 py-1 bg-slate-950 border border-slate-800 text-slate-400 rounded-lg uppercase">
|
|
SELinux: {vm.bootloader.selinuxMode}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Preset Quick Commands */}
|
|
<div className="space-y-2">
|
|
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider block">
|
|
Quick ADB Shell Presets:
|
|
</span>
|
|
<div className="flex items-center space-x-2 overflow-x-auto pb-1">
|
|
{presetCommands.map((preset) => (
|
|
<button
|
|
key={preset.cmd}
|
|
onClick={() => handleRunCommand(preset.cmd)}
|
|
disabled={isExecuting}
|
|
className="px-3 py-1.5 bg-slate-900 hover:bg-slate-800 text-slate-300 border border-slate-800 hover:border-emerald-500/50 rounded-xl text-xs font-mono whitespace-nowrap transition-all flex items-center space-x-1.5 shrink-0"
|
|
>
|
|
<Play className="w-3 h-3 text-emerald-400" />
|
|
<span>{preset.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Interactive ADB Shell Terminal View */}
|
|
<div className="bg-slate-950 border border-slate-800 rounded-2xl overflow-hidden flex flex-col h-[520px] shadow-2xl">
|
|
{/* Terminal Header */}
|
|
<div className="bg-slate-900 px-4 py-3 border-b border-slate-800 flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-3 h-3 rounded-full bg-rose-500/80" />
|
|
<div className="w-3 h-3 rounded-full bg-amber-500/80" />
|
|
<div className="w-3 h-3 rounded-full bg-emerald-500/80" />
|
|
<span className="text-xs font-mono text-slate-400 ml-2">
|
|
adb shell shell@{vm.profile.id}:/$
|
|
</span>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => setTerminalHistory([])}
|
|
className="p-1.5 text-slate-400 hover:text-rose-400 hover:bg-slate-800 rounded-lg text-xs flex items-center space-x-1 transition-colors"
|
|
title="Clear Terminal Output"
|
|
>
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|
<span className="hidden sm:inline">Clear</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Console Log Area */}
|
|
<div className="flex-1 p-4 font-mono text-xs overflow-y-auto space-y-4">
|
|
{terminalHistory.map((item, idx) => (
|
|
<div key={idx} className="space-y-1.5 border-b border-slate-900/80 pb-3">
|
|
<div className="flex items-center justify-between text-slate-400">
|
|
<div className="flex items-center space-x-2">
|
|
<span className="text-emerald-400">$</span>
|
|
<span className="font-bold text-slate-200">{item.command}</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<span className="text-[10px] text-slate-600">{item.timestamp}</span>
|
|
<button
|
|
onClick={() => handleCopy(item.output, idx)}
|
|
className="p-1 text-slate-500 hover:text-slate-200"
|
|
>
|
|
{copiedIndex === idx ? (
|
|
<Check className="w-3 h-3 text-emerald-400" />
|
|
) : (
|
|
<Copy className="w-3 h-3" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<pre className="text-slate-300 bg-slate-900/60 p-3 rounded-xl border border-slate-800/80 whitespace-pre-wrap font-mono leading-relaxed overflow-x-auto text-[11px]">
|
|
{item.output}
|
|
</pre>
|
|
</div>
|
|
))}
|
|
<div ref={terminalEndRef} />
|
|
</div>
|
|
|
|
{/* Terminal Command Input */}
|
|
<div className="p-3 bg-slate-900 border-t border-slate-800">
|
|
<form
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
handleRunCommand();
|
|
}}
|
|
className="flex items-center space-x-2"
|
|
>
|
|
<span className="text-emerald-400 font-mono font-bold text-xs">$</span>
|
|
<input
|
|
type="text"
|
|
value={commandInput}
|
|
onChange={(e) => setCommandInput(e.target.value)}
|
|
placeholder="Type adb shell command (e.g. getprop, logcat, su)..."
|
|
disabled={isExecuting}
|
|
className="flex-1 bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2 text-xs font-mono text-slate-100 placeholder-slate-600 focus:outline-none focus:border-emerald-500"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
disabled={isExecuting || !commandInput.trim()}
|
|
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 disabled:bg-slate-800 text-white font-bold rounded-xl text-xs flex items-center space-x-1.5 transition-all"
|
|
>
|
|
<Send className="w-3.5 h-3.5" />
|
|
<span>Run</span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|