Skip to content

Commit 58db9e2

Browse files
committed
[vue3] feat 命令行支持ctrl+enter输入回车 tzfun#110
1 parent 9cac9b2 commit 58db9e2

File tree

3 files changed

+125
-46
lines changed

3 files changed

+125
-46
lines changed

src/Terminal.vue

Lines changed: 107 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ import TViewerTable from "~/components/TViewerTable.vue";
5555
import THelpBox from "~/components/THelpBox.vue";
5656
import TEditor from "~/components/TEditor.vue";
5757
58-
import themeDark from "~/css/theme/dark.css"
59-
import themeLight from "~/css/theme/light.css"
58+
import themeDark from "~/css/theme/dark.css?inline"
59+
import themeLight from "~/css/theme/light.css?inline"
6060
6161
// 对应css变量 --t-font-height
6262
const FONT_HEIGHT = 19;
@@ -285,23 +285,23 @@ const tips = reactive({
285285
})
286286
287287
// references
288-
const terminalContainerRef = ref(null)
289-
const terminalHeaderRef = ref(null)
290-
const terminalWindowRef = ref(null)
291-
const terminalCmdInputRef = ref(null)
292-
const terminalAskInputRef = ref(null)
293-
const terminalInputBoxRef = ref(null)
294-
const terminalInputPromptRef = ref(null)
295-
const terminalEnFlagRef = ref(null)
296-
const terminalCnFlagRef = ref(null)
297-
const terminalTextEditorRef = ref(null)
298-
const terminalCursorRef = ref(null)
299-
const terminalHelpBoxRef = ref(null)
300-
const resizeLTRef = ref(null)
301-
const resizeRTRef = ref(null)
302-
const resizeLBRef = ref(null)
303-
const resizeRBRef = ref(null)
304-
const terminalCmdTipsRef = ref(null)
288+
const terminalContainerRef = ref<HTMLDivElement>(null)
289+
const terminalHeaderRef = ref<HTMLDivElement>(null)
290+
const terminalWindowRef = ref<HTMLDivElement>(null)
291+
const terminalCmdInputRef = ref<HTMLInputElement>(null)
292+
const terminalAskInputRef = ref<HTMLInputElement>(null)
293+
const terminalInputBoxRef = ref<HTMLParagraphElement>(null)
294+
const terminalInputPromptRef = ref<HTMLSpanElement>(null)
295+
const terminalEnFlagRef = ref<HTMLSpanElement>(null)
296+
const terminalCnFlagRef = ref<HTMLSpanElement>(null)
297+
const terminalTextEditorRef = ref<InstanceType<TEditor>>(null)
298+
const terminalCursorRef = ref<HTMLSpanElement>(null)
299+
const terminalHelpBoxRef = ref<InstanceType<THelpBox>>(null)
300+
const resizeLTRef = ref<HTMLDivElement>(null)
301+
const resizeRTRef = ref<HTMLDivElement>(null)
302+
const resizeLBRef = ref<HTMLDivElement>(null)
303+
const resizeRBRef = ref<HTMLDivElement>(null)
304+
const terminalCmdTipsRef = ref<HTMLSpanElement>(null)
305305
306306
// listeners
307307
const clickListener = ref()
@@ -421,8 +421,10 @@ onMounted(() => {
421421
return;
422422
}
423423
text = text.trim()
424+
.replace(/\r\n/g, '\n')
425+
.replace(/\r/g, '\n')
424426
const cmd = command.value;
425-
command.value = cmd && cmd.length ? `${cmd}${text}` : text;
427+
command.value = cmd && cmd.length > 0 ? `${cmd}${text}` : text;
426428
_focus()
427429
}).catch(error => {
428430
console.error(error);
@@ -949,6 +951,38 @@ const _printHelp = (regExp: RegExp, srcStr: string) => {
949951
})
950952
}
951953
954+
const _inputEnter = (e: KeyboardEvent) => {
955+
e.preventDefault()
956+
if (e.ctrlKey) {
957+
if (command.value.length > 0) {
958+
let cursorIdx = cursorConf.idx
959+
command.value = command.value.substring(0, cursorIdx) + '\n' + command.value.substring(cursorIdx)
960+
961+
cursorIdx++
962+
// 恢复光标位置
963+
nextTick(() => {
964+
terminalCmdInputRef.value.selectionStart = cursorIdx
965+
terminalCmdInputRef.value.selectionEnd = cursorIdx
966+
cursorConf.idx = cursorIdx
967+
})
968+
}
969+
} else {
970+
// 因无法阻止回车输入,这里手动删掉前后一个回车
971+
let cursorIdx = terminalCmdInputRef.value.selectionStart
972+
let enterIdx = -1
973+
if (command.value[cursorIdx] == '\n') {
974+
enterIdx = cursorIdx
975+
} else if (command.value[cursorIdx - 1] == '\n') {
976+
enterIdx = cursorIdx - 1
977+
}
978+
979+
if (enterIdx >= 0) {
980+
command.value = command.value.substring(0, enterIdx) + command.value.substring(enterIdx + 1)
981+
}
982+
_execute()
983+
}
984+
}
985+
952986
const _execute = () => {
953987
_closeTips(true)
954988
_saveCurCommand();
@@ -1209,22 +1243,25 @@ const _jumpToBottom = () => {
12091243
nextTick(() => {
12101244
let box = terminalWindowRef.value
12111245
if (box != null) {
1212-
box.scrollTo({top: box.scrollHeight, behavior: props.scrollMode})
1246+
box.scrollTo({
1247+
top: box.scrollHeight,
1248+
behavior: props.scrollMode
1249+
})
12131250
}
12141251
}).then(() => {
12151252
})
12161253
}
12171254
12181255
const _saveCurCommand = () => {
1219-
if (_nonEmpty(command.value)) {
1220-
store.push(getName(), command.value)
1256+
let cmd = command.value = command.value.trim()
1257+
if (cmd.length > 0) {
1258+
store.push(getName(), cmd)
12211259
}
12221260
12231261
let group = _newTerminalLogGroup()
1224-
12251262
group.logs.push({
12261263
type: "cmdLine",
1227-
content: `${_html(props.context)}${props.contextSuffix}${_commandFormatter(command.value)}`
1264+
content: `${_html(props.context)}${props.contextSuffix}${_commandFormatter(cmd)}`
12281265
});
12291266
_jumpToBottom()
12301267
}
@@ -1246,13 +1283,14 @@ const _resetCursorPos = (cmd?: string) => {
12461283
}
12471284
12481285
const _calculateCursorPos = (cmdStr?: string) => {
1286+
let cmd = cmdStr ? cmdStr : command.value
12491287
// idx可以认为是需要光标覆盖字符的索引
12501288
let idx = cursorConf.idx
1251-
let cmd = cmdStr ? cmdStr : command.value
12521289
12531290
_calculateByteLen()
12541291
12551292
if (idx < 0 || idx >= cmd.length) {
1293+
console.debug(`reset cursor, idx: ${idx}, cmd.length: ${cmd.length}`)
12561294
_resetCursorPos()
12571295
return
12581296
}
@@ -1266,14 +1304,32 @@ const _calculateCursorPos = (cmdStr?: string) => {
12661304
let pos = {left: 0, top: 0}
12671305
// 当前字符长度
12681306
let charWidth = cursorConf.defaultWidth
1269-
// 前一个字符的长度
1270-
let preWidth = inputBoxParam.promptWidth
1307+
// 前面字符的长度
1308+
let lastCharWidth = inputBoxParam.promptWidth
1309+
// 前一个字符是否是回车换行
1310+
let lastCharIsEnter = false
12711311
12721312
// 先找到被覆盖字符的位置
12731313
for (let i = 0; i <= idx; i++) {
1314+
let char = cmd[i]
1315+
1316+
if (lastCharIsEnter) {
1317+
pos.top += FONT_HEIGHT
1318+
pos.left = 0
1319+
lastCharWidth = 0
1320+
}
1321+
1322+
if (char === '\n') {
1323+
pos.left += lastCharWidth
1324+
lastCharIsEnter = true
1325+
continue
1326+
} else {
1327+
lastCharIsEnter = false
1328+
}
1329+
12741330
charWidth = _calculateStringWidth(cmd[i])
1275-
pos.left += preWidth
1276-
preWidth = charWidth
1331+
pos.left += lastCharWidth
1332+
lastCharWidth = charWidth
12771333
if (pos.left > lineWidth) {
12781334
// 行高 对应 css 变量 --t-font-height
12791335
pos.top += FONT_HEIGHT
@@ -1351,6 +1407,7 @@ const _switchPreCmd = () => {
13511407
_resetCursorPos()
13521408
store.setIdx(getName(), cmdIdx)
13531409
_searchCmd()
1410+
_jumpToBottom()
13541411
}
13551412
13561413
const _switchNextCmd = () => {
@@ -1366,6 +1423,7 @@ const _switchNextCmd = () => {
13661423
_resetCursorPos()
13671424
store.setIdx(getName(), cmdIdx)
13681425
_searchCmd()
1426+
_jumpToBottom()
13691427
}
13701428
13711429
const _calculateStringWidth = (str: string): number => {
@@ -1662,7 +1720,12 @@ const _commandFormatter = (cmd: string): string => {
16621720
if (props.commandFormatter) {
16631721
return props.commandFormatter(cmd)
16641722
}
1665-
return _defaultMergedCommandFormatter(cmd)
1723+
let splitsCode = []
1724+
let splits = cmd.split(/\r\n|\n|\r/g)
1725+
for (let c of splits) {
1726+
splitsCode.push(_defaultMergedCommandFormatter(c))
1727+
}
1728+
return splitsCode.join("<br/>")
16661729
}
16671730
16681731
const _onAskInput = () => {
@@ -2018,20 +2081,19 @@ defineExpose({
20182081
Press <strong>Tab</strong> to choose the selected suggestion.
20192082
</span>
20202083
</span>
2021-
<input type="text"
2022-
autofocus
2023-
v-model="command"
2024-
class="t-cmd-input t-disable-select"
2025-
ref="terminalCmdInputRef"
2026-
autocomplete="off"
2027-
auto-complete="new-password"
2028-
@keydown="_onInputKeydown"
2029-
@keyup="_onInputKeyup"
2030-
@input="_onInput"
2031-
@focusin="cursorConf.show = true"
2032-
@keyup.up.exact="_inputKeyUp"
2033-
@keyup.down.exact="_inputKeyDown"
2034-
@keyup.enter="_execute">
2084+
<textarea autofocus
2085+
v-model="command"
2086+
class="t-cmd-input t-disable-select"
2087+
ref="terminalCmdInputRef"
2088+
autocomplete="off"
2089+
auto-complete="new-password"
2090+
@keydown="_onInputKeydown"
2091+
@keyup="_onInputKeyup"
2092+
@input="_onInput"
2093+
@focusin="cursorConf.show = true"
2094+
@keyup.up.exact="_inputKeyUp"
2095+
@keyup.down.exact="_inputKeyDown"
2096+
@keyup.enter="_inputEnter"/>
20352097
</p>
20362098
</div>
20372099
</div>

src/common/util.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ export function _isSafari() {
6464
}
6565

6666
export function _getByteLen(val: string) {
67+
if (val.match(/[\n\r]/)) {
68+
return 0
69+
}
6770
let len = 0;
6871
for (let i = 0; i < val.length; i++) {
6972
// eslint-disable-next-line no-control-regex
@@ -232,6 +235,18 @@ export function _defaultMergedCommandFormatter(cmd: string): string {
232235
isCmdKey = false
233236
} else if (char.startsWith("-")) {
234237
formatted += `<span class="t-cmd-arg">${char}</span>`
238+
} else if (char === '\r') {
239+
// \r\n换行
240+
if (i < split.length - 1 && split[i + 1] === '\n') {
241+
formatted += `<br/>`
242+
i++
243+
}
244+
// \r换行
245+
else {
246+
formatted += `<br/>`
247+
}
248+
} else if (char === '\n') {
249+
formatted += `<br/>`
235250
} else if (char.length > 0) {
236251
if (char === '|') {
237252
isCmdKey = true

test/App.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,13 @@ const initLog = reactive([{
3838
type: 'normal',
3939
content: "Welcome to vue web terminal! If you are using for the first time, you can use the <span class='t-cmd-key'>help</span> command to learn. Thanks for your star support: <a class='t-a' target='_blank' href='https://github.com/tzfun/vue-web-terminal'>https://github.com/tzfun/vue-web-terminal</a>"
4040
}])
41+
const testInputValue = ref("")
4142
4243
const terminals = ref<Array<any>>([
4344
{
4445
show: true,
4546
name: 'terminal-test',
46-
context: '/vue-web-terminal/test<br/>123/线上服/2',
47+
context: '/vue-web-terminal/test<br/>123/阿里云/2',
4748
dragConf: {
4849
width: "60%",
4950
height: "50%",
@@ -316,6 +317,7 @@ const onClick = (key: string, name: string) => {
316317

317318
<button @click="getCommand">get command</button>
318319
<button @click="setCommand">set command</button>
320+
<textarea v-model="testInputValue"/>
319321

320322
<!-- <div style="width: 700px;height: 400px;margin-left: 150px;margin-top: 300px">-->
321323
<!-- <terminal-->

0 commit comments

Comments
 (0)