Module: Typr
- Included in:
- Space
- Defined in:
- lib/terminal.rb,
lib/grid.rb,
lib/line.rb,
lib/text.rb,
lib/space.rb,
lib/stack.rb,
lib/browser.rb
Overview
Terminal control: raw key input, line editing, cursor movement, ANSI colors
and terminal geometry. Widgets get the instance helpers via include Typr;
class-level helpers (Typr.width, Typr.read_key, ...) drive the whole screen.
Defined Under Namespace
Classes: Browser, Grid, Line, Space, Stack, Text
Constant Summary collapse
- MIMETYPES =
eval File.read( __dir__ + "/../share/mimetypes" )
- INPUT =
Raw /dev/tty used for key input; falls back to $stdin when unavailable.
(IO.new IO.sysopen("/dev/tty", "r")) rescue $stdin
- ERASE_LINE =
Common key aliases and the line-erase escape sequence.
"\e[K"- KEY_ESCAPE =
"\e"- KEY_RETURN =
CARRIAGE_RETURN- KEY_TAB =
"\t"- KEY_PAGEDOWN =
KEY_NPAGE- KEY_PAGEUP =
KEY_PPAGE- MODES =
Text attributes (reset, bold, italic, ...) and named 8/256-color tables.
%i[ reset bold italic underline slow fast invert ]
- COLORS =
%i[ black red green yellow blue magenta cyan white ]
- COLOR_MAP =
{ brown: 130, orange: 208, lime: 118, pink: 218, maroon: 52, navy: 18, teal: 30, olive: 100, coral: 203, tan: 180, dark_red: 88, dark_green: 22, dark_yellow: 58, dark_blue: 18, dark_magenta: 89, dark_cyan: 30 }
Class Method Summary collapse
-
.clear(mode = :screen) ⇒ Object
Clear the whole screen (:screen) or just the current line (:line).
- .column ⇒ Object
-
.exit ⇒ Object
Restore the terminal: show the cursor, reset colors and clear the screen.
- .height ⇒ Object
-
.init(default = [ :white, :black ]) ⇒ Object
Enter interactive mode: seed the default colors, hide the cursor and enable key-mode (application) escapes when stdin is a tty.
-
.on_resize(&block) ⇒ Object
Call
blockwhenever the terminal is resized (SIGWINCH). -
.position ⇒ Object
Query the terminal for the current [row, column] via DSR; [0, 0] when stdin is not a tty.
-
.read_key ⇒ Object
Reads a single keypress in raw mode.
-
.read_line(prompt = '', left: 0, top: 0, initial: '', &block) ⇒ Object
Interactive line editor used for string prompts (search, %str, ...).
-
.row ⇒ Object
Current cursor row/column (see position).
-
.size ⇒ Object
Terminal [rows, cols] (defaults to 24x80 when undetectable); width/height return the usable last column/row.
-
.text_width(str) ⇒ Object
Display width of
strafter stripping ANSI escapes. - .width ⇒ Object
-
.word_next(str, cursor) ⇒ Object
Move the cursor forward to the start of the next word in
str. -
.word_prev(str, cursor) ⇒ Object
Move the cursor back to the start of the previous word in
str.
Instance Method Summary collapse
- #background(c = ) ⇒ Object
-
#clip(str, max, side) ⇒ Object
Truncate
strto at mostmaxcells, keeping ANSI color tokens intact and clipping fromside(:left/:right). -
#coerce_type(value) ⇒ Object
Coerce a string into Integer, Float, or Boolean when it matches those forms (yes/no, true/false); otherwise return it unchanged.
- #color(c = $default) ⇒ Object
-
#color_code(color, bg = false) ⇒ Object
Build the ANSI code for
color(a Symbol name, greyN, Integer palette id, or [r,g,b] / [fg,bg] Array), optionally as a background; when false the color is emitted as a foreground. -
#draw(str) ⇒ Object
Write raw
strto stdout. -
#fade(str, side = :left, num = 3) ⇒ Object
Blend the first (or last)
numcharacters ofstrinto greys to hint at clipping;sideselects which end fades. -
#foreground(c = ) ⇒ Object
Set and apply the foreground/background color;
colorsets both from a [fg, bg] pair, a single color, or the module default. -
#get_background ⇒ Object
Read the current background/foreground color pair.
- #get_foreground ⇒ Object
-
#mode(name) ⇒ Object
Apply a text attribute (+mode+); mode_code returns the escape sequence without writing it.
- #mode_code(name) ⇒ Object
-
#move(x = 0, y = 0) ⇒ Object
Move the cursor to column
x, rowy(0-based); move_code returns the escape sequence without writing it. - #move_code(x = 0, y = 0) ⇒ Object
-
#prepare(str, max, align = :left, side = :right, fade = false) ⇒ Object
Pad or clip
strto exactlymaxcells, honoringalign(:left/:right), trimming fromside, with an optional trailingfade. -
#printables(str) ⇒ Object
Strip ANSI escape sequences from
str. -
#real_size(str) ⇒ Object
Display width of
strafter stripping ANSI escapes.
Class Method Details
.clear(mode = :screen) ⇒ Object
Clear the whole screen (:screen) or just the current line (:line).
192 193 194 |
# File 'lib/terminal.rb', line 192 def self.clear mode=:screen $>.print( { screen: CLEAR_SCREEN, line: ERASE_LINE }[mode] ) end |
.column ⇒ Object
300 |
# File 'lib/terminal.rb', line 300 def self.column; position.last end |
.exit ⇒ Object
Restore the terminal: show the cursor, reset colors and clear the screen.
317 318 319 320 321 322 323 |
# File 'lib/terminal.rb', line 317 def self.exit; extend self if $stdin.tty? $>.print CURSOR_NORMAL; $>.print ORIG_COLORS; color; clear end end |
.height ⇒ Object
296 |
# File 'lib/terminal.rb', line 296 def self.height; size.first - 1 end |
.init(default = [ :white, :black ]) ⇒ Object
Enter interactive mode: seed the default colors, hide the cursor and enable key-mode (application) escapes when stdin is a tty.
308 309 310 311 312 313 314 315 |
# File 'lib/terminal.rb', line 308 def self.init default=[ :white, :black ] $default = default.dup $color = $default.dup if $stdin.tty? print CURSOR_INVISIBLE system "tput smkx" end end |
.on_resize(&block) ⇒ Object
Call block whenever the terminal is resized (SIGWINCH).
303 304 305 |
# File 'lib/terminal.rb', line 303 def self.on_resize &block trap(:WINCH, &block) end |
.position ⇒ Object
Query the terminal for the current [row, column] via DSR; [0, 0] when stdin is not a tty.
279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/terminal.rb', line 279 def self.position return [0, 0] unless $stdin.tty? result = '' $stdin.raw do |stdin| $stdout << USER7 $stdout.flush until (char = stdin.getc) == 'R' result << char if char end end result[/[\d;]+/].split(?;).map &:to_i end |
.read_key ⇒ Object
Reads a single keypress in raw mode.
Returns the key sequence as a String (e.g. "a" or "\e[A" for up-arrow), or nil when stdin is not a tty or input is unavailable.
201 202 203 204 |
# File 'lib/terminal.rb', line 201 def self.read_key return INPUT.raw{ |tty| tty.sysread 6 } rescue nil unless INPUT.tty? INPUT.raw{ |tty| tty.sysread 6 } rescue return end |
.read_line(prompt = '', left: 0, top: 0, initial: '', &block) ⇒ Object
Interactive line editor used for string prompts (search, %str, ...).
Renders prompt and the query at (left, top) and edits it with
arrow keys, ctrl-arrow word jumps, home/end, delete and backspace.
Returns the query on enter, nil on escape. A block may inspect each
keypress after it is applied; a non-nil return short-circuits the
editor and becomes its return value. When stdin is not a tty, the
query is read from a single line of piped input instead.
Typr.read_line "/" do |key, query, cursor|
filter query
nil
end
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 |
# File 'lib/terminal.rb', line 220 def self.read_line prompt='', left: 0, top: 0, initial: '', &block return $stdin.gets&.chomp unless $stdin.tty? query, cursor = initial.dup, initial.length loop do $>.print "\e[%i;%if" % [ top + 1, left + 1 ] $>.print ERASE_LINE $>.print prompt + query $>.print "\e[%i;%if" % [ top + 1, left + 1 + text_width( prompt + query[0...cursor] ) ] key = read_key return nil if key.nil? or key == KEY_ESCAPE return query if key == KEY_RETURN or key == "\n" case key when KEY_LEFT, "\e[D"; cursor -= 1 if cursor > 0 when KEY_RIGHT, "\e[C"; cursor += 1 if cursor < query.length when "\e[1;5D", "\eOd"; cursor = word_prev query, cursor when "\e[1;5C", "\eOc"; cursor = word_next query, cursor when KEY_HOME; cursor = 0 when KEY_END; cursor = query.length when KEY_DC, "\e[3~"; query.slice!(cursor, 1) if cursor < query.length when KEY_BACKSPACE, "\b" if cursor > 0 query.slice!(cursor - 1, 1) cursor -= 1 end else if key.is_a?(String) and key.each_char.all?{ |char| char.ord.between?(32, 126) } query.insert(cursor, key) cursor += key.length end end cursor = 0 if cursor < 0 cursor = query.length if cursor > query.length result = block.call( key, query, cursor ) if block return result unless result.nil? end end |
.row ⇒ Object
Current cursor row/column (see position).
299 |
# File 'lib/terminal.rb', line 299 def self.row; position.first end |
.size ⇒ Object
Terminal [rows, cols] (defaults to 24x80 when undetectable); width/height return the usable last column/row.
294 |
# File 'lib/terminal.rb', line 294 def self.size; IO.console&.winsize || [24, 80] end |
.text_width(str) ⇒ Object
Display width of str after stripping ANSI escapes.
259 260 261 |
# File 'lib/terminal.rb', line 259 def self.text_width str Unicode::DisplayWidth.of( str.gsub(/\x1b\[[^m]+m/, '') ) end |
.width ⇒ Object
295 |
# File 'lib/terminal.rb', line 295 def self.width; size.last-1 end |
.word_next(str, cursor) ⇒ Object
Move the cursor forward to the start of the next word in str.
271 272 273 274 275 |
# File 'lib/terminal.rb', line 271 def self.word_next str, cursor cursor += 1 while cursor < str.length and str[cursor] == ' ' cursor += 1 while cursor < str.length and str[cursor] != ' ' cursor end |
.word_prev(str, cursor) ⇒ Object
Move the cursor back to the start of the previous word in str.
264 265 266 267 268 |
# File 'lib/terminal.rb', line 264 def self.word_prev str, cursor cursor -= 1 while cursor > 0 and str[cursor - 1] == ' ' cursor -= 1 while cursor > 0 and str[cursor - 1] != ' ' cursor end |
Instance Method Details
#background(c = ) ⇒ Object
181 |
# File 'lib/terminal.rb', line 181 def background c=$default[1]; $color[1]=c; draw color_code(c,true) end |
#clip(str, max, side) ⇒ Object
Truncate str to at most max cells, keeping ANSI color tokens intact
and clipping from side (:left/:right).
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
# File 'lib/terminal.rb', line 69 def clip str, max, side tokens = str.scan(/(\e\[[0-9;]*m)|([^\e]+)/).flatten.compact tokens.reverse! if side == :left kept, used = [], 0 tokens.each do |token| if token[0] == ?\e if side == :left kept << token unless kept.empty? break if used >= max else kept << token if used < max end next end break if used >= max size = Unicode::DisplayWidth.of token if used + size <= max kept << token used += size elsif used < max need = max - used chars = token.chars chars.reverse! if side == :left width = 0 out = "" chars.each do |char| width += Unicode::DisplayWidth.of char break if width > need out << char end out.reverse! if side == :left kept << out used = max end end kept.reverse!.join if side == :left kept.join end |
#coerce_type(value) ⇒ Object
Coerce a string into Integer, Float, or Boolean when it matches those forms (yes/no, true/false); otherwise return it unchanged.
129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/terminal.rb', line 129 def coerce_type value s = value.to_s return nil if s.empty? or /\A(?:nil|NULL)\z/i.match(s) case value when /^-?[\d]+$/ then s.to_i when /^-?\d*[\.\,]\d+$/ then s.to_f when /\byes\z/i then true when /\bno(ot)?\z/i then false when /true\z/i then true when /false\z/i then false else value end end |
#color(c = $default) ⇒ Object
182 183 184 185 186 |
# File 'lib/terminal.rb', line 182 def color c=$default c = [c] unless c.is_a? Array and c.count == 2 foreground c[0] if c[0] background c[1] if c[1] end |
#color_code(color, bg = false) ⇒ Object
Build the ANSI code for color (a Symbol name, greyN, Integer palette id,
or [r,g,b] / [fg,bg] Array), optionally as a background; when false the
color is emitted as a foreground.
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/terminal.rb', line 156 def color_code color, bg=false color = color.to_sym if color.is_a? String case color when Symbol; if id = COLORS.index(color); "\e[#{ id + (bg ? 40 : 30) }m" elsif color[/^gr[ae]y\d{,2}$/] "\e[%i;5;%im" % [bg ? 48 : 38, 232 + (color[/\d+/].to_f/100*23).to_i] elsif id = COLOR_MAP[color] "\e[%i;5;%im" % [bg ? 48 : 38, id] end when Integer; "\e[%i;5;%im" % [ bg ? 48 : 38, color] when Array; case color.count when 3; "\e[%i;2;%i;%i;%im" % [ bg ? 48 : 38, *color ] when 2; color_code( color[0] ) + color_code( color[1], true ) end end end |
#draw(str) ⇒ Object
Write raw str to stdout.
189 |
# File 'lib/terminal.rb', line 189 def draw str; $>.print str end |
#fade(str, side = :left, num = 3) ⇒ Object
Blend the first (or last) num characters of str into greys to hint
at clipping; side selects which end fades.
110 111 112 113 114 115 116 117 118 119 |
# File 'lib/terminal.rb', line 110 def fade str, side=:left, num=3 isleft = side == :left chars = str[ isleft ? 0..num : -num..-1 ] chars = chars.chars.map.with_index do |char,id| id = chars.size-id unless isleft color_code( "grey#{(80/chars.size)*id+10}".to_sym ) + char end.join return ( isleft ? chars + color_code($color[0]) + str[num+1..-1] : str[0..-num-1] + chars ) end |
#foreground(c = ) ⇒ Object
Set and apply the foreground/background color; color sets both from a
[fg, bg] pair, a single color, or the module default.
180 |
# File 'lib/terminal.rb', line 180 def foreground c=$default[0]; $color[0]=c; draw color_code(c) end |
#get_background ⇒ Object
Read the current background/foreground color pair.
175 |
# File 'lib/terminal.rb', line 175 def get_background; $color[1] end |
#get_foreground ⇒ Object
176 |
# File 'lib/terminal.rb', line 176 def get_foreground; $color[0] end |
#mode(name) ⇒ Object
Apply a text attribute (+mode+); mode_code returns the escape sequence without writing it.
150 |
# File 'lib/terminal.rb', line 150 def mode name; draw mode_code(name) end |
#mode_code(name) ⇒ Object
151 |
# File 'lib/terminal.rb', line 151 def mode_code name; if id = MODES.index(name.to_s) then "\e[#{ id }m" end end |
#move(x = 0, y = 0) ⇒ Object
Move the cursor to column x, row y (0-based); move_code returns the
escape sequence without writing it.
145 |
# File 'lib/terminal.rb', line 145 def move x=0,y=0; draw move_code( x, y ) end |
#move_code(x = 0, y = 0) ⇒ Object
146 |
# File 'lib/terminal.rb', line 146 def move_code x=0,y=0; "\e[%i;%if" % [ y+1, x+1 ] end |
#prepare(str, max, align = :left, side = :right, fade = false) ⇒ Object
Pad or clip str to exactly max cells, honoring align (:left/:right),
trimming from side, with an optional trailing fade.
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/terminal.rb', line 42 def prepare str, max, align=:left, side=:right, fade=false stop = width = real_size( str ) unless str.ascii_only? and width == str.size stop = width = 0 str.each_char do |c| if c == ?\e width -= str[stop..-1][/^\x1b\[[^m]+m|/].size-1 stop += 1 elsif (cwidth = (c.ascii_only? ? 1 : Unicode::DisplayWidth.of(c))) + width > max break else width += cwidth; stop += 1 end end end if ( space = ( max - width ) ) > 0 str = [ str[0..stop-1], " " * space ] str.reverse! if align == :right str.join else str = clip str, max, side str = fade str, side, fade if fade return str end end |
#printables(str) ⇒ Object
Strip ANSI escape sequences from str.
122 |
# File 'lib/terminal.rb', line 122 def printables str; str.gsub(/\x1b\[[^m]+m|/,'') end |
#real_size(str) ⇒ Object
Display width of str after stripping ANSI escapes.
125 |
# File 'lib/terminal.rb', line 125 def real_size str; Unicode::DisplayWidth.of( printables(str) ) end |