sup 0.0.1

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 (53) hide show
  1. data/History.txt +5 -0
  2. data/LICENSE +280 -0
  3. data/Manifest.txt +52 -0
  4. data/README.txt +119 -0
  5. data/Rakefile +45 -0
  6. data/bin/sup +229 -0
  7. data/bin/sup-import +162 -0
  8. data/doc/FAQ.txt +38 -0
  9. data/doc/Philosophy.txt +59 -0
  10. data/doc/TODO +31 -0
  11. data/lib/sup.rb +141 -0
  12. data/lib/sup/account.rb +53 -0
  13. data/lib/sup/buffer.rb +391 -0
  14. data/lib/sup/colormap.rb +118 -0
  15. data/lib/sup/contact.rb +40 -0
  16. data/lib/sup/draft.rb +105 -0
  17. data/lib/sup/index.rb +353 -0
  18. data/lib/sup/keymap.rb +89 -0
  19. data/lib/sup/label.rb +41 -0
  20. data/lib/sup/logger.rb +42 -0
  21. data/lib/sup/mbox.rb +51 -0
  22. data/lib/sup/mbox/loader.rb +116 -0
  23. data/lib/sup/message.rb +302 -0
  24. data/lib/sup/mode.rb +79 -0
  25. data/lib/sup/modes/buffer-list-mode.rb +37 -0
  26. data/lib/sup/modes/compose-mode.rb +33 -0
  27. data/lib/sup/modes/contact-list-mode.rb +121 -0
  28. data/lib/sup/modes/edit-message-mode.rb +162 -0
  29. data/lib/sup/modes/forward-mode.rb +38 -0
  30. data/lib/sup/modes/help-mode.rb +19 -0
  31. data/lib/sup/modes/inbox-mode.rb +45 -0
  32. data/lib/sup/modes/label-list-mode.rb +89 -0
  33. data/lib/sup/modes/label-search-results-mode.rb +29 -0
  34. data/lib/sup/modes/line-cursor-mode.rb +133 -0
  35. data/lib/sup/modes/log-mode.rb +44 -0
  36. data/lib/sup/modes/person-search-results-mode.rb +29 -0
  37. data/lib/sup/modes/poll-mode.rb +24 -0
  38. data/lib/sup/modes/reply-mode.rb +136 -0
  39. data/lib/sup/modes/resume-mode.rb +18 -0
  40. data/lib/sup/modes/scroll-mode.rb +106 -0
  41. data/lib/sup/modes/search-results-mode.rb +31 -0
  42. data/lib/sup/modes/text-mode.rb +51 -0
  43. data/lib/sup/modes/thread-index-mode.rb +389 -0
  44. data/lib/sup/modes/thread-view-mode.rb +338 -0
  45. data/lib/sup/person.rb +120 -0
  46. data/lib/sup/poll.rb +80 -0
  47. data/lib/sup/sent.rb +46 -0
  48. data/lib/sup/tagger.rb +40 -0
  49. data/lib/sup/textfield.rb +83 -0
  50. data/lib/sup/thread.rb +358 -0
  51. data/lib/sup/update.rb +21 -0
  52. data/lib/sup/util.rb +260 -0
  53. metadata +123 -0
@@ -0,0 +1,59 @@
1
+ Must an email client have a philosophy? I think so. For many people,
2
+ it is our primary means of communication. Something so important
3
+ should warrant a little thought.
4
+
5
+ So here's Sup's philosophy.
6
+
7
+ Using "traditional" email clients today is increasingly problematic.
8
+ Anyone who's on a high-traffic mailing list knows this. My ruby-talk
9
+ folder is 350 megs and Mutt sits there for 60 seconds while it opens
10
+ it. Keeping up with the all the new traffic is painful, even with
11
+ Mutt's excellent threading features, just because there's so much of
12
+ it. A single thread can span several pages. And Mutt is probably the
13
+ best email client out there in terms of threading and mailing list
14
+ support.
15
+
16
+ The principle problem with traditional clients is that they place a
17
+ high mental cost on the user for each incoming email, by forcing them
18
+ to ask:
19
+ - Should I keep this email, or delete it?
20
+ - If I keep it, where should I file it?
21
+
22
+ For example, I've spent the last 10 years of my life laboriously
23
+ hand-filing every email message I received and feeling a mild sense of
24
+ panic every time an email was both "from Mom" and "about school". The
25
+ massive amounts of email that many people receive, and the cheap cost
26
+ of storage, have made these questions both more costly and less useful
27
+ to answer.
28
+
29
+ As a long-time Mutt user, when I watched people use GMail, I saw them
30
+ use email differently from how I had ever used it. I saw that making
31
+ certain operations quantitatively easier (namely, search) resulted in
32
+ a qualitative difference in usage (and for the better!). I saw that
33
+ thread-centrism had many advantages over message-centrism.
34
+
35
+ So, in many ways, I believe GMail has taken the right approach to
36
+ handle both of the factors above, and much of the inspiration for Sup
37
+ was based on using GMail. Of course, I don't ultimately like using
38
+ GMail, which is why I created Sup in the first place.
39
+
40
+ Sup is based on the following principles, which I learned from GMail:
41
+
42
+ - An immediately accessible and fast search capability over the
43
+ entire email archive eliminates most of the need for folders,
44
+ and eliminates the necessity of having to ever delete email.
45
+
46
+ - Labels eliminate the remaining need for folders.
47
+
48
+ - A thread-centric approach to the UI is much more in line with how
49
+ people operate than dealing with individual messages is. A message
50
+ and its content deserve the same treatment in the vast majority
51
+ of cases.
52
+
53
+ Sup is also based on many ideas from mutt (and Emacs and vi!), having
54
+ to do with the fantastic productivity of a console- and key-based
55
+ application, and the usefulness of multiple buffers, etc., and the
56
+ necessity of handling multiple email accounts, but these features form
57
+ less of the philosophy and more of the general usefulness of Sup.
58
+
59
+
@@ -0,0 +1,31 @@
1
+ forward attachments
2
+ tab completion on labels, contacts
3
+ within-buffer search
4
+ contact selector in edit-message-mode
5
+ undo
6
+ maybe: filters
7
+ maybe: rangefilter on the initial inbox to only consider the most recent 1000 messages
8
+ select all, starred, to me, etc
9
+ editing of arbitrary messages
10
+ annotations on messages
11
+
12
+ x word wrap
13
+ x background indexing
14
+ x auto-insertion of draft messages
15
+ x drafts
16
+ x sent messages loader
17
+ x search: from
18
+ x contacts
19
+ x tagging for group operations
20
+ x view: starred, to me, etc
21
+ x pull in messages by subject as well in load_thread_for_
22
+ x reply+compose+forward
23
+ x resize
24
+ x buffer respawns
25
+ x readline
26
+ x "loading" message
27
+ x search: body, to/from, tags (requires: readline)
28
+ x highlighting/different color stuff
29
+ x config: your email, sendmail, etc
30
+ x status: to/from_you, cc_you_others
31
+ x status: new/not, important
@@ -0,0 +1,141 @@
1
+ require 'rubygems'
2
+ require 'yaml'
3
+ require 'zlib'
4
+ require 'thread'
5
+ require 'fileutils'
6
+ Thread.abort_on_exception = true # make debugging possible
7
+
8
+ class Object
9
+ ## this is for debugging purposes because i keep calling nil.id and
10
+ ## i want it to throw an exception
11
+ def id
12
+ raise "wrong id called"
13
+ end
14
+ end
15
+
16
+ module Redwood
17
+ VERSION = "0.0.1"
18
+
19
+ BASE_DIR = File.join(ENV["HOME"], ".sup")
20
+ CONFIG_FN = File.join(BASE_DIR, "config.yaml")
21
+ SOURCE_FN = File.join(BASE_DIR, "sources.yaml")
22
+ LABEL_FN = File.join(BASE_DIR, "labels.txt")
23
+ CONTACT_FN = File.join(BASE_DIR, "contacts.txt")
24
+ DRAFT_DIR = File.join(BASE_DIR, "drafts")
25
+ SENT_FN = File.join(BASE_DIR, "sent.mbox")
26
+
27
+ YAML_DOMAIN = "masanjin.net"
28
+ YAML_DATE = "2006-10-01"
29
+
30
+ ## one-stop shop for yamliciousness
31
+
32
+ def register_yaml klass, props
33
+ vars = props.map { |p| "@#{p}" }
34
+ path = klass.name.gsub(/::/, "/")
35
+
36
+ klass.instance_eval do
37
+ define_method(:to_yaml_properties) { vars }
38
+ define_method(:to_yaml_type) { "!#{YAML_DOMAIN},#{YAML_DATE}/#{path}" }
39
+ end
40
+
41
+ YAML.add_domain_type("#{YAML_DOMAIN},#{YAML_DATE}", path) do |type, val|
42
+ klass.new(*props.map { |p| val[p] })
43
+ end
44
+ end
45
+
46
+ def save_yaml_obj object, fn, compress=false
47
+ if compress
48
+ Zlib::GzipWriter.open(fn) { |f| f.puts object.to_yaml }
49
+ else
50
+ File.open(fn, "w") { |f| f.puts object.to_yaml }
51
+ end
52
+ end
53
+
54
+ def load_yaml_obj fn, compress=false
55
+ if File.exists? fn
56
+ if compress
57
+ Zlib::GzipReader.open(fn) { |f| YAML::load f }
58
+ else
59
+ YAML::load_file fn
60
+ end
61
+ end
62
+ end
63
+
64
+ module_function :register_yaml, :save_yaml_obj, :load_yaml_obj
65
+ end
66
+
67
+ ## set up default configuration file
68
+
69
+ if File.exists? Redwood::CONFIG_FN
70
+ $config = Redwood::load_yaml_obj Redwood::CONFIG_FN
71
+ else
72
+ $config = {
73
+ :accounts => {
74
+ :default => {
75
+ :name => "Your Name Here",
76
+ :email => "your.email.here@domain.tld",
77
+ :alternates => [],
78
+ :sendmail => "/usr/sbin/sendmail -oem -ti",
79
+ :sig_file => File.join(ENV["HOME"], ".signature")
80
+ }
81
+ },
82
+ :editor => ENV["EDITOR"] || "/usr/bin/vi",
83
+ }
84
+ begin
85
+ FileUtils.mkdir_p Redwood::BASE_DIR
86
+ Redwood::save_yaml_obj $config, Redwood::CONFIG_FN
87
+ rescue StandardError => e
88
+ $stderr.puts "warning: #{e.message}"
89
+ end
90
+ end
91
+
92
+ require "sup/util"
93
+ require "sup/update"
94
+ require "sup/message"
95
+ require "sup/mbox"
96
+ require "sup/person"
97
+ require "sup/account"
98
+ require "sup/thread"
99
+ require "sup/index"
100
+ require "sup/textfield"
101
+ require "sup/buffer"
102
+ require "sup/keymap"
103
+ require "sup/mode"
104
+ require "sup/colormap"
105
+ require "sup/label"
106
+ require "sup/contact"
107
+ require "sup/tagger"
108
+ require "sup/draft"
109
+ require "sup/poll"
110
+ require "sup/modes/scroll-mode"
111
+ require "sup/modes/text-mode"
112
+ require "sup/modes/line-cursor-mode"
113
+ require "sup/modes/help-mode"
114
+ require "sup/modes/edit-message-mode"
115
+ require "sup/modes/compose-mode"
116
+ require "sup/modes/resume-mode"
117
+ require "sup/modes/forward-mode"
118
+ require "sup/modes/reply-mode"
119
+ require "sup/modes/label-list-mode"
120
+ require "sup/modes/contact-list-mode"
121
+ require "sup/modes/thread-view-mode"
122
+ require "sup/modes/thread-index-mode"
123
+ require "sup/modes/label-search-results-mode"
124
+ require "sup/modes/search-results-mode"
125
+ require "sup/modes/person-search-results-mode"
126
+ require "sup/modes/inbox-mode"
127
+ require "sup/modes/buffer-list-mode"
128
+ require "sup/modes/log-mode"
129
+ require "sup/modes/poll-mode"
130
+ require "sup/logger"
131
+ require "sup/sent"
132
+
133
+ module Redwood
134
+ def log s; Logger.log s; end
135
+ module_function :log
136
+ end
137
+
138
+ $:.each do |base|
139
+ d = File.join base, "sup/share/modes/"
140
+ Redwood::Mode.load_all_modes d if File.directory? d
141
+ end
@@ -0,0 +1,53 @@
1
+ module Redwood
2
+
3
+ class Account < Person
4
+ attr_accessor :sendmail, :sig_file
5
+
6
+ def initialize h
7
+ super h[:name], h[:email]
8
+ @sendmail = h[:sendmail]
9
+ @sig_file = h[:signature]
10
+ end
11
+ end
12
+
13
+ class AccountManager
14
+ include Singleton
15
+
16
+ attr_accessor :default_account
17
+
18
+ def initialize accounts
19
+ @email_map = {}
20
+ @alternate_map = {}
21
+ @accounts = {}
22
+ @default_account = nil
23
+
24
+ accounts.each { |k, v| add_account v, k == :default }
25
+
26
+ self.class.i_am_the_instance self
27
+ end
28
+
29
+ def user_accounts; @accounts.keys; end
30
+ def user_emails; (@email_map.keys + @alternate_map.keys).uniq.select { |e| String === e }; end
31
+
32
+ def add_account hash, default=false
33
+ email = hash[:email]
34
+
35
+ next if @email_map.member? email
36
+ a = Account.new hash
37
+ @accounts[a] = true
38
+ @email_map[email] = a
39
+ hash[:alternates].each { |aa| @alternate_map[aa] = a }
40
+ if default
41
+ raise ArgumentError, "multiple default accounts" if @default_account
42
+ @default_account = a
43
+ end
44
+ end
45
+
46
+ def is_account? p; @accounts.member? p; end
47
+ def account_for email
48
+ @email_map[email] || @alternate_map[email] || @alternate_map.argfind { |k, v| k === email && v }
49
+ end
50
+ def is_account_email? email; !account_for(email).nil?; end
51
+ end
52
+
53
+ end
@@ -0,0 +1,391 @@
1
+ require 'thread'
2
+
3
+ module Ncurses
4
+ def rows
5
+ lame, lamer = [], []
6
+ stdscr.getmaxyx lame, lamer
7
+ lame.first
8
+ end
9
+
10
+ def cols
11
+ lame, lamer = [], []
12
+ stdscr.getmaxyx lame, lamer
13
+ lamer.first
14
+ end
15
+
16
+ ## aaahhh, user input. who would have though that such a simple
17
+ ## idea would be SO FUCKING COMPLICATED?! because apparently
18
+ ## Ncurses.getch (and Curses.getch), even in cbreak mode, BLOCKS
19
+ ## ALL THREAD ACTIVITY. as in, no threads anywhere will run while
20
+ ## it's waiting for input. ok, fine, so we wrap it in a select. Of
21
+ ## course we also rely on Ncurses.getch to tell us when an xterm
22
+ ## resize has occurred, which select won't catch, so we won't
23
+ ## resize outselves after a sigwinch until the user hits a key.
24
+ ## and installing our own sigwinch handler means that the screen
25
+ ## size returned by getmaxyx() DOESN'T UPDATE! and Kernel#trap
26
+ ## RETURNS NIL as the previous handler!
27
+ ##
28
+ ## so basically, resizing with multi-threaded ruby Ncurses
29
+ ## applications will always be broken.
30
+ ##
31
+ ## i've coined a new word for this: lametarded.
32
+ def nonblocking_getch
33
+ if IO.select([$stdin], nil, nil, nil)
34
+ Ncurses.getch
35
+ else
36
+ nil
37
+ end
38
+ end
39
+
40
+ module_function :rows, :cols, :nonblocking_getch
41
+
42
+ KEY_CANCEL = "\a"[0] # ctrl-g
43
+ end
44
+
45
+ module Redwood
46
+
47
+ class Buffer
48
+ attr_reader :mode, :x, :y, :width, :height, :title
49
+ bool_reader :dirty
50
+
51
+ def initialize window, mode, width, height, opts={}
52
+ @w = window
53
+ @mode = mode
54
+ @dirty = true
55
+ @focus = false
56
+ @title = opts[:title] || ""
57
+ @x, @y, @width, @height = 0, 0, width, height
58
+ end
59
+
60
+ def content_height; @height - 1; end
61
+ def content_width; @width; end
62
+
63
+ def resize rows, cols
64
+ @width = cols
65
+ @height = rows
66
+ mode.resize rows, cols
67
+ end
68
+
69
+ def redraw
70
+ draw if @dirty
71
+ draw_status
72
+ commit
73
+ end
74
+ def mark_dirty; @dirty = true; end
75
+
76
+ def commit
77
+ @dirty = false
78
+ @w.noutrefresh
79
+ end
80
+
81
+ def draw
82
+ @mode.draw
83
+ draw_status
84
+ commit
85
+ end
86
+
87
+ ## s nil means a blank line!
88
+ def write y, x, s, opts={}
89
+ return if x >= @width || y >= @height
90
+
91
+ @w.attrset Colormap.color_for(opts[:color] || :none, opts[:highlight])
92
+ s ||= ""
93
+ maxl = @width - x
94
+ @w.mvaddstr y, x, s[0 ... maxl]
95
+ unless s.length >= maxl || opts[:no_fill]
96
+ @w.mvaddstr(y, x + s.length, " " * (maxl - s.length))
97
+ end
98
+ end
99
+
100
+ def clear
101
+ @w.clear
102
+ end
103
+
104
+ def draw_status
105
+ write @height - 1, 0, " [#{mode.name}] #{title} #{mode.status}",
106
+ :color => :status_color
107
+ end
108
+
109
+ def focus
110
+ @focus = true
111
+ @dirty = true
112
+ @mode.focus
113
+ end
114
+
115
+ def blur
116
+ @focus = false
117
+ @dirty = true
118
+ @mode.blur
119
+ end
120
+ end
121
+
122
+ class BufferManager
123
+ include Singleton
124
+
125
+ attr_reader :focus_buf
126
+
127
+ def initialize
128
+ @name_map = {}
129
+ @buffers = []
130
+ @focus_buf = nil
131
+ @dirty = true
132
+ @minibuf_stack = []
133
+ @textfields = {}
134
+ @flash = nil
135
+ @shelled_out = false
136
+
137
+ self.class.i_am_the_instance self
138
+ end
139
+
140
+ def buffers; @name_map.to_a; end
141
+
142
+ def focus_on buf
143
+ raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless
144
+ @buffers.member? buf
145
+ return if buf == @focus_buf
146
+ @focus_buf.blur if @focus_buf
147
+ @focus_buf = buf
148
+ @focus_buf.focus
149
+ end
150
+
151
+ def raise_to_front buf
152
+ raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless
153
+ @buffers.member? buf
154
+ @buffers.delete buf
155
+ @buffers.push buf
156
+ focus_on buf
157
+ @dirty = true
158
+ end
159
+
160
+ def roll_buffers
161
+ raise_to_front @buffers.first
162
+ end
163
+
164
+ def roll_buffers_backwards
165
+ return unless @buffers.length > 1
166
+ raise_to_front @buffers[@buffers.length - 2]
167
+ end
168
+
169
+ def handle_input c
170
+ @focus_buf && @focus_buf.mode.handle_input(c)
171
+ end
172
+
173
+ def exists? n; @name_map.member? n; end
174
+ def [] n; @name_map[n]; end
175
+ def []= n, b
176
+ raise ArgumentError, "duplicate buffer name" if b && @name_map.member?(n)
177
+ @name_map[n] = b
178
+ end
179
+
180
+ def completely_redraw_screen
181
+ return if @shelled_out
182
+ Ncurses.clear
183
+ @dirty = true
184
+ draw_screen
185
+ end
186
+
187
+ def handle_resize
188
+ return if @shelled_out
189
+ rows, cols = Ncurses.rows, Ncurses.cols
190
+ @buffers.each { |b| b.resize rows - 1, cols }
191
+ completely_redraw_screen
192
+ flash "resized to #{rows}x#{cols}"
193
+ end
194
+
195
+ def draw_screen skip_minibuf=false
196
+ return if @shelled_out
197
+
198
+ ## disabling this for the time being, to help with debugging
199
+ ## (currently we only have one buffer visible at a time).
200
+ ## TODO: reenable this if we allow multiple buffers
201
+ false && @buffers.inject(@dirty) do |dirty, buf|
202
+ dirty ? buf.draw : buf.redraw
203
+ dirty || buf.dirty?
204
+ end
205
+ ## quick hack
206
+ true && (@dirty ? @buffers.last.draw : @buffers.last.redraw)
207
+
208
+ draw_minibuf unless skip_minibuf
209
+ @dirty = false
210
+ Ncurses.doupdate
211
+ end
212
+
213
+ ## gets the mode from the block, which is only called if the buffer
214
+ ## doesn't already exist. this is useful in the case that generating
215
+ ## the mode is expensive, as it often is.
216
+ def spawn_unless_exists title, opts={}
217
+ if @name_map.member? title
218
+ Redwood::log "buffer '#{title}' already exists, raising to front"
219
+ raise_to_front @name_map[title]
220
+ else
221
+ mode = yield
222
+ spawn title, mode, opts
223
+ end
224
+ @name_map[title]
225
+ end
226
+
227
+ def spawn title, mode, opts={}
228
+ realtitle = title
229
+ num = 2
230
+ while @name_map.member? realtitle
231
+ realtitle = "#{title} #{num}"
232
+ num += 1
233
+ end
234
+
235
+ Redwood::log "spawning buffer \"#{realtitle}\""
236
+ width = opts[:width] || Ncurses.cols
237
+ height = opts[:height] || Ncurses.rows - 1
238
+
239
+ ## since we are currently only doing multiple full-screen modes,
240
+ ## use stdscr for each window. once we become more sophisticated,
241
+ ## we may need to use a new Ncurses::WINDOW
242
+ ##
243
+ ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
244
+ ## (opts[:left] || 0))
245
+ w = Ncurses.stdscr
246
+ raise "nil window" unless w
247
+
248
+ b = Buffer.new w, mode, width, height, :title => realtitle
249
+ mode.buffer = b
250
+ @name_map[realtitle] = b
251
+ if opts[:hidden]
252
+ @buffers.unshift b
253
+ focus_on b unless @focus_buf
254
+ else
255
+ @buffers.push b
256
+ raise_to_front b
257
+ end
258
+ b
259
+ end
260
+
261
+ def kill_all_buffers
262
+ kill_buffer @buffers.first until @buffers.empty?
263
+ end
264
+
265
+ def kill_buffer buf
266
+ raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
267
+ Redwood::log "killing buffer \"#{buf.title}\""
268
+
269
+ buf.mode.cleanup
270
+ @buffers.delete buf
271
+ @name_map.delete buf.title
272
+ @focus_buf = nil if @focus_buf == buf
273
+ if @buffers.empty?
274
+ ## TODO: something intelligent here
275
+ ## for now I will simply prohibit killing the inbox buffer.
276
+ else
277
+ raise_to_front @buffers.last
278
+ end
279
+ end
280
+
281
+ def ask domain, question, default=nil
282
+ @textfields[domain] ||= TextField.new Ncurses.stdscr, Ncurses.rows - 1, 0,
283
+ Ncurses.cols
284
+ tf = @textfields[domain]
285
+
286
+ ## this goddamn ncurses form shit is a fucking 1970's
287
+ ## nightmare. jesus christ. the exact sequence of ncurses events
288
+ ## that needs to happen in order to display a form and have the
289
+ ## entire screen not disappear and have the cursor in the right
290
+ ## place is TOO FUCKING COMPLICATED.
291
+ tf.activate question, default
292
+ @dirty = true
293
+ draw_screen true
294
+ tf.position_cursor
295
+ Ncurses.refresh
296
+
297
+ ret = nil
298
+ while tf.handle_input(Ncurses.nonblocking_getch); end
299
+
300
+ ret = tf.value
301
+ tf.deactivate
302
+ @dirty = true
303
+
304
+ ret
305
+ end
306
+
307
+ ## some pretty lame code in here!
308
+ def ask_getch question, accept=nil
309
+ accept = accept.split(//).map { |x| x[0] } if accept
310
+
311
+ flash question
312
+ Ncurses.curs_set 1
313
+ Ncurses.move Ncurses.rows - 1, question.length + 1
314
+ Ncurses.refresh
315
+
316
+ ret = nil
317
+ done = false
318
+ until done
319
+ key = Ncurses.nonblocking_getch
320
+ if key == Ncurses::KEY_CANCEL
321
+ done = true
322
+ elsif (accept && accept.member?(key)) || !accept
323
+ ret = key
324
+ done = true
325
+ end
326
+ end
327
+
328
+ Ncurses.curs_set 0
329
+ erase_flash
330
+ draw_screen
331
+ Ncurses.curs_set 0
332
+
333
+ ret
334
+ end
335
+
336
+ def ask_yes_or_no question
337
+ [?y, ?Y].member? ask_getch(question, "ynYN")
338
+ end
339
+
340
+ def draw_minibuf
341
+ s = @flash || @minibuf_stack.reverse.find { |x| x } || ""
342
+
343
+ Ncurses.attrset Colormap.color_for(:none)
344
+ Ncurses.mvaddstr Ncurses.rows - 1, 0, s + (" " * [Ncurses.cols - s.length,
345
+ 0].max)
346
+ end
347
+
348
+ def say s, id=nil
349
+ id ||= @minibuf_stack.length
350
+ @minibuf_stack[id] = s
351
+ unless @shelled_out
352
+ draw_minibuf
353
+ Ncurses.refresh
354
+ end
355
+ id
356
+ end
357
+
358
+ def erase_flash; @flash = nil; end
359
+
360
+ def flash s
361
+ @flash = s
362
+ unless @shelled_out
363
+ draw_minibuf
364
+ Ncurses.refresh
365
+ end
366
+ end
367
+
368
+ def clear id
369
+ @minibuf_stack[id] = nil
370
+ if id == @minibuf_stack.length - 1
371
+ id.downto(0) do |i|
372
+ break unless @minibuf_stack[i].nil?
373
+ @minibuf_stack.delete_at i
374
+ end
375
+ end
376
+ unless @shelled_out
377
+ draw_minibuf
378
+ Ncurses.refresh
379
+ end
380
+ end
381
+
382
+ def shell_out command
383
+ @shelled_out = true
384
+ Ncurses.endwin
385
+ system command
386
+ Ncurses.refresh
387
+ Ncurses.curs_set 0
388
+ @shelled_out = false
389
+ end
390
+ end
391
+ end