Compare commits
2 Commits
586fe53f5f
...
468e05f7cc
| Author | SHA1 | Date | |
|---|---|---|---|
| 468e05f7cc | |||
| 13f9e5b71c |
96
.github/workflows/release.yml
vendored
Normal file
96
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
name: Build and Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*' # 当推送 v 开头的标签时触发,如 v1.0.0
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build on ${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [windows-latest, macos-latest, ubuntu-latest]
|
||||||
|
include:
|
||||||
|
- os: windows-latest
|
||||||
|
artifact_name: EasyShell-Windows
|
||||||
|
build_cmd: npm run build && npx electron-builder --win
|
||||||
|
- os: macos-latest
|
||||||
|
artifact_name: EasyShell-Mac
|
||||||
|
build_cmd: npm run build && npx electron-builder --mac
|
||||||
|
- os: ubuntu-latest
|
||||||
|
artifact_name: EasyShell-Linux
|
||||||
|
build_cmd: npm run build && npx electron-builder --linux
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Generate icons
|
||||||
|
run: npm run icons
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Build application
|
||||||
|
run: ${{ matrix.build_cmd }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact_name }}
|
||||||
|
path: |
|
||||||
|
dist/*.exe
|
||||||
|
dist/*.dmg
|
||||||
|
dist/*.AppImage
|
||||||
|
dist/*.deb
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Create Release
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: artifacts
|
||||||
|
|
||||||
|
- name: Display structure of downloaded files
|
||||||
|
run: ls -R artifacts
|
||||||
|
|
||||||
|
- name: Get version from tag
|
||||||
|
id: get_version
|
||||||
|
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
name: EasyShell ${{ steps.get_version.outputs.VERSION }}
|
||||||
|
tag_name: ${{ steps.get_version.outputs.VERSION }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
generate_release_notes: true
|
||||||
|
files: |
|
||||||
|
artifacts/**/*.exe
|
||||||
|
artifacts/**/*.dmg
|
||||||
|
artifacts/**/*.AppImage
|
||||||
|
artifacts/**/*.deb
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
43
README.md
43
README.md
@ -171,6 +171,49 @@ npm run dist
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🏷️ 版本发布
|
||||||
|
|
||||||
|
项目已配置 GitHub Actions 自动化发布流程。
|
||||||
|
|
||||||
|
### 一键发布
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 发布补丁版本 (1.0.0 -> 1.0.1)
|
||||||
|
npm run release
|
||||||
|
|
||||||
|
# 发布次要版本 (1.0.0 -> 1.1.0)
|
||||||
|
npm run release:minor
|
||||||
|
|
||||||
|
# 发布主要版本 (1.0.0 -> 2.0.0)
|
||||||
|
npm run release:major
|
||||||
|
```
|
||||||
|
|
||||||
|
发布脚本会自动:
|
||||||
|
1. ✅ 更新 `package.json` 版本号
|
||||||
|
2. ✅ 更新界面显示的版本号
|
||||||
|
3. ✅ 提交更改
|
||||||
|
4. ✅ 创建 Git 标签
|
||||||
|
5. ✅ 推送到 GitHub
|
||||||
|
6. ✅ 触发 GitHub Actions 自动构建
|
||||||
|
|
||||||
|
### 自动构建
|
||||||
|
|
||||||
|
当推送 `v*` 格式的标签时(如 `v1.0.0`),GitHub Actions 将自动:
|
||||||
|
|
||||||
|
| 平台 | 产物 |
|
||||||
|
|------|------|
|
||||||
|
| Windows | `EasyShell Setup x.x.x.exe` |
|
||||||
|
| macOS | `EasyShell-x.x.x.dmg` |
|
||||||
|
| Linux | `EasyShell-x.x.x.AppImage` |
|
||||||
|
|
||||||
|
构建完成后会自动创建 GitHub Release 并上传安装包。
|
||||||
|
|
||||||
|
### 查看构建状态
|
||||||
|
|
||||||
|
[](https://github.com/ethanfly/easyshell/actions)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📱 移动端部署
|
## 📱 移动端部署
|
||||||
|
|
||||||
### 1. 启动后端服务器
|
### 1. 启动后端服务器
|
||||||
|
|||||||
@ -17,6 +17,9 @@
|
|||||||
"version:patch": "node scripts/bump-version.js patch",
|
"version:patch": "node scripts/bump-version.js patch",
|
||||||
"version:minor": "node scripts/bump-version.js minor",
|
"version:minor": "node scripts/bump-version.js minor",
|
||||||
"version:major": "node scripts/bump-version.js major",
|
"version:major": "node scripts/bump-version.js major",
|
||||||
|
"release": "node scripts/release.js patch",
|
||||||
|
"release:minor": "node scripts/release.js minor",
|
||||||
|
"release:major": "node scripts/release.js major",
|
||||||
"icons": "node scripts/generate-icons.js",
|
"icons": "node scripts/generate-icons.js",
|
||||||
"server": "cd server && npm start",
|
"server": "cd server && npm start",
|
||||||
"server:dev": "cd server && npm run dev",
|
"server:dev": "cd server && npm run dev",
|
||||||
|
|||||||
108
scripts/release.js
Normal file
108
scripts/release.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* 发布脚本
|
||||||
|
* 自动更新版本号、提交、打标签并推送到 GitHub
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* npm run release # patch 版本 (1.0.0 -> 1.0.1)
|
||||||
|
* npm run release:minor # minor 版本 (1.0.0 -> 1.1.0)
|
||||||
|
* npm run release:major # major 版本 (1.0.0 -> 2.0.0)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const packageJsonPath = path.resolve(__dirname, '../package.json');
|
||||||
|
const titleBarPath = path.resolve(__dirname, '../src/components/TitleBar.js');
|
||||||
|
|
||||||
|
function exec(cmd, options = {}) {
|
||||||
|
console.log(`\n🔧 执行: ${cmd}`);
|
||||||
|
try {
|
||||||
|
execSync(cmd, { stdio: 'inherit', ...options });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 命令执行失败: ${cmd}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bumpVersion(type = 'patch') {
|
||||||
|
// 读取 package.json
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||||
|
let [major, minor, patch] = packageJson.version.split('.').map(Number);
|
||||||
|
|
||||||
|
// 递增版本号
|
||||||
|
if (type === 'major') {
|
||||||
|
major++;
|
||||||
|
minor = 0;
|
||||||
|
patch = 0;
|
||||||
|
} else if (type === 'minor') {
|
||||||
|
minor++;
|
||||||
|
patch = 0;
|
||||||
|
} else {
|
||||||
|
patch++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newVersion = `${major}.${minor}.${patch}`;
|
||||||
|
packageJson.version = newVersion;
|
||||||
|
|
||||||
|
// 更新 package.json
|
||||||
|
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 4) + '\n');
|
||||||
|
console.log(`📦 版本号已更新: ${newVersion}`);
|
||||||
|
|
||||||
|
// 更新 TitleBar.js
|
||||||
|
if (fs.existsSync(titleBarPath)) {
|
||||||
|
let titleBarContent = fs.readFileSync(titleBarPath, 'utf8');
|
||||||
|
const newTitleBarContent = titleBarContent.replace(
|
||||||
|
/v\d+\.\d+\.\d+/g,
|
||||||
|
`v${newVersion}`
|
||||||
|
);
|
||||||
|
fs.writeFileSync(titleBarPath, newTitleBarContent, 'utf8');
|
||||||
|
console.log('📝 TitleBar.js 版本号已更新');
|
||||||
|
}
|
||||||
|
|
||||||
|
return newVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
function release() {
|
||||||
|
const versionType = process.argv[2] || 'patch';
|
||||||
|
|
||||||
|
console.log('🚀 开始发布流程...\n');
|
||||||
|
console.log(`📋 版本类型: ${versionType}`);
|
||||||
|
|
||||||
|
// 1. 检查是否有未提交的更改
|
||||||
|
try {
|
||||||
|
const status = execSync('git status --porcelain', { encoding: 'utf8' });
|
||||||
|
if (status.trim()) {
|
||||||
|
console.log('\n⚠️ 检测到未提交的更改,将一起提交...');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('❌ Git 状态检查失败');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 更新版本号
|
||||||
|
const newVersion = bumpVersion(versionType);
|
||||||
|
const tagName = `v${newVersion}`;
|
||||||
|
|
||||||
|
// 3. 添加所有更改
|
||||||
|
exec('git add .');
|
||||||
|
|
||||||
|
// 4. 提交更改
|
||||||
|
exec(`git commit -m "chore: release ${tagName}"`);
|
||||||
|
|
||||||
|
// 5. 创建标签
|
||||||
|
exec(`git tag -a ${tagName} -m "Release ${tagName}"`);
|
||||||
|
console.log(`\n🏷️ 标签已创建: ${tagName}`);
|
||||||
|
|
||||||
|
// 6. 推送到远程
|
||||||
|
console.log('\n📤 推送到远程仓库...');
|
||||||
|
exec('git push origin main');
|
||||||
|
exec(`git push origin ${tagName}`);
|
||||||
|
|
||||||
|
console.log(`\n✅ 发布完成!`);
|
||||||
|
console.log(` 版本: ${tagName}`);
|
||||||
|
console.log(` GitHub Actions 将自动开始构建...`);
|
||||||
|
console.log(`\n🔗 查看构建进度: https://github.com/ethanfly/easyshell/actions`);
|
||||||
|
}
|
||||||
|
|
||||||
|
release();
|
||||||
@ -2,128 +2,192 @@ import React, { useEffect, useRef, useState } from 'react';
|
|||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { FiTerminal, FiFolder, FiFile, FiSettings, FiSearch, FiPackage, FiServer, FiDatabase } from 'react-icons/fi';
|
import { FiTerminal, FiFolder, FiFile, FiSettings, FiSearch, FiPackage, FiServer, FiDatabase } from 'react-icons/fi';
|
||||||
|
|
||||||
// 常用 Linux/Unix 命令库
|
// 常用 Linux/Unix 命令库 (不显示参数占位符)
|
||||||
const COMMAND_DATABASE = [
|
const COMMAND_DATABASE = [
|
||||||
// 文件操作
|
// 文件操作
|
||||||
{ cmd: 'ls', desc: '列出目录内容', category: 'file', args: '-la' },
|
{ cmd: 'ls', desc: '列出目录内容', category: 'file' },
|
||||||
{ cmd: 'cd', desc: '切换目录', category: 'file', args: '/path' },
|
{ 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: 'pwd', desc: '显示当前目录', category: 'file' },
|
||||||
{ cmd: 'mkdir', desc: '创建目录', category: 'file', args: '-p dirname' },
|
{ cmd: 'mkdir', desc: '创建目录', category: 'file' },
|
||||||
{ cmd: 'rmdir', desc: '删除空目录', category: 'file', args: 'dirname' },
|
{ cmd: 'mkdir -p', desc: '递归创建目录', category: 'file' },
|
||||||
{ cmd: 'rm', desc: '删除文件/目录', category: 'file', args: '-rf path' },
|
{ cmd: 'rmdir', desc: '删除空目录', category: 'file' },
|
||||||
{ cmd: 'cp', desc: '复制文件/目录', category: 'file', args: '-r src dst' },
|
{ cmd: 'rm', desc: '删除文件', category: 'file' },
|
||||||
{ cmd: 'mv', desc: '移动/重命名', category: 'file', args: 'src dst' },
|
{ cmd: 'rm -rf', desc: '强制递归删除', category: 'file' },
|
||||||
{ cmd: 'touch', desc: '创建空文件', category: 'file', args: 'filename' },
|
{ cmd: 'cp', desc: '复制文件', category: 'file' },
|
||||||
{ cmd: 'cat', desc: '查看文件内容', category: 'file', args: 'filename' },
|
{ cmd: 'cp -r', desc: '递归复制目录', category: 'file' },
|
||||||
{ cmd: 'head', desc: '查看文件头部', category: 'file', args: '-n 20 file' },
|
{ cmd: 'mv', desc: '移动/重命名', category: 'file' },
|
||||||
{ cmd: 'tail', desc: '查看文件尾部', category: 'file', args: '-f logfile' },
|
{ cmd: 'touch', desc: '创建空文件', category: 'file' },
|
||||||
{ cmd: 'less', desc: '分页查看文件', category: 'file', args: 'filename' },
|
{ cmd: 'cat', desc: '查看文件内容', category: 'file' },
|
||||||
{ cmd: 'more', desc: '分页显示文件', category: 'file', args: 'filename' },
|
{ cmd: 'head', desc: '查看文件头部', category: 'file' },
|
||||||
{ cmd: 'find', desc: '查找文件', category: 'file', args: '. -name "*.log"' },
|
{ cmd: 'head -n 20', desc: '查看前20行', category: 'file' },
|
||||||
{ cmd: 'locate', desc: '快速定位文件', category: 'file', args: 'filename' },
|
{ cmd: 'tail', desc: '查看文件尾部', category: 'file' },
|
||||||
{ cmd: 'chmod', desc: '修改权限', category: 'file', args: '755 file' },
|
{ cmd: 'tail -f', desc: '实时追踪日志', category: 'file' },
|
||||||
{ cmd: 'chown', desc: '修改所有者', category: 'file', args: 'user:group file' },
|
{ cmd: 'less', desc: '分页查看文件', category: 'file' },
|
||||||
{ cmd: 'ln', desc: '创建链接', category: 'file', args: '-s target link' },
|
{ cmd: 'more', desc: '分页显示文件', category: 'file' },
|
||||||
{ cmd: 'tar', desc: '打包/解包', category: 'file', args: '-zxvf file.tar.gz' },
|
{ cmd: 'find', desc: '查找文件', category: 'file' },
|
||||||
{ cmd: 'zip', desc: '压缩文件', category: 'file', args: '-r archive.zip dir' },
|
{ cmd: 'find . -name', desc: '按名称查找', category: 'file' },
|
||||||
{ cmd: 'unzip', desc: '解压 zip', category: 'file', args: 'archive.zip' },
|
{ cmd: 'locate', desc: '快速定位文件', category: 'file' },
|
||||||
{ cmd: 'gzip', desc: '压缩文件', category: 'file', args: 'filename' },
|
{ cmd: 'chmod', desc: '修改权限', category: 'file' },
|
||||||
{ cmd: 'gunzip', desc: '解压 gz', category: 'file', args: 'file.gz' },
|
{ 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', args: '-rn "pattern" .' },
|
{ cmd: 'grep', desc: '文本搜索', category: 'text' },
|
||||||
{ cmd: 'sed', desc: '流编辑器', category: 'text', args: "'s/old/new/g' file" },
|
{ cmd: 'grep -rn', desc: '递归搜索带行号', category: 'text' },
|
||||||
{ cmd: 'awk', desc: '文本处理', category: 'text', args: "'{print $1}' file" },
|
{ cmd: 'grep -i', desc: '忽略大小写搜索', category: 'text' },
|
||||||
{ cmd: 'sort', desc: '排序', category: 'text', args: '-n file' },
|
{ cmd: 'sed', desc: '流编辑器', category: 'text' },
|
||||||
{ cmd: 'uniq', desc: '去重', category: 'text', args: '-c file' },
|
{ cmd: 'awk', desc: '文本处理', category: 'text' },
|
||||||
{ cmd: 'wc', desc: '统计行/字/字符', category: 'text', args: '-l file' },
|
{ cmd: 'sort', desc: '排序', category: 'text' },
|
||||||
{ cmd: 'cut', desc: '切割文本', category: 'text', args: "-d':' -f1 file" },
|
{ cmd: 'sort -n', desc: '数字排序', category: 'text' },
|
||||||
{ cmd: 'diff', desc: '比较文件', category: 'text', args: 'file1 file2' },
|
{ cmd: 'uniq', desc: '去重', category: 'text' },
|
||||||
{ cmd: 'echo', desc: '输出文本', category: 'text', args: '"Hello World"' },
|
{ cmd: 'uniq -c', desc: '去重并计数', category: 'text' },
|
||||||
{ cmd: 'printf', desc: '格式化输出', category: 'text', args: '"%s\\n" 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: 'top', desc: '系统监控', category: 'system' },
|
||||||
{ cmd: 'htop', desc: '增强版 top', category: 'system' },
|
{ cmd: 'htop', desc: '增强版 top', category: 'system' },
|
||||||
{ cmd: 'ps', desc: '查看进程', category: 'system', args: 'aux' },
|
{ cmd: 'ps aux', desc: '查看所有进程', category: 'system' },
|
||||||
{ cmd: 'kill', desc: '终止进程', category: 'system', args: '-9 PID' },
|
{ cmd: 'ps -ef', desc: '进程树', category: 'system' },
|
||||||
{ cmd: 'killall', desc: '按名称终止', category: 'system', args: 'process_name' },
|
{ cmd: 'kill', desc: '终止进程', category: 'system' },
|
||||||
{ cmd: 'df', desc: '磁盘使用', category: 'system', args: '-h' },
|
{ cmd: 'kill -9', desc: '强制终止进程', category: 'system' },
|
||||||
{ cmd: 'du', desc: '目录大小', category: 'system', args: '-sh *' },
|
{ cmd: 'killall', desc: '按名称终止', category: 'system' },
|
||||||
{ cmd: 'free', desc: '内存使用', category: 'system', args: '-h' },
|
{ 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: 'uptime', desc: '运行时间', category: 'system' },
|
||||||
{ cmd: 'uname', desc: '系统信息', category: 'system', args: '-a' },
|
{ cmd: 'uname -a', desc: '系统信息', category: 'system' },
|
||||||
{ cmd: 'hostname', desc: '主机名', category: 'system' },
|
{ cmd: 'hostname', desc: '主机名', category: 'system' },
|
||||||
{ cmd: 'whoami', desc: '当前用户', category: 'system' },
|
{ cmd: 'whoami', desc: '当前用户', category: 'system' },
|
||||||
{ cmd: 'id', desc: '用户信息', category: 'system' },
|
{ cmd: 'id', desc: '用户信息', category: 'system' },
|
||||||
{ cmd: 'w', desc: '登录用户', category: 'system' },
|
{ cmd: 'w', desc: '登录用户', category: 'system' },
|
||||||
{ cmd: 'last', desc: '登录历史', category: 'system' },
|
{ cmd: 'last', desc: '登录历史', category: 'system' },
|
||||||
{ cmd: 'dmesg', desc: '内核消息', category: 'system', args: '| tail' },
|
{ cmd: 'dmesg | tail', desc: '内核消息', category: 'system' },
|
||||||
{ cmd: 'lsof', desc: '打开的文件', category: 'system', args: '-i :80' },
|
{ cmd: 'lsof', desc: '打开的文件', category: 'system' },
|
||||||
|
{ cmd: 'lsof -i', desc: '网络连接', category: 'system' },
|
||||||
{ cmd: 'lscpu', desc: 'CPU 信息', category: 'system' },
|
{ cmd: 'lscpu', desc: 'CPU 信息', category: 'system' },
|
||||||
{ cmd: 'lsmem', desc: '内存信息', category: 'system' },
|
{ cmd: 'lsmem', desc: '内存信息', category: 'system' },
|
||||||
{ cmd: 'lsblk', desc: '块设备', category: 'system' },
|
{ cmd: 'lsblk', desc: '块设备', category: 'system' },
|
||||||
{ cmd: 'fdisk', desc: '磁盘分区', category: 'system', args: '-l' },
|
{ cmd: 'fdisk -l', desc: '磁盘分区', category: 'system' },
|
||||||
|
|
||||||
// 网络
|
// 网络
|
||||||
{ cmd: 'ping', desc: '测试连通性', category: 'network', args: '-c 4 host' },
|
{ cmd: 'ping', desc: '测试连通性', category: 'network' },
|
||||||
{ cmd: 'curl', desc: 'HTTP 请求', category: 'network', args: '-I url' },
|
{ cmd: 'ping -c 4', desc: '发送4次ping', category: 'network' },
|
||||||
{ cmd: 'wget', desc: '下载文件', category: 'network', args: 'url' },
|
{ cmd: 'curl', desc: 'HTTP 请求', category: 'network' },
|
||||||
{ cmd: 'ssh', desc: 'SSH 连接', category: 'network', args: 'user@host' },
|
{ cmd: 'curl -I', desc: '获取响应头', category: 'network' },
|
||||||
{ cmd: 'scp', desc: '安全复制', category: 'network', args: 'file user@host:path' },
|
{ cmd: 'curl -O', desc: '下载文件', category: 'network' },
|
||||||
{ cmd: 'rsync', desc: '同步文件', category: 'network', args: '-avz src dst' },
|
{ cmd: 'wget', desc: '下载文件', category: 'network' },
|
||||||
{ cmd: 'netstat', desc: '网络统计', category: 'network', args: '-tunlp' },
|
{ cmd: 'ssh', desc: 'SSH 连接', category: 'network' },
|
||||||
{ cmd: 'ss', desc: '套接字统计', category: 'network', args: '-tunlp' },
|
{ cmd: 'scp', desc: '安全复制', category: 'network' },
|
||||||
{ cmd: 'ip', desc: 'IP 配置', category: 'network', args: 'addr show' },
|
{ 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: 'ifconfig', desc: '网络接口', category: 'network' },
|
||||||
{ cmd: 'route', desc: '路由表', category: 'network', args: '-n' },
|
{ cmd: 'route -n', desc: '路由表', category: 'network' },
|
||||||
{ cmd: 'iptables', desc: '防火墙', category: 'network', args: '-L -n' },
|
{ cmd: 'iptables -L -n', desc: '防火墙规则', category: 'network' },
|
||||||
{ cmd: 'nslookup', desc: 'DNS 查询', category: 'network', args: 'domain' },
|
{ cmd: 'nslookup', desc: 'DNS 查询', category: 'network' },
|
||||||
{ cmd: 'dig', desc: 'DNS 详细查询', category: 'network', args: 'domain' },
|
{ cmd: 'dig', desc: 'DNS 详细查询', category: 'network' },
|
||||||
{ cmd: 'traceroute', desc: '路由追踪', category: 'network', args: 'host' },
|
{ cmd: 'traceroute', desc: '路由追踪', category: 'network' },
|
||||||
{ cmd: 'tcpdump', desc: '抓包', category: 'network', args: '-i eth0' },
|
{ cmd: 'tcpdump -i', desc: '抓包', category: 'network' },
|
||||||
|
|
||||||
// 包管理
|
// 包管理
|
||||||
{ cmd: 'apt', desc: 'Debian 包管理', category: 'package', args: 'update && apt upgrade' },
|
{ cmd: 'apt update', desc: '更新软件源', category: 'package' },
|
||||||
{ cmd: 'apt-get', desc: 'APT 包管理', category: 'package', args: 'install package' },
|
{ cmd: 'apt upgrade', desc: '升级软件包', category: 'package' },
|
||||||
{ cmd: 'yum', desc: 'RHEL 包管理', category: 'package', args: 'install package' },
|
{ cmd: 'apt install', desc: '安装软件', category: 'package' },
|
||||||
{ cmd: 'dnf', desc: 'Fedora 包管理', category: 'package', args: 'install package' },
|
{ cmd: 'apt remove', desc: '卸载软件', category: 'package' },
|
||||||
{ cmd: 'pacman', desc: 'Arch 包管理', category: 'package', args: '-S package' },
|
{ cmd: 'apt search', desc: '搜索软件', category: 'package' },
|
||||||
{ cmd: 'npm', desc: 'Node 包管理', category: 'package', args: 'install package' },
|
{ cmd: 'yum install', desc: 'RHEL 安装', category: 'package' },
|
||||||
{ cmd: 'pip', desc: 'Python 包管理', category: 'package', args: 'install package' },
|
{ cmd: 'yum update', desc: 'RHEL 更新', category: 'package' },
|
||||||
{ cmd: 'pip3', desc: 'Python3 包管理', category: 'package', args: 'install 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', desc: '服务管理', category: 'service', args: 'status service' },
|
{ cmd: 'systemctl status', desc: '服务状态', category: 'service' },
|
||||||
{ cmd: 'service', desc: '服务控制', category: 'service', args: 'nginx restart' },
|
{ cmd: 'systemctl start', desc: '启动服务', category: 'service' },
|
||||||
{ cmd: 'journalctl', desc: '查看日志', category: 'service', args: '-u service -f' },
|
{ 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
|
// Docker
|
||||||
{ cmd: 'docker', desc: 'Docker 命令', category: 'docker', args: 'ps -a' },
|
{ cmd: 'docker ps', desc: '运行中容器', category: 'docker' },
|
||||||
{ cmd: 'docker-compose', desc: 'Docker Compose', category: 'docker', args: 'up -d' },
|
{ 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
|
// Git
|
||||||
{ cmd: 'git', desc: 'Git 版本控制', category: 'git', args: 'status' },
|
{ 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: 'clear', desc: '清屏', category: 'other' },
|
||||||
{ cmd: 'history', desc: '命令历史', category: 'other' },
|
{ cmd: 'history', desc: '命令历史', category: 'other' },
|
||||||
{ cmd: 'alias', desc: '别名列表', category: 'other' },
|
{ cmd: 'alias', desc: '别名列表', category: 'other' },
|
||||||
{ cmd: 'which', desc: '命令路径', category: 'other', args: 'command' },
|
{ cmd: 'which', desc: '命令路径', category: 'other' },
|
||||||
{ cmd: 'whereis', desc: '二进制位置', category: 'other', args: 'command' },
|
{ cmd: 'whereis', desc: '二进制位置', category: 'other' },
|
||||||
{ cmd: 'man', desc: '查看手册', category: 'other', args: 'command' },
|
{ cmd: 'man', desc: '查看手册', category: 'other' },
|
||||||
{ cmd: 'date', desc: '日期时间', category: 'other' },
|
{ cmd: 'date', desc: '日期时间', category: 'other' },
|
||||||
{ cmd: 'cal', desc: '日历', category: 'other' },
|
{ cmd: 'cal', desc: '日历', category: 'other' },
|
||||||
{ cmd: 'env', desc: '环境变量', category: 'other' },
|
{ cmd: 'env', desc: '环境变量', category: 'other' },
|
||||||
{ cmd: 'export', desc: '设置变量', category: 'other', args: 'VAR=value' },
|
{ cmd: 'export', desc: '设置变量', category: 'other' },
|
||||||
{ cmd: 'source', desc: '执行脚本', category: 'other', args: 'script.sh' },
|
{ cmd: 'source', desc: '执行脚本', category: 'other' },
|
||||||
{ cmd: 'crontab', desc: '定时任务', category: 'other', args: '-l' },
|
{ cmd: 'crontab -l', desc: '查看定时任务', category: 'other' },
|
||||||
{ cmd: 'nohup', desc: '后台运行', category: 'other', args: 'command &' },
|
{ cmd: 'crontab -e', desc: '编辑定时任务', category: 'other' },
|
||||||
{ cmd: 'screen', desc: '终端复用', category: 'other', args: '-S session' },
|
{ cmd: 'nohup', desc: '后台运行', category: 'other' },
|
||||||
{ cmd: 'tmux', desc: '终端复用', category: 'other', args: 'new -s session' },
|
{ cmd: 'screen -S', desc: '创建会话', category: 'other' },
|
||||||
{ cmd: 'vim', desc: '文本编辑器', category: 'other', args: 'filename' },
|
{ cmd: 'screen -r', desc: '恢复会话', category: 'other' },
|
||||||
{ cmd: 'nano', desc: '简易编辑器', category: 'other', args: 'filename' },
|
{ cmd: 'tmux new -s', desc: '创建会话', category: 'other' },
|
||||||
{ cmd: 'vi', desc: 'Vi 编辑器', category: 'other', args: 'filename' },
|
{ 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' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 获取分类图标
|
// 获取分类图标
|
||||||
@ -141,7 +205,7 @@ const getCategoryIcon = (category) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function CommandSuggestions({ input, position, onSelect, onClose, visible }) {
|
function CommandSuggestions({ input, position, onSelect, onClose, visible, cursorPosition }) {
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
const listRef = useRef(null);
|
const listRef = useRef(null);
|
||||||
const selectedRef = useRef(null);
|
const selectedRef = useRef(null);
|
||||||
@ -152,43 +216,11 @@ function CommandSuggestions({ input, position, onSelect, onClose, visible }) {
|
|||||||
|
|
||||||
const query = input.toLowerCase().trim();
|
const query = input.toLowerCase().trim();
|
||||||
|
|
||||||
// 分词,获取最后一个输入的词
|
// 匹配以输入开头的命令
|
||||||
const parts = query.split(/\s+/);
|
const results = COMMAND_DATABASE.filter(item =>
|
||||||
const lastWord = parts[parts.length - 1];
|
item.cmd.toLowerCase().startsWith(query) ||
|
||||||
const isFirstWord = parts.length === 1;
|
item.desc.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
if (!lastWord) return [];
|
|
||||||
|
|
||||||
let results = [];
|
|
||||||
|
|
||||||
if (isFirstWord) {
|
|
||||||
// 第一个词:搜索命令
|
|
||||||
results = COMMAND_DATABASE.filter(item =>
|
|
||||||
item.cmd.toLowerCase().startsWith(lastWord) ||
|
|
||||||
item.desc.toLowerCase().includes(lastWord)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// 后续词:搜索该命令的常用参数或相关命令
|
|
||||||
const baseCmd = parts[0];
|
|
||||||
const cmdInfo = COMMAND_DATABASE.find(c => c.cmd === baseCmd);
|
|
||||||
|
|
||||||
if (cmdInfo && cmdInfo.args) {
|
|
||||||
// 如果有预设参数,显示参数提示
|
|
||||||
results = [{
|
|
||||||
cmd: `${baseCmd} ${cmdInfo.args}`,
|
|
||||||
desc: `${cmdInfo.desc} (常用参数)`,
|
|
||||||
category: cmdInfo.category,
|
|
||||||
isFullCmd: true
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 也搜索其他可能的命令组合
|
|
||||||
const otherResults = COMMAND_DATABASE.filter(item =>
|
|
||||||
item.cmd.toLowerCase().startsWith(lastWord)
|
|
||||||
).slice(0, 3);
|
|
||||||
|
|
||||||
results = [...results, ...otherResults];
|
|
||||||
}
|
|
||||||
|
|
||||||
return results.slice(0, 8);
|
return results.slice(0, 8);
|
||||||
}, [input]);
|
}, [input]);
|
||||||
@ -222,12 +254,11 @@ function CommandSuggestions({ input, position, onSelect, onClose, visible }) {
|
|||||||
setSelectedIndex(prev => (prev - 1 + suggestions.length) % suggestions.length);
|
setSelectedIndex(prev => (prev - 1 + suggestions.length) % suggestions.length);
|
||||||
break;
|
break;
|
||||||
case 'Tab':
|
case 'Tab':
|
||||||
case 'Enter':
|
// 只用 Tab 选择
|
||||||
if (suggestions[selectedIndex]) {
|
if (suggestions[selectedIndex]) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const item = suggestions[selectedIndex];
|
onSelect(suggestions[selectedIndex].cmd);
|
||||||
onSelect(item.isFullCmd ? item.cmd : item.cmd);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
@ -245,32 +276,38 @@ function CommandSuggestions({ input, position, onSelect, onClose, visible }) {
|
|||||||
|
|
||||||
if (!visible || suggestions.length === 0) return null;
|
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 (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
initial={{ opacity: 0, y: -5, scale: 0.98 }}
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
exit={{ opacity: 0, y: -5, scale: 0.98 }}
|
||||||
transition={{ duration: 0.15 }}
|
transition={{ duration: 0.1 }}
|
||||||
className="absolute z-50 bg-shell-surface/95 backdrop-blur-xl border border-shell-accent/30
|
className="absolute z-50 bg-shell-surface/95 backdrop-blur-xl border border-shell-accent/30
|
||||||
rounded-lg shadow-2xl overflow-hidden min-w-[280px] max-w-[400px]"
|
rounded-lg shadow-2xl overflow-hidden min-w-[240px] max-w-[360px]"
|
||||||
style={{
|
style={positionStyle}
|
||||||
bottom: position?.bottom || 60,
|
|
||||||
left: position?.left || 16,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{/* 标题栏 */}
|
{/* 简化的标题栏 */}
|
||||||
<div className="px-3 py-1.5 border-b border-shell-border/50 flex items-center justify-between">
|
<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 uppercase tracking-wider">
|
<span className="text-[10px] text-shell-text-dim font-mono">
|
||||||
命令提示
|
↑↓ 导航
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-shell-text-dim">
|
<span className="text-[10px] text-shell-accent font-mono">
|
||||||
Tab 选择 · Esc 关闭
|
Tab 补全
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 建议列表 */}
|
{/* 建议列表 */}
|
||||||
<div ref={listRef} className="max-h-[240px] overflow-y-auto custom-scrollbar">
|
<div ref={listRef} className="max-h-[200px] overflow-y-auto custom-scrollbar">
|
||||||
{suggestions.map((item, index) => {
|
{suggestions.map((item, index) => {
|
||||||
const Icon = getCategoryIcon(item.category);
|
const Icon = getCategoryIcon(item.category);
|
||||||
const isSelected = index === selectedIndex;
|
const isSelected = index === selectedIndex;
|
||||||
@ -279,9 +316,9 @@ function CommandSuggestions({ input, position, onSelect, onClose, visible }) {
|
|||||||
<div
|
<div
|
||||||
key={`${item.cmd}-${index}`}
|
key={`${item.cmd}-${index}`}
|
||||||
ref={isSelected ? selectedRef : null}
|
ref={isSelected ? selectedRef : null}
|
||||||
onClick={() => onSelect(item.isFullCmd ? item.cmd : item.cmd)}
|
onClick={() => onSelect(item.cmd)}
|
||||||
className={`
|
className={`
|
||||||
px-3 py-2 cursor-pointer flex items-center gap-3 transition-all
|
px-2 py-1.5 cursor-pointer flex items-center gap-2 transition-all
|
||||||
${isSelected
|
${isSelected
|
||||||
? 'bg-shell-accent/20 border-l-2 border-shell-accent'
|
? 'bg-shell-accent/20 border-l-2 border-shell-accent'
|
||||||
: 'hover:bg-shell-card/50 border-l-2 border-transparent'
|
: 'hover:bg-shell-card/50 border-l-2 border-transparent'
|
||||||
@ -289,23 +326,16 @@ function CommandSuggestions({ input, position, onSelect, onClose, visible }) {
|
|||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
size={14}
|
size={12}
|
||||||
className={isSelected ? 'text-shell-accent' : 'text-shell-text-dim'}
|
className={isSelected ? 'text-shell-accent' : 'text-shell-text-dim'}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0 flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<span className={`font-mono text-xs ${isSelected ? 'text-shell-accent' : 'text-shell-text'}`}>
|
||||||
<span className={`font-mono text-sm ${isSelected ? 'text-shell-accent' : 'text-shell-text'}`}>
|
{item.cmd}
|
||||||
{item.cmd}
|
</span>
|
||||||
</span>
|
<span className="text-[10px] text-shell-text-dim truncate">
|
||||||
{item.args && !item.isFullCmd && (
|
|
||||||
<span className="text-xs text-shell-text-dim font-mono opacity-60">
|
|
||||||
{item.args}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-shell-text-dim truncate">
|
|
||||||
{item.desc}
|
{item.desc}
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -83,6 +83,7 @@ function Terminal({ tabId, hostId, isActive, onConnectionChange, onShowCommandPa
|
|||||||
const [isConnecting, setIsConnecting] = useState(false);
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
const [currentInput, setCurrentInput] = useState('');
|
const [currentInput, setCurrentInput] = useState('');
|
||||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||||
|
const [cursorPosition, setCursorPosition] = useState(null);
|
||||||
const inputBufferRef = useRef('');
|
const inputBufferRef = useRef('');
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [isReady, setIsReady] = useState(false);
|
const [isReady, setIsReady] = useState(false);
|
||||||
@ -280,6 +281,21 @@ function Terminal({ tabId, hostId, isActive, onConnectionChange, onShowCommandPa
|
|||||||
window.electronAPI.ssh.write(connectionIdRef.current, data);
|
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') {
|
if (data === '\r' || data === '\n') {
|
||||||
// 回车:清空输入缓冲区
|
// 回车:清空输入缓冲区
|
||||||
@ -291,6 +307,7 @@ function Terminal({ tabId, hostId, isActive, onConnectionChange, onShowCommandPa
|
|||||||
inputBufferRef.current = inputBufferRef.current.slice(0, -1);
|
inputBufferRef.current = inputBufferRef.current.slice(0, -1);
|
||||||
setCurrentInput(inputBufferRef.current);
|
setCurrentInput(inputBufferRef.current);
|
||||||
setShowSuggestions(inputBufferRef.current.length > 0);
|
setShowSuggestions(inputBufferRef.current.length > 0);
|
||||||
|
updateCursorPosition();
|
||||||
} else if (data === '\x1b' || data.startsWith('\x1b[')) {
|
} else if (data === '\x1b' || data.startsWith('\x1b[')) {
|
||||||
// ESC 或方向键:隐藏建议
|
// ESC 或方向键:隐藏建议
|
||||||
setShowSuggestions(false);
|
setShowSuggestions(false);
|
||||||
@ -302,6 +319,7 @@ function Terminal({ tabId, hostId, isActive, onConnectionChange, onShowCommandPa
|
|||||||
inputBufferRef.current += data;
|
inputBufferRef.current += data;
|
||||||
setCurrentInput(inputBufferRef.current);
|
setCurrentInput(inputBufferRef.current);
|
||||||
setShowSuggestions(inputBufferRef.current.length > 0);
|
setShowSuggestions(inputBufferRef.current.length > 0);
|
||||||
|
updateCursorPosition();
|
||||||
} else if (data === '\x03') {
|
} else if (data === '\x03') {
|
||||||
// Ctrl+C:清空
|
// Ctrl+C:清空
|
||||||
inputBufferRef.current = '';
|
inputBufferRef.current = '';
|
||||||
@ -611,7 +629,7 @@ function Terminal({ tabId, hostId, isActive, onConnectionChange, onShowCommandPa
|
|||||||
<CommandSuggestions
|
<CommandSuggestions
|
||||||
input={currentInput}
|
input={currentInput}
|
||||||
visible={showSuggestions && isActive && connectionId}
|
visible={showSuggestions && isActive && connectionId}
|
||||||
position={{ bottom: 60, left: 16 }}
|
cursorPosition={cursorPosition}
|
||||||
onSelect={handleSuggestionSelect}
|
onSelect={handleSuggestionSelect}
|
||||||
onClose={handleCloseSuggestions}
|
onClose={handleCloseSuggestions}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user