sup

A curses threads-with-tags style email client

sup.git

git clone https://supmua.dev/git/sup/

lib/sup/modes/scroll_mode.rb (7497B) - raw

      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