cnote 0.1.0 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9df0e76707bb7a63ff3b121e9e0b4fa6143b66c7
4
- data.tar.gz: 9950e8a6fff2d2c7de76fc9883a211b65312696e
3
+ metadata.gz: 54afc4391700ef178bc0ed861e49cdbcfe30cc73
4
+ data.tar.gz: a5aab3d03e17f4ae079f137e024ba6c6bf733373
5
5
  SHA512:
6
- metadata.gz: ee7ab59688e6773eb02255983795aca1c3957f6bd49ffc8fada57862017121d833cff46d974eef1b937bb92c806878d1b755bef8e4a7365bb7ac876c813b1d84
7
- data.tar.gz: 26d390e8ac70d6179dd256f6fa0e37a262fc1ff4dc26ee956c88c98b87a48b75066b04f37c37792d5edf3d13bb8e585a1f886410a4f1277c1902f8a6b691f877
6
+ metadata.gz: c881765e7c0c228e0e01f9c12c56ba06f9341eb0c9e184fff491d6422f61485e23a0b289f34c2f92e10a22e22ae6fae3d8959733b33a26a29f4abd3116b8854b
7
+ data.tar.gz: 624d176e63393ffa9e5f91124418ecf4780306e7227c14e84d9af043d923ccbda5ae85f5af3d5684244c06acd869efbffd0ee7681991b5738324f1e143b1bb82
data/.gitignore CHANGED
@@ -7,6 +7,7 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ *.gem
10
11
 
11
12
  # rspec failure tracking
12
13
  .rspec_status
data/Gemfile CHANGED
@@ -2,11 +2,5 @@ source "https://rubygems.org"
2
2
 
3
3
  git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
4
 
5
- # Added at 2017-09-09 01:27:23 -0700 by tony:
6
- gem "awesome_print", "~> 1.8"
7
-
8
- # Added at 2017-09-09 03:13:22 -0700 by tony:
9
- gem "colorize", "~> 0.8.1"
10
-
11
5
  # Specify your gem's dependencies in cnote.gemspec
12
6
  gemspec
data/classes/config.rb ADDED
@@ -0,0 +1,66 @@
1
+ class Config
2
+ attr_reader :note_path
3
+
4
+ def initialize(path)
5
+ path = File.expand_path path
6
+
7
+ if !File.exists? path
8
+ puts 'Welcome, new user!'
9
+
10
+ @note_path = get_note_path
11
+
12
+ File.open(path, 'w') do |file|
13
+ file.write(YAML.dump(to_hash))
14
+ end
15
+
16
+ puts "Okay, we're ready to go!"
17
+ else
18
+ conf = YAML.load(File.read(path))
19
+
20
+ @note_path = conf['note_path']
21
+ @editor = conf['editor']
22
+ @cursor = conf['prompt_cursor']
23
+ end
24
+ end
25
+
26
+ def get_note_path
27
+ path = nil
28
+
29
+ while !path or !File.exists? path
30
+ print "Enter a path for your note folder: "
31
+
32
+ path = File.expand_path gets.chomp
33
+
34
+ if File.exists? path
35
+ if !File.directory? path
36
+ puts "Hey, that's not a folder!"
37
+ end
38
+ else
39
+ puts "That folder doesn't exist yet. Do you want to create it?"
40
+ case gets.strip.downcase
41
+ when "y", "yes", "yeah", "sure", "ok", "okay", "alright", "yep", "yup"
42
+ FileUtils.mkdir_p path
43
+ puts "Done!"
44
+ else
45
+ puts "Okay."
46
+ end
47
+ end
48
+ end
49
+
50
+ return path
51
+ end
52
+
53
+ def editor
54
+ @editor || ENV['EDITOR']
55
+ end
56
+
57
+ def cursor
58
+ @cursor || '>'
59
+ end
60
+
61
+ def to_hash
62
+ {
63
+ 'note_path' => @note_path
64
+ }
65
+ end
66
+ end
data/classes/note.rb ADDED
@@ -0,0 +1,117 @@
1
+ require 'time'
2
+
3
+ class Note
4
+ attr_reader :title,
5
+ :content,
6
+ :tags,
7
+ :filename,
8
+ :path,
9
+ :modified,
10
+ :created
11
+
12
+ attr_writer :created
13
+
14
+ def initialize(path)
15
+ @meta_regex = /^<!\-{3}(.*)\-{2}>/
16
+
17
+ @content = ''
18
+ @tags = []
19
+ @filename = File.basename(path)
20
+ @path = path
21
+
22
+ refresh
23
+
24
+ @modified = File.mtime(@path) if !@modified
25
+ @created = @modified if !@created
26
+
27
+ @title = 'Untitled' if !@title
28
+ end
29
+
30
+ def refresh
31
+ File.open(@path, 'r') do |file|
32
+ file.each_line do |line|
33
+ line = line.strip
34
+ if @meta_regex =~ line
35
+ parse_meta($~[1])
36
+ elsif !@title
37
+ if line != ''
38
+ @title = line.gsub(/#|[^a-z0-9\s\.\-]/i, '').strip
39
+ end
40
+ else
41
+ @content << line + "\n"
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def add_tags(tags)
48
+ @tags = @tags.concat(tags)
49
+ @modified = Time.new
50
+ write_meta
51
+ end
52
+
53
+ def remove_tags(tags)
54
+ @tags = @tags - tags
55
+ @modified = Time.new
56
+ write_meta
57
+ end
58
+
59
+ def excerpt
60
+ @content.gsub(/[#*\-~]/i, '').strip.slice(0, 80)
61
+ end
62
+
63
+ def time_fmt(time)
64
+ time.strftime('%A, %B %e %Y, %l:%M:%S%p')
65
+ end
66
+
67
+ def update
68
+ @modified = Time.new
69
+ write_meta
70
+ end
71
+
72
+ private def parse_meta(meta)
73
+ key, value = meta.split(':', 2).map { |v| v.strip }
74
+
75
+ case key.downcase
76
+ when 'tags'
77
+ @tags = value.split(',').map { |v| v.strip }
78
+ when 'created'
79
+ puts value
80
+ @created = Time.parse(value)
81
+ when 'modified'
82
+ puts value
83
+ @modified = Time.parse(value)
84
+ end
85
+ end
86
+
87
+ private def write_meta
88
+ meta_regex = /<!\-{3}.+:(.*)\-{2}>/
89
+
90
+ File.open(@path, 'r') do |file|
91
+ contents = file.read
92
+
93
+ contents.gsub!(meta_regex, '')
94
+
95
+ trailing_empty = 0
96
+ contents.lines.reverse.each do |line|
97
+ if line.strip == ''
98
+ trailing_empty += 1
99
+ else
100
+ break
101
+ end
102
+ end
103
+
104
+ # Leave two empty lines before metadata.
105
+ contents = contents.lines.slice(0, contents.lines.length - trailing_empty).join('')
106
+
107
+ contents += "\n\n"
108
+ contents += "<!--- created: #{@created} -->\n"
109
+ contents += "<!--- modified: #{@modified} -->\n"
110
+ contents += "<!--- tags: #{@tags.join(', ')} -->\n"
111
+
112
+ File.open(@path, 'w') do |file|
113
+ file.write(contents)
114
+ end
115
+ end
116
+ end
117
+ end
data/classes/notes.rb ADDED
@@ -0,0 +1,348 @@
1
+ require 'ap'
2
+ require 'colorize'
3
+ require 'fileutils'
4
+ require 'time'
5
+ require_relative 'note'
6
+
7
+ class Notes
8
+ def initialize(config)
9
+ @config = config
10
+ @notes = Dir[File.join(@config.note_path, '**', '*')].select do |file|
11
+ File.extname(file) == '.md'
12
+ end.map do |file|
13
+ Note.new(file)
14
+ end
15
+ end
16
+
17
+ #/================================\#
18
+ # REPL type thing #
19
+ #\================================/#
20
+
21
+ def await_command(message = nil)
22
+ puts message if message
23
+ print "#{@config.cursor} ".bold.magenta
24
+ input = STDIN.gets.chomp
25
+
26
+ # Strip and process
27
+ action, *params = input.strip.gsub(/\s{2,}/, ' ').split(' ')
28
+ run_command(action || 'help', params)
29
+ end
30
+
31
+ def run_command(action, params)
32
+ case action.downcase
33
+ when 'new', 'create', 'n', 'c'
34
+ create(params)
35
+ when 'edit', 'open', 'e', 'o'
36
+ open(params)
37
+ when 'delete', 'd', 'rm'
38
+ delete(params)
39
+ when 'peek', 'p'
40
+ peek(params)
41
+ when 'tag', 't'
42
+ tag(params)
43
+ when 'untag', 'ut'
44
+ untag(params)
45
+ when 'search', 'find', 's', 'f'
46
+ search(params.join(' '))
47
+ when 'list', 'l', 'ls'
48
+ list
49
+ when 'help', 'h'
50
+ help
51
+ when 'quit', 'exit', 'close', 'q'
52
+ exit
53
+ else
54
+ puts "Sorry, didn't quite get that..."
55
+ help
56
+ end
57
+
58
+ await_command # Drop back to REPL
59
+ end
60
+
61
+ #/================================\#
62
+ # The Commands #
63
+ #\================================/#
64
+
65
+ def search(term)
66
+ term = term.downcase # Search is case insensitive
67
+ matches = @notes
68
+
69
+ if term.include? '+t '
70
+ term, tags = term.split('+t ')
71
+ tags = tags.split(' ')
72
+ puts "\n Searching: '#{term.strip}' with tags: #{tags}"
73
+ matches = matches.select do |note|
74
+ has_all_tags(note, tags)
75
+ end
76
+ elsif term.include? '-t '
77
+ term, tags = term.split('-t ')
78
+ tags = tags.split(' ')
79
+ puts "\n Searching: '#{term.strip}' without tags: #{tags}"
80
+ matches = matches.select do |note|
81
+ has_none_tags(note, tags)
82
+ end
83
+ end
84
+
85
+ term.strip!
86
+
87
+ @filtered = matches.select do |note|
88
+ note.title.downcase.include?(term) || note.content.downcase.include?(term)
89
+ end
90
+
91
+ # TODO: Sort by most relevant
92
+ # TODO: Highlight keywords where found
93
+ len = @filtered.length
94
+
95
+ print_list("Found #{len} Match#{'es' if len != 1}", @filtered)
96
+ end
97
+
98
+ def create(params)
99
+ if params.first
100
+ dirname = File.dirname(params.first)
101
+ new_filename = File.basename(params.first, File.extname(params.first)) + '.md'
102
+ rel_path = ''
103
+ tags = []
104
+
105
+ if params.include? '+t'
106
+ tags = params.slice(params.index('+t') + 1, params.length)
107
+ puts "CREATING WITH TAGS: #{tags}"
108
+ end
109
+
110
+ if dirname != '.'
111
+ rel_path = dirname
112
+ .gsub(@config.note_path, '')
113
+ .gsub(File.basename(params.first), '')
114
+ end
115
+
116
+ full_path = File.join(@config.note_path, rel_path, new_filename)
117
+
118
+ if File.exists?(full_path)
119
+ if confirm("#{'Whoa!'.bold.red} That file already exists. Overwrite it?")
120
+ File.delete(full_path)
121
+ @notes.each do |note|
122
+ if note.path == full_path
123
+ @notes.delete(note)
124
+ puts 'Removed!'
125
+ end
126
+ end
127
+ else
128
+ return
129
+ end
130
+ end
131
+
132
+ system "#{@config.editor} '#{full_path}'"
133
+
134
+ note = Note.new(full_path)
135
+ note.add_tags(tags) if tags.length > 0
136
+ note.created = Time.new
137
+ note.update
138
+
139
+ @notes << Note.new(full_path)
140
+
141
+ print_list('Created', [note])
142
+ @filtered = [note]
143
+ else
144
+ puts "Please enter a filename as the first parameter"
145
+ end
146
+ end
147
+
148
+ def open(params)
149
+ num = params.first.to_i
150
+ note = @filtered[num - 1]
151
+
152
+ if note
153
+ system "#{@config.editor} '#{note.path}'"
154
+ note.update
155
+ else
156
+ puts "Hey! There is no note #{num}! Nice try."
157
+ end
158
+ end
159
+
160
+ def delete(params)
161
+ num = params.first.to_i
162
+ note = @filtered[num - 1]
163
+
164
+ if note
165
+ if confirm("You're #{'sure'.italic} you want to delete note #{num.to_s.bold.white} with title #{note.title.bold.white}?")
166
+ FileUtils.rm(note.path)
167
+ @notes.delete(note)
168
+ @filtered.delete(note)
169
+ puts "Deleted!"
170
+ else
171
+ puts "Whew! That was close."
172
+ end
173
+ else
174
+ puts "Looks like my job is done here, since note #{num} doesn't exist anyway!"
175
+ end
176
+ end
177
+
178
+ def peek(params)
179
+ note = @filtered[params.first.to_i - 1]
180
+ if note
181
+ puts
182
+ puts '-' * 40
183
+ puts note.title.bold.white
184
+ puts note.content.split("\n").slice(0, 10)
185
+ puts
186
+ puts "... (cont'd) ...".italic.gray
187
+ puts '-' * 40
188
+ puts
189
+ else
190
+ puts "Note doesn't exist!"
191
+ end
192
+ end
193
+
194
+ def tag(params)
195
+ notes = multi_note(params)
196
+
197
+ notes.each do |note|
198
+ tags = params.slice(1, params.length)
199
+ note.add_tags(tags)
200
+ end
201
+
202
+ print_list('Changed', notes)
203
+
204
+ @filtered = notes
205
+
206
+ puts "Added #{params.length - 1} tag#{'s' if params.length != 2} to #{notes.length} note#{'s' if notes.length != 1}."
207
+ end
208
+
209
+ def untag(params)
210
+ notes = multi_note(params)
211
+
212
+ notes.each do |note|
213
+ tags = params.slice(1, params.length)
214
+ note.remove_tags(tags)
215
+ end
216
+
217
+ print_list('Changed', notes)
218
+
219
+ @filtered = notes
220
+
221
+ puts "Removed #{params.length - 1} tag#{'s' if params.length != 2} from #{notes.length} note#{'s' if notes.length != 1}."
222
+ end
223
+
224
+ def help
225
+ puts
226
+ puts "Enter a command with the structure:"
227
+ puts " #{'>'.bold.magenta} action parameter(s)"
228
+ puts
229
+ puts "Actions:"
230
+ puts " - #{'new'.bold.white} #{'filename'.italic}"
231
+ puts " - #{'edit'.bold.white} #{'note_number'.italic}"
232
+ puts " - #{'delete'.bold.white} #{'note_number'.italic}"
233
+ puts " - #{'peek'.bold.white} #{'note_number'.italic}"
234
+ puts " - #{'tag'.bold.white} #{'note_number'.italic}"
235
+ puts " - #{'untag'.bold.white} #{'note_number'.italic}"
236
+ puts " - #{'search'.bold.white} #{'search_term'.italic}"
237
+ puts " - #{'list'.bold.white}"
238
+ puts " - #{'exit'.bold.white}"
239
+ puts " - #{'help'.bold.white}"
240
+ puts
241
+ puts "Alternate actions:"
242
+ puts " Most actions also have aliases that do the same thing."
243
+ puts " These are listed for each command:"
244
+ puts " - new: create, c, n"
245
+ puts " - edit: e, open, o"
246
+ puts " - delete: d, rm"
247
+ puts " - peek: p"
248
+ puts " - tag: t"
249
+ puts " - untag: ut"
250
+ puts " - search: find, f, s"
251
+ puts " - list: l, ls"
252
+ puts " - exit: quit, q, close"
253
+ puts " - help: h"
254
+ puts
255
+ end
256
+
257
+ def list
258
+ @filtered = recently_edited_first(@notes)
259
+ print_list('All Notes', @filtered)
260
+ end
261
+
262
+ #/================================\#
263
+ # Utilities #
264
+ #\================================/#
265
+
266
+ private def print_list(title, notes)
267
+ path_length = @config.note_path.split('/').length
268
+ i = 0
269
+
270
+ puts
271
+ puts " #{title}".bold
272
+ puts " #{'-' * title.length}"
273
+ puts
274
+
275
+ notes.each do |note|
276
+ i += 1
277
+ puts "#{i}.".ljust(4) + note.title.bold
278
+ puts " #{note.path.gsub(@config.note_path, '')}".italic.light_magenta
279
+ if note.tags.length > 0
280
+ tags = note.tags.map { |tag| tag.yellow }
281
+ puts " tags: " + "[#{tags.join('] [')}]"
282
+ else
283
+ puts " <no tags>".gray
284
+ end
285
+ puts ' modified: ' + note.modified.strftime('%a, %b %e %Y, %l:%M%P').italic
286
+ puts ' created: ' + note.created.strftime('%a, %b %e %Y, %l:%M%P').italic
287
+ puts
288
+ end
289
+
290
+ puts " Listed #{i.to_s.bold} Notes"
291
+ puts
292
+ end
293
+
294
+ private def confirm(message = 'Confirm')
295
+ print "#{message} [y/n]"
296
+ case gets.chomp.strip.downcase
297
+ when 'y', 'yes', 'yeah', 'sure', 'yep', 'okay', 'aye'
298
+ return true
299
+ when 'n', 'no', 'nope', 'nay'
300
+ return false
301
+ else
302
+ return confirm("Sorry, didn't quite get that...")
303
+ end
304
+ end
305
+
306
+ private def multi_note(params)
307
+ notes = []
308
+
309
+ params.first.split(',').each do |num|
310
+ note = @filtered[num.to_i - 1]
311
+ if note
312
+ notes << note
313
+ else
314
+ puts "Note #{num} doesn't exist!"
315
+ end
316
+ end
317
+
318
+ notes
319
+ end
320
+
321
+ private def recently_edited_first(notes)
322
+ notes.sort_by { |note| note.modified }.reverse
323
+ end
324
+
325
+ private def has_all_tags(note, tags)
326
+ has = true
327
+ note_tags = note.tags
328
+ tags.each do |tag|
329
+ if !note_tags.include? tag
330
+ has = false
331
+ break
332
+ end
333
+ end
334
+ has
335
+ end
336
+
337
+ private def has_none_tags(note, tags)
338
+ doesnt_have = true
339
+ note_tags = note.tags
340
+ tags.each do |tag|
341
+ if note_tags.include? tag
342
+ doesnt_have = false
343
+ break
344
+ end
345
+ end
346
+ doesnt_have
347
+ end
348
+ end
data/gems.locked ADDED
@@ -0,0 +1,14 @@
1
+ GEM
2
+ specs:
3
+ awesome_print (1.8.0)
4
+ colorize (0.8.1)
5
+
6
+ PLATFORMS
7
+ ruby
8
+
9
+ DEPENDENCIES
10
+ awesome_print (~> 1.8)
11
+ colorize (~> 0.8.1)
12
+
13
+ BUNDLED WITH
14
+ 1.15.4
data/gems.rb ADDED
@@ -0,0 +1,6 @@
1
+
2
+ # Added at 2017-09-09 01:27:23 -0700 by tony:
3
+ gem "awesome_print", "~> 1.8"
4
+
5
+ # Added at 2017-09-09 03:13:22 -0700 by tony:
6
+ gem "colorize", "~> 0.8.1"
data/lib/cnote.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require "colorize"
2
- require_relative "cnote/config"
3
- require_relative "cnote/notes"
4
- require_relative "cnote/version"
2
+ require "cnote/config"
3
+ require "cnote/notes"
4
+ require "cnote/version"
5
5
 
6
6
  # module Cnote
7
7
  # Your code goes here...
@@ -0,0 +1,69 @@
1
+ require "yaml"
2
+ require "fileutils"
3
+
4
+ class Config
5
+ attr_reader :note_path
6
+
7
+ def initialize(path)
8
+ path = File.expand_path path
9
+
10
+ if !File.exists? path
11
+ puts "Welcome, new user!"
12
+
13
+ @note_path = get_note_path
14
+
15
+ File.open(path, "w") do |file|
16
+ file.write(YAML.dump(to_hash))
17
+ end
18
+
19
+ puts "Okay, we're ready to go!"
20
+ else
21
+ conf = YAML.load(File.read(path))
22
+
23
+ @note_path = conf["note_path"]
24
+ @editor = conf["editor"]
25
+ @cursor = conf["prompt"]
26
+ end
27
+ end
28
+
29
+ def get_note_path
30
+ path = nil
31
+
32
+ while !path or !File.exists? path
33
+ print "Enter a path for your note folder: "
34
+
35
+ path = File.expand_path gets.chomp
36
+
37
+ if File.exists? path
38
+ if !File.directory? path
39
+ puts "Hey, that's not a folder!"
40
+ end
41
+ else
42
+ puts "That folder doesn't exist yet. Do you want to create it?"
43
+ case gets.strip.downcase
44
+ when "y", "yes", "yeah", "sure", "ok", "okay", "alright", "yep", "yup"
45
+ FileUtils.mkdir_p path
46
+ puts "Done!"
47
+ else
48
+ puts "Okay."
49
+ end
50
+ end
51
+ end
52
+
53
+ return path
54
+ end
55
+
56
+ def editor
57
+ @editor || ENV["EDITOR"]
58
+ end
59
+
60
+ def cursor
61
+ @cursor || ">"
62
+ end
63
+
64
+ def to_hash
65
+ {
66
+ "note_path" => @note_path
67
+ }
68
+ end
69
+ end
data/lib/cnote/note.rb ADDED
@@ -0,0 +1,115 @@
1
+ require "time"
2
+
3
+ class Note
4
+ attr_reader :title,
5
+ :content,
6
+ :tags,
7
+ :filename,
8
+ :path,
9
+ :modified,
10
+ :created
11
+
12
+ attr_writer :created
13
+
14
+ def initialize(path)
15
+ @meta_regex = /^<!\-{3}(.*)\-{2}>/
16
+
17
+ @content = ""
18
+ @tags = []
19
+ @filename = File.basename(path)
20
+ @path = path
21
+
22
+ refresh
23
+
24
+ @modified = File.mtime(@path) if !@modified
25
+ @created = @modified if !@created
26
+
27
+ @title = "Untitled" if !@title
28
+ end
29
+
30
+ def refresh
31
+ File.open(@path, "r") do |file|
32
+ file.each_line do |line|
33
+ line = line.strip
34
+ if @meta_regex =~ line
35
+ parse_meta($~[1])
36
+ elsif !@title
37
+ if line != ""
38
+ @title = line.gsub(/#|[^a-z0-9\s\.\-]/i, "").strip
39
+ end
40
+ else
41
+ @content << line + "\n"
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def add_tags(tags)
48
+ @tags = @tags.concat(tags)
49
+ @modified = Time.new
50
+ write_meta
51
+ end
52
+
53
+ def remove_tags(tags)
54
+ @tags = @tags - tags
55
+ @modified = Time.new
56
+ write_meta
57
+ end
58
+
59
+ def excerpt
60
+ @content.gsub(/[#*\-~]/i, "").strip.slice(0, 80)
61
+ end
62
+
63
+ def time_fmt(time)
64
+ time.strftime("%A, %B %e %Y, %l:%M:%S%p")
65
+ end
66
+
67
+ def update
68
+ @modified = Time.new
69
+ write_meta
70
+ end
71
+
72
+ private def parse_meta(meta)
73
+ key, value = meta.split(":", 2).map { |v| v.strip }
74
+
75
+ case key.downcase
76
+ when "tags"
77
+ @tags = value.split(",").map { |v| v.strip }
78
+ when "created"
79
+ @created = Time.parse(value)
80
+ when "modified"
81
+ @modified = Time.parse(value)
82
+ end
83
+ end
84
+
85
+ private def write_meta
86
+ meta_regex = /<!\-{3}.+:(.*)\-{2}>/
87
+
88
+ File.open(@path, "r") do |file|
89
+ contents = file.read
90
+
91
+ contents.gsub!(meta_regex, "")
92
+
93
+ trailing_empty = 0
94
+ contents.lines.reverse.each do |line|
95
+ if line.strip == ""
96
+ trailing_empty += 1
97
+ else
98
+ break
99
+ end
100
+ end
101
+
102
+ # Leave two empty lines before metadata.
103
+ contents = contents.lines.slice(0, contents.lines.length - trailing_empty).join("")
104
+
105
+ contents += "\n\n"
106
+ contents += "<!--- created: #{@created} -->\n"
107
+ contents += "<!--- modified: #{@modified} -->\n"
108
+ contents += "<!--- tags: #{@tags.join(", ")} -->\n"
109
+
110
+ File.open(@path, "w") do |file|
111
+ file.write(contents)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,349 @@
1
+ require "ap"
2
+ require "colorize"
3
+ require "fileutils"
4
+ require "time"
5
+ require "cnote/note"
6
+
7
+ class Notes
8
+ def initialize(config)
9
+ @config = config
10
+ @notes = Dir[File.join(@config.note_path, "**", "*")].select do |file|
11
+ File.extname(file) == ".md"
12
+ end.map do |file|
13
+ Note.new(file)
14
+ end
15
+ @filtered = @notes
16
+ end
17
+
18
+ #/================================\#
19
+ # REPL type thing #
20
+ #\================================/#
21
+
22
+ def await_command(message = nil)
23
+ puts message if message
24
+ print "#{@config.cursor} ".magenta
25
+ input = STDIN.gets.chomp
26
+
27
+ # Strip and process
28
+ action, *params = input.strip.gsub(/\s{2,}/, " ").split(" ")
29
+ run_command(action || "help", params)
30
+ end
31
+
32
+ def run_command(action, params)
33
+ case action.downcase
34
+ when "new", "create", "n", "c"
35
+ create(params)
36
+ when "edit", "open", "e", "o"
37
+ open(params)
38
+ when "delete", "d", "rm"
39
+ delete(params)
40
+ when "peek", "p"
41
+ peek(params)
42
+ when "tag", "t"
43
+ tag(params)
44
+ when "untag", "ut"
45
+ untag(params)
46
+ when "search", "find", "s", "f"
47
+ search(params.join(" "))
48
+ when "list", "l", "ls"
49
+ list
50
+ when "help", "h"
51
+ help
52
+ when "quit", "exit", "close", "q"
53
+ exit
54
+ else
55
+ puts "Sorry, didn't quite get that..."
56
+ help
57
+ end
58
+
59
+ await_command # Drop back to REPL
60
+ end
61
+
62
+ #/================================\#
63
+ # The Commands #
64
+ #\================================/#
65
+
66
+ def search(term)
67
+ term = term.downcase # Search is case insensitive
68
+ matches = @notes
69
+
70
+ if term.include? "+t "
71
+ term, tags = term.split("+t ")
72
+ tags = tags.split(" ")
73
+ puts "\n Searching: '#{term.strip}' with tags: #{tags}"
74
+ matches = matches.select do |note|
75
+ has_all_tags(note, tags)
76
+ end
77
+ elsif term.include? "-t "
78
+ term, tags = term.split("-t ")
79
+ tags = tags.split(" ")
80
+ puts "\n Searching: '#{term.strip}' without tags: #{tags}"
81
+ matches = matches.select do |note|
82
+ has_none_tags(note, tags)
83
+ end
84
+ end
85
+
86
+ term.strip!
87
+
88
+ @filtered = matches.select do |note|
89
+ note.title.downcase.include?(term) || note.content.downcase.include?(term)
90
+ end
91
+
92
+ # TODO: Sort by most relevant
93
+ # TODO: Highlight keywords where found
94
+ len = @filtered.length
95
+
96
+ print_list("Found #{len} Match#{"es" if len != 1}", @filtered)
97
+ end
98
+
99
+ def create(params)
100
+ if params.first
101
+ dirname = File.dirname(params.first)
102
+ new_filename = File.basename(params.first, File.extname(params.first)) + ".md"
103
+ rel_path = ""
104
+ tags = []
105
+
106
+ if params.include? "+t"
107
+ tags = params.slice(params.index("+t") + 1, params.length)
108
+ puts "CREATING WITH TAGS: #{tags}"
109
+ end
110
+
111
+ if dirname != "."
112
+ rel_path = dirname
113
+ .gsub(@config.note_path, "")
114
+ .gsub(File.basename(params.first), "")
115
+ end
116
+
117
+ full_path = File.join(@config.note_path, rel_path, new_filename)
118
+
119
+ if File.exists?(full_path)
120
+ if confirm("#{"Whoa!".bold.red} That file already exists. Overwrite it?")
121
+ File.delete(full_path)
122
+ @notes.each do |note|
123
+ if note.path == full_path
124
+ @notes.delete(note)
125
+ puts "Removed!"
126
+ end
127
+ end
128
+ else
129
+ return
130
+ end
131
+ end
132
+
133
+ system "#{@config.editor} '#{full_path}'"
134
+
135
+ note = Note.new(full_path)
136
+ note.add_tags(tags) if tags.length > 0
137
+ note.created = Time.new
138
+ note.update
139
+
140
+ @notes << Note.new(full_path)
141
+
142
+ print_list("Created", [note])
143
+ @filtered = [note]
144
+ else
145
+ puts "Please enter a filename as the first parameter"
146
+ end
147
+ end
148
+
149
+ def open(params)
150
+ num = params.first.to_i
151
+ note = @filtered[num - 1]
152
+
153
+ if note
154
+ system "#{@config.editor} '#{note.path}'"
155
+ note.update
156
+ else
157
+ puts "Hey! There is no note #{num}! Nice try."
158
+ end
159
+ end
160
+
161
+ def delete(params)
162
+ num = params.first.to_i
163
+ note = @filtered[num - 1]
164
+
165
+ if note
166
+ if confirm("You're #{"sure".italic} you want to delete note #{num.to_s.bold.white} with title #{note.title.bold.white}?")
167
+ FileUtils.rm(note.path)
168
+ @notes.delete(note)
169
+ @filtered.delete(note)
170
+ puts "Deleted!"
171
+ else
172
+ puts "Whew! That was close."
173
+ end
174
+ else
175
+ puts "Looks like my job is done here, since note #{num} doesn't exist anyway!"
176
+ end
177
+ end
178
+
179
+ def peek(params)
180
+ note = @filtered[params.first.to_i - 1]
181
+ if note
182
+ puts
183
+ puts "-" * 40
184
+ puts note.title.bold.white
185
+ puts note.content.split("\n").slice(0, 10)
186
+ puts
187
+ puts "... (cont'd) ...".italic.gray
188
+ puts "-" * 40
189
+ puts
190
+ else
191
+ puts "Note doesn't exist!"
192
+ end
193
+ end
194
+
195
+ def tag(params)
196
+ notes = multi_note(params)
197
+
198
+ notes.each do |note|
199
+ tags = params.slice(1, params.length)
200
+ note.add_tags(tags)
201
+ end
202
+
203
+ print_list("Changed", notes)
204
+
205
+ @filtered = notes
206
+
207
+ puts "Added #{params.length - 1} tag#{"s" if params.length != 2} to #{notes.length} note#{"s" if notes.length != 1}."
208
+ end
209
+
210
+ def untag(params)
211
+ notes = multi_note(params)
212
+
213
+ notes.each do |note|
214
+ tags = params.slice(1, params.length)
215
+ note.remove_tags(tags)
216
+ end
217
+
218
+ print_list("Changed", notes)
219
+
220
+ @filtered = notes
221
+
222
+ puts "Removed #{params.length - 1} tag#{"s" if params.length != 2} from #{notes.length} note#{"s" if notes.length != 1}."
223
+ end
224
+
225
+ def help
226
+ puts
227
+ puts "Enter a command with the structure:"
228
+ puts " #{@config.cursor} action parameter(s)"
229
+ puts
230
+ puts "Actions:"
231
+ puts " - #{"new".bold.white} #{"filename".italic}"
232
+ puts " - #{"edit".bold.white} #{"note_number".italic}"
233
+ puts " - #{"delete".bold.white} #{"note_number".italic}"
234
+ puts " - #{"peek".bold.white} #{"note_number".italic}"
235
+ puts " - #{"tag".bold.white} #{"note_number".italic}"
236
+ puts " - #{"untag".bold.white} #{"note_number".italic}"
237
+ puts " - #{"search".bold.white} #{"search_term".italic}"
238
+ puts " - #{"list".bold.white}"
239
+ puts " - #{"exit".bold.white}"
240
+ puts " - #{"help".bold.white}"
241
+ puts
242
+ puts "Alternate actions:"
243
+ puts " Most actions also have aliases that do the same thing."
244
+ puts " These are listed for each command:"
245
+ puts " - new: create, c, n"
246
+ puts " - edit: e, open, o"
247
+ puts " - delete: d, rm"
248
+ puts " - peek: p"
249
+ puts " - tag: t"
250
+ puts " - untag: ut"
251
+ puts " - search: find, f, s"
252
+ puts " - list: l, ls"
253
+ puts " - exit: quit, q, close"
254
+ puts " - help: h"
255
+ puts
256
+ end
257
+
258
+ def list
259
+ @filtered = recently_edited_first(@notes)
260
+ print_list("All Notes", @filtered)
261
+ end
262
+
263
+ #/================================\#
264
+ # Utilities #
265
+ #\================================/#
266
+
267
+ private def print_list(title, notes)
268
+ path_length = @config.note_path.split("/").length
269
+ i = 0
270
+
271
+ puts
272
+ puts " #{title}".bold
273
+ puts " #{"-" * title.length}"
274
+ puts
275
+
276
+ notes.each do |note|
277
+ i += 1
278
+ puts "#{i}.".ljust(4) + note.title.bold
279
+ puts " #{note.path.gsub(@config.note_path, "")}".italic.light_magenta
280
+ if note.tags.length > 0
281
+ tags = note.tags.map { |tag| tag.yellow }
282
+ puts " tags: " + "[#{tags.join('] [')}]"
283
+ else
284
+ puts " <no tags>".gray
285
+ end
286
+ puts " modified: " + note.modified.strftime("%a, %b %e %Y, %l:%M%P").italic
287
+ puts " created: " + note.created.strftime("%a, %b %e %Y, %l:%M%P").italic
288
+ puts
289
+ end
290
+
291
+ puts " Listed #{i.to_s.bold} Notes"
292
+ puts
293
+ end
294
+
295
+ private def confirm(message = "Confirm")
296
+ print "#{message} [y/n]: "
297
+ case gets.chomp.strip.downcase
298
+ when "y", "yes", "yeah", "sure", "yep", "okay", "aye"
299
+ return true
300
+ when "n", "no", "nope", "nay"
301
+ return false
302
+ else
303
+ return confirm("Sorry, didn't quite get that...")
304
+ end
305
+ end
306
+
307
+ private def multi_note(params)
308
+ notes = []
309
+
310
+ params.first.split(",").each do |num|
311
+ note = @filtered[num.to_i - 1]
312
+ if note
313
+ notes << note
314
+ else
315
+ puts "Note #{num} doesn't exist!"
316
+ end
317
+ end
318
+
319
+ notes
320
+ end
321
+
322
+ private def recently_edited_first(notes)
323
+ notes.sort_by { |note| note.modified }.reverse
324
+ end
325
+
326
+ private def has_all_tags(note, tags)
327
+ has = true
328
+ note_tags = note.tags
329
+ tags.each do |tag|
330
+ if !note_tags.include? tag
331
+ has = false
332
+ break
333
+ end
334
+ end
335
+ has
336
+ end
337
+
338
+ private def has_none_tags(note, tags)
339
+ doesnt_have = true
340
+ note_tags = note.tags
341
+ tags.each do |tag|
342
+ if note_tags.include? tag
343
+ doesnt_have = false
344
+ break
345
+ end
346
+ end
347
+ doesnt_have
348
+ end
349
+ end
data/lib/cnote/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Cnote
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.2"
3
3
  end
data/main.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'ap'
2
+ require 'yaml'
3
+ require 'fileutils'
4
+ require_relative 'classes/config'
5
+ require_relative 'classes/notes'
6
+
7
+ command = (ARGV[0] || '').strip.downcase
8
+ config = Config.new('~/.cnote.yaml')
9
+
10
+ def help
11
+ puts "Try one of these:"
12
+ puts " cnote list"
13
+ puts " cnote search [term]"
14
+ end
15
+
16
+ case command
17
+ when 'list'
18
+ notes = Notes.new(config)
19
+ notes.list
20
+ when 'search', 'find'
21
+ notes = Notes.new(config)
22
+ notes.search(ARGV.slice(1, ARGV.length).join(' '))
23
+ when 'help'
24
+ help
25
+ else
26
+ # Start REPL
27
+ notes = Notes.new(config)
28
+ notes.await_command
29
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cnote
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony McCoy
@@ -97,10 +97,19 @@ files:
97
97
  - Rakefile
98
98
  - bin/console
99
99
  - bin/setup
100
+ - classes/config.rb
101
+ - classes/note.rb
102
+ - classes/notes.rb
100
103
  - cnote.gemspec
101
104
  - exe/cnote
105
+ - gems.locked
106
+ - gems.rb
102
107
  - lib/cnote.rb
108
+ - lib/cnote/config.rb
109
+ - lib/cnote/note.rb
110
+ - lib/cnote/notes.rb
103
111
  - lib/cnote/version.rb
112
+ - main.rb
104
113
  homepage: https://www.tonymccoy.me/cnote
105
114
  licenses:
106
115
  - MIT