0%

在 UEFI Shell 下手搓一个双面板可视化文件管理器 (VisualFileMan)

最近我在 EDK II 环境下折腾 UEFI 开发,发现原生的 UEFI Shell 采用纯命令行交互,在进行频繁的文件复制、移动和目录切换时效率很低。为了改善体验,我决定手搓一个类似于 Midnight Commander (mc) 的**双面板、纯文本可视化文件管理器 (Visual File Manager)**。

在开发过程中,我遇到了不少 UEFI 底层的坑和“玄学” Bug。这篇文章将记录整个开发过程以及遇到各类问题的解决思路。

1. 初步实现与双面板结构

整个 APP 的基础是利用了 EDK II 的 ShellLib.h 和系统协议(如 EFI_SIMPLE_FILE_SYSTEM_PROTOCOL)。
核心的数据结构分配了两个 FILE_PANE 实例,分别对应左右面板;利用 UEFI Console 的 gST->ConOut->SetCursorPosition() 把屏幕劈成了左右两半(通常是 80x25 或更大),实现了简单的文件结构展示并用颜色进行了区分(青色代表目录,绿色代表纯 .efi 可执行文件)。

然而随着功能的堆叠,坑开始一个个出现。


2. 踩坑与修复记录

问题一:上下选择时屏幕闪烁极其严重

现象: 每次按按键(哪怕只是上下挪动光标),整个页面都要“黑闪”一下才能刷新完毕,感觉非常不流畅。
原因分析:
为了保持界面干净,在绘制主循环 DrawUI() 中,我每次都调用了 gST->ConOut->ClearScreen() 来清空屏幕。ClearScreen 实际上会先将屏幕直接涂黑,然后再逐个字符覆盖。这样高频次清空重绘就会造成明显的黑屏闪烁。
解决方案:
移除主循环里的 ClearScreen()。既然每一行都有固定宽度,我们在输出文本时,根据 PaneWidth 动态计算还需要多少空格,调用自定义的 PrintCharTimes(L' ', PadLen) 补满剩余空间。
依靠“覆盖替换”而非“先清空后画”,即可做到 100% 丝滑无闪烁更新。

问题二:顶部标题栏神秘消失

现象: 在添加了最后一行底部的“快捷键提示栏”后,屏幕最顶部的那行 [FileManager v1.0] 莫名其妙不见了。
原因分析:
在我的终端输出逻辑中,底部文本填充了大量的空格以试图对齐最右侧。当向 UEFI 控制台的最后一个字符(坐标:X=79, Y=24)打印内容时,终端的光标会自动向前推进一步,导致屏幕被整行向上滚动(Auto-Scroll)!这样位于 Y=0 的顶部栏就被强行顶出了屏幕。
解决方案:
在绘制最底下一行时,即使需要填充空格,也刻意让长度等于 gColumns - 1,将最后一个字符彻底悬空不操作,避免触发底层的屏幕滚屏。

问题三:UI 交互太反人类(无光标、删除死板)

现象:

  1. F5复制/F6移动 敲目标路径时,屏幕上没有闪烁的光标,没法用左右方向键回退修改。
  2. 目标路径如果是个纯目录名(如 fs1:\test),拷贝直接失败,非得让人手工把文件名也补齐。
  3. F8删除 时必须要键盘纯手打输入 yes 才能删。
    解决方案:
    为了优化体验,我彻底重写了一套轻量级的窗口组件:
  • InputBox (光标和修改支持): 使用 gST->ConOut->EnableCursor(..., TRUE) 开启硬件光标。捕捉 SCAN_LEFT / SCAN_RIGHT / SCAN_DELETE0x08 (退格),利用字符串缓冲区的整体左移/右移自行实现了一个单行文本编辑器。
  • 智能化路径拼接: 当你在目标打下 fs1:\ 回车时,我的底层 AppendFileNameIfDirectory 函数通过尝试 ShellOpenFileByNameShellFindFirstFile 先判定目标是不是目录。如果是目录或者是盘符标识,代码自动在内存中把 \原始文件名 拼接上去。
  • ConfirmBox 抛弃键盘手敲,只捕捉 [Enter][Esc]

问题四:从根目录后退找不到文件系统,并偶发崩溃

现象:fs0:\ 下,如果按 .. 想跳出磁盘,这在传统的 UEFI Shell 环境下是无效的。且如果我们在某个子目录运行该 App(比如 fs0:\test\visual.efi ),有时会根本不显示界面,直接报错 Unsupported
原因分析:

  1. Trailing Slash (尾随斜杠) 异常: 在 UEFI FAT 底层驱动中,如果直接以读取模式去 open 一个没有随尾带有 \ 的纯目录路径(比如 fs0:\test),驱动可能会把它强行认定成“没有拓展名的 File”,从而在接下来的 ShellFindFirstFile 检索它内部节点时,抛出 EFI_UNSUPPORTED,直接引发 App 级别的 Abort 崩溃。
  2. 跨盘符迷失: 根路径没有父节点,用户就变成了“被锁在当前盘符的囚徒”。

终极解决方案与实现:
我为此创造了一个虚拟的超级挂载点—— Drives:

  • 处理 Slash:ReadDirectory 最开始,无论是 fs0: 还是复杂的路径,全部做安全化处理:如果没有,强行附加 \ 再去请求底层驱动打开,从源头断绝了因缺少反斜杠被驱动拒绝的问题。
  • Drives: 虚拟挂载点:当识别到用户在根目录的 [..] 上按 Enter 时,将路径主动劫持为 Drives:。随后调用 LocateHandleBuffer 查找系统内所有的 gEfiSimpleFileSystemProtocolGuid 句柄,再顺藤摸瓜通过 gEfiShellProtocol->GetMapFromDevicePath 解析出类似于 FS0, FS1, BLK0 的真正映射名格式化成了虚拟的内部目录。
    这样大家就能像在用真实的 OS 一样,在最外层自如地选择所有的挂载盘了!

3. 演示

主界面

main_menu

移动/重命名

move

复制

copy

删除

del

源码: UEFI VisualFileMan


4. 结语

在现代 OS 里的“基操”,一旦深入到 BIOS 层级的 UEFI 去完全手推,就会涉及到控制台重绘、按键中断监听、底层句柄状态转换等各种“返璞归真”的操作。虽然踩了不少坑,但也借此加深了对 EDK II 以及 UEFI 文件系统通信协议的理解。如果你也需要一款 UEFI 下高效的文件管理工具,这种基于 ShellLib 开发的双面视图绝对是最佳的选择, 后面继续在此基础上继续添加可编辑文本,执行efi等功能。