rnote 0.0.1
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.
- data/bin/rnote +60 -0
- data/lib/rnote.rb +75 -0
- data/lib/rnote/auth.rb +130 -0
- data/lib/rnote/cmd/create.rb +32 -0
- data/lib/rnote/cmd/edit.rb +36 -0
- data/lib/rnote/cmd/find.rb +24 -0
- data/lib/rnote/cmd/login.rb +68 -0
- data/lib/rnote/cmd/logout.rb +24 -0
- data/lib/rnote/cmd/remove.rb +38 -0
- data/lib/rnote/cmd/show.rb +37 -0
- data/lib/rnote/cmd/who.rb +16 -0
- data/lib/rnote/converter.rb +127 -0
- data/lib/rnote/edit.rb +250 -0
- data/lib/rnote/find.rb +132 -0
- data/lib/rnote/persister.rb +249 -0
- data/lib/rnote/version.rb +3 -0
- data/rnote.rdoc +5 -0
- metadata +232 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
|
2
|
+
require 'rnote/find'
|
3
|
+
require 'highline/import'
|
4
|
+
|
5
|
+
include GLI::App
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
desc 'remove an item from evernote'
|
10
|
+
arg_name 'Describe arguments to remove here'
|
11
|
+
command :remove do |verb|
|
12
|
+
|
13
|
+
verb.command :note do |noun|
|
14
|
+
|
15
|
+
Rnote::Find.include_search_options(noun)
|
16
|
+
|
17
|
+
noun.action do |global_options,options,args|
|
18
|
+
|
19
|
+
find = Rnote::Find.new($app.auth,$app.persister)
|
20
|
+
note = find.find_note(options.merge(global_options),args)
|
21
|
+
|
22
|
+
puts note.summarize
|
23
|
+
|
24
|
+
answer = agree("Delete this note. Are you sure? ")
|
25
|
+
if answer
|
26
|
+
$app.client.note_store.deleteNote(note.guid)
|
27
|
+
else
|
28
|
+
puts "Alright, delete cancelled."
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
verb.default_command :note
|
36
|
+
end
|
37
|
+
|
38
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
|
2
|
+
require 'rnote/find'
|
3
|
+
|
4
|
+
include GLI::App
|
5
|
+
|
6
|
+
|
7
|
+
desc 'output notes to the console'
|
8
|
+
command :show do |verb|
|
9
|
+
|
10
|
+
verb.desc 'find and output notes to the console'
|
11
|
+
verb.command :note do |noun|
|
12
|
+
|
13
|
+
Rnote::Find.include_search_options(noun)
|
14
|
+
|
15
|
+
noun.desc 'include title in the output'
|
16
|
+
noun.default_value true
|
17
|
+
noun.switch :'include-title', :'inc-title'
|
18
|
+
|
19
|
+
noun.action do |global_options,options,args|
|
20
|
+
|
21
|
+
find = Rnote::Find.new($app.auth,$app.persister)
|
22
|
+
note = find.find_note(options.merge(global_options),args)
|
23
|
+
|
24
|
+
content = note.txt_content
|
25
|
+
|
26
|
+
puts note.title if options[:'include-title']
|
27
|
+
puts content
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
verb.default_command :note
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
include GLI::App
|
3
|
+
|
4
|
+
desc 'see which user you are logged in as'
|
5
|
+
command :who do |c|
|
6
|
+
c.action do |global_options,options,args|
|
7
|
+
raise unless args.length == 0
|
8
|
+
|
9
|
+
if $app.auth.is_logged_in
|
10
|
+
puts $app.auth.who
|
11
|
+
else
|
12
|
+
puts 'You are not logged in as any user.'
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
require 'evernote-thrift'
|
6
|
+
|
7
|
+
module Evernote::EDAM::Error
|
8
|
+
|
9
|
+
# converting between text formats and enml
|
10
|
+
#
|
11
|
+
# we have two types of conversion
|
12
|
+
#
|
13
|
+
# simple,
|
14
|
+
# single document conversion.
|
15
|
+
# which is enml <=> markdown
|
16
|
+
#
|
17
|
+
# then our own additional wrappers we put on top of those 2 document types
|
18
|
+
# adding metadata to them.
|
19
|
+
# yaml_stream <=> notes attributes
|
20
|
+
# content is just considered an 'attribute' in the latter
|
21
|
+
#
|
22
|
+
# the yaml_stream is just a string
|
23
|
+
# the note attributes get its own class and thats where we stick the conversion routines.
|
24
|
+
|
25
|
+
class InvalidFormatError < Exception
|
26
|
+
end
|
27
|
+
|
28
|
+
class InvalidXmlError < InvalidFormatError
|
29
|
+
|
30
|
+
attr_reader :xml
|
31
|
+
|
32
|
+
def initialize(message, xml=nil)
|
33
|
+
@xml = xml
|
34
|
+
super(message)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class InvalidMarkdownError < InvalidFormatError
|
39
|
+
def initialize(message, markdown=nil)
|
40
|
+
@markdown = markdown
|
41
|
+
super(message)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
class Evernote::EDAM::Type::Note
|
48
|
+
|
49
|
+
def self.enml_to_markdown(enml)
|
50
|
+
enml_to_txt(enml)
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.markdown_to_enml(markdown)
|
54
|
+
txt_to_enml(markdown)
|
55
|
+
end
|
56
|
+
|
57
|
+
def markdown_content=(markdown_content)
|
58
|
+
self.content = self.class.markdown_to_enml(markdown_content)
|
59
|
+
end
|
60
|
+
|
61
|
+
def markdown_content
|
62
|
+
self.class.enml_to_markdown(content)
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.enml_to_txt(enml)
|
66
|
+
document = Nokogiri::XML::Document.parse(enml)
|
67
|
+
raise Evernote::EDAM::Error::InvalidXmlError.new("invalid xml",enml) unless document.root
|
68
|
+
pre_node = document.root.xpath('pre').first
|
69
|
+
if pre_node
|
70
|
+
pre_node.children.to_ary.select { |child| child.text? }.map { |child| child.to_s }.join('')
|
71
|
+
else
|
72
|
+
document.root.xpath("//text()").text
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.txt_to_enml(txt)
|
77
|
+
if txt.start_with? '<?xml'
|
78
|
+
raise Evernote::EDAM::Error::InvalidMarkdownError.new('given xml instead of txt')
|
79
|
+
end
|
80
|
+
<<EOF
|
81
|
+
<?xml version='1.0' encoding='utf-8'?>
|
82
|
+
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
|
83
|
+
<en-note>
|
84
|
+
<pre>#{txt}</pre>
|
85
|
+
</en-note>
|
86
|
+
EOF
|
87
|
+
end
|
88
|
+
|
89
|
+
def txt_content=(txt_content)
|
90
|
+
self.content = self.class.txt_to_enml(txt_content)
|
91
|
+
end
|
92
|
+
|
93
|
+
def txt_content
|
94
|
+
self.class.enml_to_txt(content)
|
95
|
+
end
|
96
|
+
|
97
|
+
# The yaml stream is what we give to the user to edit in their editor
|
98
|
+
#
|
99
|
+
# Its just a string, but its composed of 2 parts. the note attributes and the note content.
|
100
|
+
#
|
101
|
+
# 1. a small yaml document with the note attributes as a hash.
|
102
|
+
# 2. followed by the note content as markdown
|
103
|
+
def yaml_stream=(yaml_stream)
|
104
|
+
|
105
|
+
m = yaml_stream.match /^(---.+?---\n)(.*)$/m
|
106
|
+
raise "failed to parse yaml stream\n#{yaml_stream}" unless m
|
107
|
+
|
108
|
+
attributes_yaml = m[1]
|
109
|
+
markdown = m[2]
|
110
|
+
|
111
|
+
enml = self.class.txt_to_enml(markdown)
|
112
|
+
attributes_hash = YAML.load(attributes_yaml)
|
113
|
+
|
114
|
+
self.title = attributes_hash['title']
|
115
|
+
self.tagNames = attributes_hash['tagNames']
|
116
|
+
self.content = enml
|
117
|
+
end
|
118
|
+
|
119
|
+
def yaml_stream
|
120
|
+
YAML.dump({ 'title' => title, 'tagNames' => tagNames }) + "\n---\n" + self.class.enml_to_txt(content)
|
121
|
+
end
|
122
|
+
|
123
|
+
def summarize
|
124
|
+
self.txt_content[0..30]
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
data/lib/rnote/edit.rb
ADDED
@@ -0,0 +1,250 @@
|
|
1
|
+
|
2
|
+
require 'highline'
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
class WaitPidTimeout
|
6
|
+
|
7
|
+
# wait in ruby doesn't take a timeout parameter
|
8
|
+
# so, join with timeout => wait with timeout
|
9
|
+
# launch a thread that does a wait on the pid.
|
10
|
+
# then its just a matter of joining that thread, with a timeout.
|
11
|
+
|
12
|
+
def initialize(pid,timeout)
|
13
|
+
@pid = pid
|
14
|
+
@timeout = timeout
|
15
|
+
end
|
16
|
+
|
17
|
+
def wait
|
18
|
+
|
19
|
+
@thread ||= Thread.new(@pid) do |pid|
|
20
|
+
Process.waitpid(pid)
|
21
|
+
end
|
22
|
+
|
23
|
+
!!@thread.join(@timeout)
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
class Evernote::EDAM::Type::Note
|
30
|
+
|
31
|
+
def diff(new_note)
|
32
|
+
# returns a Note that represents a diff of the 2, used for client.updateNote()
|
33
|
+
|
34
|
+
raise "notes aren't copies of the same note" if self.guid != new_note.guid
|
35
|
+
|
36
|
+
contains_diff = false
|
37
|
+
|
38
|
+
diff_note = Evernote::EDAM::Type::Note.new
|
39
|
+
diff_note.guid = new_note.guid
|
40
|
+
|
41
|
+
if self.title != new_note
|
42
|
+
contains_diff = true
|
43
|
+
end
|
44
|
+
# always contains a title
|
45
|
+
diff_note.title = new_note.title
|
46
|
+
|
47
|
+
if self.content != new_note.content
|
48
|
+
contains_diff = true
|
49
|
+
diff_note.content = new_note.content
|
50
|
+
end
|
51
|
+
|
52
|
+
if self.tagNames != new_note.tagNames
|
53
|
+
contains_diff = true
|
54
|
+
diff_note.tagNames = new_note.tagNames
|
55
|
+
end
|
56
|
+
|
57
|
+
# we dont' diff tagGuids as we always want to modify tagNames instead.
|
58
|
+
# so that the evernote api will handle the tag creation and guids itself.
|
59
|
+
|
60
|
+
contains_diff ? diff_note : nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def deep_dup
|
64
|
+
duplicate = self.dup
|
65
|
+
duplicate.tagNames = self.tagNames.dup if self.tagNames
|
66
|
+
duplicate.tagGuids = self.tagGuids.dup if self.tagGuids
|
67
|
+
|
68
|
+
duplicate
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
module Rnote
|
74
|
+
|
75
|
+
class Edit
|
76
|
+
|
77
|
+
def initialize(auth)
|
78
|
+
@auth = auth
|
79
|
+
@note = Evernote::EDAM::Type::Note.new
|
80
|
+
@note.txt_content = '' # for creating new notes.
|
81
|
+
@last_saved_note = Evernote::EDAM::Type::Note.new
|
82
|
+
end
|
83
|
+
|
84
|
+
def Edit.include_set_options(noun)
|
85
|
+
noun.desc 'set the title of the note'
|
86
|
+
noun.flag :'set-title'
|
87
|
+
end
|
88
|
+
|
89
|
+
def Edit.include_editor_options(noun)
|
90
|
+
noun.desc 'watch the file while editing and upload changes (you must save the file)'
|
91
|
+
noun.default_value true
|
92
|
+
noun.switch :watch
|
93
|
+
|
94
|
+
noun.desc 'open an interactive editor to modify the note'
|
95
|
+
noun.default_value true
|
96
|
+
noun.switch :editor
|
97
|
+
end
|
98
|
+
|
99
|
+
def Edit.has_set_options(options)
|
100
|
+
options[:'set-title']
|
101
|
+
end
|
102
|
+
|
103
|
+
def options(options)
|
104
|
+
@options = options
|
105
|
+
@use_editor = options[:editor]
|
106
|
+
@watch_editor = options[:watch]
|
107
|
+
end
|
108
|
+
|
109
|
+
def note(note)
|
110
|
+
@note = note
|
111
|
+
@last_saved_note = note.deep_dup
|
112
|
+
end
|
113
|
+
|
114
|
+
def edit_action
|
115
|
+
raise if not @note or not @last_saved_note
|
116
|
+
|
117
|
+
if Edit.has_set_options(@options)
|
118
|
+
|
119
|
+
apply_set_options
|
120
|
+
|
121
|
+
if @use_editor
|
122
|
+
editor
|
123
|
+
else
|
124
|
+
# if not going to open an editor, then just update immediately
|
125
|
+
save_note
|
126
|
+
end
|
127
|
+
|
128
|
+
elsif @use_editor
|
129
|
+
# no --set options
|
130
|
+
editor
|
131
|
+
else
|
132
|
+
raise "you've specified --no-editor but provided not --set options either."
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
def apply_set_options
|
138
|
+
if @options[:'set-title']
|
139
|
+
@note.title = @options[:'set-title']
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def save_note
|
144
|
+
|
145
|
+
diff_note = @last_saved_note.diff(@note)
|
146
|
+
|
147
|
+
# only update if necessary
|
148
|
+
if diff_note
|
149
|
+
|
150
|
+
# create or update
|
151
|
+
if diff_note.guid
|
152
|
+
@auth.client.note_store.updateNote(diff_note)
|
153
|
+
else
|
154
|
+
raise "cannot create note with nil content" if diff_note.content.nil?
|
155
|
+
new_note = @auth.client.note_store.createNote(diff_note)
|
156
|
+
# a few things to copy over
|
157
|
+
@note.guid = new_note.guid
|
158
|
+
end
|
159
|
+
|
160
|
+
# track what the last version we saved was. for diffing
|
161
|
+
@last_saved_note = @note.deep_dup
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
def editor
|
167
|
+
|
168
|
+
ENV['EDITOR'] ||= 'vim'
|
169
|
+
|
170
|
+
file = Tempfile.new(['rnote','md'])
|
171
|
+
begin
|
172
|
+
|
173
|
+
# fill the tempfile with the yaml stream
|
174
|
+
yaml_stream = @note.yaml_stream
|
175
|
+
file.write(yaml_stream)
|
176
|
+
file.close()
|
177
|
+
|
178
|
+
# error detection loop, to retry editing the file
|
179
|
+
successful_edit = false
|
180
|
+
until successful_edit do
|
181
|
+
|
182
|
+
last_mtime = File.mtime(file.path)
|
183
|
+
|
184
|
+
# run editor in background
|
185
|
+
pid = fork do
|
186
|
+
exec(ENV['EDITOR'],file.path)
|
187
|
+
end
|
188
|
+
|
189
|
+
wwt = WaitPidTimeout.new(pid,1) # 1 second
|
190
|
+
|
191
|
+
editor_done = false
|
192
|
+
until editor_done do
|
193
|
+
if not @watch_editor
|
194
|
+
Process.waitpid(pid)
|
195
|
+
editor_done = true
|
196
|
+
elsif wwt.wait
|
197
|
+
# process done
|
198
|
+
editor_done = true
|
199
|
+
else
|
200
|
+
# timeout exceeded
|
201
|
+
|
202
|
+
# has the file changed?
|
203
|
+
this_mtime = File.mtime(file.path)
|
204
|
+
if this_mtime != last_mtime
|
205
|
+
# protect the running editor from our failures.
|
206
|
+
begin
|
207
|
+
update_note_from_file(file.path)
|
208
|
+
rescue Exception => e
|
209
|
+
$stderr.puts "rnote: an error occured while updating the note: #{e.message}"
|
210
|
+
end
|
211
|
+
last_mtime = this_mtime
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# one last update of the note
|
217
|
+
# this time we care if there are errors
|
218
|
+
begin
|
219
|
+
update_note_from_file(file.path)
|
220
|
+
rescue Exception => e
|
221
|
+
|
222
|
+
puts "There was an error while uploading the note"
|
223
|
+
puts e.message
|
224
|
+
puts e.backtrace.join("\n ")
|
225
|
+
|
226
|
+
successful_edit = ! agree("Return to editor? (otherwise changes will be lost) ")
|
227
|
+
else
|
228
|
+
successful_edit = true
|
229
|
+
end
|
230
|
+
|
231
|
+
end # successful edit loop
|
232
|
+
|
233
|
+
ensure
|
234
|
+
file.unlink
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
|
239
|
+
def update_note_from_file(path)
|
240
|
+
|
241
|
+
yaml_stream = File.open(path,'r').read
|
242
|
+
@note.yaml_stream = yaml_stream
|
243
|
+
|
244
|
+
save_note
|
245
|
+
end
|
246
|
+
|
247
|
+
|
248
|
+
end
|
249
|
+
|
250
|
+
end
|