748 lines
21 KiB
Rust
748 lines
21 KiB
Rust
#[cfg(all(feature = "clipboard", not(target_os = "linux")))]
|
|
extern crate clipboard;
|
|
extern crate colored;
|
|
extern crate directories;
|
|
extern crate fs2;
|
|
extern crate open;
|
|
|
|
use std::borrow::Borrow;
|
|
use std::env::{current_exe, var_os};
|
|
use std::ffi::OsStr;
|
|
use std::fmt::{Debug, Display};
|
|
#[cfg(feature = "clipboard")]
|
|
use std::io::ErrorKind as IoErrorKind;
|
|
use std::io::{stderr, stdin, Error as IoError, Write};
|
|
use std::path::Path;
|
|
#[cfg(feature = "history")]
|
|
use std::path::PathBuf;
|
|
use std::process::{exit, ExitStatus};
|
|
#[cfg(all(feature = "clipboard", target_os = "linux"))]
|
|
use std::process::{Command, Stdio};
|
|
|
|
#[cfg(all(feature = "clipboard", not(target_os = "linux")))]
|
|
use self::clipboard::{ClipboardContext, ClipboardProvider};
|
|
use self::colored::*;
|
|
#[cfg(feature = "history")]
|
|
use self::directories::ProjectDirs;
|
|
use self::fs2::available_space;
|
|
use chrono::Duration;
|
|
use failure::{err_msg, Fail};
|
|
#[cfg(all(feature = "clipboard", not(target_os = "linux")))]
|
|
use failure::{Compat, Error};
|
|
use ffsend_api::url::Url;
|
|
use rpassword::prompt_password_stderr;
|
|
|
|
use cmd::matcher::MainMatcher;
|
|
|
|
/// Print a success message.
|
|
pub fn print_success(msg: &str) {
|
|
eprintln!("{}", msg.green());
|
|
}
|
|
|
|
/// Print the given error in a proper format for the user,
|
|
/// with it's causes.
|
|
pub fn print_error<E: Fail>(err: impl Borrow<E>) {
|
|
// Report each printable error, count them
|
|
let count = err
|
|
.borrow()
|
|
.causes()
|
|
.map(|err| format!("{}", err))
|
|
.filter(|err| !err.is_empty())
|
|
.enumerate()
|
|
.map(|(i, err)| {
|
|
if i == 0 {
|
|
eprintln!("{} {}", highlight_error("error:"), err);
|
|
} else {
|
|
eprintln!("{} {}", highlight_error("caused by:"), err);
|
|
}
|
|
})
|
|
.count();
|
|
|
|
// Fall back to a basic message
|
|
if count == 0 {
|
|
eprintln!(
|
|
"{} {}",
|
|
highlight_error("error:"),
|
|
"an undefined error occurred"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Print the given error message in a proper format for the user,
|
|
/// with it's causes.
|
|
pub fn print_error_msg<S>(err: S)
|
|
where
|
|
S: AsRef<str> + Display + Debug + Sync + Send + 'static,
|
|
{
|
|
print_error(err_msg(err).compat());
|
|
}
|
|
|
|
/// Print a warning.
|
|
#[cfg(feature = "history")]
|
|
pub fn print_warning<S>(err: S)
|
|
where
|
|
S: AsRef<str> + Display + Debug + Sync + Send + 'static,
|
|
{
|
|
eprintln!("{} {}", highlight_warning("warning:"), err);
|
|
}
|
|
|
|
/// Quit the application regularly.
|
|
pub fn quit() -> ! {
|
|
exit(0);
|
|
}
|
|
|
|
/// Quit the application with an error code,
|
|
/// and print the given error.
|
|
pub fn quit_error<E: Fail>(err: E, hints: impl Borrow<ErrorHints>) -> ! {
|
|
// Print the error
|
|
print_error(err);
|
|
|
|
// Print error hints
|
|
hints.borrow().print();
|
|
|
|
// Quit
|
|
exit(1);
|
|
}
|
|
|
|
/// Quit the application with an error code,
|
|
/// and print the given error message.
|
|
pub fn quit_error_msg<S>(err: S, hints: impl Borrow<ErrorHints>) -> !
|
|
where
|
|
S: AsRef<str> + Display + Debug + Sync + Send + 'static,
|
|
{
|
|
quit_error(err_msg(err).compat(), hints);
|
|
}
|
|
|
|
/// The error hint configuration.
|
|
#[derive(Clone, Builder)]
|
|
#[builder(default)]
|
|
pub struct ErrorHints {
|
|
/// A list of info messages to print along with the error.
|
|
info: Vec<String>,
|
|
|
|
/// Show about the password option.
|
|
password: bool,
|
|
|
|
/// Show about the owner option.
|
|
owner: bool,
|
|
|
|
/// Show about the history flag.
|
|
#[cfg(feature = "history")]
|
|
history: bool,
|
|
|
|
/// Show about the force flag.
|
|
force: bool,
|
|
|
|
/// Show about the verbose flag.
|
|
verbose: bool,
|
|
|
|
/// Show about the help flag.
|
|
help: bool,
|
|
}
|
|
|
|
impl ErrorHints {
|
|
/// Check whether any hint should be printed.
|
|
pub fn any(&self) -> bool {
|
|
// Determine the result
|
|
#[allow(unused_mut)]
|
|
let mut result = self.password || self.owner || self.force || self.verbose || self.help;
|
|
|
|
// Factor in the history hint when enabled
|
|
#[cfg(feature = "history")]
|
|
{
|
|
result = result || self.history;
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// Print the error hints.
|
|
pub fn print(&self) {
|
|
// Print info messages
|
|
for msg in &self.info {
|
|
eprintln!("{} {}", highlight_info("info:"), msg);
|
|
}
|
|
|
|
// Stop if nothing should be printed
|
|
if !self.any() {
|
|
return;
|
|
}
|
|
|
|
eprint!("\n");
|
|
|
|
// Print hints
|
|
if self.password {
|
|
eprintln!(
|
|
"Use '{}' to specify a password",
|
|
highlight("--password <PASSWORD>")
|
|
);
|
|
}
|
|
if self.owner {
|
|
eprintln!(
|
|
"Use '{}' to specify an owner token",
|
|
highlight("--owner <TOKEN>")
|
|
);
|
|
}
|
|
#[cfg(feature = "history")]
|
|
{
|
|
if self.history {
|
|
eprintln!(
|
|
"Use '{}' to specify a history file",
|
|
highlight("--history <FILE>")
|
|
);
|
|
}
|
|
}
|
|
if self.force {
|
|
eprintln!("Use '{}' to force", highlight("--force"));
|
|
}
|
|
if self.verbose {
|
|
eprintln!("For detailed errors try '{}'", highlight("--verbose"));
|
|
}
|
|
if self.help {
|
|
eprintln!("For more information try '{}'", highlight("--help"));
|
|
}
|
|
|
|
// Flush
|
|
let _ = stderr().flush();
|
|
}
|
|
}
|
|
|
|
impl Default for ErrorHints {
|
|
fn default() -> Self {
|
|
ErrorHints {
|
|
info: Vec::new(),
|
|
password: false,
|
|
owner: false,
|
|
#[cfg(feature = "history")]
|
|
history: false,
|
|
force: false,
|
|
verbose: true,
|
|
help: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ErrorHintsBuilder {
|
|
/// Add a single info entry.
|
|
pub fn add_info(mut self, info: String) -> Self {
|
|
// Initialize the info list
|
|
if self.info.is_none() {
|
|
self.info = Some(Vec::new());
|
|
}
|
|
|
|
// Add the item to the info list
|
|
if let Some(ref mut list) = self.info {
|
|
list.push(info);
|
|
}
|
|
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Highlight the given text with a color.
|
|
pub fn highlight(msg: &str) -> ColoredString {
|
|
msg.yellow()
|
|
}
|
|
|
|
/// Highlight the given text with an error color.
|
|
pub fn highlight_error(msg: &str) -> ColoredString {
|
|
msg.red().bold()
|
|
}
|
|
|
|
/// Highlight the given text with an warning color.
|
|
#[cfg(feature = "history")]
|
|
pub fn highlight_warning(msg: &str) -> ColoredString {
|
|
highlight(msg).bold()
|
|
}
|
|
|
|
/// Highlight the given text with an info color
|
|
pub fn highlight_info(msg: &str) -> ColoredString {
|
|
msg.cyan()
|
|
}
|
|
|
|
/// Open the given URL in the users default browser.
|
|
/// The browsers exit statis is returned.
|
|
pub fn open_url(url: impl Borrow<Url>) -> Result<ExitStatus, IoError> {
|
|
open_path(url.borrow().as_str())
|
|
}
|
|
|
|
/// Open the given path or URL using the program configured on the system.
|
|
/// The program exit statis is returned.
|
|
pub fn open_path(path: &str) -> Result<ExitStatus, IoError> {
|
|
open::that(path)
|
|
}
|
|
|
|
/// Set the clipboard of the user to the given `content` string.
|
|
#[cfg(feature = "clipboard")]
|
|
pub fn set_clipboard(content: String) -> Result<(), ClipboardError> {
|
|
#[cfg(not(target_os = "linux"))]
|
|
{
|
|
ClipboardProvider::new()
|
|
.and_then(|mut context: ClipboardContext| context.set_contents(content))
|
|
.map_err(|err| format_err!("{}", err).compat())
|
|
.map_err(ClipboardError::Generic)
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
// Open an xclip process
|
|
let mut process = match Command::new("xclip")
|
|
.arg("-sel")
|
|
.arg("clip")
|
|
.stdin(Stdio::piped())
|
|
.spawn()
|
|
{
|
|
Ok(process) => process,
|
|
Err(err) => {
|
|
return Err(match err.kind() {
|
|
IoErrorKind::NotFound => ClipboardError::NoXclip,
|
|
_ => ClipboardError::Xclip(err),
|
|
})
|
|
}
|
|
};
|
|
|
|
// Write the contents to the xclip process
|
|
process
|
|
.stdin
|
|
.as_mut()
|
|
.unwrap()
|
|
.write_all(content.as_bytes())
|
|
.map_err(ClipboardError::Xclip)?;
|
|
|
|
// Wait for xclip to exit
|
|
let status = process.wait().map_err(ClipboardError::Xclip)?;
|
|
if !status.success() {
|
|
return Err(ClipboardError::XclipStatus(status.code().unwrap_or(0)));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "clipboard")]
|
|
#[derive(Debug, Fail)]
|
|
pub enum ClipboardError {
|
|
/// A generic error occurred while setting the clipboard contents
|
|
#[cfg(not(target_os = "linux"))]
|
|
#[fail(display = "failed to access clipboard")]
|
|
Generic(#[cause] Compat<Error>),
|
|
|
|
/// Xclip is not installed on the system, which is required for clipboard support.
|
|
#[cfg(target_os = "linux")]
|
|
#[fail(display = "failed to access clipboard, xclip is not installed")]
|
|
NoXclip,
|
|
|
|
/// An error occurred while using xclip to set the clipboard contents.
|
|
/// This problem probably occurred when stargin the xclip process, or while piping the
|
|
/// clipboard contents to the process.
|
|
#[cfg(target_os = "linux")]
|
|
#[fail(display = "failed to access clipboard using xclip")]
|
|
Xclip(#[cause] IoError),
|
|
|
|
/// Xclip unexpectetly exited with a non-successful status code.
|
|
#[cfg(target_os = "linux")]
|
|
#[fail(
|
|
display = "failed to use clipboard, xclip exited with status code {}",
|
|
_0
|
|
)]
|
|
XclipStatus(i32),
|
|
}
|
|
|
|
/// Check for an emtpy password in the given `password`.
|
|
/// If the password is emtpy the program will quit with an error unless
|
|
/// forced.
|
|
// TODO: move this to a better module
|
|
pub fn check_empty_password(password: &str, matcher_main: &MainMatcher) {
|
|
if !matcher_main.force() && password.is_empty() {
|
|
quit_error_msg(
|
|
"an empty password is not supported by the web interface",
|
|
ErrorHintsBuilder::default()
|
|
.force(true)
|
|
.verbose(false)
|
|
.build()
|
|
.unwrap(),
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Prompt the user to enter a password.
|
|
///
|
|
/// If `empty` is `false`, emtpy passwords aren't allowed unless forced.
|
|
pub fn prompt_password(main_matcher: &MainMatcher) -> String {
|
|
// Quit with an error if we may not interact
|
|
if main_matcher.no_interact() {
|
|
quit_error_msg(
|
|
"missing password, must be specified in no-interact mode",
|
|
ErrorHintsBuilder::default()
|
|
.password(true)
|
|
.verbose(false)
|
|
.build()
|
|
.unwrap(),
|
|
);
|
|
}
|
|
|
|
// Prompt for the password
|
|
match prompt_password_stderr("Password: ") {
|
|
Ok(password) => password,
|
|
Err(err) => quit_error(
|
|
err.context("failed to read password from password prompt"),
|
|
ErrorHints::default(),
|
|
),
|
|
}
|
|
}
|
|
|
|
/// Get a password if required.
|
|
/// This method will ensure a password is set (or not) in the given `password`
|
|
/// parameter, as defined by `needs`.
|
|
///
|
|
/// This method will prompt the user for a password, if one is required but
|
|
/// wasn't set. An ignore message will be shown if it was not required while it
|
|
/// was set.
|
|
pub fn ensure_password(password: &mut Option<String>, needs: bool, main_matcher: &MainMatcher) {
|
|
// Return if we're fine
|
|
if password.is_some() == needs {
|
|
return;
|
|
}
|
|
|
|
// Prompt for the password, or clear it if not required
|
|
if needs {
|
|
println!("This file is protected with a password.");
|
|
*password = Some(prompt_password(main_matcher));
|
|
} else {
|
|
println!("Ignoring password, it is not required");
|
|
*password = None;
|
|
}
|
|
}
|
|
|
|
/// Prompt the user to enter some value.
|
|
/// The prompt that is shown should be passed to `msg`,
|
|
/// excluding the `:` suffix.
|
|
pub fn prompt(msg: &str, main_matcher: &MainMatcher) -> String {
|
|
// Quit with an error if we may not interact
|
|
if main_matcher.no_interact() {
|
|
quit_error_msg(
|
|
format!(
|
|
"could not prompt for '{}' in no-interact mode, maybe specify it",
|
|
msg,
|
|
),
|
|
ErrorHints::default(),
|
|
);
|
|
}
|
|
|
|
// Show the prompt
|
|
eprint!("{}: ", msg);
|
|
let _ = stderr().flush();
|
|
|
|
// Get the input
|
|
let mut input = String::new();
|
|
if let Err(err) = stdin().read_line(&mut input) {
|
|
quit_error(
|
|
err.context("failed to read input from prompt"),
|
|
ErrorHints::default(),
|
|
);
|
|
}
|
|
|
|
// Trim and return
|
|
input.trim().to_owned()
|
|
}
|
|
|
|
/// Prompt the user for a question, allowing a yes or now answer.
|
|
/// True is returned if yes was answered, false if no.
|
|
///
|
|
/// A default may be given, which is chosen if no-interact mode is
|
|
/// enabled, or if enter was pressed by the user without entering anything.
|
|
pub fn prompt_yes(msg: &str, def: Option<bool>, main_matcher: &MainMatcher) -> bool {
|
|
// Define the available options string
|
|
let options = format!(
|
|
"[{}/{}]",
|
|
match def {
|
|
Some(def) if def => "Y",
|
|
_ => "y",
|
|
},
|
|
match def {
|
|
Some(def) if !def => "N",
|
|
_ => "n",
|
|
}
|
|
);
|
|
|
|
// Assume yes
|
|
if main_matcher.assume_yes() {
|
|
eprintln!("{} {}: yes", msg, options);
|
|
return true;
|
|
}
|
|
|
|
// Autoselect if in no-interact mode
|
|
if main_matcher.no_interact() {
|
|
if let Some(def) = def {
|
|
eprintln!("{} {}: {}", msg, options, if def { "yes" } else { "no" });
|
|
return def;
|
|
} else {
|
|
quit_error_msg(
|
|
format!(
|
|
"could not prompt question '{}' in no-interact mode, maybe specify it",
|
|
msg,
|
|
),
|
|
ErrorHints::default(),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Get the user input
|
|
let answer = prompt(&format!("{} {}", msg, options), main_matcher);
|
|
|
|
// Assume the default if the answer is empty
|
|
if answer.is_empty() && def.is_some() {
|
|
return def.unwrap();
|
|
}
|
|
|
|
// Derive a boolean and return
|
|
match derive_bool(&answer) {
|
|
Some(answer) => answer,
|
|
None => prompt_yes(msg, def, main_matcher),
|
|
}
|
|
}
|
|
|
|
/// Try to derive true or false (yes or no) from the given input.
|
|
/// None is returned if no boolean could be derived accurately.
|
|
fn derive_bool(input: &str) -> Option<bool> {
|
|
// Process the input
|
|
let input = input.trim().to_lowercase();
|
|
|
|
// Handle short or incomplete answers
|
|
match input.as_str() {
|
|
"y" | "ye" | "t" | "1" => return Some(true),
|
|
"n" | "f" | "0" => return Some(false),
|
|
_ => {}
|
|
}
|
|
|
|
// Handle complete answers with any suffix
|
|
if input.starts_with("yes") || input.starts_with("true") {
|
|
return Some(true);
|
|
}
|
|
if input.starts_with("no") || input.starts_with("false") {
|
|
return Some(false);
|
|
}
|
|
|
|
// The answer could not be determined, return none
|
|
None
|
|
}
|
|
|
|
/// Prompt the user to enter an owner token.
|
|
pub fn prompt_owner_token(main_matcher: &MainMatcher) -> String {
|
|
prompt("Owner token", main_matcher)
|
|
}
|
|
|
|
/// Get the owner token.
|
|
/// This method will ensure an owner token is set in the given `token`
|
|
/// parameter.
|
|
///
|
|
/// This method will prompt the user for the token, if it wasn't set.
|
|
pub fn ensure_owner_token(token: &mut Option<String>, main_matcher: &MainMatcher) {
|
|
// Check whehter we allow interaction
|
|
let interact = !main_matcher.no_interact();
|
|
|
|
// Notify that an owner token is required
|
|
if interact && token.is_none() {
|
|
println!("The file owner token is required for authentication.");
|
|
}
|
|
|
|
loop {
|
|
// Prompt for an owner token
|
|
if token.is_none() {
|
|
if interact {
|
|
*token = Some(prompt_owner_token(main_matcher));
|
|
} else {
|
|
quit_error_msg(
|
|
"missing owner token, must be specified in no-interact mode",
|
|
ErrorHintsBuilder::default()
|
|
.owner(true)
|
|
.verbose(false)
|
|
.build()
|
|
.unwrap(),
|
|
);
|
|
}
|
|
}
|
|
|
|
// The token must not be empty
|
|
if token.as_ref().unwrap().is_empty() {
|
|
eprintln!(
|
|
"Empty owner token given, which is invalid. Use {} to cancel.",
|
|
highlight("[CTRL+C]"),
|
|
);
|
|
*token = None;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Format the given number of bytes readable for humans.
|
|
pub fn format_bytes(bytes: u64) -> String {
|
|
let bytes = bytes as f64;
|
|
let kb = 1024f64;
|
|
match bytes {
|
|
bytes if bytes >= kb.powf(4_f64) => format!("{:.*} TiB", 2, bytes / kb.powf(4_f64)),
|
|
bytes if bytes >= kb.powf(3_f64) => format!("{:.*} GiB", 2, bytes / kb.powf(3_f64)),
|
|
bytes if bytes >= kb.powf(2_f64) => format!("{:.*} MiB", 2, bytes / kb.powf(2_f64)),
|
|
bytes if bytes >= kb => format!("{:.*} KiB", 2, bytes / kb),
|
|
_ => format!("{:.*} B", 0, bytes),
|
|
}
|
|
}
|
|
|
|
/// Format the given duration in a human readable format.
|
|
/// This method builds a string of time components to represent
|
|
/// the given duration.
|
|
///
|
|
/// The following time units are used:
|
|
/// - `w`: weeks
|
|
/// - `d`: days
|
|
/// - `h`: hours
|
|
/// - `m`: minutes
|
|
/// - `s`: seconds
|
|
///
|
|
/// Only the two most significant units are returned.
|
|
/// If the duration is zero seconds or less `now` is returned.
|
|
///
|
|
/// The following time strings may be produced:
|
|
/// - `8w6d`
|
|
/// - `23h14m`
|
|
/// - `9m55s`
|
|
/// - `1s`
|
|
/// - `now`
|
|
pub fn format_duration(duration: impl Borrow<Duration>) -> String {
|
|
// Get the total number of seconds, return immediately if zero or less
|
|
let mut secs = duration.borrow().num_seconds();
|
|
if secs <= 0 {
|
|
return "now".into();
|
|
}
|
|
|
|
// Build a list of time units, define a list for time components
|
|
let mut components = Vec::new();
|
|
let units = [
|
|
(60 * 60 * 24 * 7, "w"),
|
|
(60 * 60 * 24, "d"),
|
|
(60 * 60, "h"),
|
|
(60, "m"),
|
|
(1, "s"),
|
|
];
|
|
|
|
// Fill the list of time components based on the units which fit
|
|
for unit in &units {
|
|
if secs >= unit.0 {
|
|
components.push(format!("{}{}", secs / unit.0, unit.1));
|
|
secs %= unit.0;
|
|
}
|
|
}
|
|
|
|
// Show only the two most significant components and join them in a string
|
|
components.truncate(2);
|
|
components.join("")
|
|
}
|
|
|
|
/// Format the given boolean, as `yes` or `no`.
|
|
pub fn format_bool(b: bool) -> &'static str {
|
|
if b {
|
|
"yes"
|
|
} else {
|
|
"no"
|
|
}
|
|
}
|
|
|
|
/// Get the name of the executable that was invoked.
|
|
pub fn bin_name() -> String {
|
|
current_exe()
|
|
.ok()
|
|
.and_then(|p| p.file_name().map(|n| n.to_owned()))
|
|
.and_then(|n| n.into_string().ok())
|
|
.unwrap_or_else(|| crate_name!().into())
|
|
}
|
|
|
|
/// Ensure that there is enough free disk space available at the given `path`,
|
|
/// to store a file with the given `size`.
|
|
///
|
|
/// If an error occurred while querying the file system,
|
|
/// the error is reported to the user and the method returns.
|
|
///
|
|
/// If there is not enough disk space available,
|
|
/// an error is reported and the program will quit.
|
|
pub fn ensure_enough_space<P: AsRef<Path>>(path: P, size: u64) {
|
|
// Get the available space at this path
|
|
let space = match available_space(path) {
|
|
Ok(space) => space,
|
|
Err(err) => {
|
|
print_error(err.context("failed to check available space on disk, ignoring"));
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Return if enough disk space is avaiable
|
|
if space >= size {
|
|
return;
|
|
}
|
|
|
|
// Create an info message giving details about the required space
|
|
let info = format!(
|
|
"{} of space required, but only {} is available",
|
|
format_bytes(size),
|
|
format_bytes(space),
|
|
);
|
|
|
|
// Print an descriptive error and quit
|
|
quit_error(
|
|
err_msg("not enough disk space available in the target directory")
|
|
.context("failed to download file"),
|
|
ErrorHintsBuilder::default()
|
|
.add_info(info)
|
|
.force(true)
|
|
.verbose(false)
|
|
.build()
|
|
.unwrap(),
|
|
);
|
|
}
|
|
|
|
/// Get the project directories instance for this application.
|
|
/// This may be used to determine the project, cache, configuration, data and
|
|
/// some other directory paths.
|
|
#[cfg(feature = "history")]
|
|
pub fn app_project_dirs() -> ProjectDirs {
|
|
ProjectDirs::from("", "", crate_name!())
|
|
.expect("failed to determine location of project directories")
|
|
}
|
|
|
|
/// Get the default path to use for the history file.
|
|
#[cfg(feature = "history")]
|
|
pub fn app_history_file_path() -> PathBuf {
|
|
app_project_dirs().cache_dir().join("history.toml")
|
|
}
|
|
|
|
/// Get the default path to use for the history file, as a string.
|
|
#[cfg(feature = "history")]
|
|
pub fn app_history_file_path_string() -> String {
|
|
app_history_file_path().to_str().unwrap().to_owned()
|
|
}
|
|
|
|
/// Check whether an environment variable with the given key is present in the context of the
|
|
/// current process. The environment variable doesn't have to hold any specific value.
|
|
/// Returns `true` if present, `false` if not.
|
|
pub fn env_var_present(key: impl AsRef<OsStr>) -> bool {
|
|
var_os(key).is_some()
|
|
}
|
|
|
|
/// Get a list of all features that were enabled during compilation.
|
|
pub fn features_list() -> Vec<&'static str> {
|
|
// Build the list
|
|
#[allow(unused_mut)]
|
|
let mut features = Vec::new();
|
|
|
|
// Add each feature
|
|
#[cfg(feature = "archive")]
|
|
features.push("archive");
|
|
#[cfg(feature = "clipboard")]
|
|
features.push("clipboard");
|
|
#[cfg(feature = "history")]
|
|
features.push("history");
|
|
#[cfg(feature = "no-color")]
|
|
features.push("no-color");
|
|
|
|
features
|
|
}
|