Module: Kward::ANSI

Defined in:
lib/kward/ansi.rb

Overview

ANSI color and terminal capability helpers.

Defined Under Namespace

Classes: MarkdownStream

Constant Summary collapse

ESCAPE_PATTERN =
/\e\[[0-9;?]*[ -\/]*[@-~]/.freeze
SGR_PATTERN =
/\e\[[0-9;:]*m/.freeze
OSC_PATTERN =
/\e\][^\a]*(?:\a|\e\\)/m.freeze
STRING_ESCAPE_PATTERN =
/\e[P_X^][\s\S]*?\e\\/m.freeze
STYLES =
{
  reset: 0,
  bold: 1,
  dim: 2,
  italic: 3,
  strikethrough: 9,
  red: 31,
  green: 32,
  yellow: 33,
  blue: 34,
  magenta: 35,
  cyan: 36,
  gray: 90,
  grey: 90,
  primary_green: "38;2;138;160;106",
  bright_accent_green: "38;2;155;255;0",
  augen: "38;2;155;255;0",
  dark_forest_green: "38;2;78;88;53",
  stone: "38;2;196;192;178",
  metal_dark: "38;2;42;42;42",
  background: "38;2;22;24;22"
}.freeze

Class Method Summary collapse

Class Method Details

.blockquote(text, enabled: enabled?) ) ⇒ Object



211
212
213
# File 'lib/kward/ansi.rb', line 211

def blockquote(text, enabled: enabled?)
  "#{colorize("", :gray, enabled: enabled)} #{inline_markdown(text, enabled: enabled)}"
end

.colorize(text, *styles, enabled: enabled?) ) ⇒ Object



44
45
46
47
48
49
50
51
52
# File 'lib/kward/ansi.rb', line 44

def colorize(text, *styles, enabled: enabled?)
  string = text.to_s
  return string unless enabled

  codes = styles.flatten.map { |style| STYLES.fetch(style, style) }.compact
  return string if codes.empty?

  "\e[#{codes.join(";")}m#{string}\e[0m"
end

.disabled_color?(env) ⇒ Boolean

Returns:

  • (Boolean)


272
273
274
275
276
277
# File 'lib/kward/ansi.rb', line 272

def disabled_color?(env)
  return true if env.key?("NO_COLOR") && !env["NO_COLOR"].to_s.empty?
  return true if env["CLICOLOR"] == "0"

  env["TERM"] == "dumb"
end

.enabled?(output = $stdout, env: ENV) ⇒ Boolean

Returns:

  • (Boolean)


34
35
36
37
38
39
40
41
42
# File 'lib/kward/ansi.rb', line 34

def enabled?(output = $stdout, env: ENV)
  setting = env["KWARD_COLOR"].to_s.downcase
  return true if %w[always force forced true yes 1].include?(setting)
  return false if %w[never false no 0].include?(setting)
  return true if forced_color?(env)
  return false if disabled_color?(env)

  output.respond_to?(:tty?) && output.tty?
end

.forced_color?(env) ⇒ Boolean

Returns:

  • (Boolean)


266
267
268
269
270
# File 'lib/kward/ansi.rb', line 266

def forced_color?(env)
  force_color = env["FORCE_COLOR"]
  clicolor_force = env["CLICOLOR_FORCE"]
  (force_color && force_color != "0") || (clicolor_force && clicolor_force != "0")
end

.inline_bold(text, enabled: enabled?) ) ⇒ Object



241
242
243
244
245
# File 'lib/kward/ansi.rb', line 241

def inline_bold(text, enabled: enabled?)
  text.gsub(/\*\*([^\n]+?)\*\*/) do
    colorize(Regexp.last_match(1), :bold, enabled: enabled)
  end
end

.inline_code(line, enabled: enabled?) ) ⇒ Object



262
263
264
# File 'lib/kward/ansi.rb', line 262

def inline_code(line, enabled: enabled?)
  inline_markdown(line, enabled: enabled)
end

.inline_emphasis(text, enabled: enabled?) ) ⇒ Object



235
236
237
238
239
# File 'lib/kward/ansi.rb', line 235

def inline_emphasis(text, enabled: enabled?)
  rendered = inline_bold(text, enabled: enabled)
  rendered = inline_strikethrough(rendered, enabled: enabled)
  inline_italic(rendered, enabled: enabled)
end

.inline_italic(text, enabled: enabled?) ) ⇒ Object



253
254
255
256
257
258
259
260
# File 'lib/kward/ansi.rb', line 253

def inline_italic(text, enabled: enabled?)
  rendered = text.gsub(/(^|[\s\(\[{])\*([^*\n]+?)\*(?=$|[\s\)\]},.!?:;])/) do
    "#{Regexp.last_match(1)}#{colorize(Regexp.last_match(2), :italic, enabled: enabled)}"
  end
  rendered.gsub(/(^|[\s\(\[{])_([^_\n]+?)_(?=$|[\s\)\]},.!?:;])/) do
    "#{Regexp.last_match(1)}#{colorize(Regexp.last_match(2), :italic, enabled: enabled)}"
  end
end


225
226
227
228
229
230
231
232
233
# File 'lib/kward/ansi.rb', line 225

def inline_links(text, enabled: enabled?)
  text.split(/(\[[^\]\n]+\]\([^)\s\n]+\))/).map do |part|
    if (match = part.match(/\A\[([^\]\n]+)\]\(([^)\s\n]+)\)\z/))
      "#{colorize(match[1], :cyan, enabled: enabled)} (#{colorize(match[2], :dim, enabled: enabled)})"
    else
      inline_emphasis(part, enabled: enabled)
    end
  end.join
end

.inline_markdown(line, enabled: enabled?) ) ⇒ Object



215
216
217
218
219
220
221
222
223
# File 'lib/kward/ansi.rb', line 215

def inline_markdown(line, enabled: enabled?)
  line.to_s.split(/(`[^`\n]+`)/).map do |part|
    if part.start_with?("`") && part.end_with?("`") && part.length > 1
      "`#{colorize(part[1...-1], :dim, enabled: enabled)}`"
    else
      inline_links(part, enabled: enabled)
    end
  end.join
end

.inline_strikethrough(text, enabled: enabled?) ) ⇒ Object



247
248
249
250
251
# File 'lib/kward/ansi.rb', line 247

def inline_strikethrough(text, enabled: enabled?)
  text.gsub(/~~([^\n]+?)~~/) do
    colorize(Regexp.last_match(1), :strikethrough, enabled: enabled)
  end
end

.markdown(text, enabled: enabled?) ) ⇒ Object



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/kward/ansi.rb', line 99

def markdown(text, enabled: enabled?)
  string = text.to_s
  lines = string.lines(chomp: true)
  rendered = []
  in_fence = false

  lines.each do |line|
    if (match = line.match(/\A\s*```([^`]*)\s*\z/))
      if in_fence
        rendered << colorize("" + "" * 39, :gray, enabled: enabled)
        in_fence = false
      else
        language = match[1].to_s.strip
        label = language.empty? ? "code" : "code #{language}"
        rendered << colorize("┌─ #{label}", :gray, enabled: enabled)
        in_fence = true
      end
      next
    end

    if in_fence
      rendered << colorize("#{line}", :dim, enabled: enabled)
    else
      rendered << markdown_line(line, enabled: enabled)
    end
  end

  rendered << colorize("" + "" * 39, :gray, enabled: enabled) if in_fence
  rendered.join("\n") + (string.end_with?("\n") ? "\n" : "")
end

.markdown_heading(marker, text, enabled: enabled?) ) ⇒ Object



201
202
203
# File 'lib/kward/ansi.rb', line 201

def markdown_heading(marker, text, enabled: enabled?)
  "#{marker}#{colorize(text, :bold, enabled: enabled)}"
end

.markdown_line(line, enabled: enabled?) ) ⇒ Object



189
190
191
192
193
194
195
196
197
198
199
# File 'lib/kward/ansi.rb', line 189

def markdown_line(line, enabled: enabled?)
  if (match = line.match(/\A(\#{1,6}\s+)(.+)\z/))
    markdown_heading(match[1], match[2], enabled: enabled)
  elsif (match = line.match(/\A(\s*)[-*]\s+\[([ xX])\]\s+(.+)\z/))
    task_list_item(match[1], match[2], match[3], enabled: enabled)
  elsif (match = line.match(/\A>\s?(.*)\z/))
    blockquote(match[1], enabled: enabled)
  else
    inline_markdown(line, enabled: enabled)
  end
end

.sanitize_transcript(text) ⇒ Object



58
59
60
61
62
63
# File 'lib/kward/ansi.rb', line 58

def sanitize_transcript(text)
  string = text.to_s.gsub(OSC_PATTERN, "").gsub(STRING_ESCAPE_PATTERN, "")
  string.gsub(/\e(?:\[[0-9;:?]*[ -\/]*[@-~]|.)/m) do |sequence|
    sequence.match?(SGR_PATTERN) ? sequence : ""
  end
end

.strip(text) ⇒ Object



54
55
56
# File 'lib/kward/ansi.rb', line 54

def strip(text)
  text.to_s.gsub(ESCAPE_PATTERN, "")
end

.task_list_item(indent, marker, text, enabled: enabled?) ) ⇒ Object



205
206
207
208
209
# File 'lib/kward/ansi.rb', line 205

def task_list_item(indent, marker, text, enabled: enabled?)
  checked = marker.downcase == "x"
  box = checked ? colorize("", :green, enabled: enabled) : colorize("", :gray, enabled: enabled)
  "#{indent}#{box} #{inline_markdown(text, enabled: enabled)}"
end

.wrap_visible(text, width) ⇒ Object



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
93
94
95
96
97
# File 'lib/kward/ansi.rb', line 65

def wrap_visible(text, width)
  line_width = [width.to_i, 1].max
  rows = []
  current = +""
  visible_width = 0
  string = text.to_s
  index = 0

  while index < string.length
    if string[index] == "\e" && (match = string[index..].match(/\A\e\[[0-9;:]*m/))
      if current.empty? && rows.any?
        rows[-1] << match[0]
      else
        current << match[0]
      end
      index += match[0].length
      next
    end

    char = string[index]
    current << char
    visible_width += 1
    index += 1
    if visible_width >= line_width
      rows << current
      current = +""
      visible_width = 0
    end
  end

  rows << current unless current.empty?
  rows
end