Module: Metaclean::Display

Defined in:
lib/metaclean/display.rb

Constant Summary collapse

COLORS =
{
  reset:   "\e[0m",
  bold:    "\e[1m",
  dim:     "\e[2m",
  red:     "\e[31m",
  green:   "\e[32m",
  yellow:  "\e[33m",
  blue:    "\e[34m",
  magenta: "\e[35m",
  cyan:    "\e[36m",
  gray:    "\e[90m"
}.freeze
NON_METADATA_GROUPS =

ExifTool reports four “groups” that are descriptions of the file itself, not embedded metadata: System (filesystem stat), File (header info), ExifTool (its own version), Composite (computed values). Excluding these makes the diff focus on what actually got stripped.

%w[System File ExifTool Composite].freeze

Class Method Summary collapse

Class Method Details

.c(text, color) ⇒ Object

‘c` for “color”. Wraps text in the requested color, or returns it plain if colors are disabled. The reset code at the end stops the color from bleeding into following output.



57
58
59
60
61
# File 'lib/metaclean/display.rb', line 57

def c(text, color)
  return text.to_s unless color?

  "#{COLORS[color]}#{text}#{COLORS[:reset]}"
end

.color?Boolean

Decides whether to emit ANSI color codes. Colors are wrong when:

* stdout is a pipe/file (not a terminal) — `tty?` is false there
* NO_COLOR env var is set (de-facto convention, see no-color.org)
* we're on classic Windows cmd.exe (modern Windows Terminal is fine,
  but to be safe we require an explicit FORCE_COLOR opt-in there)

Returns:

  • (Boolean)


43
44
45
46
47
48
49
50
51
52
# File 'lib/metaclean/display.rb', line 43

def color?
  return @color if defined?(@color)

  # Per https://no-color.org: disable only when NO_COLOR is set to a
  # non-empty value. An unset or empty NO_COLOR leaves colors on.
  no_color = ENV['NO_COLOR'].to_s
  @color = $stdout.tty? && no_color.empty? && !Gem.win_platform?
  @color = true if ENV['FORCE_COLOR']
  @color
end

.count_embedded(meta) ⇒ Object

How many “real” embedded tags are there? Used for the “Before (24 embedded tags) → After (0)” summary line.



190
191
192
193
194
195
# File 'lib/metaclean/display.rb', line 190

def count_embedded(meta)
  meta.keys
      .reject { |k| k == 'SourceFile' }
      .reject { |k| NON_METADATA_GROUPS.include?(group_of(k)) }
      .size
end

.diff(before, after) ⇒ Object

Compares two metadata hashes (before vs after) and prints three sections: removed, changed, still-present. This is the “before/after” the user asked for.



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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
159
160
161
162
# File 'lib/metaclean/display.rb', line 111

def diff(before, after)
  keys = (before.keys + after.keys).uniq
                                   .reject { |k| k == 'SourceFile' }
                                   .reject { |k| NON_METADATA_GROUPS.include?(group_of(k)) }

  removed = []
  changed = []
  kept    = []

  # Classifying each key into one of three buckets keeps the rest of
  # the method simple and testable.
  keys.sort.each do |k|
    b = before[k]
    a = after[k]
    if a.nil? && !b.nil?
      removed << [k, b]
    elsif !b.nil? && a != b
      changed << [k, b, a]
    elsif !b.nil?
      kept << [k, b]
    end
  end

  if removed.any?
    section "Removed (#{removed.size})"
    removed.each do |k, b|
      puts "  #{c('-', :red)} #{c(k, :red)}  #{c(truncate(format_value(b), 60), :gray)}"
    end
  end

  if changed.any?
    section "Changed (#{changed.size})"
    changed.each do |k, b, a|
      puts "  #{c('~', :yellow)} #{c(k, :yellow)}"
      puts "      #{c('-', :red)}   #{truncate(format_value(b), 60)}"
      puts "      #{c('+', :green)} #{truncate(format_value(a), 60)}"
    end
  end

  if kept.any?
    section "Still present (#{kept.size})"
    kept.each do |k, b|
      puts "  #{c('=', :gray)} #{c(k, :gray)}  #{c(truncate(format_value(b), 60), :gray)}"
    end
  end

  if removed.empty? && changed.empty? && kept.empty?
    info 'Nothing to strip — file already clean.'
  elsif removed.empty? && changed.empty?
    info 'No tags were removed — see "Still present" above.'
  end
end

.error(text) ⇒ Object

‘error` returns a string instead of printing it — callers usually want to send it to STDERR via `warn`, not stdout via `puts`.



79
# File 'lib/metaclean/display.rb', line 79

def error(text); c("#{text}", :red); end

.format_value(v) ⇒ Object

Make any value safe to print on a single line. Hashes/Arrays get ‘inspect` (shows their structure); strings are collapsed to single spaces so a multiline tag value doesn’t wreck the table.



173
174
175
176
177
178
# File 'lib/metaclean/display.rb', line 173

def format_value(v)
  case v
  when Hash, Array then v.inspect
  else v.to_s.gsub(/\s+/, ' ')
  end
end

.group_of(key) ⇒ Object

Pull the group name out of “Group:Tag”. The ‘2` argument to split caps the result at 2 elements, so a value containing “:” doesn’t break it.



166
167
168
# File 'lib/metaclean/display.rb', line 166

def group_of(key)
  key.to_s.split(':', 2).first.to_s
end

.header(text) ⇒ Object

Visual section markers used throughout the runner’s output. Keeping them here means a single change updates the look everywhere.



65
66
67
68
69
70
# File 'lib/metaclean/display.rb', line 65

def header(text)
  puts
  puts c('' * 64, :gray)
  puts c(text, :bold)
  puts c('' * 64, :gray)
end

.info(text) ⇒ Object



73
# File 'lib/metaclean/display.rb', line 73

def info(text);    puts c("  #{text}",  :gray);  end

.metadata_table(meta, only_embedded: false) ⇒ Object

Prints a metadata Hash as a grouped, indented table. ‘only_embedded:` filters out the System/File/etc. noise.



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/metaclean/display.rb', line 83

def (meta, only_embedded: false)
  rows = meta.reject { |k, _| k == 'SourceFile' }
  rows = rows.reject { |k, _| NON_METADATA_GROUPS.include?(group_of(k)) } if only_embedded

  if rows.empty?
    info(only_embedded ? '(no embedded metadata)' : '(no metadata)')
    return
  end

  # `group_by` partitions an Enumerable into a Hash keyed by the block's
  # result. Here we group all "GPS:*" tags together, all "EXIF:*" together,
  # etc., then print each group as a labeled sub-table.
  grouped = rows.group_by { |k, _| group_of(k) }
  grouped.sort_by { |g, _| g.to_s }.each do |group, pairs|
    puts c("  [#{group}]", :magenta)
    pairs.sort_by { |k, _| k.to_s }.each do |k, v|
      tag = k.to_s.split(':', 2).last
      # `format` (alias of sprintf) does column alignment: %-38s = left-
      # aligned, padded to 38 chars.
      line = format('    %-38s %s', truncate(tag, 38), truncate(format_value(v), 60))
      puts c(line, :dim)
    end
  end
end

.section(text) ⇒ Object



72
# File 'lib/metaclean/display.rb', line 72

def section(text); puts c("#{text}",  :cyan);  end

.success(text) ⇒ Object



74
# File 'lib/metaclean/display.rb', line 74

def success(text); puts c("#{text}",  :green); end

.truncate(s, n) ⇒ Object

Truncate to N chars with a single-character ellipsis. We use “…” (one Unicode char) instead of “…” so the truncation doesn’t itself spill over the budget.



183
184
185
186
# File 'lib/metaclean/display.rb', line 183

def truncate(s, n)
  s = s.to_s
  s.length > n ? "#{s[0, n - 1]}" : s
end

.warning(text) ⇒ Object



75
# File 'lib/metaclean/display.rb', line 75

def warning(text); puts c("#{text}",  :yellow);end