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



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

Returns:

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

Raises:



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