Skip to main content

ralph_workflow/diagnostics/system/
io.rs

1// diagnostics/system/io.rs — boundary module for system information gathering.
2// File stem is `io` — recognized as boundary module by forbid_io_effects lint.
3
4// System information gathering.
5
6use std::process::Command;
7
8/// System information.
9#[derive(Debug, Clone)]
10pub struct SystemInfo {
11    pub os: String,
12    pub arch: String,
13    pub working_directory: Option<String>,
14    pub shell: Option<String>,
15    pub git_version: Option<String>,
16    pub git_repo: bool,
17    pub git_branch: Option<String>,
18    pub uncommitted_changes: Option<usize>,
19}
20
21impl SystemInfo {
22    #[must_use]
23    pub fn gather() -> Self {
24        Self::gather_with_runner(run_command)
25    }
26
27    pub fn gather_with_runner(runner: impl Fn(&str, &[&str]) -> Option<String>) -> Self {
28        let os = std::env::consts::OS.to_string();
29        let arch = std::env::consts::ARCH.to_string();
30        let working_directory = gather_working_directory();
31        let shell = gather_shell();
32        let git_version = runner("git", &["--version"]);
33        let git_repo = check_git_repo(&runner);
34        let (git_branch, uncommitted_changes) = gather_git_details(&runner, git_repo);
35
36        Self {
37            os,
38            arch,
39            working_directory,
40            shell,
41            git_version,
42            git_repo,
43            git_branch,
44            uncommitted_changes,
45        }
46    }
47}
48
49fn gather_working_directory() -> Option<String> {
50    std::env::current_dir()
51        .ok()
52        .map(|p| p.display().to_string())
53}
54
55fn gather_shell() -> Option<String> {
56    std::env::var("SHELL")
57        .ok()
58        .or_else(|| std::env::var("ComSpec").ok())
59}
60
61fn gather_git_details(
62    runner: &impl Fn(&str, &[&str]) -> Option<String>,
63    git_repo: bool,
64) -> (Option<String>, Option<usize>) {
65    let git_branch = git_repo
66        .then(|| runner("git", &["branch", "--show-current"]))
67        .flatten();
68    let uncommitted_changes = git_repo
69        .then(|| runner("git", &["status", "--porcelain"]).map(|o| o.lines().count()))
70        .flatten();
71    (git_branch, uncommitted_changes)
72}
73
74fn check_git_repo(runner: &impl Fn(&str, &[&str]) -> Option<String>) -> bool {
75    runner("git", &["rev-parse", "--is-inside-work-tree"])
76        .is_some_and(|out| out.trim() == "true")
77}
78
79fn run_command(command: &str, args: &[&str]) -> Option<String> {
80    let output = Command::new(command).args(args).output().ok()?;
81    output
82        .status
83        .success()
84        .then(|| String::from_utf8_lossy(&output.stdout).trim().to_string())
85}