Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29b41568dc | |||
| 4982228474 | |||
| 068efe3d47 | |||
| 2f42414e20 | |||
| 9dee5c9e96 | |||
| d228a87777 |
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@ -15,13 +15,13 @@ jobs:
|
||||
include:
|
||||
- os: windows-latest
|
||||
artifact_name: EasyShell-Windows
|
||||
build_cmd: npm run build && npx electron-builder --win
|
||||
build_cmd: npm run build && npx electron-builder --win --publish never
|
||||
- os: macos-latest
|
||||
artifact_name: EasyShell-Mac
|
||||
build_cmd: npm run build && npx electron-builder --mac
|
||||
build_cmd: npm run build && npx electron-builder --mac --publish never
|
||||
- os: ubuntu-latest
|
||||
artifact_name: EasyShell-Linux
|
||||
build_cmd: npm run build && npx electron-builder --linux
|
||||
build_cmd: npm run build && npx electron-builder --linux --publish never
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@ -30,11 +30,11 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: npm install
|
||||
|
||||
- name: Generate icons
|
||||
run: npm run icons
|
||||
@ -42,8 +42,6 @@ jobs:
|
||||
|
||||
- name: Build application
|
||||
run: ${{ matrix.build_cmd }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "easyshell",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "easyshell",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.3",
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^5.6.0",
|
||||
"@capacitor/app": "^5.0.6",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "easyshell",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.5",
|
||||
"description": "跨平台远程Shell管理终端 - 支持 Windows/Mac/Linux/Android",
|
||||
"author": "EasyShell Team",
|
||||
"main": "main.js",
|
||||
|
||||
288
src/components/CommandSuggestPanel.js
Normal file
288
src/components/CommandSuggestPanel.js
Normal file
@ -0,0 +1,288 @@
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { FiTerminal, FiX, FiCommand, FiFolder, FiFile, FiServer, FiDatabase, FiPackage, FiSettings, FiSearch } from 'react-icons/fi';
|
||||
|
||||
// 常用命令库
|
||||
const COMMAND_DATABASE = [
|
||||
// 文件操作
|
||||
{ cmd: 'ls', desc: '列出目录内容', category: 'file' },
|
||||
{ cmd: 'ls -la', desc: '详细列出所有文件', category: 'file' },
|
||||
{ cmd: 'ls -lh', desc: '人类可读格式', category: 'file' },
|
||||
{ cmd: 'cd', desc: '切换目录', category: 'file' },
|
||||
{ cmd: 'cd ..', desc: '返回上级目录', category: 'file' },
|
||||
{ cmd: 'cd ~', desc: '返回主目录', category: 'file' },
|
||||
{ cmd: 'cd -', desc: '返回上次目录', category: 'file' },
|
||||
{ cmd: 'pwd', desc: '显示当前目录', category: 'file' },
|
||||
{ cmd: 'mkdir', desc: '创建目录', category: 'file' },
|
||||
{ cmd: 'mkdir -p', desc: '递归创建目录', category: 'file' },
|
||||
{ cmd: 'rm', desc: '删除文件', category: 'file' },
|
||||
{ cmd: 'rm -rf', desc: '强制递归删除', category: 'file' },
|
||||
{ cmd: 'cp', desc: '复制文件', category: 'file' },
|
||||
{ cmd: 'cp -r', desc: '递归复制目录', category: 'file' },
|
||||
{ cmd: 'mv', desc: '移动/重命名', category: 'file' },
|
||||
{ cmd: 'touch', desc: '创建空文件', category: 'file' },
|
||||
{ cmd: 'cat', desc: '查看文件内容', category: 'file' },
|
||||
{ cmd: 'head -n 20', desc: '查看前20行', category: 'file' },
|
||||
{ cmd: 'tail -f', desc: '实时追踪日志', category: 'file' },
|
||||
{ cmd: 'less', desc: '分页查看文件', category: 'file' },
|
||||
{ cmd: 'find . -name', desc: '按名称查找', category: 'file' },
|
||||
{ cmd: 'chmod 755', desc: '设置可执行权限', category: 'file' },
|
||||
{ cmd: 'chown', desc: '修改所有者', category: 'file' },
|
||||
{ cmd: 'tar -zxvf', desc: '解压 tar.gz', category: 'file' },
|
||||
{ cmd: 'tar -zcvf', desc: '压缩为 tar.gz', category: 'file' },
|
||||
{ cmd: 'unzip', desc: '解压 zip', category: 'file' },
|
||||
|
||||
// 文本处理
|
||||
{ cmd: 'grep', desc: '文本搜索', category: 'text' },
|
||||
{ cmd: 'grep -rn', desc: '递归搜索带行号', category: 'text' },
|
||||
{ cmd: 'sed', desc: '流编辑器', category: 'text' },
|
||||
{ cmd: 'awk', desc: '文本处理', category: 'text' },
|
||||
{ cmd: 'sort', desc: '排序', category: 'text' },
|
||||
{ cmd: 'uniq -c', desc: '去重并计数', category: 'text' },
|
||||
{ cmd: 'wc -l', desc: '统计行数', category: 'text' },
|
||||
|
||||
// 系统
|
||||
{ cmd: 'top', desc: '系统监控', category: 'system' },
|
||||
{ cmd: 'htop', desc: '增强版 top', category: 'system' },
|
||||
{ cmd: 'ps aux', desc: '查看所有进程', category: 'system' },
|
||||
{ cmd: 'kill -9', desc: '强制终止进程', category: 'system' },
|
||||
{ cmd: 'df -h', desc: '磁盘使用情况', category: 'system' },
|
||||
{ cmd: 'du -sh *', desc: '当前目录各项大小', category: 'system' },
|
||||
{ cmd: 'free -h', desc: '内存使用情况', category: 'system' },
|
||||
{ cmd: 'uptime', desc: '运行时间', category: 'system' },
|
||||
{ cmd: 'uname -a', desc: '系统信息', category: 'system' },
|
||||
{ cmd: 'whoami', desc: '当前用户', category: 'system' },
|
||||
|
||||
// 网络
|
||||
{ cmd: 'ping -c 4', desc: '测试连通性', category: 'network' },
|
||||
{ cmd: 'curl -I', desc: '获取响应头', category: 'network' },
|
||||
{ cmd: 'wget', desc: '下载文件', category: 'network' },
|
||||
{ cmd: 'netstat -tunlp', desc: '监听端口', category: 'network' },
|
||||
{ cmd: 'ss -tunlp', desc: '套接字统计', category: 'network' },
|
||||
{ cmd: 'ip addr', desc: 'IP 地址', category: 'network' },
|
||||
|
||||
// 包管理
|
||||
{ cmd: 'apt update', desc: '更新软件源', category: 'package' },
|
||||
{ cmd: 'apt install', desc: '安装软件', category: 'package' },
|
||||
{ cmd: 'yum install', desc: 'RHEL 安装', category: 'package' },
|
||||
{ cmd: 'npm install', desc: 'Node 安装', category: 'package' },
|
||||
{ cmd: 'pip install', desc: 'Python 安装', category: 'package' },
|
||||
|
||||
// 服务
|
||||
{ cmd: 'systemctl status', desc: '服务状态', category: 'service' },
|
||||
{ cmd: 'systemctl start', desc: '启动服务', category: 'service' },
|
||||
{ cmd: 'systemctl stop', desc: '停止服务', category: 'service' },
|
||||
{ cmd: 'systemctl restart', desc: '重启服务', category: 'service' },
|
||||
{ cmd: 'journalctl -f', desc: '实时日志', category: 'service' },
|
||||
|
||||
// Docker
|
||||
{ cmd: 'docker ps', desc: '运行中容器', category: 'docker' },
|
||||
{ cmd: 'docker ps -a', desc: '所有容器', category: 'docker' },
|
||||
{ cmd: 'docker images', desc: '镜像列表', category: 'docker' },
|
||||
{ cmd: 'docker exec -it', desc: '进入容器', category: 'docker' },
|
||||
{ cmd: 'docker logs -f', desc: '查看日志', category: 'docker' },
|
||||
{ cmd: 'docker-compose up -d', desc: '启动服务', category: 'docker' },
|
||||
{ cmd: 'docker-compose down', desc: '停止服务', category: 'docker' },
|
||||
|
||||
// Git
|
||||
{ cmd: 'git status', desc: '查看状态', category: 'git' },
|
||||
{ cmd: 'git add .', desc: '添加所有文件', category: 'git' },
|
||||
{ cmd: 'git commit -m', desc: '提交', category: 'git' },
|
||||
{ cmd: 'git push', desc: '推送', category: 'git' },
|
||||
{ cmd: 'git pull', desc: '拉取', category: 'git' },
|
||||
{ cmd: 'git log --oneline', desc: '简洁日志', category: 'git' },
|
||||
|
||||
// 其他
|
||||
{ cmd: 'clear', desc: '清屏', category: 'other' },
|
||||
{ cmd: 'history', desc: '命令历史', category: 'other' },
|
||||
{ cmd: 'vim', desc: '文本编辑器', category: 'other' },
|
||||
{ cmd: 'nano', desc: '简易编辑器', category: 'other' },
|
||||
{ cmd: 'exit', desc: '退出终端', category: 'other' },
|
||||
];
|
||||
|
||||
// 获取分类图标
|
||||
const getCategoryIcon = (category) => {
|
||||
switch (category) {
|
||||
case 'file': return FiFolder;
|
||||
case 'text': return FiFile;
|
||||
case 'system': return FiServer;
|
||||
case 'network': return FiDatabase;
|
||||
case 'package': return FiPackage;
|
||||
case 'service': return FiSettings;
|
||||
case 'docker': return FiPackage;
|
||||
case 'git': return FiSearch;
|
||||
default: return FiTerminal;
|
||||
}
|
||||
};
|
||||
|
||||
function CommandSuggestPanel({ visible, input, onSelect, onClose, onOpenCommandPalette, disabled }) {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const listRef = useRef(null);
|
||||
const selectedRef = useRef(null);
|
||||
|
||||
// 使用搜索框或输入内容过滤
|
||||
const filterText = searchText || input || '';
|
||||
|
||||
// 过滤命令
|
||||
const suggestions = useMemo(() => {
|
||||
if (!filterText) return COMMAND_DATABASE.slice(0, 20);
|
||||
|
||||
const query = filterText.toLowerCase().trim();
|
||||
return COMMAND_DATABASE.filter(item =>
|
||||
item.cmd.toLowerCase().includes(query) ||
|
||||
item.desc.toLowerCase().includes(query)
|
||||
).slice(0, 20);
|
||||
}, [filterText]);
|
||||
|
||||
// 重置选中索引
|
||||
useEffect(() => {
|
||||
setSelectedIndex(0);
|
||||
}, [filterText]);
|
||||
|
||||
// 滚动到选中项
|
||||
useEffect(() => {
|
||||
if (selectedRef.current && listRef.current) {
|
||||
selectedRef.current.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
}, [selectedIndex]);
|
||||
|
||||
// 键盘导航
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
setSelectedIndex(prev => (prev + 1) % suggestions.length);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
setSelectedIndex(prev => (prev - 1 + suggestions.length) % suggestions.length);
|
||||
} else if (e.key === 'Tab' && suggestions[selectedIndex]) {
|
||||
e.preventDefault();
|
||||
onSelect(suggestions[selectedIndex].cmd);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [visible, suggestions, selectedIndex, onSelect]);
|
||||
|
||||
// 同步输入到搜索框
|
||||
useEffect(() => {
|
||||
if (input && !searchText) {
|
||||
// 如果有终端输入且搜索框为空,显示终端输入的内容
|
||||
}
|
||||
}, [input, searchText]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ x: 300, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 300, opacity: 0 }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
className="absolute top-0 right-0 bottom-0 w-72 bg-shell-surface/95 backdrop-blur-xl
|
||||
border-l border-shell-border z-20 flex flex-col"
|
||||
>
|
||||
{/* 头部 */}
|
||||
<div className="px-4 py-3 border-b border-shell-border flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-shell-accent">
|
||||
<FiTerminal size={16} />
|
||||
<span className="font-display text-sm tracking-wide">SUGGEST</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 rounded hover:bg-shell-card/50 text-shell-text-dim hover:text-shell-text transition-colors"
|
||||
>
|
||||
<FiX size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<div className="px-3 py-2 border-b border-shell-border/50">
|
||||
<div className="relative">
|
||||
<FiSearch size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-shell-text-dim" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
placeholder={input ? `当前: ${input}` : "搜索命令..."}
|
||||
className="w-full pl-9 pr-3 py-2 bg-shell-card/50 border border-shell-border/50 rounded-lg
|
||||
text-sm text-shell-text placeholder-shell-text-dim focus:outline-none
|
||||
focus:border-shell-accent/50 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 命令列表 */}
|
||||
<div ref={listRef} className="flex-1 overflow-y-auto custom-scrollbar">
|
||||
{disabled ? (
|
||||
<div className="p-4 text-center text-shell-text-dim text-sm">
|
||||
请先连接服务器
|
||||
</div>
|
||||
) : suggestions.length === 0 ? (
|
||||
<div className="p-4 text-center text-shell-text-dim text-sm">
|
||||
未找到匹配的命令
|
||||
</div>
|
||||
) : (
|
||||
suggestions.map((item, index) => {
|
||||
const Icon = getCategoryIcon(item.category);
|
||||
const isSelected = index === selectedIndex;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${item.cmd}-${index}`}
|
||||
ref={isSelected ? selectedRef : null}
|
||||
onClick={() => !disabled && onSelect(item.cmd)}
|
||||
className={`
|
||||
px-3 py-2 cursor-pointer flex items-center gap-3 transition-all border-l-2
|
||||
${isSelected
|
||||
? 'bg-shell-accent/10 border-shell-accent'
|
||||
: 'border-transparent hover:bg-shell-card/30'
|
||||
}
|
||||
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
||||
`}
|
||||
>
|
||||
<Icon
|
||||
size={14}
|
||||
className={isSelected ? 'text-shell-accent' : 'text-shell-text-dim'}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className={`font-mono text-sm ${isSelected ? 'text-shell-accent' : 'text-shell-text'}`}>
|
||||
{item.cmd}
|
||||
</div>
|
||||
<div className="text-xs text-shell-text-dim truncate">
|
||||
{item.desc}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<div className="p-3 border-t border-shell-border/50 space-y-2">
|
||||
<div className="text-[10px] text-shell-text-dim text-center mb-2">
|
||||
↑↓ 导航 · Tab 补全 · 点击选择
|
||||
</div>
|
||||
<button
|
||||
onClick={onOpenCommandPalette}
|
||||
className="w-full flex items-center justify-center gap-2 px-3 py-2
|
||||
bg-shell-accent/10 hover:bg-shell-accent/20 border border-shell-accent/30
|
||||
rounded-lg text-shell-accent text-sm transition-colors"
|
||||
>
|
||||
<FiCommand size={14} />
|
||||
<span>打开命令面板</span>
|
||||
<span className="text-xs opacity-60">Ctrl+K</span>
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
||||
export default CommandSuggestPanel;
|
||||
@ -1,349 +0,0 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { FiTerminal, FiFolder, FiFile, FiSettings, FiSearch, FiPackage, FiServer, FiDatabase } from 'react-icons/fi';
|
||||
|
||||
// 常用 Linux/Unix 命令库 (不显示参数占位符)
|
||||
const COMMAND_DATABASE = [
|
||||
// 文件操作
|
||||
{ cmd: 'ls', desc: '列出目录内容', category: 'file' },
|
||||
{ cmd: 'ls -la', desc: '详细列出所有文件', category: 'file' },
|
||||
{ cmd: 'ls -lh', desc: '人类可读格式', category: 'file' },
|
||||
{ cmd: 'cd', desc: '切换目录', category: 'file' },
|
||||
{ cmd: 'cd ..', desc: '返回上级目录', category: 'file' },
|
||||
{ cmd: 'cd ~', desc: '返回主目录', category: 'file' },
|
||||
{ cmd: 'cd -', desc: '返回上次目录', category: 'file' },
|
||||
{ cmd: 'pwd', desc: '显示当前目录', category: 'file' },
|
||||
{ cmd: 'mkdir', desc: '创建目录', category: 'file' },
|
||||
{ cmd: 'mkdir -p', desc: '递归创建目录', category: 'file' },
|
||||
{ cmd: 'rmdir', desc: '删除空目录', category: 'file' },
|
||||
{ cmd: 'rm', desc: '删除文件', category: 'file' },
|
||||
{ cmd: 'rm -rf', desc: '强制递归删除', category: 'file' },
|
||||
{ cmd: 'cp', desc: '复制文件', category: 'file' },
|
||||
{ cmd: 'cp -r', desc: '递归复制目录', category: 'file' },
|
||||
{ cmd: 'mv', desc: '移动/重命名', category: 'file' },
|
||||
{ cmd: 'touch', desc: '创建空文件', category: 'file' },
|
||||
{ cmd: 'cat', desc: '查看文件内容', category: 'file' },
|
||||
{ cmd: 'head', desc: '查看文件头部', category: 'file' },
|
||||
{ cmd: 'head -n 20', desc: '查看前20行', category: 'file' },
|
||||
{ cmd: 'tail', desc: '查看文件尾部', category: 'file' },
|
||||
{ cmd: 'tail -f', desc: '实时追踪日志', category: 'file' },
|
||||
{ cmd: 'less', desc: '分页查看文件', category: 'file' },
|
||||
{ cmd: 'more', desc: '分页显示文件', category: 'file' },
|
||||
{ cmd: 'find', desc: '查找文件', category: 'file' },
|
||||
{ cmd: 'find . -name', desc: '按名称查找', category: 'file' },
|
||||
{ cmd: 'locate', desc: '快速定位文件', category: 'file' },
|
||||
{ cmd: 'chmod', desc: '修改权限', category: 'file' },
|
||||
{ cmd: 'chmod 755', desc: '设置可执行权限', category: 'file' },
|
||||
{ cmd: 'chmod 644', desc: '设置只读权限', category: 'file' },
|
||||
{ cmd: 'chown', desc: '修改所有者', category: 'file' },
|
||||
{ cmd: 'ln -s', desc: '创建软链接', category: 'file' },
|
||||
{ cmd: 'tar', desc: '打包文件', category: 'file' },
|
||||
{ cmd: 'tar -zxvf', desc: '解压 tar.gz', category: 'file' },
|
||||
{ cmd: 'tar -zcvf', desc: '压缩为 tar.gz', category: 'file' },
|
||||
{ cmd: 'zip -r', desc: '压缩目录', category: 'file' },
|
||||
{ cmd: 'unzip', desc: '解压 zip', category: 'file' },
|
||||
{ cmd: 'gzip', desc: '压缩文件', category: 'file' },
|
||||
{ cmd: 'gunzip', desc: '解压 gz', category: 'file' },
|
||||
|
||||
// 文本处理
|
||||
{ cmd: 'grep', desc: '文本搜索', category: 'text' },
|
||||
{ cmd: 'grep -rn', desc: '递归搜索带行号', category: 'text' },
|
||||
{ cmd: 'grep -i', desc: '忽略大小写搜索', category: 'text' },
|
||||
{ cmd: 'sed', desc: '流编辑器', category: 'text' },
|
||||
{ cmd: 'awk', desc: '文本处理', category: 'text' },
|
||||
{ cmd: 'sort', desc: '排序', category: 'text' },
|
||||
{ cmd: 'sort -n', desc: '数字排序', category: 'text' },
|
||||
{ cmd: 'uniq', desc: '去重', category: 'text' },
|
||||
{ cmd: 'uniq -c', desc: '去重并计数', category: 'text' },
|
||||
{ cmd: 'wc -l', desc: '统计行数', category: 'text' },
|
||||
{ cmd: 'wc -w', desc: '统计单词数', category: 'text' },
|
||||
{ cmd: 'cut', desc: '切割文本', category: 'text' },
|
||||
{ cmd: 'diff', desc: '比较文件', category: 'text' },
|
||||
{ cmd: 'echo', desc: '输出文本', category: 'text' },
|
||||
{ cmd: 'printf', desc: '格式化输出', category: 'text' },
|
||||
|
||||
// 系统信息
|
||||
{ cmd: 'top', desc: '系统监控', category: 'system' },
|
||||
{ cmd: 'htop', desc: '增强版 top', category: 'system' },
|
||||
{ cmd: 'ps aux', desc: '查看所有进程', category: 'system' },
|
||||
{ cmd: 'ps -ef', desc: '进程树', category: 'system' },
|
||||
{ cmd: 'kill', desc: '终止进程', category: 'system' },
|
||||
{ cmd: 'kill -9', desc: '强制终止进程', category: 'system' },
|
||||
{ cmd: 'killall', desc: '按名称终止', category: 'system' },
|
||||
{ cmd: 'df -h', desc: '磁盘使用情况', category: 'system' },
|
||||
{ cmd: 'du -sh', desc: '目录大小', category: 'system' },
|
||||
{ cmd: 'du -sh *', desc: '当前目录各项大小', category: 'system' },
|
||||
{ cmd: 'free -h', desc: '内存使用情况', category: 'system' },
|
||||
{ cmd: 'uptime', desc: '运行时间', category: 'system' },
|
||||
{ cmd: 'uname -a', desc: '系统信息', category: 'system' },
|
||||
{ cmd: 'hostname', desc: '主机名', category: 'system' },
|
||||
{ cmd: 'whoami', desc: '当前用户', category: 'system' },
|
||||
{ cmd: 'id', desc: '用户信息', category: 'system' },
|
||||
{ cmd: 'w', desc: '登录用户', category: 'system' },
|
||||
{ cmd: 'last', desc: '登录历史', category: 'system' },
|
||||
{ cmd: 'dmesg | tail', desc: '内核消息', category: 'system' },
|
||||
{ cmd: 'lsof', desc: '打开的文件', category: 'system' },
|
||||
{ cmd: 'lsof -i', desc: '网络连接', category: 'system' },
|
||||
{ cmd: 'lscpu', desc: 'CPU 信息', category: 'system' },
|
||||
{ cmd: 'lsmem', desc: '内存信息', category: 'system' },
|
||||
{ cmd: 'lsblk', desc: '块设备', category: 'system' },
|
||||
{ cmd: 'fdisk -l', desc: '磁盘分区', category: 'system' },
|
||||
|
||||
// 网络
|
||||
{ cmd: 'ping', desc: '测试连通性', category: 'network' },
|
||||
{ cmd: 'ping -c 4', desc: '发送4次ping', category: 'network' },
|
||||
{ cmd: 'curl', desc: 'HTTP 请求', category: 'network' },
|
||||
{ cmd: 'curl -I', desc: '获取响应头', category: 'network' },
|
||||
{ cmd: 'curl -O', desc: '下载文件', category: 'network' },
|
||||
{ cmd: 'wget', desc: '下载文件', category: 'network' },
|
||||
{ cmd: 'ssh', desc: 'SSH 连接', category: 'network' },
|
||||
{ cmd: 'scp', desc: '安全复制', category: 'network' },
|
||||
{ cmd: 'rsync -avz', desc: '同步文件', category: 'network' },
|
||||
{ cmd: 'netstat -tunlp', desc: '监听端口', category: 'network' },
|
||||
{ cmd: 'ss -tunlp', desc: '套接字统计', category: 'network' },
|
||||
{ cmd: 'ip addr', desc: 'IP 地址', category: 'network' },
|
||||
{ cmd: 'ip route', desc: '路由表', category: 'network' },
|
||||
{ cmd: 'ifconfig', desc: '网络接口', category: 'network' },
|
||||
{ cmd: 'route -n', desc: '路由表', category: 'network' },
|
||||
{ cmd: 'iptables -L -n', desc: '防火墙规则', category: 'network' },
|
||||
{ cmd: 'nslookup', desc: 'DNS 查询', category: 'network' },
|
||||
{ cmd: 'dig', desc: 'DNS 详细查询', category: 'network' },
|
||||
{ cmd: 'traceroute', desc: '路由追踪', category: 'network' },
|
||||
{ cmd: 'tcpdump -i', desc: '抓包', category: 'network' },
|
||||
|
||||
// 包管理
|
||||
{ cmd: 'apt update', desc: '更新软件源', category: 'package' },
|
||||
{ cmd: 'apt upgrade', desc: '升级软件包', category: 'package' },
|
||||
{ cmd: 'apt install', desc: '安装软件', category: 'package' },
|
||||
{ cmd: 'apt remove', desc: '卸载软件', category: 'package' },
|
||||
{ cmd: 'apt search', desc: '搜索软件', category: 'package' },
|
||||
{ cmd: 'yum install', desc: 'RHEL 安装', category: 'package' },
|
||||
{ cmd: 'yum update', desc: 'RHEL 更新', category: 'package' },
|
||||
{ cmd: 'dnf install', desc: 'Fedora 安装', category: 'package' },
|
||||
{ cmd: 'pacman -S', desc: 'Arch 安装', category: 'package' },
|
||||
{ cmd: 'pacman -Syu', desc: 'Arch 更新', category: 'package' },
|
||||
{ cmd: 'npm install', desc: 'Node 安装', category: 'package' },
|
||||
{ cmd: 'npm run', desc: 'Node 运行脚本', category: 'package' },
|
||||
{ cmd: 'pip install', desc: 'Python 安装', category: 'package' },
|
||||
{ cmd: 'pip3 install', desc: 'Python3 安装', category: 'package' },
|
||||
|
||||
// 服务管理
|
||||
{ cmd: 'systemctl status', desc: '服务状态', category: 'service' },
|
||||
{ cmd: 'systemctl start', desc: '启动服务', category: 'service' },
|
||||
{ cmd: 'systemctl stop', desc: '停止服务', category: 'service' },
|
||||
{ cmd: 'systemctl restart', desc: '重启服务', category: 'service' },
|
||||
{ cmd: 'systemctl enable', desc: '开机启动', category: 'service' },
|
||||
{ cmd: 'systemctl disable', desc: '禁止开机启动', category: 'service' },
|
||||
{ cmd: 'journalctl -f', desc: '实时日志', category: 'service' },
|
||||
{ cmd: 'journalctl -u', desc: '服务日志', category: 'service' },
|
||||
|
||||
// Docker
|
||||
{ cmd: 'docker ps', desc: '运行中容器', category: 'docker' },
|
||||
{ cmd: 'docker ps -a', desc: '所有容器', category: 'docker' },
|
||||
{ cmd: 'docker images', desc: '镜像列表', category: 'docker' },
|
||||
{ cmd: 'docker run', desc: '运行容器', category: 'docker' },
|
||||
{ cmd: 'docker exec -it', desc: '进入容器', category: 'docker' },
|
||||
{ cmd: 'docker logs -f', desc: '查看日志', category: 'docker' },
|
||||
{ cmd: 'docker stop', desc: '停止容器', category: 'docker' },
|
||||
{ cmd: 'docker rm', desc: '删除容器', category: 'docker' },
|
||||
{ cmd: 'docker rmi', desc: '删除镜像', category: 'docker' },
|
||||
{ cmd: 'docker-compose up -d', desc: '启动服务', category: 'docker' },
|
||||
{ cmd: 'docker-compose down', desc: '停止服务', category: 'docker' },
|
||||
{ cmd: 'docker-compose logs -f', desc: '查看日志', category: 'docker' },
|
||||
|
||||
// Git
|
||||
{ cmd: 'git status', desc: '查看状态', category: 'git' },
|
||||
{ cmd: 'git add .', desc: '添加所有文件', category: 'git' },
|
||||
{ cmd: 'git commit -m', desc: '提交', category: 'git' },
|
||||
{ cmd: 'git push', desc: '推送', category: 'git' },
|
||||
{ cmd: 'git pull', desc: '拉取', category: 'git' },
|
||||
{ cmd: 'git log --oneline', desc: '简洁日志', category: 'git' },
|
||||
{ cmd: 'git diff', desc: '查看差异', category: 'git' },
|
||||
{ cmd: 'git branch', desc: '分支列表', category: 'git' },
|
||||
{ cmd: 'git checkout', desc: '切换分支', category: 'git' },
|
||||
{ cmd: 'git merge', desc: '合并分支', category: 'git' },
|
||||
|
||||
// 其他
|
||||
{ cmd: 'clear', desc: '清屏', category: 'other' },
|
||||
{ cmd: 'history', desc: '命令历史', category: 'other' },
|
||||
{ cmd: 'alias', desc: '别名列表', category: 'other' },
|
||||
{ cmd: 'which', desc: '命令路径', category: 'other' },
|
||||
{ cmd: 'whereis', desc: '二进制位置', category: 'other' },
|
||||
{ cmd: 'man', desc: '查看手册', category: 'other' },
|
||||
{ cmd: 'date', desc: '日期时间', category: 'other' },
|
||||
{ cmd: 'cal', desc: '日历', category: 'other' },
|
||||
{ cmd: 'env', desc: '环境变量', category: 'other' },
|
||||
{ cmd: 'export', desc: '设置变量', category: 'other' },
|
||||
{ cmd: 'source', desc: '执行脚本', category: 'other' },
|
||||
{ cmd: 'crontab -l', desc: '查看定时任务', category: 'other' },
|
||||
{ cmd: 'crontab -e', desc: '编辑定时任务', category: 'other' },
|
||||
{ cmd: 'nohup', desc: '后台运行', category: 'other' },
|
||||
{ cmd: 'screen -S', desc: '创建会话', category: 'other' },
|
||||
{ cmd: 'screen -r', desc: '恢复会话', category: 'other' },
|
||||
{ cmd: 'tmux new -s', desc: '创建会话', category: 'other' },
|
||||
{ cmd: 'tmux attach -t', desc: '恢复会话', category: 'other' },
|
||||
{ cmd: 'vim', desc: '文本编辑器', category: 'other' },
|
||||
{ cmd: 'nano', desc: '简易编辑器', category: 'other' },
|
||||
{ cmd: 'vi', desc: 'Vi 编辑器', category: 'other' },
|
||||
{ cmd: 'exit', desc: '退出终端', category: 'other' },
|
||||
{ cmd: 'reboot', desc: '重启系统', category: 'other' },
|
||||
{ cmd: 'shutdown -h now', desc: '立即关机', category: 'other' },
|
||||
];
|
||||
|
||||
// 获取分类图标
|
||||
const getCategoryIcon = (category) => {
|
||||
switch (category) {
|
||||
case 'file': return FiFolder;
|
||||
case 'text': return FiFile;
|
||||
case 'system': return FiServer;
|
||||
case 'network': return FiDatabase;
|
||||
case 'package': return FiPackage;
|
||||
case 'service': return FiSettings;
|
||||
case 'docker': return FiPackage;
|
||||
case 'git': return FiSearch;
|
||||
default: return FiTerminal;
|
||||
}
|
||||
};
|
||||
|
||||
function CommandSuggestions({ input, position, onSelect, onClose, visible, cursorPosition }) {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const listRef = useRef(null);
|
||||
const selectedRef = useRef(null);
|
||||
|
||||
// 根据输入过滤命令
|
||||
const suggestions = React.useMemo(() => {
|
||||
if (!input || input.length < 1) return [];
|
||||
|
||||
const query = input.toLowerCase().trim();
|
||||
|
||||
// 匹配以输入开头的命令
|
||||
const results = COMMAND_DATABASE.filter(item =>
|
||||
item.cmd.toLowerCase().startsWith(query) ||
|
||||
item.desc.toLowerCase().includes(query)
|
||||
);
|
||||
|
||||
return results.slice(0, 8);
|
||||
}, [input]);
|
||||
|
||||
// 重置选中索引
|
||||
useEffect(() => {
|
||||
setSelectedIndex(0);
|
||||
}, [input]);
|
||||
|
||||
// 滚动到选中项
|
||||
useEffect(() => {
|
||||
if (selectedRef.current && listRef.current) {
|
||||
selectedRef.current.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
}, [selectedIndex]);
|
||||
|
||||
// 键盘导航
|
||||
useEffect(() => {
|
||||
if (!visible || suggestions.length === 0) return;
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setSelectedIndex(prev => (prev + 1) % suggestions.length);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setSelectedIndex(prev => (prev - 1 + suggestions.length) % suggestions.length);
|
||||
break;
|
||||
case 'Tab':
|
||||
// 只用 Tab 选择
|
||||
if (suggestions[selectedIndex]) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onSelect(suggestions[selectedIndex].cmd);
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown, true);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown, true);
|
||||
}, [visible, suggestions, selectedIndex, onSelect, onClose]);
|
||||
|
||||
if (!visible || suggestions.length === 0) return null;
|
||||
|
||||
// 计算提示框位置(在光标下方)
|
||||
const positionStyle = cursorPosition ? {
|
||||
top: cursorPosition.top + 20,
|
||||
left: cursorPosition.left,
|
||||
} : {
|
||||
bottom: position?.bottom || 60,
|
||||
left: position?.left || 16,
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -5, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -5, scale: 0.98 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
className="absolute z-50 bg-shell-surface/95 backdrop-blur-xl border border-shell-accent/30
|
||||
rounded-lg shadow-2xl overflow-hidden min-w-[240px] max-w-[360px]"
|
||||
style={positionStyle}
|
||||
>
|
||||
{/* 简化的标题栏 */}
|
||||
<div className="px-2 py-1 border-b border-shell-border/30 flex items-center justify-between bg-shell-card/30">
|
||||
<span className="text-[10px] text-shell-text-dim font-mono">
|
||||
↑↓ 导航
|
||||
</span>
|
||||
<span className="text-[10px] text-shell-accent font-mono">
|
||||
Tab 补全
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 建议列表 */}
|
||||
<div ref={listRef} className="max-h-[200px] overflow-y-auto custom-scrollbar">
|
||||
{suggestions.map((item, index) => {
|
||||
const Icon = getCategoryIcon(item.category);
|
||||
const isSelected = index === selectedIndex;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${item.cmd}-${index}`}
|
||||
ref={isSelected ? selectedRef : null}
|
||||
onClick={() => onSelect(item.cmd)}
|
||||
className={`
|
||||
px-2 py-1.5 cursor-pointer flex items-center gap-2 transition-all
|
||||
${isSelected
|
||||
? 'bg-shell-accent/20 border-l-2 border-shell-accent'
|
||||
: 'hover:bg-shell-card/50 border-l-2 border-transparent'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Icon
|
||||
size={12}
|
||||
className={isSelected ? 'text-shell-accent' : 'text-shell-text-dim'}
|
||||
/>
|
||||
<div className="flex-1 min-w-0 flex items-center gap-2">
|
||||
<span className={`font-mono text-xs ${isSelected ? 'text-shell-accent' : 'text-shell-text'}`}>
|
||||
{item.cmd}
|
||||
</span>
|
||||
<span className="text-[10px] text-shell-text-dim truncate">
|
||||
{item.desc}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
||||
export default CommandSuggestions;
|
||||
@ -5,8 +5,8 @@ import { FitAddon } from '@xterm/addon-fit';
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
import { WebglAddon } from '@xterm/addon-webgl';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
import { FiCommand, FiRefreshCw, FiInfo, FiFolder, FiActivity, FiZap } from 'react-icons/fi';
|
||||
import CommandSuggestions from './CommandSuggestions';
|
||||
import { FiCommand, FiRefreshCw, FiInfo, FiFolder, FiActivity, FiZap, FiTerminal, FiX } from 'react-icons/fi';
|
||||
import CommandSuggestPanel from './CommandSuggestPanel';
|
||||
|
||||
function Terminal({ tabId, hostId, isActive, onConnectionChange, onShowCommandPalette, onToggleInfoPanel, onOpenSFTP, showInfoPanel, onCloseTab }) {
|
||||
const containerRef = useRef(null);
|
||||
@ -43,8 +43,6 @@ function Terminal({ tabId, hostId, isActive, onConnectionChange, onShowCommandPa
|
||||
|
||||
// 清除当前输入的内容(发送对应数量的退格)
|
||||
const backspaces = '\b'.repeat(inputBufferRef.current.length);
|
||||
// 也发送删除字符来清除显示
|
||||
const deleteChars = ' '.repeat(inputBufferRef.current.length) + '\b'.repeat(inputBufferRef.current.length);
|
||||
|
||||
// 先清除当前输入
|
||||
if (inputBufferRef.current.length > 0) {
|
||||
@ -57,16 +55,14 @@ function Terminal({ tabId, hostId, isActive, onConnectionChange, onShowCommandPa
|
||||
// 更新状态
|
||||
inputBufferRef.current = command;
|
||||
setCurrentInput(command);
|
||||
setShowSuggestions(false);
|
||||
|
||||
// 重新聚焦终端
|
||||
xtermRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
// 关闭建议
|
||||
const handleCloseSuggestions = useCallback(() => {
|
||||
setShowSuggestions(false);
|
||||
xtermRef.current?.focus();
|
||||
// 切换建议面板
|
||||
const toggleSuggestPanel = useCallback(() => {
|
||||
setShowSuggestPanel(prev => !prev);
|
||||
}, []);
|
||||
|
||||
// 当标签页变为活动状态时,自动聚焦终端
|
||||
@ -82,8 +78,7 @@ function Terminal({ tabId, hostId, isActive, onConnectionChange, onShowCommandPa
|
||||
const [connectionId, setConnectionId] = useState(null);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [currentInput, setCurrentInput] = useState('');
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const [cursorPosition, setCursorPosition] = useState(null);
|
||||
const [showSuggestPanel, setShowSuggestPanel] = useState(false);
|
||||
const inputBufferRef = useRef('');
|
||||
const [error, setError] = useState(null);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
@ -281,50 +276,23 @@ function Terminal({ tabId, hostId, isActive, onConnectionChange, onShowCommandPa
|
||||
window.electronAPI.ssh.write(connectionIdRef.current, data);
|
||||
}
|
||||
|
||||
// 更新光标位置
|
||||
const updateCursorPosition = () => {
|
||||
if (terminalRef.current && xtermRef.current) {
|
||||
const cursorY = xtermRef.current.buffer.active.cursorY;
|
||||
const cursorX = xtermRef.current.buffer.active.cursorX;
|
||||
const cellWidth = terminalRef.current.offsetWidth / xtermRef.current.cols;
|
||||
const cellHeight = terminalRef.current.offsetHeight / xtermRef.current.rows;
|
||||
|
||||
setCursorPosition({
|
||||
top: (cursorY + 1) * cellHeight + 60, // 加上工具栏高度
|
||||
left: cursorX * cellWidth + 16,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 追踪用户输入以显示命令建议
|
||||
// 追踪用户输入(用于右侧面板搜索)
|
||||
if (data === '\r' || data === '\n') {
|
||||
// 回车:清空输入缓冲区
|
||||
inputBufferRef.current = '';
|
||||
setCurrentInput('');
|
||||
setShowSuggestions(false);
|
||||
} else if (data === '\x7f' || data === '\b') {
|
||||
// 退格:删除最后一个字符
|
||||
inputBufferRef.current = inputBufferRef.current.slice(0, -1);
|
||||
setCurrentInput(inputBufferRef.current);
|
||||
setShowSuggestions(inputBufferRef.current.length > 0);
|
||||
updateCursorPosition();
|
||||
} else if (data === '\x1b' || data.startsWith('\x1b[')) {
|
||||
// ESC 或方向键:隐藏建议
|
||||
setShowSuggestions(false);
|
||||
} else if (data === '\t') {
|
||||
// Tab:如果有建议就不传递给终端
|
||||
// (Tab 补全由 CommandSuggestions 组件处理)
|
||||
} else if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
||||
// 普通字符输入
|
||||
inputBufferRef.current += data;
|
||||
setCurrentInput(inputBufferRef.current);
|
||||
setShowSuggestions(inputBufferRef.current.length > 0);
|
||||
updateCursorPosition();
|
||||
} else if (data === '\x03') {
|
||||
// Ctrl+C:清空
|
||||
inputBufferRef.current = '';
|
||||
setCurrentInput('');
|
||||
setShowSuggestions(false);
|
||||
}
|
||||
});
|
||||
|
||||
@ -575,14 +543,17 @@ function Terminal({ tabId, hostId, isActive, onConnectionChange, onShowCommandPa
|
||||
|
||||
{/* 右侧工具按钮 */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 命令提示 */}
|
||||
{/* 智能提示面板开关 */}
|
||||
<button
|
||||
onClick={onShowCommandPalette}
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded-lg btn-cyber text-sm text-shell-accent"
|
||||
title="命令面板 (Ctrl+K)"
|
||||
onClick={toggleSuggestPanel}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm transition-all
|
||||
${showSuggestPanel
|
||||
? 'bg-shell-accent/20 text-shell-accent border border-shell-accent/50'
|
||||
: 'btn-cyber text-shell-accent'}`}
|
||||
title="智能命令提示"
|
||||
>
|
||||
<FiCommand size={14} />
|
||||
<span className="hidden sm:inline font-display tracking-wide">COMMANDS</span>
|
||||
<FiTerminal size={14} />
|
||||
<span className="hidden sm:inline font-display tracking-wide">SUGGEST</span>
|
||||
</button>
|
||||
|
||||
<div className="divider-vertical h-6 mx-1" />
|
||||
@ -625,13 +596,14 @@ function Terminal({ tabId, hostId, isActive, onConnectionChange, onShowCommandPa
|
||||
style={{ minHeight: '300px', minWidth: '400px' }}
|
||||
/>
|
||||
|
||||
{/* 命令提示 */}
|
||||
<CommandSuggestions
|
||||
{/* 智能命令提示面板(右侧固定) */}
|
||||
<CommandSuggestPanel
|
||||
visible={showSuggestPanel}
|
||||
input={currentInput}
|
||||
visible={showSuggestions && isActive && connectionId}
|
||||
cursorPosition={cursorPosition}
|
||||
onSelect={handleSuggestionSelect}
|
||||
onClose={handleCloseSuggestions}
|
||||
onClose={() => setShowSuggestPanel(false)}
|
||||
onOpenCommandPalette={onShowCommandPalette}
|
||||
disabled={!connectionId}
|
||||
/>
|
||||
|
||||
{/* 底部装饰线 */}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user