refactor: 终端操作抽象.

This commit is contained in:
lijiahang
2024-01-15 19:09:12 +08:00
parent 826907380b
commit 0977ef0703
10 changed files with 470 additions and 58 deletions

View File

@@ -0,0 +1,167 @@
import type { TerminalPreference } from '@/store/modules/terminal/types';
import type { ITerminalSession, ITerminalSessionHandler, ITerminalTabManager, TerminalDomRef } from '../types/terminal.type';
import type { Terminal } from 'xterm';
import { useTerminalStore } from '@/store';
import useCopy from '@/hooks/copy';
const { copy: copyValue, readText } = useCopy();
// 终端会话处理器实现
export default class TerminalSessionHandler implements ITerminalSessionHandler {
private readonly domRef: TerminalDomRef;
private readonly inst: Terminal;
private readonly session: ITerminalSession;
private readonly preference: TerminalPreference;
private readonly tabManager: ITerminalTabManager;
constructor(session: ITerminalSession,
domRef: TerminalDomRef) {
this.session = session;
this.inst = session.inst;
this.domRef = domRef;
const { preference, tabManager } = useTerminalStore();
this.preference = preference;
this.tabManager = tabManager;
}
// 启用状态
enabledStatus(option: string): boolean {
switch (option) {
case 'paste':
case 'interrupt':
case 'enter':
case 'commandEditor':
return this.session.canWrite;
case 'disconnect':
return this.session.connected;
default:
return true;
}
}
// 复制选中
copy() {
let selection = this.inst.getSelection();
if (selection) {
// 去除尾部空格
if (this.preference.interactSetting.copyAutoTrim) {
selection = selection.trimEnd();
}
// 复制
copyValue(selection, false);
}
// 聚焦
this.inst.focus();
}
// 粘贴
paste() {
if (this.enabledStatus('paste')) {
readText().then(s => this.pasteTrimEnd(s));
}
}
// 粘贴并且去除尾部空格 (如果配置)
pasteTrimEnd(value: string) {
if (this.enabledStatus('paste')) {
if (this.preference.interactSetting.pasteAutoTrim) {
// 粘贴前去除尾部空格
this.inst.paste(value.trimEnd());
} else {
this.inst.paste(value);
}
this.inst.focus();
}
}
// 选中全部
selectAll() {
this.inst.selectAll();
this.inst.focus();
}
// 去顶部
toTop(): void {
this.inst.scrollToTop();
this.inst.focus();
}
// 去底部
toBottom(): void {
this.inst.scrollToBottom();
this.inst.focus();
}
// 打开搜索
search() {
this.domRef.searchModal?.toggle();
}
// 增大字号
fontSizePlus() {
this.fontSizeAdd(1);
}
// 减小字号
fontSizeSubtract() {
this.fontSizeAdd(-1);
}
// 字号增加
private fontSizeAdd(addSize: number) {
this.inst.options['fontSize'] = this.inst.options['fontSize'] as number + addSize;
if (this.session.connected) {
this.session.fit();
this.inst.focus();
}
}
// 打开命令编辑器
commandEditor() {
if (this.enabledStatus('commandEditor')) {
this.domRef.editorModal?.open('', '');
}
}
// ctrl + c
interrupt() {
if (this.enabledStatus('interrupt')) {
this.inst.paste(String.fromCharCode(3));
}
}
// 回车
enter() {
if (this.enabledStatus('enter')) {
this.inst.paste(String.fromCharCode(13));
}
}
// 清空
clear() {
this.inst.clear();
}
// 断开连接
disconnect() {
if (this.enabledStatus('disconnect')) {
this.session.disconnect();
}
}
// 关闭
close() {
this.tabManager.deleteTab(this.session.sessionId);
}
// 聚焦
focus() {
this.inst.focus();
}
}

View File

@@ -1,4 +1,4 @@
import type { ITerminalChannel, ITerminalSession, ITerminalSessionManager, TerminalTabItem } from '../types/terminal.type';
import type { ITerminalChannel, ITerminalSession, ITerminalSessionManager, TerminalDomRef, TerminalTabItem } from '../types/terminal.type';
import { sleep } from '@/utils';
import { InputProtocol } from '../types/terminal.protocol';
import { useDebounceFn } from '@vueuse/core';
@@ -24,7 +24,8 @@ export default class TerminalSessionManager implements ITerminalSessionManager {
}
// 打开终端会话
async openSession(tab: TerminalTabItem, dom: HTMLElement) {
async openSession(tab: TerminalTabItem,
domRef: TerminalDomRef) {
const sessionId = tab.key;
const hostId = tab.hostId as number;
// 初始化客户端
@@ -36,7 +37,7 @@ export default class TerminalSessionManager implements ITerminalSessionManager {
this.channel
);
// 初始化
session.init(dom);
session.init(domRef);
// 等待前端渲染完成
await sleep(100);
// 添加会话

View File

@@ -1,6 +1,6 @@
import type { UnwrapRef } from 'vue';
import type { TerminalPreference } from '@/store/modules/terminal/types';
import type { ITerminalChannel, ITerminalSession, TerminalAddons } from '../types/terminal.type';
import type { ITerminalChannel, ITerminalSession, ITerminalSessionHandler, TerminalAddons, TerminalDomRef } from '../types/terminal.type';
import { useTerminalStore } from '@/store';
import { fontFamilySuffix, TerminalStatus } from '../types/terminal.const';
import { InputProtocol } from '../types/terminal.protocol';
@@ -13,13 +13,17 @@ import { CanvasAddon } from 'xterm-addon-canvas';
import { WebglAddon } from 'xterm-addon-webgl';
import { playBell } from '@/utils/bell';
import useCopy from '@/hooks/copy';
import TerminalShortcutDispatcher from './terminal-shortcut-dispatch';
import TerminalSessionHandler from './terminal-session-handler';
const copy = useCopy();
// 终端会话实现
export default class TerminalSession implements ITerminalSession {
public hostId: number;
public readonly hostId: number;
public sessionId: string;
public inst: Terminal;
@@ -29,7 +33,7 @@ export default class TerminalSession implements ITerminalSession {
public status: number;
private readonly sessionId: string;
public handler: ITerminalSessionHandler;
private readonly channel: ITerminalChannel;
@@ -45,11 +49,12 @@ export default class TerminalSession implements ITerminalSession {
this.canWrite = false;
this.status = TerminalStatus.CONNECTING;
this.inst = undefined as unknown as Terminal;
this.handler = undefined as unknown as ITerminalSessionHandler;
this.addons = {} as TerminalAddons;
}
// 初始化
init(dom: HTMLElement): void {
init(domRef: TerminalDomRef): void {
const { preference } = useTerminalStore();
// 初始化实例
this.inst = new Terminal({
@@ -62,17 +67,39 @@ export default class TerminalSession implements ITerminalSession {
wordSeparator: preference.interactSetting.wordSeparator,
scrollback: preference.sessionSetting.scrollBackLine,
});
// 处理器
this.handler = new TerminalSessionHandler(this, domRef);
// 注册快捷键
this.registerShortcut(preference);
// 注册事件
this.registerEvent(dom, preference);
this.registerEvent(domRef.el, preference);
// 注册插件
this.registerAddions(preference);
this.registerAddons(preference);
// 打开终端
this.inst.open(dom);
this.inst.open(domRef.el);
// 自适应
this.addons.fit.fit();
}
// 注册快捷键
private registerShortcut(preference: UnwrapRef<TerminalPreference>) {
const dispatcher = new TerminalShortcutDispatcher(this, preference.shortcutSetting.keys);
// 处理自定义按键
this.inst.attachCustomKeyEventHandler((e: KeyboardEvent) => {
e.preventDefault();
// 未开启
if (!preference.shortcutSetting.enabled) {
return true;
}
// 只监听 keydown 事件
if (e.type !== 'keydown') {
return true;
}
// 调度快捷键
return dispatcher.dispatch(e);
});
}
// 注册事件
private registerEvent(dom: HTMLElement, preference: UnwrapRef<TerminalPreference>) {
// 注册输入事件
@@ -127,7 +154,7 @@ export default class TerminalSession implements ITerminalSession {
}
// 注册插件
private registerAddions(preference: UnwrapRef<TerminalPreference>) {
private registerAddons(preference: UnwrapRef<TerminalPreference>) {
this.addons.fit = new FitAddon();
this.addons.search = new SearchAddon();
// 超链接插件

View File

@@ -0,0 +1,167 @@
import type { TerminalShortcutKey } from '@/store/modules/terminal/types';
import type { ITerminalSession, ITerminalShortcutDispatcher } from '../types/terminal.type';
import useCopy from '@/hooks/copy';
const { readText } = useCopy();
// 终端快捷键调度实现
export default class TerminalShortcutDispatch implements ITerminalShortcutDispatcher {
private readonly session: ITerminalSession;
private readonly keys: Array<TerminalShortcutKey>;
constructor(session: ITerminalSession, keys: Array<TerminalShortcutKey>) {
this.session = session;
// this.keys = keys;
this.keys = [
{
option: 'copy',
ctrlKey: true,
shiftKey: true,
altKey: false,
key: 'C'
}, {
option: 'paste',
ctrlKey: true,
shiftKey: true,
altKey: false,
key: 'V'
}, {
option: 'toTop',
ctrlKey: true,
shiftKey: true,
altKey: false,
key: 'ArrowUp'
}, {
option: 'toBottom',
ctrlKey: true,
shiftKey: true,
altKey: false,
key: 'ArrowDown'
}, {
option: 'selectAll',
ctrlKey: true,
shiftKey: true,
altKey: false,
key: 'A'
}, {
option: 'search',
ctrlKey: true,
shiftKey: true,
altKey: false,
key: 'F'
}, {
option: 'fontSizePlus',
ctrlKey: true,
shiftKey: true,
altKey: false,
key: '+'
}, {
option: 'fontSizeSubtract',
ctrlKey: true,
shiftKey: true,
altKey: false,
key: '_'
}, {
option: 'commandEditor',
ctrlKey: true,
shiftKey: true,
altKey: false,
key: 'O'
}, {
option: 'close',
ctrlKey: true,
shiftKey: true,
altKey: false,
key: 'W'
}
];
}
// 调度快捷键
dispatch(e: KeyboardEvent): boolean {
console.log(e);
for (const key of this.keys) {
if (key.altKey === e.altKey
&& key.shiftKey === e.shiftKey
&& key.ctrlKey === e.ctrlKey
&& key.key === e.key) {
const runner = this[key.option as keyof this] as () => void;
runner && runner.apply(this);
return false;
}
}
return true;
}
// 复制
private copy(): void {
this.session.copySelection();
}
// 粘贴
private paste(): void {
// FIXME status
readText().then((e) => {
this.session.pasteTrimEnd(e);
});
}
// 去顶部
private toTop(): void {
this.session.toTop();
}
// 去底部
private toBottom(): void {
this.session.toBottom();
}
// 全选
private selectAll(): void {
this.session.selectAll();
}
// 搜索
private search(): void {
// fixme
}
// 增大字号
private fontSizePlus(): void {
this.fontSizeAdd(1);
}
// 减小字号
private fontSizeSubtract(): void {
this.fontSizeAdd(-1);
}
// 字号增加
private fontSizeAdd(addSize: number) {
this.session.setOption('fontSize', this.session.getOption('fontSize') + addSize);
if (this.session.connected) {
this.session.fit();
this.session.focus();
}
}
// 命令编辑器
private commandEditor(): void {
// fixme
}
// 关闭终端
private close(): void {
}
private to(): void {
}
// 切换 tab
// 打开 新
}