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
-
.rel_path(path, root) ⇒ Object
short_path for a path we know the root of: drop the project root (or the cwd, when the command runs from somewhere else) before shortening.
-
.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
85 |
# File 'lib/everywhere/ui.rb', line 85 def bad(msg) = Console.puts(bad_line(msg)) |
.bad_line(msg) ⇒ Object
72 |
# File 'lib/everywhere/ui.rb', line 72 def bad_line(msg) = "#{red("✗")} #{msg}" |
.blue(t) ⇒ Object
33 |
# File 'lib/everywhere/ui.rb', line 33 def blue(t) = paint(t, 34) |
.bold(t) ⇒ Object
28 |
# File 'lib/everywhere/ui.rb', line 28 def bold(t) = paint(t, 1) |
.color? ⇒ Boolean
17 18 19 20 21 22 |
# File 'lib/everywhere/ui.rb', line 17 def color? return false if ENV["NO_COLOR"] return true if ENV["CLICOLOR_FORCE"] == "1" $stdout.tty? end |
.cyan(t) ⇒ Object
35 |
# File 'lib/everywhere/ui.rb', line 35 def cyan(t) = paint(t, 36) |
.detail(msg) ⇒ Object
83 |
# File 'lib/everywhere/ui.rb', line 83 def detail(msg) = Console.puts(detail_line(msg)) |
.detail_line(msg) ⇒ Object
70 |
# File 'lib/everywhere/ui.rb', line 70 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.
103 104 105 106 107 |
# File 'lib/everywhere/ui.rb', line 103 def die!(msg) line = "#{red("✗")} #{red(msg)}" Console.error(line) raise Everywhere::Fatal.new(1, line) end |
.dim(t) ⇒ Object
29 |
# File 'lib/everywhere/ui.rb', line 29 def dim(t) = paint(t, 2) |
.elapsed(seconds) ⇒ Object
Human elapsed time — "48s", "2m 14s", "1h 3m".
177 178 179 180 181 182 183 |
# File 'lib/everywhere/ui.rb', line 177 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
36 |
# File 'lib/everywhere/ui.rb', line 36 def gray(t) = paint(t, 90) # bright black — dimmer than dim() on most themes |
.green(t) ⇒ Object
31 |
# File 'lib/everywhere/ui.rb', line 31 def green(t) = paint(t, 32) |
.magenta(t) ⇒ Object
34 |
# File 'lib/everywhere/ui.rb', line 34 def magenta(t) = paint(t, 35) |
.note(msg, marker: "·", color: :gray) ⇒ Object
95 |
# File 'lib/everywhere/ui.rb', line 95 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".
90 91 92 93 |
# File 'lib/everywhere/ui.rb', line 90 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
84 |
# File 'lib/everywhere/ui.rb', line 84 def ok(msg) = Console.puts(ok_line(msg)) |
.ok_line(msg) ⇒ Object
71 |
# File 'lib/everywhere/ui.rb', line 71 def ok_line(msg) = "#{green("✓")} #{msg}" |
.os_label(os) ⇒ Object
48 |
# File 'lib/everywhere/ui.rb', line 48 def os_label(os) = OS_LABELS.fetch(os.to_s, os.to_s) |
.paint(text, *codes) ⇒ Object
24 25 26 |
# File 'lib/everywhere/ui.rb', line 24 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.
80 |
# File 'lib/everywhere/ui.rb', line 80 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.
67 |
# File 'lib/everywhere/ui.rb', line 67 def phase_line(msg) = "\n#{bold(cyan("●"))} #{bold(msg)}" |
.red(t) ⇒ Object
30 |
# File 'lib/everywhere/ui.rb', line 30 def red(t) = paint(t, 31) |
.rel_path(path, root) ⇒ Object
short_path for a path we know the root of: drop the project root (or the cwd, when the command runs from somewhere else) before shortening.
224 |
# File 'lib/everywhere/ui.rb', line 224 def rel_path(path, root) = short_path(path.to_s.sub("#{root}/", "").sub("#{Dir.pwd}/", "")) |
.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.
211 212 213 214 215 216 217 218 219 220 |
# File 'lib/everywhere/ui.rb', line 211 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
81 |
# File 'lib/everywhere/ui.rb', line 81 def step(msg) = Console.puts(step_line(msg)) |
.step_line(msg) ⇒ Object
68 |
# File 'lib/everywhere/ui.rb', line 68 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.
202 |
# File 'lib/everywhere/ui.rb', line 202 def strip_ansi(str) = str.to_s.gsub(ANSI, "") |
.substep(msg) ⇒ Object
82 |
# File 'lib/everywhere/ui.rb', line 82 def substep(msg) = Console.puts(substep_line(msg)) |
.substep_line(msg) ⇒ Object
69 |
# File 'lib/everywhere/ui.rb', line 69 def substep_line(msg) = " #{cyan("▸")} #{msg}" |
.success(msg) ⇒ Object
87 |
# File 'lib/everywhere/ui.rb', line 87 def success(msg) = Console.puts(success_line(msg)) |
.success_line(msg) ⇒ Object
74 |
# File 'lib/everywhere/ui.rb', line 74 def success_line(msg) = "#{green("✓")} #{bold(msg)}" |
.target_label(target) ⇒ Object
"macos-arm64" → "macOS-arm64"
51 52 53 54 |
# File 'lib/everywhere/ui.rb', line 51 def target_label(target) os, arch = target.to_s.split("-", 2) [ os_label(os), arch ].compact.join("-") end |
.warn(msg) ⇒ Object
86 |
# File 'lib/everywhere/ui.rb', line 86 def warn(msg) = Console.puts(warn_line(msg)) |
.warn_line(msg) ⇒ Object
73 |
# File 'lib/everywhere/ui.rb', line 73 def warn_line(msg) = "#{yellow("!")} #{msg}" |
.yellow(t) ⇒ Object
32 |
# File 'lib/everywhere/ui.rb', line 32 def yellow(t) = paint(t, 33) |
.yn(bool) ⇒ Object
A yes/no fact, colored by truthiness (for signing receipts).
110 |
# File 'lib/everywhere/ui.rb', line 110 def yn(bool) = bool ? green("yes") : red("no") |