当前位置: 首页 > news >正文

Node.js终端Canvas渲染引擎:构建交互式TUI应用与数据可视化

1. 项目概述:在终端里“画”出交互式应用

如果你和我一样,常年与终端(Terminal)打交道,可能会觉得那些黑底白字的命令行界面虽然高效,但总少了点“生气”。无论是系统监控、日志查看,还是简单的数据展示,满屏滚动的文本行看久了难免视觉疲劳。有没有可能让终端应用也拥有图形化的界面,比如实时更新的图表、可点击的按钮,甚至是流畅的动画?这就是ghaiklor/terminal-canvas这个项目试图解决的问题。

terminal-canvas是一个用 TypeScript 编写的 Node.js 库,它的核心目标很明确:为命令行终端提供一套完整的、高性能的 2D 渲染引擎。你可以把它想象成终端里的“Canvas API”或者一个极简的“游戏引擎”。它不满足于仅仅输出彩色文字或简单的 ASCII 艺术,而是提供了一套基于单元格(cell)的像素级绘图能力,允许你以帧缓冲(Frame Buffer)的方式,在终端窗口这个“画布”上绘制点、线、矩形、圆形,渲染文本,甚至处理用户键盘和鼠标的输入事件,从而构建出复杂的、交互式的终端图形界面(TUI)。

我第一次接触这类工具是在做一个内部 DevOps 仪表盘的时候。当时需要在一个共享的服务器终端上,为团队成员实时展示多个服务的 CPU、内存占用曲线和部署状态。用传统的console.log输出表格和数字,不仅信息密度低,而且无法直观反映趋势变化。尝试了terminal-canvas后,我成功地绘制出了实时滚动的折线图,并且用不同颜色的方块代表不同服务的健康状态,整个仪表盘一目了然,体验提升了好几个档次。它特别适合需要在纯终端环境下进行数据可视化、构建监控面板、创建教育演示工具或开发终端小游戏的场景。对于 Node.js 后端开发者、运维工程师或任何想给命令行工具增加更友好交互界面的开发者来说,这个库提供了一个非常强大的底层能力。

2. 核心架构与渲染原理剖析

2.1 为什么是“Canvas”而非“像素”?

理解terminal-canvas的第一步,是弄明白它如何在本质上基于文本的终端里实现“绘图”。现代终端(如 iTerm2, Kitty, Alacritty, 甚至 Windows Terminal)大多支持True Color(24位真彩色)和一系列高级图形协议(如 Sixel、Kitty 的图形协议)。但terminal-canvas选择了一条更通用、兼容性更强的路径:单元格(Cell)渲染模型

终端屏幕可以被视为一个二维网格,每个网格位置就是一个“单元格”。传统上,一个单元格显示一个字符(包括字母、数字、符号)。terminal-canvas的巧妙之处在于,它将每个单元格视为一个可以独立设置前景色、背景色和显示内容的“像素”。虽然这个“像素”的形状是固定的(通常是等宽字体下的矩形),但通过精细控制每个单元格的颜色和内容,就能组合出连续的图形。

例如,要画一条斜线,在真正的像素显示器上,你可以计算出一条路径上所有像素点的坐标。在终端画布上,你需要将这条路径映射到单元格网格上,决定路径经过的每个单元格应该用什么字符(比如/\|-或者半角空格 )来近似表示线条,并为其设置颜色。这种模型决定了terminal-canvas渲染的图形会有一种独特的、略带“颗粒感”或“字符艺术”的风格,但这正是其魅力所在,也保证了它在绝大多数终端环境下的可运行性。

2.2 帧缓冲与双缓冲技术

为了实现流畅的动画和即时交互反馈,terminal-canvas采用了在图形学中常见的**帧缓冲(Frame Buffer)双缓冲(Double Buffering)**技术。

帧缓冲:在内存中开辟一块区域,其结构对应着终端屏幕的网格(例如 80列 x 24行)。这块内存区域存储着当前帧每一个单元格的完整状态信息(前景色、背景色、字符)。你的所有绘图操作(drawRectangle,drawText)都不是直接输出到终端,而是修改这个内存中的缓冲区。

双缓冲:这是防止屏幕撕裂(在绘制过程中用户看到不完整帧)的关键技术。terminal-canvas会维护两个帧缓冲区:一个“后台缓冲区(Back Buffer)”和一个“前台缓冲区(Front Buffer)”。所有绘图指令都作用于后台缓冲区。当一帧的所有绘制命令执行完毕后,库会执行一个“交换(Swap)”操作,将后台缓冲区的内容与前台缓冲区进行交换。然后,通过计算前后两帧缓冲区的差异(Diff),只将发生变化的单元格信息输出到终端。这种差异更新策略,相比每一帧都重绘整个屏幕,能极大减少需要传输的数据量,从而实现更高的帧率(FPS)和更低的闪烁。

注意:终端渲染的性能瓶颈往往不在于 JavaScript 的计算,而在于向终端输出数据的速度。频繁的全屏刷新会导致明显的卡顿。terminal-canvas的差异更新是保证其性能的核心,这也意味着你的绘图逻辑应尽量局部更新,避免每一帧都清除整个画布。

2.3 事件循环与输入处理

一个交互式应用离不开对用户输入(键盘、鼠标)的响应。terminal-canvas集成了一个简单的事件循环机制。它会监听终端的标准输入(stdin),将原始的输入序列(如\x1b[A代表方向上箭头)解析成结构化的事件对象(如{ name: 'keyboard', key: 'up' })。

你可以为画布实例注册事件监听器:

canvas.on('keyboard', (event) => { if (event.key === 'q') { process.exit(0); // 按 q 键退出 } else if (event.key === 'left') { player.x -= 1; // 左移角色 } });

对于鼠标,如果终端支持(通常通过启用mouse模式),它还能提供鼠标移动、点击、滚轮等事件,包括相对于画布网格的坐标。这使得实现可点击的按钮、菜单等交互组件成为可能。

3. 从零开始:构建你的第一个终端动画

理论说得再多,不如动手试一下。我们来创建一个简单的、在屏幕中弹跳的方块动画,这是熟悉terminal-canvasAPI 的绝佳起点。

3.1 环境准备与项目初始化

首先,确保你安装了 Node.js(建议版本 14 或更高)。然后创建一个新的项目目录并初始化:

mkdir bouncing-box && cd bouncing-box npm init -y npm install terminal-canvas

接下来,创建一个名为index.js的主文件。

3.2 基础绘制与动画循环

我们将一步步构建这个动画。首先,引入库并创建画布实例:

const { Canvas } = require('terminal-canvas'); const canvas = new Canvas();

创建画布后,通常需要手动启动它,并设置一些初始状态,比如隐藏光标(避免光标闪烁干扰图形)和启用鼠标支持(如果需要):

canvas.reset(); // 重置画布状态 canvas.hideCursor(); // 隐藏终端光标 // canvas.enableMouse(); // 如果需要鼠标交互则启用

现在,定义我们的小方块的状态:位置、速度、大小和颜色。

let box = { x: 10, y: 5, width: 8, height: 4, vx: 1, // x轴速度 vy: 1, // y轴速度 color: 'blue' };

核心的动画循环逻辑如下。我们使用setInterval来模拟游戏循环,每一帧做四件事:1) 更新物体状态;2) 清除上一帧;3) 绘制当前帧;4) 将帧缓冲区内容渲染到屏幕。

function drawBox() { // 使用 drawRectangle 方法绘制实心矩形 // 参数:x, y, width, height, attributes canvas.drawRectangle( box.x, box.y, box.width, box.height, { background: box.color } ); } function update() { // 1. 更新方块位置 box.x += box.vx; box.y += box.vy; // 2. 边界碰撞检测与反弹 const canvasWidth = canvas.width; const canvasHeight = canvas.height; if (box.x <= 0 || box.x + box.width >= canvasWidth) { box.vx = -box.vx; // 反转x轴速度 box.color = getRandomColor(); // 碰撞后换个颜色,增加趣味性 } if (box.y <= 0 || box.y + box.height >= canvasHeight) { box.vy = -box.vy; // 反转y轴速度 box.color = getRandomColor(); } // 3. 清除画布(用空格填充整个区域) canvas.clear(); // 这是全屏清除,对于简单动画可以接受 // 4. 绘制新位置的方块 drawBox(); // 5. 将后台缓冲区渲染到终端 canvas.flush(); } // 一个简单的随机颜色生成函数 function getRandomColor() { const colors = ['red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white']; return colors[Math.floor(Math.random() * colors.length)]; } // 启动动画循环,每秒30帧 const fps = 30; setInterval(update, 1000 / fps);

最后,别忘了在程序退出时恢复终端状态(如重新显示光标),这是一个好习惯:

process.on('SIGINT', () => { canvas.showCursor(); canvas.reset(); process.exit(0); });

运行node index.js,你就能看到一个彩色方块在终端窗口内四处弹跳,碰到边缘会变色。通过这个简单的例子,你已经掌握了状态管理、基础图形绘制和动画循环的核心概念。

实操心得:在动画循环中,canvas.clear()会清除整个画布,这在图形元素少的时候没问题。但对于复杂界面,频繁全屏清除和重绘是性能杀手。更优的做法是使用“脏矩形”技术,只重绘那些发生变化的区域。对于这个弹跳方块,我们可以先在上一个位置用背景色画一个矩形“擦除”旧方块,再在新位置绘制新方块。terminal-canvas的差异更新在底层已经做了优化,但我们在应用层减少不必要的绘制,能进一步降低 CPU 占用。

4. 深入核心API:绘制、样式与文本

掌握了动画循环,我们来深入看看terminal-canvas提供的绘图工具箱。它的 API 设计很大程度上借鉴了 Web 的 Canvas API 和浏览器 DOM 的样式模型,这对于前端开发者来说非常亲切。

4.1 图形绘制基础

库提供了一系列基础的 2D 图形绘制方法:

  • drawRectangle(x, y, width, height, attributes): 绘制矩形。通过attributes可以设置是否为填充(background色)或描边(foreground色 + 字符,如*)。
  • drawCircle(x, y, radius, attributes): 绘制圆形。在单元格网格上画圆本质上是画一个视觉上近似圆形的字符集合。
  • drawLine(x1, y1, x2, y2, attributes): 绘制直线。算法会决定路径上的单元格用什么字符连接最合适。
  • drawPolyline(points, attributes)drawPolygon(points, attributes): 绘制折线和多边形。
  • drawText(x, y, text, attributes): 在指定位置渲染文本。这是构建 UI 标签、日志输出的关键。

attributes参数是一个对象,用于定义绘制样式,是绘图的精髓所在:

const style = { foreground: 'brightWhite', // 前景色(字符颜色) background: '#336699', // 背景色,支持16色名、256色索引或RGB十六进制字符串 bold: true, // 粗体 dim: false, // 暗淡 italic: false, // 斜体(终端支持有限) underline: true, // 下划线 blink: false, // 闪烁(慎用,很恼人) inverse: false, // 反转前景/背景 hidden: false, // 隐藏文字 strike: false, // 删除线 char: '█' // 绘制图形时使用的字符,默认是空格(对于填充)或 `·`(对于描边) }; canvas.drawRectangle(5, 5, 20, 10, { background: 'red', char: ' ' }); canvas.drawText(7, 7, 'Hello Terminal Canvas', { foreground: 'yellow', bold: true });

4.2 颜色系统的深度解析

终端颜色是一个历史包袱很重的领域。terminal-canvas很好地抽象了这一点,支持三种颜色模式:

  1. 16色标准命名:如'black','red','green','yellow','blue','magenta','cyan','white',以及它们的明亮版本'brightBlack'(灰)、'brightRed'等。这是兼容性最好的模式。
  2. 256色模式:使用索引号0-255。其中0-15对应16标准色,16-231是6x6x6的RGB立方色,232-255是灰度色。你可以直接使用数字,如background: 196表示亮红色。
  3. TrueColor (24-bit RGB):使用 CSS 风格的十六进制字符串,如'#FF8800'。这是色彩最丰富的模式,但需要终端支持。现代终端模拟器基本都支持。

注意事项:在编写需要分发的工具时,颜色兼容性是个问题。一个稳健的策略是,先检测终端对颜色的支持能力(可以通过环境变量如COLORTERMprocess.env.TERM来简单判断,或者使用像chalksupports-color这样的库),然后动态降级你的调色板。例如,如果检测到只支持256色,就避免使用过于细腻的渐变色。

4.3 文本渲染与对齐

drawText是使用频率最高的 API 之一。除了基本的定位,你经常需要处理文本对齐和多行文本。

// 单行文本 canvas.drawText(10, 10, 'Left Aligned', { foreground: 'cyan' }); // 假设我们有一个固定宽度的区域,想让文本居中 const boxWidth = 30; const text = 'Centered Title'; const textX = 10 + Math.floor((boxWidth - text.length) / 2); canvas.drawText(textX, 12, text, { bold: true, foreground: 'white' }); // 手动处理多行文本 const longText = 'This is a very long sentence that needs to be wrapped into multiple lines based on a certain width.'; const maxWidth = 40; let currentY = 15; for (let i = 0; i < longText.length; i += maxWidth) { const line = longText.substring(i, i + maxWidth); canvas.drawText(10, currentY, line, {}); currentY += 1; }

对于复杂的 UI,手动计算文本位置会很繁琐。在实际项目中,我通常会基于terminal-canvas封装一些高阶的组件或辅助函数,比如一个drawTextBox(x, y, width, height, text, options)函数,自动处理文本的换行、对齐(左、中、右)和省略号(...)。

5. 构建复杂终端UI:组件化实践

当应用逻辑变复杂时,直接在全局坐标下进行绘制会迅速导致代码难以维护。我们需要引入一些抽象。虽然terminal-canvas本身不提供 UI 组件库,但我们可以借鉴前端的思想,建立简单的组件模型。

5.1 实现一个简单的按钮组件

我们来创建一个最基础的Button组件。它需要有自己的位置、尺寸、标签、状态(正常、聚焦、按下),以及点击回调。

class Button { constructor(x, y, width, label, onClick) { this.x = x; this.y = y; this.width = Math.max(width, label.length + 4); // 最小宽度 this.height = 3; // 固定高度,包含边框 this.label = label; this.onClick = onClick; this.isFocused = false; this.isPressed = false; } // 判断一个坐标点是否在按钮区域内 contains(px, py) { return px >= this.x && px < this.x + this.width && py >= this.y && py < this.y + this.height; } // 处理鼠标事件 handleMouse(event) { if (event.name === 'mouse' && event.action === 'mousedown') { if (this.contains(event.x, event.y)) { this.isPressed = true; return true; // 事件已消费 } } else if (event.name === 'mouse' && event.action === 'mouseup') { if (this.isPressed && this.contains(event.x, event.y)) { this.onClick && this.onClick(); } this.isPressed = false; } else if (event.name === 'mouse' && event.action === 'mousemove') { this.isFocused = this.contains(event.x, event.y); } return false; } // 渲染按钮到画布 render(canvas) { const style = this.getCurrentStyle(); // 绘制上边框 canvas.drawText(this.x, this.y, '┌' + '─'.repeat(this.width - 2) + '┐', style.border); // 绘制标签行(居中) const labelX = this.x + Math.floor((this.width - this.label.length) / 2); canvas.drawText(labelX, this.y + 1, this.label, style.label); // 绘制下边框 canvas.drawText(this.x, this.y + 2, '└' + '─'.repeat(this.width - 2) + '┘', style.border); // 如果被按下,绘制一个“凹陷”效果 if (this.isPressed) { canvas.drawRectangle(this.x + 1, this.y + 1, this.width - 2, 1, { background: 'black', foreground: 'white' }); } } getCurrentStyle() { if (this.isPressed) { return { border: { foreground: 'white', background: 'blue' }, label: { foreground: 'black', background: 'white', bold: true } }; } else if (this.isFocused) { return { border: { foreground: 'brightCyan', background: null }, label: { foreground: 'brightCyan', background: null, bold: true } }; } else { return { border: { foreground: 'gray', background: null }, label: { foreground: 'white', background: null } }; } } }

5.2 场景管理与主循环集成

有了组件,我们需要一个“场景”或“屏幕”来管理它们。一个简单的应用可能包含多个屏幕(如主菜单、游戏界面、设置页)。每个屏幕管理自己的组件列表和渲染逻辑。

class Scene { constructor() { this.components = []; this.canvas = null; } addComponent(component) { this.components.push(component); } setup(canvas) { this.canvas = canvas; // 场景初始化,比如创建按钮 const quitBtn = new Button(10, 5, 10, 'Quit', () => process.exit(0)); this.addComponent(quitBtn); // 注册全局事件监听,并分发给组件 canvas.on('mouse', (event) => this.handleMouse(event)); canvas.on('keyboard', (event) => this.handleKeyboard(event)); } handleMouse(event) { // 从后向前遍历,让后添加的(视觉上层的)组件先接收事件 for (let i = this.components.length - 1; i >= 0; i--) { if (this.components[i].handleMouse(event)) { break; // 如果组件消费了事件,则停止传播 } } this.render(); } handleKeyboard(event) { // 处理键盘事件,例如Tab键切换焦点 if (event.key === 'tab') { // ... 焦点切换逻辑 } this.render(); } render() { this.canvas.clear(); for (const comp of this.components) { comp.render(this.canvas); } // 渲染一些始终在顶层的元素,如FPS计数器 this.drawFPS(); this.canvas.flush(); } drawFPS() { // 实现一个简单的FPS计算和显示 } } // 在主程序中使用 const canvas = new Canvas(); const mainScene = new Scene(); canvas.reset().hideCursor().enableMouse(); mainScene.setup(canvas); mainScene.render(); // 初始渲染 // 可以在这里启动一个基于 requestAnimationFrame 的循环,持续调用 mainScene.render()

通过这种组件化架构,你可以像搭积木一样构建复杂的终端界面,例如数据仪表盘、交互式表单、甚至是简单的终端游戏。每个组件负责自己的状态、事件和渲染,主场景负责协调和调度。

6. 性能优化与高级技巧

当界面元素增多或动画复杂度上升时,性能问题就会浮现。以下是几个关键的优化方向和高级用法。

6.1 渲染性能瓶颈分析与优化

  1. 最小化重绘区域:这是最重要的原则。不要每一帧都canvas.clear()。而是记录哪些组件或区域的状态发生了改变(“脏区域”),只重绘这些区域。对于静态的背景和边框,只绘制一次即可。
  2. 节流与防抖:对于高频触发的事件(如鼠标移动),不要每次事件都触发完整渲染。可以使用setTimeoutrequestAnimationFrame进行节流,确保渲染频率不会超过屏幕刷新率(通常60Hz就足够了)。
  3. 离屏渲染:对于复杂的、不常变化的图形(比如一个徽标、一个复杂的图表背景),可以预先将其绘制到一个离屏的“Canvas”或缓冲区(可以用一个二维数组模拟),然后在每一帧直接将这些单元格数据复制到主画布上,避免重复执行绘制命令。
  4. 谨慎使用复杂样式blink(闪烁)属性会强制终端频繁重绘该单元格,可能影响性能。过于精细的 TrueColor 渐变在大型区域填充时,生成的 ANSI 转义序列也会非常长。

6.2 与其它终端库的协同

terminal-canvas专注于底层渲染和输入。在实际项目中,你可能会需要更高级的抽象。好消息是,它可以与其他流行的 Node.js 终端库配合使用。

  • blessedblessed-contrib结合blessed是一个功能完整的终端 UI 库,提供了布局、高级组件(列表、表格、表单)等。你可以用blessed构建主体 UI,而在某个需要自定义图形的box元素中,嵌入一个terminal-canvas实例来绘制特定内容。
  • inkreact-blessed结合:如果你喜欢 React 的声明式编程模型,ink允许你用 React 组件的方式构建命令行界面。虽然ink有自己的渲染器,但对于需要绝对像素控制或复杂动画的部分,理论上可以封装一个自定义组件,在内部使用terminal-canvas进行渲染。
  • 数据可视化:对于绘制图表,你可以使用terminal-canvas作为底层引擎,在上层封装自己的图表库(柱状图、折线图、饼图)。计算好数据点对应的坐标和颜色,然后用drawLinedrawRectangle等方法绘制出来。

6.3 调试与开发工具

开发终端图形应用的一个挑战是调试。你不能简单地用console.log,因为输出会破坏画布。这里有一些技巧:

  1. 日志文件:将调试信息写入一个独立的日志文件。
  2. 预留调试面板:在画布上开辟一个固定区域(比如屏幕底部几行),专门用于输出调试信息。使用drawText在这个区域打印变量状态。
  3. 使用 Node.js 调试器:通过node --inspect index.js启动,然后使用 Chrome DevTools 进行断点调试。这不会干扰终端输出。
  4. 模拟器测试:在不同的终端模拟器(iTerm2, Alacritty, Windows Terminal, GNOME Terminal)中测试你的应用,确保颜色和渲染效果一致。

7. 实战:打造一个实时系统监控仪表盘

让我们综合运用以上知识,构建一个实用的工具:一个在终端中运行的实时系统监控仪表盘。它将显示 CPU 使用率、内存占用、网络流量和进程列表。

7.1 架构设计

我们将采用简单的 MVC 模式:

  • 模型(Model):负责获取系统数据。我们将使用osps-list等 Node.js 原生或第三方模块。
  • 视图(View):负责渲染。我们创建多个“小组件”(Widget)类,每个负责渲染一部分数据(如仪表盘、图表、列表)。
  • 控制器(Controller):主循环,负责定时拉取数据、更新模型、触发视图重绘,并处理退出事件。

7.2 核心组件实现:动态图表

最核心的是如何将动态数据(如 CPU 使用率随时间的变化)绘制成图表。我们将实现一个简单的滚动折线图组件。

class LineChartWidget { constructor(x, y, width, height, title) { this.x = x; this.y = y; this.width = width; this.height = height; this.title = title; this.data = []; // 存储数据点 [value1, value2, ...] this.maxDataPoints = width - 4; // 留出边距 this.minValue = 0; this.maxValue = 100; // 初始范围,CPU百分比 } pushValue(value) { this.data.push(value); if (this.data.length > this.maxDataPoints) { this.data.shift(); // 移除最旧的数据,实现滚动效果 } // 动态调整Y轴范围,让图表更自适应 this.maxValue = Math.max(this.maxValue, ...this.data) * 1.1; // 留10%余量 this.minValue = Math.min(this.minValue, ...this.data); if (this.minValue > 0) this.minValue = 0; // 确保从0开始 } render(canvas) { // 绘制边框和标题 canvas.drawRectangle(this.x, this.y, this.width, this.height, { foreground: 'gray', char: '─' }); canvas.drawText(this.x + 2, this.y, ` ${this.title} `, { background: 'black', foreground: 'white' }); if (this.data.length < 2) return; const chartWidth = this.width - 4; const chartHeight = this.height - 2; const valueRange = this.maxValue - this.minValue; // 绘制Y轴刻度 // ... (省略刻度标签代码) // 绘制折线 const points = []; for (let i = 0; i < this.data.length; i++) { const x = this.x + 2 + i; // 从左到右 // 将数据值映射到图表高度内的Y坐标 const normalizedValue = (this.data[i] - this.minValue) / valueRange; const y = this.y + 1 + chartHeight - Math.floor(normalizedValue * chartHeight); points.push([x, y]); } // 用 drawPolyline 连接点 if (points.length >= 2) { canvas.drawPolyline(points, { foreground: 'cyan' }); } // 在最后一个点显示当前值 const lastValue = this.data[this.data.length - 1]; canvas.drawText( this.x + this.width - 6, this.y + 1, `${lastValue.toFixed(1)}%`, { foreground: 'brightYellow', bold: true } ); } }

7.3 数据获取与主循环

在主循环中,我们定时(比如每秒一次)获取系统状态,更新各个组件,然后触发渲染。

const os = require('os'); const psList = require('ps-list'); async function updateSystemStats(chartWidget, processListWidget) { // 1. CPU 使用率 (简化版,取1秒内的平均负载) const cpus = os.cpus(); let totalIdle = 0, totalTick = 0; cpus.forEach(cpu => { for (let type in cpu.times) { totalTick += cpu.times[type]; } totalIdle += cpu.times.idle; }); const idle = totalIdle / cpus.length; const total = totalTick / cpus.length; const usage = 100 - (100 * idle / total); chartWidget.pushValue(usage); // 2. 内存使用率 const totalMem = os.totalmem(); const freeMem = os.freemem(); const memUsage = 100 - (freeMem / totalMem * 100); // ... 更新内存图表 // 3. 进程列表 const processes = await psList(); // 按CPU或内存排序,取前10个 const topProcesses = processes.sort((a, b) => b.cpu - a.cpu).slice(0, 10); processListWidget.update(topProcesses); } // 主函数 async function main() { const canvas = new Canvas(); canvas.reset().hideCursor(); const cpuChart = new LineChartWidget(2, 2, 60, 15, 'CPU Usage %'); const memChart = new LineChartWidget(2, 20, 60, 10, 'Memory Usage %'); const procList = new ProcessListWidget(65, 2, 40, 30, 'Top Processes'); const scene = new Scene(); scene.addComponent(cpuChart); scene.addComponent(memChart); scene.addComponent(procList); scene.setup(canvas); // 定时更新与渲染循环 setInterval(async () => { await updateSystemStats(cpuChart, memChart, procList); scene.render(); }, 1000); // 每秒更新一次 // 退出处理 canvas.on('keyboard', (evt) => { if (evt.key === 'q' || evt.key === 'escape') { canvas.showCursor().reset(); process.exit(0); } }); } main().catch(console.error);

运行这个程序,你就能获得一个在终端中实时刷新的系统监控面板。它直观地展示了terminal-canvas在构建实用工具方面的强大能力。你可以进一步扩展它,添加磁盘 I/O 图表、网络流量图,甚至实现进程的筛选和排序功能。

http://www.jsqmd.com/news/778644/

相关文章:

  • FPA功能点分析实战:我们如何用它为团队节省了20%的预算,并说服了客户
  • 保姆级教程:用Qt和Python给你的软件加个‘扫码枪’(从模拟到真实设备调试)
  • 2026年佛山物料输送设备厂家口碑推荐榜:佛山输送机、佛山污泥破碎机、佛山皮带输送机、佛山提升机选择指南 - 海棠依旧大
  • ibkr-cli:命令行驱动盈透证券API,打造透明量化交易工作流
  • 抖音去水印工具怎么选?免费安全的去水印工具推荐,2026实测好用的方法全汇总 - 科技热点发布
  • #2026国内护墙板公司Top10推荐:广东广州等地公司工艺成熟品质可靠 - 十大品牌榜
  • 龙芯2k0300 - 走马观碑组WiFi驱动移植
  • 2026 年广州头部 GEO 公司盘点:5 家主流厂商深度测评与全场景选型指南 - GEO优化
  • AWS for SAP MCP Server 正式 GA:AI Agent 安全接入 SAP ERP
  • 五年制专转本英语备考为什么选择蓝洋五年制专转本英语培训? - 奔跑123
  • 从Turbo码到LDPC码:手把手分析5G/4G信号背后,信道编码如何‘偷偷’提升你的网速和稳定性
  • 五分钟教程使用curl命令测试taotoken大模型api连通性
  • VisionFive 2 RISC-V开发板开箱与系统配置实战
  • PREM、AK135、STW105:三大地球模型在负荷变形计算中的表现差异与选择建议
  • 量子计算模拟Fermi-Hubbard模型的技术突破与应用
  • Mac新手必看:用SourceTree和Git搞定Gitee/GitHub仓库(含SSH密钥配置避坑指南)
  • 告别玄学调试:用‘信号完整性’的视角根治Camera底层MIPI/DVP报错
  • 对话式AI智能体创建:用自然语言定制你的Gemini CLI助手
  • 3DMAX异形空间地板建模救星:用FloorGenerator搞定弧形、带洞和不规则地面
  • 2026 年苏州主轴维修厂家口碑推荐榜:苏州电主轴维修、苏州高速主轴维修、苏州精密主轴维修、苏州磨床主轴维修、苏州进口主轴维修选择指南 - 海棠依旧大
  • 蓝洋无忧单招项目核心优势 - 奔跑123
  • 蒙特卡洛算法优化N皇后问题求解
  • 苏州这边有没有比较好的专转本语文培训班? - 奔跑123
  • 对比不同模型在Taotoken平台上的实际调用成本感受
  • ide-rule:统一AI编程助手规则配置,告别多工具适配烦恼
  • 2026年苏州气流粉碎机厂家口碑推荐榜:苏州气流粉碎机、流化床气流粉碎机、GMP 标准气流粉碎机、实验室气流粉碎机、超微粉碎机、超细粉碎机选择指南 - 海棠依旧大
  • 避开DoIP诊断的隐形大坑:详解P4Server、P6时间参数与NRC 0x78响应策略
  • 麦格纳收购维宁尔:自动驾驶投资回归理性,协同驾驶成务实路径
  • #2026国内全屋定制Top10公司:广东广州等地品质首选 - 十大品牌榜
  • AppBuilder-SDK:一站式AI原生应用开发平台实战指南