1use regex::Regex;
4use std::io::{self, Write};
5use std::sync::Arc;
6use tracing::{debug, instrument};
7
8use crate::application::services::action_service::ActionService;
9use crate::application::services::bookmark_service::BookmarkService;
10use crate::application::services::interpolation_service::InterpolationService;
11use crate::application::services::template_service::TemplateService;
12use crate::cli::bookmark_commands::format_action_description;
13use crate::cli::display::{show_bookmarks, DisplayBookmark, ALL_FIELDS, DEFAULT_FIELDS};
14use crate::cli::error::{CliError, CliResult};
15use crate::domain::bookmark::Bookmark;
16use crate::domain::services::clipboard::ClipboardService;
17use crate::infrastructure::di::ServiceContainer;
18use crate::util::helper::{confirm, ensure_int_vector};
19
20#[instrument(skip_all, level = "debug")]
22pub fn process(
23 bookmarks: &[Bookmark],
24 services: &ServiceContainer,
25 settings: &crate::config::Settings,
26) -> CliResult<()> {
27 if bookmarks.is_empty() {
28 return Ok(());
29 }
30
31 let help_text = r#"
32 <n1> <n2>: performs default action on selection (open URI, copy snippet, etc.)
33 p <n1> <n2>: print id-list of selection
34 p: print all ids
35 d <n1> <n2>: delete selection
36 e <n1> <n2>: edit selection
37 t <n1> <n2>: touch selection (update timestamp)
38 y <n1> <n2>: yank/copy URL(s) to clipboard
39 q | ENTER: quit
40 h: help
41 "#;
42
43 let regex = Regex::new(r"^\d+").unwrap();
44 loop {
45 eprint!("> ");
46 io::stdout().flush().map_err(CliError::Io)?;
47
48 let mut input = String::new();
49 io::stdin().read_line(&mut input).map_err(CliError::Io)?;
50
51 let tokens = parse_input(&input);
52 if tokens.is_empty() {
53 break;
54 }
55
56 match tokens[0].as_str() {
57 "p" => {
58 if tokens.len() == 1 {
59 print_all_bookmark_ids(bookmarks)?;
61 } else if let Some(indices) = ensure_int_vector(&tokens[1..]) {
62 print_bookmark_ids(indices, bookmarks)?;
64 } else {
65 eprintln!("Invalid input, only numbers allowed");
66 continue;
67 }
68 break;
69 }
70 "d" => {
71 if let Some(indices) = ensure_int_vector(&tokens[1..]) {
72 delete_bookmarks_by_indices(
73 indices,
74 bookmarks,
75 services.bookmark_service.clone(),
76 settings,
77 )?;
78 } else {
79 eprintln!("Invalid input, only numbers allowed");
80 continue;
81 }
82 break;
83 }
84 "e" => {
85 if tokens.len() == 1 {
86 edit_all_bookmarks(
88 bookmarks,
89 services.bookmark_service.clone(),
90 services.template_service.clone(),
91 settings,
92 )?;
93 } else if let Some(indices) = ensure_int_vector(&tokens[1..]) {
94 edit_bookmarks_by_indices(
95 indices,
96 bookmarks,
97 services.bookmark_service.clone(),
98 services.template_service.clone(),
99 settings,
100 )?;
101 } else {
102 eprintln!("Invalid input, only numbers allowed");
103 continue;
104 }
105 break;
106 }
107 "t" => {
108 if let Some(indices) = ensure_int_vector(&tokens[1..]) {
109 touch_bookmarks_by_indices(
110 indices,
111 bookmarks,
112 services.bookmark_service.clone(),
113 settings,
114 )?;
115 } else {
116 eprintln!("Invalid input, only numbers allowed");
117 continue;
118 }
119 break;
120 }
121 "y" => {
122 if let Some(indices) = ensure_int_vector(&tokens[1..]) {
123 yank_bookmark_urls_by_indices(
124 indices,
125 bookmarks,
126 services.interpolation_service.clone(),
127 services.clipboard_service.clone(),
128 )?;
129 } else {
130 eprintln!("Invalid input, only numbers allowed");
131 continue;
132 }
133 break;
134 }
135 "h" => println!("{}", help_text),
136 "q" => break,
137 s if regex.is_match(s) => {
138 if let Some(indices) = ensure_int_vector(&tokens) {
139 execute_default_actions_by_indices(
141 indices,
142 bookmarks,
143 services.action_service.clone(),
144 )?;
145 } else {
146 eprintln!("Invalid input, only numbers allowed");
147 continue;
148 }
149 break;
150 }
151 _ => {
152 println!("Invalid Input");
153 println!("{}", help_text);
154 }
155 }
156 }
157
158 Ok(())
159}
160
161#[instrument(level = "trace")]
163fn parse_input(input: &str) -> Vec<String> {
164 input
165 .trim()
166 .replace(',', " ")
167 .to_lowercase()
168 .split_whitespace()
169 .map(|s| s.to_string())
170 .collect()
171}
172
173#[instrument(skip(bookmarks), level = "trace")]
175fn get_bookmark_by_index(index: i32, bookmarks: &[Bookmark]) -> Option<&Bookmark> {
176 if index < 1 || index as usize > bookmarks.len() {
177 return None;
178 }
179 Some(&bookmarks[index as usize - 1])
180}
181
182#[instrument(
183 skip(bookmarks, interpolation_service, clipboard_service),
184 level = "debug"
185)]
186fn yank_bookmark_urls_by_indices(
187 indices: Vec<i32>,
188 bookmarks: &[Bookmark],
189 interpolation_service: Arc<dyn InterpolationService>,
190 clipboard_service: Arc<dyn ClipboardService>,
191) -> CliResult<()> {
192 debug!(
193 "Yanking (copying) URLs for bookmarks at indices: {:?}",
194 indices
195 );
196
197 for index in indices {
198 match get_bookmark_by_index(index, bookmarks) {
199 Some(bookmark) => {
200 let rendered_url = match interpolation_service.render_bookmark_url(bookmark) {
202 Ok(url) => url,
203 Err(e) => {
204 eprintln!("Error rendering URL for bookmark {}: {}", index, e);
205 continue;
206 }
207 };
208
209 match clipboard_service.copy_to_clipboard(&rendered_url) {
211 Ok(_) => eprintln!("Copied to clipboard: {}", rendered_url),
212 Err(e) => eprintln!("Error copying to clipboard: {}", e),
213 }
214 }
215 None => eprintln!("Index {} out of range", index),
216 }
217 }
218
219 Ok(())
220}
221
222#[instrument(skip(action_service), level = "debug")]
224pub fn execute_bookmark_default_action(
225 bookmark: &Bookmark,
226 action_service: Arc<dyn ActionService>,
227) -> CliResult<()> {
228 let base_description = action_service.get_default_action_description(bookmark);
230 let action_description = format_action_description(base_description, bookmark.opener.as_ref());
231 debug!(
232 "Executing default action: {} for bookmark: {}",
233 action_description, bookmark.title
234 );
235
236 action_service.execute_default_action(bookmark)?;
239
240 Ok(())
241}
242
243#[instrument(skip(bookmarks, action_service), level = "debug")]
245fn execute_default_actions_by_indices(
246 indices: Vec<i32>,
247 bookmarks: &[Bookmark],
248 action_service: Arc<dyn ActionService>,
249) -> CliResult<()> {
250 debug!(
251 "Executing default actions for bookmarks at indices: {:?}",
252 indices
253 );
254
255 for index in indices {
256 match get_bookmark_by_index(index, bookmarks) {
257 Some(bookmark) => {
258 let base_description = action_service.get_default_action_description(bookmark);
260 let action_type =
261 format_action_description(base_description, bookmark.opener.as_ref());
262
263 eprintln!(
265 "Executing '{}' for bookmark: {} (ID: {})",
266 action_type,
267 bookmark.title,
268 bookmark.id.unwrap_or(0)
269 );
270
271 execute_bookmark_default_action(bookmark, action_service.clone())?
273 }
274 None => eprintln!("Index {} out of range", index),
275 }
276 }
277
278 Ok(())
279}
280
281#[instrument(skip(action_service), level = "debug")]
283pub fn open_bookmark(bookmark: &Bookmark, action_service: Arc<dyn ActionService>) -> CliResult<()> {
284 execute_bookmark_default_action(bookmark, action_service)
286}
287
288#[instrument(skip(bookmarks, bookmark_service, settings), level = "debug")]
290fn touch_bookmarks_by_indices(
291 indices: Vec<i32>,
292 bookmarks: &[Bookmark],
293 bookmark_service: Arc<dyn BookmarkService>,
294 settings: &crate::config::Settings,
295) -> CliResult<()> {
296 debug!("Touching bookmarks at indices: {:?}", indices);
297
298 for index in indices {
299 match get_bookmark_by_index(index, bookmarks) {
300 Some(bookmark) => {
301 if let Some(id) = bookmark.id {
302 bookmark_service
303 .record_bookmark_access(id)
304 .map_err(CliError::Application)?;
305
306 if let Ok(Some(updated)) = bookmark_service.get_bookmark(id) {
308 show_bookmarks(
309 &[DisplayBookmark::from_domain(&updated)],
310 ALL_FIELDS,
311 settings,
312 );
313 }
314 }
315 }
316 None => eprintln!("Index {} out of range", index),
317 }
318 }
319
320 Ok(())
321}
322
323#[instrument(skip(bookmarks), level = "debug")]
325fn print_bookmark_ids(indices: Vec<i32>, bookmarks: &[Bookmark]) -> CliResult<()> {
326 let mut ids = Vec::new();
327
328 for index in indices {
329 if let Some(bookmark) = get_bookmark_by_index(index, bookmarks) {
330 if let Some(id) = bookmark.id {
331 ids.push(id);
332 }
333 } else {
334 eprintln!("Index {} out of range", index);
335 }
336 }
337
338 if ids.is_empty() {
339 eprintln!("No bookmark IDs found for the specified indices");
340 io::stdout().flush().map_err(CliError::Io)?;
341 return Ok(());
342 }
343
344 ids.sort();
345 println!(
346 "{}",
347 ids.iter()
348 .map(|id| id.to_string())
349 .collect::<Vec<_>>()
350 .join(",")
351 );
352 io::stdout().flush().map_err(CliError::Io)?; Ok(())
355}
356
357#[instrument(skip(bookmarks), level = "debug")]
359fn print_all_bookmark_ids(bookmarks: &[Bookmark]) -> CliResult<()> {
360 let mut ids: Vec<_> = bookmarks.iter().filter_map(|b| b.id).collect();
361
362 if ids.is_empty() {
363 eprintln!("No bookmark IDs found");
364 io::stdout().flush().map_err(CliError::Io)?; return Ok(());
366 }
367
368 eprintln!("Found {} bookmark IDs", ids.len());
370
371 ids.sort();
373 println!(
374 "{}",
375 ids.iter()
376 .map(|id| id.to_string())
377 .collect::<Vec<_>>()
378 .join(",")
379 );
380 io::stdout().flush().map_err(CliError::Io)?; Ok(())
383}
384
385#[instrument(
387 skip(bookmarks, bookmark_service, template_service, settings),
388 level = "debug"
389)]
390fn edit_all_bookmarks(
391 bookmarks: &[Bookmark],
392 bookmark_service: Arc<dyn BookmarkService>,
393 template_service: Arc<dyn TemplateService>,
394 settings: &crate::config::Settings,
395) -> CliResult<()> {
396 let mut bookmark_ids = Vec::new();
398 for bookmark in bookmarks {
399 if let Some(id) = bookmark.id {
400 bookmark_ids.push(id);
401 }
402 }
403
404 if bookmark_ids.is_empty() {
405 eprintln!("No bookmarks to edit");
406 return Ok(());
407 }
408
409 edit_bookmarks(
411 bookmark_ids,
412 false,
413 bookmark_service,
414 template_service,
415 settings,
416 )
417}
418
419#[instrument(
421 skip(bookmarks, bookmark_service, template_service, settings),
422 level = "debug"
423)]
424fn edit_bookmarks_by_indices(
425 indices: Vec<i32>,
426 bookmarks: &[Bookmark],
427 bookmark_service: Arc<dyn BookmarkService>,
428 template_service: Arc<dyn TemplateService>,
429 settings: &crate::config::Settings,
430) -> CliResult<()> {
431 let mut bookmark_ids = Vec::new();
433 for index in indices {
434 if let Some(bookmark) = get_bookmark_by_index(index, bookmarks) {
435 if let Some(id) = bookmark.id {
436 bookmark_ids.push(id);
437 }
438 } else {
439 eprintln!("Index {} out of range", index);
440 }
441 }
442
443 edit_bookmarks(
445 bookmark_ids,
446 false,
447 bookmark_service,
448 template_service,
449 settings,
450 )
451}
452
453#[instrument(skip(bookmark_service, template_service, settings), level = "debug")]
455pub fn edit_bookmarks(
456 ids: Vec<i32>,
457 force_db: bool,
458 bookmark_service: Arc<dyn BookmarkService>,
459 template_service: Arc<dyn TemplateService>,
460 settings: &crate::config::Settings,
461) -> CliResult<()> {
462 let mut bookmarks_to_edit = Vec::new();
463 let mut updated_count = 0;
464
465 for id in &ids {
467 if let Ok(Some(bookmark)) = bookmark_service.get_bookmark(*id) {
468 bookmarks_to_edit.push(bookmark);
469 } else {
470 eprintln!("Bookmark with ID {} not found", id);
471 }
472 }
473
474 if bookmarks_to_edit.is_empty() {
475 eprintln!("No bookmarks found to edit");
476 return Ok(());
477 }
478
479 let display_bookmarks: Vec<_> = bookmarks_to_edit
481 .iter()
482 .map(DisplayBookmark::from_domain)
483 .collect();
484
485 show_bookmarks(&display_bookmarks, DEFAULT_FIELDS, settings);
486
487 for bookmark in &bookmarks_to_edit {
489 eprintln!(
490 "Editing: {} (ID: {})",
491 bookmark.title,
492 bookmark.id.unwrap_or(0)
493 );
494
495 if !force_db && bookmark.file_path.is_some() {
497 if let Err(e) = edit_source_file_and_sync(bookmark, &bookmark_service) {
499 eprintln!(" Failed to edit source file: {}", e);
500 eprintln!(" Falling back to database content editing...");
501 if let Err(e2) =
503 edit_database_content(bookmark, &template_service, &bookmark_service)
504 {
505 eprintln!(" Failed to edit database content: {}", e2);
506 } else {
507 updated_count += 1;
508 }
509 } else {
510 updated_count += 1;
511 }
512 } else {
513 if let Err(e) = edit_database_content(bookmark, &template_service, &bookmark_service) {
515 eprintln!(" Failed to edit bookmark: {}", e);
516 } else {
517 updated_count += 1;
518 }
519 }
520 }
521
522 eprintln!("Updated {} bookmarks", updated_count);
523 Ok(())
524}
525
526#[instrument(skip(bookmarks, bookmark_service, settings), level = "debug")]
528fn delete_bookmarks_by_indices(
529 indices: Vec<i32>,
530 bookmarks: &[Bookmark],
531 bookmark_service: Arc<dyn BookmarkService>,
532 settings: &crate::config::Settings,
533) -> CliResult<()> {
534 let mut bookmark_ids = Vec::new();
536 for index in indices {
537 if let Some(bookmark) = get_bookmark_by_index(index, bookmarks) {
538 if let Some(id) = bookmark.id {
539 bookmark_ids.push(id);
540 }
541 } else {
542 eprintln!("Index {} out of range", index);
543 }
544 }
545
546 delete_bookmarks(bookmark_ids, bookmark_service, settings)
548}
549
550#[instrument(skip(bookmark_service, settings), level = "debug")]
552pub fn delete_bookmarks(
553 ids: Vec<i32>,
554 bookmark_service: Arc<dyn BookmarkService>,
555 settings: &crate::config::Settings,
556) -> CliResult<()> {
557 let mut bookmarks_to_display = Vec::new();
559 for id in &ids {
560 if let Ok(Some(bookmark)) = bookmark_service.get_bookmark(*id) {
561 bookmarks_to_display.push(DisplayBookmark::from_domain(&bookmark));
562 }
563 }
564
565 if bookmarks_to_display.is_empty() {
566 eprintln!("No bookmarks found to delete");
567 return Ok(());
568 }
569
570 show_bookmarks(&bookmarks_to_display, DEFAULT_FIELDS, settings);
571
572 if !confirm("Delete these bookmarks?") {
574 return Err(CliError::OperationAborted);
575 }
576
577 let mut sorted_ids = ids.clone();
579 sorted_ids.sort_by(|a, b| b.cmp(a)); let mut deleted_count = 0;
583 for id in sorted_ids {
584 match bookmark_service.delete_bookmark(id) {
585 Ok(true) => deleted_count += 1,
586 Ok(false) => eprintln!("Bookmark with ID {} not found", id),
587 Err(e) => eprintln!("Error deleting bookmark with ID {}: {}", id, e),
588 }
589 }
590
591 eprintln!("Deleted {} bookmarks", deleted_count);
592 Ok(())
593}
594
595#[instrument(skip(clipboard_service), level = "debug")]
596pub fn copy_url_to_clipboard(
597 url: &str,
598 clipboard_service: Arc<dyn ClipboardService>,
599) -> CliResult<()> {
600 match clipboard_service.copy_to_clipboard(url) {
601 Ok(_) => {
602 eprintln!("Copied to clipboard: {}", url);
603 Ok(())
604 }
605 Err(e) => Err(CliError::CommandFailed(format!(
606 "Failed to copy URL to clipboard: {}",
607 e
608 ))),
609 }
610}
611
612#[instrument(skip(interpolation_service, clipboard_service), level = "debug")]
613pub fn copy_bookmark_url_to_clipboard(
614 bookmark: &Bookmark,
615 interpolation_service: Arc<dyn InterpolationService>,
616 clipboard_service: Arc<dyn ClipboardService>,
617) -> CliResult<()> {
618 let rendered_url = interpolation_service
620 .render_bookmark_url(bookmark)
621 .map_err(|e| CliError::CommandFailed(format!("Failed to render URL: {}", e)))?;
622
623 copy_url_to_clipboard(&rendered_url, clipboard_service)
625}
626
627#[instrument(skip(bookmark_service, template_service), level = "debug")]
629pub fn clone_bookmark(
630 id: i32,
631 bookmark_service: Arc<dyn BookmarkService>,
632 template_service: Arc<dyn TemplateService>,
633) -> CliResult<()> {
634 let bookmark = bookmark_service
636 .get_bookmark(id)?
637 .ok_or_else(|| CliError::InvalidInput(format!("No bookmark found with ID {}", id)))?;
638
639 println!(
640 "Cloning bookmark: {} (ID: {})",
641 bookmark.title,
642 bookmark.id.unwrap_or(0)
643 );
644
645 let mut temp_bookmark = bookmark.clone();
647 temp_bookmark.id = None;
649
650 match template_service.edit_bookmark_with_template(Some(temp_bookmark)) {
652 Ok((edited_bookmark, was_modified)) => {
653 if !was_modified {
654 println!("No changes made in editor. Bookmark not cloned.");
655 return Ok(());
656 }
657
658 match bookmark_service.add_bookmark(
660 &edited_bookmark.url,
661 Some(&edited_bookmark.title),
662 Some(&edited_bookmark.description),
663 Some(&edited_bookmark.tags),
664 false, true, ) {
667 Ok(new_bookmark) => {
668 println!(
669 "Added cloned bookmark: {} (ID: {})",
670 new_bookmark.title,
671 new_bookmark.id.unwrap_or(0)
672 );
673 }
674 Err(e) => {
675 return Err(CliError::CommandFailed(format!(
676 "Failed to add cloned bookmark: {}",
677 e
678 )));
679 }
680 }
681 }
682 Err(e) => {
683 return Err(CliError::CommandFailed(format!(
684 "Failed to edit bookmark: {}",
685 e
686 )));
687 }
688 }
689
690 Ok(())
691}
692
693fn edit_source_file_and_sync(
695 bookmark: &Bookmark,
696 bookmark_service: &Arc<dyn crate::application::services::bookmark_service::BookmarkService>,
697) -> CliResult<()> {
698 use crate::config::{load_settings, resolve_file_path};
699 use std::path::Path;
700 use std::process::Command;
701
702 let file_path_str = bookmark
704 .file_path
705 .as_ref()
706 .ok_or_else(|| CliError::InvalidInput("No file path for this bookmark".to_string()))?;
707
708 let settings = load_settings(None)
710 .map_err(|e| CliError::Other(format!("Failed to load settings: {}", e)))?;
711
712 let resolved_path = resolve_file_path(&settings, file_path_str);
714 let source_file = Path::new(&resolved_path);
715
716 if !source_file.exists() {
718 return Err(CliError::InvalidInput(format!(
719 "Source file does not exist: {}",
720 resolved_path
721 )));
722 }
723
724 eprintln!(" Editing source file: {}", resolved_path);
725
726 let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vim".to_string());
728
729 let status = Command::new(&editor)
731 .arg(&resolved_path)
732 .status()
733 .map_err(|e| {
734 CliError::CommandFailed(format!("Failed to start editor '{}': {}", editor, e))
735 })?;
736
737 if !status.success() {
738 return Err(CliError::CommandFailed(format!(
739 "Editor '{}' exited with non-zero status",
740 editor
741 )));
742 }
743
744 eprintln!(" File edited successfully, syncing changes to database...");
745
746 match sync_file_to_bookmark(bookmark, &resolved_path, bookmark_service) {
748 Ok(()) => {
749 eprintln!(" Successfully synced changes to database");
750 }
751 Err(e) => {
752 return Err(CliError::CommandFailed(format!(
753 "Failed to sync file changes to database: {}",
754 e
755 )));
756 }
757 }
758
759 Ok(())
760}
761
762fn edit_database_content(
764 bookmark: &Bookmark,
765 template_service: &Arc<dyn crate::application::services::template_service::TemplateService>,
766 bookmark_service: &Arc<dyn crate::application::services::bookmark_service::BookmarkService>,
767) -> CliResult<()> {
768 match template_service.edit_bookmark_with_template(Some(bookmark.clone())) {
769 Ok((updated_bookmark, was_modified)) => {
770 if !was_modified {
771 eprintln!(" No changes made, skipping update");
772 return Ok(());
773 }
774
775 if updated_bookmark.id.is_some() {
777 match bookmark_service.update_bookmark(updated_bookmark, false) {
779 Ok(_) => {
780 eprintln!(" Successfully updated bookmark");
781 }
782 Err(e) => return Err(CliError::Application(e)),
783 }
784 } else {
785 let new_bookmark = updated_bookmark;
787 match bookmark_service.add_bookmark(
788 &new_bookmark.url,
789 Some(&new_bookmark.title),
790 Some(&new_bookmark.description),
791 Some(&new_bookmark.tags),
792 false, true, ) {
795 Ok(_) => {
796 eprintln!(" Successfully created new bookmark");
797 }
798 Err(e) => return Err(CliError::Application(e)),
799 }
800 }
801 }
802 Err(e) => return Err(CliError::Application(e)),
803 }
804
805 Ok(())
806}
807
808fn sync_file_to_bookmark(
810 original_bookmark: &Bookmark,
811 file_path: &str,
812 bookmark_service: &Arc<dyn crate::application::services::bookmark_service::BookmarkService>,
813) -> CliResult<()> {
814 use crate::infrastructure::repositories::file_import_repository::FileImportRepository;
815 use std::path::Path;
816
817 let file_repo = FileImportRepository::new();
819 let file_data = file_repo
820 .process_file(Path::new(file_path))
821 .map_err(|e| CliError::Other(format!("Failed to process file: {}", e)))?;
822
823 let _bookmark_id = original_bookmark
825 .id
826 .ok_or_else(|| CliError::InvalidInput("Bookmark has no ID".to_string()))?;
827
828 let mut updated_bookmark = original_bookmark.clone();
830
831 updated_bookmark.title = file_data.name; updated_bookmark.url = file_data.content; updated_bookmark.tags = file_data.tags;
837
838 updated_bookmark.file_path = Some(file_data.file_path.display().to_string());
840 updated_bookmark.file_mtime = Some(file_data.file_mtime as i32);
841 updated_bookmark.file_hash = Some(file_data.file_hash);
842
843 if !file_data.content_type.is_empty() {
845 use crate::domain::system_tag::SystemTag;
846
847 let system_tags_to_remove: Vec<_> = updated_bookmark
849 .tags
850 .iter()
851 .filter(|tag| tag.is_known_system_tag())
852 .cloned()
853 .collect();
854
855 for tag in system_tags_to_remove {
856 updated_bookmark.tags.remove(&tag);
857 }
858
859 let system_tag = match file_data.content_type.as_str() {
861 "_snip_" => Some(SystemTag::Snippet),
862 "_shell_" => Some(SystemTag::Shell),
863 "_md_" => Some(SystemTag::Markdown),
864 "_env_" => Some(SystemTag::Env),
865 "_imported_" => Some(SystemTag::Text),
866 "_mem_" => Some(SystemTag::Memory),
867 _ => None,
868 };
869
870 if let Some(sys_tag) = system_tag {
871 if let Ok(tag) = sys_tag.to_tag() {
872 updated_bookmark.tags.insert(tag);
873 }
874 }
875 }
876
877 bookmark_service
879 .update_bookmark(updated_bookmark, false)
880 .map_err(CliError::Application)?;
881
882 Ok(())
883}
884
885#[cfg(test)]
886mod tests {
887 use super::*;
888 use crate::domain::tag::Tag;
889 use crate::util::testing::init_test_env;
890 use std::collections::HashSet;
891
892 #[test]
893 fn given_input_string_when_parse_input_then_returns_sorted_tokens() {
894 let input = " 1 2, 3 ";
895 let tokens = parse_input(input);
896 assert_eq!(tokens, vec!["1", "2", "3"]);
897
898 let input = "p 1,2,3";
899 let tokens = parse_input(input);
900 assert_eq!(tokens, vec!["p", "1", "2", "3"]);
901 }
902
903 #[test]
904 fn given_bookmarks_and_valid_index_when_get_bookmark_by_index_then_returns_bookmark() {
905 let mut tags = HashSet::new();
907 tags.insert(Tag::new("test").unwrap());
908
909 let bookmark1 = Bookmark {
910 id: Some(10),
911 url: "https://example.com".to_string(),
912 title: "Example".to_string(),
913 description: "An example site".to_string(),
914 tags: tags.clone(),
915 access_count: 0,
916 created_at: Some(chrono::Utc::now()),
917 updated_at: chrono::Utc::now(),
918 embedding: None,
919 content_hash: None,
920 embeddable: false,
921 file_path: None,
922 file_mtime: None,
923 file_hash: None,
924 opener: None,
925 accessed_at: None,
926 };
927
928 let bookmark2 = Bookmark {
929 id: Some(20),
930 url: "https://test.com".to_string(),
931 title: "Test".to_string(),
932 description: "A test site".to_string(),
933 tags,
934 access_count: 0,
935 created_at: Some(chrono::Utc::now()),
936 updated_at: chrono::Utc::now(),
937 embedding: None,
938 content_hash: None,
939 embeddable: false,
940 file_path: None,
941 file_mtime: None,
942 file_hash: None,
943 opener: None,
944 accessed_at: None,
945 };
946
947 let bookmarks = vec![bookmark1, bookmark2];
948
949 let bookmark = get_bookmark_by_index(1, &bookmarks);
951 assert!(bookmark.is_some());
952 assert_eq!(bookmark.unwrap().id, Some(10));
953
954 let bookmark = get_bookmark_by_index(3, &bookmarks);
956 assert!(bookmark.is_none());
957
958 let bookmark = get_bookmark_by_index(-1, &bookmarks);
960 assert!(bookmark.is_none());
961 }
962
963 #[test]
964 fn given_bookmarks_and_indices_when_yank_urls_then_copies_urls_to_clipboard() {
965 let _ = init_test_env();
967
968 let mut tags = HashSet::new();
970 tags.insert(Tag::new("test").unwrap());
971
972 let bookmark1 = Bookmark {
973 id: Some(10),
974 url: "https://example.com".to_string(),
975 title: "Example".to_string(),
976 description: "An example site".to_string(),
977 tags: tags.clone(),
978 access_count: 0,
979 created_at: Some(chrono::Utc::now()),
980 updated_at: chrono::Utc::now(),
981 embedding: None,
982 content_hash: None,
983 embeddable: false,
984 file_path: None,
985 file_mtime: None,
986 file_hash: None,
987 opener: None,
988 accessed_at: None,
989 };
990
991 let bookmark2 = Bookmark {
992 id: Some(20),
993 url: "https://test.com".to_string(),
994 title: "Test".to_string(),
995 description: "A test site".to_string(),
996 tags,
997 access_count: 0,
998 created_at: Some(chrono::Utc::now()),
999 updated_at: chrono::Utc::now(),
1000 embedding: None,
1001 content_hash: None,
1002 embeddable: false,
1003 file_path: None,
1004 file_mtime: None,
1005 file_hash: None,
1006 opener: None,
1007 accessed_at: None,
1008 };
1009
1010 let bookmarks = vec![bookmark1, bookmark2];
1011
1012 use crate::util::test_service_container::TestServiceContainer;
1016 let services = TestServiceContainer::new();
1017
1018 let result = yank_bookmark_urls_by_indices(
1019 vec![1],
1020 &bookmarks,
1021 services.interpolation_service.clone(),
1022 services.clipboard_service.clone(),
1023 );
1024
1025 assert!(result.is_ok(), "Yank operation should succeed");
1027 }
1028}