11use std:: {
22 env, fs,
33 io:: { self , IsTerminal , Read } ,
4+ path:: PathBuf ,
45 process:: Command ,
56} ;
67
@@ -13,6 +14,8 @@ use fuzzy_matcher::skim::SkimMatcherV2;
1314
1415mod encryption;
1516mod storage;
17+ mod transfer;
18+ mod var;
1619
1720use crate :: storage:: SnippetMeta ;
1821use storage:: Storage ;
@@ -31,54 +34,68 @@ enum Action {
3134 name : String ,
3235 #[ arg( short, long, help = "Copy to clipboard instead of printing" ) ]
3336 copy : bool ,
37+ #[ arg( long, value_parser = parse_key_val, help = "Substitute placeholders (key=value)" ) ]
38+ args : Vec < ( String , String ) > ,
3439 } ,
3540 #[ command( about = "Add a new snippet" , alias = "a" ) ]
3641 Add {
3742 name : String ,
3843 #[ arg( long, short, num_args = 1 , help = "Read content from a file" ) ]
3944 file_name : Option < String > ,
40- #[ arg( short, long, num_args = 1 .., help = "Tags for the snippet" ) ]
45+ #[ arg( short, long, num_args = 1 .., help = "Assign tags to the snippet" ) ]
4146 tags : Option < Vec < String > > ,
4247 #[ arg(
4348 short,
4449 long,
4550 num_args = 1 ,
46- help = "File extension for syntax highlighting in editor "
51+ help = "File extension for editor syntax highlighting"
4752 ) ]
4853 ext : Option < String > ,
49- #[ arg( long, help = "Encrypt the snippet (prompted for password) " ) ]
54+ #[ arg( long, help = "Encrypt the snippet with a password" ) ]
5055 encrypt : bool ,
51- #[ arg( long, short, num_args = 1 , help = "Add a description to metadata " ) ]
56+ #[ arg( long, short, num_args = 1 , help = "Short description for the snippet " ) ]
5257 description : Option < String > ,
5358 } ,
5459 #[ command( about = "List all snippets" , alias = "l" ) ]
5560 List {
56- #[ arg( short, long, num_args = 1 .., help = "Filter by tags " ) ]
61+ #[ arg( short, long, num_args = 1 .., help = "Filter snippets by tag " ) ]
5762 tags : Option < Vec < String > > ,
58- #[ arg( short, long, help = "Show the snippets content" ) ]
63+ #[ arg( short, long, help = "Show snippet content" ) ]
5964 show : bool ,
6065 } ,
6166 #[ command( about = "Remove a snippet" , alias = "r" ) ]
6267 Remove { name : String } ,
6368 #[ command( about = "Edit an existing snippet" , alias = "e" ) ]
6469 Edit {
6570 name : String ,
66- #[ arg( short, long, num_args = 1 .., help = "Update tags" ) ]
71+ #[ arg( short, long, num_args = 1 .., help = "Replace the snippet's tags" ) ]
6772 tags : Option < Vec < String > > ,
68- #[ arg( long, short, num_args = 1 , help = "Add a description to metadata " ) ]
73+ #[ arg( long, short, num_args = 1 , help = "Update the snippet's description " ) ]
6974 description : Option < String > ,
7075 } ,
71- #[ command( about = "Search a query in snippets " , alias = "s" ) ]
76+ #[ command( about = "Search snippets by name or content " , alias = "s" ) ]
7277 Search {
73- #[ arg( short, long, num_args = 1 .., help = "search in tags" ) ]
78+ #[ arg( short, long, num_args = 1 .., help = "Narrow search to specific tags" ) ]
7479 tags : Option < Vec < String > > ,
7580 query : String ,
7681 } ,
7782 #[ command( about = "Encrypt an existing snippet" ) ]
7883 Encrypt { name : String } ,
79-
8084 #[ command( about = "Decrypt a snippet permanently" ) ]
8185 Decrypt { name : String } ,
86+ #[ command( about = "Export a snippet to a .sinbo.json file" ) ]
87+ Export {
88+ name : String ,
89+ #[ arg(
90+ short,
91+ long,
92+ num_args = 1 ,
93+ help = "Directory to export into (default: current dir)"
94+ ) ]
95+ path : Option < PathBuf > ,
96+ } ,
97+ #[ command( about = "Import a snippet from a .sinbo.json file" ) ]
98+ Import { path : std:: path:: PathBuf } ,
8299}
83100
84101fn confirm ( ) {
@@ -144,10 +161,10 @@ fn main() -> Result<()> {
144161 let storage = Storage :: new ( ) ;
145162
146163 match args. action {
147- Action :: Get { name, copy } => {
164+ Action :: Get { name, copy, args } => {
148165 let snippet = storage. get ( & name) ?;
149166
150- let content = if snippet. encrypted {
167+ let mut content = if snippet. encrypted {
151168 let password = encryption:: prompt_password ( "Password: " ) ?;
152169 let enc_path = storage. snippet_path ( & name) . with_extension ( "enc" ) ;
153170 let bytes = encryption:: read_encrypted ( & enc_path, password. as_bytes ( ) )
@@ -157,6 +174,11 @@ fn main() -> Result<()> {
157174 snippet. content
158175 } ;
159176
177+ if !args. is_empty ( ) {
178+ let map: std:: collections:: HashMap < String , String > = args. into_iter ( ) . collect ( ) ;
179+ content = var:: substitute ( & content, & map) ?;
180+ }
181+
160182 if copy {
161183 let mut clipboard = arboard:: Clipboard :: new ( ) ?;
162184 clipboard. set_text ( & content) ?;
@@ -449,6 +471,13 @@ fn main() -> Result<()> {
449471
450472 eprintln ! ( "{} decrypted '{}'" , "sinbo" . cyan( ) . bold( ) , name. yellow( ) ) ;
451473 }
474+ Action :: Export { name, path } => {
475+ let snippet = storage. get ( & name) ?;
476+ transfer:: export ( & snippet, path) ?;
477+ }
478+ Action :: Import { path } => {
479+ transfer:: import ( path, storage) ?;
480+ }
452481 }
453482
454483 Ok ( ( ) )
@@ -460,3 +489,9 @@ fn now_secs() -> u64 {
460489 . unwrap ( )
461490 . as_secs ( )
462491}
492+
493+ fn parse_key_val ( s : & str ) -> Result < ( String , String ) , String > {
494+ s. split_once ( '=' )
495+ . map ( |( k, v) | ( k. to_string ( ) , v. to_string ( ) ) )
496+ . ok_or_else ( || format ! ( "invalid key=value pair: '{}'" , s) )
497+ }
0 commit comments