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 +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
|