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 顺带成为教程 26 PluginManager::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)

  1. 把教程 26 的 scan_and_load 换成用 list_subdirs(plugins_root);
  2. 实现 to_crlf,并确认 %s 替换对 CRLF 文件全文操作不回打乱行结构;
  3. Windows 降级路径:写一个 Screen::set_text 的 ANSI 实现(\x1b[2K\r 清一行 + 直接写),配合 is_tty 判断理由;
  4. 挑战:写 render_loop 的"GE 保护"——在降级模式下每帧 fflush,应对 Windows cmd 不回显的经典坑。

下一节 32-测试体系UnitTests.md:让每个模块的每个行为都有据可查。

posted @ 2026-09-08 13:51  IcarusLee  阅读(13)  评论(0)    收藏  举报