nin 0.0.0 → 1.3.0
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.
- 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: []
|