typr 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/text.rb CHANGED
@@ -1,14 +1,10 @@
1
- #!/usr/bin/env ruby
2
- # coding: utf-8
3
- # frozen_string_literal: true
4
-
5
1
  require 'stringio'
6
2
  require_relative 'stack.rb'
7
3
 
8
4
  module Typr
9
5
 
10
6
  # A vertically scrollable text viewer with word-wrapping, header bar,
11
- # row selection via hint characters, and search.
7
+ # row selection via hint characters, and pager-style search (/ n p).
12
8
  #
13
9
  # Accepts String, IO, or StringIO input. Content is split into display
14
10
  # rows by newlines and line width, then rendered as a sliding window.
@@ -21,8 +17,8 @@ module Typr
21
17
  # top: 2, left: 0.1, right: 0.9, bottom: -4,
22
18
  # border: :round,
23
19
  # colors: { header: [:yellow, :grey30] }
24
- # )
25
- # text.draw
20
+ # )
21
+ # text.show
26
22
 
27
23
  class Text < Stack
28
24
 
@@ -72,6 +68,7 @@ module Typr
72
68
  end
73
69
  end
74
70
  @lines << @data.length unless @lines.last == @data.length
71
+ highlight if @re
75
72
  end
76
73
  alias :<< :build
77
74
 
@@ -84,7 +81,14 @@ module Typr
84
81
  def print row=nil
85
82
  header = ( row == :header )
86
83
  return super unless row and @lines[ row + 1 ] unless header
87
- show prepare( ( header ? @header : @page[ row-@start ] ), width )
84
+ text = ( header ? @header : @page[ row-@start ] )
85
+ if row.is_a?(Integer) and @search[:matches].include? row
86
+ background @colors[:search]
87
+ draw prepare( text, width )
88
+ background
89
+ else
90
+ draw prepare( text, width )
91
+ end
88
92
  end
89
93
 
90
94
  # Returns word-boundary column offsets for a given page row.
@@ -99,9 +103,9 @@ module Typr
99
103
  # Full draw cycle: rebuilds on resize, computes visible lines,
100
104
  # and renders the viewport with borders and hints.
101
105
  #
102
- # text.draw
106
+ # text.show
103
107
 
104
- def draw
108
+ def show
105
109
  (build; @lastwidth = width) if width != @lastwidth
106
110
  range = @lines[@start..@start+height]
107
111
  page = StringIO.new @data[range.first..range.last]
@@ -115,11 +119,60 @@ module Typr
115
119
 
116
120
  def rows; @lines.count end
117
121
 
118
- # Placeholder for text search. Currently unimplemented.
122
+ ##
123
+ # Pager-style search over the buffer, modeled after less/vim.
124
+ #
125
+ # With a +query+ (String, matched literally, or Regexp) it stores the
126
+ # pattern, highlights every matching line, and scrolls to the first
127
+ # match from the current position. +dir+ is +:forward+ or +:backward+.
128
+ # Returns the matched line id, or nil when nothing matches.
129
+ #
130
+ # With no argument it prompts interactively for a pattern at the bottom
131
+ # of the screen (pressing Return searches, Escape aborts).
119
132
  #
120
- # text.search("query") # => nil
133
+ # Matching is smart-case: lowercase queries are case-insensitive;
134
+ # a query containing an uppercase letter is case-sensitive.
135
+ #
136
+ # text.search "needle" # => 12 (or nil)
137
+ # text.search /^TODO/ # => 3
138
+ # text.search "needle", :backward # search upward
139
+
140
+ def search query=nil, dir=:forward
141
+ return search_prompt if query.nil?
142
+ @search[:pattern] = query
143
+ @search[:dir] = dir
144
+ @search[:last] = nil
145
+ @re = query.is_a?(Regexp) ? query :
146
+ Regexp.new( Regexp.escape( query ),
147
+ ( query[/[A-Z]/] ? 0 : Regexp::IGNORECASE ) )
148
+ highlight
149
+ step
150
+ end
151
+
152
+ ## Jump to the next match of the last search (wrap-around). Returns its line id or nil.
153
+
154
+ def search_next; @search[:dir] = :forward; step end
155
+
156
+ ## Jump to the previous match of the last search (wrap-around). Returns its line id or nil.
157
+
158
+ def search_prev; @search[:dir] = :backward; step end
159
+
160
+ ## Recompute the set of lines containing the current pattern.
161
+
162
+ def highlight
163
+ @search[:matches] = @re ?
164
+ @lines[0...-1].each_index.select{ |i|
165
+ @data[ @lines[i] ... @lines[i+1] ][ @re ] } : []
166
+ return
167
+ end
168
+
169
+ ## Clear the search pattern and all match highlights.
121
170
 
122
- def search query
171
+ def highlight_clear
172
+ @re = @search[:pattern] = nil
173
+ @search[:matches] = []
174
+ @search[:last] = nil
175
+ return
123
176
  end
124
177
 
125
178
  # Creates a new Text widget.
@@ -130,11 +183,62 @@ module Typr
130
183
  def initialize args={}
131
184
  @tabspace = 2
132
185
  @data, @lines, @tail = "", [0], ""
186
+ @search = { pattern: nil, dir: :forward, last: nil, matches: [] }
133
187
  super args
134
- @colors = { header: :yellow }.merge( @colors || {} )
188
+ @colors = { header: :yellow, search: :grey20 }.merge( @colors || {} )
189
+ @keymap = { search: ?/, search_next: ?n, search_prev: ?p }.merge @keymap
135
190
  @lastwidth = width
136
191
  self << @input
137
192
  end
193
+
194
+ def reset type=:all
195
+ case type
196
+ when :search; highlight_clear
197
+ when :all; reset [:position, :selection, :search]; return
198
+ end
199
+ super
200
+ end
201
+
202
+ private
203
+
204
+ def live_search query
205
+ query.empty? ? highlight_clear : search(query)
206
+ end
207
+
208
+ def step
209
+ matches = @search[:matches]
210
+ return nil if matches.empty?
211
+ last = @search[:last]
212
+ if @search[:dir] == :backward
213
+ id = matches.reverse.find{ |m| last and m < last }
214
+ id ||= ( last ? matches.last : matches.reverse.find{ |m| m <= @start } || matches.last )
215
+ else
216
+ id = matches.find{ |m| last and m > last }
217
+ id ||= ( last ? matches.first : matches.find{ |m| m >= @start } || matches.first )
218
+ end
219
+ @search[:last] = id
220
+ @start = id
221
+ return id
222
+ end
223
+
224
+ def search_prompt
225
+ query = @search[:pattern].is_a?(String) ? @search[:pattern].dup : ''
226
+ saved = { re: @re, pattern: @search[:pattern],
227
+ matches: @search[:matches].dup, last: @search[:last], start: @start }
228
+ result = Typr.read_line '/', left: left, top: Typr.height - 1, initial: query,
229
+ &->(key, query, _) {
230
+ live_search query
231
+ nil
232
+ }
233
+ if result
234
+ return search(result) unless result.empty?
235
+ return search(@search[:pattern]) if @search[:pattern]
236
+ end
237
+ @re = saved[:re]; @search[:pattern] = saved[:pattern]
238
+ @search[:matches] = saved[:matches]; @search[:last] = saved[:last]
239
+ @start = saved[:start]
240
+ return
241
+ end
138
242
  end
139
243
 
140
244
  end
data/lib/typr.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # typr is a Ruby library for building interactive terminal-based user interfaces.
2
- # It provides layout primitives, widgets, and event-driven interaction built on
3
- # top of curses.
2
+ # It provides layout primitives, widgets, and event-driven interaction.
4
3
  #
5
4
  # == Widgets
6
5
  #
@@ -16,8 +15,8 @@
16
15
  # Typr.init # Initialize terminal (hide cursor, enable key mode)
17
16
  # Typr.clear # Clear entire screen
18
17
  # Typr.clear :line # Clear current line only
19
- # key = Typr.read(:key) # Read a single keypress
20
- # line = Typr.read(:line) # Read a line of text input
18
+ # key = Typr.read_key # Read a single keypress
19
+ # line = Typr.read_line "> " # Edit a line of text (enter to accept, esc to abort)
21
20
  # Typr.exit # Restore terminal state
22
21
  #
23
22
  # == Layout Properties
@@ -38,8 +37,8 @@
38
37
  # header: ['Name', 'Age'],
39
38
  # format: [:max, :right]
40
39
  # )
41
- # grid.draw
42
- # key = Typr.read(:key)
40
+ # grid.show
41
+ # key = Typr.read_key
43
42
  # Typr.exit
44
43
  #
45
44
  # == Installation