sup 0.8.1 → 0.9

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 (67) hide show
  1. data/CONTRIBUTORS +13 -6
  2. data/History.txt +19 -0
  3. data/ReleaseNotes +35 -0
  4. data/bin/sup +82 -77
  5. data/bin/sup-add +7 -7
  6. data/bin/sup-config +104 -85
  7. data/bin/sup-dump +4 -5
  8. data/bin/sup-recover-sources +9 -10
  9. data/bin/sup-sync +121 -100
  10. data/bin/sup-sync-back +18 -15
  11. data/bin/sup-tweak-labels +24 -21
  12. data/lib/sup.rb +53 -33
  13. data/lib/sup/account.rb +0 -2
  14. data/lib/sup/buffer.rb +47 -22
  15. data/lib/sup/colormap.rb +6 -6
  16. data/lib/sup/contact.rb +0 -2
  17. data/lib/sup/crypto.rb +34 -23
  18. data/lib/sup/draft.rb +6 -14
  19. data/lib/sup/ferret_index.rb +471 -0
  20. data/lib/sup/hook.rb +30 -43
  21. data/lib/sup/hook.rb.BACKUP.8625.rb +158 -0
  22. data/lib/sup/hook.rb.BACKUP.8681.rb +158 -0
  23. data/lib/sup/hook.rb.BASE.8625.rb +155 -0
  24. data/lib/sup/hook.rb.BASE.8681.rb +155 -0
  25. data/lib/sup/hook.rb.LOCAL.8625.rb +142 -0
  26. data/lib/sup/hook.rb.LOCAL.8681.rb +142 -0
  27. data/lib/sup/hook.rb.REMOTE.8625.rb +145 -0
  28. data/lib/sup/hook.rb.REMOTE.8681.rb +145 -0
  29. data/lib/sup/imap.rb +18 -8
  30. data/lib/sup/index.rb +70 -528
  31. data/lib/sup/interactive-lock.rb +74 -0
  32. data/lib/sup/keymap.rb +26 -26
  33. data/lib/sup/label.rb +2 -4
  34. data/lib/sup/logger.rb +54 -35
  35. data/lib/sup/maildir.rb +41 -6
  36. data/lib/sup/mbox.rb +1 -1
  37. data/lib/sup/mbox/loader.rb +18 -6
  38. data/lib/sup/mbox/ssh-file.rb +1 -7
  39. data/lib/sup/message-chunks.rb +36 -23
  40. data/lib/sup/message.rb +126 -46
  41. data/lib/sup/mode.rb +3 -2
  42. data/lib/sup/modes/console-mode.rb +108 -0
  43. data/lib/sup/modes/edit-message-mode.rb +15 -5
  44. data/lib/sup/modes/inbox-mode.rb +2 -4
  45. data/lib/sup/modes/label-list-mode.rb +1 -1
  46. data/lib/sup/modes/line-cursor-mode.rb +18 -18
  47. data/lib/sup/modes/log-mode.rb +29 -16
  48. data/lib/sup/modes/poll-mode.rb +7 -9
  49. data/lib/sup/modes/reply-mode.rb +5 -3
  50. data/lib/sup/modes/scroll-mode.rb +2 -2
  51. data/lib/sup/modes/search-results-mode.rb +9 -11
  52. data/lib/sup/modes/text-mode.rb +2 -2
  53. data/lib/sup/modes/thread-index-mode.rb +26 -16
  54. data/lib/sup/modes/thread-view-mode.rb +84 -39
  55. data/lib/sup/person.rb +6 -8
  56. data/lib/sup/poll.rb +46 -47
  57. data/lib/sup/rfc2047.rb +1 -5
  58. data/lib/sup/sent.rb +27 -20
  59. data/lib/sup/source.rb +90 -13
  60. data/lib/sup/textfield.rb +4 -4
  61. data/lib/sup/thread.rb +15 -13
  62. data/lib/sup/undo.rb +0 -1
  63. data/lib/sup/update.rb +0 -1
  64. data/lib/sup/util.rb +51 -43
  65. data/lib/sup/xapian_index.rb +566 -0
  66. metadata +57 -46
  67. data/lib/sup/suicide.rb +0 -36
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'rubygems'
4
4
  require 'trollop'
5
+ require 'enumerator'
5
6
  require "sup"
6
7
 
7
8
  class Float
@@ -37,8 +38,8 @@ be seen by running "sup-add --help".
37
38
 
38
39
  Options:
39
40
  EOS
40
- opt :add, "One or more labels (comma-separated) to add to every message from the specified sources", :type => String
41
- opt :remove, "One or more labels (comma-separated) to remove from every message from the specified sources, if those labels are present", :type => String
41
+ opt :add, "One or more labels (comma-separated) to add to every message from the specified sources", :default => ""
42
+ opt :remove, "One or more labels (comma-separated) to remove from every message from the specified sources, if those labels are present", :default => ""
42
43
  opt :query, "A Sup search query", :type => String
43
44
 
44
45
  text <<EOS
@@ -53,23 +54,24 @@ EOS
53
54
  end
54
55
  opts[:verbose] = true if opts[:very_verbose]
55
56
 
56
- add_labels = (opts[:add] || "").split(",").map { |l| l.intern }.uniq
57
- remove_labels = (opts[:remove] || "").split(",").map { |l| l.intern }.uniq
57
+ add_labels = opts[:add].to_set_of_symbols ","
58
+ remove_labels = opts[:remove].to_set_of_symbols ","
58
59
 
59
60
  Trollop::die "nothing to do: no labels to add or remove" if add_labels.empty? && remove_labels.empty?
61
+ Trollop::die "no sources specified" if ARGV.empty?
60
62
 
61
63
  Redwood::start
64
+ index = Redwood::Index.init
65
+ index.lock_interactively or exit
62
66
  begin
63
- index = Redwood::Index.new
64
67
  index.load
65
68
 
66
- source_ids =
67
- if opts[:all_sources]
68
- index.sources
69
- else
70
- ARGV.map do |uri|
71
- index.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
72
- end
69
+ source_ids = if opts[:all_sources]
70
+ Redwood::SourceManager.sources
71
+ else
72
+ ARGV.map do |uri|
73
+ Redwood::SourceManager.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
74
+ end
73
75
  end.map { |s| s.id }
74
76
  Trollop::die "nothing to do: no sources" if source_ids.empty?
75
77
 
@@ -81,29 +83,30 @@ begin
81
83
  end
82
84
  query += ' ' + opts[:query] if opts[:query]
83
85
 
84
- docs = Redwood::Index.run_query query
85
- num_total = docs.size
86
+ parsed_query = index.parse_query query
87
+ parsed_query.merge! :load_spam => true, :load_deleted => true, :load_killed => true
88
+ ids = Enumerable::Enumerator.new(index, :each_id, parsed_query).map
89
+ num_total = ids.size
86
90
 
87
91
  $stderr.puts "Found #{num_total} documents across #{source_ids.length} sources. Scanning..."
88
92
 
89
93
  num_changed = num_scanned = 0
90
94
  last_info_time = start_time = Time.now
91
- docs.each do |id|
95
+ ids.each do |id|
92
96
  num_scanned += 1
93
97
 
94
98
  m = index.build_message id
95
- old_labels = m.labels.clone
99
+ old_labels = m.labels.dup
96
100
 
97
101
  m.labels += add_labels
98
102
  m.labels -= remove_labels
99
- m.labels = m.labels.uniq
100
103
 
101
- unless m.labels.sort_by { |s| s.to_s } == old_labels.sort_by { |s| s.to_s }
104
+ unless m.labels == old_labels
102
105
  num_changed += 1
103
106
  puts "From #{m.from}, subject: #{m.subj}" if opts[:very_verbose]
104
- puts "#{m.id}: {#{old_labels.join ','}} => {#{m.labels.join ','}}" if opts[:verbose]
107
+ puts "#{m.id}: {#{old_labels.to_a.join ','}} => {#{m.labels.to_a.join ','}}" if opts[:verbose]
105
108
  puts if opts[:very_verbose]
106
- index.sync_message m unless opts[:dry_run]
109
+ index.update_message_state m unless opts[:dry_run]
107
110
  end
108
111
 
109
112
  if Time.now - last_info_time > 60
@@ -118,7 +121,7 @@ begin
118
121
 
119
122
  unless num_changed == 0
120
123
  $stderr.puts "Optimizing index..."
121
- index.ferret.optimize unless opts[:dry_run]
124
+ index.optimize unless opts[:dry_run]
122
125
  end
123
126
 
124
127
  rescue Exception => e
data/lib/sup.rb CHANGED
@@ -5,6 +5,10 @@ require 'thread'
5
5
  require 'fileutils'
6
6
  require 'gettext'
7
7
  require 'curses'
8
+ begin
9
+ require 'fastthread'
10
+ rescue LoadError
11
+ end
8
12
 
9
13
  class Object
10
14
  ## this is for debugging purposes because i keep calling #id on the
@@ -20,7 +24,7 @@ class Module
20
24
  vars = props.map { |p| "@#{p}" }
21
25
  klass = self
22
26
  path = klass.name.gsub(/::/, "/")
23
-
27
+
24
28
  klass.instance_eval do
25
29
  define_method(:to_yaml_properties) { vars }
26
30
  define_method(:to_yaml_type) { "!#{Redwood::YAML_DOMAIN},#{Redwood::YAML_DATE}/#{path}" }
@@ -33,7 +37,7 @@ class Module
33
37
  end
34
38
 
35
39
  module Redwood
36
- VERSION = "0.8.1"
40
+ VERSION = "0.9"
37
41
 
38
42
  BASE_DIR = ENV["SUP_BASE"] || File.join(ENV["HOME"], ".sup")
39
43
  CONFIG_FN = File.join(BASE_DIR, "config.yaml")
@@ -50,6 +54,8 @@ module Redwood
50
54
  YAML_DOMAIN = "masanjin.net"
51
55
  YAML_DATE = "2006-10-01"
52
56
 
57
+ DEFAULT_INDEX = 'ferret'
58
+
53
59
  ## record exceptions thrown in threads nicely
54
60
  @exceptions = []
55
61
  @exception_mutex = Mutex.new
@@ -79,38 +85,52 @@ module Redwood
79
85
  module_function :reporting_thread, :record_exception, :exceptions
80
86
 
81
87
  ## one-stop shop for yamliciousness
82
- def save_yaml_obj object, fn, safe=false
88
+ def save_yaml_obj o, fn, safe=false
89
+ o = if o.is_a?(Array)
90
+ o.map { |x| (x.respond_to?(:before_marshal) && x.before_marshal) || x }
91
+ elsif o.respond_to? :before_marshal
92
+ o.before_marshal
93
+ else
94
+ o
95
+ end
96
+
83
97
  if safe
84
98
  safe_fn = "#{File.dirname fn}/safe_#{File.basename fn}"
85
99
  mode = File.stat(fn).mode if File.exists? fn
86
- File.open(safe_fn, "w", mode) { |f| f.puts object.to_yaml }
100
+ File.open(safe_fn, "w", mode) { |f| f.puts o.to_yaml }
87
101
  FileUtils.mv safe_fn, fn
88
102
  else
89
- File.open(fn, "w") { |f| f.puts object.to_yaml }
103
+ File.open(fn, "w") { |f| f.puts o.to_yaml }
90
104
  end
91
105
  end
92
106
 
93
107
  def load_yaml_obj fn, compress=false
94
- if File.exists? fn
108
+ o = if File.exists? fn
95
109
  if compress
96
110
  Zlib::GzipReader.open(fn) { |f| YAML::load f }
97
111
  else
98
112
  YAML::load_file fn
99
113
  end
100
114
  end
115
+ if o.is_a?(Array)
116
+ o.each { |x| x.after_unmarshal! if x.respond_to?(:after_unmarshal!) }
117
+ else
118
+ o.after_unmarshal! if o.respond_to?(:after_unmarshal!)
119
+ end
120
+ o
101
121
  end
102
122
 
103
123
  def start
104
- Redwood::SentManager.new Redwood::SENT_FN
105
- Redwood::ContactManager.new Redwood::CONTACT_FN
106
- Redwood::LabelManager.new Redwood::LABEL_FN
107
- Redwood::AccountManager.new $config[:accounts]
108
- Redwood::DraftManager.new Redwood::DRAFT_DIR
109
- Redwood::UpdateManager.new
110
- Redwood::PollManager.new
111
- Redwood::SuicideManager.new Redwood::SUICIDE_FN
112
- Redwood::CryptoManager.new
113
- Redwood::UndoManager.new
124
+ Redwood::SentManager.init $config[:sent_source] || 'sup://sent'
125
+ Redwood::ContactManager.init Redwood::CONTACT_FN
126
+ Redwood::LabelManager.init Redwood::LABEL_FN
127
+ Redwood::AccountManager.init $config[:accounts]
128
+ Redwood::DraftManager.init Redwood::DRAFT_DIR
129
+ Redwood::UpdateManager.init
130
+ Redwood::PollManager.init
131
+ Redwood::CryptoManager.init
132
+ Redwood::UndoManager.init
133
+ Redwood::SourceManager.init
114
134
  end
115
135
 
116
136
  def finish
@@ -126,7 +146,7 @@ module Redwood
126
146
  def report_broken_sources opts={}
127
147
  return unless BufferManager.instantiated?
128
148
 
129
- broken_sources = Index.sources.select { |s| s.error.is_a? FatalSourceError }
149
+ broken_sources = SourceManager.sources.select { |s| s.error.is_a? FatalSourceError }
130
150
  unless broken_sources.empty?
131
151
  BufferManager.spawn_unless_exists("Broken source notification for #{broken_sources.join(',')}", opts) do
132
152
  TextMode.new(<<EOM)
@@ -143,7 +163,7 @@ EOM
143
163
  end
144
164
  end
145
165
 
146
- desynced_sources = Index.sources.select { |s| s.error.is_a? OutOfSyncSourceError }
166
+ desynced_sources = SourceManager.sources.select { |s| s.error.is_a? OutOfSyncSourceError }
147
167
  unless desynced_sources.empty?
148
168
  BufferManager.spawn_unless_exists("Out-of-sync source notification for #{broken_sources.join(',')}", opts) do
149
169
  TextMode.new(<<EOM)
@@ -175,6 +195,7 @@ end
175
195
  ## set up default configuration file
176
196
  if File.exists? Redwood::CONFIG_FN
177
197
  $config = Redwood::load_yaml_obj Redwood::CONFIG_FN
198
+ abort "#{Redwood::CONFIG_FN} is not a valid configuration file (it's a #{$config.class}, not a hash)" unless $config.is_a?(Hash)
178
199
  else
179
200
  require 'etc'
180
201
  require 'socket'
@@ -207,6 +228,7 @@ else
207
228
  :confirm_top_posting => true,
208
229
  :discard_snippets_from_encrypted_messages => false,
209
230
  :default_attachment_save_dir => "",
231
+ :sent_source => "sup://sent"
210
232
  }
211
233
  begin
212
234
  FileUtils.mkdir_p Redwood::BASE_DIR
@@ -222,33 +244,29 @@ require "sup/hook"
222
244
  ## we have to initialize this guy first, because other classes must
223
245
  ## reference it in order to register hooks, and they do that at parse
224
246
  ## time.
225
- Redwood::HookManager.new Redwood::HOOK_DIR
247
+ Redwood::HookManager.init Redwood::HOOK_DIR
226
248
 
227
249
  ## everything we need to get logging working
228
- require "sup/buffer"
229
- require "sup/keymap"
230
- require "sup/mode"
231
- require "sup/modes/scroll-mode"
232
- require "sup/modes/text-mode"
233
- require "sup/modes/log-mode"
234
250
  require "sup/logger"
235
- module Redwood
236
- def log s; Logger.log s; end
237
- module_function :log
238
- end
251
+ Redwood::Logger.init.add_sink $stderr
252
+ include Redwood::LogsStuff
239
253
 
240
254
  ## determine encoding and character set
241
255
  $encoding = Locale.current.charset
242
256
  if $encoding
243
- Redwood::log "using character set encoding #{$encoding.inspect}"
257
+ debug "using character set encoding #{$encoding.inspect}"
244
258
  else
245
- Redwood::log "warning: can't find character set by using locale, defaulting to utf-8"
259
+ warn "can't find character set by using locale, defaulting to utf-8"
246
260
  $encoding = "UTF-8"
247
261
  end
248
262
 
249
- ## now everything else (which can feel free to call Redwood::log at load time)
263
+ require "sup/buffer"
264
+ require "sup/keymap"
265
+ require "sup/mode"
266
+ require "sup/modes/scroll-mode"
267
+ require "sup/modes/text-mode"
268
+ require "sup/modes/log-mode"
250
269
  require "sup/update"
251
- require "sup/suicide"
252
270
  require "sup/message-chunks"
253
271
  require "sup/message"
254
272
  require "sup/source"
@@ -258,6 +276,7 @@ require "sup/imap"
258
276
  require "sup/person"
259
277
  require "sup/account"
260
278
  require "sup/thread"
279
+ require "sup/interactive-lock"
261
280
  require "sup/index"
262
281
  require "sup/textfield"
263
282
  require "sup/colormap"
@@ -288,6 +307,7 @@ require "sup/modes/buffer-list-mode"
288
307
  require "sup/modes/poll-mode"
289
308
  require "sup/modes/file-browser-mode"
290
309
  require "sup/modes/completion-mode"
310
+ require "sup/modes/console-mode"
291
311
  require "sup/sent"
292
312
 
293
313
  $:.each do |base|
@@ -25,8 +25,6 @@ class AccountManager
25
25
 
26
26
  add_account accounts[:default], true
27
27
  accounts.each { |k, v| add_account v, false unless k == :default }
28
-
29
- self.class.i_am_the_instance self
30
28
  end
31
29
 
32
30
  def user_accounts; @accounts.keys; end
@@ -25,17 +25,24 @@ module Ncurses
25
25
  def mutex; @mutex ||= Mutex.new; end
26
26
  def sync &b; mutex.synchronize(&b); end
27
27
 
28
- ## magically, this stuff seems to work now. i could swear it didn't
29
- ## before. hm.
30
28
  def nonblocking_getch
31
- if IO.select([$stdin], nil, nil, 1)
32
- Ncurses.getch
33
- else
34
- nil
29
+ ## INSANTIY
30
+ ## it is NECESSARY to wrap Ncurses.getch in a select() otherwise all
31
+ ## background threads will be BLOCKED. (except in very modern versions
32
+ ## of libncurses-ruby. the current one on ubuntu seems to work well.)
33
+ if IO.select([$stdin], nil, nil, 0.5)
34
+ c = Ncurses.getch
35
35
  end
36
36
  end
37
37
 
38
- module_function :rows, :cols, :curx, :nonblocking_getch, :mutex, :sync
38
+ ## pretends ctrl-c's are ctrl-g's
39
+ def safe_nonblocking_getch
40
+ nonblocking_getch
41
+ rescue Interrupt
42
+ KEY_CANCEL
43
+ end
44
+
45
+ module_function :rows, :cols, :curx, :nonblocking_getch, :safe_nonblocking_getch, :mutex, :sync
39
46
 
40
47
  remove_const :KEY_ENTER
41
48
  remove_const :KEY_CANCEL
@@ -70,7 +77,7 @@ class Buffer
70
77
  def content_height; @height - 1; end
71
78
  def content_width; @width; end
72
79
 
73
- def resize rows, cols
80
+ def resize rows, cols
74
81
  return if cols == @width && rows == @height
75
82
  @width = cols
76
83
  @height = rows
@@ -196,10 +203,13 @@ EOS
196
203
  @flash = nil
197
204
  @shelled = @asking = false
198
205
  @in_x = ENV["TERM"] =~ /(xterm|rxvt|screen)/
199
-
200
- self.class.i_am_the_instance self
206
+ @sigwinch_happened = false
207
+ @sigwinch_mutex = Mutex.new
201
208
  end
202
209
 
210
+ def sigwinch_happened!; @sigwinch_mutex.synchronize { @sigwinch_happened = true } end
211
+ def sigwinch_happened?; @sigwinch_mutex.synchronize { @sigwinch_happened } end
212
+
203
213
  def buffers; @name_map.to_a; end
204
214
 
205
215
  def focus_on buf
@@ -230,14 +240,20 @@ EOS
230
240
  ## have to change this. but it's not clear that we will ever actually
231
241
  ## do that.
232
242
  def roll_buffers
233
- @buffers.last.force_to_top = false
234
- raise_to_front @buffers.first
243
+ bufs = rollable_buffers
244
+ bufs.last.force_to_top = false
245
+ raise_to_front bufs.first
235
246
  end
236
247
 
237
248
  def roll_buffers_backwards
238
- return unless @buffers.length > 1
239
- @buffers.last.force_to_top = false
240
- raise_to_front @buffers[@buffers.length - 2]
249
+ bufs = rollable_buffers
250
+ return unless bufs.length > 1
251
+ bufs.last.force_to_top = false
252
+ raise_to_front bufs[bufs.length - 2]
253
+ end
254
+
255
+ def rollable_buffers
256
+ @buffers.select { |b| !b.system? || @buffers.last == b }
241
257
  end
242
258
 
243
259
  def handle_input c
@@ -261,6 +277,14 @@ EOS
261
277
  def completely_redraw_screen
262
278
  return if @shelled
263
279
 
280
+ ## this magic makes Ncurses get the new size of the screen
281
+ Ncurses.endwin
282
+ Ncurses.stdscr.keypad 1
283
+ Ncurses.curs_set 0
284
+ Ncurses.refresh
285
+ @sigwinch_mutex.synchronize { @sigwinch_happened = false }
286
+ debug "new screen size is #{Ncurses.rows} x #{Ncurses.cols}"
287
+
264
288
  status, title = get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
265
289
 
266
290
  Ncurses.sync do
@@ -366,7 +390,7 @@ EOS
366
390
  draw_screen
367
391
 
368
392
  until mode.done?
369
- c = Ncurses.nonblocking_getch
393
+ c = Ncurses.safe_nonblocking_getch
370
394
  next unless c # getch timeout
371
395
  break if c == Ncurses::KEY_CANCEL
372
396
  begin
@@ -468,7 +492,7 @@ EOS
468
492
  end
469
493
 
470
494
  if answer
471
- answer =
495
+ answer =
472
496
  if answer.empty?
473
497
  spawn_modal "file browser", FileBrowserMode.new
474
498
  elsif File.directory?(answer)
@@ -484,7 +508,7 @@ EOS
484
508
  ## returns an array of labels
485
509
  def ask_for_labels domain, question, default_labels, forbidden_labels=[]
486
510
  default_labels = default_labels - forbidden_labels - LabelManager::RESERVED_LABELS
487
- default = default_labels.join(" ")
511
+ default = default_labels.to_a.join(" ")
488
512
  default += " " unless default.empty?
489
513
 
490
514
  # here I would prefer to give more control and allow all_labels instead of
@@ -495,7 +519,7 @@ EOS
495
519
 
496
520
  return unless answer
497
521
 
498
- user_labels = answer.symbolistize
522
+ user_labels = answer.to_set_of_symbols
499
523
  user_labels.each do |l|
500
524
  if forbidden_labels.include?(l) || LabelManager::RESERVED_LABELS.include?(l)
501
525
  BufferManager.flash "'#{l}' is a reserved label!"
@@ -508,7 +532,7 @@ EOS
508
532
  def ask_for_contacts domain, question, default_contacts=[]
509
533
  default = default_contacts.map { |s| s.to_s }.join(" ")
510
534
  default += " " unless default.empty?
511
-
535
+
512
536
  recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.full_address, c.email] }
513
537
  contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.full_address, c.email] }
514
538
 
@@ -542,7 +566,7 @@ EOS
542
566
  end
543
567
 
544
568
  while true
545
- c = Ncurses.nonblocking_getch
569
+ c = Ncurses.safe_nonblocking_getch
546
570
  next unless c # getch timeout
547
571
  break unless tf.handle_input c # process keystroke
548
572
 
@@ -595,7 +619,7 @@ EOS
595
619
  ret = nil
596
620
  done = false
597
621
  until done
598
- key = Ncurses.nonblocking_getch or next
622
+ key = Ncurses.safe_nonblocking_getch or next
599
623
  if key == Ncurses::KEY_CANCEL
600
624
  done = true
601
625
  elsif accept.nil? || accept.empty? || accept.member?(key)
@@ -723,6 +747,7 @@ EOS
723
747
  Ncurses.sync do
724
748
  Ncurses.endwin
725
749
  system command
750
+ Ncurses.stdscr.keypad 1
726
751
  Ncurses.refresh
727
752
  Ncurses.curs_set 0
728
753
  end