From 5bc123932dcf823dbce42b19a9e9e67f754680e7 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 18 Sep 2026 12:58:18 +0000 Subject: [PATCH 1/5] feat(aw-sync): daemon pull is opt-in via aw-sync config.toml (#714) Erik's b8 decision: 0.14 is push-only everywhere unless the user opts in. Add the first proper config for aw-sync's config dir (dirs::get_config_dir(), previously a TODO stub): # aw-sync config pull = false # default; set true to import peers every pass Daemon subcommand: --mode changes from a defaulted SyncMode to Option so "not given" is distinguishable from an explicit choice. Effective mode is push-only when the config's pull=false, both when pull=true, and an explicit --mode always overrides the config (dirs::effective_daemon_mode). The one-shot `aw-sync sync` command is untouched -- it keeps its own pull+push default. The file is created with the commented default on first daemon start (or `status`), so users find the switch. `aw-sync status` now prints the effective mode and where the config was read from. No separate suppression was needed for the #687 "zero peers" warning: sync_run() already only captures pull-discovery warnings when mode is Pull/Both, so a push-only daemon pass naturally produces none. Git-Session-Id: 2f40c8f4-6fa5-5c1d-9313-e9e188acb41c --- aw-sync/src/dirs.rs | 116 ++++++++++++++++++++++++++++++++++++++++-- aw-sync/src/main.rs | 28 ++++++++-- aw-sync/src/status.rs | 19 +++++++ 3 files changed, 154 insertions(+), 9 deletions(-) diff --git a/aw-sync/src/dirs.rs b/aw-sync/src/dirs.rs index d8291c17..24cecf4f 100644 --- a/aw-sync/src/dirs.rs +++ b/aw-sync/src/dirs.rs @@ -1,7 +1,7 @@ use dirs::home_dir; +use serde::{Deserialize, Serialize}; use std::error::Error; use std::fs; -#[cfg(any(target_os = "android", test))] use std::path::Path; use std::path::PathBuf; @@ -35,15 +35,59 @@ pub fn resolve_profile( /// Uses the same profile appname as aw-server so a named profile (e.g. /// `research`) does not share prod's sync config. `testing` follows the /// same new-root-plus-legacy-fallback rule as aw-server. -// TODO: add proper config support #[cfg(not(target_os = "android"))] -#[allow(dead_code)] pub fn get_config_dir() -> Result> { let dir = sync_config_dir(&aw_server::dirs::appname())?; fs::create_dir_all(&dir)?; Ok(dir) } +/// aw-sync's own settings, read from `{config_dir}/config.toml`. +/// +/// `pull` controls whether the **daemon** imports peers on each pass — +/// desktop only; Android stays push-only by design +/// (ActivityWatch/aw-android#291). The one-shot `aw-sync sync` command +/// always pulls+pushes regardless of this file, and an explicit `--mode` +/// on the daemon always wins over it (ActivityWatch/aw-server-rust#714). +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct SyncConfig { + #[serde(default)] + pub pull: bool, +} + +const DEFAULT_SYNC_CONFIG_TOML: &str = "\ +# aw-sync config +pull = false # default; set true to import peers from the sync folder every pass +"; + +/// Load `config.toml` from `dir`, writing the commented default if it does +/// not exist yet (so users find the switch on first daemon start). Returns +/// the parsed config and the path it was read from. +#[cfg(not(target_os = "android"))] +pub fn load_or_create_sync_config(dir: &Path) -> Result<(SyncConfig, PathBuf), Box> { + fs::create_dir_all(dir)?; + let path = dir.join("config.toml"); + if !path.is_file() { + fs::write(&path, DEFAULT_SYNC_CONFIG_TOML)?; + } + let content = fs::read_to_string(&path)?; + let config: SyncConfig = toml::from_str(&content)?; + Ok((config, path)) +} + +/// Which `SyncMode` a daemon pass should use: an explicit `--mode` always +/// wins; otherwise the config's `pull` flag picks push-only vs both. +pub fn effective_daemon_mode( + cli_mode: Option, + pull: bool, +) -> crate::report::SyncMode { + cli_mode.unwrap_or(if pull { + crate::report::SyncMode::Both + } else { + crate::report::SyncMode::Push + }) +} + /// Path construction only — does not create directories (so tests stay off-disk). #[cfg(not(target_os = "android"))] fn sync_config_dir(appname: &str) -> Result> { @@ -112,7 +156,6 @@ pub(crate) fn files_dir_from_xdg_data_home(xdg_data_home: &Path) -> Option PathBuf { + std::env::temp_dir().join(format!( + "aw-sync-config-tests-{label}-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )) + } + + #[cfg(not(target_os = "android"))] + #[test] + fn load_or_create_sync_config_writes_commented_default_when_missing() { + let dir = temp_sync_config_dir("missing"); + let (config, path) = load_or_create_sync_config(&dir).unwrap(); + assert!(!config.pull, "default config must be pull = false"); + assert!(path.is_file()); + let content = fs::read_to_string(&path).unwrap(); + assert!( + content.contains("pull = false"), + "commented default should mention pull = false, got: {content}" + ); + let _ = fs::remove_dir_all(&dir); + } + + #[cfg(not(target_os = "android"))] + #[test] + fn load_or_create_sync_config_respects_existing_pull_true() { + let dir = temp_sync_config_dir("pull-true"); + fs::create_dir_all(&dir).unwrap(); + fs::write(dir.join("config.toml"), "pull = true\n").unwrap(); + let (config, _path) = load_or_create_sync_config(&dir).unwrap(); + assert!(config.pull); + let _ = fs::remove_dir_all(&dir); + } } diff --git a/aw-sync/src/main.rs b/aw-sync/src/main.rs index dae3608c..04e9847e 100644 --- a/aw-sync/src/main.rs +++ b/aw-sync/src/main.rs @@ -88,9 +88,10 @@ enum Commands { buckets: Option>, /// Mode to sync in. Can be "push", "pull", or "both". - /// Defaults to "both". - #[clap(long, default_value = "both")] - mode: sync::SyncMode, + /// If not given, follows aw-sync's own config.toml: push-only unless + /// `pull = true` is set there (ActivityWatch/aw-server-rust#714). + #[clap(long)] + mode: Option, /// Full path to sync db file /// Useful for syncing buckets from a specific db file in the sync directory. @@ -219,7 +220,7 @@ fn main() -> Result<(), Box> { match opts.command.unwrap_or(Commands::Daemon { start_date: None, buckets: None, - mode: sync::SyncMode::Both, + mode: None, sync_db: None, }) { // Start daemon @@ -233,7 +234,24 @@ fn main() -> Result<(), Box> { let effective_buckets = buckets; - daemon(&client, start_date, effective_buckets, sync_db, mode)?; + let sync_config_dir = dirs::get_config_dir()?; + let (sync_config, sync_config_path) = + dirs::load_or_create_sync_config(&sync_config_dir)?; + let effective_mode = dirs::effective_daemon_mode(mode, sync_config.pull); + info!( + "aw-sync config: {} (pull={}) -> daemon mode: {}", + sync_config_path.display(), + sync_config.pull, + effective_mode.as_str() + ); + + daemon( + &client, + start_date, + effective_buckets, + sync_db, + effective_mode, + )?; } // Perform sync Commands::Sync { diff --git a/aw-sync/src/status.rs b/aw-sync/src/status.rs index e1d14c6e..3c857d7d 100644 --- a/aw-sync/src/status.rs +++ b/aw-sync/src/status.rs @@ -90,6 +90,25 @@ pub fn collect_status( "UNREACHABLE" } )); + + match crate::dirs::get_config_dir() + .and_then(|dir| crate::dirs::load_or_create_sync_config(&dir)) + { + Ok((sync_config, sync_config_path)) => { + let effective_mode = crate::dirs::effective_daemon_mode(None, sync_config.pull); + out.push_str(&format!( + "daemon mode: {} (pull={}, config: {})\n", + effective_mode.as_str(), + sync_config.pull, + sync_config_path.display() + )); + } + Err(e) => { + out.push_str(&format!( + "daemon mode: (could not read aw-sync config: {e})\n" + )); + } + } out.push('\n'); match crate::report::load_last_report() { From 99d5c45040ee67e06ec17b805ebca5d0f96346f1 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 18 Sep 2026 13:24:34 +0000 Subject: [PATCH 2/5] fix(aw-sync): status reads config without creating it; suppress per-peer pull warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes from #716 review (Erik, 2026-09-18): 1. `status` is a read-only doctor. Swap `load_or_create_sync_config` for the new `read_sync_config` helper which returns `None` when the file is absent instead of writing the default. When absent, status now prints: daemon mode: push (pull=false, config: — not present, default) 2. When `pull = false`, listing every peer with "! has not been imported locally" is misleading — the daemon is doing the right thing. Replace the per-peer warnings with one top-level note: pull is off in ; peers below are visible but not imported by the daemon (set pull = true, or run `aw-sync sync`) Also adds two tests for `read_sync_config` (absent → None, no file created; present + pull=true → Some(config)). Git-Session-Id: d398 --- aw-sync/src/dirs.rs | 39 +++++++++++++++++++++++++ aw-sync/src/status.rs | 66 ++++++++++++++++++++++++++++++------------- 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/aw-sync/src/dirs.rs b/aw-sync/src/dirs.rs index 24cecf4f..1e781752 100644 --- a/aw-sync/src/dirs.rs +++ b/aw-sync/src/dirs.rs @@ -63,6 +63,10 @@ pull = false # default; set true to import peers from the sync folder every pa /// Load `config.toml` from `dir`, writing the commented default if it does /// not exist yet (so users find the switch on first daemon start). Returns /// the parsed config and the path it was read from. +// dirs.rs is compiled by both lib.rs and main.rs (dual-include); the lib +// does not call this directly (status.rs uses read_sync_config), but the +// daemon binary does via its own mod dirs copy. +#[cfg_attr(not(test), allow(dead_code))] #[cfg(not(target_os = "android"))] pub fn load_or_create_sync_config(dir: &Path) -> Result<(SyncConfig, PathBuf), Box> { fs::create_dir_all(dir)?; @@ -75,6 +79,20 @@ pub fn load_or_create_sync_config(dir: &Path) -> Result<(SyncConfig, PathBuf), B Ok((config, path)) } +/// Read-only variant for `status`: returns `(None, path)` when the file is +/// absent rather than writing the default. The daemon's `load_or_create` +/// writes on first start; the doctor should never create files. +#[cfg(not(target_os = "android"))] +pub fn read_sync_config(dir: &Path) -> Result<(Option, PathBuf), Box> { + let path = dir.join("config.toml"); + if !path.is_file() { + return Ok((None, path)); + } + let content = fs::read_to_string(&path)?; + let config: SyncConfig = toml::from_str(&content)?; + Ok((Some(config), path)) +} + /// Which `SyncMode` a daemon pass should use: an explicit `--mode` always /// wins; otherwise the config's `pull` flag picks push-only vs both. pub fn effective_daemon_mode( @@ -320,4 +338,25 @@ mod tests { assert!(config.pull); let _ = fs::remove_dir_all(&dir); } + + #[cfg(not(target_os = "android"))] + #[test] + fn read_sync_config_returns_none_when_missing_and_does_not_create_file() { + let dir = temp_sync_config_dir("read-missing"); + let (config, path) = read_sync_config(&dir).unwrap(); + assert!(config.is_none(), "should return None when file is absent"); + assert!(!path.exists(), "read_sync_config must not create the file"); + let _ = fs::remove_dir_all(&dir); + } + + #[cfg(not(target_os = "android"))] + #[test] + fn read_sync_config_reads_existing_config() { + let dir = temp_sync_config_dir("read-existing"); + fs::create_dir_all(&dir).unwrap(); + fs::write(dir.join("config.toml"), "pull = true\n").unwrap(); + let (config, _path) = read_sync_config(&dir).unwrap(); + assert!(config.unwrap().pull, "should read pull = true"); + let _ = fs::remove_dir_all(&dir); + } } diff --git a/aw-sync/src/status.rs b/aw-sync/src/status.rs index 3c857d7d..0da984ec 100644 --- a/aw-sync/src/status.rs +++ b/aw-sync/src/status.rs @@ -91,24 +91,32 @@ pub fn collect_status( } )); - match crate::dirs::get_config_dir() - .and_then(|dir| crate::dirs::load_or_create_sync_config(&dir)) - { - Ok((sync_config, sync_config_path)) => { - let effective_mode = crate::dirs::effective_daemon_mode(None, sync_config.pull); - out.push_str(&format!( - "daemon mode: {} (pull={}, config: {})\n", - effective_mode.as_str(), - sync_config.pull, - sync_config_path.display() - )); - } - Err(e) => { - out.push_str(&format!( - "daemon mode: (could not read aw-sync config: {e})\n" - )); - } - } + let (pull, config_label) = + match crate::dirs::get_config_dir().and_then(|dir| crate::dirs::read_sync_config(&dir)) { + Ok((Some(cfg), path)) => { + let effective_mode = crate::dirs::effective_daemon_mode(None, cfg.pull); + out.push_str(&format!( + "daemon mode: {} (pull={}, config: {})\n", + effective_mode.as_str(), + cfg.pull, + path.display() + )); + (cfg.pull, path.display().to_string()) + } + Ok((None, path)) => { + out.push_str(&format!( + "daemon mode: push (pull=false, config: {} — not present, default)\n", + path.display() + )); + (false, path.display().to_string()) + } + Err(e) => { + out.push_str(&format!( + "daemon mode: (could not read aw-sync config: {e})\n" + )); + (false, String::new()) + } + }; out.push('\n'); match crate::report::load_last_report() { @@ -142,7 +150,13 @@ pub fn collect_status( } } - let warnings = collect_warnings(&inspected, local_newest.as_ref(), &imported_origins); + let warnings = collect_warnings( + &inspected, + local_newest.as_ref(), + &imported_origins, + pull, + &config_label, + ); out.push('\n'); if warnings.is_empty() { out.push_str("Warnings: none\n"); @@ -183,9 +197,21 @@ fn collect_warnings( inspected: &[(SyncDirEntry, Option>)], local_newest: Option<&DateTime>, imported_origins: &HashSet, + pull: bool, + config_label: &str, ) -> Vec { let mut warnings = Vec::new(); + // When pull is off, say so once at the top (if there are visible peers) instead + // of emitting a per-peer "not imported locally" warning for every peer. + let has_peers = inspected.iter().any(|(e, _)| e.kind == SyncEntryKind::Peer); + if !pull && has_peers && !config_label.is_empty() { + warnings.push(format!( + "pull is off in {config_label}; peers below are visible but not imported by the \ + daemon (set pull = true, or run `aw-sync sync`)" + )); + } + let has_two = inspected .iter() .any(|(e, _)| e.layout == Some(SyncLayout::TwoLevel) && e.db_path.is_some()); @@ -235,7 +261,7 @@ fn collect_warnings( )); } } - if entry.kind == SyncEntryKind::Peer { + if pull && entry.kind == SyncEntryKind::Peer { if let Some(host) = &info.hostname { if !imported_origins.contains(host) { warnings.push(format!( From ccc9ce8e352e3a8917c92f57b7c55c2408332224 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 18 Sep 2026 13:42:48 +0000 Subject: [PATCH 3/5] fix(aw-sync): explicit --mode daemon startup no longer depends on config.toml An explicit --mode is documented to always win over config.toml, but the daemon still unconditionally loaded/created the config file first. A malformed or unwritable config.toml would then abort startup before daemon() ran, even with an explicit CLI override. Skip config loading entirely when --mode is given explicitly. Git-Session-Id: 212461ef-20e7-561e-b99b-a8f641b73b9a --- aw-sync/src/main.rs | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/aw-sync/src/main.rs b/aw-sync/src/main.rs index 04e9847e..15c4e0ed 100644 --- a/aw-sync/src/main.rs +++ b/aw-sync/src/main.rs @@ -234,16 +234,28 @@ fn main() -> Result<(), Box> { let effective_buckets = buckets; - let sync_config_dir = dirs::get_config_dir()?; - let (sync_config, sync_config_path) = - dirs::load_or_create_sync_config(&sync_config_dir)?; - let effective_mode = dirs::effective_daemon_mode(mode, sync_config.pull); - info!( - "aw-sync config: {} (pull={}) -> daemon mode: {}", - sync_config_path.display(), - sync_config.pull, - effective_mode.as_str() - ); + // An explicit --mode always wins and must not depend on config.toml + // being readable/writable — only touch the config file when no CLI + // mode was given. + let effective_mode = if let Some(explicit_mode) = mode { + info!( + "aw-sync: explicit --mode {} overrides config", + explicit_mode.as_str() + ); + explicit_mode + } else { + let sync_config_dir = dirs::get_config_dir()?; + let (sync_config, sync_config_path) = + dirs::load_or_create_sync_config(&sync_config_dir)?; + let effective_mode = dirs::effective_daemon_mode(None, sync_config.pull); + info!( + "aw-sync config: {} (pull={}) -> daemon mode: {}", + sync_config_path.display(), + sync_config.pull, + effective_mode.as_str() + ); + effective_mode + }; daemon( &client, From 5f995a3e85011319c34ff6b92dac03805d76f159 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 18 Sep 2026 14:15:22 +0000 Subject: [PATCH 4/5] fix(aw-sync): status never creates config dir; clarify config-derived mode is a guess - config_dir_path() constructs the aw-sync config path without creating it; get_config_dir() (used by the daemon, which is about to write config.toml there) now delegates to it. status previously called get_config_dir() before read_sync_config(), so the read-only doctor command silently created the config directory as a side effect. - status's 'daemon mode:' line is derived from config.toml alone and cannot see an explicit --mode passed to a running daemon; label it as such and point at the 'Last pass' section, which reports the mode actually used. Git-Session-Id: a7e90efc-d5b6-5d9b-a826-55b00e53d16c --- aw-sync/src/dirs.rs | 12 +++++++++++- aw-sync/src/status.rs | 44 +++++++++++++++++++++++-------------------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/aw-sync/src/dirs.rs b/aw-sync/src/dirs.rs index 1e781752..6c2d576a 100644 --- a/aw-sync/src/dirs.rs +++ b/aw-sync/src/dirs.rs @@ -35,13 +35,23 @@ pub fn resolve_profile( /// Uses the same profile appname as aw-server so a named profile (e.g. /// `research`) does not share prod's sync config. `testing` follows the /// same new-root-plus-legacy-fallback rule as aw-server. +#[allow(dead_code)] // used by the aw-sync binary; the lib copy is unused (status.rs uses config_dir_path) #[cfg(not(target_os = "android"))] pub fn get_config_dir() -> Result> { - let dir = sync_config_dir(&aw_server::dirs::appname())?; + let dir = config_dir_path()?; fs::create_dir_all(&dir)?; Ok(dir) } +/// Path to aw-sync's own config dir — construction only, does not create it. +/// For read-only callers (e.g. `status`) that must never mutate the +/// filesystem just to look at it; `get_config_dir` is for the daemon path, +/// which is about to write `config.toml` there anyway. +#[cfg(not(target_os = "android"))] +pub fn config_dir_path() -> Result> { + sync_config_dir(&aw_server::dirs::appname()) +} + /// aw-sync's own settings, read from `{config_dir}/config.toml`. /// /// `pull` controls whether the **daemon** imports peers on each pass — diff --git a/aw-sync/src/status.rs b/aw-sync/src/status.rs index 0da984ec..326cb7f1 100644 --- a/aw-sync/src/status.rs +++ b/aw-sync/src/status.rs @@ -91,32 +91,36 @@ pub fn collect_status( } )); - let (pull, config_label) = - match crate::dirs::get_config_dir().and_then(|dir| crate::dirs::read_sync_config(&dir)) { - Ok((Some(cfg), path)) => { - let effective_mode = crate::dirs::effective_daemon_mode(None, cfg.pull); - out.push_str(&format!( - "daemon mode: {} (pull={}, config: {})\n", + // Config-derived only: an explicit `--mode` on a running daemon overrides + // this and status has no way to see that from here. "Last pass" below + // reports the mode actually used on the last completed sync. + let (pull, config_label) = match crate::dirs::config_dir_path() + .and_then(|dir| crate::dirs::read_sync_config(&dir)) + { + Ok((Some(cfg), path)) => { + let effective_mode = crate::dirs::effective_daemon_mode(None, cfg.pull); + out.push_str(&format!( + "daemon mode: {} (pull={}, config: {}) — config-derived; see 'Last pass' below for the mode actually used, which wins if --mode was passed explicitly\n", effective_mode.as_str(), cfg.pull, path.display() )); - (cfg.pull, path.display().to_string()) - } - Ok((None, path)) => { - out.push_str(&format!( - "daemon mode: push (pull=false, config: {} — not present, default)\n", + (cfg.pull, path.display().to_string()) + } + Ok((None, path)) => { + out.push_str(&format!( + "daemon mode: push (pull=false, config: {} — not present, default) — config-derived; see 'Last pass' below for the mode actually used, which wins if --mode was passed explicitly\n", path.display() )); - (false, path.display().to_string()) - } - Err(e) => { - out.push_str(&format!( - "daemon mode: (could not read aw-sync config: {e})\n" - )); - (false, String::new()) - } - }; + (false, path.display().to_string()) + } + Err(e) => { + out.push_str(&format!( + "daemon mode: (could not read aw-sync config: {e})\n" + )); + (false, String::new()) + } + }; out.push('\n'); match crate::report::load_last_report() { From c3c123b99ba06e1a8e4d80b46314c5b5cd2ae1b1 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 18 Sep 2026 15:19:42 +0000 Subject: [PATCH 5/5] fix(aw-sync): namespace daemon config under [daemon] table Erik: 'Properties should be in some default namespace, not in top-level imo.' Follows aw-server's own convention of nesting a distinguishable sub-concern in its own [section] (cf. [auth] in aw-server/src/config.rs) rather than dumping it at the config's top level. pull now lives at [daemon].pull; SyncConfig wraps a new DaemonConfig. Updated the commented default, all call sites, and the dirs.rs unit tests. Git-Session-Id: bcd5de1a-146f-5d49-a776-ae61c28c2602 --- aw-sync/src/dirs.rs | 29 ++++++++++++++++++++--------- aw-sync/src/main.rs | 4 ++-- aw-sync/src/status.rs | 6 +++--- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/aw-sync/src/dirs.rs b/aw-sync/src/dirs.rs index 6c2d576a..03451ef0 100644 --- a/aw-sync/src/dirs.rs +++ b/aw-sync/src/dirs.rs @@ -52,7 +52,10 @@ pub fn config_dir_path() -> Result> { sync_config_dir(&aw_server::dirs::appname()) } -/// aw-sync's own settings, read from `{config_dir}/config.toml`. +/// `[daemon]` settings — namespaced (rather than top-level) so future +/// aw-sync settings that apply elsewhere (e.g. to the one-shot `sync` +/// command) have their own section instead of colliding with this one +/// (per-module convention: cf. aw-server's `[auth]` in `aw-server/src/config.rs`). /// /// `pull` controls whether the **daemon** imports peers on each pass — /// desktop only; Android stays push-only by design @@ -60,13 +63,21 @@ pub fn config_dir_path() -> Result> { /// always pulls+pushes regardless of this file, and an explicit `--mode` /// on the daemon always wins over it (ActivityWatch/aw-server-rust#714). #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub struct SyncConfig { +pub struct DaemonConfig { #[serde(default)] pub pull: bool, } +/// aw-sync's own settings, read from `{config_dir}/config.toml`. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct SyncConfig { + #[serde(default)] + pub daemon: DaemonConfig, +} + const DEFAULT_SYNC_CONFIG_TOML: &str = "\ # aw-sync config +[daemon] pull = false # default; set true to import peers from the sync folder every pass "; @@ -328,12 +339,12 @@ mod tests { fn load_or_create_sync_config_writes_commented_default_when_missing() { let dir = temp_sync_config_dir("missing"); let (config, path) = load_or_create_sync_config(&dir).unwrap(); - assert!(!config.pull, "default config must be pull = false"); + assert!(!config.daemon.pull, "default config must be pull = false"); assert!(path.is_file()); let content = fs::read_to_string(&path).unwrap(); assert!( - content.contains("pull = false"), - "commented default should mention pull = false, got: {content}" + content.contains("[daemon]") && content.contains("pull = false"), + "commented default should be namespaced under [daemon] and mention pull = false, got: {content}" ); let _ = fs::remove_dir_all(&dir); } @@ -343,9 +354,9 @@ mod tests { fn load_or_create_sync_config_respects_existing_pull_true() { let dir = temp_sync_config_dir("pull-true"); fs::create_dir_all(&dir).unwrap(); - fs::write(dir.join("config.toml"), "pull = true\n").unwrap(); + fs::write(dir.join("config.toml"), "[daemon]\npull = true\n").unwrap(); let (config, _path) = load_or_create_sync_config(&dir).unwrap(); - assert!(config.pull); + assert!(config.daemon.pull); let _ = fs::remove_dir_all(&dir); } @@ -364,9 +375,9 @@ mod tests { fn read_sync_config_reads_existing_config() { let dir = temp_sync_config_dir("read-existing"); fs::create_dir_all(&dir).unwrap(); - fs::write(dir.join("config.toml"), "pull = true\n").unwrap(); + fs::write(dir.join("config.toml"), "[daemon]\npull = true\n").unwrap(); let (config, _path) = read_sync_config(&dir).unwrap(); - assert!(config.unwrap().pull, "should read pull = true"); + assert!(config.unwrap().daemon.pull, "should read pull = true"); let _ = fs::remove_dir_all(&dir); } } diff --git a/aw-sync/src/main.rs b/aw-sync/src/main.rs index 15c4e0ed..d69cf192 100644 --- a/aw-sync/src/main.rs +++ b/aw-sync/src/main.rs @@ -247,11 +247,11 @@ fn main() -> Result<(), Box> { let sync_config_dir = dirs::get_config_dir()?; let (sync_config, sync_config_path) = dirs::load_or_create_sync_config(&sync_config_dir)?; - let effective_mode = dirs::effective_daemon_mode(None, sync_config.pull); + let effective_mode = dirs::effective_daemon_mode(None, sync_config.daemon.pull); info!( "aw-sync config: {} (pull={}) -> daemon mode: {}", sync_config_path.display(), - sync_config.pull, + sync_config.daemon.pull, effective_mode.as_str() ); effective_mode diff --git a/aw-sync/src/status.rs b/aw-sync/src/status.rs index 326cb7f1..25e4c3e2 100644 --- a/aw-sync/src/status.rs +++ b/aw-sync/src/status.rs @@ -98,14 +98,14 @@ pub fn collect_status( .and_then(|dir| crate::dirs::read_sync_config(&dir)) { Ok((Some(cfg), path)) => { - let effective_mode = crate::dirs::effective_daemon_mode(None, cfg.pull); + let effective_mode = crate::dirs::effective_daemon_mode(None, cfg.daemon.pull); out.push_str(&format!( "daemon mode: {} (pull={}, config: {}) — config-derived; see 'Last pass' below for the mode actually used, which wins if --mode was passed explicitly\n", effective_mode.as_str(), - cfg.pull, + cfg.daemon.pull, path.display() )); - (cfg.pull, path.display().to_string()) + (cfg.daemon.pull, path.display().to_string()) } Ok((None, path)) => { out.push_str(&format!(