taskchampion-rb 0.2.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 +7 -0
- data/.claude/settings.local.json +14 -0
- data/.rubocop.yml +21 -0
- data/CHANGELOG.md +15 -0
- data/Cargo.lock +3671 -0
- data/Cargo.toml +7 -0
- data/README.md +112 -0
- data/Rakefile +28 -0
- data/docs/API_REFERENCE.md +419 -0
- data/docs/THREAD_SAFETY.md +370 -0
- data/docs/breakthrough.md +246 -0
- data/docs/description.md +3 -0
- data/docs/phase_3_plan.md +482 -0
- data/docs/plan.md +612 -0
- data/example.md +465 -0
- data/examples/basic_usage.rb +278 -0
- data/examples/sync_workflow.rb +480 -0
- data/ext/taskchampion/Cargo.toml +20 -0
- data/ext/taskchampion/extconf.rb +6 -0
- data/ext/taskchampion/src/access_mode.rs +132 -0
- data/ext/taskchampion/src/annotation.rs +77 -0
- data/ext/taskchampion/src/dependency_map.rs +65 -0
- data/ext/taskchampion/src/error.rs +78 -0
- data/ext/taskchampion/src/lib.rs +41 -0
- data/ext/taskchampion/src/operation.rs +234 -0
- data/ext/taskchampion/src/operations.rs +180 -0
- data/ext/taskchampion/src/replica.rs +289 -0
- data/ext/taskchampion/src/status.rs +186 -0
- data/ext/taskchampion/src/tag.rs +77 -0
- data/ext/taskchampion/src/task.rs +388 -0
- data/ext/taskchampion/src/thread_check.rs +61 -0
- data/ext/taskchampion/src/util.rs +131 -0
- data/ext/taskchampion/src/working_set.rs +72 -0
- data/lib/taskchampion/version.rb +5 -0
- data/lib/taskchampion.rb +41 -0
- data/sig/taskchampion.rbs +4 -0
- data/taskchampion-0.2.0.gem +0 -0
- metadata +96 -0
@@ -0,0 +1,480 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Synchronization workflow examples for TaskChampion Ruby bindings
|
5
|
+
# This file demonstrates different sync patterns and server configurations
|
6
|
+
|
7
|
+
require 'taskchampion'
|
8
|
+
require 'securerandom'
|
9
|
+
require 'tmpdir'
|
10
|
+
require 'fileutils'
|
11
|
+
|
12
|
+
puts "TaskChampion Ruby Synchronization Examples"
|
13
|
+
puts "=" * 45
|
14
|
+
|
15
|
+
# Create temporary directories for this example
|
16
|
+
temp_base = Dir.mktmpdir("taskchampion-sync")
|
17
|
+
client1_dir = File.join(temp_base, "client1")
|
18
|
+
client2_dir = File.join(temp_base, "client2")
|
19
|
+
server_dir = File.join(temp_base, "server")
|
20
|
+
|
21
|
+
begin
|
22
|
+
# Ensure directories exist
|
23
|
+
[client1_dir, client2_dir, server_dir].each { |dir| FileUtils.mkdir_p(dir) }
|
24
|
+
|
25
|
+
puts "\nSetup:"
|
26
|
+
puts "Client 1: #{client1_dir}"
|
27
|
+
puts "Client 2: #{client2_dir}"
|
28
|
+
puts "Server: #{server_dir}"
|
29
|
+
|
30
|
+
# ========================================
|
31
|
+
# 1. LOCAL FILE SYNCHRONIZATION
|
32
|
+
# ========================================
|
33
|
+
|
34
|
+
puts "\n" + "=" * 50
|
35
|
+
puts "1. LOCAL FILE SYNCHRONIZATION"
|
36
|
+
puts "=" * 50
|
37
|
+
|
38
|
+
puts "\nCreating two client replicas..."
|
39
|
+
|
40
|
+
# Create first client replica
|
41
|
+
client1 = Taskchampion::Replica.new_on_disk(client1_dir, create_if_missing: true)
|
42
|
+
|
43
|
+
# Create second client replica
|
44
|
+
client2 = Taskchampion::Replica.new_on_disk(client2_dir, create_if_missing: true)
|
45
|
+
|
46
|
+
# Add tasks to client 1
|
47
|
+
puts "\nAdding tasks to Client 1..."
|
48
|
+
ops1 = Taskchampion::Operations.new
|
49
|
+
|
50
|
+
uuid1 = SecureRandom.uuid
|
51
|
+
task1 = client1.create_task(uuid1, ops1)
|
52
|
+
task1.set_description("Task from Client 1", ops1)
|
53
|
+
task1.set_priority("H", ops1)
|
54
|
+
task1.add_tag(Taskchampion::Tag.new("client1"), ops1)
|
55
|
+
|
56
|
+
uuid2 = SecureRandom.uuid
|
57
|
+
task2 = client1.create_task(uuid2, ops1)
|
58
|
+
task2.set_description("Shared task", ops1)
|
59
|
+
task2.add_tag(Taskchampion::Tag.new("shared"), ops1)
|
60
|
+
|
61
|
+
client1.commit_operations(ops1)
|
62
|
+
puts "Client 1 has #{client1.task_uuids.length} tasks"
|
63
|
+
|
64
|
+
# Add tasks to client 2
|
65
|
+
puts "\nAdding tasks to Client 2..."
|
66
|
+
ops2 = Taskchampion::Operations.new
|
67
|
+
|
68
|
+
uuid3 = SecureRandom.uuid
|
69
|
+
task3 = client2.create_task(uuid3, ops2)
|
70
|
+
task3.set_description("Task from Client 2", ops2)
|
71
|
+
task3.set_priority("M", ops2)
|
72
|
+
task3.add_tag(Taskchampion::Tag.new("client2"), ops2)
|
73
|
+
|
74
|
+
client2.commit_operations(ops2)
|
75
|
+
puts "Client 2 has #{client2.task_uuids.length} tasks"
|
76
|
+
|
77
|
+
# Sync Client 1 to server (upload)
|
78
|
+
puts "\nSyncing Client 1 to server..."
|
79
|
+
begin
|
80
|
+
client1.sync_to_local(server_dir, avoid_snapshots: false)
|
81
|
+
puts "✓ Client 1 synced to server successfully"
|
82
|
+
rescue => e
|
83
|
+
puts "✗ Client 1 sync failed: #{e.message}"
|
84
|
+
end
|
85
|
+
|
86
|
+
# Sync Client 2 to server (upload) and from server (download)
|
87
|
+
puts "\nSyncing Client 2 with server..."
|
88
|
+
begin
|
89
|
+
client2.sync_to_local(server_dir, avoid_snapshots: false)
|
90
|
+
puts "✓ Client 2 synced with server successfully"
|
91
|
+
puts "Client 2 now has #{client2.task_uuids.length} tasks"
|
92
|
+
rescue => e
|
93
|
+
puts "✗ Client 2 sync failed: #{e.message}"
|
94
|
+
end
|
95
|
+
|
96
|
+
# Sync Client 1 from server (download changes from Client 2)
|
97
|
+
puts "\nSyncing Client 1 from server..."
|
98
|
+
begin
|
99
|
+
client1.sync_to_local(server_dir, avoid_snapshots: false)
|
100
|
+
puts "✓ Client 1 synced from server successfully"
|
101
|
+
puts "Client 1 now has #{client1.task_uuids.length} tasks"
|
102
|
+
rescue => e
|
103
|
+
puts "✗ Client 1 sync failed: #{e.message}"
|
104
|
+
end
|
105
|
+
|
106
|
+
# Verify both clients have all tasks
|
107
|
+
puts "\nVerification after sync:"
|
108
|
+
client1_tasks = client1.task_uuids.map { |uuid| client1.task(uuid) }.compact
|
109
|
+
client2_tasks = client2.task_uuids.map { |uuid| client2.task(uuid) }.compact
|
110
|
+
|
111
|
+
puts "Client 1 tasks:"
|
112
|
+
client1_tasks.each do |task|
|
113
|
+
tags = task.tags.map(&:name).join(", ")
|
114
|
+
puts " - #{task.description} [#{tags}]"
|
115
|
+
end
|
116
|
+
|
117
|
+
puts "Client 2 tasks:"
|
118
|
+
client2_tasks.each do |task|
|
119
|
+
tags = task.tags.map(&:name).join(", ")
|
120
|
+
puts " - #{task.description} [#{tags}]"
|
121
|
+
end
|
122
|
+
|
123
|
+
# ========================================
|
124
|
+
# 2. CONFLICT RESOLUTION
|
125
|
+
# ========================================
|
126
|
+
|
127
|
+
puts "\n" + "=" * 50
|
128
|
+
puts "2. CONFLICT RESOLUTION"
|
129
|
+
puts "=" * 50
|
130
|
+
|
131
|
+
# Create conflicting changes
|
132
|
+
puts "\nCreating conflicting changes..."
|
133
|
+
|
134
|
+
# Client 1 modifies the shared task
|
135
|
+
shared_task_c1 = client1.task(uuid2)
|
136
|
+
if shared_task_c1
|
137
|
+
ops1_conflict = Taskchampion::Operations.new
|
138
|
+
shared_task_c1.set_description("Shared task - modified by Client 1", ops1_conflict)
|
139
|
+
shared_task_c1.set_priority("H", ops1_conflict)
|
140
|
+
shared_task_c1.add_tag(Taskchampion::Tag.new("modified-c1"), ops1_conflict)
|
141
|
+
client1.commit_operations(ops1_conflict)
|
142
|
+
puts "✓ Client 1 modified shared task"
|
143
|
+
end
|
144
|
+
|
145
|
+
# Client 2 also modifies the same shared task
|
146
|
+
shared_task_c2 = client2.task(uuid2)
|
147
|
+
if shared_task_c2
|
148
|
+
ops2_conflict = Taskchampion::Operations.new
|
149
|
+
shared_task_c2.set_description("Shared task - modified by Client 2", ops2_conflict)
|
150
|
+
shared_task_c2.set_priority("L", ops2_conflict)
|
151
|
+
shared_task_c2.add_tag(Taskchampion::Tag.new("modified-c2"), ops2_conflict)
|
152
|
+
client2.commit_operations(ops2_conflict)
|
153
|
+
puts "✓ Client 2 modified shared task"
|
154
|
+
end
|
155
|
+
|
156
|
+
# Sync and see how conflicts are resolved
|
157
|
+
puts "\nSyncing conflicting changes..."
|
158
|
+
|
159
|
+
# Client 1 syncs first
|
160
|
+
begin
|
161
|
+
client1.sync_to_local(server_dir, avoid_snapshots: false)
|
162
|
+
puts "✓ Client 1 synced (first to server)"
|
163
|
+
rescue => e
|
164
|
+
puts "✗ Client 1 sync failed: #{e.message}"
|
165
|
+
end
|
166
|
+
|
167
|
+
# Client 2 syncs (will resolve conflicts)
|
168
|
+
begin
|
169
|
+
client2.sync_to_local(server_dir, avoid_snapshots: false)
|
170
|
+
puts "✓ Client 2 synced (conflict resolution)"
|
171
|
+
rescue => e
|
172
|
+
puts "✗ Client 2 sync failed: #{e.message}"
|
173
|
+
end
|
174
|
+
|
175
|
+
# Client 1 syncs again to get resolved state
|
176
|
+
begin
|
177
|
+
client1.sync_to_local(server_dir, avoid_snapshots: false)
|
178
|
+
puts "✓ Client 1 synced (getting resolved state)"
|
179
|
+
rescue => e
|
180
|
+
puts "✗ Client 1 sync failed: #{e.message}"
|
181
|
+
end
|
182
|
+
|
183
|
+
# Show final state after conflict resolution
|
184
|
+
puts "\nFinal state after conflict resolution:"
|
185
|
+
final_task_c1 = client1.task(uuid2)
|
186
|
+
final_task_c2 = client2.task(uuid2)
|
187
|
+
|
188
|
+
if final_task_c1
|
189
|
+
tags = final_task_c1.tags.map(&:name).join(", ")
|
190
|
+
puts "Client 1 sees: '#{final_task_c1.description}' priority=#{final_task_c1.priority} tags=[#{tags}]"
|
191
|
+
end
|
192
|
+
|
193
|
+
if final_task_c2
|
194
|
+
tags = final_task_c2.tags.map(&:name).join(", ")
|
195
|
+
puts "Client 2 sees: '#{final_task_c2.description}' priority=#{final_task_c2.priority} tags=[#{tags}]"
|
196
|
+
end
|
197
|
+
|
198
|
+
# ========================================
|
199
|
+
# 3. SYNC PATTERNS AND BEST PRACTICES
|
200
|
+
# ========================================
|
201
|
+
|
202
|
+
puts "\n" + "=" * 50
|
203
|
+
puts "3. SYNC PATTERNS AND BEST PRACTICES"
|
204
|
+
puts "=" * 50
|
205
|
+
|
206
|
+
# Pattern 1: Sync before and after work session
|
207
|
+
puts "\nPattern 1: Sync before and after work session"
|
208
|
+
|
209
|
+
def work_session(replica, server_dir, client_name)
|
210
|
+
puts "\n#{client_name}: Starting work session..."
|
211
|
+
|
212
|
+
# 1. Sync before work (get latest changes)
|
213
|
+
begin
|
214
|
+
replica.sync_to_local(server_dir, avoid_snapshots: false)
|
215
|
+
puts " ✓ Pre-work sync completed"
|
216
|
+
rescue => e
|
217
|
+
puts " ✗ Pre-work sync failed: #{e.message}"
|
218
|
+
return false
|
219
|
+
end
|
220
|
+
|
221
|
+
# 2. Do work
|
222
|
+
operations = Taskchampion::Operations.new
|
223
|
+
work_uuid = SecureRandom.uuid
|
224
|
+
task = replica.create_task(work_uuid, operations)
|
225
|
+
task.set_description("Work session task from #{client_name}", operations)
|
226
|
+
task.add_tag(Taskchampion::Tag.new("work-session"), operations)
|
227
|
+
replica.commit_operations(operations)
|
228
|
+
puts " ✓ Created work session task"
|
229
|
+
|
230
|
+
# 3. Sync after work (share changes)
|
231
|
+
begin
|
232
|
+
replica.sync_to_local(server_dir, avoid_snapshots: false)
|
233
|
+
puts " ✓ Post-work sync completed"
|
234
|
+
return true
|
235
|
+
rescue => e
|
236
|
+
puts " ✗ Post-work sync failed: #{e.message}"
|
237
|
+
return false
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# Simulate work sessions
|
242
|
+
work_session(client1, server_dir, "Client 1")
|
243
|
+
work_session(client2, server_dir, "Client 2")
|
244
|
+
|
245
|
+
# Pattern 2: Periodic sync with error handling
|
246
|
+
puts "\nPattern 2: Periodic sync with error handling"
|
247
|
+
|
248
|
+
def periodic_sync(replica, server_dir, client_name)
|
249
|
+
max_retries = 3
|
250
|
+
retry_delay = 1 # seconds
|
251
|
+
|
252
|
+
(1..max_retries).each do |attempt|
|
253
|
+
begin
|
254
|
+
puts " #{client_name}: Sync attempt #{attempt}/#{max_retries}"
|
255
|
+
replica.sync_to_local(server_dir, avoid_snapshots: false)
|
256
|
+
puts " ✓ Sync successful"
|
257
|
+
return true
|
258
|
+
rescue => e
|
259
|
+
puts " ✗ Sync attempt #{attempt} failed: #{e.message}"
|
260
|
+
|
261
|
+
if attempt < max_retries
|
262
|
+
puts " Retrying in #{retry_delay} seconds..."
|
263
|
+
sleep(retry_delay)
|
264
|
+
retry_delay *= 2 # Exponential backoff
|
265
|
+
else
|
266
|
+
puts " Max retries reached, giving up"
|
267
|
+
return false
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
periodic_sync(client1, server_dir, "Client 1")
|
274
|
+
|
275
|
+
# Pattern 3: Sync status checking
|
276
|
+
puts "\nPattern 3: Sync status and storage information"
|
277
|
+
|
278
|
+
def sync_status(replica, client_name)
|
279
|
+
puts "\n#{client_name} Status:"
|
280
|
+
puts " Local operations: #{replica.num_local_operations}"
|
281
|
+
puts " Undo points: #{replica.num_undo_points}"
|
282
|
+
puts " Total tasks: #{replica.task_uuids.length}"
|
283
|
+
end
|
284
|
+
|
285
|
+
sync_status(client1, "Client 1")
|
286
|
+
sync_status(client2, "Client 2")
|
287
|
+
|
288
|
+
# ========================================
|
289
|
+
# 4. REMOTE SERVER SYNC (EXAMPLES)
|
290
|
+
# ========================================
|
291
|
+
|
292
|
+
puts "\n" + "=" * 50
|
293
|
+
puts "4. REMOTE SERVER SYNC (EXAMPLES)"
|
294
|
+
puts "=" * 50
|
295
|
+
|
296
|
+
puts "\nNote: These examples show the API but won't actually connect"
|
297
|
+
puts " without a real TaskWarrior server or cloud storage setup.\n"
|
298
|
+
|
299
|
+
# Example: Sync to remote TaskWarrior server
|
300
|
+
puts "Example: Sync to remote TaskWarrior server"
|
301
|
+
puts "```ruby"
|
302
|
+
puts "begin"
|
303
|
+
puts " replica.sync_to_remote("
|
304
|
+
puts " url: 'https://taskserver.example.com:53589',"
|
305
|
+
puts " client_id: 'your-client-id-here',"
|
306
|
+
puts " encryption_secret: 'your-encryption-secret',"
|
307
|
+
puts " avoid_snapshots: false"
|
308
|
+
puts " )"
|
309
|
+
puts " puts '✓ Successfully synced to remote server'"
|
310
|
+
puts "rescue Taskchampion::SyncError => e"
|
311
|
+
puts " puts '✗ Remote sync failed: #{e.message}'"
|
312
|
+
puts "rescue Taskchampion::ConfigError => e"
|
313
|
+
puts " puts '✗ Configuration error: #{e.message}'"
|
314
|
+
puts "end"
|
315
|
+
puts "```\n"
|
316
|
+
|
317
|
+
# Example: Sync to Google Cloud Platform
|
318
|
+
puts "Example: Sync to Google Cloud Storage"
|
319
|
+
puts "```ruby"
|
320
|
+
puts "begin"
|
321
|
+
puts " replica.sync_to_gcp("
|
322
|
+
puts " bucket: 'my-taskwarrior-bucket',"
|
323
|
+
puts " credential_path: '/path/to/service-account.json',"
|
324
|
+
puts " encryption_secret: 'your-encryption-secret',"
|
325
|
+
puts " avoid_snapshots: false"
|
326
|
+
puts " )"
|
327
|
+
puts " puts '✓ Successfully synced to Google Cloud'"
|
328
|
+
puts "rescue Taskchampion::SyncError => e"
|
329
|
+
puts " puts '✗ GCP sync failed: #{e.message}'"
|
330
|
+
puts "rescue Taskchampion::ConfigError => e"
|
331
|
+
puts " puts '✗ GCP configuration error: #{e.message}'"
|
332
|
+
puts "end"
|
333
|
+
puts "```\n"
|
334
|
+
|
335
|
+
# ========================================
|
336
|
+
# 5. SNAPSHOT MANAGEMENT
|
337
|
+
# ========================================
|
338
|
+
|
339
|
+
puts "\n" + "=" * 50
|
340
|
+
puts "5. SNAPSHOT MANAGEMENT"
|
341
|
+
puts "=" * 50
|
342
|
+
|
343
|
+
puts "\nSnapshots help optimize sync performance for large task databases."
|
344
|
+
puts "They can be avoided for smaller datasets or debugging purposes.\n"
|
345
|
+
|
346
|
+
# Example with snapshots (default)
|
347
|
+
puts "Sync with snapshots (default, faster for large datasets):"
|
348
|
+
begin
|
349
|
+
client1.sync_to_local(server_dir, avoid_snapshots: false)
|
350
|
+
puts "✓ Sync with snapshots completed"
|
351
|
+
rescue => e
|
352
|
+
puts "✗ Sync failed: #{e.message}"
|
353
|
+
end
|
354
|
+
|
355
|
+
# Example without snapshots
|
356
|
+
puts "\nSync without snapshots (slower, but more predictable):"
|
357
|
+
begin
|
358
|
+
client2.sync_to_local(server_dir, avoid_snapshots: true)
|
359
|
+
puts "✓ Sync without snapshots completed"
|
360
|
+
rescue => e
|
361
|
+
puts "✗ Sync failed: #{e.message}"
|
362
|
+
end
|
363
|
+
|
364
|
+
# ========================================
|
365
|
+
# 6. MULTI-CLIENT WORKFLOW
|
366
|
+
# ========================================
|
367
|
+
|
368
|
+
puts "\n" + "=" * 50
|
369
|
+
puts "6. MULTI-CLIENT WORKFLOW SIMULATION"
|
370
|
+
puts "=" * 50
|
371
|
+
|
372
|
+
puts "\nSimulating a realistic multi-client workflow..."
|
373
|
+
|
374
|
+
# Simulate desktop client
|
375
|
+
puts "\n[Desktop Client] Creating project tasks..."
|
376
|
+
desktop_ops = Taskchampion::Operations.new
|
377
|
+
|
378
|
+
project_tasks = [
|
379
|
+
"Design user interface",
|
380
|
+
"Implement authentication",
|
381
|
+
"Write unit tests",
|
382
|
+
"Deploy to staging"
|
383
|
+
]
|
384
|
+
|
385
|
+
project_uuids = []
|
386
|
+
project_tasks.each_with_index do |desc, index|
|
387
|
+
uuid = SecureRandom.uuid
|
388
|
+
project_uuids << uuid
|
389
|
+
task = client1.create_task(uuid, desktop_ops)
|
390
|
+
task.set_description(desc, desktop_ops)
|
391
|
+
task.set_priority(["H", "H", "M", "L"][index], desktop_ops)
|
392
|
+
task.add_tag(Taskchampion::Tag.new("project"), desktop_ops)
|
393
|
+
task.add_tag(Taskchampion::Tag.new("web-app"), desktop_ops)
|
394
|
+
end
|
395
|
+
|
396
|
+
client1.commit_operations(desktop_ops)
|
397
|
+
client1.sync_to_local(server_dir, avoid_snapshots: false)
|
398
|
+
puts " ✓ Desktop client created and synced #{project_tasks.length} project tasks"
|
399
|
+
|
400
|
+
# Simulate mobile client
|
401
|
+
puts "\n[Mobile Client] Adding personal tasks and checking project status..."
|
402
|
+
client2.sync_to_local(server_dir, avoid_snapshots: false) # Get project tasks
|
403
|
+
|
404
|
+
mobile_ops = Taskchampion::Operations.new
|
405
|
+
|
406
|
+
personal_tasks = [
|
407
|
+
"Buy groceries",
|
408
|
+
"Call dentist",
|
409
|
+
"Review project status"
|
410
|
+
]
|
411
|
+
|
412
|
+
personal_tasks.each do |desc|
|
413
|
+
uuid = SecureRandom.uuid
|
414
|
+
task = client2.create_task(uuid, mobile_ops)
|
415
|
+
task.set_description(desc, mobile_ops)
|
416
|
+
task.add_tag(Taskchampion::Tag.new("personal"), mobile_ops)
|
417
|
+
end
|
418
|
+
|
419
|
+
# Complete first project task from mobile
|
420
|
+
project_task = client2.task(project_uuids.first)
|
421
|
+
if project_task
|
422
|
+
project_task.set_status(Taskchampion::Status.completed, mobile_ops)
|
423
|
+
project_task.set_end(Time.now, mobile_ops)
|
424
|
+
puts " ✓ Completed '#{project_task.description}' from mobile"
|
425
|
+
end
|
426
|
+
|
427
|
+
client2.commit_operations(mobile_ops)
|
428
|
+
client2.sync_to_local(server_dir, avoid_snapshots: false)
|
429
|
+
puts " ✓ Mobile client added personal tasks and updated project"
|
430
|
+
|
431
|
+
# Desktop client gets updates
|
432
|
+
puts "\n[Desktop Client] Syncing to see mobile updates..."
|
433
|
+
client1.sync_to_local(server_dir, avoid_snapshots: false)
|
434
|
+
|
435
|
+
# Show final state
|
436
|
+
puts "\nFinal synchronized state:"
|
437
|
+
all_tasks_c1 = client1.task_uuids.map { |uuid| client1.task(uuid) }.compact
|
438
|
+
|
439
|
+
project_tasks_final = all_tasks_c1.select { |t| t.has_tag?(Taskchampion::Tag.new("project")) }
|
440
|
+
personal_tasks_final = all_tasks_c1.select { |t| t.has_tag?(Taskchampion::Tag.new("personal")) }
|
441
|
+
|
442
|
+
puts "\nProject tasks:"
|
443
|
+
project_tasks_final.each do |task|
|
444
|
+
status_icon = task.completed? ? "✓" : "○"
|
445
|
+
puts " #{status_icon} #{task.description} [#{task.priority}]"
|
446
|
+
end
|
447
|
+
|
448
|
+
puts "\nPersonal tasks:"
|
449
|
+
personal_tasks_final.each do |task|
|
450
|
+
status_icon = task.completed? ? "✓" : "○"
|
451
|
+
puts " #{status_icon} #{task.description}"
|
452
|
+
end
|
453
|
+
|
454
|
+
puts "\n" + "=" * 50
|
455
|
+
puts "SYNCHRONIZATION EXAMPLES COMPLETED"
|
456
|
+
puts "=" * 50
|
457
|
+
|
458
|
+
puts "\nKey takeaways:"
|
459
|
+
puts "✓ Local file sync enables multi-client workflows"
|
460
|
+
puts "✓ TaskChampion handles conflict resolution automatically"
|
461
|
+
puts "✓ Always sync before and after work sessions"
|
462
|
+
puts "✓ Use proper error handling and retries"
|
463
|
+
puts "✓ Monitor sync status with storage information"
|
464
|
+
puts "✓ Consider snapshot settings for performance"
|
465
|
+
|
466
|
+
rescue => e
|
467
|
+
puts "\nError during sync examples: #{e.class} - #{e.message}"
|
468
|
+
puts e.backtrace.first(5).join("\n") if e.backtrace
|
469
|
+
ensure
|
470
|
+
# Clean up temporary directories
|
471
|
+
if temp_base && File.exist?(temp_base)
|
472
|
+
FileUtils.remove_entry(temp_base)
|
473
|
+
puts "\nCleaned up temporary directories: #{temp_base}"
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
puts "\nFor more information, see:"
|
478
|
+
puts "- examples/basic_usage.rb - Basic TaskChampion usage"
|
479
|
+
puts "- docs/API_REFERENCE.md - Complete API documentation"
|
480
|
+
puts "- docs/THREAD_SAFETY.md - Thread safety guidelines"
|
@@ -0,0 +1,20 @@
|
|
1
|
+
[package]
|
2
|
+
name = "taskchampion"
|
3
|
+
version = "0.1.0"
|
4
|
+
edition = "2021"
|
5
|
+
authors = ["Tim Case <tim@2drops.net>"]
|
6
|
+
publish = false
|
7
|
+
|
8
|
+
[lib]
|
9
|
+
name = "taskchampion"
|
10
|
+
crate-type = ["cdylib"]
|
11
|
+
|
12
|
+
[dependencies]
|
13
|
+
magnus = { version = "0.7", features = ["rb-sys"] }
|
14
|
+
rb-sys = "0.9"
|
15
|
+
taskchampion = "2.0"
|
16
|
+
chrono = "0.4"
|
17
|
+
uuid = "1.0"
|
18
|
+
|
19
|
+
[features]
|
20
|
+
default = ["rb-sys/stable-api-compiled-fallback"]
|
@@ -0,0 +1,132 @@
|
|
1
|
+
use magnus::{class, function, method, prelude::*, Error, RModule, Symbol, TryConvert};
|
2
|
+
pub use taskchampion::storage::AccessMode as TCAccessMode;
|
3
|
+
|
4
|
+
#[magnus::wrap(class = "Taskchampion::AccessMode", free_immediately)]
|
5
|
+
#[derive(Clone, Copy, PartialEq)]
|
6
|
+
pub struct AccessMode(AccessModeKind);
|
7
|
+
|
8
|
+
#[derive(Clone, Copy, PartialEq, Hash)]
|
9
|
+
enum AccessModeKind {
|
10
|
+
ReadOnly,
|
11
|
+
ReadWrite,
|
12
|
+
}
|
13
|
+
|
14
|
+
impl AccessMode {
|
15
|
+
// Constructor methods
|
16
|
+
fn read_only() -> Self {
|
17
|
+
AccessMode(AccessModeKind::ReadOnly)
|
18
|
+
}
|
19
|
+
|
20
|
+
fn read_write() -> Self {
|
21
|
+
AccessMode(AccessModeKind::ReadWrite)
|
22
|
+
}
|
23
|
+
|
24
|
+
// Predicate methods
|
25
|
+
fn is_read_only(&self) -> bool {
|
26
|
+
matches!(self.0, AccessModeKind::ReadOnly)
|
27
|
+
}
|
28
|
+
|
29
|
+
fn is_read_write(&self) -> bool {
|
30
|
+
matches!(self.0, AccessModeKind::ReadWrite)
|
31
|
+
}
|
32
|
+
|
33
|
+
// String representations
|
34
|
+
fn to_s(&self) -> &'static str {
|
35
|
+
match self.0 {
|
36
|
+
AccessModeKind::ReadOnly => "read_only",
|
37
|
+
AccessModeKind::ReadWrite => "read_write",
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
fn inspect(&self) -> String {
|
42
|
+
format!("#<Taskchampion::AccessMode:{}>", self.to_s())
|
43
|
+
}
|
44
|
+
|
45
|
+
// Equality - handles any Ruby object type
|
46
|
+
fn eq(&self, other: magnus::Value) -> bool {
|
47
|
+
match <&AccessMode>::try_convert(other) {
|
48
|
+
Ok(other_mode) => self.0 == other_mode.0,
|
49
|
+
Err(_) => false, // Not an AccessMode object, so not equal
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
fn eql(&self, other: magnus::Value) -> bool {
|
54
|
+
match <&AccessMode>::try_convert(other) {
|
55
|
+
Ok(other_mode) => self.0 == other_mode.0,
|
56
|
+
Err(_) => false, // Not an AccessMode object, so not equal
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
fn hash(&self) -> u64 {
|
61
|
+
use std::collections::hash_map::DefaultHasher;
|
62
|
+
use std::hash::{Hash, Hasher};
|
63
|
+
|
64
|
+
let mut hasher = DefaultHasher::new();
|
65
|
+
std::mem::discriminant(&self.0).hash(&mut hasher);
|
66
|
+
hasher.finish()
|
67
|
+
}
|
68
|
+
|
69
|
+
// For internal use
|
70
|
+
pub fn from_symbol(sym: Symbol) -> Result<Self, Error> {
|
71
|
+
match sym.to_string().as_str() {
|
72
|
+
"read_only" => Ok(AccessMode(AccessModeKind::ReadOnly)),
|
73
|
+
"read_write" => Ok(AccessMode(AccessModeKind::ReadWrite)),
|
74
|
+
_ => Err(Error::new(
|
75
|
+
magnus::exception::arg_error(),
|
76
|
+
"Invalid access mode, expected :read_only or :read_write",
|
77
|
+
)),
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
81
|
+
pub fn to_symbol(&self) -> Symbol {
|
82
|
+
match self.0 {
|
83
|
+
AccessModeKind::ReadOnly => Symbol::new("read_only"),
|
84
|
+
AccessModeKind::ReadWrite => Symbol::new("read_write"),
|
85
|
+
}
|
86
|
+
}
|
87
|
+
}
|
88
|
+
|
89
|
+
impl From<TCAccessMode> for AccessMode {
|
90
|
+
fn from(mode: TCAccessMode) -> Self {
|
91
|
+
match mode {
|
92
|
+
TCAccessMode::ReadOnly => AccessMode(AccessModeKind::ReadOnly),
|
93
|
+
TCAccessMode::ReadWrite => AccessMode(AccessModeKind::ReadWrite),
|
94
|
+
}
|
95
|
+
}
|
96
|
+
}
|
97
|
+
|
98
|
+
impl From<AccessMode> for TCAccessMode {
|
99
|
+
fn from(mode: AccessMode) -> Self {
|
100
|
+
match mode.0 {
|
101
|
+
AccessModeKind::ReadOnly => TCAccessMode::ReadOnly,
|
102
|
+
AccessModeKind::ReadWrite => TCAccessMode::ReadWrite,
|
103
|
+
}
|
104
|
+
}
|
105
|
+
}
|
106
|
+
|
107
|
+
pub fn init(module: &RModule) -> Result<(), Error> {
|
108
|
+
let class = module.define_class("AccessMode", class::object())?;
|
109
|
+
|
110
|
+
// Constructor methods
|
111
|
+
class.define_singleton_method("read_only", function!(AccessMode::read_only, 0))?;
|
112
|
+
class.define_singleton_method("read_write", function!(AccessMode::read_write, 0))?;
|
113
|
+
|
114
|
+
// Predicate methods
|
115
|
+
class.define_method("read_only?", method!(AccessMode::is_read_only, 0))?;
|
116
|
+
class.define_method("read_write?", method!(AccessMode::is_read_write, 0))?;
|
117
|
+
|
118
|
+
// String representations
|
119
|
+
class.define_method("to_s", method!(AccessMode::to_s, 0))?;
|
120
|
+
class.define_method("inspect", method!(AccessMode::inspect, 0))?;
|
121
|
+
|
122
|
+
// Equality
|
123
|
+
class.define_method("==", method!(AccessMode::eq, 1))?;
|
124
|
+
class.define_method("eql?", method!(AccessMode::eql, 1))?;
|
125
|
+
class.define_method("hash", method!(AccessMode::hash, 0))?;
|
126
|
+
|
127
|
+
// Keep the constants for backward compatibility
|
128
|
+
module.const_set("READ_ONLY", Symbol::new("read_only"))?;
|
129
|
+
module.const_set("READ_WRITE", Symbol::new("read_write"))?;
|
130
|
+
|
131
|
+
Ok(())
|
132
|
+
}
|