1use anyhow::{Result, bail};
10use std::path::Path;
11
12use crate::config::{PerRepoConfig, Registry, Settings};
13use crate::output;
14
15struct Setting {
23 key: &'static str,
24 help: &'static str,
26 get: fn(&Settings) -> String,
27 set: fn(&mut Settings, &str) -> Result<()>,
28}
29
30const SETTINGS: &[Setting] = &[
32 Setting {
33 key: "idle_days",
34 help: "Days a repository must sit untouched before it is eligible for pruning.",
35 get: |s| s.idle_days.to_string(),
36 set: |s, v| {
37 s.idle_days = v
38 .parse()
39 .map_err(|_| anyhow::anyhow!("idle_days must be a whole number of days"))?;
40 Ok(())
41 },
42 },
43 Setting {
44 key: "min_size_mb",
45 help: "Smallest bloat directory worth deleting, in MiB. 0 removes the floor.",
46 get: |s| s.min_size_mb.to_string(),
47 set: |s, v| {
48 s.min_size_mb = v.parse().map_err(|_| {
49 anyhow::anyhow!("min_size_mb must be a whole number of MiB (0 disables the floor)")
50 })?;
51 Ok(())
52 },
53 },
54 Setting {
55 key: "scan_depth",
56 help: "How many directory levels below a repo root project discovery descends.",
57 get: |s| s.scan_depth.to_string(),
58 set: |s, v| {
59 let depth: usize = v
60 .parse()
61 .map_err(|_| anyhow::anyhow!("scan_depth must be a positive integer"))?;
62 if depth == 0 {
66 bail!("scan_depth must be at least 1 — 0 would find no projects at all.");
67 }
68 if depth > crate::constants::MAX_SCAN_DEPTH_LIMIT {
69 bail!(
70 "scan_depth must be at most {} — deeper walks stall on generated trees.",
71 crate::constants::MAX_SCAN_DEPTH_LIMIT
72 );
73 }
74 s.scan_depth = depth;
75 Ok(())
76 },
77 },
78 Setting {
79 key: "require_confirmation",
80 help: "Ask before deleting anything. Turning this off makes every run unattended.",
81 get: |s| s.require_confirmation.to_string(),
82 set: |s, v| {
83 s.require_confirmation = parse_bool("require_confirmation", v)?;
84 Ok(())
85 },
86 },
87 Setting {
88 key: "allow_manifest_rewrite",
89 help: "Let cargo and go run the sync command that rewrites tracked manifests.",
90 get: |s| s.allow_manifest_rewrite.to_string(),
91 set: |s, v| {
92 s.allow_manifest_rewrite = parse_bool("allow_manifest_rewrite", v)?;
93 Ok(())
94 },
95 },
96 Setting {
97 key: "command_timeout_secs",
98 help: "How long a lockfile command may run before it is killed.",
99 get: |s| s.command_timeout_secs.to_string(),
100 set: |s, v| {
101 let secs: u64 = v
102 .parse()
103 .map_err(|_| anyhow::anyhow!("command_timeout_secs must be a positive integer"))?;
104 if secs == 0 {
108 bail!(
109 "command_timeout_secs must be at least 1 — 0 would kill every command \
110 the instant it starts."
111 );
112 }
113 s.command_timeout_secs = secs;
114 Ok(())
115 },
116 },
117 Setting {
118 key: "auto_setup",
119 help: "Install missing integrations by itself, once per installed version.",
120 get: |s| s.auto_setup.to_string(),
121 set: |s, v| {
122 s.auto_setup = parse_bool("auto_setup", v)?;
123 Ok(())
124 },
125 },
126 Setting {
127 key: "auto_daemon",
128 help: "Register the OS scheduler so passes run without being remembered.",
129 get: |s| s.auto_daemon.to_string(),
130 set: |s, v| {
131 s.auto_daemon = parse_bool("auto_daemon", v)?;
132 Ok(())
133 },
134 },
135 Setting {
136 key: "check_interval_days",
137 help: "Days between scheduled background passes.",
138 get: |s| s.check_interval_days.to_string(),
139 set: |s, v| {
140 let days: u64 = v
141 .parse()
142 .map_err(|_| anyhow::anyhow!("check_interval_days must be a positive integer"))?;
143 if days == 0 {
145 bail!("check_interval_days must be at least 1.");
146 }
147 s.check_interval_days = days;
148 Ok(())
149 },
150 },
151 Setting {
152 key: "auto_hooks",
153 help: "Install the Git hooks that register repositories as you clone them.",
154 get: |s| s.auto_hooks.to_string(),
155 set: |s, v| {
156 s.auto_hooks = parse_bool("auto_hooks", v)?;
157 Ok(())
158 },
159 },
160 Setting {
161 key: "auto_hooks_chain",
162 help: "If another tool owns core.hooksPath, install in front of it and forward.",
163 get: |s| s.auto_hooks_chain.to_string(),
164 set: |s, v| {
165 s.auto_hooks_chain = parse_bool("auto_hooks_chain", v)?;
166 Ok(())
167 },
168 },
169 Setting {
170 key: "update_check",
171 help: "Ask GitHub for the latest release from time to time. Sends nothing but the request.",
172 get: |s| s.update_check.to_string(),
173 set: |s, v| {
174 s.update_check = parse_bool("update_check", v)?;
175 Ok(())
176 },
177 },
178 Setting {
179 key: "update_check_interval_days",
180 help: "Days between automatic release checks.",
181 get: |s| s.update_check_interval_days.to_string(),
182 set: |s, v| {
183 let days: i64 = v.parse().map_err(|_| {
184 anyhow::anyhow!("update_check_interval_days must be a positive integer")
185 })?;
186 if days < 1 {
187 bail!("update_check_interval_days must be at least 1.");
188 }
189 s.update_check_interval_days = days;
190 Ok(())
191 },
192 },
193 Setting {
194 key: "update_check_timeout_secs",
195 help: "Seconds the release check waits for GitHub. Raise it behind a slow proxy.",
196 get: |s| s.update_check_timeout_secs.to_string(),
197 set: |s, v| {
198 let secs: u64 = v.parse().map_err(|_| {
199 anyhow::anyhow!("update_check_timeout_secs must be a positive integer")
200 })?;
201 if secs == 0 {
202 bail!("update_check_timeout_secs must be at least 1.");
203 }
204 s.update_check_timeout_secs = secs;
205 Ok(())
206 },
207 },
208];
209
210fn parse_bool(key: &str, value: &str) -> Result<bool> {
211 match value.trim().to_lowercase().as_str() {
212 "true" | "yes" | "y" | "on" | "1" => Ok(true),
213 "false" | "no" | "n" | "off" | "0" => Ok(false),
214 _ => bail!("{key} must be true or false"),
215 }
216}
217
218pub fn invalid_settings(settings: &Settings) -> Vec<(&'static str, String)> {
229 SETTINGS
230 .iter()
231 .filter_map(|setting| {
232 let mut probe = settings.clone();
233 (setting.set)(&mut probe, &(setting.get)(settings))
234 .err()
235 .map(|e| (setting.key, e.to_string()))
236 })
237 .collect()
238}
239
240pub fn setting_count() -> usize {
242 SETTINGS.len()
243}
244
245fn find_setting(key: &str) -> Result<&'static Setting> {
246 SETTINGS
247 .iter()
248 .find(|s| s.key == key)
249 .ok_or_else(|| anyhow::anyhow!("Unknown config key: {key}. Valid keys: {}", valid_keys()))
250}
251
252fn valid_keys() -> String {
253 SETTINGS
254 .iter()
255 .map(|s| s.key)
256 .collect::<Vec<_>>()
257 .join(", ")
258}
259
260#[derive(Debug, PartialEq, Eq)]
262pub enum Toggle {
263 Enable,
264 Disable,
265 Status,
266}
267
268pub fn parse_toggle(action: &str) -> Result<Toggle> {
278 match action.to_lowercase().as_str() {
279 "enable" | "install" | "on" => Ok(Toggle::Enable),
280 "disable" | "uninstall" | "remove" | "off" => Ok(Toggle::Disable),
281 "" | "status" | "show" => Ok(Toggle::Status),
282 other => bail!(
283 "Unknown action `{other}`. Expected `enable`, `disable` or `status` \
284 (`install` / `uninstall` / `on` / `off` also work)."
285 ),
286 }
287}
288
289pub fn is_toggle_word(word: &str) -> bool {
295 parse_toggle(word).is_ok() && !word.is_empty()
296}
297
298fn resolve_workspace(path: &str) -> Result<std::path::PathBuf> {
305 let raw = Path::new(path);
306 if !raw.is_dir() {
307 bail!(
308 "`{path}` is neither an action nor an existing directory.\n\
309 Expected `enable`, `disable` or `status`, or a path to a repository."
310 );
311 }
312 Ok(raw.canonicalize().unwrap_or_else(|_| raw.to_path_buf()))
313}
314
315pub fn run_get(key: &str) -> Result<()> {
317 let registry = Registry::load()?;
318 let setting = find_setting(key)?;
319 println!("{key} = {}", (setting.get)(®istry.settings));
320 Ok(())
321}
322
323pub fn run_set(key: &str, value: &str) -> Result<()> {
325 let mut registry = Registry::load()?;
326 let setting = find_setting(key)?;
327 (setting.set)(&mut registry.settings, value)?;
328 registry.save()?;
329
330 output::print_success(&format!("{key} = {}", (setting.get)(®istry.settings)));
333 Ok(())
334}
335
336fn key_column_width() -> usize {
338 SETTINGS.iter().map(|s| s.key.len()).max().unwrap_or(0)
339}
340
341pub fn run_show() -> Result<()> {
343 let registry = Registry::load()?;
344 let width = key_column_width();
345
346 output::print_header("dev-prune Global Configuration");
347 for setting in SETTINGS {
348 println!(
349 " {:<width$} = {}",
350 setting.key,
351 (setting.get)(®istry.settings)
352 );
353 }
354 println!(" {:<width$} = {}", "tracked_repos", registry.repo_count());
355
356 let reg_path = Registry::registry_path()
357 .map(|p| output::clean_path(&p))
358 .unwrap_or_else(|_| "unknown".to_string());
359 println!("\n {:<width$} = {reg_path}", "registry_file");
360 println!();
361 output::print_info("Change any of these with `devp config set <key> <value>`.");
362 output::print_info("Walk through them one at a time with `devp config wizard`.");
363
364 Ok(())
365}
366
367pub fn run_wizard() -> Result<()> {
376 use std::io::{self, IsTerminal, Write};
377
378 if !io::stdin().is_terminal() {
379 bail!(
380 "`devp config wizard` needs a terminal to ask questions on.\n\
381 Use `devp config show` to read the settings and `devp config set <key> <value>` \
382 to change one."
383 );
384 }
385
386 let mut registry = Registry::load()?;
387 let width = key_column_width();
388
389 output::print_header("dev-prune configuration");
390 output::print_info("These are the defaults every run will use. Nothing has been changed yet.");
391 println!();
392 for setting in SETTINGS {
393 println!(
394 " {:<width$} = {}",
395 setting.key,
396 (setting.get)(®istry.settings)
397 );
398 println!(" {:<width$} {}", "", setting.help);
399 }
400 println!();
401
402 print!("Keep all of these? [Y/n] ");
403 io::stdout().flush()?;
404 let mut answer = String::new();
405 io::stdin().read_line(&mut answer)?;
406 let keep = !matches!(answer.trim().to_lowercase().as_str(), "n" | "no");
407
408 if keep {
409 mark_reviewed();
410 output::print_success("Keeping the defaults. `devp config set <key> <value>` changes any.");
411 return Ok(());
412 }
413
414 println!();
415 output::print_info("Enter a new value, or press Enter to keep the one shown.");
416 println!();
417
418 let mut changed = 0usize;
419 for setting in SETTINGS {
420 let current = (setting.get)(®istry.settings);
421 loop {
422 print!(" {} [{current}]: ", setting.key);
423 io::stdout().flush()?;
424 let mut line = String::new();
425 if io::stdin().read_line(&mut line)? == 0 {
428 println!();
429 break;
430 }
431 let typed = line.trim();
432 if typed.is_empty() {
433 break;
434 }
435 match (setting.set)(&mut registry.settings, typed) {
436 Ok(()) => {
437 changed += 1;
438 break;
439 }
440 Err(e) => output::print_error(&format!("{e}")),
443 }
444 }
445 }
446
447 registry.save()?;
448 mark_reviewed();
449 println!();
450 if changed == 0 {
451 output::print_success("Nothing changed — the defaults are in place.");
452 } else {
453 output::print_success(&format!(
454 "Saved {changed} {}. `devp config show` lists them all.",
455 output::plural(changed, "change", "changes")
456 ));
457 }
458 Ok(())
459}
460
461const REVIEW_MARKER: &str = "config-reviewed";
463
464pub fn config_review_is_due() -> bool {
471 Registry::config_dir()
472 .map(|dir| !dir.join(REVIEW_MARKER).exists())
473 .unwrap_or(false)
474}
475
476fn mark_reviewed() {
477 if let Ok(dir) = Registry::config_dir() {
478 let _ = std::fs::create_dir_all(&dir);
479 let _ = std::fs::write(dir.join(REVIEW_MARKER), crate::constants::VERSION);
480 }
481}
482
483pub fn skip_config_review() {
488 mark_reviewed();
489}
490
491pub fn run_global_update() -> Result<()> {
493 output::print_header("dev-prune Global Configuration Audit & Sync");
494
495 let registry = Registry::load()?;
496 let mut total_audited = 0;
497 let mut errors_found = 0;
498
499 for repo_path in registry.repositories.keys() {
500 total_audited += 1;
501 let clean = output::clean_path(repo_path);
502
503 match PerRepoConfig::load_with_diagnostics(repo_path) {
504 Ok(Some(cfg)) => {
505 if let Err(e) = cfg.save_to_repo(repo_path) {
506 output::print_error(&format!("Failed to write config for {clean}: {e}"));
507 errors_found += 1;
508 } else {
509 output::print_success(&format!("Audited & synced config for {clean}"));
510 }
511 }
512 Ok(None) => {
513 let cfg = PerRepoConfig::default();
514 if let Err(e) = cfg.save_to_repo(repo_path) {
515 output::print_error(&format!("Failed to write config for {clean}: {e}"));
516 errors_found += 1;
517 } else {
518 output::print_success(&format!(
519 "Audited & initialized default config for {clean}"
520 ));
521 }
522 }
523 Err(err_msg) => {
524 errors_found += 1;
525 output::print_error(&format!("Syntax/Schema Error in {clean}:"));
526 for line in err_msg.lines() {
527 eprintln!(" {line}");
528 }
529 output::print_info(&format!(
530 "Hint: fix the syntax by hand, or run `devp config {clean} --update` to \
531 replace the file with a valid default."
532 ));
533 }
534 }
535 }
536
537 if errors_found > 0 {
538 anyhow::bail!(
541 "Audit complete: {total_audited} repos checked, {errors_found} could not be read \
542 or written."
543 );
544 }
545 output::print_success(&format!(
546 "Audit complete: All {total_audited} registered repositories are healthy & synced!"
547 ));
548
549 Ok(())
550}
551
552pub fn run_path_config(path_str: &str, force_update: bool) -> Result<()> {
554 let raw_path = Path::new(path_str);
555
556 let path = if raw_path.exists() {
557 raw_path
558 .canonicalize()
559 .unwrap_or_else(|_| raw_path.to_path_buf())
560 } else {
561 raw_path.to_path_buf()
562 };
563
564 let clean = output::clean_path(&path);
565
566 if !path.exists() {
567 bail!("Path does not exist: {clean}");
568 }
569
570 if !crate::scanner::is_git_repo(&path) {
571 bail!(
573 "`{clean}` is not a Git repository.\n \
574 Run `git init` there first, then `devp config {clean}` again."
575 );
576 }
577
578 let mut registry = Registry::load()?;
579 if !registry.repositories.contains_key(&path) {
580 output::print_info(&format!(
581 "{clean} is not yet registered with dev-prune. Registering now..."
582 ));
583 registry.add_repo(path.clone());
584 registry.save()?;
585 }
586
587 let cfg_file = path.join(crate::constants::PER_REPO_CONFIG_FILE);
588
589 if cfg_file.exists() && !force_update {
590 output::print_header(&format!("dev-prune Per-Repo Config for {clean}"));
591 match PerRepoConfig::load_with_diagnostics(&path) {
592 Ok(cfg) => {
593 let json_str = serde_json::to_string_pretty(&cfg)?;
594 println!("{json_str}");
595 output::print_info("File location: .devprune.json");
596 }
597 Err(err_msg) => {
598 output::print_error(&format!("Invalid configuration in {clean}:"));
599 for line in err_msg.lines() {
600 eprintln!(" {line}");
601 }
602 anyhow::bail!(
605 "Run `devp config {clean} --update` to reset this file back to defaults \
606 (your current overrides in it are discarded)."
607 );
608 }
609 }
610 } else {
611 output::print_info(&format!("Initializing .devprune.json for {clean}..."));
612 let cfg = PerRepoConfig::default();
613 cfg.save_to_repo(&path)?;
614 output::print_success(&format!("Created .devprune.json in {clean}"));
615 }
616
617 Ok(())
618}
619
620fn load_workspace_config_for_write(repo_path: &Path) -> Result<PerRepoConfig> {
626 match PerRepoConfig::load_with_diagnostics(repo_path) {
627 Ok(Some(cfg)) => Ok(cfg),
628 Ok(None) => Ok(PerRepoConfig::default()),
629 Err(e) => bail!(
630 "{e}\n \
631 Fix that file, or run `devp config {} --update` to reset it back to defaults \
632 (your current overrides in it are discarded).",
633 output::clean_path(repo_path)
634 ),
635 }
636}
637
638pub fn run_daemon_toggle(path: Option<&str>, action: &str) -> Result<()> {
640 if let Some(p) = path {
641 let repo_path = resolve_workspace(p)?;
642 let mut cfg = load_workspace_config_for_write(&repo_path)?;
643 match parse_toggle(action)? {
644 Toggle::Enable => {
645 cfg.disable_daemon = false;
646 cfg.save_to_repo(&repo_path)?;
647 output::print_success(&format!(
648 "Enabled background daemon for workspace: {}",
649 output::clean_path(&repo_path)
650 ));
651 }
652 Toggle::Disable => {
653 cfg.disable_daemon = true;
654 cfg.save_to_repo(&repo_path)?;
655 output::print_success(&format!(
656 "Disabled background daemon for workspace: {}",
657 output::clean_path(&repo_path)
658 ));
659 }
660 Toggle::Status => {
661 let st = if cfg.disable_daemon {
662 "Disabled for workspace"
663 } else {
664 "Enabled for workspace"
665 };
666 output::print_info(&format!(
667 "Daemon Status ({}): {}",
668 output::clean_path(&repo_path),
669 st
670 ));
671 }
672 }
673 } else {
674 match parse_toggle(action)? {
675 Toggle::Enable => crate::commands::daemon::run_install()?,
676 Toggle::Disable => crate::commands::daemon::run_uninstall()?,
677 Toggle::Status => crate::commands::daemon::run_status()?,
678 }
679 }
680 Ok(())
681}
682
683pub fn run_hook_toggle(path: Option<&str>, action: &str, chain: bool) -> Result<()> {
685 if let Some(p) = path {
686 if chain {
687 bail!(
688 "`--chain` changes the single global `core.hooksPath`, so it has no \
689 per-workspace form. Drop the path: `devp hook install --chain`."
690 );
691 }
692 let repo_path = resolve_workspace(p)?;
693 let mut cfg = load_workspace_config_for_write(&repo_path)?;
694 match parse_toggle(action)? {
695 Toggle::Enable => {
696 cfg.disable_hooks = false;
697 cfg.save_to_repo(&repo_path)?;
698 output::print_success(&format!(
699 "Enabled background Git hooks for workspace: {}",
700 output::clean_path(&repo_path)
701 ));
702 }
703 Toggle::Disable => {
704 cfg.disable_hooks = true;
705 cfg.save_to_repo(&repo_path)?;
706 output::print_success(&format!(
707 "Disabled background Git hooks for workspace: {}",
708 output::clean_path(&repo_path)
709 ));
710 }
711 Toggle::Status => {
712 let st = if cfg.disable_hooks {
713 "Disabled for workspace"
714 } else {
715 "Enabled for workspace"
716 };
717 output::print_info(&format!(
718 "Git Hook Status ({}): {}",
719 output::clean_path(&repo_path),
720 st
721 ));
722 }
723 }
724 } else {
725 match parse_toggle(action)? {
726 Toggle::Enable => crate::commands::hook::run_install(chain)?,
727 Toggle::Disable => crate::commands::hook::run_uninstall()?,
728 Toggle::Status => crate::commands::hook::run_status()?,
729 }
730 }
731 Ok(())
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737
738 #[test]
739 fn enable_synonyms_all_resolve_to_enable() {
740 for word in ["enable", "install", "on", "INSTALL", "On"] {
741 assert_eq!(parse_toggle(word).unwrap(), Toggle::Enable, "{word}");
742 }
743 }
744
745 #[test]
746 fn disable_synonyms_all_resolve_to_disable() {
747 for word in ["disable", "uninstall", "remove", "off", "Uninstall"] {
748 assert_eq!(parse_toggle(word).unwrap(), Toggle::Disable, "{word}");
749 }
750 }
751
752 #[test]
753 fn status_is_the_default_and_is_also_spellable() {
754 for word in ["", "status", "show"] {
755 assert_eq!(parse_toggle(word).unwrap(), Toggle::Status, "{word}");
756 }
757 }
758
759 #[test]
760 fn a_typo_is_an_error_rather_than_a_silent_status_report() {
761 let err = parse_toggle("enabel").unwrap_err().to_string();
764 assert!(err.contains("enabel"), "{err}");
765 assert!(err.contains("enable"), "{err}");
766 }
767
768 #[test]
769 fn a_workspace_toggle_refuses_to_write_over_a_broken_config() {
770 let tmp = tempfile::TempDir::new().unwrap();
773 let broken = r#"{ "project_name": "api", "override_idle_days": 90, }"#;
774 std::fs::write(
775 tmp.path().join(crate::constants::PER_REPO_CONFIG_FILE),
776 broken,
777 )
778 .unwrap();
779
780 let err = load_workspace_config_for_write(tmp.path())
781 .unwrap_err()
782 .to_string();
783 assert!(err.contains("Syntax error"), "{err}");
784 assert!(err.contains("--update"), "{err}");
785
786 let on_disk =
788 std::fs::read_to_string(tmp.path().join(crate::constants::PER_REPO_CONFIG_FILE))
789 .unwrap();
790 assert_eq!(on_disk, broken);
791 }
792
793 #[test]
794 fn a_workspace_with_no_config_yet_starts_from_the_defaults() {
795 let tmp = tempfile::TempDir::new().unwrap();
796 assert_eq!(
797 load_workspace_config_for_write(tmp.path()).unwrap(),
798 PerRepoConfig::default()
799 );
800 }
801
802 #[test]
803 fn every_setting_round_trips_through_its_own_getter() {
804 let mut settings = Settings::default();
808 for setting in SETTINGS {
809 let before = (setting.get)(&settings);
810 let probe = match before.as_str() {
811 "true" => "false".to_string(),
812 "false" => "true".to_string(),
813 _ => "7".to_string(),
816 };
817 (setting.set)(&mut settings, &probe)
818 .unwrap_or_else(|e| panic!("{} rejected `{probe}`: {e}", setting.key));
819 assert_eq!(
820 (setting.get)(&settings),
821 probe,
822 "{} reads back a different field than it writes",
823 setting.key
824 );
825 }
826 }
827
828 #[test]
829 fn every_setting_is_documented_and_uniquely_named() {
830 let mut seen = std::collections::HashSet::new();
831 for setting in SETTINGS {
832 assert!(seen.insert(setting.key), "duplicate key {}", setting.key);
833 assert!(!setting.help.is_empty(), "{} has no help", setting.key);
834 assert!(
836 setting.help.ends_with('.'),
837 "{} help should read as a sentence",
838 setting.key
839 );
840 }
841 }
842
843 #[test]
844 fn the_settings_table_covers_every_field_of_settings() {
845 let json = serde_json::to_value(Settings::default()).unwrap();
849 let fields: Vec<String> = json.as_object().unwrap().keys().cloned().collect();
850 for field in fields {
851 assert!(
852 SETTINGS.iter().any(|s| s.key == field),
853 "`{field}` is a setting with no entry in SETTINGS, so `devp config set \
854 {field}` cannot reach it"
855 );
856 }
857 }
858
859 #[test]
860 fn a_rejected_value_leaves_the_previous_one_in_place() {
861 let mut settings = Settings::default();
862 assert!((find_setting("scan_depth").unwrap().set)(&mut settings, "0").is_err());
863 assert_eq!(settings.scan_depth, Settings::default().scan_depth);
864
865 assert!((find_setting("command_timeout_secs").unwrap().set)(&mut settings, "0").is_err());
866 assert!((find_setting("check_interval_days").unwrap().set)(&mut settings, "0").is_err());
867 assert!(
868 (find_setting("update_check_interval_days").unwrap().set)(&mut settings, "0").is_err()
869 );
870 }
871
872 #[test]
873 fn booleans_accept_the_words_people_actually_type() {
874 assert!(parse_bool("k", "yes").unwrap());
875 assert!(parse_bool("k", "ON").unwrap());
876 assert!(!parse_bool("k", "0").unwrap());
877 assert!(parse_bool("k", "maybe").is_err());
878 }
879
880 #[test]
881 fn an_unknown_key_lists_the_ones_that_exist() {
882 let err = match find_setting("idel_days") {
883 Ok(_) => panic!("`idel_days` is not a setting"),
884 Err(e) => e.to_string(),
885 };
886 assert!(err.contains("idle_days"), "{err}");
887 }
888
889 #[test]
890 fn a_path_is_never_mistaken_for_an_action() {
891 assert!(!is_toggle_word("~/Code/my-repo"));
893 assert!(!is_toggle_word("."));
894 assert!(!is_toggle_word(""));
895 assert!(is_toggle_word("install"));
896 }
897}