// XDG-backed configuration persistence. The config file lives at
// `$XDG_CONFIG_HOME/moui/config.json` (POSIX) or `%APPDATA%\moui\config.json`
// (Windows). Tests override the directory via `MOUI_CONFIG_DIR` so the real
// user config is never touched.

///|
fn config_dir(host : HostPlatform) -> String {
  match @env.get_env_var("MOUI_CONFIG_DIR") {
    Some(value) if value != "" => value
    _ =>
      match host {
        Windows =>
          match @env.get_env_var("APPDATA") {
            Some(value) if value != "" => join_path(value, "moui")
            _ =>
              match @env.get_env_var("USERPROFILE") {
                Some(value) if value != "" =>
                  join_path(join_path(value, "AppData"), "Roaming/moui")
                _ => "moui"
              }
          }
        _ =>
          match @env.get_env_var("XDG_CONFIG_HOME") {
            Some(value) if value != "" => join_path(value, "moui")
            _ =>
              match @env.get_env_var("HOME") {
                Some(value) if value != "" => join_path(value, ".config/moui")
                _ => ".config/moui"
              }
          }
      }
  }
}

///|
fn config_path(host : HostPlatform) -> String {
  join_path(config_dir(host), "config.json")
}

///|
fn read_config(host : HostPlatform) -> Result[CliConfig, String] {
  let path = config_path(host)
  let text = @fs.read_file_to_string(path) catch {
    _ => return Ok(default_cli_config())
  }
  let parsed = @json.parse(text) catch {
    _ => return Err("cli config is invalid JSON: " + path)
  }
  match cli_config_from_json(parsed) {
    Ok(config) => Ok(config)
    Err(message) => Err(message)
  }
}

///|
fn write_config(
  config : CliConfig,
  host : HostPlatform,
) -> Result[Unit, String] {
  let dir = config_dir(host)
  match mkdir_p(dir) {
    Ok(_) => ()
    Err(message) => return Err(message)
  }
  let path = config_path(host)
  let json_text = cli_config_to_json(config).stringify(indent=2) + "\n"
  let bytes = @utf8.encode(json_text[:], bom=false)
  replace_file_atomic(path, bytes)
}

///|
fn config_get(config : CliConfig, key_path : String) -> Result[String, String] {
  let parts = key_path.split(".").to_array()
  if parts.length() != 2 {
    return Err("config key must be 
.: " + key_path) } let section = parts[0].to_owned() let field = parts[1].to_owned() let raw : Json = match section { "harmonyos" => harmonyos_config_to_json(config.harmonyos) "android" => android_config_to_json(config.android) "ios" => ios_config_to_json(config.ios) "paths" => paths_config_to_json(config.paths) _ => return Err("unknown config section: " + section) } guard raw is Object(fields) else { return Err("config section " + section + " is not an object") } match fields.get(field) { None => Err("not set: " + key_path) Some(Null) => Err("not set: " + key_path) Some(String(value)) => Ok(value) Some(Number(value, repr=_)) if value.to_int().to_double() == value => Ok(value.to_int().to_string()) Some(Number(value, repr=_)) => Ok(value.to_string()) Some(True) => Ok("true") Some(False) => Ok("false") Some(value) => Ok(value.stringify()) } } ///| fn try_parse_int(value : String) -> Int? { if value == "" { return None } let negative = value[0] == '-' let digits = if negative { value[1:] } else { value[:] } if digits.is_empty() { return None } for c in digits { if c < '0' || c > '9' { return None } } let number = Ref(0) for c in digits { number.val = number.val * 10 + c.to_int() - '0'.to_int() } Some(if negative { -number.val } else { number.val }) } ///| fn config_set( config : CliConfig, key_path : String, value : String, ) -> Result[CliConfig, String] { let parts = key_path.split(".").to_array() if parts.length() != 2 { return Err("config key must be
.: " + key_path) } let section = parts[0].to_owned() let field = parts[1].to_owned() match section { "harmonyos" => { let harmonyos = config.harmonyos let new_harmonyos = match field { "sdkHome" => { ..harmonyos, sdk_home: Some(value), } "hvigorw" => { ..harmonyos, hvigorw: Some(value), } "ohpm" => { ..harmonyos, ohpm: Some(value), } "signingConfigFile" => { ..harmonyos, signing_config_file: Some(value), } _ => return Err("unknown harmonyos field: " + field) } Ok({ ..config, harmonyos: new_harmonyos, }) } "android" => { let android = config.android let new_android = match field { "sdkHome" => { ..android, sdk_home: Some(value), } "ndkHome" => { ..android, ndk_home: Some(value), } "compileSdk" => match try_parse_int(value) { Some(n) => { ..android, compile_sdk: Some(n), } None => return Err("android.compileSdk must be an integer") } "targetSdk" => match try_parse_int(value) { Some(n) => { ..android, target_sdk: Some(n), } None => return Err("android.targetSdk must be an integer") } _ => return Err("unknown android field: " + field) } Ok({ ..config, android: new_android, }) } "ios" => { let ios = config.ios let new_ios = match field { "sdk" => { ..ios, sdk: Some(value), } "arch" => { ..ios, arch: Some(value), } "deploymentTarget" => match try_parse_int(value) { Some(n) => { ..ios, deployment_target: Some(n), } None => return Err("ios.deploymentTarget must be an integer") } _ => return Err("unknown ios field: " + field) } Ok({ ..config, ios: new_ios, }) } "paths" => { let paths = config.paths let new_paths = match field { "moonHome" => { ..paths, moon_home: Some(value), } "mouiRoot" => { ..paths, moui_root: Some(value), } "skiaRoot" => { ..paths, skia_root: Some(value), } _ => return Err("unknown paths field: " + field) } Ok({ ..config, paths: new_paths, }) } _ => Err("unknown config section: " + section) } } ///| fn config_list_lines(config : CliConfig) -> Array[String] { let lines : Array[String] = [] let harmonyos_fields = ["sdkHome", "hvigorw", "ohpm", "signingConfigFile"] for field in harmonyos_fields { match config_get(config, "harmonyos." + field) { Ok(value) => lines.push("harmonyos." + field + " = " + value) Err(_) => () } } let android_fields = ["sdkHome", "ndkHome", "compileSdk", "targetSdk"] for field in android_fields { match config_get(config, "android." + field) { Ok(value) => lines.push("android." + field + " = " + value) Err(_) => () } } if !config.android.abi.is_empty() { lines.push("android.abi = " + config.android.abi.join(",")) } let ios_fields = ["sdk", "arch", "deploymentTarget"] for field in ios_fields { match config_get(config, "ios." + field) { Ok(value) => lines.push("ios." + field + " = " + value) Err(_) => () } } let paths_fields = ["moonHome", "mouiRoot", "skiaRoot"] for field in paths_fields { match config_get(config, "paths." + field) { Ok(value) => lines.push("paths." + field + " = " + value) Err(_) => () } } lines }