sup 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sup might be problematic. Click here for more details.

Files changed (48) hide show
  1. data/History.txt +9 -0
  2. data/Manifest.txt +5 -1
  3. data/Rakefile +2 -1
  4. data/bin/sup +27 -10
  5. data/bin/sup-add +2 -1
  6. data/bin/sup-sync-back +51 -23
  7. data/doc/FAQ.txt +29 -37
  8. data/doc/Hooks.txt +38 -0
  9. data/doc/{UserGuide.txt → NewUserGuide.txt} +27 -21
  10. data/doc/TODO +91 -57
  11. data/lib/sup.rb +17 -1
  12. data/lib/sup/buffer.rb +80 -16
  13. data/lib/sup/colormap.rb +0 -2
  14. data/lib/sup/contact.rb +3 -2
  15. data/lib/sup/crypto.rb +110 -0
  16. data/lib/sup/draft.rb +2 -6
  17. data/lib/sup/hook.rb +131 -0
  18. data/lib/sup/imap.rb +27 -16
  19. data/lib/sup/index.rb +38 -14
  20. data/lib/sup/keymap.rb +0 -2
  21. data/lib/sup/label.rb +30 -9
  22. data/lib/sup/logger.rb +12 -1
  23. data/lib/sup/maildir.rb +48 -3
  24. data/lib/sup/mbox.rb +1 -1
  25. data/lib/sup/mbox/loader.rb +22 -12
  26. data/lib/sup/mbox/ssh-loader.rb +1 -1
  27. data/lib/sup/message-chunks.rb +198 -0
  28. data/lib/sup/message.rb +154 -115
  29. data/lib/sup/modes/compose-mode.rb +18 -0
  30. data/lib/sup/modes/contact-list-mode.rb +1 -1
  31. data/lib/sup/modes/edit-message-mode.rb +112 -31
  32. data/lib/sup/modes/file-browser-mode.rb +1 -1
  33. data/lib/sup/modes/inbox-mode.rb +1 -1
  34. data/lib/sup/modes/label-list-mode.rb +8 -6
  35. data/lib/sup/modes/label-search-results-mode.rb +4 -1
  36. data/lib/sup/modes/log-mode.rb +1 -1
  37. data/lib/sup/modes/reply-mode.rb +18 -16
  38. data/lib/sup/modes/search-results-mode.rb +1 -1
  39. data/lib/sup/modes/thread-index-mode.rb +61 -33
  40. data/lib/sup/modes/thread-view-mode.rb +111 -102
  41. data/lib/sup/person.rb +5 -1
  42. data/lib/sup/poll.rb +36 -7
  43. data/lib/sup/sent.rb +1 -0
  44. data/lib/sup/source.rb +7 -3
  45. data/lib/sup/textfield.rb +48 -34
  46. data/lib/sup/thread.rb +9 -5
  47. data/lib/sup/util.rb +16 -22
  48. metadata +7 -3
@@ -13,11 +13,10 @@ cryptic messages. You can also press '?' at any point to get a list of
13
13
  keyboard commands, but in the absence of any email, these will be
14
14
  mostly useless. When you get bored, press 'q' to quit.
15
15
 
16
- To actually use Sup for email, we need to load messages into the
17
- index. The index is where Sup stores all message state (e.g. read or
18
- unread, any message labels), and all information necessary for
19
- searching and for threading messages. Sup only knows about messages in
20
- its index.
16
+ To use Sup for email, we need to load messages into the index. The
17
+ index is where Sup stores all message state (e.g. read or unread, any
18
+ message labels), and all information necessary for searching and for
19
+ threading messages. Sup only knows about messages in its index.
21
20
 
22
21
  We can add messages to the index by telling Sup about the "source"
23
22
  where the messages reside. Sources are things like IMAP folders, mbox
@@ -68,7 +67,7 @@ messages together into threads: each line in the inbox is a thread,
68
67
  and the number in parentheses is the number of messages in that
69
68
  thread. (If there's no number, there's just one message in the
70
69
  thread.) In Sup, most operations are on threads, not individual
71
- messages. The ideais that you rarely want to operate on a message
70
+ messages. The idea is that you rarely want to operate on a message
72
71
  independent of its context. You typically want to view, archive, kill,
73
72
  or label all the messages in a thread at one time.
74
73
 
@@ -168,15 +167,15 @@ can figure out how to:
168
167
  - Star an individual message, not just a thread
169
168
 
170
169
  There's one last thing to be aware of when using Sup: how it interacts
171
- with other email programs. As I described at the beginning of the
172
- document, Sup stores data about messages in the index, but doesn't
173
- duplicate the message contents themselves. The messages remain on the
174
- source. If the index and the source every fall out of sync, e.g. due
175
- to another email client modifying the source, then Sup will be unable
176
- to operate on that source. For example, for mbox files, Sup stores a
177
- byte offset into the file for each message. If a message deleted from
178
- that file by another client, or even marked as read (yeah, mbox
179
- sucks), all succeeding offsets will be wrong.
170
+ with other email programs. As I described above, Sup stores data about
171
+ messages in the index, but doesn't duplicate the message contents
172
+ themselves. The messages remain on the source. If the index and the
173
+ source every fall out of sync, e.g. due to another email client
174
+ modifying the source, then Sup will be unable to operate on that
175
+ source. For example, for mbox files, Sup stores a byte offset into the
176
+ file for each message. If a message deleted from that file by another
177
+ client, or even marked as read (yeah, mbox sucks), all succeeding
178
+ offsets will be wrong.
180
179
 
181
180
  That's the bad news. The good news is that Sup is pretty good at being
182
181
  able to detect this type of situation, and fixing it is just a matter
@@ -185,7 +184,7 @@ how to invoke sup-sync when it detects a problem. This is a
185
184
  complication you will almost certainly run in to if you use both Sup
186
185
  and another MUA on the same source, so it's good to be aware of it.
187
186
 
188
- Have fun, and let me know if you have any problems. --William
187
+ Have fun, and let me know if you have any problems!
189
188
 
190
189
  Appending A: sup-add and sup-sync
191
190
  ---------------------------------
@@ -201,7 +200,8 @@ Instead of using sup-config to add a new source, you can manually run
201
200
  Before you add the source, you need make three decisions. The first is
202
201
  whether you want Sup to regularly poll this source for new messages.
203
202
  By default it will, but if this is a source that will never have new
204
- messages, you can specify --unusual. Sup polls only "usual" sources.
203
+ messages, you can specify --unusual. Sup polls only "usual" sources
204
+ when checking for new mail (unless you manually invoke sup-sync).
205
205
 
206
206
  The second is whether you want messages from the source to be
207
207
  automatically archived. An archived message will not show up in your
@@ -225,12 +225,12 @@ import except your actual inbox. You can also specify --read to mark
225
225
  all imported messages as read; the default is to preserve the
226
226
  read/unread status from the source.
227
227
 
228
- sup-sync will now load all the messages from the source into the
228
+ Sup-sync will now load all the messages from the source into the
229
229
  index. Depending on the size of the source, this may take a while.
230
230
  Don't panic! It's a one-time process.
231
231
 
232
- Appending B: Handling high-volume mailing lists
233
- -----------------------------------------------
232
+ Appendix B: Handling high-volume mailing lists
233
+ ----------------------------------------------
234
234
 
235
235
  Here's what I recommend:
236
236
  1. Use procmail to filter messages from the list into a distinct source.
@@ -238,8 +238,14 @@ Here's what I recommend:
238
238
  on, and with a label corresponding to the mailing list name.
239
239
  (E.g.: sup-add mbox:/home/me/Mail/ruby-talk -a -l ruby-talk)
240
240
  3. Voila! Sup will load new messages into the index but not into the
241
- inbox, and you can browse the mailing list traffice at any point by
241
+ inbox, and you can browse the mailing list traffic at any point by
242
242
  searching for that label.
243
243
 
244
244
 
245
+ Appendix C: Reading blogs with Sup
246
+ ----------------------------------
245
247
 
248
+ Really, blog posts should be read like emails are read---you should be
249
+ able to mark them as unread, flag them, label them, etc. Use rss2email
250
+ to transform RSS feeds into emails, direct them all into a source, and
251
+ add that source to Sup. Voila!
data/doc/TODO CHANGED
@@ -1,12 +1,101 @@
1
- for 0.1
1
+ for 0.2
2
2
  -------
3
+
4
+ x bugfix: contacts.txt isn't parsed correctly when there are spaces in
5
+ aliases
6
+ x bugfix: @ signs in names make sendmail die silently
7
+ x bugfix: sent.mbox and >From
8
+ x bugfix: tokenized email addresses (amazon.com, etc)
9
+ x bugfix: trailing spaces in buffermanager.ask
10
+ x bugfix: need to URL-unescape maildir folders
11
+ x bugfix: downcasing tab completion
12
+ x warnings: top-posting, missing attachment
13
+ x hookability
14
+
15
+ for 0.3
16
+ -------
17
+ _ mark thread as unread should remember the unread messages and mark
18
+ only them as unread, just like gmail
19
+ _ mark thread as unread should have a version within thread-view-mode
20
+ which then also closes the buffer
21
+ _ bugfix: time zone parsing broken?
22
+ _ mailing list auto-subscribe/unsubscribe
23
+ _ forwards optionally include attachments
24
+ _ attach messages
25
+ _ flesh out gpg integration: sign & encrypt outgoing
26
+ _ mbox: don't keep filehandles open, and protect all reads with dotlockfile
27
+ _ bugfix: screwing with the headers when editing causes a crash
28
+ _ need a better way to force an address to a particular name,
29
+ for things like evite addresses
30
+ _ pressing A in thread-view-mode should jump to next message
31
+ _ imap "add all folders on this server" option in sup-add
32
+ _ for new message flashes, add new message counts until keypress
33
+ _ bugfix: missing sources should be handled better
34
+ _ search results: highlight relevant snippets and open to relevant
35
+ portion of thread
36
+ _ have "notes" (treated as emails to oneself, never sent) as
37
+ first-class objects.
38
+
39
+ future
40
+ ------
41
+ _ emlx support (some os x thing)
42
+ _ tab completion for mid-text cursors
43
+ _ bugfix: not horizontal scrolling for ncurses text field entry
44
+ _ use trac or something. this file is getting a little silly.
45
+ _ saved searches
46
+ _ bugfix: sometimes, when one new message comes into an imap folder,
47
+ we don't catch it until a reload. but we do see a message
48
+ indicating they're loaded to inbox (imap only? hard to reproduce.)
49
+ _ bugfix: ferret flakiness: just added message but can't find it.
50
+ possibly a message id tokenization issue.
51
+ _ bugfix: read before thread-index has finished loading then hides the
52
+ thread?!? wtf. (on jamie) (? still valid ?)
53
+ _ bugfix: display field width in index-mode needs to be determined
54
+ per-character rather than per-byte
55
+ _ select all, starred, to me, etc
56
+ _ undo
57
+ _ Net::SMTP support
58
+ _ ruby-talk:XXXX detection (via hooks?)
59
+ _ more control character support in buffer line editing
60
+ _ mboxz, mboxbz
61
+ _ swappable keymappings
62
+ _ bugfix: when returning from a shelling out, sometime ncurses is
63
+ crazy and refuses to interpret any keystrokes
64
+ _ configurable colors
65
+ _ better batch deletion (extend to non-mbox sources)
66
+ _ annotations on messages
67
+ _ pop support
68
+ _ toggleble wrapping of text
69
+ _ maybe: de-archived messages auto-added to inbox
70
+ _ prune old entries from people.txt so that it doesn't grow without
71
+ bound
72
+ _ maildir+ssh
73
+
74
+ maybe
75
+ -----
76
+ _ split out threading & message chunk parsing to a separate library
77
+ _ rangefilter on the initial inbox to only consider the most recent 1000 messages
78
+
79
+ denied
80
+ ------
81
+ x rss feed reading: use rss2email
82
+ x gmail support: obsoleted by imap
83
+
84
+ done
85
+ ----
86
+ x bugfix: deadlock (on rubyforge) (? still valid ?)
87
+ x bugfix: ferret corrupt index problem at index.c:901. see http://ferret.davebalmain.com/trac/ticket/279
88
+ x tab completion for to: and cc: in compose-mode
89
+ x individual labeling in thread-view-mode
90
+ x translate aliases in queries on to: and from: fields
91
+ x tab completion on labeling
3
92
  x bugfix: any interactive prompt after "No new messages." flash has an
4
93
  empty line above it.
5
94
  x detect other sup instances and do something intelligent (because
95
+ ferret crashes violently with more than one index writer open)
6
96
  x refactor all the *-search-results-mode classes
7
97
  x decode RFC 2047 ("encoded word") headers
8
98
  - see: http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/101949, http://dev.rubyonrails.org/ticket/6807
9
- ferret crashes violently with more than one index writer open)
10
99
  x create attachments
11
100
  x add arbitrary labels to sources
12
101
  x improve sup-config
@@ -21,61 +110,6 @@ x bugfix: sup-add not prompting for old accounts, i think? possibly because
21
110
  x wide character support
22
111
  x i18n support
23
112
  x tab completion on labels
24
-
25
- for next release
26
- ----------------
27
- _ use trac or something. this file is getting a little silly.
28
- _ gpg integration
29
- _ user-defined hooks
30
- _ saved searches
31
- _ bugfix: missing sources should be handled better
32
- _ tab completion for contacts
33
- _ bugfix: screwing the headers when editing causes a crash
34
- _ bugfix: sometimes, when one new message comes into an imap folder,
35
- we don't catch it until a reload. but we do see a message
36
- indicating they're loaded to inbox (imap only? hard to reproduce.)
37
- _ bugfix: need a better way to force an address to a particular name,
38
- for things like evite addresses
39
- _ bugfix: ferret flakiness: just added message but can't find it
40
- _ add new message counts until keypress
41
- _ bugfix: deadlock (on rubyforge)
42
- _ bugfix: ferret corrupt index problem at index.c:901. see http://ferret.davebalmain.com/trac/ticket/279
43
- _ bugfix: read before thread-index has finished loading then hides the
44
- thread?!? wtf. (on jamie)_ bugfix: width in index-mode needs to be
45
- determined per-character rather than per-byte
46
- _ search results: highlight relevant snippets and open to relevant
47
- portion of thread
48
- _ rss feed reading
49
- _ forward attachments
50
- _ select all, starred, to me, etc
51
- _ undo
52
- _ gmail support
53
- _ warnings: top-posting, missing attachment, ruby-talk:XXXX detection
54
- _ Net::SMTP support
55
- _ more control character support in buffer line editing
56
-
57
- future
58
- ------
59
- mboxz, mboxbz
60
- swappable keymappings
61
- bugfix: when returning from a shelling out, sometime ncurses is crazy
62
- and refuses to interpret any keystrokes
63
- better batch deletion (extend to non-mbox sources)
64
- annotations on messages
65
- pop
66
- be able to mark individual messages as spam in thread-view-mode
67
- toggle wrapping
68
- maybe: de-archived messages auto-added to inbox
69
- prune old entries from contacts.txt so that it doesn't grow without bound
70
-
71
- maybe
72
- -----
73
- split out threading & message chunk parsing to a separate library
74
- filters
75
- rangefilter on the initial inbox to only consider the most recent 1000 messages
76
-
77
- done
78
- ----
79
113
  x nice little startup config program
80
114
  x bugfix: triggering a pageup when cursor scrolling up jumps to the
81
115
  bottom of the page rather than the next line
data/lib/sup.rb CHANGED
@@ -3,6 +3,7 @@ require 'yaml'
3
3
  require 'zlib'
4
4
  require 'thread'
5
5
  require 'fileutils'
6
+ require 'curses'
6
7
 
7
8
  class Object
8
9
  ## this is for debugging purposes because i keep calling #id on the
@@ -31,7 +32,7 @@ class Module
31
32
  end
32
33
 
33
34
  module Redwood
34
- VERSION = "0.1"
35
+ VERSION = "0.2"
35
36
 
36
37
  BASE_DIR = ENV["SUP_BASE"] || File.join(ENV["HOME"], ".sup")
37
38
  CONFIG_FN = File.join(BASE_DIR, "config.yaml")
@@ -43,6 +44,7 @@ module Redwood
43
44
  SENT_FN = File.join(BASE_DIR, "sent.mbox")
44
45
  LOCK_FN = File.join(BASE_DIR, "lock")
45
46
  SUICIDE_FN = File.join(BASE_DIR, "please-kill-yourself")
47
+ HOOK_DIR = File.join(BASE_DIR, "hooks")
46
48
 
47
49
  YAML_DOMAIN = "masanjin.net"
48
50
  YAML_DATE = "2006-10-01"
@@ -111,6 +113,7 @@ module Redwood
111
113
  Redwood::UpdateManager.new
112
114
  Redwood::PollManager.new
113
115
  Redwood::SuicideManager.new Redwood::SUICIDE_FN
116
+ Redwood::CryptoManager.new
114
117
  end
115
118
 
116
119
  def finish
@@ -193,6 +196,10 @@ else
193
196
  :editor => ENV["EDITOR"] || "/usr/bin/vim -f -c 'setlocal spell spelllang=en_us' -c 'set filetype=mail'",
194
197
  :thread_by_subject => false,
195
198
  :edit_signature => false,
199
+ :ask_for_cc => true,
200
+ :ask_for_bcc => false,
201
+ :confirm_no_attachments => true,
202
+ :confirm_top_posting => true,
196
203
  }
197
204
  begin
198
205
  FileUtils.mkdir_p Redwood::BASE_DIR
@@ -203,8 +210,16 @@ else
203
210
  end
204
211
 
205
212
  require "sup/util"
213
+ require "sup/hook"
214
+
215
+ ## we have to initialize this guy first, because other classes must
216
+ ## reference it in order to register hooks, and they do that at parse
217
+ ## time.
218
+ Redwood::HookManager.new Redwood::HOOK_DIR
219
+
206
220
  require "sup/update"
207
221
  require "sup/suicide"
222
+ require "sup/message-chunks"
208
223
  require "sup/message"
209
224
  require "sup/source"
210
225
  require "sup/mbox"
@@ -224,6 +239,7 @@ require "sup/contact"
224
239
  require "sup/tagger"
225
240
  require "sup/draft"
226
241
  require "sup/poll"
242
+ require "sup/crypto"
227
243
  require "sup/modes/scroll-mode"
228
244
  require "sup/modes/text-mode"
229
245
  require "sup/modes/line-cursor-mode"
@@ -1,6 +1,8 @@
1
1
  require 'etc'
2
2
  require 'thread'
3
+ require 'ncurses'
3
4
 
5
+ if defined? Ncurses
4
6
  module Ncurses
5
7
  def rows
6
8
  lame, lamer = [], []
@@ -14,6 +16,12 @@ module Ncurses
14
16
  lamer.first
15
17
  end
16
18
 
19
+ def curx
20
+ lame, lamer = [], []
21
+ stdscr.getyx lame, lamer
22
+ lamer.first
23
+ end
24
+
17
25
  def mutex; @mutex ||= Mutex.new; end
18
26
  def sync &b; mutex.synchronize(&b); end
19
27
 
@@ -27,12 +35,16 @@ module Ncurses
27
35
  end
28
36
  end
29
37
 
30
- module_function :rows, :cols, :nonblocking_getch, :mutex, :sync
38
+ module_function :rows, :cols, :curx, :nonblocking_getch, :mutex, :sync
39
+
40
+ remove_const :KEY_ENTER
41
+ remove_const :KEY_CANCEL
31
42
 
32
43
  KEY_ENTER = 10
33
- KEY_CANCEL = ?\a # ctrl-g
44
+ KEY_CANCEL = 7 # ctrl-g
34
45
  KEY_TAB = 9
35
46
  end
47
+ end
36
48
 
37
49
  module Redwood
38
50
 
@@ -328,12 +340,29 @@ class BufferManager
328
340
 
329
341
  def ask_with_completions domain, question, completions, default=nil
330
342
  ask domain, question, default do |s|
331
- completions.select { |x| x =~ /^#{s}/i }.map { |x| [x.downcase, x] }
343
+ completions.select { |x| x =~ /^#{s}/i }.map { |x| [x, x] }
344
+ end
345
+ end
346
+
347
+ def ask_many_with_completions domain, question, completions, default=nil, sep=" "
348
+ ask domain, question, default do |partial|
349
+ prefix, target =
350
+ case partial#.gsub(/#{sep}+/, sep)
351
+ when /^\s*$/
352
+ ["", ""]
353
+ when /^(.+#{sep})$/
354
+ [$1, ""]
355
+ when /^(.*#{sep})?(.+?)$/
356
+ [$1 || "", $2]
357
+ else
358
+ raise "william screwed up completion: #{partial.inspect}"
359
+ end
360
+
361
+ completions.select { |x| x =~ /^#{target}/i }.map { |x| [prefix + x, x] }
332
362
  end
333
363
  end
334
364
 
335
- ## returns an ARRAY of filenames!
336
- def ask_for_filenames domain, question, default=nil
365
+ def ask_for_filename domain, question, default=nil
337
366
  answer = ask domain, question, default do |s|
338
367
  if s =~ /(~([^\s\/]*))/ # twiddle directory expansion
339
368
  full = $1
@@ -361,13 +390,53 @@ class BufferManager
361
390
  elsif File.directory?(answer)
362
391
  spawn_modal "file browser", FileBrowserMode.new(answer)
363
392
  else
364
- [answer]
393
+ answer
365
394
  end
366
395
  end
367
396
 
368
- answer || []
397
+ answer
398
+ end
399
+
400
+ ## returns an array of labels
401
+ def ask_for_labels domain, question, default_labels, forbidden_labels=[]
402
+ default_labels = default_labels - forbidden_labels - LabelManager::RESERVED_LABELS
403
+ default = default_labels.join(" ")
404
+ default += " " unless default.empty?
405
+
406
+ applyable_labels = (LabelManager.applyable_labels - forbidden_labels).map { |l| LabelManager.string_for l }.sort_by { |s| s.downcase }
407
+
408
+ answer = ask_many_with_completions domain, question, applyable_labels, default
409
+
410
+ return unless answer
411
+
412
+ user_labels = answer.split(/\s+/).map { |l| l.intern }
413
+ user_labels.each do |l|
414
+ if forbidden_labels.include?(l) || LabelManager::RESERVED_LABELS.include?(l)
415
+ BufferManager.flash "'#{l}' is a reserved label!"
416
+ return
417
+ end
418
+ end
419
+ user_labels
420
+ end
421
+
422
+ def ask_for_contacts domain, question, default_contacts=[]
423
+ default = default_contacts.map { |s| s.to_s }.join(" ")
424
+ default += " " unless default.empty?
425
+
426
+ recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.longname, c.email] }
427
+ contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.longname, c.email] }
428
+
429
+ Redwood::log "recent: #{recent.inspect}"
430
+
431
+ completions = (recent + contacts).flatten.uniq.sort
432
+ answer = BufferManager.ask_many_with_completions domain, question, completions, default, /\s*,\s*/
433
+
434
+ if answer
435
+ answer.split_on_commas.map { |x| ContactManager.contact_for(x.downcase) || PersonManager.person_for(x) }
436
+ end
369
437
  end
370
438
 
439
+
371
440
  def ask domain, question, default=nil, &block
372
441
  raise "impossible!" if @asking
373
442
  @asking = true
@@ -393,27 +462,22 @@ class BufferManager
393
462
 
394
463
  while true
395
464
  c = Ncurses.nonblocking_getch
396
- next unless c # getch timeout
465
+ next unless c # getch timeout
397
466
  break unless tf.handle_input c # process keystroke
398
467
 
399
468
  if tf.new_completions?
400
469
  kill_buffer completion_buf if completion_buf
401
470
 
402
- prefix_len =
403
- if tf.value =~ /\/$/
404
- 0
405
- else
406
- File.basename(tf.value).length
407
- end
471
+ shorts = tf.completions.map { |full, short| short }
472
+ prefix_len = shorts.shared_prefix.length
408
473
 
409
- mode = CompletionMode.new tf.completions.map { |full, short| short }, :header => "Possible completions for \"#{tf.value}\": ", :prefix_len => prefix_len
474
+ mode = CompletionMode.new shorts, :header => "Possible completions for \"#{tf.value}\": ", :prefix_len => prefix_len
410
475
  completion_buf = spawn "<completions>", mode, :height => 10
411
476
 
412
477
  draw_screen :skip_minibuf => true
413
478
  tf.position_cursor
414
479
  elsif tf.roll_completions?
415
480
  completion_buf.mode.roll
416
-
417
481
  draw_screen :skip_minibuf => true
418
482
  tf.position_cursor
419
483
  end