docs: update terminal intro content and components

This commit is contained in:
sanbuphy
2026-01-15 12:25:48 +08:00
parent c238f07e0d
commit 50d44f7721
8 changed files with 2023 additions and 81 deletions
@@ -15,29 +15,58 @@
<div class="arch-demo">
<div class="analogy-header">
<div class="analogy-item">
<div class="icon">🍽</div>
<div class="icon">🖥</div>
<div class="text">
<div class="role">Terminal = Table (餐桌)</div>
<div class="desc">UI & Input/Output<br>交互界面与输入输出</div>
<div class="role">Terminal (终端)</div>
<div class="desc">传声筒 / 窗口</div>
</div>
</div>
<div class="analogy-item">
<div class="icon">💁</div>
<div class="icon">🗣</div>
<div class="text">
<div class="role">Shell = Waiter (服务员)</div>
<div class="desc">Interpreter & Logic<br>解释器与逻辑处理</div>
<div class="role">Shell ()</div>
<div class="desc">翻译官 / 助手</div>
</div>
</div>
<div class="analogy-item">
<div class="icon">👨🍳</div>
<div class="icon"></div>
<div class="text">
<div class="role">Kernel = Kitchen (后厨)</div>
<div class="desc">System Execution<br>系统执行与硬件调度</div>
<div class="role">Kernel (内核)</div>
<div class="desc">大管家 / 芯片</div>
</div>
</div>
</div>
<div class="diagram-container">
<div class="diagram-container" @click="nextStep" :class="{ 'clickable': currentStep < totalSteps }">
<!-- Click Overlay Hint -->
<div class="click-overlay" v-if="currentStep === 0">
<div class="click-hint">
<span class="icon">👆</span>
<span class="text">不断点击屏幕演示 / Keep Clicking</span>
</div>
</div>
<!-- Completed Overlay -->
<div class="completed-overlay" v-if="currentStep >= totalSteps">
<div class="completed-hint" @click.stop="reset">
<span class="icon"></span>
<span class="text">演示结束点击重置 / Finished (Reset)</span>
</div>
</div>
<!-- Spaces Background -->
<div class="spaces-bg">
<div class="space user-space">
<div class="space-header">User Space (用户空间)</div>
</div>
<div class="barrier">
<div class="barrier-line"></div>
</div>
<div class="space kernel-space">
<div class="space-header">Kernel Space (内核空间)</div>
</div>
</div>
<!-- Terminal Node -->
<div class="node terminal" :class="{ active: activeNode === 'terminal' }">
<div class="node-title">TERMINAL (终端)</div>
@@ -181,8 +210,8 @@ const steps = [
{
titleEn: "3. Shell Parsing",
titleZh: "3. Shell 解析",
descEn: "The Shell receives the characters and figures out what you want.",
descZh: "Shell 接收到字符,并解析你的意图。",
descEn: "The Shell (Waiter) translates your command for the Kernel.",
descZh: "Shell(服务员)接收指令,并将其翻译成内核能听懂的请求。",
techEn: "Shell tokenizes input, finds the 'ls' executable in $PATH.",
techZh: "Shell 对输入进行分词,并在 $PATH 环境变量中查找 'ls' 可执行文件。",
action: async () => {
@@ -207,8 +236,8 @@ const steps = [
{
titleEn: "5. Kernel Execution",
titleZh: "5. 内核执行",
descEn: "The Kernel (the boss) talks to the hardware to get the actual data.",
descZh: "内核(大管家)与硬件通信以获取实际数据。",
descEn: "The Kernel (Kitchen) executes the request by accessing hardware.",
descZh: "内核(后厨)直接操作硬件(如磁盘)来执行实际任务。",
techEn: "Kernel driver accesses the file system (APFS/ext4).",
techZh: "内核驱动程序访问文件系统 (APFS/ext4)。",
action: async () => {
@@ -366,9 +395,169 @@ const reset = () => {
justify-content: space-between;
align-items: center;
position: relative;
padding: 0 10px;
/* Increase padding to accommodate labels */
padding: 40px 10px 20px 10px;
z-index: 1;
cursor: default;
transition: background 0.3s;
}
.diagram-container.clickable {
cursor: pointer;
}
.diagram-container.clickable:hover {
background: rgba(255, 255, 255, 0.02);
}
.click-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 50; /* Topmost */
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(2px);
border-radius: 12px;
animation: pulse-bg 2s infinite;
}
.click-hint {
background: #22c55e;
color: #000;
padding: 10px 20px;
border-radius: 30px;
font-weight: bold;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 15px rgba(34, 197, 94, 0.4);
transform: scale(1);
transition: transform 0.2s;
}
.diagram-container:hover .click-hint {
transform: scale(1.05);
}
@keyframes pulse-bg {
0% { background: rgba(0, 0, 0, 0.4); }
50% { background: rgba(0, 0, 0, 0.2); }
100% { background: rgba(0, 0, 0, 0.4); }
}
.completed-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 50; /* Same as click overlay */
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(2px);
animation: fade-in 0.5s;
}
.completed-hint {
background: #10b981;
color: #fff;
padding: 10px 20px;
border-radius: 30px;
font-weight: bold;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4);
cursor: pointer;
transition: transform 0.2s;
}
.completed-hint:hover {
transform: scale(1.05);
background: #059669;
}
.spaces-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
z-index: 0;
pointer-events: none;
}
.space {
height: 100%;
display: flex;
flex-direction: column;
}
.space-header {
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
padding: 8px;
opacity: 0.7;
}
.user-space {
flex: 2;
background: rgba(34, 211, 238, 0.03);
border-right: 1px dashed #3f3f46;
border-radius: 8px 0 0 8px;
align-items: flex-start;
/* Ensure User Space (containing Shell) is below the Barrier Label */
z-index: 0;
}
.user-space .space-header { color: #22d3ee; }
.kernel-space {
flex: 1;
background: rgba(239, 68, 68, 0.03);
border-radius: 0 8px 8px 0;
align-items: flex-end;
z-index: 0;
}
.kernel-space .space-header { color: #ef4444; }
.barrier {
width: 2px;
background: transparent;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 10; /* Bring Barrier to front */
}
.barrier-line {
width: 2px;
height: 100%;
background: repeating-linear-gradient(
to bottom,
#facc15 0,
#facc15 10px,
transparent 10px,
transparent 20px
);
opacity: 0.3;
}
.node {
background: #18181b;
border: 2px solid #27272a;
@@ -378,10 +567,15 @@ const reset = () => {
display: flex;
flex-direction: column;
transition: all 0.3s;
z-index: 2;
z-index: 5; /* Nodes should be above spaces but below barrier label if overlapping */
position: relative;
}
/* Specific z-index for Shell to prevent it from covering barrier label */
.node.shell {
z-index: 1;
}
.node.active {
border-color: #22c55e;
box-shadow: 0 0 15px rgba(34, 197, 94, 0.2);
@@ -0,0 +1,284 @@
<template>
<div class="buffer-demo">
<div class="terminal-frame">
<div class="window-bar">
<span class="dot red"></span>
<span class="dot yellow"></span>
<span class="dot green"></span>
<span class="title">Terminal - Buffer Switching Demo</span>
</div>
<div class="screen-container">
<!-- Main Buffer (Layer 0) -->
<div class="buffer main-buffer">
<div class="line"><span class="prompt"></span> <span class="cmd">ls -la</span></div>
<div class="line output">total 16</div>
<div class="line output">drwxr-xr-x 2 user staff 64 Jan 15 10:00 .</div>
<div class="line output">drwxr-xr-x 4 user staff 128 Jan 15 09:55 ..</div>
<div class="line output">-rw-r--r-- 1 user staff 1024 Jan 15 10:00 notes.txt</div>
<div class="line"><span class="prompt"></span> <span class="cmd">echo "Hello World"</span></div>
<div class="line output">Hello World</div>
<div class="line"><span class="prompt"></span> <span class="cmd">vim notes.txt</span></div>
<!-- The cursor would be here if not in vim -->
</div>
<!-- Alternate Buffer (Layer 1) -->
<transition name="slide-up">
<div v-if="isAltBufferActive" class="buffer alt-buffer">
<div class="vim-header">
<span class="filename">notes.txt</span>
<span class="modified">[+]</span>
</div>
<div class="vim-body">
<div class="line-num">1</div><div class="code">This is a text file opened in Vim.</div>
<div class="line-num">2</div><div class="code"></div>
<div class="line-num">3</div><div class="code">Notice how this interface takes up</div>
<div class="line-num">4</div><div class="code">the entire screen?</div>
<div class="line-num">5</div><div class="code"></div>
<div class="line-num">6</div><div class="code">It is running in the <span class="highlight">Alternate Buffer</span>.</div>
<div class="line-num">~</div>
<div class="line-num">~</div>
</div>
<div class="vim-status-bar">
<span class="mode">NORMAL</span>
<span class="file-info">notes.txt [unix] (10:24)</span>
</div>
<div class="vim-cmd-line">
<span v-if="showQuitCmd">:q</span>
</div>
</div>
</transition>
</div>
</div>
<div class="controls">
<div class="description">
<div v-if="!isAltBufferActive">
<p><strong>Current: Primary Buffer (主缓冲区)</strong></p>
<p>This is the standard scrolling log. Commands are executed line by line.</p>
<button class="action-btn" @click="openVim">
Execute `vim notes.txt`
</button>
</div>
<div v-else>
<p><strong>Current: Alternate Buffer (备用缓冲区)</strong></p>
<p>A separate "canvas" for full-screen apps. It hides the history but doesn't delete it.</p>
<button class="action-btn red" @click="quitVim">
Execute `:q` (Quit)
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isAltBufferActive = ref(false)
const showQuitCmd = ref(false)
const openVim = () => {
isAltBufferActive.value = true
showQuitCmd.value = false
}
const quitVim = () => {
showQuitCmd.value = true
setTimeout(() => {
isAltBufferActive.value = false
}, 500)
}
</script>
<style scoped>
.buffer-demo {
margin: 20px 0;
font-family: 'Menlo', 'Monaco', monospace;
}
.terminal-frame {
background: #1e1e1e;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
border: 1px solid #333;
}
.window-bar {
background: #2d2d2d;
padding: 8px 12px;
display: flex;
align-items: center;
gap: 6px;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.red { background: #ff5f56; }
.yellow { background: #ffbd2e; }
.green { background: #27c93f; }
.title {
margin-left: 10px;
font-size: 12px;
color: #999;
}
.screen-container {
position: relative;
height: 240px;
background: #000;
overflow: hidden;
}
.buffer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 10px;
box-sizing: border-box;
}
/* Main Buffer Styles */
.main-buffer {
color: #ccc;
font-size: 13px;
line-height: 1.5;
}
.prompt {
color: #27c93f;
margin-right: 8px;
}
.cmd {
font-weight: bold;
color: #fff;
}
.output {
color: #888;
}
/* Alt Buffer Styles (Vim Look) */
.alt-buffer {
background: #282c34;
color: #abb2bf;
display: flex;
flex-direction: column;
z-index: 10;
}
.vim-header {
display: none; /* Vim doesn't usually have a top header like this, but helpful for context? Maybe skip */
}
.vim-body {
flex: 1;
font-size: 14px;
line-height: 1.6;
}
.line-num {
display: inline-block;
width: 30px;
color: #5c6370;
text-align: right;
margin-right: 10px;
}
.code {
display: inline-block;
}
.highlight {
color: #e5c07b;
font-weight: bold;
}
.vim-status-bar {
background: #3e4452;
color: #ccc;
padding: 4px 10px;
font-size: 12px;
display: flex;
justify-content: space-between;
}
.mode {
font-weight: bold;
background: #98c379;
color: #282c34;
padding: 0 5px;
margin-right: 10px;
}
.vim-cmd-line {
height: 24px;
display: flex;
align-items: center;
padding: 0 5px;
}
/* Transitions */
.slide-up-enter-active,
.slide-up-leave-active {
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.slide-up-enter-from,
.slide-up-leave-to {
transform: translateY(100%);
}
.controls {
margin-top: 15px;
background: #f6f6f7;
border-radius: 8px;
padding: 15px;
border: 1px solid #eee;
}
.dark .controls {
background: #252529;
border-color: #333;
}
.action-btn {
background: #27c93f;
color: #fff;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
font-size: 14px;
transition: all 0.2s;
margin-top: 10px;
}
.action-btn:hover {
background: #22b036;
transform: translateY(-1px);
}
.action-btn.red {
background: #ff5f56;
}
.action-btn.red:hover {
background: #e0483e;
}
p {
margin: 5px 0;
font-size: 14px;
color: var(--vp-c-text-1);
}
</style>
@@ -0,0 +1,362 @@
<template>
<div class="cooked-raw-demo">
<div class="mode-switch">
<button
:class="{ active: mode === 'cooked' }"
@click="setMode('cooked')"
>
🥘 Cooked Mode (Standard)
</button>
<button
:class="{ active: mode === 'raw' }"
@click="setMode('raw')"
>
🥩 Raw Mode (Vim/Games)
</button>
</div>
<div class="demo-container" @click="focusInput">
<!-- Hidden Input for capturing keystrokes -->
<input
ref="inputRef"
type="text"
class="hidden-input"
@keydown="handleKey"
@blur="isFocused = false"
@focus="isFocused = true"
/>
<!-- Visualization -->
<div class="flow-diagram">
<!-- 1. User Input -->
<div class="stage user-input" :class="{ focused: isFocused }">
<div class="stage-title">1. Keyboard Input</div>
<div class="key-visual">
<span v-if="lastPressedKey" class="key-cap">{{ lastPressedKey }}</span>
<span v-else class="placeholder">Type here...</span>
</div>
<div class="status-text" v-if="!isFocused">Click to focus</div>
</div>
<div class="arrow"></div>
<!-- 2. OS Buffer (Only for Cooked) -->
<div class="stage buffer" :class="{ disabled: mode === 'raw', active: mode === 'cooked' }">
<div class="stage-title">
2. Line Buffer (Kernel)
<span class="badge" v-if="mode === 'cooked'">Active</span>
<span class="badge disabled" v-else>Bypassed</span>
</div>
<div class="buffer-content">
<template v-if="mode === 'cooked'">
<span v-for="(char, i) in buffer" :key="i" class="char">{{ char }}</span>
<span class="cursor">_</span>
</template>
<template v-else>
<span class="bypass-text"> Direct Pass-through </span>
</template>
</div>
<div class="explanation">
<span v-if="mode === 'cooked'">Waiting for Enter... (Backspace works)</span>
<span v-else>No buffering. Every key is sent immediately.</span>
</div>
</div>
<div class="arrow"></div>
<!-- 3. Application -->
<div class="stage app-input">
<div class="stage-title">3. Application Receives</div>
<div class="app-content">
<div v-for="(line, i) in appLines" :key="i" class="app-line">
{{ line }}
</div>
<div class="app-line current">
{{ appCurrentLine }}<span class="app-cursor">_</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const mode = ref('cooked')
const buffer = ref([])
const appLines = ref([])
const appCurrentLine = ref('')
const lastPressedKey = ref('')
const inputRef = ref(null)
const isFocused = ref(false)
const setMode = (m) => {
mode.value = m
buffer.value = []
appLines.value = []
appCurrentLine.value = ''
lastPressedKey.value = ''
// Focus input
setTimeout(() => inputRef.value?.focus(), 50)
}
const focusInput = () => {
inputRef.value?.focus()
}
const handleKey = (e) => {
e.preventDefault()
const key = e.key
// Visual feedback
if (key === ' ') lastPressedKey.value = 'Space'
else if (key === 'Enter') lastPressedKey.value = 'Enter'
else if (key === 'Backspace') lastPressedKey.value = '⌫'
else if (key.length === 1) lastPressedKey.value = key
else lastPressedKey.value = key
// Clear visual feedback after delay
setTimeout(() => {
if (lastPressedKey.value === (key === ' ' ? 'Space' : (key === 'Enter' ? 'Enter' : (key === 'Backspace' ? '⌫' : key)))) {
// lastPressedKey.value = '' // Optional: keep last key visible
}
}, 500)
if (mode.value === 'cooked') {
handleCookedMode(e)
} else {
handleRawMode(e)
}
}
const handleCookedMode = (e) => {
if (e.key === 'Enter') {
// Flush buffer to app
const text = buffer.value.join('')
appLines.value.push(text)
buffer.value = []
} else if (e.key === 'Backspace') {
buffer.value.pop()
} else if (e.key.length === 1) {
buffer.value.push(e.key)
}
}
const handleRawMode = (e) => {
// Immediate send
if (e.key === 'Enter') {
appLines.value.push(appCurrentLine.value)
appCurrentLine.value = ''
} else if (e.key === 'Backspace') {
// In raw mode, Backspace is just a control code sent to app
// But for demo visualization, let's show it effectively deletes in app if app handles it
// Or strictly show control code? Let's simulate app handling it immediately.
appCurrentLine.value = appCurrentLine.value.slice(0, -1)
} else if (e.key.length === 1) {
appCurrentLine.value += e.key
}
}
</script>
<style scoped>
.cooked-raw-demo {
margin: 24px 0;
font-family: 'Menlo', 'Monaco', monospace;
user-select: none;
}
.mode-switch {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.mode-switch button {
flex: 1;
padding: 10px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 8px;
cursor: pointer;
font-weight: bold;
transition: all 0.2s;
}
.mode-switch button.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.demo-container {
background: #1e1e1e;
border-radius: 8px;
padding: 20px;
border: 1px solid #333;
position: relative;
cursor: text;
}
.hidden-input {
position: absolute;
opacity: 0;
top: 0;
left: 0;
height: 100%;
width: 100%;
cursor: text;
}
.flow-diagram {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.stage {
width: 100%;
background: #2d2d2d;
border: 1px solid #444;
border-radius: 6px;
padding: 15px;
transition: all 0.3s;
}
.stage-title {
font-size: 12px;
color: #888;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.stage.user-input.focused {
border-color: var(--vp-c-brand);
box-shadow: 0 0 10px rgba(var(--vp-c-brand-rgb), 0.1);
}
.key-visual {
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.key-cap {
background: #eee;
color: #333;
padding: 4px 12px;
border-radius: 4px;
font-weight: bold;
box-shadow: 0 2px 0 #ccc;
font-size: 16px;
animation: pop 0.1s;
}
.placeholder {
color: #555;
font-style: italic;
}
.status-text {
text-align: center;
font-size: 11px;
color: #666;
margin-top: 5px;
}
.arrow {
color: #555;
font-size: 20px;
}
/* Buffer */
.stage.buffer.active {
background: #25332e;
border-color: #0dbc79;
}
.stage.buffer.disabled {
opacity: 0.5;
background: #222;
border-style: dashed;
}
.buffer-content {
background: #000;
padding: 10px;
border-radius: 4px;
min-height: 40px;
color: #0dbc79;
font-size: 14px;
display: flex;
align-items: center;
}
.bypass-text {
color: #e5e510;
font-style: italic;
font-size: 12px;
width: 100%;
text-align: center;
}
.explanation {
font-size: 11px;
color: #999;
margin-top: 8px;
}
/* App */
.stage.app-input {
background: #252526;
}
.app-content {
background: #000;
padding: 10px;
border-radius: 4px;
min-height: 80px;
color: #ccc;
font-size: 14px;
}
.app-line {
min-height: 20px;
}
.badge {
font-size: 10px;
padding: 2px 6px;
border-radius: 3px;
background: #0dbc79;
color: #000;
}
.badge.disabled {
background: #555;
color: #ccc;
}
.cursor, .app-cursor {
display: inline-block;
width: 8px;
background: currentColor;
animation: blink 1s infinite;
}
@keyframes pop {
0% { transform: scale(0.9); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
@keyframes blink { 50% { opacity: 0; } }
</style>
@@ -0,0 +1,418 @@
<template>
<div class="parser-demo">
<div class="demo-header">
<div class="title">转义序列解析原理 (Parser Mechanism)</div>
<div class="controls">
<button @click="reset" :disabled="isPlaying">Reset</button>
<button @click="togglePlay" :disabled="isFinished" class="play-btn">
{{ isPlaying ? '⏸ Pause' : '▶ Play Animation' }}
</button>
</div>
</div>
<!-- 1. 字节流传送带 -->
<div class="stream-container">
<div class="label">Input Byte Stream / 输入字节流</div>
<div class="stream-track">
<div class="stream-window-mask">
<div class="stream-content" :style="{ transform: `translateX(-${currentIndex * 40}px)` }">
<div
v-for="(char, index) in charStream"
:key="index"
class="char-box"
:class="{
'active': index === currentIndex,
'processed': index < currentIndex,
'special': char.isSpecial,
'arg': char.isArg
}"
>
<span class="char-val">{{ char.display }}</span>
<span class="char-code">{{ char.hex }}</span>
</div>
</div>
</div>
<!-- 指针 -->
<div class="pointer">
<div class="arrow"></div>
<div class="pointer-label">Current Byte</div>
</div>
</div>
</div>
<!-- 2. 解析器状态机 -->
<div class="parser-state-machine">
<div class="state-box" :class="{ active: parserState === 'NORMAL' }">
<div class="state-name">NORMAL</div>
<div class="state-desc">Print Characters</div>
</div>
<div class="arrow-right"></div>
<div class="state-box warning" :class="{ active: parserState === 'ESCAPE' }">
<div class="state-name">ESCAPE MODE</div>
<div class="state-desc">Buffer Command...</div>
</div>
<!-- 指令说明框 -->
<div class="action-log" v-if="lastAction">
<span class="action-icon"></span>
{{ lastAction }}
</div>
</div>
<!-- 3. 终端屏幕 -->
<div class="terminal-screen">
<div class="label">Terminal Screen / 屏幕显示</div>
<div class="screen-content">
<span
v-for="(char, index) in outputBuffer"
:key="index"
:style="char.style"
>{{ char.val }}</span><span class="cursor">_</span>
</div>
</div>
<div class="explanation">
<p>
<span class="badge normal">Normal</span> 模式下字符直接上屏
<span class="badge escape">Escape</span> 模式下遇到 <code>ESC</code> 终端<strong>停止输出</strong>开始收集字符作为指令直到指令结束 <code>m</code>并执行
</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 原始字符串: Hello [RED]World[RESET]!
// \x1B [ 3 1 m
const RAW_DATA = [
{ val: 'H', display: 'H', hex: '48' },
{ val: 'i', display: 'i', hex: '69' },
{ val: ' ', display: ' ', hex: '20' },
{ val: '\x1B', display: 'ESC', hex: '1B', isSpecial: true },
{ val: '[', display: '[', hex: '5B', isSpecial: true },
{ val: '3', display: '3', hex: '33', isArg: true },
{ val: '1', display: '1', hex: '31', isArg: true },
{ val: 'm', display: 'm', hex: '6D', isSpecial: true }, // End of seq
{ val: 'V', display: 'V', hex: '56' },
{ val: 'i', display: 'i', hex: '69' },
{ val: 'b', display: 'b', hex: '62' },
{ val: 'e', display: 'e', hex: '65' },
{ val: '\x1B', display: 'ESC', hex: '1B', isSpecial: true },
{ val: '[', display: '[', hex: '5B', isSpecial: true },
{ val: '0', display: '0', hex: '30', isArg: true },
{ val: 'm', display: 'm', hex: '6D', isSpecial: true },
{ val: '!', display: '!', hex: '21' }
]
const charStream = ref(RAW_DATA)
const currentIndex = ref(0)
const outputBuffer = ref([])
const parserState = ref('NORMAL') // NORMAL, ESCAPE
const currentStyle = ref({})
const isPlaying = ref(false)
const isFinished = ref(false)
const lastAction = ref('')
const reset = () => {
currentIndex.value = 0
outputBuffer.value = []
parserState.value = 'NORMAL'
currentStyle.value = {}
isPlaying.value = false
isFinished.value = false
lastAction.value = ''
}
const play = async () => {
if (isPlaying.value) return
isPlaying.value = true
while (currentIndex.value < charStream.value.length) {
const char = charStream.value[currentIndex.value]
// Processing Logic
if (parserState.value === 'NORMAL') {
if (char.val === '\x1B') {
parserState.value = 'ESCAPE'
lastAction.value = 'Start Sequence'
} else {
outputBuffer.value.push({
val: char.val,
style: { ...currentStyle.value }
})
lastAction.value = 'Print Char'
}
} else if (parserState.value === 'ESCAPE') {
// 简单模拟:遇到 'm' 结束
if (char.val === 'm') {
// 解析指令 (Hardcoded for demo)
const prevChar = charStream.value[currentIndex.value - 1]
if (prevChar.val === '1') {
currentStyle.value = { color: '#ff5f56', fontWeight: 'bold' }
lastAction.value = 'Execute: Set Color Red'
} else if (prevChar.val === '0') {
currentStyle.value = {}
lastAction.value = 'Execute: Reset Style'
}
// Small delay to show "Executing" state
await new Promise(r => setTimeout(r, 200))
parserState.value = 'NORMAL'
} else {
lastAction.value = 'Buffering...'
}
}
await new Promise(r => setTimeout(r, 600)) // Animation speed
currentIndex.value++
}
isPlaying.value = false
isFinished.value = true
lastAction.value = 'Done'
}
</script>
<style scoped>
.parser-demo {
background: #1e1e1e;
border-radius: 8px;
padding: 20px;
color: #fff;
font-family: 'Menlo', monospace;
margin: 20px 0;
border: 1px solid #333;
}
.demo-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.title {
font-weight: bold;
color: #ccc;
}
.controls button {
background: #333;
border: 1px solid #555;
color: white;
padding: 5px 12px;
border-radius: 4px;
cursor: pointer;
margin-left: 10px;
font-size: 12px;
}
.controls button.play-btn {
background: #0dbc79;
border-color: #0dbc79;
color: #000;
font-weight: bold;
}
.controls button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Stream Track */
.stream-container {
background: #111;
padding: 15px;
border-radius: 6px;
margin-bottom: 20px;
position: relative;
overflow: hidden;
}
.label {
font-size: 10px;
color: #666;
text-transform: uppercase;
margin-bottom: 8px;
display: block;
}
.stream-track {
position: relative;
height: 60px;
display: flex;
justify-content: center; /* Center the focus area */
}
.stream-window-mask {
width: 100%;
overflow: hidden;
position: relative;
/* Mask gradient to fade edges */
mask-image: linear-gradient(to right, transparent, black 40%, black 60%, transparent);
-webkit-mask-image: linear-gradient(to right, transparent, black 40%, black 60%, transparent);
}
.stream-content {
display: flex;
gap: 4px;
position: absolute;
left: 50%; /* Start from center */
transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1);
padding-left: 20px; /* Offset for first item */
}
.char-box {
width: 36px;
height: 48px;
background: #2d2d2d;
border: 1px solid #444;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 4px;
flex-shrink: 0;
transition: all 0.3s;
}
.char-box.active {
background: #fff;
color: #000;
transform: scale(1.1);
box-shadow: 0 0 10px rgba(255,255,255,0.5);
z-index: 10;
border-color: #fff;
}
.char-box.processed {
opacity: 0.3;
}
.char-box.special {
border-color: #e5e510;
color: #e5e510;
}
.char-box.active.special {
background: #e5e510;
color: #000;
}
.char-box.arg {
border-color: #11a8cd;
color: #11a8cd;
}
.char-box.active.arg {
background: #11a8cd;
color: #fff;
}
.char-val { font-size: 14px; font-weight: bold; }
.char-code { font-size: 9px; opacity: 0.7; margin-top: 2px; }
.pointer {
position: absolute;
bottom: -5px;
left: 50%;
transform: translateX(-50%);
text-align: center;
color: #0dbc79;
}
.arrow { font-size: 20px; line-height: 1; }
.pointer-label { font-size: 10px; white-space: nowrap; }
/* State Machine */
.parser-state-machine {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-bottom: 20px;
background: #252526;
padding: 10px;
border-radius: 6px;
height: 60px;
}
.state-box {
padding: 8px 16px;
border-radius: 4px;
background: #333;
opacity: 0.3;
text-align: center;
transition: all 0.3s;
min-width: 100px;
}
.state-box.active {
opacity: 1;
background: #0dbc79;
color: #000;
box-shadow: 0 0 15px rgba(13, 188, 121, 0.2);
}
.state-box.warning.active {
background: #e5e510;
color: #000;
}
.state-name { font-weight: bold; font-size: 12px; }
.state-desc { font-size: 10px; opacity: 0.8; }
.arrow-right { color: #555; font-size: 18px; }
.action-log {
margin-left: 20px;
padding: 4px 12px;
background: #000;
border-radius: 4px;
border: 1px solid #444;
font-size: 12px;
color: #fff;
display: flex;
align-items: center;
gap: 6px;
animation: flash 0.5s;
}
/* Screen */
.terminal-screen {
background: #000;
border: 1px solid #333;
border-radius: 6px;
padding: 15px;
min-height: 80px;
}
.screen-content {
font-size: 16px;
line-height: 1.5;
}
.cursor {
animation: blink 1s infinite;
color: #0dbc79;
}
.explanation {
margin-top: 15px;
font-size: 13px;
color: #999;
line-height: 1.5;
}
.badge {
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
color: #000;
}
.badge.normal { background: #0dbc79; }
.badge.escape { background: #e5e510; }
@keyframes blink { 50% { opacity: 0; } }
@keyframes flash { 0% { background: #333; } 100% { background: #000; } }
</style>
@@ -82,11 +82,12 @@
<div class="stream-line dashed"></div>
<div class="stream-label">Event Loop / 事件循环</div>
<div
v-if="guiEvent"
v-for="ev in guiEvents"
:key="ev.id"
class="gui-event-packet"
:style="{ left: guiEvent.progress + '%' }"
:style="{ left: ev.progress + '%' }"
>
{{ guiEvent.type }}
{{ ev.type }}
</div>
</div>
@@ -135,7 +136,7 @@ const mode = ref('cli') // 'cli' | 'gui'
const isAnimating = ref(false)
const activeChars = ref([])
const typedContent = ref('')
const demoText = 'ls -la'
const demoText = 'echo "hello world"'
const startSimulation = () => {
if (isAnimating.value) return
@@ -168,7 +169,7 @@ const startSimulation = () => {
// Animate this char
let progress = 10
const interval = setInterval(() => {
progress += 2
progress += 4 // Faster speed
const charObj = activeChars.value.find(c => c.id === charId)
if (charObj) charObj.progress = progress
@@ -180,7 +181,7 @@ const startSimulation = () => {
// Next char
index++
setTimeout(processNextChar, 300)
setTimeout(processNextChar, 100) // Faster typing
}
}, 20)
}
@@ -191,60 +192,92 @@ const startSimulation = () => {
// GUI Logic
const isGuiAnimating = ref(false)
const isGuiClicking = ref(false)
const guiEvent = ref(null)
const guiEvents = ref([]) // Array of events
const iconSelected = ref(false)
const cursorPosition = ref({ x: 50, y: 50 })
const inputMousePosition = ref({ x: 80, y: 60 }) // Input side (Invisible physical mouse)
const screenCursorPosition = ref({ x: 80, y: 60 }) // Output side (Visible screen cursor)
const cursorStyle = computed(() => ({
transform: `translate(${cursorPosition.value.x}px, ${cursorPosition.value.y}px)`
transform: `translate(${screenCursorPosition.value.x}px, ${screenCursorPosition.value.y}px)`
}))
const startGuiSimulation = () => {
if (isGuiAnimating.value) return
isGuiAnimating.value = true
iconSelected.value = false
cursorPosition.value = { x: 80, y: 60 } // Reset pos
inputMousePosition.value = { x: 80, y: 60 }
screenCursorPosition.value = { x: 80, y: 60 }
guiEvents.value = []
// 1. Move Cursor
// 1. Move Cursor (Physical Mouse Movement)
let step = 0
const moveInterval = setInterval(() => {
step++
cursorPosition.value = {
inputMousePosition.value = {
x: 80 - step * 2,
y: 60 - step * 1.5
}
// Emit Move Event frequently (Simulate high polling rate)
if (step % 2 === 0) {
const targetX = inputMousePosition.value.x
const targetY = inputMousePosition.value.y
emitGuiEvent(`Move(${Math.round(targetX)},${Math.round(targetY)})`, () => {
// When packet arrives: Update screen cursor
screenCursorPosition.value = { x: targetX, y: targetY }
})
}
if (step >= 20) {
clearInterval(moveInterval)
// 2. Click
performClick()
}
}, 20)
}, 50)
}
const emitGuiEvent = (type, onArrive) => {
const eventId = Date.now() + Math.random()
const newEvent = {
id: eventId,
type: type,
progress: 10
}
guiEvents.value.push(newEvent)
let progress = 10
const packetInterval = setInterval(() => {
progress += 2
const ev = guiEvents.value.find(e => e.id === eventId)
if (ev) ev.progress = progress
if (progress >= 90) {
clearInterval(packetInterval)
guiEvents.value = guiEvents.value.filter(e => e.id !== eventId)
// Execute callback when packet arrives at Output
if (onArrive) onArrive()
}
}, 10)
}
const performClick = () => {
setTimeout(() => {
isGuiClicking.value = true
// Send Event Packet
guiEvent.value = { type: 'Click(40,30)', progress: 10 }
let progress = 10
const packetInterval = setInterval(() => {
progress += 2
if (guiEvent.value) guiEvent.value.progress = progress
if (progress >= 90) {
clearInterval(packetInterval)
guiEvent.value = null
isGuiClicking.value = false
iconSelected.value = true // Effect
// Send Click Event
emitGuiEvent('Click(40,30)', () => {
// When packet arrives: Select icon
iconSelected.value = true
setTimeout(() => {
isGuiAnimating.value = false
}, 1000)
}
}, 10)
})
setTimeout(() => {
isGuiClicking.value = false
}, 200) // Input click feedback is fast
}, 300)
}
@@ -405,7 +438,7 @@ const performClick = () => {
top: 0;
left: 0;
pointer-events: none;
transition: transform 0.05s linear;
transition: transform 0.1s linear; /* Smooth interpolation */
filter: drop-shadow(0 1px 1px rgba(0,0,0,0.5));
}
@@ -0,0 +1,618 @@
<template>
<div class="terminal-os-demo">
<div class="os-switch">
<button
v-for="os in osList"
:key="os.id"
:class="{ active: currentOS === os.id }"
@click="switchOS(os.id)"
>
<span class="os-icon">{{ os.icon }}</span>
{{ os.name }}
</button>
</div>
<div class="terminal-window" :class="currentOS">
<div class="window-bar">
<div class="window-buttons">
<span class="btn close"></span>
<span class="btn minimize"></span>
<span class="btn maximize"></span>
</div>
<div class="window-title">{{ currentOSConfig.title }}</div>
<div class="window-controls">
<button class="control-btn" @click="resetDemo" title="Reset"></button>
</div>
</div>
<div class="terminal-content" @click="nextStep" :class="{ 'clickable': !isTyping && !isFinished }">
<!-- Start Overlay -->
<div class="start-overlay" v-if="lines.length === 0 || (lines.length === 1 && lines[0].content === '' && currentStepIndex === -1)">
<div class="start-hint">
<span class="icon">👆</span>
<span class="text">不断点击屏幕演示 / Keep Clicking</span>
</div>
</div>
<!-- Completed Overlay -->
<div class="completed-overlay" v-if="isFinished">
<div class="completed-hint" @click.stop="resetDemo">
<span class="icon"></span>
<span class="text">演示结束点击重置 / Finished (Reset)</span>
</div>
</div>
<div v-for="(line, index) in lines" :key="index" class="line">
<template v-if="line.type === 'input'">
<span class="prompt">{{ line.prompt }}</span><span class="cmd-text">{{ line.content }}</span>
</template>
<template v-else>
<span class="output-text">{{ line.content }}</span>
</template>
</div>
<!-- Active Input Line (when not animating or just waiting) -->
<div class="line input-line" v-if="lines.length === 0 || (!isTyping && lines[lines.length-1].type !== 'input' && !isFinished)">
<span class="prompt">{{ currentOSConfig.prompt }}</span>
<span class="cursor">_</span>
<span v-if="lines.length === 0" class="hint"> (点击屏幕继续 / Click screen to continue)</span>
<span v-else class="hint blink-hint"> </span>
</div>
</div>
<!-- Explanation Bar -->
<div class="explanation-bar" :class="{ visible: currentExplanation }">
<span class="icon">💡</span>
<span class="text">{{ currentExplanation }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const currentOS = ref('win-cmd')
const isTyping = ref(false)
const lines = ref([])
const currentExplanation = ref('')
const currentStepIndex = ref(-1)
const osList = [
{ id: 'win-cmd', name: 'Windows CMD', icon: '🪟' },
{ id: 'win-ps', name: 'Windows PowerShell', icon: '⚡' },
{ id: 'linux', name: 'Linux Terminal', icon: '🐧' }
]
const configs = {
'win-cmd': {
title: 'Command Prompt',
prompt: 'C:\\Users\\User>',
demo: [
{ type: 'explanation', content: '准备输入命令...' },
{ type: 'command', content: 'dir', delay: 400, explanation: '输入 `dir` (Directory)。这是 Windows 系统用来**列出当前文件夹内容**的命令。' },
{ type: 'output', content: ' Volume in drive C has no label.', delay: 100, explanation: '系统正在执行命令...' },
{ type: 'output', content: ' Volume Serial Number is A1B2-C3D4', delay: 50 },
{ type: 'output', content: '', delay: 50 },
{ type: 'output', content: ' Directory of C:\\Users\\User', delay: 50 },
{ type: 'output', content: '', delay: 50 },
{ type: 'output', content: '01/15/2026 10:00 AM <DIR> .', delay: 50 },
{ type: 'output', content: '01/15/2026 10:00 AM <DIR> ..', delay: 50 },
{ type: 'output', content: '01/15/2026 10:00 AM 128 demo.txt', delay: 50 },
{ type: 'output', content: ' 1 File(s) 128 bytes', delay: 50 },
{ type: 'output', content: ' 2 Dir(s) 50,000,000,000 bytes free', delay: 50, explanation: '系统返回了文件列表。`<DIR>` 表示这是一个文件夹,数字表示文件大小。' },
{ type: 'output', content: '', delay: 100 }
]
},
'win-ps': {
title: 'Windows PowerShell',
prompt: 'PS C:\\Users\\User>',
demo: [
{ type: 'explanation', content: '准备输入命令...' },
{ type: 'command', content: 'Get-Date', delay: 400, explanation: '输入 `Get-Date`。PowerShell 使用动词-名词的命名方式,这里是**获取当前时间**。' },
{ type: 'output', content: '', delay: 100, explanation: '系统返回了当前的日期和时间。' },
{ type: 'output', content: 'Thursday, January 15, 2026 10:00:00 AM', delay: 100 },
{ type: 'output', content: '', delay: 100 },
{ type: 'command', content: 'echo "Hello World"', delay: 400, explanation: '输入 `echo`。这是让计算机**复读**你说的话,常用于测试或打印信息。' },
{ type: 'output', content: 'Hello World', delay: 100, explanation: '计算机乖乖地输出了 "Hello World"。' }
]
},
'linux': {
title: 'user@hostname: ~',
prompt: 'user@hostname:~$ ',
demo: [
{ type: 'explanation', content: '准备输入命令...' },
{ type: 'command', content: 'ls -la', delay: 400, explanation: '输入 `ls` (List)。这是 Linux/Mac 系统用来**列出文件**的命令。`-la` 是参数,表示“列出所有文件(all)的详细信息(long)”。' },
{ type: 'output', content: 'total 8', delay: 100, explanation: '系统返回了文件列表。左边的 `drwxr-xr-x` 看起来像乱码,其实是**权限描述**(谁能读、谁能写)。' },
{ type: 'output', content: 'drwxr-xr-x 2 user user 4096 Jan 15 10:00 .', delay: 50 },
{ type: 'output', content: 'drwxr-xr-x 3 user user 4096 Jan 15 10:00 ..', delay: 50 },
{ type: 'output', content: '-rw-r--r-- 1 user user 128 Jan 15 10:00 demo.txt', delay: 50 },
{ type: 'command', content: 'whoami', delay: 400, explanation: '输入 `whoami` (Who am I)。这是一个经典的哲学命令(笑),告诉计算机:**我是谁?**(当前登录用户)。' },
{ type: 'output', content: 'user', delay: 100, explanation: '系统回答:你是 "user"。' }
]
}
}
const currentOSConfig = computed(() => configs[currentOS.value])
const isFinished = computed(() => currentOSConfig.value && currentStepIndex.value >= currentOSConfig.value.demo.length - 1)
const switchOS = (id) => {
currentOS.value = id
resetDemo()
}
const resetDemo = () => {
lines.value = []
currentExplanation.value = ''
currentStepIndex.value = -1
isTyping.value = false
// Add initial prompt
lines.value.push({ type: 'input', prompt: currentOSConfig.value.prompt, content: '' })
}
// Initial reset
watch(currentOSConfig, resetDemo, { immediate: true })
const nextStep = async () => {
if (isTyping.value || isFinished.value) return
const demoLines = currentOSConfig.value.demo
const promptText = currentOSConfig.value.prompt
// Loop to process consecutive output lines or until a pause point
while (currentStepIndex.value < demoLines.length - 1) {
currentStepIndex.value++
const step = demoLines[currentStepIndex.value]
// 1. Update Explanation if exists
if (step.explanation) {
currentExplanation.value = step.explanation
}
// 2. Handle specific types
if (step.type === 'explanation') {
// Just show explanation and pause
break
}
if (step.type === 'command') {
// Ensure input line exists
if (lines.value.length === 0 || lines.value[lines.value.length - 1].type !== 'input') {
lines.value.push({ type: 'input', prompt: promptText, content: '' })
}
// Type effect
isTyping.value = true
const text = step.content
const targetLine = lines.value[lines.value.length - 1]
for (let i = 0; i < text.length; i++) {
targetLine.content += text[i]
await new Promise(r => setTimeout(r, 30 + Math.random() * 40))
}
isTyping.value = false
// Pause after typing command
break
}
if (step.type === 'output') {
lines.value.push({ type: 'output', content: step.content })
// Logic to continue or pause:
// Pause if:
// - This output has an explanation (user needs to read)
// - Next step is NOT output (it's a command or explanation block)
// - Next step is output BUT has an explanation
if (step.explanation) {
break
}
const nextStep = demoLines[currentStepIndex.value + 1]
if (!nextStep || nextStep.type !== 'output' || nextStep.explanation) {
// If next is command, we might want to show a prompt before pausing?
// But the command step logic adds prompt.
// If we pause here, the user sees output. Next click -> types command.
// Seems correct.
break
}
// Small delay between batched outputs for visual smoothness
await new Promise(r => setTimeout(r, 50))
}
}
// If we finished everything, add a final prompt
if (isFinished.value) {
lines.value.push({ type: 'input', prompt: promptText, content: '' })
}
}
</script>
<style scoped>
.terminal-os-demo {
margin: 24px 0;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
}
.os-switch {
display: flex;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.os-switch button {
padding: 8px 16px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
color: var(--vp-c-text-1);
}
.os-switch button:hover {
background: var(--vp-c-bg-mute);
transform: translateY(-1px);
}
.os-switch button.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
box-shadow: 0 2px 8px rgba(var(--vp-c-brand-rgb), 0.3);
}
.terminal-window {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(128, 128, 128, 0.2);
transition: background 0.3s;
}
/* Windows CMD Style */
.terminal-window.win-cmd {
background: #0c0c0c;
color: #cccccc;
font-family: 'Consolas', monospace;
}
.terminal-window.win-cmd .window-bar {
background: #ffffff;
border-bottom: 1px solid #ccc;
color: #000;
}
.terminal-window.win-cmd .window-title {
color: #000;
font-weight: normal;
}
.terminal-window.win-cmd .window-buttons .btn {
background: #ccc;
border-radius: 0;
}
/* PowerShell Style */
.terminal-window.win-ps {
background: #012456;
color: #ffffff;
font-family: 'Consolas', monospace;
}
.terminal-window.win-ps .window-bar {
background: #ffffff;
border-bottom: 1px solid #ccc;
color: #000;
}
.terminal-window.win-ps .window-title {
color: #000;
}
.terminal-window.win-ps .window-buttons .btn {
background: #ccc;
border-radius: 0;
}
/* Linux Style */
.terminal-window.linux {
background: #2b2b2b;
color: #f0f0f0;
font-family: 'Ubuntu Mono', monospace;
}
.terminal-window.linux .window-bar {
background: #3e3e3e;
border-bottom: 1px solid #222;
color: #ccc;
}
.terminal-window.linux .window-buttons .btn {
border-radius: 50%;
}
.terminal-window.linux .window-buttons .close { background: #ff5f56; }
.terminal-window.linux .window-buttons .minimize { background: #ffbd2e; }
.terminal-window.linux .window-buttons .maximize { background: #27c93f; }
/* Common Layout */
.window-bar {
padding: 8px 12px;
display: flex;
align-items: center;
position: relative;
height: 36px;
justify-content: space-between;
}
.window-buttons {
display: flex;
gap: 8px;
z-index: 10;
}
.window-controls {
display: flex;
gap: 8px;
z-index: 10;
align-items: center;
}
.control-btn {
background: transparent;
border: 1px solid currentColor;
color: inherit;
opacity: 0.7;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
cursor: pointer;
transition: all 0.2s;
height: 22px;
line-height: 16px;
}
.control-btn:hover:not(:disabled) {
opacity: 1;
background: rgba(128, 128, 128, 0.2);
}
.control-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.control-btn.primary {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: #fff;
opacity: 1;
}
.control-btn.primary:hover:not(:disabled) {
background: var(--vp-c-brand-dark);
}
.btn {
width: 12px;
height: 12px;
display: inline-block;
}
.window-title {
position: absolute;
left: 0;
right: 0;
text-align: center;
font-size: 13px;
line-height: 36px;
user-select: none;
}
.terminal-content {
padding: 16px;
min-height: 240px;
font-size: 14px;
line-height: 1.6;
text-align: left;
transition: background-color 0.2s;
position: relative; /* For overlay */
}
.terminal-content.clickable {
cursor: pointer;
}
.start-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 20;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(1px);
}
.start-hint {
background: var(--vp-c-brand);
color: #fff;
padding: 8px 16px;
border-radius: 20px;
font-weight: bold;
font-size: 13px;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.completed-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 20;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(2px);
animation: fade-in 0.5s;
}
.completed-hint {
background: #10b981;
color: #fff;
padding: 8px 16px;
border-radius: 20px;
font-weight: bold;
font-size: 13px;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
cursor: pointer;
transition: transform 0.2s;
}
.completed-hint:hover {
transform: scale(1.05);
background: #059669;
}
.terminal-content.clickable:hover {
background-color: rgba(255, 255, 255, 0.03);
}
.blink-hint {
animation: blink 1s step-end infinite;
font-weight: bold;
margin-left: 5px;
color: var(--vp-c-brand);
}
.line {
white-space: pre-wrap;
word-break: break-all;
display: flex;
flex-wrap: wrap;
}
.prompt {
margin-right: 8px;
font-weight: bold;
}
/* Linux Prompt Colors */
.terminal-window.linux .prompt {
color: #87d700;
}
.cursor {
display: inline-block;
width: 8px;
height: 1.2em;
background-color: currentColor;
vertical-align: text-bottom;
animation: blink 1s step-end infinite;
opacity: 0.7;
}
/* If last line input, show cursor there */
.line:last-child .cmd-text::after {
content: '';
display: inline-block;
width: 8px;
height: 1.2em;
background-color: currentColor;
vertical-align: text-bottom;
animation: blink 1s step-end infinite;
opacity: 0.7;
margin-left: 2px;
}
/* Only show cursor on the very last line if it is input type and we are animating OR we are idle (lines=0) */
/* Actually, simpler: */
.input-line .cursor {
display: inline-block;
}
/* Hide the pseudo-element cursor if we are not on the last line or if it is output */
.line:not(:last-child) .cmd-text::after {
display: none;
}
/* Also if the last line is output, no cursor */
.line:last-child:not(:has(.prompt)) .cmd-text::after {
/* This selector is tricky. Let's rely on v-if logic in template if possible,
but since we iterate lines, I added a cursor logic in CSS.
Let's adjust template to be explicit about cursor.
*/
display: none;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.hint {
opacity: 0.5;
font-size: 0.9em;
font-style: italic;
margin-left: 10px;
}
.explanation-bar {
background: #fff;
border-top: 1px solid #ddd;
padding: 8px 12px;
font-size: 13px;
color: #333;
display: flex;
align-items: flex-start;
gap: 8px;
min-height: 40px;
opacity: 0;
transform: translateY(10px);
transition: all 0.3s;
pointer-events: none;
}
.explanation-bar.visible {
opacity: 1;
transform: translateY(0);
}
.explanation-bar .icon {
font-size: 16px;
}
.explanation-bar .text {
line-height: 1.5;
}
.terminal-window.win-cmd .explanation-bar,
.terminal-window.win-ps .explanation-bar {
background: #f0f0f0;
color: #333;
border-top-color: #ccc;
}
.terminal-window.linux .explanation-bar {
background: #222;
color: #ccc;
border-top-color: #444;
}
</style>
+9
View File
@@ -17,9 +17,14 @@ import EscapeSequences from './components/appendix/terminal-intro/EscapeSequence
import InputVisualizer from './components/appendix/terminal-intro/InputVisualizer.vue'
import SignalsDemo from './components/appendix/terminal-intro/SignalsDemo.vue'
import FlowDiagram from './components/appendix/terminal-intro/FlowDiagram.vue'
import BufferSwitchDemo from './components/appendix/terminal-intro/BufferSwitchDemo.vue'
import AdvancedTUIDemo from './components/appendix/terminal-intro/AdvancedTUIDemo.vue'
import ArchitectureDemo from './components/appendix/terminal-intro/ArchitectureDemo.vue'
import TerminalDefinition from './components/appendix/terminal-intro/TerminalDefinition.vue'
import TerminalOSDemo from './components/appendix/terminal-intro/TerminalOSDemo.vue'
import EscapeParserDemo from './components/appendix/terminal-intro/EscapeParserDemo.vue'
import CookedRawDemo from './components/appendix/terminal-intro/CookedRawDemo.vue'
export default {
extends: DefaultTheme,
@@ -32,12 +37,16 @@ export default {
app.component('TerminalGrid', TerminalGrid)
app.component('CellInspector', CellInspector)
app.component('EscapeSequences', EscapeSequences)
app.component('EscapeParserDemo', EscapeParserDemo)
app.component('CookedRawDemo', CookedRawDemo)
app.component('InputVisualizer', InputVisualizer)
app.component('SignalsDemo', SignalsDemo)
app.component('FlowDiagram', FlowDiagram)
app.component('BufferSwitchDemo', BufferSwitchDemo)
app.component('AdvancedTUIDemo', AdvancedTUIDemo)
app.component('ArchitectureDemo', ArchitectureDemo)
app.component('TerminalDefinition', TerminalDefinition)
app.component('TerminalOSDemo', TerminalOSDemo)
},
setup() {
const route = useRoute()
+58 -34
View File
@@ -4,6 +4,10 @@
## 1. 概念界定:终端是什么? (Definition)
*不同操作系统下的终端长相不同,**命令方式也不同**。点击下方按钮切换查看,注意观察 Windows 和 Linux 是如何用不同的命令(如 `dir` vs `ls`)做同一件事的:*
<TerminalOSDemo />
在图形用户界面(GUI)普及之前,终端是人类与计算机交互的主要方式。即便在今天,它依然是开发者控制计算机最精确、最高效的工具。
<TerminalDefinition />
@@ -31,25 +35,28 @@
它不处理复杂的图形、图片或视频,而是专注于**文本信息的交互**。
## 2. 核心架构:终端与 Shell (The Architecture)
## 2. 核心架构:三者关系大白话 (The Big Picture)
初学者常混淆“终端”与“Shell”,理解两者的区别是掌握命令行的关键。它们在计算机历史中演化为两个独立的组件
别被专业术语吓跑,其实它们就是三个分工明确的“打工人”
- **终端 (Terminal)**:负责“交互”。
它是一个负责显示字符、接收键盘输入的**窗口程序**。它本身不懂任何逻辑,只负责传输数据
*常见的终端软件:macOS Terminal, iTerm2, Windows Terminal, Hyper.*
- **终端 (Terminal) —— 只是个“传声筒”**
* 它只负责**显示画面**和**接收按键**
* 它本身**没有任何智能**,就像一个显示器或键盘。
* *它不管你输入的是命令还是乱码,只管把字显示出来。*
- **Shell (壳 / 命令解释器)**:负责“逻辑”。
它是运行在终端内部的**后端程序**,负责接收你的指令、解析语义、调用系统内核并返回结果
*常见的 Shellbash, zsh, fish, sh.*
- **Shell (壳) —— 真正的“翻译官”**
* 它才是有逻辑的程序
* 它负责**听懂**你的命令(比如 `ls`),把它**翻译**成电脑能听懂的指令,然后指挥内核去干活。
* *就像 Siri 或小爱同学,听懂你的话,然后去调动手机功能。*
**类比理解**
如果把计算机操作比作去餐厅点餐:
- **终端**是**餐桌和菜单**(负责展示信息、提供输入界面,但它自己不会做菜)
- **Shell**是**服务员**(负责听取你的点单,交给后厨处理,并把结果端回来)。
- **内核**是**后厨**(负责真正的执行和运算)。
- **内核 (Kernel) —— 幕后的“大管家”**
* 它是操作系统的核心,只有它能直接控制硬件(硬盘、CPU)。
* **Shell 不包含内核**,Shell 只是站在门口喊话的人,内核才是屋里干活的人
*下面的演示直观展示了从“点餐”到“上菜”的全过程:*
**一句话总结流程**
你在**终端**(窗口)打字 ➡️ **Shell**(翻译官)听懂并指挥 ➡️ **内核**(大管家)去硬件里干活。
*下面的演示展示了这个过程,注意看 Shell 和内核之间那道“墙”:*
<ArchitectureDemo />
@@ -88,9 +95,13 @@
- 序列 `\033[31m`**指令**:将后续文字颜色设为红色。
- 序列 `\033[2J`**指令**:清空屏幕。
这就是为什么有时候在日志文件中会看到乱码(如 `^[[31m`),那其实是未被正确解析的颜色指令
这就好比你和朋友约定:如果我正常说话,你就记录下来;如果我举起左手(相当于 `ESC`),接下来的那句话就是命令而不是内容
*下方组件展示了转义序列如何实时改变终端的渲染状态*
*点击下方的“播放”按钮,观察终端是如何逐个处理字符流,并识别出隐藏的指令*
<EscapeParserDemo />
*下方组件则展示了更多种类的转义序列及其渲染效果:*
<EscapeSequences />
@@ -110,21 +121,25 @@
<InputVisualizer />
## 6. 运行模式:加工与原始 (Cooked vs. Raw Mode)
## 6. 运行模式:打字机 vs 游戏机 (Cooked vs. Raw Mode)
终端有两种主要的工作模式,决定了输入数据如何被处理。理解这一点能帮你明白为什么 `ls` 命令和 `vim` 编辑器的操作体验完全不同。
终端有两种截然不同的性格。理解这一点,你就能明白为什么在终端里**打命令**和**玩贪吃蛇**是完全不同的体验
- **加工模式 (Cooked Mode / Canonical Mode)**
这是标准 Shell 的默认模式。
- **行为**终端**缓存**你的输入,允许你使用退格键修改。只有当你按下回车键(Enter)后,整行数据才会一次性发送给程序
- **类比**:像在写信,写完一整句确认无误后才寄出
- *适用场景:日常命令输入。*
- **加工模式 (Cooked Mode) —— 像打字机**
* 这是默认模式。
* **行为**:你输入的字符会被终端**暂时扣留**,直到你按下回车键(Enter)
* **好处**:这给了你修改的机会。打错了?按退格键(Backspace)删掉重写,程序根本不知道你之前打错过
* *适用场景:平时敲命令(如 `ls`, `cd`。*
- **原始模式 (Raw Mode)**
这是高级交互程序的模式。
- **行为**:终端不进行任何缓冲或处理,将每一个键(包括修饰键)**即时**发送给程序
- **类比**:像打电玩,按下一个键,角色立刻做出反应
- *适用场景:Vim 编辑器、终端游戏、交互式菜单。*
- **原始模式 (Raw Mode) —— 像游戏手柄**
* 这是“高手”模式。
* **行为**:你按下的每一个键(包括方向键、Ctrl组合键),都会**瞬间**发送给程序,没有任何缓冲
* **好处**:程序能实时响应你的操作
* *适用场景:玩终端游戏(如贪吃蛇)、使用 Vim 编辑器(一种纯键盘操作的编辑器)。*
*点击下方按钮切换模式,体验“写信”与“打游戏”的不同手感:*
<CookedRawDemo />
## 7. 进程控制:信号 (Signals)
@@ -140,15 +155,24 @@
## 8. 高级应用:全屏界面与缓冲区 (Buffers & TUI)
现代终端应用(TUI, Text User Interface)如 `vim``htop``tmux`,能够像图形软件一样利用整个屏幕,这得益于**备用屏幕缓冲区 (Alternate Screen Buffer)**
你有没有发现,当你用 `vim` 编辑文件或者用 `htop` 看系统状态时,它们会占满整个屏幕?而当你退出它们时,屏幕瞬间变回了原来的样子,之前的命令记录完全没变
终端通常维护两块“画布”:
1. **主缓冲区 (Primary Buffer)**:保存命令历史,即我们日常滚动查看的流式界面。
2. **备用缓冲区 (Alternate Buffer)**:一块独立的、不保存历史的画布,供全屏应用使用。
这是因为终端有两块“画布”在来回切换
当你打开 `vim` 时,终端切换到备用缓冲区;当你退出 `vim` 时,终端切回主缓冲区。这就是为什么退出编辑器后,之前的命令历史依然完好无损地留在屏幕上
* **主缓冲区 (Primary Buffer)**:就像**草稿本**
* 你写一行,系统回一行。
* 写满了就翻页(滚动),以前写的东西都在上面。
* *用于:日常敲命令。*
<AdvancedTUIDemo />
* **备用缓冲区 (Alternate Buffer)**:就像**黑板**。
* 程序把黑板擦干净,在上面画画(全屏显示)。
* 不管怎么画,都不会影响你桌子上的草稿本。
* 当你退出程序时,就像把黑板收起来,你又回到了草稿本面前。
* *用于:Vim, Nano, 游戏等全屏软件。*
*点击下方按钮,体验“草稿本”和“黑板”是如何瞬间切换的:*
<BufferSwitchDemo />
---