Class: RepoTender::UI::InteractiveReporter

Inherits:
Object
  • Object
show all
Defined in:
lib/repo_tender/ui/interactive_reporter.rb

Overview

Compact, single-line live progress renderer for ‘sync`. Driven by one render-loop fiber spawned as a child of the engine task via `attach(task)` — NO Ruby Thread.

Two phases under one attach/detach (GS6):

Phase 1 — Listing: fires between listing_started and listing_finished.
  Live status line shows "listing N orgs… ✓ K done". As each org
  completes, a persistent line is emitted (org name + count).

Phase 2 — Sweep: fires after run_started through run_finished.
  Reverts to the compact repo counter (synced X/N + tallies).

Output model:

- One live status line, rewritten in place via \r + \e[K.
- Persistent scrollback lines for listing phase (one per org) and
  for NON-CLEAN repos only in sweep phase.
- Total output: O(orgs + non_clean + failed + constant).

Invariants:

- The render fiber is the sole writer to `out`; worker fibers only
  mutate tally/queue state via the reporter event methods.
- `Kernel#sleep` inside the render fiber yields to the reactor
  (cooperative scheduling). Never Thread.new.
- On `^C`, the scheduler cancels the child render fiber; its `ensure`
  block restores the cursor unconditionally.

Constant Summary collapse

FRAMES =
%w[         ].freeze
ADDED_LIST_THRESHOLD =
10
IN_FLIGHT_MAX_WIDTH =
40

Instance Method Summary collapse

Constructor Details

#initialize(out, mode:, cadence: 0.1) ⇒ InteractiveReporter

Returns a new instance of InteractiveReporter.



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/repo_tender/ui/interactive_reporter.rb', line 39

def initialize(out, mode:, cadence: 0.1)
  @out = out
  @pastel = Pastel.new(enabled: mode.color)
  @cadence = cadence

  # Listing phase state
  @org_total = 0
  @org_done = 0
  @pending_org_lines = []

  # Sweep phase state
  @total = 0
  @finished = 0
  @clean_count = 0
  @nonclean_count = 0
  @failed_count = 0
  @pending_lines = []

  # In-flight tracking (insertion-ordered: last entry = most-recently-started)
  @in_flight = {}

  # End-of-run breakdown state
  @action_counts = Hash.new(0)
  @total_commits = 0
  @added_repos = []

  @frame_idx = 0
  @phase = :listing  # :listing | :sweep
  @done = false
  @render_task = nil
end

Instance Method Details

#attach(task) ⇒ Object



71
72
73
# File 'lib/repo_tender/ui/interactive_reporter.rb', line 71

def attach(task)
  @render_task = task.async { render_loop }
end

#detachObject



75
76
77
78
79
# File 'lib/repo_tender/ui/interactive_reporter.rb', line 75

def detach
  @done = true
  @render_task&.wait
  @render_task = nil
end

#listing_finishedObject



97
98
99
# File 'lib/repo_tender/ui/interactive_reporter.rb', line 97

def listing_finished
  # Phase transition handled by run_started
end

#listing_started(total:) ⇒ Object

— Listing phase events —



83
84
85
86
# File 'lib/repo_tender/ui/interactive_reporter.rb', line 83

def listing_started(total:)
  @org_total = total
  @phase = :listing
end

#org_listed(ref, count:) ⇒ Object



88
89
90
91
92
93
94
95
# File 'lib/repo_tender/ui/interactive_reporter.rb', line 88

def org_listed(ref, count:)
  @org_done += 1
  @pending_org_lines << if count
    "#{@pastel.green("")} #{ref.name}  #{count} repo(s)"
  else
    "#{@pastel.red("")} #{ref.name}  FAILED"
  end
end

#repo_failed(ref, error) ⇒ Object



136
137
138
139
140
141
142
# File 'lib/repo_tender/ui/interactive_reporter.rb', line 136

def repo_failed(ref, error)
  @in_flight.delete(ref)
  @finished += 1
  @failed_count += 1
  @action_counts[:error] += 1
  @pending_lines << "#{@pastel.red("")} #{ref}  #{error}"
end

#repo_finished(ref, status, action:, commits: 0) ⇒ Object



122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/repo_tender/ui/interactive_reporter.rb', line 122

def repo_finished(ref, status, action:, commits: 0)
  @in_flight.delete(ref)
  @finished += 1
  @action_counts[action] += 1
  @total_commits += commits
  @added_repos << ref if action == :cloned
  if status.to_s == "clean"
    @clean_count += 1
  else
    @nonclean_count += 1
    @pending_lines << "#{@pastel.yellow("")} #{ref}  #{status}"
  end
end

#repo_phase(ref, phase) ⇒ Object



112
113
114
115
116
117
118
119
120
# File 'lib/repo_tender/ui/interactive_reporter.rb', line 112

def repo_phase(ref, phase)
  return unless @in_flight.key?(ref)
  @in_flight[ref] = case phase
  when :cloning then "cloning"
  when :fast_forwarding then "fast-forwarding"
  when :switching then "switching"
  else @in_flight[ref]
  end
end

#repo_started(ref) ⇒ Object



108
109
110
# File 'lib/repo_tender/ui/interactive_reporter.rb', line 108

def repo_started(ref)
  @in_flight[ref] = "checking"
end

#run_finished(summary) ⇒ Object



144
# File 'lib/repo_tender/ui/interactive_reporter.rb', line 144

def run_finished(summary) = nil

#run_started(total:) ⇒ Object

— Sweep phase events —



103
104
105
106
# File 'lib/repo_tender/ui/interactive_reporter.rb', line 103

def run_started(total:)
  @total = total
  @phase = :sweep
end