31-跨平台适配CrossPlatform
31 · 跨平台适配 Cross-platform(One Editor, Many Systems)
本节目标(Goals):把 Vicarus 从"只活在 Linux"的舒适区拽出来——处理终端库、路径、换行、构建四类跨平台雷区。
前置知识(Prerequisites):教程 14(newline_mode_)、02-03(CMake 依赖与目标)、01(ncurses 环境)。
1. 四个必然炸的雷区
| 雷区 | Linux | Windows | macOS |
|---|---|---|---|
| 全屏终端 | ncurses(带 dev 头) | PDCurses / Windows terminal | ncurses(brew) |
| 路径分隔符 | / |
\ 或 / |
/ |
| 换行 | \n |
\r\n |
\n |
| 配置目录 | ~/.config/vicarus |
%APPDATA%\vicarus |
~/Library/Application Support/vicarus |
策略分两层:代码层用 std::filesystem + 少量 #ifdef;构建层用 CMake 对 FindCurses 的兜底与 Python3 包的平台差异容忍。
2. 终端库:依赖抽象,平台给实现
教程 07 的 Terminal 已是抽象层——真正的差异藏在这里:
# src/CMakeLists.txt:把 curses 的选择交给 CMake 的模块
find_package(Curses)
if(Curses_FOUND)
target_link_libraries(vicarus_ui PRIVATE Curses::Curses)
else()
# Windows/无 curses 环境:用一个薄的 ANSI 实现降级(见下)
add_subdirectory(platform/ansi)
target_link_libraries(vicarus_ui PRIVATE vicarus_ansi)
target_compile_definitions(vicarus_ui PRIVATE VICARUS_ANSI_FALLBACK=1)
endif()
降级策略(无 curses 也能编译):
- 用 ANSI escape sequences(
\x1b[2J清屏、\x1b[{r};{c}H定位)实现Screen子集——真机上线时换回 curses; - 编写
Terminal::is_tty():#ifdef _WIN32用_isatty,否则isatty(STDIN_FILENO)。
3. 路径与配置目录:别再手拼 /
// include/vicarus/infra/platform.hpp
#pragma once
#include <string>
#include <vector>
namespace vicarus::infra {
// 配置目录(随 OS 不同)
std::string config_dir();
std::string config_path(); // config_dir() + "/config.toml"
// 递归建目录(存在即成功)
bool ensure_dir(const std::string& path);
// 列出目录下所有子目录名(scan plugin 用)
std::vector<std::string> list_subdirs(const std::string& dir);
}
实现(src/infra/platform.cpp):
#include <vicarus/infra/platform.hpp>
#include <filesystem>
#include <cstdlib>
namespace vicarus::infra {
std::string config_dir() {
const char* env = std::getenv("VICARUS_CONFIG");
if (env && *env) return env;
#ifdef _WIN32
if (const char* ap = std::getenv("APPDATA"))
return std::string(ap) + "\\vicarus";
return "vicarus";
#else
if (const char* home = std::getenv("HOME"))
return std::string(home) + "/.config/vicarus";
return ".vicarus";
#endif
}
std::string config_path() {
namespace fs = std::filesystem;
return (fs::path(config_dir()) / "config.toml").string();
}
bool ensure_dir(const std::string& path) {
std::error_code ec;
std::filesystem::create_directories(path, ec);
return !ec;
}
std::vector<std::string> list_subdirs(const std::string& dir) {
std::vector<std::string> out;
std::error_code ec;
for (const auto& e : std::filesystem::directory_iterator(dir, ec)) {
if (ec) break;
if (e.is_directory(ec)) out.push_back(e.path().filename().string());
}
return out;
}
} // namespace vicarus::infra
细则:路径拼接一律
std::filesystem::path的/运算符,绝不用手写字符串拼接——这是跨平台 bug 的头号来源。list_subdirs顺带成为教程 26PluginManager::scan_and_load的可靠底座(替换掉手写directory_iterator)。
4. 换行:教程 14 的 newline_mode_ 落地
教程 14 已经记录了首次读取文件的换行风格。写回时保持一致:
// file_io.cpp 写路径
void write_file(const std::string& path, const Buffer& buf) {
std::string text = buf.to_string();
if (buf.newline_mode() == NewlineMode::CRLF) text = to_crlf(text);
// fout.write(...) 原样落盘
}
这样同一文件在 Windows/Unix 间往返,换行风格不被编辑器无脑"漂白"。
5. 终端检测与环境变量
// 是否"应该"提供色彩
bool terminal_has_color() {
#ifdef _WIN32
return true; // 现代 Windows Terminal 支持 ANSI
#else
const char* term = std::getenv("TERM");
const char* ct = std::getenv("COLORTERM");
return (term && std::string_view(term).find("color") != std::string_view::npos)
|| (ct && std::string_view(ct) != "false");
#endif
}
读设置时代理:
// Editor::init 中
if (terminal_has_color()) ui_options.set_color_palette(...); // 21 表的调色板
else ui_options.set_mono_palette(...);
6. 构建层:把跨平台交给 CI
代码两边都碰完之后,跨平台的"最后一公里"是构建——三操作系统各跑一遍(教程 34 的 CI 会总结成一个矩阵)。本地快速验证态:
# Linux
cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build
# Windows (PowerShell)
cmake -B build -DCMAKE_BUILD_TYPE=Release -DVICARUS_ENABLE_PYTHON=OFF && cmake --build build --config Release
# macOS (brew install ncurses python3)
cmake -B build -DCMAKE_BUILD_TYPE=Release -DCURSES_ROOT=$(brew --prefix ncurses) && cmake --build build
三个意外我最常踩:Windows 上
Python3::Python的链接路径、macOS 的 ncurses 藏在/opt/homebrew、以及 ANSI 降级模式下Screen的坐标原点差异(1 vs 0)。把"修好一次"沉淀成 CI 矩阵里的作业,别指望记得住。
本节小结(Summary)
- 跨平台四雷:终端库、路径、换行、构建,各有明确对策;
- 依赖抽象(
Terminal) +#ifdef少而准 +std::filesystem统一路径; newline_mode_换行风格写回保持一致;- ANSI 降级让无 curses 环境也能编;CI 三平台矩阵是跨平台质量的保证。
动手练习(Exercise)
- 把教程 26 的
scan_and_load换成用list_subdirs(plugins_root); - 实现
to_crlf,并确认%s替换对 CRLF 文件全文操作不回打乱行结构; - Windows 降级路径:写一个
Screen::set_text的 ANSI 实现(\x1b[2K\r清一行 + 直接写),配合is_tty判断理由; - 挑战:写
render_loop的"GE 保护"——在降级模式下每帧fflush,应对 Windows cmd 不回显的经典坑。
下一节
32-测试体系UnitTests.md:让每个模块的每个行为都有据可查。

浙公网安备 33010602011771号