Class: String

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

Overview

string_extensions.rb

Constant Summary collapse

ANSI_SGR_RE =

Strip all ANSI SGR sequences

/\e\[\d+(?:;\d+)*m/.freeze

Instance Method Summary collapse

Instance Method Details

#bdObject

bold, italic, underline, blink, reverse



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

def bd; 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



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

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)


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

def has_ansi?
  !!(self =~ Rcurses::ANSI_RE)
end

#inject(insertion, pos) ⇒ Object

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



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
159
# File 'lib/string_extensions.rb', line 127

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

#itObject



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

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

#lObject



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

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

#pureObject



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

def pure
  gsub(ANSI_SGR_RE, '')
end

#rvObject



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

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

#safe_bg(color) ⇒ Object



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

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

#safe_fg(color) ⇒ Object

Apply color only if not already colored



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

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



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

def safe_gsub(pattern, replacement = nil, &block)
  # Store all ANSI sequences and replace with placeholders
  ansi_sequences = []
  placeholder_text = self.gsub(Rcurses::ANSI_RE) 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



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

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



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

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

#ulObject



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

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

#visible_lengthObject

Get the visible length (without ANSI codes)



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

def visible_length
  pure.length
end