Class: Typr::Text

Inherits:
Stack show all
Defined in:
lib/text.rb

Overview

) text.show

Constant Summary

Constants included from Typr

ALT_SCREEN_OFF, ALT_SCREEN_ON, COLORS, COLOR_MAP, DEFAULT_KEYS, ERASE_LINE, INPUT, KEY_ALT_BACKSPACE, KEY_ALT_LEFT, KEY_ALT_RIGHT, KEY_BACK_TAB, KEY_ESCAPE, KEY_PAGEDOWN, KEY_PAGEUP, KEY_RETURN, KEY_TAB, MIMETYPES, MODES, MOUSE_OFF, MOUSE_ON, ORIG_COLORS, TERMINFO

Instance Attribute Summary collapse

Attributes inherited from Stack

#header, #hints, #hints_start, #keymap, #modifier, #selected, #separator, #start

Attributes inherited from Space

#borders, #bottom, #colors, #interval, #left, #margin, #right, #top

Instance Method Summary collapse

Methods inherited from Stack

#cycle, #cycle_back, #data_at, #down, #draw_hints, #headspace, #height, #hit, #page, #page_down, #page_up, #pick, #send, #to_bottom, #to_top, #up

Methods inherited from Space

#border=, #data_at, #draw_border, #extract_key, #height, #start, #stop, #symbolize, #width

Methods included from Typr

#background, clear, #clip, #coerce_type, #color, #color_code, column, decode_mouse, #draw, exit, #fade, #foreground, #get_background, #get_foreground, height, init, #mode, #mode_code, #move, #move_code, on_resize, position, #prepare, read_key, read_line, #real_size, row, #sanitize, size, slice_width, text_width, width, word_next, word_prev

Constructor Details

#initialize(args = {}) ⇒ Text

Creates a new Text widget.

text = Typr::Text.new(input: "hello", header: "Title")
text = Typr::Text.new(input: $stdin, tabspace: 4)


195
196
197
198
199
200
201
202
203
204
# File 'lib/text.rb', line 195

def initialize args={}
  @tabspace = 2
  @data, @lines, @tail = "", [0], ""
  @search = { pattern: nil, dir: :forward, last: nil, matches: [] }
  super args
  @colors = { header: :yellow, search: :grey20 }.merge( @colors || {} )
  @keymap = { search: ?/, search_next: ?n, search_prev: ?p }.merge @keymap
  @lastwidth = width
  self << @input
end

Instance Attribute Details

#tabspaceObject

Tabstop width in spaces. Default: 2.



26
27
28
# File 'lib/text.rb', line 26

def tabspace
  @tabspace
end

Instance Method Details

#build(input = nil) ⇒ Object Also known as: <<

Rebuilds the internal line buffer from input. Splits text at newlines and word boundaries to fit the current width. Pass nil to re-parse from the existing buffer (triggered on resize).

text.build("one\ntwo three")  # => #<Typr::Text ...>
text << "appended line"       # alias for build


35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/text.rb', line 35

def build input=nil
  ( input, @data, @lines = @data, "", [0] ) if rebuild = !input
  return self if rebuild && input == ""
  str = input.respond_to?(:read) ? input.read : input.to_s
  @tail = ""
  pos = 0
  while pos < str.size || !@tail.empty?
    capacity = width - @margin.size - real_size(@tail)
    cap = [capacity, 1].max
    added = 0; cells = 0
    while pos + added < str.size
      c = str[pos + added]
      if c == ?\e
        seq = str[(pos+added)..][/\A\e\[[0-9;]*m/]
        added += seq ? seq.size : 1
      elsif c == ?\n
        added += 1; break
      else
        cw = Unicode::DisplayWidth.of(c)
        break if cells + cw > cap
        cells += cw; added += 1
      end
    end
    line = @tail + str[pos, added]
    pos += added
    break if line.empty?
    wrap_idx = line.index("\n") || line.rindex(" ") || line.length
    wrap = [wrap_idx, line.length].min + 1
    @data += line[0...wrap]
    if wrap < line.length
      @tail = line[wrap..-1]
    else
      @tail = ""
    end
    @lines << @data.length if wrap < line.length || line.include?("\n")
  end
  until @tail.empty?
    wrap_idx = @tail.index("\n") || @tail.rindex(" ") || @tail.length
    wrap = [wrap_idx, @tail.length].min + 1
    @data += @tail[0...wrap]
    if wrap < @tail.length
      @lines << @data.length
      @tail = @tail[wrap..-1]
    else
      @tail = ""
    end
  end
  @lines << @data.length unless @lines.last == @data.length
  highlight if @re
end

#highlightObject

Recompute the set of lines containing the current pattern.



174
175
176
177
178
179
# File 'lib/text.rb', line 174

def highlight
  @search[:matches] = @re ?
    @lines[0...-1].each_index.select{ |i|
      @data[ @lines[i] ... @lines[i+1] ][ @re ] } : []
  return
end

#highlight_clearObject

Clear the search pattern and all match highlights.



183
184
185
186
187
188
# File 'lib/text.rb', line 183

def highlight_clear
  @re = @search[:pattern] = nil
  @search[:matches] = []
  @search[:last] = nil
  return
end

#positions(row) ⇒ Object

Returns word-boundary column offsets for a given page row. Used internally to place hint markers between words.

text.positions(2)  # => [0, 6, 10, ...]


111
112
113
# File 'lib/text.rb', line 111

def positions row
  [0] + @page[row-1].chars.map.with_index{|c,i| i+1 if c == @separator}.compact
end

Renders a single display row. Pass an Integer for a content row or :header for the header bar.

text.print(:header)  # renders the header bar
text.print(3)        # renders row 3 of the current page


93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/text.rb', line 93

def print row=nil
  header = ( row == :header )
  return super unless row and @lines[ row + 1 ] unless header
  text = ( header ? @header : @page[ row-@start ] )
  if row.is_a?(Integer) and @search[:matches].include? row
    background @colors[:search]
    draw prepare( text, width - @margin.size )
    background
  else
    draw prepare( text, width - @margin.size )
  end
end

#reset(type = :all) ⇒ Object

Reset state: :search clears the pattern and highlights; :all resets position, selection, and search; else falls through to Stack#reset.



208
209
210
211
212
213
214
# File 'lib/text.rb', line 208

def reset type=:all
  case type
    when :search; highlight_clear
    when :all; reset [:position, :selection, :search]; return
  end
  super
end

#rowsObject

Total number of text lines in the buffer.

text.rows  # => 42


132
# File 'lib/text.rb', line 132

def rows; @lines.count end

#search(query = nil, dir = :forward) ⇒ Object

Pager-style search over the buffer, modeled after less/vim.

With a query (String, matched literally, or Regexp) it stores the pattern, highlights every matching line, and scrolls to the first match from the current position. dir is :forward or :backward. Returns the matched line id, or nil when nothing matches.

With no argument it prompts interactively for a pattern at the bottom of the screen (pressing Return searches, Escape aborts).

Matching is smart-case: lowercase queries are case-insensitive; a query containing an uppercase letter is case-sensitive.

text.search "needle"    # => 12 (or nil)
text.search /^TODO/     # => 3
text.search "needle", :backward   # search upward


152
153
154
155
156
157
158
159
160
161
162
# File 'lib/text.rb', line 152

def search query=nil, dir=:forward
  return search_prompt if query.nil?
  @search[:pattern] = query
  @search[:dir] = dir
  @search[:last] = nil
  @re = query.is_a?(Regexp) ? query :
    Regexp.new( Regexp.escape( query ),
      ( query[/[A-Z]/] ? 0 : Regexp::IGNORECASE ) )
  highlight
  step
end

#search_nextObject

Jump to the next match of the last search (wrap-around). Returns its line id or nil.



166
# File 'lib/text.rb', line 166

def search_next; @search[:dir] = :forward; step end

#search_prevObject

Jump to the previous match of the last search (wrap-around). Returns its line id or nil.



170
# File 'lib/text.rb', line 170

def search_prev; @search[:dir] = :backward; step end

#showObject

Full draw cycle: rebuilds on resize, computes visible lines, and renders the viewport with borders and hints.

text.show


120
121
122
123
124
125
126
# File 'lib/text.rb', line 120

def show
  (build; @lastwidth = width) if width != @lastwidth
  range = @lines[@start..@start+height]
  page = StringIO.new @data[range.first..range.last]
  @page=range[1..-1].map{|pos| page.read(pos-range.first-page.pos).rstrip}
  super
end