1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
use chrono::{DateTime, Utc};
use chrono_english::{parse_date_string, Dialect};
use color_eyre::Section;
use dialoguer::{theme, Editor, Input};
use hypothesis::annotations::Selector;

use crate::errors::Apologize;

/// ASCII code of semicolon
/// TODO: Tag cannot have semicolon in it, remember to add this to the README
pub const SEMICOLON: u8 = 59;

/// Makes `DateTime` from a string, can be colloquial like "last Friday 8pm"
pub fn parse_datetime(datetime_string: &str) -> color_eyre::Result<DateTime<Utc>> {
    if datetime_string.to_ascii_lowercase() == "today" {
        Ok(Utc::now().date().and_hms(0, 0, 0))
    } else {
        Ok(parse_date_string(datetime_string, Utc::now(), Dialect::Uk)?)
    }
}

/// Splits byte array by semicolon into list of Annotation IDs
pub fn split_ids(index_list: &[u8]) -> color_eyre::Result<Vec<String>> {
    let index_list_string = std::str::from_utf8(index_list)?;
    Ok(index_list_string
        .split(';')
        .map(|x| x.to_string())
        .collect())
}

/// List of String into semicolon-joined byte array
pub fn join_ids(index_list: &[String]) -> color_eyre::Result<Vec<u8>> {
    Ok(index_list.join(";").as_bytes().to_vec())
}

/// Takes user input from terminal, optionally has a default and optionally displays it.
pub fn user_input(
    message: &str,
    default: Option<&str>,
    show_default: bool,
    allow_empty: bool,
) -> color_eyre::Result<String> {
    match default {
        Some(default) => Ok(Input::with_theme(&theme::ColorfulTheme::default())
            .with_prompt(message)
            .default(default.to_owned())
            .show_default(show_default)
            .allow_empty(allow_empty)
            .interact()?
            .trim()
            .to_owned()),
        None => Ok(
            Input::<String>::with_theme(&theme::ColorfulTheme::default())
                .with_prompt(message)
                .allow_empty(allow_empty)
                .interact()?
                .trim()
                .to_owned(),
        ),
    }
}

/// Gets input from external editor, optionally displays default text in editor
pub fn external_editor_input(default: Option<&str>, extension: &str) -> color_eyre::Result<String> {
    Ok(Editor::new()
        .trim_newlines(false)
        .extension(extension)
        .edit(default.unwrap_or(""))
        .suggestion("Set your default editor using the $EDITOR or $VISUAL environment variables")?
        .ok_or(Apologize::EditorError)
        .suggestion("Make sure to save next time!")?)
}

pub fn get_spinner(message: &str) -> indicatif::ProgressBar {
    let spinner = indicatif::ProgressBar::new_spinner();
    spinner.enable_steady_tick(200);
    spinner.set_style(
        indicatif::ProgressStyle::default_spinner()
            .tick_chars("/|\\- ")
            .template("{spinner:.dim.bold.blue} {wide_msg}"),
    );
    spinner.set_message(message);
    spinner
}

pub fn get_quotes(annotation: &hypothesis::annotations::Annotation) -> Vec<&str> {
    annotation
        .target
        .iter()
        .filter_map(|target| {
            let quotes = target
                .selector
                .iter()
                .filter_map(|selector| match selector {
                    Selector::TextQuoteSelector(selector) => Some(selector.exact.as_str()),
                    _ => None,
                })
                .collect::<Vec<_>>();
            if quotes.is_empty() {
                None
            } else {
                Some(quotes)
            }
        })
        .flat_map(|v| v.into_iter())
        .collect::<Vec<_>>()
}

/// Converts a URI into something that can be used as a folder/filename
pub fn uri_to_filename(uri: &str) -> String {
    uri.replace("://", "_")
        .replace(".", "_")
        .replace("/", "_")
        .replace(":", "_")
}