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", 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
- LOGO =
ASCII wordmark shown at the top of –help / –version. Printed by ‘banner` (see there for why it’s colored line-by-line).
<<~ART ███╗ ███╗███████╗████████╗ █████╗ ██████╗██╗ ███████╗ █████╗ ███╗ ██╗ ████╗ ████║██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔════╝██╔══██╗████╗ ██║ ██╔████╔██║█████╗ ██║ ███████║██║ ██║ █████╗ ███████║██╔██╗ ██║ ██║╚██╔╝██║██╔══╝ ██║ ██╔══██║██║ ██║ ██╔══╝ ██╔══██║██║╚██╗██║ ██║ ╚═╝ ██║███████╗ ██║ ██║ ██║╚██████╗███████╗███████╗██║ ██║██║ ╚████║ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝ ART
Class Method Summary collapse
-
.banner ⇒ Object
Red ASCII wordmark (matches Ruby’s brand color) + one-line tagline for –help / –version.
-
.c(text, color) ⇒ Object
Wrap text in a color, or pass it through plain when colors are off.
-
.color? ⇒ Boolean
Decides whether to emit ANSI color codes.
-
.count_embedded(meta) ⇒ Object
How many “real” embedded tags are there? Used for the “Before (24 embedded tags) → After (0)” summary line.
-
.diff(before, after) ⇒ Object
Compares two metadata hashes (before vs after) and prints three sections: removed, changed, still-present.
-
.embedded_key?(key) ⇒ Boolean
True when ‘key` names real embedded metadata: not the SourceFile bookkeeping key, and not one of the System/File/ExifTool/Composite groups that describe the file rather than its embedded tags.
-
.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`.
-
.format_value(v) ⇒ Object
Make any value safe to print on a single line.
-
.group_of(key) ⇒ Object
Group name out of “Group:Tag” (split caps at 2 so a “:” in the value is safe).
- .header(text) ⇒ Object
- .info(text) ⇒ Object
-
.metadata_table(meta, only_embedded: false) ⇒ Object
Prints a metadata Hash as a grouped, indented table.
-
.printable(text) ⇒ Object
Render untrusted filenames/metadata as terminal text, not terminal control.
- .section(text) ⇒ Object
- .success(text) ⇒ Object
-
.truncate(s, n) ⇒ Object
Truncate to N chars with a single-character ellipsis.
- .warning(text) ⇒ Object
Class Method Details
.banner ⇒ Object
Red ASCII wordmark (matches Ruby’s brand color) + one-line tagline for –help / –version. Colored line-by-line on purpose: ‘c` runs text through `printable`, which turns control chars (including the heredoc’s newlines) into spaces — so coloring the whole block at once would collapse the logo onto one line.
66 67 68 69 |
# File 'lib/metaclean/display.rb', line 66 def LOGO.each_line { |line| puts c(line.chomp, :red) } puts c(' strip EXIF · IPTC · XMP · GPS · ID3 — leave the file clean', :gray) end |
.c(text, color) ⇒ Object
Wrap text in a color, or pass it through plain when colors are off.
54 55 56 57 58 59 |
# File 'lib/metaclean/display.rb', line 54 def c(text, color) text = printable(text) return text 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)
42 43 44 45 46 47 48 49 50 51 |
# File 'lib/metaclean/display.rb', line 42 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? @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.
212 213 214 |
# File 'lib/metaclean/display.rb', line 212 def () .keys.count { |k| (k) } 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.
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 |
# File 'lib/metaclean/display.rb', line 113 def diff(before, after) keys = (before.keys + after.keys).uniq.select { |k| (k) } removed = [] changed = [] kept = [] 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 |
.embedded_key?(key) ⇒ Boolean
True when ‘key` names real embedded metadata: not the SourceFile bookkeeping key, and not one of the System/File/ExifTool/Composite groups that describe the file rather than its embedded tags. Single source of truth for “is this a tag we actually stripped?” — shared by the table, diff, count, removed-count and privacy-residual checks.
172 173 174 |
# File 'lib/metaclean/display.rb', line 172 def (key) key != 'SourceFile' && !NON_METADATA_GROUPS.include?(group_of(key)) 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`.
85 |
# File 'lib/metaclean/display.rb', line 85 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.
179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/metaclean/display.rb', line 179 def format_value(v) case v when Hash, Array then printable(v.inspect) else # Guard the regexp gsub against invalid-encoding tag values — gsub raises # ArgumentError on them. Exiftool.read already scrubs; this is belt-and- # suspenders so the display layer can never crash the run on hostile bytes. s = printable(v) s.gsub(/\s+/, ' ') end end |
.group_of(key) ⇒ Object
Group name out of “Group:Tag” (split caps at 2 so a “:” in the value is safe).
163 164 165 |
# File 'lib/metaclean/display.rb', line 163 def group_of(key) key.to_s.split(':', 2).first.to_s end |
.header(text) ⇒ Object
71 72 73 74 75 76 |
# File 'lib/metaclean/display.rb', line 71 def header(text) puts puts c('━' * 64, :gray) puts c(text, :bold) puts c('━' * 64, :gray) end |
.info(text) ⇒ Object
79 |
# File 'lib/metaclean/display.rb', line 79 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.
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/metaclean/display.rb', line 89 def (, only_embedded: false) rows = .reject { |k, _| k == 'SourceFile' } rows = rows.select { |k, _| (k) } if if rows.empty? info( ? '(no embedded metadata)' : '(no metadata)') return end # Group "GPS:*", "EXIF:*", … each into its own 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 line = format(' %-38s %s', truncate(tag, 38), truncate(format_value(v), 60)) puts c(line, :dim) end end end |
.printable(text) ⇒ Object
Render untrusted filenames/metadata as terminal text, not terminal control. Exif/Office/PDF metadata can contain ANSI/OSC escape bytes; printing those raw can recolor output, rewrite a terminal title, or worse. We keep the content readable by replacing C0/DEL and C1 control chars with spaces (C1, U+0080–U+009F, holds the 8-bit CSI/OSC introducers some terminals honor).
196 197 198 199 200 |
# File 'lib/metaclean/display.rb', line 196 def printable(text) s = text.to_s s = s.scrub unless s.valid_encoding? s.gsub(/[[:cntrl:]]/, ' ') end |
.section(text) ⇒ Object
78 |
# File 'lib/metaclean/display.rb', line 78 def section(text); puts c("▸ #{text}", :cyan); end |
.success(text) ⇒ Object
80 |
# File 'lib/metaclean/display.rb', line 80 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.
205 206 207 208 |
# File 'lib/metaclean/display.rb', line 205 def truncate(s, n) s = s.to_s s.length > n ? "#{s[0, n - 1]}…" : s end |
.warning(text) ⇒ Object
81 |
# File 'lib/metaclean/display.rb', line 81 def warning(text); puts c("⚠ #{text}", :yellow);end |