sup 0.10.2 → 0.11

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.

@@ -1,6 +1,14 @@
1
1
  module Redwood
2
2
 
3
3
  class Keymap
4
+
5
+ HookManager.register "keybindings", <<EOS
6
+ Add custom keybindings.
7
+ Methods:
8
+ modes: Hash from mode names to mode classes.
9
+ global_keymap: The top-level keymap.
10
+ EOS
11
+
4
12
  def initialize
5
13
  @map = {}
6
14
  @order = []
@@ -60,10 +68,31 @@ class Keymap
60
68
  end
61
69
  end
62
70
 
71
+ def delete k
72
+ kc = Keymap.keysym_to_keycode(k)
73
+ return unless @map.member? kc
74
+ entry = @map.delete kc
75
+ keys = entry[2]
76
+ keys.delete k
77
+ @order.delete entry if keys.empty?
78
+ end
79
+
80
+ def add! action, help, *keys
81
+ keys.each { |k| delete k }
82
+ add action, help, *keys
83
+ end
84
+
63
85
  def add_multi prompt, key
64
- submap = Keymap.new
65
- add submap, prompt, key
66
- yield submap
86
+ kc = Keymap.keysym_to_keycode(key)
87
+ if @map.member? kc
88
+ action = @map[kc].first
89
+ raise "existing action is not a keymap" unless action.is_a?(Keymap)
90
+ yield action
91
+ else
92
+ submap = Keymap.new
93
+ add submap, prompt, key
94
+ yield submap
95
+ end
67
96
  end
68
97
 
69
98
  def action_for kc
@@ -95,6 +124,15 @@ class Keymap
95
124
  llen = lines.max_of { |a, b| a.length }
96
125
  lines.map { |a, b| sprintf " %#{llen}s : %s", a, b }.join("\n")
97
126
  end
127
+
128
+ def self.run_hook global_keymap
129
+ modes = Hash[Mode.keymaps.map { |klass,keymap| [Mode.make_name(klass.name),klass] }]
130
+ locals = {
131
+ :modes => modes,
132
+ :global_keymap => global_keymap,
133
+ }
134
+ HookManager.run 'keybindings', locals
135
+ end
98
136
  end
99
137
 
100
138
  end
@@ -178,7 +178,7 @@ class Message
178
178
 
179
179
  ## sanitize message ids by removing spaces and non-ascii characters.
180
180
  ## also, truncate to 255 characters. all these steps are necessary
181
- ## to make ferret happy. of course, we probably fuck up a couple
181
+ ## to make the index happy. of course, we probably fuck up a couple
182
182
  ## valid message ids as well. as long as we're consistent, this
183
183
  ## should be fine, though.
184
184
  ##
@@ -10,6 +10,14 @@ class Mode
10
10
  @@keymaps[self] = keymap
11
11
  end
12
12
 
13
+ def self.keymap
14
+ @@keymaps[self] || register_keymap
15
+ end
16
+
17
+ def self.keymaps
18
+ @@keymaps
19
+ end
20
+
13
21
  def initialize
14
22
  @buffer = nil
15
23
  end
@@ -20,7 +20,6 @@ class Console
20
20
  end
21
21
 
22
22
  def xapian; Index.instance.instance_variable_get :@xapian; end
23
- def ferret; Index.instance.instance_variable_get :@index; end
24
23
 
25
24
  def loglevel; Redwood::Logger.level; end
26
25
  def set_loglevel(level); Redwood::Logger.level = level; end
@@ -29,7 +28,7 @@ class Console
29
28
 
30
29
  ## files that won't cause problems when reloaded
31
30
  ## TODO expand this list / convert to blacklist
32
- RELOAD_WHITELIST = %w(sup/xapian_index.rb sup/modes/console-mode.rb)
31
+ RELOAD_WHITELIST = %w(sup/index.rb sup/modes/console-mode.rb)
33
32
 
34
33
  def reload
35
34
  old_verbose = $VERBOSE
@@ -69,7 +68,7 @@ class ConsoleMode < LogMode
69
68
  end
70
69
 
71
70
  def initialize
72
- super
71
+ super "console"
73
72
  @console = Console.new self
74
73
  @binding = @console.instance_eval { binding }
75
74
  end
@@ -3,10 +3,6 @@ require 'socket' # just for gethostname!
3
3
  require 'pathname'
4
4
  require 'rmail'
5
5
 
6
- # from jcode.rb, not included in ruby 1.9
7
- PATTERN_UTF8 = '[\xc0-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf][\x80-\xbf]'
8
- RE_UTF8 = Regexp.new(PATTERN_UTF8, 0, 'n')
9
-
10
6
  module Redwood
11
7
 
12
8
  class SendmailCommandFailed < StandardError; end
@@ -40,6 +36,28 @@ Return value:
40
36
  none
41
37
  EOS
42
38
 
39
+ HookManager.register "mentions-attachments", <<EOS
40
+ Detects if given message mentions attachments the way it is probable
41
+ that there should be files attached to the message.
42
+ Variables:
43
+ header: a hash of headers. See 'signature' hook for documentation.
44
+ body: an array of lines of body text.
45
+ Return value:
46
+ True if attachments are mentioned.
47
+ EOS
48
+
49
+ HookManager.register "crypto-mode", <<EOS
50
+ Modifies cryptography settings based on header and message content, before
51
+ editing a new message. This can be used to set, for example, default cryptography
52
+ settings.
53
+ Variables:
54
+ header: a hash of headers. See 'signature' hook for documentation.
55
+ body: an array of lines of body text.
56
+ crypto_selector: the UI element that controls the current cryptography setting.
57
+ Return value:
58
+ none
59
+ EOS
60
+
43
61
  attr_reader :status
44
62
  attr_accessor :body, :header
45
63
  bool_reader :edited
@@ -92,6 +110,9 @@ EOS
92
110
  add_selector @crypto_selector if @crypto_selector
93
111
 
94
112
  HookManager.run "before-edit", :header => @header, :body => @body
113
+ if @crypto_selector
114
+ HookManager.run "crypto-mode", :header => @header, :body => @body, :crypto_selector => @crypto_selector
115
+ end
95
116
 
96
117
  super opts
97
118
  regen_text
@@ -193,7 +214,7 @@ protected
193
214
  end
194
215
 
195
216
  def mime_encode_subject string
196
- return string unless string.match(RE_UTF8)
217
+ return string if string.ascii_only?
197
218
  mime_encode string
198
219
  end
199
220
 
@@ -202,7 +223,7 @@ protected
202
223
  # Encode "bælammet mitt <user@example.com>" into
203
224
  # "=?utf-8?q?b=C3=A6lammet_mitt?= <user@example.com>
204
225
  def mime_encode_address string
205
- return string unless string.match(RE_UTF8)
226
+ return string if string.ascii_only?
206
227
  string.sub(RE_ADDRESS) { |match| mime_encode($1) + $2 }
207
228
  end
208
229
 
@@ -444,7 +465,11 @@ private
444
465
  end
445
466
 
446
467
  def mentions_attachments?
447
- @body.any? { |l| l =~ /^[^>]/ && l =~ /\battach(ment|ed|ing|)\b/i }
468
+ if HookManager.enabled? "mentions-attachments"
469
+ HookManager.run "mentions-attachments", :header => @header, :body => @body
470
+ else
471
+ @body.any? { |l| l =~ /^[^>]/ && l =~ /\battach(ment|ed|ing|)\b/i }
472
+ end
448
473
  end
449
474
 
450
475
  def top_posting?
@@ -115,6 +115,10 @@ class InboxMode < ThreadIndexMode
115
115
  regen_text
116
116
  end
117
117
 
118
+ def handle_idle_update sender, idle_since
119
+ flush_index
120
+ end
121
+
118
122
  def status
119
123
  super + " #{Index.size} messages in index"
120
124
  end
@@ -0,0 +1,188 @@
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
+ query = Index.parse_query search_string
90
+ total = Index.num_results_for :qobj => query[:qobj]
91
+ unread = Index.num_results_for :qobj => query[:qobj], :label => :unread
92
+ rescue Index::ParseError => e
93
+ BufferManager.flash "Problem: #{e.message}!"
94
+ total = 0
95
+ unread = 0
96
+ end
97
+ [name, search_string, total, unread]
98
+ end
99
+
100
+ if HookManager.enabled? "search-list-filter"
101
+ counts = HookManager.run "search-list-filter", :counted => counted
102
+ else
103
+ counts = counted.sort_by { |n, s, t, u| n.downcase }
104
+ end
105
+
106
+ n_width = counts.max_of { |n, s, t, u| n.length }
107
+ tmax = counts.max_of { |n, s, t, u| t }
108
+ umax = counts.max_of { |n, s, t, u| u }
109
+ s_width = counts.max_of { |n, s, t, u| s.length }
110
+
111
+ if @unread_only
112
+ counts.delete_if { | n, s, t, u | u == 0 }
113
+ end
114
+
115
+ @searches = []
116
+ counts.each do |name, search_string, total, unread|
117
+ fmt = HookManager.run "search-list-format", :n_width => n_width, :tmax => tmax, :umax => umax, :s_width => s_width
118
+ if !fmt
119
+ fmt = "%#{n_width + 1}s %5d %s, %5d unread: %s"
120
+ end
121
+ @text << [[(unread == 0 ? :labellist_old_color : :labellist_new_color),
122
+ sprintf(fmt, name, total, total == 1 ? " message" : "messages", unread, search_string)]]
123
+ @searches << [name, unread]
124
+ end
125
+
126
+ BufferManager.flash "No saved searches with unread messages!" if counts.empty? && @unread_only
127
+ end
128
+
129
+ def select_search
130
+ name, num_unread = @searches[curpos]
131
+ return unless name
132
+ SearchResultsMode.spawn_from_query SearchManager.search_string_for(name)
133
+ end
134
+
135
+ def delete_selected_search
136
+ name, num_unread = @searches[curpos]
137
+ return unless name
138
+ reload if SearchManager.delete name
139
+ end
140
+
141
+ def rename_selected_search
142
+ old_name, num_unread = @searches[curpos]
143
+ return unless old_name
144
+ new_name = BufferManager.ask :save_search, "Rename this saved search: ", old_name
145
+ return unless new_name && new_name !~ /^\s*$/ && new_name != old_name
146
+ new_name.strip!
147
+ unless SearchManager.valid_name? new_name
148
+ BufferManager.flash "Not renamed: " + SearchManager.name_format_hint
149
+ return
150
+ end
151
+ if SearchManager.all_searches.include? new_name
152
+ BufferManager.flash "Not renamed: \"#{new_name}\" already exists"
153
+ return
154
+ end
155
+ reload if SearchManager.rename old_name, new_name
156
+ set_cursor_pos @searches.index([new_name, num_unread])||curpos
157
+ end
158
+
159
+ def edit_selected_search
160
+ name, num_unread = @searches[curpos]
161
+ return unless name
162
+ old_search_string = SearchManager.search_string_for name
163
+ new_search_string = BufferManager.ask :search, "Edit this saved search: ", (old_search_string + " ")
164
+ return unless new_search_string && new_search_string !~ /^\s*$/ && new_search_string != old_search_string
165
+ reload if SearchManager.edit name, new_search_string.strip
166
+ set_cursor_pos @searches.index([name, num_unread])||curpos
167
+ end
168
+
169
+ def add_new_search
170
+ search_string = BufferManager.ask :search, "New search: "
171
+ return unless search_string && search_string !~ /^\s*$/
172
+ name = BufferManager.ask :save_search, "Name this search: "
173
+ return unless name && name !~ /^\s*$/
174
+ name.strip!
175
+ unless SearchManager.valid_name? name
176
+ BufferManager.flash "Not saved: " + SearchManager.name_format_hint
177
+ return
178
+ end
179
+ if SearchManager.all_searches.include? name
180
+ BufferManager.flash "Not saved: \"#{name}\" already exists"
181
+ return
182
+ end
183
+ reload if SearchManager.add name, search_string.strip
184
+ set_cursor_pos @searches.index(@searches.assoc(name))||curpos
185
+ end
186
+ end
187
+
188
+ end
@@ -8,6 +8,7 @@ class SearchResultsMode < ThreadIndexMode
8
8
 
9
9
  register_keymap do |k|
10
10
  k.add :refine_search, "Refine search", '|'
11
+ k.add :save_search, "Save search", '%'
11
12
  end
12
13
 
13
14
  def refine_search
@@ -16,7 +17,22 @@ class SearchResultsMode < ThreadIndexMode
16
17
  SearchResultsMode.spawn_from_query text
17
18
  end
18
19
 
19
- ## a proper is_relevant? method requires some way of asking ferret
20
+ def save_search
21
+ name = BufferManager.ask :save_search, "Name this search: "
22
+ return unless name && name !~ /^\s*$/
23
+ name.strip!
24
+ unless SearchManager.valid_name? name
25
+ BufferManager.flash "Not saved: " + SearchManager.name_format_hint
26
+ return
27
+ end
28
+ if SearchManager.all_searches.include? name
29
+ BufferManager.flash "Not saved: \"#{name}\" already exists"
30
+ return
31
+ end
32
+ BufferManager.flash "Search saved as \"#{name}\"" if SearchManager.add name, @query[:text].strip
33
+ end
34
+
35
+ ## a proper is_relevant? method requires some way of asking the index
20
36
  ## if an in-memory object satisfies a query. i'm not sure how to do
21
37
  ## that yet. in the worst case i can make an in-memory index, add
22
38
  ## the message, and search against it to see if i have > 0 results,
@@ -12,6 +12,12 @@ class ThreadIndexMode < LineCursorMode
12
12
 
13
13
  HookManager.register "index-mode-size-widget", <<EOS
14
14
  Generates the per-thread size widget for each thread.
15
+ Variables:
16
+ thread: The message thread to be formatted.
17
+ EOS
18
+
19
+ HookManager.register "index-mode-date-widget", <<EOS
20
+ Generates the per-thread date widget for each thread.
15
21
  Variables:
16
22
  thread: The message thread to be formatted.
17
23
  EOS
@@ -53,10 +59,12 @@ EOS
53
59
  def initialize hidden_labels=[], load_thread_opts={}
54
60
  super()
55
61
  @mutex = Mutex.new # covers the following variables:
56
- @threads = {}
62
+ @threads = []
57
63
  @hidden_threads = {}
58
64
  @size_widget_width = nil
59
- @size_widgets = {}
65
+ @size_widgets = []
66
+ @date_widget_width = nil
67
+ @date_widgets = []
60
68
  @tags = Tagger.new self
61
69
 
62
70
  ## these guys, and @text and @lines, are not covered
@@ -111,7 +119,7 @@ EOS
111
119
  mode = ThreadViewMode.new t, @hidden_labels, self
112
120
  BufferManager.spawn t.subj, mode
113
121
  BufferManager.draw_screen
114
- mode.jump_to_first_open
122
+ mode.jump_to_first_open if $config[:jump_to_open_message]
115
123
  BufferManager.draw_screen # lame TODO: make this unnecessary
116
124
  ## the first draw_screen is needed before topline and botline
117
125
  ## are set, and the second to show the cursor having moved
@@ -226,6 +234,8 @@ EOS
226
234
  @threads = @ts.threads.select { |t| !@hidden_threads[t] }.sort_by { |t| [t.date, t.first.id] }.reverse
227
235
  @size_widgets = @threads.map { |t| size_widget_for_thread t }
228
236
  @size_widget_width = @size_widgets.max_of { |w| w.display_length }
237
+ @date_widgets = @threads.map { |t| date_widget_for_thread t }
238
+ @date_widget_width = @date_widgets.max_of { |w| w.display_length }
229
239
  end
230
240
  set_cursor_pos @threads.index(old_cursor_thread)||curpos
231
241
 
@@ -651,7 +661,7 @@ EOS
651
661
  if (l = lines) == 0
652
662
  "line 0 of 0"
653
663
  else
654
- "line #{curpos + 1} of #{l} #{dirty? ? '*modified*' : ''}"
664
+ "line #{curpos + 1} of #{l}"
655
665
  end
656
666
  end
657
667
 
@@ -719,6 +729,10 @@ protected
719
729
  HookManager.run("index-mode-size-widget", :thread => t) || default_size_widget_for(t)
720
730
  end
721
731
 
732
+ def date_widget_for_thread t
733
+ HookManager.run("index-mode-date-widget", :thread => t) || default_date_widget_for(t)
734
+ end
735
+
722
736
  def cursor_thread; @mutex.synchronize { @threads[curpos] }; end
723
737
 
724
738
  def drop_all_threads
@@ -734,6 +748,7 @@ protected
734
748
  @hidden_threads[t] = true
735
749
  @threads.delete_at i
736
750
  @size_widgets.delete_at i
751
+ @date_widgets.delete_at i
737
752
  @tags.drop_tag_for t
738
753
  end
739
754
  end
@@ -745,9 +760,12 @@ protected
745
760
 
746
761
  @mutex.synchronize do
747
762
  @size_widgets[l] = size_widget_for_thread @threads[l]
763
+ @date_widgets[l] = date_widget_for_thread @threads[l]
748
764
 
749
- ## if the widget size has increased, we need to redraw everyone
750
- need_update = @size_widgets[l].size > @size_widget_width
765
+ ## if a widget size has increased, we need to redraw everyone
766
+ need_update =
767
+ (@size_widgets[l].size > @size_widget_width) or
768
+ (@date_widgets[l].size > @date_widget_width)
751
769
  end
752
770
 
753
771
  if need_update
@@ -793,14 +811,24 @@ protected
793
811
  result << [name, new[a]]
794
812
  end
795
813
 
814
+ if result.size == 1 && (author_and_newness = result.assoc("me"))
815
+ unless (recipients = t.participants - t.authors).empty?
816
+ result = recipients.collect do |r|
817
+ break if limit && result.size >= limit
818
+ name = (recipients.size == 1) ? r.mediumname : r.shortname
819
+ ["(#{name})", author_and_newness[1]]
820
+ end
821
+ end
822
+ end
823
+
796
824
  result
797
825
  end
798
826
 
799
827
  AUTHOR_LIMIT = 5
800
828
  def text_for_thread_at line
801
- t, size_widget = @mutex.synchronize { [@threads[line], @size_widgets[line]] }
802
-
803
- date = t.date.to_nice_s
829
+ t, size_widget, date_widget = @mutex.synchronize do
830
+ [@threads[line], @size_widgets[line], @date_widgets[line]]
831
+ end
804
832
 
805
833
  starred = t.has_label? :starred
806
834
 
@@ -851,10 +879,11 @@ protected
851
879
  snippet = t.snippet + (t.snippet.empty? ? "" : "...")
852
880
 
853
881
  size_widget_text = sprintf "%#{ @size_widget_width}s", size_widget
882
+ date_widget_text = sprintf "%#{ @date_widget_width}s", date_widget
854
883
 
855
884
  [
856
885
  [:tagged_color, @tags.tagged?(t) ? ">" : " "],
857
- [:date_color, sprintf("%#{@date_width}s", date)],
886
+ [:date_color, date_widget_text],
858
887
  (starred ? [:starred_color, "*"] : [:none, " "]),
859
888
  ] +
860
889
  from +
@@ -883,6 +912,10 @@ private
883
912
  end
884
913
  end
885
914
 
915
+ def default_date_widget_for t
916
+ t.date.to_nice_s
917
+ end
918
+
886
919
  def from_width
887
920
  [(buffer.content_width.to_f * 0.2).to_i, MIN_FROM_WIDTH].max
888
921
  end