doing 0.1.9 → 0.2.2.pre

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b8e66a72fd2de31862fdc6423f9771b4a41ac012
4
- data.tar.gz: 1bc3c5ab4be017092e802dba802c1a7c1d1721db
3
+ metadata.gz: a4b26436b15b40cf894c0d3b15dfaf367e299f95
4
+ data.tar.gz: d13cf54255ec6db1b5d24c7fec73455cb6e642f5
5
5
  SHA512:
6
- metadata.gz: e1b2d0a13c32cb31f971f85afe278138bd7ceffae608f801b3640d23a397474efca6f812830d4089b8220f149f2dc688ef80b680626caac6b98529265f68fe17
7
- data.tar.gz: 4536f5c497ef9c2510e81faa08b83786f140d85695ceb757e5233988feaa0ea26fb7f72ff83da9b6d042b6bc2df1b68944b381b1e45708c3a6e0294f0b633301
6
+ metadata.gz: 5fdaadd019f03f7a93e1fc211c20ed017502d16eade73f7f6eb616e123f14988422296c17b2a759643efcb7abbcc721a451d47fe1ede0c30e8f719c1cafc94e5
7
+ data.tar.gz: 75907e9acf02fadfa7416550cca0bbfe487452a660a8c494c4228b9e87f47b0306e79188718395da56e48e6a6c15c631e7bb34590655ec830ca59115fdf89b7e
data/README.md CHANGED
@@ -20,6 +20,8 @@ _Side note:_ I actually use the library behind this utility as part of another s
20
20
 
21
21
  Only use `sudo` if your environment requires it. If you're using the system Ruby on a Mac, for example, it will likely be necessary. If `gem install doing` fails, then run `sudo gem install doing` and provide your administrator password.
22
22
 
23
+ Run `doing config` to open your `~/.doingrc` file in the editor defined in the $EDITOR environment variable. Set up your `doing_file` right away (where you want entries to be stored), and cover the rest after you've read the docs.
24
+
23
25
  See the [support](#support) section below for troubleshooting details.
24
26
 
25
27
  ## The "doing" file
@@ -28,7 +30,9 @@ The file that stores all of your entries is generated the first time you add an
28
30
 
29
31
  The format of the "doing" file is TaskPaper-compatible. You can edit it by hand at any time (in TaskPaper or any text editor), but it uses a specific format for parsing, so be sure to maintain the dates and pipe characters.
30
32
 
31
- Notes are anything in the list without a leading hyphen and date. They belong to the entry directly before them, and they should be indented one level beyond the parent item. The `now` and `later` commands don't currently make it possible to add notes at the time of entry creation, but I have scripts that do it and will incorporate them soon.
33
+ Notes are anything in the list without a leading hyphen and date. They belong to the entry directly before them, and they should be indented one level beyond the parent item. When using the `now` and `later` commands on the command line, you can start the entry with a quote and hit return, then type the note and close the quote. Anything after the first line will be turned into a TaskPaper-compatible note for the task and can be displayed in templates using `%note`.
34
+
35
+ Notes can be prevented from ever appearing in output with the global option `--no-notes`: `doing --no-notes show all`.
32
36
 
33
37
  ## Configuration
34
38
 
@@ -39,6 +43,14 @@ A basic configuration looks like this:
39
43
  current_section: Currently
40
44
  default_template: '%date: %title%note'
41
45
  default_date_format: '%Y-%m-%d %H:%M'
46
+ views:
47
+ color:
48
+ date_format: '%F %_I:%M%P'
49
+ section: Currently
50
+ count: 10
51
+ wrap_width: 0
52
+ template: '%boldblack%date %boldgreen| %boldwhite%title%default%note'
53
+ order: desc
42
54
  templates:
43
55
  default:
44
56
  date_format: '%Y-%m-%d %H:%M'
@@ -79,7 +91,7 @@ You can rename the section that holds your current tasks. By default, this is "C
79
91
 
80
92
  The setting `editor_app` only applies to Mac OS X users. It's the default application that the command `doing open` will open your WWID file in. If this is blank, it will be opened by whatever the system default is, or you can use `-a app_name` or `-b bundle_id` to override.
81
93
 
82
- In the case of the `doing now -e` command, your $EDITOR environment variable will be used to complete the entry text and notes. Set it in your .bash_profile or whatever is appropriate for your system:
94
+ In the case of the `doing now -e` command, your $EDITOR environment variable will be used to complete the entry text and notes. Set it in your `~/.bash_profile` or whatever is appropriate for your system:
83
95
 
84
96
  export EDITOR="mate -w"
85
97
 
@@ -184,7 +196,10 @@ You can create your own "views" in the `~/.doingrc` file and view them with `doi
184
196
  count: 5
185
197
  wrap_width: 0
186
198
  date_format: '%F %_I:%M%P'
187
- template: '%date | %title%note'
199
+ template: '%date | %title%note'
200
+ order: asc
201
+ tags: done finished cancelled
202
+ tags_bool: ANY
188
203
 
189
204
  You can add additional custom views, just nest them under the "views" key (indented two spaces from the edge). Multiple views would look like this:
190
205
 
@@ -202,9 +217,13 @@ You can add additional custom views, just nest them under the "views" key (inden
202
217
  date_format: '%F %_I:%M%P'
203
218
  template: '%date | %title%note'
204
219
 
205
- The "section" key is the default section to pull entries from. Count and section can be overridden at runtime with the `-c` and `-s` flags.
220
+ The "section" key is the default section to pull entries from. Count and section can be overridden at runtime with the `-c` and `-s` flags. Setting `section` to All will combine all sections in the output.
221
+
222
+ You can add new sections with `doing add_section section_name`. You can also create them on the fly by using the `-s section_name` flag when running `doing now`. For example, `doing now -s Misc just a random side note` would create the "just a random side note" entry in a new section called "Misc," if Misc didn't already exist.
206
223
 
207
- You can add new sections with `done add_section section_name`. You can also create them on the fly by using the `-s section_name` flag when running `doing now`. For example, `doing now -s Misc just a random side note` would create the "just a random side note" entry in a new section called "Misc."
224
+ The `tags` and `tags_bool` keys allow you to specify tags that the view is filtered by. You can list multiple tags separated by spaces, and then use `tags_bool` to specify "ALL," "ANY," or "NONE" to determine how it handles the multiple tags.
225
+
226
+ The `order` key defines the sort order of the output. This is applied _after_ the tasks are retrieved and cut off at the maximum number specified in `count`.
208
227
 
209
228
  Regarding colors, you can use them to create very nice displays if you're outputting to a color terminal. Example:
210
229
 
@@ -231,13 +250,35 @@ Outputs:
231
250
 
232
251
  ### Commands:
233
252
 
234
- help - Shows a list of commands or help for one command (`doing help now`)
253
+ help - Shows a list of commands and global options
254
+ help [command] - Shows help for any command (`doing help now`)
235
255
 
236
256
  #### Adding entries:
237
257
 
238
258
  now - Add an entry
239
259
  later - Add an item to the Later section
240
- done - Add an entry tagged with @done(YYYY-mm-dd hh:mm)
260
+ done - Add a completed item with @done(date). No argument finishes last entry.
261
+
262
+ The `doing now` command can accept `-s section_name` to send the new entry straight to a non-default section.
263
+
264
+ `doing done` is used to add an entry that you've already completed. Like `now`, you can specify a section with `-s section_name`. You can also skip straight to Archive with `-a`.
265
+
266
+ You can also backdate entries using natural language with `--back 15m` or `--back "3/15 3pm"`. That will modify the timestamp of the entry. When used with `doing done`, this allows time intervals to be accurately counted when entering items after the fact.
267
+
268
+ All of these commands accept a `-e` argument. This opens your command line editor as defined in the environment variable `$EDITOR`. Add your entry, save the temp file and close it, and the new entry will be added. Anything after the first line is included as a note on the entry.
269
+
270
+ #### Modifying entries:
271
+
272
+ finish - Mark last X entries as @done
273
+ tag - Tag last entry
274
+
275
+ `doing finish` by itself is the same as `doing done` by itself. It adds `@done(timestamp)` to the last entry. It also accepts a numeric argument to complete X number of tasks back in history. Add `-a` to also archive the affected entries.
276
+
277
+ `tag` adds one or more tags to the last entry, or specify a count with `-c X`. Tags are specified as basic arguments, separated by spaces. For example:
278
+
279
+ doing tag -c 3 client cancelled
280
+
281
+ ... will mark the last three entries as "@client @cancelled." Add `-r` as a switch to remove the listed tags instead.
241
282
 
242
283
  #### Displaying entries:
243
284
 
@@ -246,18 +287,41 @@ Outputs:
246
287
  today - List entries from today
247
288
  last - Show the last entry
248
289
 
290
+ `doing show` on its own will list all entries in the "Currently" section. Add a section name as an argument to display that section instead. Use "all" to display all entries from all sections.
291
+
292
+ You can filter the `show` command by tags. Simply list them after the section name (or "all"). The boolean defaults to "ANY," meaning any entry that contains any of the listed tags will be shown. You can use `-b ALL` or `-b NONE` to change the filtering behavior: `doing show all done cancelled -b NONE` will show all tasks from all sections that do not have either "@done" or "@cancelled" tags.
293
+
294
+ Use `-c X` to limit the displayed results. Combine it with `-a newest` or `-a oldest` to choose which chronological end it trims from. You can also set the sort order of the output with `-s asc` or `-s desc`.
295
+
296
+ If you have a use for it, you can use `--csv` on the show or view commands to output the results as a comma-separated CSV to STDOUT. Redirect to a file to save it: `doing show all done --csv > ~/Desktop/done.csv`.
297
+
298
+ #### Views
299
+
300
+ view - Display a user-created view
301
+ views - List available custom views
302
+
303
+ Display any of the custom views you make in `~/.doingrc` with the `view` command. Use `doing views` to get a list of available views. Any time a section or view is specified on the command line, fuzzy matching will be used to find the closest match. Thus, `lat` will match `Later`, etc..
304
+
249
305
  #### Sections
250
306
 
251
- sections - List sections
252
- choose - Select a section to display from a menu
307
+ sections - List sections
308
+ choose - Select a section to display from a menu
309
+ add_section - Add a new section to the "doing" file
253
310
 
254
311
  #### Utilities
255
312
 
256
313
  archive - Move all but the most recent 5 entries to the Archive section
314
+ open - Open the "doing" file in an editor (OS X)
257
315
  config - Edit the default configuration
258
316
 
259
317
  ---
260
318
 
319
+ ## Extras
320
+
321
+ ### Bash completion
322
+
323
+ Add basic command line completion for Bash with [this gist](https://gist.github.com/fcrespo82/9609318).
324
+
261
325
  ## Troubleshooting
262
326
 
263
327
  ### Errors after "Successfully installed..."
data/bin/doing CHANGED
@@ -31,24 +31,37 @@ command :now do |c|
31
31
  c.desc 'Section'
32
32
  c.arg_name 'section_name'
33
33
  c.default_value wwid.current_section
34
- c.flag [:s,:section]
34
+ c.flag [:s,:section], :default_value => wwid.current_section
35
35
 
36
36
  c.desc "Edit entry with #{ENV['EDITOR']}"
37
37
  c.switch [:e,:editor]
38
38
 
39
+ c.desc 'Backdate to "date_string" (natural language)'
40
+ c.flag [:back]
41
+
39
42
  # c.desc "Edit entry with specified app"
40
43
  # c.arg_name 'editor_app'
41
44
  # c.default_value wwid.config.has_key?('editor_app') && wwid.config['editor_app'] ? wwid.config['editor_app'] : false
42
45
  # c.flag [:a,:app]
43
46
 
44
47
  c.action do |global_options,options,args|
48
+ if options[:back]
49
+ date = wwid.chronify(options[:back])
50
+ raise "Unable to parse date string" if date.nil?
51
+ else
52
+ date = Time.now
53
+ end
54
+
55
+ section = wwid.guess_section(options[:s]) || options[:s].cap_first
56
+
45
57
  if options[:e] || (args.length == 0 && STDIN.stat.size == 0)
58
+ raise "No EDITOR variable defined in environment" if ENV['EDITOR'].nil?
46
59
  input = ""
47
60
  input += args.join(" ") if args.length > 0
48
61
  input = wwid.fork_editor(input)
49
62
  if input
50
63
  title, note = wwid.format_input(input)
51
- wwid.add_item(title.cap_first, options[:s].cap_first, {:note => note})
64
+ wwid.add_item(title.cap_first, section, {:note => note, :back => date})
52
65
  wwid.write(wwid.doing_file)
53
66
  else
54
67
  raise "No content"
@@ -56,11 +69,11 @@ command :now do |c|
56
69
  else
57
70
  if args.length > 0
58
71
  title, note = wwid.format_input(args.join(" "))
59
- wwid.add_item(title.cap_first, options[:s].cap_first, {:note => note})
72
+ wwid.add_item(title.cap_first, section, {:note => note, :back => date})
60
73
  wwid.write(wwid.doing_file)
61
74
  elsif STDIN.stat.size > 0
62
75
  title, note = wwid.format_input(STDIN.read)
63
- wwid.add_item(title.cap_first, options[:s].cap_first, {:note => note})
76
+ wwid.add_item(title.cap_first, section, {:note => note, :back => date})
64
77
  wwid.write(wwid.doing_file)
65
78
  else
66
79
  raise "You must provide content when creating a new entry"
@@ -80,14 +93,25 @@ command :later do |c|
80
93
  c.default_value wwid.config.has_key?('editor_app') && wwid.config['editor_app'] ? wwid.config['editor_app'] : false
81
94
  c.flag [:a,:app]
82
95
 
96
+ c.desc 'Backdate to "date_string" (natural language)'
97
+ c.flag [:back]
98
+
83
99
  c.action do |global_options,options,args|
100
+ if options[:back]
101
+ date = wwid.chronify(options[:back])
102
+ raise "Unable to parse date string" if date.nil?
103
+ else
104
+ date = Time.now
105
+ end
106
+
84
107
  if options[:e] || (args.length == 0 && STDIN.stat.size == 0)
108
+ raise "No EDITOR variable defined in environment" if ENV['EDITOR'].nil?
85
109
  input = ""
86
110
  input += args.join(" ") if args.length > 0
87
111
  input = wwid.fork_editor(input)
88
112
  if input
89
113
  title, note = wwid.format_input(input)
90
- wwid.add_item(title.cap_first, "Later", {:note => note})
114
+ wwid.add_item(title.cap_first, "Later", {:note => note, :back => date})
91
115
  wwid.write(wwid.doing_file)
92
116
  else
93
117
  raise "No content"
@@ -95,11 +119,11 @@ command :later do |c|
95
119
  else
96
120
  if args.length > 0
97
121
  title, note = wwid.format_input(args.join(" "))
98
- wwid.add_item(title.cap_first, "Later", {:note => note})
122
+ wwid.add_item(title.cap_first, "Later", {:note => note, :back => date})
99
123
  wwid.write(wwid.doing_file)
100
124
  elsif STDIN.stat.size > 0
101
125
  title, note = wwid.format_input(STDIN.read)
102
- wwid.add_item(title.cap_first, "Later", {:note => note})
126
+ wwid.add_item(title.cap_first, "Later", {:note => note, :back => date})
103
127
  wwid.write(wwid.doing_file)
104
128
  else
105
129
  raise "You must provide content when creating a new entry"
@@ -108,11 +132,15 @@ command :later do |c|
108
132
  end
109
133
  end
110
134
 
111
- desc 'Add a completed item with @done(date)'
135
+ desc 'Add a completed item with @done(date). No argument finishes last entry.'
112
136
  arg_name 'entry'
113
137
  command :done do |c|
114
138
  c.desc 'Immediately archive the entry'
115
- c.switch [:a,:archive], :negatable => true
139
+ c.default_value false
140
+ c.switch [:a,:archive], :negatable => false, :default_value => false
141
+
142
+ c.desc 'Backdate to "date_string" (natural language)'
143
+ c.flag [:backdate]
116
144
 
117
145
  c.desc 'Section'
118
146
  c.default_value wwid.current_section
@@ -127,31 +155,43 @@ command :done do |c|
127
155
  # c.flag [:a,:app]
128
156
 
129
157
  c.action do |global_options,options,args|
130
- if options[:e] || (args.length == 0 && STDIN.stat.size == 0)
158
+ if options[:back]
159
+ date = wwid.chronify(options[:back])
160
+ raise "Unable to parse date string" if date.nil?
161
+ else
162
+ date = Time.now
163
+ end
164
+
165
+ section = wwid.guess_section(options[:s]) || options[:s].cap_first
166
+
167
+ if options[:e]
168
+ raise "No EDITOR variable defined in environment" if ENV['EDITOR'].nil?
131
169
  input = ""
132
170
  input += args.join(" ") if args.length > 0
133
171
  input = wwid.fork_editor(input)
134
172
  if input
135
173
  title, note = wwid.format_input(input)
136
174
  title += " @done(#{Time.now.strftime('%F %R')})"
137
- section = options[:a] ? "Archive" : options[:s]
138
- wwid.add_item(title.cap_first, section.cap_first, {:note => note})
175
+ section = "Archive" if options[:a]
176
+ wwid.add_item(title.cap_first, section.cap_first, {:note => note, :back => date})
139
177
  wwid.write(wwid.doing_file)
140
178
  else
141
179
  raise "No content"
142
180
  end
181
+ elsif args.length == 0 && STDIN.stat.size == 0
182
+ wwid.tag_last({:tags => ["done"], :count => 1, :section => section, :archive => options[:a], :back => date})
143
183
  else
144
184
  if args.length > 0
145
185
  title, note = wwid.format_input(args.join(" "))
146
186
  title += " @done(#{Time.now.strftime('%F %R')})"
147
- section = options[:a] ? "Archive" : options[:s]
148
- wwid.add_item(title.cap_first, section.cap_first, {:note => note})
187
+ section = "Archive" if options[:a]
188
+ wwid.add_item(title.cap_first, section.cap_first, {:note => note, :back => date})
149
189
  wwid.write(wwid.doing_file)
150
190
  elsif STDIN.stat.size > 0
151
191
  title, note = wwid.format_input(STDIN.read)
152
192
  title += " @done(#{Time.now.strftime('%F %R')})"
153
- section = options[:a] ? "Archive" : options[:s]
154
- wwid.add_item(title.cap_first, section.cap_first, {:note => note})
193
+ section = options[:a] ? "Archive" : section
194
+ wwid.add_item(title.cap_first, section.cap_first, {:note => note, :back => date})
155
195
  wwid.write(wwid.doing_file)
156
196
  else
157
197
  raise "You must provide content when creating a new entry"
@@ -160,20 +200,124 @@ command :done do |c|
160
200
  end
161
201
  end
162
202
 
203
+ desc 'Mark last X entries as @done'
204
+ arg_name 'count'
205
+ command :finish do |c|
206
+ c.desc 'Archive entries'
207
+ c.default_value false
208
+ c.switch [:a,:archive], :negatable => false, :default_value => false
209
+
210
+ c.desc 'Section'
211
+ c.default_value wwid.current_section
212
+ c.flag [:s,:section], :default_value => wwid.current_section
213
+
214
+ c.action do |global_options,options,args|
215
+
216
+ section = wwid.guess_section(options[:s]) || options[:s].cap_first
217
+
218
+ if args.length > 1
219
+ raise "Only one argument allowed"
220
+ elsif args.length == 0 || args[0] =~ /\d+/
221
+ count = args[0] ? args[0].to_i : 1
222
+ wwid.tag_last({:tags => ["done"], :count => count, :section => section, :archive => options[:a]})
223
+ else
224
+ raise "Invalid argument (specify number of recent items to mark @done)"
225
+ end
226
+ end
227
+ end
228
+
229
+ desc 'Tag last entry'
230
+ arg_name 'tag1 [tag2...]'
231
+ command :tag do |c|
232
+ c.desc 'Section'
233
+ c.default_value wwid.current_section
234
+ c.flag [:s,:section], :default_value => wwid.current_section
235
+
236
+ c.desc 'How many recent entries to tag'
237
+ c.default_value 1
238
+ c.flag [:c,:count], :default_value => 1
239
+
240
+ c.desc 'Include current date/time with tag'
241
+ c.default_value false
242
+ c.switch [:d,:date], :negatable => false, :default_value => false
243
+
244
+ c.desc 'Remove given tag(s)'
245
+ c.default_value false
246
+ c.switch [:r,:remove], :negatable => false, :default_value => false
247
+
248
+ c.action do |global_options,options,args|
249
+ if args.length == 0
250
+ raise "You must specify at least one tag"
251
+ else
252
+
253
+ section = wwid.guess_section(options[:s]) || options[:s].cap_first
254
+
255
+ count = options[:c].to_i
256
+ if args.join("") =~ /,/
257
+ tags = args.join("").split(/,/)
258
+ else
259
+ tags = args.join(" ").split(" ") # in case tags are quoted as one arg
260
+ end
261
+
262
+ tags.map!{|tag| tag.sub(/^@/,'').strip }
263
+
264
+ wwid.tag_last({:tags => tags, :count => count, :section => section, :date => options[:d], :remove => options[:r]})
265
+ end
266
+ end
267
+ end
268
+
269
+
163
270
  desc 'List all entries'
164
- arg_name 'section'
271
+ arg_name 'section [tags]'
165
272
  command :show do |c|
273
+ c.desc 'Tag boolean (AND,OR,NONE)'
274
+ c.default_value "OR"
275
+ c.flag [:b,:boolean], :default_value => "OR"
276
+
277
+ c.desc 'Max count to show'
278
+ c.default_value 0
279
+ c.flag [:c,:count], :default_value => 0
280
+
281
+ c.desc 'Age (oldest/newest)'
282
+ c.default_value 'newest'
283
+ c.flag [:a,:age], :default_value => 'newest'
284
+
285
+ c.desc 'Sort order (asc/desc)'
286
+ c.default_value 'asc'
287
+ c.flag [:s,:sort], :default_value => 'asc'
288
+
289
+ c.desc 'Output to csv'
290
+ c.default_value 'false'
291
+ c.switch [:csv], :default_value => false, :negatable => false
292
+
166
293
  c.action do |global_options,options,args|
294
+ tag_filter = false
295
+ tags = []
167
296
  if args.length > 0
168
- if wwid.sections.include? args.join(" ").cap_first
169
- section = args.join(" ").cap_first
297
+ if args[0] =~ /^all$/i
298
+ section = "All"
299
+ elsif args[0] =~ /^@/
300
+ section = "All"
301
+ tags.push(args[0].sub(/^@/,''))
170
302
  else
171
- raise "No such section: #{args.join(" ")}"
303
+ section = wwid.guess_section(args[0])
304
+ raise "No such section: #{args[0]}" unless section
305
+ end
306
+ if args.length > 1
307
+ tags += args[1..-1].map{|tag| tag.sub(/^@/,'').strip}
172
308
  end
173
309
  else
174
310
  section = wwid.current_section
175
311
  end
176
- puts wwid.list_section({:section => section, :count => 0})
312
+
313
+ unless tags.empty?
314
+ tag_filter = {
315
+ 'tags' => tags,
316
+ 'bool' => options[:b]
317
+ }
318
+ end
319
+
320
+ puts wwid.list_section({:section => section, :count => options[:c].to_i, :tag_filter => tag_filter, :age => options[:a], :order => options[:s], :csv => options[:csv]})
177
321
  end
178
322
  end
179
323
 
@@ -183,15 +327,18 @@ arg_name 'count'
183
327
  command :recent do |c|
184
328
  c.desc 'Section'
185
329
  c.default_value wwid.current_section
186
- c.flag [:s,:section]
330
+ c.flag [:s,:section], :default_value => wwid.current_section
187
331
  c.action do |global_options,options,args|
332
+
333
+ section = wwid.guess_section(options[:s]) || options[:s].cap_first
334
+
188
335
  unless global_options[:version]
189
336
  if args.length > 0
190
337
  count = args[0].to_i
191
338
  else
192
339
  count = 10
193
340
  end
194
- puts wwid.recent(count,options[:s].cap_first)
341
+ puts wwid.recent(count,section.cap_first)
195
342
  end
196
343
  end
197
344
  end
@@ -241,26 +388,48 @@ end
241
388
  desc 'Display a user-created view'
242
389
  arg_name 'view_name'
243
390
  command :view do |c|
244
- c.desc 'Section'
391
+ c.desc 'Section (override view settings)'
245
392
  c.flag [:s,:section]
246
393
 
247
- c.desc 'Count to display'
394
+ c.desc 'Count to display (override view settings)'
248
395
  c.flag [:c,:count], :must_match => /^\d+$/, :type => Integer
249
396
 
397
+ c.desc 'Output to csv'
398
+ c.default_value 'false'
399
+ c.switch [:csv], :default_value => false, :negatable => false
400
+
250
401
  c.action do |global_options,options,args|
251
402
  if args.empty?
252
403
  title = wwid.choose_view
253
404
  else
254
- title = args[0]
405
+ title = wwid.guess_view(args[0])
255
406
  end
407
+
408
+ if options[:s]
409
+ section = wwid.guess_section(options[:s]) || options[:s].cap_first
410
+ end
411
+
256
412
  view = wwid.get_view(title)
257
413
  if view
258
414
  template = view.has_key?('template') ? view['template'] : nil
259
415
  format = view.has_key?('date_format') ? view['date_format'] : nil
416
+ tags_color = view.has_key?('tags_color') ? view['tags_color'] : nil
417
+ tag_filter = false
418
+ if view.has_key?('tags')
419
+ unless view['tags'].nil? || view['tags'].empty?
420
+ tag_filter = {'tags' => [], 'bool' => "OR"}
421
+ if view['tags'].class == Array
422
+ tag_filter['tags'] = view['tags'].map{|tag| tag.strip }
423
+ else
424
+ tag_filter['tags'] = view['tags'].split(" ").map{|tag| tag.strip }
425
+ end
426
+ tag_filter['bool'] = view.has_key?('tags_bool') && !view['tags_bool'].nil? ? view['tags_bool'].upcase : "OR"
427
+ end
428
+ end
260
429
  count = options[:c] ? options[:c] : view.has_key?('count') ? view['count'] : 10
261
- section = options[:s] ? options[:s].cap_first : view.has_key?('section') ? view['section'] : wwid.current_section
430
+ section = options[:s] ? section : view.has_key?('section') ? view['section'] : wwid.current_section
262
431
  order = view.has_key?('order') ? view['order'] : "asc"
263
- puts wwid.list_section({:section => section, :count => count, :template => template, :format => format, :order => order })
432
+ puts wwid.list_section({:section => section, :count => count, :template => template, :format => format, :order => order, :tag_filter => tag_filter, :csv => options[:csv], :tags_color => tags_color })
264
433
  else
265
434
  raise "View #{title} not found in config"
266
435
  end
@@ -317,6 +486,7 @@ command :open do |c|
317
486
  elsif options[:b]
318
487
  system %Q{open -b "#{options[:b]}" "#{File.expand_path(wwid.doing_file)}"}
319
488
  elsif options[:e]
489
+ raise "No EDITOR variable defined in environment" if ENV['EDITOR'].nil?
320
490
  system %Q{$EDITOR "#{File.expand_path(wwid.doing_file)}"}
321
491
  else
322
492
  if wwid.config.has_key?('editor_app') && !wwid.config['editor_app'].nil?
@@ -335,10 +505,26 @@ desc 'Edit the configuration file'
335
505
  command :config do |c|
336
506
  c.desc 'Editor to use'
337
507
  c.default_value ENV['EDITOR']
338
- c.flag [:e,:editor]
508
+ c.flag [:e,:editor], :default_value => nil
509
+
510
+ c.desc 'Application to use (OS X only)'
511
+ c.flag [:a]
512
+
513
+ c.desc 'Application bundle id to use (OS X only)'
514
+ c.flag [:b]
339
515
 
340
516
  c.action do |global_options,options,args|
341
- system %Q{#{options[:e]} "#{File.expand_path(DOING_CONFIG)}"}
517
+ if options[:a] || options[:b]
518
+ if options[:a]
519
+ %x{open -a "#{options[:a]}" "#{File.expand_path(DOING_CONFIG)}"}
520
+ elsif options[:b]
521
+ %x{open -b #{options[:b]} "#{File.expand_path(DOING_CONFIG)}"}
522
+ end
523
+ else
524
+ raise "No EDITOR variable defined in environment" if options[:e].nil? && ENV['EDITOR'].nil?
525
+ editor = options[:e].nil? ? ENV['EDITOR'] : options[:e]
526
+ system %Q{#{editor} "#{File.expand_path(DOING_CONFIG)}"}
527
+ end
342
528
  end
343
529
  end
344
530
 
@@ -347,6 +533,7 @@ pre do |global,command,options,args|
347
533
  if global[:version]
348
534
  puts "doing v" + Doing::VERSION
349
535
  end
536
+
350
537
  # Return true to proceed; false to abort and not call the
351
538
  # chosen command
352
539
  # Use skips_pre before a command to skip this block
data/lib/doing.rb CHANGED
@@ -3,7 +3,9 @@ require 'time'
3
3
  require 'date'
4
4
  require 'yaml'
5
5
  require 'pp'
6
+ require 'csv'
6
7
  require 'tempfile'
8
+ require 'chronic'
7
9
  require 'doing/wwid.rb'
8
10
 
9
11
  DOING_CONFIG = "~/.doingrc"
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '0.1.9'
2
+ VERSION = '0.2.2.pre'
3
3
  end
data/lib/doing/wwid.rb CHANGED
@@ -41,13 +41,15 @@ class WWID
41
41
  'wrap_width' => 88
42
42
  }
43
43
  @config['views'] ||= {
44
- 'sample' => {
44
+ 'done' => {
45
45
  'date_format' => '%_I:%M%P',
46
46
  'template' => '%date | %title%note',
47
47
  'wrap_width' => 0,
48
- 'section' => 'section_name',
49
- 'count' => 5,
50
- 'order' => "desc"
48
+ 'section' => 'All',
49
+ 'count' => 0,
50
+ 'order' => 'desc',
51
+ 'tags' => 'done complete cancelled',
52
+ 'tags_bool' => 'OR'
51
53
  },
52
54
  'color' => {
53
55
  'date_format' => '%F %_I:%M%P',
@@ -89,7 +91,7 @@ class WWID
89
91
 
90
92
  lines.each {|line|
91
93
  next if line =~ /^\s*$/
92
- if line =~ /^(\w[\w ]+):\s*(@\S+\s*)*$/
94
+ if line =~ /^(\S[\S ]+):\s*(@\S+\s*)*$/
93
95
  section = $1
94
96
  @content[section] = {}
95
97
  @content[section]['original'] = line
@@ -184,6 +186,25 @@ class WWID
184
186
  [title, note]
185
187
  end
186
188
 
189
+ def chronify(input)
190
+ if input =~ /^(\d+)([mhd])?$/i
191
+ amt = $1
192
+ type = $2.nil? ? "m" : $2
193
+ input = case type.downcase
194
+ when "m"
195
+ amt + " minutes ago"
196
+ when "h"
197
+ amt + " hours ago"
198
+ when "d"
199
+ amt + " days ago"
200
+ else
201
+ input
202
+ end
203
+ end
204
+
205
+ Chronic.parse(input, {:context => :past, :ambiguous_time_range => 8})
206
+ end
207
+
187
208
  def sections
188
209
  @content.keys
189
210
  end
@@ -192,19 +213,109 @@ class WWID
192
213
  @content[title.cap_first] = {'original' => "#{title}:", 'items' => []}
193
214
  end
194
215
 
216
+ def guess_section(frag)
217
+ sections.each {|section| return section if frag.downcase == section.downcase}
218
+ section = false
219
+ re = frag.split('').join(".*?")
220
+ sections.each {|sect|
221
+ if sect =~ /#{re}/i
222
+ $stderr.puts "Assuming you meant #{sect}"
223
+ section = sect
224
+ break
225
+ end
226
+ }
227
+ unless section
228
+ alt = guess_view(frag)
229
+ if alt
230
+ raise "Did you mean `doing view #{alt}`?"
231
+ else
232
+ raise "Invalid section: #{frag}"
233
+ end
234
+ end
235
+ section.cap_first
236
+ end
237
+
238
+ def guess_view(frag)
239
+ views.each {|view| return view if frag.downcase == view.downcase}
240
+ view = false
241
+ re = frag.split('').join(".*?")
242
+ views.each {|v|
243
+ if v =~ /#{re}/i
244
+ $stderr.puts "Assuming you meant #{v}"
245
+ view = v
246
+ break
247
+ end
248
+ }
249
+ unless view
250
+ alt = guess_section(frag)
251
+ if alt
252
+ raise "Did you mean `doing show #{alt}`?"
253
+ else
254
+ raise "Invalid view: #{frag}"
255
+ end
256
+ end
257
+ view
258
+ end
259
+
195
260
  def add_item(title,section=nil,opt={})
196
261
  section ||= @current_section
197
262
  add_section(section) unless @content.has_key?(section)
198
263
  opt[:date] ||= Time.now
199
264
  opt[:note] ||= []
265
+ opt[:back] ||= Time.now
200
266
 
201
- entry = {'title' => title.strip.cap_first, 'date' => opt[:date]}
267
+ entry = {'title' => title.strip.cap_first, 'date' => opt[:back]}
202
268
  unless opt[:note] =~ /^\s*$/s
203
269
  entry['note'] = opt[:note]
204
270
  end
205
271
  @content[section]['items'].push(entry)
206
272
  end
207
273
 
274
+ def tag_last(opt={})
275
+ opt[:section] ||= @current_section
276
+ opt[:count] ||= 1
277
+ opt[:archive] ||= false
278
+ opt[:tags] ||= ["done"]
279
+ opt[:date] ||= false
280
+ opt[:remove] ||= false
281
+
282
+ opt[:section] = guess_section(opt[:section])
283
+
284
+ if @content.has_key?(opt[:section])
285
+ # sort_section(opt[:section])
286
+ # items = @content[opt[:section]]['items'].sort_by{|item| item['date'] }.reverse
287
+
288
+ @content[opt[:section]]['items'].each_with_index {|item, i|
289
+ break if i == opt[:count]
290
+ title = item['title']
291
+ opt[:tags].each {|tag|
292
+ if opt[:remove]
293
+ title.gsub!(/ @#{tag}/,'')
294
+ else
295
+ unless title =~ /@#{tag}/
296
+ if tag == "done" || opt[:date]
297
+ title += " @#{tag}(#{Time.now.strftime('%F %R')})"
298
+ else
299
+ title += " @#{tag}"
300
+ end
301
+ end
302
+ end
303
+ }
304
+ @content[opt[:section]]['items'][i]['title'] = title
305
+ }
306
+
307
+ if opt[:archive] && opt[:section] != "Archive"
308
+ archived = @content[opt[:section]]['items'][0..opt[:count]-1]
309
+ @content[opt[:section]]['items'] = @content[opt[:section]]['items'][opt[:count]..-1]
310
+ @content['Archive']['items'] = archived + @content['Archive']['items']
311
+ end
312
+
313
+ write(@doing_file)
314
+ else
315
+ raise "Section not found"
316
+ end
317
+ end
318
+
208
319
  def write(file=nil)
209
320
  if @other_content_top.empty?
210
321
  output = ""
@@ -264,17 +375,23 @@ class WWID
264
375
  opt[:section] ||= nil
265
376
  opt[:format] ||= @default_date_format
266
377
  opt[:template] ||= @default_template
378
+ opt[:age] ||= "newest"
267
379
  opt[:order] ||= "desc"
268
380
  opt[:today] ||= false
381
+ opt[:tag_filter] ||= false
382
+ opt[:tags_color] ||= false
269
383
 
270
384
  if opt[:section].nil?
271
385
  opt[:section] = @content[choose_section]
272
386
  elsif opt[:section].class == String
273
- if @content.has_key? opt[:section]
274
- opt[:section] = @content[opt[:section]]
387
+ if opt[:section] =~ /^all$/i
388
+ combined = {'items' => []}
389
+ @content.each {|k,v|
390
+ combined['items'] += v['items']
391
+ }
392
+ opt[:section] = combined
275
393
  else
276
- $stderr.puts "Section '#{opt[:section]}' not found"
277
- return
394
+ opt[:section] = @content[guess_section(opt[:section])]
278
395
  end
279
396
  end
280
397
 
@@ -285,78 +402,127 @@ class WWID
285
402
 
286
403
  items = opt[:section]['items'].sort_by{|item| item['date'] }
287
404
 
405
+ if opt[:tag_filter] && !opt[:tag_filter]['tags'].empty?
406
+ items.delete_if {|item|
407
+ if opt[:tag_filter]['bool'] =~ /(AND|ALL)/
408
+ score = 0
409
+ opt[:tag_filter]['tags'].each {|tag|
410
+ score += 1 if item['title'] =~ /@#{tag}/
411
+ }
412
+ score < opt[:tag_filter]['tags'].length
413
+ elsif opt[:tag_filter]['bool'] =~ /NONE/
414
+ del = false
415
+ opt[:tag_filter]['tags'].each {|tag|
416
+ del = true if item['title'] =~ /@#{tag}/
417
+ }
418
+ del
419
+ elsif opt[:tag_filter]['bool'] =~ /(OR|ANY)/
420
+ del = true
421
+ opt[:tag_filter]['tags'].each {|tag|
422
+ del = false if item['title'] =~ /@#{tag}/
423
+ }
424
+ del
425
+ end
426
+ }
427
+ end
428
+
288
429
  if opt[:today]
289
430
  items.delete_if {|item|
290
431
  item['date'] < Date.today.to_time
291
432
  }.reverse!
292
433
  else
293
- items = items.reverse[0..count]
434
+ if opt[:age] =~ /oldest/i
435
+ items = items[0..count]
436
+ else
437
+ items = items.reverse[0..count]
438
+ end
294
439
  end
295
440
 
296
- items.reverse! if opt[:order] =~ /^asc/i
441
+ if opt[:order] =~ /^a/i
442
+ items.reverse!
443
+ end
297
444
 
298
445
  out = ""
299
446
 
300
- items.each {|item|
301
- if (item.has_key?('note') && !item['note'].empty?) && @config[:include_notes]
302
- note_lines = item['note'].delete_if{|line| line =~ /^\s*$/ }.map{|line| "\t\t" + line.sub(/^\t\t/,'') }
303
- if opt[:wrap_width] && opt[:wrap_width] > 0
304
- width = opt[:wrap_width]
305
- note_lines.map! {|line|
306
- line.strip.gsub(/(.{1,#{width}})(\s+|\Z)/, "\t\\1\n")
307
- }
308
- end
309
- note = "\n#{note_lines.join("\n").chomp}"
310
- else
447
+ if opt[:csv]
448
+ output = [['date','title','note'].to_csv]
449
+ items.each {|i|
311
450
  note = ""
312
- end
313
- output = opt[:template].dup
314
- output.gsub!(/%[a-z]+/) do |m|
315
- if colors.has_key?(m.sub(/^%/,''))
316
- colors[m.sub(/^%/,'')]
317
- else
318
- m
319
- end
320
- end
321
- output.sub!(/%date/,item['date'].strftime(opt[:format]))
322
- output.sub!(/%shortdate/) {
323
- if item['date'] > Date.today.to_time
324
- item['date'].strftime('%_I:%M%P')
325
- elsif item['date'] > (Date.today - 7).to_time
326
- item['date'].strftime('%a %-I:%M%P')
327
- elsif item['date'].year == Date.today.year
328
- item['date'].strftime('%b %d, %-I:%M%P')
329
- else
330
- item['date'].strftime('%b %d %Y, %-I:%M%P')
451
+ if i['note']
452
+ arr = i['note'].map{|line| line.strip}.delete_if{|e| e =~ /^\s*$/}
453
+ note = arr.join("\n") unless arr.nil?
331
454
  end
455
+ output.push([i['date'],i['title'],note].to_csv)
332
456
  }
333
- output.sub!(/%title/) {|m|
334
- if opt[:wrap_width] && opt[:wrap_width] > 0
335
- item['title'].gsub(/(.{1,#{opt[:wrap_width]}})(\s+|\Z)/, "\\1\n\t ").strip
457
+ out = output.join()
458
+ else
459
+
460
+ items.each {|item|
461
+ if (item.has_key?('note') && !item['note'].empty?) && @config[:include_notes]
462
+ note_lines = item['note'].delete_if{|line| line =~ /^\s*$/ }.map{|line| "\t\t" + line.sub(/^\t\t/,'') }
463
+ if opt[:wrap_width] && opt[:wrap_width] > 0
464
+ width = opt[:wrap_width]
465
+ note_lines.map! {|line|
466
+ line.strip.gsub(/(.{1,#{width}})(\s+|\Z)/, "\t\\1\n")
467
+ }
468
+ end
469
+ note = "\n#{note_lines.join("\n").chomp}"
336
470
  else
337
- item['title'].strip
471
+ note = ""
338
472
  end
339
- }
340
- output.sub!(/%note/,note)
341
- output.sub!(/%odnote/,note.gsub(/\t\t/,"\t"))
342
- output.gsub!(/%hr(_under)?/) do |m|
343
- o = ""
344
- `tput cols`.to_i.times do
345
- o += $1.nil? ? "-" : "_"
473
+ output = opt[:template].dup
474
+
475
+ output.gsub!(/%[a-z]+/) do |m|
476
+ if colors.has_key?(m.sub(/^%/,''))
477
+ colors[m.sub(/^%/,'')]
478
+ else
479
+ m
480
+ end
346
481
  end
347
- o
348
- end
349
482
 
483
+ output.sub!(/%date/,item['date'].strftime(opt[:format]))
484
+ output.sub!(/%shortdate/) {
485
+ if item['date'] > Date.today.to_time
486
+ item['date'].strftime('%_I:%M%P')
487
+ elsif item['date'] > (Date.today - 7).to_time
488
+ item['date'].strftime('%a %-I:%M%P')
489
+ elsif item['date'].year == Date.today.year
490
+ item['date'].strftime('%b %d, %-I:%M%P')
491
+ else
492
+ item['date'].strftime('%b %d %Y, %-I:%M%P')
493
+ end
494
+ }
495
+ output.sub!(/%title/) {|m|
496
+ if opt[:wrap_width] && opt[:wrap_width] > 0
497
+ item['title'].gsub(/(.{1,#{opt[:wrap_width]}})(\s+|\Z)/, "\\1\n\t ").strip
498
+ else
499
+ item['title'].strip
500
+ end
501
+ }
502
+ if opt[:tags_color]
503
+ output.gsub!(/\s(@\S+(?:\(.*?\))?)/," #{colors[opt[:tags_color]]}\\1")
504
+ end
505
+ output.sub!(/%note/,note)
506
+ output.sub!(/%odnote/,note.gsub(/\t\t/,"\t"))
507
+ output.gsub!(/%hr(_under)?/) do |m|
508
+ o = ""
509
+ `tput cols`.to_i.times do
510
+ o += $1.nil? ? "-" : "_"
511
+ end
512
+ o
513
+ end
350
514
 
351
- out += output + "\n"
352
- }
515
+
516
+ out += output + "\n"
517
+ }
518
+ end
353
519
 
354
520
  return out
355
521
  end
356
522
 
357
523
  def archive(section=nil,count=10)
358
524
  section = choose_section if section.nil? || section =~ /choose/i
359
- section = section.cap_first
525
+ section = guess_section(section)
360
526
  if sections.include?(section)
361
527
  items = @content[section]['items']
362
528
  return if items.length < count
@@ -422,6 +588,7 @@ class WWID
422
588
  def recent(count=10,section=nil)
423
589
  cfg = @config['templates']['recent']
424
590
  section ||= @current_section
591
+ section = guess_section(section)
425
592
  list_section({:section => section, :wrap_width => cfg['wrap_width'], :count => count, :format => cfg['date_format'], :template => cfg['template'], :order => "asc"})
426
593
  end
427
594
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: doing
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: 0.2.2.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-03-18 00:00:00.000000000 Z
11
+ date: 2014-03-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -72,6 +72,26 @@ dependencies:
72
72
  - - '='
73
73
  - !ruby/object:Gem::Version
74
74
  version: 2.9.0
75
+ - !ruby/object:Gem::Dependency
76
+ name: chronic
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ~>
80
+ - !ruby/object:Gem::Version
81
+ version: '0.10'
82
+ - - '>='
83
+ - !ruby/object:Gem::Version
84
+ version: 0.10.2
85
+ type: :runtime
86
+ prerelease: false
87
+ version_requirements: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ~>
90
+ - !ruby/object:Gem::Version
91
+ version: '0.10'
92
+ - - '>='
93
+ - !ruby/object:Gem::Version
94
+ version: 0.10.2
75
95
  description: A tool for managing a TaskPaper-like file of recent activites. Perfect
76
96
  for the late-night hacker on too much caffeine to remember what they accomplished
77
97
  at 2 in the morning.
@@ -110,9 +130,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
110
130
  version: '0'
111
131
  required_rubygems_version: !ruby/object:Gem::Requirement
112
132
  requirements:
113
- - - '>='
133
+ - - '>'
114
134
  - !ruby/object:Gem::Version
115
- version: '0'
135
+ version: 1.3.1
116
136
  requirements: []
117
137
  rubyforge_project:
118
138
  rubygems_version: 2.2.2