rnote 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/bin/rnote CHANGED
@@ -6,7 +6,7 @@ include GLI::App
6
6
 
7
7
  program_desc 'Evernote Command Line'
8
8
 
9
- # version Rnote::VERSION
9
+ version Rnote::VERSION
10
10
 
11
11
  desc 'prompt for input and run an editor when necessary'
12
12
  default_value true
@@ -46,9 +46,6 @@ on_error do |exception|
46
46
  elsif exception.instance_of?(Evernote::EDAM::Error::EDAMSystemException)
47
47
  puts exception.error_message
48
48
  true
49
- elsif exception.class == Evernote::EDAM::Error::InvalidXmlError
50
- puts exception.xml
51
- true
52
49
  else
53
50
  true
54
51
  end
@@ -6,10 +6,17 @@ require 'rnote/edit'
6
6
  include GLI::App
7
7
 
8
8
 
9
- desc 'create a note and launch the editor for it'
9
+ d 'create a new note'
10
+ long_desc <<EOF
11
+ Create a new note and, optionally, launch an editor to provide its content
12
+
13
+ Unlike most commands, the command line arguments aren't used in a search.
14
+ Instead any command line arguments provided are used for the title of the new note.
15
+ EOF
10
16
  command :create do |verb|
11
- verb.command :note do |noun|
12
17
 
18
+ d 'create a new note'
19
+ verb.command :note do |noun|
13
20
 
14
21
  Rnote::Edit.include_set_options(noun)
15
22
  Rnote::Edit.include_editor_options(noun)
@@ -17,7 +24,10 @@ command :create do |verb|
17
24
  noun.action do |global_options,options,args|
18
25
 
19
26
  if args.length > 0
20
- raise "create doesn't take a search query"
27
+ if options[:'set-title']
28
+ raise "You can't use both --set-title and command line arguments at the same time to set the title of the new note."
29
+ end
30
+ options[:'set-title'] = args.join(' ')
21
31
  end
22
32
 
23
33
  edit = Rnote::Edit.new($app.auth)
@@ -4,11 +4,13 @@ require 'rnote/find'
4
4
 
5
5
  include GLI::App
6
6
 
7
-
8
- desc 'Describe edit here'
9
- arg_name 'Describe arguments to edit here'
7
+ # TODO why doesn't 'desc' work here instead of 'd'. What is over-riding it?
8
+ d 'edit/update a note'
9
+ long_desc 'Edit/update an existing note, usually by launching an editor.'
10
10
  command :edit do |verb|
11
11
 
12
+ verb.desc "edit a note"
13
+
12
14
  verb.command :note do |noun|
13
15
 
14
16
  Rnote::Edit.include_set_options(noun)
@@ -19,7 +21,7 @@ command :edit do |verb|
19
21
 
20
22
  find = Rnote::Find.new($app.auth,$app.persister)
21
23
  note = find.find_note(options.merge(global_options),args)
22
-
24
+
23
25
  edit = Rnote::Edit.new($app.auth)
24
26
  edit.options(options.merge(global_options))
25
27
  edit.note(note)
@@ -4,9 +4,15 @@ require 'rnote/find'
4
4
  include GLI::App
5
5
 
6
6
 
7
- desc 'search for notes/tags/notebooks'
7
+ d 'search for notes'
8
+ long_desc <<EOF
9
+ Provide a query and find matching notes. Provides a short summary of each note in the result.
10
+
11
+ You can run this command before running other commands that require a note to be selected, such as 'edit', or 'remove'. And then specify the result number on the next command line.
12
+ EOF
8
13
  command :find do |verb|
9
14
 
15
+ verb.desc 'find notes'
10
16
  verb.command :note do |noun|
11
17
 
12
18
  Rnote::Find.include_search_options(noun)
@@ -3,7 +3,8 @@ require 'highline/import'
3
3
 
4
4
  include GLI::App
5
5
 
6
- desc 'provide rnote credentials'
6
+ d 'login to evernote'
7
+ long_desc 'Provide credentials and log a user into evernote. The users password is never saved, but an auth token will be.'
7
8
  command :login do |c|
8
9
 
9
10
  c.desc "username"
@@ -11,7 +11,8 @@ but won't forget about a consumer key, as that by itself is not considered a log
11
11
 
12
12
  =end
13
13
 
14
- desc 'log user out of rnote'
14
+ d 'logout user'
15
+ long_desc "Log a user out of evernote. This forgets any credential information that may have been cached. currently this does not revoke the token though. It simply forgets what the token was."
15
16
  command :logout do |c|
16
17
  c.action do |global_options,options,args|
17
18
  raise unless args.length == 0
@@ -6,10 +6,11 @@ include GLI::App
6
6
 
7
7
 
8
8
 
9
- desc 'remove an item from evernote'
10
- arg_name 'Describe arguments to remove here'
11
- command :remove do |verb|
12
-
9
+ d 'remove a note'
10
+ long_desc "Remove a note, but don't expunge it. The note stays in the users Trash"
11
+ command :remove do |verb|
12
+
13
+ verb.desc 'remove a note'
13
14
  verb.command :note do |noun|
14
15
 
15
16
  Rnote::Find.include_search_options(noun)
@@ -4,27 +4,37 @@ require 'rnote/find'
4
4
  include GLI::App
5
5
 
6
6
 
7
- desc 'output notes to the console'
7
+ d 'show note content'
8
+ long_desc "output a note's content to the console."
8
9
  command :show do |verb|
9
10
 
10
- verb.desc 'find and output notes to the console'
11
+ verb.desc "output a note's content"
11
12
  verb.command :note do |noun|
12
13
 
13
14
  Rnote::Find.include_search_options(noun)
14
15
 
15
16
  noun.desc 'include title in the output'
16
17
  noun.default_value true
17
- noun.switch :'include-title', :'inc-title'
18
+ noun.switch :'include-title', :'inc-title', :'output-title', :'show-title'
19
+
20
+ noun.desc 'which format to output? (txt or enml)'
21
+ noun.default_value 'txt'
22
+ noun.flag :format
18
23
 
19
24
  noun.action do |global_options,options,args|
20
25
 
21
26
  find = Rnote::Find.new($app.auth,$app.persister)
22
27
  note = find.find_note(options.merge(global_options),args)
23
28
 
24
- content = note.txt_content
25
29
 
26
30
  puts note.title if options[:'include-title']
27
- puts content
31
+ if options[:format] == 'txt'
32
+ puts note.txt_content
33
+ elsif options[:format] == 'enml'
34
+ puts note.content
35
+ else
36
+ raise "Unknown outoput format specified."
37
+ end
28
38
 
29
39
  end
30
40
 
@@ -1,7 +1,8 @@
1
1
 
2
2
  include GLI::App
3
3
 
4
- desc 'see which user you are logged in as'
4
+ d 'which user is logged in'
5
+ long_desc 'see what username is logged in, or if your using a developer token instead of a username.'
5
6
  command :who do |c|
6
7
  c.action do |global_options,options,args|
7
8
  raise unless args.length == 0
@@ -1,3 +1,2 @@
1
-
2
1
  PRODUCTION_CONSUMER_KEY = 'dragonfax'
3
- PRODUCTION_CONSUMER_SECRET = '5b110ac7e792579c'
2
+ PRODUCTION_CONSUMER_SECRET = '5b110ac7e792579c'
@@ -4,94 +4,160 @@ require 'yaml'
4
4
 
5
5
  require 'evernote-thrift'
6
6
 
7
- module Evernote::EDAM::Error
8
7
 
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
8
+ # converting between text formats and enml
9
+ #
10
+ # we have two types of conversion
11
+ #
12
+ # simple,
13
+ # single document conversion.
14
+ # which is enml <=> txt
15
+ #
16
+ # then our own additional wrappers we put on top of those 2 document types
17
+ # adding metadata to them.
18
+ # yaml_stream <=> notes attributes
19
+ # content is just considered an 'attribute' in the latter
20
+ #
21
+ # the yaml_stream is just a string
22
+ # the note attributes get its own class and thats where we stick the conversion routines.
23
+
24
+ class Evernote::EDAM::Type::Note
25
+
26
+ # simple xhtml to txt converter
27
+ # just tries to convert evernotes simple xhtml.
28
+ # the kind its own editors create. Which doesn't involve much nesting.
29
+ class EnmlDocument < Nokogiri::XML::SAX::Document # Nokogiri SAX parser
30
+
31
+ attr_accessor :_txt, :in_div, :in_pre
32
+
33
+ def initialize
34
+ @_txt = ''
35
+ @in_div = false
36
+ @in_pre = false
37
+ super
38
+ end
29
39
 
30
- attr_reader :xml
40
+ def characters string
41
+
42
+ if ! self.in_div and ! self.in_pre and string == "\n"
43
+ # ignore lone newlines that occur outside a div
44
+ else
45
+ self._txt << string
46
+ end
47
+ end
31
48
 
32
- def initialize(message, xml=nil)
33
- @xml = xml
34
- super(message)
49
+ def start_element name, attrs = []
50
+ case name
51
+ when 'en-todo'
52
+ if Hash[attrs]['checked'] == 'true'
53
+ self._txt << '[X]'
54
+ else
55
+ self._txt << '[ ]'
56
+ end
57
+ when 'div'
58
+ self.in_div = true
59
+ when 'pre'
60
+ self.in_pre = true
61
+ else
62
+ # nothing
63
+ end
35
64
  end
36
- end
37
-
38
- class InvalidMarkdownError < InvalidFormatError
39
- def initialize(message, markdown=nil)
40
- @markdown = markdown
41
- super(message)
65
+
66
+ def end_element name
67
+ case name
68
+ when 'div'
69
+ self.in_div = false
70
+ # a newline for every div (whether its got a <br> in it or not)
71
+ self._txt << "\n"
72
+ when 'pre'
73
+ self.in_pre = false
74
+ when 'br'
75
+ # ignore it, as its always in a div, and every div will be a newline anyways
76
+ else
77
+ # nothing
78
+ end
42
79
  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)
80
+
81
+ def txt
82
+ # always remove the last newline. to match up with WYSIWYG interfaces.
83
+ self._txt.chomp
84
+ end
85
+
51
86
  end
52
87
 
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
-
88
+
65
89
  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
90
+ raise 'not given xml' if ! enml.start_with? '<?xml'
91
+
92
+ sax_document = EnmlDocument.new
93
+ parser = Nokogiri::XML::SAX::Parser.new(sax_document)
94
+ parser.parse(enml)
95
+
96
+ enml = sax_document.txt
97
+
98
+ enml
74
99
  end
75
100
 
76
101
  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
102
+ raise 'given xml instead of txt' if txt.start_with? '<?xml'
103
+
104
+ # TODO create a proper DOM, with proper xml entity escapes and tag structure
105
+
106
+ # escape any entities
107
+ txt.gsub!('<','&lt;')
108
+ txt.gsub!('>','&gt;')
109
+
110
+ # replace todo items
111
+ txt.gsub!('[ ]','<en-todo checked="false"/>')
112
+ txt.gsub!('[X]','<en-todo checked="true"/>')
113
+
114
+ # every newline becomes a <div></div>
115
+ # an empty line becomes a <div><br/></div>
116
+
117
+ lines = txt.split("\n",-1)
118
+ lines = [''] if txt == ''
119
+ raise if lines.length == 0
120
+
121
+ xhtml = lines.map { |string|
122
+ if string == ''
123
+ "<div><br/></div>\n"
124
+ else
125
+ "<div>#{string}</div>\n"
126
+ end
127
+ }.join('')
128
+
80
129
  <<EOF
81
130
  <?xml version='1.0' encoding='utf-8'?>
82
131
  <!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
83
132
  <en-note>
84
- <pre>#{txt}</pre>
85
- </en-note>
133
+ #{xhtml}</en-note>
86
134
  EOF
87
135
  end
88
136
 
89
- def txt_content=(txt_content)
90
- self.content = self.class.txt_to_enml(txt_content)
137
+ def self.enml_to_format(format,enml)
138
+ case format
139
+ when 'enml'
140
+ enml
141
+ when 'txt'
142
+ enml_to_txt(enml)
143
+ else
144
+ raise
145
+ end
146
+ end
147
+
148
+ def self.format_to_enml(format,formatted_content)
149
+ case format
150
+ when 'enml'
151
+ formatted_content
152
+ when 'txt'
153
+ txt_to_enml(formatted_content)
154
+ else
155
+ raise
156
+ end
91
157
  end
92
158
 
93
159
  def txt_content
94
- self.class.enml_to_txt(content)
160
+ self.class.enml_to_format('txt',self.content)
95
161
  end
96
162
 
97
163
  # The yaml stream is what we give to the user to edit in their editor
@@ -99,25 +165,30 @@ EOF
99
165
  # Its just a string, but its composed of 2 parts. the note attributes and the note content.
100
166
  #
101
167
  # 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)
168
+ # 2. followed by the note content as txt
169
+ def set_yaml_stream(format,yaml_stream)
104
170
 
105
171
  m = yaml_stream.match /^(---.+?---\n)(.*)$/m
106
172
  raise "failed to parse yaml stream\n#{yaml_stream}" unless m
107
173
 
108
174
  attributes_yaml = m[1]
109
- markdown = m[2]
175
+ txt = m[2]
110
176
 
111
- enml = self.class.txt_to_enml(markdown)
177
+ enml = self.class.format_to_enml(format,txt)
112
178
  attributes_hash = YAML.load(attributes_yaml)
179
+
180
+ # process tag names
181
+ # allow for comma separated tag list
182
+ tag_names = attributes_hash['tagNames']
183
+ tag_names = tag_names.split(/\s*,\s*/) if tag_names.instance_of?(String)
113
184
 
114
185
  self.title = attributes_hash['title']
115
186
  self.tagNames = attributes_hash['tagNames']
116
187
  self.content = enml
117
188
  end
118
189
 
119
- def yaml_stream
120
- YAML.dump({ 'title' => title, 'tagNames' => tagNames }) + "\n---\n" + self.class.enml_to_txt(content)
190
+ def yaml_stream(format)
191
+ YAML.dump({ 'title' => title, 'tagNames' => tagNames }) + "\n---\n" + self.class.enml_to_format(format,content)
121
192
  end
122
193
 
123
194
  def summarize
@@ -1,6 +1,7 @@
1
1
 
2
2
  require 'highline'
3
3
  require 'nokogiri'
4
+ require 'tempfile'
4
5
 
5
6
  class WaitPidTimeout
6
7
 
@@ -77,7 +78,7 @@ module Rnote
77
78
  def initialize(auth)
78
79
  @auth = auth
79
80
  @note = Evernote::EDAM::Type::Note.new
80
- @note.txt_content = '' # for creating new notes.
81
+ @note.content = @note.class.format_to_enml('txt','') # for creating new notes.
81
82
  @last_saved_note = Evernote::EDAM::Type::Note.new
82
83
  end
83
84
 
@@ -94,6 +95,10 @@ module Rnote
94
95
  noun.desc 'open an interactive editor to modify the note'
95
96
  noun.default_value true
96
97
  noun.switch :editor
98
+
99
+ noun.desc 'which format do you want to edit the note in? default is "txt", other option is "enml"'
100
+ noun.default_value 'txt'
101
+ noun.flag :format
97
102
  end
98
103
 
99
104
  def Edit.has_set_options(options)
@@ -104,6 +109,8 @@ module Rnote
104
109
  @options = options
105
110
  @use_editor = options[:editor]
106
111
  @watch_editor = options[:watch]
112
+ @format = options[:format]
113
+ raise "format #{@format} not known" unless %w{txt enml}.include?(@format)
107
114
  end
108
115
 
109
116
  def note(note)
@@ -162,16 +169,104 @@ module Rnote
162
169
  end
163
170
 
164
171
  end
172
+
173
+ # output both forms to a file, and run "diff | less"
174
+ def show_diff(original,altered)
175
+
176
+ file1 = Tempfile.new('rnote')
177
+ file2 = Tempfile.new('rnote')
178
+ begin
179
+
180
+ file1.write(original)
181
+ file1.close
182
+
183
+ file2.write(altered)
184
+ file2.close
185
+
186
+ system("diff #{file1.path} #{file2.path} | less")
187
+
188
+ raise "User cnacelled due to lost content." unless agree("Continue editing note? ")
189
+
190
+ ensure
191
+ file1.unlink
192
+ file2.unlink
193
+ end
194
+ end
195
+
196
+ # check if we lose content/formating when converting the note
197
+ # and if so ask the user if they want to continue.
198
+ def check_for_lost_content
199
+
200
+ converted_content = @note.class.enml_to_format(@format, @note.content)
201
+ unconverted_content = @note.class.format_to_enml(@format, converted_content)
202
+
203
+ if @note.content != unconverted_content
204
+ puts "Some content or formatting may be lost in the note due to editing format conversion."
205
+ reply_continue = ask("Continue editing the note? (yes/no/diff) ") { |q|
206
+ q.validate = /\A(y|n|d|q|e|c|yes|no|cancel|quit|exit|diff)\Z/i
207
+ q.responses[:not_valid] = 'Please enter "yes", "no", "diff", or "cancel".'
208
+ q.responses[:ask_on_error] = :question
209
+ }
210
+
211
+ case reply_continue.downcase
212
+ when 'y'
213
+ # nothing, continue
214
+ when 'yes'
215
+ # nothing, continue
216
+ when 'n'
217
+ raise "User cancelled due to lost content."
218
+ when 'no'
219
+ raise "User cancelled due to lost content."
220
+ when 'cancel'
221
+ raise "User cancelled due to lost content."
222
+ when 'quit'
223
+ raise "User cancelled due to lost content."
224
+ when 'exit'
225
+ raise "User cancelled due to lost content."
226
+ when 'diff'
227
+ show_diff(@note.content,converted_content)
228
+ else
229
+ raise
230
+ end
231
+
232
+
233
+ end
234
+ end
235
+
236
+ def md5(filename)
237
+ # TODO sloppy, switch with non shell command
238
+ `cat #{filename} | md5`.chomp
239
+ end
240
+
241
+ # has the file changed since the last time we checked.
242
+ def has_file_changed(file)
243
+
244
+ @last_mtime ||= nil
245
+ @last_md5 ||= nil
246
+
247
+ this_mtime = File.mtime(file.path)
248
+ this_md5 = md5(file.path)
249
+
250
+ changed = this_mtime != @last_mtime && this_md5 != @last_md5
251
+
252
+ @last_mtime = this_mtime
253
+ @last_md5 = this_md5
254
+
255
+ changed
256
+ end
257
+
165
258
 
166
259
  def editor
167
260
 
168
261
  ENV['EDITOR'] ||= 'vim'
262
+
263
+ check_for_lost_content
169
264
 
170
- file = Tempfile.new(['rnote','md'])
265
+ file = Tempfile.new(['rnote','.' + @format])
171
266
  begin
172
267
 
173
268
  # fill the tempfile with the yaml stream
174
- yaml_stream = @note.yaml_stream
269
+ yaml_stream = @note.yaml_stream(@format)
175
270
  file.write(yaml_stream)
176
271
  file.close()
177
272
 
@@ -179,7 +274,7 @@ module Rnote
179
274
  successful_edit = false
180
275
  until successful_edit do
181
276
 
182
- last_mtime = File.mtime(file.path)
277
+ has_file_changed(file) # initialize the file change tracking.
183
278
 
184
279
  # run editor in background
185
280
  pid = fork do
@@ -200,31 +295,34 @@ module Rnote
200
295
  # timeout exceeded
201
296
 
202
297
  # has the file changed?
203
- this_mtime = File.mtime(file.path)
204
- if this_mtime != last_mtime
298
+ if has_file_changed(file)
205
299
  # protect the running editor from our failures.
206
300
  begin
207
301
  update_note_from_file(file.path)
208
302
  rescue Exception => e
209
303
  $stderr.puts "rnote: an error occured while updating the note: #{e.message}"
210
304
  end
211
- last_mtime = this_mtime
212
305
  end
213
306
  end
214
307
  end
215
308
 
216
309
  # one last update of the note
217
310
  # 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) ")
311
+ if has_file_changed(file)
312
+ begin
313
+ update_note_from_file(file.path)
314
+ rescue Exception => e
315
+
316
+ puts "There was an error while uploading the note"
317
+ puts e.message
318
+ puts e.backtrace.join("\n ")
319
+
320
+ successful_edit = ! agree("Return to editor? (otherwise changes will be lost) ")
321
+ else
322
+ successful_edit = true
323
+ end
227
324
  else
325
+ # no changes to file, no need to save.
228
326
  successful_edit = true
229
327
  end
230
328
 
@@ -239,7 +337,7 @@ module Rnote
239
337
  def update_note_from_file(path)
240
338
 
241
339
  yaml_stream = File.open(path,'r').read
242
- @note.yaml_stream = yaml_stream
340
+ @note.set_yaml_stream(@format,yaml_stream)
243
341
 
244
342
  save_note
245
343
  end
@@ -101,7 +101,7 @@ module Rnote
101
101
  # no search options, one argument, and its a small number
102
102
  # they are asking to pick from the last search results
103
103
  guids = @persister.get_last_search_guids
104
- guid = guids[args[0].to_i] # the chosen note
104
+ guid = guids[args[0].to_i - 1] # the chosen note
105
105
  note = get_full_note(guid)
106
106
  results = [note] # fake a result set with it.
107
107
  else
@@ -1,3 +1,3 @@
1
1
  module Rnote
2
- VERSION = '0.0.2'
2
+ VERSION = '0.0.3'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rnote
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-04-01 00:00:00.000000000 Z
12
+ date: 2013-04-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -231,3 +231,4 @@ signing_key:
231
231
  specification_version: 3
232
232
  summary: CLI to Evernote
233
233
  test_files: []
234
+ has_rdoc: true