你是否觉得终端里 echo 出来的文字太过单调?有没有想过让普通的 HELLO 以更酷炫、更醒目的方式显示出来?比如像下面这样:
╔═════════════════════════════════════════════════════╗
║ ║
║ ██╗ ██╗ ███████╗ ██╗ ██╗ ██████╗ ║
║ ██║ ██║ ██╔════╝ ██║ ██║ ██╔═══██╗ ║
║ ███████║ █████╗ ██║ ██║ ██║ ██║ ║
║ ██╔══██║ ██╔══╝ ██║ ██║ ██║ ██║ ║
║ ██║ ██║ ███████╗ ███████╗ ███████╗ ╚██████╔╝ ║
║ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝ ╚══════╝ ╚═════╝ ║
║ ║
╚═════════════════════════════════════════════════════╝
这个想法并不复杂,说干就干,让我们用 Rust 亲手实现一个!
核心思路
实现这样一个工具的核心逻辑非常简单,主要分为三步:
- 准备字库:预先将每个字符(A-Z、0-9等)用 Unicode 方块字符“画”好,存储为一个映射表。
- 逐字转换:接收用户输入的文字,然后遍历每个字符,通过查表将其转换成对应的 ASCII 艺术字。
- 按行拼接:将所有转换后的字符的每一行拼接起来,最后再添加一个漂亮的边框。
项目结构
这个项目非常小巧,只需要三个 Rust 源文件:
src/
├── main.rs # 命令行入口,处理参数
├── banner.rs # 核心转换逻辑
└── font.rs # 字库定义
字库设计
首先,我们需要定义每个字符长什么样。为了视觉效果更好,我们使用 Unicode 中的方块字符(如 █、╗、╔ 等)来绘制,这比传统的 # 或 * 字符要美观得多。
每个字符的高度固定为 6 行。例如,字母 A 的“画法”定义如下:
vec![
" █████╗ ", // 第1行
" ██╔══██╗ ", // 第2行
" ██║ ██║ ", // 第3行
" ███████║ ", // 第4行
" ██╔══██║ ", // 第5行
" ╚═╝ ╚═╝ ", // 第6行
]
我们使用一个 HashMap<char, Vec<&str>> 来存储所有字符的映射,查询起来非常高效方便。
核心转换逻辑
关键的函数是 text_to_ascii,它的作用是将输入的文字转换为多行 ASCII 艺术字符串。
pub fn text_to_ascii(text: &str) -> Option<Vec<String>> {
let font_map = get_font_map();
let mut result: Vec<String> = vec![String::new(); FONT_HEIGHT]; // 初始化6个空字符串
for c in text.to_uppercase().chars() {
if let Some(lines) = font_map.get(&c) {
// 把每个字符的每一行追加到结果中
for (i, line) in lines.iter().enumerate() {
result[i].push_str(line);
result[i].push_str(" "); // 添加字符间距
}
}
}
Some(result)
}
举个简单的例子,输入 "AB":
- 先查找字符
A 对应的 6 行字符串,分别追加到 result[0] 到 result[5]。
- 再查找字符
B 对应的 6 行,同样追加到每一行的末尾。
- 最终,我们得到的就是拼接好的 6 行大字。
加个边框更帅气
只有文字还不够,我们可以用 Unicode 的制表符画一个边框,让输出更完整美观。
pub fn generate_banner(text: &str, padding: usize) -> String {
let ascii_lines = text_to_ascii(text)?;
// 画上边框
let top = format!("╔{}╗", "═".repeat(width));
// 画内容行(左右加空格 padding)
// ...
// 画下边框
let bottom = format!("╚{}╝", "═".repeat(width));
}
一个小坑:Unicode 宽度
这里有一个容易踩坑的地方:字符串的“长度”和它的“显示宽度”不是一回事。比如 █ 这个字符,它占用 3 个字节(len() 返回 3),但在终端里显示时只占 1 个字符的宽度。
如果直接用 len() 计算来画边框,宽度就不对了。为了解决这个问题,我们引入 unicode-width 这个 crate。
use unicode_width::UnicodeWidthStr;
let width = "█████╗".width(); // 正确得到显示宽度
命令行参数处理
为了让工具易用,我们需要处理命令行参数。这个项目比较简单,没有使用 clap 这样功能强大的库,而是手动处理。
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 || args.contains(&"--help".to_string()) {
print_help();
return;
}
let no_box = args.contains(&"--no-box".to_string());
// ...
}
使用效果
完成编码后,我们来试试效果:
# 编译
cargo build --release
# 运行
./target/release/ascii-banner "RUST"
# 不要边框
./target/release/ascii-banner "2026" --no-box
运行第一个命令,你将在终端看到如下输出:
╔══════════════════════════════════════════════╗
║ ║
║ ██████╗ ██╗ ██╗ ███████╗ ████████╗ ║
║ ██╔══██╗ ██║ ██║ ██╔════╝ ╚══██╔══╝ ║
║ ██████╔╝ ██║ ██║ ███████╗ ██║ ║
║ ██╔══██╗ ██║ ██║ ╚════██╗ ██║ ║
║ ██║ ██║ ╚██████╔╝ ███████║ ██║ ║
║ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ║
║ ║
╚══════════════════════════════════════════════╝
总结
这个项目虽然不大,但完整地串联起了几个非常有意思的技术点:
- Unicode 方块字符的应用 —— 使用 █、╔、╝ 等字符构建美观的终端输出。
- HashMap 作为字库 —— 实现了一个灵活且易于扩展的字符映射方案。
- Unicode 宽度的正确处理 —— 学会了如何准确计算多字节字符的显示宽度,这是开发跨平台命令行工具的必备知识。
- 按行拼接的二维处理思路 —— 将二维的字符图形数据,按行拼接成一维的字符串输出,这是一种常见且高效的处理模式。
完整代码已在 GitHub 开源:https://github.com/lispking/ascii-banner。欢迎 Star 和 Fork,如果你有更好的想法或发现了 Bug,也期待你的贡献。这类动手实践是理解一门语言和提升工程能力的好方法,开源实战 社区里也有很多类似的有趣项目。
如果你也想让自己的终端输出与众不同,不妨动手试试这个工具,或者基于这个思路创造你自己的版本。欢迎来 云栈社区 分享你的作品和心得。