├── .gitignore ├── .travis.yml ├── Cargo.toml ├── src ├── libcub │ ├── constants.rs │ ├── note.rs │ └── mod.rs ├── cli.yml ├── main.rs └── args.rs ├── LICENSE.md ├── README.md ├── tests └── test_lib.rs └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target 3 | **/*.rs.bk 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | matrix: 7 | allow_failures: 8 | - rust: nightly 9 | fast_finish: true 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cub" 3 | version = "0.3.3" 4 | authors = ["Andrew Huynh "] 5 | publish = false 6 | edition = "2018" 7 | 8 | [dependencies] 9 | chrono = "0.4.19" 10 | clap = { version = "2.33.3", features = ["yaml"] } 11 | env_logger = "0.7.1" 12 | dirs = "3.0.1" 13 | log = "0.4.11" 14 | rusqlite = "0.24.0" 15 | term = "0.6.1" 16 | 17 | [lib] 18 | name = "libcub" 19 | path = "src/libcub/mod.rs" 20 | 21 | [[bin]] 22 | name = "cub" 23 | path = "src/main.rs" 24 | -------------------------------------------------------------------------------- /src/libcub/constants.rs: -------------------------------------------------------------------------------- 1 | extern crate dirs; 2 | use std::path::Path; 3 | 4 | static APP_PATHS: [&str; 2] = [ 5 | "Library/Containers/net.shinyfrog.bear/Data/Documents", 6 | "Library/Group Containers/9K33E3U3T4.net.shinyfrog.bear", 7 | ]; 8 | 9 | const DB_PATH: &str = "Application Data/database.sqlite"; 10 | 11 | pub fn find_db() -> Result { 12 | let home = dirs::home_dir().unwrap(); 13 | 14 | for path in APP_PATHS.iter() { 15 | let path = home.join(format!("{}/{}", path, DB_PATH)); 16 | if let Some(path_str) = path.to_str() { 17 | if Path::new(&path_str).exists() { 18 | return Ok(String::from(path_str)); 19 | } 20 | } 21 | } 22 | 23 | Err("Unable to find Bear database.") 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Andrew Huynh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/cli.yml: -------------------------------------------------------------------------------- 1 | name: cub 2 | version: "0.3.3" 3 | author: Andrew Huynh 4 | about: Command-line Utility for Bear. 5 | args: 6 | - db: 7 | help: Bear data file to pull data from. 8 | short: d 9 | global: true 10 | takes_value: true 11 | subcommands: 12 | - ls: 13 | about: List notes. 14 | args: 15 | - all: 16 | short: a 17 | help: Show *all* notes. 18 | conflicts_with: limit 19 | - color: 20 | short: c 21 | help: Colorize notes based on their status (archived, trashed, etc.) 22 | - filter: 23 | short: f 24 | help: Filter notes by status 25 | takes_value: true 26 | multiple: true 27 | - full: 28 | help: Also print out note text 29 | - limit: 30 | short: l 31 | help: Limit the number of notes printed. 32 | takes_value: true 33 | conflicts_with: all 34 | - sort: 35 | short: s 36 | help: Sort by , 37 | takes_value: true 38 | - tags: 39 | short: t 40 | help: Filter notes by tag 41 | takes_value: true 42 | multiple: true 43 | - show: 44 | about: Show a single note. 45 | args: 46 | - NOTE: 47 | help: Note ID 48 | required: true 49 | index: 1 50 | - tags: 51 | about: Show tags -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/a5huynh/cub-cli.svg?branch=master)](https://travis-ci.org/a5huynh/cub-cli) [![license: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 2 | 3 | ## CUB 4 | 5 | CUB stands for **C**ommand-line **U**tility for [**B**ear][bear-app] and offers a simple CLI interface 6 | to the notes, tags, and media stored within your bear writer application. 7 | 8 | [bear-app]: https://bear.app/ 9 | 10 | ## TODO 11 | - [ ] List notes. 12 | - [ ] Configure rendering options 13 | - [x] limit 14 | - [x] w/ text. 15 | - [ ] w/ creation/modification date. 16 | - [x] Filter by note status. 17 | - [x] Filter by tag. 18 | - [x] View a single note. 19 | - [ ] View fun stats for all notes/note? 20 | 21 | 22 | ## Installing 23 | brew tap a5huynh/brew 24 | brew install a5huynh/brew/cub-cli 25 | 26 | 27 | ## How to do things 28 | 29 | Here is how to do some common things with the `cub` CLI. Also check out 30 | `cub --help` to see all command/options you can use. 31 | 32 | ### Listing out notes. 33 | 34 | # List _all_ notes. 35 | > cub ls 36 | 37 | # Limit output to 10 notes 38 | > cub ls --limit 10 39 | > cub ls -l 10 40 | 41 | # List notes w/ full-text 42 | # BEWARE: If you have a lot of very large notes this _will_ output the 43 | # entirety of the text to the terminal. 44 | > cub ls --text 45 | > cub ls -t 46 | 47 | ### View a note. 48 | 49 | # Notes are prefixed with their ID in the `ls` command. Use that ID 50 | # here to output the full text. 51 | > cub show 571 52 | 53 | 54 | ## Building from scratch 55 | 56 | CUB is build using the latest stable version of rust-lang and can be built 57 | from scratch using the following command: 58 | 59 | cargo build --release 60 | -------------------------------------------------------------------------------- /tests/test_lib.rs: -------------------------------------------------------------------------------- 1 | extern crate libcub; 2 | extern crate rusqlite; 3 | 4 | use libcub::{list_notes, Limit, SortOrder}; 5 | use rusqlite::{params, Connection}; 6 | 7 | /// Bootstraps a test db with a table with a similar schema to the Bear notes db 8 | /// and some notes. 9 | fn bootstrap(conn: &Connection) { 10 | conn.execute( 11 | "CREATE TABLE ZSFNOTE ( 12 | Z_PK INTEGER PRIMARY KEY, 13 | ZARCHIVED INTEGER, 14 | ZTITLE VARCHAR, 15 | ZSUBTITLE VARCHAR, 16 | ZTEXT VARCHAR, 17 | ZLASTEDITINGDEVICE VARCHAR, 18 | ZCREATIONDATE TIMESTAMP, 19 | ZMODIFICATIONDATE TIMESTAMP, 20 | ZTRASHED INTEGER)", 21 | params![], 22 | ) 23 | .unwrap(); 24 | 25 | conn.execute( 26 | "INSERT INTO ZSFNOTE ( 27 | Z_PK, ZARCHIVED, ZTITLE, ZSUBTITLE, ZTEXT, ZLASTEDITINGDEVICE, 28 | ZCREATIONDATE, ZMODIFICATIONDATE, ZTRASHED 29 | ) VALUES ( 30 | 1, 0, 'title', 'subtitle', 'text body', 'device', 0, 0, 0 31 | )", 32 | params![], 33 | ) 34 | .unwrap(); 35 | 36 | conn.execute( 37 | "INSERT INTO ZSFNOTE ( 38 | Z_PK, ZARCHIVED, ZTITLE, ZSUBTITLE, ZTEXT, ZLASTEDITINGDEVICE, 39 | ZCREATIONDATE, ZMODIFICATIONDATE, ZTRASHED 40 | ) VALUES ( 41 | 2, 0, 'title', NULL, NULL, 'device', 0, 0, 0 42 | )", 43 | params![], 44 | ) 45 | .unwrap(); 46 | } 47 | 48 | #[test] 49 | fn test_list_notes() { 50 | let conn = Connection::open_in_memory().unwrap(); 51 | bootstrap(&conn); 52 | 53 | let notes = list_notes(&conn, &[], &SortOrder::Title, &[], &Limit::INFINITE).unwrap(); 54 | assert_eq!(notes.len(), 2); 55 | } 56 | -------------------------------------------------------------------------------- /src/libcub/note.rs: -------------------------------------------------------------------------------- 1 | extern crate chrono; 2 | use chrono::prelude::*; 3 | use rusqlite::{Result, Row}; 4 | use std::fmt; 5 | 6 | #[derive(Debug, PartialEq)] 7 | pub enum NoteStatus { 8 | ARCHIVED, 9 | TRASHED, 10 | NORMAL, 11 | } 12 | 13 | impl fmt::Display for NoteStatus { 14 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 15 | match *self { 16 | NoteStatus::ARCHIVED => write!(f, "A"), 17 | NoteStatus::TRASHED => write!(f, "T"), 18 | NoteStatus::NORMAL => write!(f, "."), 19 | } 20 | } 21 | } 22 | 23 | #[derive(Debug)] 24 | pub struct Note { 25 | pub pk: i32, 26 | pub title: String, 27 | pub subtitle: Option<String>, 28 | pub text: Option<String>, 29 | pub last_editing_device: String, 30 | pub creation_date: NaiveDateTime, 31 | pub modification_date: NaiveDateTime, 32 | pub status: NoteStatus, 33 | } 34 | 35 | impl Note { 36 | pub fn from_sql(row: &Row) -> Result<Note> { 37 | let mut status = NoteStatus::NORMAL; 38 | if row.get::<usize, i32>(7)? == 1 { 39 | // Is it archived? 40 | status = NoteStatus::ARCHIVED; 41 | } else if row.get::<usize, i32>(8)? == 1 { 42 | // Is it trashed? 43 | status = NoteStatus::TRASHED; 44 | } 45 | 46 | Ok(Note { 47 | pk: row.get(0)?, 48 | title: row.get(1)?, 49 | subtitle: row.get(2)?, 50 | text: row.get(3)?, 51 | last_editing_device: row.get(4)?, 52 | creation_date: NaiveDateTime::parse_from_str( 53 | row.get::<usize, String>(5)?.as_str(), 54 | "%Y-%m-%d %H:%M:%S", 55 | ) 56 | .unwrap(), 57 | modification_date: NaiveDateTime::parse_from_str( 58 | row.get::<usize, String>(6)?.as_str(), 59 | "%Y-%m-%d %H:%M:%S", 60 | ) 61 | .unwrap(), 62 | status, 63 | }) 64 | } 65 | } 66 | 67 | impl fmt::Display for Note { 68 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 69 | write!( 70 | f, 71 | "{:-4} {} {} {}", 72 | self.pk, self.status, self.modification_date, self.title 73 | ) 74 | } 75 | } 76 | 77 | pub struct Tag { 78 | pub pk: u32, 79 | pub title: String, 80 | } 81 | 82 | impl Tag { 83 | pub fn from_sql(row: &Row) -> Result<Tag> { 84 | Ok(Tag { 85 | pk: row.get(0)?, 86 | title: row.get(1)?, 87 | }) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate clap; 3 | extern crate dirs; 4 | #[macro_use] 5 | extern crate log; 6 | extern crate env_logger; 7 | extern crate term; 8 | 9 | use clap::App; 10 | use std::io::prelude::*; 11 | 12 | mod args; 13 | use crate::args::{parse_filters, parse_limit, parse_sort, parse_tags}; 14 | 15 | extern crate libcub; 16 | use libcub::constants::find_db; 17 | use libcub::note::NoteStatus; 18 | use libcub::{connect_to_db, find_note_by_id, list_notes, list_tags}; 19 | 20 | fn main() { 21 | env_logger::init(); 22 | let yaml = load_yaml!("cli.yml"); 23 | let matches = App::from_yaml(yaml).get_matches(); 24 | let mut t = term::stdout().unwrap(); 25 | 26 | // Find the path to the Bear sqlite file. 27 | // I assume that the sqlite file is in the same place for all installs, 28 | // but make that option configurable. 29 | let db_file_path = match find_db() { 30 | Ok(db_path) => db_path, 31 | Err(message) => { 32 | eprint!("{}", message); 33 | return; 34 | } 35 | }; 36 | 37 | let db_opt = matches 38 | .value_of("db") 39 | .unwrap_or_else(|| db_file_path.as_str()); 40 | 41 | info!("db path set to: {}", db_opt); 42 | 43 | // Attempt to detect and connect to the Bear sqlite database 44 | let conn = connect_to_db(db_opt); 45 | 46 | // Parse command line args and determine which subcommand to execute. 47 | if let Some(matches) = matches.subcommand_matches("ls") { 48 | let filters = parse_filters(matches); 49 | let limit = parse_limit(matches); 50 | let sort = parse_sort(matches); 51 | let tags = parse_tags(matches); 52 | 53 | for note in list_notes(&conn, &filters, &sort, &tags, &limit).unwrap() { 54 | // Color the notes depending on the note status 55 | if matches.is_present("color") { 56 | match note.status { 57 | NoteStatus::NORMAL => t.fg(term::color::WHITE).unwrap(), 58 | NoteStatus::TRASHED => t.fg(term::color::RED).unwrap(), 59 | NoteStatus::ARCHIVED => t.fg(term::color::GREEN).unwrap(), 60 | } 61 | } 62 | 63 | writeln!(t, "{}", note).unwrap(); 64 | if matches.is_present("full") { 65 | writeln!( 66 | t, 67 | "{}", 68 | note.subtitle.unwrap_or_else(|| String::from("N/A")) 69 | ) 70 | .unwrap(); 71 | } 72 | 73 | // Unset any coloring we did 74 | if matches.is_present("color") { 75 | t.reset().unwrap(); 76 | } 77 | } 78 | } else if let Some(matches) = matches.subcommand_matches("show") { 79 | let note_id: i32 = match matches.value_of("NOTE").unwrap().parse() { 80 | Ok(value) => value, 81 | Err(_) => { 82 | writeln!(t, "Note id must be a valid integer.").unwrap(); 83 | ::std::process::exit(1); 84 | } 85 | }; 86 | 87 | let note = find_note_by_id(&conn, note_id).unwrap(); 88 | writeln!(t, "{}", note.text.unwrap_or_else(|| String::from("N/A"))).unwrap(); 89 | } else if matches.subcommand_matches("tags").is_some() { 90 | for tag in list_tags(&conn).unwrap() { 91 | writeln!(t, "{}", tag.title).unwrap(); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/libcub/mod.rs: -------------------------------------------------------------------------------- 1 | extern crate chrono; 2 | extern crate rusqlite; 3 | use rusqlite::{Connection, NO_PARAMS}; 4 | 5 | pub mod constants; 6 | pub mod note; 7 | use self::note::{Note, NoteStatus, Tag}; 8 | 9 | pub enum Limit { 10 | INFINITE, 11 | FINITE(i32), 12 | } 13 | 14 | #[derive(Debug, PartialEq)] 15 | pub enum SortOrder { 16 | DateCreated, 17 | DateUpdated, 18 | Title, 19 | } 20 | 21 | const BASE_NOTE_QUERY: &str = "SELECT 22 | Z_PK, 23 | ZTITLE, 24 | ZSUBTITLE, 25 | ZTEXT, 26 | ZLASTEDITINGDEVICE, 27 | datetime(ZCREATIONDATE, 'unixepoch', '+31 years'), 28 | datetime(ZMODIFICATIONDATE, 'unixepoch', '+31 years'), 29 | ZARCHIVED, 30 | ZTRASHED 31 | FROM ZSFNOTE"; 32 | 33 | const BASE_TAG_QUERY: &str = "SELECT Z_PK, ZTITLE FROM ZSFNOTETAG ORDER BY ZTITLE"; 34 | // Only a partial query, the full query is constructed in `apply_filters` 35 | const NOTE_TAG_PARTIAL: &str = "SELECT 36 | Z_7NOTES FROM Z_7TAGS 37 | WHERE Z_14TAGS IN 38 | (SELECT Z_PK FROM ZSFNOTETAG WHERE ZTITLE IN"; 39 | 40 | /// Detect and connect to the Bear application sqlite database. 41 | pub fn connect_to_db(datafile: &str) -> Connection { 42 | Connection::open(datafile).unwrap() 43 | } 44 | 45 | fn apply_filters( 46 | query: &str, 47 | filters: &[NoteStatus], 48 | sort_order: &SortOrder, 49 | tags: &[String], 50 | ) -> String { 51 | let mut filter_sql = Vec::new(); 52 | let mut query_str = String::from(query); 53 | 54 | for filter in filters { 55 | match filter { 56 | NoteStatus::ARCHIVED => filter_sql.push("ZARCHIVED = 1"), 57 | NoteStatus::TRASHED => filter_sql.push("ZTRASHED = 1"), 58 | NoteStatus::NORMAL => filter_sql.push("(ZARCHIVED = 0 AND ZTRASHED = 0)"), 59 | } 60 | } 61 | 62 | if !filter_sql.is_empty() { 63 | query_str = format!("{} WHERE {}", query, filter_sql.join(" OR ")); 64 | } 65 | 66 | if !tags.is_empty() { 67 | let tag_filter = format!( 68 | "Z_PK IN ({} (\"{}\")))", 69 | NOTE_TAG_PARTIAL, 70 | tags.join("\",\"") 71 | ); 72 | 73 | if !filter_sql.is_empty() { 74 | query_str = format!("{} AND ({})", query_str, tag_filter); 75 | } else { 76 | query_str = format!("{} WHERE ({})", query_str, tag_filter); 77 | } 78 | } 79 | 80 | match sort_order { 81 | SortOrder::DateCreated => { 82 | query_str = format!("{} ORDER BY ZCREATIONDATE DESC", query_str); 83 | } 84 | SortOrder::DateUpdated => { 85 | query_str = format!("{} ORDER BY ZMODIFICATIONDATE DESC", query_str); 86 | } 87 | SortOrder::Title => { 88 | query_str = format!("{} ORDER BY ZTITLE", query_str); 89 | } 90 | } 91 | 92 | query_str 93 | } 94 | 95 | /// Find a single note by ID 96 | pub fn find_note_by_id(conn: &Connection, note_id: i32) -> Result<Note, &'static str> { 97 | let mut stmt = conn 98 | .prepare(format!("{} WHERE Z_PK =?", BASE_NOTE_QUERY).as_str()) 99 | .unwrap(); 100 | let note = stmt 101 | .query_row(&[¬e_id], |row| Note::from_sql(row)) 102 | .unwrap(); 103 | 104 | Ok(note) 105 | } 106 | 107 | /// List all notes 108 | pub fn list_notes( 109 | conn: &Connection, 110 | filters: &[NoteStatus], 111 | sort_order: &SortOrder, 112 | tags: &[String], 113 | limit: &Limit, 114 | ) -> Result<Vec<Note>, &'static str> { 115 | let applied = apply_filters(&BASE_NOTE_QUERY, filters, sort_order, tags); 116 | 117 | let mut notes = Vec::new(); 118 | 119 | match limit { 120 | // Show all notes 121 | Limit::INFINITE => { 122 | let mut stmt = conn.prepare(&applied.as_str()).unwrap(); 123 | let note_iter = stmt 124 | .query_map(NO_PARAMS, |row| Note::from_sql(row)) 125 | .unwrap(); 126 | for note in note_iter { 127 | notes.push(note.unwrap()); 128 | } 129 | } 130 | // Apply limit to number of notes returned 131 | Limit::FINITE(val) => { 132 | let mut stmt = conn 133 | .prepare(format!("{} LIMIT ?", &applied.as_str()).as_str()) 134 | .unwrap(); 135 | let note_iter = stmt.query_map(&[val], |row| Note::from_sql(row)).unwrap(); 136 | for note in note_iter { 137 | notes.push(note.unwrap()); 138 | } 139 | } 140 | } 141 | 142 | Ok(notes) 143 | } 144 | 145 | /// List all tags 146 | pub fn list_tags(conn: &Connection) -> Result<Vec<Tag>, &'static str> { 147 | let mut stmt = conn.prepare(BASE_TAG_QUERY).unwrap(); 148 | let mut tags = Vec::new(); 149 | 150 | let tag_iter = stmt.query_map(NO_PARAMS, |row| Tag::from_sql(row)).unwrap(); 151 | for tag in tag_iter { 152 | tags.push(tag.unwrap()); 153 | } 154 | 155 | Ok(tags) 156 | } 157 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | /// Helper functions to parse CLI args 2 | use clap; 3 | use libcub::note::NoteStatus; 4 | 5 | use libcub::{Limit, SortOrder}; 6 | 7 | /// Create a list of filters from the command line args. 8 | pub fn parse_filters(matches: &clap::ArgMatches) -> Vec<NoteStatus> { 9 | let mut filters = Vec::new(); 10 | 11 | if matches.is_present("filter") { 12 | let matched = matches.values_of("filter").unwrap(); 13 | for filter in matched { 14 | match filter { 15 | "archived" => filters.push(NoteStatus::ARCHIVED), 16 | "normal" => filters.push(NoteStatus::NORMAL), 17 | "trashed" => filters.push(NoteStatus::TRASHED), 18 | // Simply ignore all other strings 19 | _ => {} 20 | } 21 | } 22 | } 23 | 24 | filters 25 | } 26 | 27 | /// Parse limit arg 28 | pub fn parse_limit(matches: &clap::ArgMatches) -> Limit { 29 | if matches.is_present("all") { 30 | return Limit::INFINITE; 31 | } 32 | 33 | if matches.is_present("limit") { 34 | let limit_str = matches.value_of("limit").unwrap(); 35 | return Limit::FINITE(limit_str.parse::<i32>().unwrap_or(100)); 36 | } 37 | 38 | Limit::FINITE(100) 39 | } 40 | 41 | /// Parse sort word 42 | pub fn parse_sort(matches: &clap::ArgMatches) -> SortOrder { 43 | let mut sort_order = SortOrder::DateUpdated; 44 | 45 | if matches.is_present("sort") { 46 | let sort_str = matches.value_of("sort").unwrap(); 47 | if sort_str == "created" { 48 | sort_order = SortOrder::DateCreated; 49 | } else if sort_str == "title" { 50 | sort_order = SortOrder::Title; 51 | } else if sort_str == "updated" { 52 | sort_order = SortOrder::DateUpdated; 53 | } 54 | } 55 | 56 | sort_order 57 | } 58 | 59 | /// Parse tag strings 60 | pub fn parse_tags(matches: &clap::ArgMatches) -> Vec<String> { 61 | let mut tags = Vec::new(); 62 | 63 | if matches.is_present("tags") { 64 | let matched = matches.values_of("tags").unwrap(); 65 | for tag in matched { 66 | tags.push(String::from(tag)) 67 | } 68 | } 69 | 70 | tags 71 | } 72 | 73 | #[cfg(test)] 74 | mod tests { 75 | use crate::args::{parse_filters, parse_limit, parse_sort, parse_tags, Limit, SortOrder}; 76 | use clap::App; 77 | use libcub::note::NoteStatus; 78 | 79 | #[test] 80 | fn test_parse_filters() { 81 | let yaml = load_yaml!("cli.yml"); 82 | let app = App::from_yaml(yaml); 83 | let matches = app.get_matches_from(vec!["cub", "ls", "-f", "archived"]); 84 | 85 | let subcommand = matches.subcommand_matches("ls").unwrap(); 86 | let filters = parse_filters(subcommand); 87 | assert_eq!(filters.len(), 1); 88 | assert_eq!(filters[0], NoteStatus::ARCHIVED); 89 | } 90 | 91 | #[test] 92 | fn test_parse_limit() { 93 | let yaml = load_yaml!("cli.yml"); 94 | let app = App::from_yaml(yaml); 95 | 96 | // Testing a valid limit value 97 | let matches = app.get_matches_from(vec!["cub", "ls", "-l", "42"]); 98 | let subcommand = matches.subcommand_matches("ls").unwrap(); 99 | let limit = parse_limit(subcommand); 100 | 101 | match limit { 102 | Limit::FINITE(val) => assert_eq!(val, 42), 103 | Limit::INFINITE => {} 104 | } 105 | } 106 | 107 | #[test] 108 | fn test_parse_limit_failure() { 109 | let yaml = load_yaml!("cli.yml"); 110 | let app = App::from_yaml(yaml); 111 | 112 | // Testing an invalid limit value 113 | let matches = app.get_matches_from(vec!["cub", "ls", "-l", "cheese"]); 114 | let subcommand = matches.subcommand_matches("ls").unwrap(); 115 | let limit = parse_limit(subcommand); 116 | 117 | match limit { 118 | Limit::FINITE(val) => assert_eq!(val, 100), 119 | Limit::INFINITE => {} 120 | } 121 | } 122 | 123 | #[test] 124 | fn test_parse_sort() { 125 | let yaml = load_yaml!("cli.yml"); 126 | let app = App::from_yaml(yaml); 127 | 128 | // Testing a valid limit value 129 | let matches = app.get_matches_from(vec!["cub", "ls", "-s", "created"]); 130 | let subcommand = matches.subcommand_matches("ls").unwrap(); 131 | let sort = parse_sort(subcommand); 132 | 133 | assert_eq!(sort, SortOrder::DateCreated); 134 | } 135 | 136 | #[test] 137 | fn test_parse_tags() { 138 | let yaml = load_yaml!("cli.yml"); 139 | let app = App::from_yaml(yaml); 140 | 141 | // Testing a valid limit value 142 | let matches = app.get_matches_from(vec!["cub", "ls", "-t", "cooking", "-t", "drafts"]); 143 | let subcommand = matches.subcommand_matches("ls").unwrap(); 144 | let tags = parse_tags(subcommand); 145 | 146 | assert_eq!(tags.len(), 2); 147 | assert_eq!(tags[0], "cooking"); 148 | assert_eq!(tags[1], "drafts"); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.13" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "ansi_term" 16 | version = "0.11.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 19 | dependencies = [ 20 | "winapi", 21 | ] 22 | 23 | [[package]] 24 | name = "arrayref" 25 | version = "0.3.6" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" 28 | 29 | [[package]] 30 | name = "arrayvec" 31 | version = "0.5.1" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" 34 | 35 | [[package]] 36 | name = "atty" 37 | version = "0.2.14" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 40 | dependencies = [ 41 | "hermit-abi", 42 | "libc", 43 | "winapi", 44 | ] 45 | 46 | [[package]] 47 | name = "autocfg" 48 | version = "1.0.1" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 51 | 52 | [[package]] 53 | name = "base64" 54 | version = "0.12.3" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" 57 | 58 | [[package]] 59 | name = "bitflags" 60 | version = "1.2.1" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 63 | 64 | [[package]] 65 | name = "blake2b_simd" 66 | version = "0.5.10" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" 69 | dependencies = [ 70 | "arrayref", 71 | "arrayvec", 72 | "constant_time_eq", 73 | ] 74 | 75 | [[package]] 76 | name = "cfg-if" 77 | version = "0.1.10" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 80 | 81 | [[package]] 82 | name = "chrono" 83 | version = "0.4.19" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 86 | dependencies = [ 87 | "libc", 88 | "num-integer", 89 | "num-traits", 90 | "time", 91 | "winapi", 92 | ] 93 | 94 | [[package]] 95 | name = "clap" 96 | version = "2.33.3" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 99 | dependencies = [ 100 | "ansi_term", 101 | "atty", 102 | "bitflags", 103 | "strsim", 104 | "textwrap", 105 | "unicode-width", 106 | "vec_map", 107 | "yaml-rust", 108 | ] 109 | 110 | [[package]] 111 | name = "constant_time_eq" 112 | version = "0.1.5" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" 115 | 116 | [[package]] 117 | name = "crossbeam-utils" 118 | version = "0.7.2" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" 121 | dependencies = [ 122 | "autocfg", 123 | "cfg-if", 124 | "lazy_static", 125 | ] 126 | 127 | [[package]] 128 | name = "cub" 129 | version = "0.3.3" 130 | dependencies = [ 131 | "chrono", 132 | "clap", 133 | "dirs 3.0.1", 134 | "env_logger", 135 | "log", 136 | "rusqlite", 137 | "term", 138 | ] 139 | 140 | [[package]] 141 | name = "dirs" 142 | version = "2.0.2" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" 145 | dependencies = [ 146 | "cfg-if", 147 | "dirs-sys", 148 | ] 149 | 150 | [[package]] 151 | name = "dirs" 152 | version = "3.0.1" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff" 155 | dependencies = [ 156 | "dirs-sys", 157 | ] 158 | 159 | [[package]] 160 | name = "dirs-sys" 161 | version = "0.3.5" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" 164 | dependencies = [ 165 | "libc", 166 | "redox_users", 167 | "winapi", 168 | ] 169 | 170 | [[package]] 171 | name = "env_logger" 172 | version = "0.7.1" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" 175 | dependencies = [ 176 | "atty", 177 | "humantime", 178 | "log", 179 | "regex", 180 | "termcolor", 181 | ] 182 | 183 | [[package]] 184 | name = "fallible-iterator" 185 | version = "0.2.0" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" 188 | 189 | [[package]] 190 | name = "fallible-streaming-iterator" 191 | version = "0.1.9" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 194 | 195 | [[package]] 196 | name = "getrandom" 197 | version = "0.1.15" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" 200 | dependencies = [ 201 | "cfg-if", 202 | "libc", 203 | "wasi 0.9.0+wasi-snapshot-preview1", 204 | ] 205 | 206 | [[package]] 207 | name = "hermit-abi" 208 | version = "0.1.16" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "4c30f6d0bc6b00693347368a67d41b58f2fb851215ff1da49e90fe2c5c667151" 211 | dependencies = [ 212 | "libc", 213 | ] 214 | 215 | [[package]] 216 | name = "humantime" 217 | version = "1.3.0" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" 220 | dependencies = [ 221 | "quick-error", 222 | ] 223 | 224 | [[package]] 225 | name = "lazy_static" 226 | version = "1.4.0" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 229 | 230 | [[package]] 231 | name = "libc" 232 | version = "0.2.77" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "f2f96b10ec2560088a8e76961b00d47107b3a625fecb76dedb29ee7ccbf98235" 235 | 236 | [[package]] 237 | name = "libsqlite3-sys" 238 | version = "0.20.0" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "e3a245984b1b06c291f46e27ebda9f369a94a1ab8461d0e845e23f9ced01f5db" 241 | dependencies = [ 242 | "pkg-config", 243 | "vcpkg", 244 | ] 245 | 246 | [[package]] 247 | name = "linked-hash-map" 248 | version = "0.5.3" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" 251 | 252 | [[package]] 253 | name = "log" 254 | version = "0.4.11" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" 257 | dependencies = [ 258 | "cfg-if", 259 | ] 260 | 261 | [[package]] 262 | name = "lru-cache" 263 | version = "0.1.2" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" 266 | dependencies = [ 267 | "linked-hash-map", 268 | ] 269 | 270 | [[package]] 271 | name = "memchr" 272 | version = "2.3.3" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 275 | 276 | [[package]] 277 | name = "num-integer" 278 | version = "0.1.43" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" 281 | dependencies = [ 282 | "autocfg", 283 | "num-traits", 284 | ] 285 | 286 | [[package]] 287 | name = "num-traits" 288 | version = "0.2.12" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" 291 | dependencies = [ 292 | "autocfg", 293 | ] 294 | 295 | [[package]] 296 | name = "once_cell" 297 | version = "1.12.0" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" 300 | 301 | [[package]] 302 | name = "pkg-config" 303 | version = "0.3.18" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33" 306 | 307 | [[package]] 308 | name = "quick-error" 309 | version = "1.2.3" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 312 | 313 | [[package]] 314 | name = "redox_syscall" 315 | version = "0.1.57" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" 318 | 319 | [[package]] 320 | name = "redox_users" 321 | version = "0.3.5" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" 324 | dependencies = [ 325 | "getrandom", 326 | "redox_syscall", 327 | "rust-argon2", 328 | ] 329 | 330 | [[package]] 331 | name = "regex" 332 | version = "1.3.9" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" 335 | dependencies = [ 336 | "aho-corasick", 337 | "memchr", 338 | "regex-syntax", 339 | "thread_local", 340 | ] 341 | 342 | [[package]] 343 | name = "regex-syntax" 344 | version = "0.6.18" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" 347 | 348 | [[package]] 349 | name = "rusqlite" 350 | version = "0.24.0" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "4c78c3275d9d6eb684d2db4b2388546b32fdae0586c20a82f3905d21ea78b9ef" 353 | dependencies = [ 354 | "bitflags", 355 | "fallible-iterator", 356 | "fallible-streaming-iterator", 357 | "libsqlite3-sys", 358 | "lru-cache", 359 | "memchr", 360 | "smallvec", 361 | ] 362 | 363 | [[package]] 364 | name = "rust-argon2" 365 | version = "0.8.2" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "9dab61250775933275e84053ac235621dfb739556d5c54a2f2e9313b7cf43a19" 368 | dependencies = [ 369 | "base64", 370 | "blake2b_simd", 371 | "constant_time_eq", 372 | "crossbeam-utils", 373 | ] 374 | 375 | [[package]] 376 | name = "smallvec" 377 | version = "1.8.0" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" 380 | 381 | [[package]] 382 | name = "strsim" 383 | version = "0.8.0" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 386 | 387 | [[package]] 388 | name = "term" 389 | version = "0.6.1" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "c0863a3345e70f61d613eab32ee046ccd1bcc5f9105fe402c61fcd0c13eeb8b5" 392 | dependencies = [ 393 | "dirs 2.0.2", 394 | "winapi", 395 | ] 396 | 397 | [[package]] 398 | name = "termcolor" 399 | version = "1.1.0" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" 402 | dependencies = [ 403 | "winapi-util", 404 | ] 405 | 406 | [[package]] 407 | name = "textwrap" 408 | version = "0.11.0" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 411 | dependencies = [ 412 | "unicode-width", 413 | ] 414 | 415 | [[package]] 416 | name = "thread_local" 417 | version = "1.1.4" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" 420 | dependencies = [ 421 | "once_cell", 422 | ] 423 | 424 | [[package]] 425 | name = "time" 426 | version = "0.1.44" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" 429 | dependencies = [ 430 | "libc", 431 | "wasi 0.10.0+wasi-snapshot-preview1", 432 | "winapi", 433 | ] 434 | 435 | [[package]] 436 | name = "unicode-width" 437 | version = "0.1.8" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 440 | 441 | [[package]] 442 | name = "vcpkg" 443 | version = "0.2.10" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" 446 | 447 | [[package]] 448 | name = "vec_map" 449 | version = "0.8.2" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 452 | 453 | [[package]] 454 | name = "wasi" 455 | version = "0.9.0+wasi-snapshot-preview1" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 458 | 459 | [[package]] 460 | name = "wasi" 461 | version = "0.10.0+wasi-snapshot-preview1" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 464 | 465 | [[package]] 466 | name = "winapi" 467 | version = "0.3.9" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 470 | dependencies = [ 471 | "winapi-i686-pc-windows-gnu", 472 | "winapi-x86_64-pc-windows-gnu", 473 | ] 474 | 475 | [[package]] 476 | name = "winapi-i686-pc-windows-gnu" 477 | version = "0.4.0" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 480 | 481 | [[package]] 482 | name = "winapi-util" 483 | version = "0.1.5" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 486 | dependencies = [ 487 | "winapi", 488 | ] 489 | 490 | [[package]] 491 | name = "winapi-x86_64-pc-windows-gnu" 492 | version = "0.4.0" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 495 | 496 | [[package]] 497 | name = "yaml-rust" 498 | version = "0.3.5" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "e66366e18dc58b46801afbf2ca7661a9f59cc8c5962c29892b6039b4f86fa992" 501 | --------------------------------------------------------------------------------