From: wmorgan Date: Fri, 6 Jul 2007 14:20:19 +0000 (+0000) Subject: finally, attachment support\! X-Git-Url: https://git.cworth.org/git?a=commitdiff_plain;h=ab60e88c18c49fef8907fe42ebf49a6437cbddb7;p=sup finally, attachment support\! git-svn-id: svn://rubyforge.org/var/svn/sup/trunk@473 5c8cc53c-5e98-4d25-b20a-d8db53a31250 --- diff --git a/Manifest.txt b/Manifest.txt index 440f0bb..fdaebde 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -34,6 +34,7 @@ lib/sup/mbox/ssh-loader.rb lib/sup/message.rb lib/sup/mode.rb lib/sup/modes/buffer-list-mode.rb +lib/sup/modes/completion-mode.rb lib/sup/modes/compose-mode.rb lib/sup/modes/contact-list-mode.rb lib/sup/modes/edit-message-mode.rb @@ -53,6 +54,7 @@ lib/sup/modes/search-results-mode.rb lib/sup/modes/text-mode.rb lib/sup/modes/thread-index-mode.rb lib/sup/modes/thread-view-mode.rb +lib/sup/modes/file-browser-mode.rb lib/sup/person.rb lib/sup/poll.rb lib/sup/rfc2047.rb diff --git a/Rakefile b/Rakefile index 180f0af..41225eb 100644 --- a/Rakefile +++ b/Rakefile @@ -16,7 +16,7 @@ Hoe.new('sup', Redwood::VERSION) do |p| p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[2].gsub(/^\s+/, "") p.changes = p.paragraphs_of('History.txt', 0..0).join("\n\n") p.email = "wmorgan-sup@masanjin.net" - p.extra_deps = [['ferret', '>= 0.10.13'], ['ncurses', '>= 0.9.1'], ['rmail', '>= 0.17'], 'highline', 'net-ssh', ['trollop', '>= 1.7'], 'lockfile'] + p.extra_deps = [['ferret', '>= 0.10.13'], ['ncurses', '>= 0.9.1'], ['rmail', '>= 0.17'], 'highline', 'net-ssh', ['trollop', '>= 1.7'], 'lockfile', 'mime-types'] end rule 'ss?.png' => 'ss?-small.png' do |t| diff --git a/bin/sup b/bin/sup index 83f1e1d..52de3fc 100644 --- a/bin/sup +++ b/bin/sup @@ -119,7 +119,7 @@ begin c.add :message_patina_color, Ncurses::COLOR_BLACK, Ncurses::COLOR_GREEN c.add :alternate_patina_color, Ncurses::COLOR_BLACK, Ncurses::COLOR_BLUE c.add :missing_message_color, Ncurses::COLOR_BLACK, Ncurses::COLOR_RED - c.add :mime_color, Ncurses::COLOR_CYAN, Ncurses::COLOR_BLACK + c.add :attachment_color, Ncurses::COLOR_CYAN, Ncurses::COLOR_BLACK c.add :quote_patina_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK c.add :sig_patina_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK c.add :quote_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK @@ -137,6 +137,8 @@ begin Ncurses::A_BOLD c.add :draft_notification_color, Ncurses::COLOR_RED, Ncurses::COLOR_BLACK, Ncurses::A_BOLD + c.add :completion_character_color, Ncurses::COLOR_WHITE, + Ncurses::COLOR_BLACK, Ncurses::A_BOLD end log "initializing buffer manager" @@ -261,14 +263,15 @@ ensure Redwood::finish stop_cursing + Redwood::log "stopped cursing" if SuicideManager.instantiated? && SuicideManager.die? - Redwood::log "I've been asked to commit sepuku. I obey!" + Redwood::log "I've been ordered to commit sepuku. I obey!" end case $exception when nil - Redwood::log "good night, sweet prince!" + Redwood::log "no fatal errors. good job, william." Index.save else Redwood::log "oh crap, an exception" diff --git a/doc/TODO b/doc/TODO index 1ca6d43..e84af4e 100644 --- a/doc/TODO +++ b/doc/TODO @@ -1,5 +1,6 @@ for 0.0.9 --------- +_ bugfix: screwing the headers when editing causes a crash _ bugfix: when one new message comes into an imap folder, we don't catch it until a reload (sometimes?) message indicating they're loaded to inbox (imap only?) diff --git a/lib/sup.rb b/lib/sup.rb index 6ca881f..953aa5d 100644 --- a/lib/sup.rb +++ b/lib/sup.rb @@ -244,6 +244,8 @@ require "sup/modes/inbox-mode" require "sup/modes/buffer-list-mode" require "sup/modes/log-mode" require "sup/modes/poll-mode" +require "sup/modes/file-browser-mode" +require "sup/modes/completion-mode" require "sup/logger" require "sup/sent" diff --git a/lib/sup/buffer.rb b/lib/sup/buffer.rb index ac7575f..198d4d1 100644 --- a/lib/sup/buffer.rb +++ b/lib/sup/buffer.rb @@ -1,3 +1,4 @@ +require 'etc' require 'thread' module Ncurses @@ -28,7 +29,9 @@ module Ncurses module_function :rows, :cols, :nonblocking_getch, :mutex, :sync - KEY_CANCEL = "\a"[0] # ctrl-g + KEY_ENTER = 10 + KEY_CANCEL = ?\a # ctrl-g + KEY_TAB = 9 end module Redwood @@ -204,19 +207,22 @@ class BufferManager ## disabling this for the time being, to help with debugging ## (currently we only have one buffer visible at a time). ## TODO: reenable this if we allow multiple buffers - false && @buffers.inject(@dirty) do |dirty, buf| + true && @buffers.inject(@dirty) do |dirty, buf| buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols - @dirty ? buf.draw : buf.redraw + #dirty ? buf.draw : buf.redraw + buf.draw + dirty end ## quick hack - if true + if false buf = @buffers.last buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols @dirty ? buf.draw : buf.redraw end draw_minibuf :sync => false unless opts[:skip_minibuf] + @dirty = false Ncurses.doupdate Ncurses.refresh if opts[:refresh] @@ -268,6 +274,24 @@ class BufferManager b end + ## requires the mode to have #done? and #value methods + def spawn_modal title, mode, opts={} + b = spawn title, mode, opts + draw_screen + + until mode.done? + c = Ncurses.nonblocking_getch + next unless c # getch timeout + break if c == Ncurses::KEY_CANCEL + mode.handle_input c + draw_screen + erase_flash + end + + kill_buffer b + mode.value + end + def kill_all_buffers_safely until @buffers.empty? ## inbox mode always claims it's unkillable. we'll ignore it. @@ -302,20 +326,56 @@ class BufferManager end end - ## not really thread safe. - def ask domain, question, default=nil + ## returns an ARRAY of filenames! + def ask_for_filenames domain, question, default=nil + answer = ask domain, question, default do |s| + if s =~ /(~([^\s\/]*))/ # twiddle directory expansion + full = $1 + name = $2.empty? ? Etc.getlogin : $2 + dir = Etc.getpwnam(name).dir rescue nil + if dir + [[s.sub(full, dir), "~#{name}"]] + else + users.select { |u| u =~ /^#{name}/ }.map do |u| + [s.sub("~#{name}", "~#{u}"), "~#{u}"] + end + end + else # regular filename completion + Dir["#{s}*"].sort.map do |fn| + suffix = File.directory?(fn) ? "/" : "" + [fn + suffix, File.basename(fn) + suffix] + end + end + end + + if answer + if answer.empty? + spawn_modal "file browser", FileBrowserMode.new + elsif File.directory?(answer) + spawn_modal "file browser", FileBrowserMode.new(answer) + else + [answer] + end + else + [] + end + end + + def ask domain, question, default=nil, &block raise "impossible!" if @asking + @asking = true @textfields[domain] ||= TextField.new Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols tf = @textfields[domain] + completion_buf = nil - ## this goddamn ncurses form shit is a fucking 1970's - ## nightmare. jesus christ. the exact sequence of ncurses events - ## that needs to happen in order to display a form and have the - ## entire screen not disappear and have the cursor in the right - ## place is TOO FUCKING COMPLICATED. + ## this goddamn ncurses form shit is a fucking 1970's nightmare. + ## jesus christ. the exact sequence of ncurses events that needs + ## to happen in order to display a form and have the entire screen + ## not disappear and have the cursor in the right place is TOO + ## FUCKING COMPLICATED. Ncurses.sync do - tf.activate question, default + tf.activate question, default, &block @dirty = true draw_screen :skip_minibuf => true, :sync => false end @@ -324,15 +384,42 @@ class BufferManager tf.position_cursor Ncurses.sync { Ncurses.refresh } - @asking = true - while tf.handle_input(Ncurses.nonblocking_getch); end - @asking = false + while true + c = Ncurses.nonblocking_getch + next unless c # getch timeout + break unless tf.handle_input c # process keystroke + + if tf.new_completions? + kill_buffer completion_buf if completion_buf + + prefix_len = + if tf.value =~ /\/$/ + 0 + else + File.basename(tf.value).length + end + + mode = CompletionMode.new tf.completions.map { |full, short| short }, :header => "Possible completions for \"#{tf.value}\": ", :prefix_len => prefix_len + completion_buf = spawn "", mode, :height => 10 + + draw_screen :skip_minibuf => true + tf.position_cursor + elsif tf.roll_completions? + completion_buf.mode.roll + + draw_screen :skip_minibuf => true + tf.position_cursor + end - ret = tf.value + Ncurses.sync { Ncurses.refresh } + end + Ncurses.sync { tf.deactivate } + kill_buffer completion_buf if completion_buf @dirty = true - - ret + @asking = false + draw_screen + tf.value end ## some pretty lame code in here! @@ -467,5 +554,17 @@ class BufferManager end @shelled = false end + +private + + def users + unless @users + @users = [] + while(u = Etc.getpwent) + @users << u.name + end + end + @users + end end end diff --git a/lib/sup/index.rb b/lib/sup/index.rb index 1181e80..c436ffd 100644 --- a/lib/sup/index.rb +++ b/lib/sup/index.rb @@ -189,7 +189,7 @@ EOS end def save_index fn=File.join(@dir, "ferret") - # don't have to do anything, apparently + # don't have to do anything, apparently end def contains_id? id diff --git a/lib/sup/modes/completion-mode.rb b/lib/sup/modes/completion-mode.rb new file mode 100644 index 0000000..e96b935 --- /dev/null +++ b/lib/sup/modes/completion-mode.rb @@ -0,0 +1,55 @@ +module Redwood + +class CompletionMode < ScrollMode + INTERSTITIAL = " " + + def initialize list, opts={} + @list = list + @header = opts[:header] + @prefix_len = opts[:prefix_len] + @lines = nil + super :slip_rows => 1, :twiddles => false + end + + def lines + update_lines unless @lines + @lines.length + end + + def [] i + update_lines unless @lines + @lines[i] + end + + def roll; if at_bottom? then jump_to_start else page_down end end + +private + + def update_lines + width = buffer.content_width + max_length = @list.map { |s| s.length }.max + num_per = buffer.content_width / (max_length + INTERSTITIAL.length) + @lines = [@header].compact + @list.each_with_index do |s, i| + if @prefix_len + @lines << [] if i % num_per == 0 + if @prefix_len < s.length + prefix = s[0 ... @prefix_len] + suffix = s[(@prefix_len + 1) .. -1] + char = s[@prefix_len].chr + + @lines.last += [[:none, sprintf("%#{max_length - suffix.length - 1}s", prefix)], + [:completion_character_color, char], + [:none, suffix + INTERSTITIAL]] + else + @lines.last += [[:none, sprintf("%#{max_length}s#{INTERSTITIAL}", s)]] + end + else + @lines << "" if i % num_per == 0 + @lines.last += sprintf "%#{max_length}s#{INTERSTITIAL}", s + end + end + end +end + +end diff --git a/lib/sup/modes/edit-message-mode.rb b/lib/sup/modes/edit-message-mode.rb index 67af505..5285279 100644 --- a/lib/sup/modes/edit-message-mode.rb +++ b/lib/sup/modes/edit-message-mode.rb @@ -1,5 +1,7 @@ require 'tempfile' require 'socket' # just for gethostname! +require 'pathname' +require 'rmail' module Redwood @@ -16,6 +18,8 @@ class EditMessageMode < LineCursorMode k.add :send_message, "Send message", 'y' k.add :edit, "Edit message", 'e', :enter k.add :save_as_draft, "Save as draft", 'P' + k.add :attach_file, "Attach a file", 'a' + k.add :delete_attachment, "Delete an attachment", 'd' end def initialize opts={} @@ -23,11 +27,13 @@ class EditMessageMode < LineCursorMode @body = opts.delete(:body) || [] @body += sig_lines if $config[:edit_signature] @attachments = [] + @attachment_lines = {} @message_id = "<#{Time.now.to_i}-sup-#{rand 10000}@#{Socket.gethostname}>" @edited = false + @message_id = "<#{Time.now.to_i}-sup-#{rand 10000}@#{Socket.gethostname}>" super opts - update + regen_text end def lines; @text.length end @@ -59,6 +65,20 @@ class EditMessageMode < LineCursorMode !edited? || BufferManager.ask_yes_or_no("Discard message?") end + def attach_file + fn = BufferManager.ask_for_filenames :attachment, "File name (enter for browser): " + fn.each { |f| @attachments << Pathname.new(f) } + update + end + + def delete_attachment + i = curpos - @top_lines + if i >= 0 && i < @attachments.size && BufferManager.ask_yes_or_no("Delete attachment #{@attachments[i]}?") + @attachments.delete_at i + update + end + end + protected def update @@ -67,7 +87,10 @@ protected end def regen_text - @text = header_lines(@header - NON_EDITABLE_HEADERS) + [""] + @body + top = header_lines(@header - NON_EDITABLE_HEADERS) + [""] + @text = top + @body + + @attachments.map { |f| [[:attachment_color, "+ Attachment: #{f} (#{f.human_size})"]] } + @top_lines = top.size @text += sig_lines unless $config[:edit_signature] end @@ -134,7 +157,7 @@ protected BufferManager.flash "Sending..." begin - IO.popen(acct.sendmail, "w") { |p| write_message p, true, date } + IO.popen(acct.sendmail, "w") { |p| write_full_message_to p } rescue SystemCallError end if $? == 0 @@ -153,6 +176,28 @@ protected BufferManager.flash "Saved for later editing." end + def write_full_message_to f + m = RMail::Message.new + @header.each { |k, v| m.header[k] = v.to_s unless v.to_s.empty? } + m.header["Date"] = Time.now.rfc2822 + m.header["Message-Id"] = @message_id + m.header["User-Agent"] = "Sup/#{Redwood::VERSION}" + if @attachments.empty? + m.header["Content-Disposition"] = "inline" + m.header["Content-Type"] = "text/plain; charset=#{$encoding}" + m.body = @body.join "\n" + m.body += sig_lines.join("\n") unless $config[:edit_signature] + else + body_m = RMail::Message.new + body_m.body = @body.join "\n" + body_m.body += sig_lines.join("\n") unless $config[:edit_signature] + + m.add_part body_m + @attachments.each { |fn| m.add_attachment fn.to_s } + end + f.puts m.to_s + end + ## this is going to change soon: draft messages (currently written ## with full=false) will be output as yaml. def write_message f, full=true, date=Time.now diff --git a/lib/sup/modes/file-browser-mode.rb b/lib/sup/modes/file-browser-mode.rb new file mode 100644 index 0000000..d9af957 --- /dev/null +++ b/lib/sup/modes/file-browser-mode.rb @@ -0,0 +1,108 @@ +require 'pathname' + +module Redwood + +## meant to be spawned via spawn_modal! +class FileBrowserMode < LineCursorMode + RESERVED_ROWS = 1 + + register_keymap do |k| + k.add :back, "Go back to previous directory", "B" + k.add :view, "View file", "v" + k.add :select_file_or_follow_directory, "Select the highlighted file, or follow the directory", :enter + k.add :reload, "Reload file list", "R" + end + + bool_reader :done + attr_reader :value + + def initialize dir="." + @dirs = [Pathname.new(dir).realpath] + @done = false + @value = nil + regen_text + super :skip_top_rows => RESERVED_ROWS + end + + def cwd; @dirs.last end + def lines; @text.length; end + def [] i; @text[i]; end + +protected + + def back + return if @dirs.size == 1 + @dirs.pop + reload + end + + def reload + regen_text + buffer.mark_dirty + end + + def view + name, f = @files[curpos - RESERVED_ROWS] + return unless f && f.file? + + begin + BufferManager.spawn f.to_s, TextMode.new(f.readlines.join) + rescue SystemCallError => e + BufferManager.flash e.message + end + end + + def select_file_or_follow_directory + name, f = @files[curpos - RESERVED_ROWS] + return unless f + + if f.directory? && f.to_s != "." + if f.readable? + @dirs.push f + reload + else + BufferManager.flash "Permission denied - #{f.realpath}" + end + else + begin + @value = [f.realpath.to_s] + @done = true + rescue SystemCallError => e + BufferManager.flash e.message + end + end + end + + def regen_text + @files = + begin + cwd.entries.sort_by do |f| + [f.directory? ? 0 : 1, f.basename.to_s] + end + rescue SystemCallError => e + BufferManager.flash "Error: #{e.message}" + [Pathname.new("."), Pathname.new("..")] + end.map do |f| + real_f = cwd + f + name = f.basename.to_s + + case + when real_f.symlink? + "@" + when real_f.directory? + "/" + else + "" + end + [name, real_f] + end + + size_width = @files.map { |name, f| f.human_size.length }.max + time_width = @files.map { |name, f| f.human_time.length }.max + + @text = ["#{cwd}:"] + @files.map do |name, f| + sprintf "%#{time_width}s %#{size_width}s %s", f.human_time, f.human_size, name + end + end +end + +end diff --git a/lib/sup/modes/line-cursor-mode.rb b/lib/sup/modes/line-cursor-mode.rb index a19fba1..602e721 100644 --- a/lib/sup/modes/line-cursor-mode.rb +++ b/lib/sup/modes/line-cursor-mode.rb @@ -13,7 +13,7 @@ class LineCursorMode < ScrollMode attr_reader :curpos def initialize opts={} - @cursor_top = @curpos = opts[:skip_top_rows] || 0 + @cursor_top = @curpos = opts.delete(:skip_top_rows) || 0 @load_more_callbacks = [] @load_more_callbacks_m = Mutex.new @load_more_callbacks_active = false @@ -135,7 +135,7 @@ protected end end - def jump_to_home + def jump_to_start super set_cursor_pos @cursor_top end diff --git a/lib/sup/modes/scroll-mode.rb b/lib/sup/modes/scroll-mode.rb index 31ccef0..513ecc9 100644 --- a/lib/sup/modes/scroll-mode.rb +++ b/lib/sup/modes/scroll-mode.rb @@ -21,7 +21,7 @@ class ScrollMode < Mode k.add :col_right, "Right one column", :right, 'l' k.add :page_down, "Down one page", :page_down, 'n', ' ' k.add :page_up, "Up one page", :page_up, 'p', :backspace - k.add :jump_to_home, "Jump to top", :home, '^', '1' + k.add :jump_to_start, "Jump to top", :home, '^', '1' k.add :jump_to_end, "Jump to bottom", :end, '$', '0' k.add :jump_to_left, "Jump to the left", '[' end @@ -76,17 +76,18 @@ class ScrollMode < Mode buffer.mark_dirty end + def at_top?; @topline == 0 end + def at_bottom?; @botline == lines end + def line_down; jump_to_line @topline + 1; end def line_up; jump_to_line @topline - 1; end def page_down; jump_to_line @topline + buffer.content_height - @slip_rows; end def page_up; jump_to_line @topline - buffer.content_height + @slip_rows; end - def jump_to_home; jump_to_line 0; end + def jump_to_start; jump_to_line 0; end def jump_to_end; jump_to_line lines - buffer.content_height; end - def ensure_mode_validity - @topline = @topline.clamp 0, lines - 1 - @topline = 0 if @topline < 0 # empty + @topline = @topline.clamp 0, [lines - 1, 0].max @botline = [@topline + buffer.content_height, lines].min end diff --git a/lib/sup/modes/thread-view-mode.rb b/lib/sup/modes/thread-view-mode.rb index 255b2bf..b77f71b 100644 --- a/lib/sup/modes/thread-view-mode.rb +++ b/lib/sup/modes/thread-view-mode.rb @@ -432,7 +432,7 @@ private message_patina_lines(chunk, state, start, parent, prefix, color, star_color) + (chunk.is_draft? ? [[[:draft_notification_color, prefix + " >>> This message is a draft. To edit, hit 'e'. <<<"]]] : []) when Message::Attachment - [[[:mime_color, "#{prefix}+ MIME attachment #{chunk.content_type}#{chunk.desc ? ' (' + chunk.desc + ')': ''}"]]] + [[[:attachment_color, "#{prefix}+ Attachment: #{chunk.content_type}#{chunk.desc ? ' (' + chunk.desc + ')': ''}"]]] when Message::Text t = chunk.lines if t.last =~ /^\s*$/ && t.length > 1 diff --git a/lib/sup/textfield.rb b/lib/sup/textfield.rb index ad27c33..bdd1228 100644 --- a/lib/sup/textfield.rb +++ b/lib/sup/textfield.rb @@ -2,26 +2,47 @@ require 'curses' module Redwood +## a fully-functional text field supporting completions, expansions, +## history--everything! +## +## completion is done emacs-style, and mostly depends on outside +## support, as we merely signal the existence of a new set of +## completions to show (#new_completions?) or that the current list +## of completions should be rolled if they're too large to fill the +## screen (#roll_completions?). +## +## in sup, completion support is implemented through BufferManager#ask +## and CompletionMode. class TextField - attr_reader :value - def initialize window, y, x, width @w, @x, @y = window, x, y @width = width @i = nil @history = [] + + @completion_block = nil + reset_completion_state end - def activate question, default=nil + bool_reader :new_completions, :roll_completions + attr_reader :completions + + ## when the user presses enter, we store the value in @value and + ## clean up all the ncurses cruft. before @value is set, we can + ## get the current value from ncurses. + def value; @field ? get_cur_value : @value end + + def activate question, default=nil, &block @question = question @value = nil + @completion_block = block @field = Ncurses::Form.new_field 1, @width - question.length, @y, @x + question.length, 0, 0 @form = Ncurses::Form.new_form [@field] @history[@i = @history.size] = default || "" Ncurses::Form.post_form @form - @field.set_field_buffer 0, @history[@i] + set_cur_value @history[@i] end def position_cursor @@ -33,24 +54,47 @@ class TextField end def deactivate + reset_completion_state @form.unpost_form @form.free_form @field.free_field + @field = nil Ncurses.curs_set 0 end def handle_input c - if c == 10 # Ncurses::KEY_ENTER - Ncurses::Form.form_driver @form, Ncurses::Form::REQ_VALIDATION - @value = @history[@i] = @field.field_buffer(0).gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ") + ## short-circuit exit paths + case c + when Ncurses::KEY_ENTER # submit! + @value = @history[@i] = get_cur_value return false - elsif c == Ncurses::KEY_CANCEL + when Ncurses::KEY_CANCEL # cancel @history.delete_at @i @i = @history.empty? ? nil : (@i - 1) % @history.size @value = nil return false + when Ncurses::KEY_TAB # completion + break unless @completion_block + if @completions.empty? + v = get_cur_value + c = @completion_block.call v + if c.size > 0 + set_cur_value c.map { |full, short| full }.shared_prefix + end + if c.size > 1 + @completions = c + @new_completions = true + @roll_completions = false + end + else + @new_completions = false + @roll_completions = true + end + return true end + reset_completion_state + d = case c when Ncurses::KEY_LEFT @@ -64,21 +108,39 @@ class TextField when ?\005 Ncurses::Form::REQ_END_FIELD when Ncurses::KEY_UP - @history[@i] = @field.field_buffer(0) + @history[@i] = @field.field_buffer 0 @i = (@i - 1) % @history.size - @field.set_field_buffer 0, @history[@i] + set_cur_value @history[@i] when Ncurses::KEY_DOWN - @history[@i] = @field.field_buffer(0) + @history[@i] = @field.field_buffer 0 @i = (@i + 1) % @history.size - @field.set_field_buffer 0, @history[@i] + set_cur_value @history[@i] else c end - Ncurses::Form.form_driver @form, d if d - Ncurses.refresh - + Ncurses::Form.form_driver @form, d true end + +private + + def reset_completion_state + @completions = [] + @new_completions = @roll_completions = @clear_completions = false + end + + ## ncurses inanity wrapper + def get_cur_value + Ncurses::Form.form_driver @form, Ncurses::Form::REQ_VALIDATION + @field.field_buffer(0).gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ") + end + + ## ncurses inanity wrapper + def set_cur_value v + @field.set_field_buffer 0, v + Ncurses::Form.form_driver @form, Ncurses::Form::REQ_END_FIELD + end + end end diff --git a/lib/sup/util.rb b/lib/sup/util.rb index c2dd4b0..2f89a40 100644 --- a/lib/sup/util.rb +++ b/lib/sup/util.rb @@ -1,4 +1,6 @@ require 'lockfile' +require 'mime/types' +require 'pathname' ## time for some monkeypatching! class Lockfile @@ -27,6 +29,63 @@ class Lockfile def touch_yourself; touch path end end +class Pathname + def human_size + s = + begin + size + rescue SystemCallError + return "?" + end + + if s < 1024 + s.to_s + "b" + elsif s < (1024 * 1024) + (s / 1024).to_s + "k" + elsif s < (1024 * 1024 * 1024) + (s / 1024 / 1024).to_s + "m" + else + (s / 1024 / 1024 / 1024).to_s + "g" + end + end + + def human_time + begin + ctime.strftime("%Y-%m-%d %H:%M") + rescue SystemCallError + "?" + end + end +end + +## more monkeypatching! +module RMail + class EncodingUnsupportedError < StandardError; end + + class Message + def add_attachment fn + bfn = File.basename fn + a = Message.new + t = MIME::Types.type_for(bfn).first || MIME::Types.type_for("exe").first + + a.header.add "Content-Disposition", "attachment; filename=#{bfn}" + a.header.add "Content-Type", "#{t.content_type}; name=#{bfn}" + a.header.add "Content-Transfer-Encoding", t.encoding + a.body = + case t.encoding + when "base64" + [IO.read(fn)].pack "m" + when "quoted-printable" + [IO.read(fn)].pack "M" + else + raise EncodingUnsupportedError, t.encoding + end + + add_part a + end + end +end + class Range ## only valid for integer ranges (unless I guess it's exclusive) def size @@ -77,6 +136,8 @@ class Object ## ## i'm sure there's pithy comment i could make here about the ## superiority of lisp, but fuck lisp. + ## + ## addendum: apparently this is a "k combinator". whoda thunk it? def returning x; yield x; x; end ## clone of java-style whole-method synchronization @@ -213,6 +274,20 @@ module Enumerable end best end + + ## returns the maximum shared prefix of an array of strings + ## optinally excluding a prefix + def shared_prefix exclude="" + return "" if empty? + prefix = "" + (0 ... first.length).each do |i| + c = first[i] + break unless all? { |s| s[i] == c } + next if exclude[i] == c + prefix += c.chr + end + prefix + end end class Array @@ -224,6 +299,8 @@ class Array def rest; self[1..-1]; end def to_boolean_h; Hash[*map { |x| [x, true] }.flatten]; end + + def last= e; self[-1] = e end end class Time