Slides 终端演示实战:用 Markdown 编写 ASCII 主题幻灯片的完整指南
Slides 终端演示实战:用 Markdown 编写 ASCII 主题幻灯片的完整指南
本篇指南以仓库中的官方示例 examples/ascii_slides.md 为主线,系统讲解 Terminal 演示工具 Slides 的幻灯片编写语法:从 YAML front matter 主题配置、--- 分隔符的切页规则,到 Markdown 组件支持、代码执行与预处理等进阶能力。读完你将掌握直接编写、运行、导航、搜索乃至通过 SSH 远程展示一套终端幻灯片的全套方法。
从官方示例开始:一份最小的 ASCII 幻灯片
仓库的 examples/ascii_slides.md 是一份完整可运行的示例文件,它同时演示了 front matter 配置、标题层级、列表、表格和代码块等多种语法。全文如下:
---
theme: ascii
---
# Welcome to Slides
A terminal based presentation tool
```go
package main
import "fmt"
func main() {
fmt.Println("Written in Go!")
}
```
---
## Everything is markdown
In fact this entire presentation is a markdown file
---
# h1
## h2
### h3
#### h4
##### h5
###### h6
# Markdown components
You can use everything in markdown!
* Like bulleted list
* You know the deal
1. Numbered lists too
| Tables | Too |
| ------ | ------ |
| Even | Tables |
---
All you need to do is separate slides with triple dashes `---` on a separate line,
like so:
```markdown
# Slide 1
Some stuff
---
# Slide 2
Some other stuff
```
这份示例文件可以被直接运行:
slides examples/ascii_slides.md
它展示了 Slides 的核心设计理念:演示文稿就是一个 Markdown 文件,无需任何 GUI 编辑器或转换工具,一切都在终端内完成。
幻灯片切页规则:--- 分隔符
Slides 使用独占一行的三个连字符 --- 来分隔幻灯片,这是整份语法的基础:
# Slide 1
Some stuff
---
# Slide 2
Some other stuff
从源码实现看,--- 分隔符在 internal/model/model.go 中被定义为常量 delimiter = "\n---\n",Load() 方法通过 strings.Split(content, delimiter) 将整个 Markdown 文件按分隔符切分成幻灯片数组:
const (
delimiter = "\n---\n"
)
// ...
slides := strings.Split(content, delimiter)
两点关键细节:
- 分隔符前后必须换行:只有独占一行的
---才会被识别为分页符,行内出现的---不会触发切页; - front matter 不占页:如果文件头部存在 YAML front matter(
---包裹的元数据),meta.Parse()解析成功后会将其识别为配置节而非第一张幻灯片,并自动跳过(见 internal/model/model.go)。
一切皆 Markdown:支持的组件
示例文件第二张幻灯片直言:"In fact this entire presentation is a markdown file"(事实上整份演示就是一个 Markdown 文件)。Slides 基于 charmbracelet/glamour 渲染 Markdown,因此标准 Markdown 语法均可直接使用:
- 标题层级:
# h1到###### h6共六级标题; - 无序列表:
* item/- item; - 有序列表:
1. item; - 表格:管道符语法
| A | B |; - 代码块:围栏式代码块(详见下文"代码执行");
- 引用、粗体、斜体、链接等常规 Markdown 元素。
渲染流程在 internal/model/model.go 的 View() 方法中:glamour 将当前页的 Markdown 渲染为终端彩色文本,再经由 styles.Slide.Render() 添加内边距,最终与底部状态栏(作者、日期、页码)拼合输出。
主题系统:theme: ascii 是如何生效的
示例文件头部第一行就是主题配置:
---
theme: ascii
---
theme 字段决定整份演示的配色与样式。主题选择逻辑集中在 styles/styles.go 的 SelectTheme() 函数中,它支持三类取值:
| theme 取值 | 效果 | 实现依据 |
|---|---|---|
ascii |
纯 ASCII 风格(无 ANSI 颜色码,兼容性最好) | glamour.WithStyles(glamour.ASCIIStyleConfig) |
light |
浅色背景配色 | glamour.LightStyleConfig |
dark |
深色背景配色 | glamour.DarkStyleConfig |
notty |
非 TTY 输出(管道/重定向场景) | glamour.NoTTYStyleConfig |
| 本地 JSON 路径 | 如 theme: ./path/to/theme.json,加载自定义 glamour 主题 |
os.Open + WithStylesFromJSONBytes |
| HTTP(S) 链接 | 如 theme: https://example.com/theme.json,演示前自动拉取 |
http.Get + WithStylesFromJSONBytes |
仓库内置的默认主题文件是 styles/theme.json。当未指定主题或指定的文件/链接读取失败时,getDefaultTheme() 会根据终端背景色自动选择 light/dark 或内置默认主题(styles/styles.go)。
提示:仓库的 examples/theme.json 和 examples/custom_theme.md 展示了自定义主题的具体写法,examples/custom_remote_theme.md 则演示了远程主题的用法,可作为参考。
运行与实时刷新
基本用法
slides presentation.md
如果传入的是文件名,Slides 会自动监听文件变更并实时刷新演示(每秒钟检查一次文件的修改时间,见 internal/model/model.go 的 fileWatchCmd 与 fileWatchMsg 处理逻辑),非常适合边编辑边演示的工作流。
通过 stdin 输入
slides 同样接受标准输入:
curl http://example.com/slides.md | slides
当没有传入文件名且 stdin 为空时,Slides 会显示内置的教学演示(tutorial),其内容嵌入在 internal/model/tutorial.md 中。
导航快捷键
Slides 借鉴了 Vim 的键位设计,导航状态机实现在 internal/navigation/navigation.go。完整键位如下:
| 操作 | 按键 |
|---|---|
| 回到第一页 | g g |
| 下一页 | space、right、down、enter、n、j、l、Page Down |
| 上一页 | left、up、p、h、k、N、Page Up |
| 跳转到指定页 | 数字 + G(如 5G 跳到第 5 页) |
| 跳转到最后一页 | G |
| 退出 | q、ctrl+c |
数字前缀是这套导航的精髓:先按数字再按翻页键,可一次前进/后退 N 张幻灯片。从源码看,数字会累积进缓冲区(Buffer),随后由 repeatableAction() 循环执行 navigateNext/navigatePrevious;其中 0 前缀遵循 Vim 惯例不生效(internal/navigation/navigation.go)。
搜索:按正则跳转
按 / 进入搜索模式,输入关键词后回车即可跳转到匹配的幻灯片:
- 搜索词按正则表达式解析;
- 以
/i结尾可实现大小写不敏感搜索(实现见 internal/navigation/search.go:追加(?i)内联标志); - 搜索会从当前页向后找,找不到再回绕到开头(internal/navigation/search.go);
- 搜索后按
ctrl+n跳到下一个匹配结果(搜索缓冲会保留,见Done()的设计注释); esc/ctrl+c取消搜索。
代码执行:在幻灯片里跑代码
示例文件中的 Go 代码块并不是装饰——当光标停留在包含代码块的幻灯片上时:
- 按
ctrl+e,Slides 会解析当前页的所有代码块、逐个执行,并把输出以"虚拟文本"(virtual text)的形式显示在幻灯片末尾(internal/model/model.go); - 按
y可将当前页代码块的源码复制到系统剪贴板。
执行流程见 internal/code/code.go:
- 代码块必须显式标注语言(围栏后紧跟语言名),否则无法执行;
- 代码被写入系统临时目录中的临时文件(如
slides-*.go); - 命令支持三个占位符:
<file>(临时文件完整路径)、<name>(不含扩展名的文件名)、<path>(所在目录)。
目前支持 20 种语言,完整映射表见 internal/code/languages.go,摘录如下:
| 语言 | 围栏标识 | 执行命令 |
|---|---|---|
| Go | go |
go run <file> |
| Python | python |
python <file> |
| JavaScript | javascript |
node <file> |
| Bash | bash |
bash <file> |
| Rust | rust |
rustc <file> -o <path>/<name>.run 后运行 |
| C++ | cpp |
g++ -std=c++20 -o <path>/<name>.run <file> 后运行 |
| Java / Ruby / Lua / Perl / OCaml / Elixir / Julia / Swift / Dart / Scala / Haskell / Zsh / Fish / V | 对应标识 | 各语言的解释器/编译器 |
示例文件里的 Go 代码块按下 ctrl+e 后,会在幻灯片底部输出:
package main
import "fmt"
func main() {
fmt.Println("Written in Go!")
}
即 Written in Go! 这一行文本。
预处理:演示前执行命令
除了运行时代码执行,Slides 还支持预处理:在展示前把幻灯片中的特殊代码块替换为命令输出。语法是三个波浪号 ~~~ 包裹的块,第一行是命令,块内内容作为命令的 stdin 传入,整块被命令的 stdout 替换:
```
~~~graph-easy --as=boxart
[ A ] - to -> [ B ]
~~~
```
上述内容在展示前会被替换为:
┌───┐ to ┌───┐
│ A │ ────> │ B │
└───┘ └───┘
如果想保留替换后文本的格式与换行,可以把 ~~~ 块再包进三层反引号中(如上方示例所示)。
实现细节见 internal/process/process.go:Parse() 通过正则匹配 ~~~command\n...\n~~~ 结构,Block.Execute() 将块内容通过 stdin 管道写入命令进程并捕获 stdout,Pre() 最后用输出替换原始块。
安全机制:可执行文件权限
出于安全考虑,只有具备执行权限的文件才会被预处理。这是为了防止无意中执行幻灯片中的恶意命令——readFile 在读取时检查 file.IsExecutable(s)(internal/model/model.go)。因此使用预处理功能前需要显式添加执行权限:
chmod +x file.md
如果文件带 shebang(#! 开头),Slides 会先剥离该行再处理(internal/model/model.go)。
元数据配置:front matter 详解
在 Markdown 文件顶部用 --- 包裹的 YAML 区域即 front matter,可用于自定义演示外观。该区块完全可选,省略任何字段都会使用默认值(解析逻辑见 internal/meta/meta.go)。
---
theme: ./path/to/theme.json
author: Gopher
date: MMMM dd, YYYY
paging: Slide %d / %d
---
| 字段 | 说明 | 默认值 |
|---|---|---|
theme |
glamour 主题 JSON 文件路径,也可以是远程 URL(演示前自动抓取) | 随终端环境自动选择 |
author |
显示在演示视图左下角的作者名,可设为空字符串隐藏 | 当前 OS 用户全名 |
date |
日期格式字符串,按 YYYY-MM-DD 语义格式化当天日期;若格式非法则原样显示 |
YYYY-MM-DD |
paging |
页码模板,第一个 %d 替换为当前页码,第二个 %d 替换为总页数;以 % 开头时需要用引号包裹 |
Slide %d / %d |
其中 paging 的取值逻辑在 internal/model/model.go 的 paging() 方法中:根据字符串中 %d 的数量(2 个、1 个或 0 个)决定格式化方式。
日期格式对照表
以日期 January 02, 2006(2006 年 1 月 2 日)为例:
| 值 | 翻译结果 |
|---|---|
YYYY |
2006 |
YY |
06 |
MMMM |
January |
MMM |
Jan |
MM |
01 |
mm |
1 |
DD |
02 |
dd |
2 |
这些占位符到 Go 时间格式的映射定义在 internal/meta/meta.go 的 parseDate() 中,替换完成后交给 time.Now().Format() 输出。
远程演示:通过 SSH 共享幻灯片
Slides 内置 SSH 服务器,可在没有安装 Slides 的机器上远程观看演示:
在一台机器上启动服务:
slides serve [file]
然后在任意一台有 ssh 的机器上连接(默认端口 53531):
ssh 127.0.0.1 -p 53531
使用场景包括:在不便安装 Slides 的电脑上演示、让观众在各自终端同步观看而无需分发文件和二进制。服务实现见 internal/cmd/serve.go,该命令还支持以下参数与环境变量:
| 参数 | 环境变量 | 默认值 | 说明 |
|---|---|---|---|
--keyPath |
SLIDES_SERVER_KEY_PATH |
slides |
SSH 服务器私钥路径 |
--host |
SLIDES_SERVER_HOST |
localhost |
绑定主机 |
--port |
SLIDES_SERVER_PORT |
53531 |
监听端口 |
安装方式
不同平台可用对应的包管理器安装(详见 README.md):
# macOS
brew install slides
# Arch Linux
yay -S slides
# Nixpkgs (unstable)
nix-env -iA nixpkgs.slides
# 任意支持 snapd 的 Linux
sudo snap install slides
# 通过 Go 安装
go install github.com/maaslalani/slides@latest
也可以从源码构建(需要 Go 工具链):
git clone https://github.com/maaslalani/slides.git
cd slides
go install
更多示例与深入阅读
仓库 examples 目录提供了多份可直接运行的示例文件,覆盖了本文所有主题:
- examples/ascii_slides.md:ASCII 主题 + 基础语法(本文主线);
- examples/code_blocks.md:代码块与代码执行;
- examples/preprocess.md:预处理命令;
- examples/metadata.md:front matter 元数据配置;
- examples/import.md:幻灯片导入/引用;
- examples/theme.json 与 examples/custom_theme.md:自定义主题;
- examples/custom_remote_theme.md:远程主题。
若想了解项目自身的开发与构建流程,可参阅 docs/development/README.md;导航、搜索、预处理、代码执行等模块的测试用例分别在 internal/navigation、internal/process、internal/code 目录下,可作为理解各功能边界的第一手资料。