├── .gitignore ├── assets └── screenshot.png ├── src ├── error.rs └── main.rs ├── Cargo.toml ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | .vscode -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voidstar0/tag/HEAD/assets/screenshot.png -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error)] 4 | pub enum GeneralError { 5 | #[error("IO error {0}")] 6 | IO(#[from] std::io::Error), 7 | #[error("Sql error {0}")] 8 | Sqlite(#[from] rusqlite::Error), 9 | } 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tag" 3 | version = "0.2.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | clap = { version = "3.1.6", features = ["cargo"] } 10 | rusqlite = "0.27.0" 11 | directories = "4.0.1" 12 | thiserror = "1.0.30" 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # __tag, you're it!__ 2 | ### give folders & files keywords for easy lookup 3 | 4 | ![An example of how to use tag](/assets/screenshot.png) 5 | 6 | ## Installation 7 | ```sh 8 | $ brew tap char/tap 9 | $ brew install char/tap/tag 10 | ``` 11 | 12 | ## Usage 13 | 14 | ### Mark a path 15 | ```sh 16 | $ tag mark "tag1,tag2" 17 | ``` 18 | 19 | ### Search a tag 20 | ```sh 21 | $ tag find "tag" 22 | 23 | # in current working directory 24 | $ tag find -c "tag" 25 | ``` 26 | 27 | ### Remove all tags from a path 28 | ```sh 29 | $ tag unmark 30 | ``` 31 | 32 | ### Delete a tag from all paths 33 | ```sh 34 | $ tag deltag 35 | ``` -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | 3 | use std::path::PathBuf; 4 | use std::{fs, path::Path}; 5 | 6 | use clap::Command; 7 | use directories::BaseDirs; 8 | use error::GeneralError; 9 | use rusqlite::Connection; 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct Location { 13 | location: String, 14 | } 15 | 16 | fn unmark_path(mut connection: Connection, path: &str) -> Result<(), GeneralError> { 17 | let dir = PathBuf::from(path.trim()); 18 | if !Path::new(&dir).exists() { 19 | panic!("Path does not exist"); 20 | } 21 | 22 | let absolute_path = fs::canonicalize(&dir)?.to_string_lossy().to_string(); 23 | 24 | // Use a transaction in-case we fail to insert a tag at some point. 25 | let tx = connection.transaction()?; 26 | 27 | tx.execute("DELETE FROM tagged WHERE location = ?1;", &[&absolute_path])?; 28 | 29 | tx.commit()?; 30 | 31 | Ok(()) 32 | } 33 | 34 | fn mark_path(mut connection: Connection, path: &str, tags: &str) -> Result<(), GeneralError> { 35 | let dir = PathBuf::from(path.trim()); 36 | if !Path::new(&dir).exists() { 37 | panic!("Path does not exist"); 38 | } 39 | 40 | let absolute_path = fs::canonicalize(&dir)?.to_string_lossy().to_string(); 41 | 42 | // Use a transaction in-case we fail to insert a tag at some point. 43 | let tx = connection.transaction()?; 44 | 45 | for tag in tags.split(',') { 46 | tx.execute( 47 | "INSERT OR IGNORE INTO tagged (location, tag) VALUES (?1, ?2)", 48 | &[&absolute_path, &tag.trim().into()], 49 | )?; 50 | } 51 | 52 | tx.commit()?; 53 | 54 | Ok(()) 55 | } 56 | 57 | fn find_path(mut connection: Connection, tags: &str, in_cwd: bool) -> Result<(), GeneralError> { 58 | for tag in tags.split(',') { 59 | let mut query = String::from("SELECT location FROM tagged WHERE tag LIKE ?"); 60 | let mut params: Vec = vec![tag.trim().into()]; 61 | 62 | if in_cwd { 63 | let cwd = std::env::current_dir().and_then(fs::canonicalize)?; 64 | let cwd = cwd.to_str().expect("CWD is not a valid utf8 string"); 65 | 66 | query.push_str(" AND location LIKE ?"); 67 | params.push(format!("{cwd}%")); 68 | } 69 | 70 | let tx = connection.transaction()?; 71 | tx.prepare(&query)? 72 | .query_map(rusqlite::params_from_iter(params), |row| { 73 | Ok(Location { 74 | location: row.get(0)?, 75 | }) 76 | })? 77 | .flatten() 78 | .try_for_each(|path| -> Result<(), GeneralError> { 79 | let loc = path.location; 80 | let dir = Path::new(&loc); 81 | 82 | if !dir.exists() { 83 | tx.execute("DELETE FROM tagged WHERE location = ?1;", [&loc])?; 84 | } else { 85 | println!("{}", loc); 86 | } 87 | 88 | Ok(()) 89 | })?; 90 | 91 | tx.commit()?; 92 | } 93 | 94 | Ok(()) 95 | } 96 | 97 | fn main() -> Result<(), GeneralError> { 98 | let mut dir = PathBuf::new(); 99 | if let Some(base_dirs) = BaseDirs::new() { 100 | dir.push(base_dirs.config_dir()); 101 | dir.push("tag"); 102 | dir.set_file_name("tags.db"); 103 | } 104 | 105 | let path = Path::new(&dir); 106 | 107 | if !path.exists() { 108 | if let Some(parent) = dir.parent() { 109 | fs::create_dir_all(parent)?; 110 | } 111 | } 112 | 113 | let mut connection = Connection::open(dir)?; 114 | 115 | connection.execute( 116 | "CREATE TABLE IF NOT EXISTS tagged ( 117 | id integer primary key, 118 | location text not null, 119 | tag text not null, 120 | UNIQUE(location, tag) 121 | );", 122 | [], 123 | )?; 124 | 125 | let matches = clap::command!() 126 | .propagate_version(true) 127 | .subcommand_required(true) 128 | .arg_required_else_help(true) 129 | .subcommand( 130 | Command::new("unmark") 131 | .about("Remove all tags from a specified path") 132 | .arg(clap::arg!([PATH])), 133 | ) 134 | .subcommand( 135 | Command::new("mark") 136 | .about("Give a path specified tags") 137 | .arg(clap::arg!([PATH])) 138 | .arg(clap::arg!([TAGS])), 139 | ) 140 | .subcommand(Command::new("find").about("Finds a path from tags").args(&[ 141 | clap::arg!(-c --"in-cwd" [IN_CWD] "filters by paths in the current working directory"), 142 | clap::arg!([TAGS]), 143 | ])) 144 | .subcommand( 145 | Command::new("deltag") 146 | .about("Remove specified tags from all paths") 147 | .arg(clap::arg!([TAGS])), 148 | ) 149 | .subcommand(Command::new("tags").about("Get a list of all tags")) 150 | .get_matches(); 151 | 152 | match matches.subcommand() { 153 | Some(("mark", sub_matches)) => { 154 | let path: String = sub_matches.value_of_t_or_exit("PATH"); 155 | let tags: String = sub_matches.value_of_t_or_exit("TAGS"); 156 | mark_path(connection, &path, &tags)?; 157 | } 158 | Some(("unmark", sub_matches)) => { 159 | let path: String = sub_matches.value_of_t_or_exit("PATH"); 160 | unmark_path(connection, &path)?; 161 | } 162 | Some(("find", sub_matches)) => { 163 | let tags: String = sub_matches.value_of_t_or_exit("TAGS"); 164 | let in_cwd: bool = sub_matches 165 | .value_of_t("in-cwd") 166 | .unwrap_or_else(|_| sub_matches.is_present("in-cwd")); 167 | find_path(connection, &tags, in_cwd)?; 168 | } 169 | Some(("tags", _)) => { 170 | let mut statement = connection.prepare("SELECT DISTINCT tag FROM tagged;")?; 171 | let rows = statement.query_map([], |row| row.get::(0))?; 172 | 173 | for row in rows { 174 | println!("{}", row?); 175 | } 176 | } 177 | Some(("deltag", sub_matches)) => { 178 | let tags: String = sub_matches.value_of_t_or_exit("TAGS"); 179 | let tx = connection.transaction()?; 180 | 181 | for tag in tags.split(',') { 182 | tx.execute("DELETE FROM tagged WHERE tag = ?1;", &[&tag])?; 183 | } 184 | 185 | tx.commit()?; 186 | } 187 | _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"), 188 | }; 189 | 190 | Ok(()) 191 | } 192 | -------------------------------------------------------------------------------- /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 = "ahash" 7 | version = "0.7.6" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" 10 | dependencies = [ 11 | "getrandom", 12 | "once_cell", 13 | "version_check", 14 | ] 15 | 16 | [[package]] 17 | name = "atty" 18 | version = "0.2.14" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 21 | dependencies = [ 22 | "hermit-abi", 23 | "libc", 24 | "winapi", 25 | ] 26 | 27 | [[package]] 28 | name = "autocfg" 29 | version = "1.1.0" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 32 | 33 | [[package]] 34 | name = "bitflags" 35 | version = "1.3.2" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 38 | 39 | [[package]] 40 | name = "cfg-if" 41 | version = "1.0.0" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 44 | 45 | [[package]] 46 | name = "clap" 47 | version = "3.1.6" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "d8c93436c21e4698bacadf42917db28b23017027a4deccb35dbe47a7e7840123" 50 | dependencies = [ 51 | "atty", 52 | "bitflags", 53 | "indexmap", 54 | "lazy_static", 55 | "os_str_bytes", 56 | "strsim", 57 | "termcolor", 58 | "textwrap", 59 | ] 60 | 61 | [[package]] 62 | name = "directories" 63 | version = "4.0.1" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210" 66 | dependencies = [ 67 | "dirs-sys", 68 | ] 69 | 70 | [[package]] 71 | name = "dirs-sys" 72 | version = "0.3.7" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" 75 | dependencies = [ 76 | "libc", 77 | "redox_users", 78 | "winapi", 79 | ] 80 | 81 | [[package]] 82 | name = "fallible-iterator" 83 | version = "0.2.0" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" 86 | 87 | [[package]] 88 | name = "fallible-streaming-iterator" 89 | version = "0.1.9" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 92 | 93 | [[package]] 94 | name = "getrandom" 95 | version = "0.2.5" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" 98 | dependencies = [ 99 | "cfg-if", 100 | "libc", 101 | "wasi", 102 | ] 103 | 104 | [[package]] 105 | name = "hashbrown" 106 | version = "0.11.2" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 109 | dependencies = [ 110 | "ahash", 111 | ] 112 | 113 | [[package]] 114 | name = "hashlink" 115 | version = "0.7.0" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" 118 | dependencies = [ 119 | "hashbrown", 120 | ] 121 | 122 | [[package]] 123 | name = "hermit-abi" 124 | version = "0.1.19" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 127 | dependencies = [ 128 | "libc", 129 | ] 130 | 131 | [[package]] 132 | name = "indexmap" 133 | version = "1.8.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" 136 | dependencies = [ 137 | "autocfg", 138 | "hashbrown", 139 | ] 140 | 141 | [[package]] 142 | name = "lazy_static" 143 | version = "1.4.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 146 | 147 | [[package]] 148 | name = "libc" 149 | version = "0.2.105" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013" 152 | 153 | [[package]] 154 | name = "libsqlite3-sys" 155 | version = "0.24.1" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "cb644c388dfaefa18035c12614156d285364769e818893da0dda9030c80ad2ba" 158 | dependencies = [ 159 | "pkg-config", 160 | "vcpkg", 161 | ] 162 | 163 | [[package]] 164 | name = "memchr" 165 | version = "2.4.1" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 168 | 169 | [[package]] 170 | name = "once_cell" 171 | version = "1.10.0" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" 174 | 175 | [[package]] 176 | name = "os_str_bytes" 177 | version = "6.0.0" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" 180 | dependencies = [ 181 | "memchr", 182 | ] 183 | 184 | [[package]] 185 | name = "pkg-config" 186 | version = "0.3.22" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" 189 | 190 | [[package]] 191 | name = "proc-macro2" 192 | version = "1.0.36" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" 195 | dependencies = [ 196 | "unicode-xid", 197 | ] 198 | 199 | [[package]] 200 | name = "quote" 201 | version = "1.0.16" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "b4af2ec4714533fcdf07e886f17025ace8b997b9ce51204ee69b6da831c3da57" 204 | dependencies = [ 205 | "proc-macro2", 206 | ] 207 | 208 | [[package]] 209 | name = "redox_syscall" 210 | version = "0.2.12" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "8ae183fc1b06c149f0c1793e1eb447c8b04bfe46d48e9e48bfb8d2d7ed64ecf0" 213 | dependencies = [ 214 | "bitflags", 215 | ] 216 | 217 | [[package]] 218 | name = "redox_users" 219 | version = "0.4.2" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "7776223e2696f1aa4c6b0170e83212f47296a00424305117d013dfe86fb0fe55" 222 | dependencies = [ 223 | "getrandom", 224 | "redox_syscall", 225 | "thiserror", 226 | ] 227 | 228 | [[package]] 229 | name = "rusqlite" 230 | version = "0.27.0" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a" 233 | dependencies = [ 234 | "bitflags", 235 | "fallible-iterator", 236 | "fallible-streaming-iterator", 237 | "hashlink", 238 | "libsqlite3-sys", 239 | "memchr", 240 | "smallvec", 241 | ] 242 | 243 | [[package]] 244 | name = "smallvec" 245 | version = "1.8.0" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" 248 | 249 | [[package]] 250 | name = "strsim" 251 | version = "0.10.0" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 254 | 255 | [[package]] 256 | name = "syn" 257 | version = "1.0.89" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "ea297be220d52398dcc07ce15a209fce436d361735ac1db700cab3b6cdfb9f54" 260 | dependencies = [ 261 | "proc-macro2", 262 | "quote", 263 | "unicode-xid", 264 | ] 265 | 266 | [[package]] 267 | name = "tag" 268 | version = "0.2.0" 269 | dependencies = [ 270 | "clap", 271 | "directories", 272 | "rusqlite", 273 | "thiserror", 274 | ] 275 | 276 | [[package]] 277 | name = "termcolor" 278 | version = "1.1.3" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 281 | dependencies = [ 282 | "winapi-util", 283 | ] 284 | 285 | [[package]] 286 | name = "textwrap" 287 | version = "0.15.0" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" 290 | 291 | [[package]] 292 | name = "thiserror" 293 | version = "1.0.30" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" 296 | dependencies = [ 297 | "thiserror-impl", 298 | ] 299 | 300 | [[package]] 301 | name = "thiserror-impl" 302 | version = "1.0.30" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" 305 | dependencies = [ 306 | "proc-macro2", 307 | "quote", 308 | "syn", 309 | ] 310 | 311 | [[package]] 312 | name = "unicode-xid" 313 | version = "0.2.2" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 316 | 317 | [[package]] 318 | name = "vcpkg" 319 | version = "0.2.15" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 322 | 323 | [[package]] 324 | name = "version_check" 325 | version = "0.9.4" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 328 | 329 | [[package]] 330 | name = "wasi" 331 | version = "0.10.2+wasi-snapshot-preview1" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 334 | 335 | [[package]] 336 | name = "winapi" 337 | version = "0.3.9" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 340 | dependencies = [ 341 | "winapi-i686-pc-windows-gnu", 342 | "winapi-x86_64-pc-windows-gnu", 343 | ] 344 | 345 | [[package]] 346 | name = "winapi-i686-pc-windows-gnu" 347 | version = "0.4.0" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 350 | 351 | [[package]] 352 | name = "winapi-util" 353 | version = "0.1.5" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 356 | dependencies = [ 357 | "winapi", 358 | ] 359 | 360 | [[package]] 361 | name = "winapi-x86_64-pc-windows-gnu" 362 | version = "0.4.0" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 365 | --------------------------------------------------------------------------------