diff --git a/docs/.vitepress/theme/components/appendix/terminal-intro/ArchitectureDemo.vue b/docs/.vitepress/theme/components/appendix/terminal-intro/ArchitectureDemo.vue index 32c5c75..93608f1 100644 --- a/docs/.vitepress/theme/components/appendix/terminal-intro/ArchitectureDemo.vue +++ b/docs/.vitepress/theme/components/appendix/terminal-intro/ArchitectureDemo.vue @@ -15,29 +15,58 @@
-
🍽️
+
🖥️
-
Terminal = Table (餐桌)
-
UI & Input/Output
交互界面与输入输出
+
Terminal (终端)
+
传声筒 / 窗口
-
💁‍♂️
+
🗣️
-
Shell = Waiter (服务员)
-
Interpreter & Logic
解释器与逻辑处理
+
Shell (壳)
+
翻译官 / 助手
-
👨‍🍳
+
⚙️
-
Kernel = Kitchen (后厨)
-
System Execution
系统执行与硬件调度
+
Kernel (内核)
+
大管家 / 芯片
-
+
+ +
+
+ 👆 + 不断点击屏幕演示 / Keep Clicking +
+
+ + +
+
+ + 演示结束,点击重置 / Finished (Reset) +
+
+ + +
+
+
User Space (用户空间)
+
+
+
+
+
+
Kernel Space (内核空间)
+
+
+
TERMINAL (终端)
@@ -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); diff --git a/docs/.vitepress/theme/components/appendix/terminal-intro/BufferSwitchDemo.vue b/docs/.vitepress/theme/components/appendix/terminal-intro/BufferSwitchDemo.vue new file mode 100644 index 0000000..d69b0f0 --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/terminal-intro/BufferSwitchDemo.vue @@ -0,0 +1,284 @@ + + + + + \ No newline at end of file diff --git a/docs/.vitepress/theme/components/appendix/terminal-intro/CookedRawDemo.vue b/docs/.vitepress/theme/components/appendix/terminal-intro/CookedRawDemo.vue new file mode 100644 index 0000000..de7317b --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/terminal-intro/CookedRawDemo.vue @@ -0,0 +1,362 @@ + + + + + \ No newline at end of file diff --git a/docs/.vitepress/theme/components/appendix/terminal-intro/EscapeParserDemo.vue b/docs/.vitepress/theme/components/appendix/terminal-intro/EscapeParserDemo.vue new file mode 100644 index 0000000..53e1545 --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/terminal-intro/EscapeParserDemo.vue @@ -0,0 +1,418 @@ + + + + + diff --git a/docs/.vitepress/theme/components/appendix/terminal-intro/TerminalDefinition.vue b/docs/.vitepress/theme/components/appendix/terminal-intro/TerminalDefinition.vue index 7f095dd..7cacb0e 100644 --- a/docs/.vitepress/theme/components/appendix/terminal-intro/TerminalDefinition.vue +++ b/docs/.vitepress/theme/components/appendix/terminal-intro/TerminalDefinition.vue @@ -82,11 +82,12 @@
Event Loop / 事件循环
- {{ guiEvent.type }} + {{ ev.type }}
@@ -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 + // Send Click Event + emitGuiEvent('Click(40,30)', () => { + // When packet arrives: Select icon + iconSelected.value = true - if (progress >= 90) { - clearInterval(packetInterval) - guiEvent.value = null + setTimeout(() => { + isGuiAnimating.value = false + }, 1000) + }) + + setTimeout(() => { isGuiClicking.value = false - iconSelected.value = true // Effect - - setTimeout(() => { - isGuiAnimating.value = false - }, 1000) - } - }, 10) + }, 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)); } diff --git a/docs/.vitepress/theme/components/appendix/terminal-intro/TerminalOSDemo.vue b/docs/.vitepress/theme/components/appendix/terminal-intro/TerminalOSDemo.vue new file mode 100644 index 0000000..715f850 --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/terminal-intro/TerminalOSDemo.vue @@ -0,0 +1,618 @@ + + + + + diff --git a/docs/.vitepress/theme/index.js b/docs/.vitepress/theme/index.js index a570ec5..fc7ef57 100644 --- a/docs/.vitepress/theme/index.js +++ b/docs/.vitepress/theme/index.js @@ -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() diff --git a/docs/zh-cn/appendix/terminal-intro.md b/docs/zh-cn/appendix/terminal-intro.md index 808f2f2..2c736e7 100644 --- a/docs/zh-cn/appendix/terminal-intro.md +++ b/docs/zh-cn/appendix/terminal-intro.md @@ -4,6 +4,10 @@ ## 1. 概念界定:终端是什么? (Definition) +*不同操作系统下的终端长相不同,**命令方式也不同**。点击下方按钮切换查看,注意观察 Windows 和 Linux 是如何用不同的命令(如 `dir` vs `ls`)做同一件事的:* + + + 在图形用户界面(GUI)普及之前,终端是人类与计算机交互的主要方式。即便在今天,它依然是开发者控制计算机最精确、最高效的工具。 @@ -31,25 +35,28 @@ 它不处理复杂的图形、图片或视频,而是专注于**文本信息的交互**。 -## 2. 核心架构:终端与 Shell (The Architecture) +## 2. 核心架构:三者关系大白话 (The Big Picture) -初学者常混淆“终端”与“Shell”,理解两者的区别是掌握命令行的关键。它们在计算机历史中演化为两个独立的组件: +别被专业术语吓跑,其实它们就是三个分工明确的“打工人”: -- **终端 (Terminal)**:负责“交互”。 - 它是一个负责显示字符、接收键盘输入的**窗口程序**。它本身不懂任何逻辑,只负责传输数据。 - *常见的终端软件:macOS Terminal, iTerm2, Windows Terminal, Hyper.* +- **终端 (Terminal) —— 只是个“传声筒”** + * 它只负责**显示画面**和**接收按键**。 + * 它本身**没有任何智能**,就像一个显示器或键盘。 + * *它不管你输入的是命令还是乱码,只管把字显示出来。* -- **Shell (壳 / 命令解释器)**:负责“逻辑”。 - 它是运行在终端内部的**后端程序**,负责接收你的指令、解析语义、调用系统内核并返回结果。 - *常见的 Shell:bash, zsh, fish, sh.* +- **Shell (壳) —— 真正的“翻译官”** + * 它才是有逻辑的程序。 + * 它负责**听懂**你的命令(比如 `ls`),把它**翻译**成电脑能听懂的指令,然后指挥内核去干活。 + * *就像 Siri 或小爱同学,听懂你的话,然后去调动手机功能。* -**类比理解**: -如果把计算机操作比作去餐厅点餐: -- **终端**是**餐桌和菜单**(负责展示信息、提供输入界面,但它自己不会做菜)。 -- **Shell**是**服务员**(负责听取你的点单,交给后厨处理,并把结果端回来)。 -- **内核**是**后厨**(负责真正的执行和运算)。 +- **内核 (Kernel) —— 幕后的“大管家”** + * 它是操作系统的核心,只有它能直接控制硬件(硬盘、CPU)。 + * **Shell 不包含内核**,Shell 只是站在门口喊话的人,内核才是屋里干活的人。 -*下面的演示直观展示了从“点餐”到“上菜”的全过程:* +**一句话总结流程**: +你在**终端**(窗口)打字 ➡️ **Shell**(翻译官)听懂并指挥 ➡️ **内核**(大管家)去硬件里干活。 + +*下面的演示展示了这个过程,注意看 Shell 和内核之间那道“墙”:* @@ -88,9 +95,13 @@ - 序列 `\033[31m` → **指令**:将后续文字颜色设为红色。 - 序列 `\033[2J` → **指令**:清空屏幕。 -这就是为什么有时候在日志文件中会看到乱码(如 `^[[31m`),那其实是未被正确解析的颜色指令。 +这就好比你和朋友约定:如果我正常说话,你就记录下来;如果我举起左手(相当于 `ESC`),接下来的那句话就是命令而不是内容。 -*下方组件展示了转义序列如何实时改变终端的渲染状态:* +*点击下方的“播放”按钮,观察终端是如何逐个处理字符流,并识别出隐藏的指令:* + + + +*下方组件则展示了更多种类的转义序列及其渲染效果:* @@ -110,21 +121,25 @@ -## 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 编辑器(一种纯键盘操作的编辑器)。* + +*点击下方按钮切换模式,体验“写信”与“打游戏”的不同手感:* + + ## 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)**:就像**草稿本**。 + * 你写一行,系统回一行。 + * 写满了就翻页(滚动),以前写的东西都在上面。 + * *用于:日常敲命令。* - +* **备用缓冲区 (Alternate Buffer)**:就像**黑板**。 + * 程序把黑板擦干净,在上面画画(全屏显示)。 + * 不管怎么画,都不会影响你桌子上的草稿本。 + * 当你退出程序时,就像把黑板收起来,你又回到了草稿本面前。 + * *用于:Vim, Nano, 游戏等全屏软件。* + +*点击下方按钮,体验“草稿本”和“黑板”是如何瞬间切换的:* + + ---