diff --git a/package-lock.json b/package-lock.json index 3784f62..cac0041 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "hatty", - "version": "1.0.1", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hatty", - "version": "1.0.1", + "version": "1.1.0", "license": "MIT", "dependencies": { "@xterm/addon-fit": "^0.11.0", diff --git a/package.json b/package.json index 398fc7c..4d53b10 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hatty", - "version": "1.0.1", + "version": "1.1.0", "description": "hatty - 一个超高颜值、极速响应的现代 Linux 终端", "main": "src/main/index.cjs", "scripts": { diff --git a/src/main/index.cjs b/src/main/index.cjs index 50ba72b..419bc07 100644 --- a/src/main/index.cjs +++ b/src/main/index.cjs @@ -1,4 +1,4 @@ -const { app, BrowserWindow, ipcMain } = require('electron'); +const { app, BrowserWindow, ipcMain, clipboard } = require('electron'); const path = require('path'); const os = require('os'); const { PtyManager } = require('./pty-manager.cjs'); @@ -101,6 +101,147 @@ ipcMain.on('pty:kill', (event, { id }) => { if (ptyManager) ptyManager.kill({ id }); }); +// 剪贴板 IPC +ipcMain.handle('clipboard:read', () => { + return clipboard.readText(); +}); + +ipcMain.on('clipboard:write', (event, text) => { + clipboard.writeText(text || ''); +}); + +// 获取 PTY 实时工作目录 +ipcMain.handle('pty:get-cwd', (event, { id }) => { + return ptyManager ? ptyManager.getCwd({ id }) : null; +}); + +// 检查可执行命令路径 +const fs = require('fs'); +function findExecutable(command) { + const isWindows = process.platform === 'win32'; + const exts = isWindows ? (process.env.PATHEXT || '.EXE;.CMD;.BAT').split(';') : ['']; + const dirs = (process.env.PATH || '').split(path.delimiter); + const extraDirs = [ + path.join(os.homedir(), '.local/bin'), + path.join(os.homedir(), '.npm-global/bin'), + path.join(os.homedir(), '.cargo/bin'), + '/usr/local/bin', + '/usr/bin', + '/bin', + ]; + const allDirs = [...new Set([...dirs, ...extraDirs])]; + for (const dir of allDirs) { + for (const ext of exts) { + const fullPath = path.join(dir, command + ext); + try { + if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) { + return fullPath; + } + } catch (_) {} + } + } + return null; +} + +// 检查已安装的 Agent (agy / codex / claude) +ipcMain.handle('app:get-installed-agents', () => { + const agentDefs = [ + { id: 'agy', name: 'agy', cmd: 'agy', desc: 'Antigravity CLI Agent' }, + { id: 'codex', name: 'codex', cmd: 'codex', desc: 'OpenAI CodeX CLI' }, + { id: 'claude', name: 'claude', cmd: 'claude', desc: 'Claude Code Agent' }, + ]; + return agentDefs.map((agent) => { + const execPath = findExecutable(agent.cmd); + return { + ...agent, + path: execPath, + installed: !!execPath, + }; + }); +}); + +// 文件浏览 IPC:读取目录中的文件和文件夹 +ipcMain.handle('fs:read-dir', async (event, { dirPath, showHidden = false } = {}) => { + try { + let targetPath = dirPath || '~'; + if (targetPath === '~' || targetPath.startsWith('~/')) { + targetPath = path.join(os.homedir(), targetPath.slice(targetPath === '~' ? 1 : 2)); + } + targetPath = path.resolve(targetPath); + + if (!fs.existsSync(targetPath)) { + return { success: false, error: '目录不存在: ' + targetPath, dirPath: targetPath }; + } + + const stat = await fs.promises.stat(targetPath); + if (!stat.isDirectory()) { + return { success: false, error: '路径不是有效目录: ' + targetPath, dirPath: targetPath }; + } + + const dirents = await fs.promises.readdir(targetPath, { withFileTypes: true }); + const entries = []; + + for (const d of dirents) { + const isHidden = d.name.startsWith('.'); + if (!showHidden && isHidden) continue; + + let isDirectory = d.isDirectory(); + let isFile = d.isFile(); + let isSymbolicLink = d.isSymbolicLink(); + let size = 0; + let mtime = 0; + + const fullItemPath = path.join(targetPath, d.name); + if (isSymbolicLink) { + try { + const realStat = await fs.promises.stat(fullItemPath); + isDirectory = realStat.isDirectory(); + isFile = realStat.isFile(); + size = realStat.size; + mtime = realStat.mtimeMs; + } catch (_) {} + } else { + try { + if (isFile) { + const fStat = await fs.promises.stat(fullItemPath); + size = fStat.size; + mtime = fStat.mtimeMs; + } + } catch (_) {} + } + + entries.push({ + name: d.name, + path: fullItemPath, + isDirectory, + isFile, + isSymbolicLink, + isHidden, + size, + mtime, + }); + } + + // 排序:文件夹优先,同类按文件名升序排列 + entries.sort((a, b) => { + if (a.isDirectory && !b.isDirectory) return -1; + if (!a.isDirectory && b.isDirectory) return 1; + return a.name.localeCompare(b.name, undefined, { sensitivity: 'base', numeric: true }); + }); + + const parentPath = path.dirname(targetPath); + + return { + success: true, + dirPath: targetPath, + parentPath: parentPath !== targetPath ? parentPath : null, + entries, + }; + } catch (err) { + return { success: false, error: err.message, dirPath }; + } +}); + ipcMain.handle('app:get-system-info', () => { return { platform: os.platform(), diff --git a/src/main/pty-manager.cjs b/src/main/pty-manager.cjs index 78ed165..7ff6ba3 100644 --- a/src/main/pty-manager.cjs +++ b/src/main/pty-manager.cjs @@ -99,6 +99,18 @@ class PtyManager { } } + getCwd({ id }) { + const ptyProcess = this.ptys.get(id); + if (!ptyProcess || !ptyProcess.pid) return null; + try { + if (process.platform === 'linux') { + const fs = require('fs'); + return fs.readlinkSync(`/proc/${ptyProcess.pid}/cwd`); + } + } catch (_) {} + return null; + } + killAll() { for (const [id, ptyProcess] of this.ptys.entries()) { try { diff --git a/src/preload/index.cjs b/src/preload/index.cjs index 3ba1615..36dfbd1 100644 --- a/src/preload/index.cjs +++ b/src/preload/index.cjs @@ -32,4 +32,15 @@ contextBridge.exposeInMainWorld('terminalAPI', { // 系统信息 getSystemInfo: () => ipcRenderer.invoke('app:get-system-info'), + + // 剪贴板管理 + readClipboard: () => ipcRenderer.invoke('clipboard:read'), + writeClipboard: (text) => ipcRenderer.send('clipboard:write', text), + + // PTY 与 Agent 查询 + getPtyCwd: (id) => ipcRenderer.invoke('pty:get-cwd', { id }), + getInstalledAgents: () => ipcRenderer.invoke('app:get-installed-agents'), + + // 文件浏览 + readDir: (options) => ipcRenderer.invoke('fs:read-dir', options), }); diff --git a/src/renderer/css/components.css b/src/renderer/css/components.css index 7aa254f..b13d590 100644 --- a/src/renderer/css/components.css +++ b/src/renderer/css/components.css @@ -261,3 +261,529 @@ from { transform: translateY(-10px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } + +/* 终端右键上下文菜单 */ +.terminal-context-menu { + position: fixed; + z-index: 1000; + background: var(--bg-titlebar); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-md, 6px); + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.5), 0 0 1px rgba(255, 255, 255, 0.1); + min-width: 200px; + padding: 4px; + display: none; + flex-direction: column; + gap: 2px; + user-select: none; + font-size: 12px; + color: var(--text-color); + animation: fadeIn 0.1s ease; + backdrop-filter: blur(12px); +} + +.terminal-context-menu.active { + display: flex; +} + +.ctx-menu-header { + padding: 6px 8px 3px 8px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-muted); +} + +.ctx-menu-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 10px; + border-radius: var(--border-radius-sm, 4px); + cursor: pointer; + transition: background 0.12s, color 0.12s; + color: var(--text-color); +} + +.ctx-menu-item:hover:not(.disabled) { + background: rgba(255, 255, 255, 0.08); + color: var(--text-bright); +} + +.ctx-menu-item.disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.ctx-menu-item-left { + display: flex; + align-items: center; + gap: 8px; +} + +.ctx-menu-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + color: var(--text-muted); + flex-shrink: 0; +} + +.ctx-menu-item:hover:not(.disabled) .ctx-menu-icon { + color: var(--border-active, #3b82f6); +} + +.ctx-menu-label { + font-size: 12px; +} + +.ctx-menu-shortcut { + font-size: 10px; + color: var(--text-muted); + margin-left: 12px; + opacity: 0.8; +} + +.ctx-menu-badge { + font-size: 9px; + padding: 1px 5px; + border-radius: 3px; + background: rgba(59, 130, 246, 0.15); + color: #60a5fa; + border: 1px solid rgba(96, 165, 250, 0.3); + margin-left: 12px; + letter-spacing: 0.2px; + white-space: nowrap; +} + +.ctx-menu-divider { + height: 1px; + background: var(--border-color); + margin: 4px 2px; + opacity: 0.6; +} + +#ctx-close-term:hover:not(.disabled) { + background: rgba(239, 68, 68, 0.12); + color: #f87171; +} + +#ctx-close-term:hover:not(.disabled) .ctx-menu-icon { + color: #ef4444; +} + +/* 顶部/全局图标按钮禁用状态:变灰且不可点击 */ +.btn-icon:disabled, +.btn-icon.disabled { + opacity: 0.28 !important; + cursor: not-allowed !important; + pointer-events: none !important; + filter: grayscale(1); +} + +/* 专属 Agent 面板 (AgentPanel) */ +.agent-panel { + display: flex; + flex-direction: column; + background: var(--bg-app); + border-left: 1px solid var(--border-color); + min-width: 240px; + position: relative; + overflow: hidden; +} + +.agent-panel-header { + height: 28px; + min-height: 28px; + background: var(--bg-titlebar); + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 6px 0 10px; + user-select: none; +} + +.agent-panel-header-left { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + flex: 1; +} + +.agent-panel-title { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.8px; + color: var(--accent-color, #7aa2f7); + text-transform: uppercase; + flex-shrink: 0; +} + +.agent-panel-tabs { + display: flex; + align-items: center; + gap: 4px; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: none; + min-width: 0; +} + +.agent-panel-tabs::-webkit-scrollbar { + display: none; +} + +/* Agent 迷你小标签页: 紧凑小巧,主要展示对应 agent 图标与关闭按钮 */ +.agent-tab { + height: 22px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + padding: 0 6px; + background: var(--bg-tab-inactive, rgba(255, 255, 255, 0.04)); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-sm, 4px); + cursor: pointer; + transition: all 0.15s ease; + color: var(--text-muted); + flex-shrink: 0; +} + +.agent-tab:hover { + background: var(--bg-tab-hover, rgba(255, 255, 255, 0.08)); + color: var(--text-bright); + border-color: var(--border-hover, rgba(255, 255, 255, 0.15)); +} + +.agent-tab.active { + background: var(--bg-tab-active, rgba(59, 130, 246, 0.15)); + border-color: var(--border-active, #3b82f6); + color: var(--accent-color, #7aa2f7); + box-shadow: 0 0 8px var(--accent-glow, rgba(59, 130, 246, 0.25)); +} + +.agent-tab-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + flex-shrink: 0; + transition: color 0.15s ease, opacity 0.15s ease; +} + +.agent-tab-icon svg { + width: 14px; + height: 14px; +} + +/* Agent 品牌特化专属色彩与微光效果 */ +.agent-tab[data-agent-id="agy"] .agent-tab-icon, +.ctx-menu-item#ctx-open-agy .ctx-menu-icon { + color: #4285f4; +} + +.agent-tab[data-agent-id="codex"] .agent-tab-icon, +.ctx-menu-item#ctx-open-codex .ctx-menu-icon { + color: #f1f5f9; +} + +[data-theme="light"] .agent-tab[data-agent-id="codex"] .agent-tab-icon, +[data-theme="light"] .ctx-menu-item#ctx-open-codex .ctx-menu-icon { + color: #000000; +} + +.agent-tab[data-agent-id="claude"] .agent-tab-icon, +.ctx-menu-item#ctx-open-claude .ctx-menu-icon { + color: #d97757; +} + +.agent-tab:not(.active) .agent-tab-icon { + opacity: 0.85; +} + +.agent-tab:hover .agent-tab-icon, +.agent-tab.active .agent-tab-icon { + opacity: 1; +} + +.agent-tab.active[data-agent-id="agy"] { + background: rgba(66, 133, 244, 0.16); + border-color: #4285f4; + box-shadow: 0 0 8px rgba(66, 133, 244, 0.35); +} + +.agent-tab.active[data-agent-id="codex"] { + background: rgba(255, 255, 255, 0.12); + border-color: rgba(255, 255, 255, 0.4); + box-shadow: 0 0 8px rgba(255, 255, 255, 0.25); +} + +[data-theme="light"] .agent-tab.active[data-agent-id="codex"] { + background: rgba(0, 0, 0, 0.08); + border-color: rgba(0, 0, 0, 0.5); + box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); +} + +.agent-tab.active[data-agent-id="claude"] { + background: rgba(217, 119, 87, 0.16); + border-color: #d97757; + box-shadow: 0 0 8px rgba(217, 119, 87, 0.3); +} + +/* 标签做小,只展示对应 agent 图标 (文字隐藏,hover显示title) */ +.agent-tab-name { + display: none; +} + +.agent-tab-close { + display: inline-flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + padding: 0; + margin-left: 2px; + background: transparent; + border: none; + color: var(--text-muted); + font-size: 13px; + line-height: 1; + border-radius: 2px; + cursor: pointer; + opacity: 0.6; + transition: all 0.12s ease; +} + +.agent-tab-close:hover { + opacity: 1; + background: rgba(239, 68, 68, 0.2); + color: #f87171; +} + +.agent-panel-header-actions { + display: flex; + align-items: center; + flex-shrink: 0; +} + +.agent-panel-close-btn { + width: 20px; + height: 20px; + padding: 0; +} + +.agent-panel-close-btn:hover { + background: rgba(239, 68, 68, 0.18); + color: #f87171; +} + +.agent-panel-body { + flex: 1; + width: 100%; + height: calc(100% - 28px); + position: relative; + overflow: hidden; + display: flex; +} + +.agent-tab-view { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + display: none; +} + +.agent-tab-view.active { + display: flex; +} + +/* 文件浏览面板 (FilePanel) */ +.file-panel { + display: flex; + flex-direction: column; + background: var(--bg-app); + border-right: 1px solid var(--border-color); + width: 240px; + min-width: 160px; + max-width: 500px; + height: 100%; + position: relative; + overflow: hidden; + user-select: none; + flex-shrink: 0; +} + +.file-panel-header { + height: 28px; + min-height: 28px; + background: var(--bg-titlebar); + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 6px 0 10px; +} + +.file-panel-header-left { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; + flex: 1; +} + +.file-panel-header-icon { + color: var(--accent-color, #7aa2f7); + flex-shrink: 0; +} + +.file-panel-title { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.8px; + color: var(--accent-color, #7aa2f7); + text-transform: uppercase; + white-space: nowrap; +} + +.file-panel-header-actions { + display: flex; + align-items: center; + gap: 2px; +} + +.file-panel-header-actions .btn-icon { + width: 22px; + height: 22px; + border-radius: var(--border-radius-sm, 4px); +} + +.file-toggle-hidden { + transition: all 0.15s ease; +} + +.file-toggle-hidden.active { + background: rgba(59, 130, 246, 0.2) !important; + color: var(--accent-color, #7aa2f7) !important; + box-shadow: 0 0 6px rgba(59, 130, 246, 0.3); +} + +.file-panel-pathbar { + padding: 4px 8px; + font-size: 11px; + font-family: var(--font-mono, monospace); + color: var(--text-muted); + background: rgba(0, 0, 0, 0.18); + border-bottom: 1px solid var(--border-color); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + direction: rtl; + text-align: left; +} + +.file-panel-body { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 4px; +} + +.file-list { + display: flex; + flex-direction: column; + gap: 1px; +} + +.file-item { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + font-size: 12px; + color: var(--text-main); + cursor: pointer; + border-radius: var(--border-radius-sm, 4px); + transition: background 0.12s ease, color 0.12s ease; +} + +.file-item:hover { + background: rgba(255, 255, 255, 0.07); + color: var(--text-bright); +} + +.file-item.selected { + background: rgba(59, 130, 246, 0.18); + color: var(--accent-color, #7aa2f7); +} + +.file-item.is-hidden { + opacity: 0.55; + font-style: italic; +} + +.file-item-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + flex-shrink: 0; +} + +.file-item.is-directory .file-item-icon { + color: #e0af68; +} + +.file-item.is-file .file-item-icon { + color: var(--text-muted); +} + +.file-item-name { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.file-item-size { + font-size: 10px; + color: var(--text-muted); + margin-left: auto; + flex-shrink: 0; +} + +.file-panel-empty, +.file-panel-error { + padding: 20px 10px; + text-align: center; + font-size: 11px; + color: var(--text-muted); +} + +.file-panel-error { + color: #f87171; +} + +.file-panel-footer { + height: 22px; + min-height: 22px; + background: var(--bg-titlebar); + border-top: 1px solid var(--border-color); + padding: 0 8px; + display: flex; + align-items: center; + font-size: 10px; + color: var(--text-muted); +} + + diff --git a/src/renderer/index.html b/src/renderer/index.html index 28463df..2d766c6 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -16,10 +16,10 @@
-