sup 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +12 -0
  4. data/CONTRIBUTORS +84 -0
  5. data/Gemfile +3 -0
  6. data/HACKING +42 -0
  7. data/History.txt +361 -0
  8. data/LICENSE +280 -0
  9. data/README.md +70 -0
  10. data/Rakefile +12 -0
  11. data/ReleaseNotes +231 -0
  12. data/bin/sup +434 -0
  13. data/bin/sup-add +118 -0
  14. data/bin/sup-config +243 -0
  15. data/bin/sup-dump +43 -0
  16. data/bin/sup-import-dump +101 -0
  17. data/bin/sup-psych-ify-config-files +21 -0
  18. data/bin/sup-recover-sources +87 -0
  19. data/bin/sup-sync +210 -0
  20. data/bin/sup-sync-back-maildir +127 -0
  21. data/bin/sup-tweak-labels +140 -0
  22. data/contrib/colorpicker.rb +100 -0
  23. data/contrib/completion/_sup.zsh +114 -0
  24. data/devel/console.sh +3 -0
  25. data/devel/count-loc.sh +3 -0
  26. data/devel/load-index.rb +9 -0
  27. data/devel/profile.rb +12 -0
  28. data/devel/start-console.rb +5 -0
  29. data/doc/FAQ.txt +119 -0
  30. data/doc/Hooks.txt +79 -0
  31. data/doc/Philosophy.txt +69 -0
  32. data/lib/sup.rb +467 -0
  33. data/lib/sup/account.rb +90 -0
  34. data/lib/sup/buffer.rb +768 -0
  35. data/lib/sup/colormap.rb +239 -0
  36. data/lib/sup/contact.rb +67 -0
  37. data/lib/sup/crypto.rb +461 -0
  38. data/lib/sup/draft.rb +119 -0
  39. data/lib/sup/hook.rb +159 -0
  40. data/lib/sup/horizontal_selector.rb +59 -0
  41. data/lib/sup/idle.rb +42 -0
  42. data/lib/sup/index.rb +882 -0
  43. data/lib/sup/interactive_lock.rb +89 -0
  44. data/lib/sup/keymap.rb +140 -0
  45. data/lib/sup/label.rb +87 -0
  46. data/lib/sup/logger.rb +77 -0
  47. data/lib/sup/logger/singleton.rb +10 -0
  48. data/lib/sup/maildir.rb +257 -0
  49. data/lib/sup/mbox.rb +187 -0
  50. data/lib/sup/message.rb +803 -0
  51. data/lib/sup/message_chunks.rb +328 -0
  52. data/lib/sup/mode.rb +140 -0
  53. data/lib/sup/modes/buffer_list_mode.rb +50 -0
  54. data/lib/sup/modes/completion_mode.rb +55 -0
  55. data/lib/sup/modes/compose_mode.rb +38 -0
  56. data/lib/sup/modes/console_mode.rb +125 -0
  57. data/lib/sup/modes/contact_list_mode.rb +148 -0
  58. data/lib/sup/modes/edit_message_async_mode.rb +110 -0
  59. data/lib/sup/modes/edit_message_mode.rb +728 -0
  60. data/lib/sup/modes/file_browser_mode.rb +109 -0
  61. data/lib/sup/modes/forward_mode.rb +82 -0
  62. data/lib/sup/modes/help_mode.rb +19 -0
  63. data/lib/sup/modes/inbox_mode.rb +85 -0
  64. data/lib/sup/modes/label_list_mode.rb +138 -0
  65. data/lib/sup/modes/label_search_results_mode.rb +38 -0
  66. data/lib/sup/modes/line_cursor_mode.rb +203 -0
  67. data/lib/sup/modes/log_mode.rb +57 -0
  68. data/lib/sup/modes/person_search_results_mode.rb +12 -0
  69. data/lib/sup/modes/poll_mode.rb +19 -0
  70. data/lib/sup/modes/reply_mode.rb +228 -0
  71. data/lib/sup/modes/resume_mode.rb +52 -0
  72. data/lib/sup/modes/scroll_mode.rb +252 -0
  73. data/lib/sup/modes/search_list_mode.rb +204 -0
  74. data/lib/sup/modes/search_results_mode.rb +59 -0
  75. data/lib/sup/modes/text_mode.rb +76 -0
  76. data/lib/sup/modes/thread_index_mode.rb +1033 -0
  77. data/lib/sup/modes/thread_view_mode.rb +941 -0
  78. data/lib/sup/person.rb +134 -0
  79. data/lib/sup/poll.rb +272 -0
  80. data/lib/sup/rfc2047.rb +56 -0
  81. data/lib/sup/search.rb +110 -0
  82. data/lib/sup/sent.rb +58 -0
  83. data/lib/sup/service/label_service.rb +45 -0
  84. data/lib/sup/source.rb +244 -0
  85. data/lib/sup/tagger.rb +50 -0
  86. data/lib/sup/textfield.rb +253 -0
  87. data/lib/sup/thread.rb +452 -0
  88. data/lib/sup/time.rb +93 -0
  89. data/lib/sup/undo.rb +38 -0
  90. data/lib/sup/update.rb +30 -0
  91. data/lib/sup/util.rb +747 -0
  92. data/lib/sup/util/ncurses.rb +274 -0
  93. data/lib/sup/util/path.rb +9 -0
  94. data/lib/sup/util/query.rb +17 -0
  95. data/lib/sup/util/uri.rb +15 -0
  96. data/lib/sup/version.rb +3 -0
  97. data/sup.gemspec +53 -0
  98. data/test/dummy_source.rb +61 -0
  99. data/test/gnupg_test_home/gpg.conf +1 -0
  100. data/test/gnupg_test_home/pubring.gpg +0 -0
  101. data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
  102. data/test/gnupg_test_home/receiver_secring.gpg +0 -0
  103. data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
  104. data/test/gnupg_test_home/secring.gpg +0 -0
  105. data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -0
  106. data/test/gnupg_test_home/trustdb.gpg +0 -0
  107. data/test/integration/test_label_service.rb +18 -0
  108. data/test/messages/bad-content-transfer-encoding-1.eml +8 -0
  109. data/test/messages/binary-content-transfer-encoding-2.eml +21 -0
  110. data/test/messages/missing-line.eml +9 -0
  111. data/test/test_crypto.rb +109 -0
  112. data/test/test_header_parsing.rb +168 -0
  113. data/test/test_helper.rb +7 -0
  114. data/test/test_message.rb +532 -0
  115. data/test/test_messages_dir.rb +147 -0
  116. data/test/test_yaml_migration.rb +85 -0
  117. data/test/test_yaml_regressions.rb +17 -0
  118. data/test/unit/service/test_label_service.rb +19 -0
  119. data/test/unit/test_horizontal_selector.rb +40 -0
  120. data/test/unit/util/test_query.rb +46 -0
  121. data/test/unit/util/test_string.rb +57 -0
  122. data/test/unit/util/test_uri.rb +19 -0
  123. metadata +423 -0
@@ -0,0 +1,52 @@
1
+ module Redwood
2
+
3
+ class ResumeMode < EditMessageMode
4
+ def initialize m
5
+ @m = m
6
+ @safe = false
7
+
8
+ header, body = parse_file m.draft_filename
9
+ header.delete "Date"
10
+
11
+ super :header => header, :body => body, :have_signature => true
12
+ rescue Errno::ENOENT
13
+ DraftManager.discard @m
14
+ BufferManager.flash "Draft deleted outside of sup."
15
+ end
16
+
17
+ def unsaved?; !@safe end
18
+
19
+ def killable?
20
+ return true if @safe
21
+
22
+ case BufferManager.ask_yes_or_no "Discard draft?"
23
+ when true
24
+ DraftManager.discard @m
25
+ BufferManager.flash "Draft discarded."
26
+ true
27
+ when false
28
+ if edited?
29
+ DraftManager.write_draft { |f| write_message f, false }
30
+ DraftManager.discard @m
31
+ BufferManager.flash "Draft saved."
32
+ end
33
+ true
34
+ else
35
+ false
36
+ end
37
+ end
38
+
39
+ def send_message
40
+ if super
41
+ DraftManager.discard @m
42
+ @safe = true
43
+ end
44
+ end
45
+
46
+ def save_as_draft
47
+ @safe = true
48
+ DraftManager.discard @m if super
49
+ end
50
+ end
51
+
52
+ end
@@ -0,0 +1,252 @@
1
+ module Redwood
2
+
3
+ class ScrollMode < Mode
4
+ ## we define topline and botline as the top and bottom lines of any
5
+ ## content in the currentview.
6
+
7
+ ## we left leftcol and rightcol as the left and right columns of any
8
+ ## content in the current view. but since we're operating in a
9
+ ## line-centric fashion, rightcol is always leftcol + the buffer
10
+ ## width. (whereas botline is topline + at most the buffer height,
11
+ ## and can be == to topline in the case that there's no content.)
12
+
13
+ attr_reader :status, :topline, :botline, :leftcol
14
+
15
+ register_keymap do |k|
16
+ k.add :line_down, "Down one line", :down, 'j', 'J', "\C-e"
17
+ k.add :line_up, "Up one line", :up, 'k', 'K', "\C-y"
18
+ k.add :col_left, "Left one column", :left, 'h'
19
+ k.add :col_right, "Right one column", :right, 'l'
20
+ k.add :page_down, "Down one page", :page_down, ' ', "\C-f"
21
+ k.add :page_up, "Up one page", :page_up, 'p', :backspace, "\C-b"
22
+ k.add :half_page_down, "Down one half page", "\C-d"
23
+ k.add :half_page_up, "Up one half page", "\C-u"
24
+ k.add :jump_to_start, "Jump to top", :home, '^', '1'
25
+ k.add :jump_to_end, "Jump to bottom", :end, '$', '0'
26
+ k.add :jump_to_left, "Jump to the left", '['
27
+ k.add :search_in_buffer, "Search in current buffer", '/'
28
+ k.add :continue_search_in_buffer, "Jump to next search occurrence in buffer", BufferManager::CONTINUE_IN_BUFFER_SEARCH_KEY
29
+ end
30
+
31
+ def initialize opts={}
32
+ @topline, @botline, @leftcol = 0, 0, 0
33
+ @slip_rows = opts[:slip_rows] || 0 # when we pgup/pgdown,
34
+ # how many lines do we keep?
35
+ @twiddles = opts.member?(:twiddles) ? opts[:twiddles] : true
36
+ @search_query = nil
37
+ @search_line = nil
38
+ @status = ""
39
+ super()
40
+ end
41
+
42
+ def rightcol; @leftcol + buffer.content_width; end
43
+
44
+ def draw
45
+ ensure_mode_validity
46
+ (@topline ... @botline).each { |ln| draw_line ln, :color => :text_color }
47
+ ((@botline - @topline) ... buffer.content_height).each do |ln|
48
+ if @twiddles
49
+ buffer.write ln, 0, "~", :color => :twiddle_color
50
+ else
51
+ buffer.write ln, 0, "", :color => :text_color
52
+ end
53
+ end
54
+ @status = "lines #{@topline + 1}:#{@botline}/#{lines}"
55
+ end
56
+
57
+ def in_search?; @search_line end
58
+ def cancel_search!; @search_line = nil end
59
+
60
+ def continue_search_in_buffer
61
+ unless @search_query
62
+ BufferManager.flash "No current search!"
63
+ return
64
+ end
65
+
66
+ start = @search_line || search_start_line
67
+ line, col = find_text @search_query, start
68
+ if line.nil? && (start > 0)
69
+ line, col = find_text @search_query, 0
70
+ BufferManager.flash "Search wrapped to top!" if line
71
+ end
72
+ if line
73
+ @search_line = line + 1
74
+ search_goto_pos line, col, col + @search_query.display_length
75
+ buffer.mark_dirty
76
+ else
77
+ BufferManager.flash "Not found!"
78
+ end
79
+ end
80
+
81
+ def search_in_buffer
82
+ query = BufferManager.ask :search, "search in buffer: "
83
+ return if query.nil? || query.empty?
84
+ @search_query = Regexp.escape query
85
+ continue_search_in_buffer
86
+ end
87
+
88
+ ## subclasses can override these three!
89
+ def search_goto_pos line, leftcol, rightcol
90
+ search_goto_line line
91
+
92
+ if rightcol > self.rightcol # if it's occluded...
93
+ jump_to_col [rightcol - buffer.content_width + 1, 0].max # move right
94
+ end
95
+ end
96
+ def search_start_line; @topline end
97
+ def search_goto_line line; jump_to_line line end
98
+
99
+ def col_jump
100
+ $config[:col_jump] || 2
101
+ end
102
+
103
+ def col_left
104
+ return unless @leftcol > 0
105
+ @leftcol -= col_jump
106
+ buffer.mark_dirty
107
+ end
108
+
109
+ def col_right
110
+ @leftcol += col_jump
111
+ buffer.mark_dirty
112
+ end
113
+
114
+ def jump_to_col col
115
+ col = col - (col % col_jump)
116
+ buffer.mark_dirty unless @leftcol == col
117
+ @leftcol = col
118
+ end
119
+
120
+ def jump_to_left; jump_to_col 0; end
121
+
122
+ ## set top line to l
123
+ def jump_to_line l
124
+ l = l.clamp 0, lines - 1
125
+ return if @topline == l
126
+ @topline = l
127
+ @botline = [l + buffer.content_height, lines].min
128
+ buffer.mark_dirty
129
+ end
130
+
131
+ def at_top?; @topline == 0 end
132
+ def at_bottom?; @botline == lines end
133
+
134
+ def line_down; jump_to_line @topline + 1; end
135
+ def line_up; jump_to_line @topline - 1; end
136
+ def page_down; jump_to_line @topline + buffer.content_height - @slip_rows; end
137
+ def page_up; jump_to_line @topline - buffer.content_height + @slip_rows; end
138
+ def half_page_down; jump_to_line @topline + buffer.content_height / 2; end
139
+ def half_page_up; jump_to_line @topline - buffer.content_height / 2; end
140
+ def jump_to_start; jump_to_line 0; end
141
+ def jump_to_end; jump_to_line lines - buffer.content_height; end
142
+
143
+ def ensure_mode_validity
144
+ @topline = @topline.clamp 0, [lines - 1, 0].max
145
+ @botline = [@topline + buffer.content_height, lines].min
146
+ end
147
+
148
+ def resize *a
149
+ super(*a)
150
+ ensure_mode_validity
151
+ end
152
+
153
+ protected
154
+
155
+ def find_text query, start_line
156
+ regex = /#{query}/i
157
+ (start_line ... lines).each do |i|
158
+ case(s = self[i])
159
+ when String
160
+ match = s =~ regex
161
+ return [i, match] if match
162
+ when Array
163
+ offset = 0
164
+ s.each do |color, string|
165
+ match = string =~ regex
166
+ if match
167
+ return [i, offset + match]
168
+ else
169
+ offset += string.display_length
170
+ end
171
+ end
172
+ end
173
+ end
174
+ nil
175
+ end
176
+
177
+ def draw_line ln, opts={}
178
+ regex = /(#{@search_query})/i
179
+ case(s = self[ln])
180
+ when String
181
+ if in_search?
182
+ draw_line_from_array ln, matching_text_array(s, regex), opts
183
+ else
184
+ draw_line_from_string ln, s, opts
185
+ end
186
+ when Array
187
+ if in_search?
188
+ ## seems like there ought to be a better way of doing this
189
+ array = []
190
+ s.each do |color, text|
191
+ if text =~ regex
192
+ array += matching_text_array text, regex, color
193
+ else
194
+ array << [color, text]
195
+ end
196
+ end
197
+ draw_line_from_array ln, array, opts
198
+ else
199
+ draw_line_from_array ln, s, opts
200
+ end
201
+ else
202
+ raise "unknown drawable object: #{s.inspect} in #{self} for line #{ln}" # good for debugging
203
+ end
204
+
205
+ ## speed test
206
+ # str = s.map { |color, text| text }.join
207
+ # buffer.write ln - @topline, 0, str, :color => :none, :highlight => opts[:highlight]
208
+ # return
209
+ end
210
+
211
+ def matching_text_array s, regex, oldcolor=:text_color
212
+ s.split(regex).map do |text|
213
+ next if text.empty?
214
+ if text =~ regex
215
+ [:search_highlight_color, text]
216
+ else
217
+ [oldcolor, text]
218
+ end
219
+ end.compact + [[oldcolor, ""]]
220
+ end
221
+
222
+ def draw_line_from_array ln, a, opts
223
+ xpos = 0
224
+ a.each_with_index do |(color, text), i|
225
+ raise "nil text for color '#{color}'" if text.nil? # good for debugging
226
+ l = text.display_length
227
+ no_fill = i != a.size - 1
228
+
229
+ if xpos + l < @leftcol
230
+ buffer.write ln - @topline, 0, "", :color => color,
231
+ :highlight => opts[:highlight]
232
+ elsif xpos < @leftcol
233
+ ## partial
234
+ buffer.write ln - @topline, 0, text[(@leftcol - xpos) .. -1],
235
+ :color => color,
236
+ :highlight => opts[:highlight], :no_fill => no_fill
237
+ else
238
+ buffer.write ln - @topline, xpos - @leftcol, text,
239
+ :color => color, :highlight => opts[:highlight],
240
+ :no_fill => no_fill
241
+ end
242
+ xpos += l
243
+ end
244
+ end
245
+
246
+ def draw_line_from_string ln, s, opts
247
+ buffer.write ln - @topline, 0, s[@leftcol .. -1], :highlight => opts[:highlight], :color => opts[:color]
248
+ end
249
+ end
250
+
251
+ end
252
+
@@ -0,0 +1,204 @@
1
+ module Redwood
2
+
3
+ class SearchListMode < LineCursorMode
4
+ register_keymap do |k|
5
+ k.add :select_search, "Open search results", :enter
6
+ k.add :reload, "Discard saved search list and reload", '@'
7
+ k.add :jump_to_next_new, "Jump to next new thread", :tab
8
+ k.add :toggle_show_unread_only, "Toggle between showing all saved searches and those with unread mail", 'u'
9
+ k.add :delete_selected_search, "Delete selected search", "X"
10
+ k.add :rename_selected_search, "Rename selected search", "r"
11
+ k.add :edit_selected_search, "Edit selected search", "e"
12
+ k.add :add_new_search, "Add new search", "a"
13
+ end
14
+
15
+ HookManager.register "search-list-filter", <<EOS
16
+ Filter the search list, typically to sort.
17
+ Variables:
18
+ counted: an array of counted searches.
19
+ Return value:
20
+ An array of counted searches with sort_by output structure.
21
+ EOS
22
+
23
+ HookManager.register "search-list-format", <<EOS
24
+ Create the sprintf format string for search-list-mode.
25
+ Variables:
26
+ n_width: the maximum search name width
27
+ tmax: the maximum total message count
28
+ umax: the maximum unread message count
29
+ s_width: the maximum search string width
30
+ Return value:
31
+ A format string for sprintf
32
+ EOS
33
+
34
+ def initialize
35
+ @searches = []
36
+ @text = []
37
+ @unread_only = false
38
+ super
39
+ UpdateManager.register self
40
+ regen_text
41
+ end
42
+
43
+ def cleanup
44
+ UpdateManager.unregister self
45
+ super
46
+ end
47
+
48
+ def lines; @text.length end
49
+ def [] i; @text[i] end
50
+
51
+ def jump_to_next_new
52
+ n = ((curpos + 1) ... lines).find { |i| @searches[i][1] > 0 } || (0 ... curpos).find { |i| @searches[i][1] > 0 }
53
+ if n
54
+ ## jump there if necessary
55
+ jump_to_line n unless n >= topline && n < botline
56
+ set_cursor_pos n
57
+ else
58
+ BufferManager.flash "No saved searches with unread messages."
59
+ end
60
+ end
61
+
62
+ def focus
63
+ reload # make sure unread message counts are up-to-date
64
+ end
65
+
66
+ def handle_added_update sender, m
67
+ reload
68
+ end
69
+
70
+ protected
71
+
72
+ def toggle_show_unread_only
73
+ @unread_only = !@unread_only
74
+ reload
75
+ end
76
+
77
+ def reload
78
+ regen_text
79
+ buffer.mark_dirty if buffer
80
+ end
81
+
82
+ def regen_text
83
+ @text = []
84
+ searches = SearchManager.all_searches
85
+
86
+ counted = searches.map do |name|
87
+ search_string = SearchManager.search_string_for name
88
+ begin
89
+ if SearchManager.predefined_queries.has_key? search_string
90
+ query = SearchManager.predefined_queries[search_string]
91
+ else
92
+ query = Index.parse_query search_string
93
+ end
94
+ total = Index.num_results_for :qobj => query[:qobj]
95
+ unread = Index.num_results_for :qobj => query[:qobj], :label => :unread
96
+ rescue Index::ParseError => e
97
+ BufferManager.flash "Problem: #{e.message}!"
98
+ total = 0
99
+ unread = 0
100
+ end
101
+ [name, search_string, total, unread]
102
+ end
103
+
104
+ if HookManager.enabled? "search-list-filter"
105
+ counts = HookManager.run "search-list-filter", :counted => counted
106
+ else
107
+ counts = counted.sort_by { |n, s, t, u| n.downcase }
108
+ end
109
+
110
+ n_width = counts.max_of { |n, s, t, u| n.length }
111
+ tmax = counts.max_of { |n, s, t, u| t }
112
+ umax = counts.max_of { |n, s, t, u| u }
113
+ s_width = counts.max_of { |n, s, t, u| s.length }
114
+
115
+ if @unread_only
116
+ counts.delete_if { | n, s, t, u | u == 0 }
117
+ end
118
+
119
+ @searches = []
120
+ counts.each do |name, search_string, total, unread|
121
+ fmt = HookManager.run "search-list-format", :n_width => n_width, :tmax => tmax, :umax => umax, :s_width => s_width
122
+ if !fmt
123
+ fmt = "%#{n_width + 1}s %5d %s, %5d unread: %s"
124
+ end
125
+ @text << [[(unread == 0 ? :labellist_old_color : :labellist_new_color),
126
+ sprintf(fmt, name, total, total == 1 ? " message" : "messages", unread, search_string)]]
127
+ @searches << [name, unread]
128
+ end
129
+
130
+ BufferManager.flash "No saved searches with unread messages!" if counts.empty? && @unread_only
131
+ end
132
+
133
+ def select_search
134
+ name, num_unread = @searches[curpos]
135
+ return unless name
136
+ SearchResultsMode.spawn_from_query SearchManager.search_string_for(name)
137
+ end
138
+
139
+ def delete_selected_search
140
+ name, num_unread = @searches[curpos]
141
+ return unless name
142
+ reload if SearchManager.delete name
143
+ end
144
+
145
+ def rename_selected_search
146
+ old_name, num_unread = @searches[curpos]
147
+ return unless old_name
148
+
149
+ if SearchManager.predefined_searches.has_key? old_name
150
+ BufferManager.flash "Cannot be edited: predefined search."
151
+ return
152
+ end
153
+
154
+ new_name = BufferManager.ask :save_search, "Rename this saved search: ", old_name
155
+ return unless new_name && new_name !~ /^\s*$/ && new_name != old_name
156
+ new_name.strip!
157
+ unless SearchManager.valid_name? new_name
158
+ BufferManager.flash "Not renamed: " + SearchManager.name_format_hint
159
+ return
160
+ end
161
+ if SearchManager.all_searches.include? new_name
162
+ BufferManager.flash "Not renamed: \"#{new_name}\" already exists"
163
+ return
164
+ end
165
+ reload if SearchManager.rename old_name, new_name
166
+ set_cursor_pos @searches.index([new_name, num_unread])||curpos
167
+ end
168
+
169
+ def edit_selected_search
170
+ name, num_unread = @searches[curpos]
171
+ return unless name
172
+
173
+ if SearchManager.predefined_searches.has_key? name
174
+ BufferManager.flash "Cannot be edited: predefined search."
175
+ return
176
+ end
177
+
178
+ old_search_string = SearchManager.search_string_for name
179
+ new_search_string = BufferManager.ask :search, "Edit this saved search: ", (old_search_string + " ")
180
+ return unless new_search_string && new_search_string !~ /^\s*$/ && new_search_string != old_search_string
181
+ reload if SearchManager.edit name, new_search_string.strip
182
+ set_cursor_pos @searches.index([name, num_unread])||curpos
183
+ end
184
+
185
+ def add_new_search
186
+ search_string = BufferManager.ask :search, "New search: "
187
+ return unless search_string && search_string !~ /^\s*$/
188
+ name = BufferManager.ask :save_search, "Name this search: "
189
+ return unless name && name !~ /^\s*$/
190
+ name.strip!
191
+ unless SearchManager.valid_name? name
192
+ BufferManager.flash "Not saved: " + SearchManager.name_format_hint
193
+ return
194
+ end
195
+ if SearchManager.all_searches.include? name
196
+ BufferManager.flash "Not saved: \"#{name}\" already exists"
197
+ return
198
+ end
199
+ reload if SearchManager.add name, search_string.strip
200
+ set_cursor_pos @searches.index(@searches.assoc(name))||curpos
201
+ end
202
+ end
203
+
204
+ end