sup 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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