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.
This commit is contained in:
sanbuphy
2026-01-14 19:04:09 +08:00
parent a9f4071308
commit c238f07e0d
14 changed files with 4248 additions and 1 deletions
@@ -0,0 +1,366 @@
<!--
SignalsDemo.vue
信号机制演示组件
用途
演示进程控制信号Signals如何工作特别是 `Ctrl+C` `Ctrl+Z`
说明这些组合键不是发送字符而是触发操作系统级别的中断信号
交互功能
- 模拟运行点击按钮启动一个模拟进程 `sleep 100`
- 发送信号点击按钮或快捷键发送 SIGINT/SIGTSTP
- 状态反馈展示进程状态的变化运行中 -> 被杀死/被挂起
-->
<template>
<div class="signals-demo">
<div class="left-panel">
<div class="signal-list">
<div
class="signal-item"
:class="{ active: activeSignal === 'SIGINT' }"
@click="sendSignal('SIGINT')"
>
<div class="key-combo">
<span class="key">Ctrl</span>+<span class="key">C</span>
<span class="action">Interrupt</span>
</div>
<div class="signal-name">SIGINT</div>
</div>
<div
class="signal-item"
:class="{ active: activeSignal === 'SIGTSTP' }"
@click="sendSignal('SIGTSTP')"
>
<div class="key-combo">
<span class="key">Ctrl</span>+<span class="key">Z</span>
<span class="action">Suspend</span>
</div>
<div class="signal-name">SIGTSTP</div>
</div>
</div>
<div class="info-box">
<div v-if="activeSignal === 'SIGINT'">
<div class="info-header">
<span class="highlight">Ctrl+C</span> <span class="signal-green">SIGINT</span>
</div>
<div class="info-desc">Stop the running program</div>
<p>Sends SIGINT (signal interrupt) to the foreground process. Most programs respond by stopping immediately. It's how you cancel a long-running command or exit a program that's stuck.</p>
<div class="example-box">
Example: Running `sleep 100` and pressing Ctrl+C stops it immediately.
</div>
</div>
<div v-else-if="activeSignal === 'SIGTSTP'">
<div class="info-header">
<span class="highlight">Ctrl+Z</span> <span class="signal-blue">SIGTSTP</span>
</div>
<div class="info-desc">Suspend the running program</div>
<p>Sends SIGTSTP (signal terminal stop). The process is paused and put in the background. You can resume it later with `fg` command.</p>
<div class="example-box">
Example: Pressing Ctrl+Z pauses a running editor like vim, returning you to the shell.
</div>
</div>
<div v-else>
<div class="info-header">Select a signal</div>
<p>Click on a signal type above to see how it works.</p>
</div>
</div>
</div>
<div class="right-panel">
<div class="terminal-window">
<div class="window-header">
<div class="dots"><span></span><span></span><span></span></div>
</div>
<div class="window-content">
<div v-for="(line, i) in lines" :key="i" class="term-line" :class="line.type">
{{ line.text }}
</div>
<div v-if="isRunning" class="term-line output">sleeping...</div>
<div v-if="inputBuffer" class="term-line input">
<span class="prompt">$</span> {{ inputBuffer }}<span class="cursor"></span>
</div>
<div v-else class="term-line input">
<span class="prompt">$</span> <span class="cursor"></span>
</div>
</div>
</div>
<div class="controls">
<button class="btn" @click="runCommand" :disabled="isRunning">Run Command</button>
<button class="btn" @click="sendSignal('SIGINT')">Ctrl+C</button>
<button class="btn" @click="sendSignal('SIGTSTP')">Ctrl+Z</button>
<button class="btn secondary" @click="reset">Reset</button>
</div>
<div class="state-display">
State: <span :class="stateClass">{{ processState }}</span>
</div>
<p class="instruction">
Click "Run Command" to start a simulated process, then try sending different signals.
</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeSignal = ref('SIGINT')
const isRunning = ref(false)
const lines = ref([
{ type: 'input', text: '$ sleep 100' }
])
const processState = ref('Running')
const inputBuffer = ref('')
const stateClass = computed(() => {
if (processState.value.includes('Running')) return 'state-green'
if (processState.value.includes('interrupted')) return 'state-red'
if (processState.value.includes('suspended')) return 'state-blue'
return ''
})
const runCommand = () => {
reset()
isRunning.value = true
processState.value = 'Running (PID 1234)'
}
const sendSignal = (sig) => {
activeSignal.value = sig
if (!isRunning.value && sig === 'SIGINT') return
if (sig === 'SIGINT') {
lines.value.push({ type: 'output', text: 'sleeping...' })
lines.value.push({ type: 'control', text: '^C' })
isRunning.value = false
processState.value = 'Process interrupted (killed)'
} else if (sig === 'SIGTSTP') {
lines.value.push({ type: 'output', text: 'sleeping...' })
lines.value.push({ type: 'control', text: '^Z' })
lines.value.push({ type: 'output', text: '[1]+ Stopped sleep 100' })
isRunning.value = false
processState.value = 'Process suspended (stopped)'
}
}
const reset = () => {
lines.value = [{ type: 'input', text: '$ sleep 100' }]
isRunning.value = true
processState.value = 'Running (PID 1234)'
}
// Initial state
reset()
</script>
<style scoped>
.signals-demo {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); /* 自动适应宽度,不够时换行 */
gap: 30px;
background: #09090b;
padding: 30px;
border-radius: 12px;
border: 1px solid #27272a;
font-family: 'JetBrains Mono', 'Menlo', monospace;
color: #e4e4e7;
overflow: hidden; /* 防止溢出 */
}
.left-panel {
display: flex;
flex-direction: column;
gap: 20px;
min-width: 0; /* 防止 flex 子项溢出 */
}
.signal-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: #18181b;
border: 1px solid #27272a;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.signal-item:hover {
border-color: #52525b;
}
.signal-item.active {
background: #27272a;
border-left: 3px solid #facc15;
}
.key-combo {
display: flex;
align-items: center;
gap: 5px;
}
.key {
color: #facc15;
font-weight: bold;
}
.action {
color: #a1a1aa;
margin-left: 10px;
font-size: 13px;
}
.signal-name {
color: #22d3ee;
font-weight: bold;
}
.info-box {
background: #18181b;
padding: 20px;
border-radius: 6px;
border: 1px solid #27272a;
font-size: 14px;
line-height: 1.6;
}
.info-header {
font-size: 18px;
margin-bottom: 10px;
font-weight: bold;
}
.highlight { color: #facc15; }
.signal-green { color: #22c55e; }
.signal-blue { color: #3b82f6; }
.info-desc {
color: #a1a1aa;
margin-bottom: 15px;
}
.example-box {
background: #000;
padding: 10px;
border-radius: 4px;
font-size: 13px;
color: #d4d4d8;
margin-top: 15px;
border: 1px solid #27272a;
}
.right-panel {
display: flex;
flex-direction: column;
gap: 20px;
min-width: 0; /* 防止 flex 子项溢出 */
}
.terminal-window {
background: #000;
border: 1px solid #27272a;
border-radius: 8px;
height: 200px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.window-header {
padding: 10px 15px;
border-bottom: 1px solid #27272a;
background: #18181b;
}
.dots span {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background: #3f3f46;
margin-right: 6px;
}
.window-content {
padding: 15px;
flex: 1;
font-size: 14px;
overflow-y: auto;
}
.term-line {
margin-bottom: 5px;
}
.control { color: #ef4444; }
.output { color: #d4d4d8; }
.input { color: #fff; }
.prompt { color: #71717a; margin-right: 8px; }
.cursor {
display: inline-block;
width: 8px;
height: 14px;
background: #a1a1aa;
vertical-align: middle;
}
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap; /* 允许按钮换行 */
}
.btn {
background: #18181b;
border: 1px solid #27272a;
color: #e4e4e7;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
flex: 1;
white-space: nowrap; /* 防止文字换行 */
min-width: 80px; /* 最小宽度 */
transition: all 0.2s;
font-size: 13px;
}
.btn:hover:not(:disabled) {
background: #27272a;
border-color: #52525b;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.state-display {
font-size: 16px;
margin-top: 10px;
}
.state-green { color: #22c55e; }
.state-red { color: #ef4444; }
.state-blue { color: #3b82f6; }
.instruction {
color: #a1a1aa;
font-size: 13px;
}
@media (max-width: 640px) {
.signals-demo {
padding: 20px;
gap: 20px;
}
}
</style>