nin 0.0.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []