Files
test-repo/docs/.vitepress/theme/components/appendix/terminal-intro/InputVisualizer.vue
T
sanbuphy c238f07e0d feat(docs): add terminal introduction appendix with interactive components
Add a comprehensive terminal introduction guide with interactive Vue components demonstrating terminal concepts. Includes:
- Terminal definition and architecture visualization
- Character grid and cell inspector
- ANSI escape sequences demo
- Input visualization and signal mechanisms
- Flow diagrams and TUI examples

The components are registered in the VitePress theme and linked from the appendix section. Each component includes detailed documentation and interactive elements to help users understand terminal principles.
2026-01-14 19:04:09 +08:00

293 lines
6.7 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
InputVisualizer.vue
输入可视化组件
用途
展示键盘输入在底层是如何被转换为字节流发送给终端的
纠正按键直接上屏的误区强调按键 -> 编码 -> 发送的过程
交互功能
- 键盘监听捕获用户的真实按键
- 数据展示同时显示按键名16进制字节码和转义序列如方向键
- 历史记录记录最近几次按键的编码流
-->
<template>
<div class="input-visualizer" tabindex="0" @keydown="handleKeydown" @blur="handleBlur">
<div class="focus-overlay" v-if="!isFocused" @click="focus">
<div class="focus-btn">
<span class="icon"></span>
<span>Click to Type</span>
</div>
</div>
<div class="main-display" :class="{ 'blur-content': !isFocused }">
<div class="key-name">{{ currentKey.name || 'Press any key' }}</div>
<div class="info-grid">
<div class="info-box">
<div class="label">BYTES (HEX)</div>
<div class="value highlight">{{ currentKey.bytes || '-' }}</div>
</div>
<div class="info-box">
<div class="label">SEQUENCE</div>
<div class="value code">{{ currentKey.sequence || '-' }}</div>
</div>
</div>
<div class="char-display">
Character: <span class="char-val">{{ currentKey.charDisplay || '-' }}</span>
</div>
</div>
<div class="history-strip">
<div v-for="(item, i) in history" :key="i" class="history-item">
<span class="h-name">{{ item.name }}</span>
<span class="arrow"></span>
<span class="h-bytes">{{ item.bytes }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isFocused = ref(false)
const currentKey = ref({ name: '', bytes: '', sequence: '', charDisplay: '' })
const history = ref([])
const focus = (e) => {
// Find the parent .input-visualizer and focus it
const container = e.currentTarget.closest('.input-visualizer')
if (container) {
container.focus()
isFocused.value = true
}
}
const handleBlur = () => {
isFocused.value = false
}
const handleKeydown = (e) => {
e.preventDefault()
let name = e.key
let bytes = ''
let sequence = ''
let charDisplay = e.key
// Map special keys
const keyMap = {
' ': { name: 'Space', bytes: '20', char: ' ' },
'Enter': { name: 'Enter', bytes: '0a', char: '\\n' },
'Tab': { name: 'Tab', bytes: '09', char: '\\t' },
'Escape': { name: 'Esc', bytes: '1b', char: '\\e' },
'Backspace': { name: 'Backspace', bytes: '7f', char: '\\b' },
'Delete': { name: 'Del', bytes: '1b 5b 33 7e', sequence: '^[[3~' },
'ArrowUp': { name: 'Arrow Up', bytes: '1b 5b 41', sequence: '^[[A' },
'ArrowDown': { name: 'Arrow Down', bytes: '1b 5b 42', sequence: '^[[B' },
'ArrowRight': { name: 'Arrow Right', bytes: '1b 5b 43', sequence: '^[[C' },
'ArrowLeft': { name: 'Arrow Left', bytes: '1b 5b 44', sequence: '^[[D' },
}
if (keyMap[e.key]) {
const map = keyMap[e.key]
name = map.name
bytes = map.bytes
sequence = map.sequence || ''
charDisplay = map.char || map.name
} else if (e.key.length === 1) {
// Printable characters
const code = e.key.charCodeAt(0)
bytes = code.toString(16).toLowerCase().padStart(2, '0')
if (e.ctrlKey) {
// Ctrl + Letter
name = `Ctrl+${e.key.toUpperCase()}`
const ctrlCode = code >= 97 && code <= 122 ? code - 96 : code
bytes = ctrlCode.toString(16).toLowerCase().padStart(2, '0')
sequence = '^' + e.key.toUpperCase()
charDisplay = sequence
}
} else {
// Other special keys
name = e.key
charDisplay = e.key
}
const keyData = { name, bytes, sequence, charDisplay }
currentKey.value = keyData
history.value.unshift(keyData)
if (history.value.length > 5) history.value.pop()
}
</script>
<style scoped>
.input-visualizer {
position: relative;
background: #09090b; /* Slightly lighter than pure black */
border: 1px solid #27272a;
border-radius: 12px;
padding: 30px 20px;
text-align: center;
font-family: 'JetBrains Mono', 'Menlo', monospace;
outline: none;
min-height: 320px;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
transition: border-color 0.2s, box-shadow 0.2s;
}
.input-visualizer:focus {
border-color: #10b981; /* Emerald 500 */
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
}
.focus-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(2px);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 10;
transition: all 0.2s;
}
.focus-overlay:hover {
background: rgba(0, 0, 0, 0.3);
}
.focus-btn {
background: #10b981;
color: #fff;
padding: 12px 24px;
border-radius: 8px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
transition: transform 0.1s;
}
.focus-btn:hover {
transform: translateY(-1px);
}
.focus-btn:active {
transform: translateY(1px);
}
.main-display {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transition: opacity 0.2s, filter 0.2s;
}
.blur-content {
opacity: 0.5;
filter: blur(1px);
}
.key-name {
font-size: 36px;
font-weight: 700;
color: #e4e4e7; /* Zinc 200 */
margin-bottom: 30px;
height: 50px;
line-height: 50px;
}
.info-grid {
display: flex;
justify-content: center;
gap: 24px;
margin-bottom: 30px;
width: 100%;
}
.info-box {
background: #18181b; /* Zinc 900 */
padding: 16px 20px;
border-radius: 8px;
min-width: 140px;
border: 1px solid #27272a;
}
.label {
color: #71717a; /* Zinc 500 */
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
}
.value {
font-size: 24px;
font-weight: 500;
font-variant-numeric: tabular-nums;
}
.highlight { color: #facc15; /* Yellow 400 */ }
.code { color: #22d3ee; /* Cyan 400 */ }
.char-display {
color: #a1a1aa; /* Zinc 400 */
font-size: 14px;
}
.char-val {
color: #fff;
font-weight: bold;
background: #27272a;
padding: 2px 6px;
border-radius: 4px;
margin-left: 5px;
}
.history-strip {
display: flex;
gap: 12px;
justify-content: center;
border-top: 1px solid #27272a;
padding-top: 20px;
margin-top: 20px;
flex-wrap: wrap;
}
.history-item {
display: flex;
align-items: center;
background: #18181b;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
color: #a1a1aa;
border: 1px solid #27272a;
}
.arrow {
color: #71717a; /* Lighter grey for better visibility */
margin: 0 8px;
}
.h-name {
font-weight: 500;
color: #e4e4e7;
}
.h-bytes {
color: #facc15;
font-family: monospace;
}
</style>