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

View File

@@ -1,7 +1,7 @@
package com.orion.ops.module.infra.handler.preference.model; package com.orion.ops.module.infra.handler.preference.model;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.orion.lang.able.IJsonObject;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
@@ -33,14 +33,20 @@ public class TerminalPreferenceModel implements PreferenceModel {
@Schema(description = "操作栏设置") @Schema(description = "操作栏设置")
private JSONObject actionBarSetting; private JSONObject actionBarSetting;
@Schema(description = "背景设置") @Schema(description = "交互设置")
private JSONObject backgroundSetting; private JSONObject interactSetting;
@Schema(description = "插件设置")
private JSONObject pluginsSetting;
@Schema(description = "会话设置")
private JSONObject sessionSetting;
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public static class DisplaySettingModel { public static class DisplaySettingModel implements IJsonObject {
@Schema(description = "字体样式") @Schema(description = "字体样式")
private String fontFamily; private String fontFamily;
@@ -63,14 +69,74 @@ public class TerminalPreferenceModel implements PreferenceModel {
@Schema(description = "光标闪烁") @Schema(description = "光标闪烁")
private Boolean cursorBlink; private Boolean cursorBlink;
/** }
* 转为 json
* @Data
* @return json @Builder
*/ @NoArgsConstructor
public JSONObject toJson() { @AllArgsConstructor
return JSON.parseObject(JSON.toJSONString(this)); 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; package com.orion.ops.module.infra.handler.preference.strategy;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.orion.net.host.ssh.TerminalType;
import com.orion.ops.module.infra.handler.preference.model.TerminalPreferenceModel; import com.orion.ops.module.infra.handler.preference.model.TerminalPreferenceModel;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -16,8 +17,9 @@ public class TerminalPreferenceStrategy implements IPreferenceStrategy<TerminalP
@Override @Override
public TerminalPreferenceModel getDefault() { public TerminalPreferenceModel getDefault() {
// ...快捷键 ...背景
// 默认显示设置 // 默认显示设置
JSONObject defaultDisplaySetting = TerminalPreferenceModel.DisplaySettingModel String defaultDisplaySetting = TerminalPreferenceModel.DisplaySettingModel
.builder() .builder()
.fontFamily("_") .fontFamily("_")
.fontSize(14) .fontSize(14)
@@ -27,13 +29,44 @@ public class TerminalPreferenceStrategy implements IPreferenceStrategy<TerminalP
.cursorStyle("bar") .cursorStyle("bar")
.cursorBlink(true) .cursorBlink(true)
.build() .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() return TerminalPreferenceModel.builder()
.newConnectionType("group") .newConnectionType("group")
.theme(new JSONObject()) .theme(new JSONObject())
.displaySetting(defaultDisplaySetting) .displaySetting(JSONObject.parseObject(defaultDisplaySetting))
.actionBarSetting(new JSONObject()) .actionBarSetting(new JSONObject())
.backgroundSetting(new JSONObject()) .interactSetting(JSONObject.parseObject(defaultInteractSetting))
.pluginsSetting(JSONObject.parseObject(defaultPluginsSetting))
.sessionSetting(JSONObject.parseObject(defaultSessionSetting))
.build(); .build();
} }

View File

@@ -44,7 +44,14 @@ export default defineStore('terminal', {
// 更新默认主题偏好 // 更新默认主题偏好
await this.updateTerminalPreference(PreferenceItem.THEME, data.theme); 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) { } catch (e) {
Message.error('配置加载失败'); 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" /> position="bottom" />
</a-form-item> </a-form-item>
<!-- 命令输入框 --> <!-- 命令输入框 -->
<a-form-item field="showCommandInput" label="命令输入框"> <a-form-item field="commandInput" label="命令输入框">
<a-switch v-model="formModel.commandInput" <a-switch v-model="formModel.commandInput"
class="form-item-command-input" class="form-item-command-input"
:default-checked="true" :default-checked="true"
type="round" /> type="round" />
</a-form-item> </a-form-item>
<!-- 连接状态 --> <!-- 终端连接状态 -->
<a-form-item field="showStatus" label="连接状态"> <a-form-item field="showStatus" label="终端连接状态">
<a-switch v-model="formModel.connectStatus" <a-switch v-model="formModel.connectStatus"
:default-checked="true" :default-checked="true"
type="round" /> type="round" />

View File

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

View File

@@ -9,21 +9,70 @@
<!-- 提示 --> <!-- 提示 -->
<a-alert class="mb16">修改后会立刻保存, 刷新页面后生效</a-alert> <a-alert class="mb16">修改后会立刻保存, 刷新页面后生效</a-alert>
<!-- 内容区域 --> <!-- 内容区域 -->
<div class="terminal-setting-body"> <div class="terminal-setting-body setting-body">
<a-row class="" :gutter="[16, 16]"> <a-row class="mb16" align="stretch" :gutter="16">
<a-col :span="12"> <!-- 快速滚动 -->
<div class="block-form-item-wrapper"> <block-setting-item label="快速滚动" desc="alt + 鼠标滚轮快速滚动">
<div class="block-form-item-label"> <a-switch type="round"
label v-model="formModel.fastScrollModifier"
</div> checked-value="alt"
<div class="block-form-item-desc"> unchecked-value="none" />
描述一下 </block-setting-item>
</div> <!-- 点击移动光标 -->
<div class="block-form-item-value"> <block-setting-item label="点击移动光标" desc="alt + 鼠标左键可以切换光标位置">
<a-switch /> <a-switch type="round"
</div> v-model="formModel.altClickMovesCursor" />
</div> </block-setting-item>
</a-col> </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> </a-row>
</div> </div>
</div> </div>
@@ -37,46 +86,31 @@
<script lang="ts" setup> <script lang="ts" setup>
// TODO // 快速滚动 fastScrollModifier
// 交互设置 // 点击移动光标 altClickMovesCursor
// alt + 滚轮快速滚动 fastScrollModifier 'none' | 'alt'
// alt 点击可以切换光标位置 altClickMovesCursor
// 右键选中词条 rightClickSelectsWord // 右键选中词条 rightClickSelectsWord
// 自动将选中内容复制到剪切板 onSelectionChange // 自动将选中内容复制到剪切板 selectionChangeCopy onSelectionChange
// 粘贴时删除空格 // 复制时删除空格 pasteAutoTrim
// 复制时删除空格 // 粘贴时删除空格 copyAutoTrim
// 右键粘贴 // 右键粘贴 rightClickPaste
// 启用右键菜单 // 启用右键菜单 enableRightClickMenu
// 自动检测 url 并可以点击 // 启用响铃 enableBell
// 支持显示图片 使用 sixel 打开图片 // 单词分隔符 /\()"'` -.,:;<>~!@#$%^&*|+=[]{}~?│ wordSeparator
// bell sound import { ref } from 'vue';
// 分隔符 /\()"'-.,:;<>~!@#$%^&*|+=[]{}~?│ 在终端中双击文本将使用到这些符号 wordSeparator import BlockSettingItem from './block-setting-item.vue';
const formModel = ref<Record<string, any>>({});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.block-form-item-wrapper { .setting-body {
height: 84px; flex-direction: column;
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;
}
} }
</style> </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> </h3>
</div> </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>
</div> </div>
</template> </template>
@@ -21,13 +38,25 @@
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import { terminalEmulationTypeKey } from '../../types/terminal.const';
const { toOptions } = useDictStore();
// TODO // TODO
// terminal emulation type: xterm 256color // terminalEmulationType: xterm 256color
// 回滚ScrollBack scrollback 保存在缓冲区的行数 // scrollBackLine 保存在缓冲区的行数 1000
import { ref } from 'vue';
import BlockSettingItem from './block-setting-item.vue';
import { useDictStore } from '@/store';
const formModel = ref<Record<string, any>>({});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.setting-body {
flex-direction: column;
}
</style> </style>

View File

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

View File

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

View File

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

View File

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