feat: 添加终端交互配置项.

This commit is contained in:
lijiahangmax
2024-01-12 00:08:56 +08:00
parent 13c352691a
commit cacc7de364
14 changed files with 387 additions and 77 deletions

View File

@@ -2,6 +2,7 @@ package com.orion.ops.module.asset.handler.host.terminal.session;
import com.orion.lang.utils.io.Streams;
import com.orion.net.host.SessionStore;
import com.orion.net.host.ssh.TerminalType;
import com.orion.net.host.ssh.shell.ShellExecutor;
import com.orion.ops.framework.common.constant.Const;
import com.orion.ops.framework.websocket.core.utils.WebSockets;
@@ -66,6 +67,8 @@ public class TerminalSession implements ITerminalSession {
// 打开 shell
this.executor = sessionStore.getShellExecutor();
executor.size(cols, rows);
// FIXME
executor.terminalType(TerminalType.XTERM.getType());
executor.streamHandler(this::streamHandler);
executor.callback(this::eofCallback);
executor.connect();

View File

@@ -1,7 +1,7 @@
package com.orion.ops.module.infra.handler.preference.model;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.orion.lang.able.IJsonObject;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -33,14 +33,20 @@ public class TerminalPreferenceModel implements PreferenceModel {
@Schema(description = "操作栏设置")
private JSONObject actionBarSetting;
@Schema(description = "背景设置")
private JSONObject backgroundSetting;
@Schema(description = "交互设置")
private JSONObject interactSetting;
@Schema(description = "插件设置")
private JSONObject pluginsSetting;
@Schema(description = "会话设置")
private JSONObject sessionSetting;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class DisplaySettingModel {
public static class DisplaySettingModel implements IJsonObject {
@Schema(description = "字体样式")
private String fontFamily;
@@ -63,15 +69,75 @@ public class TerminalPreferenceModel implements PreferenceModel {
@Schema(description = "光标闪烁")
private Boolean cursorBlink;
/**
* 转为 json
*
* @return json
*/
public JSONObject toJson() {
return JSON.parseObject(JSON.toJSONString(this));
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class InteractSettingModel implements IJsonObject {
@Schema(description = "快速滚动")
private Boolean fastScrollModifier;
@Schema(description = "点击移动光标")
private Boolean altClickMovesCursor;
@Schema(description = "右键选中词条")
private Boolean rightClickSelectsWord;
@Schema(description = "选中词条自动复制")
private Boolean selectionChangeCopy;
@Schema(description = "复制去除空格")
private Boolean copyAutoTrim;
@Schema(description = "粘贴去除空格")
private Boolean pasteAutoTrim;
@Schema(description = "右键粘贴")
private Boolean rightClickPaste;
@Schema(description = "启用右键菜单")
private Boolean enableRightClickMenu;
@Schema(description = "启用响铃")
private Boolean enableBell;
@Schema(description = "单词分隔符")
private String wordSeparator;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class PluginsSettingModel implements IJsonObject {
@Schema(description = "超链接插件")
private Boolean enableWeblinkPlugin;
@Schema(description = "WebGL 渲染插件")
private Boolean enableWebglPlugin;
@Schema(description = "图片渲染插件")
private Boolean enableImagePlugin;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class SessionSettingModel implements IJsonObject {
@Schema(description = "伪终端类型")
private String terminalEmulationType;
@Schema(description = "保存在缓冲区的行数")
private Integer scrollBackLine;
}
}

View File

@@ -1,6 +1,7 @@
package com.orion.ops.module.infra.handler.preference.strategy;
import com.alibaba.fastjson.JSONObject;
import com.orion.net.host.ssh.TerminalType;
import com.orion.ops.module.infra.handler.preference.model.TerminalPreferenceModel;
import org.springframework.stereotype.Component;
@@ -16,8 +17,9 @@ public class TerminalPreferenceStrategy implements IPreferenceStrategy<TerminalP
@Override
public TerminalPreferenceModel getDefault() {
// ...快捷键 ...背景
// 默认显示设置
JSONObject defaultDisplaySetting = TerminalPreferenceModel.DisplaySettingModel
String defaultDisplaySetting = TerminalPreferenceModel.DisplaySettingModel
.builder()
.fontFamily("_")
.fontSize(14)
@@ -27,13 +29,44 @@ public class TerminalPreferenceStrategy implements IPreferenceStrategy<TerminalP
.cursorStyle("bar")
.cursorBlink(true)
.build()
.toJson();
.toJsonString();
// 默认交互设置
String defaultInteractSetting = TerminalPreferenceModel.InteractSettingModel.builder()
.fastScrollModifier(true)
.altClickMovesCursor(true)
.rightClickSelectsWord(false)
.selectionChangeCopy(false)
.copyAutoTrim(false)
.pasteAutoTrim(false)
.rightClickPaste(false)
.enableRightClickMenu(true)
.enableBell(false)
.wordSeparator("/\\()\"'` -.,:;<>~!@#$%^&*|+=[]{}~?│")
.build()
.toJsonString();
// 默认插件设置
String defaultPluginsSetting = TerminalPreferenceModel.PluginsSettingModel.builder()
.enableWeblinkPlugin(true)
.enableWebglPlugin(true)
.enableImagePlugin(false)
.build()
.toJsonString();
// 默认会话设置
String defaultSessionSetting = TerminalPreferenceModel.SessionSettingModel.builder()
.terminalEmulationType(TerminalType.XTERM.getType())
.scrollBackLine(1000)
.build()
.toJsonString();
// 默认配置
return TerminalPreferenceModel.builder()
.newConnectionType("group")
.theme(new JSONObject())
.displaySetting(defaultDisplaySetting)
.displaySetting(JSONObject.parseObject(defaultDisplaySetting))
.actionBarSetting(new JSONObject())
.backgroundSetting(new JSONObject())
.interactSetting(JSONObject.parseObject(defaultInteractSetting))
.pluginsSetting(JSONObject.parseObject(defaultPluginsSetting))
.sessionSetting(JSONObject.parseObject(defaultSessionSetting))
.build();
}

View File

@@ -44,7 +44,14 @@ export default defineStore('terminal', {
// 更新默认主题偏好
await this.updateTerminalPreference(PreferenceItem.THEME, data.theme);
}
this.preference = data;
// 选择赋值
const keys = Object.keys(this.preference);
keys.forEach(key => {
const item = data[key as keyof TerminalPreference];
if (item) {
this.preference[key as keyof TerminalPreference] = item as any;
}
});
} catch (e) {
Message.error('配置加载失败');
}

View File

@@ -0,0 +1,68 @@
<template>
<a-col :span="12">
<div class="block-form-item-wrapper">
<div class="block-form-item-header">
<!-- label -->
<div class="block-form-item-label">
{{ label }}
</div>
<!-- item -->
<div class="block-form-item-value">
<slot />
</div>
</div>
<!-- 描述 -->
<div class="block-form-item-desc">
{{ desc }}
</div>
</div>
</a-col>
</template>
<script lang="ts">
export default {
name: 'blockSettingItem'
};
</script>
<script lang="ts" setup>
defineProps<{
label: string,
desc: string,
}>();
</script>
<style lang="less" scoped>
.block-form-item-wrapper {
height: 100%;
min-height: 64px;
border-radius: 4px;
background: var(--color-fill-2);
padding: 16px;
display: flex;
flex-direction: column;
.block-form-item-header {
display: flex;
justify-content: space-between;
margin-bottom: 14px;
}
.block-form-item-label {
color: var(--color-content-text-3);
font-size: 14px;
}
.block-form-item-desc {
color: var(--color-text-2);
font-size: 12px;
}
:deep(.arco-input-wrapper) {
background-color: var(--color-fill-3)
}
}
</style>

View File

@@ -21,14 +21,14 @@
position="bottom" />
</a-form-item>
<!-- 命令输入框 -->
<a-form-item field="showCommandInput" label="命令输入框">
<a-form-item field="commandInput" label="命令输入框">
<a-switch v-model="formModel.commandInput"
class="form-item-command-input"
:default-checked="true"
type="round" />
</a-form-item>
<!-- 连接状态 -->
<a-form-item field="showStatus" label="连接状态">
<!-- 终端连接状态 -->
<a-form-item field="showStatus" label="终端连接状态">
<a-switch v-model="formModel.connectStatus"
:default-checked="true"
type="round" />

View File

@@ -5,6 +5,8 @@
<h2 class="terminal-setting-title">终端设置</h2>
<!-- 交互设置 -->
<terminal-interact-block />
<!-- 插件设置 -->
<terminal-plugins-block />
<!-- 会话设置 -->
<terminal-session-block />
</div>
@@ -19,6 +21,7 @@
<script lang="ts" setup>
import TerminalInteractBlock from './terminal-interact-block.vue';
import TerminalPluginsBlock from './terminal-plugins-block.vue';
import TerminalSessionBlock from './terminal-session-block.vue';
</script>

View File

@@ -9,21 +9,70 @@
<!-- 提示 -->
<a-alert class="mb16">修改后会立刻保存, 刷新页面后生效</a-alert>
<!-- 内容区域 -->
<div class="terminal-setting-body">
<a-row class="" :gutter="[16, 16]">
<a-col :span="12">
<div class="block-form-item-wrapper">
<div class="block-form-item-label">
label
</div>
<div class="block-form-item-desc">
描述一下
</div>
<div class="block-form-item-value">
<a-switch />
</div>
</div>
</a-col>
<div class="terminal-setting-body setting-body">
<a-row class="mb16" align="stretch" :gutter="16">
<!-- 快速滚动 -->
<block-setting-item label="快速滚动" desc="alt + 鼠标滚轮快速滚动">
<a-switch type="round"
v-model="formModel.fastScrollModifier"
checked-value="alt"
unchecked-value="none" />
</block-setting-item>
<!-- 点击移动光标 -->
<block-setting-item label="点击移动光标" desc="alt + 鼠标左键可以切换光标位置">
<a-switch type="round"
v-model="formModel.altClickMovesCursor" />
</block-setting-item>
</a-row>
<a-row class="mb16" align="stretch" :gutter="16">
<!-- 右键选中词条 -->
<block-setting-item label="右键选中词条" desc="右键文本">
<a-switch type="round"
v-model="formModel.rightClickSelectsWord" />
</block-setting-item>
<!-- 选中词条自动复制 -->
<block-setting-item label="选中词条自动复制" desc="自动将选中的词条复制到剪切板">
<a-switch type="round"
v-model="formModel.selectionChangeCopy" />
</block-setting-item>
</a-row>
<a-row class="mb16" align="stretch" :gutter="16">
<!-- 复制去除空格 -->
<block-setting-item label="复制去除空格" desc="复制文本后自动删除尾部空格">
<a-switch type="round"
v-model="formModel.copyAutoTrim" />
</block-setting-item>
<!-- 粘贴去除空格 -->
<block-setting-item label="粘贴去除空格" desc="粘贴文本前自动删除尾部空格">
<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="右键自动粘贴, 启用后需要关闭右键菜单">
<a-switch type="round"
v-model="formModel.rightClickPaste" />
</block-setting-item>
<!-- 启用右键菜单 -->
<block-setting-item label="启用右键菜单" desc="右键终端将打开自定义菜单, 启用后需要关闭右键粘贴">
<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="系统接受到 \a 时候会发出响铃 (一般不用开启)">
<a-switch type="round"
v-model="formModel.enableBell" />
</block-setting-item>
<!-- 单词分隔符 -->
<block-setting-item label="单词分隔符" desc="在终端中双击文本将使用该分隔符进行分割">
<a-input size="small"
v-model="formModel.wordSeparator"
placeholder="单词分隔符"
allow-clear />
</block-setting-item>
</a-row>
</div>
</div>
@@ -37,46 +86,31 @@
<script lang="ts" setup>
// TODO
// 交互设置
// alt + 滚轮快速滚动 fastScrollModifier 'none' | 'alt'
// alt 点击可以切换光标位置 altClickMovesCursor
// 快速滚动 fastScrollModifier
// 点击移动光标 altClickMovesCursor
// 右键选中词条 rightClickSelectsWord
// 自动将选中内容复制到剪切板 onSelectionChange
// 自动将选中内容复制到剪切板 selectionChangeCopy onSelectionChange
// 粘贴时删除空格
// 复制时删除空格
// 复制时删除空格 pasteAutoTrim
// 粘贴时删除空格 copyAutoTrim
// 右键粘贴
// 启用右键菜单
// 右键粘贴 rightClickPaste
// 启用右键菜单 enableRightClickMenu
// 自动检测 url 并可以点击
// 支持显示图片 使用 sixel 打开图片
// 启用响铃 enableBell
// 单词分隔符 /\()"'` -.,:;<>~!@#$%^&*|+=[]{}~?│ wordSeparator
// bell sound
// 分隔符 /\()"'-.,:;<>~!@#$%^&*|+=[]{}~?│ 在终端中双击文本将使用到这些符号 wordSeparator
import { ref } from 'vue';
import BlockSettingItem from './block-setting-item.vue';
const formModel = ref<Record<string, any>>({});
</script>
<style lang="less" scoped>
.block-form-item-wrapper {
height: 84px;
border-radius: 4px;
background: var(--color-fill-2);
display: flex;
padding: 16px;
.block-form-item-label {
color: var(--color-content-text-3);
font-size: 15px;
font-weight: bold;
}
.block-form-item-desc {
color: var(--color-content-text-2);
font-size: 12px;
}
.setting-body {
flex-direction: column;
}
</style>

View File

@@ -0,0 +1,61 @@
<template>
<div class="terminal-setting-block">
<!-- 顶部 -->
<div class="terminal-setting-subtitle-wrapper">
<h3 class="terminal-setting-subtitle">
插件设置
</h3>
</div>
<!-- 内容区域 -->
<div class="terminal-setting-body setting-body">
<a-row class="mb16" align="stretch" :gutter="16">
<!-- 超链接插件 -->
<block-setting-item label="超链接插件" desc="自动检测 http url 并可以点击">
<a-switch type="round"
v-model="formModel.enableWeblinkPlugin"
checked-value="alt"
unchecked-value="none" />
</block-setting-item>
<!-- WebGL 渲染插件 -->
<block-setting-item label="WebGL 渲染插件" desc="使用 WebGL 加速渲染终端 (建议开启, 若无法开启终端请关闭)">
<a-switch type="round"
v-model="formModel.enableWebglPlugin" />
</block-setting-item>
</a-row>
<a-row class="mb16" align="stretch" :gutter="16">
<!-- 图片渲染插件 -->
<block-setting-item label="图片渲染插件" desc="支持使用 sixel 打开图片 (一般不需要开启)">
<a-switch type="round"
v-model="formModel.enableImagePlugin" />
</block-setting-item>
</a-row>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'TerminalPluginsBlock'
};
</script>
<script lang="ts" setup>
// fixme
// 自动检测 url 并可以点击 enableWeblinkPlugin
// 启用 webgl 支持 enableWebglPlugin
// 支持显示图片 使用 sixel 打开图片 enableImagePlugin
import { ref } from 'vue';
import BlockSettingItem from './block-setting-item.vue';
const formModel = ref<Record<string, any>>({});
</script>
<style lang="less" scoped>
.setting-body {
flex-direction: column;
}
</style>

View File

@@ -7,9 +7,26 @@
</h3>
</div>
<!-- 内容区域 -->
<div class="terminal-setting-body">
<div class="terminal-setting-body setting-body">
<a-row class="mb16" align="stretch" :gutter="16">
<!-- 终端类型 -->
<block-setting-item label="终端类型" desc="若显示异常请尝试切换此选项 兼容性 vt100 > xterm > 16color > 256color">
<a-select style="width: 160px;"
v-model="formModel.terminalEmulationType"
size="small"
:options="toOptions(terminalEmulationTypeKey)" />
</block-setting-item>
<!-- 缓冲区行数 -->
<block-setting-item label="缓冲区行数" desc="保存在缓冲区的行数, 多出的行数会被忽略, 此值越大占用内存的内存会更多">
<a-input-number v-model="formModel.scrollBackLine"
size="small"
:min="1"
:max="10000"
placeholder="缓冲区行数"
allow-clear
hide-button />
</block-setting-item>
</a-row>
</div>
</div>
</template>
@@ -21,13 +38,25 @@
</script>
<script lang="ts" setup>
import { terminalEmulationTypeKey } from '../../types/terminal.const';
const { toOptions } = useDictStore();
// TODO
// terminal emulation type: xterm 256color
// 回滚ScrollBack scrollback 保存在缓冲区的行数
// terminalEmulationType: xterm 256color
// scrollBackLine 保存在缓冲区的行数 1000
import { ref } from 'vue';
import BlockSettingItem from './block-setting-item.vue';
import { useDictStore } from '@/store';
const formModel = ref<Record<string, any>>({});
</script>
<style lang="less" scoped>
.setting-body {
flex-direction: column;
}
</style>

View File

@@ -227,7 +227,6 @@
height: 100%;
display: inline-flex;
align-items: center;
user-select: none;
.address-copy {
display: none;

View File

@@ -8,6 +8,7 @@ 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';
// 终端会话实现
export default class TerminalSession implements ITerminalSession {
@@ -48,12 +49,13 @@ export default class TerminalSession implements ITerminalSession {
this.inst = new Terminal({
...(preference.displaySetting as any),
theme: preference.theme.schema,
fastScrollModifier: 'ctrl',
fastScrollModifier: 'alt',
fontFamily: preference.displaySetting.fontFamily + fontFamilySuffix,
});
// 注册插件
this.addons.fit = new FitAddon();
this.addons.webgl = new WebglAddon();
// 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();

View File

@@ -156,10 +156,13 @@ export const extraSshAuthTypeKey = 'hostExtraSshAuthType';
// 终端状态
export const connectStatusKey = 'terminalConnectStatus';
// 终端类型
export const terminalEmulationTypeKey = 'terminalEmulationType';
// 加载的字典值
export const dictKeys = [
fontFamilyKey,
fontSizeKey, fontWeightKey,
cursorStyleKey, newConnectionTypeKey,
extraSshAuthTypeKey, connectStatusKey
fontFamilyKey, fontSizeKey,
fontWeightKey, cursorStyleKey,
newConnectionTypeKey, extraSshAuthTypeKey,
connectStatusKey, terminalEmulationTypeKey
];

View File

@@ -1,5 +1,6 @@
import type { Terminal } from 'xterm';
import type { FitAddon } from 'xterm-addon-fit';
import type { CanvasAddon } from 'xterm-addon-canvas';
import type { WebglAddon } from 'xterm-addon-webgl';
import type { WebLinksAddon } from 'xterm-addon-web-links';
import type { SearchAddon } from 'xterm-addon-search';
@@ -117,6 +118,7 @@ export interface ITerminalOutputProcessor {
export interface TerminalAddons {
fit: FitAddon;
webgl: WebglAddon;
canvas: CanvasAddon;
link: WebLinksAddon;
search: SearchAddon;
image: ImageAddon;