├── .gitignore ├── workflow ├── icon.png └── info.plist ├── Cargo.toml ├── src ├── registry │ ├── fuzzy.rs │ ├── mod.rs │ └── list.rs ├── main.rs └── index.rs ├── LICENSE-MIT ├── .github └── workflows │ └── build.yaml ├── README.md ├── Cargo.lock └── LICENSE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /workflow/crates-alfred-workflow 3 | -------------------------------------------------------------------------------- /workflow/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossmacarthur/crates.alfredworkflow/HEAD/workflow/icon.png -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crates-alfred-workflow" 3 | version = "0.6.1" 4 | authors = ["Ross MacArthur "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | publish = false 8 | 9 | [dependencies] 10 | anyhow = "1.0.86" 11 | constcat = "0.6.0" 12 | either = "1.13.0" 13 | fmutex = "0.1.0" 14 | home = "0.5.9" 15 | libc = "0.2.155" 16 | log = { version = "0.4.22", features = ["std"] } 17 | powerpack = { version = "0.6.3", features = ["detach", "logger"] } 18 | semver = { version = "1.0.23", features = ["serde"] } 19 | serde = { version = "1.0.203", features = ["derive"] } 20 | serde_json = "1.0.120" 21 | -------------------------------------------------------------------------------- /src/registry/fuzzy.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use std::path::PathBuf; 3 | 4 | pub fn eq(a: &str, b: &str) -> bool { 5 | let a = a.as_bytes(); 6 | let b = b.as_bytes(); 7 | if a.len() != b.len() { 8 | return false; 9 | } 10 | a.iter().zip(b.iter()).all(|cmp| match cmp { 11 | (b'-', b'_') | (b'_', b'-') => true, 12 | (a, b) => a == b, 13 | }) 14 | } 15 | 16 | pub fn starts_with(a: &str, b: &str) -> bool { 17 | if b.len() > a.len() { 18 | return false; 19 | } 20 | eq(&a[..b.len()], b) 21 | } 22 | 23 | #[allow(clippy::ptr_arg)] 24 | pub fn cmp(a: &PathBuf, b: &PathBuf) -> Ordering { 25 | let replace = |e: &PathBuf| e.file_name().unwrap().to_str().map(|s| s.replace('_', "-")); 26 | replace(a).cmp(&replace(b)) 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. 18 | -------------------------------------------------------------------------------- /src/registry/mod.rs: -------------------------------------------------------------------------------- 1 | mod fuzzy; 2 | mod list; 3 | 4 | use std::cmp::Ordering; 5 | use std::fs; 6 | use std::path::Path; 7 | 8 | use anyhow::Result; 9 | use semver::Version; 10 | use serde::Deserialize; 11 | 12 | use crate::index::FILES; 13 | use crate::Package; 14 | 15 | #[derive(Debug, Deserialize, PartialEq, Eq)] 16 | struct PackageVersion { 17 | name: String, 18 | vers: Version, 19 | yanked: bool, 20 | } 21 | 22 | pub fn walk(query: &str) -> Result + '_> { 23 | let index = list::all(FILES.index_dir(), query)? 24 | .into_iter() 25 | .filter_map(|path| match make_package(&path) { 26 | Ok(pkg) => Some(pkg), 27 | Err(err) => { 28 | eprintln!("Error: {}, {:?}", path.display(), err); 29 | None 30 | } 31 | }); 32 | 33 | Ok(index) 34 | } 35 | 36 | fn make_package(path: &Path) -> Result { 37 | let contents = fs::read_to_string(path)?; 38 | let PackageVersion { name, vers, .. } = contents 39 | .lines() 40 | .map(serde_json::from_str) 41 | .collect::, _>>()? 42 | .into_iter() 43 | .max_by(cmp) 44 | .unwrap(); 45 | Ok(Package::Registry { 46 | name, 47 | version: vers.to_string(), 48 | }) 49 | } 50 | 51 | /// Order by unyanked then version number. 52 | fn cmp(a: &PackageVersion, b: &PackageVersion) -> Ordering { 53 | (!a.yanked, &a.vers).cmp(&(!b.yanked, &b.vers)) 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | RUSTFLAGS: --deny warnings 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | toolchain: [stable, beta, nightly] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: dtolnay/rust-toolchain@master 20 | with: 21 | toolchain: ${{ matrix.toolchain }} 22 | components: clippy, rustfmt 23 | 24 | - name: Rustfmt 25 | run: cargo fmt -- --check 26 | 27 | - name: Clippy 28 | continue-on-error: ${{ matrix.toolchain == 'nightly' }} 29 | run: cargo clippy --workspace --all-targets 30 | 31 | check-version: 32 | needs: build 33 | if: startsWith(github.ref, 'refs/tags/') 34 | 35 | runs-on: ubuntu-latest 36 | 37 | steps: 38 | - uses: actions/checkout@v4 39 | 40 | - name: Calculate version from tag 41 | id: version 42 | run: echo "value=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 43 | 44 | - name: Check tag against package version 45 | run: grep '^version = "${{ steps.version.outputs.value }}"$' Cargo.toml 46 | 47 | release: 48 | needs: check-version 49 | runs-on: macos-latest 50 | 51 | strategy: 52 | matrix: 53 | target: [x86_64-apple-darwin, aarch64-apple-darwin] 54 | 55 | steps: 56 | - uses: actions/checkout@v4 57 | 58 | - uses: extractions/setup-crate@v1 59 | with: 60 | owner: rossmacarthur 61 | name: powerpack 62 | 63 | - uses: dtolnay/rust-toolchain@stable 64 | with: 65 | target: ${{ matrix.target }} 66 | 67 | - name: Calculate version from tag 68 | id: version 69 | run: echo "value=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 70 | 71 | - name: Archive 72 | id: archive 73 | run: | 74 | archive=crates-${{ steps.version.outputs.value }}-${{ matrix.target }}.alfredworkflow 75 | powerpack package --target ${{ matrix.target }} 76 | mv target/workflow/crates.alfredworkflow "$archive" 77 | echo "path=$archive" >> $GITHUB_OUTPUT 78 | 79 | - uses: softprops/action-gh-release@v1 80 | env: 81 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 82 | with: 83 | files: ${{ steps.archive.outputs.path }} 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # crates.alfredworkflow 2 | 3 | [![Build Status](https://badgers.space/github/checks/rossmacarthur/crates.alfredworkflow?label=build)](https://github.com/rossmacarthur/crates.alfredworkflow/actions/workflows/build.yaml?query=branch%3Atrunk) 4 | [![Latest Release](https://badgers.space/github/release/rossmacarthur/crates.alfredworkflow)](https://github.com/rossmacarthur/crates.alfredworkflow/releases/latest) 5 | 6 | 📦 Alfred workflow to search Rust crates. 7 | 8 | Screenshot 9 | 10 | ## Features 11 | 12 | - Search for crates by name. 13 | - Open the crate in the default browser. You can use modifiers to change the 14 | URL that is navigated to. 15 | - **⏎**: opens the crate in https://crates.io. 16 | - **⌥ ⏎**: opens the crate in https://lib.rs. 17 | - **⇧ ⏎**: opens the crate in https://docs.rs. 18 | - Manages a local [Crates.io index][crates.io-index]. 19 | - Shortcuts for `std`, `core`, and `alloc` crates. 20 | - Blazingly fast 🤸. 21 | 22 | ## 📦 Installation 23 | 24 | ### Pre-packaged 25 | 26 | Grab the latest release from 27 | [the releases page](https://github.com/rossmacarthur/crates.alfredworkflow/releases). 28 | 29 | Because the release contains an executable binary later versions of macOS will 30 | mark it as untrusted and Alfred won't be able to execute it. You can run the 31 | following to explicitly trust the release before installing to Alfred. 32 | ```sh 33 | xattr -c ~/Downloads/crates-*-x86_64-apple-darwin.alfredworkflow 34 | ``` 35 | 36 | ### Building from source 37 | 38 | This workflow is written in Rust, so to install it from source you will first 39 | need to install Rust and Cargo using [rustup](https://rustup.rs/). Then install 40 | [powerpack](https://github.com/rossmacarthur/powerpack). Then you can run the 41 | following to build an `.alfredworkflow` file. 42 | 43 | ```sh 44 | git clone https://github.com/rossmacarthur/crates.alfredworkflow.git 45 | cd crates.alfredworkflow 46 | powerpack package 47 | ``` 48 | 49 | The release will be available at `target/workflow/crates.alfredworkflow`. 50 | 51 | ## Configuration 52 | 53 | The workflow will automatically maintain a local index 54 | [crates.io](crates.io-index) index. The index will be stored in the workflow 55 | cache directory. The update frequency can be configured be setting the index 56 | update interval. 57 | 58 | ## Debugging 59 | 60 | If you are experiencing issues you can debug the workflow in the following way: 61 | 62 | 1. Inspect the output of the workflow by enabling debug mode in Alfred for the 63 | workflow. 64 | 65 | 2. The index is maintained asynchronously and will output any updates and errors 66 | to a log file in the Alfred cache directly under the bundle name 67 | `io.macarthur.ross.crates`. The default Alfred cache directory is 68 | `~/Library/Caches/com.runningwithcrayons.Alfred/Workflow\ Data/io.macarthur.ross.crates`. 69 | Expected logs will look like the following. 70 | ``` 71 | [2022-01-31T11:10:24] [INFO] updated index ./crates.io-index: HEAD is now at 603fff76b2 Updating crate `midpoint#0.1.2` 72 | [2022-02-04T15:06:07] [INFO] updated index ./crates.io-index: HEAD is now at 93d0942359 Updating crate `os_info_cli#2.0.0` 73 | [2022-02-06T14:41:29] [INFO] updated index ./crates.io-index: HEAD is now at 5864e33978 Updating crate `agsol-gold-bot#0.0.0-alpha.2` 74 | ``` 75 | 76 | 3. Open an [issue](https://github.com/rossmacarthur/crates.alfredworkflow/issues/new) 77 | on this repo. 78 | 79 | [crates.io-index]: https://github.com/rust-lang/crates.io-index 80 | 81 | ## License 82 | 83 | This project is distributed under the terms of both the MIT license and the 84 | Apache License (Version 2.0). 85 | 86 | See [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) for details. 87 | -------------------------------------------------------------------------------- /workflow/info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | io.macarthur.ross.crates 7 | category 8 | Productivity 9 | connections 10 | 11 | 6D932454-F130-4AC9-B3E1-53BEE5578D22 12 | 13 | 14 | destinationuid 15 | D23842F2-2271-4DAE-AC91-FD8FFA716ADB 16 | modifiers 17 | 0 18 | modifiersubtext 19 | 20 | vitoclose 21 | 22 | 23 | 24 | 25 | createdby 26 | Ross MacArthur 27 | description 28 | Search for and browse to Rust crates 29 | disabled 30 | 31 | name 32 | crates 33 | objects 34 | 35 | 36 | config 37 | 38 | browser 39 | 40 | skipqueryencode 41 | 42 | skipvarencode 43 | 44 | spaces 45 | 46 | url 47 | {query} 48 | 49 | type 50 | alfred.workflow.action.openurl 51 | uid 52 | D23842F2-2271-4DAE-AC91-FD8FFA716ADB 53 | version 54 | 1 55 | 56 | 57 | config 58 | 59 | alfredfiltersresults 60 | 61 | alfredfiltersresultsmatchmode 62 | 0 63 | argumenttreatemptyqueryasnil 64 | 65 | argumenttrimmode 66 | 0 67 | argumenttype 68 | 1 69 | escaping 70 | 0 71 | keyword 72 | crate 73 | queuedelaycustom 74 | 1 75 | queuedelayimmediatelyinitially 76 | 77 | queuedelaymode 78 | 0 79 | queuemode 80 | 2 81 | runningsubtext 82 | Loading... 83 | script 84 | 85 | scriptargtype 86 | 0 87 | scriptfile 88 | crates-alfred-workflow 89 | subtext 90 | 91 | title 92 | Search for crates 93 | type 94 | 8 95 | withspace 96 | 97 | 98 | type 99 | alfred.workflow.input.scriptfilter 100 | uid 101 | 6D932454-F130-4AC9-B3E1-53BEE5578D22 102 | version 103 | 3 104 | 105 | 106 | readme 107 | ## Features 108 | 109 | - Search for crates by name. 110 | - Open the crate in the default browser. You can use modifiers to change the 111 | URL that is navigated to. 112 | - ⏎: opens the crate in https://crates.io. 113 | - ⌥ ⏎: opens the crate in https://lib.rs. 114 | - ⇧ ⏎: opens the crate in https://docs.rs. 115 | - Shortcuts for `std`, `core`, and `alloc` crates. 116 | - Blazingly fast 🤸. 117 | 118 | ## Configuration 119 | 120 | The workflow will automatically maintain a local index crates.io index. The 121 | index will be stored in the workflow cache directory. The update frequency can 122 | be configured be setting the `INDEX_UPDATE_INTERVAL_MINS` environment variable. 123 | The default is to update every 6 hours. 124 | uidata 125 | 126 | 6D932454-F130-4AC9-B3E1-53BEE5578D22 127 | 128 | xpos 129 | 50 130 | ypos 131 | 50 132 | 133 | D23842F2-2271-4DAE-AC91-FD8FFA716ADB 134 | 135 | xpos 136 | 225 137 | ypos 138 | 50 139 | 140 | 141 | userconfigurationconfig 142 | 143 | 144 | config 145 | 146 | default 147 | 360 148 | placeholder 149 | 150 | required 151 | 152 | trim 153 | 154 | 155 | description 156 | This is the interval to check and update the local Crates.io index. It is specified in minutes. The default is 6 hours. 157 | label 158 | Index update interval 159 | type 160 | textfield 161 | variable 162 | crates_index_update_interval 163 | 164 | 165 | version 166 | 0.6.1 167 | webaddress 168 | https://github.com/rossmacarthur/crates.alfredworkflow 169 | 170 | 171 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod index; 2 | mod registry; 3 | 4 | use std::env; 5 | use std::iter; 6 | 7 | use crate::index::IndexStatus; 8 | use anyhow::Result; 9 | use constcat::concat; 10 | use either::Either; 11 | use powerpack::logger; 12 | use powerpack::{Item, Key, Modifier}; 13 | 14 | const PKG_NAME: &str = env!("CARGO_PKG_NAME"); 15 | const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); 16 | const LOG_FILE: &str = concat!(PKG_NAME, "-", PKG_VERSION, ".log"); 17 | 18 | #[derive(Debug)] 19 | pub enum Package { 20 | Builtin { name: &'static str }, 21 | Registry { name: String, version: String }, 22 | } 23 | 24 | fn builtins(query: &str) -> impl Iterator + '_ { 25 | ["alloc", "core", "std"] 26 | .iter() 27 | .filter(move |name| name.starts_with(query)) 28 | .map(|name| Package::Builtin { name }) 29 | } 30 | 31 | /// Returns an Alfred item for when no query has been typed yet. 32 | fn empty() -> Item { 33 | Item::new("Search for crates") 34 | .subtitle("Open Crates.io →") 35 | .arg("https://crates.io") 36 | .modifier( 37 | Modifier::new(Key::Option) 38 | .subtitle("Open Lib.rs →") 39 | .arg("https://lib.rs"), 40 | ) 41 | .modifier( 42 | Modifier::new(Key::Shift) 43 | .subtitle("Open Docs.rs →") 44 | .arg("https://docs.rs"), 45 | ) 46 | } 47 | 48 | /// Returns an Alfred item for when the query doesn't match any crates. 49 | fn default(query: &str) -> Item { 50 | Item::new(format!("Search for '{query}'")) 51 | .subtitle(format!("Search Crates.io for '{query}' →")) 52 | .arg(format!("https://crates.io/search?q={query}")) 53 | .modifier( 54 | Modifier::new(Key::Option) 55 | .subtitle(format!("Search Lib.rs for '{query}' →")) 56 | .arg(format!("https://lib.rs/search?q={query}")), 57 | ) 58 | .modifier( 59 | Modifier::new(Key::Shift) 60 | .subtitle(format!("Search Docs.rs for '{query}' →")) 61 | .arg(format!("https://docs.rs/releases/search?query={query}")), 62 | ) 63 | } 64 | 65 | /// Converts a registry package to an Alfred item. 66 | fn to_item(pkg: Package) -> Item { 67 | match pkg { 68 | Package::Builtin { name } => Item::new(name) 69 | .subtitle("Open official documentation (stable) →") 70 | .arg(format!("https://doc.rust-lang.org/stable/{name}/")) 71 | .autocomplete(name) 72 | .modifier( 73 | Modifier::new(Key::Shift) 74 | .subtitle("Open official documentation (nightly) →") 75 | .arg(format!("https://doc.rust-lang.org/nightly/{name}/")), 76 | ) 77 | .modifier( 78 | Modifier::new(Key::Option) 79 | .subtitle("Open official documentation (beta) →") 80 | .arg(format!("https://doc.rust-lang.org/beta/{name}/")), 81 | ), 82 | Package::Registry { name, version } => Item::new(format!("{name} v{version}")) 83 | .subtitle("Open in Crates.io →") 84 | .arg(format!("https://crates.io/crates/{name}")) 85 | .autocomplete(&name) 86 | .modifier( 87 | Modifier::new(Key::Option) 88 | .subtitle("Open in Lib.rs →") 89 | .arg(format!("https://lib.rs/crates/{name}")), 90 | ) 91 | .modifier( 92 | Modifier::new(Key::Shift) 93 | .subtitle("Open in Docs.rs →") 94 | .arg(format!("https://docs.rs/{name}")), 95 | ), 96 | } 97 | } 98 | 99 | /// Appends an item indicating the status of the index (downloading or updating). 100 | fn append_index_status(items: &mut Vec, status: IndexStatus) { 101 | match status { 102 | IndexStatus::Ready => {} 103 | IndexStatus::Downloading => items.push( 104 | Item::new("Downloading index...") 105 | .subtitle("The local Crates.io index is being downloaded. This may take a while.") 106 | .valid(false), 107 | ), 108 | IndexStatus::Updating => items.push( 109 | Item::new("Updating index...") 110 | .subtitle("The local Crates.io index is being updated") 111 | .valid(false), 112 | ), 113 | }; 114 | } 115 | 116 | fn main() -> Result<()> { 117 | logger::Builder::new().filename(LOG_FILE).try_init()?; 118 | 119 | let arg = env::args() 120 | .nth(1) 121 | .as_deref() 122 | .map(str::trim) 123 | .map(str::to_ascii_lowercase); 124 | 125 | let index_status = index::check()?; 126 | 127 | let mut items = Vec::from_iter(match arg.as_deref() { 128 | None | Some("") => Either::Left(iter::once(empty())), 129 | Some(query) => { 130 | let iter = builtins(query) 131 | .chain(registry::walk(query)?.take(10)) 132 | .map(to_item) 133 | .chain(iter::once(default(query))); 134 | Either::Right(iter) 135 | } 136 | }); 137 | 138 | append_index_status(&mut items, index_status); 139 | 140 | powerpack::output(items)?; 141 | 142 | Ok(()) 143 | } 144 | -------------------------------------------------------------------------------- /src/registry/list.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io; 3 | use std::path::{Path, PathBuf}; 4 | 5 | use crate::registry::fuzzy; 6 | 7 | trait JoinAll { 8 | fn join_all(&self, all: &[impl AsRef]) -> PathBuf; 9 | } 10 | 11 | impl JoinAll for Path { 12 | fn join_all(&self, all: &[impl AsRef]) -> PathBuf { 13 | let mut p = self.to_owned(); 14 | for segment in all { 15 | p.push(segment); 16 | } 17 | p 18 | } 19 | } 20 | 21 | enum Prefix<'a> { 22 | /// Just a single path to check. 23 | One(PathBuf), 24 | /// A directory to search with no recursion. 25 | List(PathBuf), 26 | /// A directory to search and recurse directories. 27 | Recurse(PathBuf), 28 | /// A directory to search and recurse once the matching directories. 29 | RecurseOnceMatching(PathBuf, &'a str), 30 | /// A directory to search and recurse twice the matching directories. 31 | RecurseTwiceMatching(PathBuf, &'a str), 32 | } 33 | 34 | fn prefixes(index: PathBuf, query: &str) -> Vec> { 35 | match query.len() { 36 | 0 => vec![], 37 | 1 => { 38 | vec![ 39 | // ./1/a 40 | Prefix::One(index.join_all(&["1", query])), 41 | // ./2/{q*} 42 | Prefix::List(index.join("2")), 43 | // ./3/a/{q*} 44 | Prefix::List(index.join_all(&["3", query])), 45 | // ./a*/*/{q*} 46 | Prefix::RecurseTwiceMatching(index, query), 47 | ] 48 | } 49 | 2 => { 50 | vec![ 51 | // ./2/ab 52 | Prefix::One(index.join_all(&["2", query])), 53 | // ./3/a/{q*} 54 | Prefix::List(index.join_all(&["3", &query[0..1]])), 55 | // ./ab/*/{q*} 56 | Prefix::Recurse(index.join(query)), 57 | ] 58 | } 59 | 3 => { 60 | vec![ 61 | // ./3/a/abc 62 | Prefix::One(index.join_all(&["3", &query[0..1], query])), 63 | // ./ab/c*/{q*} 64 | Prefix::RecurseOnceMatching(index.join(&query[0..2]), &query[2..3]), 65 | ] 66 | } 67 | _ => { 68 | vec![ 69 | // ./ab/cd/{q*} 70 | Prefix::List(index.join_all(&[&query[0..2], &query[2..4]])), 71 | ] 72 | } 73 | } 74 | } 75 | 76 | fn matches(entry: &fs::DirEntry, query: &str) -> bool { 77 | entry 78 | .file_name() 79 | .into_string() 80 | .map(|f| fuzzy::starts_with(&f, query)) 81 | .unwrap_or(false) 82 | } 83 | 84 | fn read_dir(dir: impl AsRef) -> io::Result> { 85 | match fs::read_dir(dir) { 86 | Ok(r) => Ok(Some(r)), 87 | Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None), 88 | Err(err) if err.raw_os_error() == Some(libc::ENOTDIR) => Ok(None), 89 | Err(err) => Err(err), 90 | } 91 | } 92 | 93 | fn append_files(r: &mut Vec, dir: impl AsRef, query: &str) -> io::Result<()> { 94 | if let Some(rd) = read_dir(dir)? { 95 | for entry in rd { 96 | let entry = entry?; 97 | if matches(&entry, query) { 98 | r.push(entry.path()); 99 | } 100 | } 101 | } 102 | Ok(()) 103 | } 104 | 105 | /// Returns a sorted list all the registry files matching the given query. 106 | pub fn all(index: impl Into, query: &str) -> io::Result> { 107 | let mut r = Vec::new(); 108 | 109 | for prefix in prefixes(index.into(), query) { 110 | match prefix { 111 | Prefix::One(path) => { 112 | if path.exists() { 113 | r.push(path) 114 | } 115 | } 116 | Prefix::List(path) => { 117 | append_files(&mut r, path, query)?; 118 | } 119 | Prefix::Recurse(path) => { 120 | if let Some(rd) = read_dir(path)? { 121 | for entry in rd { 122 | append_files(&mut r, entry?.path(), query)?; 123 | } 124 | } 125 | } 126 | Prefix::RecurseOnceMatching(path, q) => { 127 | if let Some(rd) = read_dir(path)? { 128 | for entry in rd { 129 | let entry = entry?; 130 | if matches(&entry, q) { 131 | append_files(&mut r, entry.path(), query)?; 132 | } 133 | } 134 | } 135 | } 136 | Prefix::RecurseTwiceMatching(path, q) => { 137 | if let Some(rd) = read_dir(path)? { 138 | for entry in rd { 139 | let entry = entry?; 140 | if matches(&entry, q) { 141 | if let Some(rd) = read_dir(entry.path())? { 142 | for entry in rd { 143 | append_files(&mut r, entry?.path(), query)? 144 | } 145 | } 146 | } 147 | } 148 | } 149 | } 150 | } 151 | } 152 | 153 | r.sort_by(fuzzy::cmp); 154 | 155 | Ok(r) 156 | } 157 | -------------------------------------------------------------------------------- /src/index.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io; 3 | use std::path::{Path, PathBuf}; 4 | use std::process; 5 | use std::sync::LazyLock; 6 | use std::time::Duration; 7 | 8 | use anyhow::{bail, Context, Result}; 9 | use powerpack::detach; 10 | use powerpack::env; 11 | 12 | const CRATES_IO_INDEX: &str = "https://github.com/rust-lang/crates.io-index"; 13 | pub static FILES: LazyLock = LazyLock::new(Files::new); 14 | 15 | pub enum IndexStatus { 16 | Ready, 17 | Downloading, 18 | Updating, 19 | } 20 | 21 | pub struct Files { 22 | cache_dir: PathBuf, 23 | index_dir: PathBuf, 24 | update_file: PathBuf, 25 | } 26 | 27 | impl Files { 28 | fn new() -> Self { 29 | let cache_dir = env::workflow_cache().unwrap_or_else(|| { 30 | let bundle_id = env::workflow_bundle_id() 31 | .unwrap_or_else(|| String::from("io.macarthur.ross.crates")); 32 | home::home_dir() 33 | .unwrap() 34 | .join("Library/Caches/com.runningwithcrayons.Alfred/Workflow Data") 35 | .join(&*bundle_id) 36 | }); 37 | 38 | let index_dir = cache_dir.join("crates.io-index"); 39 | let update_file = index_dir.join(".last-modified"); 40 | 41 | Self { 42 | cache_dir, 43 | index_dir, 44 | update_file, 45 | } 46 | } 47 | 48 | pub fn cache_dir(&self) -> &Path { 49 | &self.cache_dir 50 | } 51 | 52 | pub fn index_dir(&self) -> &Path { 53 | &self.index_dir 54 | } 55 | 56 | fn update_file(&self) -> &Path { 57 | &self.update_file 58 | } 59 | } 60 | 61 | fn git() -> process::Command { 62 | let mut cmd = process::Command::new("git"); 63 | cmd.stdin(process::Stdio::null()); 64 | cmd.stdout(process::Stdio::piped()); 65 | cmd.stderr(process::Stdio::null()); 66 | cmd 67 | } 68 | 69 | fn git_clone(url: &str, path: impl AsRef) -> Result<()> { 70 | let output = git() 71 | .args(["clone", "--depth", "1"]) 72 | .arg(url) 73 | .arg(path.as_ref()) 74 | .output()?; 75 | if !output.status.success() { 76 | bail!("failed to run `git clone` command"); 77 | } 78 | Ok(()) 79 | } 80 | 81 | fn git_fetch(path: impl AsRef) -> Result<()> { 82 | let output = git().arg("-C").arg(path.as_ref()).arg("fetch").output()?; 83 | if !output.status.success() { 84 | bail!("failed to run `git fetch` command"); 85 | } 86 | Ok(()) 87 | } 88 | 89 | fn git_reset(path: impl AsRef) -> Result { 90 | let output = git() 91 | .arg("-C") 92 | .arg(path.as_ref()) 93 | .args(["reset", "--hard", "origin/HEAD"]) 94 | .output()?; 95 | if !output.status.success() { 96 | bail!("failed to run `git reset` command"); 97 | } 98 | Ok(String::from_utf8(output.stdout)?.trim().into()) 99 | } 100 | 101 | fn download() -> Result<()> { 102 | maybe_run(|| { 103 | let tmp = FILES.index_dir().with_file_name("~crates.io-index"); 104 | fs::remove_dir_all(&tmp).ok(); 105 | git_clone(CRATES_IO_INDEX, &tmp)?; 106 | fs::rename(&tmp, FILES.index_dir())?; 107 | fs::File::create(FILES.update_file())?; 108 | log::info!("downloaded index to ./crates.io-index"); 109 | Ok(()) 110 | }) 111 | } 112 | 113 | fn update() -> Result<()> { 114 | maybe_run(|| { 115 | git_fetch(FILES.index_dir())?; 116 | let output = git_reset(FILES.index_dir())?; 117 | fs::File::create(FILES.update_file())?; 118 | log::info!("updated index ./crates.io-index: {}", output); 119 | Ok(()) 120 | }) 121 | } 122 | 123 | fn maybe_run(f: F) -> Result<()> 124 | where 125 | F: FnOnce() -> Result<()>, 126 | { 127 | if let Some(_guard) = lock_cache_dir()? { 128 | f()?; 129 | } 130 | Ok(()) 131 | } 132 | 133 | /// Checks that the Crates.io index is okay and returns the path to it. 134 | /// 135 | /// This function will spawn a subprocess to clone it if it missing or update it 136 | /// if it is out of date. 137 | pub fn check() -> Result { 138 | let index_dir_exists = FILES.index_dir().exists(); 139 | 140 | let index_status = { 141 | if index_dir_exists { 142 | match lock_cache_dir()? { 143 | Some(_guard) => IndexStatus::Ready, 144 | None => IndexStatus::Updating, 145 | } 146 | } else { 147 | IndexStatus::Downloading 148 | } 149 | }; 150 | 151 | if index_dir_exists { 152 | let needs_update = match fs::metadata(FILES.update_file()) { 153 | Ok(metadata) => metadata.modified()?.elapsed()? > update_interval(), 154 | Err(err) if err.kind() == io::ErrorKind::NotFound => true, 155 | Err(err) => return Err(err.into()), 156 | }; 157 | if needs_update { 158 | detach::spawn(|| { 159 | if let Err(err) = update() { 160 | log::error!("{}", detach::format_err(err.as_ref())); 161 | } 162 | })?; 163 | } 164 | } else { 165 | detach::spawn(|| { 166 | if let Err(err) = download() { 167 | log::error!("{}", detach::format_err(err.as_ref())); 168 | } 169 | })?; 170 | } 171 | 172 | Ok(index_status) 173 | } 174 | 175 | fn update_interval() -> Duration { 176 | let mins = env::var("crates_index_update_interval") 177 | .and_then(|m| m.parse().ok()) 178 | .unwrap_or(6 * 60); 179 | Duration::from_secs(mins * 60) 180 | } 181 | 182 | fn lock_cache_dir() -> Result> { 183 | fmutex::try_lock(FILES.cache_dir()) 184 | .with_context(|| format!("failed to lock `{}`", FILES.cache_dir().display())) 185 | } 186 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.96" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4" 10 | 11 | [[package]] 12 | name = "constcat" 13 | version = "0.6.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "5ffb5df6dd5dadb422897e8132f415d7a054e3cd757e5070b663f75bea1840fb" 16 | 17 | [[package]] 18 | name = "crates-alfred-workflow" 19 | version = "0.6.1" 20 | dependencies = [ 21 | "anyhow", 22 | "constcat", 23 | "either", 24 | "fmutex", 25 | "home", 26 | "libc", 27 | "log", 28 | "powerpack", 29 | "semver", 30 | "serde", 31 | "serde_json", 32 | ] 33 | 34 | [[package]] 35 | name = "either" 36 | version = "1.13.0" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 39 | 40 | [[package]] 41 | name = "fmutex" 42 | version = "0.1.0" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "01e84c17070603126a7b0cd07d0ecc8e8cca4d15b67934ac2740286a84f3086c" 45 | dependencies = [ 46 | "libc", 47 | ] 48 | 49 | [[package]] 50 | name = "home" 51 | version = "0.5.11" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 54 | dependencies = [ 55 | "windows-sys", 56 | ] 57 | 58 | [[package]] 59 | name = "itoa" 60 | version = "1.0.14" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 63 | 64 | [[package]] 65 | name = "jiff" 66 | version = "0.2.1" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "3590fea8e9e22d449600c9bbd481a8163bef223e4ff938e5f55899f8cf1adb93" 69 | dependencies = [ 70 | "jiff-tzdb-platform", 71 | "log", 72 | "portable-atomic", 73 | "portable-atomic-util", 74 | "serde", 75 | "windows-sys", 76 | ] 77 | 78 | [[package]] 79 | name = "jiff-tzdb" 80 | version = "0.1.2" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "cf2cec2f5d266af45a071ece48b1fb89f3b00b2421ac3a5fe10285a6caaa60d3" 83 | 84 | [[package]] 85 | name = "jiff-tzdb-platform" 86 | version = "0.1.2" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "a63c62e404e7b92979d2792352d885a7f8f83fd1d0d31eea582d77b2ceca697e" 89 | dependencies = [ 90 | "jiff-tzdb", 91 | ] 92 | 93 | [[package]] 94 | name = "libc" 95 | version = "0.2.169" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 98 | 99 | [[package]] 100 | name = "log" 101 | version = "0.4.25" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" 104 | 105 | [[package]] 106 | name = "memchr" 107 | version = "2.7.4" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 110 | 111 | [[package]] 112 | name = "portable-atomic" 113 | version = "1.10.0" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" 116 | 117 | [[package]] 118 | name = "portable-atomic-util" 119 | version = "0.2.4" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 122 | dependencies = [ 123 | "portable-atomic", 124 | ] 125 | 126 | [[package]] 127 | name = "powerpack" 128 | version = "0.6.3" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "2ef833f5a2c19c41047bed384282ef62bfd9a71df17e2cebc8597b9e445951e5" 131 | dependencies = [ 132 | "powerpack-detach", 133 | "powerpack-env", 134 | "powerpack-logger", 135 | "serde", 136 | "serde_json", 137 | ] 138 | 139 | [[package]] 140 | name = "powerpack-detach" 141 | version = "0.6.3" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "21c88a6c1533c248c7b03aea7f5ee5909b792d9af183cfe9b79e1e478d366dfe" 144 | dependencies = [ 145 | "libc", 146 | "log", 147 | ] 148 | 149 | [[package]] 150 | name = "powerpack-env" 151 | version = "0.6.3" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "d9aeac46b42ad05b18e0edff109fb4bc42b7e81c9789a8b5a25bd47e40f88289" 154 | dependencies = [ 155 | "home", 156 | ] 157 | 158 | [[package]] 159 | name = "powerpack-logger" 160 | version = "0.6.3" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "66434226766ce5d224cf1b4056c9dc6ed00dc402f65d7899ed67cc22166573b7" 163 | dependencies = [ 164 | "home", 165 | "jiff", 166 | "log", 167 | "powerpack-env", 168 | "thiserror", 169 | ] 170 | 171 | [[package]] 172 | name = "proc-macro2" 173 | version = "1.0.93" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 176 | dependencies = [ 177 | "unicode-ident", 178 | ] 179 | 180 | [[package]] 181 | name = "quote" 182 | version = "1.0.38" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 185 | dependencies = [ 186 | "proc-macro2", 187 | ] 188 | 189 | [[package]] 190 | name = "ryu" 191 | version = "1.0.19" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" 194 | 195 | [[package]] 196 | name = "semver" 197 | version = "1.0.25" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" 200 | dependencies = [ 201 | "serde", 202 | ] 203 | 204 | [[package]] 205 | name = "serde" 206 | version = "1.0.218" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" 209 | dependencies = [ 210 | "serde_derive", 211 | ] 212 | 213 | [[package]] 214 | name = "serde_derive" 215 | version = "1.0.218" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" 218 | dependencies = [ 219 | "proc-macro2", 220 | "quote", 221 | "syn", 222 | ] 223 | 224 | [[package]] 225 | name = "serde_json" 226 | version = "1.0.139" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" 229 | dependencies = [ 230 | "itoa", 231 | "memchr", 232 | "ryu", 233 | "serde", 234 | ] 235 | 236 | [[package]] 237 | name = "syn" 238 | version = "2.0.98" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" 241 | dependencies = [ 242 | "proc-macro2", 243 | "quote", 244 | "unicode-ident", 245 | ] 246 | 247 | [[package]] 248 | name = "thiserror" 249 | version = "2.0.11" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" 252 | dependencies = [ 253 | "thiserror-impl", 254 | ] 255 | 256 | [[package]] 257 | name = "thiserror-impl" 258 | version = "2.0.11" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" 261 | dependencies = [ 262 | "proc-macro2", 263 | "quote", 264 | "syn", 265 | ] 266 | 267 | [[package]] 268 | name = "unicode-ident" 269 | version = "1.0.17" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" 272 | 273 | [[package]] 274 | name = "windows-sys" 275 | version = "0.59.0" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 278 | dependencies = [ 279 | "windows-targets", 280 | ] 281 | 282 | [[package]] 283 | name = "windows-targets" 284 | version = "0.52.6" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 287 | dependencies = [ 288 | "windows_aarch64_gnullvm", 289 | "windows_aarch64_msvc", 290 | "windows_i686_gnu", 291 | "windows_i686_gnullvm", 292 | "windows_i686_msvc", 293 | "windows_x86_64_gnu", 294 | "windows_x86_64_gnullvm", 295 | "windows_x86_64_msvc", 296 | ] 297 | 298 | [[package]] 299 | name = "windows_aarch64_gnullvm" 300 | version = "0.52.6" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 303 | 304 | [[package]] 305 | name = "windows_aarch64_msvc" 306 | version = "0.52.6" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 309 | 310 | [[package]] 311 | name = "windows_i686_gnu" 312 | version = "0.52.6" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 315 | 316 | [[package]] 317 | name = "windows_i686_gnullvm" 318 | version = "0.52.6" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 321 | 322 | [[package]] 323 | name = "windows_i686_msvc" 324 | version = "0.52.6" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 327 | 328 | [[package]] 329 | name = "windows_x86_64_gnu" 330 | version = "0.52.6" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 333 | 334 | [[package]] 335 | name = "windows_x86_64_gnullvm" 336 | version = "0.52.6" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 339 | 340 | [[package]] 341 | name = "windows_x86_64_msvc" 342 | version = "0.52.6" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 345 | -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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 | --------------------------------------------------------------------------------