taskchampion-rb 0.3.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/CLAUDE.md +67 -0
- data/examples/basic_usage.rb +23 -13
- data/examples/pending_tasks.rb +56 -0
- data/ext/taskchampion/src/replica.rs +15 -0
- data/ext/taskchampion/src/task.rs +10 -1
- data/ext/taskchampion/src/task_data.rs +8 -10
- data/ext/taskchampion/src/util.rs +1 -1
- data/lib/taskchampion/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 494521791e308dacc686f45ef7ff87937a0038e46729092a4e145952971a5f56
|
4
|
+
data.tar.gz: ac3a7b7367b2d8565ac902981fb4c7a9ee45b7d0c825868b15591211debc733b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3d5c1e59e10ac46d98a8fc30d666dac904fcf2d14742c12713a16050a7a67d87c21e1a42ebaf7d25762541d0335c3c36ab2cf6a93bd3ed778046cd1a739840f4
|
7
|
+
data.tar.gz: 8f69921b628b5b1803a940584b05dd46abc854e50d9a3555646a141298a6c2a55008b2c8ca60b508fd53566bc6d0bd28a69f8caf76021e37a56e9fbf69bd362c
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
3.2.0
|
1
|
+
3.2.0
|
data/CLAUDE.md
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# CLAUDE.md
|
2
|
+
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
4
|
+
|
5
|
+
## Build and Development Commands
|
6
|
+
|
7
|
+
### Building the gem
|
8
|
+
```bash
|
9
|
+
bundle install
|
10
|
+
bundle exec rake compile # Compiles the Rust extension
|
11
|
+
```
|
12
|
+
|
13
|
+
### Running tests
|
14
|
+
```bash
|
15
|
+
bundle exec rake test # Run all tests
|
16
|
+
bundle exec rake test TEST=test/test_replica.rb # Run specific test file
|
17
|
+
```
|
18
|
+
|
19
|
+
### Linting
|
20
|
+
```bash
|
21
|
+
bundle exec rake rubocop # Run RuboCop linter
|
22
|
+
```
|
23
|
+
|
24
|
+
### Publishing new version
|
25
|
+
```bash
|
26
|
+
rake publish[patch] # Bump patch version and release
|
27
|
+
rake publish[minor] # Bump minor version and release
|
28
|
+
rake publish[major] # Bump major version and release
|
29
|
+
```
|
30
|
+
|
31
|
+
## Architecture Overview
|
32
|
+
|
33
|
+
This is a Ruby gem that provides bindings to TaskChampion (Rust library) for task management. The architecture consists of:
|
34
|
+
|
35
|
+
### Key Components
|
36
|
+
|
37
|
+
1. **Rust Extension** (`ext/taskchampion/`)
|
38
|
+
- Written in Rust using Magnus for Ruby-Rust interop
|
39
|
+
- Entry point: `ext/taskchampion/src/lib.rs`
|
40
|
+
- Implements core classes: Replica, Task, WorkingSet, Operations, etc.
|
41
|
+
- Thread safety enforced via `thread_check.rs` - objects can only be accessed from their creation thread
|
42
|
+
|
43
|
+
2. **Ruby Layer** (`lib/taskchampion.rb`)
|
44
|
+
- Thin wrapper that loads the Rust extension
|
45
|
+
- Extends WorkingSet and Replica classes with Ruby-specific convenience methods
|
46
|
+
- Maintains Ruby idioms (snake_case methods, `?` suffix for booleans)
|
47
|
+
|
48
|
+
3. **Core Classes**
|
49
|
+
- `Replica`: Main database interface for task storage (in-memory or on-disk)
|
50
|
+
- `Task`: Individual task with properties and methods
|
51
|
+
- `Operations`: Collection of operations for batch changes
|
52
|
+
- `WorkingSet`: Active subset of tasks
|
53
|
+
- `DependencyMap`: Task dependency management
|
54
|
+
- Error hierarchy: Error → ThreadError, StorageError, ValidationError, ConfigError
|
55
|
+
|
56
|
+
### Testing Structure
|
57
|
+
- Uses Minitest framework
|
58
|
+
- Tests organized in `test/unit/`, `test/integration/`, and `test/performance/`
|
59
|
+
- Base test class: `TaskchampionTest` in `test/test_helper.rb`
|
60
|
+
- Tests use temporary directories for file-based replicas
|
61
|
+
|
62
|
+
### Ruby API Design Principles
|
63
|
+
- Methods use snake_case (not get_/set_ prefixes)
|
64
|
+
- Boolean methods end with `?`
|
65
|
+
- Symbols for enums (`:pending`, `:completed`, `:read_only`)
|
66
|
+
- `nil` instead of None
|
67
|
+
- Keyword arguments for optional parameters
|
data/examples/basic_usage.rb
CHANGED
@@ -18,7 +18,7 @@ db_path = File.join(temp_dir, "tasks")
|
|
18
18
|
begin
|
19
19
|
# 1. CREATE TASK DATABASE
|
20
20
|
puts "\n1. Creating task database at #{db_path}"
|
21
|
-
replica = Taskchampion::Replica.new_on_disk(db_path,
|
21
|
+
replica = Taskchampion::Replica.new_on_disk(db_path, true, :read_write)
|
22
22
|
|
23
23
|
# 2. CREATE AND MODIFY TASKS
|
24
24
|
puts "\n2. Creating and modifying tasks"
|
@@ -39,8 +39,7 @@ begin
|
|
39
39
|
task1.add_tag(Taskchampion::Tag.new("ruby"), operations)
|
40
40
|
|
41
41
|
# Add annotation
|
42
|
-
|
43
|
-
task1.add_annotation(annotation, operations)
|
42
|
+
task1.add_annotation("Started learning TaskChampion", operations)
|
44
43
|
|
45
44
|
# Create second task
|
46
45
|
uuid2 = SecureRandom.uuid
|
@@ -61,7 +60,6 @@ begin
|
|
61
60
|
task3.set_description("Read TaskChampion documentation", operations)
|
62
61
|
task3.set_status(Taskchampion::Status.completed, operations)
|
63
62
|
task3.add_tag(Taskchampion::Tag.new("learning"), operations)
|
64
|
-
task3.set_end(Time.now, operations)
|
65
63
|
|
66
64
|
# 3. COMMIT CHANGES TO STORAGE
|
67
65
|
puts "\n3. Committing changes to storage"
|
@@ -88,14 +86,13 @@ begin
|
|
88
86
|
|
89
87
|
# Display tags
|
90
88
|
if !task.tags.empty?
|
91
|
-
tag_names = task.tags.map(&:
|
89
|
+
tag_names = task.tags.map(&:to_s)
|
92
90
|
puts " Tags: #{tag_names.join(', ')}"
|
93
91
|
end
|
94
92
|
|
95
93
|
# Display dates
|
96
94
|
puts " Created: #{task.entry.strftime('%Y-%m-%d %H:%M:%S')}" if task.entry
|
97
95
|
puts " Due: #{task.due.strftime('%Y-%m-%d %H:%M:%S')}" if task.due
|
98
|
-
puts " Completed: #{task.end.strftime('%Y-%m-%d %H:%M:%S')}" if task.end
|
99
96
|
|
100
97
|
# Display annotations
|
101
98
|
if !task.annotations.empty?
|
@@ -136,29 +133,42 @@ begin
|
|
136
133
|
|
137
134
|
# Find tasks due today or tomorrow
|
138
135
|
tomorrow = Time.now + 86400
|
139
|
-
due_soon = all_tasks.select { |t| t.due && t.due <= tomorrow }
|
136
|
+
due_soon = all_tasks.select { |t| t.due && t.due.to_time <= tomorrow }
|
140
137
|
puts "Tasks due soon: #{due_soon.length}"
|
141
138
|
|
142
139
|
# 6. MODIFY EXISTING TASKS
|
143
140
|
puts "\n6. Modifying existing tasks"
|
144
141
|
|
145
|
-
# Complete the first task
|
142
|
+
# Complete the first task using the done method
|
146
143
|
operations2 = Taskchampion::Operations.new
|
147
144
|
|
148
145
|
# Retrieve task fresh from storage
|
149
146
|
task_to_complete = replica.task(uuid1)
|
150
147
|
if task_to_complete && task_to_complete.pending?
|
151
148
|
puts "Completing task: #{task_to_complete.description}"
|
152
|
-
|
153
|
-
|
149
|
+
|
150
|
+
# Use the done() method - a convenience method for marking tasks as completed
|
151
|
+
task_to_complete.done(operations2)
|
154
152
|
|
155
153
|
# Add completion annotation
|
156
|
-
|
157
|
-
task_to_complete.add_annotation(completion_note, operations2)
|
154
|
+
task_to_complete.add_annotation("Completed successfully!", operations2)
|
158
155
|
|
159
156
|
# Commit the changes
|
160
157
|
replica.commit_operations(operations2)
|
161
|
-
puts "Task completed and committed"
|
158
|
+
puts "Task completed using done() method and committed"
|
159
|
+
end
|
160
|
+
|
161
|
+
# Complete the second task using traditional set_status for comparison
|
162
|
+
operations2b = Taskchampion::Operations.new
|
163
|
+
task_to_complete2 = replica.task(uuid2)
|
164
|
+
if task_to_complete2 && task_to_complete2.pending?
|
165
|
+
puts "Completing second task: #{task_to_complete2.description}"
|
166
|
+
|
167
|
+
# Alternative way: using set_status directly
|
168
|
+
task_to_complete2.set_status(Taskchampion::Status.completed, operations2b)
|
169
|
+
|
170
|
+
replica.commit_operations(operations2b)
|
171
|
+
puts "Task completed using set_status() method and committed"
|
162
172
|
end
|
163
173
|
|
164
174
|
# 7. WORKING WITH WORKING SET
|
@@ -0,0 +1,56 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'taskchampion'
|
5
|
+
require 'securerandom'
|
6
|
+
|
7
|
+
# Create an in-memory replica
|
8
|
+
replica = Taskchampion::Replica.new_in_memory
|
9
|
+
|
10
|
+
# Create several tasks with different statuses
|
11
|
+
puts "Creating tasks..."
|
12
|
+
|
13
|
+
# Create pending tasks
|
14
|
+
3.times do |i|
|
15
|
+
ops = Taskchampion::Operations.new
|
16
|
+
task = replica.create_task(SecureRandom.uuid, ops)
|
17
|
+
task.set_description("Pending task #{i + 1}", ops)
|
18
|
+
task.set_status(Taskchampion::PENDING, ops)
|
19
|
+
replica.commit_operations(ops)
|
20
|
+
puts " Created pending task: #{task.description}"
|
21
|
+
end
|
22
|
+
|
23
|
+
# Create completed tasks
|
24
|
+
2.times do |i|
|
25
|
+
ops = Taskchampion::Operations.new
|
26
|
+
task = replica.create_task(SecureRandom.uuid, ops)
|
27
|
+
task.set_description("Completed task #{i + 1}", ops)
|
28
|
+
task.set_status(Taskchampion::COMPLETED, ops)
|
29
|
+
replica.commit_operations(ops)
|
30
|
+
puts " Created completed task: #{task.description}"
|
31
|
+
end
|
32
|
+
|
33
|
+
# Create a deleted task
|
34
|
+
ops = Taskchampion::Operations.new
|
35
|
+
task = replica.create_task(SecureRandom.uuid, ops)
|
36
|
+
task.set_description("Deleted task", ops)
|
37
|
+
task.set_status(Taskchampion::DELETED, ops)
|
38
|
+
replica.commit_operations(ops)
|
39
|
+
puts " Created deleted task: #{task.description}"
|
40
|
+
|
41
|
+
puts "\n" + "=" * 50
|
42
|
+
puts "Getting pending tasks..."
|
43
|
+
puts "=" * 50
|
44
|
+
|
45
|
+
# Use the pending_tasks method to get only pending tasks
|
46
|
+
pending = replica.pending_tasks
|
47
|
+
|
48
|
+
puts "\nFound #{pending.length} pending tasks:"
|
49
|
+
pending.each_with_index do |task, index|
|
50
|
+
puts " #{index + 1}. [#{task.uuid[0..7]}...] #{task.description}"
|
51
|
+
puts " Status: #{task.status}"
|
52
|
+
puts " Active: #{task.active?}"
|
53
|
+
end
|
54
|
+
|
55
|
+
puts "\n" + "=" * 50
|
56
|
+
puts "For comparison, all_tasks returns #{replica.all_tasks.size} tasks total"
|
@@ -260,6 +260,20 @@ impl Replica {
|
|
260
260
|
|
261
261
|
Ok(tc_replica.num_undo_points().map_err(into_error)?)
|
262
262
|
}
|
263
|
+
|
264
|
+
fn pending_tasks(&self) -> Result<RArray, Error> {
|
265
|
+
let mut tc_replica = self.0.get_mut()?;
|
266
|
+
|
267
|
+
let tc_tasks = tc_replica.pending_tasks().map_err(into_error)?;
|
268
|
+
|
269
|
+
let array = RArray::new();
|
270
|
+
for tc_task in tc_tasks {
|
271
|
+
let ruby_task = crate::task::Task::from_tc_task(tc_task);
|
272
|
+
array.push(ruby_task)?;
|
273
|
+
}
|
274
|
+
|
275
|
+
Ok(array)
|
276
|
+
}
|
263
277
|
}
|
264
278
|
|
265
279
|
pub fn init(module: &RModule) -> Result<(), Error> {
|
@@ -285,6 +299,7 @@ pub fn init(module: &RModule) -> Result<(), Error> {
|
|
285
299
|
class.define_method("expire_tasks", method!(Replica::expire_tasks, 0))?;
|
286
300
|
class.define_method("num_local_operations", method!(Replica::num_local_operations, 0))?;
|
287
301
|
class.define_method("num_undo_points", method!(Replica::num_undo_points, 0))?;
|
302
|
+
class.define_method("pending_tasks", method!(Replica::pending_tasks, 0))?;
|
288
303
|
|
289
304
|
Ok(())
|
290
305
|
}
|
@@ -7,7 +7,7 @@ use crate::annotation::Annotation;
|
|
7
7
|
use crate::status::Status;
|
8
8
|
use crate::tag::Tag;
|
9
9
|
use crate::thread_check::ThreadBound;
|
10
|
-
use crate::util::{datetime_to_ruby,
|
10
|
+
use crate::util::{datetime_to_ruby, option_to_ruby, ruby_to_datetime, ruby_to_option, vec_to_ruby};
|
11
11
|
|
12
12
|
#[magnus::wrap(class = "Taskchampion::Task", free_immediately)]
|
13
13
|
pub struct Task(ThreadBound<TCTask>);
|
@@ -326,6 +326,14 @@ impl Task {
|
|
326
326
|
Ok(())
|
327
327
|
}
|
328
328
|
|
329
|
+
fn done(&self, operations: &crate::operations::Operations) -> Result<(), Error> {
|
330
|
+
let mut task = self.0.get_mut()?;
|
331
|
+
operations.with_inner_mut(|ops| {
|
332
|
+
task.done(ops)
|
333
|
+
})?;
|
334
|
+
Ok(())
|
335
|
+
}
|
336
|
+
|
329
337
|
}
|
330
338
|
|
331
339
|
// Remove AsRef implementation as it doesn't work well with thread bounds
|
@@ -384,5 +392,6 @@ pub fn init(module: &RModule) -> Result<(), Error> {
|
|
384
392
|
class.define_method("set_value", method!(Task::set_value, 3))?;
|
385
393
|
class.define_method("set_uda", method!(Task::set_uda, 4))?;
|
386
394
|
class.define_method("delete_uda", method!(Task::delete_uda, 3))?;
|
395
|
+
class.define_method("done", method!(Task::done, 1))?;
|
387
396
|
Ok(())
|
388
397
|
}
|
@@ -14,8 +14,6 @@ impl TaskData {
|
|
14
14
|
pub fn from_tc_task_data(tc_task_data: TCTaskData) -> Self {
|
15
15
|
TaskData(ThreadBound::new(tc_task_data))
|
16
16
|
}
|
17
|
-
|
18
|
-
|
19
17
|
fn inspect(&self) -> Result<String, Error> {
|
20
18
|
let task_data = self.0.get()?;
|
21
19
|
Ok(format!("#<Taskchampion::TaskData: {}>", task_data.get_uuid()))
|
@@ -45,11 +43,11 @@ impl TaskData {
|
|
45
43
|
fn to_hash(&self) -> Result<RHash, Error> {
|
46
44
|
let task_data = self.0.get()?;
|
47
45
|
let hash = RHash::new();
|
48
|
-
|
46
|
+
|
49
47
|
for (key, value) in task_data.iter() {
|
50
48
|
hash.aset(key.clone(), value.clone())?;
|
51
49
|
}
|
52
|
-
|
50
|
+
|
53
51
|
Ok(hash)
|
54
52
|
}
|
55
53
|
|
@@ -78,7 +76,7 @@ impl TaskData {
|
|
78
76
|
|
79
77
|
fn delete(&self, operations: &Operations) -> Result<(), Error> {
|
80
78
|
let mut task_data = self.0.get_mut()?;
|
81
|
-
|
79
|
+
|
82
80
|
operations.with_inner_mut(|ops| {
|
83
81
|
task_data.delete(ops);
|
84
82
|
Ok(())
|
@@ -90,16 +88,16 @@ impl TaskData {
|
|
90
88
|
|
91
89
|
fn create_task_data(uuid: String, operations: &Operations) -> Result<TaskData, Error> {
|
92
90
|
let tc_uuid = uuid2tc(&uuid)?;
|
93
|
-
|
91
|
+
|
94
92
|
// Create operations for TaskChampion
|
95
93
|
let mut tc_ops = taskchampion::Operations::new();
|
96
|
-
|
94
|
+
|
97
95
|
// Create the TaskData
|
98
96
|
let tc_task_data = TCTaskData::create(tc_uuid, &mut tc_ops);
|
99
|
-
|
97
|
+
|
100
98
|
// Add the resulting operations to the provided Operations object
|
101
99
|
operations.extend_from_tc(tc_ops.into_iter().collect())?;
|
102
|
-
|
100
|
+
|
103
101
|
Ok(TaskData(ThreadBound::new(tc_task_data)))
|
104
102
|
}
|
105
103
|
|
@@ -121,4 +119,4 @@ pub fn init(module: &RModule) -> Result<(), Error> {
|
|
121
119
|
class.define_method("delete", method!(TaskData::delete, 1))?;
|
122
120
|
|
123
121
|
Ok(())
|
124
|
-
}
|
122
|
+
}
|
data/lib/taskchampion/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: taskchampion-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tim Case
|
@@ -36,6 +36,7 @@ files:
|
|
36
36
|
- ".rubocop.yml"
|
37
37
|
- ".ruby-version"
|
38
38
|
- CHANGELOG.md
|
39
|
+
- CLAUDE.md
|
39
40
|
- Cargo.lock
|
40
41
|
- Cargo.toml
|
41
42
|
- README.md
|
@@ -48,6 +49,7 @@ files:
|
|
48
49
|
- docs/plan.md
|
49
50
|
- example.md
|
50
51
|
- examples/basic_usage.rb
|
52
|
+
- examples/pending_tasks.rb
|
51
53
|
- examples/sync_workflow.rb
|
52
54
|
- ext/taskchampion/Cargo.toml
|
53
55
|
- ext/taskchampion/extconf.rb
|