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.
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Nin
2
+ VERSION='1.3.0'
3
+ end
@@ -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
@@ -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: 0.0.0
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: 2018-12-07 00:00:00.000000000 Z
12
- dependencies: []
13
- description: Under construction
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
- homepage: https://github.com/gems/nin
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
- rubyforge_project:
40
- rubygems_version: 2.7.6
41
- signing_key:
144
+ rubygems_version: 3.1.2
145
+ signing_key:
42
146
  specification_version: 4
43
- summary: Under construction
147
+ summary: A command line todo app that provides an easy to use CLI for everyday todo
148
+ management
44
149
  test_files: []