├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE-MIT ├── README.md ├── src ├── env.rs ├── xml.rs ├── json.rs └── lib.rs └── LICENSE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | matrix: 7 | allow_failures: 8 | - rust: nightly 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | name = "alfred" 4 | version = "4.0.2" # remember to update html_root_url in lib.rs 5 | authors = ["Lily Ballard "] 6 | description = """ 7 | A library for writing Alfred workflows. 8 | 9 | http://www.alfredapp.com 10 | """ 11 | 12 | documentation = "https://docs.rs/alfred/" 13 | repository = "https://github.com/lilyball/alfred-rs" 14 | 15 | readme = "README.md" 16 | license = "MIT/Apache-2.0" 17 | 18 | [badges] 19 | travis-ci = { repository = "lilyball/alfred-rs" } 20 | 21 | [dependencies] 22 | 23 | serde_json = "1.0" 24 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Lily Ballard 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # alfred-rs 2 | 3 | [![Build Status](https://travis-ci.org/lilyball/alfred-rs.svg?branch=master)](https://travis-ci.org/lilyball/alfred-rs) 4 | [![crates.io/crates/alfred](http://meritbadge.herokuapp.com/alfred)](https://crates.io/crates/alfred) 5 | 6 | Rust library to help with creating [Alfred][alfred] [Workflows][]. 7 | 8 | [alfred]: http://www.alfredapp.com 9 | [Workflows]: http://support.alfredapp.com/workflows 10 | 11 | [API Documentation](http://docs.rs/alfred) 12 | 13 | ## Installation 14 | 15 | Add the following to your `Cargo.toml` file: 16 | 17 | ```toml 18 | [dependencies] 19 | 20 | alfred = "4.0" 21 | ``` 22 | 23 | ## License 24 | 25 | Licensed under either of 26 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or 27 | http://www.apache.org/licenses/LICENSE-2.0) 28 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or 29 | http://opensource.org/licenses/MIT) at your option. 30 | 31 | ### Contribution 32 | 33 | Unless you explicitly state otherwise, any contribution intentionally submitted 34 | for inclusion in the work by you shall be dual licensed as above, without any 35 | additional terms or conditions. 36 | 37 | ## Version History 38 | 39 | #### 4.0.2 40 | 41 | * Update crate metadata. 42 | 43 | #### 4.0.1 44 | 45 | * Make `Builder.into_json` public. 46 | * Make `Item.to_json` public, along with `to_json` methods on its helper types. 47 | 48 | #### 4.0.0 49 | 50 | * Add support for per-modifier icons. 51 | * Add support for outputting workflow variables. 52 | * Add support for outputting per-item workflow variables. 53 | * Add support for outputting per-modifier workflow variables. 54 | * Derive a few more traits on the types provided by this crate. 55 | 56 | #### 3.0.3 57 | 58 | Add 2 more functions for reading workflow environment variables. 59 | 60 | #### 3.0.2 61 | 62 | Update documentation links for crates.io. 63 | 64 | #### 3.0.1 65 | 66 | Update `serde_json` to 1.0. 67 | 68 | #### 3.0 69 | 70 | Switch from `rustc-serialize` to `serde_json` for our JSON support. 71 | 72 | #### 2.0.1 73 | 74 | Add new module `alfred::env` for accessing the Alfred workflow environment 75 | variables. 76 | 77 | #### 2.0.0 78 | 79 | Moved XML output into its own module `alfred::xml` and introduced a new module 80 | `alfred::json` for the new Alfred 3 JSON format. 81 | 82 | Updated `Item` and `ItemBuilder` with the extended modifier functionality and 83 | support for the QuickLook URL. 84 | 85 | #### 1.0.1 86 | 87 | Dual-licensed under MIT and APACHE. 88 | 89 | #### 1.0.0 90 | 91 | Rust 1.0 is out! 92 | 93 | #### 0.3.1 94 | 95 | Remove `#[unsafe_destructor]`, which no longer exists in the latest nightlies. 96 | 97 | #### 0.3.0 98 | 99 | Switch from `IntoCow<'a, str>` to `Into>`. 100 | This is technically a breaking change, but it is unlikely to affect anyone. 101 | 102 | #### 0.2.2 103 | 104 | Compatibility with the latest Rust nightly. 105 | 106 | #### 0.2.1 107 | 108 | Compatibility with the latest Rust nightly. 109 | 110 | #### 0.2 111 | 112 | Switch from `std::old_io` to `std::io`. 113 | 114 | #### 0.1.1 115 | 116 | Compatibility with the Rust nightly for 2015-02-21. 117 | 118 | #### 0.1 119 | 120 | Compatibility with the Rust 1.0 Alpha release. 121 | -------------------------------------------------------------------------------- /src/env.rs: -------------------------------------------------------------------------------- 1 | //! Accessors for workflow environment variables 2 | //! 3 | //! See https://www.alfredapp.com/help/workflows/script-environment-variables/ 4 | //! for more info. 5 | 6 | use std::env; 7 | use std::path::PathBuf; 8 | 9 | /// Returns the location of the Alfred.alfredpreferences. 10 | /// 11 | /// Example output: `"/Users/Crayons/Dropbox/Alfred/Alfred.alfredpreferences"` 12 | pub fn preferences() -> Option { 13 | env::var("alfred_preferences").ok().map(PathBuf::from) 14 | } 15 | 16 | /// Returns the location of local (Mac-specific) preferences. 17 | /// 18 | /// Example output: `"/Users/Crayons/Dropbox/Alfred/Alfred.alfredpreferences/preferences/local/adbd4f66bc3ae8493832af61a41ee609b20d8705"` 19 | pub fn local_preferences() -> Option { 20 | match (preferences(),env::var("alfred_preferences_localhash")) { 21 | (Some(mut prefs),Ok(hash)) => { 22 | prefs.extend(["preferences","local",&hash].iter()); 23 | Some(prefs) 24 | } 25 | _ => None 26 | } 27 | } 28 | 29 | /// Returns the current Alfred theme. 30 | /// 31 | /// Example output: `"alfred.theme.yosemite"` 32 | pub fn theme() -> Option { 33 | env::var("alfred_theme").ok() 34 | } 35 | 36 | /// Returns the color of the theme background. 37 | /// 38 | /// Example output: `"rgba(255,255,255,0.98)"` 39 | // TODO: can we parse this? 40 | // Is this always in rgba(), or can it be any web color? 41 | pub fn theme_background_str() -> Option { 42 | env::var("alfred_theme_background").ok() 43 | } 44 | 45 | /// Returns the color of the theme's selected item background. 46 | /// 47 | /// Example output: `"rgba(255,255,255,0.98)"` 48 | // TODO: see `theme_background_str()` 49 | pub fn theme_selection_background_str() -> Option { 50 | env::var("alfred_theme_selection_background").ok() 51 | } 52 | 53 | /// The subtext mode in the Appearance preferences. 54 | #[derive(Copy,Clone,Debug,PartialEq,Eq,Hash)] 55 | pub enum Subtext { 56 | /// Always show subtext. 57 | Always, 58 | /// Only show subtext for alternative actions. 59 | AlternativeActions, 60 | /// Only show subtext for the selected result. 61 | SelectedResult, 62 | /// Never show subtext. 63 | Never 64 | } 65 | 66 | /// Returns the subtext mode the user has selected in the Appearance preferences. 67 | pub fn theme_subtext() -> Option { 68 | match env::var("alfred_theme_subtext").as_ref().map(|s| s.as_ref()) { 69 | Ok("0") => Some(Subtext::Always), 70 | Ok("1") => Some(Subtext::AlternativeActions), 71 | Ok("2") => Some(Subtext::SelectedResult), 72 | Ok("3") => Some(Subtext::Never), 73 | _ => None 74 | } 75 | } 76 | 77 | /// Returns the version of Alfred. 78 | /// 79 | /// Example output: `"3.2.1"` 80 | pub fn version() -> Option { 81 | env::var("alfred_version").ok() 82 | } 83 | 84 | /// Returns the build of Alfred. 85 | /// 86 | /// Example output: `768` 87 | pub fn version_build() -> Option { 88 | env::var("alfred_version_build").ok().and_then(|s| s.parse().ok()) 89 | } 90 | 91 | /// Returns the bundle ID of the current running workflow. 92 | /// 93 | /// Example output: `"com.alfredapp.david.googlesuggest"` 94 | pub fn workflow_bundle_id() -> Option { 95 | env::var("alfred_workflow_bundleid").ok() 96 | } 97 | 98 | /// Returns the recommended location for volatile workflow data. 99 | /// Will only be populated if the workflow has a bundle identifier set. 100 | /// 101 | /// Example output: `"/Users/Crayons/Library/Caches/com.runningwithcrayons.Alfred-2/Workflow Data/com.alfredapp.david.googlesuggest"` 102 | pub fn workflow_cache() -> Option { 103 | env::var("alfred_workflow_cache").ok().map(PathBuf::from) 104 | } 105 | 106 | /// Returns the recommended location for non-volatile workflow data. 107 | /// Will only be populated if the workflow has a bundle identifier set. 108 | /// 109 | /// Example output: `"/Users/Crayons/Library/Application Support/Alfred 2/Workflow Data/com.alfredapp.david.googlesuggest"` 110 | pub fn workflow_data() -> Option { 111 | env::var("alfred_workflow_data").ok().map(PathBuf::from) 112 | } 113 | 114 | /// Returns the name of the currently running workflow. 115 | /// 116 | /// Example output: `"Google Suggest"` 117 | pub fn workflow_name() -> Option { 118 | env::var("alfred_workflow_name").ok() 119 | } 120 | 121 | /// Returns the unique ID of the currently running workflow. 122 | /// 123 | /// Example output: `"user.workflow.B0AC54EC-601C-479A-9428-01F9FD732959"` 124 | pub fn workflow_uid() -> Option { 125 | env::var("alfred_workflow_uid").ok() 126 | } 127 | 128 | /// Returns the version of the currently running workflow. 129 | pub fn workflow_version() -> Option { 130 | env::var("alfred_workflow_version").ok() 131 | } 132 | 133 | /// Returns `true` if the user has the debug panel open for the workflow. 134 | pub fn is_debug() -> bool { 135 | match env::var("alfred_debug") { 136 | Ok(val) => val == "1", 137 | _ => false 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/xml.rs: -------------------------------------------------------------------------------- 1 | //! Helpers for writing Alfred script filter XML output (Alfred 2) 2 | //! 3 | //! Unless you specifically need Alfred 2 compatibility, you should use the `alfred::json` module 4 | //! instead. 5 | //! 6 | //! # Example 7 | //! 8 | //! ``` 9 | //! # extern crate alfred; 10 | //! # 11 | //! # use std::io::{self, Write}; 12 | //! # 13 | //! # fn write_items() -> io::Result<()> { 14 | //! let mut xmlw = try!(alfred::XMLWriter::new(io::stdout())); 15 | //! 16 | //! let item1 = alfred::Item::new("Item 1"); 17 | //! let item2 = alfred::ItemBuilder::new("Item 2") 18 | //! .subtitle("Subtitle") 19 | //! .into_item(); 20 | //! let item3 = alfred::ItemBuilder::new("Item 3") 21 | //! .arg("Argument") 22 | //! .subtitle("Subtitle") 23 | //! .icon_filetype("public.folder") 24 | //! .into_item(); 25 | //! 26 | //! try!(xmlw.write_item(&item1)); 27 | //! try!(xmlw.write_item(&item2)); 28 | //! try!(xmlw.write_item(&item3)); 29 | //! 30 | //! let mut stdout = try!(xmlw.close()); 31 | //! stdout.flush() 32 | //! # } 33 | //! # 34 | //! # fn main() { 35 | //! # match write_items() { 36 | //! # Ok(()) => {}, 37 | //! # Err(err) => { 38 | //! # let _ = writeln!(&mut io::stderr(), "Error writing items: {}", err); 39 | //! # } 40 | //! # } 41 | //! # } 42 | //! ``` 43 | 44 | use std::borrow::Cow; 45 | use std::error; 46 | use std::fmt; 47 | use std::io; 48 | use std::io::prelude::*; 49 | use std::mem; 50 | use std::sync; 51 | 52 | use ::{Item, ItemType, Modifier, Icon}; 53 | 54 | /// Helper struct used to manage the XML serialization of `Item`s. 55 | /// 56 | /// When the `XMLWriter` is first created, the XML header is immediately 57 | /// written. When the `XMLWriter` is dropped, the XML footer is written 58 | /// and the `Write` is flushed. 59 | /// 60 | /// Any errors produced by writing the footer are silently ignored. The 61 | /// `close()` method can be used to return any such error. 62 | pub struct XMLWriter { 63 | // Option so close() can remove it 64 | // Otherwise this must always be Some() 65 | w: Option, 66 | last_err: Option 67 | } 68 | 69 | // FIXME: If io::Error gains Clone again, go back to just cloning it 70 | enum SavedError { 71 | Os(i32), 72 | Custom(SharedError) 73 | } 74 | 75 | #[derive(Clone)] 76 | struct SharedError { 77 | error: sync::Arc 78 | } 79 | 80 | impl From for SavedError { 81 | fn from(err: io::Error) -> SavedError { 82 | if let Some(code) = err.raw_os_error() { 83 | SavedError::Os(code) 84 | } else { 85 | SavedError::Custom(SharedError { error: sync::Arc::new(err) }) 86 | } 87 | } 88 | } 89 | 90 | impl SavedError { 91 | fn make_io_error(&self) -> io::Error { 92 | match *self { 93 | SavedError::Os(code) => io::Error::from_raw_os_error(code), 94 | SavedError::Custom(ref err) => { 95 | let shared_err: SharedError = err.clone(); 96 | io::Error::new(err.error.kind(), shared_err) 97 | } 98 | } 99 | } 100 | } 101 | 102 | impl error::Error for SharedError { 103 | fn description(&self) -> &str { 104 | self.error.description() 105 | } 106 | 107 | fn cause(&self) -> Option<&error::Error> { 108 | Some(&*self.error) 109 | } 110 | } 111 | 112 | impl fmt::Debug for SharedError { 113 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 114 | ::fmt(&self.error, f) 115 | } 116 | } 117 | 118 | impl fmt::Display for SharedError { 119 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 120 | ::fmt(&self.error, f) 121 | } 122 | } 123 | 124 | impl XMLWriter { 125 | /// Returns a new `XMLWriter` that writes to the given `Write`. 126 | /// 127 | /// The XML header is written immediately. 128 | pub fn new(mut w: W) -> io::Result> { 129 | match w.write_all(b"\n\n") { 130 | Ok(()) => { 131 | Ok(XMLWriter { 132 | w: Some(w), 133 | last_err: None 134 | }) 135 | } 136 | Err(err) => Err(err) 137 | } 138 | } 139 | 140 | /// Writes an `Item` to the underlying `Write`. 141 | /// 142 | /// If a previous write produced an error, any subsequent write will do 143 | /// nothing and return the same error. This is because the previous write 144 | /// may have partially completed, and attempting to write any more data 145 | /// will be unlikely to work properly. 146 | pub fn write_item(&mut self, item: &Item) -> io::Result<()> { 147 | if let Some(ref err) = self.last_err { 148 | return Err(err.make_io_error()); 149 | } 150 | let result = item.write_xml(self.w.as_mut().unwrap(), 1); 151 | match result { 152 | Err(err) => { 153 | let err: SavedError = err.into(); 154 | let io_err = err.make_io_error(); 155 | self.last_err = Some(err); 156 | Err(io_err) 157 | } 158 | x@Ok(_) => x 159 | } 160 | } 161 | 162 | /// Consumes the `XMLWriter` and writes the XML footer. 163 | /// 164 | /// This method can be used to get any error resulting from writing the 165 | /// footer. If this method is not used, the footer will be written when the 166 | /// `XMLWriter` is dropped and any error will be silently ignored. 167 | /// 168 | /// As with `write_item()`, if a previous invocation of `write_item()` 169 | /// returned an error, `close()` will return the same error without 170 | /// attempting to write the XML footer. 171 | /// 172 | /// When this method is used, the XML footer is written, but the `Write` 173 | /// is not flushed. When the `XMLWriter` is dropped without calling 174 | /// `close()`, the `Write` is flushed after the footer is written. 175 | pub fn close(mut self) -> io::Result { 176 | let last_err = self.last_err.take(); 177 | let mut w = self.w.take().unwrap(); 178 | mem::forget(self); 179 | if let Some(err) = last_err { 180 | return Err(err.make_io_error()); 181 | } 182 | try!(write_footer(&mut w)); 183 | Ok(w) 184 | } 185 | } 186 | 187 | fn write_footer(w: &mut W) -> io::Result<()> { 188 | w.write_all(b"\n") 189 | } 190 | 191 | impl Drop for XMLWriter { 192 | fn drop(&mut self) { 193 | if self.last_err.is_some() { 194 | return; 195 | } 196 | let mut w = self.w.take().unwrap(); 197 | if write_footer(&mut w).is_ok() { 198 | let _ = w.flush(); 199 | } 200 | } 201 | } 202 | 203 | /// Writes a complete XML document representing the `Item`s to the `Write`. 204 | /// 205 | /// The `Write` is flushed after the XML document is written. 206 | pub fn write_items(w: W, items: &[Item]) -> io::Result<()> { 207 | let mut xmlw = try!(XMLWriter::new(w)); 208 | for item in items.iter() { 209 | try!(xmlw.write_item(item)); 210 | } 211 | let mut w = try!(xmlw.close()); 212 | w.flush() 213 | } 214 | 215 | impl<'a> Item<'a> { 216 | /// Writes the XML fragment representing the `Item` to the `Write`. 217 | /// 218 | /// `XMLWriter` should be used instead if at all possible, in order to 219 | /// write the XML header/footer and maintain proper error discipline. 220 | pub fn write_xml(&self, w: &mut Write, indent: u32) -> io::Result<()> { 221 | fn write_indent(w: &mut Write, indent: u32) -> io::Result<()> { 222 | for _ in 0..indent { 223 | try!(w.write_all(b" ")); 224 | } 225 | Ok(()) 226 | } 227 | 228 | let mut w = io::BufWriter::with_capacity(512, w); 229 | 230 | try!(write_indent(&mut w, indent)); 231 | try!(w.write_all(b" {} 240 | ItemType::File => { 241 | try!(w.write_all(br#" type="file""#)); 242 | } 243 | ItemType::FileSkipCheck => { 244 | try!(w.write_all(br#" type="file:skipcheck""#)); 245 | } 246 | } 247 | if !self.valid { 248 | try!(w.write_all(br#" valid="no""#)); 249 | } 250 | if let Some(ref auto) = self.autocomplete { 251 | try!(write!(&mut w, r#" autocomplete="{}""#, encode_entities(auto))); 252 | } 253 | try!(w.write_all(b">\n")); 254 | 255 | try!(write_indent(&mut w, indent+1)); 256 | try!(write!(&mut w, "{}\n", encode_entities(&self.title))); 257 | 258 | if let Some(ref subtitle) = self.subtitle { 259 | try!(write_indent(&mut w, indent+1)); 260 | try!(write!(&mut w, "{}\n", encode_entities(subtitle))); 261 | } 262 | 263 | if let Some(ref icon) = self.icon { 264 | try!(write_indent(&mut w, indent+1)); 265 | match *icon { 266 | Icon::Path(ref s) => { 267 | try!(write!(&mut w, "{}\n", encode_entities(s))); 268 | } 269 | Icon::File(ref s) => { 270 | try!(write!(&mut w, "{}\n", 271 | encode_entities(s))); 272 | } 273 | Icon::FileType(ref s) => { 274 | try!(write!(&mut w, "{}\n", 275 | encode_entities(s))); 276 | } 277 | } 278 | } 279 | 280 | for (modifier, data) in &self.modifiers { 281 | try!(write_indent(&mut w, indent+1)); 282 | try!(write!(&mut w, r#" "cmd", 284 | Modifier::Option => "alt", 285 | Modifier::Control => "ctrl", 286 | Modifier::Shift => "shift", 287 | Modifier::Fn => "fn" 288 | })); 289 | try!(w.write_all(b"\n")); 300 | } 301 | 302 | if let Some(ref text) = self.text_copy { 303 | try!(write_indent(&mut w, indent+1)); 304 | try!(write!(&mut w, "{}\n", encode_entities(text))); 305 | } 306 | if let Some(ref text) = self.text_large_type { 307 | try!(write_indent(&mut w, indent+1)); 308 | try!(write!(&mut w, "{}\n", encode_entities(text))); 309 | } 310 | 311 | if let Some(ref url) = self.quicklook_url { 312 | try!(write_indent(&mut w, indent+1)); 313 | try!(write!(&mut w, "{}\n", encode_entities(url))); 314 | } 315 | 316 | try!(write_indent(&mut w, indent)); 317 | try!(w.write_all(b"\n")); 318 | 319 | w.flush() 320 | } 321 | } 322 | 323 | fn encode_entities(s: &str) -> Cow { 324 | fn encode_entity(c: char) -> Option<&'static str> { 325 | Some(match c { 326 | '<' => "<", 327 | '>' => ">", 328 | '"' => """, 329 | '&' => "&", 330 | '\0'...'\x08' | 331 | '\x0B'...'\x0C' | 332 | '\x0E'...'\x1F' | 333 | '\u{FFFE}' | '\u{FFFF}' => { 334 | // these are all invalid characters in XML 335 | "\u{FFFD}" 336 | } 337 | _ => return None 338 | }) 339 | } 340 | 341 | if s.chars().any(|c| encode_entity(c).is_some()) { 342 | let mut res = String::with_capacity(s.len()); 343 | for c in s.chars() { 344 | match encode_entity(c) { 345 | Some(ent) => res.push_str(ent), 346 | None => res.push(c) 347 | } 348 | } 349 | Cow::Owned(res) 350 | } else { 351 | Cow::Borrowed(s) 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/json.rs: -------------------------------------------------------------------------------- 1 | //! Helpers for writing Alfred script filter JSON output (Alfred 3) 2 | //! 3 | //! # Examples 4 | //! 5 | //! ### Writing items 6 | //! 7 | //! ``` 8 | //! # extern crate alfred; 9 | //! # use std::io::{self, Write}; 10 | //! # 11 | //! # fn write_items() -> io::Result<()> { 12 | //! alfred::json::write_items(io::stdout(), &[ 13 | //! alfred::Item::new("Item 1"), 14 | //! alfred::ItemBuilder::new("Item 2") 15 | //! .subtitle("Subtitle") 16 | //! .into_item(), 17 | //! alfred::ItemBuilder::new("Item 3") 18 | //! .arg("Argument") 19 | //! .subtitle("Subtitle") 20 | //! .icon_filetype("public.folder") 21 | //! .into_item() 22 | //! ]) 23 | //! # } 24 | //! # 25 | //! # fn main() { 26 | //! # match write_items() { 27 | //! # Ok(()) => {}, 28 | //! # Err(err) => { 29 | //! # let _ = writeln!(&mut io::stderr(), "Error writing items: {}", err); 30 | //! # } 31 | //! # } 32 | //! # } 33 | //! ``` 34 | //! 35 | //! ### Writing items with variables 36 | //! 37 | //! ``` 38 | //! # extern crate alfred; 39 | //! # use alfred::Modifier; 40 | //! # use std::io::{self, Write}; 41 | //! # 42 | //! # fn write_items() -> io::Result<()> { 43 | //! alfred::json::Builder::with_items(&[ 44 | //! alfred::Item::new("Item 1"), 45 | //! alfred::ItemBuilder::new("Item 2") 46 | //! .subtitle("Subtitle") 47 | //! .variable("fruit", "banana") 48 | //! .into_item(), 49 | //! alfred::ItemBuilder::new("Item 3") 50 | //! .arg("Argument") 51 | //! .subtitle("Subtitle") 52 | //! .icon_filetype("public.folder") 53 | //! .arg_mod(Modifier::Option, "Alt Argument") 54 | //! .variable_mod(Modifier::Option, "vegetable", "carrot") 55 | //! .into_item() 56 | //! ]).variable("fruit", "banana") 57 | //! .variable("vegetable", "carrot") 58 | //! .write(io::stdout()) 59 | //! # } 60 | //! # 61 | //! # fn main() { 62 | //! # match write_items() { 63 | //! # Ok(()) => {}, 64 | //! # Err(err) => { 65 | //! # let _ = writeln!(&mut io::stderr(), "Error writing items: {}", err); 66 | //! # } 67 | //! # } 68 | //! # } 69 | //! ``` 70 | 71 | use ::{Item, ItemType, Modifier, Icon, ModifierData}; 72 | use serde_json as json; 73 | use serde_json::value::Value; 74 | use std::collections::HashMap; 75 | use std::io; 76 | use std::io::prelude::*; 77 | 78 | /// Writes a complete JSON document representing the `Item`s to the `Write`. 79 | /// 80 | /// The `Write` is flushed after the JSON document is written. 81 | pub fn write_items(w: W, items: &[Item]) -> io::Result<()> { 82 | Builder::with_items(items).write(w) 83 | } 84 | 85 | /// A helper type for writing out items with top-level variables. 86 | /// 87 | /// Note: If you don't need top-level variables the `write_items()` function is easier to use. 88 | #[derive(Clone,Debug,Default)] 89 | pub struct Builder<'a> { 90 | /// The items that will be written out. 91 | pub items: &'a [Item<'a>], 92 | /// The variables that will be written out. 93 | pub variables: HashMap<&'a str, &'a str> 94 | } 95 | 96 | impl<'a> Builder<'a> { 97 | /// Returns a new `Builder` with no items. 98 | pub fn new() -> Builder<'a> { 99 | Builder { items: &[], variables: HashMap::new() } 100 | } 101 | 102 | /// Returns a new `Builder` with the given items. 103 | pub fn with_items(items: &'a [Item]) -> Builder<'a> { 104 | Builder { items, variables: HashMap::new() } 105 | } 106 | 107 | /// Writes a complete JSON document representing the items and variables to the `Write`. 108 | /// 109 | /// The `Write` is flushed after the JSON document is written. 110 | pub fn write(self, mut w: W) -> io::Result<()> { 111 | write!(&mut w, "{}", self.into_json())?; 112 | w.flush() 113 | } 114 | 115 | /// Serializes items into their JSON representation. 116 | pub fn into_json(self) -> Value { 117 | let mut root = json::Map::new(); 118 | root.insert("items".to_string(), Value::Array(self.items.into_iter() 119 | .map(|x| x.to_json()) 120 | .collect())); 121 | let mut iter = self.variables.into_iter(); 122 | if let Some(first) = iter.next() { 123 | let mut vars = json::Map::new(); 124 | vars.insert(first.0.into(), Value::String(first.1.into())); 125 | for elt in iter { 126 | vars.insert(elt.0.into(), Value::String(elt.1.into())); 127 | } 128 | root.insert("variables".to_owned(), Value::Object(vars)); 129 | } 130 | Value::Object(root) 131 | } 132 | 133 | /// Replaces the builder's items with `items`. 134 | pub fn items(mut self, items: &'a [Item]) -> Builder<'a> { 135 | self.set_items(items); 136 | self 137 | } 138 | 139 | /// Replaces the builder's variables with `variables`. 140 | pub fn variables(mut self, variables: HashMap<&'a str, &'a str>) -> Builder<'a> { 141 | self.set_variables(variables); 142 | self 143 | } 144 | 145 | /// Inserts a new variable into the builder's variables. 146 | pub fn variable(mut self, key: &'a str, value: &'a str) -> Builder<'a> { 147 | self.set_variable(key, value); 148 | self 149 | } 150 | 151 | /// Replaces the builder's items with `items`. 152 | pub fn set_items(&mut self, items: &'a [Item]) { 153 | self.items = items 154 | } 155 | 156 | /// Replaces the builder's variables with `variables`. 157 | pub fn set_variables(&mut self, variables: HashMap<&'a str, &'a str>) { 158 | self.variables = variables 159 | } 160 | 161 | /// Inserts a new variable into the builder's variables. 162 | pub fn set_variable(&mut self, key: &'a str, value: &'a str) { 163 | self.variables.insert(key, value); 164 | } 165 | } 166 | 167 | impl<'a> Item<'a> { 168 | /// Serializes the `Item` into its JSON representation. 169 | pub fn to_json(&self) -> Value { 170 | let mut d = json::Map::new(); 171 | d.insert("title".to_string(), json!(self.title)); 172 | if let Some(ref subtitle) = self.subtitle { 173 | d.insert("subtitle".to_string(), json!(subtitle)); 174 | } 175 | if let Some(ref icon) = self.icon { 176 | d.insert("icon".to_string(), icon.to_json()); 177 | } 178 | if let Some(ref uid) = self.uid { 179 | d.insert("uid".to_string(), json!(uid)); 180 | } 181 | if let Some(ref arg) = self.arg { 182 | d.insert("arg".to_string(), json!(arg)); 183 | } 184 | match self.type_ { 185 | ItemType::Default => {} 186 | ItemType::File => { 187 | d.insert("type".to_string(), json!("file")); 188 | } 189 | ItemType::FileSkipCheck => { 190 | d.insert("type".to_string(), json!("file:skipcheck")); 191 | } 192 | } 193 | if !self.valid { 194 | d.insert("valid".to_string(), Value::Bool(false)); 195 | } 196 | if let Some(ref autocomplete) = self.autocomplete { 197 | d.insert("autocomplete".to_string(), json!(autocomplete)); 198 | } 199 | if self.text_copy.is_some() || self.text_large_type.is_some() { 200 | let mut text = json::Map::new(); 201 | if let Some(ref text_copy) = self.text_copy { 202 | text.insert("copy".to_string(), json!(text_copy)); 203 | } 204 | if let Some(ref text_large_type) = self.text_large_type { 205 | text.insert("largetype".to_string(), json!(text_large_type)); 206 | } 207 | d.insert("text".to_string(), Value::Object(text)); 208 | } 209 | if let Some(ref url) = self.quicklook_url { 210 | d.insert("quicklookurl".to_string(), json!(url)); 211 | } 212 | if !self.modifiers.is_empty() { 213 | let mut mods = json::Map::with_capacity(self.modifiers.len()); 214 | for (modifier, data) in &self.modifiers { 215 | let key = match *modifier { 216 | Modifier::Command => "cmd", 217 | Modifier::Option => "alt", 218 | Modifier::Control => "ctrl", 219 | Modifier::Shift => "shift", 220 | Modifier::Fn => "fn" 221 | }.to_string(); 222 | mods.insert(key, data.to_json()); 223 | } 224 | d.insert("mods".to_string(), Value::Object(mods)); 225 | } 226 | if !self.variables.is_empty() { 227 | let mut vars = json::Map::with_capacity(self.variables.len()); 228 | for (key, value) in &self.variables { 229 | vars.insert(key.clone().into_owned(), json!(value.clone().into_owned())); 230 | } 231 | d.insert("variables".to_string(), Value::Object(vars)); 232 | } 233 | Value::Object(d) 234 | } 235 | } 236 | 237 | impl<'a> Icon<'a> { 238 | /// Serializes the `Icon` into its JSON representation. 239 | pub fn to_json(&self) -> Value { 240 | match *self { 241 | Icon::Path(ref s) => json!({"path": s}), 242 | Icon::File(ref s) => json!({"type": "fileicon", "path": s}), 243 | Icon::FileType(ref s) => json!({"type": "filetype", "path": s}) 244 | } 245 | } 246 | } 247 | 248 | impl<'a> ModifierData<'a> { 249 | /// Serializes the `ModifierData` into its JSON representation. 250 | pub fn to_json(&self) -> Value { 251 | let mut mod_ = json::Map::new(); 252 | if let Some(ref subtitle) = self.subtitle { 253 | mod_.insert("subtitle".to_string(), json!(subtitle)); 254 | } 255 | if let Some(ref arg) = self.arg { 256 | mod_.insert("arg".to_string(), json!(arg)); 257 | } 258 | if let Some(valid) = self.valid { 259 | mod_.insert("valid".to_string(), json!(valid)); 260 | } 261 | if let Some(ref icon) = self.icon { 262 | mod_.insert("icon".to_string(), icon.to_json()); 263 | } 264 | if !self.variables.is_empty() { 265 | let mut vars = json::Map::with_capacity(self.variables.len()); 266 | for (key, value) in &self.variables { 267 | vars.insert(key.clone().into_owned(), json!(value.clone().into_owned())); 268 | } 269 | mod_.insert("variables".to_string(), Value::Object(vars)); 270 | } 271 | Value::Object(mod_) 272 | } 273 | } 274 | 275 | #[test] 276 | fn test_to_json() { 277 | let item = Item::new("Item 1"); 278 | assert_eq!(item.to_json(), json!({"title": "Item 1"})); 279 | let item = ::ItemBuilder::new("Item 2") 280 | .subtitle("Subtitle") 281 | .into_item(); 282 | assert_eq!(item.to_json(), 283 | json!({ 284 | "title": "Item 2", 285 | "subtitle": "Subtitle" 286 | })); 287 | let item = ::ItemBuilder::new("Item 3") 288 | .arg("Argument") 289 | .subtitle("Subtitle") 290 | .icon_filetype("public.folder") 291 | .into_item(); 292 | assert_eq!(item.to_json(), 293 | json!({ 294 | "title": "Item 3", 295 | "subtitle": "Subtitle", 296 | "arg": "Argument", 297 | "icon": { "type": "filetype", "path": "public.folder" } 298 | })); 299 | let item = ::ItemBuilder::new("Item 4") 300 | .arg("Argument") 301 | .subtitle("Subtitle") 302 | .arg_mod(Modifier::Option, "Alt Argument") 303 | .valid_mod(Modifier::Option, false) 304 | .icon_file_mod(Modifier::Option, "opt.png") 305 | .arg_mod(Modifier::Control, "Ctrl Argument") 306 | .subtitle_mod(Modifier::Control, "Ctrl Subtitle") 307 | .icon_path_mod(Modifier::Control, "ctrl.png") 308 | .arg_mod(Modifier::Shift, "Shift Argument") 309 | .into_item(); 310 | assert_eq!(item.to_json(), 311 | json!({ 312 | "title": "Item 4", 313 | "subtitle": "Subtitle", 314 | "arg": "Argument", 315 | "mods": { 316 | "alt": { 317 | "arg": "Alt Argument", 318 | "valid": false, 319 | "icon": { "type": "fileicon", "path": "opt.png" } 320 | }, 321 | "ctrl": { 322 | "arg": "Ctrl Argument", 323 | "subtitle": "Ctrl Subtitle", 324 | "icon": { "path": "ctrl.png" } 325 | }, 326 | "shift": { 327 | "arg": "Shift Argument" 328 | } 329 | } 330 | })); 331 | let item = ::ItemBuilder::new("Item 5") 332 | .arg("Argument") 333 | .variable("fruit", "banana") 334 | .variable("vegetable", "carrot") 335 | .into_item(); 336 | assert_eq!(item.to_json(), 337 | json!({ 338 | "title": "Item 5", 339 | "arg": "Argument", 340 | "variables": { 341 | "fruit": "banana", 342 | "vegetable": "carrot" 343 | } 344 | })); 345 | let item = ::ItemBuilder::new("Item 6") 346 | .subtitle("Subtitle") 347 | .variable("fruit", "banana") 348 | .variable_mod(Modifier::Option, "vegetable", "carrot") 349 | .into_item(); 350 | assert_eq!(item.to_json(), 351 | json!({ 352 | "title": "Item 6", 353 | "subtitle": "Subtitle", 354 | "mods": { 355 | "alt": { 356 | "variables": { 357 | "vegetable": "carrot" 358 | } 359 | } 360 | }, 361 | "variables": { 362 | "fruit": "banana" 363 | } 364 | })); 365 | } 366 | 367 | #[test] 368 | fn test_builder() { 369 | let json = Builder::with_items(&[ 370 | Item::new("Item 1"), 371 | ::ItemBuilder::new("Item 2") 372 | .subtitle("Subtitle") 373 | .into_item(), 374 | ::ItemBuilder::new("Item 3") 375 | .arg("Argument") 376 | .subtitle("Subtitle") 377 | .icon_filetype("public.folder") 378 | .into_item() 379 | ]).variable("fruit", "banana") 380 | .variable("vegetable", "carrot") 381 | .into_json(); 382 | assert_eq!(json, 383 | json!({ 384 | "items": [ 385 | { 386 | "title": "Item 1" 387 | }, 388 | { 389 | "title": "Item 2", 390 | "subtitle": "Subtitle" 391 | }, 392 | { 393 | "title": "Item 3", 394 | "arg": "Argument", 395 | "subtitle": "Subtitle", 396 | "icon": { "type": "filetype", "path": "public.folder" } 397 | } 398 | ], 399 | "variables": { 400 | "fruit": "banana", 401 | "vegetable": "carrot" 402 | } 403 | })); 404 | } 405 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Lily Ballard. 2 | // Licensed under the Apache License, Version 2.0 or the MIT license 4 | // , at your 5 | // option. This file may not be copied, modified, or distributed 6 | // except according to those terms. 7 | 8 | //! Helpers for writing Alfred script filter output 9 | //! 10 | //! # Examples 11 | //! 12 | //! ### JSON output (Alfred 3) 13 | //! 14 | //! ``` 15 | //! # extern crate alfred; 16 | //! # 17 | //! # use std::io::{self, Write}; 18 | //! # 19 | //! # fn write_items() -> io::Result<()> { 20 | //! alfred::json::write_items(io::stdout(), &[ 21 | //! alfred::Item::new("Item 1"), 22 | //! alfred::ItemBuilder::new("Item 2") 23 | //! .subtitle("Subtitle") 24 | //! .into_item(), 25 | //! alfred::ItemBuilder::new("Item 3") 26 | //! .arg("Argument") 27 | //! .subtitle("Subtitle") 28 | //! .icon_filetype("public.folder") 29 | //! .into_item() 30 | //! ]) 31 | //! # } 32 | //! # 33 | //! # fn main() { 34 | //! # match write_items() { 35 | //! # Ok(()) => {}, 36 | //! # Err(err) => { 37 | //! # let _ = writeln!(&mut io::stderr(), "Error writing items: {}", err); 38 | //! # } 39 | //! # } 40 | //! # } 41 | //! ``` 42 | //! 43 | //! ### JSON output with variables (Alfred 3) 44 | //! 45 | //! ``` 46 | //! # extern crate alfred; 47 | //! # use alfred::Modifier; 48 | //! # use std::io::{self, Write}; 49 | //! # 50 | //! # fn write_items() -> io::Result<()> { 51 | //! alfred::json::Builder::with_items(&[ 52 | //! alfred::Item::new("Item 1"), 53 | //! alfred::ItemBuilder::new("Item 2") 54 | //! .subtitle("Subtitle") 55 | //! .variable("fruit", "banana") 56 | //! .into_item(), 57 | //! alfred::ItemBuilder::new("Item 3") 58 | //! .arg("Argument") 59 | //! .subtitle("Subtitle") 60 | //! .icon_filetype("public.folder") 61 | //! .arg_mod(Modifier::Option, "Alt Argument") 62 | //! .variable_mod(Modifier::Option, "vegetable", "carrot") 63 | //! .into_item() 64 | //! ]).variable("fruit", "banana") 65 | //! .variable("vegetable", "carrot") 66 | //! .write(io::stdout()) 67 | //! # } 68 | //! # 69 | //! # fn main() { 70 | //! # match write_items() { 71 | //! # Ok(()) => {}, 72 | //! # Err(err) => { 73 | //! # let _ = writeln!(&mut io::stderr(), "Error writing items: {}", err); 74 | //! # } 75 | //! # } 76 | //! # } 77 | //! ``` 78 | //! 79 | //! ### XML output (Alfred 2) 80 | //! 81 | //! ``` 82 | //! # extern crate alfred; 83 | //! # 84 | //! # use std::io::{self, Write}; 85 | //! # 86 | //! # fn write_items() -> io::Result<()> { 87 | //! let mut xmlw = try!(alfred::XMLWriter::new(io::stdout())); 88 | //! 89 | //! let item1 = alfred::Item::new("Item 1"); 90 | //! let item2 = alfred::ItemBuilder::new("Item 2") 91 | //! .subtitle("Subtitle") 92 | //! .into_item(); 93 | //! let item3 = alfred::ItemBuilder::new("Item 3") 94 | //! .arg("Argument") 95 | //! .subtitle("Subtitle") 96 | //! .icon_filetype("public.folder") 97 | //! .into_item(); 98 | //! 99 | //! try!(xmlw.write_item(&item1)); 100 | //! try!(xmlw.write_item(&item2)); 101 | //! try!(xmlw.write_item(&item3)); 102 | //! 103 | //! let mut stdout = try!(xmlw.close()); 104 | //! stdout.flush() 105 | //! # } 106 | //! # 107 | //! # fn main() { 108 | //! # match write_items() { 109 | //! # Ok(()) => {}, 110 | //! # Err(err) => { 111 | //! # let _ = writeln!(&mut io::stderr(), "Error writing items: {}", err); 112 | //! # } 113 | //! # } 114 | //! # } 115 | //! ``` 116 | 117 | #![warn(missing_docs)] 118 | 119 | #![doc(html_root_url = "https://docs.rs/alfred/4.0.2")] 120 | 121 | #[macro_use] 122 | extern crate serde_json; 123 | 124 | pub mod json; 125 | pub mod xml; 126 | pub mod env; 127 | 128 | use std::borrow::{Borrow, Cow}; 129 | use std::collections::HashMap; 130 | use std::iter::FromIterator; 131 | use std::hash::Hash; 132 | 133 | pub use self::xml::XMLWriter; 134 | 135 | /// Representation of a script filter item. 136 | #[derive(Clone,Debug,PartialEq,Eq)] 137 | pub struct Item<'a> { 138 | /// Title for the item. 139 | pub title: Cow<'a, str>, 140 | /// Subtitle for the item. 141 | /// 142 | /// The subtitle may be overridden by modifiers. 143 | pub subtitle: Option>, 144 | /// Icon for the item 145 | pub icon: Option>, 146 | 147 | /// Identifier for the results. 148 | /// 149 | /// If given, must be unique among items, and is used for prioritizing 150 | /// feedback results based on usage. If blank, Alfred presents results in 151 | /// the order given and does not learn from them. 152 | pub uid: Option>, 153 | /// The value that is passed to the next portion of the workflow when this 154 | /// item is selected. 155 | /// 156 | /// The arg may be overridden by modifiers. 157 | pub arg: Option>, 158 | /// What type of result this is. 159 | pub type_: ItemType, 160 | 161 | /// Whether or not the result is valid. 162 | /// 163 | /// When `false`, actioning the result will populate the search field with 164 | /// the `autocomplete` value instead. 165 | /// 166 | /// The validity may be overridden by modifiers. 167 | pub valid: bool, 168 | /// Autocomplete data for the item. 169 | /// 170 | /// This value is populated into the search field if the tab key is 171 | /// pressed. If `valid = false`, this value is populated if the item is 172 | /// actioned. 173 | pub autocomplete: Option>, 174 | /// What text the user gets when copying the result. 175 | /// 176 | /// This value is copied if the user presses ⌘C. 177 | pub text_copy: Option>, 178 | /// What text the user gets when displaying large type. 179 | /// 180 | /// This value is displayed if the user presses ⌘L. 181 | pub text_large_type: Option>, 182 | /// A URL to use for Quick Look. 183 | pub quicklook_url: Option>, 184 | 185 | /// Optional overrides of subtitle, arg, and valid by modifiers. 186 | pub modifiers: HashMap>, 187 | 188 | /// Variables to pass out of the script filter if this item is selected in Alfred's results. 189 | /// 190 | /// This property is only used with JSON output and only affects Alfred 3.4.1 or later. 191 | pub variables: HashMap, Cow<'a, str>>, 192 | 193 | /// Disallow struct literals for `Item`. 194 | _priv: () 195 | } 196 | 197 | impl<'a> Item<'a> { 198 | /// Returns a new `Item` with the given title. 199 | pub fn new>>(title: S) -> Item<'a> { 200 | Item { 201 | title: title.into(), 202 | subtitle: None, 203 | icon: None, 204 | uid: None, 205 | arg: None, 206 | type_: ItemType::Default, 207 | valid: true, 208 | autocomplete: None, 209 | text_copy: None, 210 | text_large_type: None, 211 | quicklook_url: None, 212 | modifiers: HashMap::new(), 213 | variables: HashMap::new(), 214 | _priv: () 215 | } 216 | } 217 | } 218 | 219 | /// Helper for building `Item` values. 220 | #[derive(Clone,Debug)] 221 | pub struct ItemBuilder<'a> { 222 | item: Item<'a> 223 | } 224 | 225 | impl<'a> ItemBuilder<'a> { 226 | /// Returns a new `ItemBuilder` with the given title. 227 | pub fn new>>(title: S) -> ItemBuilder<'a> { 228 | ItemBuilder { 229 | item: Item::new(title) 230 | } 231 | } 232 | 233 | /// Returns the built `Item`. 234 | pub fn into_item(self) -> Item<'a> { 235 | self.item 236 | } 237 | } 238 | 239 | impl<'a> ItemBuilder<'a> { 240 | /// Sets the `title` to the given value. 241 | pub fn title>>(mut self, title: S) -> ItemBuilder<'a> { 242 | self.set_title(title); 243 | self 244 | } 245 | 246 | /// Sets the default `subtitle` to the given value. 247 | /// 248 | /// This sets the default subtitle, used when no modifier is pressed, 249 | /// or when no subtitle is provided for the pressed modifier. 250 | pub fn subtitle>>(mut self, subtitle: S) -> ItemBuilder<'a> { 251 | self.set_subtitle(subtitle); 252 | self 253 | } 254 | 255 | /// Sets the `subtitle` to the given value with the given modifier. 256 | /// 257 | /// This sets the subtitle to use when the given modifier is pressed. 258 | pub fn subtitle_mod>>(mut self, modifier: Modifier, subtitle: S) 259 | -> ItemBuilder<'a> { 260 | self.set_subtitle_mod(modifier, subtitle); 261 | self 262 | } 263 | 264 | /// Sets the `icon` to an image file on disk. 265 | /// 266 | /// The path is interpreted relative to the workflow directory. 267 | pub fn icon_path>>(mut self, path: S) -> ItemBuilder<'a> { 268 | self.set_icon_path(path); 269 | self 270 | } 271 | 272 | /// Sets the `icon` to the icon for a given file on disk. 273 | /// 274 | /// The path is interpreted relative to the workflow directory. 275 | pub fn icon_file>>(mut self, path: S) -> ItemBuilder<'a> { 276 | self.set_icon_file(path); 277 | self 278 | } 279 | 280 | /// Sets the `icon` to the icon for a given file type. 281 | /// 282 | /// The type is a UTI, such as "public.jpeg". 283 | pub fn icon_filetype>>(mut self, filetype: S) -> ItemBuilder<'a> { 284 | self.set_icon_filetype(filetype); 285 | self 286 | } 287 | 288 | /// Sets the `icon` to an image file on disk for the given modifier. 289 | /// 290 | /// The path is interpreted relative to the workflow directory. 291 | /// 292 | /// This property is only used with JSON output. The legacy XML output does not include 293 | /// per-modifier icons. 294 | /// 295 | /// This property is only used with Alfred 3.4.1 or later. 296 | pub fn icon_path_mod>>(mut self, modifier: Modifier, path: S) 297 | -> ItemBuilder<'a> { 298 | self.set_icon_path_mod(modifier, path); 299 | self 300 | } 301 | 302 | /// Sets the `icon` to the icon for a given file on disk for the given modifier. 303 | /// 304 | /// The path is interpreted relative to the workflow directory. 305 | /// 306 | /// This property is only used with JSON output. The legacy XML output does not include 307 | /// per-modifier icons. 308 | /// 309 | /// This property is only used with Alfred 3.4.1 or later. 310 | pub fn icon_file_mod>>(mut self, modifier: Modifier, path: S) 311 | -> ItemBuilder<'a> { 312 | self.set_icon_file_mod(modifier, path); 313 | self 314 | } 315 | 316 | /// Sets the `icon` to the icon for a given file type for the given modifier. 317 | /// 318 | /// The type is a UTI, such as "public.jpeg". 319 | /// 320 | /// This property is only used with JSON output. The legacy XML output does not include 321 | /// per-modifier icons. 322 | /// 323 | /// This property is only used with Alfred 3.4.1 or later. 324 | pub fn icon_filetype_mod>>(mut self, modifier: Modifier, filetype: S) 325 | -> ItemBuilder<'a> { 326 | self.set_icon_filetype_mod(modifier, filetype); 327 | self 328 | } 329 | 330 | /// Sets the `uid` to the given value. 331 | pub fn uid>>(mut self, uid: S) -> ItemBuilder<'a> { 332 | self.set_uid(uid); 333 | self 334 | } 335 | 336 | /// Sets the `arg` to the given value. 337 | pub fn arg>>(mut self, arg: S) -> ItemBuilder<'a> { 338 | self.set_arg(arg); 339 | self 340 | } 341 | 342 | /// Sets the `arg` to the given value with the given modifier. 343 | /// 344 | /// This sets the arg to use when the given modifier is pressed. 345 | pub fn arg_mod>>(mut self, modifier: Modifier, arg: S) 346 | -> ItemBuilder<'a> { 347 | self.set_arg_mod(modifier, arg); 348 | self 349 | } 350 | 351 | /// Sets the `type` to the given value. 352 | pub fn type_(mut self, type_: ItemType) -> ItemBuilder<'a> { 353 | self.set_type(type_); 354 | self 355 | } 356 | 357 | /// Sets `valid` to the given value. 358 | pub fn valid(mut self, valid: bool) -> ItemBuilder<'a> { 359 | self.set_valid(valid); 360 | self 361 | } 362 | 363 | /// Sets `valid` to the given value with the given modifier. 364 | /// 365 | /// This sets the validity to use when the given modifier is pressed. 366 | pub fn valid_mod(mut self, modifier: Modifier, valid: bool) -> ItemBuilder<'a> { 367 | self.set_valid_mod(modifier, valid); 368 | self 369 | } 370 | 371 | /// Sets the subtitle, arg, validity, and icon to use with the given modifier. 372 | pub fn modifier>, S2: Into>>(mut self, 373 | modifier: Modifier, 374 | subtitle: Option, 375 | arg: Option, 376 | valid: bool, 377 | icon: Option>) 378 | -> ItemBuilder<'a> { 379 | self.set_modifier(modifier, subtitle, arg, valid, icon); 380 | self 381 | } 382 | 383 | /// Sets `autocomplete` to the given value. 384 | pub fn autocomplete>>(mut self, autocomplete: S) -> ItemBuilder<'a> { 385 | self.set_autocomplete(autocomplete); 386 | self 387 | } 388 | 389 | /// Sets `text_copy` to the given value. 390 | pub fn text_copy>>(mut self, text: S) -> ItemBuilder<'a> { 391 | self.set_text_copy(text); 392 | self 393 | } 394 | 395 | /// Sets `text_large_type` to the given value. 396 | pub fn text_large_type>>(mut self, text: S) -> ItemBuilder<'a> { 397 | self.set_text_large_type(text); 398 | self 399 | } 400 | 401 | /// Sets `quicklook_url` to the given value. 402 | pub fn quicklook_url>>(mut self, url: S) -> ItemBuilder<'a> { 403 | self.set_quicklook_url(url); 404 | self 405 | } 406 | 407 | /// Inserts a key/value pair into the item variables. 408 | /// 409 | /// Item variables are only used with JSON output and only affect Alfred 3.4.1 or later. 410 | pub fn variable(mut self, key: K, value: V) -> ItemBuilder<'a> 411 | where K: Into>, 412 | V: Into> 413 | { 414 | self.set_variable(key, value); 415 | self 416 | } 417 | 418 | /// Sets the item's variables to `variables`. 419 | /// 420 | /// Item variables are only used with JSON output and only affect Alfred 3.4.1 or later. 421 | pub fn variables(mut self, variables: I) -> ItemBuilder<'a> 422 | where I: IntoIterator, 423 | K: Into>, 424 | V: Into> 425 | { 426 | self.set_variables(variables); 427 | self 428 | } 429 | 430 | /// Inserts a key/value pair into the variables for the given modifier. 431 | /// 432 | /// Item variables are only used with JSON output and only affect Alfred 3.4.1 or later. 433 | pub fn variable_mod(mut self, modifier: Modifier, key: K, value: V) -> ItemBuilder<'a> 434 | where K: Into>, 435 | V: Into> 436 | { 437 | self.set_variable_mod(modifier, key, value); 438 | self 439 | } 440 | 441 | /// Sets the variables to `variables` for the given modifier. 442 | /// 443 | /// Item variables are only used with JSON output and only affect Alfred 3.4.1 or later. 444 | pub fn variables_mod(mut self, modifier: Modifier, variables: I) -> ItemBuilder<'a> 445 | where I: IntoIterator, 446 | K: Into>, 447 | V: Into> 448 | { 449 | self.set_variables_mod(modifier, variables); 450 | self 451 | } 452 | } 453 | 454 | impl<'a> ItemBuilder<'a> { 455 | /// Sets the `title` to the given value. 456 | pub fn set_title>>(&mut self, title: S) { 457 | self.item.title = title.into(); 458 | } 459 | 460 | /// Sets the default `subtitle` to the given value. 461 | pub fn set_subtitle>>(&mut self, subtitle: S) { 462 | self.item.subtitle = Some(subtitle.into()); 463 | } 464 | 465 | /// Unsets the default `subtitle`. 466 | pub fn unset_subtitle(&mut self) { 467 | self.item.subtitle = None; 468 | } 469 | 470 | /// Sets the `subtitle` to the given value for the given modifier. 471 | pub fn set_subtitle_mod>>(&mut self, modifier: Modifier, subtitle: S) { 472 | self.data_for_modifier(modifier).subtitle = Some(subtitle.into()); 473 | } 474 | 475 | /// Unsets the `subtitle` for the given modifier. 476 | /// 477 | /// This unsets the subtitle that's used when the given modifier is pressed. 478 | pub fn unset_subtitle_mod(&mut self, modifier: Modifier) { 479 | use std::collections::hash_map::Entry; 480 | if let Entry::Occupied(mut entry) = self.item.modifiers.entry(modifier) { 481 | entry.get_mut().subtitle = None; 482 | if entry.get().is_empty() { 483 | entry.remove(); 484 | } 485 | } 486 | } 487 | 488 | /// Clears the `subtitle` for all modifiers. 489 | /// 490 | /// This unsets both the default subtitle and the per-modifier subtitles. 491 | pub fn clear_subtitle(&mut self) { 492 | self.item.subtitle = None; 493 | for &modifier in ALL_MODIFIERS { 494 | self.unset_subtitle_mod(modifier); 495 | } 496 | } 497 | 498 | /// Sets the `icon` to an image file on disk. 499 | /// 500 | /// The path is interpreted relative to the workflow directory. 501 | pub fn set_icon_path>>(&mut self, path: S) { 502 | self.item.icon = Some(Icon::Path(path.into())); 503 | } 504 | 505 | /// Sets the `icon` to the icon for a given file on disk. 506 | /// 507 | /// The path is interpreted relative to the workflow directory. 508 | pub fn set_icon_file>>(&mut self, path: S) { 509 | self.item.icon = Some(Icon::File(path.into())); 510 | } 511 | 512 | /// Sets the `icon` to the icon for a given file type. 513 | /// 514 | /// The type is a UTI, such as "public.jpeg". 515 | pub fn set_icon_filetype>>(&mut self, filetype: S) { 516 | self.item.icon = Some(Icon::FileType(filetype.into())); 517 | } 518 | 519 | /// Unsets the `icon`. 520 | pub fn unset_icon(&mut self) { 521 | self.item.icon = None; 522 | } 523 | 524 | /// Sets `icon` to an image file on disk for the given modifier. 525 | /// 526 | /// The path is interpreted relative to the workflow directory. 527 | /// 528 | /// This property is only used with JSON output. The legacy XML output does not include 529 | /// per-modifier icons. 530 | /// 531 | /// This property is only used with Alfred 3.4.1 or later. 532 | pub fn set_icon_path_mod>>(&mut self, modifier: Modifier, path: S) { 533 | self.data_for_modifier(modifier).icon = Some(Icon::Path(path.into())); 534 | } 535 | 536 | /// Sets `icon` to the icon for a given file on disk for the given modifier. 537 | /// 538 | /// The path is interpreted relative to the workflow directory. 539 | /// 540 | /// This property is only used with JSON output. The legacy XML output does not include 541 | /// per-modifier icons. 542 | /// 543 | /// This property is only used with Alfred 3.4.1 or later. 544 | pub fn set_icon_file_mod>>(&mut self, modifier: Modifier, path: S) { 545 | self.data_for_modifier(modifier).icon = Some(Icon::File(path.into())); 546 | } 547 | 548 | /// Sets `icon` to the icon for a given file type for the given modifier. 549 | /// 550 | /// The type is a UTI, such as "public.jpeg". 551 | /// 552 | /// This property is only used with JSON output. The legacy XML output does not include 553 | /// per-modifier icons. 554 | /// 555 | /// This property is only used with Alfred 3.4.1 or later. 556 | pub fn set_icon_filetype_mod>>(&mut self, modifier: Modifier, 557 | filetype: S) { 558 | self.data_for_modifier(modifier).icon = Some(Icon::FileType(filetype.into())); 559 | } 560 | 561 | /// Unsets `icon` for the given modifier. 562 | /// 563 | /// This unsets the result icon that's used when the given modifier is pressed. 564 | pub fn unset_icon_mod(&mut self, modifier: Modifier) { 565 | use std::collections::hash_map::Entry; 566 | if let Entry::Occupied(mut entry) = self.item.modifiers.entry(modifier) { 567 | entry.get_mut().icon = None; 568 | if entry.get().is_empty() { 569 | entry.remove(); 570 | } 571 | } 572 | } 573 | 574 | /// Clears the `icon` for all modifiers. 575 | /// 576 | /// This unsets both the default icon and the per-modifier icons. 577 | pub fn clear_icon(&mut self) { 578 | self.item.icon = None; 579 | for &modifier in ALL_MODIFIERS { 580 | self.unset_icon_mod(modifier); 581 | } 582 | } 583 | 584 | /// Sets the `uid` to the given value. 585 | pub fn set_uid>>(&mut self, uid: S) { 586 | self.item.uid = Some(uid.into()); 587 | } 588 | 589 | /// Unsets the `uid`. 590 | pub fn unset_uid(&mut self) { 591 | self.item.uid = None; 592 | } 593 | 594 | /// Sets the `arg` to the given value. 595 | pub fn set_arg>>(&mut self, arg: S) { 596 | self.item.arg = Some(arg.into()); 597 | } 598 | 599 | /// Unsets the `arg`. 600 | pub fn unset_arg(&mut self) { 601 | self.item.arg = None; 602 | } 603 | 604 | /// Sets the `arg` to the given value for the given modifier. 605 | pub fn set_arg_mod>>(&mut self, modifier: Modifier, arg: S) { 606 | self.data_for_modifier(modifier).arg = Some(arg.into()); 607 | } 608 | 609 | /// Unsets the `arg` for the given modifier. 610 | /// 611 | /// This unsets the arg that's used when the given modifier is pressed. 612 | pub fn unset_arg_mod(&mut self, modifier: Modifier) { 613 | use std::collections::hash_map::Entry; 614 | if let Entry::Occupied(mut entry) = self.item.modifiers.entry(modifier) { 615 | entry.get_mut().arg = None; 616 | if entry.get().is_empty() { 617 | entry.remove(); 618 | } 619 | } 620 | } 621 | 622 | /// Clears the `arg` for all modifiers. 623 | /// 624 | /// This unsets both the default arg and the per-modifier args. 625 | pub fn clear_arg(&mut self) { 626 | self.item.arg = None; 627 | for &modifier in ALL_MODIFIERS { 628 | self.unset_arg_mod(modifier); 629 | } 630 | } 631 | 632 | /// Sets the `type` to the given value. 633 | pub fn set_type(&mut self, type_: ItemType) { 634 | self.item.type_ = type_; 635 | } 636 | 637 | // `type` doesn't need unsetting, it uses a default of DefaultItemType instead 638 | 639 | /// Sets `valid` to the given value. 640 | pub fn set_valid(&mut self, valid: bool) { 641 | self.item.valid = valid; 642 | } 643 | 644 | /// Sets `valid` to the given value for the given modifier. 645 | pub fn set_valid_mod(&mut self, modifier: Modifier, valid: bool) { 646 | self.data_for_modifier(modifier).valid = Some(valid); 647 | } 648 | 649 | /// Unsets `valid` for the given modifier. 650 | /// 651 | /// This unsets the validity that's used when the given modifier is pressed. 652 | pub fn unset_valid_mod(&mut self, modifier: Modifier) { 653 | use std::collections::hash_map::Entry; 654 | if let Entry::Occupied(mut entry) = self.item.modifiers.entry(modifier) { 655 | entry.get_mut().valid = None; 656 | if entry.get().is_empty() { 657 | entry.remove(); 658 | } 659 | } 660 | } 661 | 662 | /// Unsets `valid` for all modifiers. 663 | /// 664 | /// This resets `valid` back to the default and clears all per-modifier validity. 665 | pub fn clear_valid(&mut self) { 666 | self.item.valid = true; 667 | for &modifier in ALL_MODIFIERS { 668 | self.unset_valid_mod(modifier); 669 | } 670 | } 671 | 672 | /// Sets `autocomplete` to the given value. 673 | pub fn set_autocomplete>>(&mut self, autocomplete: S) { 674 | self.item.autocomplete = Some(autocomplete.into()); 675 | } 676 | 677 | /// Unsets `autocomplete`. 678 | pub fn unset_autocomplete(&mut self) { 679 | self.item.autocomplete = None; 680 | } 681 | 682 | /// Sets subtitle, arg, validity, and icon for the given modifier. 683 | pub fn set_modifier>, S2: Into>>(&mut self, 684 | modifier: Modifier, 685 | subtitle: Option, 686 | arg: Option, 687 | valid: bool, 688 | icon: Option>) { 689 | let data = ModifierData { 690 | subtitle: subtitle.map(Into::into), 691 | arg: arg.map(Into::into), 692 | valid: Some(valid), 693 | icon: icon, 694 | variables: HashMap::new(), 695 | _priv: () 696 | }; 697 | self.item.modifiers.insert(modifier, data); 698 | } 699 | 700 | /// Unsets subtitle, arg, and validity for the given modifier. 701 | pub fn unset_modifier(&mut self, modifier: Modifier) { 702 | self.item.modifiers.remove(&modifier); 703 | } 704 | 705 | /// Sets `text_copy` to the given value. 706 | pub fn set_text_copy>>(&mut self, text: S) { 707 | self.item.text_copy = Some(text.into()); 708 | } 709 | 710 | /// Unsets `text_copy`. 711 | pub fn unset_text_copy(&mut self) { 712 | self.item.text_copy = None; 713 | } 714 | 715 | /// Sets `text_large_type` to the given value. 716 | pub fn set_text_large_type>>(&mut self, text: S) { 717 | self.item.text_large_type = Some(text.into()); 718 | } 719 | 720 | /// Unsets `text_large_type`. 721 | pub fn unset_text_large_type(&mut self) { 722 | self.item.text_large_type = None; 723 | } 724 | 725 | /// Sets `quicklook_url` to the given value. 726 | pub fn set_quicklook_url>>(&mut self, url: S) { 727 | self.item.quicklook_url = Some(url.into()); 728 | } 729 | 730 | /// Unsets `quicklook_url`. 731 | pub fn unset_quicklook_url(&mut self) { 732 | self.item.quicklook_url = None; 733 | } 734 | 735 | /// Inserts a key/value pair into the item variables. 736 | /// 737 | /// Item variables are only used with JSON output and only affect Alfred 3.4.1 or later. 738 | pub fn set_variable(&mut self, key: K, value: V) 739 | where K: Into>, 740 | V: Into> 741 | { 742 | self.item.variables.insert(key.into(), value.into()); 743 | } 744 | 745 | /// Removes a key from the item variables. 746 | /// 747 | /// Item variables are only used with JSON output and only affect Alfred 3.4.1 or later. 748 | pub fn unset_variable(&mut self, key: &K) 749 | where Cow<'a, str>: Borrow, 750 | K: Hash + Eq 751 | { 752 | self.item.variables.remove(key); 753 | } 754 | 755 | /// Sets the item's variables to `variables`. 756 | /// 757 | /// Item variables are only used with JSON output and only affect Alfred 3.4.1 or later. 758 | pub fn set_variables(&mut self, variables: I) 759 | where I: IntoIterator, 760 | K: Into>, 761 | V: Into> 762 | { 763 | self.item.variables = HashMap::from_iter(variables.into_iter() 764 | .map(|(k,v)| (k.into(),v.into()))); 765 | } 766 | 767 | /// Removes all item variables. 768 | /// 769 | /// This does not affect per-modifier variables. 770 | /// 771 | /// Item variables are only used with JSON output and only affect Alfred 3.4.1 or later. 772 | pub fn unset_variables(&mut self) { 773 | self.item.variables.clear() 774 | } 775 | 776 | /// Inserts a key/value pair into the variables for the given modifier. 777 | /// 778 | /// Item variables are only used with JSON output and only affect Alfred 3.4.1 or later. 779 | pub fn set_variable_mod(&mut self, modifier: Modifier, key: K, value: V) 780 | where K: Into>, 781 | V: Into> 782 | { 783 | self.data_for_modifier(modifier).variables.insert(key.into(), value.into()); 784 | } 785 | 786 | /// Removes a key from the variables for the given modifier. 787 | /// 788 | /// Item variables are only used with JSON output and only affect Alfred 3.4.1 or later. 789 | pub fn unset_variable_mod(&mut self, modifier: Modifier, key: &K) 790 | where Cow<'a, str>: Borrow, 791 | K: Hash + Eq 792 | { 793 | use std::collections::hash_map::Entry; 794 | if let Entry::Occupied(mut entry) = self.item.modifiers.entry(modifier) { 795 | entry.get_mut().variables.remove(key); 796 | if entry.get().is_empty() { 797 | entry.remove(); 798 | } 799 | } 800 | } 801 | 802 | /// Sets the variables to `variables` for the given modifier. 803 | /// 804 | /// Item variables are only used with JSON output and only affect Alfred 3.4.1 or later. 805 | pub fn set_variables_mod(&mut self, modifier: Modifier, variables: I) 806 | where I: IntoIterator, 807 | K: Into>, 808 | V: Into> 809 | { 810 | self.data_for_modifier(modifier).variables = 811 | HashMap::from_iter(variables.into_iter().map(|(k,v)| (k.into(), v.into()))); 812 | } 813 | 814 | /// Removes all variables for the given modifier. 815 | /// 816 | /// Item variables are only used with JSON output and only affect Alfred 3.4.1 or later. 817 | pub fn unset_variables_mod(&mut self, modifier: Modifier) { 818 | use std::collections::hash_map::Entry; 819 | if let Entry::Occupied(mut entry) = self.item.modifiers.entry(modifier) { 820 | entry.get_mut().variables.clear(); 821 | if entry.get().is_empty() { 822 | entry.remove(); 823 | } 824 | } 825 | } 826 | 827 | /// Removes all item variables and all per-modifier variables. 828 | /// 829 | /// Item variables are only used with JSON output and only affect Alfred 3.4.1 or later. 830 | pub fn clear_variables(&mut self) { 831 | self.unset_variables(); 832 | for &modifier in ALL_MODIFIERS { 833 | self.unset_variables_mod(modifier); 834 | } 835 | } 836 | 837 | fn data_for_modifier(&mut self, modifier: Modifier) -> &mut ModifierData<'a> { 838 | self.item.modifiers.entry(modifier).or_insert_with(Default::default) 839 | } 840 | } 841 | 842 | /// Keyboard modifiers. 843 | // As far as I can tell, Alfred doesn't support modifier combinations. 844 | #[derive(Copy,Clone,Debug,PartialEq,Eq,Hash)] 845 | pub enum Modifier { 846 | /// Command key 847 | Command, 848 | /// Option/Alt key 849 | Option, 850 | /// Control key 851 | Control, 852 | /// Shift key 853 | Shift, 854 | /// Fn key 855 | Fn 856 | } 857 | 858 | const ALL_MODIFIERS: &'static [Modifier] = &[Modifier::Command, Modifier::Option, 859 | Modifier::Control, Modifier::Shift, Modifier::Fn]; 860 | 861 | /// Optional overrides of subtitle, arg, and valid for modifiers. 862 | #[derive(Clone,Debug,PartialEq,Eq,Default)] 863 | pub struct ModifierData<'a> { 864 | /// The subtitle to use for the current modifier. 865 | pub subtitle: Option>, 866 | /// The arg to use for the current modifier. 867 | pub arg: Option>, 868 | /// The validity to use for the current modifier. 869 | pub valid: Option, 870 | /// The result icon to use for the current modifier. 871 | /// 872 | /// This icon is only supported when using JSON output. The legacy XML output format does not 873 | /// support per-modifier icons. 874 | /// 875 | /// This icon is only used with Alfred 3.4.1 or later. 876 | pub icon: Option>, 877 | 878 | /// Variables to pass out of the script filter if the item is selected in Alfred's results 879 | /// using this modifier. 880 | /// 881 | /// This property is only used with JSON output and only affects Alfred 3.4.1 or later. 882 | pub variables: HashMap, Cow<'a, str>>, 883 | 884 | /// Disallow struct literals for `ModifierData`. 885 | _priv: () 886 | } 887 | 888 | impl<'a> ModifierData<'a> { 889 | /// Returns a new `ModifierData` where all fields are `None`. 890 | pub fn new() -> ModifierData<'a> { 891 | Default::default() 892 | } 893 | 894 | fn is_empty(&self) -> bool { 895 | self.subtitle.is_none() 896 | && self.arg.is_none() 897 | && self.valid.is_none() 898 | && self.icon.is_none() 899 | && self.variables.is_empty() 900 | } 901 | } 902 | 903 | /// Item icons 904 | #[derive(Clone,Debug,PartialEq,Eq,Hash)] 905 | pub enum Icon<'a> { 906 | /// Path to an image file on disk relative to the workflow directory. 907 | Path(Cow<'a, str>), 908 | /// Path to a file whose icon will be used. 909 | File(Cow<'a, str>), 910 | /// UTI for a file type to use (e.g. public.folder). 911 | FileType(Cow<'a, str>) 912 | } 913 | 914 | /// Item types 915 | #[derive(Copy,Clone,Debug,PartialEq,Eq,Hash)] 916 | pub enum ItemType { 917 | /// Default type for an item. 918 | Default, 919 | /// Type representing a file. 920 | /// 921 | /// Alredy checks that the file exists on disk, and hides the result if it 922 | /// does not. 923 | File, 924 | /// Type representing a file, with filesystem checks skipped. 925 | /// 926 | /// Similar to `File` but skips the check to ensure the file exists. 927 | FileSkipCheck 928 | } 929 | 930 | #[test] 931 | fn test_variables() { 932 | // Because we're using generics with the set/unset variables methods, let's make sure it 933 | // actually works as expected with the types we want to support. 934 | let mut builder = ItemBuilder::new("Name"); 935 | builder.set_variable("fruit", "banana"); 936 | builder.set_variable("vegetable".to_owned(), Cow::Borrowed("carrot")); 937 | let item = builder.clone().into_item(); 938 | assert_eq!(item.variables.get("fruit").as_ref().map(|x| x.as_ref()), Some("banana")); 939 | assert_eq!(item.variables.get("vegetable").as_ref().map(|x| x.as_ref()), Some("carrot")); 940 | assert_eq!(item.variables.get("meat"), None); 941 | builder.unset_variable("fruit"); 942 | builder.unset_variable("vegetable"); 943 | let item = builder.into_item(); 944 | assert_eq!(item.variables, HashMap::new()); 945 | } 946 | --------------------------------------------------------------------------------