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.
- data/History.txt +5 -0
- data/LICENSE +280 -0
- data/Manifest.txt +52 -0
- data/README.txt +119 -0
- data/Rakefile +45 -0
- data/bin/sup +229 -0
- data/bin/sup-import +162 -0
- data/doc/FAQ.txt +38 -0
- data/doc/Philosophy.txt +59 -0
- data/doc/TODO +31 -0
- data/lib/sup.rb +141 -0
- data/lib/sup/account.rb +53 -0
- data/lib/sup/buffer.rb +391 -0
- data/lib/sup/colormap.rb +118 -0
- data/lib/sup/contact.rb +40 -0
- data/lib/sup/draft.rb +105 -0
- data/lib/sup/index.rb +353 -0
- data/lib/sup/keymap.rb +89 -0
- data/lib/sup/label.rb +41 -0
- data/lib/sup/logger.rb +42 -0
- data/lib/sup/mbox.rb +51 -0
- data/lib/sup/mbox/loader.rb +116 -0
- data/lib/sup/message.rb +302 -0
- data/lib/sup/mode.rb +79 -0
- data/lib/sup/modes/buffer-list-mode.rb +37 -0
- data/lib/sup/modes/compose-mode.rb +33 -0
- data/lib/sup/modes/contact-list-mode.rb +121 -0
- data/lib/sup/modes/edit-message-mode.rb +162 -0
- data/lib/sup/modes/forward-mode.rb +38 -0
- data/lib/sup/modes/help-mode.rb +19 -0
- data/lib/sup/modes/inbox-mode.rb +45 -0
- data/lib/sup/modes/label-list-mode.rb +89 -0
- data/lib/sup/modes/label-search-results-mode.rb +29 -0
- data/lib/sup/modes/line-cursor-mode.rb +133 -0
- data/lib/sup/modes/log-mode.rb +44 -0
- data/lib/sup/modes/person-search-results-mode.rb +29 -0
- data/lib/sup/modes/poll-mode.rb +24 -0
- data/lib/sup/modes/reply-mode.rb +136 -0
- data/lib/sup/modes/resume-mode.rb +18 -0
- data/lib/sup/modes/scroll-mode.rb +106 -0
- data/lib/sup/modes/search-results-mode.rb +31 -0
- data/lib/sup/modes/text-mode.rb +51 -0
- data/lib/sup/modes/thread-index-mode.rb +389 -0
- data/lib/sup/modes/thread-view-mode.rb +338 -0
- data/lib/sup/person.rb +120 -0
- data/lib/sup/poll.rb +80 -0
- data/lib/sup/sent.rb +46 -0
- data/lib/sup/tagger.rb +40 -0
- data/lib/sup/textfield.rb +83 -0
- data/lib/sup/thread.rb +358 -0
- data/lib/sup/update.rb +21 -0
- data/lib/sup/util.rb +260 -0
- metadata +123 -0
data/doc/Philosophy.txt
ADDED
@@ -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
|
+
|
data/doc/TODO
ADDED
@@ -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
|
data/lib/sup.rb
ADDED
@@ -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
|
data/lib/sup/account.rb
ADDED
@@ -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
|
data/lib/sup/buffer.rb
ADDED
@@ -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
|