todo.txt 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +7 -0
  3. data/Gemfile.lock +29 -0
  4. data/MIT-LICENSE +21 -0
  5. data/NOTES.md +0 -0
  6. data/README.md +11 -0
  7. data/lib/todo.rb +20 -0
  8. data/lib/todo/cli.rb +39 -0
  9. data/lib/todo/cli/archive.rb +39 -0
  10. data/lib/todo/cli/cmd.rb +26 -0
  11. data/lib/todo/cli/list.rb +42 -0
  12. data/lib/todo/cli/push.rb +42 -0
  13. data/lib/todo/cli/toggle.rb +22 -0
  14. data/lib/todo/data/item.rb +58 -0
  15. data/lib/todo/data/list.rb +64 -0
  16. data/lib/todo/data/matcher.rb +42 -0
  17. data/lib/todo/data/parser.rb +41 -0
  18. data/lib/todo/helpers/hash/compact.rb +13 -0
  19. data/lib/todo/helpers/hash/format.rb +13 -0
  20. data/lib/todo/helpers/hash/slice.rb +17 -0
  21. data/lib/todo/helpers/object/presence.rb +26 -0
  22. data/lib/todo/helpers/string/camelize.rb +16 -0
  23. data/lib/todo/src/file.rb +24 -0
  24. data/lib/todo/src/idonethis.rb +52 -0
  25. data/lib/todo/src/io.rb +29 -0
  26. data/lib/todo/support/http.rb +31 -0
  27. data/lib/todo/support/options_parser.rb +31 -0
  28. data/lib/todo/version.rb +3 -0
  29. data/lib/todo/view.rb +49 -0
  30. data/spec/spec_helper.rb +9 -0
  31. data/spec/support/io.rb +9 -0
  32. data/spec/todo/cli/archive_spec.rb +16 -0
  33. data/spec/todo/cli/list_spec.rb +13 -0
  34. data/spec/todo/cli/toggle_spec.rb +25 -0
  35. data/spec/todo/data/item_spec.rb +94 -0
  36. data/spec/todo/data/list_spec.rb +59 -0
  37. data/spec/todo/data/parser_spec.rb +27 -0
  38. data/spec/todo/helpers/hash/format_spec.rb +9 -0
  39. data/spec/todo/helpers/hash/slice_spec.rb +8 -0
  40. data/spec/todo/helpers/object/presence_spec.rb +42 -0
  41. data/spec/todo/helpers/string/string_spec.rb +7 -0
  42. data/spec/todo/src/file_spec.rb +33 -0
  43. data/spec/todo/src/io_spec.rb +18 -0
  44. data/spec/todo/view_spec.rb +17 -0
  45. data/todo.gemspec +20 -0
  46. data/todo.md +10 -0
  47. metadata +89 -0
@@ -0,0 +1,42 @@
1
+ module Todo
2
+ module Data
3
+ class Matcher < Struct.new(:item, :data)
4
+ def matches?
5
+ return true if data.empty?
6
+ data[:id] ? match_id : match_data
7
+ end
8
+
9
+ private
10
+
11
+ def match_data
12
+ data.keys.inject(true) do |result, key|
13
+ result && send(:"match_#{key}")
14
+ end
15
+ end
16
+
17
+ def match_id
18
+ item.id.to_i == data[:id].to_i
19
+ end
20
+
21
+ def match_text
22
+ item.text.include?(data[:text])
23
+ end
24
+
25
+ def match_status
26
+ item.status && item.status.to_sym == normalize_status(data[:status].to_sym)
27
+ end
28
+
29
+ def match_since
30
+ item.done_date.to_s >= data[:since].to_s
31
+ end
32
+
33
+ def match_before
34
+ item.done_date.to_s < data[:before].to_s
35
+ end
36
+
37
+ def normalize_status(status)
38
+ status == :pending ? :pend : status
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,41 @@
1
+ require 'todo'
2
+ require 'todo/helpers/hash/compact'
3
+
4
+ module Todo
5
+ module Data
6
+ class Parser < Struct.new(:line)
7
+ STATUS = /^(#{STATUSES.values.join('|')}){1}\s*/
8
+ ID = /\s*\[(\d+)\]/
9
+ TAG = /\s*([^\s]+):([^\s]+)/
10
+
11
+ include Helpers::Hash::Compact
12
+
13
+ def parse
14
+ compact(
15
+ id: id,
16
+ text: text,
17
+ status: status,
18
+ tags: tags,
19
+ )
20
+ end
21
+
22
+ private
23
+
24
+ def status
25
+ STATUSES.invert[line.match(STATUS) && $1]
26
+ end
27
+
28
+ def id
29
+ line =~ ID && $1.to_i
30
+ end
31
+
32
+ def text
33
+ line.sub(STATUS, '').gsub(ID, '').gsub(TAG, '').rstrip
34
+ end
35
+
36
+ def tags
37
+ Hash[line.scan(TAG).map { |key, value| [key.to_sym, value] }]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,13 @@
1
+ module Todo
2
+ module Helpers
3
+ module Hash
4
+ module Compact
5
+ def compact(hash)
6
+ hash.reject { |key, value| value.nil? }
7
+ end
8
+
9
+ extend self
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Todo
2
+ module Helpers
3
+ module Hash
4
+ module Format
5
+ def to_pairs(hash, separator = '=')
6
+ hash.map { |key, value| [key, value].join(separator) }
7
+ end
8
+
9
+ extend self
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ module Todo
2
+ module Helpers
3
+ module Hash
4
+ module Slice
5
+ def slice(hash, *keys)
6
+ hash.select { |key, value| keys.include?(key) }
7
+ end
8
+
9
+ def except(hash, *keys)
10
+ hash.reject { |key, value| keys.include?(key) }
11
+ end
12
+
13
+ extend self
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ module Todo
2
+ module Helpers
3
+ module Object
4
+ module Presence
5
+ def present?(obj)
6
+ case obj
7
+ when NilClass, FalseClass
8
+ false
9
+ when TrueClass, Symbol, Numeric
10
+ true
11
+ when ::String, ::Array, ::Hash
12
+ !obj.empty?
13
+ else
14
+ fail "Unexpected object type #{obj.inspect}"
15
+ end
16
+ end
17
+
18
+ def blank?(obj)
19
+ not present?(obj)
20
+ end
21
+
22
+ extend self
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ module Todo
2
+ module Helpers
3
+ module String
4
+ module Camelize
5
+ def camelize(string)
6
+ string.to_s.
7
+ sub(/^[a-z\d]*/) { $&.capitalize }.
8
+ gsub(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }.
9
+ gsub('/', '::')
10
+ end
11
+
12
+ extend self
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ module Todo
2
+ module Src
3
+ class File
4
+ attr_reader :path, :mode
5
+
6
+ def initialize(path, opts = {})
7
+ @path = path
8
+ @mode = opts[:mode] || 'w+'
9
+ end
10
+
11
+ def exists?
12
+ ::File.exists?(path)
13
+ end
14
+
15
+ def read
16
+ exists? ? ::File.readlines(path).map(&:rstrip) : []
17
+ end
18
+
19
+ def write(lines)
20
+ ::File.open(path, mode) { |f| f.puts(lines.join("\n")) }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,52 @@
1
+ require 'json'
2
+ require 'todo/support/http'
3
+
4
+ module Todo
5
+ module Src
6
+ class Idonthis < Struct.new(:config, :opts)
7
+ URL = 'https://idonethis.com/api/v0.1/dones/'
8
+
9
+ def include?(item)
10
+ texts.include?(item.text) or ids.include?(item.id)
11
+ end
12
+
13
+ def write(lines)
14
+ lines.each { |line| post(line) }
15
+ end
16
+
17
+ private
18
+
19
+ def texts
20
+ items.map(&:text)
21
+ end
22
+
23
+ def ids
24
+ items.map(&:id).compact
25
+ end
26
+
27
+ def items
28
+ @items ||= fetch.map { |item| Item.new(nil, item['raw_text']) }
29
+ end
30
+
31
+ def fetch
32
+ JSON.parse(get.body)['results'] || []
33
+ end
34
+
35
+ def get
36
+ Http.new(URL, headers: headers, params: params).get
37
+ end
38
+
39
+ def post(text)
40
+ Http.new(URL, headers: headers).post(raw_text: text, team: config[:team])
41
+ end
42
+
43
+ def headers
44
+ { 'Authorization' => "Token #{config[:token]}" }
45
+ end
46
+
47
+ def params
48
+ { owner: config[:username], team: config[:team], done_date_after: opts[:since], page_size: 100 }
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,29 @@
1
+ module Todo
2
+ module Src
3
+ class Io
4
+ attr_reader :opts
5
+
6
+ def initialize(opts = {})
7
+ @opts = opts
8
+ end
9
+
10
+ def read
11
+ @lines ||= input.readlines.map(&:rstrip)
12
+ end
13
+
14
+ def write(lines)
15
+ output.puts(lines.join("\n"))
16
+ end
17
+
18
+ private
19
+
20
+ def input
21
+ opts[:in] || $stdin
22
+ end
23
+
24
+ def output
25
+ opts[:out] || $stdout
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+
4
+ module Todo
5
+ module Support
6
+ class Http
7
+ attr_reader :uri, :headers
8
+
9
+ def initialize(url, opts)
10
+ @uri = URI(url).tap { |url| url.query = URI.encode_www_form(opts[:params] || {}) }
11
+ @headers = opts[:headers] || {}
12
+ end
13
+
14
+ def get
15
+ client = Net::HTTP.new(url.host, url.port)
16
+ client.use_ssl = true
17
+ response = client.get(uri, headers)
18
+ raise "Failed to get #{url.to_s}" unless response.code.to_s[0] == '2'
19
+ response
20
+ end
21
+
22
+ def post(data)
23
+ request = Net::HTTP::Post.new(url, headers)
24
+ request.form_data = data
25
+ response = http.request(request)
26
+ raise "Failed to post to #{url.to_s}: #{response.body}" unless response.code.to_s[0] == '2'
27
+ response
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ require 'optparse'
2
+
3
+ module Todo
4
+ module Support
5
+ module OptionsParser
6
+ def opt(*args, &block)
7
+ rules << [args, block]
8
+ end
9
+
10
+ def parse(args)
11
+ opts = {}
12
+ args = parser(opts).parse(args)
13
+ [args, opts]
14
+ end
15
+
16
+ private
17
+
18
+ def rules
19
+ @rules ||= (superclass.instance_variable_get(:@rules) || []).dup
20
+ end
21
+
22
+ def parser(opts)
23
+ OptionParser.new do |parser|
24
+ rules.each do |args, block|
25
+ parser.on(*args) { |value| block.call(opts, value.strip) }
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,3 @@
1
+ module Todo
2
+ VERSION = '0.0.3'
3
+ end
data/lib/todo/view.rb ADDED
@@ -0,0 +1,49 @@
1
+ require 'todo'
2
+ require 'todo/helpers/hash/format'
3
+ require 'todo/helpers/object/presence'
4
+
5
+ module Todo
6
+ class View < Struct.new(:items, :cols)
7
+ COLS = [:status, :text, :tags, :id]
8
+
9
+ include Helpers::Hash::Format, Helpers::Object::Presence
10
+
11
+ def render
12
+ items.map { |item| format(item) }
13
+ end
14
+
15
+ private
16
+
17
+ def format(item)
18
+ cols.map { |col| format_col(col, item.send(col)) }.compact.join(' ')
19
+ end
20
+
21
+ def format_col(col, value)
22
+ send(:"format_#{col}", value) if present?(value)
23
+ end
24
+
25
+ def format_status(status)
26
+ STATUSES[status]
27
+ end
28
+
29
+ def format_id(id)
30
+ "[#{id}]"
31
+ end
32
+
33
+ def format_text(text)
34
+ text
35
+ end
36
+
37
+ def format_done_date(date)
38
+ date
39
+ end
40
+
41
+ def format_tags(tags)
42
+ to_pairs(tags, ':') if tags.any?
43
+ end
44
+
45
+ def cols
46
+ super || COLS
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,9 @@
1
+ require 'time'
2
+ require 'support/io'
3
+
4
+ NOW = Time.parse('2015-12-01 00:02:00 +0200')
5
+
6
+ RSpec.configure do |c|
7
+ c.mock_with :mocha
8
+ c.before { Time.stubs(:now).returns(NOW) }
9
+ end
@@ -0,0 +1,9 @@
1
+ require 'stringio'
2
+
3
+ module Support
4
+ class Io < StringIO
5
+ def readlines
6
+ string.split("\n")
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ require 'todo/cli/archive'
2
+ require 'fileutils'
3
+
4
+ describe Todo::Cli::Archive do
5
+ let(:lines) { ['- foo [1]', 'x bar done:2015-11-01 [2]', 'x baz done:2015-12-01 [3]'] }
6
+ let(:io) { Support::Io.new(lines.join("\n")) }
7
+ let(:out) { Support::Io.new }
8
+ let(:opts) { { archive: '/tmp/todo.archive.txt', in: io, out: out } }
9
+
10
+ subject { described_class.new(nil, opts) }
11
+ before { subject.run }
12
+ after { FileUtils.rm_f(opts[:archive]) }
13
+
14
+ it { expect(out.readlines).to eq ['- foo [1]', 'x baz done:2015-12-01 [3]'] }
15
+ it { expect(File.read(opts[:archive]).strip).to eq 'x bar done:2015-11-01 [2]' }
16
+ end