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 dev dock. 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 dev relays 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

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

Returns:

  • (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.

Raises:



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")