Class: Reclaim::CLI

Inherits:
Object
  • Object
show all
Defined in:
lib/reclaim/cli.rb

Overview

CLI Interface

Class Method Summary collapse

Class Method Details

.add_task_arguments(parser, options) ⇒ Object



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/reclaim/cli.rb', line 8

def self.add_task_arguments(parser, options)
  parser.on('--title TITLE', 'Task title') { |v| options[:title] = v }
  parser.on('--due DUE', 'Due date (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS, or "none" to clear)') do |v|
    options[:due_date] = parse_clearable_date(v)
  end
  parser.on('--priority PRIORITY', ['P1', 'P2', 'P3', 'P4'], 'Task priority') { |v| options[:priority] = v.downcase.to_sym }
  parser.on('--duration DURATION', Float, 'Task duration in hours') { |v| options[:duration] = v }
  parser.on('--min-chunk MIN', Float, 'Minimum chunk size in hours') { |v| options[:min_chunk_size] = v }
  parser.on('--max-chunk MAX', Float, 'Maximum chunk size in hours') { |v| options[:max_chunk_size] = v }
  parser.on('--min-work MIN', Float, 'Minimum work duration in hours') { |v| options[:min_work_duration] = v }
  parser.on('--max-work MAX', Float, 'Maximum work duration in hours') { |v| options[:max_work_duration] = v }
  parser.on('--snooze DATETIME', 'Start after this date/time (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS, or "none" to clear)') do |v|
    options[:snooze_until] = parse_clearable_date(v)
  end
  parser.on('--defer DATETIME', 'Start after this date/time (synonym for --snooze, or "none" to clear)') do |v|
    options[:snooze_until] = parse_clearable_date(v)
  end
  parser.on('--start DATETIME', 'Specific start time (YYYY-MM-DDTHH:MM:SS, or "none" to clear)') do |v|
    options[:start] = parse_clearable_date(v)
  end
  parser.on('--time-scheme SCHEME', 'Time scheme ID or name (e.g., "work", "personal", "Work Hours", or UUID)') { |v| options[:time_scheme] = v }
  parser.on('--split [CHUNK_SIZE]', 'Allow task to be split into smaller chunks. Optional: specify min chunk size in hours (e.g. 0.5 for 30min)') do |v|
    options[:allow_splitting] = true
    options[:split_chunk_size] = v.to_f if v && v.to_f > 0
  end
  parser.on('--private PRIVATE', 'Make task private (true/false)') do |v|
    options[:always_private] = case v.downcase
                               when 'true', '1', 'yes', 'y' then true
                               when 'false', '0', 'no', 'n' then false
                               else
                                 puts "✗ Invalid value for --private. Use true/false"
                                 exit(1)
                               end
  end
  parser.on('--category CATEGORY', 'Event category') { |v| options[:event_category] = v }
  parser.on('--color COLOR', 'Event color') { |v| options[:event_color] = v }
  parser.on('--notes NOTES', 'Task notes/description') { |v| options[:notes] = v }
end

.complete_task(client, task_id) ⇒ Object



270
271
272
273
274
275
276
# File 'lib/reclaim/cli.rb', line 270

def self.complete_task(client, task_id)
  task = client.complete_task(task_id)
  puts "✓ Completed task: #{task.title}"
rescue NotFoundError
  puts "✗ Task #{task_id} not found"
  exit(1)
end

.create_task(client, options) ⇒ Object



232
233
234
235
236
237
238
# File 'lib/reclaim/cli.rb', line 232

def self.create_task(client, options)
  task = client.create_task(**options)
  puts "✓ Created task: #{task.title} (ID: #{task.id})"
rescue InvalidRecordError => e
  puts "✗ Error creating task: #{e.message}"
  exit(1)
end

.delete_task(client, task_id) ⇒ Object



278
279
280
281
282
283
284
# File 'lib/reclaim/cli.rb', line 278

def self.delete_task(client, task_id)
  client.delete_task(task_id)
  puts "✓ Deleted task: #{task_id}"
rescue NotFoundError
  puts "✗ Task #{task_id} not found"
  exit(1)
end

.get_task(client, task_id) ⇒ Object



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/reclaim/cli.rb', line 240

def self.get_task(client, task_id)
  task = client.get_task(task_id)

  puts "\nTask: #{task.title}"
  puts "ID: #{task.id}"
  puts "Priority: #{task.priority}"
  puts "Status: #{task.status}"
  puts "Duration: #{task.duration} hours" if task.duration
  puts "Due: #{task.due_date_formatted}" if task.due_date
  puts "Time Scheme: #{task.time_scheme_id}" if task.time_scheme_id
  puts "Private: #{task.always_private}" if task.always_private
  puts "Category: #{task.event_category}" if task.event_category
  puts "Color: #{task.event_color}" if task.event_color
  puts "Notes: #{task.notes}" if task.notes && !task.notes.empty?
rescue NotFoundError
  puts "✗ Task #{task_id} not found"
  exit(1)
end

.list_tasks(client, filter = nil) ⇒ Object

CLI command implementations



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/reclaim/cli.rb', line 210

def self.list_tasks(client, filter = nil)
  tasks = client.list_tasks(filter: filter)

  if tasks.empty?
    puts "No tasks found#{filter ? " matching filter '#{filter}'" : ''}."
    return
  end

  puts "\nYour Reclaim Tasks#{filter ? " (#{filter})" : ''}:"
  puts '-' * 50

  tasks.each do |task|
    status_icon = task.completed? ? '✓' : '○'
    due_str = task.due_date ? " (due: #{task.due_date_formatted})" : ''

    puts "#{status_icon} #{task.title}#{due_str}"
    puts "   ID: #{task.id} | Priority: #{task.priority} | Status: #{task.status}"
  end

  puts "\nTotal: #{tasks.length} tasks"
end

.list_time_schemes(client, help_aliases = false) ⇒ Object



286
287
288
289
290
291
292
293
294
295
# File 'lib/reclaim/cli.rb', line 286

def self.list_time_schemes(client, help_aliases = false)
  puts client.format_time_schemes

  if help_aliases
    puts "\nCommon Aliases:"
    puts "• work, working hours, business hours → Finds schemes containing 'work'"
    puts "• personal, off hours, off-hours, private → Finds schemes containing 'personal'"
    puts "• You can also use partial matches (e.g., 'Work' matches 'Work Hours')"
  end
end

.parse_clearable_date(value) ⇒ Object

Parse date values that can be cleared with special keywords



48
49
50
51
52
53
54
# File 'lib/reclaim/cli.rb', line 48

def self.parse_clearable_date(value)
  return nil if value.nil?
  # Handle special clear keywords
  return nil if ['none', 'clear', 'null', ''].include?(value.downcase.strip)
  # Otherwise return the date string as-is for the API to parse
  value
end

.runObject



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/reclaim/cli.rb', line 124

def self.run
  command = ARGV.shift || 'list'

  # Handle help flag
  if command == '--help' || command == '-h'
    command = 'help'
  end

  # If no command provided, default to listing active tasks
  if command == 'list' && ARGV.empty?
    ARGV.unshift('active')
  end

  begin
    client = Reclaim::Client.new
  rescue AuthenticationError => e
    puts "✗ #{e.message}"
    exit(1)
  end

  case command
  when 'list'
    filter = ARGV.shift
    if filter && !['active', 'completed', 'overdue'].include?(filter)
      show_help_and_exit("Invalid filter '#{filter}'. Valid options: active, completed, overdue")
    end
    list_tasks(client, filter&.to_sym)

  when 'create'
    options = {}
    parser = OptionParser.new
    add_task_arguments(parser, options)
    parser.parse!(ARGV)

    show_help_and_exit("Task title is required. Use --title TITLE") if options[:title].nil?

    create_task(client, options)

  when 'get'
    task_id = ARGV.shift
    show_help_and_exit("Task ID is required") if task_id.nil?
    get_task(client, task_id)

  when 'update'
    task_id = ARGV.shift
    show_help_and_exit("Task ID is required") if task_id.nil?

    options = {}
    parser = OptionParser.new
    add_task_arguments(parser, options)
    parser.parse!(ARGV)

    show_help_and_exit("No update fields provided") if options.empty?
    update_task(client, task_id, options)

  when 'complete'
    task_id = ARGV.shift
    show_help_and_exit("Task ID is required") if task_id.nil?
    complete_task(client, task_id)

  when 'delete'
    task_id = ARGV.shift
    show_help_and_exit("Task ID is required") if task_id.nil?
    delete_task(client, task_id)

  when 'list-schemes'
    help_aliases = false
    parser = OptionParser.new
    parser.on('--help-aliases', 'Show common aliases for time schemes') { help_aliases = true }
    parser.parse!(ARGV)

    list_time_schemes(client, help_aliases)

  when 'help'
    show_help_and_exit

  else
    show_help_and_exit("Unknown command '#{command}'")
  end

rescue StandardError => e
  puts "✗ Error: #{e.message}"
  exit(1)
end

.show_help_and_exit(message = nil) ⇒ Object



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/reclaim/cli.rb', line 56

def self.show_help_and_exit(message = nil)
  puts "✗ #{message}" if message
  puts "    Reclaim Task CRUD Operations\n\n    Usage: reclaim [COMMAND] [OPTIONS]\n\n    Commands:\n      list [FILTER]           List tasks (optional filter: active, completed, overdue)\n                              (default: lists active tasks when no command given)\n      create                  Create a new task (requires --title)\n      get TASK_ID            Get task details\n      update TASK_ID         Update a task\n      complete TASK_ID       Mark task as complete (ARCHIVED status)\n      delete TASK_ID         Delete a task (permanent deletion)\n      list-schemes           List available time schemes\n      help                   Show this help message\n\n    Task Options:\n      --title TITLE          Task title\n      --due DATE             Due date (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS, or \"none\" to clear)\n      --priority PRIORITY    Task priority (P1, P2, P3, P4)\n      --duration HOURS       Task duration in hours (e.g., 0.25 for 15min, 1.5 for 90min)\n      --split [CHUNK_SIZE]   Allow task splitting. Optional: min chunk size in hours (default: no splitting)\n      --min-chunk HOURS      Minimum chunk size in hours (only with --split)\n      --max-chunk HOURS      Maximum chunk size in hours (only with --split)\n      --min-work HOURS       Minimum work duration in hours\n      --max-work HOURS       Maximum work duration in hours\n      --defer DATE           Start after this date/time (synonym for --snooze, or \"none\" to clear)\n      --snooze DATE          Start after this date/time (or \"none\" to clear)\n      --start DATE           Specific start time (or \"none\" to clear)\n      --time-scheme SCHEME   Time scheme ID or name\n      --private BOOL         Make task private (true/false)\n      --category CATEGORY    Event category\n      --color COLOR          Event color\n      --notes TEXT           Task notes/description\n\n    Clearing Dates:\n      Use \"none\", \"clear\", or \"null\" as the value to remove a date field.\n      Examples:\n        reclaim update abc123 --due none           # Clear due date\n        reclaim update abc123 --defer clear        # Clear deferred start date\n        reclaim update abc123 --start null         # Clear specific start time\n\n    Time Scheme Aliases:\n      work, working hours, business hours  \u2192 Finds schemes containing 'work'\n      personal, off hours, private         \u2192 Finds schemes containing 'personal'\n\n    Status Values:\n      SCHEDULED, IN_PROGRESS, COMPLETE (still active), ARCHIVED (truly complete)\n\n    ID Tracking for GTD Integration:\n      Store Reclaim task IDs in NEXT.md as [Reclaim:id] for sync operations.\n\n    Examples:\n      reclaim                                   # Lists active tasks (default)\n      reclaim list active                       # Lists active tasks (explicit)\n      reclaim list completed                    # Lists completed tasks\n      reclaim create --title \"Important Work\" --due 2025-08-15 --priority P1 --duration 2\n      reclaim create --title \"Research\" --duration 3 --split 0.5  # Allow splitting with 30min minimum chunks\n      reclaim create --title \"Deep Work\" --duration 4             # No splitting (default)\n      reclaim update abc123 --title \"Updated Title\" --priority P2\n      reclaim complete abc123\n      reclaim list-schemes\n  HELP\n  exit(0)\nend\n"

.update_task(client, task_id, options) ⇒ Object



259
260
261
262
263
264
265
266
267
268
# File 'lib/reclaim/cli.rb', line 259

def self.update_task(client, task_id, options)
  task = client.update_task(task_id, **options)
  puts "✓ Updated task: #{task.title}"
rescue NotFoundError
  puts "✗ Task #{task_id} not found"
  exit(1)
rescue InvalidRecordError => e
  puts "✗ Error updating task: #{e.message}"
  exit(1)
end