ghr-cli 0.7.8

A fast terminal dashboard for GitHub pull requests, issues, and notifications.
use super::*;

pub(super) fn runtime_info_body(app: &AppState, config: &Config, paths: &Paths) -> String {
    let cwd = std::env::current_dir()
        .map(|path| path.display().to_string())
        .unwrap_or_else(|error| format!("unavailable ({error})"));
    let repo = app
        .current_repo_scope()
        .unwrap_or_else(|| "(none)".to_string());
    let section = app
        .current_section()
        .map(|section| section.title.as_str())
        .unwrap_or("(none)");
    let selected = selected_item_summary(app.current_item());
    let item_count = app
        .sections
        .iter()
        .map(|section| section.items.len())
        .sum::<usize>();

    [
        format!("version: {}", env!("CARGO_PKG_VERSION")),
        format!("pid: {}", std::process::id()),
        format!("ghr memory: {}", process_memory_summary()),
        format!("gh: {}", github_cli_version_summary()),
        String::new(),
        format!("config: {}", paths.config_path.display()),
        format!("db: {}", paths.db_path.display()),
        format!("log: {}", paths.log_path.display()),
        format!("state: {}", paths.state_path.display()),
        String::new(),
        format!("cwd: {cwd}"),
        format!("log_level: {}", config.defaults.log_level),
        format!("view: {}", app.active_view),
        format!("focus: {}", app.focus.as_state_str()),
        format!("details_mode: {}", app.details_mode.as_state_str()),
        format!("section: {section}"),
        format!("repo: {repo}"),
        format!("selected: {selected}"),
        format!(
            "mouse: {}",
            if app.mouse_capture_enabled {
                "tui"
            } else {
                "text selection"
            }
        ),
        format!(
            "sections: {}, cached items: {item_count}",
            app.sections.len()
        ),
        format!("ignored items: {}", app.ignored_items.len()),
        format!("recent items: {}", app.recent_items.len()),
    ]
    .join("\n")
}

fn selected_item_summary(item: Option<&WorkItem>) -> String {
    let Some(item) = item else {
        return "(none)".to_string();
    };
    let number = item
        .number
        .map(|number| format!("#{number}"))
        .unwrap_or_else(|| "-".to_string());
    format!(
        "{} {} {}",
        item_kind_label(item.kind),
        number,
        truncate_inline(&item.title, 72)
    )
}

fn process_memory_summary() -> String {
    let Some((rss_kib, virtual_kib)) = process_memory_kib() else {
        return "unavailable".to_string();
    };

    match (rss_kib, virtual_kib) {
        (Some(rss), Some(virtual_size)) => {
            format!(
                "rss {}, virtual {}",
                format_kib(rss),
                format_kib(virtual_size)
            )
        }
        (Some(rss), None) => format!("rss {}", format_kib(rss)),
        (None, Some(virtual_size)) => format!("virtual {}", format_kib(virtual_size)),
        (None, None) => "unavailable".to_string(),
    }
}

fn process_memory_kib() -> Option<(Option<u64>, Option<u64>)> {
    process_memory_kib_from_proc_status().or_else(process_memory_kib_from_ps)
}

fn process_memory_kib_from_proc_status() -> Option<(Option<u64>, Option<u64>)> {
    let status = std::fs::read_to_string("/proc/self/status").ok()?;
    let rss = proc_status_kib(&status, "VmRSS:");
    let virtual_size = proc_status_kib(&status, "VmSize:");
    (rss.is_some() || virtual_size.is_some()).then_some((rss, virtual_size))
}

fn proc_status_kib(status: &str, label: &str) -> Option<u64> {
    status.lines().find_map(|line| {
        let value = line.strip_prefix(label)?.trim();
        value.split_whitespace().next()?.parse::<u64>().ok()
    })
}

fn process_memory_kib_from_ps() -> Option<(Option<u64>, Option<u64>)> {
    let pid = std::process::id().to_string();
    let output = Command::new("ps")
        .args(["-o", "rss=", "-o", "vsz=", "-p", &pid])
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    let text = String::from_utf8_lossy(&output.stdout);
    let mut values = text
        .split_whitespace()
        .filter_map(|value| value.parse().ok());
    let rss = values.next();
    let virtual_size = values.next();
    (rss.is_some() || virtual_size.is_some()).then_some((rss, virtual_size))
}

fn format_kib(kib: u64) -> String {
    let mib = kib as f64 / 1024.0;
    if mib >= 1024.0 {
        format!("{:.2} GiB", mib / 1024.0)
    } else {
        format!("{mib:.1} MiB")
    }
}

#[cfg(test)]
fn github_cli_version_summary() -> String {
    "not checked in tests".to_string()
}

#[cfg(not(test))]
fn github_cli_version_summary() -> String {
    debug!(command = "gh --version", "gh request started");
    let output = Command::new("gh")
        .env("GH_PROMPT_DISABLED", "1")
        .arg("--version")
        .output();
    match output {
        Ok(output) => {
            debug!(
                command = "gh --version",
                status = %output.status,
                success = output.status.success(),
                stdout_bytes = output.stdout.len(),
                stderr_bytes = output.stderr.len(),
                "gh request finished"
            );
            if output.status.success() {
                String::from_utf8_lossy(&output.stdout)
                    .lines()
                    .next()
                    .map(str::trim)
                    .filter(|line| !line.is_empty())
                    .unwrap_or("installed")
                    .to_string()
            } else {
                error!(
                    command = "gh --version",
                    status = %output.status,
                    message = %gh_version_output_message(&output),
                    stdout_bytes = output.stdout.len(),
                    stderr_bytes = output.stderr.len(),
                    "gh request returned failure"
                );
                let stderr = String::from_utf8_lossy(&output.stderr);
                stderr
                    .lines()
                    .next()
                    .map(str::trim)
                    .filter(|line| !line.is_empty())
                    .unwrap_or("unavailable")
                    .to_string()
            }
        }
        Err(error) => {
            debug!(
                command = "gh --version",
                error = %error,
                "gh request failed to start"
            );
            error!(
                command = "gh --version",
                error = %error,
                "gh request failed to start"
            );
            format!("unavailable ({error})")
        }
    }
}

#[cfg(not(test))]
fn gh_version_output_message(output: &std::process::Output) -> String {
    let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
    let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if stderr.is_empty() { stdout } else { stderr }
}