feat: 终端全局快捷键.

This commit is contained in:
lijiahang
2024-01-16 17:25:01 +08:00
parent 80829b51c1
commit 220a0d4035
9 changed files with 202 additions and 146 deletions

View File

@@ -57,7 +57,7 @@ export default defineStore('terminal', {
pluginsSetting: {} as TerminalPluginsSetting,
sessionSetting: {} as TerminalSessionSetting,
shortcutSetting: {
enabled: true,
enabled: false,
keys: []
} as TerminalShortcutSetting,
},
@@ -79,7 +79,16 @@ export default defineStore('terminal', {
// 更新默认主题偏好
await this.updateTerminalPreference(TerminalPreferenceItem.THEME, data.theme);
}
// 选择赋值
// 移除禁用的快捷键
if (data.shortcutSetting?.enabled) {
data.shortcutSetting.keys = data.shortcutSetting.keys.filter(s => s.enabled);
} else {
data.shortcutSetting = {
enabled: false,
keys: []
};
}
// 选择赋值 (不能修改引用)
const keys = Object.keys(this.preference);
keys.forEach(key => {
const item = data[key as keyof TerminalPreference];

View File

@@ -76,9 +76,10 @@ export interface TerminalShortcutSetting {
// 终端快捷键
export interface TerminalShortcutKey {
option: string;
item: string;
ctrlKey: boolean;
shiftKey: boolean;
altKey: boolean;
code: string;
enabled: boolean;
}

View File

@@ -35,9 +35,10 @@
</script>
<script lang="ts" setup>
import { TerminalTabType, InnerTabs } from '../../types/terminal.const';
import { TerminalTabType, InnerTabs, TerminalTabShortcutItems } from '../../types/terminal.const';
import { useTerminalStore } from '@/store';
import { watch } from 'vue';
import { onMounted, onUnmounted, watch } from 'vue';
import { addEventListen, removeEventListen } from '@/utils/event';
import EmptyRecommend from './empty-recommend.vue';
import NewConnectionView from '../new-connection/new-connection-view.vue';
import TerminalDisplaySetting from '../view-setting/terminal-display-setting.vue';
@@ -45,29 +46,91 @@
import TerminalGeneralSetting from '../view-setting/terminal-general-setting.vue';
import TerminalView from '../xterm/terminal-view.vue';
const { tabManager, sessionManager } = useTerminalStore();
const { preference, tabManager, sessionManager } = useTerminalStore();
// 监听 tab 修改
watch(() => tabManager.active, active => {
if (!active) {
watch(() => tabManager.active, (active, before) => {
if (before) {
// 失焦已经切换的终端
const beforeTab = tabManager.items.find(s => s.key === before);
if (beforeTab && beforeTab?.type === TerminalTabType.TERMINAL) {
sessionManager.getSession(before)?.blur();
}
}
if (active) {
// 获取 activeTab
const activeTab = tabManager.items.find(s => s.key === active);
if (!activeTab) {
return;
}
// 修改标题
document.title = activeTab.title;
// 终端自动聚焦
if (activeTab?.type === TerminalTabType.TERMINAL) {
sessionManager.getSession(active)?.focus();
}
} else {
// 修改标题
document.title = '主机终端';
return;
}
// 获取 tab
const tab = tabManager.items.find(s => s.key === active);
if (!tab) {
return;
}
// 修改标题
document.title = tab.title;
// terminal 自动聚焦
if (tab?.type === TerminalTabType.TERMINAL) {
sessionManager.getSession(active)?.focus();
}
});
// TODO 快捷键逻辑 主机加载逻辑 加载中逻辑
// 处理快捷键逻辑
const handlerKeyboard = (event: Event) => {
// 当前页面非 terminal 的时候再触发快捷键 (terminal 有内置逻辑)
if (tabManager.active
&& tabManager.items.find(s => s.key === tabManager.active)?.type === TerminalTabType.TERMINAL) {
return;
}
const e = event as KeyboardEvent;
// 检测触发的快捷键
const key = preference.shortcutSetting.keys.find(key => {
return key.code === e.code
&& key.altKey === e.altKey
&& key.shiftKey === e.shiftKey
&& key.ctrlKey === e.ctrlKey;
});
if (!key) {
return;
}
// 触发逻辑
switch (key.item) {
case TerminalTabShortcutItems.CLOSE_TAB.item:
// 关闭 tab
if (tabManager.active) {
tabManager.deleteTab(tabManager.active);
}
break;
case TerminalTabShortcutItems.CHANGE_TO_PREV_TAB.item:
// 切换至前一个 tab
tabManager.changeToPrevTab();
break;
case TerminalTabShortcutItems.CHANGE_TO_NEXT_TAB.item:
// 切换至后一个 tab
tabManager.changeToNextTab();
break;
case TerminalTabShortcutItems.OPEN_NEW_CONNECT_TAB.item:
// 切换到新建连接 tab
tabManager.openTab(InnerTabs.NEW_CONNECTION);
break;
default:
break;
}
};
// 监听键盘事件
onMounted(() => {
if (preference.shortcutSetting.enabled) {
addEventListen(window, 'keydown', handlerKeyboard);
}
});
// 移除键盘事件
onUnmounted(() => {
if (preference.shortcutSetting.enabled) {
removeEventListen(window, 'keydown', handlerKeyboard);
}
});
</script>

View File

@@ -41,7 +41,7 @@
v-model="formModel.copyAutoTrim" />
</block-setting-item>
<!-- 粘贴去除空格 -->
<block-setting-item label="粘贴去除空格" desc="粘贴文本前自动删除尾部空格 如: 命令输入框, 命令编辑器, 右键粘贴, 粘贴按钮, 右键菜单粘贴, 自定义粘贴快捷键. (系统快捷键无法干预 如: ctrl + shift + v, shift + insert)">
<block-setting-item label="粘贴去除空格" desc="粘贴文本前自动删除尾部空格 如: 命令输入框, 命令编辑器, 右键粘贴, 粘贴按钮, 右键菜单粘贴, 自定义粘贴快捷键">
<a-switch type="round"
v-model="formModel.pasteAutoTrim" />
</block-setting-item>

View File

@@ -29,94 +29,7 @@ export default class TerminalSessionHandler implements ITerminalSessionHandler {
this.domRef = domRef;
const { preference, tabManager } = useTerminalStore();
this.interactSetting = preference.interactSetting;
// this.shortcutKeys = preference.shortcutSetting.keys;
this.shortcutKeys = [
{
option: 'copy',
ctrlKey: true,
shiftKey: true,
altKey: false,
code: 'KeyC'
}, {
option: 'paste',
ctrlKey: true,
shiftKey: true,
altKey: false,
code: 'KeyV'
}, {
option: 'toTop',
ctrlKey: true,
shiftKey: true,
altKey: false,
code: 'ArrowUp'
}, {
option: 'toBottom',
ctrlKey: true,
shiftKey: true,
altKey: false,
code: 'ArrowDown'
}, {
option: 'selectAll',
ctrlKey: true,
shiftKey: true,
altKey: false,
code: 'KeyA'
}, {
option: 'search',
ctrlKey: true,
shiftKey: true,
altKey: false,
code: 'KeyF'
}, {
option: 'fontSizePlus',
ctrlKey: true,
shiftKey: false,
altKey: true,
code: 'Equal'
}, {
option: 'fontSizeSubtract',
ctrlKey: true,
shiftKey: false,
altKey: true,
code: 'Minus'
}, {
option: 'commandEditor',
ctrlKey: true,
shiftKey: false,
altKey: true,
code: 'KeyE'
}, {
option: 'close',
ctrlKey: true,
shiftKey: false,
altKey: true,
code: 'KeyW'
}, {
option: 'changeToPrev',
ctrlKey: true,
shiftKey: false,
altKey: true,
code: 'ArrowLeft'
}, {
option: 'changeToNext',
ctrlKey: true,
shiftKey: false,
altKey: true,
code: 'ArrowRight'
}, {
option: 'openCopyTerminal',
ctrlKey: true,
shiftKey: false,
altKey: true,
code: 'KeyO'
}, {
option: 'openNewConnect',
ctrlKey: true,
shiftKey: false,
altKey: true,
code: 'KeyN'
},
];
this.shortcutKeys = preference.shortcutSetting.keys;
this.tabManager = tabManager;
}
@@ -158,7 +71,7 @@ export default class TerminalSessionHandler implements ITerminalSessionHandler {
});
if (key) {
// 调用处理方法
this.invokeHandle.call(this, key.option);
this.invokeHandle.call(this, key.item);
return false;
} else {
return true;
@@ -271,22 +184,22 @@ export default class TerminalSessionHandler implements ITerminalSessionHandler {
}
// 切换到前一个 tab
changeToPrev() {
this.tabManager.changeToPrev();
changeToPrevTab() {
this.tabManager.changeToPrevTab();
}
// 切换到后一个 tab
changeToNext() {
this.tabManager.changeToNext();
changeToNextTab() {
this.tabManager.changeToNextTab();
}
// 复制终端
openCopyTerminal() {
// 复制终端 tab
openCopyTerminalTab() {
useTerminalStore().openCopyTerminal(this.session.hostId);
}
// 打开新建连接页面
openNewConnect() {
// 打开新建连接 tab
openNewConnectTab() {
this.tabManager.openTab(InnerTabs.NEW_CONNECTION);
}

View File

@@ -2,8 +2,8 @@ import type { UnwrapRef } from 'vue';
import type { TerminalPreference } from '@/store/modules/terminal/types';
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';
import { fontFamilySuffix, TerminalStatus } from '../types/terminal.const';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { WebLinksAddon } from 'xterm-addon-web-links';
@@ -13,6 +13,7 @@ import { CanvasAddon } from 'xterm-addon-canvas';
import { WebglAddon } from 'xterm-addon-webgl';
import { playBell } from '@/utils/bell';
import TerminalSessionHandler from './terminal-session-handler';
import { addEventListen } from '@/utils/event';
// 终端会话实现
export default class TerminalSession implements ITerminalSession {
@@ -82,16 +83,14 @@ export default class TerminalSession implements ITerminalSession {
// 处理自定义按键
this.inst.attachCustomKeyEventHandler((e: KeyboardEvent) => {
e.preventDefault();
// 未开启
if (!preference.shortcutSetting.enabled) {
return true;
// 触发快捷键检测
if (e.type === 'keydown'
&& preference.shortcutSetting.enabled
&& preference.shortcutSetting.keys.length) {
// 触发快捷键
return this.handler.triggerShortcutKey(e);
}
// 只监听 keydown 事件
if (e.type !== 'keydown') {
return true;
}
// 触发快捷键
return this.handler.triggerShortcutKey(e);
return true;
});
}
@@ -134,7 +133,7 @@ export default class TerminalSession implements ITerminalSession {
});
});
// 设置右键选项
dom.addEventListener('contextmenu', async () => {
addEventListen(dom, 'contextmenu', async () => {
// 右键粘贴逻辑
if (preference.interactSetting.rightClickPaste) {
if (!this.canWrite || !this.connected) {
@@ -195,16 +194,21 @@ export default class TerminalSession implements ITerminalSession {
this.inst.write(value);
}
// 自适应
fit(): void {
this.addons.fit?.fit();
}
// 聚焦
focus(): void {
this.inst.focus();
}
// 失焦
blur(): void {
this.inst.blur();
}
// 自适应
fit(): void {
this.addons.fit?.fit();
}
// 查找
find(word: string, next: boolean, options: ISearchOptions): void {
if (next) {
@@ -229,7 +233,7 @@ export default class TerminalSession implements ITerminalSession {
Object.values(this.addons)
.filter(Boolean)
.forEach(s => s.dispose());
// 卸载实体
// 卸载终端
this.inst.dispose();
} catch (e) {
}

View File

@@ -45,12 +45,12 @@ export default class TerminalTabManager implements ITerminalTabManager {
}
// 切换到前一个 tab
changeToPrev() {
changeToPrevTab() {
this.changeToIndex(this.getCurrentTabIndex() - 1);
}
// 切换到后一个 tab
changeToNext() {
changeToNextTab() {
this.changeToIndex(this.getCurrentTabIndex() + 1);
}

View File

@@ -127,6 +127,66 @@ export const ActionBarItems = [
}
];
// 终端 tab 快捷键操作
export const TerminalTabShortcutItems = {
CHANGE_TO_PREV_TAB: {
item: 'changeToPrevTab',
content: '切换为前一个 tab'
},
CHANGE_TO_NEXT_TAB: {
item: 'changeToNextTab',
content: '切换为后一个 tab'
},
CLOSE_TAB: {
item: 'closeTab',
content: '关闭当前 tab'
},
OPEN_NEW_CONNECT_TAB: {
item: 'openNewConnectTab',
content: '打开新建连接 tab'
},
OPEN_COPY_TERMINAL_TAB: {
item: 'openCopyTerminalTab',
content: '复制当前终端 tab'
},
COPY: {
item: 'copy',
content: '复制'
},
PASTE: {
item: 'paste',
content: '粘贴'
},
TO_TOP: {
item: 'toTop',
content: '去顶部'
},
TO_BOTTOM: {
item: 'toBottom',
content: '去底部'
},
SELECT_ALL: {
item: 'selectAll',
content: '全选'
},
SEARCH: {
item: 'search',
content: '搜索'
},
FONT_SIZE_PLUS: {
item: 'fontSizePlus',
content: '增大字号'
},
FONT_SIZE_SUBTRACT: {
item: 'fontSizeSubtract',
content: '减小字号'
},
COMMAND_EDITOR: {
item: 'commandEditor',
content: '命令编辑器'
},
};
// 打开 sshModal key
export const openSshModalKey = Symbol();

View File

@@ -45,6 +45,12 @@ export interface ContextMenuItem {
content: string;
}
// 快捷键元素
export interface ShortcutKeyItem {
item: string;
content: string;
}
// ssh 额外配置
export interface SshExtraModel {
authType?: string;
@@ -98,9 +104,9 @@ export interface ITerminalTabManager {
// 打开 tab
openTab: (tab: TerminalTabItem) => void;
// 切换到前一个 tab
changeToPrev: () => void;
changeToPrevTab: () => void;
// 切换到后一个 tab
changeToNext: () => void;
changeToNextTab: () => void;
// 切换索引 tab
changeToIndex: (index: number) => void;
// 清空
@@ -232,11 +238,11 @@ export interface ITerminalSessionHandler {
// 关闭 tab
closeTab: () => void;
// 切换到前一个 tab
changeToPrev: () => void;
changeToPrevTab: () => void;
// 切换到后一个 tab
changeToNext: () => void;
// 复制终端
openCopyTerminal: () => void;
// 打开新建连接页面
openNewConnect: () => void;
changeToNextTab: () => void;
// 复制终端 tab
openCopyTerminalTab: () => void;
// 打开新建连接 tab
openNewConnectTab: () => void;
}