├── src ├── utils │ ├── mod.rs │ └── ui.rs ├── commands │ ├── logout.rs │ ├── sync.rs │ ├── login.rs │ ├── mod.rs │ └── list.rs ├── store.rs ├── main.rs ├── api.rs ├── cli.rs └── models.rs ├── .gitignore ├── curl-http ├── README.md ├── Cargo.toml └── src │ └── lib.rs ├── anyshortcut-cli.jpg ├── storage-derive ├── Cargo.toml └── src │ └── lib.rs ├── ci ├── before_deploy.ps1 ├── build_in_docker.sh └── before_deploy.sh ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE-MIT ├── .travis.yml ├── README.md ├── LICENSE-APACHE └── Cargo.lock /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ui; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | target 4 | **/*.rs.bk 5 | -------------------------------------------------------------------------------- /curl-http/README.md: -------------------------------------------------------------------------------- 1 | # curl-http 2 | A lightweight http client base on curl 3 | -------------------------------------------------------------------------------- /anyshortcut-cli.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyshortcut/anyshortcut-cli/HEAD/anyshortcut-cli.jpg -------------------------------------------------------------------------------- /storage-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "storage-derive" 3 | version = "0.1.0" 4 | authors = ["Folyd "] 5 | edition = "2018" 6 | publish = false 7 | 8 | [lib] 9 | proc-macro = true 10 | 11 | [dependencies] 12 | syn = "1" 13 | quote = "1" -------------------------------------------------------------------------------- /ci/before_deploy.ps1: -------------------------------------------------------------------------------- 1 | New-Item -ItemType Directory -Name temp 2 | Copy-Item README.md temp 3 | Copy-Item LICENSE-MIT temp 4 | Copy-Item LICENSE-APACHE temp 5 | Get-ChildItem target\$Env:TARGET\release 6 | Copy-Item target\$Env:TARGET\release\$Env:PROJECT_NAME.exe temp 7 | 7z a $Env:PROJECT_NAME-$Env:TRAVIS_TAG-$Env:TARGET.zip temp\* 8 | Get-ChildItem -------------------------------------------------------------------------------- /src/commands/logout.rs: -------------------------------------------------------------------------------- 1 | use clap::ArgMatches; 2 | 3 | use crate::models::*; 4 | use crate::store::Storage; 5 | 6 | pub fn execute(_: &ArgMatches) -> anyhow::Result<()> { 7 | Meta::clear()?; 8 | PrimaryShortcutVec::clear()?; 9 | SecondaryShortcutMap::clear()?; 10 | println!("You are logout successfully."); 11 | 12 | Ok(()) 13 | } 14 | -------------------------------------------------------------------------------- /curl-http/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "curl-http" 3 | version = "0.1.0" 4 | authors = ["Folyd "] 5 | description = "A lightweight http client base on curl." 6 | license = "MIT OR Apache-2.0" 7 | homepage = "https://github.com/Folyd/curl-http" 8 | edition = "2018" 9 | 10 | [dependencies] 11 | curl = "0.4.22" 12 | serde = "1.0" 13 | serde_json = "1.0" 14 | thiserror = "1.0.22" -------------------------------------------------------------------------------- /src/commands/sync.rs: -------------------------------------------------------------------------------- 1 | use ansi_term::Color::Red; 2 | use clap::ArgMatches; 3 | 4 | use crate::models::Meta; 5 | 6 | pub fn execute(_: &ArgMatches) -> anyhow::Result<()> { 7 | if Meta::has_token() { 8 | super::sync_all_shortcuts(); 9 | } else { 10 | println!( 11 | "{}", 12 | Red.paint("Can't sync data, you are not in login state. Please run login first.") 13 | ); 14 | } 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /ci/build_in_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | BUILD_DIR="/work" 5 | 6 | docker run \ 7 | -w ${BUILD_DIR} \ 8 | -v `pwd`:${BUILD_DIR}:ro \ 9 | -v `pwd`/target:${BUILD_DIR}/target \ 10 | -v $HOME/.cargo/registry:/root/.cargo/registry \ 11 | -it messense/rust-musl-cross:${DOCKER_IMAGE_TAG} \ 12 | cargo build --release --target ${TARGET} --verbose 13 | 14 | # Fix permissions for shared directories 15 | USER_ID=$(id -u) 16 | GROUP_ID=$(id -g) 17 | sudo chown -R ${USER_ID}:${GROUP_ID} target/ $HOME/.cargo 18 | -------------------------------------------------------------------------------- /src/utils/ui.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::io::Write; 3 | 4 | pub fn prompt_to_continue(message: &str) -> io::Result { 5 | loop { 6 | print!("{} [y/n] ", message); 7 | io::stdout().flush()?; 8 | 9 | let mut buffer = String::new(); 10 | io::stdin().read_line(&mut buffer)?; 11 | let input = buffer.trim(); 12 | 13 | if input == "y" { 14 | return Ok(true); 15 | } else if input == "n" { 16 | return Ok(false); 17 | } 18 | println!("invalid input!"); 19 | } 20 | } 21 | 22 | pub fn prompt(message: &str) -> io::Result { 23 | loop { 24 | print!("{}", message); 25 | io::stdout().flush()?; 26 | 27 | let mut buffer = String::new(); 28 | io::stdin().read_line(&mut buffer)?; 29 | let input = buffer.trim(); 30 | if !input.is_empty() { 31 | return Ok(input.to_owned()); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to *anyshortcut-cli* 2 | 3 | **Thank you very much for considering to contribute to this project!** 4 | 5 | We welcome any form of contribution: 6 | 7 | * New issues (feature requests, bug reports, questions, ideas, ...) 8 | * Pull requests (documentation improvements, code improvements, new features, ...) 9 | 10 | **Note**: Before you take the time to open a pull request, please open a ticket first. This will 11 | give us the chance to discuss any potential changes first. 12 | 13 | ## Important links 14 | 15 | * [Open issues](https://github.com/anyshortcut/anyshortcut-cli/issues) 16 | * [Open pull requests](https://github.com/anyshortcut/anyshortcut-cli/pulls) 17 | * [Read README](https://github.com/anyshortcut/anyshortcut-cli/blob/master/README.md) 18 | * [LICENSE-APACHE](https://github.com/anyshortcut/anyshortcut-cli/blob/master/LICENSE-APACHE) and [LICENSE-MIT](https://github.com/anyshortcut/anyshortcut-cli/blob/master/LICENSE-MIT) 19 | 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "anyshortcut" 3 | version = "0.2.0" 4 | authors = ["Folyd "] 5 | description = "A blaze fast way to launch your favorite website in Terminal." 6 | homepage = "https://github.com/anyshortcut/anyshortcut-cli" 7 | license = "MIT OR Apache-2.0" 8 | build = "build.rs" 9 | edition = "2018" 10 | 11 | [workspace] 12 | members = [ 13 | "curl-http", 14 | "storage-derive", 15 | ] 16 | 17 | [dependencies] 18 | dirs = "1.0.4" 19 | open = "1.2.2" 20 | serde = "1.0.71" 21 | serde_derive = "1.0.79" 22 | serde_json = "1.0.27" 23 | chrono = "0.4.6" 24 | ansi_term = "0.11.0" 25 | 26 | storage-derive = { path = "./storage-derive" } 27 | curl-http = { path = "./curl-http" } 28 | thiserror = "1.0.22" 29 | anyhow = "1.0.35" 30 | 31 | [dependencies.clap] 32 | version = "2.32" 33 | default-features = false 34 | features = ["color", "vec_map"] 35 | 36 | [dev-dependencies] 37 | itertools = "0.7.8" 38 | 39 | [build-dependencies.clap] 40 | version = "2.32" 41 | default-features = false 42 | -------------------------------------------------------------------------------- /ci/before_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Building and packaging for release 3 | 4 | set -ex 5 | 6 | build() { 7 | cargo build --target "$TARGET" --release --verbose 8 | } 9 | 10 | pack() { 11 | local tempdir 12 | local out_dir 13 | local package_name 14 | 15 | tempdir=$(mktemp -d 2>/dev/null || mktemp -d -t tmp) 16 | out_dir=$(pwd) 17 | package_name="$PROJECT_NAME-$TRAVIS_TAG-$TARGET" 18 | 19 | # create a "staging" directory 20 | mkdir "$tempdir/$package_name" 21 | 22 | # copying the main binary 23 | ls -al "target/$TARGET/release" 24 | cp "target/$TARGET/release/$PROJECT_NAME" "$tempdir/$package_name/" 25 | strip "$tempdir/$package_name/$PROJECT_NAME" 26 | 27 | # readme and license 28 | cp README.md "$tempdir/$package_name" 29 | cp LICENSE-MIT "$tempdir/$package_name" 30 | cp LICENSE-APACHE "$tempdir/$package_name" 31 | 32 | # archiving 33 | pushd "$tempdir" 34 | tar czf "$out_dir/$package_name.tar.gz" "$package_name"/* 35 | popd 36 | rm -r "$tempdir" 37 | } 38 | 39 | 40 | main() { 41 | pack 42 | } 43 | 44 | main 45 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 anyshortcut 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/commands/login.rs: -------------------------------------------------------------------------------- 1 | use clap::ArgMatches; 2 | 3 | use crate::api::Api; 4 | use crate::models::Meta; 5 | use crate::store::Storage; 6 | use crate::utils::ui; 7 | use anyhow::Result; 8 | 9 | pub fn execute(matches: &ArgMatches) -> Result<()> { 10 | if let Some(access_token) = matches.value_of("token") { 11 | return handle_login(access_token); 12 | } 13 | 14 | println!("This helps you signing in your anyshortcut-cli with an authentication token."); 15 | println!("If you do not yet have a token ready we can bring up a browser for you"); 16 | println!("to create a token now."); 17 | println!(); 18 | 19 | if ui::prompt_to_continue("Open browser now?")? { 20 | let url = "https://anyshortcut.com/account"; 21 | if open::that(url).is_err() { 22 | println!("Cannot open browser. Please manually go to {}", &url); 23 | } 24 | } 25 | 26 | while let Err(error) = handle_login(&ui::prompt("Enter your token:")?) { 27 | println!("{}", error); 28 | } 29 | 30 | Ok(()) 31 | } 32 | 33 | fn handle_login(access_token: &str) -> Result<()> { 34 | let api = Api::get_current(); 35 | api.login_with_access_token(&access_token).map(|_| { 36 | println!("Valid access token."); 37 | Meta { 38 | token: access_token.to_string(), 39 | } 40 | .persist() 41 | .unwrap_or_else(|error| println!("{}", error)); 42 | 43 | super::sync_all_shortcuts(); 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /src/store.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use serde::de::DeserializeOwned; 3 | use serde::Serialize; 4 | use std::fs; 5 | use std::fs::File; 6 | use std::path::PathBuf; 7 | 8 | /// Get user storage directory. 9 | pub fn get_store_directory() -> Result { 10 | let mut path = dirs::home_dir().ok_or_else(|| anyhow!("Could not find home dir"))?; 11 | path.push(".anyshortcut"); 12 | // Create the directory if not exist before write date to file. 13 | fs::create_dir_all(path.to_str().unwrap())?; 14 | Ok(path) 15 | } 16 | 17 | /// **Storage** trait which required supertraits (Serialize, DeserializeOwned) 18 | /// to persist() and parse() target file. 19 | pub trait Storage: Serialize + DeserializeOwned { 20 | /// Get storage file name. 21 | fn get_file_name() -> String; 22 | 23 | fn persist(&self) -> Result<()> { 24 | let mut path = get_store_directory()?; 25 | path.push(Self::get_file_name()); 26 | 27 | let file = File::create(path)?; 28 | serde_json::to_writer_pretty(file, &self)?; 29 | Ok(()) 30 | } 31 | 32 | fn parse() -> Result { 33 | let mut path = get_store_directory()?; 34 | path.push(Self::get_file_name()); 35 | 36 | let file = File::open(path)?; 37 | Ok(serde_json::from_reader(file)?) 38 | } 39 | 40 | fn clear() -> Result<()> { 41 | let mut path = get_store_directory()?; 42 | path.push(Self::get_file_name()); 43 | 44 | if path.exists() { 45 | fs::remove_file(path).unwrap(); 46 | } 47 | 48 | Ok(()) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /storage-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use quote::quote; 4 | 5 | use proc_macro::TokenStream; 6 | 7 | /// 8 | /// A small derive procedure macro to implement **Storage** trait. 9 | /// 10 | /// ``` 11 | /// #[derive(Storage)] 12 | /// #[store_at = "meta.json"] 13 | /// pub struct Meta { 14 | /// pub token: String, 15 | /// } 16 | /// ``` 17 | /// The `store_at` attribute is required. 18 | /// 19 | #[proc_macro_derive(Storage, attributes(store_at))] 20 | pub fn derive_storage(input: TokenStream) -> TokenStream { 21 | let parse = syn::parse(input).unwrap(); 22 | impl_derive_storage_macro(&parse) 23 | } 24 | 25 | fn impl_derive_storage_macro(derive_input: &syn::DeriveInput) -> TokenStream { 26 | let name = &derive_input.ident; 27 | 28 | let store_at: &syn::MetaNameValue = &derive_input 29 | .attrs 30 | .iter() 31 | .find_map(get_meta_items) 32 | .expect("Expect a store_at attribute"); 33 | let file_name = &store_at.lit; 34 | let gen = quote! { 35 | impl Storage for #name { 36 | fn get_file_name() -> String { 37 | #file_name.to_string() 38 | } 39 | } 40 | }; 41 | gen.into() 42 | } 43 | 44 | fn get_meta_items(attr: &syn::Attribute) -> Option { 45 | if let Ok(syn::Meta::NameValue(meta_name_value)) = attr.parse_meta() { 46 | match meta_name_value.path.get_ident() { 47 | Some(ident) if ident == "store_at" => Some(meta_name_value), 48 | _ => None, 49 | } 50 | } else { 51 | None 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::process; 2 | 3 | use clap::ArgMatches; 4 | 5 | use crate::commands::{list, login, logout, sync}; 6 | use crate::models::ShortcutManager; 7 | 8 | mod api; 9 | mod cli; 10 | mod commands; 11 | mod models; 12 | mod store; 13 | mod utils; 14 | 15 | fn main() { 16 | let matches = cli::build_cli().get_matches(); 17 | 18 | match handle_matches(&matches) { 19 | Ok(()) => process::exit(0), 20 | Err(error) => { 21 | println!("Error: {}", error); 22 | process::exit(1); 23 | } 24 | }; 25 | } 26 | 27 | fn handle_matches(matches: &ArgMatches) -> anyhow::Result<()> { 28 | match matches.subcommand() { 29 | ("login", Some(login_matches)) => login::execute(&login_matches)?, 30 | ("logout", Some(logout_matches)) => logout::execute(&logout_matches)?, 31 | ("sync", Some(sync_matches)) => sync::execute(&sync_matches)?, 32 | ("list", Some(list_matches)) => list::execute(&list_matches)?, 33 | _ => { 34 | match ( 35 | matches.value_of("primary_key"), 36 | matches.value_of("secondary_key"), 37 | ) { 38 | (Some(primary_key), Some(secondary_key)) => { 39 | ShortcutManager::open_secondary(primary_key, secondary_key); 40 | } 41 | (Some(primary_key), None) => { 42 | ShortcutManager::open_primary(primary_key); 43 | } 44 | (_, _) => { 45 | // No args or subcommand provided, print the long help message. 46 | cli::build_cli().print_long_help()?; 47 | } 48 | } 49 | } 50 | }; 51 | 52 | Ok(()) 53 | } 54 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | use ansi_term::Color::{Cyan, Green}; 2 | 3 | use crate::api::Api; 4 | use crate::store::{self, Storage}; 5 | 6 | pub mod list; 7 | pub mod login; 8 | pub mod logout; 9 | pub mod sync; 10 | 11 | /// Sync all shortcuts 12 | pub fn sync_all_shortcuts() { 13 | println!(); 14 | println!("Syncing your shortcut data..."); 15 | match Api::get_current().get_all_shortcuts() { 16 | Ok(response) => { 17 | response 18 | .primary 19 | .persist() 20 | .unwrap_or_else(|error| println!("{}", error)); 21 | response 22 | .secondary 23 | .persist() 24 | .unwrap_or_else(|error| println!("{}", error)); 25 | 26 | println!(); 27 | println!("{}", Green.paint("Shortcuts synced success!")); 28 | println!( 29 | "Primary shortcut number: {}", 30 | Cyan.paint(response.primary.len().to_string()) 31 | ); 32 | println!( 33 | "Secondary shortcut number: {}", 34 | Cyan.paint( 35 | response 36 | .secondary 37 | .values() 38 | .fold(0, |acc, shortcuts| acc + shortcuts.len()) 39 | .to_string() 40 | ) 41 | ); 42 | println!(); 43 | 44 | // Get the store directory PathBuf object. 45 | let dir = store::get_store_directory().unwrap(); 46 | println!( 47 | "All your data stored at {} directory.", 48 | Cyan.paint(format!("{}", dir.display())) 49 | ); 50 | } 51 | Err(error) => println!("{}", error), 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: generic 3 | services: docker 4 | sudo: required 5 | 6 | env: 7 | global: 8 | # Default target on travis-ci. 9 | # Used as conditional check in the install stage 10 | - HOST=x86_64-unknown-linux-gnu 11 | - PROJECT_NAME=anyshortcut-cli 12 | 13 | cache: 14 | directories: 15 | - $HOME/.cargo 16 | - $TRAVIS_BUILD_DIR/target 17 | 18 | matrix: 19 | include: 20 | # Linux 21 | - os: linux 22 | env: 23 | - TARGET=i686-unknown-linux-musl 24 | - DOCKER_IMAGE_TAG=i686-musl 25 | script: bash ci/build_in_docker.sh 26 | 27 | - os: linux 28 | env: 29 | - TARGET=x86_64-unknown-linux-musl 30 | - DOCKER_IMAGE_TAG=x86_64-musl 31 | script: bash ci/build_in_docker.sh 32 | 33 | # OSX 34 | - os: osx 35 | language: rust 36 | env: TARGET=x86_64-apple-darwin 37 | script: 38 | - cargo build --release --target $TARGET --verbose 39 | 40 | - os: windows 41 | language: rust 42 | env: TARGET=x86_64-pc-windows-msvc 43 | script: 44 | - cargo build --release --target $TARGET --verbose 45 | before_deploy: 46 | - powershell -executionpolicy bypass -File "ci\before_deploy.ps1" 47 | 48 | before_install: true 49 | 50 | before_deploy: 51 | - bash ci/before_deploy.sh 52 | 53 | deploy: 54 | provider: releases 55 | api_key: 56 | # Generate secure token with travis encrypt --pro 57 | secure: "VSjB9datAXSA678W+fF5M2KELZpIucBUQATRuHXTwq5VYxhiy7fvX8lM5fEGZAUVqFQJDo9Isf75GHtMoR0dmjxqIXitdDkD117xo8Ta6uq11q1SCx+OGLh9rlNtVLSPOxL+VdhN5CmoccBzf01Lv2yiWPIzOd/u6OfK0MVAQ3C+Gp4aCdpI5AfoSgYXUnYtNLo9p4ul9eTyrzqDAZbRoYNx71rFUBz2kWmnifb+vBbg6+w+dxs+UAFj5+f29S7DGV0vyIMYocrXhjfkGu2jkS6tVFRZLrGAm2Yknh7A7A0brTdeQaItbmW6ddAR7uD7AlXvR5pND5RwME217GHYklmYg2eP084idfqvNMLdomF8WTt5PB1tVUg4NxasQLki/Qi/cUDeWARFbCsN86YYX78t7UFMkvR5mBdfe7HCUUiPBaelM7YF86jp20Sb6Dc9/RXqbzrXN0EHIhD0/YoIMC5JUyZDiYK5Bg2z9yd1YO/VBLFTng5gt9+M//abDlq00ufnjlEIgOtFBGjWNuG/iKS6A0qdwGI+bcm7S/VnqewfAhLHNDzXtUqSs1NWK2j496sZLCQcVaZnB2fvoUIwx8ah+d8tkoaB5YC15NlBXSt2iG7ijjxk5TPSJaYOwmX93fVQF1HfB/n0kxad10ldgkd1RtGhkLJK8k/nj23CZOM=" 58 | file_glob: true 59 | file: $PROJECT_NAME-$TRAVIS_TAG-$TARGET.* 60 | skip_cleanup: true 61 | on: 62 | tags: true 63 | 64 | before_cache: 65 | # Travis can't cache files that are not readable by "others" 66 | - chmod -R a+r $HOME/.cargo 67 | 68 | branches: 69 | only: 70 | - "/^v\\d+\\.\\d+\\.\\d+.*$/" 71 | - master 72 | 73 | notifications: 74 | email: 75 | on_success: never 76 | -------------------------------------------------------------------------------- /src/commands/list.rs: -------------------------------------------------------------------------------- 1 | use ansi_term::Color::{Cyan, Red}; 2 | use clap::ArgMatches; 3 | 4 | use crate::models::{Shortcut, ShortcutManager}; 5 | 6 | pub fn execute(matches: &ArgMatches) -> anyhow::Result<()> { 7 | if matches.is_present("primary") { 8 | if let Some(shortcuts) = ShortcutManager::get_primary_shortcuts() { 9 | shortcuts.iter().for_each(Shortcut::pretty_print); 10 | 11 | println!(); 12 | println!( 13 | "Total primary shortcut number: {}", 14 | Cyan.paint(shortcuts.len().to_string()) 15 | ); 16 | println!( 17 | "Total primary shortcut open times: {}", 18 | Cyan.paint( 19 | shortcuts 20 | .iter() 21 | .fold(0, |acc, shortcut| acc + shortcut.open_times) 22 | .to_string() 23 | ) 24 | ); 25 | } else { 26 | println!("{}", Red.paint("No primary shortcut found")); 27 | }; 28 | } else if matches.is_present("secondary") { 29 | if let Some(domain_shortcut_map) = ShortcutManager::get_secondary_shortcuts() { 30 | let mut total_number = 0; 31 | let mut total_open_times = 0; 32 | for (domain, shortcuts) in domain_shortcut_map.iter() { 33 | println!(); 34 | println!("[{}]", Cyan.bold().paint(domain)); 35 | 36 | shortcuts.iter().for_each(Shortcut::pretty_print); 37 | total_number += shortcuts.len(); 38 | total_open_times += shortcuts 39 | .iter() 40 | .fold(0, |acc, shortcut| acc + shortcut.open_times); 41 | } 42 | 43 | println!(); 44 | println!( 45 | "Total domain number: {}", 46 | Cyan.paint(domain_shortcut_map.len().to_string()) 47 | ); 48 | println!( 49 | "Total secondary shortcut number: {}", 50 | Cyan.paint(total_number.to_string()) 51 | ); 52 | println!( 53 | "Total secondary shortcut open times: {}", 54 | Cyan.paint(total_open_times.to_string()) 55 | ); 56 | } else { 57 | println!("{}", Red.paint("No secondary shortcut found.")); 58 | } 59 | } else if matches.is_present("compound") { 60 | if let Some(shortcuts) = ShortcutManager::get_compound_shortcuts() { 61 | shortcuts.iter().for_each(Shortcut::pretty_print); 62 | 63 | println!(); 64 | println!( 65 | "Total compound shortcut number: {}", 66 | Cyan.paint(shortcuts.len().to_string()) 67 | ); 68 | println!( 69 | "Total compound shortcut open times: {}", 70 | Cyan.paint( 71 | shortcuts 72 | .iter() 73 | .fold(0, |acc, shortcut| acc + shortcut.open_times) 74 | .to_string() 75 | ) 76 | ); 77 | } else { 78 | println!("{}", Red.paint("No compound shortcut found.")); 79 | } 80 | } else { 81 | println!("{}", matches.usage()); 82 | println!("For detail usage, please run with -h or --help.") 83 | } 84 | 85 | Ok(()) 86 | } 87 | -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::rc::Rc; 3 | 4 | use anyhow::{Context, Result}; 5 | use clap::crate_version; 6 | use serde::de::DeserializeOwned; 7 | use serde_derive::Deserialize; 8 | use thiserror::Error; 9 | 10 | use curl_http::{Client, Response}; 11 | 12 | use crate::models::*; 13 | 14 | const API_URL: &str = "https://api.anyshortcut.com"; 15 | 16 | thread_local! { 17 | static API: Rc = Rc::new(Api::new()); 18 | } 19 | 20 | #[derive(Deserialize, Debug)] 21 | pub struct ApiResponse { 22 | pub code: u32, 23 | pub data: T, 24 | pub message: String, 25 | } 26 | 27 | #[derive(Debug, Error)] 28 | pub struct ApiError { 29 | pub code: u32, 30 | pub message: String, 31 | } 32 | 33 | #[derive(Copy, Clone, Eq, PartialEq, Debug, Error)] 34 | pub enum ApiErrorKind { 35 | #[error("Access token required.")] 36 | AccessTokenRequired, 37 | #[error("Invalid access token.")] 38 | InvalidToken, 39 | #[error("Unknown error.")] 40 | UnknownError, 41 | } 42 | 43 | impl fmt::Display for ApiResponse { 44 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 45 | write!(f, "{{")?; 46 | write!(f, "\n code: {},", self.code)?; 47 | write!(f, "\n data: {},", self.data)?; 48 | write!(f, "\n message: {}", self.message)?; 49 | write!(f, "\n}}")?; 50 | Ok(()) 51 | } 52 | } 53 | 54 | impl fmt::Display for ApiError { 55 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 56 | write!(f, "{{")?; 57 | write!(f, "\n code: {},", self.code)?; 58 | write!(f, "\n message: {}", self.message)?; 59 | write!(f, "\n}}")?; 60 | Ok(()) 61 | } 62 | } 63 | 64 | impl From> for ApiError { 65 | fn from(response: ApiResponse) -> ApiError { 66 | ApiError { 67 | code: response.code, 68 | message: response.message, 69 | } 70 | } 71 | } 72 | 73 | pub struct Api { 74 | client: Client, 75 | } 76 | 77 | impl Api { 78 | pub fn new() -> Api { 79 | let mut client = Client::new(API_URL); 80 | client.set_user_agent(&format!("anyshortcut-cli/{}", crate_version!())); 81 | Api { client } 82 | } 83 | 84 | /// Returns the current api for the thread. 85 | pub fn get_current() -> Rc { 86 | API.with(std::clone::Clone::clone) 87 | } 88 | 89 | pub fn login_with_access_token(&self, access_token: &str) -> Result { 90 | let response = self 91 | .client 92 | .get(&format!("/user/login?access_token={}", access_token))?; 93 | self.handle_http_response(&response) 94 | } 95 | 96 | pub fn get_all_shortcuts(&self) -> Result { 97 | let access_token = Meta::get_token(); 98 | let response = self.client.get(&format!( 99 | "/shortcuts/all?nested=false&access_token={}", 100 | access_token 101 | ))?; 102 | self.handle_http_response(&response) 103 | } 104 | 105 | /// Handle http response internally to return correct api error according to api response code. 106 | fn handle_http_response(&self, response: &Response) -> Result { 107 | let api_response = response.deserialize::>()?; 108 | match api_response.code { 109 | 200 => { 110 | let response = response.deserialize::>()?; 111 | Ok(response.data) 112 | } 113 | 1000 => Err(ApiError::from(api_response)).context(ApiErrorKind::AccessTokenRequired), 114 | 1001 | 1002 => Err(ApiError::from(api_response)).context(ApiErrorKind::InvalidToken), 115 | _ => Err(ApiError::from(api_response)).context(ApiErrorKind::UnknownError), 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./anyshortcut-cli.jpg) 2 | # Anyshortcut Command Line Interface 3 | 4 | [![Travis Build Status](https://travis-ci.com/anyshortcut/anyshortcut-cli.svg?branch=master)](https://travis-ci.com/anyshortcut/anyshortcut-cli) 5 | [![license-mit](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/anyshortcut/anyshortcut-cli/blob/master/LICENSE-MIT) 6 | [![license-apache](https://img.shields.io/badge/license-Apache-yellow.svg)](https://github.com/anyshortcut/anyshortcut-cli/blob/master/LICENSE-APACHE) 7 | [![Version info](https://img.shields.io/crates/v/anyshortcut.svg)](https://crates.io/crates/anyshortcut) 8 | 9 | A blaze fast way to launch your favorite website in Terminal. 10 | 11 | ## Installation 12 | 13 | #### Cargo 14 | 15 | `cargo install anyshortcut` 16 | 17 | #### Homebrew 18 | 19 | `brew install anyshortcut` 20 | 21 | > Unmerged: https://github.com/Homebrew/homebrew-core/pull/33198 22 | 23 | #### Install binary file from Github release 24 | 25 | https://github.com/anyshortcut/anyshortcut-cli/releases 26 | 27 | **Recommend** 28 | 29 | > Give it an alias name such as **as** by adding following line to your 30 | > **.bashrc** or **.zshrc** file: 31 | > 32 | > ```shell 33 | > alias as=$(which anyshortcut) 34 | > ``` 35 | > 36 | > then source your profile to make it works. 37 | 38 | 39 | ## Usage 40 | 41 | ``` 42 | $ anyshortcut 43 | A blaze fast way to launch your favorite website in Terminal. 44 | 45 | USAGE: 46 | anyshortcut [ARGS] [SUBCOMMAND] 47 | 48 | ARGS: 49 | 50 | Using primary shortcut key (A~Z|0~9) or compound shortcut key (AA~ZZ) to open the url. 51 | 52 | 53 | Use secondary shortcut key (A~Z|0~9) to open the url. 54 | 55 | 56 | SUBCOMMANDS: 57 | list List shortcuts. 58 | login Login with the token. 59 | logout Logout and clean local data. 60 | sync Sync all shortcuts after login. 61 | 62 | ``` 63 | 64 | - `as ` 65 | 66 | Using the primary shortcut to launch the website. 67 | 68 | **PRIMARY_KEY** is in the form of a case-insensitive alphanumeric letter range **A ~ Z** or **0 ~ 9**. 69 | 70 | For example: 71 | ``` 72 | $ as g 73 | Url: https://www.google.com/ 74 | ``` 75 | 76 | - `as ` 77 | 78 | Using the compound shortcut to launch the website. 79 | 80 | **COMPOUND_KEY** is in the form of two case-insensitive alphabet letters range **AA ~ ZZ**. 81 | 82 | For example: 83 | ``` 84 | $ as db 85 | Url: https://www.dropbox.com/ 86 | ``` 87 | 88 | - `as | ` 89 | 90 | Using the secondary shortcut to launch the website. 91 | 92 | **SECONDARY_KEY** is in the form of an case-insensitive alphanumeric letter range **A ~ Z** or **0 ~ 9**. 93 | 94 | For example: 95 | ``` 96 | $ as g t 97 | Url: https://translate.google.com/ 98 | ``` 99 | 100 | - `as login [TOKEN]` or `as login` to prompt input `TOKEN` 101 | 102 | Login with access token then sync your shortcuts automatically. You can find the access token at 103 | official website [Account Profile](https://anyshortcut.com/account#/profile/) -> **API Access**. 104 | 105 | - `as sync` 106 | 107 | Sync your shortcuts to local manually. 108 | 109 | ``` 110 | $ as sync 111 | Syncing your shortcut data... 112 | 113 | Shortcuts synced success! 114 | Primary shortcut number: 120 115 | Secondary shortcut number: 150 116 | 117 | All your data stored at ~/.anyshortcut directory. 118 | ``` 119 | 120 | - `as list` 121 | 122 | List your shortcuts. 123 | 124 | ``` 125 | USAGE: 126 | anyshortcut list [OPTIONS] 127 | 128 | OPTIONS: 129 | -c, --compound List all compound shortcuts. 130 | -p, --primary List all primary shortcuts. 131 | -s, --secondary List all secondary shortcuts. 132 | ``` 133 | 134 | - `as logout` 135 | 136 | Logout and all local data will be cleaned. 137 | 138 | ## Future plans 139 | 140 | - [ ] Support bind shortcut 141 | - [ ] Support delete shortcut 142 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{crate_description, crate_name, crate_version}; 2 | use clap::{App, AppSettings, Arg, SubCommand}; 3 | 4 | pub fn build_cli() -> App<'static, 'static> { 5 | App::new(crate_name!()) 6 | .version(crate_version!()) 7 | .about(crate_description!()) 8 | .global_setting(AppSettings::ColoredHelp) 9 | .global_setting(AppSettings::UnifiedHelpMessage) 10 | .setting(AppSettings::DisableHelpSubcommand) 11 | .setting(AppSettings::VersionlessSubcommands) 12 | .max_term_width(100) 13 | .help_message("Print this help message.") 14 | .version_message("Show version information.") 15 | .args(&[ 16 | Arg::with_name("primary_key") 17 | .value_name("PRIMARY_KEY | COMPOUND_KEY") 18 | .help("Using primary shortcut key (A~Z|0~9) or compound shortcut key (AA~ZZ) to open the url.") 19 | .index(1) 20 | .validator(validate_primary_key), 21 | Arg::with_name("secondary_key") 22 | .value_name("SECONDARY_KEY") 23 | .help("Use secondary shortcut key (A~Z|0~9) to open the url.") 24 | .index(2) 25 | .validator(validate_secondary_key), 26 | ]) 27 | .subcommand( 28 | SubCommand::with_name("login") 29 | .about("Login with the token") 30 | .arg( 31 | Arg::with_name("token") 32 | .value_name("TOKEN") 33 | .help("[TOKEN] obtained from user dashboard.") 34 | .takes_value(true) 35 | .empty_values(false)), 36 | ) 37 | .subcommand( 38 | SubCommand::with_name("sync") 39 | .about("Sync all shortcuts after login."), 40 | ) 41 | .subcommand( 42 | SubCommand::with_name("list") 43 | .about("List shortcuts.") 44 | .arg( 45 | Arg::with_name("primary") 46 | .long("primary") 47 | .short("p") 48 | .conflicts_with_all(&["secondary", "compound"]) 49 | .help("List all primary shortcuts."), 50 | ) 51 | .arg( 52 | Arg::with_name("secondary") 53 | .long("secondary") 54 | .short("s") 55 | .conflicts_with_all(&["primary", "compound"]) 56 | .help("List all secondary shortcuts."), 57 | ) 58 | .arg( 59 | Arg::with_name("compound") 60 | .long("compound") 61 | .short("c") 62 | .help("List all compound shortcuts."), 63 | ), 64 | ) 65 | .subcommand( 66 | SubCommand::with_name("logout") 67 | .about("Logout and clean local data."), 68 | ) 69 | } 70 | 71 | /// Validate primary key format. 72 | /// Including one-letter primary key and two-letters compound key. 73 | fn validate_primary_key(key: String) -> Result<(), String> { 74 | if key.chars().all(|c| c.is_ascii_alphanumeric()) { 75 | if key.is_empty() || key.len() > 2 { 76 | return Err(String::from("Invalid key length")); 77 | } 78 | 79 | Ok(()) 80 | } else { 81 | Err(String::from("Invalid primary key")) 82 | } 83 | } 84 | 85 | /// Validate secondary key format. 86 | fn validate_secondary_key(key: String) -> Result<(), String> { 87 | if key.len() == 1 && key.chars().all(|c| c.is_ascii_alphanumeric()) { 88 | Ok(()) 89 | } else { 90 | Err(String::from("Invalid secondary key")) 91 | } 92 | } 93 | 94 | #[cfg(test)] 95 | mod tests { 96 | use clap::ErrorKind; 97 | use itertools::Itertools; 98 | 99 | use super::*; 100 | 101 | #[test] 102 | fn test_args_conflict() { 103 | for pair in vec!["-p", "-c", "-s"].iter().combinations(2) { 104 | let mut args: Vec<&str> = vec![crate_name!(), "list"]; 105 | args.extend(pair); 106 | let res = build_cli().get_matches_from_safe(args); 107 | 108 | assert_eq!(res.unwrap_err().kind, ErrorKind::ArgumentConflict); 109 | } 110 | } 111 | 112 | #[test] 113 | fn test_args_validator() { 114 | let args = vec![crate_name!(), "a", "a"]; 115 | let res = build_cli().get_matches_from_safe(args); 116 | assert!(res.is_ok()); 117 | 118 | let args = vec![crate_name!(), "aaaa"]; 119 | let res = build_cli().get_matches_from_safe(args); 120 | assert!(res.is_err()); 121 | 122 | let args = vec![crate_name!(), "a*"]; 123 | let res = build_cli().get_matches_from_safe(args); 124 | assert!(res.is_err()); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/models.rs: -------------------------------------------------------------------------------- 1 | use ansi_term::Color::Yellow; 2 | use ansi_term::{ANSIString, ANSIStrings, Style}; 3 | use chrono::{TimeZone, Utc}; 4 | use serde_derive::{Deserialize, Serialize}; 5 | use std::collections::HashMap; 6 | use std::fmt; 7 | use std::ops::Deref; 8 | use storage_derive::Storage; 9 | 10 | use crate::store::Storage; 11 | 12 | #[derive(Storage, Serialize, Deserialize, Debug)] 13 | #[store_at = "meta.json"] 14 | pub struct Meta { 15 | pub token: String, 16 | } 17 | 18 | /// 19 | /// The Shortcut struct. 20 | /// 21 | #[derive(Serialize, Deserialize, Debug, Clone)] 22 | pub struct Shortcut { 23 | pub id: u32, 24 | pub key: ShortcutKey, 25 | pub url: String, 26 | pub title: String, 27 | pub comment: Option, 28 | pub domain: ShortcutDomain, 29 | pub open_times: i32, 30 | #[serde(rename = "created_time")] 31 | pub timestamp: i64, 32 | } 33 | 34 | impl Shortcut { 35 | pub fn pretty_print(&self) { 36 | println!(); 37 | println!("{}", "-".repeat(60)); 38 | let key_str: &[ANSIString<'static>] = &[ 39 | Yellow.paint("["), 40 | Yellow.bold().paint(self.key.to_uppercase()), 41 | Yellow.paint("]"), 42 | ]; 43 | println!( 44 | "{} {}", 45 | ANSIStrings(key_str), 46 | Style::new().bold().paint(&self.title) 47 | ); 48 | 49 | println!(); 50 | self.fixed_label_print("Url:", &self.url); 51 | self.fixed_label_print( 52 | "Comment:", 53 | self.comment.as_ref().unwrap_or(&String::from("")), 54 | ); 55 | self.fixed_label_print("Domain:", &self.domain); 56 | self.fixed_label_print("Open times:", &self.open_times); 57 | self.fixed_label_print("Created at:", Utc.timestamp_millis(self.timestamp)); 58 | println!(); 59 | } 60 | 61 | fn fixed_label_print(&self, label: &str, text: impl fmt::Display) { 62 | println!("{:14}{}", label, text); 63 | } 64 | } 65 | 66 | /// A aliased type for shortcut key. 67 | pub type ShortcutKey = String; 68 | /// A aliased type for shortcut domain name. 69 | pub type ShortcutDomain = String; 70 | 71 | #[derive(Storage, Serialize, Deserialize, Debug)] 72 | #[store_at = "primary.json"] 73 | pub struct PrimaryShortcutVec(Vec); 74 | 75 | #[derive(Storage, Serialize, Deserialize, Debug)] 76 | #[store_at = "secondary.json"] 77 | pub struct SecondaryShortcutMap(HashMap>); 78 | 79 | #[derive(Serialize, Deserialize, Debug)] 80 | pub struct ShortcutData { 81 | pub primary: PrimaryShortcutVec, 82 | pub secondary: SecondaryShortcutMap, 83 | } 84 | 85 | pub struct ShortcutManager {} 86 | 87 | impl ShortcutManager { 88 | pub fn get_primary_shortcuts() -> Option> { 89 | PrimaryShortcutVec::parse().ok().map(|shortcuts| { 90 | shortcuts 91 | .iter() 92 | .cloned() 93 | .filter(|shortcut| shortcut.key.len() == 1) 94 | .collect() 95 | }) 96 | } 97 | 98 | pub fn get_compound_shortcuts() -> Option> { 99 | PrimaryShortcutVec::parse().ok().map(|shortcuts| { 100 | shortcuts 101 | .iter() 102 | .cloned() 103 | .filter(|shortcut| shortcut.key.len() == 2) 104 | .collect() 105 | }) 106 | } 107 | 108 | pub fn get_secondary_shortcuts() -> Option { 109 | SecondaryShortcutMap::parse().ok() 110 | } 111 | 112 | pub fn get_primary_by_key(key: &str) -> Option { 113 | match PrimaryShortcutVec::parse() { 114 | Ok(shortcuts) => shortcuts 115 | .iter() 116 | .cloned() 117 | .find(|shortcut| shortcut.key.eq_ignore_ascii_case(key)), 118 | Err(_) => None, 119 | } 120 | } 121 | 122 | pub fn get_secondary_by_domain_key(domain: &str, key: &str) -> Option { 123 | match SecondaryShortcutMap::parse() { 124 | Ok(domain_shortcut_map) => { 125 | if let Some(shortcuts) = domain_shortcut_map.get(domain) { 126 | shortcuts 127 | .iter() 128 | .cloned() 129 | .find(|shortcut| shortcut.key.eq_ignore_ascii_case(key)) 130 | } else { 131 | None 132 | } 133 | } 134 | Err(_) => None, 135 | } 136 | } 137 | 138 | #[allow(unused)] 139 | pub fn get_secondary_by_keys(primary_key: &str, secondary_key: &str) -> Option { 140 | if let Some(shortcut) = Self::get_primary_by_key(primary_key) { 141 | Self::get_secondary_by_domain_key(&shortcut.domain, secondary_key) 142 | } else { 143 | None 144 | } 145 | } 146 | 147 | pub fn open_primary(key: &str) { 148 | if let Some(shortcut) = Self::get_primary_by_key(key) { 149 | Self::open_shortcut(&shortcut); 150 | } else { 151 | println!("Not primary shortcut (key: {}) found.", key.to_uppercase()); 152 | } 153 | } 154 | 155 | pub fn open_secondary(primary_key: &str, secondary_key: &str) { 156 | if let Some(primary_shortcut) = Self::get_primary_by_key(primary_key) { 157 | let domain = &primary_shortcut.domain; 158 | if let Some(shortcut) = Self::get_secondary_by_domain_key(domain, secondary_key) { 159 | Self::open_shortcut(&shortcut); 160 | } else { 161 | println!( 162 | "No secondary shortcut (key: {}) found.", 163 | secondary_key.to_uppercase() 164 | ); 165 | } 166 | } else { 167 | println!( 168 | "Not primary shortcut (key: {}) found.", 169 | primary_key.to_uppercase() 170 | ); 171 | } 172 | } 173 | 174 | fn open_shortcut(shortcut: &Shortcut) { 175 | match open::that(shortcut.url.clone()) { 176 | Ok(_) => println!("Url: {}", shortcut.url), 177 | Err(error) => println!("{}", error), 178 | } 179 | } 180 | } 181 | 182 | impl Meta { 183 | pub fn get_token() -> String { 184 | match Self::parse() { 185 | Ok(meta) => meta.token, 186 | Err(_) => String::from(""), 187 | } 188 | } 189 | 190 | pub fn has_token() -> bool { 191 | Self::parse().is_ok() 192 | } 193 | } 194 | 195 | impl Deref for PrimaryShortcutVec { 196 | type Target = Vec; 197 | 198 | fn deref(&self) -> &Vec { 199 | &self.0 200 | } 201 | } 202 | 203 | impl Deref for SecondaryShortcutMap { 204 | type Target = HashMap>; 205 | 206 | fn deref(&self) -> &HashMap> { 207 | &self.0 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /curl-http/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::cell::{RefCell, RefMut}; 2 | use std::fmt; 3 | use std::io::{Read, Write}; 4 | 5 | use serde::de::DeserializeOwned; 6 | use serde::{self, Serialize}; 7 | use thiserror::Error; 8 | 9 | /// Shortcut alias for results of this module. 10 | pub type Result = std::result::Result; 11 | 12 | /// A enum represents HTTP methods. 13 | #[derive(PartialEq, Debug)] 14 | pub enum Method { 15 | Get, 16 | Head, 17 | Post, 18 | Put, 19 | Delete, 20 | } 21 | 22 | impl fmt::Display for Method { 23 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 24 | match *self { 25 | Method::Get => write!(f, "GET"), 26 | Method::Head => write!(f, "HEAD"), 27 | Method::Post => write!(f, "POST"), 28 | Method::Put => write!(f, "PUT"), 29 | Method::Delete => write!(f, "DELETE"), 30 | } 31 | } 32 | } 33 | 34 | /// 35 | /// A Http client base on curl. 36 | /// 37 | pub struct Client { 38 | shared_handle: RefCell, 39 | base_url: String, 40 | user_agent: String, 41 | } 42 | 43 | impl Client { 44 | /// Initialize a curl http client based on the **base_url**. 45 | pub fn new(base_url: &str) -> Client { 46 | Client { 47 | shared_handle: RefCell::new(curl::easy::Easy::new()), 48 | base_url: base_url.to_string(), 49 | user_agent: "curl-http".to_string(), 50 | } 51 | } 52 | 53 | /// Set the User-Agent header. Default is `curl-http` 54 | pub fn set_user_agent(&mut self, user_agent: &str) { 55 | self.user_agent = user_agent.to_string(); 56 | } 57 | 58 | /// Make a specific method request. 59 | pub fn request(&self, method: Method, endpoint: &str) -> Result { 60 | let url = format!("{}{}", self.base_url, endpoint); 61 | let mut handle = self.shared_handle.borrow_mut(); 62 | handle.reset(); 63 | Request::new(handle, method, &url)?.with_user_agent(&self.user_agent) 64 | } 65 | 66 | /// High level HTTP **GET** method 67 | pub fn get(&self, endpoint: &str) -> Result { 68 | self.request(Method::Get, endpoint)?.send() 69 | } 70 | 71 | /// High level HTTP **POST** method 72 | pub fn post(&self, endpoint: &str, body: &S) -> Result { 73 | self.request(Method::Post, endpoint)? 74 | .with_json_body(body)? 75 | .send() 76 | } 77 | 78 | /// High level HTTP **PUT** method 79 | pub fn put(&self, endpoint: &str, body: &S) -> Result { 80 | self.request(Method::Put, endpoint)? 81 | .with_json_body(body)? 82 | .send() 83 | } 84 | 85 | /// High level HTTP **DELETE** method 86 | pub fn delete(&self, endpoint: &str) -> Result { 87 | self.request(Method::Delete, endpoint)?.send() 88 | } 89 | } 90 | 91 | /// The struct represents the HTTP request. 92 | pub struct Request<'a> { 93 | handle: RefMut<'a, curl::easy::Easy>, 94 | headers: curl::easy::List, 95 | url: String, 96 | body: Option>, 97 | } 98 | 99 | impl<'a> Request<'a> { 100 | pub fn new( 101 | mut handle: RefMut<'a, curl::easy::Easy>, 102 | method: Method, 103 | url: &str, 104 | ) -> Result> { 105 | match method { 106 | Method::Get => handle.get(true)?, 107 | Method::Head => { 108 | handle.get(true)?; 109 | handle.custom_request("HEAD")?; 110 | handle.nobody(true)?; 111 | } 112 | Method::Post => handle.custom_request("POST")?, 113 | Method::Put => handle.custom_request("PUT")?, 114 | Method::Delete => handle.custom_request("DELETE")?, 115 | } 116 | 117 | Ok(Request { 118 | handle, 119 | headers: curl::easy::List::new(), 120 | url: url.to_string(), 121 | body: None, 122 | }) 123 | } 124 | 125 | /// Set the HTTP header. 126 | pub fn with_header(mut self, key: &str, value: &str) -> Result> { 127 | self.headers.append(&format!("{}: {}", key, value))?; 128 | Ok(self) 129 | } 130 | 131 | /// Set custom User-Agent. 132 | pub fn with_user_agent(mut self, ua: &str) -> Result> { 133 | self.headers.append(&format!("User-Agent: {}", ua))?; 134 | Ok(self) 135 | } 136 | 137 | /// Set custom url arguments or querystring. 138 | pub fn with_arguments(mut self, args: &str) -> Result> { 139 | self.url = format!("{}?{}", self.url, args); 140 | Ok(self) 141 | } 142 | 143 | /// Set the JSON request body for the request. 144 | pub fn with_json_body(mut self, body: &S) -> Result> { 145 | let mut body_bytes: Vec = vec![]; 146 | // Serialize json object to bytes 147 | serde_json::to_writer(&mut body_bytes, &body).map_err(|_| RequestError::InvalidJsonBody)?; 148 | 149 | self.body = Some(body_bytes); 150 | self.headers.append("Content-Type: application/json")?; 151 | Ok(self) 152 | } 153 | 154 | /// Sends the request and reads the response body into the response object. 155 | pub fn send(mut self) -> Result { 156 | self.handle.http_headers(self.headers)?; 157 | self.handle.url(&self.url)?; 158 | 159 | match self.body { 160 | Some(ref body) => { 161 | let mut body: &[u8] = &body[..]; 162 | self.handle.upload(true)?; 163 | self.handle.in_filesize(body.len() as u64)?; 164 | handle_request(&mut self.handle, &mut |buffer| { 165 | body.read(buffer).unwrap_or(0) 166 | }) 167 | } 168 | None => handle_request(&mut self.handle, &mut |_| 0), 169 | } 170 | } 171 | } 172 | 173 | fn handle_request( 174 | handle: &mut curl::easy::Easy, 175 | read: &mut dyn FnMut(&mut [u8]) -> usize, 176 | ) -> Result { 177 | let mut response_body = vec![]; 178 | let mut response_headers = vec![]; 179 | 180 | { 181 | let mut handle = handle.transfer(); 182 | 183 | handle.read_function(move |buffer| Ok(read(buffer)))?; 184 | 185 | handle.write_function(|data| { 186 | Ok(match response_body.write_all(data) { 187 | Ok(_) => data.len(), 188 | Err(_) => 0, 189 | }) 190 | })?; 191 | 192 | handle.header_function(|data| { 193 | response_headers.push(String::from_utf8_lossy(data).into_owned()); 194 | true 195 | })?; 196 | handle.perform()?; 197 | } 198 | 199 | Ok(Response { 200 | status: handle.response_code()?, 201 | headers: response_headers, 202 | body: Some(response_body), 203 | }) 204 | } 205 | 206 | /// Type alias for **u32** http status. 207 | pub type HttpStatus = u32; 208 | 209 | /// The struct represents the HTTP response. 210 | #[derive(Clone, Debug)] 211 | pub struct Response { 212 | status: HttpStatus, 213 | headers: Vec, 214 | body: Option>, 215 | } 216 | 217 | impl Response { 218 | pub fn status(&self) -> HttpStatus { 219 | self.status 220 | } 221 | 222 | pub fn failed(&self) -> bool { 223 | self.status >= 400 && self.status <= 600 224 | } 225 | 226 | pub fn ok(&self) -> bool { 227 | !self.failed() 228 | } 229 | 230 | /// Deserialize the response body into the given type 231 | pub fn deserialize(&self) -> Result { 232 | if self.ok() { 233 | Ok(serde_json::from_reader(match self.body { 234 | Some(ref body) => body, 235 | None => &b""[..], 236 | }) 237 | .map_err(|_| RequestError::InvalidJson)?) 238 | } else { 239 | Err(RequestError::RequestFailed) 240 | } 241 | } 242 | } 243 | 244 | #[derive(Error, Debug)] 245 | pub enum RequestError { 246 | #[error(transparent)] 247 | CurlError(#[from] curl::Error), 248 | #[error("Request failed")] 249 | RequestFailed, 250 | #[error("Could not serialize value as JSON")] 251 | InvalidJsonBody, 252 | #[error("Could not parse JSON response")] 253 | InvalidJson, 254 | } 255 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 anyshortcut 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "ansi_term" 5 | version = "0.11.0" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 8 | dependencies = [ 9 | "winapi", 10 | ] 11 | 12 | [[package]] 13 | name = "anyhow" 14 | version = "1.0.35" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "2c0df63cb2955042487fad3aefd2c6e3ae7389ac5dc1beb28921de0b69f779d4" 17 | 18 | [[package]] 19 | name = "anyshortcut" 20 | version = "0.2.0" 21 | dependencies = [ 22 | "ansi_term", 23 | "anyhow", 24 | "chrono", 25 | "clap", 26 | "curl-http", 27 | "dirs", 28 | "itertools", 29 | "open", 30 | "serde", 31 | "serde_derive", 32 | "serde_json", 33 | "storage-derive", 34 | "thiserror", 35 | ] 36 | 37 | [[package]] 38 | name = "argon2rs" 39 | version = "0.2.5" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "3f67b0b6a86dae6e67ff4ca2b6201396074996379fba2b92ff649126f37cb392" 42 | dependencies = [ 43 | "blake2-rfc", 44 | "scoped_threadpool", 45 | ] 46 | 47 | [[package]] 48 | name = "arrayvec" 49 | version = "0.4.7" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "a1e964f9e24d588183fcb43503abda40d288c8657dfc27311516ce2f05675aef" 52 | dependencies = [ 53 | "nodrop", 54 | ] 55 | 56 | [[package]] 57 | name = "atty" 58 | version = "0.2.11" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" 61 | dependencies = [ 62 | "libc", 63 | "termion", 64 | "winapi", 65 | ] 66 | 67 | [[package]] 68 | name = "autocfg" 69 | version = "1.0.1" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 72 | 73 | [[package]] 74 | name = "backtrace" 75 | version = "0.3.9" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "89a47830402e9981c5c41223151efcced65a0510c13097c769cede7efb34782a" 78 | dependencies = [ 79 | "backtrace-sys", 80 | "cfg-if", 81 | "libc", 82 | "rustc-demangle", 83 | "winapi", 84 | ] 85 | 86 | [[package]] 87 | name = "backtrace-sys" 88 | version = "0.1.24" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "c66d56ac8dabd07f6aacdaf633f4b8262f5b3601a810a0dcddffd5c22c69daa0" 91 | dependencies = [ 92 | "cc", 93 | "libc", 94 | ] 95 | 96 | [[package]] 97 | name = "bitflags" 98 | version = "1.0.4" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" 101 | 102 | [[package]] 103 | name = "blake2-rfc" 104 | version = "0.2.18" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" 107 | dependencies = [ 108 | "arrayvec", 109 | "constant_time_eq", 110 | ] 111 | 112 | [[package]] 113 | name = "cc" 114 | version = "1.0.25" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "f159dfd43363c4d08055a07703eb7a3406b0dac4d0584d96965a3262db3c9d16" 117 | 118 | [[package]] 119 | name = "cfg-if" 120 | version = "0.1.5" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "0c4e7bb64a8ebb0d856483e1e682ea3422f883c5f5615a90d51a2c82fe87fdd3" 123 | 124 | [[package]] 125 | name = "chrono" 126 | version = "0.4.6" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "45912881121cb26fad7c38c17ba7daa18764771836b34fab7d3fbd93ed633878" 129 | dependencies = [ 130 | "num-integer", 131 | "num-traits", 132 | "time", 133 | ] 134 | 135 | [[package]] 136 | name = "clap" 137 | version = "2.32.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "b957d88f4b6a63b9d70d5f454ac8011819c6efa7727858f458ab71c756ce2d3e" 140 | dependencies = [ 141 | "ansi_term", 142 | "atty", 143 | "bitflags", 144 | "textwrap", 145 | "unicode-width", 146 | "vec_map", 147 | ] 148 | 149 | [[package]] 150 | name = "constant_time_eq" 151 | version = "0.1.3" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "8ff012e225ce166d4422e0e78419d901719760f62ae2b7969ca6b564d1b54a9e" 154 | 155 | [[package]] 156 | name = "curl" 157 | version = "0.4.34" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "e268162af1a5fe89917ae25ba3b0a77c8da752bdc58e7dbb4f15b91fbd33756e" 160 | dependencies = [ 161 | "curl-sys", 162 | "libc", 163 | "openssl-probe", 164 | "openssl-sys", 165 | "schannel", 166 | "socket2", 167 | "winapi", 168 | ] 169 | 170 | [[package]] 171 | name = "curl-http" 172 | version = "0.1.0" 173 | dependencies = [ 174 | "curl", 175 | "serde", 176 | "serde_json", 177 | "thiserror", 178 | ] 179 | 180 | [[package]] 181 | name = "curl-sys" 182 | version = "0.4.39+curl-7.74.0" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "07a8ce861e7b68a0b394e814d7ee9f1b2750ff8bd10372c6ad3bacc10e86f874" 185 | dependencies = [ 186 | "cc", 187 | "libc", 188 | "libz-sys", 189 | "openssl-sys", 190 | "pkg-config", 191 | "vcpkg", 192 | "winapi", 193 | ] 194 | 195 | [[package]] 196 | name = "dirs" 197 | version = "1.0.4" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "88972de891f6118092b643d85a0b28e0678e0f948d7f879aa32f2d5aafe97d2a" 200 | dependencies = [ 201 | "libc", 202 | "redox_users", 203 | "winapi", 204 | ] 205 | 206 | [[package]] 207 | name = "either" 208 | version = "1.5.0" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "3be565ca5c557d7f59e7cfcf1844f9e3033650c929c6566f511e8005f205c1d0" 211 | 212 | [[package]] 213 | name = "failure" 214 | version = "0.1.8" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" 217 | dependencies = [ 218 | "backtrace", 219 | "failure_derive", 220 | ] 221 | 222 | [[package]] 223 | name = "failure_derive" 224 | version = "0.1.8" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" 227 | dependencies = [ 228 | "proc-macro2 1.0.24", 229 | "quote 1.0.7", 230 | "syn 1.0.53", 231 | "synstructure", 232 | ] 233 | 234 | [[package]] 235 | name = "fuchsia-zircon" 236 | version = "0.3.3" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" 239 | dependencies = [ 240 | "bitflags", 241 | "fuchsia-zircon-sys", 242 | ] 243 | 244 | [[package]] 245 | name = "fuchsia-zircon-sys" 246 | version = "0.3.3" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" 249 | 250 | [[package]] 251 | name = "itertools" 252 | version = "0.7.8" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "f58856976b776fedd95533137617a02fb25719f40e7d9b01c7043cd65474f450" 255 | dependencies = [ 256 | "either", 257 | ] 258 | 259 | [[package]] 260 | name = "itoa" 261 | version = "0.4.3" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b" 264 | 265 | [[package]] 266 | name = "lazy_static" 267 | version = "1.1.0" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "ca488b89a5657b0a2ecd45b95609b3e848cf1755da332a0da46e2b2b1cb371a7" 270 | dependencies = [ 271 | "version_check", 272 | ] 273 | 274 | [[package]] 275 | name = "libc" 276 | version = "0.2.43" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "76e3a3ef172f1a0b9a9ff0dd1491ae5e6c948b94479a3021819ba7d860c8645d" 279 | 280 | [[package]] 281 | name = "libz-sys" 282 | version = "1.1.2" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655" 285 | dependencies = [ 286 | "cc", 287 | "libc", 288 | "pkg-config", 289 | "vcpkg", 290 | ] 291 | 292 | [[package]] 293 | name = "nodrop" 294 | version = "0.1.12" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "9a2228dca57108069a5262f2ed8bd2e82496d2e074a06d1ccc7ce1687b6ae0a2" 297 | 298 | [[package]] 299 | name = "num-integer" 300 | version = "0.1.39" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea" 303 | dependencies = [ 304 | "num-traits", 305 | ] 306 | 307 | [[package]] 308 | name = "num-traits" 309 | version = "0.2.6" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1" 312 | 313 | [[package]] 314 | name = "open" 315 | version = "1.2.2" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "eedfa0ca7b54d84d948bfd058b8f82e767d11f362dd78c36866fd1f69c175867" 318 | dependencies = [ 319 | "winapi", 320 | ] 321 | 322 | [[package]] 323 | name = "openssl-probe" 324 | version = "0.1.2" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" 327 | 328 | [[package]] 329 | name = "openssl-sys" 330 | version = "0.9.54" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "1024c0a59774200a555087a6da3f253a9095a5f344e353b212ac4c8b8e450986" 333 | dependencies = [ 334 | "autocfg", 335 | "cc", 336 | "libc", 337 | "pkg-config", 338 | "vcpkg", 339 | ] 340 | 341 | [[package]] 342 | name = "pkg-config" 343 | version = "0.3.14" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c" 346 | 347 | [[package]] 348 | name = "proc-macro2" 349 | version = "0.4.19" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "ffe022fb8c8bd254524b0b3305906c1921fa37a84a644e29079a9e62200c3901" 352 | dependencies = [ 353 | "unicode-xid 0.1.0", 354 | ] 355 | 356 | [[package]] 357 | name = "proc-macro2" 358 | version = "1.0.24" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" 361 | dependencies = [ 362 | "unicode-xid 0.2.1", 363 | ] 364 | 365 | [[package]] 366 | name = "quote" 367 | version = "0.6.8" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "dd636425967c33af890042c483632d33fa7a18f19ad1d7ea72e8998c6ef8dea5" 370 | dependencies = [ 371 | "proc-macro2 0.4.19", 372 | ] 373 | 374 | [[package]] 375 | name = "quote" 376 | version = "1.0.7" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" 379 | dependencies = [ 380 | "proc-macro2 1.0.24", 381 | ] 382 | 383 | [[package]] 384 | name = "rand" 385 | version = "0.4.3" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "8356f47b32624fef5b3301c1be97e5944ecdd595409cc5da11d05f211db6cfbd" 388 | dependencies = [ 389 | "fuchsia-zircon", 390 | "libc", 391 | "winapi", 392 | ] 393 | 394 | [[package]] 395 | name = "redox_syscall" 396 | version = "0.1.40" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" 399 | 400 | [[package]] 401 | name = "redox_termios" 402 | version = "0.1.1" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" 405 | dependencies = [ 406 | "redox_syscall", 407 | ] 408 | 409 | [[package]] 410 | name = "redox_users" 411 | version = "0.2.0" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "214a97e49be64fd2c86f568dd0cb2c757d2cc53de95b273b6ad0a1c908482f26" 414 | dependencies = [ 415 | "argon2rs", 416 | "failure", 417 | "rand", 418 | "redox_syscall", 419 | ] 420 | 421 | [[package]] 422 | name = "rustc-demangle" 423 | version = "0.1.9" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "bcfe5b13211b4d78e5c2cadfebd7769197d95c639c35a50057eb4c05de811395" 426 | 427 | [[package]] 428 | name = "ryu" 429 | version = "0.2.6" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "7153dd96dade874ab973e098cb62fcdbb89a03682e46b144fd09550998d4a4a7" 432 | 433 | [[package]] 434 | name = "schannel" 435 | version = "0.1.13" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "dc1fabf2a7b6483a141426e1afd09ad543520a77ac49bd03c286e7696ccfd77f" 438 | dependencies = [ 439 | "lazy_static", 440 | "winapi", 441 | ] 442 | 443 | [[package]] 444 | name = "scoped_threadpool" 445 | version = "0.1.9" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" 448 | 449 | [[package]] 450 | name = "serde" 451 | version = "1.0.79" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "84257ccd054dc351472528c8587b4de2dbf0dc0fe2e634030c1a90bfdacebaa9" 454 | 455 | [[package]] 456 | name = "serde_derive" 457 | version = "1.0.79" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "31569d901045afbff7a9479f793177fe9259819aff10ab4f89ef69bbc5f567fe" 460 | dependencies = [ 461 | "proc-macro2 0.4.19", 462 | "quote 0.6.8", 463 | "syn 0.15.14", 464 | ] 465 | 466 | [[package]] 467 | name = "serde_json" 468 | version = "1.0.27" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "59790990c5115d16027f00913e2e66de23a51f70422e549d2ad68c8c5f268f1c" 471 | dependencies = [ 472 | "itoa", 473 | "ryu", 474 | "serde", 475 | ] 476 | 477 | [[package]] 478 | name = "socket2" 479 | version = "0.3.8" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "c4d11a52082057d87cb5caa31ad812f4504b97ab44732cd8359df2e9ff9f48e7" 482 | dependencies = [ 483 | "cfg-if", 484 | "libc", 485 | "redox_syscall", 486 | "winapi", 487 | ] 488 | 489 | [[package]] 490 | name = "storage-derive" 491 | version = "0.1.0" 492 | dependencies = [ 493 | "quote 1.0.7", 494 | "syn 1.0.53", 495 | ] 496 | 497 | [[package]] 498 | name = "syn" 499 | version = "0.15.14" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "baaba45c6bf60fe29aaf241fa33306c0b75c801edea8378263a8f043b09a5634" 502 | dependencies = [ 503 | "proc-macro2 0.4.19", 504 | "quote 0.6.8", 505 | "unicode-xid 0.1.0", 506 | ] 507 | 508 | [[package]] 509 | name = "syn" 510 | version = "1.0.53" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "8833e20724c24de12bbaba5ad230ea61c3eafb05b881c7c9d3cfe8638b187e68" 513 | dependencies = [ 514 | "proc-macro2 1.0.24", 515 | "quote 1.0.7", 516 | "unicode-xid 0.2.1", 517 | ] 518 | 519 | [[package]] 520 | name = "synstructure" 521 | version = "0.12.4" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" 524 | dependencies = [ 525 | "proc-macro2 1.0.24", 526 | "quote 1.0.7", 527 | "syn 1.0.53", 528 | "unicode-xid 0.2.1", 529 | ] 530 | 531 | [[package]] 532 | name = "termion" 533 | version = "1.5.1" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" 536 | dependencies = [ 537 | "libc", 538 | "redox_syscall", 539 | "redox_termios", 540 | ] 541 | 542 | [[package]] 543 | name = "textwrap" 544 | version = "0.10.0" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "307686869c93e71f94da64286f9a9524c0f308a9e1c87a583de8e9c9039ad3f6" 547 | dependencies = [ 548 | "unicode-width", 549 | ] 550 | 551 | [[package]] 552 | name = "thiserror" 553 | version = "1.0.22" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "0e9ae34b84616eedaaf1e9dd6026dbe00dcafa92aa0c8077cb69df1fcfe5e53e" 556 | dependencies = [ 557 | "thiserror-impl", 558 | ] 559 | 560 | [[package]] 561 | name = "thiserror-impl" 562 | version = "1.0.22" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56" 565 | dependencies = [ 566 | "proc-macro2 1.0.24", 567 | "quote 1.0.7", 568 | "syn 1.0.53", 569 | ] 570 | 571 | [[package]] 572 | name = "time" 573 | version = "0.1.40" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "d825be0eb33fda1a7e68012d51e9c7f451dc1a69391e7fdc197060bb8c56667b" 576 | dependencies = [ 577 | "libc", 578 | "redox_syscall", 579 | "winapi", 580 | ] 581 | 582 | [[package]] 583 | name = "unicode-width" 584 | version = "0.1.5" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" 587 | 588 | [[package]] 589 | name = "unicode-xid" 590 | version = "0.1.0" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" 593 | 594 | [[package]] 595 | name = "unicode-xid" 596 | version = "0.2.1" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 599 | 600 | [[package]] 601 | name = "vcpkg" 602 | version = "0.2.6" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "def296d3eb3b12371b2c7d0e83bfe1403e4db2d7a0bba324a12b21c4ee13143d" 605 | 606 | [[package]] 607 | name = "vec_map" 608 | version = "0.8.1" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" 611 | 612 | [[package]] 613 | name = "version_check" 614 | version = "0.1.4" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "7716c242968ee87e5542f8021178248f267f295a5c4803beae8b8b7fd9bc6051" 617 | 618 | [[package]] 619 | name = "winapi" 620 | version = "0.3.5" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "773ef9dcc5f24b7d850d0ff101e542ff24c3b090a9768e03ff889fdef41f00fd" 623 | dependencies = [ 624 | "winapi-i686-pc-windows-gnu", 625 | "winapi-x86_64-pc-windows-gnu", 626 | ] 627 | 628 | [[package]] 629 | name = "winapi-i686-pc-windows-gnu" 630 | version = "0.4.0" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 633 | 634 | [[package]] 635 | name = "winapi-x86_64-pc-windows-gnu" 636 | version = "0.4.0" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 639 | --------------------------------------------------------------------------------