Migrate config load/save/atomic_write to bread_utils::tomlcfg

load_doc/save_doc/atomic_write's bodies now delegate to
bread_utils::tomlcfg and bread_utils::atomic (path dependency for now, see
the TODO in Cargo.toml) instead of owning the temp-then-rename +
.bak-before-overwrite logic locally. Public function names/signatures in
config/mod.rs are unchanged, so none of the ~10 call sites across
ui/views/*.rs needed touching.

This is the other half of tonight's earlier BOS fix pass: that pass gave
breadhelp/src/config.rs its own byte-for-byte copy of this exact logic
(its own doc comment says "same discipline as bos-settings/src/config/
mod.rs::atomic_write") rather than sharing it — breadhelp's migration
follows in the next commit.

Builds clean; all 11 existing tests pass, including the atomic-write
backup/no-leftover-tmp-file test that now exercises the delegated code.
This commit is contained in:
Breadway 2026-07-17 09:30:36 +08:00
parent da6af6abf9
commit f3a5839cf1
3 changed files with 21 additions and 49 deletions

11
Cargo.lock generated
View file

@ -32,6 +32,7 @@ version = "0.6.3"
dependencies = [ dependencies = [
"async-channel", "async-channel",
"bread-theme", "bread-theme",
"bread-utils",
"glib", "glib",
"gtk4", "gtk4",
"serde", "serde",
@ -51,6 +52,16 @@ dependencies = [
"serde_json", "serde_json",
] ]
[[package]]
name = "bread-utils"
version = "0.2.3"
dependencies = [
"dirs",
"serde",
"serde_json",
"toml_edit 0.22.27",
]
[[package]] [[package]]
name = "cairo-rs" name = "cairo-rs"
version = "0.22.0" version = "0.22.0"

View file

@ -17,3 +17,5 @@ toml = "0.8"
# drops the rest of the user's config file. # drops the rest of the user's config file.
toml_edit = "0.22" toml_edit = "0.22"
async-channel = "2" async-channel = "2"
# TODO(owner): switch to tag-pinned git dependency once bread-utils is merged and tagged, matching the bread-theme pattern
bread-utils = { path = "../bread-ecosystem-fix-worktree/bread-utils", features = ["toml"] }

View file

@ -21,27 +21,12 @@ use toml_edit::{value, Array, DocumentMut, Item, Table, Value};
/// breadcrumbs' saved network passwords, ...). Back up the unparseable file /// breadcrumbs' saved network passwords, ...). Back up the unparseable file
/// once before falling back, so a bad edit is always recoverable. /// once before falling back, so a bad edit is always recoverable.
pub fn load_doc(path: &Path) -> DocumentMut { pub fn load_doc(path: &Path) -> DocumentMut {
let Ok(text) = std::fs::read_to_string(path) else { bread_utils::tomlcfg::load_doc("bos-settings", path)
return DocumentMut::default();
};
match text.parse::<DocumentMut>() {
Ok(doc) => doc,
Err(e) => {
let backup = PathBuf::from(format!("{}.bak", path.display()));
eprintln!(
"bos-settings: {} failed to parse ({e}); backed up to {} before falling back to defaults",
path.display(),
backup.display()
);
let _ = std::fs::write(&backup, &text);
DocumentMut::default()
}
}
} }
/// Write the document back to disk, creating parent dirs as needed. /// Write the document back to disk, creating parent dirs as needed.
pub fn save_doc(path: &Path, doc: &DocumentMut) -> Result<(), Box<dyn Error>> { pub fn save_doc(path: &Path, doc: &DocumentMut) -> Result<(), Box<dyn Error>> {
atomic_write(path, &doc.to_string())?; bread_utils::tomlcfg::save_doc(path, doc)?;
Ok(()) Ok(())
} }
@ -50,42 +35,16 @@ pub fn save_doc(path: &Path, doc: &DocumentMut) -> Result<(), Box<dyn Error>> {
/// ///
/// Every config-writing view in this app (TOML via `save_doc` above, and the /// Every config-writing view in this app (TOML via `save_doc` above, and the
/// plain-JSON views — keybinds, autostart, appearance/settings.json, /// plain-JSON views — keybinds, autostart, appearance/settings.json,
/// monitors.json, breadbar's CSS) should go through this instead of a bare /// monitors.json, breadbar's CSS) goes through this instead of a bare
/// `std::fs::write`: writing straight to the target path means a crash, /// `std::fs::write` — see `bread_utils::atomic::write_atomic_backed_up`'s
/// power loss, or disk-full error mid-write can leave the file truncated or /// doc comment for why (crash/power-loss safety via temp-then-rename, plus
/// corrupted with no way back. Writing to a temp file in the *same* /// a `.bak` of whatever was there before).
/// directory first, then `rename`-ing it over the target, avoids that — a
/// rename within one filesystem is atomic, so the target either has the old
/// complete contents or the new complete contents, never a partial write.
/// Backing up the previous file first (best-effort — the write can still
/// proceed if the backup fails, e.g. read-only source) means even a
/// successful-but-wrong write is always recoverable from `<path>.bak`.
pub fn atomic_write(path: &Path, contents: &str) -> std::io::Result<()> { pub fn atomic_write(path: &Path, contents: &str) -> std::io::Result<()> {
if let Some(parent) = path.parent() { bread_utils::atomic::write_atomic_backed_up(path, contents)
std::fs::create_dir_all(parent)?;
}
if path.exists() {
let backup = PathBuf::from(format!("{}.bak", path.display()));
let _ = std::fs::copy(path, &backup);
}
let dir = path.parent().map(Path::to_path_buf).unwrap_or_else(|| PathBuf::from("."));
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("config");
let tmp_path = dir.join(format!(".{file_name}.tmp.{}", std::process::id()));
std::fs::write(&tmp_path, contents)?;
std::fs::rename(&tmp_path, path)?;
Ok(())
} }
pub fn config_dir() -> PathBuf { pub fn config_dir() -> PathBuf {
// Honour XDG_CONFIG_HOME if set; otherwise fall back to $HOME/.config. bread_utils::xdg::config_home()
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
let p = PathBuf::from(xdg);
if p.is_absolute() {
return p;
}
}
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
PathBuf::from(home).join(".config")
} }
// --- typed readers (walk a dotted path, return None if absent/wrong type) --- // --- typed readers (walk a dotted path, return None if absent/wrong type) ---