typr 1.7.0 → 1.9.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/terminal.rb CHANGED
@@ -46,48 +46,80 @@ module Typr
46
46
  const_set cap.upcase, entry[cap] || seq
47
47
  end
48
48
 
49
- # Common key aliases and the line-erase escape sequence.
49
+ # Line-erase escape sequence (used by {Typr.clear} :line).
50
50
  ERASE_LINE = "\e[K"
51
51
  # Reset foreground/background to the terminal defaults.
52
52
  ORIG_COLORS = "\e[39;49m"
53
+ # Escape: abort a pick or prompt.
53
54
  KEY_ESCAPE = "\e"
55
+ # Enter: confirm a pick (alias of CARRIAGE_RETURN).
54
56
  KEY_RETURN = CARRIAGE_RETURN
57
+ # Tab: cycle hint chunks; completes the word in {Typr.read_line}.
55
58
  KEY_TAB = "\t"
59
+ # Shift+Tab: cycle hint chunks backwards.
56
60
  KEY_BACK_TAB = "\e[Z"
61
+ # Alt+Backspace: delete the word before the cursor.
57
62
  KEY_ALT_BACKSPACE = "\e\x7f"
63
+ # Alt+Left: move the cursor to the start of the previous word.
58
64
  KEY_ALT_LEFT = "\e[1;3D"
65
+ # Alt+Right: move the cursor to the start of the next word.
59
66
  KEY_ALT_RIGHT = "\e[1;3C"
67
+ # Page down: jump one viewport (alias of KEY_NPAGE).
60
68
  KEY_PAGEDOWN = KEY_NPAGE
69
+ # Page up: jump one viewport backwards (alias of KEY_PPAGE).
61
70
  KEY_PAGEUP = KEY_PPAGE
62
71
 
63
- # Enable/disable mouse reporting. Universal private modes, independent of
64
- # terminfo: 1000 = button press/release/wheel, 1006 = SGR coordinates.
72
+ # Enable mouse reporting (buttons + wheel): 1000 press/release, 1006 SGR coordinates.
65
73
  MOUSE_ON = "\e[?1000h\e[?1006h"
74
+ # Disable mouse reporting.
66
75
  MOUSE_OFF = "\e[?1000l\e[?1006l"
76
+ # Enter the alternate screen buffer.
67
77
  ALT_SCREEN_ON = "\e[?1049h"
78
+ # Leave the alternate screen buffer.
68
79
  ALT_SCREEN_OFF = "\e[?1049l"
69
80
 
70
81
  # A mouse event decoded from the input stream. +x+ / +y+ are 1-based
71
82
  # terminal coordinates; +button+ is 0=left, 1=middle, 2=right, or 4-7 for
72
83
  # the wheel (up/down/left/right); +modifiers+ holds the SGR shift/alt/ctrl
73
84
  # bits. Non-widget code can test `key.is_a?(Typr::Mouse)`.
85
+ #
86
+ # Typr::Mouse.new(0, 10, 5, :press, 0)
87
+ # m = Typr.decode_mouse "\e[<0;10;5M"
88
+ # m.press? #=> true
89
+ # m.left? #=> true
90
+ # m.alt? #=> true (with the SGR alt bit set)
91
+ # m.to_s #=> "press button=0 x=10 y=5"
74
92
  Mouse = Struct.new(:button, :x, :y, :action, :modifiers) do
93
+ # True on button press (the moment to pick a row).
75
94
  def press?; action == :press end
95
+ # True on button release.
76
96
  def release?; action == :release end
97
+ # True for any wheel button (4-7).
77
98
  def wheel?; button.between?(4, 7) end
99
+ # True for wheel-up (button 4); scrolls a picker up.
78
100
  def wheel_up?; button == 4 end
101
+ # True for wheel-down (button 5); scrolls a picker down.
79
102
  def wheel_down?; button == 5 end
103
+ # True for the left button; pickers treat it as a hint-digit press.
80
104
  def left?; button == 0 end
105
+ # True for the middle button.
81
106
  def middle?; button == 1 end
107
+ # True for the right button.
82
108
  def right?; button == 2 end
109
+ # True when the SGR alt/meta bit is set — callers may treat an
110
+ # Alt+click like a right-click.
83
111
  def alt?; ( modifiers & 8 ) != 0 end
112
+ # Human-readable one-line summary.
84
113
  def to_s; "%s button=%i x=%i y=%i%s" % [ action, button, x, y,
85
114
  ( modifiers > 0 ? " modifiers=#{modifiers}" : "" ) ] end
86
115
  end
87
116
 
88
117
  # Text attributes (reset, bold, italic, ...) and named 8/256-color tables.
89
- MODES = %i[ reset bold italic underline slow fast invert ]
118
+ MODES = %i[ reset bold dim italic underline slow fast invert ]
119
+ MODES[25] = :steady
120
+ # The eight basic color names accepted by {#color_code}.
90
121
  COLORS = %i[ black red green yellow blue magenta cyan white ]
122
+ # Named colors mapped to 8-bit palette ids (used by {#color_code}).
91
123
  COLOR_MAP = {
92
124
  brown: 130, orange: 208, lime: 118, pink: 218,
93
125
  maroon: 52, navy: 18, teal: 30, olive: 100,
@@ -96,35 +128,41 @@ module Typr
96
128
  dark_blue: 18, dark_magenta: 89, dark_cyan: 30
97
129
  }
98
130
 
99
- # Pad or clip +str+ to exactly +max+ cells, honoring +align+ (:left/:right),
100
- # trimming from +side+, with an optional trailing +fade+.
101
- def prepare str, max, align=:left, side=:right, fade=false
102
- stop = width = real_size( str )
103
- unless str.ascii_only? and width == str.size
104
- stop = width = 0
105
- str.each_char do |c|
106
- if c == ?\e
107
- width -= str[stop..-1][/^\x1b\[[^m]+m|/].size-1
108
- stop += 1
109
- elsif (cwidth = (c.ascii_only? ? 1 : Unicode::DisplayWidth.of(c))) +
110
- width > max
111
- break
112
- else width += cwidth; stop += 1 end
113
- end
114
- end
115
- if ( space = ( max - width ) ) > 0
116
- str = [ str[0..stop-1], " " * space ]
117
- str.reverse! if align == :right
118
- str.join
119
- else
120
- str = clip str, max, side
121
- str = fade str, side, fade if fade
122
- return str
123
- end
131
+ ##
132
+ # Fit +str+ to exactly +max+ cells: pad the shortfall with spaces
133
+ # (before the text when +align+ is :right), or truncate the overflow
134
+ # from +side+, keeping ANSI escapes intact.
135
+ #
136
+ # space.prepare "ab", 5 #=> "ab "
137
+ # space.prepare "ab", 5, :right #=> " ab"
138
+ # space.prepare "long text", 4 #=> "long" (clipped)
139
+ # space.prepare "\e[31mRED\e[0m", 5 #=> "\e[31mRED\e[0m "
140
+ #
141
+ # @param str [String] text; ANSI escapes count as zero width
142
+ # @param max [Integer] target width in cells
143
+ # @param align [Symbol] :left pads after, :right pads before
144
+ # @param side [Symbol] truncation side, :left keeps the tail
145
+ # @return [String]
146
+
147
+ def prepare str, max, align=:left, side=:right
148
+ str = clip str, max, side if real_size(str) >= max
149
+ pad = " " * (max - real_size(str))
150
+ align == :right ? pad + str : str + pad
124
151
  end
125
152
 
153
+ ##
126
154
  # Truncate +str+ to at most +max+ cells, keeping ANSI color tokens intact
127
155
  # and clipping from +side+ (:left/:right).
156
+ #
157
+ # space.clip "abcdef", 4, :right #=> "abcd"
158
+ # space.clip "abcdef", 4, :left #=> "cdef"
159
+ # space.clip "\e[31mlongtext\e[0m", 4, :left #=> "\e[31mtext"
160
+ #
161
+ # @param str [String] text possibly containing ANSI escapes
162
+ # @param max [Integer] maximum width in cells
163
+ # @param side [Symbol] :left keeps the tail, :right keeps the head
164
+ # @return [String]
165
+
128
166
  def clip str, max, side
129
167
  tokens = str.scan(/(\e\[[0-9;]*m)|([^\e]+)/).flatten.compact
130
168
  tokens.reverse! if side == :left
@@ -164,8 +202,17 @@ module Typr
164
202
  kept.join
165
203
  end
166
204
 
205
+ ##
167
206
  # Blend the first (or last) +num+ characters of +str+ into greys to hint
168
207
  # at clipping; +side+ selects which end fades.
208
+ #
209
+ # space.fade "longtext", :right #=> "long" + grey(e,x,t)
210
+ #
211
+ # @param str [String] already-clipped text
212
+ # @param side [Symbol] :left or :right (which end fades)
213
+ # @param num [Integer] number of characters to fade
214
+ # @return [String]
215
+
169
216
  def fade str, side=:left, num=3
170
217
  isleft = side == :left
171
218
  chars = str[ isleft ? 0..num : -num..-1 ]
@@ -177,19 +224,44 @@ module Typr
177
224
  str[0..-num-1] + chars )
178
225
  end
179
226
 
227
+ ##
180
228
  # Strip ANSI escape sequences and control characters (except \n, \t)
181
229
  # from shell-generated text, leaving only printable content.
230
+ #
231
+ # space.sanitize "\e[31mred\e[0m plain" #=> "red plain"
232
+ #
233
+ # @param str [String] raw text
234
+ # @return [String]
235
+
182
236
  def sanitize str
183
237
  str.scrub
184
238
  .gsub(/\e(?:\[[0-9;?]*[ -\/]*[@-~]|\][^\a\e]*(?:\a|\e\\)|[()=><0A])/, '')
185
239
  .gsub(/[\x00-\x08\x0b-\x1f\x7f]/, '')
186
240
  end
187
241
 
242
+ ##
188
243
  # Display width of +str+ after stripping ANSI escapes and control chars.
244
+ #
245
+ # space.real_size "\e[31mabc\e[0m" #=> 3
246
+ #
247
+ # @param str [String] text possibly containing ANSI escapes
248
+ # @return [Integer] width in cells
249
+
189
250
  def real_size str; Unicode::DisplayWidth.of( sanitize(str) ) end
190
251
 
252
+ ##
191
253
  # Coerce a string into Integer, Float, or Boolean when it matches those
192
- # forms (yes/no, true/false); otherwise return it unchanged.
254
+ # forms (yes/no, true/false); otherwise return it unchanged. Empty
255
+ # strings and +nil+/+NULL+ become nil.
256
+ #
257
+ # space.coerce_type "42" #=> 42
258
+ # space.coerce_type "3.14" #=> 3.14
259
+ # space.coerce_type "yes" #=> true
260
+ # space.coerce_type "hello" #=> "hello" (unchanged)
261
+ #
262
+ # @param value [Object] cell value (usually a String)
263
+ # @return [Integer, Float, Boolean, nil, Object]
264
+
193
265
  def coerce_type value
194
266
  s = value.to_s
195
267
  return nil if s.empty? or /\A(?:nil|NULL)\z/i.match(s)
@@ -204,19 +276,67 @@ module Typr
204
276
  end
205
277
  end
206
278
 
207
- # Move the cursor to column +x+, row +y+ (0-based); move_code returns the
208
- # escape sequence without writing it.
279
+ ##
280
+ # Move the cursor to column +x+, row +y+ (0-based); move_code returns
281
+ # the escape sequence without writing it.
282
+ #
283
+ # space.move 0, 0 # prints "\e[1;1f"
284
+ # space.move_code 0, 0 #=> "\e[1;1f"
285
+ #
286
+ # @param x [Integer] column (0-based)
287
+ # @param y [Integer] row (0-based)
288
+ # @return [void] / [String] move_code returns the sequence
289
+
209
290
  def move x=0,y=0; draw move_code( x, y ) end
291
+
292
+ ##
293
+ # The escape sequence for {#move}, without writing it.
294
+ #
295
+ # space.move_code 0, 0 #=> "\e[1;1f"
296
+ #
297
+ # @param x [Integer] column (0-based)
298
+ # @param y [Integer] row (0-based)
299
+ # @return [String]
300
+
210
301
  def move_code x=0,y=0; "\e[%i;%if" % [ y+1, x+1 ] end
211
302
 
303
+ ##
212
304
  # Apply a text attribute (+mode+); mode_code returns the escape sequence
213
305
  # without writing it.
306
+ #
307
+ # space.mode :bold # prints "\e[1m"
308
+ # space.mode_code :bold #=> "\e[1m"
309
+ #
310
+ # @param name [Symbol] one of +MODES+ (:bold, :underline, :invert, ...)
311
+ # @return [void] / [String] mode_code returns the sequence
312
+
214
313
  def mode name; draw mode_code(name) end
314
+
315
+ ##
316
+ # The escape sequence for {#mode}, without writing it.
317
+ #
318
+ # space.mode_code :bold #=> "\e[1m"
319
+ #
320
+ # @param name [Symbol] one of +MODES+
321
+ # @return [String, nil] nil for unknown attributes
322
+
215
323
  def mode_code name; if id = MODES.index(name.to_s.to_sym) then "\e[#{ id }m" end end
216
324
 
325
+ ##
217
326
  # Build the ANSI code for +color+ (a Symbol name, greyN, Integer palette id,
218
327
  # or [r,g,b] / [fg,bg] Array), optionally as a background; when false the
219
328
  # color is emitted as a foreground.
329
+ #
330
+ # space.color_code :red #=> "\e[31m"
331
+ # space.color_code :red, true #=> "\e[41m" (background)
332
+ # space.color_code :grey50 #=> "\e[38;5;243m"
333
+ # space.color_code [10, 200, 30] #=> "\e[38;2;10;200;30m"
334
+ # space.color_code [:cyan, :black] #=> "\e[36m\e[40m" (fg, bg pair)
335
+ #
336
+ # @param color [Symbol, Integer, Array] color name, palette id, RGB, or pair
337
+ # @param bg [Boolean] emit as background when true
338
+ # @return [String, nil] ANSI escape sequence (nil for unknown names)
339
+
220
340
  def color_code color, bg=false
221
341
  color = color.to_sym if color.is_a? String
222
342
  case color
@@ -287,43 +407,102 @@ module Typr
287
407
  }.join
288
408
  end
289
409
 
410
+ ##
290
411
  # Read the current background/foreground color pair.
412
+ #
413
+ # space.get_foreground #=> :white
414
+ # space.get_background #=> :black
415
+ #
416
+ # @return [Object] the active color value
417
+
291
418
  def get_background; $color[1] end
419
+
420
+ ##
421
+ # The currently applied foreground color.
422
+ #
423
+ # space.get_foreground #=> :white
424
+ #
425
+ # @return [Object] the active color value
426
+
292
427
  def get_foreground; $color[0] end
293
428
 
429
+ ##
294
430
  # Set and apply the foreground/background color; +color+ sets both from a
295
431
  # [fg, bg] pair, a single color, or the module default.
432
+ #
433
+ # space.foreground :red # print in red
434
+ # space.background :blue # paint the background blue
435
+ # space.color [:cyan, :black] # set both
436
+ # space.color # restore the default pair
437
+ #
438
+ # @param c [Symbol, Integer, Array, nil] color or [fg, bg] pair
439
+ # @return [void]
440
+
296
441
  def foreground c=$default[0]; $color[0]=c; draw color_code(c) end
442
+
443
+ ##
444
+ # Paint the background with +c+ (see {#foreground}).
445
+ #
446
+ # space.background :blue
447
+ #
448
+ # @param c [Symbol, Integer, Array, nil] background color
449
+ # @return [void]
450
+
297
451
  def background c=$default[1]; $color[1]=c; draw color_code(c,true) end
452
+
453
+ ##
454
+ # Set both foreground and background: a [fg, bg] pair, a single color
455
+ # applied to both, or no argument to restore the module default pair.
456
+ #
457
+ # space.color [:cyan, :black]
458
+ # space.color # default pair back
459
+ #
460
+ # @param c [Symbol, Integer, Array, nil] color or [fg, bg] pair
461
+ # @return [void]
462
+
298
463
  def color c=$default
299
464
  c = [c] unless c.is_a? Array and c.count == 2
300
465
  foreground c[0] if c[0]
301
466
  background c[1] if c[1]
302
467
  end
303
468
 
469
+ ##
304
470
  # Write raw +str+ to stdout.
471
+ #
472
+ # space.draw "hi" # prints hi at the cursor
473
+ #
474
+ # @param str [String] escape sequences and text to emit
475
+ # @return [nil]
476
+
305
477
  def draw str; $>.print str end
306
478
 
479
+ ##
307
480
  # Clear the whole screen (:screen) or just the current line (:line).
481
+ #
482
+ # Typr.clear # clear the screen
483
+ # Typr.clear :line # clear the current line
484
+ #
485
+ # @param mode [:screen, :line]
486
+ # @return [nil]
487
+
308
488
  def self.clear mode=:screen
309
489
  $>.print( { screen: CLEAR_SCREEN, line: ERASE_LINE }[mode] )
310
490
  end
311
491
 
312
- ## Reads a single keypress in raw mode.
492
+ ##
493
+ # Reads a single keypress in raw mode.
313
494
  #
314
495
  # Returns the key sequence as a String (e.g. "a" or "\e[A" for up-arrow),
315
496
  # or a Typr::Mouse for a mouse report, or nil when stdin is not a tty or
316
- # input is unavailable.
497
+ # input is unavailable. Keys arriving inside an overtype quiet window
498
+ # ({#suppress_keys}) are discarded first.
499
+ #
500
+ # key = Typr.read_key #=> "s" | "\e[A" | #<Typr::Mouse> | nil
501
+ #
502
+ # @return [String, Typr::Mouse, nil]
317
503
 
318
504
  def self.read_key
319
- if quiet_window?
320
- loop do
321
- remaining = @suppress_until - Process.clock_gettime(Process::CLOCK_MONOTONIC)
322
- break if remaining <= 0
323
- break unless IO.select([INPUT], nil, nil, remaining)
324
- begin INPUT.read_nonblock(1024) rescue break end
325
- end
326
- end
505
+ drain_quiet
327
506
  read = ->(tty) do
328
507
  str = tty.sysread 6
329
508
  if str.start_with?("\e[M") and str.size < 6
@@ -343,9 +522,16 @@ module Typr
343
522
  decode_mouse(raw) || raw
344
523
  end
345
524
 
346
- ## Decode a raw input string into a {Typr::Mouse} event, or nil when +raw+
525
+ ##
526
+ # Decode a raw input string into a {Typr::Mouse} event, or nil when +raw+
347
527
  # is not a mouse report. Handles both SGR (CSI < b ; x ; y M/m) and the
348
528
  # legacy X10 (CSI M + three bytes) encodings.
529
+ #
530
+ # Typr.decode_mouse "\e[<0;10;5M" #=> #<struct button=0 x=10 y=5 action=:press modifiers=0>
531
+ # Typr.decode_mouse "abc" #=> nil
532
+ #
533
+ # @param raw [String] raw bytes read from the terminal
534
+ # @return [Typr::Mouse, nil]
349
535
 
350
536
  def self.decode_mouse raw
351
537
  if (m = raw.match(/\A\e\[<(\d+);(\d+);(\d+)([Mm])\z/))
@@ -391,23 +577,55 @@ module Typr
391
577
  # subprocess handoff via +flush_overtype+.
392
578
  OVERTYPE_QUIET = 0.5
393
579
 
580
+ ##
581
+ # Arm the overtype quiet window: every key arriving in the next
582
+ # +timeout+ seconds is discarded by {Typr.read_key}, so a double-tapped
583
+ # Enter or trailing characters after an auto-confirmed pick cannot
584
+ # re-trigger anything. Called by +Grid#pick+ on auto-return.
585
+ #
586
+ # Typr.suppress_keys # 0.5s window (OVERTYPE_QUIET)
587
+ # Typr.suppress_keys 1.0 # custom window in seconds
588
+ #
589
+ # @param timeout [Numeric] window length in seconds
590
+ # @return [Numeric] the monotonic deadline
591
+
394
592
  def self.suppress_keys timeout=OVERTYPE_QUIET
593
+ @suppress_quiet = timeout
395
594
  @suppress_until = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
396
595
  end
397
596
 
398
- def self.quiet_window?
399
- @suppress_until and $stdin.tty? and
597
+ ##
598
+ # Wait out an armed overtype window: switch to raw mode (canonical-
599
+ # buffered keystrokes are invisible to select) and discard arrivals
600
+ # until a full window passes with none — each arrival restarts it.
601
+ # No-op unless armed by {Typr.suppress_keys}. Best-effort: never raises.
602
+ #
603
+ # @return [nil]
604
+
605
+ def self.drain_quiet
606
+ return unless @suppress_until and $stdin.tty? and
400
607
  @suppress_until > Process.clock_gettime(Process::CLOCK_MONOTONIC)
608
+ INPUT.raw do
609
+ loop do
610
+ wait = @suppress_until - Process.clock_gettime(Process::CLOCK_MONOTONIC)
611
+ break if wait <= 0
612
+ break unless IO.select([INPUT], nil, nil, wait)
613
+ begin INPUT.read_nonblock(1024)
614
+ rescue IO::WaitReadable then break end
615
+ @suppress_until = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @suppress_quiet
616
+ end
617
+ end
618
+ nil
619
+ rescue StandardError
620
+ nil
401
621
  end
402
622
 
403
- def self.flush_overtype
404
- drain_input if quiet_window?
405
- end
623
+ private_class_method :drain_quiet
406
624
 
407
625
  ## Interactive line editor used for string prompts (search, %str, ...).
408
626
  #
409
627
  # Renders +prompt+ and the query at (left, top) and edits it with
410
- # arrow keys, ctrl-arrow word jumps, home/end, delete and backspace.
628
+ # arrow keys, alt-arrow word jumps, home/end, delete and backspace.
411
629
  # Returns the query on enter, nil on escape. A block may inspect each
412
630
  # keypress after it is applied; a non-nil return short-circuits the
413
631
  # editor and becomes its return value. When stdin is not a tty, the
@@ -422,10 +640,14 @@ module Typr
422
640
  # query as it is rendered; matched regions restore the prompt's trailing
423
641
  # color code.
424
642
  #
643
+ # +history+ is an Array of previous queries recalled with Up/Down;
644
+ # +redraw+ repaints the caller's screen after a completion pick.
645
+ #
425
646
  # Typr.read_line "/" do |key, query, cursor|
426
647
  # filter query
427
648
  # nil
428
649
  # end
650
+ # Typr.read_line "> ", initial: "ls ", history: @recent #=> "ls -l"
429
651
 
430
652
  def self.read_line prompt='', left: 0, top: 0, initial: '', completions: nil, redraw: nil, regex: nil, history: nil, &block
431
653
  return $stdin.gets&.chomp unless $stdin.tty?
@@ -434,13 +656,16 @@ module Typr
434
656
  $>.print CURSOR_NORMAL
435
657
  begin
436
658
  loop do
659
+ $>.print CURSOR_INVISIBLE
437
660
  $>.print "\e[%i;%if" % [ top + 1, left + 1 ]
438
- $>.print ERASE_LINE
439
661
  before = text_width( prompt + query[0...cursor] )
440
662
  offset = [ left + before - Typr.width, 0 ].max
441
663
  display = prompt + paint( query, regex, prompt[/\e\[[0-9;]*m\z/].to_s )
442
- $>.print slice_width( display, offset, Typr.width - left + 1 )
664
+ line = slice_width( display, offset, Typr.width - left + 1 )
665
+ $>.print line
666
+ $>.print ' ' * (Typr.width - left + 1 - text_width(line))
443
667
  $>.print "\e[%i;%if" % [ top + 1, left + 1 + before - offset ]
668
+ $>.print CURSOR_NORMAL
444
669
  key = read_key
445
670
  return nil if key.nil? or key == KEY_ESCAPE
446
671
  return query if key == KEY_RETURN or key == "\n"
@@ -521,14 +746,30 @@ module Typr
521
746
  end
522
747
  end
523
748
 
749
+ ##
524
750
  # Display width of +str+ after stripping ANSI escapes.
751
+ #
752
+ # Typr.text_width "\e[31mabc\e[0m" #=> 3
753
+ #
754
+ # @param str [String] text possibly containing ANSI escapes
755
+ # @return [Integer] width in cells
756
+
525
757
  def self.text_width str
526
758
  Unicode::DisplayWidth.of( str.gsub(/\x1b\[[^m]+m/, '') )
527
759
  end
528
760
 
761
+ ##
529
762
  # Visible substring of +str+ starting at display-width +offset+, at most
530
763
  # +width+ cells wide. ANSI escapes are zero-width and carried through so
531
764
  # color state applies inside the window.
765
+ #
766
+ # Typr.slice_width "hello world", 6, 5 #=> "world"
767
+ #
768
+ # @param str [String] text possibly containing ANSI escapes
769
+ # @param offset [Integer] display-width start of the window
770
+ # @param width [Integer] window size in cells
771
+ # @return [String]
772
+
532
773
  def self.slice_width str, offset, width
533
774
  vis = 0
534
775
  slice = +""
@@ -548,22 +789,46 @@ module Typr
548
789
  slice
549
790
  end
550
791
 
792
+ ##
551
793
  # Move the cursor back to the start of the previous word in +str+.
794
+ #
795
+ # Typr.word_prev "foo bar baz", 8 #=> 4
796
+ #
797
+ # @param str [String] the text being edited
798
+ # @param cursor [Integer] current cursor position
799
+ # @return [Integer] new cursor position
800
+
552
801
  def self.word_prev str, cursor
553
802
  cursor -= 1 while cursor > 0 and str[cursor - 1] == ' '
554
803
  cursor -= 1 while cursor > 0 and str[cursor - 1] != ' '
555
804
  cursor
556
805
  end
557
806
 
807
+ ##
558
808
  # Move the cursor forward to the start of the next word in +str+.
809
+ #
810
+ # Typr.word_next "foo bar baz", 4 #=> 7
811
+ #
812
+ # @param str [String] the text being edited
813
+ # @param cursor [Integer] current cursor position
814
+ # @return [Integer] new cursor position
815
+
559
816
  def self.word_next str, cursor
560
817
  cursor += 1 while cursor < str.length and str[cursor] == ' '
561
818
  cursor += 1 while cursor < str.length and str[cursor] != ' '
562
819
  cursor
563
820
  end
564
821
 
822
+ private_class_method :slice_width, :word_prev, :word_next
823
+
824
+ ##
565
825
  # Query the terminal for the current [row, column] via DSR; [0, 0] when
566
- # stdin is not a tty.
826
+ # stdin is not a tty. Blocks until the terminal answers \e[6n.
827
+ #
828
+ # Typr.position #=> [12, 40] (row, column)
829
+ #
830
+ # @return [Array<Integer>] [row, column]
831
+
567
832
  def self.position
568
833
  return [0, 0] unless $stdin.tty?
569
834
  result = ''
@@ -577,22 +842,78 @@ module Typr
577
842
  result[/[\d;]+/].split(?;).map &:to_i
578
843
  end
579
844
 
580
- # Terminal [rows, cols] (defaults to 24x80 when undetectable); width/height
581
- # return the usable last column/row.
845
+ ##
846
+ # Terminal [rows, cols] (defaults to 24x80 when undetectable);
847
+ # +width+/+height+ return the usable last column/row.
848
+ #
849
+ # Typr.size #=> [24, 80]
850
+ # Typr.width #=> 79
851
+ # Typr.height #=> 23
852
+ #
853
+ # @return [Array<Integer>] [rows, cols]
854
+
582
855
  def self.size; IO.console&.winsize || [24, 80] end
856
+
857
+ ##
858
+ # Usable last column (terminal width minus one).
859
+ #
860
+ # Typr.width #=> 79
861
+ #
862
+ # @return [Integer]
863
+
583
864
  def self.width; size.last-1 end
865
+
866
+ ##
867
+ # Usable last row (terminal height minus one).
868
+ #
869
+ # Typr.height #=> 23
870
+ #
871
+ # @return [Integer]
872
+
584
873
  def self.height; size.first - 1 end
585
874
 
586
- # Current cursor row/column (see position).
875
+ ##
876
+ # Current cursor row/column (see {position}).
877
+ #
878
+ # Typr.row #=> 12
879
+ # Typr.column #=> 40
880
+ #
881
+ # @return [Integer]
882
+
587
883
  def self.row; position.first end
884
+
885
+ ##
886
+ # Current cursor column (see {position}).
887
+ #
888
+ # Typr.column #=> 40
889
+ #
890
+ # @return [Integer]
891
+
588
892
  def self.column; position.last end
589
893
 
894
+ ##
590
895
  # Call +block+ whenever the terminal is resized (SIGWINCH).
896
+ #
897
+ # Typr.on_resize { grid.show }
898
+ #
899
+ # @param block [Proc] handler invoked on resize
900
+ # @return [Integer] the signal handler (trap's return)
901
+
591
902
  def self.on_resize &block
592
903
  trap(:WINCH, &block)
593
904
  end
594
- # Enter interactive mode: seed the default colors, hide the cursor, enable
595
- # key-mode (application) escapes and mouse reporting when stdin is a tty.
905
+
906
+ ##
907
+ # Enter interactive mode: seed the default colors, hide the cursor,
908
+ # enable key-mode (application) escapes and mouse reporting when stdin
909
+ # is a tty.
910
+ #
911
+ # Typr.init # default [:white, :black]
912
+ # Typr.init [:cyan, :grey10] # custom default pair
913
+ #
914
+ # @param default [Array<Symbol>] initial [fg, bg] pair
915
+ # @return [void]
916
+
596
917
  def self.init default=[ :white, :black ]
597
918
  $default = default.dup
598
919
  $color = $default.dup
@@ -602,8 +923,13 @@ module Typr
602
923
  print MOUSE_ON
603
924
  end
604
925
  end
605
- # Restore the terminal: show the cursor, reset keypad/colors, disable mouse
606
- # reporting and clear.
926
+ ##
927
+ # Restore the terminal: show the cursor, reset keypad/colors, disable
928
+ # mouse reporting, and clear.
929
+ #
930
+ # Typr.exit # => nil
931
+ #
932
+ # @return [nil]
607
933
  def self.exit;
608
934
  extend self
609
935
  if $stdin.tty?
@@ -619,14 +945,15 @@ if __FILE__ == $0
619
945
  begin
620
946
  include Typr
621
947
  Typr.init
622
- colors = COLORS + (1..9).map{|i| "grey#{i*10}" }
948
+ colors = COLORS + COLOR_MAP.keys.map(&:to_s) + (0..10).map{|i| "grey#{i*10}" }
623
949
  chars = (33..126).map(&:chr)
624
950
  slice = chars.count / colors.count + 1
625
951
  characters = chars.each_slice( slice ).map(&:join)
626
952
  key,fg,bg = "",150,0
627
953
  Typr.clear
954
+ rows = [colors.count, Typr.height - 4].min
628
955
  loop do
629
- colors.each_with_index{ |color,y|
956
+ colors.first(rows).each_with_index{ |color,y|
630
957
  characters.each_with_index{ |chars,x|
631
958
  foreground color.to_sym;
632
959
  background colors[x].to_sym;
@@ -634,20 +961,21 @@ begin
634
961
  draw chars
635
962
  }
636
963
  }
964
+ move 0, rows
637
965
  (0...256).each{ |i| background i; draw ' ' }
638
966
 
639
967
  move 0, Typr.height
640
968
  color [:white, :black]
641
- p "KEY: %s / %s" % [key, key]
969
+ draw "KEY: %s / %s" % [key, key]
642
970
  move 15, Typr.height
643
971
  color [fg,bg]
644
972
  print " %s / %s " % [fg,bg]
645
973
  key = Typr.read_key
646
974
  case key
647
- when KEY_UP; fg+=1
648
- when KEY_DOWN; fg-=1
649
- when KEY_LEFT; bg-=1
650
- when KEY_RIGHT; bg+=1
975
+ when KEY_UP; fg = [fg + 1, 255].min
976
+ when KEY_DOWN; fg = [fg - 1, 0].max
977
+ when KEY_LEFT; bg = [bg - 1, 0].max
978
+ when KEY_RIGHT; bg = [bg + 1, 255].max
651
979
  when KEY_ESCAPE; exit
652
980
  end
653
981
  end