From mboxrd@z Thu Jan 1 00:00:00 1970 Received: by 10.213.28.69 with SMTP id l5cs472841ebc; Tue, 19 Jan 2010 19:56:18 -0800 (PST) Received: by 10.224.44.164 with SMTP id a36mr5980713qaf.149.1263959777327; Tue, 19 Jan 2010 19:56:17 -0800 (PST) Return-Path: Received: from rubyforge.org (rubyforge.org [205.234.109.19]) by mx.google.com with ESMTP id 2si9999843qwi.27.2010.01.19.19.56.16; Tue, 19 Jan 2010 19:56:17 -0800 (PST) Received-SPF: pass (google.com: domain of sup-devel-bounces@rubyforge.org designates 205.234.109.19 as permitted sender) client-ip=205.234.109.19; Authentication-Results: mx.google.com; spf=pass (google.com: domain of sup-devel-bounces@rubyforge.org designates 205.234.109.19 as permitted sender) smtp.mail=sup-devel-bounces@rubyforge.org; dkim=neutral (body hash did not verify) header.i=@gmail.com Received: from rubyforge.org (rubyforge.org [127.0.0.1]) by rubyforge.org (Postfix) with ESMTP id B430D1D788B6; Tue, 19 Jan 2010 22:56:08 -0500 (EST) Received: from mail-iw0-f195.google.com (mail-iw0-f195.google.com [209.85.223.195]) by rubyforge.org (Postfix) with ESMTP id 91C72167830B for ; Tue, 19 Jan 2010 22:56:06 -0500 (EST) Received: by iwn33 with SMTP id 33so3840110iwn.29 for ; Tue, 19 Jan 2010 19:56:05 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=gamma; h=domainkey-signature:received:received:content-type:cc:subject:from :to:in-reply-to:references:date:message-id:user-agent :content-transfer-encoding; bh=06Lr/77JbC8bZGsOy3mQsUAhXA7zp5Hy47Q5l9QeVhM=; b=fybLiYTphANogNy5e5QybCoHYu5MDfKznJJAjDqnJv3X14+czkgqwjf9QEXkmB5MXK HyxbbP4LxdhxnXL4A4mllfr5iqW6z2sq1O8+SOQNSrdAbNPdpoyyZM6BLx0X2Fdl5OTK nlwmFDDhBd1neR4acq6hIi0DTt8sMSTHthqpc= DomainKey-Signature: a=rsa-sha1; c=nofws; d=gmail.com; s=gamma; h=content-type:cc:subject:from:to:in-reply-to:references:date :message-id:user-agent:content-transfer-encoding; b=jarb9C7nrLVGWz+zzTH3Y89pvxmlN/KF3Tdl06POSaNhzAm5ILBh89lbBrhiWAu1Ct 0DEEJ5WFRuO1NYHRquYE8MMrwivTXto8SI6UHv3oD7k+dXPt+5AyvcyMCF92DpNW7ySw SRZPRYxrZNENscRRdiplz7FhuSHBO++bl1D9c= Received: by 10.231.146.129 with SMTP id h1mr1508850ibv.71.1263959332738; Tue, 19 Jan 2010 19:48:52 -0800 (PST) Received: from localhost (c-76-98-110-216.hsd1.nj.comcast.net [76.98.110.216]) by mx.google.com with ESMTPS id 23sm3598594iwn.7.2010.01.19.19.48.51 (version=TLSv1/SSLv3 cipher=RC4-MD5); Tue, 19 Jan 2010 19:48:52 -0800 (PST) From: Eric Sherman To: Rich Lane In-reply-to: <1263920054-sup-9400@zyrg.net> References: <1263858172-sup-547@changeling.local> <1263920054-sup-9400@zyrg.net> Date: Tue, 19 Jan 2010 22:48:50 -0500 Message-Id: <1263959248-sup-4163@changeling.local> User-Agent: Sup/git Cc: sup-devel Subject: Re: [sup-devel] [PATCHv2] Saved Search Support X-BeenThere: sup-devel@rubyforge.org X-Mailman-Version: 2.1.12 Precedence: list Reply-To: Sup developer discussion List-Id: Sup developer discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , MIME-Version: 1.0 Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Sender: sup-devel-bounces@rubyforge.org Errors-To: sup-devel-bounces@rubyforge.org Start an index search with a \ backslash and press enter to get a list of searches that were previously saved from search-results-mode with % percent or added from search-list-mode directly. Saved searches may be used in other searches by enclosing their names in {} curly braces. Search names may contain letters, numbers, underscores and dashes. New Key Bindings global \ open search-list-mode search-list-mode X Delete selected search r Rename selected search e Edit selected search a Add new search search-results-mode % Save search New Hooks search-list-filter search-list-format Search String Expansion Include saved searches in other searches by enclosing their names in {} curly braces. The name and enclosing braces are replaced by the actual search string and enclosing () parens. low_traffic: has:foo OR has:bar a_slow_week: {low_traffic} AND after:(7 days ago) {a_slow_week} expands to "(has:foo OR has:bar) AND after:(7 days ago)" and may be used in a global search, a refinement or another saved search. A search including the undefined {baz} will fail. To search for a literal string enclosed in curly braces, escape the curly braces with \ backslash: "\{baz\}". There is no nesting limit and searches are always expanded completely before they are turned into proper queries for the index. Save File Format Searches are read from ~/.sup/searches.txt on startup and saved at exit. The format is "name: search_string". Here's a silly example: core: {me} AND NOT {crap} AND NOT {weak} crap: is:leadlogger OR is:alert OR is:rzp me: to:me OR from:me recent: after:(14 days ago) top: {core} AND {recent} weak: is:feed OR is:list OR is:ham --- bin/sup | 11 ++- lib/sup.rb | 5 + lib/sup/modes/search-list-mode.rb | 188 ++++++++++++++++++++++++++++++++++ lib/sup/modes/search-results-mode.rb | 23 ++++- lib/sup/search.rb | 72 +++++++++++++ 5 files changed, 294 insertions(+), 5 deletions(-) create mode 100644 lib/sup/modes/search-list-mode.rb create mode 100644 lib/sup/search.rb diff --git a/bin/sup b/bin/sup index 8bf640b..fb19795 100755 --- a/bin/sup +++ b/bin/sup @@ -303,9 +303,14 @@ begin b, new = bm.spawn_unless_exists("Contact List") { ContactListMode.new } b.mode.load_in_background if new when :search - query = BufferManager.ask :search, "search all messages: " - next unless query && query !~ /^\s*$/ - SearchResultsMode.spawn_from_query query + query = BufferManager.ask :search, "Search all messages (enter for saved searches): " + unless query.nil? + if query.empty? + bm.spawn_unless_exists("Saved searches") { SearchListMode.new } + else + SearchResultsMode.spawn_from_query query + end + end when :search_unread SearchResultsMode.spawn_from_query "is:unread" when :list_labels diff --git a/lib/sup.rb b/lib/sup.rb index e03a35d..b9dc749 100644 --- a/lib/sup.rb +++ b/lib/sup.rb @@ -50,6 +50,7 @@ module Redwood LOCK_FN = File.join(BASE_DIR, "lock") SUICIDE_FN = File.join(BASE_DIR, "please-kill-yourself") HOOK_DIR = File.join(BASE_DIR, "hooks") + SEARCH_FN = File.join(BASE_DIR, "searches.txt") YAML_DOMAIN = "masanjin.net" YAML_DATE = "2006-10-01" @@ -131,12 +132,14 @@ module Redwood Redwood::CryptoManager.init Redwood::UndoManager.init Redwood::SourceManager.init + Redwood::SearchManager.init Redwood::SEARCH_FN end def finish Redwood::LabelManager.save if Redwood::LabelManager.instantiated? Redwood::ContactManager.save if Redwood::ContactManager.instantiated? Redwood::BufferManager.deinstantiate! if Redwood::BufferManager.instantiated? + Redwood::SearchManager.save if Redwood::SearchManager.instantiated? end ## not really a good place for this, so I'll just dump it here. @@ -341,6 +344,8 @@ require "sup/modes/file-browser-mode" require "sup/modes/completion-mode" require "sup/modes/console-mode" require "sup/sent" +require "sup/search" +require "sup/modes/search-list-mode" $:.each do |base| d = File.join base, "sup/share/modes/" diff --git a/lib/sup/modes/search-list-mode.rb b/lib/sup/modes/search-list-mode.rb new file mode 100644 index 0000000..076c3d9 --- /dev/null +++ b/lib/sup/modes/search-list-mode.rb @@ -0,0 +1,188 @@ +module Redwood + +class SearchListMode < LineCursorMode + register_keymap do |k| + k.add :select_search, "Open search results", :enter + k.add :reload, "Discard saved search list and reload", '@' + k.add :jump_to_next_new, "Jump to next new thread", :tab + k.add :toggle_show_unread_only, "Toggle between showing all saved searches and those with unread mail", 'u' + k.add :delete_selected_search, "Delete selected search", "X" + k.add :rename_selected_search, "Rename selected search", "r" + k.add :edit_selected_search, "Edit selected search", "e" + k.add :add_new_search, "Add new search", "a" + end + + HookManager.register "search-list-filter", < 0 } || (0 ... curpos).find { |i| @searches[i][1] > 0 } + if n + ## jump there if necessary + jump_to_line n unless n >= topline && n < botline + set_cursor_pos n + else + BufferManager.flash "No saved searches with unread messages." + end + end + + def focus + reload # make sure unread message counts are up-to-date + end + + def handle_added_update sender, m + reload + end + +protected + + def toggle_show_unread_only + @unread_only = !@unread_only + reload + end + + def reload + regen_text + buffer.mark_dirty if buffer + end + + def regen_text + @text = [] + searches = SearchManager.all_searches + + counted = searches.map do |name| + search_string = SearchManager.search_string_for name + expanded_search_string= SearchManager.expand search_string + if expanded_search_string + query = Index.parse_query expanded_search_string + total = Index.num_results_for :qobj => query[:qobj] + unread = Index.num_results_for :qobj => query[:qobj], :label => :unread + else + total = 0 + unread = 0 + end + [name, search_string, total, unread] + end + + if HookManager.enabled? "search-list-filter" + counts = HookManager.run "search-list-filter", :counted => counted + else + counts = counted.sort_by { |n, s, t, u| n.downcase } + end + + n_width = counts.max_of { |n, s, t, u| n.length } + tmax = counts.max_of { |n, s, t, u| t } + umax = counts.max_of { |n, s, t, u| u } + s_width = counts.max_of { |n, s, t, u| s.length } + + if @unread_only + counts.delete_if { | n, s, t, u | u == 0 } + end + + @searches = [] + counts.each do |name, search_string, total, unread| + fmt = HookManager.run "search-list-format", :n_width => n_width, :tmax => tmax, :umax => umax, :s_width => s_width + if !fmt + fmt = "%#{n_width + 1}s %5d %s, %5d unread: %s" + end + @text << [[(unread == 0 ? :labellist_old_color : :labellist_new_color), + sprintf(fmt, name, total, total == 1 ? " message" : "messages", unread, search_string)]] + @searches << [name, unread] + end + + BufferManager.flash "No saved searches with unread messages!" if counts.empty? && @unread_only + end + + def select_search + name, num_unread = @searches[curpos] + return unless name + SearchResultsMode.spawn_from_query SearchManager.search_string_for(name) + end + + def delete_selected_search + name, num_unread = @searches[curpos] + return unless name + reload if SearchManager.delete name + end + + def rename_selected_search + old_name, num_unread = @searches[curpos] + return unless old_name + new_name = BufferManager.ask :save_search, "Rename this saved search: ", old_name + return unless new_name && new_name !~ /^\s*$/ && new_name != old_name + new_name.strip! + unless SearchManager.valid_name? new_name + BufferManager.flash "Not renamed: " + SearchManager.name_format_hint + return + end + if SearchManager.all_searches.include? new_name + BufferManager.flash "Not renamed: \"#{new_name}\" already exists" + return + end + reload if SearchManager.rename old_name, new_name + set_cursor_pos @searches.index([new_name, num_unread])||curpos + end + + def edit_selected_search + name, num_unread = @searches[curpos] + return unless name + old_search_string = SearchManager.search_string_for name + new_search_string = BufferManager.ask :search, "Edit this saved search: ", (old_search_string + " ") + return unless new_search_string && new_search_string !~ /^\s*$/ && new_search_string != old_search_string + reload if SearchManager.edit name, new_search_string.strip + set_cursor_pos @searches.index([name, num_unread])||curpos + end + + def add_new_search + search_string = BufferManager.ask :search, "New search: " + return unless search_string && search_string !~ /^\s*$/ + name = BufferManager.ask :save_search, "Name this search: " + return unless name && name !~ /^\s*$/ + name.strip! + unless SearchManager.valid_name? name + BufferManager.flash "Not saved: " + SearchManager.name_format_hint + return + end + if SearchManager.all_searches.include? name + BufferManager.flash "Not saved: \"#{name}\" already exists" + return + end + reload if SearchManager.add name, search_string.strip + set_cursor_pos @searches.index(@searches.assoc(name))||curpos + end +end + +end diff --git a/lib/sup/modes/search-results-mode.rb b/lib/sup/modes/search-results-mode.rb index 121e817..14d42b5 100644 --- a/lib/sup/modes/search-results-mode.rb +++ b/lib/sup/modes/search-results-mode.rb @@ -8,14 +8,30 @@ class SearchResultsMode < ThreadIndexMode register_keymap do |k| k.add :refine_search, "Refine search", '|' + k.add :save_search, "Save search", '%' end def refine_search - text = BufferManager.ask :search, "refine query: ", (@query[:text] + " ") + text = BufferManager.ask :search, "refine query: ", (@query[:unexpanded_text] + " ") return unless text && text !~ /^\s*$/ SearchResultsMode.spawn_from_query text end + def save_search + name = BufferManager.ask :save_search, "Name this search: " + return unless name && name !~ /^\s*$/ + name.strip! + unless SearchManager.valid_name? name + BufferManager.flash "Not saved: " + SearchManager.name_format_hint + return + end + if SearchManager.all_searches.include? name + BufferManager.flash "Not saved: \"#{name}\" already exists" + return + end + BufferManager.flash "Search saved as \"#{name}\"" if SearchManager.add name, @query[:unexpanded_text].strip + end + ## a proper is_relevant? method requires some way of asking ferret ## if an in-memory object satisfies a query. i'm not sure how to do ## that yet. in the worst case i can make an in-memory index, add @@ -24,8 +40,11 @@ class SearchResultsMode < ThreadIndexMode def self.spawn_from_query text begin - query = Index.parse_query(text) + expanded_text = SearchManager.expand text + return unless expanded_text + query = Index.parse_query expanded_text return unless query + query[:unexpanded_text] = text short_text = text.length < 20 ? text : text[0 ... 20] + "..." mode = SearchResultsMode.new query BufferManager.spawn "search: \"#{short_text}\"", mode diff --git a/lib/sup/search.rb b/lib/sup/search.rb new file mode 100644 index 0000000..799ca89 --- /dev/null +++ b/lib/sup/search.rb @@ -0,0 +1,72 @@ +module Redwood + +class SearchManager + include Singleton + + def initialize fn + @fn = fn + @searches = {} + if File.exists? fn + IO.foreach(fn) do |l| + l =~ /^([^:]*): (.*)$/ or raise "can't parse #{fn} line #{l.inspect}" + @searches[$1] = $2 + end + end + @modified = false + end + + def all_searches; return @searches.keys.sort; end + def search_string_for name; return @searches[name]; end + def valid_name? name; name =~ /^[\w-]+$/; end + def name_format_hint; "letters, numbers, underscores and dashes only"; end + + def add name, search_string + return unless valid_name? name + @searches[name] = search_string + @modified = true + end + + def rename old, new + return unless @searches.has_key? old + search_string = @searches[old] + delete old if add new, search_string + end + + def edit name, search_string + return unless @searches.has_key? name + @searches[name] = search_string + @modified = true + end + + def delete name + return unless @searches.has_key? name + @searches.delete name + @modified = true + end + + def expand search_string + expanded = search_string.dup + until (matches = expanded.scan(/\{([\w-]+)\}/).flatten).empty? + if !(unknown = matches - @searches.keys).empty? + error_message = "Unknown \"#{unknown.join('", "')}\" when expanding \"#{search_string}\"" + elsif expanded.size >= 2048 + error_message = "Check for infinite recursion in \"#{search_string}\"" + end + if error_message + warn error_message + BufferManager.flash error_message + return false + end + matches.each { |n| expanded.gsub! "{#{n}}", "(#{@searches[n]})" if @searches.has_key? n } + end + return expanded + end + + def save + return unless @modified + File.open(@fn, "w") { |f| @searches.sort.each { |(n, s)| f.puts "#{n}: #{s}" } } + @modified = false + end +end + +end -- 1.6.6 _______________________________________________ Sup-devel mailing list Sup-devel@rubyforge.org http://rubyforge.org/mailman/listinfo/sup-devel