cnote 0.1.0 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Gemfile +0 -6
- data/classes/config.rb +66 -0
- data/classes/note.rb +117 -0
- data/classes/notes.rb +348 -0
- data/gems.locked +14 -0
- data/gems.rb +6 -0
- data/lib/cnote.rb +3 -3
- data/lib/cnote/config.rb +69 -0
- data/lib/cnote/note.rb +115 -0
- data/lib/cnote/notes.rb +349 -0
- data/lib/cnote/version.rb +1 -1
- data/main.rb +29 -0
- metadata +10 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 54afc4391700ef178bc0ed861e49cdbcfe30cc73
|
4
|
+
data.tar.gz: a5aab3d03e17f4ae079f137e024ba6c6bf733373
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c881765e7c0c228e0e01f9c12c56ba06f9341eb0c9e184fff491d6422f61485e23a0b289f34c2f92e10a22e22ae6fae3d8959733b33a26a29f4abd3116b8854b
|
7
|
+
data.tar.gz: 624d176e63393ffa9e5f91124418ecf4780306e7227c14e84d9af043d923ccbda5ae85f5af3d5684244c06acd869efbffd0ee7681991b5738324f1e143b1bb82
|
data/.gitignore
CHANGED
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
data/gems.rb
ADDED
data/lib/cnote.rb
CHANGED
data/lib/cnote/config.rb
ADDED
@@ -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
|
data/lib/cnote/notes.rb
ADDED
@@ -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
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.
|
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
|