Module: Everywhere::UI
- Defined in:
- lib/everywhere/ui.rb
Overview
Terminal output helpers. Colors follow the informal standard: on when stdout is a TTY or CLICOLOR_FORCE=1, always off when NO_COLOR is set.
The hosted runner streams every release through a pipe (not a TTY) into the
Platform web log, so it exports CLICOLOR_FORCE=1 to keep our color — the web
UI renders ANSI. That's why we force rather than sniff the TTY there.
Defined Under Namespace
Classes: Status
Constant Summary collapse
- OS_LABELS =
Display casing for os tokens. Target IDs ("macos-arm64") stay lowercase wherever they're typed or stored; use these only in human-facing output.
{ "macos" => "macOS", "ios" => "iOS", "android" => "Android", "windows" => "Windows", "linux" => "Linux" }.freeze
- FRAMES =
Braille spinner frames, shared by the transient Status line and the
every devdock. Ten frames at ~200ms is one revolution every two seconds — legible as motion without being distracting. %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
- ANSI =
Escape sequences a tool writes when it thinks it's on a terminal — which, since
every devrelays its children over a pty, they all do. Two forms matter in practice: SGR color (CSI), and OSC-8 hyperlinks, which cargo wraps around the profile name in its own status lines. / \e\][^\a\e]*(?:\a|\e\\) # OSC ... terminated by BEL or ST | \e\[[0-9;?]*[ -\/]*[@-~] # CSI ... final byte | \e[@-Z\\-_] # two-character escapes /x
Class Method Summary collapse
- .bad(msg) ⇒ Object
- .bad_line(msg) ⇒ Object
- .blue(t) ⇒ Object
- .bold(t) ⇒ Object
- .color? ⇒ Boolean
- .cyan(t) ⇒ Object
- .detail(msg) ⇒ Object
- .detail_line(msg) ⇒ Object
-
.die!(msg) ⇒ Object
Ends the current unit of work.
- .dim(t) ⇒ Object
-
.elapsed(seconds) ⇒ Object
Human elapsed time — "48s", "2m 14s", "1h 3m".
-
.gray(t) ⇒ Object
bright black — dimmer than dim() on most themes.
- .green(t) ⇒ Object
- .magenta(t) ⇒ Object
- .note(msg, marker: "·", color: :gray) ⇒ Object
-
.note_line(msg, marker: "·", color: :gray) ⇒ Object
An indented, dim outcome under a substep — e.g.
- .ok(msg) ⇒ Object
- .ok_line(msg) ⇒ Object
- .os_label(os) ⇒ Object
- .paint(text, *codes) ⇒ Object
-
.phase(msg) ⇒ Object
Everything prints through Console, which serializes concurrent writers and gives the
every devdock a chance to repaint around each line. -
.phase_line(msg) ⇒ Object
--- output levels --------------------------------------------------------.
- .red(t) ⇒ Object
-
.short_path(str) ⇒ Object
Collapse the noisy machine paths that external tools echo (codesign, notarytool, stapler) down to something a human can scan.
- .step(msg) ⇒ Object
- .step_line(msg) ⇒ Object
-
.strip_ansi(str) ⇒ Object
For MATCHING, never for display: a filter that pattern-matches colored output has to look past the codes, but the colors are still what the developer wants to read.
- .substep(msg) ⇒ Object
- .substep_line(msg) ⇒ Object
- .success(msg) ⇒ Object
- .success_line(msg) ⇒ Object
-
.target_label(target) ⇒ Object
"macos-arm64" → "macOS-arm64".
- .warn(msg) ⇒ Object
- .warn_line(msg) ⇒ Object
- .yellow(t) ⇒ Object
-
.yn(bool) ⇒ Object
A yes/no fact, colored by truthiness (for signing receipts).
Class Method Details
.bad(msg) ⇒ Object
84 |
# File 'lib/everywhere/ui.rb', line 84 def bad(msg) = Console.puts(bad_line(msg)) |
.bad_line(msg) ⇒ Object
71 |
# File 'lib/everywhere/ui.rb', line 71 def bad_line(msg) = "#{red("✗")} #{msg}" |
.blue(t) ⇒ Object
32 |
# File 'lib/everywhere/ui.rb', line 32 def blue(t) = paint(t, 34) |
.bold(t) ⇒ Object
27 |
# File 'lib/everywhere/ui.rb', line 27 def bold(t) = paint(t, 1) |
.color? ⇒ Boolean
16 17 18 19 20 21 |
# File 'lib/everywhere/ui.rb', line 16 def color? return false if ENV["NO_COLOR"] return true if ENV["CLICOLOR_FORCE"] == "1" $stdout.tty? end |
.cyan(t) ⇒ Object
34 |
# File 'lib/everywhere/ui.rb', line 34 def cyan(t) = paint(t, 36) |
.detail(msg) ⇒ Object
82 |
# File 'lib/everywhere/ui.rb', line 82 def detail(msg) = Console.puts(detail_line(msg)) |
.detail_line(msg) ⇒ Object
69 |
# File 'lib/everywhere/ui.rb', line 69 def detail_line(msg) = " #{gray("·")} #{gray(msg)}" |
.die!(msg) ⇒ Object
Ends the current unit of work. At the top level Ruby exits 1 and prints
nothing extra, so this behaves exactly like the Kernel#abort it replaced —
but inside every dev the worker thread running the build catches the
Fatal, marks that target failed, and hands you back the key menu with the
dev server still serving. The message is the same string abort received,
so every assert_raises(SystemExit) reading err.message is unaffected.
102 103 104 105 106 |
# File 'lib/everywhere/ui.rb', line 102 def die!(msg) line = "#{red("✗")} #{red(msg)}" Console.error(line) raise Everywhere::Fatal.new(1, line) end |
.dim(t) ⇒ Object
28 |
# File 'lib/everywhere/ui.rb', line 28 def dim(t) = paint(t, 2) |
.elapsed(seconds) ⇒ Object
Human elapsed time — "48s", "2m 14s", "1h 3m".
176 177 178 179 180 181 182 |
# File 'lib/everywhere/ui.rb', line 176 def elapsed(seconds) secs = seconds.to_i return "#{secs}s" if secs < 60 return "#{secs / 60}m #{secs % 60}s" if secs < 3600 "#{secs / 3600}h #{(secs % 3600) / 60}m" end |
.gray(t) ⇒ Object
bright black — dimmer than dim() on most themes
35 |
# File 'lib/everywhere/ui.rb', line 35 def gray(t) = paint(t, 90) # bright black — dimmer than dim() on most themes |
.green(t) ⇒ Object
30 |
# File 'lib/everywhere/ui.rb', line 30 def green(t) = paint(t, 32) |
.magenta(t) ⇒ Object
33 |
# File 'lib/everywhere/ui.rb', line 33 def magenta(t) = paint(t, 35) |
.note(msg, marker: "·", color: :gray) ⇒ Object
94 |
# File 'lib/everywhere/ui.rb', line 94 def note(msg, marker: "·", color: :gray) = Console.puts(note_line(msg, marker: marker, color: color)) |
.note_line(msg, marker: "·", color: :gray) ⇒ Object
An indented, dim outcome under a substep — e.g. "accepted", "valid on disk".
89 90 91 92 |
# File 'lib/everywhere/ui.rb', line 89 def note_line(msg, marker: "·", color: :gray) tint = respond_to?(color) ? method(color) : method(:gray) " #{tint.call(marker)} #{tint.call(msg)}" end |
.ok(msg) ⇒ Object
83 |
# File 'lib/everywhere/ui.rb', line 83 def ok(msg) = Console.puts(ok_line(msg)) |
.ok_line(msg) ⇒ Object
70 |
# File 'lib/everywhere/ui.rb', line 70 def ok_line(msg) = "#{green("✓")} #{msg}" |
.os_label(os) ⇒ Object
47 |
# File 'lib/everywhere/ui.rb', line 47 def os_label(os) = OS_LABELS.fetch(os.to_s, os.to_s) |
.paint(text, *codes) ⇒ Object
23 24 25 |
# File 'lib/everywhere/ui.rb', line 23 def paint(text, *codes) color? ? "\e[#{codes.join(";")}m#{text}\e[0m" : text.to_s end |
.phase(msg) ⇒ Object
Everything prints through Console, which serializes concurrent writers and
gives the every dev dock a chance to repaint around each line. With no
dock installed it is a plain $stdout.write, resolved per call so
capture_io still works.
79 |
# File 'lib/everywhere/ui.rb', line 79 def phase(msg) = Console.puts(phase_line(msg)) |
.phase_line(msg) ⇒ Object
--- output levels --------------------------------------------------------
phase ● a major phase boundary (build / sign / notarize)
step → a top-level action inside a phase
substep ▸ a sub-action under a step (indented)
detail · raw-ish detail under a substep (indented + dim)
Every level has a *_line string builder (used by LogFilter, which returns lines for someone else to print) and a printer that puts it. ok / success / warn / bad / die! carry outcomes at any level.
66 |
# File 'lib/everywhere/ui.rb', line 66 def phase_line(msg) = "\n#{bold(cyan("●"))} #{bold(msg)}" |
.red(t) ⇒ Object
29 |
# File 'lib/everywhere/ui.rb', line 29 def red(t) = paint(t, 31) |
.short_path(str) ⇒ Object
Collapse the noisy machine paths that external tools echo (codesign,
notarytool, stapler) down to something a human can scan. In particular the
runner works out of a deep /var/folders/…/every-runner…/src/ sandbox — we
strip that prefix so only the meaningful dist/App.app tail remains. Then
$HOME → ~ and the cwd → a leading (dropped) prefix.
210 211 212 213 214 215 216 217 218 219 |
# File 'lib/everywhere/ui.rb', line 210 def short_path(str) s = str.to_s.dup # Runner/local temp sandboxes: keep only the part after the extracted src/. s = s.gsub(%r{/(?:private/)?(?:var|tmp)/[^\s"']*?/every-[a-z]+[0-9A-Za-z_-]+/src/}, "") # Any other everywhere-* scratch dir: keep the basename tail. s = s.gsub(%r{/(?:private/)?(?:var|tmp)/[^\s"']*?/everywhere-[a-z]+[0-9A-Za-z_.-]*/}, "") home = ENV["HOME"] s = s.gsub("#{home}/", "~/") if home && !home.empty? s end |
.step(msg) ⇒ Object
80 |
# File 'lib/everywhere/ui.rb', line 80 def step(msg) = Console.puts(step_line(msg)) |
.step_line(msg) ⇒ Object
67 |
# File 'lib/everywhere/ui.rb', line 67 def step_line(msg) = "#{cyan("→")} #{msg}" |
.strip_ansi(str) ⇒ Object
For MATCHING, never for display: a filter that pattern-matches colored output has to look past the codes, but the colors are still what the developer wants to read.
201 |
# File 'lib/everywhere/ui.rb', line 201 def strip_ansi(str) = str.to_s.gsub(ANSI, "") |
.substep(msg) ⇒ Object
81 |
# File 'lib/everywhere/ui.rb', line 81 def substep(msg) = Console.puts(substep_line(msg)) |
.substep_line(msg) ⇒ Object
68 |
# File 'lib/everywhere/ui.rb', line 68 def substep_line(msg) = " #{cyan("▸")} #{msg}" |
.success(msg) ⇒ Object
86 |
# File 'lib/everywhere/ui.rb', line 86 def success(msg) = Console.puts(success_line(msg)) |
.success_line(msg) ⇒ Object
73 |
# File 'lib/everywhere/ui.rb', line 73 def success_line(msg) = "#{green("✓")} #{bold(msg)}" |
.target_label(target) ⇒ Object
"macos-arm64" → "macOS-arm64"
50 51 52 53 |
# File 'lib/everywhere/ui.rb', line 50 def target_label(target) os, arch = target.to_s.split("-", 2) [ os_label(os), arch ].compact.join("-") end |
.warn(msg) ⇒ Object
85 |
# File 'lib/everywhere/ui.rb', line 85 def warn(msg) = Console.puts(warn_line(msg)) |
.warn_line(msg) ⇒ Object
72 |
# File 'lib/everywhere/ui.rb', line 72 def warn_line(msg) = "#{yellow("!")} #{msg}" |
.yellow(t) ⇒ Object
31 |
# File 'lib/everywhere/ui.rb', line 31 def yellow(t) = paint(t, 33) |
.yn(bool) ⇒ Object
A yes/no fact, colored by truthiness (for signing receipts).
109 |
# File 'lib/everywhere/ui.rb', line 109 def yn(bool) = bool ? green("yes") : red("no") |