toodledo 1.3.5 → 1.3.8

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,17 @@
1
+ == 1.3.8
2
+
3
+ * Rubygems does not allow you to publish the same version, even if yanked. Thanks SO much.
4
+
5
+ == 1.3.7
6
+
7
+ * do not nuke the token on disconnect *unless* it is too old. avoids requesting too many tokens. (ahoward / da18e37aa61abf6a07fa)
8
+ * Add date methods to a few other places, add tag methods, expand and update tests (all pass) (rleber)
9
+ * Added function to allow the simple adding of a date to task using #[2011-03-19] (cescalante, rleber)
10
+
11
+ == 1.3.6
12
+
13
+ * Get out of the automatic reconnect business.
14
+
1
15
  == 1.3.5 / 2010-10-15
2
16
 
3
17
  * Patches for raising a better error when no internet connection, recovering from state where no internet + token expired.
@@ -1,9 +1,9 @@
1
- = toodledo
1
+ # toodledo
2
2
 
3
- * http://toodledo.rubyforge.org
4
- * mailto:will@tersesystems.com
3
+ * https://github.com/wsargent/toodledo
4
+ * will.sargent@gmail.com
5
5
 
6
- == DESCRIPTION:
6
+ ## DESCRIPTION
7
7
 
8
8
  This is a Ruby API and client for http://toodledo.com, a task management
9
9
  website. It implements all of the calls from Toodledo's developer API, and
@@ -17,7 +17,7 @@ as part of a web application. Custom private RSS feed? Want to have the Mac
17
17
  read out your top priority? Input tasks through Quicksilver? Print out
18
18
  tasks with a BetaBrite? It can all happen.
19
19
 
20
- == FEATURES/PROBLEMS:
20
+ ## FEATURES/PROBLEMS
21
21
 
22
22
  * Command line client interface
23
23
  * Interactive client interface
@@ -25,100 +25,113 @@ tasks with a BetaBrite? It can all happen.
25
25
  * Supports Proxy and SSL usage
26
26
  * Easy configuration and automation (Quicksilver / Scripts / Automator)
27
27
 
28
- == SYNOPSIS:
28
+ ## SYNOPSIS
29
29
 
30
- === SETUP:
30
+ ### SETUP
31
31
 
32
- The first thing you should do is open up your browser and go to:
32
+ You will need an account on Toodledo. Once you have that and you're logged in, go to:
33
33
 
34
- http://www.toodledo.com/info/api_doc.php
34
+ http://www.toodledo.com/info/api_doc.php
35
35
 
36
36
  and retrieve your userid. You will need this for setup.
37
37
 
38
- Then, install toodledo. This is either 'gem install toodledo' or
39
- 'sudo gem install toodledo' depending on your platform.
38
+ Then, type
40
39
 
41
- Then, type 'toodledo setup' and enter your userid and password in
42
- the spaces provided. Then save the file, and you're good to go.
40
+ gem install toodledo
41
+ toodledo setup
43
42
 
44
- === COMMAND LINE:
43
+ and enter your userid and password in the spaces provided. Then save the file, and you're good to go.
44
+
45
+ ### COMMAND LINE
45
46
 
46
47
  You can add tasks. The simplest form is here:
47
48
 
48
- toodledo add 'This is a test'
49
+ toodledo add 'This is a test'
49
50
 
50
51
  But tasks don't have to be simple. Toodledo has a particularly rich model of
51
52
  a task, and allows full GTD type state to be attached to them. The syntax
52
53
  for the client is as follows:
53
54
 
54
- *Folder
55
- @Context
56
- ^Goal
57
- !Priority
55
+ *Folder
56
+ @Context
57
+ ^Goal
58
+ !Priority
59
+ #DueDate
60
+ %Tags
58
61
 
59
62
  You can encase the symbol with square brackets if there is a space involved:
60
63
 
61
- *[Blue Sky]
62
- @[Someday / Maybe]
63
- ^[Write Toodledo Ruby API]
64
- !top
64
+ *[Blue Sky]
65
+ @[Someday / Maybe]
66
+ ^[Write Toodledo Ruby API]
67
+ !top
68
+ #[2011-03-18] or #[today]
69
+ %[foo bar]
70
+
71
+ You can only provide one folder, context, goal, priority, or date, but you can
72
+ provide multiple tags, using the syntax shown above (i.e. foo and bar are two
73
+ separate tags).
65
74
 
66
75
  Let's use the command line client to list only the tasks you have in the office:
67
76
 
68
- toodledo list '@Office *Action'
77
+ toodledo tasks '@Office *Action'
69
78
 
70
79
  Now let's add a task with several symbols:
71
80
 
72
- toodledo add '*Action @Programming ^[Write Toodledo Ruby API] Write docs'
81
+ toodledo add '*Action @Programming ^[Write Toodledo Ruby API] Write docs'
82
+
83
+ Now let's add a different task with a date and tags:
84
+
85
+ toodledo add Write more docs #today %for_my_boss
73
86
 
74
87
  You can also edit tasks, using the task id. This sets the folder to Someday:
75
88
 
76
- toodledo edit '*Someday 15934131'
89
+ toodledo edit '*Someday 15934131'
77
90
 
78
91
  And finally you can complete or delete tasks, again using the task id.
79
92
 
80
- toodledo complete 15934131
81
- toodledo delete 15934131
93
+ toodledo complete 15934131
94
+ toodledo delete 15934131
82
95
 
83
- === INTERACTIVE MODE:
96
+ ### INTERACTIVE MODE
84
97
 
85
98
  Toodledo also comes with an interactive mode that is used if no arguments are
86
99
  found:
87
100
 
88
- toodledo
89
- > add This is a test
90
-
91
- You can type help at the prompt for a complete list of commands. The client
101
+ toodledo
102
+ > add This is a test
103
+
104
+ You can type 'help' at the prompt for a complete list of commands. The client
92
105
  makes for a nice way to enter in tasks as you think of them.
93
106
 
94
107
  The client will also allow you to set up filters. Filters are added with
95
108
  the symbols, so in interactive mode
96
109
 
97
- filter @Office *Action
98
- list
110
+ filter @Office *Action
111
+ tasks
99
112
 
100
113
  Then it produces the same results as:
101
114
 
102
- toodledo list '@Office *Action'
115
+ toodledo tasks '@Office *Action'
103
116
 
104
117
  Finally, if you want to write your own scripts, working with Toodledo is very
105
118
  simple, since it will use the YAML config file:
106
119
 
107
- require 'rubygems'
108
- require 'toodledo'
109
- Toodledo.begin do |session|
110
- # work with session
111
- end
120
+ require 'rubygems'
121
+ require 'toodledo'
122
+ Toodledo.begin do |session|
123
+ # work with session
124
+ end
112
125
 
113
126
  If you want to work with the session directly, then you should do
114
127
  this instead:
115
128
 
116
- require 'rubygems'
117
- require 'toodledo'
118
- session = Session.new(userid, password)
119
- session.connect()
129
+ require 'rubygems'
130
+ require 'toodledo'
131
+ session = Session.new(userid, password)
132
+ session.connect()
120
133
 
121
- == REQUIREMENTS:
134
+ ## REQUIREMENTS
122
135
 
123
136
  * A connection to the Internet
124
137
  * An account to http://toodledo.com
@@ -127,11 +140,11 @@ this instead:
127
140
  * highline
128
141
  * rubygems
129
142
 
130
- == INSTALL:
143
+ ## INSTALL
131
144
 
132
145
  * sudo gem install toodledo
133
146
  * toodledo setup (sets up the YAML file with your credentials)
134
147
  * toodledo
135
148
 
136
- == LICENSE:
149
+ ## LICENSE:
137
150
  GPL v3
data/Rakefile CHANGED
@@ -1,24 +1,66 @@
1
1
  # -*- ruby -*-
2
2
 
3
3
  require 'rubygems'
4
- require 'hoe'
5
- $:.unshift(File.dirname(__FILE__) + "/lib")
6
- require 'toodledo'
7
-
8
- Hoe.spec('toodledo') do |p|
9
- p.rubyforge_name = 'toodledo'
10
- p.version = Toodledo::VERSION
11
- p.author = 'Will Sargent'
12
- p.email = 'will@tersesystems.com'
13
- p.summary = 'A command line client and API to Toodledo'
14
- p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
15
- p.url = "http://gemcutter.org/gems/toodledo"
16
- p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
17
- p.rsync_args << ' --exclude=statsvn/'
18
- p.test_globs = ["test/**/*_test.rb"]
19
- p.extra_deps << ['cmdparse', '>= 0']
20
- p.extra_deps << ['highline', '>= 0']
21
- p.extra_dev_deps << [ 'flexmock', '>= 0']
4
+ require 'rake'
5
+
6
+ begin
7
+ require 'jeweler'
8
+ require 'lib/toodledo/version'
9
+ Jeweler::Tasks.new do |gem|
10
+ gem.version = Toodledo::Version::VERSION
11
+
12
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
13
+ gem.name = "toodledo"
14
+ gem.author = 'Will Sargent'
15
+ gem.email = 'will.sargent@gmail.com'
16
+ gem.summary = 'A command line client and API to Toodledo'
17
+ gem.description = <<-EOF
18
+ This is a Ruby API and client for http://toodledo.com, a task management
19
+ website. It implements all of the calls from Toodledo's developer API, and
20
+ provides a nice wrapper around the functionality.
21
+
22
+ The client allows you to work with Toodledo from the command line. It will
23
+ work in either interactive or command line mode.
24
+
25
+ You can also use the client in your shell scripts, or use the API directly
26
+ as part of a web application. Custom private RSS feed? Want to have the Mac
27
+ read out your top priority? Input tasks through Quicksilver? Print out
28
+ tasks with a BetaBrite? It can all happen.
29
+ EOF
30
+ gem.homepage = "http://github.com/wsargent/toodledo"
31
+ gem.authors = ["Will Sargent"]
32
+
33
+ gem.executables = [ 'toodledo' ]
34
+
35
+ gem.add_dependency('cmdparse')
36
+ gem.add_dependency('highline')
37
+
38
+ gem.add_development_dependency('flexmock')
39
+ end
40
+
41
+ # Set up publishing to rubygems.
42
+ Jeweler::RubygemsDotOrgTasks.new
43
+ rescue LoadError
44
+ puts "Cannot load jeweler"
45
+ end
46
+
47
+ require 'rake/testtask'
48
+ Rake::TestTask.new(:test) do |test|
49
+ test.libs << 'lib' << 'test'
50
+ test.pattern = 'test/**/*_test.rb'
51
+ test.verbose = true
52
+ end
53
+
54
+ task :default => :test
55
+
56
+ require 'rake/rdoctask'
57
+ Rake::RDocTask.new do |rdoc|
58
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
59
+
60
+ rdoc.rdoc_dir = 'rdoc'
61
+ rdoc.title = "toodledo #{version}"
62
+ rdoc.rdoc_files.include('README*')
63
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
64
  end
23
65
 
24
66
  # vim: syntax=Ruby
data/lib/toodledo.rb CHANGED
@@ -2,11 +2,13 @@
2
2
  # The top level Toodledo module. This does very little that is
3
3
  # interesting. You probably want to look at Toodledo::Session
4
4
  #
5
+
6
+ require 'toodledo/version'
7
+
5
8
  module Toodledo
6
9
 
7
- # Required for gem
8
- VERSION = '1.3.5'
9
-
10
+ VERSION = ::Toodledo::Version::VERSION
11
+
10
12
  # Returns the configuration object.
11
13
  def self.get_config()
12
14
  return @@config
@@ -65,7 +67,6 @@ module Toodledo
65
67
  end
66
68
 
67
69
  require 'toodledo/server_error'
68
- require 'toodledo/item_not_found_error'
69
70
  require 'toodledo/invalid_configuration_error'
70
71
  require 'toodledo/status'
71
72
  require 'toodledo/task'
@@ -1,840 +1,893 @@
1
- require 'rubygems'
2
-
3
- require 'cmdparse'
4
- require 'fileutils'
5
- require 'highline/import'
6
- require 'yaml'
7
-
8
- require 'toodledo'
9
- require 'toodledo/command_line/parser_helper'
10
- require 'toodledo/command_line/base_command'
11
- require 'toodledo/command_line/interactive_command'
12
- require 'toodledo/command_line/stdin_command'
13
- require 'toodledo/command_line/setup_command'
14
-
15
- # CREATE
16
- require 'toodledo/command_line/add_command'
17
-
18
- # READ
19
- require 'toodledo/command_line/hotlist_command'
20
- require 'toodledo/command_line/list_tasks_command'
21
- require 'toodledo/command_line/list_tasks_by_context_command'
22
- require 'toodledo/command_line/list_folders_command'
23
- require 'toodledo/command_line/list_contexts_command'
24
- require 'toodledo/command_line/list_goals_command'
25
-
26
- # UPDATE
27
- require 'toodledo/command_line/edit_command'
28
- require 'toodledo/command_line/complete_command'
29
-
30
- # DELETE
31
- require 'toodledo/command_line/delete_command'
32
-
33
- # FORMATTERS
34
- require 'toodledo/command_line/task_formatter'
35
- require 'toodledo/command_line/context_formatter'
36
- require 'toodledo/command_line/folder_formatter'
37
- require 'toodledo/command_line/goal_formatter'
38
-
39
- module Toodledo
40
- module CommandLine
41
-
42
- #
43
- # The toodledo client. This provides a command line based client to the
44
- # user and gives a good overview of the capabilities of the API as well.
45
- #
46
- # Author:: Will Sargent (mailto:will@tersesystems.com)
47
- # Copyright:: Copyright (c) 2008 Will Sargent
48
- # License:: GLPL v3
49
- class Client
50
-
51
- include Toodledo::CommandLine::ParserHelper
52
-
53
- HOME = ENV["HOME"] || ENV["HOMEPATH"] || File::expand_path("~")
54
- TOODLEDO_D = File::join(HOME, ".toodledo")
55
- CONFIG_F = File::join(TOODLEDO_D, "user-config.yml")
56
-
57
- # We must use __FILE__ instead of DATA because this is now a library
58
- # and DATA is relative to $0, not __FILE__.
59
- CONFIG = File.read(__FILE__).split(/__END__/).last.gsub(/#\{(.*)\}/) { eval $1 }
60
-
61
- #
62
- # Creates the client object.
63
- #
64
- def initialize(userconfig=CONFIG_F, opts={})
65
- @filters = {}
66
- @debug = false
67
- @logger = Logger.new(STDOUT)
68
- @logger.level = Logger::FATAL
69
-
70
- @userconfig = test(?e, userconfig) ? IO::read(userconfig) : CONFIG
71
- @userconfig = YAML.load(@userconfig).merge(opts)
72
- @formatters = {
73
- :task => TaskFormatter.new,
74
- :goal => GoalFormatter.new,
75
- :context => ContextFormatter.new,
76
- :folder => FolderFormatter.new
77
- }
78
- end
79
-
80
- #
81
- # Returns debugging status.
82
- #
83
- def debug?
84
- return @debug
85
- end
86
-
87
- #
88
- # Sets the debugging on or off.
89
- #
90
- def debug=(is_debug)
91
- @debug = is_debug
92
- if (@debug == true)
93
- @logger.level = Logger::DEBUG
94
- else
95
- @logger.level = Logger::FATAL
96
- end
97
- end
98
-
99
- #
100
- # Returns the logger.
101
- #
102
- def logger
103
- return @logger
104
- end
105
-
106
- #
107
- # Invites the user to setup the YAML file.
108
- #
109
- def setup
110
- FileUtils::mkdir_p TOODLEDO_D, :mode => 0700 unless test ?d, TOODLEDO_D
111
- test ?e, CONFIG_F and FileUtils::mv CONFIG_F, "#{CONFIG_F}.bak"
112
- config = CONFIG[/\A.*(?=^\# AUTOCONFIG)/m]
113
- open(CONFIG_F, "w") { |f| f.write config }
114
-
115
- edit = (ENV["EDITOR"] || ENV["EDIT"] || "vi") + " '#{CONFIG_F}'"
116
- system edit or puts "edit '#{CONFIG_F}'"
117
- end
118
-
119
- #
120
- # Displays the configuration information that the session is
121
- # currently using.
122
- #
123
- def show_config(session)
124
- base_url = session.base_url
125
- user_id = session.user_id
126
- proxy = session.proxy
127
-
128
- print "base_url = #{base_url}"
129
- print "user_id = #{user_id}"
130
- print "proxy = #{proxy.inspect}"
131
- end
132
-
133
- # Sets the context filter. Subsequent calls to show tasks
134
- # will only show tasks that have this context.
135
- #
136
- def set_filter(session, input)
137
- logger.debug("set_filter(#{input})")
138
-
139
- input.strip!
140
-
141
- context = parse_context(input)
142
- if (context != nil)
143
- c = session.get_context_by_name(context)
144
- if (c == nil)
145
- print "No such context: #{context}"
146
- return
147
- end
148
- @filters[:context] = c
149
- end
150
-
151
- goal = parse_goal(input)
152
- if (goal != nil)
153
- g = session.get_goal_by_name(goal)
154
- if (g == nil)
155
- print "No such goal: #{goal}"
156
- return
157
- end
158
- @filters[:goal] = g
159
- end
160
-
161
- folder = parse_folder(input)
162
- if (folder != nil)
163
- f = session.get_folder_by_name(folder)
164
- if (f == nil)
165
- print "No such folder: #{folder}"
166
- end
167
- @filters[:folder] = f
168
- end
169
-
170
- priority = parse_priority(input)
171
- if (priority != nil)
172
- @filters[:priority] = priority
173
- end
174
-
175
- if (logger)
176
- logger.debug("@filters = #{@filters.inspect}")
177
- end
178
- end
179
-
180
- #
181
- # Shows all the filters.
182
- #
183
- def list_filters()
184
- if (@filters == nil || @filters.empty?)
185
- print "No filters."
186
- return
187
- end
188
-
189
- @filters.each do |k, v|
190
- if (v.respond_to? :name)
191
- name = v.name
192
- else
193
- name = v
194
- end
195
- print "#{k}: #{name}\n"
196
- end
197
- end
198
-
199
- #
200
- # Clears all the filters.
201
- #
202
- def unfilter()
203
- @filters = {}
204
- print "Filters cleared.\n"
205
- end
206
-
207
- #
208
- # Displays the 'hotlist' of tasks. This shows all the uncompleted items with
209
- # priority set to 3 or 2. There's no facility in the API for this, so we have
210
- # to cheat a bit.
211
- #
212
- # It may be worthwhile to allow the ability to tweak what constitutes a 'hotlist'
213
- # but that'll come by demand. Or patches. Fully documented patches, mmmm.
214
- #
215
- def hotlist(session, input)
216
- logger.debug("hotlist: #{input}")
217
-
218
- # See if there's input following the command.
219
- context = parse_context(input)
220
- folder = parse_folder(input)
221
- goal = parse_goal(input)
222
- priority = parse_priority(input)
223
-
224
- params = { :notcomp => true }
225
-
226
- # If there are, they override what we have set.
227
- if (folder != nil)
228
- params.merge!({ :folder => folder })
229
- end
230
-
231
- if (context != nil)
232
- params.merge!({ :context => context })
233
- end
234
-
235
- if (goal != nil)
236
- params.merge!({ :goal => goal })
237
- end
238
-
239
- if (priority != nil)
240
- params.merge!({ :priority => priority })
241
- end
242
-
243
- tasks = session.get_tasks(params)
244
-
245
- # Highest priority first
246
- tasks.sort! do |a, b|
247
- b.priority <=> a.priority
248
- end
249
-
250
- # filter on our end.
251
- # Surprisingly, we can't search for "greater than 0 priority" with the API.
252
- not_important = Priority::MEDIUM
253
-
254
- for task in tasks
255
- if (task.priority > not_important)
256
- print @formatters[:task].format(task)
257
- end
258
- end
259
- end
260
-
261
- #
262
- # Lists tasks (subject to any filters that may be present).
263
- #
264
- def list_tasks(session, input)
265
- logger.debug("list_tasks(#{input})")
266
-
267
- params = { :notcomp => true }
268
-
269
- params.merge!(@filters)
270
-
271
- # See if there's input following the 'tasks' command.
272
- context = parse_context(input)
273
- folder = parse_folder(input)
274
- goal = parse_goal(input)
275
- priority = parse_priority(input)
276
-
277
- # If there are, they override what we have set.
278
- if (folder != nil)
279
- params.merge!({ :folder => folder })
280
- end
281
-
282
- if (context != nil)
283
- params.merge!({ :context => context })
284
- end
285
-
286
- if (goal != nil)
287
- params.merge!({ :goal => goal })
288
- end
289
-
290
- if (priority != nil)
291
- params.merge!({ :priority => priority })
292
- end
293
-
294
- tasks = session.get_tasks(params)
295
-
296
- # Highest priority first
297
- tasks.sort! do |a, b|
298
- b.priority <=> a.priority
299
- end
300
-
301
- for task in tasks
302
- print @formatters[:task].format(task)
303
- end
304
- end
305
-
306
- #
307
- # Prints all active tasks nested by context.
308
- #
309
- def list_tasks_by_context(session, line)
310
- folder = parse_folder(line)
311
-
312
- session.get_contexts().each do |context|
313
- criteria = { :folder => folder, :context => context, :notcomp => true }
314
- tasks = session.get_tasks(criteria)
315
- print "#{context.name}" if (! tasks.empty?)
316
- tasks.each { |task| print " " + @formatters[:task].format(task) }
317
- end
318
- end
319
-
320
- #
321
- # Lists the goals. Takes an optional argument of
322
- # 'short', 'medium' or 'life'.
323
- #
324
- def list_goals(session, input)
325
-
326
- input.strip!
327
- input.downcase!
328
-
329
- goals = session.get_goals()
330
-
331
- goals.sort! do |a, b|
332
- a.level <=> b.level
333
- end
334
-
335
- level_filter = nil
336
- case input
337
- when 'short'
338
- level_filter = Goal::SHORT_LEVEL
339
- when 'medium'
340
- level_filter = Goal::MEDIUM_LEVEL
341
- when 'life'
342
- level_filter = Goal::LIFE_LEVEL
343
- end
344
-
345
- for goal in goals
346
- if (level_filter != nil && goal.level != level_filter)
347
- next # skip this goal if it doesn't meet the filter
348
- end
349
- print @formatters[:goal].format(goal)
350
- end
351
- end
352
-
353
- #
354
- # Lists the contexts.
355
- #
356
- def list_contexts(session, input)
357
- params = { }
358
-
359
- contexts = session.get_contexts()
360
-
361
- for context in contexts
362
- print @formatters[:context].format(context)
363
- end
364
- end
365
-
366
- #
367
- # Lists the folders.
368
- #
369
- def list_folders(session, input)
370
- params = { }
371
-
372
- folders = session.get_folders()
373
-
374
- for folder in folders
375
- print @formatters[:folder].format(folder)
376
- end
377
- end
378
-
379
- # Adds a single task, using toodledo symbols. This is the most general way to
380
- # add a task right now. If you have symbols which have spaces, then you must
381
- # encase them in square brackets.
382
- #
383
- # The order of symbols does not matter, but the title must be the last thing
384
- # on the line.
385
- #
386
- # add @[Deep Space] !top *Action ^[For Great Justice] Take off every Zig
387
- #
388
- def add_task(session, line)
389
- context = parse_context(line)
390
- folder = parse_folder(line)
391
- goal = parse_goal(line)
392
- priority = parse_priority(line)
393
- title = parse_remainder(line)
394
-
395
- params = {}
396
- if (priority != nil)
397
- params.merge!({ :priority => priority })
398
- end
399
-
400
- if (folder != nil)
401
- params.merge!({ :folder => folder })
402
- end
403
-
404
- if (context != nil)
405
- params.merge!({ :context => context })
406
- end
407
-
408
- if (goal != nil)
409
- params.merge!({ :goal => goal })
410
- end
411
-
412
- # If we got nothing but 'add' then ask for it explicitly.
413
- if (title == nil)
414
- title = ask("Task name: ") { |q| q.readline = true }
415
- end
416
-
417
- task_id = session.add_task(title, params)
418
-
419
- print "Task #{task_id} added."
420
- end
421
-
422
- #
423
- # Adds context.
424
- #
425
- def add_context(session, input)
426
-
427
- title = input.strip
428
-
429
- context_id = session.add_context(title)
430
-
431
- print "Context #{context_id} added."
432
- end
433
-
434
- #
435
- # Adds goal.
436
- #
437
- def add_goal(session, input)
438
- input.strip!
439
-
440
- # Assume that a goal is short, medium or life, and
441
- # don't stick a symbol on it.
442
- level = parse_level(input)
443
- if (level == nil)
444
- level = Toodledo::Goal::SHORT_LEVEL
445
- else
446
- input = clean(LEVEL_REGEXP, input)
447
- input.strip!
448
- end
449
-
450
- goal_id = session.add_goal(input, level)
451
-
452
- print "Goal #{goal_id} added."
453
- end
454
-
455
- def add_folder(session, input)
456
-
457
- title = input.strip
458
-
459
- folder_id = session.add_folder(title)
460
-
461
- print "Folder #{folder_id} added."
462
- end
463
-
464
- #
465
- # Archives a folder.
466
- #
467
- def archive_folder(session, line)
468
-
469
- line.strip!
470
-
471
- folder_id = line
472
- params = { :archived => 1 }
473
- session.edit_folder(folder_id, params)
474
-
475
- print "Folder #{folder_id} archived."
476
- end
477
-
478
- def archive_goal(session, line)
479
- # Not implemented! No way to edit a goal.
480
- end
481
-
482
- #
483
- # Edits a single task. This method allows you to change the symbols on a
484
- # task. Note that you must specify the ID here.
485
- #
486
- # edit *Action !top 12345
487
- def edit_task(session, input)
488
- logger.debug("edit_task: #{input.inspect}")
489
-
490
- context = parse_context(input)
491
- folder = parse_folder(input)
492
- goal = parse_goal(input)
493
- priority = parse_priority(input)
494
- task_id = parse_remainder(input)
495
-
496
- logger.debug("edit_task: task_id = #{task_id}")
497
-
498
- if (task_id == nil)
499
- task_id = ask("Task ID?: ") { |q| q.readline = true }
500
- end
501
-
502
- task_id.strip!
503
-
504
- params = { }
505
-
506
- if (folder != nil)
507
- params.merge!({ :folder => folder })
508
- end
509
-
510
- if (context != nil)
511
- params.merge!({ :context => context })
512
- end
513
-
514
- if (goal != nil)
515
- params.merge!({ :goal => goal })
516
- end
517
-
518
- if (priority != nil)
519
- params.merge!({ :priority => priority })
520
- end
521
-
522
- session.edit_task(task_id, params)
523
-
524
- print "Task #{task_id} edited."
525
- end
526
-
527
- # Masks the task as completed. Uses a task id as argument.
528
- #
529
- # complete 123
530
- #
531
- def complete_task(session, line)
532
- task_id = line
533
-
534
- if (task_id == nil)
535
- task_id = ask("Task ID?: ") { |q| q.readline = true }
536
- end
537
-
538
- task_id.strip!
539
-
540
- params = { :completed => 1 }
541
- if (session.edit_task(task_id, params))
542
- print "Task #{task_id} completed."
543
- else
544
- print "Task #{task_id} could not be completed!"
545
- end
546
- end
547
-
548
-
549
- # Deletes a task, using the task id.
550
- #
551
- # delete 123
552
- #
553
- def delete_task(session, line)
554
- logger.debug("delete_task: #{line.inspect}")
555
- task_id = line
556
-
557
- if (task_id == nil)
558
- task_id = ask("Task ID?: ") { |q| q.readline = true }
559
- end
560
-
561
- task_id.strip!
562
-
563
- if (session.delete_task(task_id))
564
- print "Task #{task_id} deleted."
565
- else
566
- print "Task #{task_id} could not be deleted!"
567
- end
568
- end
569
-
570
- #
571
- # Deletes context.
572
- #
573
- def delete_context(session, line)
574
- logger.debug("delete_context #{line.inspect}")
575
-
576
- id = line
577
-
578
- id.strip!
579
-
580
- if (session.delete_context(id))
581
- print "Context #{id} deleted."
582
- else
583
- print "Context #{id} could not be deleted!"
584
- end
585
- end
586
-
587
- #
588
- # Deletes goal.
589
- #
590
- def delete_goal(session, line)
591
- id = line
592
-
593
- id.strip!
594
-
595
- if (session.delete_goal(id))
596
- print "Goal #{id} deleted."
597
- else
598
- print "Goal #{id} could not be deleted!"
599
- end
600
- end
601
-
602
- #
603
- # Deletes folder
604
- #
605
- def delete_folder(session, line)
606
- id = line
607
-
608
- id.strip!
609
-
610
- if (session.delete_folder(id))
611
- print "Folder #{id} deleted."
612
- else
613
- print "Folder #{id} could not be deleted!"
614
- end
615
- end
616
-
617
- #
618
- # Prints out a single line.
619
- #
620
- def print(line = nil)
621
- if (line == nil)
622
- puts
623
- else
624
- puts line
625
- end
626
- end
627
-
628
- #
629
- # Displays the help message.
630
- #
631
- def help()
632
- print "hotlist Shows the hotlist"
633
- print "folders Shows all folders"
634
- print "goals Shows all goals"
635
- print "contexts Shows all contexts"
636
- print "tasks Shows tasks ('tasks *Action @Home')"
637
- print
638
- print "add Adds task ('add *Action @Home Eat breakfast')"
639
- print " folder Adds a folder ('add folder MyFolder')"
640
- print " context Adds a context ('add context MyContext')"
641
- print " goal Adds a goal ('add goal MyGoal')"
642
- print "edit Edits a task ('edit *Action 1134')"
643
- print "complete Completes a task ('complete 1234')"
644
- print "delete Deletes a task ('delete 1134')"
645
- print " folder Deletes a folder ('delete folder 1')"
646
- print " context Deletes a context ('delete context 2')"
647
- print " goal Deletes a goal ('delete goal 3')"
648
- print
649
- print "archive Archives a folder ('archive 1234')"
650
- print "filter Defines filters ('filter *Action @Someday')"
651
- print "unfilter Removes all filters"
652
- print "filters Displays the list of filters"
653
- print
654
- print "help or ? Displays this help message"
655
- print "quit or exit Leaves the application"
656
- end
657
-
658
- def clean(regexp, input)
659
- return input.sub(regexp, '')
660
- end
661
-
662
- def execute_command(session, input)
663
- case input
664
- when /^help/, /^\s*\?/
665
- help()
666
-
667
- when /^add/
668
- line = clean(/^add/, input)
669
- line.strip!
670
- case line
671
- when /folder/
672
- add_folder(session, clean(/folder/, line))
673
- when /context/
674
- add_context(session, clean(/context/, line))
675
- when /goal/
676
- add_goal(session, clean(/goal/, line))
677
- else
678
- add_task(session, line)
679
- end
680
-
681
- when /^edit/
682
- line = clean(/^edit/, input)
683
- edit_task(session, line)
684
-
685
- when /^delete/
686
- line = clean(/^delete/, input)
687
- line.strip!
688
- case line
689
- when /folder/
690
- delete_folder(session, clean(/folder/, line))
691
- when /context/
692
- delete_context(session, clean(/context/, line))
693
- when /goal/
694
- delete_goal(session, clean(/goal/, line))
695
- else
696
- delete_task(session, line)
697
- end
698
-
699
- when /^archive/
700
- archive_folder(session, clean(/^archive/, input))
701
-
702
- when /^hotlist/
703
- line = clean(/^hotlist/, input)
704
- hotlist(session, line)
705
-
706
- when /^complete/
707
- line = clean(/^complete/, input)
708
- complete_task(session, line)
709
-
710
- when /^tasks/
711
- line = clean(/^(tasks)/, input)
712
- list_tasks(session, line)
713
-
714
- when /^folders/
715
- line = clean(/^folders/, input)
716
- list_folders(session,line)
717
-
718
- when /^goals/
719
- line = clean(/^goals/, input)
720
- list_goals(session,line)
721
-
722
- when /^contexts/
723
- line = clean(/^contexts/, input)
724
- list_contexts(session,line)
725
-
726
- when /^filters/
727
- list_filters()
728
-
729
- when /^filter/
730
- line = clean(/^filter/, input)
731
- set_filter(session, line)
732
-
733
- when /^config/
734
- show_config(session)
735
-
736
- when /^unfilter/
737
- unfilter()
738
-
739
- when /debug/
740
- self.debug = ! self.debug?
741
-
742
- when /^quit/, /^exit/
743
- exit 0
744
- else
745
- print "'#{input}' is not a command: type help for a list"
746
- end
747
- end
748
-
749
- #
750
- # Runs the client main command. This is what gets run from 'toodledo'.
751
- # Ironically doesn't do much except for set up the commands and parse
752
- # arguments from the command line. The MainCommand class does the
753
- # actual command loop.
754
- #
755
- def main()
756
- # Set the configuration from the YAML file.
757
- Toodledo.set_config(@userconfig)
758
-
759
- # Set up the command parser.
760
- graceful_exception = true
761
- partial_cmd_matching = true
762
- cmd = CmdParse::CommandParser.new(graceful_exception, partial_cmd_matching)
763
- cmd.program_name = "toodledo"
764
- cmd.program_version = Toodledo::VERSION
765
-
766
- # Options (must be before help and version are added)
767
- cmd.options = CmdParse::OptionParserWrapper.new do |opt|
768
- opt.separator "Global options:"
769
- opt.on("--debug", "Print debugging information") {|t| self.debug = true }
770
- end
771
-
772
- # this is the default command if we don't receive any options.
773
- cmd.add_command(InteractiveCommand.new(self), true)
774
-
775
- cmd.add_command(StdinCommand.new(self))
776
-
777
- cmd.add_command(AddTaskCommand.new(self))
778
-
779
- cmd.add_command(ListTasksCommand.new(self))
780
- cmd.add_command(ListFoldersCommand.new(self))
781
- cmd.add_command(ListGoalsCommand.new(self))
782
- cmd.add_command(ListContextsCommand.new(self))
783
-
784
- cmd.add_command(EditCommand.new(self))
785
- cmd.add_command(CompleteCommand.new(self))
786
- cmd.add_command(DeleteTaskCommand.new(self))
787
- cmd.add_command(HotlistCommand.new(self))
788
- cmd.add_command(SetupCommand.new(self))
789
-
790
- cmd.add_command(CmdParse::HelpCommand.new)
791
- cmd.add_command(CmdParse::VersionCommand.new)
792
-
793
- cmd.parse
794
-
795
- # Return a good exit status.
796
- return 0
797
- rescue InvalidConfigurationError => e
798
- logger.debug(e)
799
- print "The client is missing (or cannot use) the user id or password it needs to connect."
800
- print "Run 'toodledo setup' and save the file to fix this."
801
- return -1
802
- rescue ServerError => e
803
- print "The server returned a fatal error: #{e.message}"
804
- return -1
805
- end
806
- end #class
807
- end
808
- end
809
-
810
- __END__
811
- #
812
- # The connection to Toodledo.
813
- #
814
- connection:
815
- #
816
- # If you have a Pro account, you can use HTTPS instead of HTTP
817
- url: http://www.toodledo.com/api.php
818
-
819
- #
820
- # If you are logged in to Toodledo, you should be able to see
821
- # your userid at this URL:
822
- #
823
- # http://www.toodledo.com/info/api_doc.php
824
- #
825
- user_id:
826
-
827
- #
828
- # Your password
829
- #
830
- password:
831
-
832
- #
833
- # Uncomment this section if you are working through a proxy
834
- #
835
- #proxy:
836
- # host:
837
- # port:
838
- # user:
839
- # password:
840
- # AUTOCONFIG:
1
+ require 'rubygems'
2
+
3
+ require 'cmdparse'
4
+ require 'fileutils'
5
+ require 'highline/import'
6
+ require 'yaml'
7
+
8
+ require 'toodledo'
9
+ require 'toodledo/command_line/parser_helper'
10
+ require 'toodledo/command_line/base_command'
11
+ require 'toodledo/command_line/interactive_command'
12
+ require 'toodledo/command_line/stdin_command'
13
+ require 'toodledo/command_line/setup_command'
14
+
15
+ # CREATE
16
+ require 'toodledo/command_line/add_command'
17
+
18
+ # READ
19
+ require 'toodledo/command_line/hotlist_command'
20
+ require 'toodledo/command_line/list_tasks_command'
21
+ require 'toodledo/command_line/list_tasks_by_context_command'
22
+ require 'toodledo/command_line/list_folders_command'
23
+ require 'toodledo/command_line/list_contexts_command'
24
+ require 'toodledo/command_line/list_goals_command'
25
+
26
+ # UPDATE
27
+ require 'toodledo/command_line/edit_command'
28
+ require 'toodledo/command_line/complete_command'
29
+
30
+ # DELETE
31
+ require 'toodledo/command_line/delete_command'
32
+
33
+ # FORMATTERS
34
+ require 'toodledo/command_line/task_formatter'
35
+ require 'toodledo/command_line/context_formatter'
36
+ require 'toodledo/command_line/folder_formatter'
37
+ require 'toodledo/command_line/goal_formatter'
38
+
39
+ module Toodledo
40
+ module CommandLine
41
+
42
+ #
43
+ # The toodledo client. This provides a command line based client to the
44
+ # user and gives a good overview of the capabilities of the API as well.
45
+ #
46
+ # Author:: Will Sargent (mailto:will@tersesystems.com)
47
+ # Copyright:: Copyright (c) 2008 Will Sargent
48
+ # License:: GLPL v3
49
+ class Client
50
+
51
+ include Toodledo::CommandLine::ParserHelper
52
+
53
+ HOME = ENV["HOME"] || ENV["HOMEPATH"] || File::expand_path("~")
54
+ TOODLEDO_D = File::join(HOME, ".toodledo")
55
+ CONFIG_F = File::join(TOODLEDO_D, "user-config.yml")
56
+
57
+ # We must use __FILE__ instead of DATA because this is now a library
58
+ # and DATA is relative to $0, not __FILE__.
59
+ CONFIG = File.read(__FILE__).split(/__END__/).last.gsub(/#\{(.*)\}/) { eval $1 }
60
+
61
+ #
62
+ # Creates the client object.
63
+ #
64
+ def initialize(userconfig=CONFIG_F, opts={})
65
+ @filters = {}
66
+ @debug = false
67
+ @logger = Logger.new(STDOUT)
68
+ @logger.level = Logger::FATAL
69
+
70
+ @userconfig = test(?e, userconfig) ? IO::read(userconfig) : CONFIG
71
+ @userconfig = YAML.load(@userconfig).merge(opts)
72
+ @formatters = {
73
+ :task => TaskFormatter.new,
74
+ :goal => GoalFormatter.new,
75
+ :context => ContextFormatter.new,
76
+ :folder => FolderFormatter.new
77
+ }
78
+ end
79
+
80
+ #
81
+ # Returns debugging status.
82
+ #
83
+ def debug?
84
+ return @debug
85
+ end
86
+
87
+ #
88
+ # Sets the debugging on or off.
89
+ #
90
+ def debug=(is_debug)
91
+ @debug = is_debug
92
+ if (@debug == true)
93
+ @logger.level = Logger::DEBUG
94
+ else
95
+ @logger.level = Logger::FATAL
96
+ end
97
+ end
98
+
99
+ #
100
+ # Returns the logger.
101
+ #
102
+ def logger
103
+ return @logger
104
+ end
105
+
106
+ #
107
+ # Invites the user to setup the YAML file.
108
+ #
109
+ def setup
110
+ FileUtils::mkdir_p TOODLEDO_D, :mode => 0700 unless test ?d, TOODLEDO_D
111
+ test ?e, CONFIG_F and FileUtils::mv CONFIG_F, "#{CONFIG_F}.bak"
112
+ config = CONFIG[/\A.*(?=^\# AUTOCONFIG)/m]
113
+ open(CONFIG_F, "w") { |f| f.write config }
114
+
115
+ edit = (ENV["EDITOR"] || ENV["EDIT"] || "vi") + " '#{CONFIG_F}'"
116
+ system edit or puts "edit '#{CONFIG_F}'"
117
+ end
118
+
119
+ #
120
+ # Displays the configuration information that the session is
121
+ # currently using.
122
+ #
123
+ def show_config(session)
124
+ base_url = session.base_url
125
+ user_id = session.user_id
126
+ proxy = session.proxy
127
+
128
+ print "base_url = #{base_url}"
129
+ print "user_id = #{user_id}"
130
+ print "proxy = #{proxy.inspect}"
131
+ end
132
+
133
+ # Sets the context filter. Subsequent calls to show tasks
134
+ # will only show tasks that have this context.
135
+ #
136
+ def set_filter(session, input)
137
+ logger.debug("set_filter(#{input})")
138
+
139
+ input.strip!
140
+
141
+ context = parse_context(input)
142
+ if (context != nil)
143
+ c = session.get_context_by_name(context)
144
+ if (c == nil)
145
+ print "No such context: #{context}"
146
+ return
147
+ end
148
+ @filters[:context] = c
149
+ end
150
+
151
+ goal = parse_goal(input)
152
+ if (goal != nil)
153
+ g = session.get_goal_by_name(goal)
154
+ if (g == nil)
155
+ print "No such goal: #{goal}"
156
+ return
157
+ end
158
+ @filters[:goal] = g
159
+ end
160
+
161
+ folder = parse_folder(input)
162
+ if (folder != nil)
163
+ f = session.get_folder_by_name(folder)
164
+ if (f == nil)
165
+ print "No such folder: #{folder}"
166
+ end
167
+ @filters[:folder] = f
168
+ end
169
+
170
+ priority = parse_priority(input)
171
+ if (priority != nil)
172
+ @filters[:priority] = priority
173
+ end
174
+
175
+ date = parse_date(input)
176
+ if (priority != nil)
177
+ @filters[:duedate] = date
178
+ end
179
+
180
+ tag = parse_tag(input)
181
+ if (priority != nil)
182
+ @filters[:tag] = tag
183
+ end
184
+
185
+ if (logger)
186
+ logger.debug("@filters = #{@filters.inspect}")
187
+ end
188
+ end
189
+
190
+ #
191
+ # Shows all the filters.
192
+ #
193
+ def list_filters()
194
+ if (@filters == nil || @filters.empty?)
195
+ print "No filters."
196
+ return
197
+ end
198
+
199
+ @filters.each do |k, v|
200
+ if (v.respond_to? :name)
201
+ name = v.name
202
+ else
203
+ name = v
204
+ end
205
+ print "#{k}: #{name}\n"
206
+ end
207
+ end
208
+
209
+ #
210
+ # Clears all the filters.
211
+ #
212
+ def unfilter()
213
+ @filters = {}
214
+ print "Filters cleared.\n"
215
+ end
216
+
217
+ #
218
+ # Displays the 'hotlist' of tasks. This shows all the uncompleted items with
219
+ # priority set to 3 or 2. There's no facility in the API for this, so we have
220
+ # to cheat a bit.
221
+ #
222
+ # It may be worthwhile to allow the ability to tweak what constitutes a 'hotlist'
223
+ # but that'll come by demand. Or patches. Fully documented patches, mmmm.
224
+ #
225
+ def hotlist(session, input)
226
+ logger.debug("hotlist: #{input}")
227
+
228
+ # See if there's input following the command.
229
+ context = parse_context(input)
230
+ folder = parse_folder(input)
231
+ goal = parse_goal(input)
232
+ priority = parse_priority(input)
233
+ date = parse_date(input)
234
+ tag = parse_tag(input)
235
+
236
+ params = { :notcomp => true }
237
+
238
+ # If there are, they override what we have set.
239
+ if (folder != nil)
240
+ params.merge!({ :folder => folder })
241
+ end
242
+
243
+ if (context != nil)
244
+ params.merge!({ :context => context })
245
+ end
246
+
247
+ if (goal != nil)
248
+ params.merge!({ :goal => goal })
249
+ end
250
+
251
+ if (priority != nil)
252
+ params.merge!({ :priority => priority })
253
+ end
254
+
255
+ if (date != nil)
256
+ params.merge!({ :duedate => date })
257
+ end
258
+
259
+ if (tag != nil)
260
+ params.merge!({ :tag => tag })
261
+ end
262
+
263
+ tasks = session.get_tasks(params)
264
+
265
+ # Highest priority first
266
+ tasks.sort! do |a, b|
267
+ b.priority <=> a.priority
268
+ end
269
+
270
+ # filter on our end.
271
+ # Surprisingly, we can't search for "greater than 0 priority" with the API.
272
+ not_important = Priority::MEDIUM
273
+
274
+ for task in tasks
275
+ if (task.priority > not_important)
276
+ print @formatters[:task].format(task)
277
+ end
278
+ end
279
+ end
280
+
281
+ #
282
+ # Lists tasks (subject to any filters that may be present).
283
+ #
284
+ def list_tasks(session, input)
285
+ logger.debug("list_tasks(#{input})")
286
+
287
+ params = { :notcomp => true }
288
+
289
+ params.merge!(@filters)
290
+
291
+ # See if there's input following the 'tasks' command.
292
+ # TODO This is the same code as in hotlist. It's also repetitive. Refactor me!
293
+ context = parse_context(input)
294
+ folder = parse_folder(input)
295
+ goal = parse_goal(input)
296
+ priority = parse_priority(input)
297
+ date = parse_date(input)
298
+ tag = parse_tag(input)
299
+
300
+ # If there are, they override what we have set.
301
+ if (folder != nil)
302
+ params.merge!({ :folder => folder })
303
+ end
304
+
305
+ if (context != nil)
306
+ params.merge!({ :context => context })
307
+ end
308
+
309
+ if (goal != nil)
310
+ params.merge!({ :goal => goal })
311
+ end
312
+
313
+ if (priority != nil)
314
+ params.merge!({ :priority => priority })
315
+ end
316
+
317
+ if (date != nil)
318
+ params.merge!({ :duedate => date })
319
+ end
320
+
321
+ if (tag != nil)
322
+ params.merge!({ :tag => tag })
323
+ end
324
+
325
+ tasks = session.get_tasks(params)
326
+
327
+ # Highest priority first
328
+ tasks.sort! do |a, b|
329
+ b.priority <=> a.priority
330
+ end
331
+
332
+ for task in tasks
333
+ print @formatters[:task].format(task)
334
+ end
335
+ end
336
+
337
+ #
338
+ # Prints all active tasks nested by context.
339
+ #
340
+ def list_tasks_by_context(session, line)
341
+ folder = parse_folder(line)
342
+
343
+ session.get_contexts().each do |context|
344
+ criteria = { :folder => folder, :context => context, :notcomp => true }
345
+ tasks = session.get_tasks(criteria)
346
+ print "#{context.name}" if (! tasks.empty?)
347
+ tasks.each { |task| print " " + @formatters[:task].format(task) }
348
+ end
349
+ end
350
+
351
+ #
352
+ # Lists the goals. Takes an optional argument of
353
+ # 'short', 'medium' or 'life'.
354
+ #
355
+ def list_goals(session, input)
356
+
357
+ input.strip!
358
+ input.downcase!
359
+
360
+ goals = session.get_goals()
361
+
362
+ goals.sort! do |a, b|
363
+ a.level <=> b.level
364
+ end
365
+
366
+ level_filter = nil
367
+ case input
368
+ when 'short'
369
+ level_filter = Goal::SHORT_LEVEL
370
+ when 'medium'
371
+ level_filter = Goal::MEDIUM_LEVEL
372
+ when 'life'
373
+ level_filter = Goal::LIFE_LEVEL
374
+ end
375
+
376
+ for goal in goals
377
+ if (level_filter != nil && goal.level != level_filter)
378
+ next # skip this goal if it doesn't meet the filter
379
+ end
380
+ print @formatters[:goal].format(goal)
381
+ end
382
+ end
383
+
384
+ #
385
+ # Lists the contexts.
386
+ #
387
+ def list_contexts(session, input)
388
+ params = { }
389
+
390
+ contexts = session.get_contexts()
391
+
392
+ for context in contexts
393
+ print @formatters[:context].format(context)
394
+ end
395
+ end
396
+
397
+ #
398
+ # Lists the folders.
399
+ #
400
+ def list_folders(session, input)
401
+ params = { }
402
+
403
+ folders = session.get_folders()
404
+
405
+ for folder in folders
406
+ print @formatters[:folder].format(folder)
407
+ end
408
+ end
409
+
410
+ # Adds a single task, using toodledo symbols. This is the most general way to
411
+ # add a task right now. If you have symbols which have spaces, then you must
412
+ # encase them in square brackets.
413
+ #
414
+ # The order of symbols does not matter, but the title must be the last thing
415
+ # on the line.
416
+ #
417
+ # add @[Deep Space] !top *Action ^[For Great Justice] Take off every Zig
418
+ #
419
+ def add_task(session, line)
420
+ # TODO Yet again, essentially the same code as list and hotlist; Refactor
421
+ context = parse_context(line)
422
+ folder = parse_folder(line)
423
+ goal = parse_goal(line)
424
+ priority = parse_priority(line)
425
+ date = parse_date(line)
426
+ tag = parse_tag(line)
427
+ title = parse_remainder(line)
428
+
429
+ params = {}
430
+ if (priority != nil)
431
+ params.merge!({ :priority => priority })
432
+ end
433
+
434
+ if (folder != nil)
435
+ params.merge!({ :folder => folder })
436
+ end
437
+
438
+ if (context != nil)
439
+ params.merge!({ :context => context })
440
+ end
441
+
442
+ if (goal != nil)
443
+ params.merge!({ :goal => goal })
444
+ end
445
+
446
+ if (date != nil)
447
+ params.merge!({ :duedate => date })
448
+ end
449
+
450
+ if (tag != nil)
451
+ params.merge!({ :tag => tag })
452
+ end
453
+
454
+ # If we got nothing but 'add' then ask for it explicitly.
455
+ if (title == nil)
456
+ title = ask("Task name: ") { |q| q.readline = true }
457
+ end
458
+
459
+ task_id = session.add_task(title, params)
460
+
461
+ print "Task #{task_id} added."
462
+ end
463
+
464
+ #
465
+ # Adds context.
466
+ #
467
+ def add_context(session, input)
468
+
469
+ title = input.strip
470
+
471
+ context_id = session.add_context(title)
472
+
473
+ print "Context #{context_id} added."
474
+ end
475
+
476
+ #
477
+ # Adds goal.
478
+ #
479
+ def add_goal(session, input)
480
+ input.strip!
481
+
482
+ # Assume that a goal is short, medium or life, and
483
+ # don't stick a symbol on it.
484
+ level = parse_level(input)
485
+ if (level == nil)
486
+ level = Toodledo::Goal::SHORT_LEVEL
487
+ else
488
+ input = clean(LEVEL_REGEXP, input)
489
+ input.strip!
490
+ end
491
+
492
+ goal_id = session.add_goal(input, level)
493
+
494
+ print "Goal #{goal_id} added."
495
+ end
496
+
497
+ def add_folder(session, input)
498
+
499
+ title = input.strip
500
+
501
+ folder_id = session.add_folder(title)
502
+
503
+ print "Folder #{folder_id} added."
504
+ end
505
+
506
+ #
507
+ # Archives a folder.
508
+ #
509
+ def archive_folder(session, line)
510
+
511
+ line.strip!
512
+
513
+ folder_id = line
514
+ params = { :archived => 1 }
515
+ session.edit_folder(folder_id, params)
516
+
517
+ print "Folder #{folder_id} archived."
518
+ end
519
+
520
+ def archive_goal(session, line)
521
+ # Not implemented! No way to edit a goal.
522
+ end
523
+
524
+ #
525
+ # Edits a single task. This method allows you to change the symbols on a
526
+ # task. Note that you must specify the ID here.
527
+ #
528
+ # edit *Action !top 12345
529
+ def edit_task(session, input)
530
+ logger.debug("edit_task: #{input.inspect}")
531
+
532
+ # TODO And again... Refactor
533
+ context = parse_context(input)
534
+ folder = parse_folder(input)
535
+ goal = parse_goal(input)
536
+ priority = parse_priority(input)
537
+ date = parse_date(input)
538
+ tag = parse_tag(input)
539
+ task_id = parse_remainder(input)
540
+
541
+ logger.debug("edit_task: task_id = #{task_id}")
542
+
543
+ if (task_id == nil)
544
+ task_id = ask("Task ID?: ") { |q| q.readline = true }
545
+ end
546
+
547
+ task_id.strip!
548
+
549
+ params = { }
550
+
551
+ if (folder != nil)
552
+ params.merge!({ :folder => folder })
553
+ end
554
+
555
+ if (context != nil)
556
+ params.merge!({ :context => context })
557
+ end
558
+
559
+ if (goal != nil)
560
+ params.merge!({ :goal => goal })
561
+ end
562
+
563
+ if (priority != nil)
564
+ params.merge!({ :priority => priority })
565
+ end
566
+
567
+ if (date != nil)
568
+ params.merge!({ :duedate => date })
569
+ end
570
+
571
+ if (tag != nil)
572
+ params.merge!({ :tag => tag })
573
+ end
574
+
575
+ session.edit_task(task_id, params)
576
+
577
+ print "Task #{task_id} edited."
578
+ end
579
+
580
+ # Masks the task as completed. Uses a task id as argument.
581
+ #
582
+ # complete 123
583
+ #
584
+ def complete_task(session, line)
585
+ task_id = line
586
+
587
+ if (task_id == nil)
588
+ task_id = ask("Task ID?: ") { |q| q.readline = true }
589
+ end
590
+
591
+ task_id.strip!
592
+
593
+ params = { :completed => 1 }
594
+ if (session.edit_task(task_id, params))
595
+ print "Task #{task_id} completed."
596
+ else
597
+ print "Task #{task_id} could not be completed!"
598
+ end
599
+ end
600
+
601
+
602
+ # Deletes a task, using the task id.
603
+ #
604
+ # delete 123
605
+ #
606
+ def delete_task(session, line)
607
+ logger.debug("delete_task: #{line.inspect}")
608
+ task_id = line
609
+
610
+ if (task_id == nil)
611
+ task_id = ask("Task ID?: ") { |q| q.readline = true }
612
+ end
613
+
614
+ task_id.strip!
615
+
616
+ if (session.delete_task(task_id))
617
+ print "Task #{task_id} deleted."
618
+ else
619
+ print "Task #{task_id} could not be deleted!"
620
+ end
621
+ end
622
+
623
+ #
624
+ # Deletes context.
625
+ #
626
+ def delete_context(session, line)
627
+ logger.debug("delete_context #{line.inspect}")
628
+
629
+ id = line
630
+
631
+ id.strip!
632
+
633
+ if (session.delete_context(id))
634
+ print "Context #{id} deleted."
635
+ else
636
+ print "Context #{id} could not be deleted!"
637
+ end
638
+ end
639
+
640
+ #
641
+ # Deletes goal.
642
+ #
643
+ def delete_goal(session, line)
644
+ id = line
645
+
646
+ id.strip!
647
+
648
+ if (session.delete_goal(id))
649
+ print "Goal #{id} deleted."
650
+ else
651
+ print "Goal #{id} could not be deleted!"
652
+ end
653
+ end
654
+
655
+ #
656
+ # Deletes folder
657
+ #
658
+ def delete_folder(session, line)
659
+ id = line
660
+
661
+ id.strip!
662
+
663
+ if (session.delete_folder(id))
664
+ print "Folder #{id} deleted."
665
+ else
666
+ print "Folder #{id} could not be deleted!"
667
+ end
668
+ end
669
+
670
+ #
671
+ # Prints out a single line.
672
+ #
673
+ def print(line = nil)
674
+ if (line == nil)
675
+ puts
676
+ else
677
+ puts line
678
+ end
679
+ end
680
+
681
+ #
682
+ # Displays the help message.
683
+ #
684
+ def help()
685
+ print "hotlist Shows the hotlist"
686
+ print "folders Shows all folders"
687
+ print "goals Shows all goals"
688
+ print "contexts Shows all contexts"
689
+ print "tasks Shows tasks ('tasks *Action @Home')"
690
+ print
691
+ print "add Adds task ('add *Action @Home Eat breakfast')"
692
+ print " folder Adds a folder ('add folder MyFolder')"
693
+ print " context Adds a context ('add context MyContext')"
694
+ print " goal Adds a goal ('add goal MyGoal')"
695
+ print "edit Edits a task ('edit *Action 1134')"
696
+ print "complete Completes a task ('complete 1234')"
697
+ print "delete Deletes a task ('delete 1134')"
698
+ print " folder Deletes a folder ('delete folder 1')"
699
+ print " context Deletes a context ('delete context 2')"
700
+ print " goal Deletes a goal ('delete goal 3')"
701
+ print
702
+ print "archive Archives a folder ('archive 1234')"
703
+ print "filter Defines filters ('filter *Action @Someday')"
704
+ print "unfilter Removes all filters"
705
+ print "filters Displays the list of filters"
706
+ print
707
+ print "help or ? Displays this help message"
708
+ print "quit or exit Leaves the application"
709
+ end
710
+
711
+ def clean(regexp, input)
712
+ return input.sub(regexp, '')
713
+ end
714
+
715
+ def execute_command(session, input)
716
+ case input
717
+ when /^help/, /^\s*\?/
718
+ help()
719
+
720
+ when /^add/
721
+ line = clean(/^add/, input)
722
+ line.strip!
723
+ case line
724
+ when /folder/
725
+ add_folder(session, clean(/folder/, line))
726
+ when /context/
727
+ add_context(session, clean(/context/, line))
728
+ when /goal/
729
+ add_goal(session, clean(/goal/, line))
730
+ else
731
+ add_task(session, line)
732
+ end
733
+
734
+ when /^edit/
735
+ line = clean(/^edit/, input)
736
+ edit_task(session, line)
737
+
738
+ when /^delete/
739
+ line = clean(/^delete/, input)
740
+ line.strip!
741
+ case line
742
+ when /folder/
743
+ delete_folder(session, clean(/folder/, line))
744
+ when /context/
745
+ delete_context(session, clean(/context/, line))
746
+ when /goal/
747
+ delete_goal(session, clean(/goal/, line))
748
+ else
749
+ delete_task(session, line)
750
+ end
751
+
752
+ when /^archive/
753
+ archive_folder(session, clean(/^archive/, input))
754
+
755
+ when /^hotlist/
756
+ line = clean(/^hotlist/, input)
757
+ hotlist(session, line)
758
+
759
+ when /^complete/
760
+ line = clean(/^complete/, input)
761
+ complete_task(session, line)
762
+
763
+ when /^tasks/
764
+ line = clean(/^(tasks)/, input)
765
+ list_tasks(session, line)
766
+
767
+ when /^folders/
768
+ line = clean(/^folders/, input)
769
+ list_folders(session,line)
770
+
771
+ when /^goals/
772
+ line = clean(/^goals/, input)
773
+ list_goals(session,line)
774
+
775
+ when /^contexts/
776
+ line = clean(/^contexts/, input)
777
+ list_contexts(session,line)
778
+
779
+ when /^filters/
780
+ list_filters()
781
+
782
+ when /^filter/
783
+ line = clean(/^filter/, input)
784
+ set_filter(session, line)
785
+
786
+ when /^config/
787
+ show_config(session)
788
+
789
+ when /^unfilter/
790
+ unfilter()
791
+
792
+ when /debug/
793
+ self.debug = ! self.debug?
794
+
795
+ when /^quit/, /^exit/
796
+ exit 0
797
+ else
798
+ print "'#{input}' is not a command: type help for a list"
799
+ end
800
+ end
801
+
802
+ #
803
+ # Runs the client main command. This is what gets run from 'toodledo'.
804
+ # Ironically doesn't do much except for set up the commands and parse
805
+ # arguments from the command line. The MainCommand class does the
806
+ # actual command loop.
807
+ #
808
+ def main()
809
+ # Set the configuration from the YAML file.
810
+ Toodledo.set_config(@userconfig)
811
+
812
+ # Set up the command parser.
813
+ graceful_exception = true
814
+ partial_cmd_matching = true
815
+ cmd = CmdParse::CommandParser.new(graceful_exception, partial_cmd_matching)
816
+ cmd.program_name = "toodledo"
817
+ cmd.program_version = Toodledo::VERSION
818
+
819
+ # Options (must be before help and version are added)
820
+ cmd.options = CmdParse::OptionParserWrapper.new do |opt|
821
+ opt.separator "Global options:"
822
+ opt.on("--debug", "Print debugging information") {|t| self.debug = true }
823
+ end
824
+
825
+ # this is the default command if we don't receive any options.
826
+ cmd.add_command(InteractiveCommand.new(self), true)
827
+
828
+ cmd.add_command(StdinCommand.new(self))
829
+
830
+ cmd.add_command(AddTaskCommand.new(self))
831
+
832
+ cmd.add_command(ListTasksCommand.new(self))
833
+ cmd.add_command(ListFoldersCommand.new(self))
834
+ cmd.add_command(ListGoalsCommand.new(self))
835
+ cmd.add_command(ListContextsCommand.new(self))
836
+
837
+ cmd.add_command(EditCommand.new(self))
838
+ cmd.add_command(CompleteCommand.new(self))
839
+ cmd.add_command(DeleteTaskCommand.new(self))
840
+ cmd.add_command(HotlistCommand.new(self))
841
+ cmd.add_command(SetupCommand.new(self))
842
+
843
+ cmd.add_command(CmdParse::HelpCommand.new)
844
+ cmd.add_command(CmdParse::VersionCommand.new)
845
+
846
+ cmd.parse
847
+
848
+ # Return a good exit status.
849
+ return 0
850
+ rescue InvalidConfigurationError => e
851
+ logger.debug(e)
852
+ print "The client is missing (or cannot use) the user id or password it needs to connect."
853
+ print "Run 'toodledo setup' and save the file to fix this."
854
+ return -1
855
+ rescue ServerError => e
856
+ print "The server returned a fatal error: #{e.message}"
857
+ return -1
858
+ end
859
+ end #class
860
+ end
861
+ end
862
+
863
+ __END__
864
+ #
865
+ # The connection to Toodledo.
866
+ #
867
+ connection:
868
+ #
869
+ # If you have a Pro account, you can use HTTPS instead of HTTP
870
+ url: http://www.toodledo.com/api.php
871
+
872
+ #
873
+ # If you are logged in to Toodledo, you should be able to see
874
+ # your userid at this URL:
875
+ #
876
+ # http://www.toodledo.com/info/api_doc.php
877
+ #
878
+ user_id:
879
+
880
+ #
881
+ # Your password
882
+ #
883
+ password:
884
+
885
+ #
886
+ # Uncomment this section if you are working through a proxy
887
+ #
888
+ #proxy:
889
+ # host:
890
+ # port:
891
+ # user:
892
+ # password:
893
+ # AUTOCONFIG: