legion-tty 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../screens/base'
4
+ require_relative '../theme'
5
+
6
+ module Legion
7
+ module TTY
8
+ module Screens
9
+ # rubocop:disable Metrics/ClassLength
10
+ class Dashboard < Base
11
+ def initialize(app)
12
+ super
13
+ @last_refresh = nil
14
+ @refresh_interval = 5
15
+ @cached_data = {}
16
+ end
17
+
18
+ def activate
19
+ refresh_data
20
+ end
21
+
22
+ # rubocop:disable Metrics/AbcSize
23
+ def render(width, height)
24
+ refresh_data if stale?
25
+
26
+ rows = []
27
+ rows.concat(render_header(width))
28
+ rows.concat(render_services_panel(width))
29
+ rows.concat(render_extensions_panel(width))
30
+ rows.concat(render_system_panel(width))
31
+ rows.concat(render_activity_panel(width, remaining_height(height, rows.size)))
32
+ rows.concat(render_help_bar(width))
33
+
34
+ pad_to_height(rows, height)
35
+ end
36
+
37
+ # rubocop:enable Metrics/AbcSize
38
+
39
+ def handle_input(key)
40
+ case key
41
+ when 'r', :f5
42
+ refresh_data
43
+ :handled
44
+ when 'q', :escape
45
+ :pop_screen
46
+ else
47
+ :pass
48
+ end
49
+ end
50
+
51
+ def refresh_data
52
+ @last_refresh = Time.now
53
+ @cached_data = {
54
+ services: probe_services,
55
+ extensions: discover_extensions,
56
+ system: system_info,
57
+ activity: recent_activity
58
+ }
59
+ end
60
+
61
+ private
62
+
63
+ def stale?
64
+ @last_refresh.nil? || (Time.now - @last_refresh) > @refresh_interval
65
+ end
66
+
67
+ def render_header(width)
68
+ title = Theme.c(:primary, ' LEGION DASHBOARD ')
69
+ timestamp = Theme.c(:muted, @last_refresh&.strftime('%H:%M:%S') || '--:--:--')
70
+ line = "#{title}#{' ' * [width - 30, 0].max}#{timestamp}"
71
+ [line, Theme.c(:muted, '-' * width)]
72
+ end
73
+
74
+ def render_services_panel(_width)
75
+ services = @cached_data[:services] || {}
76
+ lines = [Theme.c(:accent, ' Services')]
77
+ services.each do |name, info|
78
+ icon = info[:running] ? Theme.c(:success, "\u2713") : Theme.c(:error, "\u2717")
79
+ port_str = Theme.c(:muted, ":#{info[:port]}")
80
+ lines << " #{icon} #{name} #{port_str}"
81
+ end
82
+ lines << ''
83
+ lines
84
+ end
85
+
86
+ def render_extensions_panel(_width)
87
+ extensions = @cached_data[:extensions] || []
88
+ count = extensions.size
89
+ lines = [Theme.c(:accent, " Extensions (#{count})")]
90
+ if extensions.empty?
91
+ lines << Theme.c(:muted, ' No lex-* gems found')
92
+ else
93
+ extensions.first(8).each do |ext|
94
+ lines << " #{Theme.c(:secondary, ext)}"
95
+ end
96
+ remaining = count - 8
97
+ lines << Theme.c(:muted, " ... and #{remaining} more") if remaining.positive?
98
+ end
99
+ lines << ''
100
+ lines
101
+ end
102
+
103
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
104
+ def render_system_panel(_width)
105
+ sys = @cached_data[:system] || {}
106
+ lines = [Theme.c(:accent, ' System')]
107
+ lines << " Ruby: #{Theme.c(:secondary, sys[:ruby_version] || 'unknown')}"
108
+ lines << " OS: #{Theme.c(:secondary, sys[:os] || 'unknown')}"
109
+ lines << " Host: #{Theme.c(:secondary, sys[:hostname] || 'unknown')}"
110
+ lines << " PID: #{Theme.c(:secondary, sys[:pid]&.to_s || 'unknown')}"
111
+ lines << " Memory: #{Theme.c(:secondary, sys[:memory] || 'unknown')}"
112
+ lines << ''
113
+ lines
114
+ end
115
+
116
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
117
+
118
+ def render_activity_panel(_width, max_lines)
119
+ activity = @cached_data[:activity] || []
120
+ lines = [Theme.c(:accent, ' Recent Activity')]
121
+ if activity.empty?
122
+ lines << Theme.c(:muted, ' No recent activity')
123
+ else
124
+ available = [max_lines - 2, 1].max
125
+ activity.last(available).each do |entry|
126
+ lines << " #{Theme.c(:muted, entry)}"
127
+ end
128
+ end
129
+ lines << ''
130
+ lines
131
+ end
132
+
133
+ def render_help_bar(width)
134
+ help = " #{Theme.c(:muted, 'r')}=refresh #{Theme.c(:muted, 'q')}=back " \
135
+ "#{Theme.c(:muted, 'Ctrl+D')}=dashboard #{Theme.c(:muted, 'Ctrl+C')}=quit"
136
+ [Theme.c(:muted, '-' * width), help]
137
+ end
138
+
139
+ def remaining_height(total, used)
140
+ [total - used - 4, 3].max
141
+ end
142
+
143
+ def pad_to_height(rows, height)
144
+ if rows.size < height
145
+ rows + Array.new(height - rows.size, '')
146
+ else
147
+ rows.first(height)
148
+ end
149
+ end
150
+
151
+ def probe_services
152
+ require 'socket'
153
+ {
154
+ rabbitmq: { port: 5672, running: port_open?(5672) },
155
+ redis: { port: 6379, running: port_open?(6379) },
156
+ memcached: { port: 11_211, running: port_open?(11_211) },
157
+ vault: { port: 8200, running: port_open?(8200) },
158
+ postgres: { port: 5432, running: port_open?(5432) }
159
+ }
160
+ end
161
+
162
+ def port_open?(port)
163
+ ::Socket.tcp('127.0.0.1', port, connect_timeout: 0.5) { true }
164
+ rescue StandardError
165
+ false
166
+ end
167
+
168
+ def discover_extensions
169
+ Gem::Specification.select { |s| s.name.start_with?('lex-') }.map(&:name).sort
170
+ rescue StandardError
171
+ []
172
+ end
173
+
174
+ def system_info
175
+ {
176
+ ruby_version: RUBY_VERSION,
177
+ os: RUBY_PLATFORM,
178
+ hostname: ::Socket.gethostname,
179
+ pid: ::Process.pid,
180
+ memory: format_memory
181
+ }
182
+ rescue StandardError
183
+ {}
184
+ end
185
+
186
+ def format_memory
187
+ if RUBY_PLATFORM.include?('darwin')
188
+ rss = `ps -o rss= -p #{::Process.pid} 2>/dev/null`.strip.to_i
189
+ "#{(rss / 1024.0).round(1)} MB"
190
+ else
191
+ match = `cat /proc/#{::Process.pid}/status 2>/dev/null`.match(/VmRSS:\s+(\d+)/)
192
+ rss_kb = match ? match[1].to_i : 0
193
+ "#{(rss_kb / 1024.0).round(1)} MB"
194
+ end
195
+ rescue StandardError
196
+ 'unknown'
197
+ end
198
+
199
+ def recent_activity
200
+ log_path = File.expand_path('~/.legionio/logs/tty-boot.log')
201
+ return [] unless File.exist?(log_path)
202
+
203
+ File.readlines(log_path, chomp: true).last(20)
204
+ rescue StandardError
205
+ []
206
+ end
207
+ end
208
+ # rubocop:enable Metrics/ClassLength
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,463 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../screens/base'
4
+ require_relative '../components/digital_rain'
5
+ require_relative '../components/wizard_prompt'
6
+ require_relative '../background/scanner'
7
+ require_relative '../background/github_probe'
8
+ require_relative '../background/kerberos_probe'
9
+ require_relative '../boot_logger'
10
+ require_relative '../theme'
11
+
12
+ module Legion
13
+ module TTY
14
+ module Screens
15
+ # rubocop:disable Metrics/ClassLength
16
+ class Onboarding < Base
17
+ TYPED_DELAY = 0.05
18
+
19
+ def initialize(app, wizard: nil, output: $stdout, skip_rain: false)
20
+ super(app)
21
+ @wizard = wizard || Components::WizardPrompt.new
22
+ @output = output
23
+ @skip_rain = skip_rain
24
+ @scan_queue = Queue.new
25
+ @github_queue = Queue.new
26
+ @github_quick_queue = Queue.new
27
+ @kerberos_queue = Queue.new
28
+ @kerberos_identity = nil
29
+ @github_quick = nil
30
+ @log = BootLogger.new
31
+ end
32
+
33
+ def activate
34
+ @log.log('onboarding', 'activate started')
35
+ start_background_threads
36
+ run_rain unless @skip_rain
37
+ run_intro
38
+ config = run_wizard
39
+ @log.log('wizard', "name=#{config[:name]} provider=#{config[:provider]}")
40
+ scan_data, github_data = collect_background_results
41
+ run_reveal(name: config[:name], scan_data: scan_data, github_data: github_data)
42
+ @log.log('onboarding', 'activate complete')
43
+ build_onboarding_result(config, scan_data, github_data)
44
+ end
45
+
46
+ # rubocop:disable Metrics/AbcSize
47
+ def run_rain
48
+ require 'tty-cursor'
49
+ require 'tty-font'
50
+ width = terminal_width
51
+ height = terminal_height
52
+ rain = Components::DigitalRain.new(width: width, height: height)
53
+ rain.run(duration_seconds: 20, fps: 18, output: @output)
54
+ @output.print ::TTY::Cursor.clear_screen
55
+ font = ::TTY::Font.new(:standard)
56
+ title = font.write('LEGION')
57
+ title.each_line do |line|
58
+ @output.puts line.center(width)
59
+ end
60
+ @output.puts Theme.c(:muted, 'async cognition engine').center(width + 20)
61
+ sleep 5
62
+ @output.print ::TTY::Cursor.clear_screen
63
+ end
64
+ # rubocop:enable Metrics/AbcSize
65
+
66
+ def run_intro
67
+ # Collect background results that ran during the rain
68
+ collect_kerberos_identity
69
+ collect_github_quick
70
+
71
+ sleep 2
72
+ typed_output('...')
73
+ sleep 1.2
74
+ @output.puts
75
+ @output.puts
76
+ typed_output("Hello. I'm Legion.")
77
+ @output.puts
78
+ sleep 1.5
79
+ if @kerberos_identity
80
+ run_intro_with_identity
81
+ else
82
+ typed_output("Let's get you set up.")
83
+ @output.puts
84
+ @output.puts
85
+ end
86
+ run_intro_with_github if @github_quick
87
+ end
88
+
89
+ def run_wizard
90
+ name = ask_for_name
91
+ sleep 0.8
92
+ typed_output(" Nice to meet you, #{name}.")
93
+ @output.puts
94
+ sleep 1
95
+ typed_output("Let's get you connected.")
96
+ @output.puts
97
+ @output.puts
98
+ provider = @wizard.select_provider
99
+ sleep 0.5
100
+ api_key = @wizard.ask_api_key(provider: provider)
101
+ { name: name, provider: provider, api_key: api_key }
102
+ end
103
+
104
+ def start_background_threads
105
+ @log.log('threads', 'launching scanner, kerberos probe, github quick probe')
106
+ @scanner = Background::Scanner.new(logger: @log)
107
+ @github_probe = Background::GitHubProbe.new(logger: @log)
108
+ @kerberos_probe = Background::KerberosProbe.new(logger: @log)
109
+ @scanner.run_async(@scan_queue)
110
+ @kerberos_probe.run_async(@kerberos_queue)
111
+ @github_probe.run_quick_async(@github_quick_queue)
112
+ end
113
+
114
+ def collect_background_results
115
+ @log.log('collect', 'waiting for scanner results (10s timeout)')
116
+ scan_result = drain_with_timeout(@scan_queue, timeout: 10)
117
+ scan_data = scan_result&.dig(:data) || { services: {}, repos: [], tools: {} }
118
+ log_scan_data(scan_data)
119
+
120
+ # Now launch GitHub probe with discovered remotes
121
+ remotes = scan_data[:repos]&.filter_map { |r| r[:remote] } || []
122
+ @log.log('collect', "launching github probe with #{remotes.size} remotes")
123
+ @github_probe.run_async(@github_queue, remotes: remotes, quick_profile: @github_quick)
124
+ github_result = drain_with_timeout(@github_queue, timeout: 8)
125
+ github_data = github_result&.dig(:data)
126
+ log_github_data(github_data)
127
+ [scan_data, github_data]
128
+ end
129
+
130
+ def run_reveal(name:, scan_data:, github_data:)
131
+ require 'tty-box'
132
+ @output.puts
133
+ typed_output('One moment...')
134
+ @output.puts
135
+ sleep 1.5
136
+ summary = build_summary(name: name, scan_data: scan_data, github_data: github_data)
137
+ box = ::TTY::Box.frame(summary, padding: 1, border: :thick)
138
+ @output.puts box
139
+ @output.puts
140
+ @wizard.confirm('Does this look right?')
141
+ @output.puts
142
+ sleep 0.8
143
+ typed_output("Let's chat.")
144
+ @output.puts
145
+ sleep 1
146
+ end
147
+
148
+ def build_summary(name:, scan_data:, github_data:)
149
+ lines = ["Hello, #{name}!", '', "Here's what I found:"]
150
+ lines.concat(identity_summary_lines)
151
+ lines.concat(scan_summary_lines(scan_data))
152
+ lines.concat(github_summary_lines(github_data))
153
+ lines.join("\n")
154
+ end
155
+
156
+ private
157
+
158
+ def build_onboarding_result(config, scan_data, github_data)
159
+ {
160
+ **config,
161
+ identity: @kerberos_identity,
162
+ github: github_data,
163
+ scan: scan_data
164
+ }
165
+ end
166
+
167
+ def collect_github_quick
168
+ @log.log('github', 'collecting quick profile (3s timeout)')
169
+ result = drain_with_timeout(@github_quick_queue, timeout: 3)
170
+ @github_quick = result&.dig(:data)
171
+ if @github_quick
172
+ @log.log('github', "quick profile: #{@github_quick[:username]} " \
173
+ "commits_week=#{@github_quick[:commits_this_week]} " \
174
+ "commits_month=#{@github_quick[:commits_this_month]}")
175
+ else
176
+ @log.log('github', 'no quick profile available')
177
+ end
178
+ end
179
+
180
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
181
+ def run_intro_with_github
182
+ gh = @github_quick
183
+ name = gh[:name] || gh[:username]
184
+
185
+ if gh[:commits_this_week]&.positive?
186
+ typed_output("#{gh[:commits_this_week]} commits this week, #{name}. You've been busy.")
187
+ @output.puts
188
+ sleep 1
189
+ elsif gh[:commits_this_month]&.positive?
190
+ typed_output("#{gh[:commits_this_month]} commits this month across GitHub.")
191
+ @output.puts
192
+ sleep 1
193
+ end
194
+
195
+ total = (gh[:public_repos] || 0) + (gh[:private_repos] || 0)
196
+ if total.positive?
197
+ typed_output("#{total} repositories. I can work with that.")
198
+ @output.puts
199
+ sleep 0.8
200
+ end
201
+
202
+ @output.puts
203
+ end
204
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
205
+
206
+ def collect_kerberos_identity
207
+ @log.log('kerberos', 'collecting identity (2s timeout)')
208
+ result = drain_with_timeout(@kerberos_queue, timeout: 2)
209
+ @kerberos_identity = result&.dig(:data)
210
+ if @kerberos_identity
211
+ @log.log_hash('kerberos', 'identity found', @kerberos_identity)
212
+ else
213
+ @log.log('kerberos', 'no identity found (klist failed or no ticket)')
214
+ end
215
+ end
216
+
217
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
218
+ def run_intro_with_identity
219
+ id = @kerberos_identity
220
+ typed_output("I see you, #{id[:first_name]}.")
221
+ @output.puts
222
+ sleep 1.2
223
+
224
+ if id[:title]
225
+ typed_output("#{id[:title]}, #{id[:company] || id[:department]}")
226
+ @output.puts
227
+ sleep 0.8
228
+ end
229
+
230
+ if id[:city] && id[:state]
231
+ typed_output("#{id[:city]}, #{id[:state]}")
232
+ @output.puts
233
+ sleep 0.8
234
+ end
235
+
236
+ if id[:tenure_years]
237
+ typed_output("#{format_tenure(id[:tenure_years])} and counting.")
238
+ @output.puts
239
+ sleep 1
240
+ end
241
+
242
+ @output.puts
243
+ typed_output("I've been looking forward to meeting you.")
244
+ @output.puts
245
+ @output.puts
246
+ end
247
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
248
+
249
+ def ask_for_name
250
+ if @kerberos_identity
251
+ @wizard.ask_name_with_default(@kerberos_identity[:first_name])
252
+ else
253
+ @wizard.ask_name
254
+ end
255
+ end
256
+
257
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
258
+ def identity_summary_lines
259
+ return [] unless @kerberos_identity
260
+
261
+ id = @kerberos_identity
262
+ lines = ['']
263
+ lines << "Identity: #{id[:display_name]} (#{id[:principal]})"
264
+ lines << " Title: #{id[:title]}" if id[:title]
265
+ lines << " Org: #{[id[:department], id[:company]].compact.join(' / ')}" if id[:department] || id[:company]
266
+ lines << " Location: #{[id[:city], id[:state], id[:country]].compact.join(', ')}" if id[:city]
267
+ lines << " Tenure: #{format_tenure(id[:tenure_years])}" if id[:tenure_years]
268
+ lines << " Email: #{id[:email]}" if id[:email]
269
+ lines
270
+ end
271
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
272
+
273
+ def scan_summary_lines(scan_data)
274
+ return [] unless scan_data.is_a?(Hash)
275
+
276
+ services = scan_data[:services]
277
+ return [] unless services.is_a?(Hash)
278
+
279
+ running = services.values.select { |s| s[:running] }.map { |s| s[:name] }
280
+ return [] if running.empty?
281
+
282
+ ['', "Running services: #{running.join(', ')}"]
283
+ end
284
+
285
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
286
+ def github_summary_lines(github_data)
287
+ return [] unless github_data.is_a?(Hash)
288
+
289
+ username = github_data[:username]
290
+ return [] unless username
291
+
292
+ lines = ['', "GitHub: #{username}"]
293
+ profile = github_data[:profile]
294
+ if profile.is_a?(Hash)
295
+ lines << " Name: #{profile[:name]}" if profile[:name]
296
+ lines << " Company: #{profile[:company]}" if profile[:company]
297
+ lines << " Public repos: #{profile[:public_repos]}" if profile[:public_repos]
298
+ lines << " Private repos: #{profile[:private_repos]}" if profile[:private_repos]
299
+ lines << " Followers: #{profile[:followers]}" if profile[:followers]
300
+ end
301
+
302
+ orgs = github_data[:orgs]
303
+ lines << " Orgs: #{orgs.map { |o| o[:login] }.join(', ')}" if orgs.is_a?(Array) && !orgs.empty?
304
+
305
+ prs = github_data[:recent_prs]
306
+ if prs.is_a?(Array) && !prs.empty?
307
+ lines << ' Recent PRs:'
308
+ prs.first(3).each do |pr|
309
+ lines << " #{pr[:state] == 'open' ? '○' : '●'} #{pr[:repo]}: #{pr[:title]}"
310
+ end
311
+ end
312
+
313
+ notifs = github_data[:notifications]
314
+ lines << " Notifications: #{notifs.size} unread" if notifs.is_a?(Array) && !notifs.empty?
315
+
316
+ lines
317
+ end
318
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
319
+
320
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
321
+ def format_tenure(tenure)
322
+ return tenure.to_s unless tenure.is_a?(Hash)
323
+
324
+ parts = []
325
+ y = tenure[:years]
326
+ m = tenure[:months]
327
+ d = tenure[:days]
328
+ parts << "#{y} year#{'s' if y != 1}" if y&.positive?
329
+ parts << "#{m} month#{'s' if m != 1}" if m&.positive?
330
+ parts << "#{d} day#{'s' if d != 1}" if d&.positive?
331
+ parts.join(', ')
332
+ end
333
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
334
+
335
+ def typed_output(text, delay: TYPED_DELAY)
336
+ text.chars.each do |char|
337
+ @output.print Theme.c(:primary, char)
338
+ @output.flush if @output.respond_to?(:flush)
339
+ sleep delay
340
+ end
341
+ end
342
+
343
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
344
+ def log_scan_data(scan_data)
345
+ services = scan_data[:services] || {}
346
+ running = services.values.select { |s| s[:running] }.map { |s| s[:name] }
347
+ stopped = services.values.reject { |s| s[:running] }.map { |s| s[:name] }
348
+ @log.log('scanner', "services running: #{running.join(', ').then { |s| s.empty? ? 'none' : s }}")
349
+ @log.log('scanner', "services stopped: #{stopped.join(', ').then { |s| s.empty? ? 'none' : s }}")
350
+
351
+ repos = scan_data[:repos] || []
352
+ @log.log('scanner', "git repos found: #{repos.size}")
353
+ repos.each do |r|
354
+ @log.log('scanner', " repo: #{r[:name]} branch=#{r[:branch]} lang=#{r[:language]} remote=#{r[:remote]}")
355
+ end
356
+
357
+ tools = scan_data[:tools] || {}
358
+ @log.log('scanner', "top shell commands: #{tools.first(10).map { |k, v| "#{k}(#{v})" }.join(', ')}")
359
+
360
+ configs = scan_data[:configs] || []
361
+ @log.log('scanner', "config files: #{configs.join(', ').then { |s| s.empty? ? 'none' : s }}")
362
+ end
363
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
364
+
365
+ # rubocop:disable Metrics/AbcSize
366
+ def log_github_data(github_data)
367
+ unless github_data.is_a?(Hash)
368
+ @log.log('github', 'no data returned')
369
+ return
370
+ end
371
+ @log.log('github', "username: #{github_data[:username] || 'unknown'}")
372
+ @log.log('github', "authenticated: #{github_data[:authenticated]}")
373
+
374
+ profile = github_data[:profile]
375
+ if profile.is_a?(Hash)
376
+ @log.log('github', " name: #{profile[:name]}")
377
+ @log.log('github', " email: #{profile[:email]}")
378
+ @log.log('github', " company: #{profile[:company]}")
379
+ @log.log('github', " location: #{profile[:location]}")
380
+ @log.log('github', " public_repos: #{profile[:public_repos]}")
381
+ @log.log('github', " private_repos: #{profile[:private_repos]}")
382
+ @log.log('github', " followers: #{profile[:followers]} following: #{profile[:following]}")
383
+ end
384
+
385
+ orgs = github_data[:orgs] || []
386
+ @log.log('github', "orgs: #{orgs.map { |o| o[:login] }.join(', ').then { |s| s.empty? ? 'none' : s }}")
387
+
388
+ log_github_repos(github_data)
389
+ log_github_activity(github_data)
390
+ end
391
+ # rubocop:enable Metrics/AbcSize
392
+
393
+ # rubocop:disable Metrics/AbcSize
394
+ def log_github_repos(github_data)
395
+ public_repos = github_data[:public_repos] || []
396
+ @log.log('github', "public repos: #{public_repos.size}")
397
+ public_repos.first(5).each do |r|
398
+ @log.log('github', " #{r[:full_name]} (#{r[:language]}) updated=#{r[:updated_at]}")
399
+ end
400
+
401
+ private_repos = github_data[:private_repos] || []
402
+ @log.log('github', "private repos: #{private_repos.size}")
403
+ private_repos.first(5).each do |r|
404
+ @log.log('github', " #{r[:full_name]} (#{r[:language]}) updated=#{r[:updated_at]}")
405
+ end
406
+
407
+ starred = github_data[:starred] || []
408
+ @log.log('github', "recently starred: #{starred.size}")
409
+ end
410
+ # rubocop:enable Metrics/AbcSize
411
+
412
+ # rubocop:disable Metrics/AbcSize
413
+ def log_github_activity(github_data)
414
+ prs = github_data[:recent_prs] || []
415
+ @log.log('github', "recent PRs: #{prs.size}")
416
+ prs.first(5).each do |pr|
417
+ @log.log('github', " [#{pr[:state]}] #{pr[:repo]}: #{pr[:title]}")
418
+ end
419
+
420
+ events = github_data[:events] || []
421
+ @log.log('github', "recent events: #{events.size}")
422
+ events.first(5).each do |e|
423
+ @log.log('github', " #{e[:type]} on #{e[:repo]} at #{e[:created_at]}")
424
+ end
425
+
426
+ notifs = github_data[:notifications] || []
427
+ @log.log('github', "notifications: #{notifs.size}")
428
+ notifs.first(5).each do |n|
429
+ @log.log('github', " [#{n[:reason]}] #{n[:repo]}: #{n[:title]}")
430
+ end
431
+ end
432
+ # rubocop:enable Metrics/AbcSize
433
+
434
+ def drain_with_timeout(queue, timeout:)
435
+ deadline = Time.now + timeout
436
+ loop do
437
+ return queue.pop(true) unless queue.empty?
438
+ return nil if Time.now >= deadline
439
+
440
+ sleep 0.1
441
+ end
442
+ rescue ThreadError
443
+ nil
444
+ end
445
+
446
+ def terminal_width
447
+ require 'tty-screen'
448
+ ::TTY::Screen.width
449
+ rescue StandardError
450
+ 80
451
+ end
452
+
453
+ def terminal_height
454
+ require 'tty-screen'
455
+ ::TTY::Screen.height
456
+ rescue StandardError
457
+ 24
458
+ end
459
+ end
460
+ # rubocop:enable Metrics/ClassLength
461
+ end
462
+ end
463
+ end