From 7ba3ae8d5cf64a1740177cb11be5a06765442599 Mon Sep 17 00:00:00 2001 From: Vasil Topalovic Date: Mon, 20 Jul 2026 20:22:02 -0400 Subject: [PATCH] fix: put Windows user config under %APPDATA%\1coder Use the usual Roaming AppData location instead of a Unix-style %USERPROFILE%\.config path when XDG_CONFIG_HOME is unset. --- README.md | 1 + config/config.example.toml | 3 +- core/config/config.cpp | 11 +++++ core/config/config.h | 3 +- docs/README.md | 2 +- docs/config.md | 4 +- tests/test_config.cpp | 92 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 112 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 266faea..589a5f4 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ language servers, or the font, create: ~/.config/1coder/config.toml ``` +On Windows: `%APPDATA%\1coder\config.toml`. (`$XDG_CONFIG_HOME/1coder/config.toml` when that variable is set.) A full example with every default binding is in diff --git a/config/config.example.toml b/config/config.example.toml index a298ed7..adf2969 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -1,4 +1,5 @@ -# Example 1coder config — copy to ~/.config/1coder/config.toml and edit. +# Example 1coder config — copy to ~/.config/1coder/config.toml and edit +# (Windows: %APPDATA%\1coder\config.toml). # # The editor ships with these defaults compiled in. You do not need this file # for them to work. Keep only the keys you want to change, or start from this diff --git a/core/config/config.cpp b/core/config/config.cpp index 09f118f..cd4e554 100644 --- a/core/config/config.cpp +++ b/core/config/config.cpp @@ -273,12 +273,23 @@ void ParseLspTable(Arena *arena, const TomlValue *table, Config *config, ErrorLo String8 ConfigDefaultPath(Arena *arena) { if (!arena) return String8{}; + + // Explicit XDG wins on every platform when set. String8 xdg = OsGetEnv(arena, Str8Lit("XDG_CONFIG_HOME")); if (xdg.size > 0) return OsPathJoin(arena, xdg, Str8Lit("1coder/config.toml")); +#if defined(_WIN32) + // Native Windows convention: %APPDATA%\1coder\config.toml + String8 appdata = OsGetEnv(arena, Str8Lit("APPDATA")); + if (appdata.size > 0) return OsPathJoin(arena, appdata, Str8Lit("1coder/config.toml")); + String8 home = HomeDirectory(arena); + if (home.size == 0) return String8{}; + return OsPathJoin(arena, home, Str8Lit("AppData/Roaming/1coder/config.toml")); +#else String8 home = HomeDirectory(arena); if (home.size == 0) return String8{}; return OsPathJoin(arena, home, Str8Lit(".config/1coder/config.toml")); +#endif } ConfigLoadResult ConfigParse(Arena *arena, String8 text, String8 path) { diff --git a/core/config/config.h b/core/config/config.h index 46f9f67..a7732dc 100644 --- a/core/config/config.h +++ b/core/config/config.h @@ -64,7 +64,8 @@ struct ConfigLoadResult { bool ok; }; -// $XDG_CONFIG_HOME/1coder/config.toml, else ~/.config/1coder/config.toml. +// $XDG_CONFIG_HOME/1coder/config.toml when set; else ~/.config/1coder/config.toml +// on Unix, or %APPDATA%\1coder\config.toml on Windows. [[nodiscard]] String8 ConfigDefaultPath(Arena *arena); // Parse TOML text into a Config. Does not touch the editor. diff --git a/docs/README.md b/docs/README.md index 58485e7..c18a373 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,7 +7,7 @@ the editor properly, or for changing it. ## Using it -- [Configuration](config.md) — `~/.config/1coder/config.toml`, reload, error log +- [Configuration](config.md) — user config path, reload, error log - [Keybindings](keybindings.md) — the complete key reference - [Language server protocol](lsp.md) — auto-detected servers, navigation, completion - [The command window](commands.md) — `:` syntax, and how to add a command diff --git a/docs/config.md b/docs/config.md index 616d558..cd368bb 100644 --- a/docs/config.md +++ b/docs/config.md @@ -6,7 +6,9 @@ startup and again when you run `:config-reload`. ## Location 1. `$XDG_CONFIG_HOME/1coder/config.toml` if `XDG_CONFIG_HOME` is set -2. otherwise `~/.config/1coder/config.toml` (`%USERPROFILE%\.config\1coder\config.toml` on Windows) +2. otherwise: + - Unix / macOS: `~/.config/1coder/config.toml` + - Windows: `%APPDATA%\1coder\config.toml` (typically `C:\Users\\AppData\Roaming\1coder\config.toml`) Missing file → defaults, no error. diff --git a/tests/test_config.cpp b/tests/test_config.cpp index fdb44e3..19a58fa 100644 --- a/tests/test_config.cpp +++ b/tests/test_config.cpp @@ -43,6 +43,68 @@ struct ScopedFixtureDir { ~ScopedFixtureDir() { (void)OsDirDeleteRecursive(path); } }; +struct ScopedEnvVar { + Arena *arena; + const char *name; + bool had_old_value; + String8 old_value; + + ScopedEnvVar(Arena *arena_, const char *name_, String8 value) : arena(arena_), name(name_) { + const char *old = getenv(name); + had_old_value = old != nullptr; + if (had_old_value) old_value = PushStr8Copy(arena, Str8C(old)); + Set(value); + } + + ~ScopedEnvVar() { + if (had_old_value) { + Set(old_value); + } else { +#if defined(_WIN32) + CHECK(SetEnvironmentVariableA(name, nullptr) != 0); +#else + CHECK(unsetenv(name) == 0); +#endif + } + } + + void Set(String8 value) { +#if defined(_WIN32) + CHECK(SetEnvironmentVariableA(name, PushCStr(arena, value)) != 0); +#else + CHECK(setenv(name, PushCStr(arena, value), 1) == 0); +#endif + } + +}; + +struct ScopedEnvUnset { + Arena *arena; + const char *name; + bool had_old_value; + String8 old_value; + + ScopedEnvUnset(Arena *arena_, const char *name_) : arena(arena_), name(name_) { + const char *old = getenv(name); + had_old_value = old != nullptr; + if (had_old_value) old_value = PushStr8Copy(arena, Str8C(old)); +#if defined(_WIN32) + CHECK(SetEnvironmentVariableA(name, nullptr) != 0); +#else + CHECK(unsetenv(name) == 0); +#endif + } + + ~ScopedEnvUnset() { + if (!had_old_value) return; +#if defined(_WIN32) + CHECK(SetEnvironmentVariableA(name, PushCStr(arena, old_value)) != 0); +#else + CHECK(setenv(name, PushCStr(arena, old_value), 1) == 0); +#endif + } +}; + } // namespace TEST(toml_parses_tables_strings_and_arrays) { @@ -180,3 +242,33 @@ TEST(config_example_file_parses) { CHECK(loaded.ok); CHECK(loaded.config.binding_count > 100); } + +TEST(config_default_path_platform_location) { + ArenaScope scope; + ScopedFixtureDir dir(scope.arena, "default_path"); + + { + ScopedEnvVar xdg(scope.arena, "XDG_CONFIG_HOME", dir.path); + String8 path = ConfigDefaultPath(scope.arena); + String8 expect = OsPathJoin(scope.arena, dir.path, Str8Lit("1coder/config.toml")); + CHECK_STR(path, expect); + } + +#if defined(_WIN32) + { + ScopedEnvUnset xdg(scope.arena, "XDG_CONFIG_HOME"); + ScopedEnvVar appdata(scope.arena, "APPDATA", dir.path); + String8 path = ConfigDefaultPath(scope.arena); + String8 expect = OsPathJoin(scope.arena, dir.path, Str8Lit("1coder/config.toml")); + CHECK_STR(path, expect); + } +#else + { + ScopedEnvUnset xdg(scope.arena, "XDG_CONFIG_HOME"); + ScopedEnvVar home(scope.arena, "HOME", dir.path); + String8 path = ConfigDefaultPath(scope.arena); + String8 expect = OsPathJoin(scope.arena, dir.path, Str8Lit(".config/1coder/config.toml")); + CHECK_STR(path, expect); + } +#endif +}