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.
@@ -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