Class: String

Inherits:
Object
  • Object
show all
Defined in:
lib/string_extensions.rb

Overview

string_extensions.rb

Instance Method Summary collapse

Instance Method Details

#bObject

bold, italic, underline, blink, reverse



48
# File 'lib/string_extensions.rb', line 48

def b; color(self, "\e[1m",   "\e[22m"); end

#bg(color) ⇒ Object

256-color or truecolor RGB backgroundbreset only the bg (SGR 49)



16
17
18
19
20
21
22
23
24
# File 'lib/string_extensions.rb', line 16

def bg(color)
  sp, ep = if color.to_s =~ /\A[0-9A-Fa-f]{6}\z/
    r, g, b = color.scan(/../).map { |c| c.to_i(16) }
    ["\e[48;2;#{r};#{g};#{b}m", "\e[49m"]
  else
    ["\e[48;5;#{color}m", "\e[49m"]
  end
  color(self, sp, ep)
end

#c(code) ⇒ Object

Combined code: “foo”.c(“FF0000,00FF00,bui”) — 6-hex or decimal for fg, then for bg, then letters b/i/u/l/r



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/string_extensions.rb', line 63

def c(code)
  parts = code.split(',')
  seq   = []

  fg = parts.shift
  if fg =~ /\A[0-9A-Fa-f]{6}\z/
    r,g,b = fg.scan(/../).map{|c|c.to_i(16)}
    seq << "38;2;#{r};#{g};#{b}"
  elsif fg =~ /\A\d+\z/
    seq << "38;5;#{fg}"
  end

  if parts.any?
    bg = parts.shift
    if bg =~ /\A[0-9A-Fa-f]{6}\z/
      r,g,b = bg.scan(/../).map{|c|c.to_i(16)}
      seq << "48;2;#{r};#{g};#{b}"
    elsif bg =~ /\A\d+\z/
      seq << "48;5;#{bg}"
    end
  end

  seq << '1' if code.include?('b')
  seq << '3' if code.include?('i')
  seq << '4' if code.include?('u')
  seq << '5' if code.include?('l')
  seq << '7' if code.include?('r')

  "\e[#{seq.join(';')}m#{self}\e[0m"
end

#clean_ansiObject

Remove stray leading/trailing reset if the string has no other styling



100
101
102
# File 'lib/string_extensions.rb', line 100

def clean_ansi
  gsub(/\A(?:\e\[0m)+/, '').gsub(/\e\[0m\z/, '')
end

#color(text, sp, ep = "\e[0m") ⇒ Object

Internal helper - wraps text in start/end sequences, and re-applies start on every newline.



56
57
58
59
# File 'lib/string_extensions.rb', line 56

def color(text, sp, ep = "\e[0m")
  t = text.gsub("\n", "#{ep}\n#{sp}")
  "#{sp}#{t}#{ep}"
end

#fb(fg_color, bg_color) ⇒ Object

Both fg and bg in one go



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/string_extensions.rb', line 27

def fb(fg_color, bg_color)
  parts = []
  if fg_color.to_s =~ /\A[0-9A-Fa-f]{6}\z/
    r, g, b = fg_color.scan(/../).map { |c| c.to_i(16) }
    parts << "38;2;#{r};#{g};#{b}"
  else
    parts << "38;5;#{fg_color}"
  end

  if bg_color.to_s =~ /\A[0-9A-Fa-f]{6}\z/
    r, g, b = bg_color.scan(/../).map { |c| c.to_i(16) }
    parts << "48;2;#{r};#{g};#{b}"
  else
    parts << "48;5;#{bg_color}"
  end

  sp = "\e[#{parts.join(';')}m"
  color(self, sp, "\e[39;49m")
end

#fg(color) ⇒ Object

256-color or truecolor RGB foregroundbreset only the fg (SGR 39)



5
6
7
8
9
10
11
12
13
# File 'lib/string_extensions.rb', line 5

def fg(color)
  sp, ep = if color.to_s =~ /\A[0-9A-Fa-f]{6}\z/
    r, g, b = color.scan(/../).map { |c| c.to_i(16) }
    ["\e[38;2;#{r};#{g};#{b}m", "\e[39m"]
  else
    ["\e[38;5;#{color}m", "\e[39m"]
  end
  color(self, sp, ep)
end

#has_ansi?Boolean

Check if string contains ANSI codes

Returns:

  • (Boolean)


192
193
194
# File 'lib/string_extensions.rb', line 192

def has_ansi?
  !!(self =~ /\e\[[0-9;]*m/)
end

#iObject



49
# File 'lib/string_extensions.rb', line 49

def i; color(self, "\e[3m",   "\e[23m"); end

#inject(insertion, pos) ⇒ Object

Insert insertion at visible position pos (negative → end), respecting and re-inserting existing ANSI sequences.



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/string_extensions.rb', line 126

def inject(insertion, pos)
  pure_txt      = pure
  visible_len   = pure_txt.length
  pos           = visible_len if pos < 0

  count, out, i, injected = 0, '', 0, false

  while i < length
    if self[i] == "\e" && (m = self[i..-1].match(/\A(\e\[\d+(?:;\d+)*m)/))
      out << m[1]
      i   += m[1].length
    else
      if count == pos && !injected
        out << insertion
        injected = true
      end
      out << self[i]
      count  += 1
      i      += 1
    end
  end

  unless injected
    if out =~ /(\e\[\d+(?:;\d+)*m)\z/
      trailing = $1
      out      = out[0...-trailing.length] + insertion + trailing
    else
      out << insertion
    end
  end

  out
end

#lObject



51
# File 'lib/string_extensions.rb', line 51

def l; color(self, "\e[5m",   "\e[25m"); end

#pureObject

Strip all ANSI SGR sequences



95
96
97
# File 'lib/string_extensions.rb', line 95

def pure
  gsub(/\e\[\d+(?:;\d+)*m/, '')
end

#rObject



52
# File 'lib/string_extensions.rb', line 52

def r; color(self, "\e[7m",   "\e[27m"); end

#safe_bg(color) ⇒ Object



206
207
208
# File 'lib/string_extensions.rb', line 206

def safe_bg(color)
  has_ansi? ? self : bg(color)
end

#safe_fg(color) ⇒ Object

Apply color only if not already colored



202
203
204
# File 'lib/string_extensions.rb', line 202

def safe_fg(color)
  has_ansi? ? self : fg(color)
end

#safe_gsub(pattern, replacement = nil, &block) ⇒ Object

Safely apply regex replacements without corrupting ANSI sequences This method temporarily removes ANSI codes, applies the regex, then restores them



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/string_extensions.rb', line 162

def safe_gsub(pattern, replacement = nil, &block)
  # Store all ANSI sequences and replace with placeholders
  ansi_sequences = []
  placeholder_text = self.gsub(/\e\[[0-9;]*m/) do |match|
    ansi_sequences << match
    "⟨ANSI#{ansi_sequences.length - 1}"
  end
  
  # Apply the regex to the placeholder text
  result = if block_given?
    placeholder_text.gsub(pattern, &block)
  else
    placeholder_text.gsub(pattern, replacement)
  end
  
  # Restore ANSI sequences
  ansi_sequences.each_with_index do |ansi, index|
    result.gsub!("⟨ANSI#{index}", ansi)
  end
  
  result
end

#safe_gsub!(pattern, replacement = nil, &block) ⇒ Object

Safe version of gsub! that modifies in place



186
187
188
189
# File 'lib/string_extensions.rb', line 186

def safe_gsub!(pattern, replacement = nil, &block)
  result = safe_gsub(pattern, replacement, &block)
  self.replace(result)
end

#shorten(n) ⇒ Object

Truncate the visible length to n, but preserve embedded ANSI



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/string_extensions.rb', line 105

def shorten(n)
  count = 0
  out   = ''
  i     = 0

  while i < length && count < n
    if self[i] == "\e" && (m = self[i..-1].match(/\A(\e\[\d+(?:;\d+)*m)/))
      out << m[1]
      i   += m[1].length
    else
      out << self[i]
      i   += 1
      count += 1
    end
  end

  out
end

#uObject



50
# File 'lib/string_extensions.rb', line 50

def u; color(self, "\e[4m",   "\e[24m"); end

#visible_lengthObject

Get the visible length (without ANSI codes)



197
198
199
# File 'lib/string_extensions.rb', line 197

def visible_length
  pure.length
end