Commit 6cec87e2e8ab
Changed files (4)
dots
.config
claude
skills
Org
tools
dots/.config/claude/skills/Org/tools/tests/batch-functions-test.el
@@ -322,5 +322,69 @@ Priority mapping: '1'=1, '2'=2, '3'=3, '4'=4, '5'=5."
(should value)
(should (string= value "test-value")))))))
+;;; Tests for Bulk Operations
+
+(ert-deftest test-org-batch-bulk-update-state ()
+ "Test bulk updating state of multiple tasks."
+ (batch-test--with-temp-org-file
+ "* Work\n** TODO Task 1\n** TODO Task 2 :urgent:\n** NEXT Task 3\n"
+ (lambda (temp-file)
+ (let ((count (org-batch-bulk-update-state temp-file "TODO" "DONE")))
+ (should (= count 2))
+ ;; Verify states were updated
+ (let ((todos (org-batch-list-todos temp-file)))
+ (let ((task1 (batch-test--find-item-by-heading todos "Task 1"))
+ (task2 (batch-test--find-item-by-heading todos "Task 2")))
+ (should (string= (alist-get 'todo task1) "DONE"))
+ (should (string= (alist-get 'todo task2) "DONE"))))))))
+
+(ert-deftest test-org-batch-bulk-update-state-with-tag-filter ()
+ "Test bulk updating state with tag filter."
+ (batch-test--with-temp-org-file
+ "* Work\n** TODO Task 1 :urgent:\n** TODO Task 2\n** TODO Task 3 :urgent:\n"
+ (lambda (temp-file)
+ (let ((count (org-batch-bulk-update-state temp-file "TODO" "NEXT" '("urgent"))))
+ (should (= count 2))
+ ;; Verify only urgent tasks were updated
+ (let ((todos (org-batch-list-todos temp-file)))
+ (let ((task1 (batch-test--find-item-by-heading todos "Task 1"))
+ (task2 (batch-test--find-item-by-heading todos "Task 2"))
+ (task3 (batch-test--find-item-by-heading todos "Task 3")))
+ (should (string= (alist-get 'todo task1) "NEXT"))
+ (should (string= (alist-get 'todo task2) "TODO"))
+ (should (string= (alist-get 'todo task3) "NEXT"))))))))
+
+(ert-deftest test-org-batch-bulk-add-tags ()
+ "Test bulk adding tags to multiple tasks."
+ (batch-test--with-temp-org-file
+ "* Work\n** TODO Task 1\n** TODO Task 2\n** NEXT Task 3\n"
+ (lambda (temp-file)
+ (let ((count (org-batch-bulk-add-tags temp-file "TODO" '("review" "urgent"))))
+ (should (= count 2))
+ ;; Verify tags were added
+ (let ((todos (org-batch-list-todos temp-file)))
+ (let ((task1 (batch-test--find-item-by-heading todos "Task 1"))
+ (task2 (batch-test--find-item-by-heading todos "Task 2")))
+ (should (member "review" (alist-get 'tags task1)))
+ (should (member "urgent" (alist-get 'tags task1)))
+ (should (member "review" (alist-get 'tags task2)))
+ (should (member "urgent" (alist-get 'tags task2)))))))))
+
+(ert-deftest test-org-batch-bulk-set-priority ()
+ "Test bulk setting priority for multiple tasks."
+ (batch-test--with-temp-org-file
+ "* Work\n** TODO Task 1\n** TODO Task 2\n** NEXT Task 3\n"
+ (lambda (temp-file)
+ (let ((count (org-batch-bulk-set-priority temp-file "TODO" 1)))
+ (should (= count 2))
+ ;; Verify priorities were set
+ (let ((todos (org-batch-list-todos temp-file)))
+ (let ((task1 (batch-test--find-item-by-heading todos "Task 1"))
+ (task2 (batch-test--find-item-by-heading todos "Task 2"))
+ (task3 (batch-test--find-item-by-heading todos "Task 3")))
+ (should (= (alist-get 'priority task1) 1))
+ (should (= (alist-get 'priority task2) 1))
+ (should-not (alist-get 'priority task3))))))))
+
(provide 'batch-functions-test)
;;; batch-functions-test.el ends here
dots/.config/claude/skills/Org/tools/batch-functions.el
@@ -627,6 +627,73 @@ Returns count of archived items."
(write-region (point-min) (point-max) file))
count))
+;;; Bulk Operations
+
+(defun org-batch-bulk-update-state (file filter-state new-state &optional filter-tags)
+ "Update all tasks matching FILTER-STATE in FILE to NEW-STATE.
+FILTER-TAGS: Optional list of tags to further filter tasks.
+Returns count of updated tasks."
+ (let ((count 0))
+ (with-temp-buffer
+ (insert-file-contents file)
+ (org-mode)
+ (goto-char (point-min))
+ (while (re-search-forward org-heading-regexp nil t)
+ (org-back-to-heading t)
+ (let ((todo (org-get-todo-state))
+ (tags (org-get-tags)))
+ (when (and todo
+ (string= todo filter-state)
+ ;; Tag filter (match any)
+ (or (null filter-tags)
+ (and tags (seq-intersection filter-tags tags))))
+ (let ((org-log-done (if (string= new-state "DONE") 'time nil)))
+ (org-todo new-state))
+ (setq count (1+ count))))
+ (forward-line 1))
+ (write-region (point-min) (point-max) file))
+ count))
+
+(defun org-batch-bulk-add-tags (file filter-state new-tags)
+ "Add NEW-TAGS to all tasks with FILTER-STATE in FILE.
+Returns count of updated tasks."
+ (let ((count 0))
+ (with-temp-buffer
+ (insert-file-contents file)
+ (org-mode)
+ (goto-char (point-min))
+ (while (re-search-forward org-heading-regexp nil t)
+ (org-back-to-heading t)
+ (let ((todo (org-get-todo-state)))
+ (when (and todo (string= todo filter-state))
+ (let* ((current-tags (org-get-tags))
+ (combined-tags (delete-dups (append current-tags new-tags))))
+ (org-set-tags combined-tags))
+ (setq count (1+ count))))
+ (forward-line 1))
+ (write-region (point-min) (point-max) file))
+ count))
+
+(defun org-batch-bulk-set-priority (file filter-state priority)
+ "Set PRIORITY for all tasks with FILTER-STATE in FILE.
+Returns count of updated tasks."
+ (let ((count 0)
+ (priority-cookie (format " [#%d]" priority)))
+ (with-temp-buffer
+ (insert-file-contents file)
+ (org-mode)
+ (goto-char (point-min))
+ (while (re-search-forward (concat "^\\(\\*+ " (regexp-quote filter-state) "\\) \\(?:\\[#[1-5]\\] \\)?") nil t)
+ (goto-char (match-end 1))
+ ;; Remove existing priority if present
+ (when (looking-at " \\[#[1-5]\\]")
+ (delete-region (point) (+ (point) 5)))
+ ;; Insert new priority
+ (insert priority-cookie)
+ (setq count (1+ count)))
+ (write-region (point-min) (point-max) file))
+ count))
+
;;; Output Functions
(defun org-batch-output-json (success data &optional error)
dots/.config/claude/skills/Org/tools/org-manager
@@ -171,6 +171,22 @@ PROPERTY OPERATIONS:
list-properties <file> <heading>
List all properties of a heading
+BULK OPERATIONS:
+ bulk-update-state <file> <filter-state> <new-state> [filter-tags]
+ Update all tasks matching filter-state to new-state
+ Optional: filter by tags (comma-separated)
+ Example: org-manager bulk-update-state todos.org TODO DONE work,urgent
+
+ bulk-add-tags <file> <filter-state> <tags>
+ Add tags to all tasks with filter-state
+ Tags: comma-separated list
+ Example: org-manager bulk-add-tags todos.org NEXT urgent,review
+
+ bulk-set-priority <file> <filter-state> <priority>
+ Set priority for all tasks with filter-state
+ Priority: 1-5 (1=highest, 5=lowest)
+ Example: org-manager bulk-set-priority todos.org TODO 1
+
DENOTE COMMANDS:
denote-create <title> <tags> [--signature=SIG] [--category=CAT] [--directory=DIR] [--content=FILE]
Create a denote-formatted note with proper naming and frontmatter
@@ -731,6 +747,70 @@ cmd_list_properties() {
run_elisp "$elisp"
}
+# Bulk operations
+
+cmd_bulk_update_state() {
+ local file="$1"
+ local filter_state="$2"
+ local new_state="$3"
+ local filter_tags="$4"
+
+ [[ -f "$file" ]] || error "File not found: $file"
+ [[ -n "$filter_state" ]] || error "Filter state required (TODO, NEXT, etc.)"
+ [[ -n "$new_state" ]] || error "New state required (DONE, NEXT, etc.)"
+
+ local tags_list="nil"
+ if [[ -n "$filter_tags" ]]; then
+ tags_list="'($(echo "$filter_tags" | sed 's/,/ /g' | sed 's/\([^ ]*\)/"\1"/g'))"
+ fi
+
+ local elisp="(progn
+ (let ((count (org-batch-bulk-update-state \"$file\" \"$filter_state\" \"$new_state\" $tags_list)))
+ (org-batch-output-json t (list :updated count :filter_state \"$filter_state\" :new_state \"$new_state\")))
+ (kill-emacs 0))"
+
+ run_elisp "$elisp"
+}
+
+cmd_bulk_add_tags() {
+ local file="$1"
+ local filter_state="$2"
+ local tags="$3"
+
+ [[ -f "$file" ]] || error "File not found: $file"
+ [[ -n "$filter_state" ]] || error "Filter state required (TODO, NEXT, etc.)"
+ [[ -n "$tags" ]] || error "Tags required (comma-separated)"
+
+ local tags_list
+ tags_list="'($(echo "$tags" | sed 's/,/ /g' | sed 's/\([^ ]*\)/"\1"/g'))"
+
+ local elisp="(progn
+ (let ((count (org-batch-bulk-add-tags \"$file\" \"$filter_state\" $tags_list)))
+ (org-batch-output-json t (list :updated count :filter_state \"$filter_state\" :tags_added t)))
+ (kill-emacs 0))"
+
+ run_elisp "$elisp"
+}
+
+cmd_bulk_set_priority() {
+ local file="$1"
+ local filter_state="$2"
+ local priority="$3"
+
+ [[ -f "$file" ]] || error "File not found: $file"
+ [[ -n "$filter_state" ]] || error "Filter state required (TODO, NEXT, etc.)"
+ [[ -n "$priority" ]] || error "Priority required (1-5)"
+
+ [[ "$priority" =~ ^[1-5]$ ]] || error "Priority must be 1-5"
+
+ local elisp="(progn
+ (let ((count (org-batch-bulk-set-priority \"$file\" \"$filter_state\" $priority)))
+ (org-batch-output-json t (list :updated count :filter_state \"$filter_state\" :priority $priority)))
+ (kill-emacs 0))"
+
+ run_elisp "$elisp"
+}
+
# Denote commands
cmd_denote_create() {
@@ -956,6 +1036,15 @@ main() {
list-properties)
cmd_list_properties "$@"
;;
+ bulk-update-state)
+ cmd_bulk_update_state "$@"
+ ;;
+ bulk-add-tags)
+ cmd_bulk_add_tags "$@"
+ ;;
+ bulk-set-priority)
+ cmd_bulk_set_priority "$@"
+ ;;
denote-create)
cmd_denote_create "$@"
;;
dots/.config/claude/skills/Org/SKILL.md
@@ -82,6 +82,21 @@ Provide reliable, programmatic access to org-mode files using Emacs batch mode a
./tools/org-manager set-property ~/desktop/org/todos.org "Task name" "STATUS" "In Progress"
```
+#### Bulk Operations
+```bash
+# Update all tasks matching a state to a new state
+./tools/org-manager bulk-update-state ~/desktop/org/todos.org "TODO" "DONE"
+
+# Update with tag filter (only tasks with specific tags)
+./tools/org-manager bulk-update-state ~/desktop/org/todos.org "TODO" "DONE" "work,urgent"
+
+# Add tags to all tasks with a specific state
+./tools/org-manager bulk-add-tags ~/desktop/org/todos.org "NEXT" "urgent,review"
+
+# Set priority for all tasks with a specific state
+./tools/org-manager bulk-set-priority ~/desktop/org/todos.org "TODO" 1
+```
+
#### Denote Operations
```bash
# Create denote-formatted note