feat: 补全终端交互逻辑.
This commit is contained in:
@@ -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('复制失败');
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)),
|
||||
// 回车
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
// 处理连接消息
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export const InputProtocol = {
|
||||
// 连接主机
|
||||
CONNECT: {
|
||||
type: 'co',
|
||||
template: ['type', 'sessionId', 'cols', 'rows']
|
||||
template: ['type', 'sessionId', 'terminalType', 'cols', 'rows']
|
||||
},
|
||||
// 关闭连接
|
||||
CLOSE: {
|
||||
|
||||
@@ -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;
|
||||
// 去底部
|
||||
|
||||
Reference in New Issue
Block a user