typr 1.8.0 → 1.9.2

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