feat: 补全终端交互逻辑.

This commit is contained in:
lijiahangmax
2024-01-12 02:05:27 +08:00
parent 6c0ad50b4e
commit c1a046f30e
14 changed files with 150 additions and 52 deletions

View File

@@ -4,14 +4,14 @@ import { Message } from '@arco-design/web-vue';
export default function useCopy() {
const { copy: c } = useClipboard();
// 复制
const copy = async (value: string | undefined, tips = `${value} 已复制`) => {
const copy = async (value: string | undefined, tips: string | boolean = `${value} 已复制`) => {
try {
if (!value) {
return;
}
await c(value);
if (tips) {
Message.success(tips);
Message.success(tips as string);
}
} catch (e) {
Message.error('复制失败');

View File

@@ -17,7 +17,6 @@
</div>
</div>
</a-col>
</template>
<script lang="ts">
@@ -64,6 +63,7 @@
:deep(.arco-input-wrapper) {
background-color: var(--color-fill-3)
}
:deep(.arco-select) {
background-color: var(--color-fill-3)
}

View File

@@ -24,12 +24,12 @@
</a-row>
<a-row class="mb16" align="stretch" :gutter="16">
<!-- 右键选中词条 -->
<block-setting-item label="右键选中词条" desc="右键文本">
<block-setting-item label="右键选中词条" desc="右键文本后会根据单词分隔符自动选中词条">
<a-switch type="round"
v-model="formModel.rightClickSelectsWord" />
</block-setting-item>
<!-- 选中词条自动复制 -->
<block-setting-item label="选中词条自动复制" desc="自动将选中的词条复制到剪切板">
<!-- 选中自动复制 -->
<block-setting-item label="选中自动复制" desc="自动将选中的文本复制到剪切板">
<a-switch type="round"
v-model="formModel.selectionChangeCopy" />
</block-setting-item>
@@ -41,14 +41,14 @@
v-model="formModel.copyAutoTrim" />
</block-setting-item>
<!-- 粘贴去除空格 -->
<block-setting-item label="粘贴去除空格" desc="粘贴文本前自动删除尾部空格">
<block-setting-item label="粘贴去除空格" desc="粘贴文本前自动删除尾部空格 如: 命令输入框, 命令编辑器, 右键粘贴, 粘贴按钮, 右键菜单粘贴, 自定义粘贴快捷键. (系统快捷键无法干预 如: ctrl + shift + v, shift + insert)">
<a-switch type="round"
v-model="formModel.pasteAutoTrim" />
</block-setting-item>
</a-row>
<a-row class="mb16" align="stretch" :gutter="16">
<!-- 右键粘贴 -->
<block-setting-item label="右键粘贴" desc="右键自动粘贴, 启用后需要关闭右键菜单">
<block-setting-item label="右键粘贴" desc="右键自动粘贴, 启用后需要关闭右键菜单 (若开启了右键选中词条, 有选中的文本时, 右键粘贴无效)">
<a-switch type="round"
v-model="formModel.rightClickPaste" />
</block-setting-item>

View File

@@ -83,8 +83,9 @@
const session = ref<ITerminalSession>();
// FIXME
// 右键菜单补充
// 搜索 search color 配置
// 右键菜单补充 启用右键菜单 enableRightClickMenu 粘贴逻辑
// 快捷键补充 粘贴逻辑
// 截屏
// 主机获取逻辑 最近连接逻辑
@@ -100,7 +101,7 @@
// 发送命令
const writeCommand = (value: string) => {
if (session.value?.canWrite) {
session.value.paste(value);
session.value.pasteTrimEnd(value);
}
};
@@ -129,9 +130,9 @@
// 全选
checkAll: () => session.value?.selectAll(),
// 复制选中部分
copy: () => copy(session.value?.getSelection(), '已复制'),
copy: () => session.value?.copySelection(),
// 粘贴
paste: async () => session.value?.paste(await readText()),
paste: async () => session.value?.pasteTrimEnd(await readText()),
// ctrl + c
interrupt: () => session.value?.paste(String.fromCharCode(3)),
// 回车

View File

@@ -1,11 +1,7 @@
import {
ITerminalChannel,
ITerminalOutputProcessor,
ITerminalSessionManager,
OutputPayload
} from '../types/terminal.type';
import { ITerminalChannel, ITerminalOutputProcessor, ITerminalSessionManager, OutputPayload } from '../types/terminal.type';
import { InputProtocol } from '../types/terminal.protocol';
import { TerminalStatus } from '../types/terminal.const';
import { useTerminalStore } from '@/store';
// 终端输出消息体处理器实现
export default class TerminalOutputProcessor implements ITerminalOutputProcessor {
@@ -29,8 +25,14 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
session.status = TerminalStatus.CLOSED;
return;
}
const { preference } = useTerminalStore();
// 发送 connect 命令
this.channel.send(InputProtocol.CONNECT, { sessionId, cols: session.inst.cols, rows: session.inst.rows });
this.channel.send(InputProtocol.CONNECT, {
sessionId,
terminalType: preference.sessionSetting.terminalEmulationType || 'xterm',
cols: session.inst.cols,
rows: session.inst.rows
});
}
// 处理连接消息

View File

@@ -1,14 +1,20 @@
import type { UnwrapRef } from 'vue';
import type { TerminalPreference } from '@/store/modules/terminal/types';
import type { ITerminalChannel, ITerminalSession, TerminalAddons } from '../types/terminal.type';
import { useTerminalStore } from '@/store';
import { fontFamilySuffix, TerminalStatus } from '../types/terminal.const';
import { InputProtocol } from '../types/terminal.protocol';
import { ITerminalOptions, Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { WebglAddon } from 'xterm-addon-webgl';
import { WebLinksAddon } from 'xterm-addon-web-links';
import { SearchAddon } from 'xterm-addon-search';
import { ImageAddon } from 'xterm-addon-image';
import { CanvasAddon } from 'xterm-addon-canvas';
import { WebglAddon } from 'xterm-addon-webgl';
import { playBell } from '@/utils/bell';
import useCopy from '@/hooks/copy';
const copy = useCopy();
// 终端会话实现
export default class TerminalSession implements ITerminalSession {
@@ -49,33 +55,29 @@ export default class TerminalSession implements ITerminalSession {
this.inst = new Terminal({
...(preference.displaySetting as any),
theme: preference.theme.schema,
fastScrollModifier: 'alt',
fastScrollModifier: !!preference.interactSetting.fastScrollModifier ? 'alt' : 'none',
altClickMovesCursor: !!preference.interactSetting.altClickMovesCursor,
rightClickSelectsWord: !!preference.interactSetting.rightClickSelectsWord,
fontFamily: preference.displaySetting.fontFamily + fontFamilySuffix,
wordSeparator: preference.interactSetting.wordSeparator,
scrollback: preference.sessionSetting.scrollBackLine,
});
// 注册快捷键
// 注册事件
this.registerEvent(dom, preference);
// 注册插件
this.addons.fit = new FitAddon();
// this.addons.webgl = new WebglAddon();
this.addons.canvas = new CanvasAddon();
this.addons.link = new WebLinksAddon();
this.addons.search = new SearchAddon();
this.addons.image = new ImageAddon();
for (const addon of Object.values(this.addons)) {
this.inst.loadAddon(addon);
}
this.registerAddions(preference);
// 打开终端
this.inst.open(dom);
// 自适应
this.addons.fit.fit();
}
// 设置已连接
connect(): void {
this.status = TerminalStatus.CONNECTED;
this.connected = true;
this.inst.focus();
// 注册事件
private registerEvent(dom: HTMLElement, preference: UnwrapRef<TerminalPreference>) {
// 注册输入事件
this.inst.onData(s => {
if (!this.canWrite) {
if (!this.canWrite || !this.connected) {
return;
}
// 输入
@@ -84,14 +86,81 @@ export default class TerminalSession implements ITerminalSession {
command: s
});
});
// 启用响铃
if (preference.interactSetting.enableBell) {
this.inst.onBell(() => {
// 播放蜂鸣
playBell();
});
}
// 选中复制
if (preference.interactSetting.selectionChangeCopy) {
this.inst.onSelectionChange(() => {
// 复制选中内容
this.copySelection();
});
}
// 注册 resize 事件
this.inst.onResize(({ cols, rows }) => {
if (!this.connected) {
return;
}
this.channel.send(InputProtocol.RESIZE, {
sessionId: this.sessionId,
cols,
rows
});
});
// 设置右键选项
dom.addEventListener('contextmenu', async (event) => {
// 如果开启了右键粘贴 右键选中 右键菜单 则关闭默认右键菜单
if (preference.interactSetting.rightClickSelectsWord
|| preference.interactSetting.rightClickPaste
|| preference.interactSetting.enableRightClickMenu) {
event.preventDefault();
}
// 右键粘贴逻辑
if (preference.interactSetting.rightClickPaste) {
if (!this.canWrite || !this.connected) {
return;
}
// 未开启右键选中 || 开启并无选中的内容则粘贴
if (!preference.interactSetting.rightClickSelectsWord || !this.inst.hasSelection()) {
this.pasteTrimEnd(await copy.readText());
}
}
});
}
// 注册插件
private registerAddions(preference: UnwrapRef<TerminalPreference>) {
this.addons.fit = new FitAddon();
this.addons.search = new SearchAddon();
// 超链接插件
if (preference.pluginsSetting.enableWeblinkPlugin) {
this.addons.weblink = new WebLinksAddon();
}
if (preference.pluginsSetting.enableWebglPlugin) {
// WebGL 渲染插件
this.addons.webgl = new WebglAddon();
} else {
// canvas 渲染插件
this.addons.canvas = new CanvasAddon();
}
// 图片渲染插件
if (preference.pluginsSetting.enableImagePlugin) {
this.addons.image = new ImageAddon();
}
for (const addon of Object.values(this.addons)) {
this.inst.loadAddon(addon);
}
}
// 设置已连接
connect(): void {
this.status = TerminalStatus.CONNECTED;
this.connected = true;
this.inst.focus();
}
// 设置是否可写
@@ -132,15 +201,36 @@ export default class TerminalSession implements ITerminalSession {
this.inst.focus();
}
// 粘贴并且去除尾部空格 (如果配置)
pasteTrimEnd(value: string): void {
if (useTerminalStore().preference.interactSetting.pasteAutoTrim) {
// 粘贴前去除尾部空格
this.inst.paste(value.trimEnd());
} else {
this.inst.paste(value);
}
this.inst.focus();
}
// 选中全部
selectAll(): void {
this.inst.selectAll();
this.inst.focus();
}
// 获取选中
getSelection(): string {
const selection = this.inst.getSelection();
// 复制选中
copySelection(): string {
let selection = this.inst.getSelection();
if (selection) {
// 去除尾部空格
const { preference } = useTerminalStore();
if (preference.interactSetting.copyAutoTrim) {
selection = selection.trimEnd();
}
// 复制
copy.copy(selection, false);
}
// 聚焦
this.inst.focus();
return selection;
}

View File

@@ -8,7 +8,7 @@ export const InputProtocol = {
// 连接主机
CONNECT: {
type: 'co',
template: ['type', 'sessionId', 'cols', 'rows']
template: ['type', 'sessionId', 'terminalType', 'cols', 'rows']
},
// 关闭连接
CLOSE: {

View File

@@ -119,7 +119,7 @@ export interface TerminalAddons {
fit: FitAddon;
webgl: WebglAddon;
canvas: CanvasAddon;
link: WebLinksAddon;
weblink: WebLinksAddon;
search: SearchAddon;
image: ImageAddon;
}
@@ -152,10 +152,12 @@ export interface ITerminalSession {
clear: () => void;
// 粘贴
paste: (value: string) => void;
// 粘贴并且去除尾部空格 (如果配置)
pasteTrimEnd: (value: string) => void;
// 选中全部
selectAll: () => void;
// 获取选中
getSelection: () => string;
// 复制选中
copySelection: () => string;
// 去顶部
toTop: () => void;
// 去底部