feat(headless): complete portable single-session host

This commit is contained in:
Erik 2026-07-27 07:36:53 +02:00
parent fbebb91848
commit f8cb840fb1
30 changed files with 3571 additions and 32 deletions

View file

@ -0,0 +1,99 @@
using AcDream.Headless.Configuration;
namespace AcDream.Headless.Platform;
internal sealed record HeadlessPathSet(
string ConfigDirectory,
string DataDirectory,
string CacheDirectory)
{
internal static HeadlessPathSet Resolve(
HeadlessPathOverrides overrides,
IHeadlessPlatformEnvironment? platform = null)
{
ArgumentNullException.ThrowIfNull(overrides);
platform ??= HeadlessPlatformEnvironment.Instance;
string config;
string data;
string cache;
if (platform.IsWindows)
{
string roaming = RequireFolder(
platform,
Environment.SpecialFolder.ApplicationData);
string local = RequireFolder(
platform,
Environment.SpecialFolder.LocalApplicationData);
config = Path.Combine(roaming, "acdream");
data = Path.Combine(local, "acdream");
cache = Path.Combine(local, "acdream", "cache");
}
else
{
string home = RequireFolder(
platform,
Environment.SpecialFolder.UserProfile);
config = ResolveXdg(
platform,
"XDG_CONFIG_HOME",
Path.Combine(home, ".config"),
"acdream");
data = ResolveXdg(
platform,
"XDG_DATA_HOME",
Path.Combine(home, ".local", "share"),
"acdream");
cache = ResolveXdg(
platform,
"XDG_CACHE_HOME",
Path.Combine(home, ".cache"),
"acdream");
}
return new HeadlessPathSet(
Normalize(overrides.ConfigDirectory ?? config, platform),
Normalize(overrides.DataDirectory ?? data, platform),
Normalize(overrides.CacheDirectory ?? cache, platform));
}
private static string ResolveXdg(
IHeadlessPlatformEnvironment platform,
string variable,
string fallback,
string leaf)
{
string? configured = platform.GetEnvironmentVariable(variable);
string root = string.IsNullOrWhiteSpace(configured)
? fallback
: configured;
return Path.Combine(root, leaf);
}
private static string RequireFolder(
IHeadlessPlatformEnvironment platform,
Environment.SpecialFolder folder)
{
string value = platform.GetFolderPath(folder);
if (string.IsNullOrWhiteSpace(value))
{
throw new HeadlessConfigurationException(
$"The operating system did not provide {folder}.");
}
return value;
}
private static string Normalize(
string path,
IHeadlessPlatformEnvironment platform)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new HeadlessConfigurationException(
"Headless directories cannot be empty.");
}
return Path.TrimEndingDirectorySeparator(
Path.GetFullPath(path, platform.CurrentDirectory));
}
}

View file

@ -0,0 +1,28 @@
namespace AcDream.Headless.Platform;
internal interface IHeadlessPlatformEnvironment
{
bool IsWindows { get; }
string CurrentDirectory { get; }
string? GetEnvironmentVariable(string name);
string GetFolderPath(Environment.SpecialFolder folder);
}
internal sealed class HeadlessPlatformEnvironment
: IHeadlessPlatformEnvironment
{
internal static HeadlessPlatformEnvironment Instance { get; } = new();
private HeadlessPlatformEnvironment()
{
}
public bool IsWindows => OperatingSystem.IsWindows();
public string CurrentDirectory => Environment.CurrentDirectory;
public string? GetEnvironmentVariable(string name) =>
Environment.GetEnvironmentVariable(name);
public string GetFolderPath(Environment.SpecialFolder folder) =>
Environment.GetFolderPath(folder);
}