pub(super) fn set_key_in_hjson_block(
raw: &str,
block: &str,
key: &str,
value_lit: &str,
) -> Result<String, String> {
let lines: Vec<&str> = raw.split_inclusive('\n').collect();
if lines.is_empty() {
return Err("config file is empty".into());
}
let block_prefix = format!("{block}:");
let block_open_idx = lines.iter().position(|l| {
let trimmed = l.trim_start();
!trimmed.starts_with("//") && trimmed.starts_with(&block_prefix)
});
let block_open_idx = block_open_idx
.ok_or_else(|| format!("no `{block}:` block found in HJSON"))?;
let mut depth: i32 = 0;
let mut block_started = false;
let mut block_end: Option<usize> = None;
for (i, line) in lines.iter().enumerate().skip(block_open_idx) {
let code = line.split("//").next().unwrap_or("");
for c in code.chars() {
match c {
'{' => {
depth += 1;
block_started = true;
}
'}' => depth -= 1,
_ => {}
}
}
if block_started && depth == 0 {
block_end = Some(i);
break;
}
}
let block_end = block_end
.ok_or_else(|| format!("unterminated `{block}: {{` block — check brace balance"))?;
let key_unquoted = format!("{key}:");
let key_quoted = format!("\"{key}\":");
let mut depth: i32 = 0;
let mut target_idx: Option<usize> = None;
for (i, line) in lines.iter().enumerate().take(block_end + 1).skip(block_open_idx) {
let depth_before = depth;
let code = line.split("//").next().unwrap_or("");
for c in code.chars() {
match c {
'{' => depth += 1,
'}' => depth -= 1,
_ => {}
}
}
if i == block_open_idx {
continue;
}
if depth_before == 1 {
let trimmed = line.trim_start();
if trimmed.starts_with("//") {
continue;
}
if trimmed.starts_with(&key_unquoted) || trimmed.starts_with(&key_quoted) {
target_idx = Some(i);
break;
}
}
}
let mut out = String::with_capacity(raw.len() + value_lit.len());
match target_idx {
Some(idx) => {
let mut rewrote = false;
for (i, line) in lines.iter().enumerate() {
if i == idx {
rewrote = true;
let (eol, core): (&str, &str) =
if let Some(stripped) = line.strip_suffix("\r\n") {
("\r\n", stripped)
} else if let Some(stripped) = line.strip_suffix('\n') {
("\n", stripped)
} else {
("", *line)
};
let colon_pos = core.find(':').ok_or_else(|| {
format!("`{key}` line missing `:` separator — unexpected HJSON")
})?;
let head = &core[..=colon_pos]; let tail = &core[colon_pos + 1..];
let comment_pos = tail.find("//");
let (_old_value, comment_suffix) = match comment_pos {
Some(p) => (&tail[..p], &tail[p..]),
None => (tail, ""),
};
if comment_suffix.is_empty() {
out.push_str(&format!("{head} {value_lit}{eol}"));
} else {
out.push_str(&format!("{head} {value_lit} {comment_suffix}{eol}"));
}
} else {
out.push_str(line);
}
}
if !rewrote {
return Err("internal error: target line not rewritten".into());
}
Ok(out)
}
None => {
let block_indent: String = lines[block_open_idx]
.chars()
.take_while(|c| *c == ' ' || *c == '\t')
.collect();
let child_indent = format!("{block_indent} ");
for (i, line) in lines.iter().enumerate() {
out.push_str(line);
if i == block_open_idx {
let eol = if line.ends_with("\r\n") {
"\r\n"
} else if line.ends_with('\n') {
"\n"
} else {
"\n"
};
out.push_str(&format!("{child_indent}{key}: {value_lit}{eol}"));
}
}
Ok(out)
}
}
}
pub(super) fn set_llm_default_in_hjson(
raw: &str,
new_default: &str,
) -> Result<String, String> {
let quote_needed = new_default.is_empty()
|| !new_default
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-');
let value_lit = if quote_needed {
format!(
"\"{}\"",
new_default.replace('\\', "\\\\").replace('"', "\\\"")
)
} else {
new_default.to_string()
};
set_key_in_hjson_block(raw, "llm", "default", &value_lit)
}
pub(super) fn set_sound_enabled_in_hjson(
raw: &str,
enabled: bool,
) -> Result<String, String> {
let value_lit = if enabled { "true" } else { "false" };
match set_key_in_hjson_block(raw, "sound", "enabled", value_lit) {
Ok(s) => Ok(s),
Err(reason) if reason.contains("no `sound:` block") => {
insert_sound_block_before_root_close(raw, value_lit)
}
Err(other) => Err(other),
}
}
pub(super) fn insert_sound_block_before_root_close(
raw: &str,
value_lit: &str,
) -> Result<String, String> {
let lines: Vec<&str> = raw.split_inclusive('\n').collect();
let root_close_idx = lines.iter().enumerate().rev().find_map(|(i, l)| {
let code = l.split("//").next().unwrap_or("");
let trimmed = code.trim();
if trimmed == "}" {
Some(i)
} else {
None
}
});
let root_close_idx = root_close_idx.ok_or_else(|| {
"no root closing `}` found — file shape unrecognised".to_string()
})?;
let block = format!(
"\n // Typewriter SFX (Ctrl+B E to toggle).\n sound: {{\n enabled: {value_lit}\n volume: 0.6\n }}\n"
);
let mut out = String::with_capacity(raw.len() + block.len());
for (i, line) in lines.iter().enumerate() {
if i == root_close_idx {
out.push_str(&block);
}
out.push_str(line);
}
Ok(out)
}