jja 0.9.1

swiss army knife for chess file formats
Documentation
//
// jja: swiss army knife for chess file formats
// src/system.rs: External system interface (tty detection, invoke editor, ...)
//
// Copyright (c) 2023 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0-or-later

use std::{
    fmt,
    io::{self, stdin, stdout, Read},
    sync::atomic::{AtomicBool, AtomicUsize},
};

use dialoguer::Editor;
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use is_terminal::IsTerminal;
use once_cell::sync::Lazy;
use sha2::{Digest, Sha256};

use crate::{quote::print_quote, tr};

/// An error enum for handling errors that may occur when editing a temporary file.
#[derive(Debug)]
pub enum EditTempfileError {
    /// An error variant indicating that the edited buffer is empty.
    EmptyBuffer,
    /// An error variant indicating that the edited contents are identical to the original
    /// contents.
    IdenticalContents,
    /// An error variant indicating that there was an input/output error when editing the file.
    InputOutputError(io::Error),
    /// An error variant indicating that there was an error spawning the editor process.
    EditorError(dialoguer::Error),
}

impl fmt::Display for EditTempfileError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            EditTempfileError::EmptyBuffer => write!(f, "{}", tr!("Empty buffer after editing")),
            EditTempfileError::IdenticalContents => {
                write!(f, "{}", tr!("Contents are identical after editing"))
            }
            EditTempfileError::InputOutputError(err) => {
                write!(
                    f,
                    "{}",
                    tr!("Input output error during edit: {}", format!("{err:?}"))
                )
            }
            EditTempfileError::EditorError(err) => {
                write!(
                    f,
                    "{}",
                    tr!("Editor error during edit: {}", format!("{err:?}"))
                )
            }
        }
    }
}

impl std::error::Error for EditTempfileError {}

/* TTY handling */
/// A global boolean variable indicating whether stdout is a TTY.
/// Initialized lazily with `std::io::stdout().is_terminal()`.
/// The system call will be executed only once, when `STDOUT_IS_TTY` is accessed for the first time.
pub static STDOUT_IS_TTY: Lazy<bool> = Lazy::new(|| stdout().is_terminal());

/// A global boolean variable indicating whether stdin is a TTY.
/// Initialized lazily with `std::io::stdin().is_terminal()`.
/// The system call will be executed only once, when `STDIN_IS_TTY` is accessed for the first time.
pub static STDIN_IS_TTY: Lazy<bool> = Lazy::new(|| stdin().is_terminal());

/// Progress bar handling
pub static DISPLAY_PROGRESS: Lazy<AtomicBool> = Lazy::new(|| AtomicBool::new(true));

/// Multithreading
pub static NPROC: Lazy<AtomicUsize> = Lazy::new(|| AtomicUsize::new(num_cpus::get()));

/// Number of threads as a string
pub static NPROC_STR: Lazy<String> =
    Lazy::new(|| NPROC.load(std::sync::atomic::Ordering::SeqCst).to_string());

/// `edit_tempfile` allows the user to edit a temporary file using their system's default text editor.
/// If standard input is not a TTY, then this function reads the contents from standard input
/// instead.
///
/// # Arguments
///
/// * `contents: Option<String>` - The initial content of the temporary file. Pass `Some(contents)`
/// to set the initial content, or `None` for an empty file.
/// * `extension: Option<String` - The file extension of the temporary file. Pass `Some(extension)`
/// to set the file extension, or `None` for the default extension, `.jja-tmp`.
///
/// # Returns
///
/// * `Result<String, EditTempFileError>` - Returns a `Result` containing a `String` with
/// the edited content of the temporary file if successful, or an error if there was an issue.
///
/// # Errors
///
/// This function will return an error if there is an issue creating the temporary file, invoking
/// the editor, or reading the edited content.
pub fn edit_tempfile(
    contents: Option<String>,
    extension: Option<String>,
) -> Result<String, EditTempfileError> {
    if !*STDIN_IS_TTY {
        let mut buf = String::new();
        io::stdin()
            .read_to_string(&mut buf)
            .map_err(EditTempfileError::InputOutputError)?;
        return Ok(buf);
    }

    let mut editor = Editor::new();
    let editor = editor
        .require_save(true)
        .trim_newlines(false)
        .extension(&extension.unwrap_or(".jja-tmp".to_string()));
    let contents = contents.unwrap_or_default();

    let buf = if let Some(buf) = editor
        .edit(&contents)
        .map_err(EditTempfileError::EditorError)?
    {
        buf
    } else {
        if *STDOUT_IS_TTY {
            print_quote(None, false);
        }
        return Err(EditTempfileError::EmptyBuffer);
    };

    let mut hasher = Sha256::new();
    hasher.update(contents.as_bytes());
    let hash_orig = hasher.finalize();

    let mut hasher = Sha256::new();
    hasher.update(buf.as_bytes());
    let hash_data = hasher.finalize();

    if hash_orig == hash_data {
        if *STDOUT_IS_TTY {
            print_quote(None, false);
        }
        return Err(EditTempfileError::IdenticalContents);
    }

    Ok(buf)
}

#[cfg(unix)]
/// Gets the current user's username on Unix systems.
///
/// # Errors
/// Returns an error if the user information cannot be retrieved.
pub fn get_username() -> Result<String, Box<dyn std::error::Error>> {
    use nix::unistd::{getuid, User};

    let user = User::from_uid(getuid())?;
    let username = match user {
        Some(u) => {
            let gecos_str = u.gecos.to_string_lossy().into_owned();
            let name = gecos_str.split(',').next().unwrap_or_default();
            if name.is_empty() {
                u.name
            } else {
                name.to_owned()
            }
        }
        None => return Err(tr!("Failed to get user information").into()),
    };

    Ok(username)
}

#[cfg(windows)]
/// Gets the current user's username on Windows systems.
///
/// # Errors
/// Returns an error if the user information cannot be retrieved.
pub fn get_username() -> Result<String, Box<dyn std::error::Error>> {
    use username::get_user_name;
    get_user_name().map_err(|e| e.into())
}

/// Creates and returns a progress bar with a specified size.
pub fn get_progress_bar(size: u64) -> ProgressBar {
    if !DISPLAY_PROGRESS.load(std::sync::atomic::Ordering::SeqCst) {
        return ProgressBar::hidden();
    }

    let st = ProgressStyle::default_bar()
        .template(concat!(
            "{msg:<15.bold.magenta}",
            " [{pos:.bold.yellow}/{len:.bold.green}]",
            " {per_sec:.bold.green}",
            " [{wide_bar:.cyan/blue}]",
            " [{elapsed_precise:.yellow}] ETA {eta_precise}"
        ))
        .expect(&tr!("Error in progress bar template, please report a bug!"))
        .progress_chars("=>-");

    let pb = ProgressBar::new(size);
    pb.set_style(st);
    pb.set_draw_target(ProgressDrawTarget::stderr_with_hz(1));

    pb
}

/// Creates and returns a progress spinner.
pub fn get_progress_spinner() -> ProgressBar {
    if !DISPLAY_PROGRESS.load(std::sync::atomic::Ordering::SeqCst) {
        return ProgressBar::hidden();
    }

    let st = ProgressStyle::default_spinner()
        .template(concat!(
            "{msg:<15.bold.magenta}",
            " [{pos:.bold.yellow}/?]",
            " {per_sec:.bold.green}",
            " [{elapsed_precise:.yellow}]"
        ))
        .expect(&tr!(
            "Error in progress spinner template, please report a bug!"
        ))
        .tick_chars("-\\|/x");

    let pb = ProgressBar::new_spinner();
    pb.set_style(st);
    pb.set_draw_target(ProgressDrawTarget::stderr_with_hz(1));

    pb
}