toodledo 1.3.5 → 1.3.8
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.
- data/History.txt +14 -0
- data/{README.txt → README.md} +61 -48
- data/Rakefile +60 -18
- data/lib/toodledo.rb +5 -4
- data/lib/toodledo/command_line/client.rb +893 -840
- data/lib/toodledo/command_line/parser_helper.rb +24 -1
- data/lib/toodledo/command_line/task_formatter.rb +174 -150
- data/lib/toodledo/priority.rb +1 -0
- data/lib/toodledo/repeat.rb +2 -0
- data/lib/toodledo/server_error.rb +17 -0
- data/lib/toodledo/session.rb +53 -18
- data/lib/toodledo/status.rb +37 -35
- data/lib/toodledo/task.rb +8 -3
- data/lib/toodledo/version.rb +11 -0
- data/test/client_test.rb +60 -12
- data/test/helper.rb +11 -0
- data/test/parser_helper_test.rb +36 -6
- data/test/session_test.rb +221 -39
- data/test/toodledo_functional_test.rb +33 -7
- data/toodledo.gemspec +97 -0
- metadata +35 -60
- data/Manifest.txt +0 -41
- data/lib/toodledo/item_not_found_error.rb +0 -8
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.
|
data/{README.txt → README.md}
RENAMED
@@ -1,9 +1,9 @@
|
|
1
|
-
|
1
|
+
# toodledo
|
2
2
|
|
3
|
-
*
|
4
|
-
*
|
3
|
+
* https://github.com/wsargent/toodledo
|
4
|
+
* will.sargent@gmail.com
|
5
5
|
|
6
|
-
|
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
|
-
|
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
|
-
|
28
|
+
## SYNOPSIS
|
29
29
|
|
30
|
-
|
30
|
+
### SETUP
|
31
31
|
|
32
|
-
|
32
|
+
You will need an account on Toodledo. Once you have that and you're logged in, go to:
|
33
33
|
|
34
|
-
|
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,
|
39
|
-
'sudo gem install toodledo' depending on your platform.
|
38
|
+
Then, type
|
40
39
|
|
41
|
-
|
42
|
-
|
40
|
+
gem install toodledo
|
41
|
+
toodledo setup
|
43
42
|
|
44
|
-
|
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
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
77
|
+
toodledo tasks '@Office *Action'
|
69
78
|
|
70
79
|
Now let's add a task with several symbols:
|
71
80
|
|
72
|
-
|
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
|
-
|
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
|
-
|
81
|
-
|
93
|
+
toodledo complete 15934131
|
94
|
+
toodledo delete 15934131
|
82
95
|
|
83
|
-
|
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
|
-
|
89
|
-
|
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
|
-
|
98
|
-
|
110
|
+
filter @Office *Action
|
111
|
+
tasks
|
99
112
|
|
100
113
|
Then it produces the same results as:
|
101
114
|
|
102
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
129
|
+
require 'rubygems'
|
130
|
+
require 'toodledo'
|
131
|
+
session = Session.new(userid, password)
|
132
|
+
session.connect()
|
120
133
|
|
121
|
-
|
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
|
-
|
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
|
-
|
149
|
+
## LICENSE:
|
137
150
|
GPL v3
|
data/Rakefile
CHANGED
@@ -1,24 +1,66 @@
|
|
1
1
|
# -*- ruby -*-
|
2
2
|
|
3
3
|
require 'rubygems'
|
4
|
-
require '
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
8
|
-
|
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
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
end
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
print "
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
#
|
210
|
-
#
|
211
|
-
#
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
#
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
if (
|
240
|
-
params.merge!({ :
|
241
|
-
end
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
#
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
end
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
#
|
354
|
-
#
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
#
|
385
|
-
#
|
386
|
-
#
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
title =
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
end
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
print "
|
462
|
-
end
|
463
|
-
|
464
|
-
#
|
465
|
-
#
|
466
|
-
#
|
467
|
-
def
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
#
|
528
|
-
#
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
end
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
if (
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
#
|
603
|
-
#
|
604
|
-
#
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
if (
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
def
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
line
|
724
|
-
|
725
|
-
|
726
|
-
when
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
when
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
#
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
#
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
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:
|