todo.txt 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +29 -0
- data/MIT-LICENSE +21 -0
- data/NOTES.md +0 -0
- data/README.md +11 -0
- data/lib/todo.rb +20 -0
- data/lib/todo/cli.rb +39 -0
- data/lib/todo/cli/archive.rb +39 -0
- data/lib/todo/cli/cmd.rb +26 -0
- data/lib/todo/cli/list.rb +42 -0
- data/lib/todo/cli/push.rb +42 -0
- data/lib/todo/cli/toggle.rb +22 -0
- data/lib/todo/data/item.rb +58 -0
- data/lib/todo/data/list.rb +64 -0
- data/lib/todo/data/matcher.rb +42 -0
- data/lib/todo/data/parser.rb +41 -0
- data/lib/todo/helpers/hash/compact.rb +13 -0
- data/lib/todo/helpers/hash/format.rb +13 -0
- data/lib/todo/helpers/hash/slice.rb +17 -0
- data/lib/todo/helpers/object/presence.rb +26 -0
- data/lib/todo/helpers/string/camelize.rb +16 -0
- data/lib/todo/src/file.rb +24 -0
- data/lib/todo/src/idonethis.rb +52 -0
- data/lib/todo/src/io.rb +29 -0
- data/lib/todo/support/http.rb +31 -0
- data/lib/todo/support/options_parser.rb +31 -0
- data/lib/todo/version.rb +3 -0
- data/lib/todo/view.rb +49 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/support/io.rb +9 -0
- data/spec/todo/cli/archive_spec.rb +16 -0
- data/spec/todo/cli/list_spec.rb +13 -0
- data/spec/todo/cli/toggle_spec.rb +25 -0
- data/spec/todo/data/item_spec.rb +94 -0
- data/spec/todo/data/list_spec.rb +59 -0
- data/spec/todo/data/parser_spec.rb +27 -0
- data/spec/todo/helpers/hash/format_spec.rb +9 -0
- data/spec/todo/helpers/hash/slice_spec.rb +8 -0
- data/spec/todo/helpers/object/presence_spec.rb +42 -0
- data/spec/todo/helpers/string/string_spec.rb +7 -0
- data/spec/todo/src/file_spec.rb +33 -0
- data/spec/todo/src/io_spec.rb +18 -0
- data/spec/todo/view_spec.rb +17 -0
- data/todo.gemspec +20 -0
- data/todo.md +10 -0
- 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,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
|
data/lib/todo/src/io.rb
ADDED
@@ -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
|
data/lib/todo/version.rb
ADDED
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
|
data/spec/spec_helper.rb
ADDED
data/spec/support/io.rb
ADDED
@@ -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
|