├── .gitignore ├── declarations.d.ts ├── .npmignore ├── src ├── components │ ├── autoCompleteSettingsTab.scss │ ├── autoCompleteAIDialog.scss │ ├── autoCompleteSettingsTab.ts │ ├── autoCompleteHintMenu.pug │ ├── autoCompleteAIDialog.pug │ ├── autoCompleteHintMenu.scss │ ├── autoCompleteSettingsTab.pug │ ├── autoCompleteAIDialog.ts │ └── autoCompleteHintMenu.ts ├── icons │ └── bird.svg ├── settingsTabProvider.ts ├── services │ ├── signalService.ts │ ├── styleService.ts │ ├── provider │ │ ├── baseProvider.ts │ │ ├── quickCmdContentProvider.ts │ │ ├── aiContentProvider.ts │ │ ├── argumentsContentProvider.ts │ │ └── historyProvider.ts │ ├── contentProvider.ts │ ├── manager │ │ ├── baseManager.ts │ │ └── simpleContentManager.ts │ ├── myLogService.ts │ ├── translateService.ts │ └── menuService.ts ├── api │ └── pluginType.d.ts ├── hotkeyProvider.ts ├── configProvider.ts ├── buttonProvider.ts ├── index.ts ├── terminalDecorator.ts ├── utils │ └── commonUtils.ts └── static │ └── i18n.yaml ├── scripts └── release_note.py ├── test2.html ├── tsconfig.json ├── CHANGELOG.md ├── .github ├── workflows │ ├── mark_stale.yml │ ├── thank_feedback.yml │ └── release.yml └── ISSUE_TEMPLATE │ ├── bug_report_zh_cn.yml │ └── bug_report.yml ├── README_zh-CN.md ├── webpack.config.js ├── docs ├── INIT_zh-CN.md └── INIT_en-US.md ├── README.md ├── package.json └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.yaml' { 2 | const content: any; 3 | export default content; 4 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test2.html 3 | .git 4 | .github 5 | # 排除配置文件 6 | tsconfig.json 7 | webpack.config.js 8 | yarn.lock -------------------------------------------------------------------------------- /src/components/autoCompleteSettingsTab.scss: -------------------------------------------------------------------------------- 1 | a.og-tac-a { 2 | color: rgb(13, 110, 253); 3 | text-decoration: underline; 4 | 5 | &:hover { 6 | 7 | color: rgba(13, 109, 253, 0.671); 8 | text-decoration: underline; 9 | cursor: pointer; 10 | text-decoration: dotted; 11 | } 12 | } -------------------------------------------------------------------------------- /scripts/release_note.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | with open('CHANGELOG.md', 'r', encoding='utf-8') as f: 4 | readme_str = f.read() 5 | 6 | match_obj = re.search(r'(?<=### )[\s\S]*?(?=#)', readme_str, re.DOTALL) 7 | if match_obj: 8 | h3_title = match_obj.group(0) 9 | with open('result.txt', 'w') as f: 10 | f.write(h3_title) 11 | else: 12 | with open('result.txt', 'w') as f: 13 | f.write("") -------------------------------------------------------------------------------- /src/icons/bird.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/autoCompleteAIDialog.scss: -------------------------------------------------------------------------------- 1 | .rounded-rectangle { 2 | border-radius: 15px; 3 | border: 1px solid #ddd; 4 | padding: 10px; 5 | margin-bottom: 10px; 6 | cursor: pointer; 7 | } 8 | 9 | .rounded-rectangle.active { 10 | background-color: #f0f0f0; 11 | border-color: #007bff; 12 | } 13 | 14 | .rate-safe { 15 | color: #28a745; 16 | } 17 | .rate-warn { 18 | color: #ffc107; 19 | } 20 | .rate-danger { 21 | color: #dc3545; 22 | } -------------------------------------------------------------------------------- /src/settingsTabProvider.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | import { SettingsTabProvider } from 'tabby-settings' 3 | 4 | import { AutoCompleteSettingsTabComponent } from './components/autoCompleteSettingsTab' 5 | 6 | /** @hidden */ 7 | @Injectable() 8 | export class AutoCompleteSettingsTabProvider extends SettingsTabProvider { 9 | id = 'ogautocomplete' 10 | // icon 11 | title = 'Quick Hint' 12 | 13 | getComponentType (): any { 14 | return AutoCompleteSettingsTabComponent 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/services/signalService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { Subject } from "rxjs"; 3 | 4 | @Injectable({providedIn: 'root'}) 5 | export class MySignalService { 6 | private menuStatusNS: Subject = new Subject(); 7 | public menuStatus$ = this.menuStatusNS.asObservable(); 8 | 9 | private startCompleteNowNS: Subject = new Subject(); 10 | public startCompleteNow$ = this.startCompleteNowNS.asObservable(); 11 | 12 | public changeMenuStatus() { 13 | this.menuStatusNS.next(); 14 | } 15 | 16 | public hintNow() { 17 | this.startCompleteNowNS.next(); 18 | } 19 | } -------------------------------------------------------------------------------- /test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 16 | 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "target": "es2016", 7 | "esModuleInterop": true, 8 | "noImplicitAny": false, 9 | "removeComments": false, 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "sourceMap": true, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "noUnusedLocals": false, 17 | "declaration": true, 18 | "declarationDir": "dist", 19 | "lib": [ 20 | "dom", 21 | "es2015", 22 | "es7" 23 | ] 24 | }, 25 | "exclude": ["node_modules", "dist"] 26 | } 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 更新日志 2 | 3 | ### v1.0.0 (2025-08-24) 4 | 5 | - 新增:为命令参数进行补全提示; 6 | - 改进:ContentProvider API现在提供光标位置; 7 | - 改进:单一匹配项过长时,超长部分强制隐藏; 8 | - 变更:`i18n.yaml`文件结构; 9 | 10 | --- 11 | 12 | - Added: Autocompletion hints for command parameters 13 | - Improved: `ContentProvider` API now provides cursor position 14 | - Improved: When a single match item is too long, the overflow part is forcibly hidden 15 | - Changed: Structure of the `i18n.yaml` file 16 | 17 | ### v0.1.3 (2025年7月23日) 18 | 19 | - 修复:由于插件Bug导致长时间运行后性能下降的问题(插件错误频繁创建临时div元素、且未删除); 20 | 21 | ### v0.1.2 (2025年3月2日) 22 | 23 | - 改进:命令保存的筛选过滤;排除 cd, 等 24 | - 改进:命令保存尽量仍然使用上屏的内容,如果可以,不再使用回传; 25 | - 改进:立即提示;(获取当前命令内容,立刻触发提示或刷新提示) 26 | - 修复:`getLastStateLine()` (和有时出现的“抓不到最后一行命令”的错误,似乎是一同出现的。) 27 | - 改进:提示数量限制:history provider提供5个,在有其他提示存在时,限制出现的提示总数为7,history至少2; 28 | 29 | ### v0.1.1 (2025年1月6日) 30 | 31 | - 修复:提示菜单显示位置异常靠左; 32 | - 修复:有覆盖浮窗时,仍接管上下方向键的问题; 33 | - 改进:一些样式调整; -------------------------------------------------------------------------------- /src/services/styleService.ts: -------------------------------------------------------------------------------- 1 | import { DOCUMENT } from "@angular/common"; 2 | import { Inject, Injectable } from "@angular/core"; 3 | import { ConfigService } from "tabby-core"; 4 | 5 | @Injectable({providedIn: 'root'}) 6 | export class StyleService { 7 | constructor( 8 | @Inject(DOCUMENT) private document: Document, 9 | private configService: ConfigService 10 | ) { 11 | this.loadStyle(); 12 | } 13 | 14 | loadStyle() { 15 | this.removeStyle(); 16 | const styleElem = this.document.createElement('style'); 17 | styleElem.setAttribute("id", "og-tac-style") 18 | styleElem.textContent = ` 19 | app-root>.content .tab-bar .btn-tab-bar svg.og-tac-tool-btn, 20 | app-root>.content .tab-bar .btn-tab-bar svg.og-tac-tool-btn path { 21 | fill: none; 22 | } 23 | `; 24 | this.document.head.appendChild(styleElem); 25 | } 26 | removeStyle() { 27 | this.document.getElementById("og-tac-style")?.remove(); 28 | } 29 | } -------------------------------------------------------------------------------- /src/services/provider/baseProvider.ts: -------------------------------------------------------------------------------- 1 | import { EnvBasicInfo, OptionItem, TerminalSessionInfo } from "api/pluginType"; 2 | import { MyLogger } from "services/myLogService"; 3 | import { ConfigService } from "tabby-core"; 4 | 5 | export interface OptionItemResultWrap { 6 | optionItem: OptionItem[]; 7 | envBasicInfo: EnvBasicInfo; 8 | type: string; 9 | } 10 | 11 | export class BaseContentProvider { 12 | protected static providerTypeKey: string = "ERROR_THIS_NOT_USED_FOR_PROVIDER"; 13 | constructor( 14 | protected logger: MyLogger, 15 | protected configService: ConfigService, 16 | ) { 17 | 18 | } 19 | async getQuickCmdList(inputCmd: string, cursorIndexAt: number, envBasicInfo: EnvBasicInfo): Promise { 20 | // do sth 21 | 22 | return null; 23 | } 24 | async userInputCmd(inputCmd: string, terminalSessionInfo: TerminalSessionInfo): Promise { 25 | 26 | } 27 | userSelectedCallback(inputCmd: string): void { 28 | 29 | } 30 | } -------------------------------------------------------------------------------- /src/components/autoCompleteSettingsTab.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core' 2 | import { AutoCompleteTranslateService } from 'services/translateService'; 3 | import { PlatformService, TranslateService } from "tabby-core"; 4 | import { ConfigService } from 'tabby-core' 5 | 6 | @Component({ 7 | template: require('./autoCompleteSettingsTab.pug'), 8 | styles: [require("./autoCompleteSettingsTab.scss")] 9 | }) 10 | export class AutoCompleteSettingsTabComponent { 11 | agents = [ 12 | 'Bonzi', 13 | 14 | ] 15 | constructor ( 16 | public config: ConfigService, 17 | private translate: AutoCompleteTranslateService, 18 | private platform: PlatformService 19 | ) { 20 | // console.log(this.translate.instant('Application')); 21 | } 22 | openGithub() { 23 | this.platform.openExternal('https://github.com/OpaqueGlass/tabby-quick-cmds-hint') 24 | } 25 | openNewIssue() { 26 | this.platform.openExternal('https://github.com/OpaqueGlass/tabby-quick-cmds-hint/issues/new') 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/services/contentProvider.ts: -------------------------------------------------------------------------------- 1 | // import { 2 | // Inject, 3 | // Injectable, 4 | // } from '@angular/core'; 5 | // import { DOCUMENT } from '@angular/common'; 6 | // import { ConfigService } from 'tabby-core'; 7 | // import { OptionItem } from 'api/pluginType'; 8 | // import { QuickCmdContentProvider } from './provider/quickCmdContentProvider'; 9 | 10 | // @Injectable({ 11 | // providedIn: 'root' 12 | // }) 13 | // export class ContentProviderService { 14 | // constructor( 15 | // private config: ConfigService, 16 | // @Inject(DOCUMENT) private document: Document 17 | // ) {} 18 | 19 | // public getContentList(inputCmd: string): OptionItem[] { 20 | // const result: OptionItem[] = []; 21 | // const envBasicInfo = {config: this.config, document: this.document}; 22 | // result.push(...QuickCmdContentProvider.getQuickCmdList(inputCmd, envBasicInfo)); 23 | // return result; 24 | // } 25 | // } 26 | 27 | // interface EnvBasicInfo { 28 | // config: ConfigService; 29 | // document: Document; 30 | // } 31 | -------------------------------------------------------------------------------- /src/api/pluginType.d.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from 'tabby-core'; 2 | import { BaseTerminalProfile, BaseTerminalTabComponent } from 'tabby-terminal'; 3 | 4 | interface EnvBasicInfo { 5 | config: ConfigService; // tabby提供的设置服务 6 | document: Document; // window.document 7 | tab: BaseTerminalTabComponent; // tabby提供的tab组件 8 | sessionId: string; // 插件自行赋予的id,用于区分不同的会话,重新到相同主机连接时会变化 9 | } 10 | 11 | interface TerminalSessionInfo { 12 | config: ConfigService; 13 | tab: BaseTerminalTabComponent; 14 | sessionId: string; 15 | matchedByRegExp: boolean; 16 | } 17 | export interface OptionItem { 18 | name: string; // 显示在候选区中的名称 19 | content: string; // 实际上屏内容 20 | type: string; // 类型,请仅一个字母或符号表示 21 | desp: string; // 描述,这将显示在所有候选项的下方 22 | callback?: () => OptionItem[] | null; // 回调函数,用于生成下一级候选项,返回null则认为Provider自行实现了下一级,或其他插入方式 23 | clearThenInput?: boolean; // 先清空整行、再进行上屏?默认为true 24 | doNotEnterExec?: boolean; // 请勿回车上屏并执行,默认为false,此项为true则会在 25 | backgroundColor?: string; // 颜色,为了用户自定义可能会移除 26 | color?: string; // 颜色 27 | } -------------------------------------------------------------------------------- /src/hotkeyProvider.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | import { HotkeyDescription, HotkeyProvider } from 'tabby-core' 3 | 4 | /** @hidden */ 5 | @Injectable() 6 | export class AutoCompleteHotkeyProvider extends HotkeyProvider { 7 | hotkeys: HotkeyDescription[] = [ 8 | { 9 | id: 'ogautocomplete_stop', 10 | name:'Stop AutoComplete Or Reopen it', 11 | }, 12 | { 13 | id: "ogautocomplete_dev", 14 | name: "Open Dev Tools", 15 | }, 16 | { 17 | id: "ogautocomplete_init_scripts", 18 | name: "Init QuickCmdHint for Current Session (Bash only)", 19 | }, 20 | { 21 | id: "ogautocomplete_ask_ai", 22 | name: "AI Command Generation", 23 | }, 24 | { 25 | id: "ogautocomplete_hint_now", 26 | name: "Show complete menu now" 27 | } 28 | ] 29 | 30 | constructor ( 31 | ) { super() } 32 | 33 | async provide (): Promise { 34 | return [ 35 | ...this.hotkeys 36 | ] 37 | } 38 | } -------------------------------------------------------------------------------- /.github/workflows/mark_stale.yml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | 6 | permissions: 7 | issues: write 8 | pull-requests: write 9 | 10 | jobs: 11 | close-issues: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/stale@v9 15 | with: 16 | days-before-issue-stale: 45 17 | days-before-issue-close: 14 18 | stale-issue-label: "stale" 19 | stale-issue-message: "This issue has been marked as stale because it has been open for 45 days with no activity. If you believe this issue is still relevant, please comment to keep it active. Otherwise, it will be closed in 14 days. Thank you for your contributions!" 20 | # close-issue-message: "This issue is about to be closed because it has been inactive for 14 days since being marked as stale. If you still have questions or concerns, please leave a comment." 21 | exempt-issue-labels: "in-progress,working on,working_on,stage/working on,stage/queue,stage/queue(next)" 22 | exempt-all-milestones: true 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | -------------------------------------------------------------------------------- /src/components/autoCompleteHintMenu.pug: -------------------------------------------------------------------------------- 1 | .og-autocomplete-list.list-group( 2 | [ngStyle]="{'font-size': configService.store.ogAutoCompletePlugin.appearance.fontSize + 'px'}", 3 | [attr.theme-mode]="themeMode", 4 | [attr.theme-name]="themeName" 5 | ) 6 | .og-autocomplete-main-text {{ mainText }} 7 | .og-autocomplete-item-list 8 | ng-container(*ngFor='let option of options; let i = index') 9 | .og-autocomplete-item( 10 | [attr.data-index]='i', 11 | [class.og-tac-selected]='i === currentItemIndex', 12 | (click)='inputItem(i, 0)' 13 | ) 14 | div.og-ac-type( 15 | [attr.data-cmd-type]='option.type' 16 | ) {{option.type}} 17 | div.og-ac-name {{option.name}} 18 | div.og-ac-desp {{option.desp}} 19 | ng-container(*ngIf='options.length > 0 && currentItemIndex != -1 && isValidStr(options[currentItemIndex]?.desp)') 20 | .og-autocomplete-footer {{options[currentItemIndex].desp}} 21 | ng-container(*ngIf='options.length === 0') 22 | .og-autocomplete-footer(translate='menu.noresult') 23 | -------------------------------------------------------------------------------- /README_zh-CN.md: -------------------------------------------------------------------------------- 1 | # tabby-quick-cmds-hint 2 | **中文** | [English](README.md) 3 | 4 | 这是用于[Tabby](https://github.com/Eugeny/tabby)的简单命令补全提示插件。 5 | 6 | ## 特点 7 | 8 | - 读取匹配在 `tabby-quick-cmds` 插件中保存命令 9 | - 使用 AI 补全命令(仅在快捷键触发时可用) 10 | - 匹配提示命令参数(先完整输入命令名称,再空格输入匹配内容)(匹配内容由AI生成,请注意检验) 11 | - 读取匹配历史记录 12 | 13 | ## 快速开始 14 | 15 | > [!NOTE] 16 | > 17 | > - 插件仍在开发中。 18 | > - 插件仅针对 “ssh连接运行Ubuntu、Shell为bash的服务器” 进行测试和开发,其他情况可能存在问题。阅读[docs/INIT_zh-CN.md](./docs/INIT_zh-CN.md)了解更多信息。 19 | > - 在控制台输出的日志比较乱。 20 | 21 | 1. 22 | - 对于 `bash` 用户: 23 | - 基本功能:在 `~/.bashrc` 中添加以下脚本 24 | ```bash 25 | export PS1="$PS1\[\e]1337;CurrentDir="'$(pwd)\a\]' 26 | ``` 27 | 2. 下载并启用 `tabby-quick-cmds` 插件,本插件主要检索 `tabby-quick-cmds` 中保存的命令。添加一些命令。 28 | 3. 开启令人烦躁的提示体验。 29 | 30 | > 如果这对你有帮助,请考虑Star本项目。 31 | 32 | 33 | 34 | ## 参考与鸣谢 35 | 36 | > 一些 *开发者* 或 *插件直接使用的包* 未在此列出,请参考贡献者列表或 `package.json` 文件。 37 | 38 | - [tabby-clippy](https://github.com/Eugeny/tabby-clippy) Tabby 的示例插件 39 | - [minyoad/terminus-quick-cmds](https://github.com/minyoad/terminus-quick-cmds) / [Domain/terminus-quick-cmds](https://github.com/Domain/terminus-quick-cmds) 40 | - [lucide-icon](https://lucide.dev/) svg图标 -------------------------------------------------------------------------------- /src/configProvider.ts: -------------------------------------------------------------------------------- 1 | import { ConfigProvider } from 'tabby-core' 2 | 3 | /** @hidden */ 4 | export class AutoCompleteConfigProvider extends ConfigProvider { 5 | defaults = { 6 | ogAutoCompletePlugin: { 7 | agent: 'OGAutoComplete', 8 | debugLevel: 3, 9 | autoInit: false, 10 | enableCompleteWithCompleteStart: true, 11 | menuShowItemMaxCount: 7, 12 | ai: { 13 | openAIBaseUrl: "https://api.openai.com/v1", 14 | openAIKey: "", 15 | openAIModel: "gpt-4o-mini", 16 | }, 17 | appearance: { 18 | "fontSize": 15, 19 | }, 20 | useRegExpDetectPrompt: true, 21 | customRegExp: "", 22 | history: { 23 | "enable": false, 24 | "countInRegExp": true, 25 | }, 26 | arguments: { 27 | "enable": false, 28 | } 29 | }, 30 | hotkeys: { 31 | 'ogautocomplete_stop': [], 32 | 'ogautocomplete_dev': [], 33 | "ogautocomplete_init_scripts": [], 34 | "ogautocomplete_ask_ai": [], 35 | "ogautocomplete_hint_now": [], 36 | }, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/autoCompleteAIDialog.pug: -------------------------------------------------------------------------------- 1 | .modal-body 2 | div.askInputArea 3 | input.form-control.quickCmd( 4 | type='text', 5 | [(ngModel)]='askUserInput', 6 | autofocus, 7 | [placeholder]="translate('ui.ask_ai')", 8 | (keyup)='handleKeydown($event)', 9 | ) 10 | button.btn.btn-outline-primary((click)='ask()') {{translate('ui.ask')}} 11 | div(*ngIf='loadingFlag') 12 | .spinner-border.text-primary(role='status') 13 | span.sr-only {{translate('ui.loading')}} 14 | div(*ngIf='isValidStr(notReady)') 15 | span {{translate('ui.error_occurred', {"desp": notReady})}} 16 | .list-group.mt-3.connections-list 17 | ng-container(*ngFor='let cmd of commands; let i = index') 18 | .list-group-item.list-group-item-action.d-flex.align-items-center.rounded-rectangle( 19 | [ngClass]="{'active': i === selectedIndex}", 20 | (click)='userSelected(cmd, $event)', 21 | ) 22 | .mr-auto 23 | div 24 | code.command-text {{cmd.command}} 25 | //- .text-muted {{cmd.text}}{{cmd.appendCR ? "\\n" : ""}} 26 | .description {{cmd.desp}} 27 | .danger-rating([ngClass]="getRatingColor(cmd)") {{translate('ui.danger_rating')}}: {{cmd.dangerRating}} -------------------------------------------------------------------------------- /src/services/manager/baseManager.ts: -------------------------------------------------------------------------------- 1 | import { Subscription } from "rxjs"; 2 | import { AddMenuService } from "services/menuService"; 3 | import { MyLogger } from "services/myLogService"; 4 | import { ConfigService } from "tabby-core"; 5 | import { BaseTerminalProfile, BaseTerminalTabComponent } from "tabby-terminal"; 6 | import { generateUUID } from "utils/commonUtils"; 7 | import { OnDestroy } from '@angular/core'; 8 | 9 | export class BaseManager implements OnDestroy { 10 | protected sessionUniqueId: string; 11 | protected profileUniqueId: string; 12 | protected subscriptionList: Array; 13 | constructor( 14 | public tab: BaseTerminalTabComponent, 15 | public logger: MyLogger, 16 | public addMenuService: AddMenuService, 17 | public configService: ConfigService 18 | ) { 19 | this.profileUniqueId = tab.profile.id; 20 | this.sessionUniqueId = generateUUID(); 21 | this.subscriptionList = new Array(); 22 | } 23 | handleInput: (buffers: Buffer[])=>void | null = null; 24 | handleOutput:(data: string[])=>void | null = null; 25 | destroy():void { 26 | this.logger.debug("Manager Destroying"); 27 | for (let subscription of this.subscriptionList) { 28 | subscription?.unsubscribe(); 29 | } 30 | } 31 | ngOnDestroy():void { 32 | this.destroy(); 33 | } 34 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | target: 'node', 5 | entry: 'src/index.ts', 6 | devtool: 'source-map', 7 | context: __dirname, 8 | mode: 'development', 9 | output: { 10 | path: path.resolve(__dirname, 'dist'), 11 | filename: 'index.js', 12 | pathinfo: true, 13 | libraryTarget: 'umd', 14 | devtoolModuleFilenameTemplate: 'webpack-tabby-clippy:///[resource-path]', 15 | }, 16 | resolve: { 17 | modules: ['.', 'src', 'node_modules'].map(x => path.join(__dirname, x)), 18 | extensions: ['.ts', '.js'], 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.ts$/, 24 | loader: 'ts-loader', 25 | options: { 26 | configFile: path.resolve(__dirname, 'tsconfig.json'), 27 | }, 28 | }, 29 | { 30 | test: /\.scss/, 31 | use: ['to-string-loader', 'css-loader', 'sass-loader'], 32 | }, 33 | { 34 | test: /\.css/, 35 | use: ['to-string-loader', 'css-loader'], 36 | }, 37 | { test: /\.pug$/, use: ['apply-loader', 'pug-loader'] }, 38 | { 39 | test: /\.yaml$/, 40 | use: 'raw-loader', 41 | }, 42 | { 43 | test: /\.svg$/, 44 | use: 'svg-inline-loader', 45 | } 46 | ] 47 | }, 48 | externals: [ 49 | 'fs', 50 | 'ngx-toastr', 51 | /^rxjs/, 52 | /^@angular/, 53 | /^@ng-bootstrap/, 54 | /^tabby-/, 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /docs/INIT_zh-CN.md: -------------------------------------------------------------------------------- 1 | # 初次使用说明和插件实现方式的解释 2 | 3 | > 最后更新于:v0.1.2 更新时。 4 | 5 | ## 插件实现方式说明 6 | 7 | ### 1. 命令开始输入判断 8 | 9 | 插件通过正则匹配原始输出中的`CurrentDir.*\a`确认命令输入prompt的结束位置,清理其中的转义符 10 | ,并由此判断prompt的结束和命令的开始。 11 | 12 | 因此,`CurrentDir`相关的提示必须在命令输入提示prompt的末尾出现。否则插件将会把prompt错误 13 | 识别为命令的一部分。 14 | 15 | 16 | ### 2. 历史命令记录 17 | 18 | > 我正在尝试移除这个前置条件。 19 | > 20 | > 因为这样获得的命令将是实际执行的,可能包含不需要的信息(如`[[]]`判断、alias)。 21 | 22 | 插件基于执行命令后,shell通过`]2323;Command=$(cmd)\x07`返回执行的命令记录。这里是自定义 23 | 的转义序列,没有参考来源。如果和其他已有实现冲突,请反馈。 24 | 25 | 26 | ## 针对一些shell的配置参考 27 | 28 | > 开发者仅针对 Ubuntu下bash 进行开发与测试,不保证在其他shell中的可用性。 29 | > 30 | > 理论上只要shell满足`实现方式说明`中的规则,即可以被识别,但不同shell可能存在差异。 31 | > 32 | > 如果在bash中出现问题,请反馈bug。如果在其他shell中存在问题,请考虑提交PR。 33 | 34 | #### bash 35 | 36 | 直接参考[tabby/wiki/Shell-working-directory-reporting#bash](https://github.com/Eugeny/tabby/wiki/Shell-working-directory-reporting#bash)即可。 37 | 38 | 基本功能: 39 | 40 | ```bash 41 | export PS1="$PS1\[\e]1337;CurrentDir="'$(pwd)\a\]' 42 | ``` 43 | 44 | > 命令历史记录: 45 | > 46 | > ```bash 47 | > function preexec_invoke_exec() { 48 | > printf "\033]2323;Command=%s\007" "$1" 49 | > } 50 | > 51 | > trap 'preexec_invoke_exec "$BASH_COMMAND"' DEBUG 52 | > 53 | > ``` 54 | 55 | #### fish 56 | 57 | 需要修改默认的`fish_prompt`函数。 58 | 59 | 原函数代码可以参考`/usr/share/fish/functions/fish_prompt.fish @ line 4`(或fish终端输入`type fish_prompt`) 60 | 61 | 需要在函数最后补充echo 62 | 63 | ```fish 64 | echo -en "\e]1337;CurrentDir=$PWD\x7" 65 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tabby-quick-cmds-hint 2 | 3 | [中文](README_zh-CN.md) | **English** 4 | 5 | This is a simple complete hint (i.e. auto-complete) plugin for [Tabby](https://github.com/Eugeny/tabby). 6 | 7 | ## Features 8 | 9 | - Reads and matches commands saved in the `tabby-quick-cmds` plugin 10 | - Uses AI to complete commands (available only when triggered by a shortcut key) 11 | - Matches and suggests command parameters (first enter the full command name, then type a space followed by matching content) (the matching content is AI-generated, please verify carefully) 12 | - Reads and matches command history 13 | 14 | 15 | ## Quick Start 16 | 17 | > [!NOTE] 18 | > 19 | > - This plugin is still in development. 20 | > - It may not be compatible with shells other than Bash, or with systems other than Ubuntu. See [docs/INIT.md](./docs/INIT_en-US.md) for more info. 21 | > - The console log output may also be quite messy. 22 | 23 | 1. 24 | - For `bash` user: 25 | - basic function: add the following scripts to `~/.bashrc` 26 | ```bash 27 | export PS1="$PS1\[\e]1337;CurrentDir="'$(pwd)\a\]' 28 | ``` 29 | 2. Download and enable `tabby-quick-cmds` plugin. Add some commands. 30 | 3. Start annoying hint experience. 31 | 32 | ## Reference & Appreciations 33 | 34 | > Some *developers* or *packages directly used in this plugin* are not listed. Please refer to the contributors list or `package.json`. 35 | 36 | - [tabby-clippy](https://github.com/Eugeny/tabby-clippy) An example plugin for Tabby 37 | - [minyoad/terminus-quick-cmds](https://github.com/minyoad/terminus-quick-cmds) / [Domain/terminus-quick-cmds](https://github.com/Domain/terminus-quick-cmds) 38 | - [lucide-icon](https://lucide.dev/) the SVG icon -------------------------------------------------------------------------------- /src/services/myLogService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ConfigService, Logger, LogService } from 'tabby-core'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class MyLogger { 8 | private logger: Logger; 9 | private name: string; 10 | constructor( 11 | log: LogService, 12 | private configService: ConfigService 13 | ) { 14 | this.name = 'quick-cmds-hint'; 15 | // tabby的Logger不支持,继续传参,只能类似%d这样的模板字符串,我们这边还是自定义省心点 16 | this.logLevel = configService.store?.ogAutoCompletePlugin?.debugLevel ?? 3; 17 | configService.changed$.subscribe(() => { 18 | this.setLogLevel(configService.store.ogAutoCompletePlugin.debugLevel); 19 | }); 20 | } 21 | 22 | private logLevel: number = 3; 23 | 24 | setLogLevel(level: number) { 25 | this.logLevel = level; 26 | } 27 | getLogLevel() { 28 | return this.logLevel; 29 | } 30 | 31 | messyDebug(...args: any[]) { 32 | if (this.logLevel <= -1) { 33 | console.debug(`%c[${this.name}] `, "color: #aaa", ...args); 34 | } 35 | } 36 | 37 | debug(...args: any[]) { 38 | if (this.logLevel <= 0) { 39 | console.debug(`%c[${this.name}] `, "color: #aaa", ...args); 40 | } 41 | } 42 | 43 | log(...args: any[]) { 44 | if (this.logLevel <= 1) { 45 | console.info(`%c[${this.name}] `, "color: #aaa", ...args); 46 | } 47 | } 48 | 49 | warn(...args: any[]) { 50 | if (this.logLevel <= 2) { 51 | console.warn(`%c[${this.name}] `, "color: #aaa", ...args); 52 | } 53 | } 54 | 55 | error(...args: any[]) { 56 | if (this.logLevel <= 3) { 57 | console.error(`%c[${this.name}] `, "color: #aaa", ...args); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/services/provider/quickCmdContentProvider.ts: -------------------------------------------------------------------------------- 1 | import { OptionItem, EnvBasicInfo } from "../../api/pluginType"; 2 | import Fuse from 'fuse.js'; 3 | import { BaseContentProvider, OptionItemResultWrap } from "./baseProvider"; 4 | import { MyLogger } from "services/myLogService"; 5 | import { Injectable } from "@angular/core"; 6 | import { ConfigService } from "tabby-core"; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class QuickCmdContentProvider extends BaseContentProvider { 12 | protected static providerTypeKey: string = "q"; 13 | constructor( 14 | protected logger: MyLogger, 15 | protected configService: ConfigService, 16 | ) { 17 | super(logger, configService); 18 | } 19 | 20 | async getQuickCmdList(inputCmd: string, cursorIndexAt: number, envBasicInfo: EnvBasicInfo): Promise { 21 | const result: OptionItem[] = []; 22 | const options = { 23 | keys: ['name'], // 搜索的字段 24 | threshold: 0.3, // 控制匹配的模糊度 25 | includeScore: true // 包含得分 26 | }; 27 | if (envBasicInfo.config.store.qc == undefined || envBasicInfo.config.store.qc.cmds == undefined) { 28 | return null; 29 | } 30 | const dataList = envBasicInfo.config.store.qc.cmds.map((oneCmd) => { 31 | return { 32 | name: oneCmd.name, 33 | content: oneCmd.text, 34 | desp: "", 35 | type: QuickCmdContentProvider.providerTypeKey 36 | } as OptionItem; 37 | }); 38 | const fuse = new Fuse(dataList, options); 39 | this.logger.log("匹配结果", fuse.search(inputCmd)); 40 | result.push(...fuse.search(inputCmd).map((value)=>value.item as OptionItem)); 41 | return { 42 | optionItem: result, 43 | envBasicInfo: envBasicInfo, 44 | type: QuickCmdContentProvider.providerTypeKey 45 | }; 46 | } 47 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tabby-quick-cmds-hint", 3 | "version": "1.0.0", 4 | "description": "It's a simple complete hint (i.e. auto-complete) plugin for tabby. (beta)", 5 | "repository": { 6 | "url": "git+https://github.com/OpaqueGlass/tabby-quick-cmds-hint.git" 7 | }, 8 | "homepage": "https://github.com/OpaqueGlass/tabby-quick-cmds-hint", 9 | "author": { 10 | "name": "OpaqueGlass" 11 | }, 12 | "license": "AGPL-3.0", 13 | "keywords": [ 14 | "tabby-plugin" 15 | ], 16 | "main": "dist/index.js", 17 | "typings": "dist/index.d.ts", 18 | "scripts": { 19 | "build": "webpack --progress --color", 20 | "watch": "webpack --progress --color --watch", 21 | "prepublishOnly": "npm run build" 22 | }, 23 | "files": [ 24 | "dist" 25 | ], 26 | "devDependencies": { 27 | "@angular/animations": "^15", 28 | "@angular/common": "^15", 29 | "@angular/core": "^15", 30 | "@angular/forms": "^15", 31 | "@angular/platform-browser": "^15", 32 | "@ng-bootstrap/ng-bootstrap": "^17.0.1", 33 | "@ngx-translate/core": "^16.0.3", 34 | "@types/node": "^22.5.4", 35 | "@types/webpack-env": "^1.16.0", 36 | "apply-loader": "^2.0.0", 37 | "clippyjs": "^0.0.3", 38 | "css-loader": "^7.1.2", 39 | "node-sass": "^9.0.0", 40 | "pug": "^3.0.3", 41 | "pug-loader": "^2.4.0", 42 | "raw-loader": "^4.0.2", 43 | "rxjs": "^7.3.0", 44 | "sass": "^1.78.0", 45 | "sass-loader": "^16.0.1", 46 | "style-loader": "^4.0.0", 47 | "tabby-core": "^1.0.197-nightly.1", 48 | "tabby-settings": "^1.0.197-nightly.1", 49 | "tabby-terminal": "^1.0.197-nightly.1", 50 | "to-string-loader": "^1.2.0", 51 | "ts-loader": "^9.5.1", 52 | "typescript": "^5.5.4", 53 | "webpack": "^5.24.4", 54 | "webpack-cli": "^5.1.4" 55 | }, 56 | "dependencies": { 57 | "@xterm/addon-unicode11": "^0.8.0", 58 | "@xterm/xterm": "^5.4.0", 59 | "fuse.js": "^7.0.0", 60 | "js-yaml": "^4.1.0", 61 | "openai": "^4.67.1", 62 | "strip-ansi": "^7.1.0", 63 | "svg-inline-loader": "^0.8.2" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_zh_cn.yml: -------------------------------------------------------------------------------- 1 | name: 问题反馈 2 | description: 提交非预期行为、错误或缺陷报告 3 | assignees: [] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | 感谢提交问题反馈!请尽可能详细地填写以下内容,以帮助我们理解和解决问题。 9 | 10 | - type: textarea 11 | id: problem-description 12 | attributes: 13 | label: 问题现象 14 | description: 尽可能详细地描述问题的表现。 15 | placeholder: 在此描述问题...在粘贴错误日志时,请使用markdown代码块 16 | validations: 17 | required: true 18 | 19 | - type: markdown 20 | attributes: 21 | value: | 22 | 如果问题不能稳定重现,请修改“设置-命令提示-Debug-显示详细日志和调错信息”为0。并关注开发者工具(设置-应用-开启开发者工具)-控制台(Console)中的错误提示,将遇到问题时的控制台日志截图上传。 23 | 24 | - type: textarea 25 | id: reproduce-steps 26 | attributes: 27 | label: 复现操作 28 | description: 描述重现问题所需要的步骤或设置项。如果不能稳定重现,请说明问题的发生频率,并上传错误提示信息。 29 | placeholder: | 30 | 1. 步骤一 31 | 2. 步骤二 32 | 3. 步骤三 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | id: screenshots-or-recordings 38 | attributes: 39 | label: 截图或录屏说明 40 | description: (可不填)请上传截图或录屏来演示问题和复现步骤。 41 | placeholder: 在此上传截图或录屏... 42 | 43 | - type: textarea 44 | id: expected-behavior 45 | attributes: 46 | label: 预期行为 47 | description: (可不填)描述你认为插件应当表现出怎样的行为或结果 48 | placeholder: 在此描述预期行为... 49 | 50 | - type: textarea 51 | attributes: 52 | label: 设备和系统信息 53 | description: | 54 | 示例: 55 | - **操作系统**: Windows11 24H2 56 | - **Tabby**: v1.0.215 57 | value: | 58 | - 操作系统: 59 | - Tabby: 60 | render: markdown 61 | validations: 62 | required: true 63 | 64 | - type: checkboxes 65 | id: check_list 66 | attributes: 67 | label: 检查单 68 | description: 在提交前,请确认这些事项 69 | options: 70 | - label: 我已经查询了issue列表,我认为没有人反馈过类似问题 71 | required: true 72 | - label: 我已经将插件升级到最新版本 73 | required: true 74 | 75 | - type: textarea 76 | id: additional-info 77 | attributes: 78 | label: 其他补充信息 79 | description: (可不填)如有其他相关信息,请在此提供。 80 | placeholder: 在此提供补充信息... 81 | -------------------------------------------------------------------------------- /src/services/translateService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { TranslateService } from "tabby-core"; 3 | import yaml from 'js-yaml'; 4 | import yamlFileContent from '../static/i18n.yaml'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class AutoCompleteTranslateService { 8 | constructor( 9 | private translate: TranslateService, 10 | ) { 11 | this.initMyTranslate(); 12 | } 13 | initMyTranslate() { 14 | const data = yaml.load(yamlFileContent); 15 | function transform(obj) { 16 | const result = {}; 17 | 18 | function recurse(current, path) { 19 | if (typeof current === "object" && !Array.isArray(current)) { 20 | // 判断是不是语言节点(全是字符串) 21 | const keys = Object.keys(current); 22 | const allStrings = keys.every(k => typeof current[k] === "string"); 23 | if (allStrings) { 24 | keys.forEach(lang => { 25 | if (!result[lang]) result[lang] = {}; 26 | setByPath(result[lang], path, current[lang]); 27 | }); 28 | } else { 29 | keys.forEach(k => recurse(current[k], path.concat(k))); 30 | } 31 | } 32 | } 33 | 34 | function setByPath(obj, path, value) { 35 | let cur = obj; 36 | for (let i = 0; i < path.length - 1; i++) { 37 | if (!cur[path[i]]) cur[path[i]] = {}; 38 | cur = cur[path[i]]; 39 | } 40 | cur[path[path.length - 1]] = value; 41 | } 42 | 43 | recurse(obj, []); 44 | return result; 45 | } 46 | const result = transform(data); 47 | console.warn("setTrans", result); 48 | for (const langKey of Object.keys(result)) { 49 | console.warn("setTrans", langKey, result[langKey]) 50 | this.translate.setTranslation(langKey.replace("_", "-"), result[langKey], true); 51 | } 52 | } 53 | test() { 54 | 55 | } 56 | } -------------------------------------------------------------------------------- /.github/workflows/thank_feedback.yml: -------------------------------------------------------------------------------- 1 | name: Issue Commenter 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | permissions: 8 | issues: write 9 | pull-requests: write 10 | 11 | jobs: 12 | comment: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check author 16 | uses: actions/github-script@v7 17 | id: checkskip 18 | with: 19 | script: | 20 | const issueAuthor = context.payload.issue.user.login; 21 | const excludedUser = [context.repo.owner]; 22 | if (-1 !== excludedUser.indexOf(issueAuthor)) { 23 | core.setOutput('skip', 'true'); 24 | } else { 25 | core.setOutput('skip', 'false'); 26 | } 27 | 28 | - uses: gacts/is-stargazer@v1 29 | if: steps.checkskip.outputs.skip != 'true' 30 | id: check-star 31 | 32 | - name: Comment 1 33 | if: steps.check-star.outputs.is-stargazer == 'true' && steps.checkskip.outputs.skip != 'true' 34 | uses: actions/github-script@v7 35 | with: 36 | script: | 37 | github.rest.issues.createComment({ 38 | issue_number: context.issue.number, 39 | owner: context.repo.owner, 40 | repo: context.repo.repo, 41 | body: `Thank you for your feedback! We appreciate you taking the time to contribute. 42 | 43 | We usually respond within 14 days. To ensure you don't miss any updates, we recommend subscribing to email notifications or checking back regularly. 44 | 45 | Also, Thank you for starring our repository!🌟` 46 | }) 47 | 48 | - name: Comment 2 49 | if: steps.check-star.outputs.is-stargazer == 'false' && steps.checkskip.outputs.skip != 'true' 50 | uses: actions/github-script@v7 51 | with: 52 | script: | 53 | github.rest.issues.createComment({ 54 | issue_number: context.issue.number, 55 | owner: context.repo.owner, 56 | repo: context.repo.repo, 57 | body: `Thank you for your feedback. We appreciate you taking the time to contribute. 58 | 59 | We usually respond within 14 days. To ensure you don't miss any updates, we recommend subscribing to email notifications or checking back regularly.` 60 | }) 61 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release on Tag Push 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: 9 | contents: write 10 | id-token: write 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | # Checkout 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | 20 | # Install Node.js 21 | - name: Install Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 22 25 | registry-url: "https://registry.npmjs.org" 26 | 27 | - name: Setup yarn 28 | run: npm i -g yarn 29 | 30 | # Get yarn store directory 31 | - name: Get yarn store directory 32 | id: yarn-cache 33 | shell: bash 34 | run: | 35 | echo "STORE_PATH=$(yarn cache dir)" >> $GITHUB_OUTPUT 36 | 37 | # Setup yarn cache 38 | - name: Setup yarn cache 39 | uses: actions/cache@v3 40 | with: 41 | path: ${{ steps.yarn-cache.outputs.STORE_PATH }} 42 | key: ${{ runner.os }}-yarn-store-${{ hashFiles('**/yarn.lock') }} 43 | restore-keys: | 44 | ${{ runner.os }}-yarn-store- 45 | 46 | # Install dependencies 47 | - name: Install dependencies 48 | run: yarn install 49 | 50 | # Build for production 51 | - name: Build for production 52 | run: yarn build 53 | 54 | # Package as zip file 55 | - name: Zip it 56 | run: zip -r dist.zip dist/ 57 | 58 | # Get changelog 59 | - name: Set up Python 60 | uses: actions/setup-python@v2 61 | with: 62 | python-version: '3.8' 63 | 64 | - name: Get CHANGELOGS 65 | run: python ./scripts/release_note.py 66 | 67 | - name: Release 68 | uses: softprops/action-gh-release@v1 69 | with: 70 | body_path: ./result.txt 71 | prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') || contains(github.ref, 'dev') }} 72 | token: ${{ secrets.GITHUB_TOKEN }} 73 | files: dist.zip 74 | 75 | - uses: JS-DevTools/npm-publish@v3 76 | if: ${{ ! (contains(github.ref, 'beta') || contains(github.ref, 'alpha') || contains(github.ref, 'dev')) }} 77 | with: 78 | token: ${{ secrets.NPM_TOKEN }} 79 | provenance: true 80 | -------------------------------------------------------------------------------- /docs/INIT_en-US.md: -------------------------------------------------------------------------------- 1 | # Initial Usage Guide and Explanation of Plugin Implementation 2 | 3 | > Last updated: During v0.1.0 release. 4 | 5 | ## Explanation of Plugin Implementation 6 | 7 | ### 1. Determining Command Input Start 8 | 9 | The plugin uses a regular expression to match `CurrentDir.*\a` in the raw output to identify the end of the command input prompt. It cleans up any escape characters in this section, thus determining the prompt's end and the command's start. 10 | 11 | As a result, the `CurrentDir`-related prompt must appear at the end of the command input prompt. Otherwise, the plugin will not function correctly. 12 | 13 | ### 2. Recording Historical Commands 14 | 15 | > I'm tring to remove this prerequisite. 16 | > 17 | > Because the commands obtained this way will be the actual executed ones, they may contain unnecessary information (such as `[[ ]]` judgments, alias). 18 | 19 | The plugin records executed commands based on the shell's return of the command via `]2323;Command=$(cmd)\x07` after execution. This uses a custom escape sequence with no external references. If this conflicts with other existing implementations, please provide feedback. 20 | 21 | ## Configuration References for Specific Shells 22 | 23 | > The developer has only tested and developed this plugin on Bash under Ubuntu. Compatibility with other shells is not guaranteed. 24 | > 25 | > Theoretically, any shell that adheres to the rules described in the "Explanation of Plugin Implementation" section should work. However, variations between shells may cause issues. 26 | > 27 | > If you encounter issues with Bash, please report a bug. For issues with other shells, consider submitting a pull request (PR). 28 | 29 | #### Bash 30 | 31 | Refer directly to [tabby/wiki/Shell-working-directory-reporting#bash](https://github.com/Eugeny/tabby/wiki/Shell-working-directory-reporting#bash). 32 | 33 | ```bash 34 | export PS1="$PS1\[\e]1337;CurrentDir="'$(pwd)\a\]' 35 | ``` 36 | 37 | > To use history: 38 | > 39 | > ```bash 40 | > function preexec_invoke_exec() { 41 | > printf "\033]2323;Command=%s\007" "$1" 42 | > } 43 | > 44 | > trap 'preexec_invoke_exec "$BASH_COMMAND"' DEBUG 45 | > ``` 46 | 47 | #### Fish 48 | 49 | You need to modify the default `fish_prompt` function. 50 | 51 | You can find the original function code at `/usr/share/fish/functions/fish_prompt.fish @ line 4` (or by running `type fish_prompt` in the Fish shell). 52 | 53 | Add the following `echo` statement at the end of the function: 54 | 55 | ```fish 56 | echo -en "\e]1337;CurrentDir=$PWD\x7" 57 | ``` -------------------------------------------------------------------------------- /src/services/provider/aiContentProvider.ts: -------------------------------------------------------------------------------- 1 | import { OptionItem, EnvBasicInfo } from "../../api/pluginType"; 2 | import OpenAI from "openai"; 3 | import { BaseContentProvider, OptionItemResultWrap } from "./baseProvider"; 4 | import { MyLogger } from "services/myLogService"; 5 | import { ConfigService } from "tabby-core"; 6 | import { NgbModal, NgbModalRef } from "@ng-bootstrap/ng-bootstrap"; 7 | import { AutoCompleteAIDialogComponent } from "components/autoCompleteAIDialog"; 8 | 9 | 10 | 11 | // @Injectable({ 12 | // providedIn: 'root' 13 | // }) 14 | export class AIContentProvider extends BaseContentProvider { 15 | protected static providerTypeKey: string = "a"; 16 | openai: OpenAI; 17 | private recentEnvBasicInfo: EnvBasicInfo; 18 | private recentDialogRef: NgbModalRef; 19 | constructor( 20 | protected logger: MyLogger, 21 | protected configService: ConfigService, 22 | private ngbModal: NgbModal, 23 | ) { 24 | super(logger, configService); 25 | } 26 | 27 | // TODO: 需要提供给gpt具体的终端等信息 28 | async getQuickCmdList(inputCmd: string, cursorIndexAt: number, envBasicInfo: EnvBasicInfo): Promise { 29 | // xs 30 | // const quickCmdList = response.split("\n").map((cmd) => { 31 | // return { 32 | // label: cmd, 33 | // description: "Quick Command", 34 | // detail: "Quick Command" 35 | // }; 36 | // }); 37 | // return quickCmdList; 38 | this.recentEnvBasicInfo = envBasicInfo; 39 | return { 40 | optionItem: [ 41 | { 42 | name: "ask ai for help", 43 | content: inputCmd, 44 | desp: "", 45 | callback: this.userAskAiCallback.bind(this, inputCmd, this.recentEnvBasicInfo), 46 | type: AIContentProvider.providerTypeKey 47 | } 48 | ], 49 | envBasicInfo: envBasicInfo, 50 | type: AIContentProvider.providerTypeKey 51 | }; 52 | } 53 | async userAskAiCallback(inputCmd: string, envBasicInfo: EnvBasicInfo) { 54 | if (this.recentDialogRef) { 55 | this.recentDialogRef.close(); 56 | } 57 | this.logger.log("inputCmd", inputCmd); 58 | this.recentDialogRef = this.ngbModal.open(AutoCompleteAIDialogComponent); 59 | // 处理 60 | 61 | // 返回结果 62 | // this.recentDialogRef.componentInstance.inputCmd = inputCmd; 63 | 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/buttonProvider.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; 3 | import { AutoCompleteAIDialogComponent } from 'components/autoCompleteAIDialog'; 4 | import { AppService, ConfigService, HostWindowService, HotkeysService, ToolbarButton, ToolbarButtonProvider } from 'tabby-core'; 5 | import { inputInitScripts, sendInput } from 'utils/commonUtils'; 6 | import { Subject } from "rxjs"; 7 | import { MySignalService } from 'services/signalService'; 8 | 9 | @Injectable() 10 | export class ButtonProvider extends ToolbarButtonProvider { 11 | private recentDialogRef: any; 12 | 13 | private currentStatus: boolean; 14 | // private menuStatusNS: Subject = new Subject(); 15 | // public menuStatus$ = this.menuStatusNS.asObservable(); 16 | constructor ( 17 | hotkeys: HotkeysService, 18 | private hostWnd: HostWindowService, 19 | private app: AppService, 20 | private ngbModal: NgbModal, 21 | private signalService: MySignalService, 22 | private configService: ConfigService, 23 | ) { 24 | super(); 25 | this.currentStatus = configService.store?.ogAutoCompletePlugin?.enableCompleteWithCompleteStart; 26 | // 仅注册在 ToolbarButtonProvider 中有效? 27 | hotkeys.hotkey$.subscribe(async (hotkey) => { 28 | if (hotkey === 'ogautocomplete_dev') { 29 | this.openDevTools(); 30 | } else if (hotkey === 'ogautocomplete_init_scripts') { 31 | inputInitScripts(this.app); 32 | } else if (hotkey === 'ogautocomplete_ask_ai') { 33 | if (this.recentDialogRef) { 34 | this.recentDialogRef.close(); 35 | } 36 | this.recentDialogRef = this.ngbModal.open(AutoCompleteAIDialogComponent); 37 | } else if (hotkey === "ogautocomplete_stop") { 38 | signalService.changeMenuStatus(); 39 | } else if (hotkey === "ogautocomplete_hint_now") { 40 | signalService.hintNow(); 41 | } 42 | }); 43 | } 44 | 45 | private openDevTools() { 46 | this.hostWnd.openDevTools(); 47 | } 48 | 49 | provide(): ToolbarButton[] { 50 | const that = this; 51 | return [{ 52 | icon: require('./icons/bird.svg'), 53 | weight: 5, 54 | title: 'Start or stop quick-cmd-hint', 55 | touchBarNSImage: 'NSTouchBarComposeTemplate', 56 | click: async () => { 57 | that.signalService.changeMenuStatus(); 58 | } 59 | }]; 60 | } 61 | 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * tabby-quick-cmds-hint: A simple complete hint plugin for tabby. 3 | * Copyright (C) 2025 OpaqueGlass and other developers 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | import { NgModule } from '@angular/core' 19 | import { CommonModule } from '@angular/common' 20 | import { FormsModule } from '@angular/forms' 21 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap' 22 | import { ConfigProvider, HotkeyProvider, ToolbarButtonProvider } from 'tabby-core' 23 | import TabbyCoreModule from 'tabby-core' 24 | import { SettingsTabProvider } from 'tabby-settings' 25 | import { AutoCompleteConfigProvider } from './configProvider' 26 | import { AutoCompleteSettingsTabProvider } from './settingsTabProvider' 27 | import { AutoCompleteSettingsTabComponent } from 'components/autoCompleteSettingsTab' 28 | import { TerminalDecorator } from 'tabby-terminal' 29 | import { AutoCompleteTerminalDecorator } from 'terminalDecorator' 30 | import { AutoCompleteHintMenuComponent } from 'components/autoCompleteHintMenu' 31 | import { AddMenuService } from 'services/menuService' 32 | import { AutoCompleteHotkeyProvider } from 'hotkeyProvider' 33 | import { ButtonProvider } from 'buttonProvider' 34 | import { AutoCompleteAIDialogComponent } from 'components/autoCompleteAIDialog' 35 | import { AutoCompleteTranslateService } from 'services/translateService' 36 | 37 | 38 | 39 | 40 | @NgModule({ 41 | imports: [ 42 | NgbModule, 43 | CommonModule, 44 | FormsModule, 45 | TabbyCoreModule, 46 | ], 47 | providers: [ 48 | { provide: ConfigProvider, useClass: AutoCompleteConfigProvider, multi: true }, 49 | { provide: SettingsTabProvider, useClass: AutoCompleteSettingsTabProvider, multi: true }, 50 | { provide: TerminalDecorator, useClass: AutoCompleteTerminalDecorator, multi: true }, 51 | { provide: HotkeyProvider, useClass: AutoCompleteHotkeyProvider, multi: true }, 52 | { provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true }, 53 | AddMenuService, 54 | AutoCompleteTranslateService, 55 | ], 56 | declarations: [ 57 | AutoCompleteSettingsTabComponent, 58 | AutoCompleteHintMenuComponent, 59 | AutoCompleteAIDialogComponent, 60 | ], 61 | }) 62 | export default class AutoCompleteModule { } 63 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report unexpected behavior, errors, or defects 3 | assignees: [] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for submitting a bug report! Please fill out the following details as thoroughly as possible to help us understand and resolve the issue. 9 | 10 | - type: textarea 11 | id: problem-description 12 | attributes: 13 | label: Problem Description 14 | description: Please describe the problem in as much detail as possible. 15 | placeholder: Describe the problem here... when pasting error logs, please use markdown code blocks. 16 | validations: 17 | required: true 18 | 19 | - type: markdown 20 | attributes: 21 | value: | 22 | If the problem is not reproducible consistently, please set "Settings - Command Prompt - Debug - Show Detailed Logs and Debug Information" to 0. Also, please monitor the Console logs in the Developer Tools (Settings - Apps - Enable Developer Tools), and upload a screenshot of the console log when you encounter the issue. 23 | 24 | - type: textarea 25 | id: reproduce-steps 26 | attributes: 27 | label: Reproduction Steps 28 | description: Describe the steps or settings needed to reproduce the issue. If the problem is not consistently reproducible, please note the frequency and upload any error messages. 29 | placeholder: | 30 | 1. Step one 31 | 2. Step two 32 | 3. Step three 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | id: screenshots-or-recordings 38 | attributes: 39 | label: Screenshots or Recordings 40 | description: (Optional) Please upload screenshots or recordings to demonstrate the issue and reproduction steps. 41 | placeholder: Upload screenshots or recordings here... 42 | 43 | - type: textarea 44 | id: expected-behavior 45 | attributes: 46 | label: Expected Behavior 47 | description: (Optional) Describe what you expect the plugin to do or how it should behave. 48 | placeholder: Describe the expected behavior here... 49 | 50 | - type: textarea 51 | attributes: 52 | label: Device and System Information 53 | description: | 54 | Example: 55 | - **Operating System**: Windows 11 24H2 56 | - **Tabby**: v1.0.215 57 | value: | 58 | - Operating System: 59 | - Tabby: 60 | render: markdown 61 | validations: 62 | required: true 63 | 64 | - type: checkboxes 65 | id: check_list 66 | attributes: 67 | label: Check list 68 | description: Please confirm the following things before raising an issue. 69 | options: 70 | - label: I have checked the issue list and believe no one has reported a similar issue 71 | required: true 72 | - label: I have updated the plugin to the latest version 73 | required: true 74 | 75 | - type: textarea 76 | id: additional-info 77 | attributes: 78 | label: Additional Information 79 | description: (Optional) If you have any additional related information, please provide it here. 80 | placeholder: Provide additional information here... 81 | -------------------------------------------------------------------------------- /src/components/autoCompleteHintMenu.scss: -------------------------------------------------------------------------------- 1 | .og-autocomplete-list { 2 | background-color: var(--theme-bg-more); 3 | border: var(--bs-list-group-border-width) solid var(--bs-list-group-border-color); 4 | color: var(--bs-list-group-color); 5 | .og-autocomplete-item-list { 6 | overflow-y: auto; 7 | max-height: 250px; 8 | } 9 | 10 | & .og-autocomplete-item.og-tac-selected, 11 | & .og-autocomplete-item:hover { 12 | background-color: var(--bs-list-group-active-bg); 13 | color: var(--bs-list-group-active-color); 14 | border-color: var(--bs-list-group-active-border-color); 15 | } 16 | } 17 | .og-autocomplete-main-text { 18 | line-height: 1em; 19 | height: 1.5em; 20 | overflow: auto; 21 | &::-webkit-scrollbar { 22 | display: none; 23 | } 24 | } 25 | 26 | .og-autocomplete-list { 27 | position: absolute; 28 | z-index: 20; 29 | max-width: 350px; 30 | min-width: 350px; 31 | opacity: 0; 32 | pointer-events: none; 33 | //display: none; 34 | border-radius: 7px; 35 | overflow: hidden; 36 | } 37 | 38 | .og-autocomplete-item { 39 | /* padding: 2px; */ 40 | margin-bottom: 1px; 41 | cursor: pointer; 42 | width: auto; 43 | display: flex; 44 | flex-direction: row; 45 | max-width: 35vw; 46 | } 47 | 48 | .og-autocomplete-footer { 49 | padding: 8px; 50 | border-top: 1px solid #ddd; 51 | } 52 | 53 | .og-autocomplete-item { 54 | display: flex; 55 | align-items: stretch; /* 子项等高 */ 56 | max-width: 35vw; 57 | margin-bottom: 1px; 58 | cursor: pointer; 59 | } 60 | 61 | /* ac-type 占满高度,最大宽度 1em */ 62 | .og-autocomplete-item .og-ac-type { 63 | flex: 0 0 1em; 64 | max-width: 1em; 65 | display: flex; 66 | align-items: center; 67 | justify-content: center; 68 | padding: 0 2px; 69 | box-sizing: border-box; 70 | } 71 | 72 | /* 其余内容占用剩余空间 */ 73 | .og-autocomplete-item .og-ac-name, 74 | .og-autocomplete-item .og-ac-desp { 75 | flex: 1 1 auto; 76 | min-width: 0; /* 防止 flex 子项溢出 */ 77 | padding: 4px; 78 | overflow: hidden; 79 | display: -webkit-box; 80 | -webkit-box-orient: vertical; 81 | -webkit-line-clamp: 2; 82 | line-clamp: 2; 83 | text-overflow: ellipsis; 84 | } 85 | 86 | .og-autocomplete-item .og-ac-name { 87 | word-break: break-all; 88 | } 89 | 90 | .og-autocomplete-item .og-ac-desp { 91 | color: var(--bs-gray); 92 | font-size: 0.9em; 93 | text-align: right; 94 | } 95 | 96 | .og-ac-type { 97 | background-color: #b235eb; 98 | color: #fff; 99 | &[data-cmd-type='q'] { 100 | background-color: #d4d424; 101 | color: #0c0333; 102 | } 103 | &[data-cmd-type='h'] { 104 | background-color: green; 105 | } 106 | &[data-cmd-type='a'] { 107 | background-color: rgba(153, 2, 2, 0.527); 108 | } 109 | } 110 | /* var(--bs-list-group-active-border-color) 111 | var(--bs-list-group-active-color) var(--bs-list-group-active-bg) 112 | 113 | var(--bs-list-group-color) 114 | 115 | var(--bs-list-group-bg) 116 | 117 | var(--bs-list-group-border-width) solid var(--bs-list-group-border-color) 118 | */ 119 | // .og-autocomplete-list[theme-mode='dark'] { 120 | // background-color: rgb(8, 2, 36); 121 | // border: 1px solid #6f6f6f; 122 | // color: #565555; 123 | 124 | // & .og-autocomplete-item.og-tac-selected { 125 | // background-color: #7b14aa; 126 | // } 127 | 128 | // & .og-autocomplete-item:hover { 129 | // background-color: #7e14afa9; 130 | 131 | // } 132 | // & .og-autocomplete-footer { 133 | // background-color: #0c0333; 134 | // } 135 | // & .og-ac-name { 136 | // color: #ddd; 137 | // } 138 | // } 139 | // .og-autocomplete-list[theme-mode='light'] { 140 | // background-color: hsl(0, 0%, 93.2%); 141 | // // border: 1px solid var(--bs-list-group-border-color); 142 | // color: rgb(42, 44, 51); 143 | 144 | // & .og-autocomplete-item.og-tac-selected, 145 | // & .og-autocomplete-item:hover { 146 | // background-color: rgb(184, 199, 251); 147 | // color: rgb(8, 36, 137); 148 | // border-color: rgb(13, 110, 253); 149 | // } 150 | // } 151 | -------------------------------------------------------------------------------- /src/terminalDecorator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * tabby-quick-cmds-hint: A simple complete hint plugin for tabby. 3 | * Copyright (C) 2025 OpaqueGlass and other developers 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | import { Injectable } from '@angular/core' 19 | import { bufferTime, Subscription } from 'rxjs' 20 | import { AddMenuService } from 'services/menuService'; 21 | import { SimpleManager } from 'services/manager/simpleContentManager'; 22 | import { MyLogger } from 'services/myLogService'; 23 | import { AppService, ConfigService, NotificationsService } from 'tabby-core'; 24 | import { TerminalDecorator, BaseTerminalTabComponent, BaseTerminalProfile } from 'tabby-terminal' 25 | import { cleanTerminalText, generateUUID, inputInitScripts, sleep } from 'utils/commonUtils'; 26 | import { MySignalService } from 'services/signalService'; 27 | 28 | 29 | @Injectable() 30 | export class AutoCompleteTerminalDecorator extends TerminalDecorator { 31 | hintMenu: any; 32 | constructor ( 33 | private addMenuService: AddMenuService, 34 | private configService: ConfigService, 35 | private logger: MyLogger, 36 | private app: AppService, 37 | private notification: NotificationsService, 38 | private signalService: MySignalService 39 | ) { 40 | super() 41 | addMenuService.insertComponent(); 42 | } 43 | 44 | attach (tab: BaseTerminalTabComponent): void { 45 | // TODO: 这里最好是区分一下终端,给个实例什么的,另外,可能可以通过currentPwd判断是否 46 | this.logger.log("tab内容判断", tab); 47 | this.logger.log("tab内容判断", tab.element.nativeElement); 48 | // 连接时提示使用init命令 49 | const sessionChangedSubscription = tab.sessionChanged$.subscribe(session => { 50 | this.logger.log("tab内容判断sessionChanged", tab.session?.supportsWorkingDirectory(), tab.title); 51 | this.logger.log("tab内容判断sessionChanged", session?.supportsWorkingDirectory()); 52 | // 这个changed涉及重新连接什么的,所以,如果为false时没有,如果为session undefined就是没连上 53 | // 可以考虑给上自动加入脚本,但windows就hh 54 | if (session?.supportsWorkingDirectory()) { 55 | // 如果已经有了,就不需要操作,隐藏标签? 56 | } else if (session && !session?.supportsWorkingDirectory()) { 57 | // 提示添加 58 | // 或者自动加入 59 | if (this.configService.store.ogAutoCompletePlugin.autoInit) { 60 | setTimeout(()=>{inputInitScripts(this.app);}, 300); 61 | } 62 | } 63 | }); 64 | super.subscribeUntilDetached(tab, sessionChangedSubscription); 65 | // END 66 | 67 | tab.addEventListenerUntilDestroyed(tab.element.nativeElement.querySelector(".xterm-helper-textarea"), 'focusout', async () => { 68 | // 这里需要延迟,否则无法点击上屏 69 | await sleep(200); 70 | if (this.configService.store.ogAutoCompletePlugin.debugLevel > 1) { 71 | this.addMenuService.hideMenu(); 72 | } 73 | this.logger.log("focus out"); 74 | }, true); 75 | 76 | const mangager = new SimpleManager(tab, this.logger, this.addMenuService, this.configService, this.notification, this.signalService); 77 | if (mangager.handleInput) { 78 | super.subscribeUntilDetached(tab, tab.input$.pipe(bufferTime(300)).subscribe(mangager.handleInput)); 79 | } 80 | if (mangager.handleOutput) { 81 | super.subscribeUntilDetached(tab, tab.output$.pipe(bufferTime(300)).subscribe(mangager.handleOutput)); 82 | } 83 | super.subscribeUntilDetached(tab, tab.sessionChanged$.subscribe(mangager.handleSessionChanged)); 84 | const destroySub = tab.destroyed$.subscribe(()=>{ 85 | mangager.destroy(); 86 | destroySub.unsubscribe(); 87 | }); 88 | // ???? 89 | // tab.sessionChanged$.subscribe(session => { 90 | // if (session) { 91 | // this.attachToSession(session) 92 | // } 93 | // }) 94 | // if (tab.session) { 95 | // this.attachToSession(tab.session) 96 | // } 97 | } 98 | 99 | 100 | private processBackspaces(input: string) { 101 | let result = []; // 用数组来存储最终结果,处理效率更高 102 | 103 | for (let char of input) { 104 | if (char === '\b' || char === '\u007F' || char === "\x07") { 105 | // 遇到退格字符,删除前一个字符(如果有) 106 | if (result.length > 0) { 107 | result.pop(); 108 | } 109 | } else if (char === "\x15" || char === "\u0015") { 110 | result = []; 111 | } else { 112 | // 非退格字符,直接加入结果 113 | result.push(char); 114 | } 115 | } 116 | 117 | // 将数组转换为字符串并返回 118 | return result.join(''); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/components/autoCompleteSettingsTab.pug: -------------------------------------------------------------------------------- 1 | h3(translate='settings.title') 2 | 3 | .alert.alert-info.d-flex.align-items-center 4 | .me-auto 5 | div(translate="intro.tested_scenarios") 6 | strong(translate='intro.ubuntu_centos') 7 | span(translate='intro.or') 8 | strong(translate='intro.centos') 9 | span(translate='intro.via_ssh') 10 | strong(translate='intro.ssh') 11 | span(translate='intro.using_bash') 12 | strong(translate='intro.bash') 13 | span(translate='intro.on_windows') 14 | div 15 | p(translate='intro.usage_instruction') 16 | code(translate='intro.tab_key') 17 | span(translate='intro.tab_action') 18 | code(translate='intro.enter_key') 19 | span(translate='intro.enter_action') 20 | 21 | .alert.alert-info.d-flex.align-items-center(*ngIf='config.store["qc"] == undefined || config.store["qc"] == {}') 22 | .me-auto 23 | div(translate='intro.plugin_requirement') 24 | 25 | h3(translate='settings.general') 26 | 27 | .form-line 28 | .header 29 | .title(translate='settings.use_regexp_prompt') 30 | .description(translate='settings.use_regexp_prompt_desc') 31 | toggle( 32 | [(ngModel)]='config.store.ogAutoCompletePlugin.useRegExpDetectPrompt', 33 | (ngModelChange)='config.save()', 34 | ) 35 | 36 | .form-line 37 | .header 38 | .title(translate='settings.auto_init') 39 | .description(translate='settings.auto_init_desc') 40 | toggle( 41 | [(ngModel)]='config.store.ogAutoCompletePlugin.autoInit', 42 | (ngModelChange)='config.save()', 43 | ) 44 | 45 | .form-line 46 | .header 47 | .title(translate='settings.enable_on_startup') 48 | .description(translate='settings.enable_on_startup_desc') 49 | toggle( 50 | [(ngModel)]='config.store.ogAutoCompletePlugin.enableCompleteWithCompleteStart', 51 | (ngModelChange)='config.save()', 52 | ) 53 | 54 | .form-line 55 | .header 56 | .title(translate='settings.menu_show_item_max') 57 | .description(translate='settings.menu_show_item_max_desc') 58 | input.form-control( 59 | type='number', 60 | min=2, 61 | [(ngModel)]='config.store.ogAutoCompletePlugin.menuShowItemMaxCount', 62 | (ngModelChange)='config.save()', 63 | ) 64 | 65 | 66 | h3(translate='ai.title') 67 | 68 | .form-line 69 | .header 70 | .title(translate='ai.openai_base_url') 71 | .description(translate='ai.openai_base_url') 72 | input.form-control( 73 | type='text', 74 | [(ngModel)]='config.store.ogAutoCompletePlugin.ai.openAIBaseUrl', 75 | (ngModelChange)='config.save()', 76 | ) 77 | 78 | .form-line 79 | .header 80 | .title(translate='ai.openai_key') 81 | .description(translate='ai.openai_key_desc') 82 | input.form-control( 83 | type='password', 84 | [(ngModel)]='config.store.ogAutoCompletePlugin.ai.openAIKey', 85 | (ngModelChange)='config.save()', 86 | ) 87 | 88 | 89 | .form-line 90 | .header 91 | .title(translate='ai.openai_model') 92 | .description(translate='ai.openai_model') 93 | input.form-control( 94 | type='text', 95 | [(ngModel)]='config.store.ogAutoCompletePlugin.ai.openAIModel', 96 | (ngModelChange)='config.save()', 97 | ) 98 | 99 | 100 | h3(translate='appearance.title') 101 | 102 | .form-line 103 | .header 104 | .title(translate='appearance.font_size') 105 | input.form-control( 106 | type='number', 107 | max=90, 108 | min=2, 109 | [(ngModel)]='config.store.ogAutoCompletePlugin.appearance.fontSize', 110 | (ngModelChange)='config.save()', 111 | ) 112 | 113 | 114 | h3(translate='history.title') 115 | 116 | .form-line(translate='history.manage_desc') 117 | 118 | .form-line 119 | .header 120 | .title(translate='history.enable') 121 | .description(translate='history.enable_desc') 122 | toggle( 123 | [(ngModel)]='config.store.ogAutoCompletePlugin.history.enable', 124 | (ngModelChange)='config.save()', 125 | ) 126 | 127 | .form-line 128 | .header 129 | .title(translate='history.count_regexp') 130 | .description(translate='history.count_regexp_desc') 131 | toggle( 132 | [(ngModel)]='config.store.ogAutoCompletePlugin.history.countInRegExp', 133 | (ngModelChange)='config.save()', 134 | ) 135 | 136 | h3(translate='arguments.title') 137 | 138 | .form-line(translate='arguments.manage_desc') 139 | 140 | .form-line 141 | .header 142 | .title(translate='arguments.enable') 143 | .description(translate='arguments.enable_desc') 144 | toggle( 145 | [(ngModel)]='config.store.ogAutoCompletePlugin.arguments.enable', 146 | (ngModelChange)='config.save()', 147 | ) 148 | 149 | h3(translate='debug.title') 150 | 151 | .form-line 152 | .header 153 | .title(translate='debug.enable_debug') 154 | .description(translate='debug.log_level_desc') 155 | input.form-control( 156 | type='number', 157 | max=4, 158 | min=-2, 159 | [(ngModel)]='config.store.ogAutoCompletePlugin.debugLevel', 160 | (ngModelChange)='config.save()', 161 | ) 162 | 163 | 164 | .form-line 165 | .header 166 | .title(translate='debug.custom_regexp') 167 | .description(translate='debug.custom_regexp_desc') 168 | textarea.form-control( 169 | rows='2', 170 | [(ngModel)]='config.store.ogAutoCompletePlugin.customRegExp', 171 | (ngModelChange)='config.save()' 172 | ) 173 | 174 | .footer-of-setting 175 | div() 176 | span(translate='support.give_star_prefix') 177 | a.og-tac-a((click)='openGithub()',translate='support.give_star_link') 178 | span(translate='support.give_star_suffix') 179 | div() 180 | span(translate='support.issue_prefix') 181 | a.og-tac-a((click)='openNewIssue()',translate='support.issue_link') 182 | span(translate='support.issue_suffix') 183 | -------------------------------------------------------------------------------- /src/utils/commonUtils.ts: -------------------------------------------------------------------------------- 1 | import { Terminal } from "@xterm/xterm"; 2 | import { clear } from "console"; 3 | import stripAnsi from "strip-ansi"; 4 | import { AppService, BaseTabComponent, SplitTabComponent } from "tabby-core"; 5 | import { BaseTerminalProfile, BaseTerminalTabComponent } from "tabby-terminal"; 6 | 7 | export function isValidStr(input: string) { 8 | return input !== undefined && input !== null && input !== ''; 9 | } 10 | 11 | export function cleanTerminalText(input: string) { 12 | const cleanNotVisibleExp = /[\x1b\x07]\[(?:[0-9]{1,2}(?:;[0-9]{1,2})*)?[a-zA-Z]|[\x1b\x07]\].*?\x07|[\x1b\x07]\[\?.*?[hl]|[\x1b\x07]\[>4;m|[\x1b\x07]\>|\x1B\(B|\x1b\[\>\d+;\d+m/g; 13 | // fish (B 14 | 15 | let result = input.replace(cleanNotVisibleExp, ''); 16 | return result; 17 | } 18 | 19 | 20 | export function sleep(ms: number) { 21 | return new Promise(resolve => setTimeout(resolve, ms)); 22 | } 23 | 24 | /** 25 | * 26 | * @param tab terminal标签页 27 | * @param cmd 待输入的命令 请注意 \\s将被认为延迟输入,\\x将被认为是16进制字符 28 | * @param appendCR 是否在命令后追加回车 29 | * @param singleLine 是否忽略换行,而一次只输入一行 30 | * @param clearFirst 是否在输入前清空终端 31 | * @param refocus 是否在输入后重新聚焦终端 32 | */ 33 | export async function sendInput({tab, cmd, appendCR = false, 34 | singleLine = false, clearFirst = false, refocus = true}: { 35 | tab: BaseTabComponent | SplitTabComponent, 36 | cmd: string, 37 | appendCR?: boolean, 38 | singleLine?: boolean, 39 | clearFirst?: boolean, 40 | refocus?: boolean 41 | }) { 42 | if (tab instanceof SplitTabComponent) { 43 | sendInput({ 44 | tab: (tab as SplitTabComponent).getFocusedTab(), 45 | cmd: cmd, 46 | appendCR: appendCR, 47 | singleLine: singleLine, 48 | clearFirst: clearFirst, 49 | refocus: refocus 50 | }); 51 | } 52 | if (tab instanceof BaseTerminalTabComponent) { 53 | let currentTab = tab as BaseTerminalTabComponent; 54 | console.debug("tab", currentTab); 55 | console.debug("Sending " + cmd); 56 | 57 | let cmds = cmd.split(/(?:\r\n|\r|\n)/) 58 | 59 | if (clearFirst) { 60 | const endKeyEvent = new KeyboardEvent('keydown', { 61 | key: 'End', 62 | code: 'End', 63 | keyCode: 35, 64 | which: 35, 65 | bubbles: true, 66 | cancelable: true 67 | }); 68 | window.document.dispatchEvent(endKeyEvent); 69 | currentTab.sendInput("\u0015"); //Ctrl + E 70 | // currentTab.sendInput("\u0003"); //Ctrl + c // have stability issue 71 | } 72 | 73 | for (let i = 0; i < cmds.length; i++) { 74 | let cmd = cmds[i]; 75 | console.debug("Sending " + cmd); 76 | 77 | 78 | if (cmd.startsWith('\\s')) { 79 | cmd = cmd.replace('\\s', ''); 80 | let sleepTime = parseInt(cmd); 81 | 82 | await sleep(sleepTime); 83 | 84 | console.debug('sleep time: ' + sleepTime); 85 | continue; 86 | } 87 | 88 | if (cmd.startsWith('\\x')) { 89 | cmd = cmd.replace(/\\x([0-9a-f]{2})/ig, function (_, pair) { 90 | return String.fromCharCode(parseInt(pair, 16)); 91 | }); 92 | } 93 | if (i != cmds.length - 1) { 94 | cmd = cmd + "\n"; 95 | } 96 | if (appendCR) { 97 | cmd = cmd + "\n"; 98 | } 99 | currentTab.sendInput(cmd); 100 | // 点击会导致失去聚焦,可能这里也需要携带参数 101 | if (refocus) { 102 | currentTab.frontend.focus(); 103 | } 104 | } 105 | 106 | } 107 | } 108 | 109 | export function resetAndClearXterm(xterm: Terminal) { 110 | console.log("清屏"); 111 | xterm.clear(); 112 | xterm.write('\x1b[2J'); 113 | } 114 | 115 | export function cleanTextByNewXterm(input: string) { 116 | // 15ms 117 | return new Promise((resolve) => { 118 | let term = new Terminal(); 119 | let dom = null; 120 | const existDom = document.getElementById("ogmytempxterm"); 121 | if (existDom) { 122 | dom = existDom; 123 | } else { 124 | dom = document.createElement("div"); 125 | dom.classList.add("ogmytempxterm"); 126 | dom.setAttribute("id", "ogmytempxterm"); 127 | window.document.body.appendChild(dom); 128 | } 129 | term.open(dom); 130 | term.write(input, ()=>{ 131 | term.selectAll(); 132 | const result = term.getSelection(); 133 | term.dispose(); 134 | resolve(trimLineTextFromXterm(result)); 135 | }); 136 | }); 137 | } 138 | 139 | export function inputInitScripts(app: AppService) { 140 | const scripts = ` if [[ -z "$DIR_REPORTING_ENABLED" ]]; then export DIR_REPORTING_ENABLED=1; if [[ $SHELL == */bash ]]; then export PS1="$PS1\\[\\e]1337;CurrentDir=$(pwd)\a\]"; elif [[ $SHELL == */zsh ]]; then precmd() { echo -n "\\x1b]1337;CurrentDir=$(pwd)\\x07"; }; elif [[ $SHELL == */fish ]]; then fish -c 'function __tabby_working_directory_reporting --on-event fish_prompt; echo -en "\e]1337;CurrentDir=$PWD\\x7"; end'; else echo "Unsupported shell"; fi; fi`; 141 | // 需要转义 ", /等,另外,前导空格可以避免写入history 142 | const bashCommand = ` if [[ -z "$DIR_REPORTING_ENABLED" ]]; then export DIR_REPORTING_ENABLED=1; if [[ $SHELL == */bash ]]; then export PS1="$PS1\\[\\e]1337;CurrentDir=$(pwd)\\a\\]"; elif [[ $SHELL == */zsh ]]; then precmd() { echo -n "\\x1b]1337;CurrentDir=$(pwd)\\x07"; }; elif [[ $SHELL == */fish ]]; then fish -c 'function __tabby_working_directory_reporting --on-event fish_prompt; echo -en "\\e]1337;CurrentDir=$PWD\\x7"; end'; else echo "Unsupported shell"; fi; fi`; 143 | // Add history support 144 | const commandV2 = ` if [[ -z "$DIR_REPORTING_ENABLED" ]]; then export DIR_REPORTING_ENABLED=1; if [[ $SHELL == */bash ]]; then export PS1="$PS1\\[\\e]1337;CurrentDir=$(pwd)\\a\\]"; function preexec_invoke_exec() { printf "\\033]2323;Command=%s\\007" "$1"; }; trap 'preexec_invoke_exec "$BASH_COMMAND"' DEBUG; elif [[ $SHELL == */zsh ]]; then precmd() { echo -n "\\x1b]1337;CurrentDir=$(pwd)\\x07"; }; elif [[ $SHELL == */fish ]]; then fish -c 'function __tabby_working_directory_reporting --on-event fish_prompt; echo -en "\\e]1337;CurrentDir=$PWD\\x7"; end'; else echo "Unsupported shell"; fi; fi`; 145 | 146 | sendInput({ 147 | "tab": app.activeTab, 148 | "cmd": commandV2, 149 | "appendCR": true, 150 | }); 151 | 152 | } 153 | 154 | export function generateUUID() { 155 | let uuid = ''; 156 | let i = 0; 157 | let random = 0; 158 | 159 | for (i = 0; i < 36; i++) { 160 | if (i === 8 || i === 13 || i === 18 || i === 23) { 161 | uuid += '-'; 162 | } else if (i === 14) { 163 | uuid += '4'; 164 | } else { 165 | random = Math.random() * 16 | 0; 166 | if (i === 19) { 167 | random = (random & 0x3) | 0x8; 168 | } 169 | uuid += (random).toString(16); 170 | } 171 | } 172 | 173 | return uuid; 174 | } 175 | 176 | export function trimLineTextFromXterm(input: string) { 177 | return input.replace(/^\s*[\r\n]/gm, ""); 178 | } 179 | 180 | export function simpleHash(str: string) { 181 | let hash = 0; 182 | for (let i = 0; i < str.length; i++) { 183 | const char = str.charCodeAt(i); 184 | hash = (hash << 5) - hash + char; 185 | hash = hash & hash; 186 | } 187 | return hash.toString(); 188 | } -------------------------------------------------------------------------------- /src/services/provider/argumentsContentProvider.ts: -------------------------------------------------------------------------------- 1 | import { OptionItem, EnvBasicInfo } from "../../api/pluginType"; 2 | import Fuse from 'fuse.js'; 3 | import { BaseContentProvider, OptionItemResultWrap } from "./baseProvider"; 4 | import { MyLogger } from "services/myLogService"; 5 | import { Injectable } from "@angular/core"; 6 | import { ConfigService, TranslateService } from "tabby-core"; 7 | import { isValidStr } from "utils/commonUtils"; 8 | import * as yaml from 'js-yaml'; 9 | import yamlFileContent from '../../static/autoComplete.yaml'; 10 | 11 | /** 12 | * 命令参数自动补全提供器 13 | * 14 | * 字段说明: 15 | * - name: 显示名称,支持占位符(如 ),会在 UI 中显示给用户 16 | * - content: 实际输入内容,用户选择该项后实际输入到终端的内容 17 | * - despLangKey: 可选,描述的翻译键,格式为 i18n.yaml 中的路径 18 | * 19 | * 示例: 20 | * - name: "add " // 显示:add 21 | * content: "add" // 输入:add(不包含占位符) 22 | * despLangKey: "arguments_complete.git.add" // 翻译:将文件添加到暂存区 23 | * 24 | * 如果不提供 despLangKey,将使用 name 作为描述显示 25 | */ 26 | 27 | interface CommandExample { 28 | text: string; 29 | desc: string; 30 | } 31 | 32 | /** 33 | * 命令参数配置接口 34 | */ 35 | interface CommandArgument { 36 | /** 显示名称,支持占位符如 ,在UI中显示给用户 */ 37 | name: string; 38 | /** 实际输入内容,用户选择后输入到终端的内容 */ 39 | content: string; 40 | /** 可选,描述的翻译键,如 "arguments_complete.git.add" */ 41 | despLangKey?: string; 42 | } 43 | 44 | /** 45 | * 命令配置接口 46 | */ 47 | interface CommandConfig { 48 | /** 命令名称,如 "git", "docker" */ 49 | command: string; 50 | /** 参数列表 */ 51 | arguments: CommandArgument[]; 52 | } 53 | 54 | interface AutoCompleteConfig { 55 | commands: CommandConfig[]; 56 | } 57 | 58 | 59 | @Injectable({ 60 | providedIn: 'root' 61 | }) 62 | export class ArgumentsContentProvider extends BaseContentProvider { 63 | protected static providerTypeKey: string = "a"; 64 | private yamlConfig: AutoCompleteConfig | null = null; 65 | private fuseOptions = { 66 | keys: ['name', 'content', 'desp'], 67 | threshold: 0.3, 68 | includeScore: true, 69 | includeMatches: true 70 | }; 71 | 72 | constructor( 73 | protected logger: MyLogger, 74 | protected configService: ConfigService, 75 | protected translateService: TranslateService, 76 | ) { 77 | super(logger, configService); 78 | this.loadYamlConfig(); 79 | } 80 | 81 | /** 82 | * 加载YAML配置文件 83 | */ 84 | private loadYamlConfig(): void { 85 | try { 86 | this.yamlConfig = yaml.load(yamlFileContent) as AutoCompleteConfig; 87 | this.logger.log("YAML配置加载成功", this.yamlConfig); 88 | } catch (error) { 89 | this.logger.log("YAML配置加载失败", error); 90 | this.yamlConfig = null; 91 | } 92 | } 93 | 94 | /** 95 | * 根据命令名称查找对应的参数配置 96 | */ 97 | private findCommandConfig(commandName: string): CommandConfig | null { 98 | if (!this.yamlConfig || !this.yamlConfig.commands) { 99 | return null; 100 | } 101 | return this.yamlConfig.commands.find(cmd => cmd.command === commandName) || null; 102 | } 103 | 104 | /** 105 | * 将命令参数转换为OptionItem数组 106 | */ 107 | private convertArgumentsToOptionItems(commandConfig: CommandConfig, inputCmd: string, backspaceCount: number = 0): OptionItem[] { 108 | const result: OptionItem[] = []; 109 | 110 | if (!commandConfig.arguments) { 111 | return result; 112 | } 113 | 114 | // 生成退格符串 115 | const backspaceString = '\b'.repeat(backspaceCount); 116 | 117 | // 遍历所有参数 118 | commandConfig.arguments.forEach(arg => { 119 | let description = arg.name; // 默认使用 name 作为描述 120 | 121 | // 如果提供了翻译键,尝试获取翻译 122 | if (arg.despLangKey) { 123 | const translatedDesc = this.translateService.instant(arg.despLangKey); 124 | // 如果翻译成功(返回值不等于翻译键本身),使用翻译结果 125 | if (translatedDesc !== arg.despLangKey) { 126 | description = translatedDesc; 127 | } 128 | } 129 | 130 | // 创建参数选项,在content前加上退格符 131 | const optionItem: OptionItem = { 132 | name: arg.name, // 显示名称,包含占位符提示 133 | content: backspaceString + arg.content, // 实际输入内容,前面加上退格符 134 | desp: description, // 描述:翻译结果或回退到 name 135 | type: ArgumentsContentProvider.providerTypeKey, 136 | doNotEnterExec: true, 137 | clearThenInput: false, 138 | }; 139 | result.push(optionItem); 140 | }); 141 | 142 | return result; 143 | } 144 | 145 | async getQuickCmdList(inputCmd: string, cursorIndexAt: number, envBasicInfo: EnvBasicInfo): Promise { 146 | if (!envBasicInfo.config.store.ogAutoCompletePlugin.arguments.enable) { 147 | return null; 148 | } 149 | const result: OptionItem[] = []; 150 | 151 | // 处理光标位置:以光标前的内容为基础进行匹配 152 | const beforeCursor = inputCmd.substring(0, cursorIndexAt); 153 | const afterCursor = inputCmd.substring(cursorIndexAt); 154 | 155 | // 清理并解析光标前的命令 156 | const cleanCmd = beforeCursor.replace(new RegExp("\\s+", "g"), " ").trim(); 157 | const cmdParts = cleanCmd.split(" "); 158 | const mainExecCmd = cmdParts[0]; 159 | 160 | if (cmdParts.length <= 1) { 161 | return { 162 | optionItem: result, 163 | envBasicInfo: envBasicInfo, 164 | type: ArgumentsContentProvider.providerTypeKey 165 | }; 166 | } 167 | if (!isValidStr(mainExecCmd)) { 168 | return { 169 | optionItem: result, 170 | envBasicInfo: envBasicInfo, 171 | type: ArgumentsContentProvider.providerTypeKey 172 | }; 173 | } 174 | 175 | // 计算需要清除的内容:从光标前第一个空格到光标位置的内容 176 | const lastSpaceIndex = beforeCursor.lastIndexOf(" "); 177 | const contentToClear = lastSpaceIndex === -1 ? 178 | beforeCursor.substring(mainExecCmd.length).trimStart() : // 如果没有空格,清除主命令后的内容 179 | beforeCursor.substring(lastSpaceIndex + 1); // 从最后一个空格后开始清除 180 | 181 | // 计算需要的退格符数量 182 | const backspaceCount = contentToClear.length; 183 | 184 | // 获取用于匹配的当前输入(光标前最后一个空格到光标位置的内容) 185 | const currentInputForMatching = lastSpaceIndex === -1 ? "" : contentToClear; 186 | 187 | this.logger.debug("处理命令参数补全", { 188 | inputCmd, 189 | cursorIndexAt, 190 | beforeCursor, 191 | afterCursor, 192 | mainExecCmd, 193 | cmdParts, 194 | contentToClear, 195 | backspaceCount, 196 | currentInputForMatching, 197 | lastSpaceIndex 198 | }); 199 | 200 | // 从YAML配置中查找对应的命令 201 | const commandConfig = this.findCommandConfig(mainExecCmd); 202 | 203 | if (commandConfig) { 204 | // 获取YAML配置中的参数建议,传入退格符数量 205 | const yamlBasedOptions = this.convertArgumentsToOptionItems(commandConfig, inputCmd, backspaceCount); 206 | result.push(...yamlBasedOptions); 207 | 208 | this.logger.log(`找到${mainExecCmd}命令的${yamlBasedOptions.length}个参数建议`); 209 | } 210 | 211 | // 如果有结果,使用Fuse.js进行二次过滤和排序 212 | if (result.length > 0) { 213 | // 使用光标前最后一个空格到光标位置的内容进行匹配 214 | if (currentInputForMatching) { 215 | const fuse = new Fuse(result, this.fuseOptions); 216 | const filteredResults = fuse.search(currentInputForMatching); 217 | return { 218 | optionItem: filteredResults.map(item => item.item), 219 | envBasicInfo: envBasicInfo, 220 | type: ArgumentsContentProvider.providerTypeKey 221 | }; 222 | } 223 | } 224 | 225 | this.logger.debug(`最终返回${result.length}个参数建议`); 226 | 227 | return { 228 | optionItem: result, 229 | envBasicInfo: envBasicInfo, 230 | type: ArgumentsContentProvider.providerTypeKey 231 | }; 232 | } 233 | } -------------------------------------------------------------------------------- /src/components/autoCompleteAIDialog.ts: -------------------------------------------------------------------------------- 1 | import { Component, forwardRef, HostListener, Inject, Input } from '@angular/core'; 2 | import { EnvBasicInfo } from 'api/pluginType'; 3 | import OpenAI from 'openai'; 4 | import { MyLogger } from 'services/myLogService'; 5 | import { AppService, ConfigService, TranslateService } from 'tabby-core'; 6 | import jsYaml from "js-yaml" 7 | import { AddMenuService } from 'services/menuService'; 8 | import { isValidStr, sendInput } from 'utils/commonUtils'; 9 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; 10 | import { AutoCompleteTranslateService } from 'services/translateService'; 11 | 12 | interface AICommandItem { 13 | command: string; 14 | desp: string; 15 | dangerRating: number; 16 | } 17 | 18 | @Component({ 19 | template: require('./autoCompleteAIDialog.pug'), 20 | styles: [require('./autoCompleteAIDialog.scss')], 21 | }) 22 | export class AutoCompleteAIDialogComponent { 23 | commands: AICommandItem[] = []; 24 | selectedIndex: number = -1; 25 | loadingFlag: boolean = false; 26 | private openAI: OpenAI; 27 | askUserInput: string = ""; 28 | notReady: string = ""; 29 | constructor( 30 | protected logger: MyLogger, 31 | protected configService: ConfigService, 32 | @Inject(forwardRef(() => AddMenuService)) protected addMenuService: AddMenuService, 33 | protected appService: AppService, 34 | protected activeModel: NgbActiveModal, 35 | protected myTranslate: TranslateService, 36 | protected autoCompleteTranslate: AutoCompleteTranslateService, 37 | ) { 38 | 39 | } 40 | 41 | ngOnInit() { 42 | this.loadOpenAIConfig(); 43 | this.logger.log("AI panel init", this.askUserInput); 44 | if (isValidStr(this.askUserInput)) { 45 | // this.ask(); 46 | } 47 | this.askUserInput = this.addMenuService.getCurrentCmd(); 48 | } 49 | 50 | loadOpenAIConfig() { 51 | try { 52 | this.logger.log("infos", this.configService.store.ogAutoCompletePlugin.ai.openAIBaseUrl) 53 | // https://github.com/openai/openai-node/tree/v4#configuring-an-https-agent-eg-for-proxies 54 | // if need proxy 55 | this.openAI = new OpenAI({ 56 | apiKey: this.configService.store.ogAutoCompletePlugin.ai.openAIKey, 57 | baseURL: this.configService.store.ogAutoCompletePlugin.ai.openAIBaseUrl, 58 | dangerouslyAllowBrowser: true, 59 | }); 60 | } catch(err) { 61 | this.logger.warn("Error occured while loading openai", err); 62 | } 63 | } 64 | 65 | ask() { 66 | this.loadingFlag = true; 67 | this.notReady = ""; 68 | this.askForCmd(this.askUserInput, null, this.openAI) 69 | .then((commands) => { 70 | this.loadingFlag = false; 71 | if (commands) { 72 | this.commands = commands; 73 | } else { 74 | this.commands = []; 75 | this.notReady = "Can't recognize the response from AI"; 76 | } 77 | }).catch(err=>{ 78 | this.logger.error("While asking to gpt, an error occured", err); 79 | this.notReady = err.message; 80 | this.loadingFlag = false; 81 | }); 82 | } 83 | 84 | async askForCmd(inputCmd: string, envBasicInfo: EnvBasicInfo, openai: OpenAI): Promise { 85 | const prompt = ` 86 | Given the following user request: "${inputCmd}" 87 | And the current terminal state: "${envBasicInfo || "Not Provided"}" 88 | Generate 3 suggested terminal commands based on the input and state. 89 | Each command should include: 90 | 1. The command itself 91 | 2. A brief description of what the command does 92 | 3. A danger rating on a scale from 0 (safe) to 5 (dangerous), where: 93 | - 0: Very safe 94 | - 1-2: Low risk (e.g., read-only commands) 95 | - 3-4: Moderate risk (e.g., file or system changes) 96 | - 5: Very dangerous (e.g., data loss or system instability possible) 97 | 98 | Respond with the following JSON format only: 99 | 100 | \`\`\`json 101 | [ 102 | { 103 | "command": "", 104 | "desp": "", 105 | "dangerRating": 106 | }, 107 | { 108 | "command": "", 109 | "desp": "", 110 | "dangerRating": 111 | }, 112 | { 113 | "command": "", 114 | "desp": "", 115 | "dangerRating": 116 | } 117 | ] 118 | \`\`\` 119 | `; 120 | // // TESTONLY 121 | // const response = ` 122 | // \`\`\`json 123 | // [ 124 | // { 125 | // "command": "nvidia-smi -q -d POWER", 126 | // "desp": "Displays detailed information about the power consumption of the NVIDIA GPU.", 127 | // "dangerRating": 0 128 | // }, 129 | // { 130 | // "command": "nvidia-smi -pl ", 131 | // "desp": "Sets the power limit of the GPU to the specified value (in watts), allowing control over power usage.", 132 | // "dangerRating": 3 133 | // }, 134 | // { 135 | // "command": "nvidia-smi --auto-boost-default=0", 136 | // "desp": "Disables the default auto-boost feature, which can lead to reduced performance in exchange for lower power consumption.", 137 | // "dangerRating": 2 138 | // } 139 | // ] 140 | // \`\`\` 141 | // ` 142 | const response = await this.initiateConversation(prompt, this.openAI); 143 | this.logger.log("AI Response", response); 144 | return this.parseAIResponse(response, "json"); 145 | } 146 | 147 | async initiateConversation(prompt: string, openai: OpenAI): Promise { 148 | const response = await openai.chat.completions.create({ 149 | model: this.configService.store.ogAutoCompletePlugin.ai.openAIModel, 150 | messages: [{role: 'user', content: prompt}], 151 | }); 152 | //@ts-ignore 153 | if (response?.error) { 154 | //@ts-ignore 155 | throw new Error(JSON.stringify(response.error)); 156 | } 157 | return response.choices[0].message.content; 158 | } 159 | 160 | async parseAIResponse(response: string, type: "json"|"yaml"): Promise { 161 | const codeBlockRegex = /```(json|yaml)([\s\S]*?)```/; 162 | const match = response.match(codeBlockRegex); 163 | const content = match ? match[2] : response; 164 | let result = null; 165 | if (type == "json") { 166 | try { 167 | result = JSON.parse(content) as AICommandItem[]; 168 | } catch (jsonError) { 169 | this.logger.warn("Error parsing JSON response", jsonError); 170 | } 171 | } else { 172 | try { 173 | result = jsYaml.load(content) as AICommandItem[]; 174 | } catch (yamlError) { 175 | this.logger.warn("Error parsing YAML response", yamlError); 176 | } 177 | } 178 | return result; 179 | } 180 | 181 | handleKeydown(event: KeyboardEvent) { 182 | if (event.key === 'ArrowDown') { 183 | this.selectedIndex = (this.selectedIndex + 1) % this.commands.length; 184 | } else if (event.key === 'ArrowUp') { 185 | this.selectedIndex = (this.selectedIndex - 1 + this.commands.length) % this.commands.length; 186 | } else if (event.key === 'Enter') { 187 | if (this.selectedIndex >= 0) { 188 | this.userSelected(this.commands[this.selectedIndex]); 189 | }else if (this.selectedIndex < 0) { 190 | this.ask(); 191 | } 192 | } 193 | } 194 | getRatingColor(cmd: AICommandItem) { 195 | if (cmd.dangerRating <= 2) { 196 | return {"rate-safe": true}; 197 | } 198 | if (cmd.dangerRating <= 4) { 199 | return {"rate-warn": true}; 200 | } 201 | return {"rate-danger": true}; 202 | } 203 | 204 | userSelected(cmd: AICommandItem) { 205 | this.logger.log("userSelected"); 206 | sendInput({ 207 | tab: this.appService.activeTab, 208 | cmd: this.avoidDirectRun(cmd.command), 209 | appendCR: false, 210 | clearFirst: true, 211 | refocus: true 212 | }); 213 | this.activeModel.close("任务完成"); 214 | } 215 | 216 | avoidDirectRun(cmd: string) { 217 | return cmd.replace(/\n/g, ' '); 218 | } 219 | isValidStr(s: string) { 220 | return isValidStr(s); 221 | } 222 | 223 | // 翻译方法 224 | translate(key: string, params?: any): string { 225 | return this.myTranslate.instant(key, params); 226 | } 227 | } -------------------------------------------------------------------------------- /src/services/provider/historyProvider.ts: -------------------------------------------------------------------------------- 1 | import { EnvBasicInfo, OptionItem, TerminalSessionInfo } from "api/pluginType"; 2 | import { MyLogger } from "services/myLogService"; 3 | import { BaseContentProvider, OptionItemResultWrap } from "./baseProvider"; 4 | import Fuse from "fuse.js"; 5 | import { Injectable } from "@angular/core"; 6 | import { ConfigService } from "tabby-core"; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class HistoryContentProvider extends BaseContentProvider { 12 | protected static providerTypeKey: string = "h"; 13 | private dbName = "og_tac_HistoryDB"; 14 | private storeName = "CmdHistory"; 15 | private db: IDBDatabase; 16 | constructor( 17 | protected logger: MyLogger, 18 | protected configService: ConfigService, 19 | ) { 20 | super(logger, configService); 21 | this.openDB().then((db) => { 22 | this.db = db; 23 | // 清理历史记录 24 | const currentDate = new Date(); 25 | const pastDate = new Date(currentDate.setDate(currentDate.getDate() - 60)); 26 | this.clearOldCmdHistories(db, pastDate).then(() => { 27 | this.logger.log("清理历史记录成功"); 28 | }).catch((err) => { 29 | this.logger.error("清理历史记录失败", err); 30 | }); 31 | }).catch((err)=>{ 32 | this.logger.error("打开数据库失败", err); 33 | }); 34 | } 35 | async getQuickCmdList(inputCmd: string, cursorIndexAt: number, envBasicInfo: EnvBasicInfo): Promise { 36 | if (this.db == null) { 37 | return null; 38 | } 39 | if (!this.configService.store.ogAutoCompletePlugin.history.enable) { 40 | return null; 41 | } 42 | const result: OptionItem[] = []; 43 | // FIXME: 控制检索范围 44 | const dbList = await this.getHistoryFromDB(this.db, envBasicInfo.tab.profile.id, 1000, null, null, 3); 45 | dbList.sort((a, b)=>{return b.time - a.time}); 46 | this.logger.debug("db list", dbList); 47 | const options = { 48 | keys: ['cmd'], // 搜索的字段 49 | threshold: 0.2, // 控制匹配的模糊度 50 | includeScore: true // 包含得分 51 | }; 52 | const fuse = new Fuse(dbList, options); 53 | const searchResult = fuse.search(inputCmd); 54 | this.logger.log("匹配结果", searchResult); 55 | result.push(...searchResult.slice(0, 7).map((value)=>{ 56 | return { 57 | name: value.item.cmd, 58 | content: value.item.cmd, 59 | desp: "", 60 | type: HistoryContentProvider.providerTypeKey 61 | } as OptionItem 62 | })); 63 | this.logger.log("result", result); 64 | // result.push(...dbList.map((value) => { 65 | // return { 66 | // name: value.cmd, 67 | // content: value.cmd, 68 | // desp: "", 69 | // type: HistoryContentProvider.providerTypeKey 70 | // } as OptionItem; 71 | // })); 72 | // do sth 73 | return { 74 | optionItem: result, 75 | envBasicInfo: envBasicInfo, 76 | type: HistoryContentProvider.providerTypeKey 77 | } as OptionItemResultWrap; 78 | } 79 | async userInputCmd(inputCmd: string, terminalSessionInfo: TerminalSessionInfo): Promise { 80 | if (this.db == null) { 81 | return null; 82 | } 83 | if (!this.configService.store.ogAutoCompletePlugin.history.enable) { 84 | return null; 85 | } 86 | if (!this.configService.store.ogAutoCompletePlugin.history.countInRegExp && terminalSessionInfo.matchedByRegExp) { 87 | return null; 88 | } 89 | inputCmd = inputCmd.trim(); 90 | if (!this.checkCmd(inputCmd)) { 91 | return ; 92 | } 93 | this.addOrUpdateCmdHistory(this.db, {time: new Date(), cmd: inputCmd, profileId: terminalSessionInfo.tab.profile.id}).catch((err)=>{ 94 | this.logger.error(err); 95 | }); 96 | } 97 | checkCmd(inputCmd: string):boolean { 98 | if (inputCmd.match(new RegExp("^(rm |\\[\\[|cd )", "gm"))) { 99 | this.logger.debug("命令保存:Reject for black list", inputCmd); 100 | return false; 101 | } 102 | // 键入一部分的命令不予处理 103 | if (inputCmd.endsWith("/")) { 104 | return false; 105 | } 106 | return true; 107 | } 108 | userSelectedCallback(inputCmd: string): void { 109 | 110 | } 111 | async openDB(): Promise { 112 | const request = indexedDB.open(this.dbName, 2); 113 | return new Promise((resolve, reject) => { 114 | request.onupgradeneeded = (event) => { 115 | const db = request.result; 116 | if (!db.objectStoreNames.contains(this.storeName)) { 117 | const objectStore = db.createObjectStore(this.storeName, { keyPath: "id", autoIncrement: true }); 118 | objectStore.createIndex("profileId", "profileId", { unique: false }); 119 | objectStore.createIndex("cmd", "cmd", { unique: false }); 120 | objectStore.createIndex("time", "time", { unique: false }); 121 | } 122 | // 升级数据库也需要在这里处理! 123 | }; 124 | request.onsuccess = () => resolve(request.result); 125 | request.onerror = () => reject(request.error); 126 | }); 127 | } 128 | async addCmdHistory(db: IDBDatabase, cmdHistory: { time: Date, count: number, cmd: string, profileId: string }): Promise { 129 | return new Promise((resolve, reject) => { 130 | const transaction = db.transaction(this.storeName, "readwrite"); 131 | const store = transaction.objectStore(this.storeName); 132 | const request = store.add(cmdHistory); 133 | request.onsuccess = () => { 134 | resolve(); 135 | }; 136 | request.onerror = () => { 137 | reject(request.error); 138 | }; 139 | }); 140 | } 141 | async getHistoryFromDB(db: IDBDatabase, profileId: string, limit: number, startTime: Date, endTime: Date, countLimit?: number): Promise { 142 | return new Promise((resolve, reject) => { 143 | const transaction = db.transaction(this.storeName, "readonly"); 144 | const store = transaction.objectStore(this.storeName); 145 | const index = store.index("profileId"); 146 | const range = IDBKeyRange.only(profileId); 147 | const query = index.openCursor(range); 148 | const results: any[] = []; 149 | 150 | query.onsuccess = (event) => { 151 | const cursor = query.result; 152 | if (cursor) { 153 | const record = cursor.value; 154 | if ((startTime == null && record.time >= startTime) 155 | && (endTime == null || record.time <= endTime) && (countLimit == null || record.count >= countLimit)) { 156 | results.push(record); 157 | if (results.length >= limit) { 158 | resolve(results); 159 | return; 160 | } 161 | } 162 | cursor.continue(); 163 | } else { 164 | resolve(results); 165 | } 166 | }; 167 | query.onerror = () => { 168 | reject(query.error); 169 | }; 170 | }); 171 | } 172 | async clearOldCmdHistories(db: IDBDatabase, beforeDate: Date): Promise { 173 | return new Promise((resolve, reject) => { 174 | const transaction = db.transaction(this.storeName, "readwrite"); 175 | const store = transaction.objectStore(this.storeName); 176 | const index = store.index("time"); 177 | const range = IDBKeyRange.upperBound(beforeDate); 178 | const query = index.openCursor(range); 179 | 180 | query.onsuccess = (event) => { 181 | const cursor = query.result; 182 | if (cursor) { 183 | store.delete(cursor.primaryKey); 184 | cursor.continue(); 185 | } else { 186 | resolve(); 187 | } 188 | }; 189 | query.onerror = () => { 190 | reject(query.error); 191 | }; 192 | }); 193 | } 194 | async findExactCmdHistory(db: IDBDatabase, cmd: string, profileId: string): Promise { 195 | if (await this.isIndexedDBEmpty(db)) { 196 | return []; 197 | } 198 | return new Promise((resolve, reject) => { 199 | const transaction = db.transaction(this.storeName, "readonly"); 200 | const store = transaction.objectStore(this.storeName); 201 | const index = store.index("cmd"); 202 | const query = index.openCursor(IDBKeyRange.only(cmd)); 203 | const results: any[] = []; 204 | query.onsuccess = (event) => { 205 | const cursor = query.result; 206 | if (cursor) { 207 | if (cursor.value.profileId === profileId) { 208 | results.push(cursor.value); 209 | } 210 | cursor.continue(); 211 | } else { 212 | resolve(results); 213 | } 214 | }; 215 | query.onerror = () => { 216 | reject(query.error); 217 | }; 218 | }); 219 | } 220 | 221 | // 添加或更新记录 222 | async addOrUpdateCmdHistory(db: IDBDatabase, cmdHistory: { time: Date, cmd: string, profileId: string }): Promise { 223 | const isEmpty = await this.isIndexedDBEmpty(db); 224 | const existingRecordList = await this.findExactCmdHistory(db, cmdHistory.cmd, cmdHistory.profileId); 225 | const transaction = db.transaction(this.storeName, "readwrite"); 226 | const store = transaction.objectStore(this.storeName); 227 | let haveExistingRecord = false; 228 | // let existingRecord = null; 229 | for (let i = 0; i < existingRecordList.length; i++) { 230 | const record = existingRecordList[i]; 231 | if (record.profileId === cmdHistory.profileId) { 232 | record.count += 1; 233 | record.time = cmdHistory.time; 234 | store.put(record); 235 | haveExistingRecord = true; 236 | break; 237 | } 238 | } 239 | 240 | if (!haveExistingRecord){ 241 | store.add({ ...cmdHistory, count: 1 }); 242 | } 243 | 244 | return new Promise((resolve, reject) => { 245 | transaction.oncomplete = () => { 246 | resolve(); 247 | }; 248 | transaction.onerror = () => { 249 | reject(transaction.error); 250 | }; 251 | }); 252 | } 253 | isIndexedDBEmpty(db: IDBDatabase) { 254 | return new Promise((resolve, reject) => { 255 | const transaction = db.transaction(this.storeName, 'readonly'); 256 | const store = transaction.objectStore(this.storeName); 257 | 258 | const countRequest = store.count(); 259 | 260 | countRequest.onerror = (event) => { 261 | reject(event); 262 | }; 263 | 264 | countRequest.onsuccess = (event) => { 265 | resolve(countRequest.result === 0); 266 | }; 267 | }); 268 | } 269 | } -------------------------------------------------------------------------------- /src/components/autoCompleteHintMenu.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * tabby-quick-cmds-hint: A simple complete hint plugin for tabby. 3 | * Copyright (C) 2025 OpaqueGlass and other developers 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | import { Component, ElementRef, Inject, Input, Renderer2, SimpleChanges, type OnChanges, ChangeDetectorRef } from '@angular/core' 19 | import { OptionItem } from '../api/pluginType' 20 | import { AppService, ConfigService, PlatformService, ThemesService } from 'tabby-core'; 21 | import { isValidStr, sendInput } from 'utils/commonUtils'; 22 | import { MyLogger } from 'services/myLogService'; 23 | import { DOCUMENT } from '@angular/common'; 24 | 25 | @Component({ 26 | template: require('./autoCompleteHintMenu.pug'), 27 | styles: [require('./autoCompleteHintMenu.scss')], 28 | }) 29 | export class AutoCompleteHintMenuComponent { 30 | mainText: string = "Hello, world! This is the tabby-auto-complete plugin speaking~"; 31 | options: OptionItem[] = []; 32 | currentItemIndex: number = -1; 33 | recentTargetElement: HTMLElement; 34 | showingFlag: boolean = false; 35 | contentGroups: {[key: string]: OptionItem[]} = {}; 36 | themeMode: string = "dark"; 37 | themeName: string = "NotSet"; 38 | constructor( 39 | private renderer: Renderer2, 40 | private elRef: ElementRef, 41 | private app: AppService, 42 | private logger: MyLogger, 43 | @Inject(DOCUMENT) private document: Document, 44 | private configService: ConfigService, 45 | private themeService: ThemesService, 46 | private platformService: PlatformService, 47 | ) { 48 | this.currentItemIndex = -1; 49 | this.contentGroups = { 50 | "q": [],// quick cmd 51 | "h": [],// highlight 52 | "a": [],// ai 53 | }; 54 | this.themeChanged(); 55 | this.themeService.themeChanged$.subscribe(()=>{ 56 | this.themeChanged(); 57 | }) 58 | } 59 | 60 | themeChanged() { 61 | this.themeMode = this.configService.store.appearance.colorSchemeMode; 62 | let theme: 'dark'|'light' = 'dark' 63 | if (this.configService.store.appearance.colorSchemeMode === 'light') { 64 | theme = 'light' 65 | } else if (this.configService.store.appearance.colorSchemeMode === 'auto') { 66 | //@ts-ignore 67 | theme = this.platformService?.getTheme() ?? 'dark'; 68 | } 69 | this.themeMode = theme; 70 | 71 | if (theme === 'light') { 72 | this.themeName = this.configService.store.terminal.lightColorScheme.name 73 | } else { 74 | this.themeName = this.configService.store.terminal.colorScheme.name 75 | } 76 | } 77 | 78 | private doRegetItems() { 79 | // 记录当前的option 80 | const currentOption = this.getCurrentItem(); 81 | 82 | // 按照 contentGroups中的key顺序,遍历,将结果加入到options中 83 | this.options = []; 84 | // 限制个数 85 | let totalItemCount = 0; 86 | for (let key in this.contentGroups) { 87 | totalItemCount += this.contentGroups[key].length; 88 | } 89 | // 计算每组的最大显示数 90 | const maxTotal = this.configService.store.ogAutoCompletePlugin.menuShowItemMaxCount; 91 | const groupKeys = Object.keys(this.contentGroups); 92 | const groupCount = groupKeys.length; 93 | // 统计每组实际数量 94 | const groupActualCounts = groupKeys.map(key => this.contentGroups[key].length); 95 | // 先分配平均值 96 | let perGroupMax = Math.floor(maxTotal / groupCount); 97 | // 计算每组实际可分配的数量 98 | let groupShowCounts = groupActualCounts.map(count => Math.min(count, perGroupMax)); 99 | // 计算剩余可分配数量 100 | let used = groupShowCounts.reduce((a, b) => a + b, 0); 101 | let left = maxTotal - used; 102 | // 按顺序分配剩余数量给还有剩余的组 103 | while (left > 0) { 104 | let distributed = false; 105 | for (let i = 0; i < groupCount && left > 0; i++) { 106 | if (groupActualCounts[i] > groupShowCounts[i]) { 107 | groupShowCounts[i]++; 108 | left--; 109 | distributed = true; 110 | } 111 | } 112 | if (!distributed) break; // 没有可分配的了 113 | } 114 | // 合并结果 115 | for (let i = 0; i < groupCount; i++) { 116 | this.options = this.options.concat(this.contentGroups[groupKeys[i]].slice(0, groupShowCounts[i])); 117 | } 118 | if (this.options.length == 0) { 119 | this.hideAutocompleteList(); 120 | return; 121 | } else { 122 | this.showAutocompleteList(this.document.querySelector('.content-tab-active.active .focused .xterm-helper-textarea')); 123 | } 124 | // 调整后,仍然选择之前的option 125 | if (currentOption) { 126 | const newIndex = this.options.findIndex(option => option.content === currentOption.content && option.type === currentOption.type); 127 | this.currentItemIndex = newIndex !== -1 ? newIndex : -1; 128 | } else { 129 | this.currentItemIndex = -1; 130 | } 131 | } 132 | 133 | 134 | public setContent(newVal: OptionItem[], type: string) { 135 | this.contentGroups[type] = newVal; 136 | this.doRegetItems(); 137 | // this.options = newVal; 138 | // this.currentItemIndex = -1; 139 | } 140 | 141 | public test(text: string) { 142 | this.mainText = text; 143 | } 144 | private refreshMainText() { 145 | if (this.currentItemIndex < 0 || this.currentItemIndex >= this.options.length) { 146 | this.mainText = "No such item"; 147 | return; 148 | } 149 | this.mainText = this.options[this.currentItemIndex].content ?? ""; 150 | } 151 | 152 | private clearContent() { 153 | this.showingFlag = false; 154 | this.options = []; 155 | this.currentItemIndex = -1; 156 | for (let key in this.contentGroups) { 157 | this.contentGroups[key] = []; 158 | } 159 | } 160 | 161 | public clearSelection() { 162 | this.currentItemIndex = -1; 163 | } 164 | 165 | // 在输入框的事件中调用此函数 166 | showAutocompleteList(targetElement: HTMLElement) { 167 | if (this.showingFlag) { 168 | this.logger.debug("已经显示,不再多次处理"); 169 | setTimeout(this.adjustPosition.bind(this), 0); 170 | return; 171 | } 172 | const listEl = this.elRef.nativeElement.children[0]; 173 | this.recentTargetElement = targetElement; 174 | // make sure adjustPosition is called after the list is shown 175 | setTimeout(this.adjustPosition.bind(this), 0); 176 | // 显示自动完成列表 177 | this.renderer.setStyle(listEl, 'opacity', '1'); 178 | this.renderer.setStyle(listEl, 'pointer-events', 'auto'); 179 | this.showingFlag = true; 180 | } 181 | 182 | adjustPosition() { 183 | const listEl = this.elRef.nativeElement.children[0]; 184 | const targetRect = this.recentTargetElement.getBoundingClientRect(); 185 | // 获取窗口的高度 186 | const viewportHeight = window.document.querySelector("ssh-tab .terminal.xterm")?.clientHeight || window.innerHeight; 187 | const viewportWidth = window.document.querySelector("ssh-tab .terminal.xterm")?.clientWidth || window.innerWidth; 188 | 189 | // 下方位置和上方位置 190 | const belowPosition = targetRect.bottom; 191 | const abovePosition = targetRect.top - listEl.offsetHeight; 192 | this.logger.debug("height(liOffset, belowPosition, viewport)", listEl.clientHeight, belowPosition, viewportHeight); 193 | this.logger.debug("width(liOffset, rightPositionLeft, viewport)", listEl.clientWidth, viewportWidth - targetRect.left, viewportWidth, targetRect) 194 | 195 | // 决定显示在下方还是上方 196 | let topPosition: number; 197 | let leftPosition: number; 198 | const fontSize: number = this.configService.store.terminal.fontSize; 199 | if (belowPosition + listEl.offsetHeight + fontSize <= viewportHeight) { 200 | // 下方有足够空间 201 | topPosition = belowPosition; 202 | this.logger.debug("Down") 203 | } else { 204 | // 上方有足够空间 205 | topPosition = abovePosition; 206 | } 207 | // 需要判定左右空间 208 | if (targetRect.left - listEl.offsetWidth >= 0 && targetRect.left + listEl.offsetWidth > viewportWidth) { 209 | leftPosition = targetRect.left - listEl.offsetWidth; 210 | } else { 211 | leftPosition = targetRect.left; 212 | } 213 | // 设置max-height: 214 | 215 | // 设置位置样式 216 | this.renderer.setStyle(listEl, 'top', `${topPosition}px`); 217 | this.renderer.setStyle(listEl, 'left', `${leftPosition}px`); 218 | // this.renderer.setStyle(listEl, 'width', `${targetRect.width}px`); 219 | } 220 | 221 | // 隐藏自动完成列表 222 | public hideAutocompleteList() { 223 | this.clearContent(); 224 | const listEl = this.elRef.nativeElement.children[0]; 225 | if (listEl) { 226 | this.renderer.setStyle(listEl, 'opacity', '0'); 227 | this.renderer.setStyle(listEl, 'pointer-events', 'no'); 228 | } 229 | this.showingFlag = false; 230 | } 231 | 232 | public getShowingStatus() { 233 | return this.showingFlag; 234 | } 235 | 236 | selectUp() { 237 | if (!this.showingFlag) { 238 | this.logger.log("不再显示") 239 | return null; 240 | } 241 | if (this.currentItemIndex >= 0) { 242 | this.currentItemIndex--; 243 | setTimeout(this.scrollIntoVisible.bind(this), 0); 244 | } else { 245 | this.logger.log("???", this.currentItemIndex); 246 | return null; 247 | } 248 | this.refreshMainText(); 249 | return this.currentItemIndex; 250 | } 251 | selectDown() { 252 | if (!this.showingFlag) { 253 | return null; 254 | } 255 | if (this.currentItemIndex < this.options.length - 1) { 256 | this.currentItemIndex++; 257 | setTimeout(this.scrollIntoVisible.bind(this), 0); 258 | // setTimeout(this.adjustPosition.bind(this), 0); 259 | } else { 260 | return this.currentItemIndex; 261 | } 262 | this.refreshMainText(); 263 | return this.currentItemIndex; 264 | } 265 | 266 | getCurrentItem() { 267 | if (this.currentItemIndex < 0) { 268 | return null; 269 | } 270 | return this.options[this.currentItemIndex]; 271 | } 272 | 273 | getCurrentIndex() { 274 | return this.currentItemIndex; 275 | } 276 | 277 | /** 278 | * 将用户选择项目上屏 279 | * @param index cmd index 280 | * @param type 类型:0 仅上屏 1上屏并回车 281 | */ 282 | inputItem(index: number, type: number) { 283 | this.logger.log(`Selected index: ${index}, type: ${type}, content: ${JSON.stringify(this.options)}`); 284 | if (this.options[index].callback) { 285 | const newOptionList = this.options[index].callback(); 286 | if (newOptionList == null || newOptionList.length == 0) { 287 | this.hideAutocompleteList(); 288 | return; 289 | } else { 290 | this.clearContent(); 291 | this.setContent(newOptionList, this.options[index].type); 292 | return; 293 | } 294 | } 295 | sendInput({ 296 | tab: this.app.activeTab, 297 | cmd: this.options[index].content, 298 | appendCR: type == 1 && !this.options[index].doNotEnterExec, 299 | singleLine: false, 300 | clearFirst: this.options[index].clearThenInput === undefined ? true : this.options[index].clearThenInput, 301 | refocus: true 302 | }); 303 | if (type == 1) { 304 | this.hideAutocompleteList(); 305 | } 306 | this.clearSelection(); 307 | // 上屏完毕可能还需要调用focus 308 | } 309 | isValidStr(str: string) { 310 | return isValidStr(str); 311 | } 312 | scrollIntoVisible() { 313 | // const outerContainer = categoriesContainer.value; 314 | // let container = this.elRef.nativeElement.querySelector(".og-autocomplete-item-list"); 315 | let container = null; 316 | // danger: 这和DOM结构密切相关;由于缓存和更新延迟,不能直接使用querySelector定位 317 | const selectedElement = this.elRef.nativeElement.querySelector(".og-tac-selected"); 318 | if (selectedElement) { 319 | container = selectedElement.closest('.og-autocomplete-item-list'); 320 | const containerRect = container.getBoundingClientRect(); 321 | const elementRect = selectedElement.getBoundingClientRect(); 322 | if (elementRect.top < containerRect.top) { 323 | container.scrollTop -= (containerRect.top - elementRect.top) + elementRect.height; 324 | } else if (elementRect.bottom > containerRect.bottom) { 325 | container.scrollTop += (elementRect.bottom - containerRect.bottom) + elementRect.height; 326 | } 327 | } 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/services/menuService.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * tabby-quick-cmds-hint: A simple complete hint plugin for tabby. 3 | * Copyright (C) 2025 OpaqueGlass and other developers 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | import { 19 | ApplicationRef, 20 | Injector, 21 | EmbeddedViewRef, 22 | Inject, 23 | Injectable, 24 | ComponentFactoryResolver, 25 | ComponentRef, 26 | forwardRef, 27 | } from '@angular/core'; 28 | import { DOCUMENT } from '@angular/common'; 29 | import { AutoCompleteHintMenuComponent } from '../components/autoCompleteHintMenu'; 30 | import { MyLogger } from './myLogService'; 31 | import { BaseContentProvider, OptionItemResultWrap } from './provider/baseProvider'; 32 | import { QuickCmdContentProvider } from './provider/quickCmdContentProvider'; 33 | import { EnvBasicInfo, TerminalSessionInfo } from 'api/pluginType'; 34 | import { ConfigService } from 'tabby-core'; 35 | import { BaseTerminalProfile, BaseTerminalTabComponent } from 'tabby-terminal'; 36 | import { HistoryContentProvider } from './provider/historyProvider'; 37 | import { Subject } from 'rxjs'; 38 | import { MySignalService } from './signalService'; 39 | import { AutoCompleteTranslateService } from './translateService'; 40 | import { StyleService } from './styleService'; 41 | import { ArgumentsContentProvider } from './provider/argumentsContentProvider'; 42 | 43 | @Injectable({ 44 | providedIn: 'root' 45 | }) 46 | export class AddMenuService { 47 | private componentRef: ComponentRef; 48 | private lastCmd: string; 49 | private lastCursorIndexAt: number; 50 | private recentUuid: string; 51 | private currentCmd: string; // 仅用于对外呈现 52 | private recentBlockedUuid: string; 53 | private recentBlockedKeyup: string; 54 | private currentSessionId: string; 55 | private contentProviderList: BaseContentProvider[]; // 选项提供列表,用于异步获取 56 | // 回车输入状态 57 | private enterNotificationSubject: Subject = new Subject(); 58 | public enterNotification$ = this.enterNotificationSubject.asObservable(); 59 | // 补全菜单工作状态 60 | private menuStatus: boolean = true; 61 | private menuStatusNotificationSubject: Subject = new Subject(); 62 | public menuStatus$ = this.menuStatusNotificationSubject.asObservable(); 63 | // 上下菜单状态 64 | private recentHistoryJumpStatus: boolean = false; 65 | constructor( 66 | private appRef: ApplicationRef, 67 | private injector: Injector, 68 | private componentFactoryResolver: ComponentFactoryResolver, 69 | @Inject(DOCUMENT) private document: Document, 70 | private logger: MyLogger, 71 | private configService: ConfigService, 72 | quickCmdContentProvider: QuickCmdContentProvider, 73 | historyContentProvider: HistoryContentProvider, 74 | argumentsContentProvider: ArgumentsContentProvider, 75 | private myTranslate: AutoCompleteTranslateService, // 这个东西,放在Provider、index都会导致其他中文内容丢失 76 | // openAIContentProvider: OpenAIContentProvider, 77 | // private buttonProvider: ButtonProvider, // 直接引用会卡在Cannot access 'AddMenuService' before initialization 78 | private signalService: MySignalService, 79 | private cssService: StyleService 80 | ) { 81 | this.menuStatus = configService.store.ogAutoCompletePlugin.enableCompleteWithCompleteStart; 82 | document.addEventListener("keydown", this.handleKeyDown.bind(this), true); 83 | document.addEventListener("keyup", this.handleKeyUp.bind(this), true); 84 | this.contentProviderList = [ 85 | quickCmdContentProvider, 86 | historyContentProvider, 87 | argumentsContentProvider, 88 | ]; 89 | logger.log("Add menu service init"); 90 | if (this.menuStatus) { 91 | this.enable(); 92 | } else { 93 | this.disable(); 94 | } 95 | signalService.menuStatus$.subscribe(()=>{ 96 | if (this.getStatus()) { 97 | this.disable(); 98 | } else { 99 | this.enable(); 100 | } 101 | }) 102 | // buttonProvider.menuStatus$.subscribe(()=>{ 103 | // if (this.getStatus()) { 104 | // this.disable(); 105 | // } else { 106 | // this.enable(); 107 | // } 108 | // }); 109 | } 110 | 111 | // 插入组件的方法 112 | public insertComponent() { 113 | this.logger.log("插入提示菜单组件"); 114 | // 获取目标 DOM 元素 115 | const target = this.document.querySelector('app-root'); 116 | 117 | if (target) { 118 | const componentFactory = this.componentFactoryResolver.resolveComponentFactory(AutoCompleteHintMenuComponent); 119 | this.componentRef = componentFactory.create(this.injector); 120 | this.appRef.attachView(this.componentRef.hostView); 121 | const domElem = (this.componentRef.hostView as EmbeddedViewRef).rootNodes[0] as HTMLElement; 122 | target.appendChild(domElem); 123 | } 124 | // this.document.addEventListener('keydown', this.handleKeyDown.bind(this), true); 125 | } 126 | 127 | public showMenu() { 128 | this.componentRef.instance.showAutocompleteList(this.document.querySelector('.xterm-helper-textarea')); 129 | } 130 | 131 | public hideMenu() { 132 | // fix componentRef is undefined in constructor 133 | this.componentRef?.instance?.hideAutocompleteList(); 134 | this.clearCurrentTabCache(); 135 | } 136 | 137 | private clearCurrentTabCache() { 138 | this.currentSessionId = null; 139 | this.recentUuid = null; 140 | this.currentCmd = ""; 141 | } 142 | 143 | public isCurrentTabMatch(sessionId: string, uuid: string) { 144 | if (this.currentSessionId == null) { 145 | this.currentSessionId = sessionId; 146 | return true; 147 | } 148 | if (uuid && uuid === this.recentBlockedUuid && this.currentSessionId === sessionId) { 149 | return false; 150 | } else if (this.currentSessionId === sessionId) { 151 | return true; 152 | } 153 | return false; 154 | } 155 | 156 | private optionItemTypePostProcess(resultWrap: OptionItemResultWrap) { 157 | if (resultWrap == null) { 158 | this.logger.debug("Reject for no response"); 159 | return; 160 | } 161 | if (resultWrap.optionItem == null) { 162 | this.logger.debug("Reject for empty"); 163 | return; 164 | } 165 | if (resultWrap.envBasicInfo == null) { 166 | this.logger.debug("Reject for envBasicInfo"); 167 | return; 168 | } 169 | if (resultWrap.envBasicInfo.sessionId !== this.currentSessionId) { 170 | this.logger.debug("Reject for sessionId unique", resultWrap.envBasicInfo.sessionId, this.currentSessionId); 171 | return; 172 | } 173 | this.logger.debug("Provider 返回option", resultWrap.optionItem); 174 | this.componentRef.instance.setContent(resultWrap.optionItem, resultWrap.type); 175 | } 176 | 177 | /** 178 | * 向Provider广播用户输入的,已回车的cmd 179 | * @param cmd 用户输入的cmd 180 | * @param sessionId sessionId,和会话相关 181 | * @param tab tab实例 182 | * @param matchedByRegExp 183 | */ 184 | public broadcastUserEnteredCmd(cmd: string, sessionId: string, tab: BaseTerminalTabComponent, matchedByRegExp: boolean) { 185 | const terminalSessionInfo: TerminalSessionInfo = { 186 | config: this.configService, 187 | tab: tab, 188 | sessionId: sessionId, 189 | matchedByRegExp: matchedByRegExp 190 | } 191 | this.contentProviderList.forEach((provider) => { 192 | provider.userInputCmd(cmd, terminalSessionInfo).catch((err) => { 193 | this.logger.error("插入新命令失败", err); 194 | }); 195 | }); 196 | } 197 | 198 | public disable() { 199 | this.menuStatus = false; 200 | this.hideMenu(); 201 | this.menuStatusNotificationSubject.next(this.menuStatus); 202 | this.document.querySelector(".og-tac-tool-btn").setAttribute("stroke", "purple"); 203 | } 204 | 205 | public enable() { 206 | this.menuStatus = true; 207 | this.currentSessionId = ""; 208 | this.lastCmd = ""; 209 | this.lastCursorIndexAt = -1; 210 | this.recentBlockedUuid = ""; 211 | this.menuStatusNotificationSubject.next(this.menuStatus); 212 | this.document.querySelector(".og-tac-tool-btn").setAttribute("stroke", "green"); 213 | } 214 | 215 | /** 216 | * 是否启用的控制,和menu是否正显示无关 217 | * @returns 218 | */ 219 | public getStatus() { 220 | return this.menuStatus; 221 | } 222 | 223 | public sendCurrentText(text: string, cursorIndexAt: number, uuid: string, sessionId: string, tab: BaseTerminalTabComponent, ignoreStatus) { 224 | this.currentCmd = text; 225 | if (!this.menuStatus && !ignoreStatus) { 226 | this.logger.debug("Ignore sended cmd for menuStatus == false") 227 | return; 228 | } 229 | if (this.recentHistoryJumpStatus) { 230 | this.logger.debug("Ignored due to recent history input"); 231 | return; 232 | } 233 | if (this.lastCmd === text && this.lastCursorIndexAt === cursorIndexAt && this.currentSessionId == sessionId) { 234 | // 和上一个一致,无需处理 235 | this.logger.debug("和上一个一致,无需处理"); 236 | return; 237 | } 238 | if (text.length < 2) { 239 | this.hideMenu(); 240 | return; 241 | } 242 | if (uuid && this.recentBlockedUuid === uuid) { 243 | this.logger.debug("uuid被阻止"); 244 | return; 245 | } 246 | this.logger.debug("进入处理", text) 247 | this.recentUuid = uuid; 248 | this.currentSessionId = sessionId; 249 | 250 | 251 | // TODO:异步:遍历所有 252 | // 改成异步的,另外,除了结果外还需要回传传过去的text、uuid、tab-id信息,避免插入到错误的tab提示中 253 | const envBasicInfo: EnvBasicInfo = { 254 | config: this.configService, 255 | document: this.document, 256 | tab: tab, 257 | sessionId: sessionId 258 | } 259 | this.contentProviderList.forEach((provider) => { 260 | provider.getQuickCmdList(text, cursorIndexAt, envBasicInfo) 261 | .then(this.optionItemTypePostProcess.bind(this)).catch((err)=>{ 262 | this.logger.error("获取快捷命令列表失败", err); 263 | }); 264 | }); 265 | 266 | this.componentRef.instance.test(text); 267 | this.lastCmd = text; 268 | this.lastCursorIndexAt = cursorIndexAt; 269 | } 270 | 271 | public getCurrentCmd() { 272 | return this.currentCmd; 273 | } 274 | 275 | private handleKeyUp(event: KeyboardEvent) { 276 | const key = event.key; 277 | if (key === this.recentBlockedKeyup) { 278 | this.recentBlockedKeyup = null; 279 | event.preventDefault(); 280 | event.stopPropagation(); 281 | event.stopImmediatePropagation(); 282 | this.logger.debug("blocked key up", key) 283 | return; 284 | } 285 | } 286 | 287 | private handleKeyDown(event: KeyboardEvent) { 288 | const key = event.key; 289 | let actFlag = false; 290 | // this.logger.messyDebug("handle key down", event.key) 291 | if (key === 'ArrowUp' && !this.hasFloatWnd()) { 292 | if (this.componentRef.instance.selectUp() !== null) { 293 | actFlag = true; 294 | } else { 295 | this.logger.debug("up 不操作"); 296 | this.recentHistoryJumpStatus = true; 297 | this.hideMenu(); 298 | } 299 | } else if (key === 'ArrowDown' && !this.hasFloatWnd()) { 300 | if (this.componentRef.instance.selectDown() !== null) { 301 | actFlag = true; 302 | } else { 303 | this.recentHistoryJumpStatus = true; 304 | this.hideMenu(); 305 | } 306 | } else if (key === 'Enter' && !this.hasFloatWnd()) { 307 | const currentIndex = this.componentRef.instance.currentItemIndex; 308 | this.enterNotificationSubject.next(); 309 | if (currentIndex != -1 && this.componentRef.instance.showingFlag) { 310 | this.componentRef.instance.inputItem(currentIndex, 1); 311 | actFlag = true; 312 | this.logger.debug("handle enter: input") 313 | } else { 314 | this.hideMenu(); 315 | this.logger.debug("handle enter: hide") 316 | } 317 | } else if (key === 'Escape') { 318 | this.recentBlockedUuid = this.recentUuid; 319 | if (this.componentRef.instance.showingFlag) { 320 | this.hideMenu(); 321 | actFlag = true; 322 | } 323 | } else if (key === 'Tab' && !this.hasFloatWnd()) { 324 | const currentIndex = this.componentRef.instance.currentItemIndex; 325 | if (currentIndex != -1) { 326 | this.componentRef.instance.inputItem(currentIndex, 0); 327 | actFlag = true; 328 | } 329 | } else if (key === 'Backspace' && !this.hasFloatWnd()) { 330 | this.componentRef.instance.clearSelection(); 331 | } else { 332 | this.recentHistoryJumpStatus = false; 333 | return; 334 | } 335 | if (actFlag) { 336 | event.stopImmediatePropagation(); 337 | event.stopPropagation(); 338 | event.preventDefault(); 339 | this.recentBlockedKeyup = key; 340 | } else { 341 | this.logger.debug("No act") 342 | } 343 | 344 | } 345 | hasFloatWnd(): boolean { 346 | const floatLayers = this.document.querySelectorAll("ngb-modal-window[role]") 347 | if (floatLayers == null || floatLayers.length == 0) { 348 | return false; 349 | } 350 | return true; 351 | } 352 | 353 | 354 | } 355 | -------------------------------------------------------------------------------- /src/services/manager/simpleContentManager.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * tabby-quick-cmds-hint: A simple complete hint plugin for tabby. 3 | * Copyright (C) 2025 OpaqueGlass and other developers 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { cleanTerminalText, cleanTextByNewXterm, generateUUID, isValidStr, simpleHash } from "utils/commonUtils"; 20 | import { BaseManager } from "./baseManager"; 21 | import { BaseTerminalProfile, BaseTerminalTabComponent } from "tabby-terminal"; 22 | import { MyLogger } from "services/myLogService"; 23 | import { AddMenuService } from "services/menuService"; 24 | import { ConfigService, NotificationsService } from "tabby-core"; 25 | import { MySignalService } from "services/signalService"; 26 | 27 | interface LastStateLinesObj { 28 | raw: string; 29 | cleaned: string; 30 | } 31 | export class SimpleManager extends BaseManager { 32 | // 命令输入状态,由enter清除,由匹配到prefix开始 33 | private cmdStatusFlag: boolean; 34 | private userImputedFlag: boolean; 35 | private currentLine: string; 36 | private recentCleanPrompt: string; 37 | // 命令输入id,用于区分一次输入 38 | private recentUuid: string; 39 | private recentStateLineHash: string; 40 | // private regExp: RegExp; 41 | // 使用正则表达式匹配的 42 | private usingRegExp: boolean; 43 | constructor( 44 | public tab: BaseTerminalTabComponent, 45 | public logger: MyLogger, 46 | public addMenuService: AddMenuService, 47 | public configService: ConfigService, 48 | public notification: NotificationsService, 49 | private signalService: MySignalService 50 | ) { 51 | super(tab, logger, addMenuService, configService); 52 | this.currentLine = ""; 53 | this.subscriptionList.push(addMenuService.enterNotification$.subscribe(this.endCmdStatus.bind(this))); 54 | this.subscriptionList.push(signalService.startCompleteNow$.subscribe(this.suggestNow.bind(this))); 55 | } 56 | async endCmdStatus() { 57 | this.logger.debug("收到Enter信号", this) 58 | this.cmdStatusFlag = false; 59 | if (!this.tab.hasFocus) { 60 | return; 61 | } 62 | const lastStateLineObj = await this.getLastStateLine(); 63 | // 检查 64 | // FIXME: 避免进vim之后会出现的,每次回车都广播 65 | const [cmd, _] = await this.getCmd(lastStateLineObj.raw, lastStateLineObj.cleaned); 66 | if (isValidStr(cmd) && cmd[0] != " " && !cmd.trim().endsWith("/")) { 67 | this.logger.log("广播命令", cmd); 68 | this.addMenuService.broadcastUserEnteredCmd(cmd, this.sessionUniqueId, this.tab, this.usingRegExp); 69 | } 70 | } 71 | handleInput = (buffers: Buffer[]) => { 72 | return; 73 | } 74 | handleOutput = async (data: string[]) => { 75 | const outputString = data.join(''); 76 | if (!this.tab.frontend.saveState) { 77 | this.logger.debug("当前终端不支持saveState"); 78 | return; 79 | } 80 | const allStateStr = this.tab.frontend.saveState(); 81 | const lines = allStateStr.trim().split("\n"); 82 | const lastStateLinesStr = lines.slice(-1).join("\n"); 83 | 84 | // 重复响应判定,应对screen等停滞更新的情况 85 | const last5Line = lines.slice(lines.length - 5).join("\n"); 86 | const recentStateLinesHash = simpleHash(last5Line); 87 | if (recentStateLinesHash == this.recentStateLineHash) { 88 | this.logger.messyDebug("由于重复,本次不响应", last5Line, recentStateLinesHash); 89 | return 90 | } else { 91 | this.recentStateLineHash = recentStateLinesHash; 92 | } 93 | // 正则匹配获取prompt prefix,先执行 94 | if (this.configService.store.ogAutoCompletePlugin.useRegExpDetectPrompt == true) { 95 | const cleanLastStateLine = cleanTerminalText(lastStateLinesStr); 96 | const matchResult = cleanLastStateLine.match(this.loadRegExp()); 97 | this.logger.debug("RegExp Debug [MatchResult, lastLine]", matchResult, lastStateLinesStr); 98 | if (matchResult) { 99 | this.recentCleanPrompt = matchResult[0]; 100 | this.cmdStatusFlag = true; 101 | this.recentUuid = generateUUID(); 102 | this.usingRegExp = true; 103 | } 104 | } 105 | // 从 转移序列 获取prompt prefix 106 | if (outputString.match(new RegExp("]1337;CurrentDir="))) { 107 | // 获取最后一行 108 | const lastRawLine = outputString.split("\n").slice(-1)[0]; 109 | const startRegExp = /.*\x1b\]1337;CurrentDir=.*?\x07/gm; 110 | const matchGroup = lastRawLine.match(startRegExp); 111 | let lastValidPrompt = ""; 112 | this.logger.debug("最后一行原文本", lastRawLine); 113 | this.logger.debug("匹配到的前缀", matchGroup); 114 | if (matchGroup && matchGroup.length > 0) { 115 | lastValidPrompt = matchGroup[matchGroup.length - 1]; 116 | // 获取清理后内容 117 | this.recentCleanPrompt = await this.cleanTerminalText(lastValidPrompt) 118 | this.logger.log("更新:清理后命令前缀", this.recentCleanPrompt); 119 | this.cmdStatusFlag = true; 120 | this.recentUuid = generateUUID(); 121 | this.usingRegExp = false; 122 | } else { 123 | this.logger.warn("没有匹配到命令开始"); 124 | } 125 | } 126 | // 检测命令执行,必须在原始未清理的内容中,全部输出中获取;主要用于保存历史 127 | // const replayCmdPrefix = "]2323;Command="; 128 | // if (outputString.match(new RegExp(replayCmdPrefix)) ) { 129 | // const startRegExp = /.*\x1b\]2323;Command=[^\x07]*\x07/gm; 130 | // const matchGroup = outputString.match(startRegExp); 131 | // let cmd = ""; 132 | // if (matchGroup && matchGroup.length > 0) { 133 | // cmd = matchGroup[matchGroup.length - 1]; 134 | // cmd = cmd.replace(replayCmdPrefix, ""); 135 | // cmd = cmd.replace("\x07", ""); 136 | // // cmd = cmd.trim(); 137 | // cmd = cmd.replace(/\s+$/, ""); 138 | // } 139 | // // 避免把乱七八糟的转义码当做history 140 | // this.logger.debug("识别到的执行命令", cmd); 141 | // const cleanedCmd = await this.cleanTerminalText(cmd); 142 | // // 存在转义符的、空格开始的命令不计入历史 143 | // this.logger.debug("检查历史保存判定", cleanedCmd, cleanedCmd == cmd); 144 | // if (isValidStr(cmd) && cleanedCmd == cmd && !cmd.startsWith(" ")) { 145 | // // 处理black list,一些类型的不保存到历史 146 | // this.logger.log("广播命令", cmd); 147 | // this.addMenuService.broadcastNewCmd(cmd, this.sessionUniqueId, this.tab); 148 | // } 149 | // } 150 | 151 | // 发送并处理正在输入的命令 152 | this.logger.messyDebug("lastSerialLine", lastStateLinesStr); 153 | this.getCmdAndSuggest(await this.getLastStateLine()); 154 | } 155 | /** 156 | * 获取输出内容中的文本 157 | * @returns 158 | */ 159 | getLastStateLine = async (): Promise => { 160 | if (!this || !this.tab || !this.tab.frontend) { 161 | this.logger.debug("WARN, lost frontend", this.tab); 162 | } 163 | let allStateStr = this.tab.frontend.saveState(); 164 | try { 165 | // @ts-ignore 166 | if (this.tab.frontend.xterm._addonManager._addons) { 167 | let serializeAddon = null; 168 | // @ts-ignore 169 | for (let i of this.tab.frontend.xterm._addonManager._addons) { 170 | if (i.instance?.serialize) { 171 | serializeAddon = i.instance; 172 | break; 173 | } 174 | } 175 | // @ts-ignore 176 | allStateStr = serializeAddon.serialize({ 177 | excludeAltBuffer: false, 178 | excludeModes: true, 179 | scrollback: 200, 180 | }); 181 | this.logger.debug("使用xterm内部Serialze api"); 182 | // @ts-ignore 183 | this.logger.debug("使用xterm内部Serialze api", this.tab.frontend?.xterm); 184 | } else { 185 | this.logger.debug("使用包装API"); 186 | } 187 | } catch (e) { 188 | this.logger.error("During getting serial state (beta), an ERROR occured. Fallback to origin API. ", e); 189 | } 190 | 191 | const cleanedAllStateStr = await cleanTextByNewXterm(allStateStr); 192 | const cleanedLines = cleanedAllStateStr.trim().split("\n"); 193 | const lastCleanedStateLineStr = cleanedLines.slice(-1).join("\n"); 194 | 195 | // FIX: 有时state捕捉到空白行的问题 196 | const lines = allStateStr.split("\n"); 197 | const lastRawStateLineStr = lines.slice(-1).join("\n"); 198 | return { 199 | "raw": lastRawStateLineStr, 200 | "cleaned": lastCleanedStateLineStr 201 | } as LastStateLinesObj 202 | } 203 | /** 204 | * 从输入字符串中,获取用户输入的命令 205 | * @param rawLine 原始stateline,取最后一行 206 | * @param cleanedLine 清理转义符后的stateline,取最后一行 207 | * @returns 用户输入的命令,可能为空字符串 208 | */ 209 | async getCmd(rawLine: string, cleanedLine: string): Promise<[string, number]> { 210 | let cmd = ""; 211 | // some times  still not provided in vim, tmux or screen 212 | // "" means cursor go to next line. in most cases, it means the command is finished 213 | const moveDownRegExp = /\x1b\[[0-9]*B/gm; 214 | const moveUpRegExp = /\x1b\[[0-9]*A/gm; 215 | const containMoveDownFlag = rawLine.match(moveDownRegExp); 216 | const containMoveUpFlag = rawLine.match(moveUpRegExp); 217 | const cleanedLastStateLineStr = await cleanTextByNewXterm(rawLine); 218 | if (this.recentCleanPrompt && cleanedLastStateLineStr.includes(this.recentCleanPrompt) && !containMoveDownFlag) { 219 | const actualLastLine = containMoveUpFlag ? cleanedLine : cleanedLastStateLineStr; 220 | const firstValieIndex = actualLastLine.lastIndexOf(this.recentCleanPrompt) + this.recentCleanPrompt.length; 221 | cmd = actualLastLine.slice(firstValieIndex); 222 | this.logger.messyDebug("命令为", cmd); 223 | } else if (this.tab.hasFocus) { 224 | this.logger.messyDebug("getCmd未匹配 [recentCleanPrompt, isIncludeCleanPrompt, isContainMoveDown, cmdStatus]", this.recentCleanPrompt, cleanedLastStateLineStr.includes(this.recentCleanPrompt), !containMoveDownFlag, this.cmdStatusFlag) 225 | } 226 | let cursorIndexAt = cmd.length; 227 | try { 228 | // @ts-ignore 229 | cursorIndexAt = this.tab.frontend.xterm.buffer.active.cursorX - this.recentCleanPrompt.length; 230 | // @ts-ignore 231 | this.logger.messyDebug("命令位置检查", this.tab.frontend.xterm.buffer.active.cursorX, this.tab.frontend.xterm.buffer.active.cursorY, this.tab.frontend.xterm.buffer.active.cursorX - this.recentCleanPrompt.length, cmd.slice(0, this.tab.frontend.xterm.buffer.active.cursorX - this.recentCleanPrompt.length)); 232 | } catch (e) { 233 | this.logger.messyDebug("ERROR: 定位光标位置失败") 234 | } finally { 235 | if (cursorIndexAt <= -1 || cursorIndexAt > cmd.length) { 236 | cursorIndexAt = cmd.length; 237 | } 238 | } 239 | return [cmd, cursorIndexAt]; 240 | } 241 | /** 242 | * 发送命令,给出提示菜单 243 | * @param cmd 提示的命令 244 | */ 245 | sendCmd(cmd: string, cursorIndexAt = -1, force: boolean = false) { 246 | if (isValidStr(cmd) && this.tab.hasFocus) { 247 | this.logger.messyDebug("menu sending", cmd); 248 | this.addMenuService.sendCurrentText(cmd, cursorIndexAt, this.recentUuid, this.sessionUniqueId, this.tab, force); 249 | } else if (this.tab.hasFocus) { 250 | if (this.configService.store.ogAutoCompletePlugin.debugLevel < 0) { 251 | this.logger.debug("menu close"); 252 | } 253 | this.addMenuService.hideMenu(); 254 | } 255 | } 256 | /** 257 | * 258 | * @param lastStateLineStr 259 | * @param force 忽略当前禁用状态,强制提出提示菜单 260 | */ 261 | async getCmdAndSuggest(lastStateLineObj: LastStateLinesObj, force: boolean=false) { 262 | const cleanedLastSerialLinesStr = cleanTerminalText(lastStateLineObj.raw); 263 | const [cmd, cursorIndexAt] = await this.getCmd(lastStateLineObj.raw, lastStateLineObj.cleaned); 264 | if (isValidStr(cmd) && this.cmdStatusFlag) { 265 | this.logger.messyDebug("命令为", cmd); 266 | this.sendCmd(cmd, cursorIndexAt, force); 267 | } else if (this.tab.hasFocus) { 268 | this.logger.messyDebug("menu close by not match or cmd disabled", this.recentCleanPrompt, cleanedLastSerialLinesStr.includes(this.recentCleanPrompt), !lastStateLineObj.raw.includes("[")); 269 | this.addMenuService.hideMenu(); 270 | } 271 | } 272 | async suggestNow() { 273 | if (!this.tab.hasFocus) { 274 | return; 275 | } 276 | this.recentUuid = generateUUID(); 277 | this.getCmdAndSuggest(await this.getLastStateLine(), true); 278 | } 279 | handleSessionChanged = (session) => { 280 | this.logger.log("session changed", session); 281 | this.addMenuService.hideMenu(); 282 | this.sessionUniqueId = generateUUID(); 283 | } 284 | async cleanTerminalText(text: string): Promise { 285 | const cleanByRegExp = cleanTerminalText(text); 286 | this.logger.debug("清理后命令(一致?)", cleanByRegExp == text, cleanByRegExp); 287 | const cleanByXterm = await cleanTextByNewXterm(text); 288 | // if (!isValidStr(cleanByXterm?.trim())) { 289 | // return cleanByRegExp; 290 | // } 291 | // if (cleanByRegExp !== cleanByXterm && this.configService.store.ogAutoCompletePlugin.debugLevel < 2) { 292 | // this.notification.error("[tabbyquick-hint-debug-report]清理不一致"); 293 | // this.logger.warn("清理不一致", cleanByRegExp + " != " + cleanByXterm); 294 | // } 295 | return cleanByXterm; 296 | 297 | } 298 | loadRegExp() { 299 | let regExp = /[^$#\n]*([a-zA-Z0-9_]+@[a-zA-Z0-9_-]+(:| )\S*)([\$\#]) {0,1}/; 300 | if (!isValidStr(this.configService.store.ogAutoCompletePlugin.customRegExp?.trim())) { 301 | return regExp; 302 | } 303 | try { 304 | regExp = new RegExp(this.configService.store.ogAutoCompletePlugin.customRegExp) 305 | } catch (e) { 306 | this.logger.error("Custom RegExp ERROR", e); 307 | } 308 | return regExp; 309 | } 310 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . -------------------------------------------------------------------------------- /src/static/i18n.yaml: -------------------------------------------------------------------------------- 1 | settings: 2 | title: 3 | en_US: "Quick Cmd Hint Settings" 4 | zh_CN: "快速命令提示设置" 5 | general: 6 | en_US: "General" 7 | zh_CN: "通用设置" 8 | use_regexp_prompt: 9 | en_US: "Use regular expressions to identify shell prompts" 10 | zh_CN: "使用正则表达式识别shell prompt" 11 | use_regexp_prompt_desc: 12 | en_US: "When enabled, the prompt may be triggered by mistake, or the wrong command may be captured, but it can avoid modifying the shell configuration file" 13 | zh_CN: "启用后,可能错误地触发提示,或捕获到错误的命令,但可以避免修改shell配置文件" 14 | auto_init: 15 | en_US: "Try to run init scripts when session connected" 16 | zh_CN: "在每次连接后,自动尝试运行初始化脚本" 17 | auto_init_desc: 18 | en_US: "For Bash Only. In some cases, it may not work properly." 19 | zh_CN: "仅支持Bash。一些情况下不能正常运行。" 20 | enable_on_startup: 21 | en_US: "Enable completion when startup" 22 | zh_CN: "在Tabby启动后,启用命令补全提示" 23 | enable_on_startup_desc: 24 | en_US: "When disabled, the autocomplete menu will not appear automatically, and users will need to enable the feature manually. You can manually control the auto-completion feature via the bird icon in the toolbar. Green means it is enabled." 25 | zh_CN: "禁用后,自动完成菜单不会自动出现,用户需要手动启用该功能。您可以通过工具栏中的小鸟图标手动控制自动完成功能。绿色意味着已启用。" 26 | menu_show_item_max: 27 | en_US: Max Hint Item Count 28 | zh_CN: 提示菜单显示数量限制 29 | menu_show_item_max_desc: 30 | en_US: When there are enough matches, regardless of the settings, at least 3 items of each enabled type will be displayed. 31 | zh_CN: 匹配数量足够的情况下,无论如何设定,各启用类型至少显示3个。 32 | 33 | # AI相关设置 34 | ai: 35 | title: 36 | en_US: "AI" 37 | zh_CN: "AI" 38 | openai_base_url: 39 | en_US: "Open AI Base URL" 40 | zh_CN: "Open AI Base URL" 41 | openai_key: 42 | en_US: "Open AI Key" 43 | zh_CN: "Open AI Key" 44 | openai_key_desc: 45 | en_US: "Note that all information will be saved in plain text locally" 46 | zh_CN: "请注意所有信息将明文保存在本地" 47 | openai_model: 48 | en_US: "Open AI Model name" 49 | zh_CN: "Open AI 模型名称" 50 | 51 | # 外观设置 52 | appearance: 53 | title: 54 | en_US: "Appearance" 55 | zh_CN: "外观" 56 | font_size: 57 | en_US: "Font size" 58 | zh_CN: "字体大小" 59 | 60 | # 历史记录设置 61 | history: 62 | title: 63 | en_US: "History" 64 | zh_CN: "历史记录" 65 | manage_desc: 66 | en_US: "Open \"Developer Tools\", \"Application\", \"Storage\" \"Indexed DB\" in the list on the left, and manage the saved history in \"og_tac_HistoryDB\"." 67 | zh_CN: "打开\"开发者工具\",\"应用\",左侧列表中的\"存储\"\"Indexed DB\",\"og_tac_HistoryDB\"中管理已保存的历史记录。" 68 | enable: 69 | en_US: "Enable the history of user input commands" 70 | zh_CN: "启用用户输入命令历史" 71 | enable_desc: 72 | en_US: "Turning this off will still create the database (but without recording history) and will not clear the history that was previously saved." 73 | zh_CN: "关闭此项,仍然会创建历史记录数据库(但不记录历史),先前记录的历史不会被自动清除。" 74 | count_regexp: 75 | en_US: "Record input commands matched by regular expressions" 76 | zh_CN: "记录由正则表达式匹配到的命令" 77 | count_regexp_desc: 78 | en_US: "It may record incorrect or incomplete commands. When disabled, only input commands matched by the prompt escape sequence will be saved." 79 | zh_CN: "可能记录到错误或不完整的命令。禁用后,只保存由prompt转义序列匹配的输入命令。" 80 | 81 | arguments: 82 | title: 83 | en_US: "Arguments Complete" 84 | zh_CN: "参数提示" 85 | manage_desc: 86 | en_US: "The preset content matched by the argument prompts is generated by AI and has not been reviewed by the developers. Please carefully verify the correctness of the argument prompts. The current functionality is still under testing, and during argument input completion, it may not check for or remove/partially remove existing content. If you encounter any bugs, please provide feedback. If you need more argument prompts, please submit a PR." 87 | zh_CN: "参数提示匹配到的预设内容由AI生成,开发者没有进行检查,请注意检查内容的正确性;目前功能仍在测试,参数输入上屏时,可能不会判断删除/部分删除已有内容。如有bug请反馈,如需要更多参数提示请PR。" 88 | enable: 89 | en_US: "Enable Arguments Complete" 90 | zh_CN: "启用命令参数提示补全" 91 | enable_desc: 92 | en_US: "" 93 | zh_CN: "" 94 | 95 | # 调试设置 96 | debug: 97 | title: 98 | en_US: "Debug" 99 | zh_CN: "调试" 100 | enable_debug: 101 | en_US: "Enable debug mode and detailed logs" 102 | zh_CN: "显示详细日志和调错信息" 103 | log_level_desc: 104 | en_US: "0 All, 1 log, 2 warn, 3 ErrorOnly, 4 No log" 105 | zh_CN: "0 输出全部日志,1 输出日志,2 输出警告,3 仅输出错误,4 不输出日志" 106 | custom_regexp: 107 | en_US: "Use a custom regular expression to match the shell prompt" 108 | zh_CN: "使用自定义正则表达式匹配shell prompt" 109 | custom_regexp_desc: 110 | en_US: "If it doesn't work, don't forget to check the output in the developer tools" 111 | zh_CN: "如果没有生效,别忘记查看开发者工具中的输出" 112 | 113 | # 通用UI 114 | ui: 115 | loading: 116 | en_US: "Loading..." 117 | zh_CN: "加载中..." 118 | ask: 119 | en_US: "Ask" 120 | zh_CN: "询问" 121 | ask_ai: 122 | en_US: "Ask AI" 123 | zh_CN: "询问 AI" 124 | no_results: 125 | en_US: "No results found" 126 | zh_CN: "未找到结果" 127 | error_occurred: 128 | en_US: "Error occurred: {desp}" 129 | zh_CN: "发生错误:{desp}" 130 | danger_rating: 131 | en_US: "Danger Rating" 132 | zh_CN: "危险级别" 133 | quick_hint: 134 | en_US: "Quick Hint" 135 | zh_CN: "命令提示" 136 | show_complete_menu: 137 | en_US: "Show complete menu now" 138 | zh_CN: "立刻显示提示菜单" 139 | 140 | # 插件介绍和说明 141 | intro: 142 | plugin_requirement: 143 | en_US: "To use this plugin, first download and enable the plugin `tabby-quick-cmds`, and then create some commands within it." 144 | zh_CN: "为使用此插件,请先下载启用tabby-quick-cmds插件,并在其中创建一些命令。" 145 | tested_scenarios: 146 | en_US: "The plugin has been primarily tested in the following scenarios: connecting to servers running" 147 | zh_CN: "插件主要在以下情况进行测试:连接到运行" 148 | ubuntu_centos: 149 | en_US: "Ubuntu" 150 | zh_CN: "Ubuntu" 151 | or: 152 | en_US: "or" 153 | zh_CN: "或" 154 | centos: 155 | en_US: "CentOS" 156 | zh_CN: "CentOS" 157 | via_ssh: 158 | en_US: "via" 159 | zh_CN: "的设备(通过" 160 | ssh: 161 | en_US: "SSH" 162 | zh_CN: "SSH" 163 | using_bash: 164 | en_US: "; using" 165 | zh_CN: ");使用" 166 | bash: 167 | en_US: "Bash" 168 | zh_CN: "Bash" 169 | on_windows: 170 | en_US: "as the shell environment; and using Tabby on Windows systems." 171 | zh_CN: "作为Shell;在Windows系统上使用Tabby。" 172 | usage_instruction: 173 | en_US: "After enabling the plugin, entering content will trigger a prompt. (Tips mainly come from quick-cmds plug-in, see README.md) Use the up and down arrow keys to select an item. Pressing" 174 | zh_CN: "启用此插件,输入内容将触发提示。(提示内容主要来源于 quick-cmds 插件,详见README.md)使用方向上下键选择一个项目。按下" 175 | tab_key: 176 | en_US: "Tab" 177 | zh_CN: "Tab" 178 | tab_action: 179 | en_US: "will input the selected item into the terminal, while pressing" 180 | zh_CN: "将输入选择的项目到终端。按下" 181 | enter_key: 182 | en_US: "Enter" 183 | zh_CN: "Enter" 184 | enter_action: 185 | en_US: "will input the selected item into the terminal and execute it by pressing enter." 186 | zh_CN: "将输入选择的项目到终端,并回车执行它。" 187 | 188 | # 菜单和工具 189 | menu: 190 | open_dev_tools: 191 | en_US: "Open Dev Tools" 192 | zh_CN: "打开开发者工具" 193 | init_current_session: 194 | en_US: "Init QuickCmdHint for Current Session (Bash only)" 195 | zh_CN: "为当前会话初始化快速命令提示(仅支持Bash)" 196 | ai_command_generation: 197 | en_US: "AI Command Generation" 198 | zh_CN: "让AI生成命令" 199 | noresult: 200 | en_US: "No results found" 201 | zh_CN: "无匹配结果" 202 | 203 | # 支持和反馈 204 | support: 205 | give_star_prefix: 206 | en_US: "⭐ If this project helps you, please" 207 | zh_CN: "⭐ 如果这个项目对你有帮助,请" 208 | give_star_link: 209 | en_US: "give it a star!" 210 | zh_CN: "点亮Star。" 211 | give_star_suffix: 212 | en_US: "Your support is greatly appreciated and motivates me to keep improving it. 😊" 213 | zh_CN: "感谢你的支持!这将鼓励我继续改进它。😊" 214 | issue_prefix: 215 | en_US: "💡 If you encounter any issues," 216 | zh_CN: "💡 如果遇到问题," 217 | issue_link: 218 | en_US: "feel free to open an issue on GitHub!" 219 | zh_CN: "请到Github创建issue" 220 | issue_suffix: 221 | en_US: "😊" 222 | zh_CN: "😊" 223 | 224 | # 测试用例 225 | test: 226 | en_US: "test" 227 | zh_CN: "testA" 228 | 229 | # 命令参数补全 230 | arguments_complete: 231 | git: 232 | add: 233 | en_US: "Add files to staging area" 234 | zh_CN: "将文件添加到暂存区" 235 | add_all: 236 | en_US: "Add all files in current directory" 237 | zh_CN: "添加当前目录的所有文件" 238 | add_all_changes: 239 | en_US: "Add all changes including deletions" 240 | zh_CN: "添加所有更改包括删除的文件" 241 | commit: 242 | en_US: "Commit staged changes with message" 243 | zh_CN: "提交暂存的更改并添加提交信息" 244 | commit_amend: 245 | en_US: "Amend the last commit" 246 | zh_CN: "修改最后一次提交" 247 | commit_all: 248 | en_US: "Add all changes and commit with message" 249 | zh_CN: "添加所有更改并提交" 250 | push: 251 | en_US: "Push changes to remote repository" 252 | zh_CN: "推送更改到远程仓库" 253 | push_upstream: 254 | en_US: "Push and set upstream branch" 255 | zh_CN: "推送并设置上游分支" 256 | push_force: 257 | en_US: "Force push (dangerous operation)" 258 | zh_CN: "强制推送(危险操作)" 259 | pull: 260 | en_US: "Pull changes from remote repository" 261 | zh_CN: "从远程仓库拉取更改" 262 | pull_rebase: 263 | en_US: "Pull with rebase strategy" 264 | zh_CN: "使用 rebase 方式拉取" 265 | branch_list: 266 | en_US: "List all branches" 267 | zh_CN: "列出所有分支" 268 | branch_create: 269 | en_US: "Create a new branch" 270 | zh_CN: "创建新分支" 271 | branch_delete: 272 | en_US: "Delete a branch" 273 | zh_CN: "删除分支" 274 | checkout: 275 | en_US: "Switch to a branch or restore files" 276 | zh_CN: "切换分支或恢复文件" 277 | checkout_create: 278 | en_US: "Create and switch to new branch" 279 | zh_CN: "创建并切换到新分支" 280 | merge: 281 | en_US: "Merge branches" 282 | zh_CN: "合并分支" 283 | status: 284 | en_US: "Show working directory status" 285 | zh_CN: "显示工作目录状态" 286 | status_short: 287 | en_US: "Show status in short format" 288 | zh_CN: "以简短格式显示状态" 289 | log_oneline: 290 | en_US: "Show commit history in one line" 291 | zh_CN: "单行显示提交历史" 292 | log_graph: 293 | en_US: "Show commit history with graph" 294 | zh_CN: "图形化显示提交历史" 295 | log_limit: 296 | en_US: "Show limited number of commits" 297 | zh_CN: "显示指定数量的提交" 298 | docker: 299 | # 基础系统命令 300 | info: 301 | en_US: "Display Docker system information" 302 | zh_CN: "显示 Docker 系统信息" 303 | login: 304 | en_US: "Login to Docker registry server" 305 | zh_CN: "登录到 Docker 仓库服务器" 306 | login_default: 307 | en_US: "Login to default Docker registry" 308 | zh_CN: "登录到默认 Docker 仓库" 309 | help: 310 | en_US: "Show help information" 311 | zh_CN: "显示帮助信息" 312 | 313 | # 镜像管理命令 314 | images: 315 | en_US: "List Docker images" 316 | zh_CN: "列出 Docker 镜像" 317 | images_all: 318 | en_US: "List all Docker images including intermediate" 319 | zh_CN: "列出所有 Docker 镜像(包括中间镜像)" 320 | pull: 321 | en_US: "Pull an image from registry" 322 | zh_CN: "从仓库拉取镜像" 323 | push: 324 | en_US: "Push an image to registry" 325 | zh_CN: "推送镜像到仓库" 326 | build: 327 | en_US: "Build Docker image with tag" 328 | zh_CN: "构建带标签的 Docker 镜像" 329 | build_no_cache: 330 | en_US: "Build image without using cache" 331 | zh_CN: "不使用缓存构建镜像" 332 | rmi: 333 | en_US: "Remove one or more images" 334 | zh_CN: "删除一个或多个镜像" 335 | rmi_force: 336 | en_US: "Force remove one or more images" 337 | zh_CN: "强制删除一个或多个镜像" 338 | history: 339 | en_US: "Show the history of an image" 340 | zh_CN: "显示镜像的历史变更" 341 | tag: 342 | en_US: "Create a tag for an image" 343 | zh_CN: "为镜像创建标签" 344 | 345 | # 容器运行命令 346 | run_interactive: 347 | en_US: "Run container in interactive mode" 348 | zh_CN: "以交互模式运行容器" 349 | run_detached: 350 | en_US: "Run container in detached mode" 351 | zh_CN: "以后台模式运行容器" 352 | run_port: 353 | en_US: "Run container with port mapping" 354 | zh_CN: "运行容器并映射端口" 355 | run_remove: 356 | en_US: "Run container and remove it after exit" 357 | zh_CN: "运行容器,退出后自动删除" 358 | run_env: 359 | en_US: "Run container with environment variables" 360 | zh_CN: "运行容器并设置环境变量" 361 | run_volume: 362 | en_US: "Run container with volume mounting" 363 | zh_CN: "运行容器并挂载数据卷" 364 | run_name: 365 | en_US: "Run container with custom name" 366 | zh_CN: "运行容器并指定名称" 367 | run_network: 368 | en_US: "Run container with specific network" 369 | zh_CN: "运行容器并指定网络" 370 | 371 | # 容器管理命令 372 | ps: 373 | en_US: "List running containers" 374 | zh_CN: "列出运行中的容器" 375 | ps_all: 376 | en_US: "List all containers" 377 | zh_CN: "列出所有容器" 378 | start: 379 | en_US: "Start one or more stopped containers" 380 | zh_CN: "启动一个或多个已停止的容器" 381 | stop: 382 | en_US: "Stop one or more running containers" 383 | zh_CN: "停止一个或多个运行中的容器" 384 | restart: 385 | en_US: "Restart one or more containers" 386 | zh_CN: "重启一个或多个容器" 387 | rm: 388 | en_US: "Remove one or more containers" 389 | zh_CN: "删除一个或多个容器" 390 | rm_force: 391 | en_US: "Force remove one or more containers" 392 | zh_CN: "强制删除一个或多个容器" 393 | exec_bash: 394 | en_US: "Execute bash in running container" 395 | zh_CN: "在运行中的容器内执行 bash" 396 | exec_sh: 397 | en_US: "Execute shell in running container" 398 | zh_CN: "在运行中的容器内执行 shell" 399 | logs: 400 | en_US: "Show container logs" 401 | zh_CN: "显示容器日志" 402 | logs_follow: 403 | en_US: "Follow container logs output" 404 | zh_CN: "实时跟踪容器日志输出" 405 | inspect: 406 | en_US: "Display detailed information about container" 407 | zh_CN: "显示容器的详细信息" 408 | 409 | # 数据卷命令 410 | volume_create: 411 | en_US: "Create a new volume" 412 | zh_CN: "创建新的数据卷" 413 | volume_list: 414 | en_US: "List all volumes" 415 | zh_CN: "列出所有数据卷" 416 | volume_inspect: 417 | en_US: "Display detailed information about volume" 418 | zh_CN: "显示数据卷的详细信息" 419 | volume_remove: 420 | en_US: "Remove one or more volumes" 421 | zh_CN: "删除一个或多个数据卷" 422 | volume_prune: 423 | en_US: "Remove all unused volumes" 424 | zh_CN: "删除所有未使用的数据卷" 425 | 426 | # 网络命令 427 | network_list: 428 | en_US: "List all networks" 429 | zh_CN: "列出所有网络" 430 | network_create: 431 | en_US: "Create a new network" 432 | zh_CN: "创建新的网络" 433 | network_remove: 434 | en_US: "Remove one or more networks" 435 | zh_CN: "删除一个或多个网络" 436 | network_inspect: 437 | en_US: "Display detailed information about network" 438 | zh_CN: "显示网络的详细信息" 439 | network_connect: 440 | en_US: "Connect container to a network" 441 | zh_CN: "将容器连接到网络" 442 | 443 | # 系统管理命令 444 | system_df: 445 | en_US: "Show Docker filesystem usage" 446 | zh_CN: "显示 Docker 文件系统使用情况" 447 | system_prune: 448 | en_US: "Remove unused data" 449 | zh_CN: "清理未使用的数据" 450 | system_prune_all: 451 | en_US: "Remove all unused data including images" 452 | zh_CN: "清理所有未使用的数据(包括镜像)" 453 | system_info: 454 | en_US: "Display system-wide information" 455 | zh_CN: "显示系统级信息" 456 | 457 | # Docker Compose 相关 458 | compose_up: 459 | en_US: "Create and start containers using compose" 460 | zh_CN: "使用 compose 创建并启动容器" 461 | compose_up_detached: 462 | en_US: "Create and start containers in detached mode" 463 | zh_CN: "以后台模式创建并启动容器" 464 | compose_down: 465 | en_US: "Stop and remove containers, networks" 466 | zh_CN: "停止并删除容器、网络" 467 | compose_ps: 468 | en_US: "List containers managed by compose" 469 | zh_CN: "列出由 compose 管理的容器" 470 | tar: 471 | extract: 472 | en_US: "Extract files from archive" 473 | zh_CN: "从归档文件中解压文件" 474 | create: 475 | en_US: "Create a new archive file" 476 | zh_CN: "创建新的归档文件" 477 | list: 478 | en_US: "List contents of archive" 479 | zh_CN: "列出归档文件的内容" 480 | create_gzip: 481 | en_US: "Create gzip compressed archive" 482 | zh_CN: "创建 gzip 压缩的归档文件" 483 | extract_gzip: 484 | en_US: "Extract gzip compressed archive" 485 | zh_CN: "解压 gzip 压缩的归档文件" 486 | list_gzip: 487 | en_US: "List contents of gzip archive" 488 | zh_CN: "列出 gzip 归档文件的内容" 489 | create_bzip2: 490 | en_US: "Create bzip2 compressed archive" 491 | zh_CN: "创建 bzip2 压缩的归档文件" 492 | extract_bzip2: 493 | en_US: "Extract bzip2 compressed archive" 494 | zh_CN: "解压 bzip2 压缩的归档文件" 495 | list_bzip2: 496 | en_US: "List contents of bzip2 archive" 497 | zh_CN: "列出 bzip2 归档文件的内容" 498 | create_xz: 499 | en_US: "Create xz compressed archive (high compression)" 500 | zh_CN: "创建 xz 压缩的归档文件(高压缩率)" 501 | extract_xz: 502 | en_US: "Extract xz compressed archive" 503 | zh_CN: "解压 xz 压缩的归档文件" 504 | list_xz: 505 | en_US: "List contents of xz archive" 506 | zh_CN: "列出 xz 归档文件的内容" 507 | exclude: 508 | en_US: "Exclude files matching pattern" 509 | zh_CN: "排除匹配模式的文件" 510 | exclude_from: 511 | en_US: "Exclude files listed in file" 512 | zh_CN: "排除文件中列出的文件" 513 | extract_to: 514 | en_US: "Extract to specified directory" 515 | zh_CN: "解压到指定目录" 516 | incremental: 517 | en_US: "Create incremental backup" 518 | zh_CN: "创建增量备份" 519 | rsync: 520 | recursive: 521 | en_US: "Recursive sync (basic)" 522 | zh_CN: "递归同步(基础)" 523 | archive: 524 | en_US: "Archive mode (preserve metadata)" 525 | zh_CN: "归档模式(保留元数据)" 526 | archive_verbose: 527 | en_US: "Archive mode with verbose output" 528 | zh_CN: "归档模式并显示详细信息" 529 | dry_run: 530 | en_US: "Simulate sync without actual execution" 531 | zh_CN: "模拟同步,不实际执行" 532 | dry_run_verbose: 533 | en_US: "Verbose dry run simulation" 534 | zh_CN: "详细的模拟同步" 535 | mirror: 536 | en_US: "Mirror sync (delete extra files in destination)" 537 | zh_CN: "镜像同步(删除目标中多余的文件)" 538 | exclude: 539 | en_US: "Exclude files matching pattern" 540 | zh_CN: "排除匹配模式的文件" 541 | exclude_from: 542 | en_US: "Exclude files listed in file" 543 | zh_CN: "排除文件中列出的文件" 544 | include_only: 545 | en_US: "Include only specific patterns" 546 | zh_CN: "仅包含特定模式的文件" 547 | remote_upload: 548 | en_US: "Upload to remote server via SSH" 549 | zh_CN: "通过SSH上传到远程服务器" 550 | remote_download: 551 | en_US: "Download from remote server via SSH" 552 | zh_CN: "从远程服务器通过SSH下载" 553 | ssh_port: 554 | en_US: "Use custom SSH port" 555 | zh_CN: "使用自定义SSH端口" 556 | daemon: 557 | en_US: "Sync using rsync daemon protocol" 558 | zh_CN: "使用rsync守护进程协议同步" 559 | daemon_url: 560 | en_US: "Sync using rsync:// URL" 561 | zh_CN: "使用rsync://协议同步" 562 | incremental: 563 | en_US: "Incremental backup with hard links" 564 | zh_CN: "使用硬链接的增量备份" 565 | progress: 566 | en_US: "Show progress and allow partial transfers" 567 | zh_CN: "显示进度并允许断点续传" 568 | resume: 569 | en_US: "Resume interrupted transfers" 570 | zh_CN: "恢复中断的传输" 571 | checksum: 572 | en_US: "Use checksum for file comparison" 573 | zh_CN: "使用校验和比较文件" 574 | compress: 575 | en_US: "Compress data during transfer" 576 | zh_CN: "传输时压缩数据" 577 | bandwidth: 578 | en_US: "Limit bandwidth usage (KB/s)" 579 | zh_CN: "限制带宽使用(KB/s)" 580 | max_size: 581 | en_US: "Skip files larger than specified size" 582 | zh_CN: "跳过大于指定大小的文件" 583 | min_size: 584 | en_US: "Skip files smaller than specified size" 585 | zh_CN: "跳过小于指定大小的文件" 586 | update: 587 | en_US: "Skip files newer in destination" 588 | zh_CN: "跳过目标中较新的文件" 589 | ignore_existing: 590 | en_US: "Skip files that exist in destination" 591 | zh_CN: "跳过目标中已存在的文件" 592 | existing_only: 593 | en_US: "Only update files that exist in destination" 594 | zh_CN: "仅更新目标中已存在的文件" 595 | crontab: 596 | list: 597 | en_US: "List current user's crontab" 598 | zh_CN: "列出当前用户的定时任务" 599 | edit: 600 | en_US: "Edit current user's crontab" 601 | zh_CN: "编辑当前用户的定时任务" 602 | remove: 603 | en_US: "Remove current user's crontab" 604 | zh_CN: "删除当前用户的定时任务" 605 | remove_interactive: 606 | en_US: "Remove crontab with confirmation prompt" 607 | zh_CN: "删除定时任务(需确认)" 608 | list_user: 609 | en_US: "List specified user's crontab" 610 | zh_CN: "列出指定用户的定时任务" 611 | edit_user: 612 | en_US: "Edit specified user's crontab" 613 | zh_CN: "编辑指定用户的定时任务" 614 | remove_user: 615 | en_US: "Remove specified user's crontab" 616 | zh_CN: "删除指定用户的定时任务" 617 | remove_user_interactive: 618 | en_US: "Remove user's crontab with confirmation" 619 | zh_CN: "删除指定用户的定时任务(需确认)" 620 | install_file: 621 | en_US: "Install crontab from file" 622 | zh_CN: "从文件安装定时任务" 623 | install_file_user: 624 | en_US: "Install crontab from file for specified user" 625 | zh_CN: "为指定用户从文件安装定时任务" 626 | install_stdin: 627 | en_US: "Install crontab from standard input" 628 | zh_CN: "从标准输入安装定时任务" 629 | journalctl: 630 | view_all: 631 | en_US: "View all system logs" 632 | zh_CN: "查看所有系统日志" 633 | follow: 634 | en_US: "Follow log output in real time" 635 | zh_CN: "实时跟踪日志输出" 636 | follow_long: 637 | en_US: "Follow log output in real time (long option)" 638 | zh_CN: "实时跟踪日志输出(完整选项)" 639 | lines: 640 | en_US: "Show specified number of recent lines" 641 | zh_CN: "显示指定行数的最新日志" 642 | lines_long: 643 | en_US: "Show specified number of recent lines (long option)" 644 | zh_CN: "显示指定行数的最新日志(完整选项)" 645 | lines_50: 646 | en_US: "Show last 50 lines" 647 | zh_CN: "显示最后50行日志" 648 | lines_100: 649 | en_US: "Show last 100 lines" 650 | zh_CN: "显示最后100行日志" 651 | since: 652 | en_US: "Show logs since specified time" 653 | zh_CN: "显示指定时间后的日志" 654 | until: 655 | en_US: "Show logs until specified time" 656 | zh_CN: "显示指定时间前的日志" 657 | since_today: 658 | en_US: "Show logs since today" 659 | zh_CN: "显示今天的日志" 660 | since_yesterday: 661 | en_US: "Show logs since yesterday" 662 | zh_CN: "显示昨天以来的日志" 663 | since_hour: 664 | en_US: "Show logs from last hour" 665 | zh_CN: "显示最近1小时的日志" 666 | priority_error: 667 | en_US: "Show only error messages" 668 | zh_CN: "仅显示错误消息" 669 | priority_warning: 670 | en_US: "Show warning and error messages" 671 | zh_CN: "显示警告和错误消息" 672 | priority_info: 673 | en_US: "Show info, warning and error messages" 674 | zh_CN: "显示信息、警告和错误消息" 675 | priority: 676 | en_US: "Filter by log priority level" 677 | zh_CN: "按日志优先级过滤" 678 | unit: 679 | en_US: "Show logs for specific systemd unit" 680 | zh_CN: "显示指定系统服务的日志" 681 | unit_long: 682 | en_US: "Show logs for specific systemd unit (long option)" 683 | zh_CN: "显示指定系统服务的日志(完整选项)" 684 | unit_nginx: 685 | en_US: "Show nginx service logs" 686 | zh_CN: "显示 nginx 服务日志" 687 | unit_ssh: 688 | en_US: "Show SSH service logs" 689 | zh_CN: "显示 SSH 服务日志" 690 | unit_docker: 691 | en_US: "Show Docker service logs" 692 | zh_CN: "显示 Docker 服务日志" 693 | kernel: 694 | en_US: "Show kernel messages" 695 | zh_CN: "显示内核消息" 696 | dmesg: 697 | en_US: "Show kernel messages (dmesg equivalent)" 698 | zh_CN: "显示内核消息(相当于dmesg)" 699 | boot: 700 | en_US: "Show logs from current boot" 701 | zh_CN: "显示当前启动的日志" 702 | boot_long: 703 | en_US: "Show logs from current boot (long option)" 704 | zh_CN: "显示当前启动的日志(完整选项)" 705 | boot_previous: 706 | en_US: "Show logs from previous boot" 707 | zh_CN: "显示上一次启动的日志" 708 | list_boots: 709 | en_US: "List all available boot sessions" 710 | zh_CN: "列出所有可用的启动会话" 711 | uid: 712 | en_US: "Show logs from specific user ID" 713 | zh_CN: "显示指定用户ID的日志" 714 | pid: 715 | en_US: "Show logs from specific process ID" 716 | zh_CN: "显示指定进程ID的日志" 717 | command: 718 | en_US: "Show logs from specific command" 719 | zh_CN: "显示指定命令的日志" 720 | output_json: 721 | en_US: "Output in JSON format" 722 | zh_CN: "以JSON格式输出" 723 | output_short: 724 | en_US: "Output in short format" 725 | zh_CN: "以简短格式输出" 726 | output_verbose: 727 | en_US: "Output in verbose format" 728 | zh_CN: "以详细格式输出" 729 | output: 730 | en_US: "Specify output format" 731 | zh_CN: "指定输出格式" 732 | reverse: 733 | en_US: "Show newest entries first" 734 | zh_CN: "最新条目优先显示" 735 | reverse_long: 736 | en_US: "Show newest entries first (long option)" 737 | zh_CN: "最新条目优先显示(完整选项)" 738 | no_pager: 739 | en_US: "Don't use pager for output" 740 | zh_CN: "不使用分页器输出" 741 | disk_usage: 742 | en_US: "Show journal disk usage" 743 | zh_CN: "显示日志磁盘使用情况" 744 | vacuum_size: 745 | en_US: "Remove logs to reduce disk usage to specified size" 746 | zh_CN: "删除日志以减少磁盘使用到指定大小" 747 | vacuum_time: 748 | en_US: "Remove logs older than specified time" 749 | zh_CN: "删除超过指定时间的日志" 750 | follow_unit: 751 | en_US: "Follow logs for specific service" 752 | zh_CN: "实时跟踪指定服务的日志" 753 | detailed_unit: 754 | en_US: "Show detailed logs for specific service" 755 | zh_CN: "显示指定服务的详细日志" 756 | lines_follow: 757 | en_US: "Show last 50 lines and follow" 758 | zh_CN: "显示最后50行并实时跟踪" 759 | npm: 760 | install: 761 | en_US: "Install packages" 762 | zh_CN: "安装包" 763 | run: 764 | en_US: "Run scripts" 765 | zh_CN: "运行脚本" 766 | build: 767 | en_US: "Build project" 768 | zh_CN: "构建项目" 769 | test: 770 | en_US: "Run tests" 771 | zh_CN: "运行测试" 772 | df: 773 | human_readable: 774 | en_US: "Display sizes in human-readable format (1K, 1M, 1G)" 775 | zh_CN: "以人类可读格式显示大小 (1K, 1M, 1G)" 776 | si_format: 777 | en_US: "Display sizes using SI units (1000-based)" 778 | zh_CN: "使用 SI 单位显示大小(1000进制)" 779 | inodes: 780 | en_US: "Display inode information instead of block usage" 781 | zh_CN: "显示 inode 信息而不是块使用情况" 782 | print_type: 783 | en_US: "Print filesystem type" 784 | zh_CN: "显示文件系统类型" 785 | type_filter: 786 | en_US: "Show only specified filesystem type" 787 | zh_CN: "只显示指定的文件系统类型" 788 | exclude_type: 789 | en_US: "Exclude specified filesystem type" 790 | zh_CN: "排除指定的文件系统类型" 791 | all_filesystems: 792 | en_US: "Include dummy filesystems" 793 | zh_CN: "包含虚拟文件系统" 794 | local_only: 795 | en_US: "Limit listing to local filesystems" 796 | zh_CN: "仅显示本地文件系统" 797 | posix_format: 798 | en_US: "Use POSIX output format" 799 | zh_CN: "使用 POSIX 输出格式" 800 | show_total: 801 | en_US: "Display a grand total" 802 | zh_CN: "显示总计" 803 | human_with_type: 804 | en_US: "Human-readable format with filesystem type" 805 | zh_CN: "人类可读格式并显示文件系统类型" 806 | human_inodes: 807 | en_US: "Human-readable inode information" 808 | zh_CN: "人类可读的 inode 信息" 809 | human_local: 810 | en_US: "Human-readable format for local filesystems only" 811 | zh_CN: "仅本地文件系统的人类可读格式" 812 | human_total: 813 | en_US: "Human-readable format with total" 814 | zh_CN: "人类可读格式并显示总计" 815 | human_type_total: 816 | en_US: "Human-readable format with type and total" 817 | zh_CN: "人类可读格式显示类型和总计" 818 | exclude_temp: 819 | en_US: "Exclude temporary filesystems" 820 | zh_CN: "排除临时文件系统" 821 | ext4_only: 822 | en_US: "Show only ext4 filesystems" 823 | zh_CN: "仅显示 ext4 文件系统" 824 | xfs_only: 825 | en_US: "Show only XFS filesystems" 826 | zh_CN: "仅显示 XFS 文件系统" 827 | root_fs: 828 | en_US: "Show disk usage for root filesystem" 829 | zh_CN: "显示根文件系统磁盘使用情况" 830 | home_fs: 831 | en_US: "Show disk usage for /home directory" 832 | zh_CN: "显示 /home 目录磁盘使用情况" 833 | var_fs: 834 | en_US: "Show disk usage for /var directory" 835 | zh_CN: "显示 /var 目录磁盘使用情况" 836 | netstat: 837 | listening: 838 | en_US: "Show only listening sockets" 839 | zh_CN: "仅显示监听端口" 840 | all: 841 | en_US: "Show all sockets (listening and non-listening)" 842 | zh_CN: "显示所有套接字(监听和非监听)" 843 | tcp: 844 | en_US: "Show TCP connections only" 845 | zh_CN: "仅显示 TCP 连接" 846 | udp: 847 | en_US: "Show UDP connections only" 848 | zh_CN: "仅显示 UDP 连接" 849 | numeric: 850 | en_US: "Show numerical addresses instead of resolving hosts" 851 | zh_CN: "显示数字地址而不解析主机名" 852 | program: 853 | en_US: "Show PID and name of programs" 854 | zh_CN: "显示进程 PID 和程序名" 855 | route: 856 | en_US: "Display routing table" 857 | zh_CN: "显示路由表" 858 | interfaces: 859 | en_US: "Display network interfaces" 860 | zh_CN: "显示网络接口" 861 | statistics: 862 | en_US: "Display network statistics" 863 | zh_CN: "显示网络统计信息" 864 | continuous: 865 | en_US: "Display continuously (refresh every second)" 866 | zh_CN: "持续显示(每秒刷新)" 867 | verbose: 868 | en_US: "Verbose output with additional information" 869 | zh_CN: "详细输出,显示额外信息" 870 | wide: 871 | en_US: "Don't truncate IP addresses (wide output)" 872 | zh_CN: "不截断 IP 地址(宽输出)" 873 | listening_tcp: 874 | en_US: "Show listening TCP sockets" 875 | zh_CN: "显示监听的 TCP 套接字" 876 | listening_udp: 877 | en_US: "Show listening UDP sockets" 878 | zh_CN: "显示监听的 UDP 套接字" 879 | listening_numeric: 880 | en_US: "Show listening sockets with numeric addresses" 881 | zh_CN: "显示监听套接字的数字地址" 882 | listening_numeric_program: 883 | en_US: "Show listening sockets with numeric addresses and programs" 884 | zh_CN: "显示监听套接字的数字地址和程序信息" 885 | all_numeric: 886 | en_US: "Show all sockets with numeric addresses" 887 | zh_CN: "显示所有套接字的数字地址" 888 | all_numeric_program: 889 | en_US: "Show all sockets with numeric addresses and programs" 890 | zh_CN: "显示所有套接字的数字地址和程序信息" 891 | all_numeric_tcp: 892 | en_US: "Show all TCP sockets with numeric addresses" 893 | zh_CN: "显示所有 TCP 套接字的数字地址" 894 | all_numeric_udp: 895 | en_US: "Show all UDP sockets with numeric addresses" 896 | zh_CN: "显示所有 UDP 套接字的数字地址" 897 | tcp_udp_listening_numeric_program: 898 | en_US: "Show TCP/UDP listening sockets with numeric addresses and programs" 899 | zh_CN: "显示 TCP/UDP 监听套接字的数字地址和程序信息" 900 | route_numeric: 901 | en_US: "Display routing table with numeric addresses" 902 | zh_CN: "显示路由表的数字地址" 903 | interfaces_extended: 904 | en_US: "Display network interfaces with extended information" 905 | zh_CN: "显示网络接口的扩展信息" 906 | statistics_tcp: 907 | en_US: "Display TCP statistics" 908 | zh_CN: "显示 TCP 统计信息" 909 | statistics_udp: 910 | en_US: "Display UDP statistics" 911 | zh_CN: "显示 UDP 统计信息" 912 | ipv4: 913 | en_US: "Show IPv4 connections only" 914 | zh_CN: "仅显示 IPv4 连接" 915 | ipv6: 916 | en_US: "Show IPv6 connections only" 917 | zh_CN: "仅显示 IPv6 连接" 918 | unix: 919 | en_US: "Show Unix domain sockets" 920 | zh_CN: "显示 Unix 域套接字" 921 | nvidia_smi: 922 | # 基础命令 923 | list_gpus: 924 | en_US: "List all NVIDIA GPUs with UUIDs" 925 | zh_CN: "列出所有 NVIDIA GPU 及其 UUID" 926 | 927 | # 查询相关命令 928 | query: 929 | en_US: "Display detailed GPU information" 930 | zh_CN: "显示详细的 GPU 信息" 931 | query_long: 932 | en_US: "Display detailed GPU information (long option)" 933 | zh_CN: "显示详细的 GPU 信息(完整选项)" 934 | query_specific: 935 | en_US: "Display detailed information for specific GPU" 936 | zh_CN: "显示指定 GPU 的详细信息" 937 | query_xml: 938 | en_US: "Display detailed information in XML format" 939 | zh_CN: "以 XML 格式显示详细信息" 940 | query_memory: 941 | en_US: "Display memory information only" 942 | zh_CN: "仅显示内存信息" 943 | query_utilization: 944 | en_US: "Display utilization information only" 945 | zh_CN: "仅显示利用率信息" 946 | query_ecc: 947 | en_US: "Display ECC error information only" 948 | zh_CN: "仅显示 ECC 错误信息" 949 | query_temperature: 950 | en_US: "Display temperature information only" 951 | zh_CN: "仅显示温度信息" 952 | query_power: 953 | en_US: "Display power information only" 954 | zh_CN: "仅显示功耗信息" 955 | query_clock: 956 | en_US: "Display clock information only" 957 | zh_CN: "仅显示时钟信息" 958 | 959 | # 循环监控命令 960 | loop: 961 | en_US: "Continuously report data at specified interval" 962 | zh_CN: "以指定间隔持续报告数据" 963 | loop_1s: 964 | en_US: "Monitor GPU status every 1 second" 965 | zh_CN: "每1秒监控 GPU 状态" 966 | loop_5s: 967 | en_US: "Monitor GPU status every 5 seconds" 968 | zh_CN: "每5秒监控 GPU 状态" 969 | loop_ms: 970 | en_US: "Continuously report data at millisecond interval" 971 | zh_CN: "以毫秒间隔持续报告数据" 972 | 973 | # 特定查询命令 974 | query_gpu_basic: 975 | en_US: "Query GPU name and memory information" 976 | zh_CN: "查询 GPU 名称和内存信息" 977 | query_utilization_csv: 978 | en_US: "Query GPU and memory utilization in CSV format" 979 | zh_CN: "以 CSV 格式查询 GPU 和内存利用率" 980 | query_temp_power: 981 | en_US: "Query temperature and power in clean CSV format" 982 | zh_CN: "以简洁 CSV 格式查询温度和功耗" 983 | query_compute_apps: 984 | en_US: "List currently active compute processes" 985 | zh_CN: "列出当前活跃的计算进程" 986 | 987 | # 拓扑和连接信息 988 | topo_matrix: 989 | en_US: "Display GPU topology matrix" 990 | zh_CN: "显示 GPU 拓扑矩阵" 991 | topo_p2p: 992 | en_US: "Display peer-to-peer topology information" 993 | zh_CN: "显示点对点拓扑信息" 994 | nvlink_status: 995 | en_US: "Display NVLink status information" 996 | zh_CN: "显示 NVLink 状态信息" 997 | 998 | # 设备修改命令(需要root权限) 999 | persistence_enable: 1000 | en_US: "Enable persistence mode (requires root)" 1001 | zh_CN: "启用持久模式(需要root权限)" 1002 | persistence_disable: 1003 | en_US: "Disable persistence mode (requires root)" 1004 | zh_CN: "禁用持久模式(需要root权限)" 1005 | compute_default: 1006 | en_US: "Set compute mode to default (requires root)" 1007 | zh_CN: "设置计算模式为默认(需要root权限)" 1008 | compute_exclusive_thread: 1009 | en_US: "Set compute mode to exclusive thread (requires root)" 1010 | zh_CN: "设置计算模式为独占线程(需要root权限)" 1011 | compute_prohibited: 1012 | en_US: "Set compute mode to prohibited (requires root)" 1013 | zh_CN: "设置计算模式为禁止(需要root权限)" 1014 | 1015 | # 时钟控制命令 1016 | lock_gpu_clocks: 1017 | en_US: "Lock GPU clocks to specified range (requires root)" 1018 | zh_CN: "锁定 GPU 时钟到指定范围(需要root权限)" 1019 | lock_memory_clocks: 1020 | en_US: "Lock memory clocks to specified range (requires root)" 1021 | zh_CN: "锁定内存时钟到指定范围(需要root权限)" 1022 | reset_gpu_clocks: 1023 | en_US: "Reset GPU clocks to default (requires root)" 1024 | zh_CN: "重置 GPU 时钟到默认值(需要root权限)" 1025 | reset_memory_clocks: 1026 | en_US: "Reset memory clocks to default (requires root)" 1027 | zh_CN: "重置内存时钟到默认值(需要root权限)" 1028 | 1029 | # 功耗管理 1030 | power_limit: 1031 | en_US: "Set power limit in watts (requires root)" 1032 | zh_CN: "设置功耗限制(瓦特,需要root权限)" 1033 | 1034 | # GPU重置 1035 | gpu_reset: 1036 | en_US: "Reset GPU (requires root, use with caution)" 1037 | zh_CN: "重置 GPU(需要root权限,谨慎使用)" 1038 | gpu_reset_bus: 1039 | en_US: "Reset GPU using bus reset (requires root)" 1040 | zh_CN: "使用总线重置 GPU(需要root权限)" 1041 | 1042 | # MIG相关 1043 | mig_enable: 1044 | en_US: "Enable Multi-Instance GPU mode (requires root)" 1045 | zh_CN: "启用多实例 GPU 模式(需要root权限)" 1046 | mig_disable: 1047 | en_US: "Disable Multi-Instance GPU mode (requires root)" 1048 | zh_CN: "禁用多实例 GPU 模式(需要root权限)" 1049 | mig_list_gpu: 1050 | en_US: "List MIG GPU instances" 1051 | zh_CN: "列出 MIG GPU 实例" 1052 | 1053 | # 输出格式化 1054 | output_file: 1055 | en_US: "Redirect output to specified file" 1056 | zh_CN: "将输出重定向到指定文件" 1057 | xml_format: 1058 | en_US: "Produce XML output format" 1059 | zh_CN: "生成 XML 输出格式" 1060 | dtd: 1061 | en_US: "Embed DTD in XML output" 1062 | zh_CN: "在 XML 输出中嵌入 DTD" 1063 | --------------------------------------------------------------------------------