Skip to main content

jja/
system.rs

1//
2// jja: swiss army knife for chess file formats
3// src/system.rs: External system interface (tty detection, invoke editor, ...)
4//
5// Copyright (c) 2023 Ali Polatel <alip@chesswob.org>
6//
7// SPDX-License-Identifier: GPL-3.0-or-later
8
9use std::{
10    fmt,
11    io::{self, stdin, stdout, Read},
12    sync::atomic::{AtomicBool, AtomicUsize},
13};
14
15use dialoguer::Editor;
16use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
17use is_terminal::IsTerminal;
18use once_cell::sync::Lazy;
19use sha2::{Digest, Sha256};
20
21use crate::{quote::print_quote, tr};
22
23/// An error enum for handling errors that may occur when editing a temporary file.
24#[derive(Debug)]
25pub enum EditTempfileError {
26    /// An error variant indicating that the edited buffer is empty.
27    EmptyBuffer,
28    /// An error variant indicating that the edited contents are identical to the original
29    /// contents.
30    IdenticalContents,
31    /// An error variant indicating that there was an input/output error when editing the file.
32    InputOutputError(io::Error),
33    /// An error variant indicating that there was an error spawning the editor process.
34    EditorError(dialoguer::Error),
35}
36
37impl fmt::Display for EditTempfileError {
38    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
39        match self {
40            EditTempfileError::EmptyBuffer => write!(f, "{}", tr!("Empty buffer after editing")),
41            EditTempfileError::IdenticalContents => {
42                write!(f, "{}", tr!("Contents are identical after editing"))
43            }
44            EditTempfileError::InputOutputError(err) => {
45                write!(
46                    f,
47                    "{}",
48                    tr!("Input output error during edit: {}", format!("{err:?}"))
49                )
50            }
51            EditTempfileError::EditorError(err) => {
52                write!(
53                    f,
54                    "{}",
55                    tr!("Editor error during edit: {}", format!("{err:?}"))
56                )
57            }
58        }
59    }
60}
61
62impl std::error::Error for EditTempfileError {}
63
64/* TTY handling */
65/// A global boolean variable indicating whether stdout is a TTY.
66/// Initialized lazily with `std::io::stdout().is_terminal()`.
67/// The system call will be executed only once, when `STDOUT_IS_TTY` is accessed for the first time.
68pub static STDOUT_IS_TTY: Lazy<bool> = Lazy::new(|| stdout().is_terminal());
69
70/// A global boolean variable indicating whether stdin is a TTY.
71/// Initialized lazily with `std::io::stdin().is_terminal()`.
72/// The system call will be executed only once, when `STDIN_IS_TTY` is accessed for the first time.
73pub static STDIN_IS_TTY: Lazy<bool> = Lazy::new(|| stdin().is_terminal());
74
75/// Progress bar handling
76pub static DISPLAY_PROGRESS: Lazy<AtomicBool> = Lazy::new(|| AtomicBool::new(true));
77
78/// Multithreading
79pub static NPROC: Lazy<AtomicUsize> = Lazy::new(|| AtomicUsize::new(num_cpus::get()));
80
81/// Number of threads as a string
82pub static NPROC_STR: Lazy<String> =
83    Lazy::new(|| NPROC.load(std::sync::atomic::Ordering::SeqCst).to_string());
84
85/// `edit_tempfile` allows the user to edit a temporary file using their system's default text editor.
86/// If standard input is not a TTY, then this function reads the contents from standard input
87/// instead.
88///
89/// # Arguments
90///
91/// * `contents: Option<String>` - The initial content of the temporary file. Pass `Some(contents)`
92/// to set the initial content, or `None` for an empty file.
93/// * `extension: Option<String` - The file extension of the temporary file. Pass `Some(extension)`
94/// to set the file extension, or `None` for the default extension, `.jja-tmp`.
95///
96/// # Returns
97///
98/// * `Result<String, EditTempFileError>` - Returns a `Result` containing a `String` with
99/// the edited content of the temporary file if successful, or an error if there was an issue.
100///
101/// # Errors
102///
103/// This function will return an error if there is an issue creating the temporary file, invoking
104/// the editor, or reading the edited content.
105pub fn edit_tempfile(
106    contents: Option<String>,
107    extension: Option<String>,
108) -> Result<String, EditTempfileError> {
109    if !*STDIN_IS_TTY {
110        let mut buf = String::new();
111        io::stdin()
112            .read_to_string(&mut buf)
113            .map_err(EditTempfileError::InputOutputError)?;
114        return Ok(buf);
115    }
116
117    let mut editor = Editor::new();
118    let editor = editor
119        .require_save(true)
120        .trim_newlines(false)
121        .extension(&extension.unwrap_or(".jja-tmp".to_string()));
122    let contents = contents.unwrap_or_default();
123
124    let buf = if let Some(buf) = editor
125        .edit(&contents)
126        .map_err(EditTempfileError::EditorError)?
127    {
128        buf
129    } else {
130        if *STDOUT_IS_TTY {
131            print_quote(None, false);
132        }
133        return Err(EditTempfileError::EmptyBuffer);
134    };
135
136    let mut hasher = Sha256::new();
137    hasher.update(contents.as_bytes());
138    let hash_orig = hasher.finalize();
139
140    let mut hasher = Sha256::new();
141    hasher.update(buf.as_bytes());
142    let hash_data = hasher.finalize();
143
144    if hash_orig == hash_data {
145        if *STDOUT_IS_TTY {
146            print_quote(None, false);
147        }
148        return Err(EditTempfileError::IdenticalContents);
149    }
150
151    Ok(buf)
152}
153
154#[cfg(unix)]
155/// Gets the current user's username on Unix systems.
156///
157/// # Errors
158/// Returns an error if the user information cannot be retrieved.
159pub fn get_username() -> Result<String, Box<dyn std::error::Error>> {
160    use nix::unistd::{getuid, User};
161
162    let user = User::from_uid(getuid())?;
163    let username = match user {
164        Some(u) => {
165            let gecos_str = u.gecos.to_string_lossy().into_owned();
166            let name = gecos_str.split(',').next().unwrap_or_default();
167            if name.is_empty() {
168                u.name
169            } else {
170                name.to_owned()
171            }
172        }
173        None => return Err(tr!("Failed to get user information").into()),
174    };
175
176    Ok(username)
177}
178
179#[cfg(windows)]
180/// Gets the current user's username on Windows systems.
181///
182/// # Errors
183/// Returns an error if the user information cannot be retrieved.
184pub fn get_username() -> Result<String, Box<dyn std::error::Error>> {
185    use username::get_user_name;
186    get_user_name().map_err(|e| e.into())
187}
188
189/// Creates and returns a progress bar with a specified size.
190pub fn get_progress_bar(size: u64) -> ProgressBar {
191    if !DISPLAY_PROGRESS.load(std::sync::atomic::Ordering::SeqCst) {
192        return ProgressBar::hidden();
193    }
194
195    let st = ProgressStyle::default_bar()
196        .template(concat!(
197            "{msg:<15.bold.magenta}",
198            " [{pos:.bold.yellow}/{len:.bold.green}]",
199            " {per_sec:.bold.green}",
200            " [{wide_bar:.cyan/blue}]",
201            " [{elapsed_precise:.yellow}] ETA {eta_precise}"
202        ))
203        .expect(&tr!("Error in progress bar template, please report a bug!"))
204        .progress_chars("=>-");
205
206    let pb = ProgressBar::new(size);
207    pb.set_style(st);
208    pb.set_draw_target(ProgressDrawTarget::stderr_with_hz(1));
209
210    pb
211}
212
213/// Creates and returns a progress spinner.
214pub fn get_progress_spinner() -> ProgressBar {
215    if !DISPLAY_PROGRESS.load(std::sync::atomic::Ordering::SeqCst) {
216        return ProgressBar::hidden();
217    }
218
219    let st = ProgressStyle::default_spinner()
220        .template(concat!(
221            "{msg:<15.bold.magenta}",
222            " [{pos:.bold.yellow}/?]",
223            " {per_sec:.bold.green}",
224            " [{elapsed_precise:.yellow}]"
225        ))
226        .expect(&tr!(
227            "Error in progress spinner template, please report a bug!"
228        ))
229        .tick_chars("-\\|/x");
230
231    let pb = ProgressBar::new_spinner();
232    pb.set_style(st);
233    pb.set_draw_target(ProgressDrawTarget::stderr_with_hz(1));
234
235    pb
236}