nin 0.0.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/.travis.yml +5 -0
- data/Gemfile +9 -0
- data/LICENSE +21 -0
- data/README.md +112 -0
- data/Rakefile +17 -0
- data/bin/nin +17 -0
- data/demo.gif +0 -0
- data/lib/nin.rb +38 -57
- data/lib/nin/command.rb +116 -0
- data/lib/nin/error.rb +5 -0
- data/lib/nin/extensions/array_extensions.rb +41 -0
- data/lib/nin/extensions/date_extensions.rb +20 -0
- data/lib/nin/extensions/integer_extensions.rb +5 -0
- data/lib/nin/extensions/nil_class_extensions.rb +5 -0
- data/lib/nin/extensions/object_extensions.rb +5 -0
- data/lib/nin/extensions/string_extensions.rb +5 -0
- data/lib/nin/integration/service.rb +9 -0
- data/lib/nin/integration/service/todoist.rb +94 -0
- data/lib/nin/integration/synchronizer.rb +12 -0
- data/lib/nin/integration/synchronizer/todoist.rb +112 -0
- data/lib/nin/integration/todoist.rb +5 -0
- data/lib/nin/integration/todoist/client.rb +25 -0
- data/lib/nin/integration/todoist/client/base_client.rb +13 -0
- data/lib/nin/integration/todoist/client/item.rb +25 -0
- data/lib/nin/integration/todoist/client/project.rb +25 -0
- data/lib/nin/integration/todoist/client/sync.rb +33 -0
- data/lib/nin/item.rb +70 -0
- data/lib/nin/parser.rb +43 -0
- data/lib/nin/presenters/item_presenter.rb +53 -0
- data/lib/nin/presenters/todo_presenter.rb +17 -0
- data/lib/nin/todo.rb +214 -0
- data/lib/nin/version.rb +3 -0
- data/lib/nin/yaml_store.rb +31 -0
- data/nin.gemspec +27 -0
- metadata +117 -12
data/lib/nin/parser.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module Nin
|
2
|
+
class Parser
|
3
|
+
def initialize(desc)
|
4
|
+
@desc = desc
|
5
|
+
end
|
6
|
+
|
7
|
+
def call
|
8
|
+
[@desc, extract_date, extract_tags]
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def extract_date
|
14
|
+
date_pattern = /@[A-Z0-9,-\/]+/i
|
15
|
+
date = @desc.scan(date_pattern).last
|
16
|
+
|
17
|
+
return nil if date.nil?
|
18
|
+
|
19
|
+
date.gsub!('@', '')
|
20
|
+
strip_tags(date_pattern)
|
21
|
+
|
22
|
+
begin
|
23
|
+
Chronic.parse(date).to_date
|
24
|
+
rescue NoMethodError
|
25
|
+
raise InvalidDateFormatError
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def extract_tags
|
30
|
+
tag_pattern = /#[A-Z0-9_]+/i
|
31
|
+
tags = @desc.scan(tag_pattern).map { |tag| tag.gsub!('#', '') }
|
32
|
+
|
33
|
+
strip_tags(tag_pattern)
|
34
|
+
|
35
|
+
tags
|
36
|
+
end
|
37
|
+
|
38
|
+
def strip_tags(pattern)
|
39
|
+
@desc.gsub!(pattern, '')
|
40
|
+
@desc.strip!
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Nin
|
2
|
+
module Presenter
|
3
|
+
class ItemPresenter < ::SimpleDelegator
|
4
|
+
def call(options = {})
|
5
|
+
_id, _completed, _date, _desc, _tags = id, decorate_completed, decorate_date, decorate_desc, decorate_tags
|
6
|
+
|
7
|
+
separating_spaces = options.fetch(:separating_spaces, 2)
|
8
|
+
id_spaces = options.fetch(:longest_id, 1) + separating_spaces
|
9
|
+
completed_spaces = _completed.length + separating_spaces
|
10
|
+
date_spaces = _date.length + (options.fetch(:longest_date, 11) - _date.remove_color.length) + separating_spaces
|
11
|
+
|
12
|
+
sprintf("%-#{id_spaces}d %-#{completed_spaces}s %-#{date_spaces}s %s %s",
|
13
|
+
_id, _completed, _date, _desc, _tags)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def decorate_desc
|
19
|
+
if self.archived?
|
20
|
+
self.desc.yellow
|
21
|
+
elsif self.completed?
|
22
|
+
self.desc.white
|
23
|
+
else
|
24
|
+
self.desc
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def decorate_completed
|
29
|
+
if self.completed?
|
30
|
+
'[x]'.green
|
31
|
+
else
|
32
|
+
'[ ]'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def decorate_date
|
37
|
+
date_in_words = '@' << self.date.humanize
|
38
|
+
|
39
|
+
if self.past?
|
40
|
+
date_in_words.magenta
|
41
|
+
elsif self.today?
|
42
|
+
date_in_words.bold.cyan
|
43
|
+
else
|
44
|
+
date_in_words.cyan
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def decorate_tags
|
49
|
+
self.tags.map { |tag| tag.dup.prepend('#') }.join(' ').blue
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Nin
|
2
|
+
module Presenter
|
3
|
+
class TodoPresenter < ::SimpleDelegator
|
4
|
+
def call
|
5
|
+
return 'No todo items yet. Call `nin add hello world` to add a new item.' if self.empty?
|
6
|
+
|
7
|
+
formatting_options = { separating_spaces: 4,
|
8
|
+
longest_id: self.map(&:id).max.length,
|
9
|
+
longest_date: 11 }
|
10
|
+
|
11
|
+
self.map do |item|
|
12
|
+
Presenter::ItemPresenter.new(item).call(formatting_options)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/nin/todo.rb
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
|
3
|
+
module Nin
|
4
|
+
class Todo
|
5
|
+
attr_accessor :items
|
6
|
+
attr_reader :store
|
7
|
+
|
8
|
+
def initialize(config, options = {})
|
9
|
+
@store = config.fetch(:store)
|
10
|
+
@integration_syncrhonizer = config.fetch(:integration_syncrhonizer, nil)
|
11
|
+
@options = options
|
12
|
+
@items = load_items_sorted
|
13
|
+
end
|
14
|
+
|
15
|
+
def list
|
16
|
+
sync(:read, true, items: @items, next_id: next_id) unless @options[:local]
|
17
|
+
|
18
|
+
items_to_list = if @options[:archived]
|
19
|
+
@items
|
20
|
+
else
|
21
|
+
unarchived_items
|
22
|
+
end
|
23
|
+
|
24
|
+
puts Presenter::TodoPresenter.new(items_to_list).call
|
25
|
+
end
|
26
|
+
|
27
|
+
def add(desc, date, tags)
|
28
|
+
item = Item.new(next_id, desc, date, tags)
|
29
|
+
@items << item
|
30
|
+
|
31
|
+
fork_sync(:add, true, item: item)
|
32
|
+
|
33
|
+
@store.write(to_hash)
|
34
|
+
end
|
35
|
+
|
36
|
+
def edit(id, desc, date, tags)
|
37
|
+
item = find_by_id(id)
|
38
|
+
item.edit(desc, date, tags)
|
39
|
+
|
40
|
+
fork_sync(:edit, false, items: item)
|
41
|
+
|
42
|
+
@store.write(to_hash)
|
43
|
+
end
|
44
|
+
|
45
|
+
def prioritize(id, step = 1)
|
46
|
+
item_to_prioritize = find_by_id(id)
|
47
|
+
item_group = @items.group_by(&:date)[item_to_prioritize.date]
|
48
|
+
|
49
|
+
new_id, actual_step = item_group.map(&:id).round_shift(id, step)
|
50
|
+
step_sign = actual_step > 0 ? +1 : -1
|
51
|
+
|
52
|
+
items_to_reprioritize = item_group.where(:id) do |item_id|
|
53
|
+
step_sign * item_id < step_sign * id
|
54
|
+
end.limit(actual_step)
|
55
|
+
|
56
|
+
item_to_prioritize.id = new_id
|
57
|
+
items_to_reprioritize.each { |item| item.id += step_sign }
|
58
|
+
|
59
|
+
@store.write(to_hash)
|
60
|
+
end
|
61
|
+
|
62
|
+
def complete(*ids)
|
63
|
+
items = ids.map do |id|
|
64
|
+
item = find_by_id(id.to_i)
|
65
|
+
item.toggle_completed!
|
66
|
+
|
67
|
+
item
|
68
|
+
end
|
69
|
+
|
70
|
+
fork_sync(:edit_completed, false, items: items)
|
71
|
+
|
72
|
+
@store.write(to_hash)
|
73
|
+
end
|
74
|
+
|
75
|
+
def archive(*ids)
|
76
|
+
unless @options[:completed_only]
|
77
|
+
ids.each do |id|
|
78
|
+
item = find_by_id(id.to_i)
|
79
|
+
item.toggle_archived!
|
80
|
+
end
|
81
|
+
else
|
82
|
+
completed_unarchived_items.each(&:toggle_archived!)
|
83
|
+
end
|
84
|
+
|
85
|
+
@store.write(to_hash)
|
86
|
+
end
|
87
|
+
|
88
|
+
def delete_archived
|
89
|
+
delete(*archived_items.map(&:id))
|
90
|
+
end
|
91
|
+
|
92
|
+
def delete(*ids)
|
93
|
+
uids = ids.map do |id|
|
94
|
+
item = find_by_id(id.to_i)
|
95
|
+
@items.delete(item)
|
96
|
+
|
97
|
+
item.uid
|
98
|
+
end
|
99
|
+
|
100
|
+
fork_sync(:delete, false, ids: uids)
|
101
|
+
|
102
|
+
reset_item_indices!
|
103
|
+
end
|
104
|
+
|
105
|
+
def analyze
|
106
|
+
items_to_analyze = @items.where(:completed, true)
|
107
|
+
|
108
|
+
histogram = items_to_analyze.group_by(&:date).map { |k, v| [k, v.size] }
|
109
|
+
histogram.each do |date, size|
|
110
|
+
puts "#{date} : #{'*' * size}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def sync(op, store_write = false, params = {})
|
115
|
+
return unless @integration_syncrhonizer
|
116
|
+
|
117
|
+
begin
|
118
|
+
Timeout::timeout(@integration_syncrhonizer.timeout_interval) {
|
119
|
+
@integration_syncrhonizer.sync(op, params)
|
120
|
+
reset_item_indices! if store_write
|
121
|
+
}
|
122
|
+
rescue Timeout::Error
|
123
|
+
puts 'Syncing timed out. Showing local items...'
|
124
|
+
puts
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def fork_sync(op, store_write = false, params = {})
|
129
|
+
return unless @integration_syncrhonizer
|
130
|
+
|
131
|
+
pid = fork do
|
132
|
+
@integration_syncrhonizer.sync(op, params)
|
133
|
+
@store.write(to_hash) if store_write
|
134
|
+
|
135
|
+
exit
|
136
|
+
end
|
137
|
+
Process.detach(pid)
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def load_items_sorted
|
143
|
+
load_items.sort_by { |item| [item.date, item.id] }
|
144
|
+
end
|
145
|
+
|
146
|
+
def load_items
|
147
|
+
items = []
|
148
|
+
@store.read.map do |key, values|
|
149
|
+
date = key.dup
|
150
|
+
values.map do |item|
|
151
|
+
items << Item.new(item.fetch('id').to_i,
|
152
|
+
item.fetch('desc'),
|
153
|
+
date,
|
154
|
+
item.fetch('tags'),
|
155
|
+
item.fetch('uid', nil),
|
156
|
+
item.fetch('completed'),
|
157
|
+
item.fetch('archived'))
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
items
|
162
|
+
end
|
163
|
+
|
164
|
+
def archived_items
|
165
|
+
@items.where(:archived?, true)
|
166
|
+
end
|
167
|
+
|
168
|
+
def unarchived_items
|
169
|
+
@items.where(:archived?, false)
|
170
|
+
end
|
171
|
+
|
172
|
+
def completed_unarchived_items
|
173
|
+
unarchived_items.where(:completed, true)
|
174
|
+
end
|
175
|
+
|
176
|
+
def find_by_id(id)
|
177
|
+
found_item = @items.find_by(:id, id)
|
178
|
+
|
179
|
+
raise ItemNotFoundError unless found_item
|
180
|
+
|
181
|
+
found_item
|
182
|
+
end
|
183
|
+
|
184
|
+
def to_hash
|
185
|
+
groupped_items.reduce({}) do |hash, (date, items)|
|
186
|
+
hash.update(date => items.map(&:to_h))
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def groupped_items
|
191
|
+
@items.group_by { |item| item.date.to_s }
|
192
|
+
end
|
193
|
+
|
194
|
+
def next_id
|
195
|
+
last_id + 1
|
196
|
+
end
|
197
|
+
|
198
|
+
def last_id
|
199
|
+
begin
|
200
|
+
@items.sort_by(&:id).last.id
|
201
|
+
rescue NoMethodError
|
202
|
+
0
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def reset_item_indices!
|
207
|
+
@items.each.with_index(1) do |item, index|
|
208
|
+
item.id = index
|
209
|
+
end
|
210
|
+
|
211
|
+
@store.write(to_hash)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
data/lib/nin/version.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module Nin
|
2
|
+
class YamlStore
|
3
|
+
DEFAULT_FILE = "#{ENV['HOME']}/.todos.yaml"
|
4
|
+
|
5
|
+
attr_reader :file
|
6
|
+
|
7
|
+
def initialize(file = DEFAULT_FILE)
|
8
|
+
@file = file
|
9
|
+
|
10
|
+
init_store
|
11
|
+
end
|
12
|
+
|
13
|
+
def read
|
14
|
+
Psych.load_file(@file) || {} # Psych returns false if the file is empty
|
15
|
+
end
|
16
|
+
|
17
|
+
def write(hash)
|
18
|
+
File.open(@file, 'w') do |file|
|
19
|
+
file.write(Psych.dump(hash))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def init_store
|
26
|
+
return if File.exist?(@file)
|
27
|
+
|
28
|
+
File.open(@file, "w")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/nin.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require File.expand_path('../lib/nin/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Ahmed Saleh"]
|
6
|
+
gem.email = 'aonemdsaleh@gmail.com'
|
7
|
+
gem.description = "A simple, full-featured command line todo app"
|
8
|
+
gem.summary = "A command line todo app that provides an easy to use CLI for everyday todo management"
|
9
|
+
gem.homepage = 'https://github.com/aonemd/nin'
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\).reject do |f|
|
12
|
+
f.match(%r{^(test|spec|features)/})
|
13
|
+
end
|
14
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
15
|
+
gem.executables = ["nin"]
|
16
|
+
|
17
|
+
gem.name = 'nin'
|
18
|
+
gem.version = Nin::VERSION
|
19
|
+
gem.license = 'MIT'
|
20
|
+
|
21
|
+
gem.add_dependency 'chronic'
|
22
|
+
gem.add_dependency 'colored2'
|
23
|
+
gem.add_dependency 'http'
|
24
|
+
|
25
|
+
gem.add_development_dependency 'rake'
|
26
|
+
gem.add_development_dependency 'minitest'
|
27
|
+
end
|
metadata
CHANGED
@@ -1,27 +1,132 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nin
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ahmed Saleh
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
13
|
-
|
11
|
+
date: 2020-10-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: chronic
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: colored2
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: http
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: minitest
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: A simple, full-featured command line todo app
|
14
84
|
email: aonemdsaleh@gmail.com
|
15
|
-
executables:
|
85
|
+
executables:
|
86
|
+
- nin
|
16
87
|
extensions: []
|
17
88
|
extra_rdoc_files: []
|
18
89
|
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- ".travis.yml"
|
92
|
+
- Gemfile
|
93
|
+
- LICENSE
|
94
|
+
- README.md
|
95
|
+
- Rakefile
|
96
|
+
- bin/nin
|
97
|
+
- demo.gif
|
19
98
|
- lib/nin.rb
|
20
|
-
|
99
|
+
- lib/nin/command.rb
|
100
|
+
- lib/nin/error.rb
|
101
|
+
- lib/nin/extensions/array_extensions.rb
|
102
|
+
- lib/nin/extensions/date_extensions.rb
|
103
|
+
- lib/nin/extensions/integer_extensions.rb
|
104
|
+
- lib/nin/extensions/nil_class_extensions.rb
|
105
|
+
- lib/nin/extensions/object_extensions.rb
|
106
|
+
- lib/nin/extensions/string_extensions.rb
|
107
|
+
- lib/nin/integration/service.rb
|
108
|
+
- lib/nin/integration/service/todoist.rb
|
109
|
+
- lib/nin/integration/synchronizer.rb
|
110
|
+
- lib/nin/integration/synchronizer/todoist.rb
|
111
|
+
- lib/nin/integration/todoist.rb
|
112
|
+
- lib/nin/integration/todoist/client.rb
|
113
|
+
- lib/nin/integration/todoist/client/base_client.rb
|
114
|
+
- lib/nin/integration/todoist/client/item.rb
|
115
|
+
- lib/nin/integration/todoist/client/project.rb
|
116
|
+
- lib/nin/integration/todoist/client/sync.rb
|
117
|
+
- lib/nin/item.rb
|
118
|
+
- lib/nin/parser.rb
|
119
|
+
- lib/nin/presenters/item_presenter.rb
|
120
|
+
- lib/nin/presenters/todo_presenter.rb
|
121
|
+
- lib/nin/todo.rb
|
122
|
+
- lib/nin/version.rb
|
123
|
+
- lib/nin/yaml_store.rb
|
124
|
+
- nin.gemspec
|
125
|
+
homepage: https://github.com/aonemd/nin
|
21
126
|
licenses:
|
22
127
|
- MIT
|
23
128
|
metadata: {}
|
24
|
-
post_install_message:
|
129
|
+
post_install_message:
|
25
130
|
rdoc_options: []
|
26
131
|
require_paths:
|
27
132
|
- lib
|
@@ -36,9 +141,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
36
141
|
- !ruby/object:Gem::Version
|
37
142
|
version: '0'
|
38
143
|
requirements: []
|
39
|
-
|
40
|
-
|
41
|
-
signing_key:
|
144
|
+
rubygems_version: 3.1.2
|
145
|
+
signing_key:
|
42
146
|
specification_version: 4
|
43
|
-
summary:
|
147
|
+
summary: A command line todo app that provides an easy to use CLI for everyday todo
|
148
|
+
management
|
44
149
|
test_files: []
|